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",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.45",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.1.6",
"vue3-markdown-it": "^1.0.10",
"vue3-ts-jsoneditor": "^2.7.1"

View File

@ -466,4 +466,20 @@ export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/
* @param params
* @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),
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`),
// 钉钉部门人员
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`),
// 微信部门人员
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),
// 解绑

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@ const defaultOptions = {
};
export const useSceneStore = defineStore('scene', () => {
const data = reactive<FormModelType | any>({
const data = reactive<FormModelType>({
trigger: { type: ''},
options: defaultOptions,
branches: defaultBranches,
@ -116,67 +116,3 @@ export const useSceneStore = defineStore('scene', () => {
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 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 { useInstanceStore } from "@/store/instance";
import { useMenuStore } from "@/store/menu";
import { Button, Descriptions, Modal } from "ant-design-vue"
import styles from './index.module.less'
@ -14,6 +16,10 @@ const ManualInspection = defineComponent({
const { data } = props
const instanceStore = useInstanceStore();
const menuStory = useMenuStore();
const dataRender = () => {
if (data.type === 'device' || data.type === 'product') {
return (
@ -207,7 +213,13 @@ const ManualInspection = defineComponent({
emit('save', data)
}}
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>
</Modal>

View File

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

View File

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

View File

@ -1,3 +1,54 @@
<!-- 坐标点拾取组件 -->
<template>
<div>AMap</div>
</template>
<div style="width: 100%; height: 400px">
<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>
<div>Charts</div>
</template>
<a-spin :spinning="loading">
<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'">
{{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
</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'">
<a-space>
<a-button
@ -30,7 +37,7 @@
@click="_download(record)"
><AIcon type="DownloadOutlined"
/></a-button>
<a-button type="link"
<a-button type="link" @click="showDetail(record)"
><AIcon type="SearchOutlined"
/></a-button>
</a-space>
@ -38,6 +45,28 @@
</template>
</a-table>
</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>
<script lang="ts" setup>
@ -46,6 +75,8 @@ import { useInstanceStore } from '@/store/instance';
import encodeQuery from '@/utils/encodeQuery';
import moment from 'moment';
import { getType } from '../index';
import ValueRender from '../ValueRender.vue';
import JsonViewer from 'vue-json-viewer';
const _props = defineProps({
data: {
@ -57,8 +88,11 @@ const _props = defineProps({
default: () => [],
},
});
const instanceStore = useInstanceStore();
const dataSource = ref({});
const current = ref<any>({});
const visible = ref<boolean>(false);
const columns = computed(() => {
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 resp = await getPropertyData(
instanceStore.current.id,
@ -99,7 +138,7 @@ const queryPropertyData = async (params: any) => {
...params,
terms: {
property: _props.data.id,
timestamp$BTW: _props.time?.length ? _props.time : [],
timestamp$BTW: _props.time,
},
sorts: { timestamp: 'desc' },
}),
@ -109,14 +148,20 @@ const queryPropertyData = async (params: any) => {
}
};
watchEffect(() => {
if (_props.data.id) {
queryPropertyData({
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
pageIndex: 0,
});
watch(
() => [_props.data.id, _props.time],
([newVal]) => {
if (newVal) {
queryPropertyData({
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
pageIndex: 0,
});
}
},
{
deep: true, immediate: true
}
});
);
const onChange = (page: any) => {
queryPropertyData({
@ -140,4 +185,10 @@ const _download = (record: any) => {
//
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">
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></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="列表">
<Table :data="props.data" :time="_getTimes" />
</a-tab-pane>
<a-tab-pane key="charts" tab="图表">
<Charts />
<Charts :data="props.data" :time="_getTimes" />
</a-tab-pane>
<a-tab-pane key="geo" tab="轨迹">
<AMap />
<a-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'">
<PropertyAMap :data="props.data" :time="_getTimes" />
</a-tab-pane>
</a-tabs>
</div>
@ -21,7 +21,7 @@
import type { Dayjs } from 'dayjs';
import TimeComponent from './TimeComponent.vue'
import Charts from './Charts.vue'
import AMap from './AMap.vue'
import PropertyAMap from './PropertyAMap.vue'
import Table from './Table.vue'
const props = defineProps({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,167 +1,180 @@
<template>
<page-container>
<Search
:columns="query.columns"
target="product-manage"
@search="handleSearch"
/>
<JTable
:columns="columns"
:request="queryProductList"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="add"
><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',
}"
<Search
:columns="query.columns"
target="product-manage"
@search="handleSearch"
/>
<JTable
:columns="columns"
:request="queryProductList"
ref="tableRef"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
>
<template #headerTitle>
<a-space>
<a-button type="primary" @click="add"
><plus-outlined />新增</a-button
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3
<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>
<slot name="img">
<img
:src="
slotProps.photoUrl ||
getImage('/device-product.png')
"
class="productImg"
/>
</slot>
</template>
<template #content>
<Ellipsis
><span
@click.stop="handleView(slotProps.id)"
style="font-weight: 600"
style="font-weight: 600; font-size: 16px"
>
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
</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>
<Ellipsis
><div>
{{ slotProps?.accessName }}
</div></Ellipsis
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<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-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<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
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
:disabled="i.disabled"
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save
ref="saveRef"
:isAdd="isAdd"
:title="title"
@success="refresh"
/>
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<!-- 新增编辑 -->
<Save ref="saveRef" :isAdd="isAdd" :title="title" @success="refresh" />
</page-container>
</template>
@ -193,11 +206,11 @@ import { isNoCommunity, downloadObject } from '@/utils/utils';
import { omit } from 'lodash-es';
import { typeOptions } from '@/components/Search/util';
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 isAdd = ref<number>(0);
const title = ref<string>('');
@ -425,7 +438,7 @@ const beforeUpload = (file: any) => {
* 查看
*/
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;
background: #f0f2f5;
}
.productImg {
width: 88px;
height: 88px;
}
.productName {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</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"
>
<a-row :gutter="[0, 10]">
<a-col :span="22">
<a-col :span="!!route.query.id ? 24 : 22">
<a-select
v-model:value="formData.productId"
placeholder="请选择所属产品"
@ -66,7 +66,7 @@
</a-select-option>
</a-select>
</a-col>
<a-col :span="2">
<a-col :span="2" v-if="!route.query.id">
<a-button
type="link"
@click="saveProductVis = true"
@ -132,12 +132,11 @@
placeholder="请输入说明"
/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 0, span: 3 }">
<a-form-item>
<a-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
style="width: 100%"
>
保存
</a-button>
@ -356,6 +355,8 @@ const getDetail = async () => {
// formData.value = res.result;
Object.assign(formData.value, res.result);
formData.value.channel = res.result.provider;
console.log('formData.value: ', formData.value);
};
onMounted(() => {
@ -367,6 +368,7 @@ onMounted(() => {
*/
const btnLoading = ref<boolean>(false);
const handleSubmit = () => {
// console.log('formData.value: ', formData.value);
validate()
.then(async () => {
btnLoading.value = true;

View File

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

View File

@ -333,23 +333,9 @@ watch(
msgType.value = MSG_TYPE[val];
formData.value.provider =
formData.value.provider !== ':id'
route.params.id !== ':id'
? formData.value.provider
: 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,
);
const clearValid = () => {
setTimeout(() => {
clearValidate();
}, 200);
};
const getDetail = async () => {
if (route.params.id === ':id') return;
const res = await configApi.detail(route.params.id as string);
@ -444,7 +424,7 @@ const handleTypeChange = () => {
setTimeout(() => {
formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles();
resetPublicFiles();
}, 0);
};
@ -454,7 +434,48 @@ const handleTypeChange = () => {
const handleProviderChange = () => {
formData.value.configuration =
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"
width="80%"
>
<a-row :gutter="10">
<a-row :gutter="10" class="model-body">
<a-col :span="4">
<a-input
v-model:value="deptName"
@ -40,6 +40,7 @@
:dataSource="dataSource"
:loading="tableLoading"
model="table"
noPagination
>
<template #headerTitle>
<a-button type="primary" @click="handleAutoBind">
@ -273,14 +274,24 @@ const getActions = (
* 自动绑定
*/
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('操作成功');
getTableData();
});
};
/**
* 获取钉钉部门用户
* 获取钉钉/微信部门用户
*/
const getDeptUsers = async () => {
let res = null;
@ -304,14 +315,24 @@ const getBindUsers = async () => {
return res?.result;
};
/**
* 获取所有用户
* 获取所有用户未绑定的用户
*/
const allUserList = ref([]);
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) => ({
label: m.name,
value: m.id,
...m,
}));
return result;
};
@ -326,31 +347,36 @@ const getTableData = () => {
Promise.all<any>([getDeptUsers(), getBindUsers(), getAllUsers()]).then(
(res) => {
dataSource.value = [];
const [deptUsers, bindUsers, allUsers] = res;
(deptUsers || []).forEach((item: any) => {
const [deptUsers, bindUsers, unBindUsers] = res;
(deptUsers || []).forEach((deptUser: any) => {
//
let unBindUser = unBindUsers.find(
(f: any) => f.name === deptUser?.name,
);
//
const bindUser = bindUsers.find(
(f: any) => f.thirdPartyUserId === item.id,
);
//
const allUser = allUsers.find(
(f: any) => f.id === bindUser?.userId,
(f: any) => f.thirdPartyUserId === deptUser.id,
);
if (bindUser) {
unBindUser = unBindUsers.find(
(f: any) => f.id === bindUser.userId,
);
}
dataSource.value.push({
thirdPartyUserId: item.id,
thirdPartyUserName: item.name,
userId: bindUser?.userId,
userName: allUser
? `${allUser.name}(${allUser.username})`
thirdPartyUserId: deptUser.id,
thirdPartyUserName: deptUser.name,
bindId: bindUser?.userId,
userId: unBindUser?.id,
userName: unBindUser
? `${unBindUser.name}(${unBindUser.username})`
: '',
status: {
text: bindUser?.providerName ? '已绑定' : '未绑定',
value: bindUser?.providerName ? 'success' : 'error',
},
bindId: bindUser?.id,
});
});
console.log('dataSource.value: ', dataSource.value);
// console.log('dataSource.value: ', dataSource.value);
},
);
tableLoading.value = false;
@ -369,7 +395,11 @@ watch(
*/
const bindVis = ref(false);
const confirmLoading = ref(false);
const formData = ref({ userId: '' });
const formData = ref({
userId: '',
thirdPartyUserId: '',
thirdPartyUserName: '',
});
const formRules = ref({
userId: [{ required: true, message: '请选择用户', trigger: 'change' }],
});
@ -381,7 +411,8 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
const handleBind = (row: any) => {
bindVis.value = true;
formData.value = row;
// formData.value = row;
Object.assign(formData.value, row);
getAllUsers();
};
@ -402,8 +433,8 @@ const filterOption = (input: string, option: any) => {
const handleBindSubmit = () => {
validate().then(async () => {
const params = {
// providerName: formData.value.thirdPartyUserName,
// thirdPartyUserId: formData.value.thirdPartyUserId,
providerName: formData.value.thirdPartyUserName,
thirdPartyUserId: formData.value.thirdPartyUserId,
userId: formData.value.userId,
};
confirmLoading.value = true;
@ -434,8 +465,13 @@ const handleBindSubmit = () => {
};
const handleCancel = () => {
bindVis.value = false;
resetFields()
resetFields();
};
</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-upload>
<a-popconfirm
title="确认导出当前页数据"
title="确认导出"
ok-text="确定"
cancel-text="取消"
@confirm="handleExport"
@ -308,7 +308,7 @@ const beforeUpload = (file: any) => {
* 导出
*/
const handleExport = () => {
downloadObject(configRef.value.dataSource, `通知配置`);
downloadObject(configRef.value._dataSource, `通知配置`);
};
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);
fileList.value[targetFileIdx].name = info.file.name;
fileList.value[targetFileIdx].location = info.file.response?.result;
emit(
'update:attachments',
fileList.value.map(({ name, location }) => ({ name, location })),
);
emitEvents();
}
};
@ -107,6 +104,7 @@ const handleDelete = (id: string | undefined) => {
const idx = fileList.value.findIndex((f) => f.id === id);
fileList.value.splice(idx, 1);
emitEvents();
};
/**
@ -118,6 +116,15 @@ const handleAdd = () => {
name: '',
location: '',
});
emitEvents();
};
const emitEvents = () => {
emit(
'update:attachments',
fileList.value.map(({ name, location }) => ({ name, location })),
);
};
/**

View File

@ -106,7 +106,8 @@
<a-form-item label="收信部门">
<ToOrg
v-model:toParty="
formData.template.toParty
formData.template
.departmentIdList
"
:type="formData.type"
:config-id="formData.configId"
@ -132,7 +133,7 @@
</template>
<ToUser
v-model:toUser="
formData.template.toUser
formData.template.userIdList
"
:type="formData.type"
:config-id="formData.configId"
@ -800,26 +801,59 @@ const formData = ref<TemplateFormData>({
});
/**
* 重置公用字段值
* 重置字段值
*/
const resetPublicFiles = () => {
formData.value.template.message = '';
formData.value.configId = undefined;
if (
formData.value.provider === 'dingTalkMessage' ||
formData.value.type === 'weixin'
) {
formData.value.template.toTag = undefined;
formData.value.template.toUser = undefined;
formData.value.template.agentId = undefined;
switch (formData.value.provider) {
case 'dingTalkMessage':
formData.value.template.agentId = '';
formData.value.template.message = '';
formData.value.template.departmentIdList = '';
formData.value.template.userIdList = '';
break;
case 'dingTalkRobotWebHook':
formData.value.template.message = '';
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;
if (formData.value.type === 'email')
formData.value.template.toParty = undefined;
// formData.value.description = '';
formData.value.configId = undefined;
formData.value.variableDefinitions = [];
handleMessageTypeChange();
};
//
@ -831,15 +865,8 @@ watch(
route.params.id !== ':id'
? formData.value.provider
: 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();
// clearValid();
// console.log('formData.value: ', formData.value);
if (val === 'sms') {
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({
type: [{ required: true, message: '请选择通知方式' }],
@ -917,7 +935,7 @@ watch(
() => formData.value.template.markdown?.title,
(val) => {
if (!val) return;
variableReg(val);
variableReg();
},
{ deep: true },
);
@ -926,7 +944,7 @@ watch(
() => formData.value.template.link?.title,
(val) => {
if (!val) return;
variableReg(val);
variableReg();
},
{ deep: true },
);
@ -935,7 +953,7 @@ watch(
() => formData.value.template.subject,
(val) => {
if (!val) return;
variableReg(val);
variableReg();
},
{ deep: true },
);
@ -945,7 +963,7 @@ watch(
() => formData.value.template.message,
(val) => {
if (!val) return;
variableReg(val);
variableReg();
},
{ deep: true },
);
@ -954,21 +972,42 @@ watch(
() => formData.value.template.body,
(val) => {
if (!val) return;
variableReg(val);
variableReg();
},
{ 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
*/
const variableReg = (value: string) => {
const variableReg = () => {
const _val = spliceStr();
//
const oldKey = formData.value.variableDefinitions?.map((m) => m.id);
// ${}
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 result = newKey?.map((m) =>
oldKey.includes(m)
@ -980,28 +1019,37 @@ const variableReg = (value: string) => {
format: '%s',
},
);
formData.value.variableDefinitions = [
...new Set([
...formData.value.variableDefinitions,
...(result as IVariableDefinitions[]),
]),
];
formData.value.variableDefinitions = result as IVariableDefinitions[];
};
/**
* 钉钉机器人 消息类型选择改变
*/
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.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(() => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value.template: ', formData.value.template);
resetPublicFiles();
}, 0);
};
@ -1047,6 +1096,8 @@ const handleTypeChange = () => {
const handleProviderChange = () => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value: ', formData.value);
// console.log('formData.value.template: ', formData.value.template);
getConfigList();
resetPublicFiles();
};
@ -1112,8 +1163,9 @@ const handleSubmit = () => {
setTimeout(() => {
validate()
.then(async () => {
formData.value.template.ttsCode =
formData.value.template.templateCode;
if (formData.value.provider === 'ttsCode')
formData.value.template.ttsCode =
formData.value.template.templateCode;
btnLoading.value = true;
let res;
if (!formData.value.id) {

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
@click='save'
@cancel='cancel'
>
<a-steps :current='addModel.stepNumber'>
<a-steps :current='addModel.stepNumber' @change='stepChange'>
<a-step>
<template #title>选择产品</template>
</a-step>
@ -17,19 +17,28 @@
<template #title>触发类型</template>
</a-step>
</a-steps>
<a-divider style='margin-bottom: 0px' />
<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>
<template #footer>
<div class='steps-action'>
<template>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-else>上一步</a-button>
</template>
<template>
<a-button type='primary' v-if='addModel.stepNumber < 2'>下一步</a-button>
<a-button type='primary' v-else>确定</a-button>
</template>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-else @click='prev'>上一步</a-button>
<a-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</a-button>
<a-button type='primary' v-else @click='saveClick'>确定</a-button>
</div>
</template>
</a-modal>
@ -37,10 +46,12 @@
<script setup lang='ts' name='AddModel'>
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 { detail as deviceDetail } from '@/api/device/instance'
import Product from './Product.vue'
import DeviceSelect from './DeviceSelect.vue'
import Type from './Type.vue'
type Emit = {
(e: 'cancel'): void
@ -54,11 +65,7 @@ interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
orgId: Array<{ label: string, value: string }>
productDetail: any
selectorValues: Array<{ label: string, value: string }>
metadata: {
properties?: any[]
functions?: any[]
events?: any[]
}
metadata: metadataType
}
const emit = defineEmits<Emit>()
@ -97,39 +104,56 @@ const handleOptions = () => {
}
const prev = () => {
addModel.stepNumber = addModel.stepNumber - 1
}
const cancel = () => {
emit("cancel")
}
const handleMetadata = (metadata: string) => {
const handleMetadata = (metadata?: string) => {
try {
addModel.metadata = JSON.parse(metadata)
addModel.metadata = JSON.parse(metadata || "{}")
} catch (e) {
console.warn('handleMetadata: ' + e)
}
}
const save = async () => {
if (addModel.stepNumber === 0) {
const save = async (step?: number) => {
let _step = step !== undefined ? step : addModel.stepNumber
if (_step === 0) {
addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error')
} else if (addModel.stepNumber === 1) {
} else if (_step === 1) {
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')
}
//
if (isFixed && addModel.selectorValues?.length === 1) {
const resp = await deviceDetail(addModel.selectorValues[0].value)
addModel.metadata
handleMetadata(resp.result.metadata)
} else {
handleMetadata(addModel.productDetail?.metadata)
}
//
addModel.stepNumber = 2
} else {
}
// handleOptions()
// emit('update:value', {})
}
const saveClick = () => save()
const stepChange = (step: number) => {
if (step !== 0) {
save(step - 1)
} else {
addModel.stepNumber = 0
}
}
</script>
<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'
@search="handleSearch"
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
:columns='columns'
ref='actionRef'
model='CARD'
:columns='columns'
:params='params'
:request='productQuery'
:gridColumn='2'
model='CARD'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="selectedRowKeys.includes(slotProps.id)"
:active="rowKey === slotProps.id"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'success', 0: 'error', }"
@ -23,13 +30,17 @@
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3 style="font-weight: 600" >
{{ slotProps.name }}
</h3>
<div style='width: calc(100% - 100px)'>
<Ellipsis>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
@ -51,16 +62,25 @@ import { getTreeData_api } from '@/api/system/department'
import { isNoCommunity } from '@/utils/utils'
import { getImage } from '@/utils/comm'
type Emit = {
(e: 'update:rowKey', data: string): void
(e: 'update:detail', data: string): void
}
const actionRef = ref()
const params = ref({})
const props = defineProps({
rowKey: {
type: String,
default: ''
},
detail: {
type: Object,
default: () => ({})
}
})
const selectedRowKeys = ref(props.rowKey)
const emit = defineEmits<Emit>()
const columns = [
{
@ -69,12 +89,19 @@ const columns = [
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
},
{
title: '网关类型',
@ -199,7 +226,6 @@ const columns = [
const handleSearch = (p: any) => {
params.value = p
actionRef.value.required()
}
const productQuery = (p: any) => {
@ -217,12 +243,8 @@ const productQuery = (p: any) => {
}
const handleClick = (detail: any) => {
const _selected = new Set(selectedRowKeys.value)
if (_selected.has(detail.id)) {
_selected.delete(detail.id)
} else {
_selected.add(detail.id)
}
emit('update:rowKey', detail.id)
emit('update:detail', detail)
}
</script>
@ -230,5 +252,7 @@ const handleClick = (detail: any) => {
<style scoped lang='less'>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</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'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const { data } = storeToRefs<any>(sceneStore)
const visible = ref(false)
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;
};
export type optionItem = {
label: string
value: string
}
type Action = {
executor: string;
configuration: Record<string, unknown>;
@ -311,3 +316,9 @@ export interface FormModelType {
options?: Record<string, any>;
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>
<div class="form-label-container">
<span class="text">{{ props.text }}</span>
<span class="required">*</span>
<span class="required" v-show="props.required">*</span>
<a-tooltip>
<template #title>{{ props.tooltip }}</template>
<AIcon type="QuestionCircleOutlined" style="color: #00000073;cursor: inherit;" />
<AIcon type="QuestionCircleOutlined" class="icon" />
</a-tooltip>
</div>
</template>
@ -24,11 +24,15 @@ const props = defineProps<{
.required {
display: inline-block;
margin-right: 4px;
color: #ff4d4f;
font-size: 14px;
font-family: SimSun, sans-serif;
line-height: 1;
}
.icon {
color: #00000073;
cursor: inherit;
margin-left: 4px;
}
}
</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-row :gutter="24">
<a-col :span="14">
<a-form
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>
<EditForm @change-apply-type="chengeType" />
</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-card>
</page-container>
@ -355,151 +13,11 @@
<script setup lang="ts">
import Does from './components/Does.vue';
import FormLabel from './components/FormLabel.vue';
import { getImage } from '@/utils/comm';
import type { applyType, formType } from './typing';
const routeQuery = useRoute().query;
import EditForm from './components/EditForm.vue';
import type { applyType } from './typing';
const initForm: formType = {
name: '',
provider: 'internal-standalone',
integrationModes: [],
config: '',
description: '',
page: {
baseUrl: '',
routeType: 'hash',
},
apiClient: {
baseUrl: '',
authConfig: {
type: '',
oauth2: {
authorizationUrl: '',
tokenUrl: '',
redirectUri: '',
clientId: '',
clientSecret: '',
},
},
},
const rightType = ref<applyType>('internal-standalone');
const chengeType = (newType: applyType) => {
rightType.value = newType;
};
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>
<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'
| 'dingtalk-ent-app'
| 'third-party'
export type dictType = {
id: string;
name: string;
children?: dictType
}[];
export type optionsType = {
label: string,
value: string;
disabled?: boolean
}[]
export type formType = {
id?:string,
name: string;
provider: applyType;
integrationModes: string[];
config: string;
description: string;
page: {
baseUrl:string,
routeType:'hash' | 'history'
},
apiClient: {
page: { // 页面集成
baseUrl: string,
authConfig: {
type:string,
oauth2 :{
authorizationUrl:string,
tokenUrl: string,
redirectUri:string,
clientId:string,
clientSecret:string
routeType: 'hash' | 'history',
parameters: optionsType
},
apiClient: { // API客户端
baseUrl: string, // 接口地址
headers: optionsType, // 请求头
parameters: optionsType, // 请求参数
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