Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
# Conflicts: # package-lock.json # yarn.lock
This commit is contained in:
commit
e6383367c8
|
@ -15,9 +15,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
"@vuemap/vue-amap": "^1.1.20",
|
"@vuemap/vue-amap": "^1.1.20",
|
||||||
|
"@vueuse/core": "^9.10.0",
|
||||||
"ant-design-vue": "^3.2.15",
|
"ant-design-vue": "^3.2.15",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"echarts": "^5.4.1",
|
"echarts": "^5.4.1",
|
||||||
|
"event-source-polyfill": "^1.0.31",
|
||||||
"jetlinks-store": "^0.0.3",
|
"jetlinks-store": "^0.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1,25 @@
|
||||||
|
import { BASE_API_PATH } from "@/utils/variable";
|
||||||
|
import server from '@/utils/request'
|
||||||
|
import { SearchHistoryList } from 'components/Search/types'
|
||||||
|
|
||||||
|
export const FILE_UPLOAD = `${BASE_API_PATH}/file/static`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存查询记录
|
||||||
|
* @param data
|
||||||
|
* @param target
|
||||||
|
*/
|
||||||
|
export const saveSearchHistory = (data: any, target:string) => server.post(`/user/settings/${target}`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询记录
|
||||||
|
* @param target
|
||||||
|
*/
|
||||||
|
export const getSearchHistory = (target:string) => server.get<SearchHistoryList[]>(`/user/settings/${target}`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定查询记录
|
||||||
|
* @param id
|
||||||
|
* @param target
|
||||||
|
*/
|
||||||
|
export const deleteSearchHistory = (target:string, id:string) => server.remove<SearchHistoryList[]>(`/user/settings/${target}/${id}`)
|
|
@ -1,4 +1,5 @@
|
||||||
import server from '@/utils/request'
|
import server from '@/utils/request'
|
||||||
|
import { BASE_API_PATH } from '@/utils/variable'
|
||||||
import { DeviceInstance } from '@/views/device/instance/typings'
|
import { DeviceInstance } from '@/views/device/instance/typings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,4 +22,80 @@ export const saveMetadata = (id: string, data: string) => server.put(`/device/in
|
||||||
* @param id 设备ID
|
* @param id 设备ID
|
||||||
* @returns 设备详情
|
* @returns 设备详情
|
||||||
*/
|
*/
|
||||||
export const detail = (id: string) => server.get<DeviceInstance>(`/device-instance/${id}/detail`)
|
export const detail = (id: string) => server.get<DeviceInstance>(`/device-instance/${id}/detail`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询数据
|
||||||
|
* @param data 分页搜索数据
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const query = (data?: Record<string, any>) => server.post('/device-instance/_query', data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除设备
|
||||||
|
* @param id 设备ID
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const _delete = (id: string) => server.remove(`/device-instance/${id}`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用设备
|
||||||
|
* @param id 设备ID
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const _deploy = (id: string) => server.post(`/device-instance/${id}/deploy`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用设备
|
||||||
|
* @param id 设备ID
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const _undeploy = (id: string) => server.post(`/device-instance/${id}/undeploy`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量激活设备
|
||||||
|
* @param data 设备id数组
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const batchDeployDevice = (data: string[]) => server.put(`/device-instance/batch/_deploy`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量注销设备
|
||||||
|
* @param data 设备id数组
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const batchUndeployDevice = (data: string[]) => server.put(`/device-instance/batch/_unDeploy`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
* @param data 设备id数组
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const batchDeleteDevice = (data: string[]) => server.put(`/device-instance/batch/_delete`, data)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载设备模板
|
||||||
|
* @param productId 产品id
|
||||||
|
* @param type 文件类型
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备导入
|
||||||
|
* @param productId 产品id
|
||||||
|
* @param type 文件类型
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备导出
|
||||||
|
* @param productId 产品id
|
||||||
|
* @param type 文件类型
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deviceExport = (productId: string, type: string) => `${BASE_API_PATH}/device-instance${!!productId ? '/' + productId : ''}/export.${type}`
|
||||||
|
|
||||||
|
|
|
@ -36,4 +36,10 @@ export const getCodecs = () => server.get<{id: string, name: string}>('/device/p
|
||||||
* @param id 产品ID
|
* @param id 产品ID
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`)
|
export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品分类
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export const category = (data: any) => server.post('/device/category/_tree', data)
|
|
@ -5,4 +5,4 @@ export const getDeviceCount_api = () => server.get(`/device/instance/_count`);
|
||||||
// 产品数量
|
// 产品数量
|
||||||
export const getProductCount_api = (data) => server.post(`/device-product/_count`, data);
|
export const getProductCount_api = (data) => server.post(`/device-product/_count`, data);
|
||||||
// 查询产品列表
|
// 查询产品列表
|
||||||
export const getProductList_api = (data) => server.get(`/device/product/_query/no-paging?paging=false`, data);
|
export const getProductList_api = (data) => server.get(`/device/product/_query/no-paging?paging=false`, data);
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import server from '@/utils/request';
|
|
||||||
|
|
||||||
export const getProviders = () => server.get(`/gateway/device/providers`);
|
|
||||||
|
|
||||||
export const detail = (id) => server.get(`/gateway/device/${id}`);
|
|
||||||
|
|
||||||
export const getNetworkList = (networkType, data, params) =>
|
|
||||||
server.get(
|
|
||||||
`/network/config/${networkType}/_alive?include=${params.include}`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getProtocolList = (transport, params) =>
|
|
||||||
server.get(`/protocol/supports/${transport ? transport : ''}`, params);
|
|
||||||
|
|
||||||
export const getConfigView = (id, transport) =>
|
|
||||||
server.get(`/protocol/${id}/transport/${transport}`);
|
|
||||||
|
|
||||||
export const getChildConfigView = (id) =>
|
|
||||||
server.get(`/protocol/${id}/transports`);
|
|
||||||
|
|
||||||
export const save = (data) => server.post(`/gateway/device`, data);
|
|
||||||
|
|
||||||
export const update = (data) => server.patch(`/gateway/device`, data);
|
|
||||||
|
|
||||||
export const list = (data) =>
|
|
||||||
server.post(`/gateway/device/detail/_query`, data);
|
|
||||||
|
|
||||||
export const undeploy = (id) => server.post(`/gateway/device/${id}/_shutdown`);
|
|
||||||
|
|
||||||
export const deploy = (id) => server.post(`/gateway/device/${id}/_startup`);
|
|
||||||
|
|
||||||
export const del = (id) => server.remove(`/gateway/device/${id}`);
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
|
||||||
|
export const getProviders = () => server.get(`/gateway/device/providers`);
|
||||||
|
|
||||||
|
export const detail = (id: string) => server.get(`/gateway/device/${id}`);
|
||||||
|
|
||||||
|
export const getNetworkList = (
|
||||||
|
networkType: string,
|
||||||
|
include: string,
|
||||||
|
data: Object,
|
||||||
|
) =>
|
||||||
|
server.get(
|
||||||
|
`/network/config/${networkType}/_alive?include=${include}`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getProtocolList = (transport: string, params: Object) =>
|
||||||
|
server.get(`/protocol/supports/${transport ? transport : ''}`, params);
|
||||||
|
|
||||||
|
export const getConfigView = (id: string, transport: string) =>
|
||||||
|
server.get(`/protocol/${id}/transport/${transport}`);
|
||||||
|
|
||||||
|
export const getChildConfigView = (id: string) =>
|
||||||
|
server.get(`/protocol/${id}/transports`);
|
||||||
|
|
||||||
|
export const save = (data: Object) => server.post(`/gateway/device`, data);
|
||||||
|
|
||||||
|
export const update = (data: Object) => server.patch(`/gateway/device`, data);
|
||||||
|
|
||||||
|
export const list = (data: Object) =>
|
||||||
|
server.post(`/gateway/device/detail/_query`, data);
|
||||||
|
|
||||||
|
export const undeploy = (id: string) =>
|
||||||
|
server.post(`/gateway/device/${id}/_shutdown`);
|
||||||
|
|
||||||
|
export const deploy = (id: string) =>
|
||||||
|
server.post(`/gateway/device/${id}/_startup`);
|
||||||
|
|
||||||
|
export const del = (id: string) => server.remove(`/gateway/device/${id}`);
|
||||||
|
|
||||||
|
export const getResourcesCurrent = () =>
|
||||||
|
server.get(`/network/resources/alive/_current`);
|
||||||
|
|
||||||
|
export const getClusters = () =>
|
||||||
|
server.get(`network/resources/clusters`);
|
|
@ -1,3 +0,0 @@
|
||||||
import server from '@/utils/request'
|
|
||||||
|
|
||||||
export const save = (data) => server.post(`/network/certificate`, data)
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
import { BASE_API_PATH } from '@/utils/variable';
|
||||||
|
|
||||||
|
export const NETWORK_CERTIFICATE_UPLOAD = `${BASE_API_PATH}/network/certificate/upload`;
|
||||||
|
|
||||||
|
|
||||||
|
export const save = (data: object) => server.post(`/network/certificate`, data);
|
|
@ -12,4 +12,6 @@ export const postInitSet = (data) => server.post(`/user/settings/init`, data)
|
||||||
|
|
||||||
export const systemVersion = () => server.get(`/system/version`)
|
export const systemVersion = () => server.get(`/system/version`)
|
||||||
|
|
||||||
export const bindInfo = () => server.get(`/application/sso/_all`)
|
export const bindInfo = () => server.get(`/application/sso/_all`)
|
||||||
|
|
||||||
|
export const settingDetail = (scopes) => server.get(`/system/config/${scopes}`)
|
|
@ -0,0 +1,8 @@
|
||||||
|
import server from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询数据
|
||||||
|
* @param data 分页搜索数据
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const query = (data: Record<string, any>) => server.post('/dueros/product/_query', data)
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { patch, post, get } from '@/utils/request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 列表
|
||||||
|
list: (data: any) => post(`/notifier/config/_query`, data),
|
||||||
|
// 详情
|
||||||
|
detail: (id: string): any => get(`/notifier/config/${id}`),
|
||||||
|
// 新增
|
||||||
|
save: (data: any) => post(`/notifier/config`, data),
|
||||||
|
// 修改
|
||||||
|
update: (data: any) => patch(`/notifier/config`, data)
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { patch, post, get } from '@/utils/request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 列表
|
||||||
|
list: (data: any) => post(`/notifier/template/_query`, data),
|
||||||
|
// 详情
|
||||||
|
detail: (id: string): any => get(`/notifier/template/${id}`),
|
||||||
|
// 新增
|
||||||
|
save: (data: any) => post(`/notifier/template`, data),
|
||||||
|
// 修改
|
||||||
|
update: (data: any) => patch(`/notifier/template`, data)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
export const save_api = (data: any) => server.post(`/system/config/scope/_save`, data)
|
||||||
|
// 获取详情
|
||||||
|
export const getDetails_api = (data: any) => server.post(`/system/config/scopes`, data)
|
|
@ -0,0 +1,16 @@
|
||||||
|
import server from '@/utils/request';
|
||||||
|
|
||||||
|
// 获取角色列表
|
||||||
|
export const getRoleList_api = (data: any): Promise<any> => server.post(`/role/_query/`, data);
|
||||||
|
// 删除角色
|
||||||
|
export const delRole_api = (id: string): Promise<any> => server.remove(`/role/${id}`);
|
||||||
|
// 保存角色
|
||||||
|
export const saveRole_api = (data: any): Promise<any> => server.post(`/role`, data);
|
||||||
|
// 获取角色对应的权限树
|
||||||
|
export const getPrimissTree_api = (id: string): Promise<any> => server.get(`/menu/role/${id}/_grant/tree`);
|
||||||
|
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
export const getUserByRole_api = (data: any): Promise<any> => server.post(`/user/_query/`, data);
|
||||||
|
// 将用户与该角色进行绑定
|
||||||
|
export const bindUser_api = (roleId:string, data: string[]): Promise<any> => server.post(`/role/${roleId}/users/_bind`, data);
|
|
@ -1,7 +0,0 @@
|
||||||
import { createFromIconfontCN } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
const AliIcon = createFromIconfontCN({
|
|
||||||
scriptUrl: '/icons/iconfont.js', // 在 iconfont.cn 上生成
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AliIcon
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createFromIconfontCN } from '@ant-design/icons-vue';
|
||||||
|
import * as $Icon from '@ant-design/icons-vue';
|
||||||
|
import { createVNode } from 'vue';
|
||||||
|
|
||||||
|
const AliIcon = createFromIconfontCN({
|
||||||
|
scriptUrl: '/icons/iconfont.js', // 在 iconfont.cn 上生成
|
||||||
|
});
|
||||||
|
|
||||||
|
const AntdIcon = (props: {type: string}) => {
|
||||||
|
const {type} = props;
|
||||||
|
let antIcon: {[key: string]: any} = $Icon
|
||||||
|
return createVNode(antIcon[type])
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconKeys = [
|
||||||
|
'EyeOutlined',
|
||||||
|
'EditOutlined',
|
||||||
|
'PlusOutlined',
|
||||||
|
'DeleteOutlined',
|
||||||
|
'CheckCircleOutlined',
|
||||||
|
'StopOutlined',
|
||||||
|
'CheckOutlined',
|
||||||
|
'CloseOutlined',
|
||||||
|
'DownOutlined',
|
||||||
|
'ImportOutlined',
|
||||||
|
'ExportOutlined',
|
||||||
|
'SyncOutlined',
|
||||||
|
'ExclamationCircleOutlined',
|
||||||
|
'UploadOutlined'
|
||||||
|
]
|
||||||
|
|
||||||
|
const Icon = (props: {type: string}) => {
|
||||||
|
if(iconKeys.includes(props.type)) return <AntdIcon {...props} />
|
||||||
|
return <AliIcon {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Icon
|
|
@ -6,8 +6,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { StatusColorEnum } from '@/utils/consts.ts';
|
// import { StatusColorEnum } from '@/utils/consts.ts';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
text: {
|
text: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -15,10 +14,10 @@ const props = defineProps({
|
||||||
status: {
|
status: {
|
||||||
type: String || Number,
|
type: String || Number,
|
||||||
default: 'default',
|
default: 'default',
|
||||||
validator: (value) => {
|
// validator: (value) => {
|
||||||
// 这个值必须匹配下列字符串中的一个
|
// // 这个值必须匹配下列字符串中的一个
|
||||||
return Object.keys(StatusColorEnum).includes(value);
|
// return Object.keys(StatusColorEnum).includes(value);
|
||||||
},
|
// },
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 自定义status值颜色
|
* 自定义status值颜色
|
||||||
|
|
|
@ -54,8 +54,8 @@
|
||||||
delete: item.key === 'delete',
|
delete: item.key === 'delete',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- <slot name="actions" v-bind="item"></slot> -->
|
<slot name="actions" v-bind="item"></slot>
|
||||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||||
<a-button :disabled="item.disabled">
|
<a-button :disabled="item.disabled">
|
||||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
@ -108,7 +108,6 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
statusText: {
|
statusText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '正常',
|
default: '正常',
|
||||||
|
@ -229,6 +228,10 @@ const handleClick = () => {
|
||||||
transform: skewX(-45deg);
|
transform: skewX(-45deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.card-item-content-title) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-mask {
|
.card-mask {
|
||||||
|
@ -284,13 +287,14 @@ const handleClick = () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
& > span,
|
& > :deep(span, button) {
|
||||||
button {
|
width: 100%;
|
||||||
width: 100% !important;
|
border-radius: 0;
|
||||||
border-radius: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
:deep(button) {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
color: #2f54eb;
|
color: #2f54eb;
|
||||||
|
@ -322,7 +326,7 @@ const handleClick = () => {
|
||||||
flex-basis: 60px;
|
flex-basis: 60px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
||||||
button {
|
:deep(button) {
|
||||||
background: @error-color-deprecated-bg;
|
background: @error-color-deprecated-bg;
|
||||||
border: 1px solid @error-color-outline;
|
border: 1px solid @error-color-outline;
|
||||||
|
|
||||||
|
@ -348,7 +352,7 @@ const handleClick = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] {
|
:deep(button[disabled]) {
|
||||||
background: @disabled-bg;
|
background: @disabled-bg;
|
||||||
border-color: @disabled-color;
|
border-color: @disabled-color;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<a-space align="end">
|
||||||
|
<a-radio-group button-style="solid" v-model:value="modelValue.fileType" placeholder="请选择文件格式">
|
||||||
|
<a-radio-button value="xlsx">xlsx</a-radio-button>
|
||||||
|
<a-radio-button value="csv">csv</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-checkbox v-model:checked="modelValue.autoDeploy">自动启用</a-checkbox>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
autoDeploy: boolean,
|
||||||
|
fileType: 'xlsx' | 'csv'
|
||||||
|
}
|
||||||
|
type Emits = {
|
||||||
|
(e: 'update:modelValue', data: Partial<Props>): void;
|
||||||
|
};
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 组件双向绑定的值
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Props>,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
fileType: 'xlsx',
|
||||||
|
autoDeploy: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
|
@ -31,7 +31,7 @@
|
||||||
v-model:value='formData.data[item.name]'
|
v-model:value='formData.data[item.name]'
|
||||||
:options='item.options'
|
:options='item.options'
|
||||||
/>
|
/>
|
||||||
<a-inputnumber
|
<a-input-number
|
||||||
v-else-if='item.component === componentType.inputNumber'
|
v-else-if='item.component === componentType.inputNumber'
|
||||||
v-bind='item.componentProps'
|
v-bind='item.componentProps'
|
||||||
v-model:value='formData.data[item.name]'
|
v-model:value='formData.data[item.name]'
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<a-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
name="avatar"
|
||||||
|
list-type="picture-card"
|
||||||
|
class="avatar-uploader"
|
||||||
|
:show-upload-list="false"
|
||||||
|
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
@change="handleChange"
|
||||||
|
>
|
||||||
|
<img v-if="imageUrl" :src="imageUrl" alt="avatar" />
|
||||||
|
<div v-else>
|
||||||
|
<loading-outlined v-if="loading"></loading-outlined>
|
||||||
|
<plus-outlined v-else></plus-outlined>
|
||||||
|
<div class="ant-upload-text">Upload</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { message, UploadChangeParam, UploadProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const handleChange = (info: UploadChangeParam) => {
|
||||||
|
// if (info.file.status === 'uploading') {
|
||||||
|
// loading.value = true;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (info.file.status === 'done') {
|
||||||
|
// // Get this url from response in real world.
|
||||||
|
// getBase64(info.file.originFileObj, (base64Url: string) => {
|
||||||
|
// imageUrl.value = base64Url;
|
||||||
|
// loading.value = false;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// if (info.file.status === 'error') {
|
||||||
|
// loading.value = false;
|
||||||
|
// message.error('upload error');
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file: UploadProps['fileList'][number]) => {
|
||||||
|
// const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||||
|
// if (!isJpgOrPng) {
|
||||||
|
// message.error('You can only upload JPG file!');
|
||||||
|
// }
|
||||||
|
// const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
// if (!isLt2M) {
|
||||||
|
// message.error('Image must smaller than 2MB!');
|
||||||
|
// }
|
||||||
|
// return isJpgOrPng && isLt2M;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.avatar-uploader {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: rgba(0,0,0,.06);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
:deep(.ant-upload.ant-upload-select-picture-card) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,6 +32,7 @@ self.MonacoEnvironment = {
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: [String, Number],
|
modelValue: [String, Number],
|
||||||
|
theme: { type: String, default: 'vs-dark' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
@ -48,7 +49,7 @@ onMounted(() => {
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
theme: 'vs-dark', // 主题色: vs(默认高亮), vs-dark(黑色), hc-black(高亮黑色)
|
theme: props.theme, // 主题色: vs(默认高亮), vs-dark(黑色), hc-black(高亮黑色)
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.onDidChangeModelContent(() => {
|
instance.onDidChangeModelContent(() => {
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<a-space align="end">
|
||||||
|
<a-upload
|
||||||
|
v-model:fileList="modelValue.upload"
|
||||||
|
name="file"
|
||||||
|
:action="FILE_UPLOAD"
|
||||||
|
:headers="{
|
||||||
|
'X-Access-Token': LocalStore.get(TOKEN_KEY)
|
||||||
|
}"
|
||||||
|
accept=".xlsx,.csv"
|
||||||
|
:maxCount="1"
|
||||||
|
:showUploadList="false"
|
||||||
|
@change="uploadChange"
|
||||||
|
>
|
||||||
|
<a-button>
|
||||||
|
<template #icon><AIcon type="UploadOutlined" /></template>
|
||||||
|
文件上传
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<div style="margin-left: 20px">
|
||||||
|
<a-space>
|
||||||
|
<a @click="downFile('xlsx')">.xlsx</a>
|
||||||
|
<a @click="downFile('csv')">.csv</a>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
<div style="margin-top: 20px" v-if="importLoading">
|
||||||
|
<a-badge v-if="flag" status="processing" text="进行中" />
|
||||||
|
<a-badge v-else status="success" text="已完成" />
|
||||||
|
<span>总数量:{{count}}</span>
|
||||||
|
<p style="color: red">{{errMessage}}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { FILE_UPLOAD } from '@/api/comm'
|
||||||
|
import { TOKEN_KEY } from '@/utils/variable';
|
||||||
|
import { LocalStore } from '@/utils/comm';
|
||||||
|
import { downloadFile } from '@/utils/utils';
|
||||||
|
import { deviceImport, deviceTemplateDownload } from '@/api/device/instance'
|
||||||
|
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
type Emits = {
|
||||||
|
(e: 'update:modelValue', data: string[]): void;
|
||||||
|
};
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 组件双向绑定的值
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
fileType: 'xlsx',
|
||||||
|
autoDeploy: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const importLoading = ref<boolean>(false)
|
||||||
|
const flag = ref<boolean>(false)
|
||||||
|
const count = ref<number>(0)
|
||||||
|
const errMessage = ref<string>('')
|
||||||
|
|
||||||
|
const downFile = (type: string) => {
|
||||||
|
downloadFile(deviceTemplateDownload(props.product, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitData = async (fileUrl: string) => {
|
||||||
|
if (!!fileUrl) {
|
||||||
|
count.value = 0
|
||||||
|
errMessage.value = ''
|
||||||
|
flag.value = true
|
||||||
|
const autoDeploy = !!props?.file?.autoDeploy || false;
|
||||||
|
importLoading.value = true
|
||||||
|
let dt = 0;
|
||||||
|
const source = new EventSourcePolyfill(deviceImport(props.product, fileUrl, autoDeploy));
|
||||||
|
source.onmessage = (e: any) => {
|
||||||
|
const res = JSON.parse(e.data);
|
||||||
|
if (res.success) {
|
||||||
|
const temp = res.result.total;
|
||||||
|
dt += temp;
|
||||||
|
count.value = dt
|
||||||
|
} else {
|
||||||
|
errMessage.value = res.message || '失败'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
source.onerror = (e: { status: number; }) => {
|
||||||
|
if (e.status === 403) errMessage.value = '暂无权限,请联系管理员'
|
||||||
|
flag.value = false
|
||||||
|
source.close();
|
||||||
|
};
|
||||||
|
source.onopen = () => {};
|
||||||
|
} else {
|
||||||
|
message.error('请先上传文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadChange = async (info: Record<string, any>) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
const resp: any = info.file.response || { result: '' };
|
||||||
|
await submitData(resp?.result || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<a-dropdown-button
|
||||||
|
type='primary'
|
||||||
|
@click='click'
|
||||||
|
placement='bottomLeft'
|
||||||
|
:visible='historyVisible'
|
||||||
|
@visibleChange='visibleChange'
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<template v-if='!showEmpty'>
|
||||||
|
<a-menu-item v-for='item in historyList' :key='item.id'>
|
||||||
|
<div class='history-item'>
|
||||||
|
<span @click.stop='itemClick(item.content)'>{{ item.name }}</span>
|
||||||
|
<a-popconfirm
|
||||||
|
title='确认删除吗?'
|
||||||
|
placement='top'
|
||||||
|
@confirm.stop='deleteHistory(item.id)'
|
||||||
|
:okButtonProps='{
|
||||||
|
loading: deleteLoading
|
||||||
|
}'
|
||||||
|
>
|
||||||
|
<span class='delete'>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</span>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</a-menu-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class='history-empty'>
|
||||||
|
<a-empty />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
<template #icon>
|
||||||
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
|
</a-dropdown-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts' name='SearchHistory'>
|
||||||
|
import { SearchOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { deleteSearchHistory, getSearchHistory } from '@/api/comm'
|
||||||
|
import type { SearchHistoryList } from 'components/Search/types'
|
||||||
|
|
||||||
|
type Emit = {
|
||||||
|
(event: 'click'): void
|
||||||
|
(event: 'itemClick', data: string): void
|
||||||
|
}
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
target: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const historyList = ref<SearchHistoryList[]>([])
|
||||||
|
const historyVisible = ref(false)
|
||||||
|
const deleteLoading = ref(false)
|
||||||
|
const showEmpty = ref(false)
|
||||||
|
|
||||||
|
const visibleChange = async (visible: boolean) => {
|
||||||
|
historyVisible.value = visible
|
||||||
|
if (visible) {
|
||||||
|
const resp = await getSearchHistory(props.target)
|
||||||
|
if (resp.success && resp.result.length) {
|
||||||
|
historyList.value = resp.result.filter(item => item.content)
|
||||||
|
showEmpty.value = false
|
||||||
|
} else {
|
||||||
|
showEmpty.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const click = () => {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemClick = (content: string) => {
|
||||||
|
historyVisible.value = false
|
||||||
|
emit('itemClick', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHistory = async (id: string) => {
|
||||||
|
deleteLoading.value = true
|
||||||
|
const resp = await deleteSearchHistory(props.target, id)
|
||||||
|
deleteLoading.value = false
|
||||||
|
if (resp.success) {
|
||||||
|
historyVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang='less'>
|
||||||
|
.history-empty {
|
||||||
|
width: 200px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: @box-shadow-base;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
width: 200px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
padding: 0 6px;
|
||||||
|
flex: 0 0 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='JSearch-item'>
|
<div class='JSearch-item'>
|
||||||
<div class='JSearch-item--type'>
|
<div class='JSearch-item--type' v-if='expand'>
|
||||||
<a-select
|
<a-select
|
||||||
v-if='index !== 1 && index !== 4'
|
v-if='index !== 1 && index !== 4'
|
||||||
:options='typeOptions'
|
:options='typeOptions'
|
||||||
v-model:value='termsModel.type'
|
v-model:value='termsModel.type'
|
||||||
style='width: 100%;'
|
style='width: 100%;'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
|
@ -17,55 +18,86 @@
|
||||||
class='JSearch-item--column'
|
class='JSearch-item--column'
|
||||||
:options='columnOptions'
|
:options='columnOptions'
|
||||||
v-model:value='termsModel.column'
|
v-model:value='termsModel.column'
|
||||||
|
@change='columnChange'
|
||||||
/>
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
class='JSearch-item--termType'
|
class='JSearch-item--termType'
|
||||||
:options='termTypeOptions'
|
:options='termTypeOptions.option'
|
||||||
v-model:value='termsModel.termType'
|
v-model:value='termsModel.termType'
|
||||||
|
@change='termTypeChange'
|
||||||
/>
|
/>
|
||||||
<div class='JSearch-item--value'>
|
<div class='JSearch-item--value'>
|
||||||
<a-input
|
<a-input
|
||||||
v-if='component === componentType.input'
|
v-if='component === componentType.input'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
v-else-if='component === componentType.select'
|
v-else-if='component === componentType.select'
|
||||||
|
showSearch
|
||||||
|
:loading='optionLoading'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:options='options'
|
:options='options'
|
||||||
|
style='width: 100%'
|
||||||
|
:filterOption='(v, option) => filterTreeSelectNode(v, option, "label")'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-inputnumber
|
<a-input-number
|
||||||
v-else-if='component === componentType.inputNumber'
|
v-else-if='component === componentType.inputNumber'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-input-password
|
<a-input-password
|
||||||
v-else-if='component === componentType.password'
|
v-else-if='component === componentType.password'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-switch
|
<a-switch
|
||||||
v-else-if='component === componentType.switch'
|
v-else-if='component === componentType.switch'
|
||||||
v-model:checked='termsModel.value'
|
v-model:checked='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-radio-group
|
<a-radio-group
|
||||||
v-else-if='component === componentType.radio'
|
v-else-if='component === componentType.radio'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-checkbox-group
|
<a-checkbox-group
|
||||||
v-else-if='component === componentType.checkbox'
|
v-else-if='component === componentType.checkbox'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:options='options'
|
:options='options'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-time-picker
|
<a-time-picker
|
||||||
v-else-if='component === componentType.time'
|
v-else-if='component === componentType.time'
|
||||||
|
valueFormat='HH:mm:ss'
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
v-else-if='component === componentType.date'
|
v-else-if='component === componentType.date'
|
||||||
|
showTime
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
|
valueFormat='YYYY-MM-DD HH:mm:ss'
|
||||||
|
style='width: 100%'
|
||||||
|
@change='valueChange'
|
||||||
/>
|
/>
|
||||||
<a-tree-select
|
<a-tree-select
|
||||||
v-else-if='component === componentType.tree'
|
v-else-if='component === componentType.treeSelect'
|
||||||
|
showSearch
|
||||||
v-model:value='termsModel.value'
|
v-model:value='termsModel.value'
|
||||||
:tree-data='options'
|
:tree-data='options'
|
||||||
|
style='width: 100%'
|
||||||
|
:fieldNames='{ label: "name", value: "id" }'
|
||||||
|
@change='valueChange'
|
||||||
|
:filterTreeNode='(v, option) => filterSelectNode(v, option)'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,27 +105,201 @@
|
||||||
|
|
||||||
<script setup lang='ts' name='SearchItem'>
|
<script setup lang='ts' name='SearchItem'>
|
||||||
import { componentType } from 'components/Form'
|
import { componentType } from 'components/Form'
|
||||||
import { typeOptions } from './util'
|
import { typeOptions, termType } from './util'
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import type { SearchItemData, SearchProps, Terms } from './types'
|
||||||
|
import { cloneDeep, get, isArray, isFunction } from 'lodash-es'
|
||||||
|
import { filterTreeSelectNode, filterSelectNode } from '@/utils/comm'
|
||||||
|
|
||||||
|
type ItemType = SearchProps['type']
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'change', data: SearchItemData): void
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
columns: {
|
||||||
type: String,
|
type: Array as PropType<SearchProps[]>,
|
||||||
default: componentType.input
|
default: () => [],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
index: {
|
index: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
expand: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
termsItem: {
|
||||||
|
type: Object as PropType<Terms>,
|
||||||
|
default: {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const termsModel = reactive({})
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
const options = ref([])
|
const termsModel = reactive<SearchItemData>({
|
||||||
|
type: 'or',
|
||||||
|
value: '',
|
||||||
|
termType: 'like',
|
||||||
|
column: ''
|
||||||
|
})
|
||||||
|
|
||||||
const columnOptions = reactive([])
|
const component = ref(componentType.input)
|
||||||
|
|
||||||
const termTypeOptions = reactive([])
|
const options = ref<any[]>([])
|
||||||
|
|
||||||
|
const columnOptions = ref<({ label: string, value: string})[]>([])
|
||||||
|
const columnOptionMap = new Map()
|
||||||
|
|
||||||
|
const termTypeOptions = reactive({option: termType})
|
||||||
|
|
||||||
|
const optionLoading = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型切换默termType值
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
const getTermType = (type?: ItemType) => {
|
||||||
|
termTypeOptions.option = termType
|
||||||
|
switch (type) {
|
||||||
|
case 'select':
|
||||||
|
case 'treeSelect':
|
||||||
|
case 'number':
|
||||||
|
return 'eq'
|
||||||
|
case 'date':
|
||||||
|
case 'time':
|
||||||
|
// 时间只有大于或小于两个值
|
||||||
|
termTypeOptions.option = termType.filter(item => ['gt','lt'].includes(item.value))
|
||||||
|
return 'gt'
|
||||||
|
default:
|
||||||
|
return 'like'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型返回组件
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
const getComponent = (type?: ItemType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'select':
|
||||||
|
component.value = componentType.select
|
||||||
|
break;
|
||||||
|
case 'treeSelect':
|
||||||
|
component.value = componentType.treeSelect
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
component.value = componentType.date
|
||||||
|
break;
|
||||||
|
case 'time':
|
||||||
|
component.value = componentType.time
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
component.value = componentType.inputNumber
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
component.value = componentType.input
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleItemOptions = (option?: any[] | Function) => {
|
||||||
|
options.value = []
|
||||||
|
if (isArray(option)) {
|
||||||
|
options.value = option
|
||||||
|
} else if (isFunction(option)) {
|
||||||
|
optionLoading.value = true
|
||||||
|
option().then((res: any[]) => {
|
||||||
|
optionLoading.value = false
|
||||||
|
options.value = res
|
||||||
|
}).catch((_: any) => {
|
||||||
|
optionLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnChange = (value: string, isChange: boolean) => {
|
||||||
|
const item = columnOptionMap.get(value)
|
||||||
|
optionLoading.value = false
|
||||||
|
// 设置value为undefined
|
||||||
|
termsModel.column = value
|
||||||
|
termsModel.termType = item.defaultTermType || getTermType(item.type)
|
||||||
|
|
||||||
|
getComponent(item.type) // 处理Item的组件类型
|
||||||
|
|
||||||
|
// 处理options 以及 request
|
||||||
|
if ('options' in item) {
|
||||||
|
handleItemOptions(item.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
termsModel.value = undefined
|
||||||
|
|
||||||
|
if (isChange) {
|
||||||
|
valueChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleItem = () => {
|
||||||
|
columnOptionMap.clear()
|
||||||
|
columnOptions.value = []
|
||||||
|
if (!props.columns.length) return
|
||||||
|
|
||||||
|
columnOptions.value = props.columns.map(item => { // 对columns进行Map处理以及值处理
|
||||||
|
columnOptionMap.set(item.column, item)
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.column
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取第一个值
|
||||||
|
const sortColumn = cloneDeep(props.columns)
|
||||||
|
sortColumn?.sort((a, b) => a.sortIndex! - b.sortIndex!)
|
||||||
|
|
||||||
|
const _index = props.index > sortColumn.length ? sortColumn.length - 1 : props.index
|
||||||
|
const _itemColumn = sortColumn[_index - 1]
|
||||||
|
|
||||||
|
columnChange(_itemColumn.column, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const termTypeChange = () => {
|
||||||
|
valueChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueChange = () => {
|
||||||
|
|
||||||
|
emit('change', {
|
||||||
|
type: termsModel.type,
|
||||||
|
value: termsModel.value,
|
||||||
|
termType: termsModel.termType,
|
||||||
|
column: termsModel.column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItem()
|
||||||
|
|
||||||
|
watch( props.termsItem, (newValue) => {
|
||||||
|
|
||||||
|
const path = props.index < 4 ? [0, 'terms', props.index - 1] : [1, 'terms', props.index - 4]
|
||||||
|
const itemData: SearchItemData = get(newValue.terms, path)
|
||||||
|
if (itemData) {
|
||||||
|
termsModel.type = itemData.type
|
||||||
|
termsModel.column = itemData.column
|
||||||
|
termsModel.termType = itemData.termType
|
||||||
|
termsModel.value = itemData.value
|
||||||
|
const item = columnOptionMap.get(itemData.column)
|
||||||
|
getComponent(item.type) // 处理Item的组件类型
|
||||||
|
|
||||||
|
// 处理options 以及 request
|
||||||
|
if ('options' in item) {
|
||||||
|
handleItemOptions(item.options)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleItem()
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -103,7 +309,7 @@ const termTypeOptions = reactive([])
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.JSearch-item--type {
|
.JSearch-item--type {
|
||||||
min-width: 120px;
|
min-width: 80px;
|
||||||
> span {
|
> span {
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -111,11 +317,11 @@ const termTypeOptions = reactive([])
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--column {
|
.JSearch-item--column {
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--termType {
|
.JSearch-item--termType {
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JSearch-item--value {
|
.JSearch-item--value {
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<a-popover
|
||||||
|
title='搜索名称'
|
||||||
|
trigger='click'
|
||||||
|
v-model:visible='visible'
|
||||||
|
@visibleChange='visibleChange'
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div style='width: 240px'>
|
||||||
|
<a-form ref='formRef' :model='modelRef'>
|
||||||
|
<a-form-item
|
||||||
|
name='name'
|
||||||
|
:rules='[
|
||||||
|
{ required: true, message: "请输入名称"}
|
||||||
|
]'
|
||||||
|
>
|
||||||
|
<a-textarea
|
||||||
|
v-model:value='modelRef.name'
|
||||||
|
:rows='3'
|
||||||
|
:maxlength='200'
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<a-button
|
||||||
|
:loading='saveHistoryLoading'
|
||||||
|
type='primary'
|
||||||
|
class='save-btn'
|
||||||
|
@click='saveHistory'
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-button>
|
||||||
|
<template #icon>
|
||||||
|
<SaveOutlined />
|
||||||
|
</template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts' name='SaveHistory'>
|
||||||
|
import type { Terms } from './types'
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { saveSearchHistory } from '@/api/comm'
|
||||||
|
import { SaveOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
terms: {
|
||||||
|
type: Object as PropType<Terms>,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchName = ref('')
|
||||||
|
|
||||||
|
const saveHistoryLoading = ref(false)
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const modelRef = reactive({
|
||||||
|
name: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前查询条件
|
||||||
|
*/
|
||||||
|
const saveHistory = async () => {
|
||||||
|
// 获取当前查询条件并转化为字符串
|
||||||
|
const formData = await formRef.value.validate()
|
||||||
|
if (formData) {
|
||||||
|
formData.content = JSON.stringify(props.terms)
|
||||||
|
saveHistoryLoading.value = true
|
||||||
|
const resp = await saveSearchHistory(formData, props.target)
|
||||||
|
saveHistoryLoading.value = false
|
||||||
|
if (resp.success) {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleChange = (e: boolean) => {
|
||||||
|
visible.value = e
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang='less'>
|
||||||
|
.save-btn {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,72 +1,381 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='JSearch-content'>
|
<div class='JSearch-warp' ref='searchRef'>
|
||||||
<div class='left'>
|
<!-- 高级模式 -->
|
||||||
<SearchItem :index='1' />
|
<div v-if='props.type === "advanced"' :class='["JSearch-content senior", expand ? "senior-expand" : "", screenSize ? "big" : "small"]'>
|
||||||
<SearchItem :index='2' />
|
<div :class='["JSearch-items", expand ? "items-expand" : "", layout]'>
|
||||||
<SearchItem :index='3' />
|
<div class='left'>
|
||||||
|
<SearchItem :expand='expand' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms'/>
|
||||||
|
<SearchItem v-if='expand' :expand='expand' :index='2' :columns='searchItems' @change='(v) => itemValueChange(v, 2)' :termsItem='terms'/>
|
||||||
|
<SearchItem v-if='expand' :expand='expand' :index='3' :columns='searchItems' @change='(v) => itemValueChange(v, 3)' :termsItem='terms'/>
|
||||||
|
</div>
|
||||||
|
<div class='center' v-if='expand'>
|
||||||
|
<a-select
|
||||||
|
v-model:value='termType'
|
||||||
|
class='center-select'
|
||||||
|
:options='typeOptions'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class='right' v-if='expand'>
|
||||||
|
<SearchItem :expand='expand' :index='4' :columns='searchItems' @change='(v) => itemValueChange(v, 4)' :termsItem='terms'/>
|
||||||
|
<SearchItem :expand='expand' :index='5' :columns='searchItems' @change='(v) => itemValueChange(v, 5)' :termsItem='terms'/>
|
||||||
|
<SearchItem :expand='expand' :index='6' :columns='searchItems' @change='(v) => itemValueChange(v, 6)' :termsItem='terms'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class='["JSearch-footer", expand ? "expand" : ""]'>
|
||||||
|
<div class='JSearch-footer--btns'>
|
||||||
|
<History :target='target' @click='searchSubmit' @itemClick='historyItemClick' />
|
||||||
|
<SaveHistory :terms='terms' :target='target'/>
|
||||||
|
<a-button @click='reset'>
|
||||||
|
<template #icon><RedoOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-button type='link' class='more-btn' @click='expandChange'>
|
||||||
|
更多筛选
|
||||||
|
<DownOutlined :class='["more-icon",expand ? "more-up" : "more-down"]' />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='center'>
|
<!-- 简单模式 -->
|
||||||
<a-select
|
<div v-else class='JSearch-content simple big'>
|
||||||
:options='typeOptions'
|
<div class='JSearch-items'>
|
||||||
/>
|
<div class='left'>
|
||||||
</div>
|
<SearchItem :expand='false' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms'/>
|
||||||
<div class='right'>
|
</div>
|
||||||
<SearchItem :index='4' />
|
</div>
|
||||||
<SearchItem :index='5' />
|
<div class='JSearch-footer'>
|
||||||
<SearchItem :index='6' />
|
<div class='JSearch-footer--btns'>
|
||||||
|
<a-button type="primary" @click='searchSubmit'>
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
搜索
|
||||||
|
</a-button>
|
||||||
|
<a-button @click='reset'>
|
||||||
|
<template #icon><RedoOutlined /></template>
|
||||||
|
重置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang='ts' name='Search'>
|
<script setup lang='ts' name='Search'>
|
||||||
import SearchItem from './Item.vue'
|
import SearchItem from './Item.vue'
|
||||||
import { typeOptions } from './util'
|
import { typeOptions } from './util'
|
||||||
|
import { useElementSize, useUrlSearchParams } from '@vueuse/core'
|
||||||
|
import { cloneDeep, isFunction, isString, set } from 'lodash-es'
|
||||||
|
import { SearchOutlined, DownOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { JColumnsProps } from 'components/Table/types'
|
||||||
|
import SaveHistory from './SaveHistory.vue'
|
||||||
|
import History from './History.vue'
|
||||||
|
import type { SearchItemData, SearchProps, Terms } from './types'
|
||||||
|
|
||||||
|
type UrlParam = {
|
||||||
|
q: string | null
|
||||||
|
target: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'search', data: Terms): void
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultParams: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array as PropType<JColumnsProps[]>,
|
||||||
default: () => []
|
default: () => [],
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'advanced'
|
default: 'advanced'
|
||||||
},
|
},
|
||||||
|
target: {
|
||||||
key: {
|
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const searchRef = ref(null)
|
||||||
|
const { width } = useElementSize(searchRef)
|
||||||
|
|
||||||
|
const urlParams = useUrlSearchParams<UrlParam>('hash')
|
||||||
|
|
||||||
|
// 是否展开更多筛选
|
||||||
|
const expand = ref(false)
|
||||||
|
|
||||||
|
// 第一组,第二组的关系
|
||||||
|
const termType = ref('and')
|
||||||
|
// 搜索历史记录
|
||||||
|
const historyList = ref([])
|
||||||
|
|
||||||
|
// 组件方向
|
||||||
|
const layout = ref('horizontal')
|
||||||
|
// 当前组件宽度 true 大于1000
|
||||||
|
const screenSize = ref(true)
|
||||||
|
|
||||||
|
const searchItems = ref<SearchProps[]>([])
|
||||||
|
// 当前查询条件
|
||||||
|
const terms = reactive<Terms>({ terms: [] })
|
||||||
|
|
||||||
|
const columnOptionMap = new Map()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const expandChange = () => {
|
||||||
|
expand.value = !expand.value
|
||||||
|
}
|
||||||
|
|
||||||
const searchParams = reactive({
|
const searchParams = reactive({
|
||||||
data: {}
|
data: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleItems = () => {
|
||||||
|
searchItems.value = []
|
||||||
|
columnOptionMap.clear()
|
||||||
|
props.columns!.forEach((item, index) => {
|
||||||
|
if (item.search && Object.keys(item.search).length) {
|
||||||
|
columnOptionMap.set(item.dataIndex, item.search)
|
||||||
|
searchItems.value.push({
|
||||||
|
...item.search,
|
||||||
|
sortIndex: item.search.first ? 0 : index + 1,
|
||||||
|
title: item.title,
|
||||||
|
column: item.dataIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemValueChange = (value: SearchItemData, index: number) => {
|
||||||
|
if (index < 4) { // 第一组数据
|
||||||
|
set(terms.terms, [0, 'terms', index - 1], value)
|
||||||
|
} else { // 第二组数据
|
||||||
|
set(terms.terms, [1, 'terms', index - 4], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUrlParams = () => {
|
||||||
|
urlParams.q = JSON.stringify(terms)
|
||||||
|
urlParams.target = props.target
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理termType为like,nlike的值
|
||||||
|
* @param v
|
||||||
|
*/
|
||||||
|
const handleLikeValue = (v: string) => {
|
||||||
|
if (isString(v)) {
|
||||||
|
return v.split('').reduce((pre: string, next: string) => {
|
||||||
|
let _next = next
|
||||||
|
if (next === '\\') {
|
||||||
|
_next = '\\\\'
|
||||||
|
} else if (next === '%') {
|
||||||
|
_next = '\\%'
|
||||||
|
}
|
||||||
|
return pre + _next
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理为外部使用
|
||||||
|
*/
|
||||||
|
const handleParamsFormat = () => {
|
||||||
|
// 过滤掉terms中value无效的item
|
||||||
|
const cloneParams = cloneDeep(terms)
|
||||||
|
return {
|
||||||
|
terms: cloneParams.terms.map(item => {
|
||||||
|
if (item.terms) {
|
||||||
|
item.terms = item.terms.filter(iItem => iItem && iItem.value)
|
||||||
|
.map(iItem => {
|
||||||
|
// 处理handleValue和rename
|
||||||
|
const _item = columnOptionMap.get(iItem.column)
|
||||||
|
if (_item.rename) {
|
||||||
|
iItem.column = _item.rename
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_item.handleValue && isFunction(_item.handleValue)) {
|
||||||
|
iItem.value = _item.handleValue(iItem.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['like','nlike'].includes(iItem.termType) && !!iItem.value) {
|
||||||
|
iItem.value = `%${handleLikeValue(iItem.value)}%`
|
||||||
|
}
|
||||||
|
return iItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交
|
||||||
|
*/
|
||||||
|
const searchSubmit = () => {
|
||||||
|
emit('search', handleParamsFormat())
|
||||||
|
if (props.type === 'advanced') {
|
||||||
|
addUrlParams()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置查询
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
terms.terms = []
|
||||||
|
expand.value = false
|
||||||
|
if (props.type === 'advanced') {
|
||||||
|
urlParams.q = null
|
||||||
|
urlParams.target = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(width, (value) => {
|
||||||
|
if (value < 1000) {
|
||||||
|
layout.value = 'vertical'
|
||||||
|
screenSize.value = false
|
||||||
|
} else {
|
||||||
|
layout.value = 'horizontal'
|
||||||
|
screenSize.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const historyItemClick = (content: string) => {
|
||||||
|
try {
|
||||||
|
terms.terms = JSON.parse(content)?.terms || []
|
||||||
|
if (terms.terms.length === 2) {
|
||||||
|
expand.value = true
|
||||||
|
}
|
||||||
|
addUrlParams()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Search组件中handleUrlParams处理JSON时异常:【${e}】`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理URL中的查询参数
|
||||||
|
* @param _params
|
||||||
|
*/
|
||||||
|
const handleUrlParams = (_params: UrlParam) => {
|
||||||
|
// URL中的target和props的一致,则还原查询参数
|
||||||
|
if (_params.target === props.target && _params.q) {
|
||||||
|
try {
|
||||||
|
terms.terms = JSON.parse(_params.q)?.terms || []
|
||||||
|
if (terms.terms.length === 2) {
|
||||||
|
expand.value = true
|
||||||
|
}
|
||||||
|
emit('search', handleParamsFormat())
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Search组件中handleUrlParams处理JSON时异常:【${e}】`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
handleUrlParams(urlParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
handleItems()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang='less'>
|
<style scoped lang='less'>
|
||||||
.JSearch-content {
|
.JSearch-warp {
|
||||||
display: flex;
|
padding: 24px;
|
||||||
gap: 16px;
|
background-color: #fff;
|
||||||
.left, & .right {
|
|
||||||
|
.JSearch-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
flex-direction: column;
|
|
||||||
width: 0;
|
.JSearch-items,& .JSearch-footer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 0;
|
}
|
||||||
}
|
|
||||||
|
.JSearch-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.left, & .right {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
.left,& .right,& .center {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.center-select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.JSearch-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 64px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.expand {
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.center {
|
.more-btn {
|
||||||
display: flex;
|
position: absolute;
|
||||||
flex-direction: column;
|
right: 0;
|
||||||
justify-content: center;
|
}
|
||||||
flex-basis: 120px;
|
}
|
||||||
|
|
||||||
|
.JSearch-footer--btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.senior-expand {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-up {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.big {
|
||||||
|
gap: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.simple {
|
||||||
|
.JSearch-items {
|
||||||
|
flex-grow: 4;
|
||||||
|
}
|
||||||
|
.JSearch-footer {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Search组件
|
||||||
|
|
||||||
|
- 需要结合Table使用
|
||||||
|
|
||||||
|
## 属性
|
||||||
|
|
||||||
|
| 名称 | 说明 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| columns | 查询下拉列表 | JColumnsProps[] | [] |
|
||||||
|
| type | 查询模式 | 'advanced', 'simple' | 'advanced' |
|
||||||
|
| target | 查询组件唯一key | String | |
|
||||||
|
| search | 查询回调事件 | Function | |
|
||||||
|
|
||||||
|
> JColumnsProps[*].search
|
||||||
|
|
||||||
|
| 名称 | 说明 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| rename | 用来重命名查询字段值 | String | |
|
||||||
|
| type | 查询值组件类型 | 'select', 'number', 'string', 'treeSelect', 'date', 'time' | |
|
||||||
|
| options | Select和TreeSelect组件下拉值 | Array, Promise | |
|
||||||
|
| first | 控制查询字段下拉默认值,默认为name即名称 | Boolean | |
|
||||||
|
| defaultTermType | 查询条件 | String | |
|
||||||
|
| handleValue | 处理单个查询value值 | Function | |
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
> columns中包含search属性才会出现在查询下拉中
|
||||||
|
|
||||||
|
```vue
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const search = (params) => {
|
||||||
|
}
|
||||||
|
<Search
|
||||||
|
:columns='columns'
|
||||||
|
target='device'
|
||||||
|
@search='search'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
> rename的作用在于search抛出params会根据rename修改数据中column的值
|
||||||
|
|
||||||
|
```vue
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
rename: 'TestName'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const search = (params) => {
|
||||||
|
terms: [
|
||||||
|
{
|
||||||
|
column: 'TestName',
|
||||||
|
value: '',
|
||||||
|
termType: 'like'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<Search
|
||||||
|
:columns='columns'
|
||||||
|
target='device'
|
||||||
|
@search='search'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
> defaultTermType的作用在于设置查询条件,相关条件参考util中的termType
|
||||||
|
|
||||||
|
```vue
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
defaultTermType: 'gt'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const search = (params) => {
|
||||||
|
terms: [
|
||||||
|
{
|
||||||
|
column: 'TestName',
|
||||||
|
value: '',
|
||||||
|
termType: 'gt'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<Search
|
||||||
|
:columns='columns'
|
||||||
|
target='device'
|
||||||
|
@search='search'
|
||||||
|
/>
|
||||||
|
```
|
|
@ -0,0 +1,49 @@
|
||||||
|
export interface SearchBaseProps {
|
||||||
|
rename?: string
|
||||||
|
type?: 'select' | 'number' | 'string' | 'treeSelect' | 'date' | 'time'
|
||||||
|
format?: string
|
||||||
|
options?: any[] | Function
|
||||||
|
first?: boolean
|
||||||
|
defaultTermType?: string // 默认 eq
|
||||||
|
title?: ColumnType.title
|
||||||
|
sortIndex?: number
|
||||||
|
handleValue?: (value: SearchItemData) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchItemProps {
|
||||||
|
rename?: SearchBaseProps['rename']
|
||||||
|
title: string
|
||||||
|
column: ColumnType.dataIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchItemData {
|
||||||
|
column: ColumnType.dataIndex
|
||||||
|
value: any
|
||||||
|
termType: string
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TermsItem {
|
||||||
|
terms: SearchItemData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Terms {
|
||||||
|
terms: TermsItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortItem {
|
||||||
|
name: string
|
||||||
|
order?: 'desc' | 'asc'
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchHistoryList {
|
||||||
|
content?: string
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchProps extends SearchBaseProps, SearchItemProps {
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,17 @@
|
||||||
export const typeOptions = [
|
export const typeOptions = [
|
||||||
{ label: '或者', value: 'or' },
|
{ label: '或者', value: 'or' },
|
||||||
{ label: '并且', value: 'and' },
|
{ label: '并且', value: 'and' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const termType = [
|
||||||
|
{ label: '=', value: 'eq' },
|
||||||
|
{ label: '!=', value: 'not' },
|
||||||
|
{ label: '包含', value: 'like' },
|
||||||
|
{ label: '不包含', value: 'nlike' },
|
||||||
|
{ label: '>', value: 'gt' },
|
||||||
|
{ label: '>=', value: 'gte' },
|
||||||
|
{ label: '<', value: 'lt' },
|
||||||
|
{ label: '<=', value: 'lte' },
|
||||||
|
{ label: '属于', value: 'in' },
|
||||||
|
{ label: '不属于', value: 'nin' },
|
||||||
|
];
|
|
@ -11,16 +11,21 @@ enum ModelEnum {
|
||||||
CARD = 'CARD',
|
CARD = 'CARD',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TypeEnum {
|
||||||
|
TREE = 'TREE',
|
||||||
|
PAGE = 'PAGE',
|
||||||
|
}
|
||||||
|
|
||||||
type RequestData = {
|
type RequestData = {
|
||||||
code: string;
|
code: string;
|
||||||
result: {
|
result: {
|
||||||
data: Record<string, any>[] | undefined;
|
data?: Record<string, any>[] | undefined;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
status: number;
|
status: number;
|
||||||
} & Record<string, any>;
|
} | Record<string, any>;
|
||||||
|
|
||||||
export interface ActionsType {
|
export interface ActionsType {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -39,16 +44,10 @@ export interface JColumnProps extends ColumnProps{
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JTableProps extends TableProps{
|
export interface JTableProps extends TableProps{
|
||||||
request?: (params: Record<string, any> & {
|
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
|
||||||
pageSize: number;
|
|
||||||
pageIndex: number;
|
|
||||||
}) => Promise<Partial<RequestData>>;
|
|
||||||
cardBodyClass?: string;
|
cardBodyClass?: string;
|
||||||
columns: JColumnProps[];
|
columns: JColumnProps[];
|
||||||
params?: Record<string, any> & {
|
params?: Record<string, any>;
|
||||||
pageSize: number;
|
|
||||||
pageIndex: number;
|
|
||||||
};
|
|
||||||
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
||||||
// actions?: ActionsType[];
|
// actions?: ActionsType[];
|
||||||
noPagination?: boolean;
|
noPagination?: boolean;
|
||||||
|
@ -64,6 +63,8 @@ export interface JTableProps extends TableProps{
|
||||||
*/
|
*/
|
||||||
gridColumns?: number[];
|
gridColumns?: number[];
|
||||||
alertRender?: boolean;
|
alertRender?: boolean;
|
||||||
|
type?: keyof typeof TypeEnum;
|
||||||
|
defaultParams?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JTable = defineComponent<JTableProps>({
|
const JTable = defineComponent<JTableProps>({
|
||||||
|
@ -74,6 +75,7 @@ const JTable = defineComponent<JTableProps>({
|
||||||
],
|
],
|
||||||
emits: [
|
emits: [
|
||||||
'modelChange', // 切换卡片和表格
|
'modelChange', // 切换卡片和表格
|
||||||
|
'reload' // 刷新数据
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
request: {
|
request: {
|
||||||
|
@ -96,10 +98,6 @@ const JTable = defineComponent<JTableProps>({
|
||||||
type: [String, undefined],
|
type: [String, undefined],
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
// actions: {
|
|
||||||
// type: Array as PropType<ActionsType[]>,
|
|
||||||
// default: () => []
|
|
||||||
// },
|
|
||||||
noPagination: {
|
noPagination: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
@ -127,9 +125,22 @@ const JTable = defineComponent<JTableProps>({
|
||||||
alertRender: {
|
alertRender: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'PAGE'
|
||||||
|
},
|
||||||
|
defaultParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as any,
|
} as any,
|
||||||
setup(props: JTableProps ,{ slots, emit }){
|
setup(props: JTableProps ,{ slots, emit, expose }){
|
||||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
|
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
|
||||||
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
|
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
|
||||||
const column = ref<number>(props.gridColumn || 4);
|
const column = ref<number>(props.gridColumn || 4);
|
||||||
|
@ -162,25 +173,36 @@ const JTable = defineComponent<JTableProps>({
|
||||||
const handleSearch = async (_params?: Record<string, any>) => {
|
const handleSearch = async (_params?: Record<string, any>) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if(props.request) {
|
if(props.request) {
|
||||||
const resp = await props.request({
|
const resp = await props.request({
|
||||||
|
pageIndex: 0,
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
pageIndex: 1,
|
...props.defaultParams,
|
||||||
..._params
|
..._params,
|
||||||
|
terms: [
|
||||||
|
...(props.defaultParams?.terms || []),
|
||||||
|
...(_params?.terms || [])
|
||||||
|
]
|
||||||
})
|
})
|
||||||
if(resp.status === 200){
|
if(resp.status === 200){
|
||||||
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
if(props.type === 'PAGE'){
|
||||||
if(resp.result?.data?.length === 0 && resp.result.total && resp.result.pageSize && resp.result.pageIndex) {
|
// 判断如果是最后一页且最后一页为空,就跳转到前一页
|
||||||
handleSearch({
|
if(resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
|
||||||
..._params,
|
handleSearch({
|
||||||
pageSize: pageSize.value,
|
..._params,
|
||||||
pageIndex: pageIndex.value - 1,
|
pageSize: pageSize.value,
|
||||||
})
|
pageIndex: pageIndex.value > 0 ? pageIndex.value - 1 : 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_dataSource.value = resp.result?.data || []
|
||||||
|
pageIndex.value = resp.result?.pageIndex || 0
|
||||||
|
pageSize.value = resp.result?.pageSize || 6
|
||||||
|
total.value = resp.result?.total || 0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_dataSource.value = resp.result?.data || []
|
_dataSource.value = resp?.result || []
|
||||||
pageIndex.value = resp.result?.pageIndex || 0
|
|
||||||
pageSize.value = resp.result?.pageSize || 6
|
|
||||||
total.value = resp.result?.total || 0
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_dataSource.value = []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_dataSource.value = props?.dataSource || []
|
_dataSource.value = props?.dataSource || []
|
||||||
|
@ -188,9 +210,13 @@ const JTable = defineComponent<JTableProps>({
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watch(
|
||||||
handleSearch(props.params)
|
() => props.params,
|
||||||
})
|
(newValue) => {
|
||||||
|
handleSearch(newValue)
|
||||||
|
},
|
||||||
|
{deep: true, immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
|
@ -201,6 +227,23 @@ const JTable = defineComponent<JTableProps>({
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.onresize = null
|
window.onresize = null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新数据
|
||||||
|
* @param _params
|
||||||
|
*/
|
||||||
|
const reload = (_params?: Record<string, any>) => {
|
||||||
|
handleSearch({
|
||||||
|
..._params,
|
||||||
|
pageSize: 12,
|
||||||
|
pageIndex: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出方法
|
||||||
|
*/
|
||||||
|
expose({ reload })
|
||||||
|
|
||||||
return () => <Spin spinning={loading.value}>
|
return () => <Spin spinning={loading.value}>
|
||||||
<div class={styles["jtable-body"]}>
|
<div class={styles["jtable-body"]}>
|
||||||
|
@ -233,7 +276,7 @@ const JTable = defineComponent<JTableProps>({
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
emit('cancelSelect')
|
emit('cancelSelect')
|
||||||
}}
|
}}
|
||||||
closeText={<a>取消选择</a>}
|
closeText={<a-button type="link">取消选择</a-button>}
|
||||||
/>
|
/>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
@ -282,7 +325,7 @@ const JTable = defineComponent<JTableProps>({
|
||||||
</div>
|
</div>
|
||||||
{/* 分页 */}
|
{/* 分页 */}
|
||||||
{
|
{
|
||||||
_dataSource.value.length && !props.noPagination &&
|
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
|
||||||
<div class={styles['jtable-pagination']}>
|
<div class={styles['jtable-pagination']}>
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -292,14 +335,16 @@ const JTable = defineComponent<JTableProps>({
|
||||||
current={pageIndex.value}
|
current={pageIndex.value}
|
||||||
pageSize={pageSize.value}
|
pageSize={pageSize.value}
|
||||||
pageSizeOptions={['12', '24', '48', '60', '100']}
|
pageSizeOptions={['12', '24', '48', '60', '100']}
|
||||||
showTotal={(total, range) => {
|
showTotal={(num) => {
|
||||||
return `第 ${range[0]} - ${range[1]} 条/总共 ${total} 条`
|
const minSize = pageIndex.value * pageSize.value + 1;
|
||||||
|
const MaxSize = (pageIndex.value + 1) * pageSize.value;
|
||||||
|
return `第 ${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num} 条`;
|
||||||
}}
|
}}
|
||||||
onChange={(page, size) => {
|
onChange={(page, size) => {
|
||||||
handleSearch({
|
handleSearch({
|
||||||
...props.params,
|
...props.params,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
pageIndex: pageSize.value === size ? page : 1,
|
pageIndex: pageSize.value === size ? page : 0
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<a-table rowKey="id" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false" :scroll="{ x: 1366 }">
|
<a-table rowKey="id" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false">
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<!-- <template v-if="column.key === 'action'">
|
<!-- <template v-if="column.key === 'action'">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { SearchProps } from 'components/Search/types'
|
||||||
|
import { ColumnType } from 'ant-design-vue/es/table'
|
||||||
|
|
||||||
|
export interface JColumnsProps extends ColumnType{
|
||||||
|
scopedSlots?: boolean;
|
||||||
|
search: SearchProps
|
||||||
|
}
|
|
@ -45,7 +45,7 @@
|
||||||
<template #addonAfter>
|
<template #addonAfter>
|
||||||
<a-upload
|
<a-upload
|
||||||
name="file"
|
name="file"
|
||||||
:action="action"
|
:action="FILE_UPLOAD"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:showUploadList="false"
|
:showUploadList="false"
|
||||||
@change="handleFileChange"
|
@change="handleFileChange"
|
||||||
|
@ -89,6 +89,7 @@ import GeoComponent from '@/components/GeoComponent/index.vue';
|
||||||
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
|
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
|
||||||
import { LocalStore } from '@/utils/comm';
|
import { LocalStore } from '@/utils/comm';
|
||||||
import { ItemData, ITypes } from './types';
|
import { ItemData, ITypes } from './types';
|
||||||
|
import { FILE_UPLOAD } from '@/api/comm';
|
||||||
|
|
||||||
type Emits = {
|
type Emits = {
|
||||||
(e: 'update:modelValue', data: string | number | boolean): void;
|
(e: 'update:modelValue', data: string | number | boolean): void;
|
||||||
|
@ -161,7 +162,6 @@ const handleItemModalSubmit = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 文件上传
|
// 文件上传
|
||||||
const action = ref<string>(`${BASE_API_PATH}/file/static`);
|
|
||||||
const headers = ref({ [TOKEN_KEY]: LocalStore.get(TOKEN_KEY) });
|
const headers = ref({ [TOKEN_KEY]: LocalStore.get(TOKEN_KEY) });
|
||||||
const handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => {
|
const handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => {
|
||||||
if (info.file.status === 'done') {
|
if (info.file.status === 'done') {
|
||||||
|
|
|
@ -6,6 +6,9 @@ import TitleComponent from "./TitleComponent/index.vue";
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import CardBox from './CardBox/index.vue';
|
import CardBox from './CardBox/index.vue';
|
||||||
import Search from './Search'
|
import Search from './Search'
|
||||||
|
import NormalUpload from './NormalUpload/index.vue'
|
||||||
|
import FileFormat from './FileFormat/index.vue'
|
||||||
|
import JUpload from './JUpload/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
|
@ -16,5 +19,8 @@ export default {
|
||||||
.component('Form', Form)
|
.component('Form', Form)
|
||||||
.component('CardBox', CardBox)
|
.component('CardBox', CardBox)
|
||||||
.component('Search', Search)
|
.component('Search', Search)
|
||||||
|
.component('NormalUpload', NormalUpload)
|
||||||
|
.component('FileFormat', FileFormat)
|
||||||
|
.component('JUpload', JUpload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ const filterPath = [
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const token = LocalStore.get(TOKEN_KEY)
|
const token = LocalStore.get(TOKEN_KEY)
|
||||||
|
// TODO 切换路由取消请求
|
||||||
if (token || filterPath.includes(to.path)) {
|
if (token || filterPath.includes(to.path)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -66,6 +66,11 @@ export default [
|
||||||
},
|
},
|
||||||
// end: 测试用, 可删除
|
// end: 测试用, 可删除
|
||||||
|
|
||||||
|
// 设备管理
|
||||||
|
{
|
||||||
|
path: '/device/Instance',
|
||||||
|
component: () => import('@/views/device/Instance/index.vue')
|
||||||
|
},
|
||||||
// link 运维管理
|
// link 运维管理
|
||||||
{
|
{
|
||||||
path: '/link/log',
|
path: '/link/log',
|
||||||
|
@ -88,9 +93,21 @@ export default [
|
||||||
component: () => import('@/views/link/AccessConfig/Detail/index.vue')
|
component: () => import('@/views/link/AccessConfig/Detail/index.vue')
|
||||||
},
|
},
|
||||||
// system 系统管理
|
// system 系统管理
|
||||||
|
{
|
||||||
|
path:'/system/Basis',
|
||||||
|
component: ()=>import('@/views/system/Basis/index.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path:'/system/api',
|
path:'/system/api',
|
||||||
components: ()=>import('@/views/system/apiPage/index')
|
component: ()=>import('@/views/system/apiPage/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/system/Role',
|
||||||
|
component: ()=>import('@/views/system/Role/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/system/Role/detail/:id',
|
||||||
|
component: ()=>import('@/views/system/Role/Detail/index.vue')
|
||||||
},
|
},
|
||||||
// 初始化
|
// 初始化
|
||||||
{
|
{
|
||||||
|
@ -102,4 +119,13 @@ export default [
|
||||||
path: '/iot-card/home',
|
path: '/iot-card/home',
|
||||||
component: () => import('@/views/iot-card/Home/index.vue')
|
component: () => import('@/views/iot-card/Home/index.vue')
|
||||||
},
|
},
|
||||||
|
// 北向输出
|
||||||
|
{
|
||||||
|
path: '/northbound/DuerOS',
|
||||||
|
component: () => import('@/views/northbound/DuerOS/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/northbound/AliCloud',
|
||||||
|
component: () => import('@/views/northbound/AliCloud/index.vue')
|
||||||
|
},
|
||||||
]
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore({
|
||||||
|
id: 'menu',
|
||||||
|
state: () => ({
|
||||||
|
menus: {} as {[key: string]: string},
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
hasPermission(state) {
|
||||||
|
return (menuCode: string | string[]) => {
|
||||||
|
if (!menuCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!!Object.keys(state.menus).length) {
|
||||||
|
if (typeof menuCode === 'string') {
|
||||||
|
return !!this.menus[menuCode]
|
||||||
|
}
|
||||||
|
return menuCode.some(code => !!this.menus[code])
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { TOKEN_KEY } from '@/utils/variable'
|
||||||
|
import { Terms } from 'components/Search/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态图片资源处理
|
* 静态图片资源处理
|
||||||
* @param path {String} 路径
|
* @param path {String} 路径
|
||||||
|
@ -29,4 +32,28 @@ export const LocalStore = {
|
||||||
removeAll() {
|
removeAll() {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getToken = () => {
|
||||||
|
return LocalStore.get(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TreeSelect过滤
|
||||||
|
* @param value 过滤值
|
||||||
|
* @param treeNode
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export const filterTreeSelectNode = (value: string, treeNode: any, key: string = 'name'): boolean => {
|
||||||
|
return treeNode[key]?.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select过滤
|
||||||
|
* @param value 过滤值
|
||||||
|
* @param option
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
export const filterSelectNode = (value: string, option: any, key: string = 'label'): boolean => {
|
||||||
|
return option[key]?.includes(value)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
export default function encodeQuery(params: any) {
|
||||||
|
if (!params) return {};
|
||||||
|
const queryParam = {
|
||||||
|
// pageIndex: 0,
|
||||||
|
current: params.current,
|
||||||
|
};
|
||||||
|
const { terms, sorts } = params;
|
||||||
|
Object.keys(params).forEach((key: string) => {
|
||||||
|
if (key === 'terms') {
|
||||||
|
let index = 0;
|
||||||
|
if (!terms) return;
|
||||||
|
Object.keys(terms).forEach((k: string) => {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
terms[k] === '' ||
|
||||||
|
terms[k] === undefined ||
|
||||||
|
terms[k].length === 0 ||
|
||||||
|
terms[k] === {} ||
|
||||||
|
terms[k] === null
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (k.indexOf('$LIKE') > -1 && terms[k].toString().indexOf('%') === -1) {
|
||||||
|
terms[k] = `%${terms[k]}%`;
|
||||||
|
}
|
||||||
|
if (k.indexOf('$IN') > -1) {
|
||||||
|
terms[k] = terms[k].toString();
|
||||||
|
} else if (k.indexOf('$START') > -1) {
|
||||||
|
terms[k] = `%${terms[k]}`;
|
||||||
|
} else if (k.indexOf('$END') > -1) {
|
||||||
|
terms[k] = `${terms[k]}%`;
|
||||||
|
}
|
||||||
|
if (k.indexOf('@') > -1) {
|
||||||
|
const temp = k.split('@');
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
queryParam[`terms[${index}].column`] = temp[0];
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
queryParam[`terms[${index}].type`] = temp[1];
|
||||||
|
} else {
|
||||||
|
queryParam[`terms[${index}].column`] = k;
|
||||||
|
}
|
||||||
|
queryParam[`terms[${index}].value`] = terms[k];
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (key === 'sorts') {
|
||||||
|
// 当前Ant Design排序只支持单字段排序
|
||||||
|
if (!sorts) return;
|
||||||
|
Object.keys(sorts).forEach((s, index) => {
|
||||||
|
queryParam[`sorts[${index}].name`] = s;
|
||||||
|
queryParam[`sorts[${index}].order`] = sorts[s].replace('end', '');
|
||||||
|
});
|
||||||
|
// if (Object.keys(sorts).length > 0) {
|
||||||
|
// queryParam[`sorts[0].name`] = sorts.field;
|
||||||
|
// queryParam[`sorts[0].order`] = (sorts.order || '').replace('end', '');
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
queryParam[key] = params[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// queryParam.pageIndex = current - 1;
|
||||||
|
|
||||||
|
return queryParam;
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
|
import moment from "moment";
|
||||||
|
import { LocalStore } from "./comm";
|
||||||
|
import { TOKEN_KEY } from "./variable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把数据下载成JSON
|
* 把数据下载成JSON
|
||||||
* @param record
|
* @param record
|
||||||
|
@ -18,4 +22,34 @@ export const downloadObject = (record: Record<string, any>, fileName: string, fo
|
||||||
ghostLink.click();
|
ghostLink.click();
|
||||||
//移除
|
//移除
|
||||||
document.body.removeChild(ghostLink);
|
document.body.removeChild(ghostLink);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
* @param url 下载链接
|
||||||
|
* @param params 参数
|
||||||
|
*/
|
||||||
|
export const downloadFile = (url: string, params?: Record<string, any>) => {
|
||||||
|
const formElement = document.createElement('form');
|
||||||
|
formElement.style.display = 'display:none;';
|
||||||
|
formElement.method = 'GET';
|
||||||
|
formElement.action = url;
|
||||||
|
// 添加参数
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach((key: string) => {
|
||||||
|
const inputElement = document.createElement('input');
|
||||||
|
inputElement.type = 'hidden';
|
||||||
|
inputElement.name = key;
|
||||||
|
inputElement.value = params[key];
|
||||||
|
formElement.appendChild(inputElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const inputElement = document.createElement('input');
|
||||||
|
inputElement.type = 'hidden';
|
||||||
|
inputElement.name = ':X_Access_Token';
|
||||||
|
inputElement.value = LocalStore.get(TOKEN_KEY);
|
||||||
|
formElement.appendChild(inputElement);
|
||||||
|
document.body.appendChild(formElement);
|
||||||
|
formElement.submit();
|
||||||
|
document.body.removeChild(formElement);
|
||||||
};
|
};
|
|
@ -3,5 +3,3 @@ export const BASE_API_PATH = import.meta.env.VITE_APP_BASE_API
|
||||||
export const TOKEN_KEY = 'X-Access-Token'
|
export const TOKEN_KEY = 'X-Access-Token'
|
||||||
|
|
||||||
export const Version_Code = 'version_code'
|
export const Version_Code = 'version_code'
|
||||||
|
|
||||||
export const NETWORK_CERTIFICATE_UPLOAD = '/network/certificate/upload'
|
|
|
@ -1,11 +1,111 @@
|
||||||
<template>
|
<template>
|
||||||
<div class='search'>
|
<div class='search'>
|
||||||
<Search />
|
<Search
|
||||||
|
:columns='columns'
|
||||||
|
target='device-instance-search'
|
||||||
|
@search='search'
|
||||||
|
/>
|
||||||
|
<Search
|
||||||
|
type='simple'
|
||||||
|
:columns='columns'
|
||||||
|
target='product'
|
||||||
|
@search='search'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup name='demoSearch'>
|
||||||
|
|
||||||
|
import { category } from '../../api/device/product'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
search: {
|
||||||
|
rename: 'deviceId',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '测试1',
|
||||||
|
value: 'test1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试2',
|
||||||
|
value: 'test2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试3',
|
||||||
|
value: 'test3'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
handleValue: (v) => {
|
||||||
|
return '123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
dataIndex: 'sortIndex',
|
||||||
|
key: 'sortIndex',
|
||||||
|
scopedSlots: true,
|
||||||
|
search: {
|
||||||
|
type: 'number',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'date',
|
||||||
|
key: 'date',
|
||||||
|
search: {
|
||||||
|
type: 'date',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间2',
|
||||||
|
dataIndex: 'date2',
|
||||||
|
key: 'date2',
|
||||||
|
search: {
|
||||||
|
type: 'time',
|
||||||
|
defaultTermType: 'lt'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'classifiedName',
|
||||||
|
key: 'classifiedName',
|
||||||
|
search: {
|
||||||
|
first: true,
|
||||||
|
type: 'treeSelect',
|
||||||
|
options: async () => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
category().then(resp => {
|
||||||
|
res(resp.result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 250,
|
||||||
|
scopedSlots: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const search = (params) => {
|
||||||
|
console.log(params)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
@cancelSelect="cancelSelect"
|
@cancelSelect="cancelSelect"
|
||||||
>
|
>
|
||||||
<template #headerTitle>
|
<template #headerTitle>
|
||||||
<a-button type="primary">新增</a-button>
|
<a-button type="primary" @click="add">新增</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template #card="slotProps">
|
<template #card="slotProps">
|
||||||
<CardBox
|
<CardBox
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</template>
|
</template>
|
||||||
<!-- <template #actions="item">
|
<template #actions="item">
|
||||||
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||||
<a-button :disabled="item.disabled">
|
<a-button :disabled="item.disabled">
|
||||||
<DeleteOutlined v-if="item.key === 'delete'" />
|
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template> -->
|
</template>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</template>
|
</template>
|
||||||
<template #id="slotProps">
|
<template #id="slotProps">
|
||||||
|
@ -83,8 +83,10 @@ import server from "@/utils/request";
|
||||||
import type { ActionsType } from '@/components/Table/index.vue'
|
import type { ActionsType } from '@/components/Table/index.vue'
|
||||||
import { getImage } from '@/utils/comm';
|
import { getImage } from '@/utils/comm';
|
||||||
import { DeleteOutlined } from '@ant-design/icons-vue'
|
import { DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
const request = (data: any) => server.post(`/device-product/_query`, data)
|
const request = (data: any) => server.post(`/device-product/_query`, data)
|
||||||
|
// const request = (data: any) => server.post(`/device/category/_tree`, {paging: false})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
@ -152,26 +154,26 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
title: '导入'
|
title: '导入'
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
icon: 'icon-xiazai'
|
icon: 'icon-xiazai'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
// disabled: true,
|
|
||||||
text: "删除",
|
text: "删除",
|
||||||
disabled: !!data?.state,
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
||||||
},
|
},
|
||||||
// popConfirm: {
|
popConfirm: {
|
||||||
// title: '确认删除?'
|
title: '确认删除?'
|
||||||
// },
|
},
|
||||||
|
|
||||||
icon: 'icon-huishouzhan'
|
icon: 'icon-huishouzhan'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = h('p', 'hi')
|
const add = () => {
|
||||||
|
message.warn('123')
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :maskClosable="false" width="800px" :visible="true" title="导出" @ok="handleOk" @cancel="handleCancel">
|
||||||
|
<div style="background-color: rgb(236, 237, 238)">
|
||||||
|
<p style="padding: 10px">
|
||||||
|
<AIcon type="ExclamationCircleOutlined" />
|
||||||
|
选择单个产品时可导出其下属设备的详细数据,不选择产品时导出所有设备的基础数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
<a-form :layout="'vertical'">
|
||||||
|
<a-form-item label="产品">
|
||||||
|
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
|
||||||
|
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="文件格式">
|
||||||
|
<a-radio-group button-style="solid" v-model:value="modelRef.fileType" placeholder="请选择文件格式">
|
||||||
|
<a-radio-button value="xlsx">xlsx</a-radio-button>
|
||||||
|
<a-radio-button value="csv">csv</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { queryNoPagingPost } from '@/api/device/product'
|
||||||
|
import { downloadFile } from '@/utils/utils'
|
||||||
|
import encodeQuery from '@/utils/encodeQuery'
|
||||||
|
import { BASE_API_PATH } from '@/utils/variable'
|
||||||
|
import { deviceExport } from '@/api/device/instance'
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const modelRef = reactive({
|
||||||
|
product: undefined,
|
||||||
|
fileType: 'xlsx'
|
||||||
|
});
|
||||||
|
|
||||||
|
const productList = ref<Record<string, any>[]>([])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
queryNoPagingPost({paging: false}).then(resp => {
|
||||||
|
if(resp.status === 200){
|
||||||
|
productList.value = resp.result as Record<string, any>[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{immediate: true, deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
const params = encodeQuery(props.data);
|
||||||
|
downloadFile(deviceExport(modelRef.product || "", modelRef.fileType),params);
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :maskClosable="false" width="800px" :visible="true" title="导入" @ok="handleCancel" @cancel="handleCancel">
|
||||||
|
<div style="margin-top: 10px">
|
||||||
|
<a-form :layout="'vertical'">
|
||||||
|
<a-row>
|
||||||
|
<a-col span="24">
|
||||||
|
<a-form-item label="产品" required>
|
||||||
|
<a-select showSearch v-model:value="modelRef.product" placeholder="请选择产品">
|
||||||
|
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col span="24">
|
||||||
|
<a-form-item label="文件格式" v-if="modelRef.product">
|
||||||
|
<FileFormat v-model="modelRef.file" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col span="12">
|
||||||
|
<a-form-item label="文件上传" v-if="modelRef.product">
|
||||||
|
<NormalUpload :product="modelRef.product" v-model="modelRef.upload" :file="modelRef.file" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { queryNoPagingPost } from '@/api/device/product'
|
||||||
|
import { Form } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const productList = ref<Record<string, any>[]>([])
|
||||||
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
const modelRef = reactive({
|
||||||
|
product: undefined,
|
||||||
|
upload: [],
|
||||||
|
file: {
|
||||||
|
fileType: 'xlsx',
|
||||||
|
autoDeploy: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
queryNoPagingPost({paging: false}).then(resp => {
|
||||||
|
if(resp.status === 200){
|
||||||
|
productList.value = resp.result as Record<string, any>[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{immediate: true, deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :maskClosable="false" width="800px" :visible="true" title="当前进度" @ok="handleCancel" @cancel="handleCancel">
|
||||||
|
<div>
|
||||||
|
<a-badge v-if="flag" status="processing" text="进行中" />
|
||||||
|
<a-badge v-else status="success" text="已完成" />
|
||||||
|
</div>
|
||||||
|
<p>总数量:{{count}}</p>
|
||||||
|
<a style="color: red">{{errMessage}}</a>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
const props = defineProps({
|
||||||
|
api: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const eventSource = ref<Record<string, any>>({})
|
||||||
|
const count = ref<number>(0)
|
||||||
|
const flag = ref<boolean>(false)
|
||||||
|
const errMessage = ref<string>('')
|
||||||
|
const isSource = ref<boolean>(false)
|
||||||
|
const id = ref<string>('')
|
||||||
|
const source = ref<Record<string, any>>({})
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getData = (api: string) => {
|
||||||
|
let dt = 0
|
||||||
|
const _source = new EventSourcePolyfill(api)
|
||||||
|
source.value = _source
|
||||||
|
_source.onmessage = (e: any) => {
|
||||||
|
const res = JSON.parse(e.data);
|
||||||
|
switch (props.type) {
|
||||||
|
case 'active':
|
||||||
|
if (res.success) {
|
||||||
|
dt += res.total;
|
||||||
|
count.value = dt
|
||||||
|
} else {
|
||||||
|
if (res.source) {
|
||||||
|
const msg = `${res.source.name}: ${res.message}`;
|
||||||
|
errMessage.value = msg
|
||||||
|
id.value = res.source.id
|
||||||
|
isSource.value = true
|
||||||
|
} else {
|
||||||
|
errMessage.value = res.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'sync':
|
||||||
|
dt += res;
|
||||||
|
count.value = dt
|
||||||
|
break;
|
||||||
|
case 'import':
|
||||||
|
if (res.success) {
|
||||||
|
const temp = res.result.total;
|
||||||
|
dt += temp;
|
||||||
|
count.value = dt
|
||||||
|
} else {
|
||||||
|
errMessage.value = res.message
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_source.onerror = () => {
|
||||||
|
flag.value = false
|
||||||
|
_source.close();
|
||||||
|
};
|
||||||
|
_source.onopen = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.api,
|
||||||
|
(newValue) => {
|
||||||
|
if(newValue) {
|
||||||
|
getData(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{deep: true, immediate: true}
|
||||||
|
)
|
||||||
|
</script>
|
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :maskClosable="false" width="650px" :visible="true" title="新增" @ok="handleCancel" @cancel="handleCancel">
|
||||||
|
<div style="margin-top: 10px">
|
||||||
|
<a-form :layout="'vertical'">
|
||||||
|
<a-row type="flex">
|
||||||
|
<a-col flex="180px">
|
||||||
|
<a-form-item required>
|
||||||
|
<JUpload />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col flex="auto">
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input v-model:value="modelRef.id" placeholder="请输入ID" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="名称" required>
|
||||||
|
<a-input v-model:value="modelRef.name" placeholder="请输入名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item label="产品" required>
|
||||||
|
<a-select showSearch v-model:value="modelRef.productId" placeholder="请选择产品">
|
||||||
|
<a-select-option :value="item.id" v-for="item in productList" :key="item.id" :title="item.name"></a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="说明">
|
||||||
|
<a-textarea v-model:value="modelRef.describe" placeholder="请输入说明" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { queryNoPagingPost } from '@/api/device/product'
|
||||||
|
import { Form } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'save'])
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const productList = ref<Record<string, any>[]>([])
|
||||||
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
const modelRef = reactive({
|
||||||
|
productId: undefined,
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
describe: '',
|
||||||
|
photoUrl: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
queryNoPagingPost({paging: false}).then(resp => {
|
||||||
|
if(resp.status === 200){
|
||||||
|
productList.value = resp.result as Record<string, any>[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{immediate: true, deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,406 @@
|
||||||
|
<template>
|
||||||
|
<JTable
|
||||||
|
ref="instanceRef"
|
||||||
|
:columns="columns"
|
||||||
|
:request="query"
|
||||||
|
:defaultParams="{sorts: [{name: 'createTime', order: 'desc'}]}"
|
||||||
|
:rowSelection="{
|
||||||
|
selectedRowKeys: _selectedRowKeys,
|
||||||
|
onChange: onSelectChange
|
||||||
|
}"
|
||||||
|
@cancelSelect="cancelSelect"
|
||||||
|
:params="params"
|
||||||
|
>
|
||||||
|
<template #headerTitle>
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="handleAdd">新增</a-button>
|
||||||
|
<a-dropdown>
|
||||||
|
<a-button>批量操作 <AIcon type="DownOutlined" /></a-button>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-button @click="exportVisible = true"><AIcon type="ExportOutlined" />批量导出设备</a-button>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-button @click="importVisible = true"><AIcon type="ImportOutlined" />批量导入设备</a-button>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-popconfirm @confirm="activeAllDevice" title="确认激活全部设备?">
|
||||||
|
<a-button type="primary" ghost><AIcon type="CheckCircleOutlined" />激活全部设备</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item>
|
||||||
|
<a-button @click="syncDeviceStatus" type="primary"><AIcon type="SyncOutlined" />同步设备状态</a-button>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item v-if="_selectedRowKeys.length">
|
||||||
|
<a-popconfirm @confirm="delSelectedDevice" title="已启用的设备无法删除,确认删除选中的禁用状态设备?">
|
||||||
|
<a-button type="primary" danger><AIcon type="DeleteOutlined" />删除选中设备</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item v-if="_selectedRowKeys.length" title="确认激活选中设备?">
|
||||||
|
<a-popconfirm @confirm="activeSelectedDevice" >
|
||||||
|
<a-button type="primary"><AIcon type="CheckOutlined" />激活选中设备</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item v-if="_selectedRowKeys.length">
|
||||||
|
<a-popconfirm @confirm="disabledSelectedDevice" title="确认禁用选中设备?">
|
||||||
|
<a-button type="primary" danger><AIcon type="StopOutlined" />禁用选中设备</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #card="slotProps">
|
||||||
|
<CardBox
|
||||||
|
:value="slotProps"
|
||||||
|
@click="handleClick"
|
||||||
|
:actions="getActions(slotProps, 'card')"
|
||||||
|
v-bind="slotProps"
|
||||||
|
:active="_selectedRowKeys.includes(slotProps.id)"
|
||||||
|
:status="slotProps.state.value"
|
||||||
|
:statusText="slotProps.state.text"
|
||||||
|
:statusNames="{
|
||||||
|
online: 'success',
|
||||||
|
offline: 'error',
|
||||||
|
notActive: 'warning',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #img>
|
||||||
|
<slot name="img">
|
||||||
|
<img :src="getImage('/device/instance/device-card.png')" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<h3 class="card-item-content-title" @click.stop="handleView(slotProps.id)">{{ slotProps.name }}</h3>
|
||||||
|
<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>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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.text" :status="statusMap.get(slotProps.state.value)" />
|
||||||
|
</template>
|
||||||
|
<template #action="slotProps">
|
||||||
|
<a-space :size="16">
|
||||||
|
<a-tooltip
|
||||||
|
v-for="i in getActions(slotProps, 'table')"
|
||||||
|
:key="i.key"
|
||||||
|
v-bind="i.tooltip"
|
||||||
|
>
|
||||||
|
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm" :disabled="i.disabled">
|
||||||
|
<a-button
|
||||||
|
:disabled="i.disabled"
|
||||||
|
style="padding: 0"
|
||||||
|
type="link"
|
||||||
|
><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>
|
||||||
|
<Import v-if="importVisible" @close="importVisible = false" />
|
||||||
|
<Export v-if="exportVisible" @close="exportVisible = false" :data="params" />
|
||||||
|
<Process v-if="operationVisible" @close="operationVisible = false" :api="api" :type="type" />
|
||||||
|
<Save v-if="visible" :data="current" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { query, _delete, _deploy, _undeploy, batchUndeployDevice, batchDeployDevice, batchDeleteDevice } from '@/api/device/instance'
|
||||||
|
import type { ActionsType } from '@/components/Table/index.vue'
|
||||||
|
import { getImage, LocalStore } from '@/utils/comm';
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import Import from './Import/index.vue'
|
||||||
|
import Export from './Export/index.vue'
|
||||||
|
import Process from './Process/index.vue'
|
||||||
|
import Save from './Save/index.vue'
|
||||||
|
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
|
||||||
|
|
||||||
|
const instanceRef = ref<Record<string, any>>({});
|
||||||
|
const params = ref<Record<string, any>>({})
|
||||||
|
const _selectedRowKeys = ref<string[]>([])
|
||||||
|
const importVisible = ref<boolean>(false)
|
||||||
|
const exportVisible = ref<boolean>(false)
|
||||||
|
const visible = ref<boolean>(false)
|
||||||
|
const current = ref<Record<string, any>>({})
|
||||||
|
const operationVisible = ref<boolean>(false)
|
||||||
|
const api = ref<string>('')
|
||||||
|
const type = ref<string>('')
|
||||||
|
|
||||||
|
const statusMap = new Map();
|
||||||
|
statusMap.set('online', 'processing');
|
||||||
|
statusMap.set('offline', 'error');
|
||||||
|
statusMap.set('notActive', 'warning');
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设备名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品名称',
|
||||||
|
dataIndex: 'productName',
|
||||||
|
key: 'productName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
scopedSlots: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'state',
|
||||||
|
key: 'state',
|
||||||
|
scopedSlots: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '说明',
|
||||||
|
dataIndex: 'describe',
|
||||||
|
key: 'describe'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 250,
|
||||||
|
scopedSlots: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const paramsFormat = (config: Record<string, any>, _terms: Record<string, any>, name?: string) => {
|
||||||
|
if (config?.terms && Array.isArray(config.terms) && config?.terms.length > 0) {
|
||||||
|
(config?.terms || []).map((item: Record<string, any>, index: number) => {
|
||||||
|
if (item?.type) {
|
||||||
|
_terms[`${name ? `${name}.` : ''}terms[${index}].type`] = item.type;
|
||||||
|
}
|
||||||
|
paramsFormat(item, _terms, `${name ? `${name}.` : ''}terms[${index}]`);
|
||||||
|
});
|
||||||
|
} else if (!config?.terms && Object.keys(config).length > 0) {
|
||||||
|
Object.keys(config).forEach((key) => {
|
||||||
|
if (config[key]) {
|
||||||
|
_terms[`${name ? `${name}.` : ''}${key}`] = config[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleParams = (config: Record<string, any>) => {
|
||||||
|
const _terms: Record<string, any> = {};
|
||||||
|
paramsFormat(config, _terms);
|
||||||
|
if(Object.keys(_terms._value).length && Object.keys(_terms).length) {
|
||||||
|
const url = new URLSearchParams();
|
||||||
|
Object.keys(_terms).forEach((key) => {
|
||||||
|
url.append(key, _terms[key]);
|
||||||
|
});
|
||||||
|
return url.toString();
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增
|
||||||
|
*/
|
||||||
|
const handleAdd = () => {
|
||||||
|
visible.value = true
|
||||||
|
current.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
*/
|
||||||
|
const handleView = (id: string) => {
|
||||||
|
message.warn(id + '暂未开发')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActions = (data: Partial<Record<string, any>>, type: 'card' | 'table'): ActionsType[] => {
|
||||||
|
if(!data) return []
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
key: 'view',
|
||||||
|
text: "查看",
|
||||||
|
tooltip: {
|
||||||
|
title: '查看'
|
||||||
|
},
|
||||||
|
icon: 'EyeOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
handleView(data.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
text: "编辑",
|
||||||
|
tooltip: {
|
||||||
|
title: '编辑'
|
||||||
|
},
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
onClick: () => {
|
||||||
|
visible.value = true
|
||||||
|
current.value = data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'action',
|
||||||
|
text: data.state.value !== 'notActive' ? "禁用" : "启用",
|
||||||
|
tooltip: {
|
||||||
|
title: data.state.value !== 'notActive' ? "禁用" : "启用",
|
||||||
|
},
|
||||||
|
icon: data.state.value !== 'notActive' ? 'StopOutlined' : 'CheckCircleOutlined',
|
||||||
|
popConfirm: {
|
||||||
|
title: `确认${data.state.value !== 'notActive' ? "禁用" : "启用"}?`,
|
||||||
|
onConfirm: async () => {
|
||||||
|
let response = undefined
|
||||||
|
if(data.state.value !== 'notActive') {
|
||||||
|
response = await _undeploy(data.id)
|
||||||
|
} else {
|
||||||
|
response = await _deploy(data.id)
|
||||||
|
}
|
||||||
|
if(response && response.status === 200) {
|
||||||
|
message.success('操作成功!')
|
||||||
|
instanceRef.value?.reload()
|
||||||
|
} else {
|
||||||
|
message.error('操作失败!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
text: "删除",
|
||||||
|
disabled: data.state.value !== 'notActive',
|
||||||
|
tooltip: {
|
||||||
|
title: data.state.value !== 'notActive' ? '已启用的设备不能删除' : '删除'
|
||||||
|
},
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除?',
|
||||||
|
onConfirm: async () => {
|
||||||
|
const resp = await _delete(data.id)
|
||||||
|
if(resp.status === 200) {
|
||||||
|
message.success('操作成功!')
|
||||||
|
instanceRef.value?.reload()
|
||||||
|
} else {
|
||||||
|
message.error('操作失败!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: 'DeleteOutlined'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if(type === 'card') return actions.filter((i: ActionsType) => i.key !== 'view')
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectChange = (keys: string[]) => {
|
||||||
|
_selectedRowKeys.value = [...keys]
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelSelect = () => {
|
||||||
|
_selectedRowKeys.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (dt: any) => {
|
||||||
|
if(_selectedRowKeys.value.includes(dt.id)) {
|
||||||
|
const _index = _selectedRowKeys.value.findIndex(i => i === dt.id)
|
||||||
|
_selectedRowKeys.value.splice(_index, 1)
|
||||||
|
} else {
|
||||||
|
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAllDevice = () => {
|
||||||
|
type.value = 'active'
|
||||||
|
const activeAPI = `${BASE_API_PATH}/device-instance/deploy?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
|
||||||
|
api.value = activeAPI
|
||||||
|
operationVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDeviceStatus = () => {
|
||||||
|
type.value = 'sync'
|
||||||
|
const syncAPI = `${BASE_API_PATH}/device-instance/state/_sync?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}&${handleParams(params)}`;
|
||||||
|
api.value = syncAPI
|
||||||
|
operationVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const delSelectedDevice = async () => {
|
||||||
|
const resp = await batchDeleteDevice(_selectedRowKeys.value)
|
||||||
|
if(resp.status === 200){
|
||||||
|
message.success('操作成功!')
|
||||||
|
_selectedRowKeys.value = []
|
||||||
|
instanceRef.value?.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSelectedDevice = async () => {
|
||||||
|
const resp = await batchDeployDevice(_selectedRowKeys.value)
|
||||||
|
if(resp.status === 200){
|
||||||
|
message.success('操作成功!')
|
||||||
|
_selectedRowKeys.value = []
|
||||||
|
instanceRef.value?.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledSelectedDevice = async () => {
|
||||||
|
const resp = await batchUndeployDevice(_selectedRowKeys.value)
|
||||||
|
if(resp.status === 200){
|
||||||
|
message.success('操作成功!')
|
||||||
|
_selectedRowKeys.value = []
|
||||||
|
instanceRef.value?.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,158 @@
|
||||||
|
<template>
|
||||||
|
<a-drawer :mask-closable="false" title="查看物模型" width="700" v-model:visible="_visible" destroy-on-close @close="close">
|
||||||
|
<template #extra>
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="handleExport">
|
||||||
|
导出
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="cat-content">
|
||||||
|
<p class="cat-tip">
|
||||||
|
物模型是对设备在云端的功能描述,包括设备的属性、服务和事件。物联网平台通过定义一种物的描述语言来描述物模型,称之为
|
||||||
|
TSL(即 Thing Specification Language),采用 JSON 格式,您可以根据 TSL
|
||||||
|
组装上报设备的数据。您可以导出完整物模型,用于云端应用开发。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a-tabs @change="handleConvertMetadata">
|
||||||
|
<a-tab-pane v-for="item in codecs" :tab-key="item.id" :key="item.id">
|
||||||
|
<div class="cat-panel">
|
||||||
|
<!-- TODO 代码编辑器 -->
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts" name="Cat">
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import { downloadObject } from '@/utils/utils'
|
||||||
|
import { useInstanceStore } from '@/store/instance';
|
||||||
|
import { useProductStore } from '@/store/product';
|
||||||
|
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||||
|
import { convertMetadata, getCodecs, detail as productDetail } from '@/api/device/product';
|
||||||
|
import { detail } from '@/api/device/instance'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
type: 'product' | 'device';
|
||||||
|
}
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', data: boolean): void;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emits = defineEmits<Emits>()
|
||||||
|
const route = useRoute()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const _visible = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.visible;
|
||||||
|
},
|
||||||
|
set: (val: any) => {
|
||||||
|
emits('update:visible', val);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emits('update:visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceStore = useInstanceStore()
|
||||||
|
const productStore = useProductStore()
|
||||||
|
const metadataMap = {
|
||||||
|
product: productStore.current?.metadata as string,
|
||||||
|
device: instanceStore.current?.metadata as string,
|
||||||
|
};
|
||||||
|
const metadata = metadataMap[props.type];
|
||||||
|
const value = ref(metadata)
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
downloadObject(
|
||||||
|
JSON.parse(value.value),
|
||||||
|
`${props.type === 'device'
|
||||||
|
? instanceStore.current?.name
|
||||||
|
: productStore.current?.name
|
||||||
|
}-物模型`,
|
||||||
|
'YYYY/MM/DD',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
message.error('请先配置物模型');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConvertMetadata = (key: Key) => {
|
||||||
|
if (key === 'alink') {
|
||||||
|
value.value = '';
|
||||||
|
if (metadata) {
|
||||||
|
convertMetadata('to', 'alink', JSON.parse(metadata)).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
value.value = JSON.stringify(res.result)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value.value = metadata;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const codecs = ref<{ id: string; name: string }[]>()
|
||||||
|
|
||||||
|
const routeChange = async (id: string) => {
|
||||||
|
const res = await getCodecs()
|
||||||
|
if (res.status === 200) {
|
||||||
|
codecs.value = [{ id: 'jetlinks', name: 'jetlinks' }].concat(res.result)
|
||||||
|
}
|
||||||
|
if (props.type === 'device' && id) {
|
||||||
|
detail(id as string).then((resp) => {
|
||||||
|
if (resp.status === 200) {
|
||||||
|
instanceStore.setCurrent(resp.result);
|
||||||
|
const _metadata = resp.result?.metadata;
|
||||||
|
value.value = _metadata;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
(id) => routeChange(id as string),
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.visible) {
|
||||||
|
loading.value = true
|
||||||
|
const { id } = route.params
|
||||||
|
if (props.type === 'device') {
|
||||||
|
detail(id as string).then((resp) => {
|
||||||
|
loading.value = false
|
||||||
|
instanceStore.setCurrent(resp.result)
|
||||||
|
value.value = resp.result.metadata
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
productDetail(id as string).then((resp) => {
|
||||||
|
loading.value = false
|
||||||
|
// productStore.setCurrent(resp.result)
|
||||||
|
value.value = resp.result.metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.cat-content {
|
||||||
|
background: #F6F6F6;
|
||||||
|
|
||||||
|
.cat-tip {
|
||||||
|
padding: 10px;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-panel {
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
height: 670px;
|
||||||
|
width: 650px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,262 @@
|
||||||
|
<template>
|
||||||
|
<a-modal :mask-closable="false" title="导入物模型" destroy-on-close v-model:visible="_visible" @cancel="close"
|
||||||
|
@ok="handleImport" :confirm-loading="loading">
|
||||||
|
<div class="import-content">
|
||||||
|
<p class="import-tip">
|
||||||
|
<exclamation-circle-outlined style="margin-right: 5px" />
|
||||||
|
导入的物模型会覆盖原来的属性、功能、事件、标签,请谨慎操作。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a-form layout="vertical" v-model="formModel">
|
||||||
|
<a-form-item label="导入方式" v-bind="validateInfos.type">
|
||||||
|
<a-select v-if="type === 'product'" v-model:value="formModel.type">
|
||||||
|
<a-select-option value="copy">拷贝产品</a-select-option>
|
||||||
|
<a-select-option value="import">导入物模型</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="选择产品" v-bind="validateInfos.copy" v-if="formModel.type === 'copy'">
|
||||||
|
<a-select :options="productList" v-model:value="formModel.copy" option-filter-prop="label"></a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="物模型类型" v-bind="validateInfos.metadata"
|
||||||
|
v-if="type === 'device' || formModel.type === 'import'">
|
||||||
|
<a-select v-model:value="formModel.metadata">
|
||||||
|
<a-select-option value="jetlinks">Jetlinks物模型</a-select-option>
|
||||||
|
<a-select-option value="alink">阿里云物模型TSL</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="导入类型" v-bind="validateInfos.metadataType"
|
||||||
|
v-if="type === 'device' || formModel.type === 'import'">
|
||||||
|
<a-select v-model:value="formModel.metadataType">
|
||||||
|
<a-select-option value="file">文件上传</a-select-option>
|
||||||
|
<a-select-option value="script">脚本</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="文件上传" v-bind="validateInfos.upload" v-if="formModel.metadataType === 'file'">
|
||||||
|
<a-upload v-model:file-list="formModel.upload" name="files" :before-upload="beforeUpload" accept=".json"
|
||||||
|
:show-upload-list="false"></a-upload>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="物模型" v-bind="validateInfos.import" v-if="formModel.metadataType === 'script'">
|
||||||
|
<!-- TODO代码编辑器 -->
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts" name="Import">
|
||||||
|
import { useForm } from 'ant-design-vue/es/form';
|
||||||
|
import { saveMetadata } from '@/api/device/instance'
|
||||||
|
import { queryNoPagingPost, convertMetadata, modify } from '@/api/device/product'
|
||||||
|
import type { DefaultOptionType } from 'ant-design-vue/es/select';
|
||||||
|
import { UploadProps } from 'ant-design-vue/es';
|
||||||
|
import type { DeviceMetadata } from '@/views/device/Product/typings'
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import { Store } from 'jetlinks-store';
|
||||||
|
import { SystemConst } from '@/utils/consts';
|
||||||
|
import { useInstanceStore } from '@/store/instance'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const instanceStore = useInstanceStore()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean,
|
||||||
|
type: 'device' | 'product',
|
||||||
|
}
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', data: boolean): void;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emits = defineEmits<Emits>()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const _visible = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.visible;
|
||||||
|
},
|
||||||
|
set: (val: any) => {
|
||||||
|
emits('update:visible', val);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emits('update:visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** form表单 */
|
||||||
|
const formModel = reactive<Record<string, any>>({
|
||||||
|
type: 'import',
|
||||||
|
metadata: 'jetlinks',
|
||||||
|
metadataType: 'script',
|
||||||
|
})
|
||||||
|
const rules = reactive({
|
||||||
|
type: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择导入方式',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
copy: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择产品',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择物模型类型',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadataType: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择导入类型',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
upload: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请上传文件',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
import: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入物模型',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const { validate, validateInfos } = useForm(formModel, rules);
|
||||||
|
const onSubmit = () => {
|
||||||
|
validate().then(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const productList = ref<DefaultOptionType[]>([])
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
const { id } = route.params || {}
|
||||||
|
const product = await queryNoPagingPost({
|
||||||
|
paging: false,
|
||||||
|
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||||
|
terms: [{ column: 'id$not', value: id }],
|
||||||
|
}) as any
|
||||||
|
productList.value = product.result.filter((i: any) => i?.metadata).map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.metadata,
|
||||||
|
key: item.id
|
||||||
|
})) as DefaultOptionType[]
|
||||||
|
}
|
||||||
|
loadData()
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = file => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file);
|
||||||
|
reader.onload = (json) => {
|
||||||
|
formModel.import = json.target?.result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const operateLimits = (mdata: DeviceMetadata) => {
|
||||||
|
const obj: DeviceMetadata = { ...mdata };
|
||||||
|
const old = JSON.parse(instanceStore.detail?.metadata || '{}');
|
||||||
|
const fid = instanceStore.detail?.features?.map(item => item.id);
|
||||||
|
if (fid?.includes('eventNotModifiable')) {
|
||||||
|
obj.events = old?.events || [];
|
||||||
|
}
|
||||||
|
if (fid?.includes('propertyNotModifiable')) {
|
||||||
|
obj.properties = old?.properties || [];
|
||||||
|
}
|
||||||
|
(obj?.events || []).map((item, index) => {
|
||||||
|
return { ...item, sortsIndex: index };
|
||||||
|
});
|
||||||
|
(obj?.properties || []).map((item, index) => {
|
||||||
|
return { ...item, sortsIndex: index };
|
||||||
|
});
|
||||||
|
(obj?.functions || []).map((item, index) => {
|
||||||
|
return { ...item, sortsIndex: index };
|
||||||
|
});
|
||||||
|
(obj?.tags || []).map((item, index) => {
|
||||||
|
return { ...item, sortsIndex: index };
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
validate().then(async (data) => {
|
||||||
|
loading.value = true
|
||||||
|
if (data.metadata === 'alink') {
|
||||||
|
const res = await convertMetadata('from', 'alink', data.import)
|
||||||
|
if (res.status === 200) {
|
||||||
|
const metadata = JSON.stringify(operateLimits(res.result))
|
||||||
|
const { id } = route.params || {}
|
||||||
|
if (props?.type === 'device') {
|
||||||
|
await saveMetadata(id as string, metadata)
|
||||||
|
} else {
|
||||||
|
await modify(id as string, { metadata: metadata })
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
// MetadataAction.insert(JSON.parse(metadata || '{}'));
|
||||||
|
message.success('导入成功')
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
message.error('发生错误!')
|
||||||
|
}
|
||||||
|
Store.set(SystemConst.GET_METADATA, true)
|
||||||
|
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const _object = JSON.parse(data[props?.type === 'device' ? 'import' : data?.type] || '{}')
|
||||||
|
if (
|
||||||
|
!(!!_object?.properties || !!_object?.events || !!_object?.functions || !!_object?.tags)
|
||||||
|
) {
|
||||||
|
message.error('物模型数据不正确')
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { id } = route.params || {}
|
||||||
|
const params = {
|
||||||
|
id,
|
||||||
|
metadata: JSON.stringify(operateLimits(_object as DeviceMetadata)),
|
||||||
|
};
|
||||||
|
const paramsDevice = JSON.stringify(operateLimits(_object as DeviceMetadata))
|
||||||
|
let resp = undefined
|
||||||
|
if (props?.type === 'device') {
|
||||||
|
resp = await saveMetadata(id as string, paramsDevice)
|
||||||
|
} else {
|
||||||
|
resp = await modify(id as string, params)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
if (resp.status === 200) {
|
||||||
|
if (props?.type === 'device') {
|
||||||
|
const metadata: DeviceMetadata = JSON.parse(paramsDevice || '{}')
|
||||||
|
// MetadataAction.insert(metadata);
|
||||||
|
message.success('导入成功')
|
||||||
|
} else {
|
||||||
|
const metadata: DeviceMetadata = JSON.parse(params?.metadata || '{}')
|
||||||
|
// MetadataAction.insert(metadata);
|
||||||
|
message.success('导入成功')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Store.set(SystemConst.GET_METADATA, true)
|
||||||
|
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||||
|
close();
|
||||||
|
} catch (e) {
|
||||||
|
loading.value = false
|
||||||
|
message.error(e === 'error' ? '物模型数据不正确' : '上传json格式的物模型文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// const showProduct = computed(() => formModel.type === 'copy')
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.import-content {
|
||||||
|
background: rgb(236, 237, 238);
|
||||||
|
|
||||||
|
.import-tip {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class='device-detail-metadata' style="position: relative;">
|
||||||
|
<div class="tips" style="width: 40%">
|
||||||
|
<a-tooltip :title="instanceStore.detail?.independentMetadata && type === 'device'
|
||||||
|
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
|
||||||
|
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'">
|
||||||
|
<div class="ellipsis">
|
||||||
|
<info-circle-outlined style="margin-right: 3px" />
|
||||||
|
{{
|
||||||
|
instanceStore.detail?.independentMetadata && type === 'device'
|
||||||
|
? '该设备已脱离产品物模型,修改产品物模型对该设备无影响'
|
||||||
|
: '设备会默认继承产品的物模型,修改设备物模型后将脱离产品物模型'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<a-tabs class="metadataNav" destroyInactiveTabPane>
|
||||||
|
<template #rightExtra>
|
||||||
|
<a-space>
|
||||||
|
<PermissionButton v-if="type === 'device'" :hasPermission="`${permission}:update`"
|
||||||
|
:popConfirm="{ title: '确认重置?', onConfirm: resetMetadata, }" :tooltip="{ title: '重置后将使用产品的物模型配置' }"
|
||||||
|
key="reload">
|
||||||
|
重置操作
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton :isPermission="`${permission}:update`" @click="visible = true">快速导入</PermissionButton>
|
||||||
|
<PermissionButton :isPermission="`${permission}:update`" @click="cat = true">物模型TSL</PermissionButton>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-tab-pane tab="属性定义" key="properties">
|
||||||
|
<BaseMetadata target={props.type} type="properties" :permission="permission" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane tab="功能定义" key="functions">
|
||||||
|
<BaseMetadata target={props.type} type="functions" :permission="permission" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane tab="事件定义" key="events">
|
||||||
|
<BaseMetadata target={props.type} type="events" :permission="permission" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane tab="标签定义" key="tags">
|
||||||
|
<BaseMetadata target={props.type} type="tags" :permission="permission" />
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<Import :visible="visible" :type="type" @close="visible = false" />
|
||||||
|
<Cat :visible="cat" @close="cat = false" :type="type" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts" name="Metadata">
|
||||||
|
import PermissionButton from '@/components/PermissionButton/index.vue'
|
||||||
|
import { deleteMetadata } from '@/api/device/instance.js'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { Store } from 'jetlinks-store'
|
||||||
|
import { SystemConst } from '@/utils/consts'
|
||||||
|
import { useInstanceStore } from '@/store/instance'
|
||||||
|
import Import from './Import/index.vue'
|
||||||
|
import Cat from './Cat/index.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const instanceStore = useInstanceStore()
|
||||||
|
interface Props {
|
||||||
|
type: 'product' | 'device';
|
||||||
|
independentMetadata?: boolean;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const permission = computed(() => props.type === 'device' ? 'device/Instance' : 'device/Product')
|
||||||
|
const visible = ref(false)
|
||||||
|
const cat = ref(false)
|
||||||
|
|
||||||
|
// 重置物模型
|
||||||
|
const resetMetadata = async () => {
|
||||||
|
const { id } = route.params
|
||||||
|
const resp = await deleteMetadata(id as string)
|
||||||
|
if (resp.status === 200) {
|
||||||
|
message.info('操作成功')
|
||||||
|
|
||||||
|
Store.set(SystemConst.REFRESH_DEVICE, true)
|
||||||
|
setTimeout(() => {
|
||||||
|
Store.set(SystemConst.REFRESH_METADATA_TABLE, true)
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.device-detail-metadata {
|
||||||
|
.tips {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
z-index: 1;
|
||||||
|
margin-left: 330px;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadataNav {
|
||||||
|
:global {
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,23 +23,16 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
import { bootConfig } from "../index";
|
||||||
type configItem = {
|
|
||||||
auth: boolean;
|
|
||||||
link: string;
|
|
||||||
english: string;
|
|
||||||
label: string;
|
|
||||||
params?: object;
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
cardData: Array<configItem>,
|
cardData: Array<bootConfig>,
|
||||||
cardTitle: String,
|
cardTitle: String,
|
||||||
});
|
});
|
||||||
const { cardData, cardTitle } = toRefs(props);
|
const { cardData, cardTitle } = toRefs(props);
|
||||||
|
|
||||||
const jumpPage = (row: configItem): void => {
|
const jumpPage = (row: bootConfig): void => {
|
||||||
if (row.auth && row.link) {
|
if (row.auth && row.link) {
|
||||||
router.push(`${row.link}${objToParams(row.params || {})}`);
|
router.push(`${row.link}${objToParams(row.params || {})}`);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -28,24 +28,16 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
import { bootConfig } from "../index";
|
||||||
type configItem = {
|
|
||||||
auth: boolean;
|
|
||||||
link: string;
|
|
||||||
english: string;
|
|
||||||
label: string;
|
|
||||||
params?: object;
|
|
||||||
image: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
cardData: Array<configItem>,
|
cardData: Array<bootConfig>,
|
||||||
cardTitle: String,
|
cardTitle: String,
|
||||||
});
|
});
|
||||||
const { cardData, cardTitle } = toRefs(props);
|
const { cardData, cardTitle } = toRefs(props);
|
||||||
|
|
||||||
const jumpPage = (row: configItem): void => {
|
const jumpPage = (row: bootConfig): void => {
|
||||||
if (row.auth && row.link) {
|
if (row.auth && row.link) {
|
||||||
router.push(`${row.link}${objToParams(row.params || {})}`);
|
router.push(`${row.link}${objToParams(row.params || {})}`);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<a-row :gutter="24" class="top" style="margin-bottom: 24px">
|
<a-row :gutter="24" class="top" style="margin-bottom: 24px">
|
||||||
<a-col :span="6" class="left">
|
<a-col :span="6" class="left">
|
||||||
<BootCardSmall
|
<BootCardSmall
|
||||||
:cardData="devBootConfig"
|
:cardData="deviceBootConfig"
|
||||||
cardTitle="物联网引导"
|
cardTitle="物联网引导"
|
||||||
/>
|
/>
|
||||||
<div style="width: 100%; height: 24px"></div>
|
<div style="width: 100%; height: 24px"></div>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<StepCard
|
<StepCard
|
||||||
cardTitle="设备接入推荐步骤"
|
cardTitle="设备接入推荐步骤"
|
||||||
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
|
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
|
||||||
:dataList="devStepDetails"
|
:dataList="deviceStepDetails"
|
||||||
style="margin-bottom: 24px"
|
style="margin-bottom: 24px"
|
||||||
/>
|
/>
|
||||||
<StepCard
|
<StepCard
|
||||||
|
@ -39,152 +39,26 @@ import BootCardSmall from '../BootCardSmall.vue';
|
||||||
import DeviceCountCard from '../DeviceCountCard.vue';
|
import DeviceCountCard from '../DeviceCountCard.vue';
|
||||||
import BasicCountCard from '../BasicCountCard.vue';
|
import BasicCountCard from '../BasicCountCard.vue';
|
||||||
import PlatformPicCard from '../PlatformPicCard.vue';
|
import PlatformPicCard from '../PlatformPicCard.vue';
|
||||||
import { recommendList } from '../../index';
|
|
||||||
|
|
||||||
import StepCard from '../StepCard.vue';
|
import StepCard from '../StepCard.vue';
|
||||||
|
|
||||||
// 物联网引导-数据
|
import {
|
||||||
const devBootConfig = [
|
deviceBootConfig as _deviceBootConfig,
|
||||||
{
|
deviceStepDetails,
|
||||||
english: 'STEP1',
|
opsBootConfig as _opsBootConfig,
|
||||||
label: '创建产品',
|
opsStepDetails,
|
||||||
link: '/a',
|
} from '../../modules/config';
|
||||||
auth: true,
|
|
||||||
save: true,
|
const deviceImages = [
|
||||||
image: '/images/home/guide-home1.png',
|
'/images/home/guide-home1.png',
|
||||||
},
|
'/images/home/guide-home2.png',
|
||||||
{
|
'/images/home/guide-home3.png',
|
||||||
english: 'STEP2',
|
|
||||||
label: '创建设备',
|
|
||||||
link: '/b',
|
|
||||||
auth: true,
|
|
||||||
save: true,
|
|
||||||
image: '/images/home/guide-home2.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP3',
|
|
||||||
label: '规则引擎',
|
|
||||||
link: '/c',
|
|
||||||
auth: false,
|
|
||||||
save: true,
|
|
||||||
image: '/images/home/guide-home3.png',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
// 运维管理引导-数据
|
const opsImages = [
|
||||||
const opsBootConfig = [
|
'/images/home/guide-home4.png',
|
||||||
{
|
'/images/home/guide-home5.png',
|
||||||
english: 'STEP1',
|
'/images/home/guide-home6.png',
|
||||||
label: '创建产品',
|
|
||||||
link: '/a',
|
|
||||||
auth: true,
|
|
||||||
save: true,
|
|
||||||
image: '/images/home/guide-home4.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP2',
|
|
||||||
label: '创建设备',
|
|
||||||
link: '/b',
|
|
||||||
auth: true,
|
|
||||||
save: true,
|
|
||||||
image: '/images/home/guide-home5.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP3',
|
|
||||||
label: '规则引擎',
|
|
||||||
link: '/c',
|
|
||||||
auth: false,
|
|
||||||
save: true,
|
|
||||||
image: '/images/home/guide-home6.png',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
// 设备接入推荐步骤-数据
|
|
||||||
const devStepDetails = [
|
const deviceBootConfig = _deviceBootConfig.map((item,i) => ({...item, image: deviceImages[i]}));
|
||||||
{
|
const opsBootConfig = _opsBootConfig.map((item,i) => ({...item, image: opsImages[i]}));
|
||||||
title: '创建产品',
|
|
||||||
details:
|
|
||||||
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
|
|
||||||
iconUrl: '/images/home/bottom-4.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '配置产品接入方式',
|
|
||||||
details:
|
|
||||||
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
|
|
||||||
iconUrl: '/images/home/bottom-1.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
dialogTag: 'accessMethod',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '添加测试设备',
|
|
||||||
details: '添加单个设备,用于验证产品模型是否配置正确。',
|
|
||||||
iconUrl: '/images/home/bottom-5.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '功能调试',
|
|
||||||
details:
|
|
||||||
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
|
|
||||||
iconUrl: '/images/home/bottom-2.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
dialogTag: 'funcTest',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '批量添加设备',
|
|
||||||
details: '批量添加同一产品下的设备',
|
|
||||||
iconUrl: '/images/home/bottom-3.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: false,
|
|
||||||
},
|
|
||||||
] as recommendList[];
|
|
||||||
// 运维管理推荐步骤-数据
|
|
||||||
const opsStepDetails = [
|
|
||||||
{
|
|
||||||
title: '协议管理',
|
|
||||||
details:
|
|
||||||
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
|
|
||||||
iconUrl: '/images/home/bottom-1.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
params: {
|
|
||||||
a: 1,
|
|
||||||
save: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '证书管理',
|
|
||||||
details: '统一维护平台内的证书,用于数据通信加密。',
|
|
||||||
iconUrl: '/images/home/bottom-6.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
params: {
|
|
||||||
a: 1,
|
|
||||||
save: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '网络组件',
|
|
||||||
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
|
|
||||||
iconUrl: '/images/home/bottom-3.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '设备接入网关',
|
|
||||||
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
|
|
||||||
iconUrl: '/images/home/bottom-4.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '日志管理',
|
|
||||||
details: '监控系统日志,及时处理系统异常。',
|
|
||||||
iconUrl: '/images/home/bottom-5.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: false,
|
|
||||||
},
|
|
||||||
] as recommendList[];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="device-home-container">
|
<div class="device-home-container">
|
||||||
<a-row :gutter="10">
|
<a-row :gutter="10">
|
||||||
<a-col :span="14">
|
<a-col :span="14">
|
||||||
<BootCard :cardData="bootConfig" cardTitle="运维引导" />
|
<BootCard :cardData="opsBootConfig" cardTitle="运维引导" />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="10">
|
<a-col :span="10">
|
||||||
<BasicCountCard />
|
<BasicCountCard />
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
<StepCard
|
<StepCard
|
||||||
cardTitle="运维管理推荐步骤"
|
cardTitle="运维管理推荐步骤"
|
||||||
tooltip="请根据业务需要对下述步骤进行选择性操作。"
|
tooltip="请根据业务需要对下述步骤进行选择性操作。"
|
||||||
:dataList="stepDetails"
|
:dataList="opsStepDetails"
|
||||||
/>
|
/>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,74 +27,8 @@ import BasicCountCard from '../BasicCountCard.vue';
|
||||||
import PlatformPicCard from '../PlatformPicCard.vue';
|
import PlatformPicCard from '../PlatformPicCard.vue';
|
||||||
import StepCard from '../StepCard.vue';
|
import StepCard from '../StepCard.vue';
|
||||||
|
|
||||||
import {recommendList} from '../../index'
|
import { opsBootConfig, opsStepDetails } from '../../modules/config';
|
||||||
// import {getImage} from '@/utils/comm'
|
|
||||||
|
|
||||||
// 运维引导-数据
|
|
||||||
const bootConfig = [
|
|
||||||
{
|
|
||||||
english: 'STEP1',
|
|
||||||
label: '设备接入配置',
|
|
||||||
link: '/a',
|
|
||||||
auth: true,
|
|
||||||
save: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP2',
|
|
||||||
label: '日志排查',
|
|
||||||
link: '/b',
|
|
||||||
auth: true,
|
|
||||||
save: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP3',
|
|
||||||
label: '实时监控',
|
|
||||||
link: '/c',
|
|
||||||
auth: false,
|
|
||||||
save: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// 运维管理推荐步骤-数据
|
|
||||||
const stepDetails = [
|
|
||||||
{
|
|
||||||
title: '创建产品',
|
|
||||||
details:
|
|
||||||
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
|
|
||||||
iconUrl: '/images/home/bottom-4.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '配置产品接入方式',
|
|
||||||
details:
|
|
||||||
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
|
|
||||||
iconUrl: '/images/home/bottom-1.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '添加测试设备',
|
|
||||||
details: '添加单个设备,用于验证产品模型是否配置正确。',
|
|
||||||
iconUrl: '/images/home/bottom-5.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '功能调试',
|
|
||||||
details:
|
|
||||||
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
|
|
||||||
iconUrl: '/images/home/bottom-2.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '批量添加设备',
|
|
||||||
details: '批量添加同一产品下的设备',
|
|
||||||
iconUrl: '/images/home/bottom-3.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: false,
|
|
||||||
},
|
|
||||||
] as recommendList[];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="device-home-container">
|
<div class="device-home-container">
|
||||||
<a-row :gutter="10">
|
<a-row :gutter="10">
|
||||||
<a-col :span="14">
|
<a-col :span="14">
|
||||||
<BootCard :cardData="bootConfig" cardTitle="物联网引导" />
|
<BootCard :cardData="deviceBootConfig" cardTitle="物联网引导" />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="10">
|
<a-col :span="10">
|
||||||
<DeviceCountCard />
|
<DeviceCountCard />
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
<StepCard
|
<StepCard
|
||||||
cardTitle="设备接入推荐步骤"
|
cardTitle="设备接入推荐步骤"
|
||||||
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
|
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
|
||||||
:dataList="stepDetails"
|
:dataList="deviceStepDetails"
|
||||||
/>
|
/>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,73 +27,7 @@ import DeviceCountCard from '../DeviceCountCard.vue';
|
||||||
import PlatformPicCard from '../PlatformPicCard.vue';
|
import PlatformPicCard from '../PlatformPicCard.vue';
|
||||||
import StepCard from '../StepCard.vue';
|
import StepCard from '../StepCard.vue';
|
||||||
|
|
||||||
import {recommendList} from '../../index'
|
import { deviceBootConfig, deviceStepDetails } from '../../modules/config';
|
||||||
|
|
||||||
// import {getImage} from '@/utils/comm'
|
|
||||||
|
|
||||||
// 物联网引导-数据
|
|
||||||
const bootConfig = [
|
|
||||||
{
|
|
||||||
english: 'STEP1',
|
|
||||||
label: '创建产品',
|
|
||||||
link: '/a',
|
|
||||||
auth: true,
|
|
||||||
params: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP2',
|
|
||||||
label: '创建设备',
|
|
||||||
link: '/b',
|
|
||||||
auth: true,
|
|
||||||
params: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
english: 'STEP3',
|
|
||||||
label: '规则引擎',
|
|
||||||
link: '/c',
|
|
||||||
auth: false,
|
|
||||||
params: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// 设备接入推荐步骤-数据
|
|
||||||
const stepDetails = [
|
|
||||||
{
|
|
||||||
title: '协议管理',
|
|
||||||
details:
|
|
||||||
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
|
|
||||||
iconUrl: '/images/home/bottom-1.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '证书管理',
|
|
||||||
details: '统一维护平台内的证书,用于数据通信加密。',
|
|
||||||
iconUrl: '/images/home/bottom-6.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '网络组件',
|
|
||||||
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
|
|
||||||
iconUrl: '/images/home/bottom-3.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '设备接入网关',
|
|
||||||
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
|
|
||||||
iconUrl: '/images/home/bottom-4.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '日志管理',
|
|
||||||
details: '监控系统日志,及时处理系统异常。',
|
|
||||||
iconUrl: '/images/home/bottom-5.png',
|
|
||||||
linkUrl: '/a',
|
|
||||||
auth: false,
|
|
||||||
},
|
|
||||||
] as recommendList[];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -8,43 +8,20 @@
|
||||||
:getContainer="getContainer"
|
:getContainer="getContainer"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
>
|
>
|
||||||
<div class="search">
|
<Search />
|
||||||
<a-select
|
|
||||||
v-model:value="form.key"
|
|
||||||
style="width: 100px;margin-right: 20px;"
|
|
||||||
:options="productList"
|
|
||||||
/>
|
|
||||||
<a-select
|
|
||||||
v-model:value="form.relation"
|
|
||||||
style="width: 100px;margin-right: 20px;"
|
|
||||||
:options="productList"
|
|
||||||
/>
|
|
||||||
<a-input v-model:value="form.keyValue" allow-clear style="width: 230px;margin-right: 50px;" />
|
|
||||||
|
|
||||||
<a-button type="primary" @click="clickSearch" style="margin-right: 10px;">
|
|
||||||
<template #icon><SearchOutlined /></template>
|
|
||||||
搜索
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="clickReset">
|
|
||||||
<template #icon><reload-outlined /></template>
|
|
||||||
重置
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<JTable :columns="columns" model="TABLE"> </JTable>
|
<JTable :columns="columns" model="TABLE"> </JTable>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button key="back" @click="visible = false
|
<a-button key="back" @click="visible = false">取消</a-button>
|
||||||
">取消</a-button>
|
<a-button key="submit" type="primary" @click="handleOk"
|
||||||
<a-button key="submit" type="primary" @click="handleOk">确认</a-button>
|
>确认</a-button
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ComponentInternalInstance } from 'vue';
|
import { ComponentInternalInstance } from 'vue';
|
||||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { productItem, deviceInfo } from '../../index';
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
const emits = defineEmits(['confirm']);
|
const emits = defineEmits(['confirm']);
|
||||||
|
@ -64,7 +41,6 @@ watch(
|
||||||
() => {
|
() => {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
clickReset();
|
clickReset();
|
||||||
getOptions();
|
|
||||||
clickSearch();
|
clickSearch();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -75,10 +51,7 @@ const form = ref({
|
||||||
relation: '',
|
relation: '',
|
||||||
keyValue: '',
|
keyValue: '',
|
||||||
});
|
});
|
||||||
const productList = ref<[productItem] | []>([]);
|
|
||||||
const getOptions = () => {
|
|
||||||
productList.value = [];
|
|
||||||
};
|
|
||||||
const clickSearch = () => {};
|
const clickSearch = () => {};
|
||||||
const clickReset = () => {
|
const clickReset = () => {
|
||||||
Object.entries(form.value).forEach(([prop]) => {
|
Object.entries(form.value).forEach(([prop]) => {
|
||||||
|
@ -114,9 +87,6 @@ const columns = [
|
||||||
key: 'status',
|
key: 'status',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableData = ref<deviceInfo[]>([]);
|
|
||||||
const selectItem: deviceInfo | {} = {};
|
|
||||||
const getList = () => {};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@ -125,5 +95,4 @@ const getList = () => {};
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,11 +8,6 @@ export interface recommendList {
|
||||||
auth: boolean;
|
auth: boolean;
|
||||||
dialogTag?: 'accessMethod' | 'funcTest';
|
dialogTag?: 'accessMethod' | 'funcTest';
|
||||||
}
|
}
|
||||||
// 产品列表里的每项
|
|
||||||
export interface productItem {
|
|
||||||
label: string;
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface deviceInfo {
|
export interface deviceInfo {
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
|
@ -20,4 +15,13 @@ export interface deviceInfo {
|
||||||
productName: string,
|
productName: string,
|
||||||
createTime: string,
|
createTime: string,
|
||||||
status: boolean
|
status: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface bootConfig {
|
||||||
|
english: string,
|
||||||
|
label: string,
|
||||||
|
link: string,
|
||||||
|
auth: boolean,
|
||||||
|
image?:string,
|
||||||
|
params?: object,
|
||||||
}
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
// import {getImage} from '@/utils/comm'
|
||||||
|
import { usePermissionStore } from "@/store/permission";
|
||||||
|
import { recommendList, bootConfig } from "../index";
|
||||||
|
|
||||||
|
|
||||||
|
// 权限控制
|
||||||
|
const hasPermission = usePermissionStore().hasPermission;
|
||||||
|
const productPermission = (action: string) =>
|
||||||
|
hasPermission(`device/Product:${action}`);
|
||||||
|
const devicePermission = (action: string) =>
|
||||||
|
hasPermission(`device/Instance:${action}`);
|
||||||
|
const rulePermission = (action: string) =>
|
||||||
|
hasPermission(`rule-engine/Instance:${action}`);
|
||||||
|
|
||||||
|
|
||||||
|
// 物联网引导-数据
|
||||||
|
export const deviceBootConfig: bootConfig[] = [
|
||||||
|
{
|
||||||
|
english: 'STEP1',
|
||||||
|
label: '创建产品',
|
||||||
|
link: '/a',
|
||||||
|
auth: productPermission('add'),
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: 'STEP2',
|
||||||
|
label: '创建设备',
|
||||||
|
link: '/b',
|
||||||
|
auth: devicePermission('add'),
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: 'STEP3',
|
||||||
|
label: '规则引擎',
|
||||||
|
link: '/c',
|
||||||
|
auth: rulePermission('add'),
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 设备接入推荐步骤-数据
|
||||||
|
export const deviceStepDetails: recommendList[] = [
|
||||||
|
{
|
||||||
|
title: '创建产品',
|
||||||
|
details:
|
||||||
|
'产品是设备的集合,通常指一组具有相同功能的设备。物联设备必须通过产品进行接入方式配置。',
|
||||||
|
iconUrl: '/images/home/bottom-4.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: productPermission('add'),
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '配置产品接入方式',
|
||||||
|
details:
|
||||||
|
'通过产品对同一类型的设备进行统一的接入方式配置。请参照设备铭牌说明选择匹配的接入方式。',
|
||||||
|
iconUrl: '/images/home/bottom-1.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: productPermission('update'),
|
||||||
|
dialogTag: 'accessMethod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '添加测试设备',
|
||||||
|
details: '添加单个设备,用于验证产品模型是否配置正确。',
|
||||||
|
iconUrl: '/images/home/bottom-5.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: devicePermission('add'),
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '功能调试',
|
||||||
|
details:
|
||||||
|
'对添加的测试设备进行功能调试,验证能否连接到平台,设备功能是否配置正确。',
|
||||||
|
iconUrl: '/images/home/bottom-2.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: devicePermission('update'),
|
||||||
|
dialogTag: 'funcTest',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '批量添加设备',
|
||||||
|
details: '批量添加同一产品下的设备',
|
||||||
|
iconUrl: '/images/home/bottom-3.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: devicePermission('import'),
|
||||||
|
params: {
|
||||||
|
import: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// 运维管理引导-数据
|
||||||
|
export const opsBootConfig: bootConfig[] = [
|
||||||
|
{
|
||||||
|
english: 'STEP1',
|
||||||
|
label: '设备接入配置',
|
||||||
|
link: '/a',
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: 'STEP2',
|
||||||
|
label: '日志排查',
|
||||||
|
link: '/b',
|
||||||
|
auth: true,
|
||||||
|
params: {
|
||||||
|
key: 'system',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: 'STEP3',
|
||||||
|
label: '实时监控',
|
||||||
|
link: '/c',
|
||||||
|
auth: false,
|
||||||
|
params: {
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 运维管理推荐步骤-数据
|
||||||
|
export const opsStepDetails: recommendList[] = [
|
||||||
|
{
|
||||||
|
title: '协议管理',
|
||||||
|
details:
|
||||||
|
'根据业务需求自定义开发对应的产品(设备模型)接入协议,并上传到平台。',
|
||||||
|
iconUrl: '/images/home/bottom-1.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: true,
|
||||||
|
params: {
|
||||||
|
a: 1,
|
||||||
|
save: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证书管理',
|
||||||
|
details: '统一维护平台内的证书,用于数据通信加密。',
|
||||||
|
iconUrl: '/images/home/bottom-6.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: true,
|
||||||
|
params: {
|
||||||
|
a: 1,
|
||||||
|
save: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '网络组件',
|
||||||
|
details: '根据不同的传输类型配置平台底层网络组件相关参数。',
|
||||||
|
iconUrl: '/images/home/bottom-3.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设备接入网关',
|
||||||
|
details: '根据不同的传输类型,关联消息协议,配置设备接入网关相关参数。',
|
||||||
|
iconUrl: '/images/home/bottom-4.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '日志管理',
|
||||||
|
details: '监控系统日志,及时处理系统异常。',
|
||||||
|
iconUrl: '/images/home/bottom-5.png',
|
||||||
|
linkUrl: '/a',
|
||||||
|
auth: false,
|
||||||
|
params: {
|
||||||
|
key: 'system',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
|
@ -9,8 +9,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="!id"><a @click="goBack">返回</a></div>
|
<div v-if="!id"><a @click="goBack">返回</a></div>
|
||||||
<AccessNetwork v-if="showType==='network'" :provider="provider" :data="data" />
|
<AccessNetwork
|
||||||
<Media v-if="showType==='media'" :provider="provider" :data="data" />
|
v-if="showType === 'network'"
|
||||||
|
:provider="provider"
|
||||||
|
:data="data"
|
||||||
|
/>
|
||||||
|
<Media v-if="showType === 'media'" :provider="provider" />
|
||||||
|
<Channel v-if="showType === 'channel'" :provider="provider" />
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
@ -22,7 +27,7 @@ import AccessNetwork from '../components/Network.vue';
|
||||||
import Provider from '../components/Provider/index.vue';
|
import Provider from '../components/Provider/index.vue';
|
||||||
import { getProviders, detail } from '@/api/link/accessConfig';
|
import { getProviders, detail } from '@/api/link/accessConfig';
|
||||||
import Media from '../components/Media/index.vue';
|
import Media from '../components/Media/index.vue';
|
||||||
|
import Channel from '../components/Channel/index.vue';
|
||||||
|
|
||||||
// const router = useRouter();
|
// const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -34,14 +39,13 @@ const type = ref(false);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const provider = ref({});
|
const provider = ref({});
|
||||||
const data = ref({});
|
const data = ref({});
|
||||||
const showType = ref('')
|
const showType = ref('');
|
||||||
|
|
||||||
const goProviders = (param: object) => {
|
const goProviders = (param: object) => {
|
||||||
showType.value = param.type
|
showType.value = param.type;
|
||||||
provider.value = param;
|
provider.value = param;
|
||||||
type.value = false;
|
type.value = false;
|
||||||
console.log(1123,showType.value,param);
|
console.log(1123, showType.value, param);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
|
@ -59,22 +63,22 @@ const queryProviders = async () => {
|
||||||
const edge: any[] = [];
|
const edge: any[] = [];
|
||||||
resp.result.map((item) => {
|
resp.result.map((item) => {
|
||||||
if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
|
if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
|
||||||
item.type='media'
|
item.type = 'media';
|
||||||
media.push(item);
|
media.push(item);
|
||||||
} else if (item.id === 'OneNet' || item.id === 'Ctwing') {
|
} else if (item.id === 'OneNet' || item.id === 'Ctwing') {
|
||||||
item.type='cloud'
|
item.type = 'cloud';
|
||||||
cloud.push(item);
|
cloud.push(item);
|
||||||
} else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
|
} else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
|
||||||
item.type='channel'
|
item.type = 'channel';
|
||||||
channel.push(item);
|
channel.push(item);
|
||||||
} else if (
|
} else if (
|
||||||
item.id === 'official-edge-gateway' ||
|
item.id === 'official-edge-gateway' ||
|
||||||
item.id === 'edge-child-device'
|
item.id === 'edge-child-device'
|
||||||
) {
|
) {
|
||||||
item.type='edge'
|
item.type = 'edge';
|
||||||
edge.push(item);
|
edge.push(item);
|
||||||
} else {
|
} else {
|
||||||
item.type='network'
|
item.type = 'network';
|
||||||
network.push(item);
|
network.push(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -114,7 +118,7 @@ const queryProviders = async () => {
|
||||||
title: '官方接入',
|
title: '官方接入',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dataSource.value = list
|
dataSource.value = list;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
}}</a-tooltip>
|
}}</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="checked-icon">
|
<div class="checked-icon">
|
||||||
<div><a-icon type="check" /></div>
|
<div><CheckOutlined /></div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup name="AccessCard">
|
<script lang="ts" setup name="AccessCard">
|
||||||
|
import { CheckOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
const emit = defineEmits(['checkedChange']);
|
const emit = defineEmits(['checkedChange']);
|
||||||
|
|
||||||
|
@ -36,13 +36,10 @@ const props = defineProps({
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(1112,props);
|
|
||||||
|
|
||||||
const checkedChange=(id:string)=>{
|
|
||||||
emit('checkedChange', id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
const checkedChange = (id: string) => {
|
||||||
|
emit('checkedChange', id);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div v-if="type === 'channel'" class="card-last">
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<title-component data="基本信息" />
|
||||||
|
<div>
|
||||||
|
<a-form
|
||||||
|
:model="formState"
|
||||||
|
name="basic"
|
||||||
|
autocomplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="onFinish"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="名称"
|
||||||
|
name="name"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入名称',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入名称"
|
||||||
|
v-model:value="formState.name"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="说明" name="description">
|
||||||
|
<a-textarea
|
||||||
|
placeholder="请输入说明"
|
||||||
|
:rows="4"
|
||||||
|
v-model:value="formState.description"
|
||||||
|
show-count
|
||||||
|
:maxlength="200"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" html-type="submit"
|
||||||
|
>保存</a-button
|
||||||
|
>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="config-right">
|
||||||
|
<div class="config-right-item">
|
||||||
|
<title-component data="配置概览" />
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
接入方式:{{ provider.name }}
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{ provider.description }}
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
消息协议:{{ provider.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item">
|
||||||
|
<title-component data="设备接入指引" />
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
1、配置{{ provider.name }}通道
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
2、创建{{ provider.name }}设备接入网关
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
3、创建产品,并选中接入方式为{{ provider.name }}
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
4、添加设备,单独为每一个设备进行数据点绑定
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="AccessMedia">
|
||||||
|
import { message, Form } from 'ant-design-vue';
|
||||||
|
import type { FormInstance } from 'ant-design-vue';
|
||||||
|
import { update, save } from '@/api/link/accessConfig';
|
||||||
|
|
||||||
|
interface FormState {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
const route = useRoute();
|
||||||
|
const id = route.query.id;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
provider: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const type = ref(props.provider.type);
|
||||||
|
|
||||||
|
const formState = reactive<FormState>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
const onFinish = async (values: any) => {
|
||||||
|
const providerId = props.provider.id;
|
||||||
|
const params = {
|
||||||
|
...values,
|
||||||
|
provider: providerId,
|
||||||
|
protocol: providerId,
|
||||||
|
transport: providerId === 'modbus-tcp' ? 'MODBUS_TCP' : 'OPC_UA',
|
||||||
|
channel: providerId === 'modbus-tcp' ? 'modbus' : 'opc-ua',
|
||||||
|
};
|
||||||
|
const resp = !!id ? await update({ ...params, id }) : await save(params);
|
||||||
|
if (resp.status === 200) {
|
||||||
|
message.success('操作成功!');
|
||||||
|
// if (params.get('save')) {
|
||||||
|
// if ((window as any).onTabSaveSuccess) {
|
||||||
|
// if (resp.result) {
|
||||||
|
// (window as any).onTabSaveSuccess(resp.result);
|
||||||
|
// setTimeout(() => window.close(), 300);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
history.back();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.container {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-last {
|
||||||
|
padding-right: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.config-right {
|
||||||
|
padding: 20px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.config-right-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.config-right-item-title {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right-item-context {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,818 @@
|
||||||
|
<template>
|
||||||
|
<div style="margin-top: 10px">
|
||||||
|
<a-steps :current="stepCurrent">
|
||||||
|
<a-step v-for="item in steps" :key="item" :title="item" />
|
||||||
|
</a-steps>
|
||||||
|
<div class="steps-content">
|
||||||
|
<div class="steps-box" v-if="current === 0">
|
||||||
|
<div class="alert">
|
||||||
|
<question-circle-outlined />
|
||||||
|
配置设备信令参数
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a-form
|
||||||
|
:model="formState"
|
||||||
|
ref="formRef1"
|
||||||
|
name="basic"
|
||||||
|
autocomplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="SIP 域"
|
||||||
|
name="domain"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入SIP 域',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
max: 64,
|
||||||
|
message: '最大可输入64个字符',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formState.domain"
|
||||||
|
placeholder="请输入SIP 域"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="SIP ID"
|
||||||
|
name="sipId"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入SIP ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
max: 64,
|
||||||
|
message: '最大可输入64个字符',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formState.sipId"
|
||||||
|
placeholder="请输入SIP ID"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
name="shareCluster"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择集群',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
集群
|
||||||
|
<span style="color: red; margin: 0 4px 0 -2px"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
<p>
|
||||||
|
共享配置:集群下所有节点共用同一配置
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
独立配置:集群下不同节点使用不同配置
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<question-circle-outlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-radio-group
|
||||||
|
v-model:value="formState.shareCluster"
|
||||||
|
>
|
||||||
|
<a-radio :value="true">共享配置</a-radio>
|
||||||
|
<a-radio :value="false">独立配置</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<div v-if="formState.shareCluster" class="form-item1">
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item
|
||||||
|
label="SIP 地址"
|
||||||
|
:name="['hostPort', 'host']"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择SIP地址',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formState.hostPort.host
|
||||||
|
"
|
||||||
|
style="width: 105%"
|
||||||
|
:disabled="true"
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
>
|
||||||
|
<a-select-option value="0.0.0.0"
|
||||||
|
>0.0.0.0</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item
|
||||||
|
:name="['hostPort', 'port']"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择端口',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="form-label"></div>
|
||||||
|
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formState.hostPort.port
|
||||||
|
"
|
||||||
|
:options="sipList"
|
||||||
|
placeholder="请选择端口"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item
|
||||||
|
label="公网 Host"
|
||||||
|
:name="['hostPort', 'publicHost']"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入IP地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||||
|
message: '请输入正确的IP地址',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
style="width: 105%"
|
||||||
|
v-model:value="
|
||||||
|
formState.hostPort.publicHost
|
||||||
|
"
|
||||||
|
placeholder="请输入IP地址"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item
|
||||||
|
:name="['hostPort', 'publicPort']"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '输入端口',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="form-label"></div>
|
||||||
|
|
||||||
|
<a-input-number
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
v-model:value="
|
||||||
|
formState.hostPort.publicPort
|
||||||
|
"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
<div v-if="!formState.shareCluster">
|
||||||
|
<a-form
|
||||||
|
ref="formRef2"
|
||||||
|
layout="vertical"
|
||||||
|
name="dynamic_form_nest_item"
|
||||||
|
:model="dynamicValidateForm"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(
|
||||||
|
cluster, index
|
||||||
|
) in dynamicValidateForm.cluster"
|
||||||
|
:key="cluster.id"
|
||||||
|
>
|
||||||
|
<a-collapse v-model:activeKey="activeKey">
|
||||||
|
<a-collapse-panel
|
||||||
|
:key="cluster.id"
|
||||||
|
:header="`#${index + 1}.节点`"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<delete-outlined
|
||||||
|
@click="removeCluster(cluster)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item
|
||||||
|
:name="[
|
||||||
|
'cluster',
|
||||||
|
index,
|
||||||
|
'clusterNodeId',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="form-label">
|
||||||
|
节点名称
|
||||||
|
</div>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
cluster.clusterNodeId
|
||||||
|
"
|
||||||
|
:options="clustersList"
|
||||||
|
placeholder="请选择节点名称"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="
|
||||||
|
filterOption
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item
|
||||||
|
:name="[
|
||||||
|
'cluster',
|
||||||
|
index,
|
||||||
|
'host',
|
||||||
|
]"
|
||||||
|
:rules="{
|
||||||
|
required: true,
|
||||||
|
message:
|
||||||
|
'请选择SIP 地址',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="form-label">
|
||||||
|
SIP 地址
|
||||||
|
<span
|
||||||
|
class="form-label-required"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
<p>
|
||||||
|
绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<question-circle-outlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
cluster.host
|
||||||
|
"
|
||||||
|
:options="sipListOption"
|
||||||
|
placeholder="请选择IP地址"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="
|
||||||
|
filterOption
|
||||||
|
"
|
||||||
|
style="width: 110%"
|
||||||
|
@change="
|
||||||
|
handleChangeForm2Sip(
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item
|
||||||
|
:name="[
|
||||||
|
'cluster',
|
||||||
|
index,
|
||||||
|
'port',
|
||||||
|
]"
|
||||||
|
:rules="{
|
||||||
|
required: true,
|
||||||
|
message: '请选择端口',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-label"
|
||||||
|
></div>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
cluster.port
|
||||||
|
"
|
||||||
|
:options="
|
||||||
|
sipListIndex[index]
|
||||||
|
"
|
||||||
|
placeholder="请选择端口"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
:filter-option="
|
||||||
|
filterOption
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item
|
||||||
|
:name="[
|
||||||
|
'cluster',
|
||||||
|
index,
|
||||||
|
'publicHost',
|
||||||
|
]"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message:
|
||||||
|
'请输入公网 Host',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.([0-9]|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||||
|
message:
|
||||||
|
'请输入正确的IP地址',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="form-label">
|
||||||
|
公网 Host
|
||||||
|
<span
|
||||||
|
class="form-label-required"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
<p>
|
||||||
|
监听指定端口的请求
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<question-circle-outlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<a-input
|
||||||
|
style="width: 110%"
|
||||||
|
v-model:value="
|
||||||
|
cluster.publicHost
|
||||||
|
"
|
||||||
|
placeholder="请输入IP地址"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item
|
||||||
|
:name="[
|
||||||
|
'cluster',
|
||||||
|
index,
|
||||||
|
'publicPort',
|
||||||
|
]"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message:
|
||||||
|
'请输入端口',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-label"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<a-input-number
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
v-model:value="
|
||||||
|
cluster.publicPort
|
||||||
|
"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</div>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button
|
||||||
|
style="margin-top: 10px"
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
@click="addCluster"
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="steps-box" v-else>
|
||||||
|
<div
|
||||||
|
class="card-last"
|
||||||
|
:style="`max-height:${
|
||||||
|
clientHeight > 900 ? 750 : clientHeight * 0.7
|
||||||
|
}px`"
|
||||||
|
>
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<title-component data="基本信息" />
|
||||||
|
<div>
|
||||||
|
<a-form :model="form" layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
label="名称"
|
||||||
|
v-bind="validateInfos.name"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="form.name"
|
||||||
|
allowClear
|
||||||
|
placeholder="请输入名称"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
label="说明"
|
||||||
|
v-bind="validateInfos.description"
|
||||||
|
>
|
||||||
|
<a-textarea
|
||||||
|
placeholder="请输入说明"
|
||||||
|
:rows="4"
|
||||||
|
v-model:value="form.description"
|
||||||
|
show-count
|
||||||
|
:maxlength="200"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="config-right">
|
||||||
|
<div class="config-right-item">
|
||||||
|
<div class="config-right-item-title">
|
||||||
|
接入方式
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{ provider.name }}
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{ provider.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item">
|
||||||
|
<div class="config-right-item-title">
|
||||||
|
消息协议
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
provider?.id === 'fixed-media'
|
||||||
|
? 'URL'
|
||||||
|
: 'SIP'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="steps-action">
|
||||||
|
<a-button v-if="[0].includes(current)" @click="next">
|
||||||
|
下一步
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="current === 1" type="primary" @click="saveData">
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="current > 0" style="margin-left: 8px" @click="prev">
|
||||||
|
上一步
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="AccessNetwork">
|
||||||
|
import { message, Form } from 'ant-design-vue';
|
||||||
|
import type { FormInstance } from 'ant-design-vue';
|
||||||
|
import { getResourcesCurrent, getClusters } from '@/api/link/accessConfig';
|
||||||
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { update, save } from '@/api/link/accessConfig';
|
||||||
|
|
||||||
|
interface Form2 {
|
||||||
|
clusterNodeId: string;
|
||||||
|
port: string;
|
||||||
|
host: string;
|
||||||
|
publicPort: string;
|
||||||
|
publicHost: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
interface FormState {
|
||||||
|
domain: string;
|
||||||
|
sipId: string;
|
||||||
|
shareCluster: boolean;
|
||||||
|
hostPort: {
|
||||||
|
port: string;
|
||||||
|
host: string;
|
||||||
|
publicPort: string;
|
||||||
|
publicHost: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
provider: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const id = route.query.id;
|
||||||
|
|
||||||
|
const activeKey: any = ref([]);
|
||||||
|
const clientHeight = document.body.clientHeight;
|
||||||
|
|
||||||
|
const formRef1 = ref<FormInstance>();
|
||||||
|
const formRef2 = ref<FormInstance>();
|
||||||
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
const current = ref(0);
|
||||||
|
const stepCurrent = ref(0);
|
||||||
|
const steps = ref(['信令配置', '完成']);
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
const formState = reactive<FormState>({
|
||||||
|
domain: '',
|
||||||
|
sipId: '',
|
||||||
|
shareCluster: true,
|
||||||
|
hostPort: {
|
||||||
|
port: '',
|
||||||
|
host: '0.0.0.0',
|
||||||
|
publicPort: '',
|
||||||
|
publicHost: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let params = {
|
||||||
|
configuration: {},
|
||||||
|
};
|
||||||
|
let sipListConst: any = [];
|
||||||
|
const sipListOption = ref([]);
|
||||||
|
const sipList = ref([]);
|
||||||
|
const sipListIndex: any = ref([]);
|
||||||
|
const clustersList = ref([]);
|
||||||
|
|
||||||
|
const dynamicValidateForm = reactive<{ cluster: Form2[] }>({
|
||||||
|
cluster: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeCluster = (item: Form2) => {
|
||||||
|
let index = dynamicValidateForm.cluster.indexOf(item);
|
||||||
|
if (index !== -1) {
|
||||||
|
dynamicValidateForm.cluster.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCluster = () => {
|
||||||
|
const id = Date.now();
|
||||||
|
dynamicValidateForm.cluster.push({
|
||||||
|
clusterNodeId: '',
|
||||||
|
port: '',
|
||||||
|
host: '',
|
||||||
|
publicPort: '',
|
||||||
|
publicHost: '',
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
activeKey.value = [...activeKey.value, id.toString()];
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOption = (input: string, option: any) => {
|
||||||
|
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeForm2Sip = (index: number) => {
|
||||||
|
dynamicValidateForm.cluster[index].port = '';
|
||||||
|
const value = dynamicValidateForm.cluster[index].host;
|
||||||
|
sipListIndex.value[index] = sipListConst
|
||||||
|
.find((i: any) => i.host === value)
|
||||||
|
?.portList.map((i: any) => {
|
||||||
|
return {
|
||||||
|
value: JSON.stringify({
|
||||||
|
host: value,
|
||||||
|
port: i.port,
|
||||||
|
}),
|
||||||
|
label: `${i.transports.join('/')} (${i.port})`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { resetFields, validate, validateInfos } = useForm(
|
||||||
|
form,
|
||||||
|
reactive({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveData = () => {
|
||||||
|
validate().then(async (values) => {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
...values,
|
||||||
|
provider: 'gb28181-2016',
|
||||||
|
transport: 'SIP',
|
||||||
|
channel: 'gb28181',
|
||||||
|
};
|
||||||
|
const resp = !!id
|
||||||
|
? await update({ ...params, id })
|
||||||
|
: await save(params);
|
||||||
|
if (resp.status === 200) {
|
||||||
|
message.success('操作成功!');
|
||||||
|
// if (params.get('save')) {
|
||||||
|
// if ((window as any).onTabSaveSuccess) {
|
||||||
|
// if (resp.result) {
|
||||||
|
// (window as any).onTabSaveSuccess(resp.result);
|
||||||
|
// setTimeout(() => window.close(), 300);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
history.back();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = async () => {
|
||||||
|
let data1: any = await formRef1.value?.validate();
|
||||||
|
if (data1.hostPort?.port) {
|
||||||
|
const port = JSON.parse(data1.hostPort.port).port;
|
||||||
|
data1.hostPort.port = port;
|
||||||
|
}
|
||||||
|
if (!data1?.shareCluster) {
|
||||||
|
let data2 = await formRef2.value?.validate();
|
||||||
|
if (data2 && data2?.cluster) {
|
||||||
|
data2.cluster.forEach((i: any) => {
|
||||||
|
i.enabled = true;
|
||||||
|
i.port = JSON.parse(i.port).port;
|
||||||
|
});
|
||||||
|
data1 = {
|
||||||
|
...data1,
|
||||||
|
...data2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.value = current.value + 1;
|
||||||
|
params.configuration = data1;
|
||||||
|
};
|
||||||
|
const prev = () => {
|
||||||
|
current.value = current.value - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getResourcesCurrent().then((resp) => {
|
||||||
|
if (resp.status === 200) {
|
||||||
|
sipListConst = resp.result;
|
||||||
|
sipListOption.value = sipListConst.map((i) => ({
|
||||||
|
value: i.host,
|
||||||
|
label: i.host,
|
||||||
|
}));
|
||||||
|
|
||||||
|
sipList.value = sipListConst
|
||||||
|
.find((i) => i.host === '0.0.0.0')
|
||||||
|
?.portList.map((i) => {
|
||||||
|
return {
|
||||||
|
value: JSON.stringify({
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: i.port,
|
||||||
|
}),
|
||||||
|
label: `${i.transports.join('/')} (${i.port})`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getClusters().then((resp) => {
|
||||||
|
if (resp.status === 200) {
|
||||||
|
const list = resp.result.map((i) => ({
|
||||||
|
value: i.id,
|
||||||
|
label: i.name,
|
||||||
|
}));
|
||||||
|
clustersList.value = list;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
current,
|
||||||
|
(v) => {
|
||||||
|
stepCurrent.value = v;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.steps-content {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.steps-box {
|
||||||
|
min-height: 400px;
|
||||||
|
.card-item {
|
||||||
|
padding-right: 5px;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.card-last {
|
||||||
|
padding-right: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.steps-action {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 10px;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
line-height: 40px;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
margin: 15px 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right {
|
||||||
|
padding: 20px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.config-right-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.config-right-item-title {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right-item-context {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item1 {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
height: 30px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
.form-label-required {
|
||||||
|
color: red;
|
||||||
|
margin: 0 4px 0 -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,30 +1,160 @@
|
||||||
<template>
|
<template>
|
||||||
<div style="margin-top: 10px">
|
<div class="container">
|
||||||
111
|
<div v-if="channel === 'fixed-media'" class="card-last">
|
||||||
</div>
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col :span="12">
|
||||||
|
<title-component data="基本信息" />
|
||||||
|
<div>
|
||||||
|
<a-form
|
||||||
|
:model="formState"
|
||||||
|
name="basic"
|
||||||
|
autocomplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="onFinish"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="名称"
|
||||||
|
name="name"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入名称',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
placeholder="请输入名称"
|
||||||
|
v-model:value="formState.name"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="说明" name="description">
|
||||||
|
<a-textarea
|
||||||
|
placeholder="请输入说明"
|
||||||
|
:rows="4"
|
||||||
|
v-model:value="formState.description"
|
||||||
|
show-count
|
||||||
|
:maxlength="200"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" html-type="submit"
|
||||||
|
>保存</a-button
|
||||||
|
>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<div class="config-right">
|
||||||
|
<div class="config-right-item">
|
||||||
|
<div class="config-right-item-title">接入方式</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{ provider.name }}
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{ provider.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-right-item">
|
||||||
|
<div class="config-right-item-title">消息协议</div>
|
||||||
|
<div class="config-right-item-context">
|
||||||
|
{{
|
||||||
|
provider.id === 'fixed-media'
|
||||||
|
? 'URL'
|
||||||
|
: 'SIP'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="channel === 'gb28181'">
|
||||||
|
<GB28181 :provider="props.provider"></GB28181>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup name="AccessMedia">
|
<script lang="ts" setup name="AccessMedia">
|
||||||
|
import { message, Form } from 'ant-design-vue';
|
||||||
|
import type { FormInstance } from 'ant-design-vue';
|
||||||
|
import GB28181 from './GB28181.vue';
|
||||||
|
import { update, save } from '@/api/link/accessConfig';
|
||||||
|
|
||||||
|
interface FormState {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
const route = useRoute();
|
||||||
|
const id = route.query.id;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
provider: {
|
provider: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = ref(props.provider.channel)
|
const channel = ref(props.provider.channel);
|
||||||
|
|
||||||
console.log(211,props);
|
|
||||||
|
|
||||||
|
|
||||||
|
const formState = reactive<FormState>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
const onFinish = async (values: any) => {
|
||||||
|
const params = {
|
||||||
|
...values,
|
||||||
|
provider: 'fixed-media',
|
||||||
|
transport: 'URL',
|
||||||
|
channel: 'fixed-media',
|
||||||
|
};
|
||||||
|
const resp = !!id ? await update({ ...params, id }) : await save(params);
|
||||||
|
if (resp.status === 200) {
|
||||||
|
message.success('操作成功!');
|
||||||
|
// if (params.get('save')) {
|
||||||
|
// if ((window as any).onTabSaveSuccess) {
|
||||||
|
// if (resp.result) {
|
||||||
|
// (window as any).onTabSaveSuccess(resp.result);
|
||||||
|
// setTimeout(() => window.close(), 300);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
history.back();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.container {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-last {
|
||||||
|
padding-right: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.config-right {
|
||||||
|
padding: 20px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.config-right-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.config-right-item-title {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-right-item-context {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="steps-content">
|
<div class="steps-content">
|
||||||
<div class="steps-box" v-if="current === 0">
|
<div class="steps-box" v-if="current === 0">
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
<a-icon type="info-circle" style="margin-right: 10px" />
|
<question-circle-outlined />
|
||||||
选择与设备通信的网络组件
|
选择与设备通信的网络组件
|
||||||
</div>
|
</div>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="steps-box" v-else-if="current === 1">
|
<div class="steps-box" v-else-if="current === 1">
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
<a-icon type="info-circle" style="margin-right: 10px" />
|
<question-circle-outlined />
|
||||||
使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作
|
使用选择的消息协议,对网络组件通信数据进行编解码、认证等操作
|
||||||
</div>
|
</div>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
|
@ -126,7 +126,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="steps-box" v-else>
|
<div class="steps-box" v-else>
|
||||||
<div class="card-last">
|
<div
|
||||||
|
class="card-last"
|
||||||
|
:style="`max-height:${
|
||||||
|
clientHeight > 900 ? 750 : clientHeight * 0.7
|
||||||
|
}px`"
|
||||||
|
>
|
||||||
<a-row :gutter="[24, 24]">
|
<a-row :gutter="[24, 24]">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<title-component data="基本信息" />
|
<title-component data="基本信息" />
|
||||||
|
@ -190,7 +195,6 @@
|
||||||
v-if="config.document"
|
v-if="config.document"
|
||||||
>
|
>
|
||||||
<Markdown :source="config.document" />
|
<Markdown :source="config.document" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -255,22 +259,32 @@
|
||||||
:scroll="{ y: 300 }"
|
:scroll="{ y: 300 }"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
#stream
|
#bodyCell="{ column, text, record }"
|
||||||
slot-scope="text, record"
|
|
||||||
>
|
>
|
||||||
<span
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
record.upstream &&
|
column.dataIndex ===
|
||||||
record.downstream
|
'stream'
|
||||||
"
|
"
|
||||||
>上行、下行</span
|
|
||||||
>
|
|
||||||
<span v-else-if="record.upstream"
|
|
||||||
>上行</span
|
|
||||||
>
|
|
||||||
<span v-else-if="record.downstream"
|
|
||||||
>下行</span
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
record.upstream &&
|
||||||
|
record.downstream
|
||||||
|
"
|
||||||
|
>上行、下行</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else-if="record.upstream"
|
||||||
|
>上行</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
record.downstream
|
||||||
|
"
|
||||||
|
>下行</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -281,11 +295,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="steps-action">
|
<div class="steps-action">
|
||||||
<a-button
|
<a-button v-if="[0, 1].includes(current)" @click="next">
|
||||||
v-if="[0, 1].includes(current)"
|
|
||||||
type="primary"
|
|
||||||
@click="next"
|
|
||||||
>
|
|
||||||
下一步
|
下一步
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-if="current === 2" type="primary" @click="saveData">
|
<a-button v-if="current === 2" type="primary" @click="saveData">
|
||||||
|
@ -314,10 +324,9 @@ import {
|
||||||
} from '../Detail/data';
|
} from '../Detail/data';
|
||||||
import AccessCard from './AccessCard/index.vue';
|
import AccessCard from './AccessCard/index.vue';
|
||||||
import { message, Form } from 'ant-design-vue';
|
import { message, Form } from 'ant-design-vue';
|
||||||
import type { FormInstance } from 'ant-design-vue';
|
import type { FormInstance, TableColumnType } from 'ant-design-vue';
|
||||||
import Markdown from 'vue3-markdown-it';
|
import Markdown from 'vue3-markdown-it';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
//测试数据1
|
//测试数据1
|
||||||
const resultList1 = [
|
const resultList1 = [
|
||||||
{
|
{
|
||||||
|
@ -363,13 +372,158 @@ const resultList1 = [
|
||||||
// metadata: '',
|
// metadata: '',
|
||||||
// };
|
// };
|
||||||
const result2 = {
|
const result2 = {
|
||||||
"id": "MQTT",
|
id: 'MQTT',
|
||||||
"name": "MQTT",
|
name: 'MQTT',
|
||||||
"features": [],
|
features: [
|
||||||
"routes": [],
|
{
|
||||||
"document": "# MQTT认证说明\r\nCONNECT报文:\r\n```text\r\nclientId: 设备ID\r\nusername: secureId+\"|\"+timestamp\r\npassword: md5(secureId+\"|\"+timestamp+\"|\"+secureKey)\r\n ```\r\n\r\n说明: secureId以及secureKey在创建设备产品或设备实例时进行配置. \r\ntimestamp为当前系统时间戳(毫秒),与系统时间不能相差5分钟.\r\nmd5为32位,不区分大小写.",
|
id: 'supportFirmware',
|
||||||
"metadata": "{\"functions\":[],\"name\":\"test\",\"description\":\"测试用\",\"id\":\"test\",\"properties\":[{\"valueType\":{\"round\":\"HALF_UP\",\"type\":\"double\"},\"name\":\"温度\",\"id\":\"t\"},{\"valueType\":{\"round\":\"HALF_UP\",\"type\":\"int\"},\"name\":\"状态\",\"id\":\"state\"}],\"events\":[],\"tags\":[]}"
|
name: '支持固件升级',
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/properties/report',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '属性上报',
|
||||||
|
description: '上报物模型属性数据',
|
||||||
|
example: '{"properties":{"属性ID":"属性值"}}',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/properties/report',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/properties/read',
|
||||||
|
upstream: false,
|
||||||
|
downstream: true,
|
||||||
|
qos: 0,
|
||||||
|
group: '读取属性',
|
||||||
|
description: '平台下发读取物模型属性数据指令',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,回复时需要一致.","properties":["属性ID"]}',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/properties/read',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/properties/read/reply',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '读取属性',
|
||||||
|
description: '对平台下发的读取属性指令进行响应',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,与读取指令中的ID一致.","properties":{"属性ID":"属性值"}}',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/properties/read/reply',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/properties/write',
|
||||||
|
upstream: false,
|
||||||
|
downstream: true,
|
||||||
|
qos: 0,
|
||||||
|
group: '修改属性',
|
||||||
|
description: '平台下发修改物模型属性数据指令',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,回复时需要一致.","properties":{"属性ID":"属性值"}}',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/properties/write',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/properties/write/reply',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '修改属性',
|
||||||
|
description: '对平台下发的修改属性指令进行响应',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,与修改指令中的ID一致.","properties":{"属性ID":"属性值"}}',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/properties/write/reply',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/event/{eventId:事件ID}',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '事件上报',
|
||||||
|
description: '上报物模型事件数据',
|
||||||
|
example: '{"data":{"key":"value"}}',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/event/{eventId:事件ID}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/function/invoke',
|
||||||
|
upstream: false,
|
||||||
|
downstream: true,
|
||||||
|
qos: 0,
|
||||||
|
group: '调用功能',
|
||||||
|
description: '平台下发功能调用指令',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,回复时需要一致.","functionId":"功能标识","inputs":[{"name":"参数名","value":"参数值"}]}',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/function/invoke',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/function/invoke/reply',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '调用功能',
|
||||||
|
description: '设备响应平台下发的功能调用指令',
|
||||||
|
example:
|
||||||
|
'{"messageId":"消息ID,与下发指令中的messageId一致.","output":"输出结果,格式与物模型中定义的类型一致"',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/function/invoke/reply',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/child/{childDeviceId:子设备ID}/{#:子设备相应操作的topic}',
|
||||||
|
upstream: true,
|
||||||
|
downstream: true,
|
||||||
|
qos: 0,
|
||||||
|
group: '子设备消息',
|
||||||
|
description: '网关上报或者平台下发子设备消息',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/child/{childDeviceId:子设备ID}/{#:子设备相应操作的topic}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/child-reply/{childDeviceId:子设备ID}/{#:子设备相应操作的topic}',
|
||||||
|
upstream: true,
|
||||||
|
downstream: true,
|
||||||
|
qos: 0,
|
||||||
|
group: '子设备消息',
|
||||||
|
description: '网关回复平台下发给子设备的指令结果',
|
||||||
|
address:
|
||||||
|
'/{productId:产品ID}/{deviceId:设备ID}/child-reply/{childDeviceId:子设备ID}/{#:子设备相应操作的topic}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/tags',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '更新标签',
|
||||||
|
description: '更新标签数据',
|
||||||
|
example: '{"tags":{"key","value"}}',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/tags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/online',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '状态管理',
|
||||||
|
description: '设备上线',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/online',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
topic: '/{productId:产品ID}/{deviceId:设备ID}/offline',
|
||||||
|
upstream: true,
|
||||||
|
downstream: false,
|
||||||
|
qos: 0,
|
||||||
|
group: '状态管理',
|
||||||
|
description: '设备离线',
|
||||||
|
address: '/{productId:产品ID}/{deviceId:设备ID}/offline',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
document:
|
||||||
|
'### 认证说明\r\n\r\nCONNECT报文:\r\n```text\r\nclientId: 设备ID\r\nusername: secureId+"|"+timestamp\r\npassword: md5(secureId+"|"+timestamp+"|"+secureKey)\r\n ```\r\n\r\n说明: secureId以及secureKey在创建设备产品或设备实例时进行配置. \r\ntimestamp为当前时间戳(毫秒),与服务器时间不能相差5分钟.\r\nmd5为32位,不区分大小写.',
|
||||||
|
metadata: '',
|
||||||
|
};
|
||||||
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
var d = new Date().getTime();
|
var d = new Date().getTime();
|
||||||
|
@ -400,6 +554,8 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clientHeight = document.body.clientHeight;
|
||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
@ -412,7 +568,7 @@ const allProcotolList = ref([]);
|
||||||
const networkCurrent = ref('');
|
const networkCurrent = ref('');
|
||||||
const procotolCurrent = ref('');
|
const procotolCurrent = ref('');
|
||||||
let config = ref({});
|
let config = ref({});
|
||||||
let columnsMQTT = ref([]);
|
let columnsMQTT = ref(<TableColumnType>[]);
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -422,14 +578,18 @@ const { resetFields, validate, validateInfos } = useForm(
|
||||||
form,
|
form,
|
||||||
reactive({
|
reactive({
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '请输入证书名称', trigger: 'blur' },
|
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||||
{ max: 64, message: '最多可输入64个字符' },
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryNetworkList = async (id: string, params: object, data = {}) => {
|
const queryNetworkList = async (id: string, include: string, data = {}) => {
|
||||||
const resp = await getNetworkList(NetworkTypeMapping.get(id), data, params);
|
const resp = await getNetworkList(
|
||||||
|
NetworkTypeMapping.get(id),
|
||||||
|
include,
|
||||||
|
data,
|
||||||
|
);
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
networkList.value = resp.result;
|
networkList.value = resp.result;
|
||||||
}
|
}
|
||||||
|
@ -463,9 +623,7 @@ const addNetwork = () => {
|
||||||
tab.onTabSaveSuccess = (value) => {
|
tab.onTabSaveSuccess = (value) => {
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
networkCurrent.value = value.result.id;
|
networkCurrent.value = value.result.id;
|
||||||
queryNetworkList(props.provider?.id, {
|
queryNetworkList(props.provider?.id, networkCurrent.value || '');
|
||||||
include: networkCurrent.value || '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -488,20 +646,14 @@ const checkedChange = (id: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkSearch = (value: string) => {
|
const networkSearch = (value: string) => {
|
||||||
queryNetworkList(
|
queryNetworkList(props.provider.id, networkCurrent.value || '', {
|
||||||
props.provider.id,
|
terms: [
|
||||||
{
|
{
|
||||||
include: networkCurrent.value || '',
|
column: 'name$LIKE',
|
||||||
},
|
value: `%${value}%`,
|
||||||
{
|
},
|
||||||
terms: [
|
],
|
||||||
{
|
});
|
||||||
column: 'name$LIKE',
|
|
||||||
value: `%${value}%`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
const procotolChange = (id: string) => {
|
const procotolChange = (id: string) => {
|
||||||
if (!props.data.id) {
|
if (!props.data.id) {
|
||||||
|
@ -576,7 +728,7 @@ const next = async () => {
|
||||||
//使用测试数据2
|
//使用测试数据2
|
||||||
config.value = result2;
|
config.value = result2;
|
||||||
current.value = current.value + 1;
|
current.value = current.value + 1;
|
||||||
columnsMQTT = [
|
columnsMQTT.value = [
|
||||||
{
|
{
|
||||||
title: '分组',
|
title: '分组',
|
||||||
dataIndex: 'group',
|
dataIndex: 'group',
|
||||||
|
@ -584,20 +736,24 @@ const next = async () => {
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
customRender: (value, row, index) => {
|
customCell: (record: object, rowIndex: number) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
children: value,
|
children: record,
|
||||||
attrs: {},
|
rowSpan: 0,
|
||||||
};
|
};
|
||||||
const list = (config && config.routes) || [];
|
const list =
|
||||||
const arr = list.filter((res) => {
|
(config.value && config.value.routes) || [];
|
||||||
return res.group == row.group;
|
|
||||||
});
|
const arr = list.filter(
|
||||||
if (index == 0 || list[index - 1].group !== row.group) {
|
(res: object) => res.group == record.group,
|
||||||
obj.attrs.rowSpan = arr.length;
|
);
|
||||||
} else {
|
|
||||||
obj.attrs.rowSpan = 0;
|
if (
|
||||||
}
|
rowIndex == 0 ||
|
||||||
|
list[rowIndex - 1].group !== record.group
|
||||||
|
)
|
||||||
|
obj.rowSpan = arr.length;
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -605,6 +761,7 @@ const next = async () => {
|
||||||
title: 'topic',
|
title: 'topic',
|
||||||
dataIndex: 'topic',
|
dataIndex: 'topic',
|
||||||
key: 'topic',
|
key: 'topic',
|
||||||
|
align: 'center',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -614,7 +771,6 @@ const next = async () => {
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
scopedSlots: { customRender: 'stream' },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '说明',
|
title: '说明',
|
||||||
|
@ -698,9 +854,7 @@ onMounted(() => {
|
||||||
procotolCurrent.value = props.data.protocol;
|
procotolCurrent.value = props.data.protocol;
|
||||||
current.value = 0;
|
current.value = 0;
|
||||||
networkCurrent.value = props.data.channelId;
|
networkCurrent.value = props.data.channelId;
|
||||||
queryNetworkList(props.provider.id, {
|
queryNetworkList(props.provider.id, networkCurrent.value);
|
||||||
include: networkCurrent.value,
|
|
||||||
});
|
|
||||||
procotolCurrent.value = props.data.protocol;
|
procotolCurrent.value = props.data.protocol;
|
||||||
steps.value = ['网络组件', '消息协议', '完成'];
|
steps.value = ['网络组件', '消息协议', '完成'];
|
||||||
} else {
|
} else {
|
||||||
|
@ -711,9 +865,7 @@ onMounted(() => {
|
||||||
} else {
|
} else {
|
||||||
if (props.provider?.id) {
|
if (props.provider?.id) {
|
||||||
if (props.provider.channel !== 'child-device') {
|
if (props.provider.channel !== 'child-device') {
|
||||||
queryNetworkList(props.provider.id, {
|
queryNetworkList(props.provider.id, '');
|
||||||
include: '',
|
|
||||||
});
|
|
||||||
steps.value = ['网络组件', '消息协议', '完成'];
|
steps.value = ['网络组件', '消息协议', '完成'];
|
||||||
current.value = 0;
|
current.value = 0;
|
||||||
} else {
|
} else {
|
||||||
|
@ -755,7 +907,6 @@ watch(
|
||||||
}
|
}
|
||||||
.card-last {
|
.card-last {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
max-height: 580px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<a-upload
|
<a-upload
|
||||||
accept=".pem"
|
accept=".pem"
|
||||||
listType="text"
|
listType="text"
|
||||||
:action="`${BASE_API_PATH}${NETWORK_CERTIFICATE_UPLOAD}`"
|
:action="NETWORK_CERTIFICATE_UPLOAD"
|
||||||
:headers="{
|
:headers="{
|
||||||
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
|
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
|
||||||
}"
|
}"
|
||||||
|
@ -31,11 +31,8 @@ import { UploadOutlined } from '@ant-design/icons-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import type { UploadChangeParam } from 'ant-design-vue';
|
import type { UploadChangeParam } from 'ant-design-vue';
|
||||||
import { LocalStore } from '@/utils/comm';
|
import { LocalStore } from '@/utils/comm';
|
||||||
import {
|
import { TOKEN_KEY } from '@/utils/variable';
|
||||||
BASE_API_PATH,
|
import { NETWORK_CERTIFICATE_UPLOAD } from '@/api/link/certificate';
|
||||||
TOKEN_KEY,
|
|
||||||
NETWORK_CERTIFICATE_UPLOAD,
|
|
||||||
} from '@/utils/variable';
|
|
||||||
import type { UploadProps } from 'ant-design-vue';
|
import type { UploadProps } from 'ant-design-vue';
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change']);
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
|
@ -97,12 +97,6 @@ import { message, Form } from 'ant-design-vue';
|
||||||
import { getImage } from '@/utils/comm';
|
import { getImage } from '@/utils/comm';
|
||||||
import CertificateFile from './CertificateFile.vue';
|
import CertificateFile from './CertificateFile.vue';
|
||||||
import type { UploadChangeParam } from 'ant-design-vue';
|
import type { UploadChangeParam } from 'ant-design-vue';
|
||||||
import { LocalStore } from '@/utils/comm';
|
|
||||||
import {
|
|
||||||
BASE_API_PATH,
|
|
||||||
TOKEN_KEY,
|
|
||||||
NETWORK_CERTIFICATE_UPLOAD,
|
|
||||||
} from '@/utils/variable';
|
|
||||||
import { save } from '@/api/link/certificate';
|
import { save } from '@/api/link/certificate';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div>123</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<JTable
|
||||||
|
:columns="columns"
|
||||||
|
:request="request"
|
||||||
|
>
|
||||||
|
<template #headerTitle>
|
||||||
|
<a-button type="primary" @click="add">新增</a-button>
|
||||||
|
</template>
|
||||||
|
<template #card="slotProps">
|
||||||
|
<CardBox
|
||||||
|
:value="slotProps"
|
||||||
|
@click="handleClick"
|
||||||
|
:actions="getActions(slotProps)"
|
||||||
|
v-bind="slotProps"
|
||||||
|
:status="slotProps.state ? 'success' : 'error'"
|
||||||
|
>
|
||||||
|
<template #img>
|
||||||
|
<slot name="img">
|
||||||
|
<img :src="getImage('/device-product.png')" />
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<h3>{{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-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
|
||||||
|
<a-button :disabled="item.disabled">
|
||||||
|
<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">
|
||||||
|
<DeleteOutlined v-if="item.key === 'delete'" />
|
||||||
|
<template v-else>
|
||||||
|
<AIcon :type="item.icon" />
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</CardBox>
|
||||||
|
</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">
|
||||||
|
<a-button :disabled="i.disabled" style="padding: 0" type="link"><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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { query } from '@/api/northbound/dueros'
|
||||||
|
import type { ActionsType } from '@/components/Table/index.vue'
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
|
const request = (data: any) => query({})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
scopedSlots: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'classifiedName',
|
||||||
|
key: 'classifiedName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 250,
|
||||||
|
scopedSlots: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleClick = (dt: any) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||||
|
if(!data){
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
text: "编辑",
|
||||||
|
tooltip: {
|
||||||
|
title: '编辑'
|
||||||
|
},
|
||||||
|
icon: 'icon-rizhifuwu'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'import',
|
||||||
|
text: "导入",
|
||||||
|
tooltip: {
|
||||||
|
title: '导入'
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
icon: 'icon-xiazai'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
text: "删除",
|
||||||
|
tooltip: {
|
||||||
|
title: !!data?.state ? '正常的产品不能删除' : '删除'
|
||||||
|
},
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除?'
|
||||||
|
},
|
||||||
|
icon: 'icon-huishouzhan'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = () => {
|
||||||
|
message.warn('123')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,65 +1,63 @@
|
||||||
<!-- webhook请求头可编辑表格 -->
|
<!-- webhook请求头可编辑表格 -->
|
||||||
<template>
|
<template>
|
||||||
<a-table
|
<div class="table-wrapper">
|
||||||
:columns="columns"
|
<a-table
|
||||||
:data-source="dataSource"
|
:columns="columns"
|
||||||
bordered
|
:data-source="dataSource"
|
||||||
:pagination="false"
|
bordered
|
||||||
>
|
:pagination="false"
|
||||||
<template #bodyCell="{ column, text, record }">
|
>
|
||||||
<template v-if="['KEY', 'VALUE'].includes(column.dataIndex)">
|
<template #bodyCell="{ column, text, record }">
|
||||||
<a-input v-model="record[column.dataIndex]" />
|
<template v-if="['key', 'value'].includes(column.dataIndex)">
|
||||||
|
<a-input v-model:value="record[column.dataIndex]" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.dataIndex === 'operation'">
|
||||||
|
<a-button type="text">
|
||||||
|
<template #icon>
|
||||||
|
<delete-outlined @click="handleDelete(record.id)" />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.dataIndex === 'operation'">
|
</a-table>
|
||||||
<a-button type="text">
|
<a-button
|
||||||
<template #icon>
|
type="dashed"
|
||||||
<delete-outlined @click="handleDelete(record.idx)" />
|
@click="handleAdd"
|
||||||
</template>
|
style="width: 100%; margin-top: 5px"
|
||||||
</a-button>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<plus-outlined />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
添加
|
||||||
</a-table>
|
</a-button>
|
||||||
<a-button
|
</div>
|
||||||
type="dashed"
|
|
||||||
@click="handleAdd"
|
|
||||||
style="width: 100%; margin-top: 5px"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<plus-outlined />
|
|
||||||
</template>
|
|
||||||
添加
|
|
||||||
</a-button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
// import { cloneDeep } from 'lodash-es';
|
import { PropType } from 'vue';
|
||||||
// import { defineComponent, reactive, ref } from 'vue';
|
import { IHeaders } from '../../types';
|
||||||
// import type { UnwrapRef } from 'vue';
|
|
||||||
|
|
||||||
interface DataItem {
|
type Emits = {
|
||||||
idx: number;
|
(e: 'update:headers', data: IHeaders[]): void;
|
||||||
KEY: string;
|
};
|
||||||
VALUE: string;
|
const emit = defineEmits<Emits>();
|
||||||
}
|
|
||||||
|
|
||||||
const data: DataItem[] = [];
|
const props = defineProps({
|
||||||
for (let i = 0; i < 2; i++) {
|
headers: {
|
||||||
data.push({
|
type: Array as PropType<IHeaders[]>,
|
||||||
idx: i,
|
default: () => [],
|
||||||
KEY: `key ${i}`,
|
},
|
||||||
VALUE: `value${i}`,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'KEY',
|
title: 'KEY',
|
||||||
dataIndex: 'KEY',
|
dataIndex: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'VALUE',
|
title: 'VALUE',
|
||||||
dataIndex: 'VALUE',
|
dataIndex: 'value',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
@ -69,17 +67,20 @@ const columns = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataSource = ref(data);
|
const dataSource = computed({
|
||||||
console.log('dataSource: ', dataSource.value);
|
get: () => props.headers,
|
||||||
|
set: (val) => emit('update:headers', val),
|
||||||
|
});
|
||||||
|
|
||||||
const handleDelete = (idx: number) => {
|
const handleDelete = (id: number) => {
|
||||||
|
const idx = dataSource.value.findIndex((f) => f.id === id);
|
||||||
dataSource.value.splice(idx, 1);
|
dataSource.value.splice(idx, 1);
|
||||||
};
|
};
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
dataSource.value.push({
|
dataSource.value.push({
|
||||||
idx: dataSource.value.length + 1,
|
id: dataSource.value.length,
|
||||||
KEY: `key ${dataSource.value.length + 1}`,
|
key: '',
|
||||||
VALUE: `value ${dataSource.value.length + 1}`,
|
value: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const AliyunSms = () => {
|
||||||
|
const accessKey = getImage(
|
||||||
|
'/notice/doc/config/aliyun-sms-voice/AccesskeyIDSecret.jpg',
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
阿里云管理控制台:
|
||||||
|
<a
|
||||||
|
href="https://home.console.aliyun.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://home.console.aliyun.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1.概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、RegionID</h2>
|
||||||
|
<div>
|
||||||
|
阿里云内部给每台机器设置的唯一编号。请根据购买的阿里云服务器地址进行填写。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
阿里云地域和可用区对照表地址:https://help.aliyun.com/document_detail/40654.html?spm=a2c6h.13066369.0.0.54a174710O7rWH
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2、AccesskeyID/Secret</h2>
|
||||||
|
<div>
|
||||||
|
<div>用于程序通知方式调用云服务费API的用户标识和秘钥</div>
|
||||||
|
<div>
|
||||||
|
获取路径:“阿里云管理控制台”--“用户头像”--“”--“AccessKey管理”--“查看”
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={accessKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AliyunSms;
|
|
@ -0,0 +1,45 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const AliyunVoice = () => {
|
||||||
|
const accessKey = getImage(
|
||||||
|
'/notice/doc/config/aliyun-sms-voice/AccesskeyIDSecret.jpg',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
阿里云管理控制台:
|
||||||
|
<a
|
||||||
|
href="https://home.console.aliyun.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://home.console.aliyun.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、RegionID</h2>
|
||||||
|
<div>
|
||||||
|
阿里云服务地域与对应的RegionID。请根据购买的阿里云服务器地域进行选择。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2、AccesskeyID/Secret</h2>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
用于程序通知方式调用云服务费API的用户标识和秘钥获取路径:“阿里云管理控制台”--“用户头像”--“”--“AccessKey管理”--“查看”
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={accessKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AliyunVoice;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const DingTalk = () => {
|
||||||
|
const appKey = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-message/01-AppKey.jpg',
|
||||||
|
);
|
||||||
|
const appSecret = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-message/02-AppSecret.jpg',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
钉钉开放平台:
|
||||||
|
<a
|
||||||
|
href="https://open-dev.dingtalk.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://open-dev.dingtalk.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、AppKey</h2>
|
||||||
|
<div>
|
||||||
|
企业内部应用的唯一身份标识。在钉钉开发者后台创建企业内部应用后,系统会自动生成一对AppKey和AppSecret。
|
||||||
|
</div>
|
||||||
|
<div>获取路径:“钉钉开放平台”--“应用开发”--“应用信息”</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2、AppSecret</h2>
|
||||||
|
<div>
|
||||||
|
<div>钉钉应用对应的调用密钥</div>
|
||||||
|
<div>获取路径:“钉钉开放平台”--“应用开发”--“应用信息”</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appSecret} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DingTalk;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const DingTalkRebot = () => {
|
||||||
|
const groupSetting = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-rebot/01-group-setting.jpg',
|
||||||
|
);
|
||||||
|
const rebot = getImage('/notice/doc/config/dingTalk-rebot/02-rebot.jpg');
|
||||||
|
const webhook = getImage(
|
||||||
|
'/notice/doc/config/dingTalk-rebot/03-Webhook.jpg',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2> 1、WebHook</h2>
|
||||||
|
<div>
|
||||||
|
在钉钉群内每创建一个钉钉群自定义机器人都会产生唯一的WebHook地址。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
获取路径:“钉钉桌面客户端”--“群设置”--“智能群助手”--“机器人信息”
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={rebot} />
|
||||||
|
</div>
|
||||||
|
<h2>1、登录钉钉桌面客户端,进入群设置</h2>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={groupSetting} />
|
||||||
|
</div>
|
||||||
|
<h2>2、点击智能群助手,查看机器人信息</h2>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={webhook} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DingTalkRebot;
|
|
@ -0,0 +1,27 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const Email = () => {
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<h2>1、 服务器地址</h2>
|
||||||
|
<div>
|
||||||
|
下拉可选择国内常用的邮箱服务配置,也支持手动输入其他地址。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
系统使用POP协议。POP允许电子邮件客户端下载服务器上的邮件,但是您在电子邮件客户端的操作(如:移动邮件、标记已读等),这时不会反馈到服务器上。
|
||||||
|
</div>
|
||||||
|
<h2>2、发件人</h2>
|
||||||
|
<div>用于发送邮件时“发件人“信息的显示</div>
|
||||||
|
<h2>3、 用户名</h2>
|
||||||
|
<div>用该账号进行发送邮件。</div>
|
||||||
|
<h2>4、密码</h2>
|
||||||
|
<div>用于账号身份认证,认证通过后可通过该账号进行发送邮件。</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Email;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const Webhook = () => {
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
webhook是一个接收HTTP请求的URL(本平台默认只支持HTTP
|
||||||
|
POST请求),实现了Webhook的第三方系统可以基于该URL订阅本平台系统信息,本平台按配置把特定的事件结果推送到指定的地址,便于系统做后续处理。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<h2>1、Webhook</h2>
|
||||||
|
<div>Webhook地址。</div>
|
||||||
|
|
||||||
|
<h2>2、请求头</h2>
|
||||||
|
<div>
|
||||||
|
支持根据系统提供的接口设置不同的请求头。如 Accept-Language
|
||||||
|
、Content-Type
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Webhook;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const WeixinApp = () => {
|
||||||
|
const appId = getImage('/notice/doc/config/weixin-official/01-AppID.jpg');
|
||||||
|
const appSecret = getImage(
|
||||||
|
'/notice/doc/config/weixin-official/02-AppSecret.jpg',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
微信公众平台:
|
||||||
|
<a
|
||||||
|
href="https://mp.weixin.qq.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://mp.weixin.qq.com/
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、AppID</h2>
|
||||||
|
<div>微信服务号的唯一专属编号。</div>
|
||||||
|
<div>
|
||||||
|
获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>2、AppSecret</h2>
|
||||||
|
<div>
|
||||||
|
<div>公众号开发者身份的密码</div>
|
||||||
|
<div>
|
||||||
|
获取路径:“微信公众平台”管理后台--“设置与开发”--“基本配置”
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={appSecret} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default WeixinApp;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const WeixinCorp = () => {
|
||||||
|
const corpId = getImage(
|
||||||
|
'/notice/doc/config/weixin-corp/01-corpId.jpg',
|
||||||
|
);
|
||||||
|
const corpSecret = getImage(
|
||||||
|
'/notice/doc/config/weixin-corp/02-corpSecret.jpg',
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div class={'doc'}>
|
||||||
|
<div class={'url'}>
|
||||||
|
企业微信管理后台:
|
||||||
|
<a
|
||||||
|
href="https://work.weixin.qq.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://work.weixin.qq.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知配置可以结合通知配置为告警消息通知提供支撑。也可以用于系统中其他自定义模块的调用。
|
||||||
|
</div>
|
||||||
|
<h1>2.通知配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、corpId</h2>
|
||||||
|
<div>企业号的唯一专属编号。</div>
|
||||||
|
<div>获取路径:“企业微信”管理后台--“我的企业”--“企业ID”</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={corpId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>2、corpSecret</h2>
|
||||||
|
<div>
|
||||||
|
<div>应用的唯一secret,一个企业微信中可以有多个corpSecret</div>
|
||||||
|
<div>
|
||||||
|
获取路径:“企业微信”--“应用与小程序”--“自建应用”中获取
|
||||||
|
</div>
|
||||||
|
<div class={'image'}>
|
||||||
|
<Image width="100%" src={corpSecret} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default WeixinCorp;
|
|
@ -0,0 +1,35 @@
|
||||||
|
.doc {
|
||||||
|
height: 750px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: rgba(#000, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
.url {
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: #2f54eb;
|
||||||
|
background-color: rgba(#a7bdf7, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 16px 0;
|
||||||
|
color: rgba(#000, 0.85);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 6px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import DingTalk from './DingTalk';
|
||||||
|
import DingTalkRebot from './DingTalkRebot';
|
||||||
|
import AliyunSms from './AliyunSms';
|
||||||
|
import AliyunVoice from './AliyunVoice';
|
||||||
|
import Email from './Email';
|
||||||
|
import Webhook from './Webhook';
|
||||||
|
import WeixinApp from './WeixinApp';
|
||||||
|
import WeixinCorp from './WeixinCorp';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Doc',
|
||||||
|
props: {
|
||||||
|
docData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const docMap = {
|
||||||
|
weixin: {
|
||||||
|
corpMessage: <WeixinCorp />,
|
||||||
|
officialMessage: <WeixinApp />,
|
||||||
|
},
|
||||||
|
dingTalk: {
|
||||||
|
dingTalkMessage: <DingTalk />,
|
||||||
|
dingTalkRobotWebHook: <DingTalkRebot />,
|
||||||
|
},
|
||||||
|
voice: {
|
||||||
|
aliyun: <AliyunVoice />,
|
||||||
|
},
|
||||||
|
sms: {
|
||||||
|
aliyunSms: <AliyunSms />,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
embedded: <Email />,
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
http: <Webhook />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
docMap?.[props.docData.type]?.[props.docData.provider]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
});
|
|
@ -259,6 +259,7 @@
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
|
:loading="btnLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
|
@ -266,14 +267,16 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" :push="2"></a-col>
|
<a-col :span="12" :push="2">
|
||||||
|
<Doc :docData="formData" />
|
||||||
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getImage, LocalStore } from '@/utils/comm';
|
import { getImage } from '@/utils/comm';
|
||||||
import { Form } from 'ant-design-vue';
|
import { Form } from 'ant-design-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { ConfigFormData } from '../types';
|
import { ConfigFormData } from '../types';
|
||||||
|
@ -283,8 +286,12 @@ import {
|
||||||
MSG_TYPE,
|
MSG_TYPE,
|
||||||
} from '@/views/notice/const';
|
} from '@/views/notice/const';
|
||||||
import regionList from './regionId';
|
import regionList from './regionId';
|
||||||
import EditTable from './components/EditTable.vue'
|
import EditTable from './components/EditTable.vue';
|
||||||
|
import configApi from '@/api/notice/config';
|
||||||
|
import Doc from './doc/index';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
// 消息类型
|
// 消息类型
|
||||||
|
@ -306,25 +313,31 @@ const formData = ref<ConfigFormData>({
|
||||||
configuration: {
|
configuration: {
|
||||||
appKey: '',
|
appKey: '',
|
||||||
appSecret: '',
|
appSecret: '',
|
||||||
url: '',
|
|
||||||
},
|
},
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'dingTalkMessage',
|
provider: 'dingTalkMessage',
|
||||||
type: NOTICE_METHOD[0].value,
|
type: 'dingTalk',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据通知方式展示对应的字段
|
// 根据通知方式展示对应的字段
|
||||||
watch(
|
watch(
|
||||||
() => formData.value.type,
|
() => formData.value.type,
|
||||||
(val) => {
|
(val) => {
|
||||||
formData.value.configuration = CONFIG_FIELD_MAP[val];
|
// formData.value.configuration = Object.values<any>(CONFIG_FIELD_MAP[val])[0];
|
||||||
msgType.value = MSG_TYPE[val];
|
msgType.value = MSG_TYPE[val];
|
||||||
|
|
||||||
formData.value.provider = msgType.value[0].value;
|
formData.value.provider = msgType.value[0].value;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
computed(() =>
|
||||||
|
Object.assign(
|
||||||
|
formData.value.configuration,
|
||||||
|
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 验证规则
|
// 验证规则
|
||||||
const formRules = ref({
|
const formRules = ref({
|
||||||
type: [{ required: true, message: '请选择通知方式' }],
|
type: [{ required: true, message: '请选择通知方式' }],
|
||||||
|
@ -383,25 +396,56 @@ const formRules = ref({
|
||||||
pattern:
|
pattern:
|
||||||
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
|
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
|
||||||
message: 'Webhook需要是一个合法的URL',
|
message: 'Webhook需要是一个合法的URL',
|
||||||
trigger: 'blur',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetFields, validate, validateInfos } = useForm(
|
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||||
formData.value,
|
formData.value,
|
||||||
formRules.value,
|
formRules.value,
|
||||||
);
|
);
|
||||||
console.log('validateInfos: ', validateInfos);
|
watch(
|
||||||
|
() => formData.value.type,
|
||||||
|
() => {
|
||||||
|
clearValidate();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDetail = async () => {
|
||||||
|
const res = await configApi.detail(route.params.id as string);
|
||||||
|
// console.log('res: ', res);
|
||||||
|
formData.value = res.result;
|
||||||
|
// console.log('formData.value: ', formData.value);
|
||||||
|
};
|
||||||
|
getDetail();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单提交
|
* 表单提交
|
||||||
*/
|
*/
|
||||||
|
const btnLoading = ref<boolean>(false);
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
validate()
|
validate()
|
||||||
.then(async () => {})
|
.then(async () => {
|
||||||
.catch((err) => {});
|
// console.log('formData.value: ', formData.value);
|
||||||
|
btnLoading.value = true;
|
||||||
|
let res;
|
||||||
|
if (!formData.value.id) {
|
||||||
|
res = await configApi.save(formData.value);
|
||||||
|
} else {
|
||||||
|
res = await configApi.update(formData.value);
|
||||||
|
}
|
||||||
|
// console.log('res: ', res);
|
||||||
|
if (res?.success) {
|
||||||
|
message.success('保存成功');
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
btnLoading.value = false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err: ', err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,20 @@
|
||||||
<div class="page-container">通知配置</div>
|
<div class="page-container">通知配置</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import configApi from '@/api/notice/config';
|
||||||
|
|
||||||
|
const getList = async () => {
|
||||||
|
const res = await configApi.list({
|
||||||
|
current: 1,
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 12,
|
||||||
|
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||||
|
terms: [],
|
||||||
|
});
|
||||||
|
console.log('res: ', res);
|
||||||
|
};
|
||||||
|
getList();
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped></style>
|
||||||
|
|
|
@ -1,37 +1,70 @@
|
||||||
interface IHeaders {
|
export interface IHeaders {
|
||||||
|
id?: number;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
export interface IConfiguration {
|
||||||
|
// 钉钉
|
||||||
|
appKey?: string;
|
||||||
|
appSecret?: string;
|
||||||
|
url?: string;
|
||||||
|
// 微信
|
||||||
|
corpId?: string;
|
||||||
|
corpSecret?: string;
|
||||||
|
// 邮件
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
ssl?: boolean;
|
||||||
|
sender?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
// 语音
|
||||||
|
regionId?: string;
|
||||||
|
accessKeyId?: string;
|
||||||
|
secret?: string;
|
||||||
|
// 短信
|
||||||
|
regionId?: string;
|
||||||
|
accessKeyId?: string;
|
||||||
|
secret?: string;
|
||||||
|
// webhook
|
||||||
|
// url?: string;
|
||||||
|
headers?: IHeaders[];
|
||||||
|
}
|
||||||
export type ConfigFormData = {
|
export type ConfigFormData = {
|
||||||
configuration: {
|
configuration: IConfiguration;
|
||||||
// 钉钉
|
// configuration: {
|
||||||
appKey?: string;
|
// // 钉钉
|
||||||
appSecret?: string;
|
// appKey?: string;
|
||||||
url?: string;
|
// appSecret?: string;
|
||||||
// 微信
|
// url?: string;
|
||||||
corpId?: string;
|
// // 微信
|
||||||
corpSecret?: string;
|
// corpId?: string;
|
||||||
// 邮件
|
// corpSecret?: string;
|
||||||
host?: string;
|
// // 邮件
|
||||||
port?: number;
|
// host?: string;
|
||||||
ssl?: boolean;
|
// port?: number;
|
||||||
sender?: string;
|
// ssl?: boolean;
|
||||||
username?: string;
|
// sender?: string;
|
||||||
password?: string;
|
// username?: string;
|
||||||
// 语音
|
// password?: string;
|
||||||
regionId?: string;
|
// // 语音
|
||||||
accessKeyId?: string;
|
// regionId?: string;
|
||||||
secret?: string;
|
// accessKeyId?: string;
|
||||||
// 短信
|
// secret?: string;
|
||||||
regionId?: string;
|
// // 短信
|
||||||
accessKeyId?: string;
|
// regionId?: string;
|
||||||
secret?: string;
|
// accessKeyId?: string;
|
||||||
// webhook
|
// secret?: string;
|
||||||
// url?: string;
|
// // webhook
|
||||||
headers?: IHeaders[];
|
// // url?: string;
|
||||||
};
|
// headers?: IHeaders[];
|
||||||
|
// };
|
||||||
description: string;
|
description: string;
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
id?: string;
|
||||||
|
maxRetryTimes?: number;
|
||||||
|
creatorId?: string;
|
||||||
|
createTime?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
<!-- webhook请求头可编辑表格 -->
|
||||||
|
<template>
|
||||||
|
<div class="attachment-wrapper">
|
||||||
|
<div
|
||||||
|
class="attachment-item"
|
||||||
|
v-for="(item, index) in fileList"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="item.name">
|
||||||
|
<template #addonAfter>
|
||||||
|
<a-upload
|
||||||
|
name="file"
|
||||||
|
:action="FILE_UPLOAD"
|
||||||
|
:headers="{
|
||||||
|
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
|
||||||
|
}"
|
||||||
|
:showUploadList="false"
|
||||||
|
@change="handleChange"
|
||||||
|
>
|
||||||
|
<upload-outlined />
|
||||||
|
</a-upload>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<delete-outlined @click="handleDelete" style="cursor: pointer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
type="dashed"
|
||||||
|
@click="handleAdd"
|
||||||
|
style="width: 100%; margin-top: 5px"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<plus-outlined />
|
||||||
|
</template>
|
||||||
|
添加
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="Attachments">
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { IAttachments } from '../../types';
|
||||||
|
import { FILE_UPLOAD } from '@/api/comm';
|
||||||
|
import { LocalStore } from '@/utils/comm';
|
||||||
|
import { TOKEN_KEY } from '@/utils/variable';
|
||||||
|
import { UploadChangeParam } from 'ant-design-vue';
|
||||||
|
|
||||||
|
type Emits = {
|
||||||
|
(e: 'update:attachments', data: IAttachments[]): void;
|
||||||
|
};
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attachments: {
|
||||||
|
type: Array as PropType<IAttachments[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (info: UploadChangeParam) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
const result = info.file.response?.result;
|
||||||
|
console.log('result: ', result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileList = ref<IAttachments[]>([]);
|
||||||
|
watch(
|
||||||
|
() => props.attachments,
|
||||||
|
(val) => {
|
||||||
|
fileList.value = val;
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
const idx = fileList.value.findIndex((f) => f.id === id);
|
||||||
|
fileList.value.splice(idx, 1);
|
||||||
|
emit('update:attachments', fileList.value);
|
||||||
|
};
|
||||||
|
const handleAdd = () => {
|
||||||
|
fileList.value.push({
|
||||||
|
id: fileList.value.length,
|
||||||
|
name: '',
|
||||||
|
location: '',
|
||||||
|
});
|
||||||
|
emit('update:attachments', fileList.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.attachment-wrapper {
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,43 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const AliyunSms = () => {
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
阿里云短信服务平台:
|
||||||
|
<a
|
||||||
|
href="https://dysms.console.aliyun.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://dysms.console.aliyun.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
使用阿里云短信时需先在阿里云短信服务平台创建短信模板。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2> 1、绑定配置</h2>
|
||||||
|
<div> 使用固定的通知配置发送此通知模板</div>
|
||||||
|
<h2> 2、模板</h2>
|
||||||
|
<div> 阿里云短信平台自定义的模板名称</div>
|
||||||
|
<h2> 3、收信人</h2>
|
||||||
|
<div>
|
||||||
|
{' '}
|
||||||
|
当前仅支持国内手机号,此处若不填,则在模板调试和配置告警通知时手动填写
|
||||||
|
</div>
|
||||||
|
<h2> 4、签名</h2>
|
||||||
|
<div> 用于短信内容签名信息显示,需在阿里云短信进行配置。</div>
|
||||||
|
<h2> 5、变量属性</h2>
|
||||||
|
<div>
|
||||||
|
需要在当前页面手动设置与阿里云短信模板中一样的变量,否则会导致发送异常。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AliyunSms;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const AliyunVoice = () => {
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
阿里云语音服务平台:
|
||||||
|
<a href="https://account.console.aliyun.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://account.console.aliyun.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
使用阿里云语音时需先在阿里云语音服务平台创建语音模板。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、绑定配置</h2>
|
||||||
|
<div> 使用固定的通知配置发送此通知模板</div>
|
||||||
|
<h2>2、类型</h2>
|
||||||
|
<div> 阿里云语音通知类型,当类型为验证码类型时可配置变量。</div>
|
||||||
|
<h2> 3、模板ID</h2>
|
||||||
|
<div> 阿里云语音对每一条语音通知分配的唯一ID标识</div>
|
||||||
|
<h2> 4、被叫号码</h2>
|
||||||
|
<div> 当前仅支持国内手机号,此处若不填,则在模板调试和配置告警通知时手动填写。</div>
|
||||||
|
<div>若您使用的语音通知文件为公共模式外呼,则该参数值不填。</div>
|
||||||
|
<div>若您使用的语音通知文件为专属模式外呼,则必须传入已购买的号码,仅支持一个号码。</div>
|
||||||
|
<h2> 5、被叫显号</h2>
|
||||||
|
<div> 用户呼叫号码显示,必须是在阿里云购买的号码。</div>
|
||||||
|
<h2> 6、播放次数</h2>
|
||||||
|
<div> 最多可播放3次</div>
|
||||||
|
<h2> 7、模板内容</h2>
|
||||||
|
<div>
|
||||||
|
仅当通知类型为验证码类型时可进行配置,变量标识需要阿里云模板中的标识一致,支持填写带变量的动态模板。
|
||||||
|
变量填写规范示例:${'{name}'}
|
||||||
|
。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知是填写。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AliyunVoice;
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const DingTalk = () => {
|
||||||
|
const agentId = getImage('/notice/doc/template/dingTalk-message/01-Agentid.jpg');
|
||||||
|
// const userId = getImage('/notice/doc/template/dingTalk-message/02-user-id.jpg');
|
||||||
|
// const dept = getImage('/notice/doc/template/dingTalk-message/03-dept.jpg');
|
||||||
|
const a = '{name}';
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
钉钉开放平台:
|
||||||
|
<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://open-dev.dingtalk.com
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
钉钉管理后台:
|
||||||
|
<a href="https://www.dingtalk.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://www.dingtalk.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
<div>使用钉钉消息通知时需在钉钉开放平台中创建好对应的应用</div>
|
||||||
|
</div>
|
||||||
|
<h1> 2.模板配置说明</h1>
|
||||||
|
<h2> 1、绑定配置</h2>
|
||||||
|
<div> 使用固定的通知配置发送此通知模板</div>
|
||||||
|
<h2> 2、Agentid</h2>
|
||||||
|
<div> 应用唯一标识</div>
|
||||||
|
<div> 获取路径:“钉钉开放平台”--“应用开发”--“查看应用”</div>
|
||||||
|
<div class="image">
|
||||||
|
<Image width="100%" src={agentId} />
|
||||||
|
</div>
|
||||||
|
<h2> 3、收信人、收信部门</h2>
|
||||||
|
<div>若不填写收信人,则在模板调试和配置告警通知时手动填写。</div>
|
||||||
|
{/*<div> 收信人ID获取路径:“钉钉管理后台”--“通讯录”--“查看用户”</div>*/}
|
||||||
|
{/*<div> 收信部门ID获取路径:“钉钉管理后台”--“通讯录”--“编辑部门”</div>*/}
|
||||||
|
{/*<div class="image">*/}
|
||||||
|
{/* <Image width="100%" src={userId} />*/}
|
||||||
|
{/* <Image width="100%" src={dept} />*/}
|
||||||
|
{/*</div>*/}
|
||||||
|
<h2> 4、模板内容</h2>
|
||||||
|
<div>
|
||||||
|
支持填写带变量的动态模板。变量填写规范示例:${a}
|
||||||
|
。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DingTalk;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const DingTalkRebot = () => {
|
||||||
|
const b = '{name}';
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
钉钉开放平台:
|
||||||
|
<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://open-dev.dingtalk.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
使用钉钉群机器人消息通知时需在钉钉开放平台中创建好对应的机器人,再到钉钉客户端在对应的群中绑定智能机器人。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2> 1、绑定配置</h2>
|
||||||
|
<div> 使用固定的通知配置发送此通知模板</div>
|
||||||
|
<h2> 2、消息类型</h2>
|
||||||
|
<div> 目前支持text、markdown、link3种。</div>
|
||||||
|
<h2> 3、模板内容</h2>
|
||||||
|
<div>
|
||||||
|
支持填写带变量的动态模板。变量填写规范示例:${b}
|
||||||
|
。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DingTalkRebot;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const Email = () => {
|
||||||
|
const a = '{标题}';
|
||||||
|
const b = '{name}';
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
服务器地址支持自定义输入。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
<div>
|
||||||
|
{/* <h2> 1、服务器地址</h2>
|
||||||
|
<div>服务器地址支持自定义输入</div> */}
|
||||||
|
<h2> 1、标题</h2>
|
||||||
|
<div>支持输入变量,变量格式${a}</div>
|
||||||
|
<h2> 2、收件人</h2>
|
||||||
|
<div> 支持录入多个邮箱地址,可填写变量参数。</div>
|
||||||
|
<h2> 3、模板内容</h2>
|
||||||
|
<div>
|
||||||
|
支持填写带变量的动态模板。变量填写规范示例:${b}
|
||||||
|
。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Email;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const Webhook = () => {
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
<div>
|
||||||
|
1、请求体 请求体中的数据来自于发送通知时指定的所有变量,也可通过自定义的方式进行变量配置。
|
||||||
|
使用webhook通知时,系统会将该事件通过您指定的URL地址,以POST方式发送。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Webhook;
|
|
@ -0,0 +1,58 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const WeixinApp = () => {
|
||||||
|
const appId = getImage('/notice/doc/template/weixin-official/02-mini-Program-Appid.png');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
企业微信管理后台:
|
||||||
|
<a href="https://work.weixin.qq.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://work.weixin.qq.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
</div>
|
||||||
|
<h1>2.模板配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2>1、绑定配置</h2>
|
||||||
|
<div>使用固定的通知配置发送此通知模板</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>2、用户标签</h2>
|
||||||
|
<div>以标签的维度通知该标签下所有用户</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>3、消息模板</h2>
|
||||||
|
<div>微信公众号中配置的消息模板</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>4、模板跳转链接</h2>
|
||||||
|
<div>点击消息之后进行页面跳转</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>5、跳转小程序Appid</h2>
|
||||||
|
<div>点击消息之后打开对应的小程序</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>6、跳转小程序具体路径</h2>
|
||||||
|
<div>点击消息之后跳转到小程序的具体页面</div>
|
||||||
|
<div class="image">
|
||||||
|
<Image width="100%" src={appId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>7、模板内容</h2>
|
||||||
|
<div>
|
||||||
|
支持填写带变量的动态模板。变量填写规范示例:${name}
|
||||||
|
。填写动态参数后,可对变量的名称、类型、格式进行配置,以便告警通知时填写。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default WeixinApp;
|
|
@ -0,0 +1,48 @@
|
||||||
|
import './index.less';
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
|
||||||
|
const WeixinCorp = () => {
|
||||||
|
const agentId = getImage('/notice/doc/template/weixin-corp/01-Agentid.jpg');
|
||||||
|
const userId = getImage('/notice/doc/template/weixin-corp/02-userID.jpg');
|
||||||
|
const toDept = getImage('/notice/doc/template/weixin-corp/03-toDept.jpg');
|
||||||
|
const toTags = getImage('/notice/doc/template/weixin-corp/04-toTags.jpg');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="doc">
|
||||||
|
<div class="url">
|
||||||
|
企业微信管理后台:
|
||||||
|
<a href="https://work.weixin.qq.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
https://work.weixin.qq.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>1. 概述</h1>
|
||||||
|
<div>
|
||||||
|
通知模板结合通知配置为告警消息通知提供支撑。通知模板只能调用同一类型的通知配置服务。
|
||||||
|
</div>
|
||||||
|
<h1>2.模版配置说明</h1>
|
||||||
|
<div>
|
||||||
|
<h2> 1、绑定配置</h2>
|
||||||
|
<div> 使用固定的通知配置发送此通知模板</div>
|
||||||
|
<h2> 2、Agentid</h2>
|
||||||
|
<div> 应用唯一标识</div>
|
||||||
|
<div> 获取路径:“企业微信”管理后台--“应用管理”--“应用”--“查看应用”</div>
|
||||||
|
<div class="image">
|
||||||
|
<Image width="100%" src={agentId} />
|
||||||
|
</div>
|
||||||
|
<h2> 3、收信人ID、收信部门ID、标签推送</h2>
|
||||||
|
<div>
|
||||||
|
接收通知的3种方式,3个字段若在此页面都没有填写,则在模板调试和配置告警通知时需要手动填写
|
||||||
|
</div>
|
||||||
|
<div> 收信人ID获取路径:【通讯录】-{'>'}【成员信息】查看成员账号</div>
|
||||||
|
<div> 收信组织ID获取路径:【通讯录】-{'>'}【部门信息】查看部门ID</div>
|
||||||
|
<div class="image">
|
||||||
|
<Image width="100%" src={userId} />
|
||||||
|
<Image width="100%" src={toDept} />
|
||||||
|
<Image width="100%" src={toTags} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default WeixinCorp;
|
|
@ -0,0 +1,39 @@
|
||||||
|
.doc {
|
||||||
|
height: 750px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: rgba(#000, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
|
||||||
|
.url {
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: #2f54eb;
|
||||||
|
background-color: rgba(#a7bdf7, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 16px 0;
|
||||||
|
color: rgba(#000, 0.85);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 6px 0;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import DingTalk from './DingTalk';
|
||||||
|
import DingTalkRebot from './DingTalkRebot';
|
||||||
|
import AliyunSms from './AliyunSms';
|
||||||
|
import AliyunVoice from './AliyunVoice';
|
||||||
|
import Email from './Email';
|
||||||
|
import Webhook from './Webhook';
|
||||||
|
import WeixinApp from './WeixinApp';
|
||||||
|
import WeixinCorp from './WeixinCorp';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Doc',
|
||||||
|
props: {
|
||||||
|
docData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const docMap = {
|
||||||
|
weixin: {
|
||||||
|
corpMessage: <WeixinCorp />,
|
||||||
|
officialMessage: <WeixinApp />,
|
||||||
|
},
|
||||||
|
dingTalk: {
|
||||||
|
dingTalkMessage: <DingTalk />,
|
||||||
|
dingTalkRobotWebHook: <DingTalkRebot />,
|
||||||
|
},
|
||||||
|
voice: {
|
||||||
|
aliyun: <AliyunVoice />,
|
||||||
|
},
|
||||||
|
sms: {
|
||||||
|
aliyunSms: <AliyunSms />,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
embedded: <Email />,
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
http: <Webhook />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
docMap?.[props.docData.type]?.[props.docData.provider]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,8 +1,596 @@
|
||||||
<!-- 通知模板详情 -->
|
<!-- 通知模板详情 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">通知模板详情</div>
|
<div class="page-container">
|
||||||
|
<a-card>
|
||||||
|
<a-row>
|
||||||
|
<a-col :span="10">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
label="通知方式"
|
||||||
|
v-bind="validateInfos.type"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="formData.type"
|
||||||
|
placeholder="请选择通知方式"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in NOTICE_METHOD"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="名称" v-bind="validateInfos.name">
|
||||||
|
<a-input
|
||||||
|
v-model:value="formData.name"
|
||||||
|
placeholder="请输入名称"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
label="类型"
|
||||||
|
v-bind="validateInfos.provider"
|
||||||
|
v-if="
|
||||||
|
formData.type !== 'email' &&
|
||||||
|
formData.type !== 'webhook'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<RadioCard
|
||||||
|
:options="msgType"
|
||||||
|
v-model="formData.provider"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
label="绑定配置"
|
||||||
|
v-bind="validateInfos.configId"
|
||||||
|
v-if="formData.type !== 'email'"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="formData.configId"
|
||||||
|
placeholder="请选择绑定配置"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 钉钉 -->
|
||||||
|
<template v-if="formData.type === 'dingTalk'">
|
||||||
|
<template
|
||||||
|
v-if="formData.provider === 'dingTalkMessage'"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="AgentId"
|
||||||
|
v-bind="validateInfos['template.agentId']"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.agentId
|
||||||
|
"
|
||||||
|
placeholder="请输入AppSecret"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
formData.provider === 'dingTalkRobotWebHook'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="消息类型"
|
||||||
|
v-bind="
|
||||||
|
validateInfos['template.messageType']
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formData.template.messageType
|
||||||
|
"
|
||||||
|
placeholder="请选择消息类型"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
formData.template.messageType ===
|
||||||
|
'markdown'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="标题"
|
||||||
|
v-bind="
|
||||||
|
validateInfos[
|
||||||
|
'template.markdown.title'
|
||||||
|
]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- <a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.markdown
|
||||||
|
?.title
|
||||||
|
"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
/> -->
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- <template
|
||||||
|
v-if="
|
||||||
|
formData.template.messageType === 'link'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
label="标题"
|
||||||
|
v-bind="
|
||||||
|
validateInfos['template.link.title']
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.link?.title
|
||||||
|
"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="图片链接">
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.link?.picUrl
|
||||||
|
"
|
||||||
|
placeholder="请输入图片链接"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="内容链接">
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.link
|
||||||
|
?.messageUrl
|
||||||
|
"
|
||||||
|
placeholder="请输入内容链接"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template> -->
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<!-- 微信 -->
|
||||||
|
<template v-if="formData.type === 'weixin'">
|
||||||
|
<a-form-item
|
||||||
|
label="AgentId"
|
||||||
|
v-bind="validateInfos['template.agentId']"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formData.template.agentId"
|
||||||
|
placeholder="请输入agentId"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-row :gutter="10">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="收信人">
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formData.template.toUser
|
||||||
|
"
|
||||||
|
placeholder="请选择收信人"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="收信部门">
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formData.template.toParty
|
||||||
|
"
|
||||||
|
placeholder="请选择收信部门"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item label="标签推送">
|
||||||
|
<a-select
|
||||||
|
v-model:value="formData.template.toTag"
|
||||||
|
placeholder="请选择标签推送"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- 邮件 -->
|
||||||
|
<template v-if="formData.type === 'email'">
|
||||||
|
<a-form-item
|
||||||
|
label="标题"
|
||||||
|
v-bind="validateInfos['template.subject']"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formData.template.subject"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="收件人">
|
||||||
|
<a-select
|
||||||
|
v-model:value="formData.template.sendTo"
|
||||||
|
placeholder="请选择收件人"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="附件信息">
|
||||||
|
<Attachments
|
||||||
|
v-model:attachments="
|
||||||
|
formData.template.attachments
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- 语音 -->
|
||||||
|
<template v-if="formData.type === 'voice'">
|
||||||
|
<a-form-item
|
||||||
|
label="类型"
|
||||||
|
v-bind="validateInfos['template.templateType']"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formData.template.templateType
|
||||||
|
"
|
||||||
|
placeholder="请选择类型"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in VOICE_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-row :gutter="10">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="模板ID"
|
||||||
|
v-bind="
|
||||||
|
validateInfos[
|
||||||
|
'template.templateCode'
|
||||||
|
]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.templateCode
|
||||||
|
"
|
||||||
|
placeholder="请输入模板ID"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="被叫号码">
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.calledNumber
|
||||||
|
"
|
||||||
|
placeholder="请输入被叫号码"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item label="被叫显号">
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.calledShowNumbers
|
||||||
|
"
|
||||||
|
placeholder="请输入被叫显号"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="播放次数">
|
||||||
|
<a-input
|
||||||
|
v-model:value="formData.template.playTimes"
|
||||||
|
placeholder="请输入播放次数"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
label="模板内容"
|
||||||
|
v-if="formData.template.templateType === 'tts'"
|
||||||
|
>
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="formData.template.ttsCode"
|
||||||
|
show-count
|
||||||
|
:rows="5"
|
||||||
|
placeholder="内容中的变量将用于阿里云语音验证码"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- 短信 -->
|
||||||
|
<template v-if="formData.type === 'sms'">
|
||||||
|
<a-row :gutter="10">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="模板"
|
||||||
|
v-bind="validateInfos['template.code']"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="
|
||||||
|
formData.template.code
|
||||||
|
"
|
||||||
|
placeholder="请选择模板"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in ROBOT_MSG_TYPE"
|
||||||
|
:key="index"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="收信人">
|
||||||
|
<a-input
|
||||||
|
v-model:value="
|
||||||
|
formData.template.phoneNumber
|
||||||
|
"
|
||||||
|
placeholder="请输入收信人"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-form-item
|
||||||
|
label="签名"
|
||||||
|
v-bind="validateInfos['template.signName']"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formData.template.signName"
|
||||||
|
placeholder="请输入签名"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- webhook -->
|
||||||
|
<template v-if="formData.type === 'webhook'">
|
||||||
|
<a-form-item label="请求体">
|
||||||
|
<a-radio-group
|
||||||
|
v-model:value="
|
||||||
|
formData.template.contextAsBody
|
||||||
|
"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
<a-radio :value="true">默认</a-radio>
|
||||||
|
<a-radio :value="false">自定义</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="formData.template.body"
|
||||||
|
placeholder="请求体中的数据来自于发送通知时指定的所有变量"
|
||||||
|
v-if="formData.template.contextAsBody"
|
||||||
|
disabled
|
||||||
|
:rows="5"
|
||||||
|
/>
|
||||||
|
<div v-else style="height: 400px">
|
||||||
|
<MonacoEditor
|
||||||
|
theme="vs"
|
||||||
|
v-model:modelValue="
|
||||||
|
formData.template.body
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<a-form-item label="说明">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
show-count
|
||||||
|
:maxlength="200"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="请输入说明"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :wrapper-col="{ offset: 0, span: 3 }">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="btnLoading"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12" :push="2">
|
||||||
|
<Doc :docData="formData" />
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { getImage } from '@/utils/comm';
|
||||||
|
import { Form } from 'ant-design-vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { TemplateFormData } from '../types';
|
||||||
|
import {
|
||||||
|
NOTICE_METHOD,
|
||||||
|
TEMPLATE_FIELD_MAP,
|
||||||
|
MSG_TYPE,
|
||||||
|
ROBOT_MSG_TYPE,
|
||||||
|
VOICE_TYPE,
|
||||||
|
} from '@/views/notice/const';
|
||||||
|
import templateApi from '@/api/notice/template';
|
||||||
|
import Doc from './doc/index';
|
||||||
|
import MonacoEditor from '@/components/MonacoEditor/index.vue';
|
||||||
|
import Attachments from './components/Attachments.vue'
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
// 消息类型
|
||||||
|
const msgType = ref([
|
||||||
|
{
|
||||||
|
label: '钉钉消息',
|
||||||
|
value: 'dingTalkMessage',
|
||||||
|
logo: getImage('/notice/dingtalk.png'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '群机器人消息',
|
||||||
|
value: 'dingTalkRobotWebHook',
|
||||||
|
logo: getImage('/notice/dingTalk-rebot.png'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = ref<TemplateFormData>({
|
||||||
|
template: {},
|
||||||
|
name: '',
|
||||||
|
type: 'email',
|
||||||
|
provider: 'embedded',
|
||||||
|
description: '',
|
||||||
|
variableDefinitions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据通知方式展示对应的字段
|
||||||
|
watch(
|
||||||
|
() => formData.value.type,
|
||||||
|
(val) => {
|
||||||
|
// formData.value.template = TEMPLATE_FIELD_MAP[val];
|
||||||
|
msgType.value = MSG_TYPE[val];
|
||||||
|
|
||||||
|
formData.value.provider = msgType.value[0].value;
|
||||||
|
console.log('formData.value.template: ', formData.value.template);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
computed(() => {
|
||||||
|
console.log('formData.value.type: ', formData.value.type);
|
||||||
|
Object.assign(
|
||||||
|
formData.value.template,
|
||||||
|
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const formRules = ref({
|
||||||
|
type: [{ required: true, message: '请选择通知方式' }],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入名称' },
|
||||||
|
{ max: 64, message: '最多可输入64个字符' },
|
||||||
|
],
|
||||||
|
provider: [{ required: true, message: '请选择类型' }],
|
||||||
|
configId: [{ required: true, message: '请选择绑定配置' }],
|
||||||
|
// 钉钉
|
||||||
|
'template.agentId': [{ required: true, message: '请输入agentId' }],
|
||||||
|
'template.messageType': [{ required: true, message: '请选择消息类型' }],
|
||||||
|
'template.markdown.title': [{ required: true, message: '请输入标题' }],
|
||||||
|
// 'template.url': [{ required: true, message: '请输入WebHook' }],
|
||||||
|
// 微信
|
||||||
|
// 'template.agentId': [{ required: true, message: '请输入agentId' }],
|
||||||
|
// 邮件
|
||||||
|
'template.subject': [{ required: true, message: '请输入标题' }],
|
||||||
|
// 阿里云语音
|
||||||
|
'template.templateType': [{ required: true, message: '请选择类型' }],
|
||||||
|
'template.templateCode': [{ required: true, message: '请输入模板ID' }],
|
||||||
|
// 短信
|
||||||
|
'template.code': [{ required: true, message: '请选择模板' }],
|
||||||
|
'template.signName': [{ required: true, message: '请输入签名' }],
|
||||||
|
// webhook
|
||||||
|
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||||
|
formData.value,
|
||||||
|
formRules.value,
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => formData.value.type,
|
||||||
|
() => {
|
||||||
|
clearValidate();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDetail = async () => {
|
||||||
|
const res = await templateApi.detail(route.params.id as string);
|
||||||
|
// console.log('res: ', res);
|
||||||
|
formData.value = res.result;
|
||||||
|
// console.log('formData.value: ', formData.value);
|
||||||
|
};
|
||||||
|
// getDetail();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单提交
|
||||||
|
*/
|
||||||
|
const btnLoading = ref<boolean>(false);
|
||||||
|
const handleSubmit = () => {
|
||||||
|
validate()
|
||||||
|
.then(async () => {
|
||||||
|
console.log('formData.value: ', formData.value);
|
||||||
|
btnLoading.value = true;
|
||||||
|
// let res;
|
||||||
|
// if (!formData.value.id) {
|
||||||
|
// res = await templateApi.save(formData.value);
|
||||||
|
// } else {
|
||||||
|
// res = await templateApi.update(formData.value);
|
||||||
|
// }
|
||||||
|
// // console.log('res: ', res);
|
||||||
|
// if (res?.success) {
|
||||||
|
// message.success('保存成功');
|
||||||
|
// router.back();
|
||||||
|
// }
|
||||||
|
btnLoading.value = false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('err: ', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.page-container {
|
||||||
|
background: #f0f2f5;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue