Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-02-27 10:53:07 +08:00
commit a77997d87f
63 changed files with 5663 additions and 2075 deletions

View File

@ -37,6 +37,7 @@
"unplugin-vue-components": "^0.22.12", "unplugin-vue-components": "^0.22.12",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue3-markdown-it": "^1.0.10", "vue3-markdown-it": "^1.0.10",
"vue3-ts-jsoneditor": "^2.7.1" "vue3-ts-jsoneditor": "^2.7.1"

View File

@ -466,4 +466,20 @@ export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/
* @param params * @param params
* @returns * @returns
*/ */
export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params) export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params)
/**
*
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/agg/_query`, data)
/**
*
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)

View File

@ -16,21 +16,21 @@ export default {
debug: (data: any, configId: string, templateId: string) => post(`/notifier/${configId}/${templateId}/_send`, data), debug: (data: any, configId: string, templateId: string) => post(`/notifier/${configId}/${templateId}/_send`, data),
getHistory: (data: any, id: string) => post(`/notify/history/config/${id}/_query`, data), getHistory: (data: any, id: string) => post(`/notify/history/config/${id}/_query`, data),
// 获取所有平台用户 // 获取所有平台用户
getPlatformUsers: () => post<any>(`/user/_query/no-paging`, { paging: false }), getPlatformUsers: (data: any) => post<any>(`/user/_query/no-paging`, data),
// 钉钉部门 // 钉钉部门
dingTalkDept: (id: string) => get<any>(`/notifier/dingtalk/corp/${id}/departments/tree`), dingTalkDept: (id: string) => get<any>(`/notifier/dingtalk/corp/${id}/departments/tree`),
// 钉钉部门人员 // 钉钉部门人员
getDingTalkUsers: (configId: string, deptId: string) => get(`/notifier/dingtalk/corp/${configId}/${deptId}/users`), getDingTalkUsers: (configId: string, deptId: string) => get<any>(`/notifier/dingtalk/corp/${configId}/${deptId}/users`),
// 钉钉已经绑定的人员 // 钉钉已经绑定的人员
getDingTalkBindUsers: (id: string) => get(`/user/third-party/dingTalk_dingTalkMessage/${id}`), getDingTalkBindUsers: (id: string) => get<any>(`/user/third-party/dingTalk_dingTalkMessage/${id}`),
// 钉钉绑定用户 // 钉钉绑定用户
dingTalkBindUser: (data: any, id: string) => patch(`/user/third-party/dingTalk_dingTalkMessage/${id}`, data), dingTalkBindUser: (data: { userId: string; providerName: string; thirdPartyUserId: string }[], id: string) => patch(`/user/third-party/dingTalk_dingTalkMessage/${id}`, data),
// 微信部门 // 微信部门
weChatDept: (id: string) => get<any>(`/notifier/wechat/corp/${id}/departments`), weChatDept: (id: string) => get<any>(`/notifier/wechat/corp/${id}/departments`),
// 微信部门人员 // 微信部门人员
getWeChatUsers: (configId: string, deptId: string) => get(`/notifier/wechat/corp/${configId}/${deptId}/users`), getWeChatUsers: (configId: string, deptId: string) => get<any>(`/notifier/wechat/corp/${configId}/${deptId}/users`),
// 微信已经绑定的人员 // 微信已经绑定的人员
getWeChatBindUsers: (id: string) => get(`/user/third-party/weixin_corpMessage/${id}`), getWeChatBindUsers: (id: string) => get<any>(`/user/third-party/weixin_corpMessage/${id}`),
// 微信绑定用户 // 微信绑定用户
weChatBindUser: (data: any, id: string) => patch(`/user/third-party/weixin_corpMessage/${id}`, data), weChatBindUser: (data: any, id: string) => patch(`/user/third-party/weixin_corpMessage/${id}`, data),
// 解绑 // 解绑

View File

@ -18,4 +18,20 @@ export const remove = (id:string) => server.remove(`/alarm/config/${id}`);
/** /**
* *
*/ */
export const _execute = (data:any) => server.post('/scene/batch/_execute',data) export const _execute = (data:any) => server.post('/scene/batch/_execute',data);
/**
*
*/
export const getScene = (params:Record<string,any>) => server.get('/scene/_query/no-paging?paging=false',params);
/**
*
*/
export const getTargetTypes = () => server.get('/alarm/config/target-type/supports');
/**
*
*/
export const save = (data:any) =>server.post('/alarm/config',data);
/**
*
*/
export const detail = (id:string) => server.get(`/alarm/config/${id}`);

View File

@ -5,4 +5,6 @@ export const modify = (id: string, data: any) => server.put(`/scene/${id}`, data
export const save = (data: any) => server.post(`/scene`, data) export const save = (data: any) => server.post(`/scene`, data)
export const detail = (id: string) => server.get(`/scene/${id}`) export const detail = (id: string) => server.get(`/scene/${id}`)
export const query = (data: any) => server.post('/scene/_query/',data);

View File

@ -7,3 +7,14 @@ export const getApplyList_api = (data: any) => server.post(`/application/_query/
export const changeApplyStatus_api = (id:string,data: any) => server.put(`/application/${id}`, data) export const changeApplyStatus_api = (id:string,data: any) => server.put(`/application/${id}`, data)
// 删除应用 // 删除应用
export const delApply_api = (id:string) => server.remove(`/application/${id}`) export const delApply_api = (id:string) => server.remove(`/application/${id}`)
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);
// 获取组织列表
export const getAppInfo_api = (id:string) => server.get(`/application/${id}`);
// 新增应用
export const addApp_api = (data:object) => server.post(`/application`, data);
// 更新应用
export const updateApp_api = (id:string, data:object) => server.put(`/application/${id}`, data);

View File

@ -98,7 +98,11 @@ const props = defineProps({
class: { class: {
type: String, type: String,
default: '' default: ''
} },
// defaultTerms: {
// type: Object,
// default: () => ({})
// }
}) })
const searchRef = ref(null) const searchRef = ref(null)
@ -223,6 +227,7 @@ const handleParamsFormat = () => {
*/ */
const searchSubmit = () => { const searchSubmit = () => {
emit('search', handleParamsFormat()) emit('search', handleParamsFormat())
console.log('searchSubmit')
if (props.type === 'advanced') { if (props.type === 'advanced') {
addUrlParams() addUrlParams()
} }

View File

@ -144,6 +144,10 @@ const JTable = defineComponent<JTableProps>({
pageSize: 12 pageSize: 12
} }
} }
},
scroll: {
type: Object,
default: () => { x: 1366 }
} }
} as any, } as any,
setup(props: JTableProps, { slots, emit, expose }) { setup(props: JTableProps, { slots, emit, expose }) {
@ -260,7 +264,7 @@ const JTable = defineComponent<JTableProps>({
/** /**
* *
*/ */
expose({ reload }) expose({ reload, _dataSource })
return () => <Spin spinning={loading.value}> return () => <Spin spinning={loading.value}>
<div class={styles["jtable-body"]} style={{ ...props.bodyStyle }}> <div class={styles["jtable-body"]} style={{ ...props.bodyStyle }}>
@ -331,7 +335,7 @@ const JTable = defineComponent<JTableProps>({
pagination={false} pagination={false}
rowKey="id" rowKey="id"
rowSelection={props.rowSelection} rowSelection={props.rowSelection}
scroll={{ x: 1366 }} scroll={props.scroll}
v-slots={{ v-slots={{
bodyCell: (dt: Record<string, any>) => { bodyCell: (dt: Record<string, any>) => {
const { column, record } = dt; const { column, record } = dt;

View File

@ -4,6 +4,8 @@ import { filterAsnycRouter, MenuItem } from '@/utils/menu'
import { isArray } from 'lodash-es' import { isArray } from 'lodash-es'
import { usePermissionStore } from './permission' import { usePermissionStore } from './permission'
import router from '@/router' import router from '@/router'
import { message } from 'ant-design-vue'
import { onlyMessage } from '@/utils/comm'
const defaultOwnParams = [ const defaultOwnParams = [
{ {
@ -77,6 +79,7 @@ export const useMenuStore = defineStore({
name, params, query name, params, query
}) })
} else { } else {
onlyMessage('暂无权限,请联系管理员', 'error')
console.warn(`没有找到对应的页面: ${name}`) console.warn(`没有找到对应的页面: ${name}`)
} }
}, },

View File

@ -68,7 +68,7 @@ const defaultOptions = {
}; };
export const useSceneStore = defineStore('scene', () => { export const useSceneStore = defineStore('scene', () => {
const data = reactive<FormModelType | any>({ const data = reactive<FormModelType>({
trigger: { type: ''}, trigger: { type: ''},
options: defaultOptions, options: defaultOptions,
branches: defaultBranches, branches: defaultBranches,
@ -116,67 +116,3 @@ export const useSceneStore = defineStore('scene', () => {
getDetail getDetail
} }
}) })
//
// export const useSceneStore = defineStore({
// id: 'scene',
// state: (): DataType => {
// return {
// data: {
// trigger: { type: ''},
// options: defaultOptions,
// branches: defaultBranches,
// description: ''
// },
// productCache: {}
// }
// },
// actions: {
// /**
// * 初始化数据
// */
// initData() {
//
// },
// /**
// * 获取详情
// * @param id
// */
// async getDetail(id: string) {
// const resp = await detail(id)
// if (resp.success) {
// const result = resp.result as SceneItem
// const triggerType = result.triggerType
// let branches: any[] = result.branches
//
// if (!branches) {
// branches = cloneDeep(defaultBranches)
// if (triggerType === 'device') {
// branches.push(null)
// }
// } else {
// const branchesLength = branches.length;
// if (
// triggerType === 'device' &&
// ((branchesLength === 1 && !!branches[0]?.when?.length) || // 有一组数据并且when有值
// (branchesLength > 1 && !branches[branchesLength - 1]?.when?.length)) // 有多组否则数据并且最后一组when有值
// ) {
// branches.push(null);
// }
// }
//
// this.data = {
// ...result,
// trigger: result.trigger || {},
// branches: cloneDeep(assignmentKey(branches)),
// options: {...defaultOptions, ...result.options },
// }
// }
// },
// getProduct() {
//
// }
// },
// getters: {
//
// }
// })

View File

@ -7,4 +7,12 @@ export const isUrl = (path: string): boolean => urlReg.test(path)
export const inputReg = /^[a-zA-Z0-9_\-]+$/ export const inputReg = /^[a-zA-Z0-9_\-]+$/
export const isInput = (value: string) => inputReg.test(value) export const isInput = (value: string) => inputReg.test(value)
// cron 表达式
export const CronRegEx = new RegExp(
'^\\s*($|#|\\w+\\s*=|(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?(?:,(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?)*)\\s+(\\?|\\*|(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?(?:,(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?)*)\\s+(\\?|\\*|(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?(?:,(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?)*|\\?|\\*|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*)\\s+(\\?|\\*|(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?(?:,(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?)*|\\?|\\*|(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?(?:,(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?)*)(|\\s)+(\\?|\\*|(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?(?:,(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?)*))$',
);
export const isCron = (value: string) => CronRegEx.test(value)

View File

@ -1,4 +1,6 @@
import AIcon from "@/components/AIcon"; import AIcon from "@/components/AIcon";
import { useInstanceStore } from "@/store/instance";
import { useMenuStore } from "@/store/menu";
import { Button, Descriptions, Modal } from "ant-design-vue" import { Button, Descriptions, Modal } from "ant-design-vue"
import styles from './index.module.less' import styles from './index.module.less'
@ -14,6 +16,10 @@ const ManualInspection = defineComponent({
const { data } = props const { data } = props
const instanceStore = useInstanceStore();
const menuStory = useMenuStore();
const dataRender = () => { const dataRender = () => {
if (data.type === 'device' || data.type === 'product') { if (data.type === 'device' || data.type === 'product') {
return ( return (
@ -207,7 +213,13 @@ const ManualInspection = defineComponent({
emit('save', data) emit('save', data)
}} }}
onCancel={() => { onCancel={() => {
// TODO 跳转设备和产品 if (data.type === 'device') {
instanceStore.tabActiveKey = 'Info'
} else if (data.type === 'product') {
menuStory.jumpPage('device/Product/Detail', { id: data.productId, tab: 'access' });
} else {
menuStory.jumpPage('link/AccessConfig/Detail', { id: data.configuration?.id });
}
}}> }}>
<div style={{ display: 'flex' }}>{dataRender()}</div> <div style={{ display: 'flex' }}>{dataRender()}</div>
</Modal> </Modal>

View File

@ -11,6 +11,8 @@ import _ from "lodash"
import DiagnosticAdvice from './DiagnosticAdvice' import DiagnosticAdvice from './DiagnosticAdvice'
import ManualInspection from './ManualInspection' import ManualInspection from './ManualInspection'
import { deployDevice } from "@/api/initHome" import { deployDevice } from "@/api/initHome"
import PermissionButton from '@/components/PermissionButton/index.vue'
import { useMenuStore } from "@/store/menu"
type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel' type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel'
@ -41,6 +43,7 @@ const Status = defineComponent({
const diagnoseData = ref<Partial<Record<string, any>>>() const diagnoseData = ref<Partial<Record<string, any>>>()
const bindParentVisible = ref<boolean>(false) const bindParentVisible = ref<boolean>(false)
const menuStory = useMenuStore();
const configuration = reactive<{ const configuration = reactive<{
product: Record<string, any>, product: Record<string, any>,
@ -57,19 +60,8 @@ const Status = defineComponent({
artificialData.value = params artificialData.value = params
} }
// TODO
const jumpAccessConfig = () => { const jumpAccessConfig = () => {
// const purl = getMenuPathByCode(MENUS_CODE['device/Product/Detail']); menuStory.jumpPage('device/Product/Detail', { id: unref(device).productId, tab: 'access' });
// if (purl) {
// history.push(
// `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], device.productId)}`,
// {
// tab: 'access',
// },
// );
// } else {
// message.error('规则可能有加密处理,请联系管理员');
// }
}; };
const jumpDeviceConfig = () => { const jumpDeviceConfig = () => {
@ -123,34 +115,40 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={ text={
<span><Popconfirm <span>
title="确认启用" <PermissionButton
onConfirm={async () => { type="link"
const res = await startNetwork( hasPermission="link/Type:action"
unref(gateway)?.channelId, popConfirm={{
); title: '确认启用',
if (res.status === 200) { onConfirm: async () => {
message.success('操作成功!'); const res = await startNetwork(
list.value = modifyArrayList( unref(gateway)?.channelId,
list.value, );
{ if (res.status === 200) {
key: 'network', message.success('操作成功!');
name: '网络组件', list.value = modifyArrayList(
desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败', list.value,
status: 'success', {
text: '正常', key: 'network',
info: null, name: '网络组件',
}, desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
); status: 'success',
} text: '正常',
}} info: null,
> },
<Button type="link" style="padding: 0"></Button> );
</Popconfirm></span> }
}
}}
>
</PermissionButton>
</span>
} }
/> />
</div> </div>
</div> </div >
) : ( ) : (
<div> <div>
<div class={styles.infoItem}> <div class={styles.infoItem}>
@ -287,28 +285,31 @@ const Status = defineComponent({
<Badge <Badge
status="default" status="default"
text={<span> text={<span>
<Popconfirm <PermissionButton
title="确认启用" hasPermission="link/Type:action"
onConfirm={async () => { popConfirm={{
const resp = await startGateway(unref(device).accessId || ''); title: '确认启用',
if (resp.status === 200) { onConfirm: async () => {
message.success('操作成功!'); const resp = await startGateway(unref(device).accessId || '');
list.value = modifyArrayList( if (resp.status === 200) {
list.value, message.success('操作成功!');
{ list.value = modifyArrayList(
key: 'gateway', list.value,
name: '设备接入网关', {
desc: desc, key: 'gateway',
status: 'success', name: '设备接入网关',
text: '正常', desc: desc,
info: null, status: 'success',
}, text: '正常',
); info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span>} </span>}
/> />
</div> </div>
@ -411,28 +412,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="link/AccessConfig:action"
const resp = await startGateway(unref(device).accessId || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await startGateway(unref(device).accessId || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'gateway', list.value = modifyArrayList(
name: '设备接入网关', list.value,
desc: desc, {
status: 'success', key: 'gateway',
text: '正常', name: '设备接入网关',
info: null, desc: desc,
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -519,28 +524,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
const resp = await _deploy(response?.result?.id || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await _deploy(response?.result?.id || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'parent-device', list.value = modifyArrayList(
name: '网关父设备', list.value,
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败', {
status: 'success', key: 'parent-device',
text: '正常', name: '网关父设备',
info: null, desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />
@ -623,28 +632,32 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Product:action"
const resp = await _deployProduct(unref(device).productId || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
message.success('操作成功!'); onConfirm: async () => {
list.value = modifyArrayList( const resp = await _deployProduct(unref(device).productId || '');
list.value, if (resp.status === 200) {
{ message.success('操作成功!');
key: 'product', list.value = modifyArrayList(
name: '产品状态', list.value,
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败', {
status: 'success', key: 'product',
text: '正常', name: '产品状态',
info: null, desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
@ -695,29 +708,34 @@ const Status = defineComponent({
status="default" status="default"
text={ text={
<span> <span>
<Popconfirm
title="确认启用" <PermissionButton
onConfirm={async () => { hasPermission="device/Instance:action"
const resp = await _deploy(unref(device)?.id || ''); popConfirm={{
if (resp.status === 200) { title: '确认启用',
instanceStore.current.state = { value: 'offline', text: '离线' } onConfirm: async () => {
message.success('操作成功!'); const resp = await _deploy(unref(device)?.id || '');
list.value = modifyArrayList( if (resp.status === 200) {
list.value, instanceStore.current.state = { value: 'offline', text: '离线' }
{ message.success('操作成功!');
key: 'device', list.value = modifyArrayList(
name: '设备状态', list.value,
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败', {
status: 'success', key: 'device',
text: '正常', name: '设备状态',
info: null, desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
}, status: 'success',
); text: '正常',
info: null,
},
);
}
} }
}} }}
> >
<Button type="link" style="padding: 0"></Button>
</Popconfirm> </PermissionButton>
</span> </span>
} }
/> />

View File

@ -5,7 +5,7 @@
:columns="columns" :columns="columns"
:request="_getEventList" :request="_getEventList"
model="TABLE" model="TABLE"
:bodyStyle="{padding: '0 24px'}" :bodyStyle="{ padding: '0 24px' }"
> >
<template #timestamp="slotProps"> <template #timestamp="slotProps">
{{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }} {{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
@ -19,18 +19,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import moment from 'moment' import moment from 'moment';
import { getEventList } from '@/api/device/instance' import { getEventList } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance' import { useInstanceStore } from '@/store/instance';
import { Modal } from 'ant-design-vue' import { Modal } from 'ant-design-vue';
const events = defineProps({ const events = defineProps({
data: { data: {
type: Object, type: Object,
default: () => {} default: () => {},
} },
}) });
const instanceStore = useInstanceStore() const instanceStore = useInstanceStore();
const columns = ref<Record<string, any>>([ const columns = ref<Record<string, any>>([
{ {
@ -38,43 +38,52 @@ const columns = ref<Record<string, any>>([
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
scopedSlots: true, scopedSlots: true,
search: {
type: 'date',
},
}, },
{ {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
key: 'action', key: 'action',
scopedSlots: true, scopedSlots: true,
} },
]) ]);
const params = ref<Record<string, any>>({}) const params = ref<Record<string, any>>({});
const _getEventList = () => getEventList(instanceStore.current.id || '', events.data.id || '', params.value) const _getEventList = () =>
getEventList(
instanceStore.current.id || '',
events.data.id || '',
params.value,
);
watchEffect(() => { watchEffect(() => {
if(events.data?.valueType?.type === 'object'){ if (events.data?.valueType?.type === 'object') {
(events.data.valueType?.properties || []).map((i: any) => { (events.data.valueType?.properties || []).map((i: any) => {
columns.value.splice(0, 0, { columns.value.splice(0, 0, {
key: i.id, key: i.id,
title: i.name, title: i.name,
dataIndex: `${i.id}_format` dataIndex: `${i.id}_format`,
}) search: {
}) type: 'string',
},
});
});
} else { } else {
columns.value.splice(0, 0, { columns.value.splice(0, 0, {
title: '数据', title: '数据',
dataIndex: 'value', dataIndex: 'value',
}) });
} }
}) });
const detail = () => { const detail = () => {
Modal.info({ Modal.info({
title: () => '详情', title: () => '详情',
width: 850, width: 850,
content: () => h('div', {}, [ content: () => h('div', {}, [h('p', '暂未开发')]),
h('p', '暂未开发'), okText: '关闭',
]), });
okText: '关闭' };
});
}
</script> </script>

View File

@ -1,3 +1,54 @@
<!-- 坐标点拾取组件 -->
<template> <template>
<div>AMap</div> <div style="width: 100%; height: 400px">
</template> <div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space>
<a-button type="primary" @click="start">开始动画</a-button>
<a-button type="primary" @click="stop">停止动画</a-button>
</a-space>
</div>
</div>
<el-amap :center="center" :zooms="[3, 20]" @init="initMap" ref="map"></el-amap>
</div>
</template>
<script setup lang="ts">
import { initAMapApiLoader } from '@vuemap/vue-amap';
import AMapUI from '@vuemap/vue-amap'
import '@vuemap/vue-amap/dist/style.css';
initAMapApiLoader({
key: 'a0415acfc35af15f10221bfa5a6850b4',
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0',
});
interface EmitProps {
(e: 'update:points', data: string): void;
}
const props = defineProps({
points: { type: Array, default: () => [] },
});
const emit = defineEmits<EmitProps>();
// ()
const mapPoint = ref('');
const map = ref(null);
const center = ref([106.55, 29.56]);
const marker = ref(null);
/**
* 地图初始化
* @param e
*/
const initMap = (e: any) => {
console.log(e)
// map = e;
// const pointStr = mapPoint.value as string;
};
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="chart" ref="chart"></div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
const { proxy } = <any>getCurrentInstance();
const props = defineProps({
//
options:{
type:Object,
default:()=>{}
}
});
/**
* 绘制图表
*/
const createChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.chart);
myChart.setOption(props.options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
};
watch(
() => props.options,
() => createChart(),
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,3 +1,218 @@
<template> <template>
<div>Charts</div> <a-spin :spinning="loading">
</template> <div>
<a-space>
<div>
统计周期
<a-select v-model:value="cycle" style="width: 120px">
<a-select-option value="*" v-if="_type"
>实际值</a-select-option
>
<a-select-option value="1m">按分钟统计</a-select-option>
<a-select-option value="1h">按小时统计</a-select-option>
<a-select-option value="1d">按天统计</a-select-option>
<a-select-option value="1w">按周统计</a-select-option>
<a-select-option value="1M">按月统计</a-select-option>
</a-select>
</div>
<div v-if="cycle !== '*' && _type">
统计规则
<a-select v-model:value="agg" style="width: 120px">
<a-select-option value="AVG">平均值</a-select-option>
<a-select-option value="MAX">最大值</a-select-option>
<a-select-option value="MIN">最小值</a-select-option>
<a-select-option value="COUNT">总数</a-select-option>
</a-select>
</div>
</a-space>
</div>
<div style="width: 100%; height: 500px">
<Chart :options="options" v-if="chartsList.length" />
<JEmpty v-else />
</div>
</a-spin>
</template>
<script lang="ts" setup>
import { getPropertiesInfo, getPropertiesList } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import Chart from './Chart.vue';
import * as echarts from 'echarts';
const list = ['int', 'float', 'double', 'long'];
const prop = defineProps({
data: {
type: Object,
default: () => {},
},
time: {
type: Array,
default: () => [],
},
});
const cycle = ref<string>('*');
const agg = ref<string>('AVG');
const loading = ref<boolean>(false);
const chartsList = ref<any[]>([]);
const instanceStore = useInstanceStore();
const options = ref({});
const _type = computed(() => {
const flag = list.includes(prop.data?.valueType?.type || '')
cycle.value = flag ? '*' : '1m'
return flag
});
const queryChartsAggList = async () => {
loading.value = true;
const resp = await getPropertiesInfo(instanceStore.current.id, {
columns: [
{
property: prop.data.id,
alias: prop.data.id,
agg: agg.value,
},
],
query: {
interval: cycle.value,
format: 'yyyy-MM-dd HH:mm:ss',
from: prop.time[0],
to: prop.time[1],
},
});
loading.value = false;
if (resp.status === 200) {
const dataList: any[] = [
{
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
},
];
(resp.result as any[]).forEach((i: any) => {
dataList.push({
...i,
year: i.time,
value: Number(i[prop.data.id || '']),
type: prop.data?.name || '',
});
});
dataList.push({
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
});
chartsList.value = (dataList || []).reverse();
}
};
const queryChartsList = async () => {
loading.value = true;
const resp = await getPropertiesList(
instanceStore.current.id,
prop.data.id,
{
paging: false,
terms: [
{
column: 'timestamp$BTW',
value:
prop.time[0] && prop.time[1]
? [prop.time[0], prop.time[1]]
: [],
type: 'and',
},
],
sorts: [{ name: 'timestamp', order: 'asc' }],
},
);
loading.value = false;
if (resp.status === 200) {
const dataList: any[] = [
{
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
},
];
(resp.result as any)?.data?.forEach((i: any) => {
dataList.push({
...i,
year: i.timestamp,
value: i.value,
type: prop.data?.name || '',
});
});
dataList.push({
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
});
chartsList.value = dataList || [];
}
};
const getOptions = (arr: any[]) => {
options.value = {
xAxis: {
type: 'category',
data: arr.map((item) => {
return echarts.format.formatTime(
'yyyy-MM-dd\nhh:mm:ss',
item.year,
false,
);
}),
name: '时间',
},
yAxis: {
type: 'value',
name: arr[0]?.type,
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 10,
},
{
start: 0,
end: 10,
},
],
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
},
series: [
{
data: arr.map((i: any) => i.value),
type: 'line',
areaStyle: {},
},
],
};
};
watch(
() => [cycle, agg],
([newCycle, newAgg]) => {
if (newCycle.value === '*' && _type.value) {
queryChartsList();
} else {
queryChartsAggList();
}
},
{ deep: true, immediate: true },
);
watchEffect(() => {
if (chartsList.value.length) {
getOptions(chartsList.value);
}
});
</script>

View File

@ -0,0 +1,74 @@
<template>
<a-spin :spinning="loading">
<div style="position: relative">
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
<a-space>
<a-button type="primary">开始动画</a-button>
<a-button type="primary">停止动画</a-button>
</a-space>
</div>
</div>
<AMap :points="geoList" />
</a-spin>
</template>
<script lang="ts" setup>
import { getPropertyData } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery';
import AMap from './AMap.vue';
const instanceStore = useInstanceStore();
const prop = defineProps({
data: {
type: Object,
default: () => {},
},
time: {
type: Array,
default: () => [],
},
});
const geoList = ref<any[]>([]);
const loading = ref<boolean>(false);
const query = async () => {
loading.value = true;
const resp = await getPropertyData(
instanceStore.current.id,
encodeQuery({
paging: false,
terms: {
property: prop.data.id,
timestamp$BTW: prop.time[0] && prop.time[1] ? prop.time : [],
},
sorts: { timestamp: 'asc' },
}),
);
loading.value = false;
if (resp.status === 200) {
const list: any[] = [];
((resp.result as any)?.data || []).forEach((item: any) => {
list.push([item.value.lon, item.value.lat]);
});
geoList.value = list
}
};
watch(
() => [prop.data.id, prop.time],
([newVal]) => {
if (newVal) {
query();
}
},
{
deep: true, immediate: true
}
);
</script>
<style lang="less" scoped>
</style>

View File

@ -18,6 +18,13 @@
<template v-if="column.key === 'timestamp'"> <template v-if="column.key === 'timestamp'">
{{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }} {{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
</template> </template>
<template v-if="column.key === 'value'">
<ValueRender
type="table"
:data="_props.data"
:value="{ formatValue: record.value }"
/>
</template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <a-space>
<a-button <a-button
@ -30,7 +37,7 @@
@click="_download(record)" @click="_download(record)"
><AIcon type="DownloadOutlined" ><AIcon type="DownloadOutlined"
/></a-button> /></a-button>
<a-button type="link" <a-button type="link" @click="showDetail(record)"
><AIcon type="SearchOutlined" ><AIcon type="SearchOutlined"
/></a-button> /></a-button>
</a-space> </a-space>
@ -38,6 +45,28 @@
</template> </template>
</a-table> </a-table>
</div> </div>
<a-modal
title="详情"
:visible="visible"
@ok="visible = false"
@cancel="visible = false"
>
<div>自定义属性</div>
<JsonViewer
v-if="
data?.valueType?.type === 'object' ||
data?.valueType?.type === 'array'
"
:expand-depth="5"
:value="current.formatValue"
/>
<a-textarea
v-else-if="data?.valueType?.type === 'file'"
:value="current.formatValue"
:row="3"
/>
<a-input v-else disabled :value="current.formatValue" />
</a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -46,6 +75,8 @@ import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery'; import encodeQuery from '@/utils/encodeQuery';
import moment from 'moment'; import moment from 'moment';
import { getType } from '../index'; import { getType } from '../index';
import ValueRender from '../ValueRender.vue';
import JsonViewer from 'vue-json-viewer';
const _props = defineProps({ const _props = defineProps({
data: { data: {
@ -57,8 +88,11 @@ const _props = defineProps({
default: () => [], default: () => [],
}, },
}); });
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const dataSource = ref({}); const dataSource = ref({});
const current = ref<any>({});
const visible = ref<boolean>(false);
const columns = computed(() => { const columns = computed(() => {
const arr: any[] = [ const arr: any[] = [
@ -92,6 +126,11 @@ const showLoad = computed(() => {
); );
}); });
const showDetail = (item: any) => {
visible.value = true;
current.value = item;
};
const queryPropertyData = async (params: any) => { const queryPropertyData = async (params: any) => {
const resp = await getPropertyData( const resp = await getPropertyData(
instanceStore.current.id, instanceStore.current.id,
@ -99,7 +138,7 @@ const queryPropertyData = async (params: any) => {
...params, ...params,
terms: { terms: {
property: _props.data.id, property: _props.data.id,
timestamp$BTW: _props.time?.length ? _props.time : [], timestamp$BTW: _props.time,
}, },
sorts: { timestamp: 'desc' }, sorts: { timestamp: 'desc' },
}), }),
@ -109,14 +148,20 @@ const queryPropertyData = async (params: any) => {
} }
}; };
watchEffect(() => { watch(
if (_props.data.id) { () => [_props.data.id, _props.time],
queryPropertyData({ ([newVal]) => {
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10, if (newVal) {
pageIndex: 0, queryPropertyData({
}); pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
pageIndex: 0,
});
}
},
{
deep: true, immediate: true
} }
}); );
const onChange = (page: any) => { const onChange = (page: any) => {
queryPropertyData({ queryPropertyData({
@ -140,4 +185,10 @@ const _download = (record: any) => {
// //
document.body.removeChild(downNode); document.body.removeChild(downNode);
}; };
</script> </script>
<style lang="less" scoped>
:deep(.ant-pagination-item) {
display: none !important;
}
</style>

View File

@ -2,15 +2,15 @@
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel"> <a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div> <div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
<div> <div>
<a-tabs v-model:activeKey="activeKey"> <a-tabs v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
<a-tab-pane key="table" tab="列表"> <a-tab-pane key="table" tab="列表">
<Table :data="props.data" :time="_getTimes" /> <Table :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="charts" tab="图表"> <a-tab-pane key="charts" tab="图表">
<Charts /> <Charts :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="geo" tab="轨迹"> <a-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'">
<AMap /> <PropertyAMap :data="props.data" :time="_getTimes" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
@ -21,7 +21,7 @@
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import TimeComponent from './TimeComponent.vue' import TimeComponent from './TimeComponent.vue'
import Charts from './Charts.vue' import Charts from './Charts.vue'
import AMap from './AMap.vue' import PropertyAMap from './PropertyAMap.vue'
import Table from './Table.vue' import Table from './Table.vue'
const props = defineProps({ const props = defineProps({

View File

@ -1,15 +1,15 @@
<template> <template>
<a-card :hoverable="true" class="card-box"> <a-card :hoverable="true" class="card-box">
<!-- <a-spin :spinning="loading"> --> <!-- <a-spin :spinning="loading"> -->
<div class="card-container"> <div class="card-container">
<div class="header"> <div class="header">
<div class="title">{{ _props.data.name }}</div> <div class="title">{{ _props.data.name }}</div>
<div class="extra"> <div class="extra">
<a-space :size="16"> <a-space :size="16">
<template v-for="i in actions" :key="i.key">
<a-tooltip <a-tooltip
v-for="i in actions"
:key="i.key"
v-bind="i.tooltip" v-bind="i.tooltip"
v-if="i.key !== 'edit'"
> >
<a-button <a-button
style="padding: 0; margin: 0" style="padding: 0; margin: 0"
@ -17,26 +17,48 @@
:disabled="i.disabled" :disabled="i.disabled"
@click="i.onClick && i.onClick(data)" @click="i.onClick && i.onClick(data)"
> >
<AIcon :type="i.icon" style="color: #323130; font-size: 12px" /> <AIcon
:type="i.icon"
style="color: #323130; font-size: 12px"
/>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</a-space> <PermissionButton
</div> :disabled="i.disabled"
</div> v-else
<div class="value"> :popConfirm="i.popConfirm"
<ValueRender :data="data" :value="_props.data" type="card" /> :tooltip="i.tooltip"
</div> @click="i.onClick && i.onClick(slotProps)"
<div class="bottom"> type="link"
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div> style="padding: 0px"
<div class="time-value">{{_props?.data?.timeString || '--'}}</div> :hasPermission="'device/Instance:update'"
>
<template #icon
><AIcon :type="i.icon" style="color: #323130; font-size: 12px"
/></template>
</PermissionButton>
</template>
</a-space>
</div> </div>
</div> </div>
<div class="value">
<ValueRender :data="data" :value="_props.data" type="card" />
</div>
<div class="bottom">
<div style="color: rgba(0, 0, 0, 0.65); font-size: 12px">
更新时间
</div>
<div class="time-value">
{{ _props?.data?.timeString || '--' }}
</div>
</div>
</div>
<!-- </a-spin> --> <!-- </a-spin> -->
</a-card> </a-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ValueRender from './ValueRender.vue' import ValueRender from './ValueRender.vue';
const _props = defineProps({ const _props = defineProps({
data: { data: {
type: Object, type: Object,
@ -44,7 +66,7 @@ const _props = defineProps({
}, },
actions: { actions: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
}); });
// const loading = ref<boolean>(true); // const loading = ref<boolean>(true);
@ -101,6 +123,6 @@ const _props = defineProps({
color: #000; color: #000;
} }
} }
} }
} }
</style> </style>

View File

@ -13,23 +13,18 @@
<a-image :src="value?.formatValue" /> <a-image :src="value?.formatValue" />
</template> </template>
<template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)"> <template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)">
<!-- TODO 视频组件缺失 -->
</template> </template>
<template v-else> <template v-else>
<!-- <json-viewer <JsonViewer
:value="{ :expand-depth="5"
'id': '123' :value="value?.formatValue"
}" />
copyable
boxed
sort
></json-viewer> -->
</template> </template>
</a-modal> </a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import JsonViewer from 'vue3-json-viewer'; import JsonViewer from 'vue-json-viewer';
const _data = defineProps({ const _data = defineProps({
type: { type: {
@ -46,9 +41,6 @@ const handleCancel = () => {
_emit('close'); _emit('close');
}; };
// watchEffect(() => {
// console.log(_data.value?.formatValue)
// })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="value"> <div class="value">
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div> <div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div>
<div v-else-if="data?.valueType?.type === 'file'"> <div v-else-if="_data.data?.valueType?.type === 'file'">
<template v-if="data?.valueType?.fileType === 'base64'"> <template v-if="data?.valueType?.fileType === 'base64'">
<div :class="valueClass" v-if="!!getType(value?.formatValue)"> <div :class="valueClass" v-if="!!getType(value?.formatValue)">
<img :src="imgMap.get(_type)" @error="onError" /> <img :src="imgMap.get(_type)" @error="onError" />
@ -36,10 +36,10 @@
</template> </template>
</template> </template>
</div> </div>
<div v-else-if="data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass"> <div v-else-if="_data.data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
<img :src="imgMap.get('obj')" /> <img :src="imgMap.get('obj')" />
</div> </div>
<div v-else-if="data?.valueType?.type === 'geoPoint' || data?.valueType?.type === 'array'" :class="valueClass"> <div v-else-if="_data.data?.valueType?.type === 'geoPoint' || _data.data?.valueType?.type === 'array'" :class="valueClass">
{{JSON.stringify(value?.formatValue)}} {{JSON.stringify(value?.formatValue)}}
</div> </div>
<div v-else :class="valueClass"> <div v-else :class="valueClass">
@ -53,7 +53,7 @@
import { getImage } from "@/utils/comm"; import { getImage } from "@/utils/comm";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import ValueDetail from './ValueDetail.vue' import ValueDetail from './ValueDetail.vue'
import {getType, imgMap} from './index' import {getType, imgMap, imgList, videoList, fileList} from './index'
const _data = defineProps({ const _data = defineProps({
data: { data: {
@ -115,7 +115,6 @@ const getDetail = (_type: string) => {
_types.value = flag _types.value = flag
visible.value = true visible.value = true
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -32,20 +32,30 @@
</template> </template>
<template #action="slotProps"> <template #action="slotProps">
<a-space :size="16"> <a-space :size="16">
<a-tooltip <template v-for="i in getActions(slotProps)" :key="i.key">
v-for="i in getActions(slotProps)" <a-tooltip v-bind="i.tooltip" v-if="i.key !== 'edit'">
:key="i.key" <a-button
v-bind="i.tooltip" style="padding: 0"
> type="link"
<a-button :disabled="i.disabled"
style="padding: 0" @click="i.onClick && i.onClick(slotProps)"
type="link" >
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
<PermissionButton
:disabled="i.disabled" :disabled="i.disabled"
v-else
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
@click="i.onClick && i.onClick(slotProps)" @click="i.onClick && i.onClick(slotProps)"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:update'"
> >
<AIcon :type="i.icon" /> <template #icon><AIcon :type="i.icon" /></template>
</a-button> </PermissionButton>
</a-tooltip> </template>
</a-space> </a-space>
</template> </template>
<template #paginationRender> <template #paginationRender>
@ -76,7 +86,11 @@
@close="indicatorVisible = false" @close="indicatorVisible = false"
:data="currentInfo" :data="currentInfo"
/> />
<Detail v-if="detailVisible" :data="currentInfo" @close="detailVisible = false" /> <Detail
v-if="detailVisible"
:data="currentInfo"
@close="detailVisible = false"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -240,11 +254,15 @@ const subscribeProperty = () => {
?.pipe(map((res: any) => res.payload)) ?.pipe(map((res: any) => res.payload))
.subscribe((payload) => { .subscribe((payload) => {
list.value = [...list.value, payload]; list.value = [...list.value, payload];
unref(list).sort((a: any, b: any) => a.timestamp - b.timestamp) unref(list)
.forEach((item: any) => { .sort((a: any, b: any) => a.timestamp - b.timestamp)
const { value } = item; .forEach((item: any) => {
propertyValue.value[value?.property] = { ...item, ...value }; const { value } = item;
}); propertyValue.value[value?.property] = {
...item,
...value,
};
});
// list.value = [...list.value, payload]; // list.value = [...list.value, payload];
// throttle(valueChange(list.value), 500); // throttle(valueChange(list.value), 500);
}); });
@ -337,8 +355,8 @@ const onSearch = () => {
}; };
onUnmounted(() => { onUnmounted(() => {
subRef.value && subRef.value?.unsubscribe() subRef.value && subRef.value?.unsubscribe();
}) });
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -2,7 +2,7 @@
<page-container <page-container
:tabList="list" :tabList="list"
@back="onBack" @back="onBack"
:tabActiveKey="instanceStore.active" :tabActiveKey="instanceStore.tabActiveKey"
@tabChange="onTabChange" @tabChange="onTabChange"
> >
<template #title> <template #title>

View File

@ -155,13 +155,12 @@
/> />
</template> </template>
<template #content> <template #content>
<h3 <Ellipsis style="width: calc(100% - 100px)">
class="card-item-content-title" <span style="font-size: 16px; font-weight: 600" @click.stop="handleView(slotProps.id)">
@click.stop="handleView(slotProps.id)" {{ slotProps.name }}
> </span>
{{ slotProps.name }} </Ellipsis>
</h3> <a-row style="margin-top: 20px">
<a-row>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text"> <div class="card-item-content-text">
设备类型 设备类型
@ -172,7 +171,9 @@
<div class="card-item-content-text"> <div class="card-item-content-text">
产品名称 产品名称
</div> </div>
<div>{{ slotProps.productName }}</div> <Ellipsis style="width: 100%">
{{ slotProps.productName }}
</Ellipsis>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>

View File

@ -1,167 +1,180 @@
<template> <template>
<page-container> <page-container>
<Search <Search
:columns="query.columns" :columns="query.columns"
target="product-manage" target="product-manage"
@search="handleSearch" @search="handleSearch"
/> />
<JTable <JTable
:columns="columns" :columns="columns"
:request="queryProductList" :request="queryProductList"
ref="tableRef" ref="tableRef"
:defaultParams="{ :defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }], sorts: [{ name: 'createTime', order: 'desc' }],
}" }"
:params="params" :params="params"
> >
<template #headerTitle> <template #headerTitle>
<a-space> <a-space>
<a-button type="primary" @click="add" <a-button type="primary" @click="add"
><plus-outlined />新增</a-button ><plus-outlined />新增</a-button
>
<a-upload
name="file"
accept=".json"
:showUploadList="false"
:before-upload="beforeUpload"
>
<a-button>导入</a-button>
</a-upload>
</a-space>
</template>
<template #deviceType="slotProps">
<div>{{ slotProps.deviceType.text }}</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
> >
<template #img> <a-upload
<slot name="img"> name="file"
<img :src="getImage('/device-product.png')" /> accept=".json"
</slot> :showUploadList="false"
</template> :before-upload="beforeUpload"
<template #content> >
<h3 <a-button>导入</a-button>
</a-upload>
</a-space>
</template>
<template #deviceType="slotProps">
<div>{{ slotProps.deviceType.text }}</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
@click="handleClick"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:active="_selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
>
<template #img>
<slot name="img">
<img
:src="
slotProps.photoUrl ||
getImage('/device-product.png')
"
class="productImg"
/>
</slot>
</template>
<template #content>
<Ellipsis
><span
@click.stop="handleView(slotProps.id)" @click.stop="handleView(slotProps.id)"
style="font-weight: 600" style="font-weight: 600; font-size: 16px"
> >
{{ slotProps.name }} {{ slotProps.name }}
</h3> </span></Ellipsis
<a-row> >
<a-col :span="12"> <a-row>
<div class="card-item-content-text"> <a-col :span="12">
设备类型 <div class="card-item-content-text">
</div> 设备类型
<div>直连设备</div> </div>
</a-col> <div>{{ slotProps?.deviceType?.text }}</div>
</a-row> </a-col>
</template> <a-col :span="12">
<template #actions="item"> <div class="card-item-content-text">
<a-tooltip 接入方式
v-bind="item.tooltip" </div>
:title="item.disabled && item.tooltip.title" <Ellipsis
> ><div>
<a-popconfirm {{ slotProps?.accessName }}
v-if="item.popConfirm" </div></Ellipsis
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
> >
<a-button :disabled="item.disabled"> </a-col>
<AIcon </a-row>
type="DeleteOutlined" </template>
v-if="item.key === 'delete'" <template #actions="item">
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state === 1 ? '正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template>
<template #id="slotProps">
<a>{{ slotProps.id }}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip <a-tooltip
v-for="i in getActions(slotProps)" v-bind="item.tooltip"
:key="i.key" :title="item.disabled && item.tooltip.title"
v-bind="i.tooltip"
> >
<a-popconfirm <a-popconfirm
v-if="i.popConfirm" v-if="item.popConfirm"
v-bind="i.popConfirm" v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
> >
<a-button <a-button :disabled="item.disabled">
:disabled="i.disabled" <AIcon
style="padding: 0" type="DeleteOutlined"
type="link" v-if="item.key === 'delete'"
><AIcon :type="i.icon" />
/></a-button> <template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</a-popconfirm> </a-popconfirm>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-button>
</template>
</a-tooltip>
</template>
</CardBox>
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state === 1 ? '正常' : '禁用'"
:status="statusMap.get(slotProps.state)"
/>
</template>
<template #id="slotProps">
<a>{{ slotProps.id }}</a>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<a-button <a-button
:disabled="i.disabled"
style="padding: 0" style="padding: 0"
type="link" type="link"
v-else ><AIcon :type="i.icon"
@click="i.onClick && i.onClick(slotProps)" /></a-button>
> </a-popconfirm>
<a-button <a-button
:disabled="i.disabled" style="padding: 0"
style="padding: 0" type="link"
type="link" v-else
><AIcon :type="i.icon" @click="i.onClick && i.onClick(slotProps)"
/></a-button> >
</a-button> <a-button
</a-tooltip> :disabled="i.disabled"
</a-space> style="padding: 0"
</template> type="link"
</JTable> ><AIcon :type="i.icon"
<!-- 新增编辑 --> /></a-button>
<Save </a-button>
ref="saveRef" </a-tooltip>
:isAdd="isAdd" </a-space>
:title="title" </template>
@success="refresh" </JTable>
/> <!-- 新增编辑 -->
<Save ref="saveRef" :isAdd="isAdd" :title="title" @success="refresh" />
</page-container> </page-container>
</template> </template>
@ -193,11 +206,11 @@ import { isNoCommunity, downloadObject } from '@/utils/utils';
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
import { typeOptions } from '@/components/Search/util'; import { typeOptions } from '@/components/Search/util';
import Save from './Save/index.vue'; import Save from './Save/index.vue';
import { useMenuStore } from 'store/menu' import { useMenuStore } from 'store/menu';
/** /**
* 表格数据 * 表格数据
*/ */
const menuStory = useMenuStore() const menuStory = useMenuStore();
const router = useRouter(); const router = useRouter();
const isAdd = ref<number>(0); const isAdd = ref<number>(0);
const title = ref<string>(''); const title = ref<string>('');
@ -425,7 +438,7 @@ const beforeUpload = (file: any) => {
* 查看 * 查看
*/ */
const handleView = (id: string) => { const handleView = (id: string) => {
menuStory.jumpPage('device/Product/Detail',{id}) menuStory.jumpPage('device/Product/Detail', { id });
}; };
/** /**
@ -643,4 +656,13 @@ const handleSearch = (e: any) => {
padding: 20px; padding: 20px;
background: #f0f2f5; background: #f0f2f5;
} }
.productImg {
width: 88px;
height: 88px;
}
.productName {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</style> </style>

View File

@ -0,0 +1,174 @@
<template>
<page-container>
<Search
type="simple"
:columns="columns"
target="product"
@search="handleSearch"
/>
<JTable
ref="instanceRef"
:columns="columns"
:request="(e:any) => templateApi.getHistory(e, data.id)"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
terms: [{ column: 'notifyType$IN', value: data.type }],
}"
:params="params"
model="table"
>
<template #notifyTime="slotProps">
{{ moment(slotProps.notifyTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
<template #state="slotProps">
<a-space>
<a-badge
:status="slotProps.state.value"
:text="slotProps.state.text"
></a-badge>
<AIcon
v-if="slotProps.state.value === 'error'"
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleError(slotProps.errorStack)"
/>
</a-space>
</template>
<template #action="slotProps">
<AIcon
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleDetail(slotProps.context)"
/>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import templateApi from '@/api/notice/template';
import { PropType } from 'vue';
import moment from 'moment';
import { Modal } from 'ant-design-vue';
type Emits = {
(e: 'update:visible', data: boolean): void;
};
// const emit = defineEmits<Emits>();
const props = defineProps({
visible: { type: Boolean, default: false },
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
// const _vis = computed({
// get: () => props.visible,
// set: (val) => emit('update:visible', val),
// });
// watch(
// () => _vis.value,
// (val) => {
// if (val) handleSearch({ terms: [] });
// },
// );
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '发送时间',
dataIndex: 'notifyTime',
key: 'notifyTime',
scopedSlots: true,
search: {
type: 'date',
handleValue: (v: any) => {
return v;
},
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'error' },
],
handleValue: (v: any) => {
return v;
},
},
},
{
title: '操作',
key: 'action',
scopedSlots: true,
},
];
const params = ref<Record<string, any>>({});
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
// console.log('handleSearch e:', e);
params.value = e;
// console.log('params.value: ', params.value);
};
/**
* 查看错误信息
*/
const handleError = (e: any) => {
Modal.info({
title: '错误信息',
content: h(
'p',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
},
},
JSON.stringify(e),
),
});
};
/**
* 查看详情
*/
const handleDetail = (e: any) => {
Modal.info({
title: '详情信息',
content: h(
'p',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
},
},
JSON.stringify(e),
),
});
};
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,114 @@
export type CatalogItemType = {
district?: string;
device?: string;
platform?: string;
user?: string;
platform_outer?: string;
ext?: string;
};
export interface CatalogItem {
id: string;
channelId: string;
deviceId: string;
name: string;
type: CatalogItemType;
createTime: number;
modifyTime: number;
children?: CatalogItem[];
}
export type ChannelStatusType =
| 'online'
| 'lost'
| 'defect'
| 'add'
| 'delete'
| 'update'
| 'offline';
export type PtzType = 'unknown' | 'ball' | 'hemisphere' | 'fixed' | 'remoteControl';
export type CatalogType = keyof CatalogItemType;
export type ChannelType =
| 'dv_no_storage'
| 'dv_has_storage'
| 'dv_decoder'
| 'networking_monitor_server'
| 'media_proxy'
| 'web_access_server'
| 'video_management_server'
| 'network_matrix'
| 'network_controller'
| 'network_alarm_machine'
| 'dvr'
| 'video_server'
| 'encoder'
| 'decoder'
| 'video_switching_matrix'
| 'audio_switching_matrix'
| 'alarm_controller'
| 'nvr'
| 'hvr'
| 'camera'
| 'ipc'
| 'display'
| 'alarm_input'
| 'alarm_output'
| 'audio_input'
| 'audio_output'
| 'mobile_trans'
| 'other_outer'
| 'center_server'
| 'web_server'
| 'media_dispatcher'
| 'proxy_server'
| 'secure_server'
| 'alarm_server'
| 'database_server'
| 'gis_server'
| 'management_server'
| 'gateway_server'
| 'media_storage_server'
| 'signaling_secure_gateway'
| 'business_group'
| 'virtual_group'
| 'center_user'
| 'end_user'
| 'media_iap'
| 'media_ops'
| 'district'
| 'other';
export interface ChannelItem {
id: string;
deviceId: string;
deviceName: string;
channelId: string;
name: string;
manufacturer: string;
model: string;
address: string;
provider: string;
status: {
value: string;
text: string;
};
others: object;
description: string;
parentChannelId: string;
subCount: integer;
civilCode: string;
ptzType: PtzType;
catalogType: CatalogType;
channelType: ChannelType;
catalogCode: string;
longitude: number;
latitude: number;
createTime: number;
modifyTime: number;
parentId: string;
gb28181ProxyStream: boolean;
gb28181ChannelId: string;
}

View File

@ -51,7 +51,7 @@
v-bind="validateInfos.productId" v-bind="validateInfos.productId"
> >
<a-row :gutter="[0, 10]"> <a-row :gutter="[0, 10]">
<a-col :span="22"> <a-col :span="!!route.query.id ? 24 : 22">
<a-select <a-select
v-model:value="formData.productId" v-model:value="formData.productId"
placeholder="请选择所属产品" placeholder="请选择所属产品"
@ -66,7 +66,7 @@
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-col> </a-col>
<a-col :span="2"> <a-col :span="2" v-if="!route.query.id">
<a-button <a-button
type="link" type="link"
@click="saveProductVis = true" @click="saveProductVis = true"
@ -132,12 +132,11 @@
placeholder="请输入说明" placeholder="请输入说明"
/> />
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{ offset: 0, span: 3 }"> <a-form-item>
<a-button <a-button
type="primary" type="primary"
@click="handleSubmit" @click="handleSubmit"
:loading="btnLoading" :loading="btnLoading"
style="width: 100%"
> >
保存 保存
</a-button> </a-button>
@ -356,6 +355,8 @@ const getDetail = async () => {
// formData.value = res.result; // formData.value = res.result;
Object.assign(formData.value, res.result); Object.assign(formData.value, res.result);
formData.value.channel = res.result.provider; formData.value.channel = res.result.provider;
console.log('formData.value: ', formData.value);
}; };
onMounted(() => { onMounted(() => {
@ -367,6 +368,7 @@ onMounted(() => {
*/ */
const btnLoading = ref<boolean>(false); const btnLoading = ref<boolean>(false);
const handleSubmit = () => { const handleSubmit = () => {
// console.log('formData.value: ', formData.value);
validate() validate()
.then(async () => { .then(async () => {
btnLoading.value = true; btnLoading.value = true;

View File

@ -261,9 +261,13 @@ const getActions = (
}, },
icon: 'EditOutlined', icon: 'EditOutlined',
onClick: () => { onClick: () => {
menuStory.jumpPage('media/Device/Save', { menuStory.jumpPage(
id: data.id, 'media/Device/Save',
}); {},
{
id: data.id,
},
);
}, },
}, },
{ {
@ -277,10 +281,14 @@ const getActions = (
// router.push( // router.push(
// `/media/device/Channel?id=${data.id}&type=${data.provider}`, // `/media/device/Channel?id=${data.id}&type=${data.provider}`,
// ); // );
menuStory.jumpPage('media/Device/Channel', { menuStory.jumpPage(
id: data.id, 'media/Device/Channel',
type: data.provider, {},
}); {
id: data.id,
type: data.provider,
},
);
}, },
}, },
{ {

View File

@ -333,23 +333,9 @@ watch(
msgType.value = MSG_TYPE[val]; msgType.value = MSG_TYPE[val];
formData.value.provider = formData.value.provider =
formData.value.provider !== ':id' route.params.id !== ':id'
? formData.value.provider ? formData.value.provider
: msgType.value[0].value; : msgType.value[0].value;
// formData.value.configuration =
// CONFIG_FIELD_MAP[val][formData.value.provider];
// clearValid();
},
);
watch(
() => formData.value.provider,
(val) => {
// formData.value.configuration =
// CONFIG_FIELD_MAP[formData.value.type][val];
// clearValid();
}, },
); );
@ -421,12 +407,6 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
formRules.value, formRules.value,
); );
const clearValid = () => {
setTimeout(() => {
clearValidate();
}, 200);
};
const getDetail = async () => { const getDetail = async () => {
if (route.params.id === ':id') return; if (route.params.id === ':id') return;
const res = await configApi.detail(route.params.id as string); const res = await configApi.detail(route.params.id as string);
@ -444,7 +424,7 @@ const handleTypeChange = () => {
setTimeout(() => { setTimeout(() => {
formData.value.configuration = formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider]; CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles(); resetPublicFiles();
}, 0); }, 0);
}; };
@ -454,7 +434,48 @@ const handleTypeChange = () => {
const handleProviderChange = () => { const handleProviderChange = () => {
formData.value.configuration = formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider]; CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles(); resetPublicFiles();
};
/**
* 重置字段值
*/
const resetPublicFiles = () => {
switch (formData.value.provider) {
case 'dingTalkMessage':
formData.value.configuration.appKey = '';
formData.value.configuration.appSecret = '';
break;
case 'dingTalkRobotWebHook':
formData.value.configuration.url = '';
break;
case 'corpMessage':
formData.value.configuration.corpId = '';
formData.value.configuration.corpSecret = '';
break;
case 'embedded':
formData.value.configuration.host = '';
formData.value.configuration.port = 25;
formData.value.configuration.ssl = false;
formData.value.configuration.sender = '';
formData.value.configuration.username = '';
formData.value.configuration.password = '';
break;
case 'aliyun':
formData.value.configuration.regionId = '';
formData.value.configuration.accessKeyId = '';
formData.value.configuration.secret = '';
break;
case 'aliyunSms':
formData.value.configuration.regionId = '';
formData.value.configuration.accessKeyId = '';
formData.value.configuration.secret = '';
break;
case 'http':
formData.value.configuration.url = undefined;
formData.value.configuration.headers = [];
break;
}
}; };
/** /**

View File

@ -7,7 +7,7 @@
@cancel="_vis = false" @cancel="_vis = false"
width="80%" width="80%"
> >
<a-row :gutter="10"> <a-row :gutter="10" class="model-body">
<a-col :span="4"> <a-col :span="4">
<a-input <a-input
v-model:value="deptName" v-model:value="deptName"
@ -40,6 +40,7 @@
:dataSource="dataSource" :dataSource="dataSource"
:loading="tableLoading" :loading="tableLoading"
model="table" model="table"
noPagination
> >
<template #headerTitle> <template #headerTitle>
<a-button type="primary" @click="handleAutoBind"> <a-button type="primary" @click="handleAutoBind">
@ -273,14 +274,24 @@ const getActions = (
* 自动绑定 * 自动绑定
*/ */
const handleAutoBind = () => { const handleAutoBind = () => {
configApi.dingTalkBindUser([], props.data.id).then(() => { const arr = dataSource.value
.filter((item: any) => item.userId && item.status.value === 'error')
.map((i: any) => {
return {
userId: i.userId,
providerName: i.userName,
thirdPartyUserId: i.thirdPartyUserId,
};
});
// console.log('arr: ', arr);
configApi.dingTalkBindUser(arr, props.data.id).then(() => {
message.success('操作成功'); message.success('操作成功');
getTableData(); getTableData();
}); });
}; };
/** /**
* 获取钉钉部门用户 * 获取钉钉/微信部门用户
*/ */
const getDeptUsers = async () => { const getDeptUsers = async () => {
let res = null; let res = null;
@ -304,14 +315,24 @@ const getBindUsers = async () => {
return res?.result; return res?.result;
}; };
/** /**
* 获取所有用户 * 获取所有用户未绑定的用户
*/ */
const allUserList = ref([]); const allUserList = ref([]);
const getAllUsers = async () => { const getAllUsers = async () => {
const { result } = await configApi.getPlatformUsers(); const params = {
paging: false,
terms: [
{
column: `id$user-third$${props.data.type}_${props.data.provider}$not`,
value: props.data.id,
},
],
};
const { result } = await configApi.getPlatformUsers(params);
allUserList.value = result.map((m: any) => ({ allUserList.value = result.map((m: any) => ({
label: m.name, label: m.name,
value: m.id, value: m.id,
...m,
})); }));
return result; return result;
}; };
@ -326,31 +347,36 @@ const getTableData = () => {
Promise.all<any>([getDeptUsers(), getBindUsers(), getAllUsers()]).then( Promise.all<any>([getDeptUsers(), getBindUsers(), getAllUsers()]).then(
(res) => { (res) => {
dataSource.value = []; dataSource.value = [];
const [deptUsers, bindUsers, allUsers] = res; const [deptUsers, bindUsers, unBindUsers] = res;
(deptUsers || []).forEach((item: any) => { (deptUsers || []).forEach((deptUser: any) => {
//
let unBindUser = unBindUsers.find(
(f: any) => f.name === deptUser?.name,
);
// //
const bindUser = bindUsers.find( const bindUser = bindUsers.find(
(f: any) => f.thirdPartyUserId === item.id, (f: any) => f.thirdPartyUserId === deptUser.id,
);
//
const allUser = allUsers.find(
(f: any) => f.id === bindUser?.userId,
); );
if (bindUser) {
unBindUser = unBindUsers.find(
(f: any) => f.id === bindUser.userId,
);
}
dataSource.value.push({ dataSource.value.push({
thirdPartyUserId: item.id, thirdPartyUserId: deptUser.id,
thirdPartyUserName: item.name, thirdPartyUserName: deptUser.name,
userId: bindUser?.userId, bindId: bindUser?.userId,
userName: allUser userId: unBindUser?.id,
? `${allUser.name}(${allUser.username})` userName: unBindUser
? `${unBindUser.name}(${unBindUser.username})`
: '', : '',
status: { status: {
text: bindUser?.providerName ? '已绑定' : '未绑定', text: bindUser?.providerName ? '已绑定' : '未绑定',
value: bindUser?.providerName ? 'success' : 'error', value: bindUser?.providerName ? 'success' : 'error',
}, },
bindId: bindUser?.id,
}); });
}); });
console.log('dataSource.value: ', dataSource.value); // console.log('dataSource.value: ', dataSource.value);
}, },
); );
tableLoading.value = false; tableLoading.value = false;
@ -369,7 +395,11 @@ watch(
*/ */
const bindVis = ref(false); const bindVis = ref(false);
const confirmLoading = ref(false); const confirmLoading = ref(false);
const formData = ref({ userId: '' }); const formData = ref({
userId: '',
thirdPartyUserId: '',
thirdPartyUserName: '',
});
const formRules = ref({ const formRules = ref({
userId: [{ required: true, message: '请选择用户', trigger: 'change' }], userId: [{ required: true, message: '请选择用户', trigger: 'change' }],
}); });
@ -381,7 +411,8 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
const handleBind = (row: any) => { const handleBind = (row: any) => {
bindVis.value = true; bindVis.value = true;
formData.value = row; // formData.value = row;
Object.assign(formData.value, row);
getAllUsers(); getAllUsers();
}; };
@ -402,8 +433,8 @@ const filterOption = (input: string, option: any) => {
const handleBindSubmit = () => { const handleBindSubmit = () => {
validate().then(async () => { validate().then(async () => {
const params = { const params = {
// providerName: formData.value.thirdPartyUserName, providerName: formData.value.thirdPartyUserName,
// thirdPartyUserId: formData.value.thirdPartyUserId, thirdPartyUserId: formData.value.thirdPartyUserId,
userId: formData.value.userId, userId: formData.value.userId,
}; };
confirmLoading.value = true; confirmLoading.value = true;
@ -434,8 +465,13 @@ const handleBindSubmit = () => {
}; };
const handleCancel = () => { const handleCancel = () => {
bindVis.value = false; bindVis.value = false;
resetFields() resetFields();
}; };
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.model-body {
height: 600px;
overflow-y: auto;
}
</style>

View File

@ -29,7 +29,7 @@
<a-button>导入</a-button> <a-button>导入</a-button>
</a-upload> </a-upload>
<a-popconfirm <a-popconfirm
title="确认导出当前页数据" title="确认导出"
ok-text="确定" ok-text="确定"
cancel-text="取消" cancel-text="取消"
@confirm="handleExport" @confirm="handleExport"
@ -308,7 +308,7 @@ const beforeUpload = (file: any) => {
* 导出 * 导出
*/ */
const handleExport = () => { const handleExport = () => {
downloadObject(configRef.value.dataSource, `通知配置`); downloadObject(configRef.value._dataSource, `通知配置`);
}; };
const syncVis = ref(false); const syncVis = ref(false);

View File

@ -92,10 +92,7 @@ const handleChange = (info: UploadChangeParam, id: string | undefined) => {
const targetFileIdx = fileList.value.findIndex((f) => f.id === id); const targetFileIdx = fileList.value.findIndex((f) => f.id === id);
fileList.value[targetFileIdx].name = info.file.name; fileList.value[targetFileIdx].name = info.file.name;
fileList.value[targetFileIdx].location = info.file.response?.result; fileList.value[targetFileIdx].location = info.file.response?.result;
emit( emitEvents();
'update:attachments',
fileList.value.map(({ name, location }) => ({ name, location })),
);
} }
}; };
@ -107,6 +104,7 @@ const handleDelete = (id: string | undefined) => {
const idx = fileList.value.findIndex((f) => f.id === id); const idx = fileList.value.findIndex((f) => f.id === id);
fileList.value.splice(idx, 1); fileList.value.splice(idx, 1);
emitEvents();
}; };
/** /**
@ -118,6 +116,15 @@ const handleAdd = () => {
name: '', name: '',
location: '', location: '',
}); });
emitEvents();
};
const emitEvents = () => {
emit(
'update:attachments',
fileList.value.map(({ name, location }) => ({ name, location })),
);
}; };
/** /**

View File

@ -106,7 +106,8 @@
<a-form-item label="收信部门"> <a-form-item label="收信部门">
<ToOrg <ToOrg
v-model:toParty=" v-model:toParty="
formData.template.toParty formData.template
.departmentIdList
" "
:type="formData.type" :type="formData.type"
:config-id="formData.configId" :config-id="formData.configId"
@ -132,7 +133,7 @@
</template> </template>
<ToUser <ToUser
v-model:toUser=" v-model:toUser="
formData.template.toUser formData.template.userIdList
" "
:type="formData.type" :type="formData.type"
:config-id="formData.configId" :config-id="formData.configId"
@ -800,26 +801,59 @@ const formData = ref<TemplateFormData>({
}); });
/** /**
* 重置公用字段值 * 重置字段值
*/ */
const resetPublicFiles = () => { const resetPublicFiles = () => {
formData.value.template.message = ''; switch (formData.value.provider) {
formData.value.configId = undefined; case 'dingTalkMessage':
formData.value.template.agentId = '';
if ( formData.value.template.message = '';
formData.value.provider === 'dingTalkMessage' || formData.value.template.departmentIdList = '';
formData.value.type === 'weixin' formData.value.template.userIdList = '';
) { break;
formData.value.template.toTag = undefined; case 'dingTalkRobotWebHook':
formData.value.template.toUser = undefined; formData.value.template.message = '';
formData.value.template.agentId = undefined; formData.value.template.messageType = 'markdown';
formData.value.template.markdown = { text: '', title: '' };
break;
case 'corpMessage':
formData.value.template.agentId = '';
formData.value.template.message = '';
formData.value.template.toParty = '';
formData.value.template.toUser = '';
formData.value.template.toTag = '';
break;
case 'embedded':
formData.value.template.subject = '';
formData.value.template.message = '';
formData.value.template.text = '';
formData.value.template.sendTo = [];
formData.value.template.attachments = [];
break;
case 'aliyun':
formData.value.template.templateType = 'tts';
formData.value.template.templateCode = '';
formData.value.template.ttsCode = '';
formData.value.template.message = '';
formData.value.template.playTimes = 1;
formData.value.template.calledShowNumbers = '';
formData.value.template.calledNumber = '';
break;
case 'aliyunSms':
formData.value.template.code = '';
formData.value.template.message = '';
formData.value.template.phoneNumber = '';
formData.value.template.signName = '';
break;
case 'http':
formData.value.template.contextAsBody = true;
formData.value.template.body = '';
break;
} }
if (formData.value.type === 'weixin')
formData.value.template.toParty = undefined; formData.value.configId = undefined;
if (formData.value.type === 'email')
formData.value.template.toParty = undefined;
// formData.value.description = '';
formData.value.variableDefinitions = []; formData.value.variableDefinitions = [];
handleMessageTypeChange();
}; };
// //
@ -831,15 +865,8 @@ watch(
route.params.id !== ':id' route.params.id !== ':id'
? formData.value.provider ? formData.value.provider
: msgType.value[0].value; : msgType.value[0].value;
// formData.value.provider = formData.value.provider || msgType.value[0].value;
// console.log('formData.value.template: ', formData.value.template);
// formData.value.template =
// TEMPLATE_FIELD_MAP[val][formData.value.provider];
if (val !== 'email') getConfigList(); if (val !== 'email') getConfigList();
// clearValid();
// console.log('formData.value: ', formData.value);
if (val === 'sms') { if (val === 'sms') {
getTemplateList(); getTemplateList();
@ -848,15 +875,6 @@ watch(
}, },
); );
// watch(
// () => formData.value.provider,
// (val) => {
// formData.value.template = TEMPLATE_FIELD_MAP[formData.value.type][val];
// clearValid();
// },
// );
// //
const formRules = ref({ const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }], type: [{ required: true, message: '请选择通知方式' }],
@ -917,7 +935,7 @@ watch(
() => formData.value.template.markdown?.title, () => formData.value.template.markdown?.title,
(val) => { (val) => {
if (!val) return; if (!val) return;
variableReg(val); variableReg();
}, },
{ deep: true }, { deep: true },
); );
@ -926,7 +944,7 @@ watch(
() => formData.value.template.link?.title, () => formData.value.template.link?.title,
(val) => { (val) => {
if (!val) return; if (!val) return;
variableReg(val); variableReg();
}, },
{ deep: true }, { deep: true },
); );
@ -935,7 +953,7 @@ watch(
() => formData.value.template.subject, () => formData.value.template.subject,
(val) => { (val) => {
if (!val) return; if (!val) return;
variableReg(val); variableReg();
}, },
{ deep: true }, { deep: true },
); );
@ -945,7 +963,7 @@ watch(
() => formData.value.template.message, () => formData.value.template.message,
(val) => { (val) => {
if (!val) return; if (!val) return;
variableReg(val); variableReg();
}, },
{ deep: true }, { deep: true },
); );
@ -954,21 +972,42 @@ watch(
() => formData.value.template.body, () => formData.value.template.body,
(val) => { (val) => {
if (!val) return; if (!val) return;
variableReg(val); variableReg();
}, },
{ deep: true }, { deep: true },
); );
/**
* 将需要提取变量的字段值拼接为一个字符串, 用于统一提取变量
*/
const spliceStr = () => {
let variableFieldsStr = formData.value.template.message;
if (formData.value.provider === 'dingTalkRobotWebHook') {
if (formData.value.template.messageType === 'markdown')
variableFieldsStr += formData.value.template.markdown
?.title as string;
if (formData.value.template.messageType === 'link')
variableFieldsStr += formData.value.template.link?.title as string;
}
if (formData.value.provider === 'embedded')
variableFieldsStr += formData.value.template.subject as string;
if (formData.value.provider === 'http')
variableFieldsStr += formData.value.template.body as string;
// console.log('variableFieldsStr: ', variableFieldsStr);
return variableFieldsStr || '';
};
/** /**
* 根据字段输入内容, 提取变量 * 根据字段输入内容, 提取变量
* @param value * @param value
*/ */
const variableReg = (value: string) => { const variableReg = () => {
const _val = spliceStr();
// //
const oldKey = formData.value.variableDefinitions?.map((m) => m.id); const oldKey = formData.value.variableDefinitions?.map((m) => m.id);
// ${} // ${}
const pattern = /(?<=\$\{).*?(?=\})/g; const pattern = /(?<=\$\{).*?(?=\})/g;
const titleList = value.match(pattern)?.filter((f) => f); const titleList = _val.match(pattern)?.filter((f) => f);
const newKey = [...new Set(titleList)]; const newKey = [...new Set(titleList)];
const result = newKey?.map((m) => const result = newKey?.map((m) =>
oldKey.includes(m) oldKey.includes(m)
@ -980,28 +1019,37 @@ const variableReg = (value: string) => {
format: '%s', format: '%s',
}, },
); );
formData.value.variableDefinitions = [ formData.value.variableDefinitions = result as IVariableDefinitions[];
...new Set([
...formData.value.variableDefinitions,
...(result as IVariableDefinitions[]),
]),
];
}; };
/** /**
* 钉钉机器人 消息类型选择改变 * 钉钉机器人 消息类型选择改变
*/ */
const handleMessageTypeChange = () => { const handleMessageTypeChange = () => {
delete formData.value.template.markdown;
delete formData.value.template.link;
delete formData.value.template.text;
if (formData.value.template.messageType === 'link') {
formData.value.template.link = {
title: '',
picUrl: '',
messageUrl: '',
text: formData.value.template.message as string,
};
}
if (formData.value.template.messageType === 'markdown') {
formData.value.template.markdown = {
title: '',
text: formData.value.template.message as string,
};
}
if (formData.value.template.messageType === 'text') {
formData.value.template.text = {
content: formData.value.template.message as string,
};
}
formData.value.variableDefinitions = []; formData.value.variableDefinitions = [];
formData.value.template.message = ''; formData.value.template.message = '';
if (formData.value.template.link) {
formData.value.template.link.title = '';
formData.value.template.link.picUrl = '';
formData.value.template.link.messageUrl = '';
}
if (formData.value.template.markdown) {
formData.value.template.markdown.title = '';
}
}; };
/** /**
@ -1037,6 +1085,7 @@ const handleTypeChange = () => {
setTimeout(() => { setTimeout(() => {
formData.value.template = formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider]; TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value.template: ', formData.value.template);
resetPublicFiles(); resetPublicFiles();
}, 0); }, 0);
}; };
@ -1047,6 +1096,8 @@ const handleTypeChange = () => {
const handleProviderChange = () => { const handleProviderChange = () => {
formData.value.template = formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider]; TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value: ', formData.value);
// console.log('formData.value.template: ', formData.value.template);
getConfigList(); getConfigList();
resetPublicFiles(); resetPublicFiles();
}; };
@ -1112,8 +1163,9 @@ const handleSubmit = () => {
setTimeout(() => { setTimeout(() => {
validate() validate()
.then(async () => { .then(async () => {
formData.value.template.ttsCode = if (formData.value.provider === 'ttsCode')
formData.value.template.templateCode; formData.value.template.ttsCode =
formData.value.template.templateCode;
btnLoading.value = true; btnLoading.value = true;
let res; let res;
if (!formData.value.id) { if (!formData.value.id) {

View File

@ -29,7 +29,7 @@
<a-button>导入</a-button> <a-button>导入</a-button>
</a-upload> </a-upload>
<a-popconfirm <a-popconfirm
title="确认导出当前页数据" title="确认导出"
ok-text="确定" ok-text="确定"
cancel-text="取消" cancel-text="取消"
@confirm="handleExport" @confirm="handleExport"
@ -314,7 +314,7 @@ const beforeUpload = (file: any) => {
* 导出 * 导出
*/ */
const handleExport = () => { const handleExport = () => {
downloadObject(configRef.value.dataSource, `通知配置`); downloadObject(configRef.value._dataSource, `通知配置`);
}; };
const syncVis = ref(false); const syncVis = ref(false);

View File

@ -27,16 +27,22 @@ interface ILink {
messageUrl: string; messageUrl: string;
text: string; text: string;
} }
interface IText {
content: string;
}
export type TemplateFormData = { export type TemplateFormData = {
template: { template: {
// 钉钉消息 // 钉钉消息
agentId?: string; agentId?: string;
message?: string; message?: string;
departmentIdList?: string;
userIdList?: string;
// 钉钉机器人 // 钉钉机器人
messageType?: string; messageType?: string;
markdown?: IMarkDown; markdown?: IMarkDown;
link?: ILink; link?: ILink;
text?: IText;
// 微信 // 微信
// agentId?: string; // agentId?: string;
// message?: string; // message?: string;

View File

@ -147,10 +147,12 @@ export const TEMPLATE_FIELD_MAP = {
dingTalkMessage: { dingTalkMessage: {
agentId: '', agentId: '',
message: '', message: '',
departmentIdList: '',
userIdList: ''
}, },
dingTalkRobotWebHook: { dingTalkRobotWebHook: {
message: '', message: '',
messageType: '', messageType: 'markdown',
markdown: { markdown: {
text: '', text: '',
title: '', title: '',

View File

@ -0,0 +1,203 @@
<template>
<div>
<a-form layout="vertical" :rules="rule" :model="form" ref="formRef">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="名称" name="name">
<a-input
placeholder="请输入名称"
v-model:value="form.name"
></a-input> </a-form-item
></a-col>
<a-col :span="12">
<a-form-item label="类型" name="targetType">
<a-select
:options="options"
v-model:value="form.targetType"
:disabled="selectDisable"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="级别" name="level">
<a-radio-group v-model:value="form.level">
<a-radio-button
v-for="(item, index) in levelOption"
:key="index"
:value="item.value"
>
<div
style="
text-align: center;
margin-top: 10px;
font-size: 15px;
width: 90%;
"
>
<img
:src="getImage(`/alarm/alarm${index + 1}.png`)"
style="height: 40px"
alt=""
/>{{ item.label }}
</div>
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea v-model:value="form.description"></a-textarea>
</a-form-item>
<a-button type="primary" @click="handleSave" :loading="loading"
>保存</a-button
>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { getTargetTypes, save, detail } from '@/api/rule-engine/configuration';
import { queryLevel } from '@/api/rule-engine/config';
import { query } from '@/api/rule-engine/scene';
import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue';
import { useMenuStore } from '@/store/menu';
import { useRoute } from 'vue-router';
import { Store } from 'jetlinks-store';
const route = useRoute();
const id = route.query?.id;
let selectDisable = ref(false);
const queryData = () => {
if (id) {
detail(id).then((res) => {
if (res.status === 200) {
form.level = res?.result?.level;
form.name = res?.result?.name;
form.targetType = res?.result?.targetType;
form.description = res?.result?.description;
Store.set('configuration-data', res.result);
query({
terms: [
{
terms: [
{
column: 'id',
termType: 'alarm-bind-rule',
value: id,
},
],
type: 'and',
},
],
sorts: [
{
name: 'createTime',
order: 'desc',
},
],
}).then((resq) => {
if (resq.status === 200) {
selectDisable.value = !!resq.result.data?.length;
}
});
}
});
}
};
const rule = {
name: [
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多输入64个字符',
},
],
targetType: [
{
required: true,
message: '请选择类型',
},
],
level: [
{
required: true,
message: '请选择级别',
},
],
description: [
{
max: 200,
message: '最多可输入200个字符',
},
],
};
let form = reactive({
level: '',
targetType: '',
name: '',
description: '',
});
let options = ref();
let levelOption = ref();
let loading = ref(false);
const formRef = ref();
const menuStory = useMenuStore();
const getSupports = async () => {
let res = await getTargetTypes();
if (res.status === 200) {
options.value = res.result.map(
(item: { id: string; name: string }) => ({
label: item.name,
value: item.id,
}),
);
}
};
getSupports();
const getLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
levelOption.value = res.result?.levels
?.filter((i: any) => i?.level && i?.title)
.map((item: { level: number; title: string }) => ({
label: item.title,
value: item.level,
}));
}
});
};
getLevel();
const handleSave = async () => {
loading.value = true;
formRef.value
.validate()
.then(async () => {
const res = await save(form);
loading.value = false;
if (res.status === 200) {
message.success('操作成功');
menuStory.jumpPage(
'rule-engine/Alarm/Configuration/Save',
{},
{ id: res.result?.id },
);
if (!id) {
Store.set('configuration-data', res.result);
}
}
})
.catch((error) => {
loading.value = false;
console.log(error);
});
};
queryData();
</script>
<style lang="less" scoped>
.ant-radio-button-wrapper {
margin: 10px 15px 0 0;
width: 125px;
height: 100%;
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<div>123</div>
</template>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<page-container>
<a-card>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="基础配置">
<Base/>
</a-tab-pane>
<a-tab-pane key="2" tab="关联场景联动">
<Scene></Scene>
</a-tab-pane>
<a-tab-pane key="3" tab="告警记录"></a-tab-pane>
</a-tabs>
</a-card>
</page-container>
</template>
<script lang="ts" setup>
import Base from './Base/index.vue';
import Scene from './Scene/index.vue'
const activeKey = ref('2');
</script>
<style lang="less" scoped>
</style>

View File

@ -2,7 +2,7 @@
<page-container> <page-container>
<div> <div>
<Search <Search
:columns="query.columns" :columns="columns"
target="device-instance" target="device-instance"
@search="handleSearch" @search="handleSearch"
></Search> ></Search>
@ -13,6 +13,7 @@
:defaultParams="{ :defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }], sorts: [{ name: 'createTime', order: 'desc' }],
}" }"
:params="params"
> >
<template #headerTitle> <template #headerTitle>
<a-space> <a-space>
@ -41,23 +42,27 @@
</slot> </slot>
</template> </template>
<template #content> <template #content>
<h3 style="font-weight: 600"> <Ellipsis>
{{ slotProps.name }} <span style="font-weight: 600; font-size: 16px">
</h3> {{ slotProps.name }}
</span>
</Ellipsis>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<div class="content-des-title"> <div class="content-des-title">
关联场景联动 关联场景联动
</div> </div>
<div class="rule-desc"> <Ellipsis
{{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }} ><div>
</div> {{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }}
</div></Ellipsis
>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<div class="content-des-title"> <div class="content-des-title">
告警级别 告警级别
</div> </div>
<div class="rule-desc"> <div>
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title || {{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level }} slotProps.level }}
</div> </div>
@ -200,50 +205,124 @@ import {
_disable, _disable,
remove, remove,
_execute, _execute,
getScene,
} from '@/api/rule-engine/configuration'; } from '@/api/rule-engine/configuration';
import { queryLevel } from '@/api/rule-engine/config'; import { queryLevel } from '@/api/rule-engine/config';
import { Store } from 'jetlinks-store'; import { Store } from 'jetlinks-store';
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { useMenuStore } from '@/store/menu';
import encodeQuery from '@/utils/encodeQuery';
import { useStorage } from '@vueuse/core';
const params = ref<Record<string, any>>({}); const params = ref<Record<string, any>>({});
let isAdd = ref<number>(0); let isAdd = ref<number>(0);
let title = ref<string>(''); let title = ref<string>('');
const tableRef = ref<Record<string, any>>({}); const tableRef = ref<Record<string, any>>({});
const menuStory = useMenuStore();
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
search: {
type: 'string',
},
}, },
{ {
title: '类型', title: '类型',
dataIndex: 'targetType', dataIndex: 'targetType',
key: 'targetType', key: 'targetType',
scopedSlots: true, scopedSlots: true,
search: {
type: 'select',
options: [
{
label: '产品',
value: 'product',
},
{
label: '设备',
value: 'device',
},
{
label: '组织',
value: 'org',
},
{
label: '其他',
value: 'other',
},
],
},
}, },
{ {
title: '告警级别', title: '告警级别',
dataIndex: 'level', dataIndex: 'level',
key: 'level', key: 'level',
scopedSlots: true, scopedSlots: true,
search: {
type: 'select',
options: async () => {
const res = await queryLevel();
if (res.status === 200) {
return (res?.result?.levels || [])
.filter((i: any) => i?.level && i?.title)
.map((item: any) => ({
label: item.title,
value: item.level,
}));
}
return [];
},
},
}, },
{ {
title: '关联场景联动', title: '关联场景联动',
dataIndex: 'sceneId', dataIndex: 'sceneId',
wdith: 250, wdith: 250,
scopedSlots: true, scopedSlots: true,
search: {
type: 'select',
options: async () => {
const res = await getScene(
encodeQuery({
sorts: { createTime: 'desc' },
}),
);
if(res.status === 200){
return res.result.map((item:any) => ({label:item.name, value:item.id}))
}
return [];
},
},
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'state', dataIndex: 'state',
key: 'state', key: 'state',
scopedSlots: true, scopedSlots: true,
search: {
type: 'select',
options: [
{
label: '正常',
value: 'enabled',
},
{
label: '禁用',
value: 'disabled',
},
],
},
}, },
{ {
title: '说明', title: '说明',
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
search:{
type:'string',
}
}, },
{ {
title: '操作', title: '操作',
@ -259,44 +338,6 @@ const map = {
org: '组织', org: '组织',
other: '其他', other: '其他',
}; };
const query = {
columns: [
{
title: '名称',
dataIndex: 'name',
key: 'name',
search: {
type: 'string',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
search: {
type: 'select',
options: [
{
label: '正常',
value: 'enabled',
},
{
label: '禁用',
value: 'disabled',
},
],
},
},
{
title: '说明',
key: 'description',
dataIndex: 'description',
search: {
type: 'string',
},
},
],
};
const handleSearch = (e: any) => { const handleSearch = (e: any) => {
params.value = e; params.value = e;
}; };
@ -355,9 +396,7 @@ const getActions = (
icon: 'EditOutlined', icon: 'EditOutlined',
onClick: () => { onClick: () => {
title.value = '编辑'; menuStory.jumpPage('rule-engine/Alarm/Configuration/Save',{},{id:data.id});
isAdd.value = 2;
nextTick(() => {});
}, },
}, },
{ {
@ -421,14 +460,12 @@ const getActions = (
return actions.filter((i: ActionsType) => i.key !== 'view'); return actions.filter((i: ActionsType) => i.key !== 'view');
return actions; return actions;
}; };
const add = () => {
menuStory.jumpPage('rule-engine/Alarm/Configuration/Save');
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.content-des-title { .content-des-title {
font-size: 12px; font-size: 12px;
} }
.rule-desc {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</style> </style>

View File

@ -0,0 +1,9 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>

View File

@ -1,7 +1,11 @@
<template> <template>
<page-container> <page-container>
<div> <div>
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search> <Search
:columns="query.columns"
target="device-instance"
@search="handleSearch"
></Search>
<JTable <JTable
:columns="columns" :columns="columns"
:request="queryList" :request="queryList"
@ -14,7 +18,7 @@
<template #headerTitle> <template #headerTitle>
<a-space> <a-space>
<a-button type="primary" @click="add" <a-button type="primary" @click="add"
><plus-outlined/>新增</a-button ><plus-outlined />新增</a-button
> >
</a-space> </a-space>
</template> </template>
@ -36,14 +40,18 @@
</slot> </slot>
</template> </template>
<template #content> <template #content>
<h3 style="font-weight: 600"> <Ellipsis>
{{ slotProps.name }} <span style="font-weight: 600; font-size: 16px">
</h3> {{ slotProps.name }}
</span>
</Ellipsis>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<div class="rule-desc"> <Ellipsis>
{{ slotProps.description }} <div>
</div> {{ slotProps.description }}
</div>
</Ellipsis>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>
@ -154,7 +162,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import JTable from '@/components/Table'; import JTable from '@/components/Table';
import type { InstanceItem } from './typings'; import type { InstanceItem } from './typings';
import { queryList , startRule , stopRule , deleteRule} from '@/api/rule-engine/instance'; import {
queryList,
startRule,
stopRule,
deleteRule,
} from '@/api/rule-engine/instance';
import type { ActionsType } from '@/components/Table/index.vue'; import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@ -266,7 +279,10 @@ const getActions = (
tooltip: { tooltip: {
title: data.state?.value !== 'disable' ? '禁用' : '启用', title: data.state?.value !== 'disable' ? '禁用' : '启用',
}, },
icon: data.state?.value !== 'disable' ? 'StopOutlined' : 'CheckCircleOutlined', icon:
data.state?.value !== 'disable'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: { popConfirm: {
title: `确认${data.state !== 'disable' ? '禁用' : '启用'}?`, title: `确认${data.state !== 'disable' ? '禁用' : '启用'}?`,
onConfirm: async () => { onConfirm: async () => {
@ -332,9 +348,4 @@ const handleSearch = (e: any) => {
}; };
</script> </script>
<style scoped> <style scoped>
.rule-desc {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</style> </style>

View File

@ -6,7 +6,7 @@
@click='save' @click='save'
@cancel='cancel' @cancel='cancel'
> >
<a-steps :current='addModel.stepNumber'> <a-steps :current='addModel.stepNumber' @change='stepChange'>
<a-step> <a-step>
<template #title>选择产品</template> <template #title>选择产品</template>
</a-step> </a-step>
@ -17,19 +17,28 @@
<template #title>触发类型</template> <template #title>触发类型</template>
</a-step> </a-step>
</a-steps> </a-steps>
<a-divider style='margin-bottom: 0px' />
<div class='steps-content'> <div class='steps-content'>
<Product :rowKey='addModel.productId' /> <Product v-if='addModel.stepNumber === 0' v-model:rowKey='addModel.productId' v-model:detail='addModel.productDetail' />
<DeviceSelect
v-else-if='addModel.stepNumber === 1'
:productId='addModel.productId'
v-model:deviceKeys='addModel.deviceKeys'
v-model:orgId='addModel.orgId'
v-model:selector='addModel.selector'
v-model:selectorValues='addModel.selectorValues'
/>
<Type
v-else-if='addModel.stepNumber === 2'
:metadata='addModel.metadata'
/>
</div> </div>
<template #footer> <template #footer>
<div class='steps-action'> <div class='steps-action'>
<template> <a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button> <a-button v-else @click='prev'>上一步</a-button>
<a-button v-else>上一步</a-button> <a-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</a-button>
</template> <a-button type='primary' v-else @click='saveClick'>确定</a-button>
<template>
<a-button type='primary' v-if='addModel.stepNumber < 2'>下一步</a-button>
<a-button type='primary' v-else>确定</a-button>
</template>
</div> </div>
</template> </template>
</a-modal> </a-modal>
@ -37,10 +46,12 @@
<script setup lang='ts' name='AddModel'> <script setup lang='ts' name='AddModel'>
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { TriggerDevice } from '@/views/rule-engine/Scene/typings' import type { metadataType, TriggerDevice } from '@/views/rule-engine/Scene/typings'
import { onlyMessage } from '@/utils/comm' import { onlyMessage } from '@/utils/comm'
import { detail as deviceDetail } from '@/api/device/instance' import { detail as deviceDetail } from '@/api/device/instance'
import Product from './Product.vue' import Product from './Product.vue'
import DeviceSelect from './DeviceSelect.vue'
import Type from './Type.vue'
type Emit = { type Emit = {
(e: 'cancel'): void (e: 'cancel'): void
@ -54,11 +65,7 @@ interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
orgId: Array<{ label: string, value: string }> orgId: Array<{ label: string, value: string }>
productDetail: any productDetail: any
selectorValues: Array<{ label: string, value: string }> selectorValues: Array<{ label: string, value: string }>
metadata: { metadata: metadataType
properties?: any[]
functions?: any[]
events?: any[]
}
} }
const emit = defineEmits<Emit>() const emit = defineEmits<Emit>()
@ -97,39 +104,56 @@ const handleOptions = () => {
} }
const prev = () => {
addModel.stepNumber = addModel.stepNumber - 1
}
const cancel = () => { const cancel = () => {
emit("cancel") emit("cancel")
} }
const handleMetadata = (metadata: string) => { const handleMetadata = (metadata?: string) => {
try { try {
addModel.metadata = JSON.parse(metadata) addModel.metadata = JSON.parse(metadata || "{}")
} catch (e) { } catch (e) {
console.warn('handleMetadata: ' + e) console.warn('handleMetadata: ' + e)
} }
} }
const save = async () => { const save = async (step?: number) => {
if (addModel.stepNumber === 0) { let _step = step !== undefined ? step : addModel.stepNumber
if (_step === 0) {
addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error') addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error')
} else if (addModel.stepNumber === 1) { } else if (_step === 1) {
const isFixed = addModel.selector === 'fixed' // const isFixed = addModel.selector === 'fixed' //
if ((['fixed', 'org'].includes(addModel.selector) ) && addModel.selectorValues?.length) { if ((['fixed', 'org'].includes(addModel.selector) ) && !addModel.selectorValues?.length) {
return onlyMessage(isFixed ? '请选择设备' : '请选择部门', 'error') return onlyMessage(isFixed ? '请选择设备' : '请选择部门', 'error')
} }
// //
if (isFixed && addModel.selectorValues?.length === 1) { if (isFixed && addModel.selectorValues?.length === 1) {
const resp = await deviceDetail(addModel.selectorValues[0].value) const resp = await deviceDetail(addModel.selectorValues[0].value)
addModel.metadata handleMetadata(resp.result.metadata)
} else { } else {
handleMetadata(addModel.productDetail?.metadata)
} }
// addModel.stepNumber = 2
} else {
} }
// handleOptions() // handleOptions()
// emit('update:value', {}) // emit('update:value', {})
} }
const saveClick = () => save()
const stepChange = (step: number) => {
if (step !== 0) {
save(step - 1)
} else {
addModel.stepNumber = 0
}
}
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,187 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
ref='actionRef'
model='CARD'
:columns='columns'
:request='deviceQuery'
:gridColumn='2'
:params='params'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="deviceRowKeys.includes(slotProps.id)"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
@click="handleClick"
>
<template #img>
<slot name="img">
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" />
</slot>
</template>
<template #content>
<Ellipsis style='width: calc(100% - 100px)'>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>{{ slotProps.deviceType?.text }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<div>{{ slotProps.productName }}</div>
</a-col>
</a-row>
</template>
</CardBox>
</template>
</j-table>
</template>
<script setup lang='ts' name='DeviceSelectList'>
import type { PropType } from 'vue'
import { getImage } from '@/utils/comm'
import { query } from '@/api/device/instance'
import { cloneDeep } from 'lodash-es'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
}
const actionRef = ref()
const params = ref({})
const context = inject('SceneDeviceAddModel')
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
default: () => ([])
},
productId: {
type: String,
default: ''
}
})
const emit = defineEmits<Emit>()
const deviceRowKeys = computed(() => {
return props.rowKeys.map(item => item.value)
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string'
}
},
{
title: '设备名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 200,
search: {
type: 'date'
}
},
{
title: '状态',
dataIndex: 'state',
width: 90,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
]
}
},
]
const handleSearch = (p: any) => {
params.value = p
}
const deviceQuery = (p: any) => {
const sorts: any = [];
if (props.rowKeys) {
props.rowKeys.forEach(rowKey => {
sorts.push({
name: 'id',
value: rowKey,
});
})
}
sorts.push({ name: 'createTime', order: 'desc' });
const terms = [
...p.terms,
{ terms: [{ column: "productId", value: props.productId }]}
]
return query({ ...p, terms, sorts })
}
const handleClick = (detail: any) => {
const cloneRowKeys = cloneDeep(props.rowKeys)
const indexOf = cloneRowKeys.findIndex(item => item.value === detail.id)
if (indexOf !== -1) {
cloneRowKeys.splice(indexOf, 1)
} else {
cloneRowKeys.push({
name: detail.name,
value: detail.id
})
}
console.log('cloneRowKeys', cloneRowKeys)
emit('update', cloneRowKeys)
}
</script>
<style scoped>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class='device-select'>
<TopCard :options='typeList' v-model:value='selectorModel' @select='select' />
<DeviceList v-if='selectorModel === "fixed"' :productId='productId' :row-keys='devices' @update='updateDevice' />
<OrgList v-else-if='selectorModel === "org"' :productId='productId' :row-keys='orgIds' @update='updateOrg' />
</div>
</template>
<script setup lang='ts'>
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
import DeviceList from './DeviceList.vue'
import OrgList from './OrgList.vue'
import { getImage } from '@/utils/comm'
import type { PropType } from 'vue'
type ItemType = {
name: string,
value: string
}
type Emit = {
(e: 'update:selector', data: string): void
(e: 'update:selectorValues', data: ItemType[]): void
(e: 'update:deviceKeys', data: ItemType[]): void
(e: 'update:orgId', data: ItemType[]): void
}
const emit = defineEmits<Emit>()
const props = defineProps({
productId: {
type: String,
default: ''
},
selector: {
type: String,
default: ''
},
device: {
type: Array as PropType<ItemType[]>,
default: () => []
},
orgId: {
type: Array as PropType<ItemType[]>,
default: () => []
}
})
const selectorModel = ref(props.selector)
const devices = ref(props.device)
const orgIds = ref(props.orgId)
const typeList = [
{ label: '自定义', value: 'fixed', tip: '自定义选择当前产品下的任意设备', img: getImage('/scene/device-custom.png')},
{ label: '全部', value: 'all', tip: '产品下的所有设备', img: getImage('/scene/trigger-device-all.png')},
{ label: '按组织', value: 'org', tip: '选择产品下归属于具体组织的设备', img: getImage('/scene/trigger-device-org.png')},
]
const select = (s: string) => {
selectorModel.value = s
emit('update:selector', s)
emit('update:selectorValues', [])
}
const updateDevice = (d: any[]) => {
devices.value = d
emit('update:deviceKeys', d)
emit('update:selectorValues', d)
}
const updateOrg = (d: any[]) => {
orgIds.value = d
emit('update:orgId', d)
emit('update:selectorValues', d)
}
</script>
<style scoped lang='less'>
.device-select{
margin-top: 24px;
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-category"
/>
<a-divider style='margin: 0' />
<JTable
ref="instanceRef"
model='TABLE'
type='TREE'
:columns="columns"
:request="query"
:scroll="{
y: 350
}"
:expandable='{
expandedRowKeys: openKeys,
onExpandedRowsChange: expandedRowChange,
}'
:rowSelection='{
type: "radio",
selectedRowKeys: orgRowKeys,
onChange: selectedRowChange
}'
:onChange='tableChange'
>
</JTable>
</template>
<script setup lang='ts' name='OrgList'>
import type { PropType } from 'vue'
import { getExpandedRowById } from './util'
import { getTreeData_api } from '@/api/system/department'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
}
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
default: () => ([])
},
productId: {
type: String,
default: ''
}
})
const emit = defineEmits<Emit>()
const params = ref()
const openKeys = ref<string[]>([])
const selectedRowKeys = ref(props.rowKeys.map(item => item.value))
const sortParam = ref<{ name:string, order: string }>({ name: 'sortIndex', order: 'asc' })
const iniPage = ref(true)
const orgRowKeys = computed(() => {
return props.rowKeys.map(item => item.value)
})
const columns = [
{
title: '名称',
width: 300,
ellipsis: true,
dataIndex: 'name',
},
{
title: '排序',
dataIndex: 'sortIndex',
sorter: true,
},
]
const handleSearch = (p: any) => {
params.value = p
}
const tableChange = (_: any, f: any, sorter: any) => {
if (sorter.order) {
sortParam.value = { name: sorter.columnKey, order: (sorter.order as string).replace('end', ''), }
} else {
sortParam.value = { name: 'sortIndex', order: 'asc' }
}
}
const query = async (p: any) => {
const _params: any = {
paging: false,
sorts: [sortParam.value],
}
if (p.terms && p.terms.length) {
_params.terms = p.terms
}
const resp = await getTreeData_api(_params)
if (iniPage.value && props.rowKeys.length) {
iniPage.value = false
openKeys.value = getExpandedRowById(props.rowKeys[0]?.value, resp.result as any[])
}
return resp
}
const selectedRowChange = (_: any, selectedRows: any[]) => {
const item = selectedRows[0]
console.log(selectedRows)
emit('update', item ? [{ name: item.name, value: item.id }] : [])
}
const expandedRowChange = (keys: string[]) => {
openKeys.value = keys
}
</script>
<style scoped>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -4,18 +4,25 @@
type='simple' type='simple'
@search="handleSearch" @search="handleSearch"
class='search' class='search'
target="scene-triggrt-device-device"
/> />
<a-divider style='margin: 0' />
<j-table <j-table
:columns='columns'
ref='actionRef' ref='actionRef'
model='CARD'
:columns='columns'
:params='params'
:request='productQuery' :request='productQuery'
:gridColumn='2' :gridColumn='2'
model='CARD' :bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
> >
<template #card="slotProps"> <template #card="slotProps">
<CardBox <CardBox
:value='slotProps' :value='slotProps'
:active="selectedRowKeys.includes(slotProps.id)" :active="rowKey === slotProps.id"
:status="slotProps.state" :status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'" :statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'success', 0: 'error', }" :statusNames="{ 1: 'success', 0: 'error', }"
@ -23,13 +30,17 @@
> >
<template #img> <template #img>
<slot name="img"> <slot name="img">
<img :src="getImage('/device-product.png')" /> <img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot> </slot>
</template> </template>
<template #content> <template #content>
<h3 style="font-weight: 600" > <div style='width: calc(100% - 100px)'>
{{ slotProps.name }} <Ellipsis>
</h3> <span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<div class="card-item-content-text"> <div class="card-item-content-text">
@ -51,16 +62,25 @@ import { getTreeData_api } from '@/api/system/department'
import { isNoCommunity } from '@/utils/utils' import { isNoCommunity } from '@/utils/utils'
import { getImage } from '@/utils/comm' import { getImage } from '@/utils/comm'
type Emit = {
(e: 'update:rowKey', data: string): void
(e: 'update:detail', data: string): void
}
const actionRef = ref() const actionRef = ref()
const params = ref({}) const params = ref({})
const props = defineProps({ const props = defineProps({
rowKey: { rowKey: {
type: String, type: String,
default: '' default: ''
},
detail: {
type: Object,
default: () => ({})
} }
}) })
const selectedRowKeys = ref(props.rowKey) const emit = defineEmits<Emit>()
const columns = [ const columns = [
{ {
@ -69,12 +89,19 @@ const columns = [
width: 300, width: 300,
ellipsis: true, ellipsis: true,
fixed: 'left', fixed: 'left',
search: {
type: 'string',
},
}, },
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
width: 200, width: 200,
ellipsis: true, ellipsis: true,
search: {
type: 'string',
first: true
}
}, },
{ {
title: '网关类型', title: '网关类型',
@ -199,7 +226,6 @@ const columns = [
const handleSearch = (p: any) => { const handleSearch = (p: any) => {
params.value = p params.value = p
actionRef.value.required()
} }
const productQuery = (p: any) => { const productQuery = (p: any) => {
@ -217,12 +243,8 @@ const productQuery = (p: any) => {
} }
const handleClick = (detail: any) => { const handleClick = (detail: any) => {
const _selected = new Set(selectedRowKeys.value) emit('update:rowKey', detail.id)
if (_selected.has(detail.id)) { emit('update:detail', detail)
_selected.delete(detail.id)
} else {
_selected.add(detail.id)
}
} }
</script> </script>
@ -230,5 +252,7 @@ const handleClick = (detail: any) => {
<style scoped lang='less'> <style scoped lang='less'>
.search { .search {
margin-bottom: 0; margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
} }
</style> </style>

View File

@ -0,0 +1,100 @@
<template>
<div class='type'>
<a-form ref='typeForm' :model='formModel' layout='vertical' :colon='false'>
<a-form-item
required
label='触发类型'
>
<TopCard
:label-bottom='true'
:options='options'
v-model:value='formModel.operator'
/>
</a-form-item>
<Timer v-if='showTimer' />
</a-form>
</div>
</template>
<script setup lang='ts'>
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
import { getImage } from '@/utils/comm'
import { metadataType } from '@/views/rule-engine/Scene/typings'
import type { PropType } from 'vue'
import { TypeEnum } from '@/views/rule-engine/Scene/Save/Device/util'
import Timer from '../components/Timer.vue'
const props = defineProps({
metadata: {
type: Object as PropType<metadataType>,
default: () => ({})
}
})
const formModel = reactive({
operator: 'online',
})
const readProperties = ref<any[]>([])
const writeProperties = ref<any[]>([])
const options = computed(() => {
const baseOptions = [
{
label: '设备上线',
value: 'online',
img: getImage('/scene/online.png'),
},
{
label: '设备离线',
value: 'offline',
img: getImage('/scene/offline.png'),
},
]
if (props.metadata.events?.length) {
baseOptions.push(TypeEnum.reportEvent)
}
if (props.metadata.properties?.length) {
const _properties = props.metadata.properties
readProperties.value = _properties.filter((item: any) => item.expands.type?.includes('read'))
writeProperties.value = _properties.filter((item: any) => item.expands.type?.includes('write'))
const reportProperties = _properties.filter((item: any) => item.expands.type?.includes('report'))
if (readProperties.value.length) {
baseOptions.push(TypeEnum.readProperty)
}
if (writeProperties.value.length) {
baseOptions.push(TypeEnum.writeProperty)
}
if (reportProperties.length) {
baseOptions.push(TypeEnum.reportProperty)
}
}
if (props.metadata.functions?.length) {
baseOptions.push(TypeEnum.invokeFunction)
}
return baseOptions
})
const showTimer = computed(() => {
return ['readProperty', 'writeProperty', 'invokeFunction'].includes(formModel.operator)
})
</script>
<style scoped lang='less'>
.type {
max-height: calc(100vh - 350px);
overflow-x: hidden;
overflow-y: auto;
margin-top: 24px;
}
</style>

View File

@ -26,7 +26,8 @@ import AddButton from '../components/AddButton.vue'
import Title from '../components/Title.vue' import Title from '../components/Title.vue'
const sceneStore = useSceneStore() const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore) const { data } = storeToRefs<any>(sceneStore)
const visible = ref(false) const visible = ref(false)
const rules = [{ const rules = [{

View File

@ -0,0 +1,69 @@
import { getImage } from '@/utils/comm'
export const TypeName = {
online: '设备上线',
offline: '设备离线',
reportEvent: '事件上报',
reportProperty: '属性上报',
readProperty: '读取属性',
writeProperty: '修改属性',
invokeFunction: '功能调用',
};
export const TypeEnum = {
reportProperty: {
label: '属性上报',
value: 'reportProperty',
img: getImage('/scene/reportProperty.png'),
},
reportEvent: {
label: '事件上报',
value: 'reportEvent',
img: getImage('/scene/reportProperty.png'),
},
readProperty: {
label: '读取属性',
value: 'readProperty',
img: getImage('/scene/readProperty.png'),
},
writeProperty: {
label: '修改属性',
value: 'writeProperty',
img: getImage('/scene/writeProperty.png'),
},
invokeFunction: {
label: '功能调用',
value: 'invokeFunction',
img: getImage('/scene/invokeFunction.png'),
},
};
export const getExpandedRowById = (id: string, data: any[]): string[] => {
const expandedKeys:string[] = []
const dataMap = new Map()
const flatMapData = (flatData: any[]) => {
flatData.forEach(item => {
dataMap.set(item.id, { pid: item.parentId })
if (item.children && item.children.length) {
flatMapData(item.children)
}
})
}
const getExp = (_id: string) => {
const item = dataMap.get(_id)
if (item) {
expandedKeys.push(_id)
if (dataMap.has(dataMap)) {
getExp(item.pid)
}
}
}
flatMapData(data)
getExp(id)
return expandedKeys
}

View File

@ -0,0 +1,138 @@
<template>
<a-form
ref='timerForm'
:model='formModel'
layout='vertical'
:colon='false'
>
<a-form-item name='trigger'>
<a-radio-group
v-model:value='formModel.trigger'
:options='[
{ label: "按周", value: "week" },
{ label: "按月", value: "month" },
{ label: "cron表达式", value: "cron" },
]'
option-type='button'
button-style='solid'
/>
</a-form-item>
<a-form-item v-if='showCron' name='cron'>
<a-input placeholder='corn表达式' v-model='formModel.cron' />
</a-form-item>
<template v-else>
<a-form-item name='when'>
</a-form-item>
<a-form-item name='mod'>
<a-radio-group
v-model:value='formModel.mod'
:options='[
{ label: "周期执行", value: "period" },
{ label: "执行一次", value: "once" },
]'
option-type='button'
button-style='solid'
/>
</a-form-item>
</template>
<a-space v-if='showOnce' style='display: flex;gap: 24px'>
<a-form-item :name="['once', 'time']">
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%' format='HH:mm:ss' />
</a-form-item>
<a-form-item> 执行一次 </a-form-item>
</a-space>
<a-space v-if='showPeriod' style='display: flex;gap: 24px'>
<a-form-item>
<a-time-range-picker
valueFormat='HH:mm:ss'
:value='[
formModel.period.from,
formModel.period.to,
]'
@change='(v) => {
formModel.period.from = v[0]
formModel.period.to = v[1]
}'
/>
</a-form-item>
<a-form-item></a-form-item>
<a-form-item
:name='["period", "every"]'
:rules='[{ required: true, message: "请输入时间" }]'
>
<a-input-number
placeholder='请输入时间'
style='max-width: 170px'
:precision='0'
:min='1'
:max='59'
v-model:value='formModel.period.every'
>
<template #addonAfter>
<a-select
v-model:value='formModel.period.unit'
:options='[
{ label: "秒", value: "seconds" },
{ label: "分", value: "minutes" },
{ label: "小时", value: "hours" },
]'
/>
</template>
</a-input-number>
</a-form-item>
<a-form-item>执行一次</a-form-item>
</a-space>
</a-form>
</template>
<script setup lang='ts' name='Timer'>
import type { PropType } from 'vue'
import moment from 'moment'
type NameType = string[] | string
const props = defineProps({
name: {
type: [String, Array] as PropType<NameType>,
default: ''
},
value: {
type: Object,
default: () => ({})
}
})
const formModel = reactive({
trigger: 'week',
when: [],
mod: 'period',
cron: undefined,
once: {
time: ''
},
period: {
from: moment(new Date()).startOf('day').format('HH:mm:ss'),
to: moment(new Date()).endOf('day').format('HH:mm:ss'),
every: 1,
unit: 'seconds'
}
})
const showCron = computed(() => {
return formModel.trigger === 'cron'
})
const showOnce = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'once'
})
const showPeriod = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'period'
})
</script>
<style scoped lang='less'>
</style>

View File

@ -0,0 +1,167 @@
<template>
<div :class='classNames'>
<div
v-for='item in options'
:key='item.value'
:class='[
"trigger-way-item",
value === item.value ? "active" : "",
labelBottom ? "label-bottom" : ""
]'
@click='() => click(item.value)'
>
<div class='way-item-title'>
<span class='label'>{{ item.label }}</span>
<a-popover v-if='item.tip' :content='item.tip'>
<AIcon type='QuestionCircleOutlined' class='icon' />
</a-popover>
</div>
<div class='way-item-image'>
<img
width='48'
v-bind='item.imgProps'
:src='item.img'
/>
</div>
</div>
</div>
</template>
<script setup lang='ts' name='TopCard'>
import type { PropType } from 'vue'
type optionsType = {
label: string
value: string
img?: string
tip?: string
imgProps: Record<string, any>
}
type Emit = {
(e: 'update:value', data: string): void
(e: 'select', data: string): void
}
const props = defineProps({
options: {
type: Array as PropType<optionsType[]>,
default: () => ([])
},
value: {
type: String,
default: ''
},
class: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
labelBottom: {
type: Boolean,
default: false
}
})
const classNames = computed(() => {
return [
props.class,
'trigger-way-warp',
props.disabled ? 'disabled' : ''
]
})
const emit = defineEmits<Emit>()
const click = (value: string) => {
emit('update:value', value)
emit('select', value)
}
</script>
<style scoped lang='less'>
.trigger-way-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.trigger-way-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 237px;
//width: 100%;
padding: 12px 16px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.way-item-title {
span {
font-size: 16px;
}
.label {
padding-right: 6px;
color: rgba(#000, 0.64);
}
.icon {
color: rgba(#000, 0.5);
}
}
.way-item-image {
margin: 0 !important;
opacity: 0.6;
}
&:hover {
//color: @primary-color-hover;
.way-item-image {
opacity: 0.8;
}
}
&.active {
border-color: @primary-color-active;
.way-item-image {
opacity: 1;
}
}
&.label-bottom {
flex-direction: column-reverse;
grid-gap: 16px;
gap: 0;
align-items: center;
width: auto;
padding: 8px 16px;
p {
margin: 0;
}
}
}
&.disabled {
.trigger-way-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -9,6 +9,11 @@ type State = {
text: string; text: string;
}; };
export type optionItem = {
label: string
value: string
}
type Action = { type Action = {
executor: string; executor: string;
configuration: Record<string, unknown>; configuration: Record<string, unknown>;
@ -311,3 +316,9 @@ export interface FormModelType {
options?: Record<string, any>; options?: Record<string, any>;
description?: string; description?: string;
} }
export type metadataType = {
properties?: any[]
functions?: any[]
events?: any[]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="form-label-container"> <div class="form-label-container">
<span class="text">{{ props.text }}</span> <span class="text">{{ props.text }}</span>
<span class="required">*</span> <span class="required" v-show="props.required">*</span>
<a-tooltip> <a-tooltip>
<template #title>{{ props.tooltip }}</template> <template #title>{{ props.tooltip }}</template>
<AIcon type="QuestionCircleOutlined" style="color: #00000073;cursor: inherit;" /> <AIcon type="QuestionCircleOutlined" class="icon" />
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -24,11 +24,15 @@ const props = defineProps<{
.required { .required {
display: inline-block; display: inline-block;
margin-right: 4px;
color: #ff4d4f; color: #ff4d4f;
font-size: 14px; font-size: 14px;
font-family: SimSun, sans-serif; font-family: SimSun, sans-serif;
line-height: 1; line-height: 1;
} }
.icon {
color: #00000073;
cursor: inherit;
margin-left: 4px;
}
} }
</style> </style>

View File

@ -0,0 +1,49 @@
<template>
<a-modal
v-model:visible="dialog.visible"
title="集成菜单"
width="600px"
@ok="dialog.handleOk"
class="edit-dialog-container"
:confirmLoading="dialog.loading"
cancelText="取消"
okText="确定"
>
</a-modal>
</template>
<script setup lang="ts">
const emits = defineEmits(['confirm']);
//
const dialog = reactive({
visible: false,
loading: false,
handleOk: () => {
emits('confirm');
},
/**
* 设置表单类型
* @param type 弹窗类型
* @param defaultForm 表单回显对象
*/
changeVisible: () => {
dialog.visible = true;
}
});
//
defineExpose({
openDialog: dialog.changeVisible,
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,144 @@
<template>
<div class="request-table-container">
<a-table
:columns="columns"
:data-source="tableData"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'key'">
<a-input v-model:value="record.label" />
</template>
<template v-else-if="column.dataIndex === 'value'">
<a-input
v-model:value="record.value"
v-if="props.valueType === 'input'"
/>
<a-select
v-else-if="props.valueType === 'select'"
v-model:value="record.value"
>
<a-select-option
v-for="item in props.valueOptions"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button
type="link"
@click="removeRow((current - 1) * 10 + index)"
>
<AIcon type="DeleteOutlined" />
</a-button>
</template>
</template>
</a-table>
<a-pagination
v-show="props.value.length > 10"
v-model:current="current"
:page-size="10"
:total="props.value.length"
show-less-items
/>
<a-button type="dashed" @click="addRow" class="add-btn">
<AIcon type="PlusOutlined" />新增
</a-button>
</div>
</template>
<script setup lang="ts">
import type { optionsType } from '../typing';
const emits = defineEmits(['update:value']);
const props = withDefaults(
defineProps<{
value: optionsType;
valueType?: 'input' | 'select';
valueOptions?: optionsType;
}>(),
{
valueType: 'input',
},
);
const columns = [
{
title: 'KEY',
dataIndex: 'key',
width: '40%'
},
{
title: 'VALUE',
dataIndex: 'value',
width: '40%'
},
{
title: ' ',
dataIndex: 'action',
width: '20%'
},
];
const current = ref<number>(1);
const tableData = computed(() => {
return props.value.slice((current.value - 1) * 10, current.value * 10);
});
watch(
() => props.value,
(n, o) => {
if (!o || n.length === o.length) return;
//
else if (n.length > o.length) {
//
if (o.length % 10 === 0 && n.length > 10)
current.value = current.value + 1;
} else {
//
//
if (o.length % 10 === 1 && o.length > 10)
current.value = current.value - 1;
}
},
{
immediate: true,
},
);
function removeRow(index: number) {
const left = props.value.slice(0, index++);
const right = props.value.slice(index, props.value.length);
emits('update:value', [...left, ...right]);
}
function addRow() {
const newRow = {
label: '',
value: '',
};
console.log(111);
emits('update:value', [...props.value, newRow]);
}
</script>
<style lang="less" scoped>
.request-table-container {
width: 100%;
:deep(.ant-btn-link) {
color: #000000d9;
&:hover {
color: #1d39c4;
}
}
.add-btn {
width: 100%;
display: block;
margin-top: 10px;
}
}
</style>

View File

@ -3,351 +3,9 @@
<a-card class="save-container"> <a-card class="save-container">
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="14"> <a-col :span="14">
<a-form <EditForm @change-apply-type="chengeType" />
ref="formRef"
:model="form.data"
layout="vertical"
class="form"
>
<a-form-item label="名称" name="name">
<a-input
v-model:value="form.data.name"
placeholder="请输入名称"
/>
</a-form-item>
<a-form-item label="应用" name="provider">
<a-radio-group
v-model:value="form.data.provider"
class="radio-container"
@change="form.data.integrationModes = []"
>
<a-radio-button value="internal-standalone">
<div>
<a-image
:preview="false"
:src="
getImage('/apply/provider1.png')
"
width="64px"
height="64px"
/>
<p>内部独立应用</p>
</div>
</a-radio-button>
<a-radio-button value="internal-integrated">
<div>
<a-image
:preview="false"
:src="
getImage('/apply/provider2.png')
"
/>
<p>内部集成应用</p>
</div>
</a-radio-button>
<a-radio-button value="wechat-webapp">
<div>
<a-image
:preview="false"
:src="
getImage('/apply/provider3.png')
"
/>
<p>微信网站应用</p>
</div>
</a-radio-button>
<a-radio-button value="dingtalk-ent-app">
<div>
<a-image
:preview="false"
:src="
getImage('/apply/provider4.png')
"
/>
<p>钉钉企业内部应用</p>
</div>
</a-radio-button>
<a-radio-button value="third-party">
<div>
<a-image
:preview="false"
:src="
getImage('/apply/provider5.png')
"
/>
<p>第三方应用</p>
</div>
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="接入方式" name="integrationModes">
<a-checkbox-group
v-model:value="form.data.integrationModes"
:options="joinOptions"
/>
</a-form-item>
<a-collapse
v-model:activeKey="form.integrationModesISO"
>
<a-collapse-panel
key="page"
v-show="
form.data.integrationModes.includes('page')
"
header="页面集成"
>
<a-form-item
:name="['page', 'baseUrl']"
class="resetLabel"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="接入地址"
required
tooltip="填写访问其它平台的地址"
/>
</template>
<a-input
v-model:value="form.data.page.baseUrl"
placeholder="请输入接入地址"
/>
</a-form-item>
<a-form-item
label="路由方式"
:name="['page', 'routeType']"
:rules="[
{
required: true,
},
]"
>
<a-select
v-model:value="form.data.page.routeType"
style="width: 120px"
>
<a-select-option value="hash"
>hash</a-select-option
>
<a-select-option value="history"
>history</a-select-option
>
</a-select>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
key="apiClient"
v-show="
form.data.integrationModes.includes(
'apiClient',
)
"
header="API客户端"
>
<a-form-item
class="resetLabel"
:name="['apiClient', 'baseUrl']"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="接口地址"
required
tooltip="访问Api服务的地址"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.baseUrl
"
placeholder="请输入接入地址"
/>
</a-form-item>
<a-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'authorizationUrl',
]"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="授权地址"
required
tooltip="认证授权地址"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.authConfig
.oauth2.authorizationUrl
"
placeholder="请输入授权地址"
/>
</a-form-item>
<a-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'tokenUrl',
]"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="token地址"
required
tooltip="设置token令牌的地址"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.authConfig
.oauth2.tokenUrl
"
placeholder="请输入token地址"
/>
</a-form-item>
<a-form-item
label="回调地址"
:name="[
'apiClient',
'authConfig',
'oauth2',
'redirectUri',
]"
>
<template #label>
<FormLabel
text="回调地址"
tooltip="授权完成后跳转到具体页面的回调地址"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.authConfig
.oauth2.redirectUri
"
placeholder="请输入回调地址"
/>
</a-form-item>
<a-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientId',
]"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="appId"
required
tooltip="第三方应用唯一标识"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.authConfig
.oauth2.clientId
"
placeholder="请输入appId"
/>
</a-form-item>
<a-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientSecret',
]"
:rules="[
{
required: true,
},
]"
>
<template #label>
<FormLabel
text="appKey"
required
tooltip="第三方应用唯一标识的密钥"
/>
</template>
<a-input
v-model:value="
form.data.apiClient.authConfig
.oauth2.clientSecret
"
placeholder="请输入appKey"
/>
</a-form-item>
<a-form-item label="请求头"> </a-form-item>
<a-form-item label="参数"> </a-form-item>
</a-collapse-panel>
<a-collapse-panel
key="apiServer"
v-show="
form.data.integrationModes.includes(
'apiServer',
)
"
header="API服务"
>
</a-collapse-panel>
<a-collapse-panel
key="ssoClient"
v-show="
form.data.integrationModes.includes(
'ssoClient',
)
"
header="单点登录"
>
</a-collapse-panel>
</a-collapse>
<a-form-item label="说明" name="description">
<a-textarea
v-model:value="form.data.description"
placeholder="请输入说明"
showCount
:maxlength="200"
:rows="5"
/>
</a-form-item>
</a-form>
<a-button v-if="!routeQuery.view">保存</a-button>
</a-col> </a-col>
<a-col :span="10"><Does :type="form.data.provider" /></a-col> <a-col :span="10"><Does :type="rightType" /></a-col>
</a-row> </a-row>
</a-card> </a-card>
</page-container> </page-container>
@ -355,151 +13,11 @@
<script setup lang="ts"> <script setup lang="ts">
import Does from './components/Does.vue'; import Does from './components/Does.vue';
import FormLabel from './components/FormLabel.vue'; import EditForm from './components/EditForm.vue';
import { getImage } from '@/utils/comm'; import type { applyType } from './typing';
import type { applyType, formType } from './typing';
const routeQuery = useRoute().query;
const initForm: formType = { const rightType = ref<applyType>('internal-standalone');
name: '', const chengeType = (newType: applyType) => {
provider: 'internal-standalone', rightType.value = newType;
integrationModes: [],
config: '',
description: '',
page: {
baseUrl: '',
routeType: 'hash',
},
apiClient: {
baseUrl: '',
authConfig: {
type: '',
oauth2: {
authorizationUrl: '',
tokenUrl: '',
redirectUri: '',
clientId: '',
clientSecret: '',
},
},
},
}; };
const form = reactive({
data: { ...initForm },
integrationModesISO: [] as string[],
});
const joinOptions = computed(() => {
if (form.data.provider === 'internal-standalone')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
{
label: 'API服务',
value: 'apiServer',
},
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'internal-integrated')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
];
else if (form.data.provider === 'wechat-webapp')
return [
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'dingtalk-ent-app')
return [
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'third-party')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
{
label: 'API服务',
value: 'apiServer',
},
{
label: '单点登录',
value: 'ssoClient',
},
];
});
</script> </script>
<style lang="less" scoped>
.save-container {
.form {
.ant-form-item {
&.resetLabel {
:deep(.ant-form-item-required) {
&::before {
display: none;
}
}
}
.ant-select {
width: 100% !important;
}
}
.radio-container {
.ant-radio-button-wrapper {
height: 120px;
width: 120px;
padding: 0 15px;
box-sizing: content-box;
margin-right: 20px;
> :last-child {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
> div {
width: 100%;
text-align: center;
}
:deep(.ant-image) {
width: 64px;
height: 64px;
}
p {
margin: 0;
}
}
}
}
}
}
</style>

View File

@ -3,28 +3,93 @@ export type applyType = 'internal-standalone'
| 'internal-integrated' | 'internal-integrated'
| 'dingtalk-ent-app' | 'dingtalk-ent-app'
| 'third-party' | 'third-party'
export type dictType = {
id: string;
name: string;
children?: dictType
}[];
export type optionsType = {
label: string,
value: string;
disabled?: boolean
}[]
export type formType = { export type formType = {
id?:string,
name: string; name: string;
provider: applyType; provider: applyType;
integrationModes: string[]; integrationModes: string[];
config: string; config: string;
description: string; description: string;
page: { page: { // 页面集成
baseUrl:string,
routeType:'hash' | 'history'
},
apiClient: {
baseUrl: string, baseUrl: string,
authConfig: { routeType: 'hash' | 'history',
type:string, parameters: optionsType
oauth2 :{ },
authorizationUrl:string, apiClient: { // API客户端
tokenUrl: string, baseUrl: string, // 接口地址
redirectUri:string, headers: optionsType, // 请求头
clientId:string, parameters: optionsType, // 请求参数
clientSecret:string authConfig: { // 认证配置
type: 'none' | 'bearer' | 'oauth2' | 'basic' | 'other', // 类型, 可选值none, bearer, oauth2, basic, other
bearer: { token: string }, // 授权信息
basic: { username: string, password: string }, // 基本信息
token: string,
oauth2: { // OAuth2信息
authorizationUrl: string, // 授权地址
tokenUrl: string, // token地址
redirectUri: string, // 重定向地址
clientId: string, // 客户端ID
clientSecret: string, // 客户端密钥
grantType: 'authorization_code' | 'client_credentials' | '', // 类型
accessTokenProperty: string, // token属性名
tokenRequestType: 'POST_URI' | 'POST_BODY' | '' // token请求方式, 可选值POST_URIPOST_BODY
} }
} }
},
apiServer: { // API服务
appId: string,
secureKey: string, // 密钥
redirectUri: string, // 重定向URL
roleIdList: string[], // 角色列表
orgIdList: string[], // 部门列表
ipWhiteList: string, // IP白名单
signature: 'MD5' | 'SHA256' | '', // 签名方式, 可选值MD5SHA256
enableOAuth2: boolean, // 是否启用OAuth2
},
sso: { // 统一单点登陆集成
configuration: { // 配置
oauth2: { // Oauth2单点登录配置
type?: string, // 认证方式
authorizationUrl: string, // 授权地址
redirectUri: string, // 重定向地址
clientId: string, // 客户端ID
clientSecret: string, // 客户端密钥
userInfoUrl: string, // 用户信息接口
scope: string, // scope
logoUrl?:string, // logo
userProperty: { // 用户属性字段信息
userId: string, // 用户ID
username: string, // 用户名
name: string, // 名称
avatar: string, // 头像
email: string, // 邮箱
telephone: string, // 电话
description: string, // 说明
},
grantType: 'authorization_code' | 'client_credentials' | '', // 类型
tokenUrl: string, // token地址
accessTokenProperty: string, // token属性名
tokenRequestType: 'POST_URI' | 'POST_BODY' | '', // token请求方式
},
appId: string, // 微信单点登录配置
appKey: string, // 钉钉单点登录配置
appSecret: string, // 钉钉、微信单点登录配置
},
autoCreateUser: boolean, // 是否自动创建平台用户
usernamePrefix: string, // 用户ID前缀
roleIdList: string[], // 自动创建平台用户时角色列表
orgIdList: string[], // 自动创建平台用户时部门列表
defaultPasswd: string, // 默认密码
} }
} }

1555
yarn.lock

File diff suppressed because it is too large Load Diff