Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

# Conflicts:
#	package-lock.json
#	yarn.lock
This commit is contained in:
xiongqian 2023-01-18 10:47:48 +08:00
commit e6383367c8
113 changed files with 8087 additions and 761 deletions

View File

@ -15,9 +15,11 @@
"dependencies": {
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vuemap/vue-amap": "^1.1.20",
"@vueuse/core": "^9.10.0",
"ant-design-vue": "^3.2.15",
"axios": "^1.2.1",
"echarts": "^5.4.1",
"event-source-polyfill": "^1.0.31",
"jetlinks-store": "^0.0.3",
"js-cookie": "^3.0.1",
"less": "^4.1.3",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

25
src/api/comm.ts Normal file
View File

@ -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}`)

View File

@ -1,4 +1,5 @@
import server from '@/utils/request'
import { BASE_API_PATH } from '@/utils/variable'
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
* @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}`

View File

@ -36,4 +36,10 @@ export const getCodecs = () => server.get<{id: string, name: string}>('/device/p
* @param id ID
* @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)

View File

@ -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 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);

View File

@ -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}`);

View File

@ -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`);

View File

@ -1,3 +0,0 @@
import server from '@/utils/request'
export const save = (data) => server.post(`/network/certificate`, data)

View File

@ -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);

View File

@ -12,4 +12,6 @@ export const postInitSet = (data) => server.post(`/user/settings/init`, data)
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}`)

View File

View File

@ -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)

12
src/api/notice/config.ts Normal file
View File

@ -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)
}

View File

@ -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)
}

6
src/api/system/basis.ts Normal file
View File

@ -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)

16
src/api/system/role.ts Normal file
View File

@ -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);

View File

@ -1,7 +0,0 @@
import { createFromIconfontCN } from '@ant-design/icons-vue';
const AliIcon = createFromIconfontCN({
scriptUrl: '/icons/iconfont.js', // 在 iconfont.cn 上生成
});
export default AliIcon

View File

@ -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

View File

@ -6,8 +6,7 @@
</template>
<script setup lang="ts">
import { StatusColorEnum } from '@/utils/consts.ts';
// import { StatusColorEnum } from '@/utils/consts.ts';
const props = defineProps({
text: {
type: String,
@ -15,10 +14,10 @@ const props = defineProps({
status: {
type: String || Number,
default: 'default',
validator: (value) => {
//
return Object.keys(StatusColorEnum).includes(value);
},
// validator: (value) => {
// //
// return Object.keys(StatusColorEnum).includes(value);
// },
},
/**
* 自定义status值颜色

View File

@ -54,8 +54,8 @@
delete: item.key === 'delete',
}"
>
<!-- <slot name="actions" v-bind="item"></slot> -->
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<slot name="actions" v-bind="item"></slot>
<!-- <a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
<template v-else>
@ -72,7 +72,7 @@
<span>{{ item.text }}</span>
</template>
</a-button>
</template>
</template> -->
</div>
</div>
</slot>
@ -108,7 +108,6 @@ const props = defineProps({
type: Boolean,
default: true,
},
statusText: {
type: String,
default: '正常',
@ -229,6 +228,10 @@ const handleClick = () => {
transform: skewX(-45deg);
}
}
:deep(.card-item-content-title) {
cursor: pointer;
}
}
.card-mask {
@ -284,13 +287,14 @@ const handleClick = () => {
display: flex;
flex-grow: 1;
& > span,
button {
width: 100% !important;
border-radius: 0 !important;
& > :deep(span, button) {
width: 100%;
border-radius: 0;
}
button {
:deep(button) {
width: 100%;
border-radius: 0;
background: #f6f6f6;
border: 1px solid #e6e6e6;
color: #2f54eb;
@ -322,7 +326,7 @@ const handleClick = () => {
flex-basis: 60px;
flex-grow: 0;
button {
:deep(button) {
background: @error-color-deprecated-bg;
border: 1px solid @error-color-outline;
@ -348,7 +352,7 @@ const handleClick = () => {
}
}
button[disabled] {
:deep(button[disabled]) {
background: @disabled-bg;
border-color: @disabled-color;

View File

@ -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>

View File

@ -31,7 +31,7 @@
v-model:value='formData.data[item.name]'
:options='item.options'
/>
<a-inputnumber
<a-input-number
v-else-if='item.component === componentType.inputNumber'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'

View File

@ -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>

View File

@ -32,6 +32,7 @@ self.MonacoEnvironment = {
const props = defineProps({
modelValue: [String, Number],
theme: { type: String, default: 'vs-dark' },
});
const emit = defineEmits(['update:modelValue']);
@ -48,7 +49,7 @@ onMounted(() => {
tabSize: 2,
automaticLayout: true,
scrollBeyondLastLine: false,
theme: 'vs-dark', // : vs(), vs-dark(), hc-black()
theme: props.theme, // : vs(), vs-dark(), hc-black()
});
instance.onDidChangeModelContent(() => {

View File

@ -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>

View File

@ -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>

View File

@ -1,11 +1,12 @@
<template>
<div class='JSearch-item'>
<div class='JSearch-item--type'>
<div class='JSearch-item--type' v-if='expand'>
<a-select
v-if='index !== 1 && index !== 4'
:options='typeOptions'
v-model:value='termsModel.type'
style='width: 100%;'
@change='valueChange'
/>
<span v-else>
{{
@ -17,55 +18,86 @@
class='JSearch-item--column'
:options='columnOptions'
v-model:value='termsModel.column'
@change='columnChange'
/>
<a-select
class='JSearch-item--termType'
:options='termTypeOptions'
:options='termTypeOptions.option'
v-model:value='termsModel.termType'
@change='termTypeChange'
/>
<div class='JSearch-item--value'>
<a-input
v-if='component === componentType.input'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-select
v-else-if='component === componentType.select'
showSearch
:loading='optionLoading'
v-model:value='termsModel.value'
: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-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-input-password
v-else-if='component === componentType.password'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-switch
v-else-if='component === componentType.switch'
v-model:checked='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-radio-group
v-else-if='component === componentType.radio'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-checkbox-group
v-else-if='component === componentType.checkbox'
v-model:value='termsModel.value'
:options='options'
style='width: 100%'
@change='valueChange'
/>
<a-time-picker
v-else-if='component === componentType.time'
valueFormat='HH:mm:ss'
v-model:value='termsModel.value'
style='width: 100%'
@change='valueChange'
/>
<a-date-picker
v-else-if='component === componentType.date'
showTime
v-model:value='termsModel.value'
valueFormat='YYYY-MM-DD HH:mm:ss'
style='width: 100%'
@change='valueChange'
/>
<a-tree-select
v-else-if='component === componentType.tree'
v-else-if='component === componentType.treeSelect'
showSearch
v-model:value='termsModel.value'
:tree-data='options'
style='width: 100%'
:fieldNames='{ label: "name", value: "id" }'
@change='valueChange'
:filterTreeNode='(v, option) => filterSelectNode(v, option)'
/>
</div>
</div>
@ -73,27 +105,201 @@
<script setup lang='ts' name='SearchItem'>
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({
component: {
type: String,
default: componentType.input
columns: {
type: Array as PropType<SearchProps[]>,
default: () => [],
required: true
},
index: {
type: Number,
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
// valueundefined
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 => { // columnsMap
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>
@ -103,7 +309,7 @@ const termTypeOptions = reactive([])
gap: 16px;
.JSearch-item--type {
min-width: 120px;
min-width: 80px;
> span {
line-height: 34px;
font-weight: bold;
@ -111,11 +317,11 @@ const termTypeOptions = reactive([])
}
.JSearch-item--column {
min-width: 120px;
min-width: 100px;
}
.JSearch-item--termType {
min-width: 120px;
min-width: 100px;
}
.JSearch-item--value {

View File

@ -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>

View File

@ -1,72 +1,381 @@
<template>
<div class='JSearch-content'>
<div class='left'>
<SearchItem :index='1' />
<SearchItem :index='2' />
<SearchItem :index='3' />
<div class='JSearch-warp' ref='searchRef'>
<!-- 高级模式 -->
<div v-if='props.type === "advanced"' :class='["JSearch-content senior", expand ? "senior-expand" : "", screenSize ? "big" : "small"]'>
<div :class='["JSearch-items", expand ? "items-expand" : "", layout]'>
<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 class='center'>
<a-select
:options='typeOptions'
/>
</div>
<div class='right'>
<SearchItem :index='4' />
<SearchItem :index='5' />
<SearchItem :index='6' />
<!-- 简单模式 -->
<div v-else class='JSearch-content simple big'>
<div class='JSearch-items'>
<div class='left'>
<SearchItem :expand='false' :index='1' :columns='searchItems' @change='(v) => itemValueChange(v, 1)' :termsItem='terms'/>
</div>
</div>
<div class='JSearch-footer'>
<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>
</template>
</template>
<script setup lang='ts' name='Search'>
import SearchItem from './Item.vue'
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({
defaultParams: {
type: Object,
default: () => ({})
},
columns: {
type: Array,
default: () => []
type: Array as PropType<JColumnsProps[]>,
default: () => [],
required: true
},
type: {
type: String,
default: 'advanced'
},
key: {
target: {
type: String,
default: '',
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({
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为likenlike的值
* @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 = () => {
// termsvalueitem
const cloneParams = cloneDeep(terms)
return {
terms: cloneParams.terms.map(item => {
if (item.terms) {
item.terms = item.terms.filter(iItem => iItem && iItem.value)
.map(iItem => {
// handleValuerename
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) => {
// URLtargetprops
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>
<style scoped lang='less'>
.JSearch-content {
display: flex;
gap: 16px;
.left, & .right {
.JSearch-warp {
padding: 24px;
background-color: #fff;
.JSearch-content {
display: flex;
gap: 16px;
flex-direction: column;
width: 0;
flex-grow: 1;
min-width: 0;
}
gap: 12px;
.JSearch-items,& .JSearch-footer {
flex-grow: 1;
}
.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 {
display: flex;
flex-direction: column;
justify-content: center;
flex-basis: 120px;
.more-btn {
position: absolute;
right: 0;
}
}
.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>

View File

@ -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'
/>
```

49
src/components/Search/types.d.ts vendored Normal file
View File

@ -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 {
}

View File

@ -1,4 +1,17 @@
export const typeOptions = [
{ label: '或者', value: 'or' },
{ 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' },
];

View File

@ -11,16 +11,21 @@ enum ModelEnum {
CARD = 'CARD',
}
enum TypeEnum {
TREE = 'TREE',
PAGE = 'PAGE',
}
type RequestData = {
code: string;
result: {
data: Record<string, any>[] | undefined;
data?: Record<string, any>[] | undefined;
pageIndex: number;
pageSize: number;
total: number;
};
status: number;
} & Record<string, any>;
} | Record<string, any>;
export interface ActionsType {
key: string;
@ -39,16 +44,10 @@ export interface JColumnProps extends ColumnProps{
}
export interface JTableProps extends TableProps{
request?: (params: Record<string, any> & {
pageSize: number;
pageIndex: number;
}) => Promise<Partial<RequestData>>;
request?: (params?: Record<string, any>) => Promise<Partial<RequestData>>;
cardBodyClass?: string;
columns: JColumnProps[];
params?: Record<string, any> & {
pageSize: number;
pageIndex: number;
};
params?: Record<string, any>;
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
// actions?: ActionsType[];
noPagination?: boolean;
@ -64,6 +63,8 @@ export interface JTableProps extends TableProps{
*/
gridColumns?: number[];
alertRender?: boolean;
type?: keyof typeof TypeEnum;
defaultParams?: Record<string, any>;
}
const JTable = defineComponent<JTableProps>({
@ -74,6 +75,7 @@ const JTable = defineComponent<JTableProps>({
],
emits: [
'modelChange', // 切换卡片和表格
'reload' // 刷新数据
],
props: {
request: {
@ -96,10 +98,6 @@ const JTable = defineComponent<JTableProps>({
type: [String, undefined],
default: undefined
},
// actions: {
// type: Array as PropType<ActionsType[]>,
// default: () => []
// },
noPagination: {
type: Boolean,
default: false
@ -127,9 +125,22 @@ const JTable = defineComponent<JTableProps>({
alertRender: {
type: Boolean,
default: true
},
type: {
type: String,
default: 'PAGE'
},
defaultParams: {
type: Object,
default: () => {
return {
pageIndex: 0,
pageSize: 12
}
}
}
} as any,
setup(props: JTableProps ,{ slots, emit }){
setup(props: JTableProps ,{ slots, emit, expose }){
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
const column = ref<number>(props.gridColumn || 4);
@ -162,25 +173,36 @@ const JTable = defineComponent<JTableProps>({
const handleSearch = async (_params?: Record<string, any>) => {
loading.value = true
if(props.request) {
const resp = await props.request({
const resp = await props.request({
pageIndex: 0,
pageSize: 12,
pageIndex: 1,
..._params
...props.defaultParams,
..._params,
terms: [
...(props.defaultParams?.terms || []),
...(_params?.terms || [])
]
})
if(resp.status === 200){
// 判断如果是最后一页且最后一页为空,就跳转到前一页
if(resp.result?.data?.length === 0 && resp.result.total && resp.result.pageSize && resp.result.pageIndex) {
handleSearch({
..._params,
pageSize: pageSize.value,
pageIndex: pageIndex.value - 1,
})
if(props.type === 'PAGE'){
// 判断如果是最后一页且最后一页为空,就跳转到前一页
if(resp.result.total && resp.result.pageSize && resp.result.pageIndex && resp.result?.data?.length === 0) {
handleSearch({
..._params,
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 {
_dataSource.value = resp.result?.data || []
pageIndex.value = resp.result?.pageIndex || 0
pageSize.value = resp.result?.pageSize || 6
total.value = resp.result?.total || 0
_dataSource.value = resp?.result || []
}
} else {
_dataSource.value = []
}
} else {
_dataSource.value = props?.dataSource || []
@ -188,9 +210,13 @@ const JTable = defineComponent<JTableProps>({
loading.value = false
}
watchEffect(() => {
handleSearch(props.params)
})
watch(
() => props.params,
(newValue) => {
handleSearch(newValue)
},
{deep: true, immediate: true}
)
onMounted(() => {
window.onresize = () => {
@ -201,6 +227,23 @@ const JTable = defineComponent<JTableProps>({
onUnmounted(() => {
window.onresize = null
})
/**
*
* @param _params
*/
const reload = (_params?: Record<string, any>) => {
handleSearch({
..._params,
pageSize: 12,
pageIndex: 0
})
}
/**
*
*/
expose({ reload })
return () => <Spin spinning={loading.value}>
<div class={styles["jtable-body"]}>
@ -233,7 +276,7 @@ const JTable = defineComponent<JTableProps>({
onClose={() => {
emit('cancelSelect')
}}
closeText={<a></a>}
closeText={<a-button type="link"></a-button>}
/>
</div> : null
}
@ -282,7 +325,7 @@ const JTable = defineComponent<JTableProps>({
</div>
{/* 分页 */}
{
_dataSource.value.length && !props.noPagination &&
(!!_dataSource.value.length) && !props.noPagination && props.type === 'PAGE' &&
<div class={styles['jtable-pagination']}>
<Pagination
size="small"
@ -292,14 +335,16 @@ const JTable = defineComponent<JTableProps>({
current={pageIndex.value}
pageSize={pageSize.value}
pageSizeOptions={['12', '24', '48', '60', '100']}
showTotal={(total, range) => {
return `${range[0]} - ${range[1]} 条/总共 ${total}`
showTotal={(num) => {
const minSize = pageIndex.value * pageSize.value + 1;
const MaxSize = (pageIndex.value + 1) * pageSize.value;
return `${minSize} - ${MaxSize > num ? num : MaxSize} 条/总共 ${num}`;
}}
onChange={(page, size) => {
handleSearch({
...props.params,
pageSize: size,
pageIndex: pageSize.value === size ? page : 1,
pageIndex: pageSize.value === size ? page : 0
})
}}
/>

View File

@ -41,7 +41,7 @@
</div>
</div>
<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 v-if="column.key === 'action'">
<a-space>

7
src/components/Table/types.d.ts vendored Normal file
View File

@ -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
}

View File

@ -45,7 +45,7 @@
<template #addonAfter>
<a-upload
name="file"
:action="action"
:action="FILE_UPLOAD"
:headers="headers"
:showUploadList="false"
@change="handleFileChange"
@ -89,6 +89,7 @@ import GeoComponent from '@/components/GeoComponent/index.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { ItemData, ITypes } from './types';
import { FILE_UPLOAD } from '@/api/comm';
type Emits = {
(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 handleFileChange = (info: UploadChangeParam<UploadFile<any>>) => {
if (info.file.status === 'done') {

View File

@ -6,6 +6,9 @@ import TitleComponent from "./TitleComponent/index.vue";
import Form from './Form';
import CardBox from './CardBox/index.vue';
import Search from './Search'
import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue'
import JUpload from './JUpload/index.vue'
export default {
install(app: App) {
@ -16,5 +19,8 @@ export default {
.component('Form', Form)
.component('CardBox', CardBox)
.component('Search', Search)
.component('NormalUpload', NormalUpload)
.component('FileFormat', FileFormat)
.component('JUpload', JUpload)
}
}

View File

@ -15,7 +15,7 @@ const filterPath = [
router.beforeEach((to, from, next) => {
const token = LocalStore.get(TOKEN_KEY)
// TODO 切换路由取消请求
if (token || filterPath.includes(to.path)) {
next()
} else {

View File

@ -66,6 +66,11 @@ export default [
},
// end: 测试用, 可删除
// 设备管理
{
path: '/device/Instance',
component: () => import('@/views/device/Instance/index.vue')
},
// link 运维管理
{
path: '/link/log',
@ -88,9 +93,21 @@ export default [
component: () => import('@/views/link/AccessConfig/Detail/index.vue')
},
// system 系统管理
{
path:'/system/Basis',
component: ()=>import('@/views/system/Basis/index.vue')
},
{
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',
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')
},
]

24
src/store/menu.ts Normal file
View File

@ -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
}
}
}
})

View File

@ -1,3 +1,6 @@
import { TOKEN_KEY } from '@/utils/variable'
import { Terms } from 'components/Search/types'
/**
*
* @param path {String}
@ -29,4 +32,28 @@ export const LocalStore = {
removeAll() {
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)
}

64
src/utils/encodeQuery.ts Normal file
View File

@ -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;
}

View File

@ -1,3 +1,7 @@
import moment from "moment";
import { LocalStore } from "./comm";
import { TOKEN_KEY } from "./variable";
/**
* JSON
* @param record
@ -18,4 +22,34 @@ export const downloadObject = (record: Record<string, any>, fileName: string, fo
ghostLink.click();
//移除
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);
};

View File

@ -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 Version_Code = 'version_code'
export const NETWORK_CERTIFICATE_UPLOAD = '/network/certificate/upload'

View File

@ -1,11 +1,111 @@
<template>
<div class='search'>
<Search />
<Search
:columns='columns'
target='device-instance-search'
@search='search'
/>
<Search
type='simple'
:columns='columns'
target='product'
@search='search'
/>
</div>
</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>
<style scoped>

View File

@ -10,7 +10,7 @@
@cancelSelect="cancelSelect"
>
<template #headerTitle>
<a-button type="primary">新增</a-button>
<a-button type="primary" @click="add">新增</a-button>
</template>
<template #card="slotProps">
<CardBox
@ -37,7 +37,7 @@
</a-col>
</a-row>
</template>
<!-- <template #actions="item">
<template #actions="item">
<a-popconfirm v-if="item.popConfirm" v-bind="item.popConfirm">
<a-button :disabled="item.disabled">
<DeleteOutlined v-if="item.key === 'delete'" />
@ -56,7 +56,7 @@
</template>
</a-button>
</template>
</template> -->
</template>
</CardBox>
</template>
<template #id="slotProps">
@ -83,8 +83,10 @@ import server from "@/utils/request";
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) => server.post(`/device-product/_query`, data)
// const request = (data: any) => server.post(`/device/category/_tree`, {paging: false})
const columns = [
{
@ -152,26 +154,26 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
tooltip: {
title: '导入'
},
disabled: true,
icon: 'icon-xiazai'
},
{
key: 'delete',
// disabled: true,
text: "删除",
disabled: !!data?.state,
tooltip: {
title: !!data?.state ? '正常的产品不能删除' : '删除'
},
// popConfirm: {
// title: '?'
// },
popConfirm: {
title: '确认删除?'
},
icon: 'icon-huishouzhan'
}
]
}
const p = h('p', 'hi')
const add = () => {
message.warn('123')
}
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -23,23 +23,16 @@
<script setup lang="ts">
import { message } from 'ant-design-vue';
type configItem = {
auth: boolean;
link: string;
english: string;
label: string;
params?: object;
};
import { bootConfig } from "../index";
const router = useRouter();
const props = defineProps({
cardData: Array<configItem>,
cardData: Array<bootConfig>,
cardTitle: String,
});
const { cardData, cardTitle } = toRefs(props);
const jumpPage = (row: configItem): void => {
const jumpPage = (row: bootConfig): void => {
if (row.auth && row.link) {
router.push(`${row.link}${objToParams(row.params || {})}`);
} else {

View File

@ -28,24 +28,16 @@
<script setup lang="ts">
import { message } from 'ant-design-vue';
type configItem = {
auth: boolean;
link: string;
english: string;
label: string;
params?: object;
image: string;
};
import { bootConfig } from "../index";
const router = useRouter();
const props = defineProps({
cardData: Array<configItem>,
cardData: Array<bootConfig>,
cardTitle: String,
});
const { cardData, cardTitle } = toRefs(props);
const jumpPage = (row: configItem): void => {
const jumpPage = (row: bootConfig): void => {
if (row.auth && row.link) {
router.push(`${row.link}${objToParams(row.params || {})}`);
} else {

View File

@ -3,7 +3,7 @@
<a-row :gutter="24" class="top" style="margin-bottom: 24px">
<a-col :span="6" class="left">
<BootCardSmall
:cardData="devBootConfig"
:cardData="deviceBootConfig"
cardTitle="物联网引导"
/>
<div style="width: 100%; height: 24px"></div>
@ -23,7 +23,7 @@
<StepCard
cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="devStepDetails"
:dataList="deviceStepDetails"
style="margin-bottom: 24px"
/>
<StepCard
@ -39,152 +39,26 @@ import BootCardSmall from '../BootCardSmall.vue';
import DeviceCountCard from '../DeviceCountCard.vue';
import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import { recommendList } from '../../index';
import StepCard from '../StepCard.vue';
// -
const devBootConfig = [
{
english: 'STEP1',
label: '创建产品',
link: '/a',
auth: true,
save: true,
image: '/images/home/guide-home1.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',
},
import {
deviceBootConfig as _deviceBootConfig,
deviceStepDetails,
opsBootConfig as _opsBootConfig,
opsStepDetails,
} from '../../modules/config';
const deviceImages = [
'/images/home/guide-home1.png',
'/images/home/guide-home2.png',
'/images/home/guide-home3.png',
];
// -
const opsBootConfig = [
{
english: 'STEP1',
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 opsImages = [
'/images/home/guide-home4.png',
'/images/home/guide-home5.png',
'/images/home/guide-home6.png',
];
// -
const devStepDetails = [
{
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[];
const deviceBootConfig = _deviceBootConfig.map((item,i) => ({...item, image: deviceImages[i]}));
const opsBootConfig = _opsBootConfig.map((item,i) => ({...item, image: opsImages[i]}));
</script>

View File

@ -2,7 +2,7 @@
<div class="device-home-container">
<a-row :gutter="10">
<a-col :span="14">
<BootCard :cardData="bootConfig" cardTitle="运维引导" />
<BootCard :cardData="opsBootConfig" cardTitle="运维引导" />
</a-col>
<a-col :span="10">
<BasicCountCard />
@ -15,7 +15,7 @@
<StepCard
cardTitle="运维管理推荐步骤"
tooltip="请根据业务需要对下述步骤进行选择性操作。"
:dataList="stepDetails"
:dataList="opsStepDetails"
/>
</a-row>
</div>
@ -27,74 +27,8 @@ import BasicCountCard from '../BasicCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue';
import {recommendList} from '../../index'
// import {getImage} from '@/utils/comm'
import { opsBootConfig, opsStepDetails } from '../../modules/config';
// -
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>
<style lang="less" scoped>

View File

@ -2,7 +2,7 @@
<div class="device-home-container">
<a-row :gutter="10">
<a-col :span="14">
<BootCard :cardData="bootConfig" cardTitle="物联网引导" />
<BootCard :cardData="deviceBootConfig" cardTitle="物联网引导" />
</a-col>
<a-col :span="10">
<DeviceCountCard />
@ -15,7 +15,7 @@
<StepCard
cardTitle="设备接入推荐步骤"
tooltip="不同的设备因为通信协议的不同,存在接入步骤的差异"
:dataList="stepDetails"
:dataList="deviceStepDetails"
/>
</a-row>
</div>
@ -27,73 +27,7 @@ import DeviceCountCard from '../DeviceCountCard.vue';
import PlatformPicCard from '../PlatformPicCard.vue';
import StepCard from '../StepCard.vue';
import {recommendList} from '../../index'
// 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[];
import { deviceBootConfig, deviceStepDetails } from '../../modules/config';
</script>
<style lang="less" scoped>

View File

@ -8,43 +8,20 @@
:getContainer="getContainer"
:maskClosable="false"
>
<div class="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>
<Search />
<JTable :columns="columns" model="TABLE"> </JTable>
<template #footer>
<a-button key="back" @click="visible = false
">取消</a-button>
<a-button key="submit" type="primary" @click="handleOk">确认</a-button>
<a-button key="back" @click="visible = false">取消</a-button>
<a-button key="submit" type="primary" @click="handleOk"
>确认</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ComponentInternalInstance } from 'vue';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { productItem, deviceInfo } from '../../index';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emits = defineEmits(['confirm']);
@ -64,7 +41,6 @@ watch(
() => {
visible.value = true;
clickReset();
getOptions();
clickSearch();
},
);
@ -75,10 +51,7 @@ const form = ref({
relation: '',
keyValue: '',
});
const productList = ref<[productItem] | []>([]);
const getOptions = () => {
productList.value = [];
};
const clickSearch = () => {};
const clickReset = () => {
Object.entries(form.value).forEach(([prop]) => {
@ -114,9 +87,6 @@ const columns = [
key: 'status',
},
];
const tableData = ref<deviceInfo[]>([]);
const selectItem: deviceInfo | {} = {};
const getList = () => {};
</script>
<style lang="less" scoped>
@ -125,5 +95,4 @@ const getList = () => {};
display: flex;
}
}
</style>

View File

@ -8,11 +8,6 @@ export interface recommendList {
auth: boolean;
dialogTag?: 'accessMethod' | 'funcTest';
}
// 产品列表里的每项
export interface productItem {
label: string;
value: string
}
export interface deviceInfo {
deviceId: string,
@ -20,4 +15,13 @@ export interface deviceInfo {
productName: string,
createTime: string,
status: boolean
}
export interface bootConfig {
english: string,
label: string,
link: string,
auth: boolean,
image?:string,
params?: object,
}

View File

@ -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',
}
},
];

View File

@ -9,8 +9,13 @@
</div>
<div v-else>
<div v-if="!id"><a @click="goBack">返回</a></div>
<AccessNetwork v-if="showType==='network'" :provider="provider" :data="data" />
<Media v-if="showType==='media'" :provider="provider" :data="data" />
<AccessNetwork
v-if="showType === 'network'"
:provider="provider"
:data="data"
/>
<Media v-if="showType === 'media'" :provider="provider" />
<Channel v-if="showType === 'channel'" :provider="provider" />
</div>
</a-card>
</a-spin>
@ -22,7 +27,7 @@ import AccessNetwork from '../components/Network.vue';
import Provider from '../components/Provider/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
import Media from '../components/Media/index.vue';
import Channel from '../components/Channel/index.vue';
// const router = useRouter();
const route = useRoute();
@ -34,14 +39,13 @@ const type = ref(false);
const loading = ref(true);
const provider = ref({});
const data = ref({});
const showType = ref('')
const showType = ref('');
const goProviders = (param: object) => {
showType.value = param.type
const goProviders = (param: object) => {
showType.value = param.type;
provider.value = param;
type.value = false;
console.log(1123,showType.value,param);
console.log(1123, showType.value, param);
};
const goBack = () => {
@ -59,22 +63,22 @@ const queryProviders = async () => {
const edge: any[] = [];
resp.result.map((item) => {
if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
item.type='media'
item.type = 'media';
media.push(item);
} else if (item.id === 'OneNet' || item.id === 'Ctwing') {
item.type='cloud'
item.type = 'cloud';
cloud.push(item);
} else if (item.id === 'modbus-tcp' || item.id === 'opc-ua') {
item.type='channel'
item.type = 'channel';
channel.push(item);
} else if (
item.id === 'official-edge-gateway' ||
item.id === 'edge-child-device'
) {
item.type='edge'
item.type = 'edge';
edge.push(item);
} else {
item.type='network'
item.type = 'network';
network.push(item);
}
});
@ -114,7 +118,7 @@ const queryProviders = async () => {
title: '官方接入',
});
}
dataSource.value = list
dataSource.value = list;
}
};

View File

@ -16,13 +16,13 @@
}}</a-tooltip>
</div>
<div class="checked-icon">
<div><a-icon type="check" /></div>
<div><CheckOutlined /></div>
</div>
</a-card>
</template>
<script lang="ts" setup name="AccessCard">
import { CheckOutlined } from '@ant-design/icons-vue';
const emit = defineEmits(['checkedChange']);
@ -36,13 +36,10 @@ const props = defineProps({
default: () => {},
},
});
console.log(1112,props);
const checkedChange=(id:string)=>{
emit('checkedChange', id);
}
const checkedChange = (id: string) => {
emit('checkedChange', id);
};
</script>
<style lang="less" scoped>

View File

@ -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>

View File

@ -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>

View File

@ -1,30 +1,160 @@
<template>
<div style="margin-top: 10px">
111
</div>
<div class="container">
<div v-if="channel === 'fixed-media'" 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">
<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>
<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({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const channel = ref(props.provider.channel)
console.log(211,props);
const channel = ref(props.provider.channel);
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>
<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>

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<a-icon type="info-circle" style="margin-right: 10px" />
<question-circle-outlined />
选择与设备通信的网络组件
</div>
<div class="search">
@ -93,7 +93,7 @@
</div>
<div class="steps-box" v-else-if="current === 1">
<div class="alert">
<a-icon type="info-circle" style="margin-right: 10px" />
<question-circle-outlined />
使用选择的消息协议对网络组件通信数据进行编解码认证等操作
</div>
<div class="search">
@ -126,7 +126,12 @@
</div>
</div>
<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-col :span="12">
<title-component data="基本信息" />
@ -190,7 +195,6 @@
v-if="config.document"
>
<Markdown :source="config.document" />
</div>
</div>
<div
@ -255,22 +259,32 @@
:scroll="{ y: 300 }"
>
<template
#stream
slot-scope="text, record"
#bodyCell="{ column, text, record }"
>
<span
<template
v-if="
record.upstream &&
record.downstream
column.dataIndex ===
'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>
</a-table>
</div>
@ -281,11 +295,7 @@
</div>
</div>
<div class="steps-action">
<a-button
v-if="[0, 1].includes(current)"
type="primary"
@click="next"
>
<a-button v-if="[0, 1].includes(current)" @click="next">
下一步
</a-button>
<a-button v-if="current === 2" type="primary" @click="saveData">
@ -314,10 +324,9 @@ import {
} from '../Detail/data';
import AccessCard from './AccessCard/index.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 { QuestionCircleOutlined } from '@ant-design/icons-vue';
//1
const resultList1 = [
{
@ -363,13 +372,158 @@ const resultList1 = [
// metadata: '',
// };
const result2 = {
"id": "MQTT",
"name": "MQTT",
"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位,不区分大小写.",
"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\":[]}"
}
id: 'MQTT',
name: 'MQTT',
features: [
{
id: 'supportFirmware',
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() {
var d = new Date().getTime();
@ -400,6 +554,8 @@ const props = defineProps({
},
});
const clientHeight = document.body.clientHeight;
const formRef = ref<FormInstance>();
const useForm = Form.useForm;
@ -412,7 +568,7 @@ const allProcotolList = ref([]);
const networkCurrent = ref('');
const procotolCurrent = ref('');
let config = ref({});
let columnsMQTT = ref([]);
let columnsMQTT = ref(<TableColumnType>[]);
const form = reactive({
name: '',
description: '',
@ -422,14 +578,18 @@ const { resetFields, validate, validateInfos } = useForm(
form,
reactive({
name: [
{ required: true, message: '请输入证书名称', trigger: 'blur' },
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
}),
);
const queryNetworkList = async (id: string, params: object, data = {}) => {
const resp = await getNetworkList(NetworkTypeMapping.get(id), data, params);
const queryNetworkList = async (id: string, include: string, data = {}) => {
const resp = await getNetworkList(
NetworkTypeMapping.get(id),
include,
data,
);
if (resp.status === 200) {
networkList.value = resp.result;
}
@ -463,9 +623,7 @@ const addNetwork = () => {
tab.onTabSaveSuccess = (value) => {
if (value.success) {
networkCurrent.value = value.result.id;
queryNetworkList(props.provider?.id, {
include: networkCurrent.value || '',
});
queryNetworkList(props.provider?.id, networkCurrent.value || '');
}
};
};
@ -488,20 +646,14 @@ const checkedChange = (id: string) => {
};
const networkSearch = (value: string) => {
queryNetworkList(
props.provider.id,
{
include: networkCurrent.value || '',
},
{
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
},
);
queryNetworkList(props.provider.id, networkCurrent.value || '', {
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
});
};
const procotolChange = (id: string) => {
if (!props.data.id) {
@ -576,7 +728,7 @@ const next = async () => {
//使2
config.value = result2;
current.value = current.value + 1;
columnsMQTT = [
columnsMQTT.value = [
{
title: '分组',
dataIndex: 'group',
@ -584,20 +736,24 @@ const next = async () => {
ellipsis: true,
align: 'center',
width: 100,
customRender: (value, row, index) => {
customCell: (record: object, rowIndex: number) => {
const obj = {
children: value,
attrs: {},
children: record,
rowSpan: 0,
};
const list = (config && config.routes) || [];
const arr = list.filter((res) => {
return res.group == row.group;
});
if (index == 0 || list[index - 1].group !== row.group) {
obj.attrs.rowSpan = arr.length;
} else {
obj.attrs.rowSpan = 0;
}
const list =
(config.value && config.value.routes) || [];
const arr = list.filter(
(res: object) => res.group == record.group,
);
if (
rowIndex == 0 ||
list[rowIndex - 1].group !== record.group
)
obj.rowSpan = arr.length;
return obj;
},
},
@ -605,6 +761,7 @@ const next = async () => {
title: 'topic',
dataIndex: 'topic',
key: 'topic',
align: 'center',
ellipsis: true,
},
{
@ -614,7 +771,6 @@ const next = async () => {
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'stream' },
},
{
title: '说明',
@ -698,9 +854,7 @@ onMounted(() => {
procotolCurrent.value = props.data.protocol;
current.value = 0;
networkCurrent.value = props.data.channelId;
queryNetworkList(props.provider.id, {
include: networkCurrent.value,
});
queryNetworkList(props.provider.id, networkCurrent.value);
procotolCurrent.value = props.data.protocol;
steps.value = ['网络组件', '消息协议', '完成'];
} else {
@ -711,9 +865,7 @@ onMounted(() => {
} else {
if (props.provider?.id) {
if (props.provider.channel !== 'child-device') {
queryNetworkList(props.provider.id, {
include: '',
});
queryNetworkList(props.provider.id, '');
steps.value = ['网络组件', '消息协议', '完成'];
current.value = 0;
} else {
@ -755,7 +907,6 @@ watch(
}
.card-last {
padding-right: 5px;
max-height: 580px;
overflow-y: auto;
overflow-x: hidden;
}

View File

@ -10,7 +10,7 @@
<a-upload
accept=".pem"
listType="text"
:action="`${BASE_API_PATH}${NETWORK_CERTIFICATE_UPLOAD}`"
:action="NETWORK_CERTIFICATE_UPLOAD"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
@ -31,11 +31,8 @@ import { UploadOutlined } from '@ant-design/icons-vue';
import { message } 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 { TOKEN_KEY } from '@/utils/variable';
import { NETWORK_CERTIFICATE_UPLOAD } from '@/api/link/certificate';
import type { UploadProps } from 'ant-design-vue';
const emit = defineEmits(['update:modelValue', 'change']);

View File

@ -97,12 +97,6 @@ import { message, Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import CertificateFile from './CertificateFile.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';
const router = useRouter();

View File

@ -0,0 +1,7 @@
<template>
<div>123</div>
</template>
<script setup>
</script>

View File

@ -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>

View File

@ -1,65 +1,63 @@
<!-- webhook请求头可编辑表格 -->
<template>
<a-table
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<template v-if="['KEY', 'VALUE'].includes(column.dataIndex)">
<a-input v-model="record[column.dataIndex]" />
<div class="table-wrapper">
<a-table
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<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 v-else-if="column.dataIndex === 'operation'">
<a-button type="text">
<template #icon>
<delete-outlined @click="handleDelete(record.idx)" />
</template>
</a-button>
</a-table>
<a-button
type="dashed"
@click="handleAdd"
style="width: 100%; margin-top: 5px"
>
<template #icon>
<plus-outlined />
</template>
</template>
</a-table>
<a-button
type="dashed"
@click="handleAdd"
style="width: 100%; margin-top: 5px"
>
<template #icon>
<plus-outlined />
</template>
添加
</a-button>
添加
</a-button>
</div>
</template>
<script setup lang="ts">
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
// import { cloneDeep } from 'lodash-es';
// import { defineComponent, reactive, ref } from 'vue';
// import type { UnwrapRef } from 'vue';
import { PropType } from 'vue';
import { IHeaders } from '../../types';
interface DataItem {
idx: number;
KEY: string;
VALUE: string;
}
type Emits = {
(e: 'update:headers', data: IHeaders[]): void;
};
const emit = defineEmits<Emits>();
const data: DataItem[] = [];
for (let i = 0; i < 2; i++) {
data.push({
idx: i,
KEY: `key ${i}`,
VALUE: `value${i}`,
});
}
const props = defineProps({
headers: {
type: Array as PropType<IHeaders[]>,
default: () => [],
},
});
const columns = [
{
title: 'KEY',
dataIndex: 'KEY',
dataIndex: 'key',
},
{
title: 'VALUE',
dataIndex: 'VALUE',
dataIndex: 'value',
},
{
title: '操作',
@ -69,17 +67,20 @@ const columns = [
},
];
const dataSource = ref(data);
console.log('dataSource: ', dataSource.value);
const dataSource = computed({
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);
};
const handleAdd = () => {
dataSource.value.push({
idx: dataSource.value.length + 1,
KEY: `key ${dataSource.value.length + 1}`,
VALUE: `value ${dataSource.value.length + 1}`,
id: dataSource.value.length,
key: '',
value: '',
});
};
</script>

View File

@ -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>1RegionID</h2>
<div>
</div>
<div>
https://help.aliyun.com/document_detail/40654.html?spm=a2c6h.13066369.0.0.54a174710O7rWH
</div>
</div>
<h2>2AccesskeyID/Secret</h2>
<div>
<div>API的用户标识和秘钥</div>
<div>
------AccessKey管理--
</div>
<div class={'image'}>
<Image width="100%" src={accessKey} />
</div>
</div>
</div>
);
};
export default AliyunSms;

View File

@ -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>1RegionID</h2>
<div>
RegionID
</div>
</div>
<h2>2AccesskeyID/Secret</h2>
<div>
<div>
API的用户标识和秘钥获取路径------AccessKey管理--
</div>
</div>
<div class={'image'}>
<Image width="100%" src={accessKey} />
</div>
</div>
);
};
export default AliyunVoice;

View File

@ -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>1AppKey</h2>
<div>
AppKey和AppSecret
</div>
<div>----</div>
<div class={'image'}>
<Image width="100%" src={appKey} />
</div>
</div>
<h2>2AppSecret</h2>
<div>
<div></div>
<div>----</div>
<div class={'image'}>
<Image width="100%" src={appSecret} />
</div>
</div>
</div>
);
};
export default DingTalk;

View File

@ -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> 1WebHook</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;

View File

@ -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;

View File

@ -0,0 +1,23 @@
import './index.less';
const Webhook = () => {
return (
<div class={'doc'}>
<h1>1. </h1>
<div>
webhook是一个接收HTTP请求的URLHTTP
POST请求Webhook的第三方系统可以基于该URL订阅本平台系统信息便
</div>
<h1>2.</h1>
<h2>1Webhook</h2>
<div>Webhook地址</div>
<h2>2</h2>
<div>
Accept-Language
Content-Type
</div>
</div>
);
};
export default Webhook;

View File

@ -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>1AppID</h2>
<div></div>
<div>
----
</div>
<div class={'image'}>
<Image width="100%" src={appId} />
</div>
</div>
<h2>2AppSecret</h2>
<div>
<div></div>
<div>
----
</div>
<div class={'image'}>
<Image width="100%" src={appSecret} />
</div>
</div>
</div>
);
};
export default WeixinApp;

View File

@ -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>1corpId</h2>
<div></div>
<div>----ID</div>
<div class={'image'}>
<Image width="100%" src={corpId} />
</div>
</div>
<h2>2corpSecret</h2>
<div>
<div>secret,corpSecret</div>
<div>
----
</div>
<div class={'image'}>
<Image width="100%" src={corpSecret} />
</div>
</div>
</div>
);
};
export default WeixinCorp;

View File

@ -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;
}
}

View File

@ -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]
)
},
});

View File

@ -259,6 +259,7 @@
<a-button
type="primary"
@click="handleSubmit"
:loading="btnLoading"
style="width: 100%"
>
保存
@ -266,14 +267,16 @@
</a-form-item>
</a-form>
</a-col>
<a-col :span="12" :push="2"></a-col>
<a-col :span="12" :push="2">
<Doc :docData="formData" />
</a-col>
</a-row>
</a-card>
</div>
</template>
<script setup lang="ts">
import { getImage, LocalStore } from '@/utils/comm';
import { getImage } from '@/utils/comm';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { ConfigFormData } from '../types';
@ -283,8 +286,12 @@ import {
MSG_TYPE,
} from '@/views/notice/const';
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;
//
@ -306,25 +313,31 @@ const formData = ref<ConfigFormData>({
configuration: {
appKey: '',
appSecret: '',
url: '',
},
description: '',
name: '',
provider: 'dingTalkMessage',
type: NOTICE_METHOD[0].value,
type: 'dingTalk',
});
//
watch(
() => formData.value.type,
(val) => {
formData.value.configuration = CONFIG_FIELD_MAP[val];
// formData.value.configuration = Object.values<any>(CONFIG_FIELD_MAP[val])[0];
msgType.value = MSG_TYPE[val];
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({
type: [{ required: true, message: '请选择通知方式' }],
@ -383,25 +396,56 @@ const formRules = ref({
pattern:
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/,
message: 'Webhook需要是一个合法的URL',
trigger: 'blur',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
});
const { resetFields, validate, validateInfos } = useForm(
const { resetFields, validate, validateInfos, clearValidate } = useForm(
formData.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 = () => {
validate()
.then(async () => {})
.catch((err) => {});
.then(async () => {
// 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>

View File

@ -3,6 +3,20 @@
<div class="page-container">通知配置</div>
</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>

View File

@ -1,37 +1,70 @@
interface IHeaders {
export interface IHeaders {
id?: number;
key: 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 = {
configuration: {
// 钉钉
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[];
};
configuration: IConfiguration;
// configuration: {
// // 钉钉
// 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[];
// };
description: string;
name: string;
provider: string;
type: string;
id?: string;
maxRetryTimes?: number;
creatorId?: string;
createTime?: number;
};

View File

@ -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>

View File

@ -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;

View File

@ -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> 3ID</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;

View File

@ -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> 2Agentid</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;

View File

@ -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> textmarkdownlink3种</div>
<h2> 3</h2>
<div>
${b}
便
</div>
</div>
</div>
);
};
export default DingTalkRebot;

View File

@ -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;

View File

@ -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;

View File

@ -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>5Appid</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;

View File

@ -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> 2Agentid</h2>
<div> </div>
<div> ------</div>
<div class="image">
<Image width="100%" src={agentId} />
</div>
<h2> 3IDID</h2>
<div>
33
</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;

View File

@ -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;
}
}

View File

@ -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]
)
},
});

View File

@ -1,8 +1,596 @@
<!-- 通知模板详情 -->
<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>
<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