Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev
This commit is contained in:
commit
76dfb3a797
|
@ -37,6 +37,7 @@
|
|||
"unplugin-vue-components": "^0.22.12",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-json-viewer": "^3.0.4",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-markdown-it": "^1.0.10",
|
||||
"vue3-ts-jsoneditor": "^2.7.1"
|
||||
|
|
|
@ -51,3 +51,9 @@ export const queryDevice = () =>
|
|||
|
||||
export const validateVersion = (productId: string, versionOrder: number) =>
|
||||
server.get(`/firmware/${productId}/${versionOrder}/exists`);
|
||||
|
||||
export const queryDetailList = (data: Record<string, unknown>) =>
|
||||
server.post(`/device-instance/detail/_query`, data);
|
||||
|
||||
export const queryDetailListNoPaging = (data: Record<string, unknown>) =>
|
||||
server.post(`/device-instance/detail/_query/no-paging`, data);
|
||||
|
|
|
@ -458,4 +458,28 @@ export const treeEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/
|
|||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-collector-save/invoke`, data)
|
||||
export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-collector-save/invoke`, data)
|
||||
|
||||
/**
|
||||
* 查询属性详情
|
||||
* @param deviceId
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params)
|
||||
|
||||
/**
|
||||
* 聚合查询设备属性
|
||||
* @param deviceId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/agg/_query`, data)
|
||||
|
||||
/**
|
||||
* 聚合查询设备属性
|
||||
* @param deviceId
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query`, data)
|
|
@ -0,0 +1,15 @@
|
|||
import server from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 列表
|
||||
list: (data: any, id: string) => server.post(`/media/device/${id}/channel/_query`, data),
|
||||
// 详情
|
||||
detail: (id: string): any => server.get(`/media/channel/${id}`),
|
||||
// 验证通道ID是否存在
|
||||
validateField: (params: string): any => server.get(`/media/channel/channelId/_validate`, params),
|
||||
// 新增
|
||||
save: (data: any) => server.post(`/media/channel`, data),
|
||||
// 修改
|
||||
update: (data: any) => server.put(`/media/channel`, data),
|
||||
del: (id: string) => server.remove(`media/channel/${id}`),
|
||||
}
|
|
@ -16,21 +16,21 @@ export default {
|
|||
debug: (data: any, configId: string, templateId: string) => post(`/notifier/${configId}/${templateId}/_send`, data),
|
||||
getHistory: (data: any, id: string) => post(`/notify/history/config/${id}/_query`, data),
|
||||
// 获取所有平台用户
|
||||
getPlatformUsers: () => post<any>(`/user/_query/no-paging`, { paging: false }),
|
||||
getPlatformUsers: (data: any) => post<any>(`/user/_query/no-paging`, data),
|
||||
// 钉钉部门
|
||||
dingTalkDept: (id: string) => get<any>(`/notifier/dingtalk/corp/${id}/departments/tree`),
|
||||
// 钉钉部门人员
|
||||
getDingTalkUsers: (configId: string, deptId: string) => get(`/notifier/dingtalk/corp/${configId}/${deptId}/users`),
|
||||
getDingTalkUsers: (configId: string, deptId: string) => get<any>(`/notifier/dingtalk/corp/${configId}/${deptId}/users`),
|
||||
// 钉钉已经绑定的人员
|
||||
getDingTalkBindUsers: (id: string) => get(`/user/third-party/dingTalk_dingTalkMessage/${id}`),
|
||||
getDingTalkBindUsers: (id: string) => get<any>(`/user/third-party/dingTalk_dingTalkMessage/${id}`),
|
||||
// 钉钉绑定用户
|
||||
dingTalkBindUser: (data: any, id: string) => patch(`/user/third-party/dingTalk_dingTalkMessage/${id}`, data),
|
||||
dingTalkBindUser: (data: { userId: string; providerName: string; thirdPartyUserId: string }[], id: string) => patch(`/user/third-party/dingTalk_dingTalkMessage/${id}`, data),
|
||||
// 微信部门
|
||||
weChatDept: (id: string) => get<any>(`/notifier/wechat/corp/${id}/departments`),
|
||||
// 微信部门人员
|
||||
getWeChatUsers: (configId: string, deptId: string) => get(`/notifier/wechat/corp/${configId}/${deptId}/users`),
|
||||
getWeChatUsers: (configId: string, deptId: string) => get<any>(`/notifier/wechat/corp/${configId}/${deptId}/users`),
|
||||
// 微信已经绑定的人员
|
||||
getWeChatBindUsers: (id: string) => get(`/user/third-party/weixin_corpMessage/${id}`),
|
||||
getWeChatBindUsers: (id: string) => get<any>(`/user/third-party/weixin_corpMessage/${id}`),
|
||||
// 微信绑定用户
|
||||
weChatBindUser: (data: any, id: string) => patch(`/user/third-party/weixin_corpMessage/${id}`, data),
|
||||
// 解绑
|
||||
|
|
|
@ -18,4 +18,20 @@ export const remove = (id:string) => server.remove(`/alarm/config/${id}`);
|
|||
/**
|
||||
* 手动触发告警
|
||||
*/
|
||||
export const _execute = (data:any) => server.post('/scene/batch/_execute',data)
|
||||
export const _execute = (data:any) => server.post('/scene/batch/_execute',data);
|
||||
/**
|
||||
* 下拉框场景数据
|
||||
*/
|
||||
export const getScene = (params:Record<string,any>) => server.get('/scene/_query/no-paging?paging=false',params);
|
||||
/**
|
||||
* 获取配置类型
|
||||
*/
|
||||
export const getTargetTypes = () => server.get('/alarm/config/target-type/supports');
|
||||
/**
|
||||
* 保存基本设置
|
||||
*/
|
||||
export const save = (data:any) =>server.post('/alarm/config',data);
|
||||
/**
|
||||
* 获取基础设置数据
|
||||
*/
|
||||
export const detail = (id:string) => server.get(`/alarm/config/${id}`);
|
|
@ -5,4 +5,6 @@ export const modify = (id: string, data: any) => server.put(`/scene/${id}`, data
|
|||
|
||||
export const save = (data: any) => server.post(`/scene`, data)
|
||||
|
||||
export const detail = (id: string) => server.get(`/scene/${id}`)
|
||||
export const detail = (id: string) => server.get(`/scene/${id}`)
|
||||
|
||||
export const query = (data: any) => server.post('/scene/_query/',data);
|
|
@ -4,6 +4,30 @@ import server from '@/utils/request';
|
|||
// 获取应用管理列表
|
||||
export const getApplyList_api = (data: any) => server.post(`/application/_query/`, data)
|
||||
// 修改应用状态
|
||||
export const changeApplyStatus_api = (id:string,data: any) => server.put(`/application/${id}`, data)
|
||||
export const changeApplyStatus_api = (id: string, data: any) => server.put(`/application/${id}`, data)
|
||||
// 删除应用
|
||||
export const delApply_api = (id:string) => server.remove(`/application/${id}`)
|
||||
export const delApply_api = (id: string) => server.remove(`/application/${id}`)
|
||||
|
||||
|
||||
|
||||
// 获取组织列表
|
||||
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);
|
||||
// 获取组织列表
|
||||
export const getAppInfo_api = (id: string) => server.get(`/application/${id}`);
|
||||
// 新增应用
|
||||
export const addApp_api = (data: object) => server.post(`/application`, data);
|
||||
// 更新应用
|
||||
export const updateApp_api = (id: string, data: object) => server.put(`/application/${id}`, data);
|
||||
|
||||
|
||||
// ---------集成菜单-----------
|
||||
|
||||
// 获取所属系统
|
||||
export const getOwner_api = (data: object) => server.post(`/menu/owner`, data);
|
||||
export const getOwnerStandalone_api = (appId: string, data: object) => server.post(`/application/${appId}/_/api/menu/owner`, data);
|
||||
|
||||
// 获取对应系统菜单树
|
||||
export const getOwnerTree_api = (owner: string) => server.post(`/menu/owner/tree/${owner}`, {});
|
||||
export const getOwnerTreeStandalone_api = (appId: string, owner: string) => server.post(`/application/${appId}/_/api/menu/owner/tree/${owner}`, {});
|
||||
// 保存集成菜单
|
||||
export const saveOwnerMenu_api = (owner: string, appId: string, data: object) => server.patch(`/menu/owner/${owner}/${appId}/_all`, data);
|
||||
|
|
|
@ -53,7 +53,13 @@ const iconKeys = [
|
|||
'FileTextOutlined',
|
||||
'UploadOutlined',
|
||||
'LikeOutlined',
|
||||
'ArrowLeftOutlined'
|
||||
'ArrowLeftOutlined',
|
||||
'DownloadOutlined',
|
||||
'PauseOutlined',
|
||||
'ControlOutlined',
|
||||
'RedoOutlined',
|
||||
'VideoCameraOutlined',
|
||||
'HistoryOutlined',
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -40,11 +40,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-mask" v-if="props.hasMark">
|
||||
<div class="mask-content">
|
||||
<slot name="mark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<slot name="bottom-tool">
|
||||
<div v-if="showTool && actions && actions.length" class="card-tools">
|
||||
<div
|
||||
v-if="showTool && actions && actions.length"
|
||||
class="card-tools"
|
||||
>
|
||||
<div
|
||||
v-for="item in actions"
|
||||
:key="item.key"
|
||||
|
@ -53,8 +61,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>
|
||||
|
@ -79,10 +87,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SearchOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import BadgeStatus from '@/components/BadgeStatus/index.vue';
|
||||
import { StatusColorEnum } from '@/utils/consts.ts';
|
||||
import type { ActionsType } from '@/components/Table/index.vue'
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
type EmitProps = {
|
||||
|
@ -90,14 +102,14 @@ type EmitProps = {
|
|||
(e: 'click', data: Record<string, any>): void;
|
||||
};
|
||||
|
||||
type TableActionsType = Partial<ActionsType>
|
||||
type TableActionsType = Partial<ActionsType>;
|
||||
|
||||
const emit = defineEmits<EmitProps>();
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => {}
|
||||
default: () => {},
|
||||
},
|
||||
showStatus: {
|
||||
type: Boolean,
|
||||
|
@ -124,8 +136,12 @@ const props = defineProps({
|
|||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
hasMark: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
|
@ -167,9 +183,13 @@ const handleClick = () => {
|
|||
position: relative;
|
||||
border: 1px solid #e6e6e6;
|
||||
|
||||
&.hover {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 24px rgba(#000, 0.1);
|
||||
|
||||
.card-mask {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
@ -269,12 +289,12 @@ const handleClick = () => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-color: rgba(#000, 0);
|
||||
background-color: rgba(#000, .5);
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
> div {
|
||||
.mask-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -282,11 +302,6 @@ const handleClick = () => {
|
|||
height: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&.show {
|
||||
background-color: rgba(#000, 0.5);
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,11 @@ const props = defineProps({
|
|||
class: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
// defaultTerms: {
|
||||
// type: Object,
|
||||
// default: () => ({})
|
||||
// }
|
||||
})
|
||||
|
||||
const searchRef = ref(null)
|
||||
|
@ -223,6 +227,7 @@ const handleParamsFormat = () => {
|
|||
*/
|
||||
const searchSubmit = () => {
|
||||
emit('search', handleParamsFormat())
|
||||
console.log('searchSubmit')
|
||||
if (props.type === 'advanced') {
|
||||
addUrlParams()
|
||||
}
|
||||
|
|
|
@ -144,6 +144,10 @@ const JTable = defineComponent<JTableProps>({
|
|||
pageSize: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll: {
|
||||
type: Object,
|
||||
default: () => { x: 1366 }
|
||||
}
|
||||
} as any,
|
||||
setup(props: JTableProps, { slots, emit, expose }) {
|
||||
|
@ -260,7 +264,7 @@ const JTable = defineComponent<JTableProps>({
|
|||
/**
|
||||
* 导出方法
|
||||
*/
|
||||
expose({ reload })
|
||||
expose({ reload, _dataSource })
|
||||
|
||||
return () => <Spin spinning={loading.value}>
|
||||
<div class={styles["jtable-body"]} style={{ ...props.bodyStyle }}>
|
||||
|
@ -331,7 +335,7 @@ const JTable = defineComponent<JTableProps>({
|
|||
pagination={false}
|
||||
rowKey="id"
|
||||
rowSelection={props.rowSelection}
|
||||
scroll={{ x: 1366 }}
|
||||
scroll={props.scroll}
|
||||
v-slots={{
|
||||
bodyCell: (dt: Record<string, any>) => {
|
||||
const { column, record } = dt;
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="jtable-body">
|
||||
<div class="jtable-body-header">
|
||||
<div class="jtable-body-header-left">
|
||||
<slot name="headerTitle"></slot>
|
||||
</div>
|
||||
<div class="jtable-body-header-right" v-if="!model">
|
||||
<div class="jtable-setting-item" :class="[ModelEnum.CARD === _model ? 'active' : '']" @click="modelChange(ModelEnum.CARD)">
|
||||
<AppstoreOutlined />
|
||||
</div>
|
||||
<div class="jtable-setting-item" :class="[ModelEnum.TABLE === _model ? 'active' : '']" @click="modelChange(ModelEnum.TABLE)">
|
||||
<UnorderedListOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jtable-content">
|
||||
<div class="jtable-alert" v-if="rowSelection && rowSelection.selectedRowKeys && rowSelection.selectedRowKeys.length">
|
||||
<a-alert :message="'已选择' + rowSelection.selectedRowKeys.length + '项'" type="info" :afterClose="handleAlertClose">
|
||||
<template #closeText>
|
||||
<a>取消选择</a>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
<div v-if="_model === ModelEnum.CARD" class="jtable-card">
|
||||
<div
|
||||
v-if="_dataSource.length"
|
||||
class="jtable-card-items"
|
||||
:style="{gridTemplateColumns: `repeat(${column}, 1fr)`}"
|
||||
>
|
||||
<div
|
||||
class="jtable-card-item"
|
||||
v-for="(item, index) in _dataSource"
|
||||
:key="index"
|
||||
>
|
||||
<slot name="card" v-bind="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-table rowKey="operationId" :rowSelection="rowSelection" :columns="[..._columns]" :dataSource="_dataSource" :pagination="false">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- <template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-tooltip v-for="i in actions" :key="i.key" v-bind="i.tooltip">
|
||||
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
|
||||
<a><AIcon :type="i.icon" /></a>
|
||||
</a-popconfirm>
|
||||
<a v-else @click="i.onClick && i.onClick(record)">
|
||||
<AIcon :type="i.icon" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template> -->
|
||||
<template v-if="column.scopedSlots">
|
||||
<slot :name="column.key" :row="record"></slot>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jtable-pagination" v-if="_dataSource.length && !noPagination">
|
||||
<a-pagination
|
||||
size="small"
|
||||
:total="total"
|
||||
:showQuickJumper="false"
|
||||
:showSizeChanger="true"
|
||||
v-model:current="pageIndex"
|
||||
v-model:page-size="pageSize"
|
||||
:show-total="(total, range) => `第 ${range[0]} - ${range[1]} 条/总共 ${total} 条`"
|
||||
@change="pageChange"
|
||||
:page-size-options="[12, 24, 48, 60, 100]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="JTable">
|
||||
import { UnorderedListOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
|
||||
import type { TableProps, ColumnsType } from 'ant-design-vue/es/table'
|
||||
import type { TooltipProps } from 'ant-design-vue/es/tooltip'
|
||||
import type { PopconfirmProps } from 'ant-design-vue/es/popconfirm'
|
||||
import { Empty } from 'ant-design-vue'
|
||||
import { CSSProperties } from 'vue';
|
||||
|
||||
enum ModelEnum {
|
||||
TABLE = 'TABLE',
|
||||
CARD = 'CARD',
|
||||
}
|
||||
|
||||
type RequestData = {
|
||||
code: string;
|
||||
result: {
|
||||
data: Record<string, any>[] | undefined;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
status: number;
|
||||
} & Record<string, any>;
|
||||
|
||||
export interface ActionsType {
|
||||
key: string;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
permission?: boolean;
|
||||
onClick?: (data: any) => void;
|
||||
style?: CSSProperties;
|
||||
tooltip?: TooltipProps;
|
||||
popConfirm?: PopconfirmProps;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface JColumnsProps extends ColumnsType{
|
||||
scopedSlots?: boolean; // 是否为插槽 true: 是 false: 否
|
||||
}
|
||||
|
||||
export interface JTableProps extends TableProps{
|
||||
request?: (params: Record<string, any> & {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
}) => Promise<Partial<RequestData>>;
|
||||
cardBodyClass?: string;
|
||||
columns: JColumnsProps;
|
||||
params?: Record<string, any> & {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
};
|
||||
model?: keyof typeof ModelEnum | undefined; // 显示table还是card
|
||||
actions?: ActionsType[];
|
||||
noPagination?: boolean;
|
||||
rowSelection?: TableProps['rowSelection'];
|
||||
cardProps?: Record<string, any>;
|
||||
dataSource?: Record<string, any>[];
|
||||
}
|
||||
// props
|
||||
const props = withDefaults(defineProps<JTableProps>(), {
|
||||
cardBodyClass: '',
|
||||
request: undefined,
|
||||
})
|
||||
|
||||
// emit
|
||||
const emit = defineEmits<{
|
||||
(e: 'cancelSelect'): void
|
||||
}>()
|
||||
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
|
||||
|
||||
const _model = ref<keyof typeof ModelEnum>(props.model ? props.model : ModelEnum.CARD); // 模式切换
|
||||
const column = ref<number>(4);
|
||||
const _dataSource = ref<Record<string, any>[]>([])
|
||||
const pageIndex = ref<number>(0)
|
||||
const pageSize = ref<number>(6)
|
||||
const total = ref<number>(0)
|
||||
const _columns = ref<Record<string, any>[]>([...props.columns])
|
||||
const loading = ref<boolean>(true)
|
||||
|
||||
// 方法
|
||||
// 切换卡片和表格
|
||||
const modelChange = (type: keyof typeof ModelEnum) => {
|
||||
_model.value = type
|
||||
}
|
||||
/**
|
||||
* 请求数据
|
||||
*/
|
||||
const handleSearch = async (_params?: Record<string, any>) => {
|
||||
loading.value = true
|
||||
if(props.request) {
|
||||
const resp = await props.request({
|
||||
pageSize: 12,
|
||||
pageIndex: 1,
|
||||
..._params
|
||||
})
|
||||
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,
|
||||
})
|
||||
} 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 = props?.dataSource || []
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
/**
|
||||
* 页码变化
|
||||
*/
|
||||
const pageChange = (page: number, size: number) => {
|
||||
handleSearch({
|
||||
...props.params,
|
||||
pageSize: size,
|
||||
pageIndex: pageSize.value === size ? page : 1,
|
||||
})
|
||||
}
|
||||
|
||||
// alert关闭,取消选择
|
||||
const handleAlertClose = () => {
|
||||
emit('cancelSelect')
|
||||
}
|
||||
|
||||
// watchEffect(() => {
|
||||
// if(Array.isArray(props.actions) && props.actions.length) {
|
||||
// _columns.value = [...props.columns,
|
||||
// {
|
||||
// title: '操作',
|
||||
// key: 'action',
|
||||
// fixed: 'right',
|
||||
// width: 250
|
||||
// }
|
||||
// ]
|
||||
// } else {
|
||||
// _columns.value = [...props.columns]
|
||||
// }
|
||||
// })
|
||||
|
||||
watchEffect(() => {
|
||||
handleSearch(props.params)
|
||||
})
|
||||
// TODO 选择的双向绑定和图标的渲染
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.jtable-body {
|
||||
width: 100%;
|
||||
padding: 0 24px 24px;
|
||||
background-color: white;
|
||||
.jtable-body-header {
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.jtable-body-header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.jtable-setting-item {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color-hover;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: @primary-color-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.jtable-content {
|
||||
.jtable-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.jtable-card {
|
||||
.jtable-card-items {
|
||||
display: grid;
|
||||
grid-gap: 26px;
|
||||
.jtable-card-item {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.jtable-pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
/deep/ .ant-pagination-item {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,8 @@ import { filterAsnycRouter, MenuItem } from '@/utils/menu'
|
|||
import { isArray } from 'lodash-es'
|
||||
import { usePermissionStore } from './permission'
|
||||
import router from '@/router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { onlyMessage } from '@/utils/comm'
|
||||
|
||||
const defaultOwnParams = [
|
||||
{
|
||||
|
@ -77,6 +79,7 @@ export const useMenuStore = defineStore({
|
|||
name, params, query
|
||||
})
|
||||
} else {
|
||||
onlyMessage('暂无权限,请联系管理员', 'error')
|
||||
console.warn(`没有找到对应的页面: ${name}`)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -68,7 +68,7 @@ const defaultOptions = {
|
|||
};
|
||||
|
||||
export const useSceneStore = defineStore('scene', () => {
|
||||
const data = reactive<FormModelType | any>({
|
||||
const data = reactive<FormModelType>({
|
||||
trigger: { type: ''},
|
||||
options: defaultOptions,
|
||||
branches: defaultBranches,
|
||||
|
@ -116,67 +116,3 @@ export const useSceneStore = defineStore('scene', () => {
|
|||
getDetail
|
||||
}
|
||||
})
|
||||
//
|
||||
// export const useSceneStore = defineStore({
|
||||
// id: 'scene',
|
||||
// state: (): DataType => {
|
||||
// return {
|
||||
// data: {
|
||||
// trigger: { type: ''},
|
||||
// options: defaultOptions,
|
||||
// branches: defaultBranches,
|
||||
// description: ''
|
||||
// },
|
||||
// productCache: {}
|
||||
// }
|
||||
// },
|
||||
// actions: {
|
||||
// /**
|
||||
// * 初始化数据
|
||||
// */
|
||||
// initData() {
|
||||
//
|
||||
// },
|
||||
// /**
|
||||
// * 获取详情
|
||||
// * @param id
|
||||
// */
|
||||
// async getDetail(id: string) {
|
||||
// const resp = await detail(id)
|
||||
// if (resp.success) {
|
||||
// const result = resp.result as SceneItem
|
||||
// const triggerType = result.triggerType
|
||||
// let branches: any[] = result.branches
|
||||
//
|
||||
// if (!branches) {
|
||||
// branches = cloneDeep(defaultBranches)
|
||||
// if (triggerType === 'device') {
|
||||
// branches.push(null)
|
||||
// }
|
||||
// } else {
|
||||
// const branchesLength = branches.length;
|
||||
// if (
|
||||
// triggerType === 'device' &&
|
||||
// ((branchesLength === 1 && !!branches[0]?.when?.length) || // 有一组数据并且when有值
|
||||
// (branchesLength > 1 && !branches[branchesLength - 1]?.when?.length)) // 有多组否则数据,并且最后一组when有值
|
||||
// ) {
|
||||
// branches.push(null);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.data = {
|
||||
// ...result,
|
||||
// trigger: result.trigger || {},
|
||||
// branches: cloneDeep(assignmentKey(branches)),
|
||||
// options: {...defaultOptions, ...result.options },
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// getProduct() {
|
||||
//
|
||||
// }
|
||||
// },
|
||||
// getters: {
|
||||
//
|
||||
// }
|
||||
// })
|
|
@ -7,4 +7,12 @@ export const isUrl = (path: string): boolean => urlReg.test(path)
|
|||
|
||||
export const inputReg = /^[a-zA-Z0-9_\-]+$/
|
||||
|
||||
export const isInput = (value: string) => inputReg.test(value)
|
||||
export const isInput = (value: string) => inputReg.test(value)
|
||||
|
||||
// cron 表达式
|
||||
|
||||
export const CronRegEx = new RegExp(
|
||||
'^\\s*($|#|\\w+\\s*=|(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?(?:,(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?)*)\\s+(\\?|\\*|(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?(?:,(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?)*)\\s+(\\?|\\*|(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?(?:,(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?)*|\\?|\\*|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*)\\s+(\\?|\\*|(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?(?:,(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?)*|\\?|\\*|(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?(?:,(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?)*)(|\\s)+(\\?|\\*|(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?(?:,(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?)*))$',
|
||||
);
|
||||
|
||||
export const isCron = (value: string) => CronRegEx.test(value)
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search :columns="columns" target="northbound-aliyun" @search="handleSearch" />
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="northbound-aliyun"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="instanceRef"
|
||||
:columns="columns"
|
||||
|
@ -24,22 +28,15 @@
|
|||
<CardBox
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:status="slotProps.state?.value"
|
||||
:statusText="slotProps.state?.text"
|
||||
:statusNames="{
|
||||
enabled: 'success',
|
||||
disabled: 'error'
|
||||
disabled: 'error',
|
||||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
:src="
|
||||
getImage('/northbound/aliyun.png')
|
||||
"
|
||||
/>
|
||||
</slot>
|
||||
<img :src="getImage('/northbound/aliyun.png')" />
|
||||
</template>
|
||||
<template #content>
|
||||
<h3
|
||||
|
@ -114,23 +111,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
query,
|
||||
_undeploy,
|
||||
_deploy,
|
||||
_delete
|
||||
} from '@/api/northbound/alicloud';
|
||||
import { query, _undeploy, _deploy, _delete } from '@/api/northbound/alicloud';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useMenuStore } from 'store/menu'
|
||||
import { useMenuStore } from 'store/menu';
|
||||
|
||||
const router = useRouter();
|
||||
const instanceRef = ref<Record<string, any>>({});
|
||||
const params = ref<Record<string, any>>({});
|
||||
const current = ref<Record<string, any>>({});
|
||||
|
||||
const menuStory = useMenuStore()
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const statusMap = new Map();
|
||||
statusMap.set('enabled', 'success');
|
||||
|
@ -167,7 +159,7 @@ const columns = [
|
|||
type: 'select',
|
||||
options: [
|
||||
{ label: '正常', value: 'enabled' },
|
||||
{ label: '禁用', value: 'disabled' }
|
||||
{ label: '禁用', value: 'disabled' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -184,14 +176,14 @@ const columns = [
|
|||
* 新增
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
menuStory.jumpPage('Northbound/AliCloud/Detail', { id: ':id'})
|
||||
menuStory.jumpPage('Northbound/AliCloud/Detail', { id: ':id' });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
const handleView = (id: string) => {
|
||||
menuStory.jumpPage('Northbound/AliCloud/Detail', { id }, { type: 'view'})
|
||||
menuStory.jumpPage('Northbound/AliCloud/Detail', { id }, { type: 'view' });
|
||||
};
|
||||
|
||||
const getActions = (
|
||||
|
@ -219,7 +211,11 @@ const getActions = (
|
|||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
menuStory.jumpPage('Northbound/AliCloud/Detail', { id: data.id }, { type: 'edit'})
|
||||
menuStory.jumpPage(
|
||||
'Northbound/AliCloud/Detail',
|
||||
{ id: data.id },
|
||||
{ type: 'edit' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -283,6 +279,6 @@ const getActions = (
|
|||
};
|
||||
|
||||
const handleSearch = (_params: any) => {
|
||||
params.value = _params
|
||||
}
|
||||
params.value = _params;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
<CardBox
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:status="slotProps.state?.value"
|
||||
:statusText="slotProps.state?.text"
|
||||
:statusNames="{
|
||||
|
@ -37,9 +36,7 @@
|
|||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/cloud/dueros.png')" />
|
||||
</slot>
|
||||
<img :src="getImage('/cloud/dueros.png')" />
|
||||
</template>
|
||||
<template #content>
|
||||
<h3
|
||||
|
|
|
@ -183,7 +183,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { message, Form } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import type { UploadChangeParam } from 'ant-design-vue';
|
||||
import FileUpload from './FileUpload.vue';
|
||||
import { save, update, queryProduct } from '@/api/device/firmware';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
|
@ -260,7 +259,7 @@ const { resetFields, validate, validateInfos } = useForm(
|
|||
{ required: true, message: '请输入版本号' },
|
||||
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
|
||||
],
|
||||
versionOrder: [{ required: true, message: '请输入版本号' }],
|
||||
versionOrder: [{ required: true, message: '请输入版本序号' }],
|
||||
signMethod: [{ required: true, message: '请选择签名方式' }],
|
||||
sign: [
|
||||
{ required: true, message: '请输入签名' },
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<template lang="">
|
||||
<a-modal
|
||||
title="查看"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:visible="true"
|
||||
width="500px"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<span>失败原因:{{ data }}</span>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup name="TaskDetailSavePage">
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const handleOk = () => {
|
||||
handleCancel();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,437 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div>
|
||||
<div class="state-container">
|
||||
<div
|
||||
class="state-body"
|
||||
v-for="item in stateList"
|
||||
:key="item.key"
|
||||
>
|
||||
<div class="state-content">
|
||||
<div class="state-header">
|
||||
<div class="state-title">
|
||||
<a-badge
|
||||
:text="item.name"
|
||||
:color="colorMap.get(item.key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="state-title-right">
|
||||
<div>
|
||||
<a-popconfirm
|
||||
title="确定批量重试?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="confirm"
|
||||
v-if="
|
||||
item.key === 'failed' &&
|
||||
stateInfo?.mode?.value === 'push'
|
||||
"
|
||||
>
|
||||
<a href="#">批量重试</a>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
|
||||
<div class="img">
|
||||
<img
|
||||
:src="buttonImg"
|
||||
@click="handleRefresh(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="state-box">
|
||||
<div
|
||||
class="state-left"
|
||||
:style="`color: ${colorMap.get(item.key)}`"
|
||||
>
|
||||
{{ state[item.key] }}
|
||||
</div>
|
||||
<img class="state-right" :src="item.img" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
model="TABLE"
|
||||
:columns="columns"
|
||||
:request="history"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: defaultParams,
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #createTime="slotProps">
|
||||
<span>{{
|
||||
moment(slotProps.createTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
}}</span>
|
||||
</template>
|
||||
<template #productId="slotProps">
|
||||
<span>{{ slotProps.productName }}</span>
|
||||
</template>
|
||||
<template #state="slotProps">
|
||||
<a-badge
|
||||
:text="slotProps.state.text"
|
||||
:color="colorMap.get(slotProps.state.value)"
|
||||
/>
|
||||
</template>
|
||||
<template #progress="slotProps">
|
||||
<span>{{ slotProps.progress }}%</span>
|
||||
</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>
|
||||
<Save :data="current" v-if="visible" @change="saveChange" />
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="TaskDetailPage">
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import {
|
||||
taskById,
|
||||
history,
|
||||
historyCount,
|
||||
queryProduct,
|
||||
startTask,
|
||||
startOneTask,
|
||||
} from '@/api/device/firmware';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import moment from 'moment';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import Save from './Save.vue';
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const params = ref<Record<string, any>>({});
|
||||
const taskId = route.params?.id as string;
|
||||
const visible = ref(false);
|
||||
const current = ref({});
|
||||
const productOptions = ref([]);
|
||||
|
||||
const colorMap = new Map();
|
||||
colorMap.set('waiting', '#FF9000');
|
||||
colorMap.set('processing', '#4293FF');
|
||||
colorMap.set('failed', '#F76F5D');
|
||||
colorMap.set('success', '#24B276');
|
||||
colorMap.set('canceled', '#999');
|
||||
|
||||
const stateList = [
|
||||
{
|
||||
key: 'waiting',
|
||||
name: '等待升级',
|
||||
img: getImage('/firmware/waiting.png'),
|
||||
},
|
||||
{
|
||||
key: 'processing',
|
||||
name: '升级中',
|
||||
img: getImage('/firmware/loading.png'),
|
||||
},
|
||||
{
|
||||
key: 'success',
|
||||
name: '升级完成',
|
||||
img: getImage('/firmware/finish.png'),
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
name: '升级失败',
|
||||
img: getImage('/firmware/error.png'),
|
||||
},
|
||||
{
|
||||
key: 'canceled',
|
||||
name: '已停止',
|
||||
img: getImage('/firmware/cancel.png'),
|
||||
},
|
||||
];
|
||||
|
||||
const buttonImg = getImage('/firmware/button.png');
|
||||
const state = ref({
|
||||
waiting: 0,
|
||||
processing: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
canceled: 0,
|
||||
});
|
||||
const stateInfo = ref();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'deviceName',
|
||||
key: 'deviceName',
|
||||
fixed: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属产品',
|
||||
dataIndex: 'productId',
|
||||
key: 'productId',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
// search: {
|
||||
// type: 'select',
|
||||
// options: productOptions,
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
key: 'completeTime',
|
||||
ellipsis: true,
|
||||
dataIndex: 'completeTime',
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
ellipsis: true,
|
||||
scopedSlots: true,
|
||||
width: 200,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: stateList.map((item) => ({
|
||||
value: item.key,
|
||||
label: item.name,
|
||||
})),
|
||||
},
|
||||
scopedSlots: true,
|
||||
width: 200,
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultParams = [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'taskId',
|
||||
value: taskId,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const Actions = [
|
||||
{
|
||||
key: 'eye',
|
||||
text: '查看',
|
||||
tooltip: {
|
||||
title: '查看',
|
||||
},
|
||||
icon: 'EyeOutlined',
|
||||
onClick: async () => {
|
||||
handlEye(data.errorReason);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'try',
|
||||
text: '重试',
|
||||
tooltip: {
|
||||
title: '重试',
|
||||
},
|
||||
icon: 'RedoOutlined',
|
||||
onClick: async () => {
|
||||
handlTry(data.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return Actions;
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlEye = (data: string) => {
|
||||
current.value = data || '';
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlTry = async (id: string) => {
|
||||
const res = await startOneTask([id]);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
const saveChange = (value: boolean) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
};
|
||||
const confirm = async (e: MouseEvent) => {
|
||||
const res = await startTask(taskId, ['failed']);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
handleRefresh('failed');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async (key: string) => {
|
||||
const terms = cloneDeep(defaultParams);
|
||||
terms[0].terms.push({ column: 'state', value: key });
|
||||
const res = await historyCount({ terms });
|
||||
if (res.success) {
|
||||
state.value[key] = res?.result || 0;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
stateList.forEach((item) => {
|
||||
handleRefresh(item.key);
|
||||
});
|
||||
taskById(taskId).then((res) => {
|
||||
if (res.success) {
|
||||
stateInfo.value = res?.result;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.state-container {
|
||||
width: 100%;
|
||||
min-height: 148px;
|
||||
background-color: #ffffff;
|
||||
padding: 24px 12px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.state-body {
|
||||
background: linear-gradient(
|
||||
135.62deg,
|
||||
#f6f7fd 22.27%,
|
||||
rgba(255, 255, 255, 0.86) 91.82%
|
||||
);
|
||||
min-width: 185px;
|
||||
max-width: 580px;
|
||||
flex: 1px;
|
||||
margin: 0 12px;
|
||||
.state-content {
|
||||
width: 100% -15px;
|
||||
height: 100%;
|
||||
margin: 15px 0 0 15px;
|
||||
align-content: center;
|
||||
|
||||
.state-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 22px;
|
||||
.state-title-right {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
.img {
|
||||
width: 22px;
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 22px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
.img:active {
|
||||
border: 1px #40a9ff solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
.state-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.state-left {
|
||||
flex: 1;
|
||||
font-size: 52px;
|
||||
}
|
||||
.state-right {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
margin-top: -31px;
|
||||
max-width: 120px;
|
||||
max-height: 94px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,289 @@
|
|||
<template lang="">
|
||||
<a-input
|
||||
placeholder="请选择设备"
|
||||
v-model:value="checkLable"
|
||||
:disabled="true"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<AIcon
|
||||
:class="data.view ? 'disabled' : ''"
|
||||
type="EditOutlined"
|
||||
@click="onVisible"
|
||||
/>
|
||||
</template>
|
||||
</a-input>
|
||||
<a-modal
|
||||
v-if="visible"
|
||||
title="选择设备"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:visible="true"
|
||||
width="80%"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="search"
|
||||
@search="handleSearch"
|
||||
type="simple"
|
||||
/>
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
model="TABLE"
|
||||
:columns="columns"
|
||||
:request="queryDetailList"
|
||||
:defaultParams="defaultParams"
|
||||
:rowSelection="{
|
||||
selectedRowKeys: _selectedRowKeys,
|
||||
onSelect: onSelectChange,
|
||||
onSelectAll: onSelectAllChange,
|
||||
}"
|
||||
@cancelSelect="cancelSelect"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-checkbox
|
||||
v-model:checked="state.checkAll"
|
||||
:indeterminate="state.indeterminate"
|
||||
@change="onCheckAllChange"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
全选
|
||||
</a-checkbox>
|
||||
</template>
|
||||
<template #productId="slotProps">
|
||||
<span>{{ slotProps.productName }}</span>
|
||||
</template>
|
||||
<template #state="slotProps">
|
||||
<a-badge
|
||||
:text="slotProps.state?.text"
|
||||
:status="statusMap.get(slotProps.state?.value)"
|
||||
/>
|
||||
</template>
|
||||
<template #version="slotProps">
|
||||
<span>{{ slotProps.firmwareInfo?.version || '' }}</span>
|
||||
</template>
|
||||
<template #registerTime="slotProps">
|
||||
<span>{{
|
||||
moment(slotProps.registerTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}}</span>
|
||||
</template>
|
||||
</JTable>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup name="SelectDevicesPage">
|
||||
import {
|
||||
queryDetailListNoPaging,
|
||||
queryDetailList,
|
||||
} from '@/api/device/firmware';
|
||||
import moment from 'moment';
|
||||
|
||||
type T = any;
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const params = ref<Record<string, any>>({});
|
||||
const visible = ref(false);
|
||||
const _selectedRowKeys = ref<string[]>([]);
|
||||
|
||||
const state = reactive({
|
||||
indeterminate: false,
|
||||
checkAll: false,
|
||||
checkedList: [],
|
||||
});
|
||||
let checkAllData: T[] = [];
|
||||
const checkAllDataMap = new Map();
|
||||
const checkLable = ref();
|
||||
|
||||
const defaultParams = {
|
||||
context: {
|
||||
includeTags: false,
|
||||
includeBind: false,
|
||||
includeRelations: false,
|
||||
},
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'productId',
|
||||
value: route.query.productId,
|
||||
},
|
||||
],
|
||||
type: 'and',
|
||||
},
|
||||
],
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
};
|
||||
|
||||
const statusMap = new Map();
|
||||
statusMap.set('online', 'success');
|
||||
statusMap.set('offline', 'error');
|
||||
statusMap.set('notActive', 'warning');
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
dataIndex: 'id',
|
||||
fixed: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '设备名称',
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '固件版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
key: 'registerTime',
|
||||
dataIndex: 'registerTime',
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '禁用', value: 'notActive' },
|
||||
],
|
||||
},
|
||||
width: 150,
|
||||
},
|
||||
];
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
Object.assign(state, {
|
||||
checkedList: e.target.checked ? checkAllData : [],
|
||||
indeterminate: false,
|
||||
});
|
||||
_selectedRowKeys.value = state.checkedList;
|
||||
};
|
||||
|
||||
const onSelectChange = (record: T[], selected: boolean, selectedRows: T[]) => {
|
||||
_selectedRowKeys.value = selected
|
||||
? [...getSetRowKey(selectedRows)]
|
||||
: _selectedRowKeys.value.filter((item: T) => item !== record?.id);
|
||||
};
|
||||
const onSelectAllChange = (
|
||||
selected: boolean,
|
||||
selectedRows: T[],
|
||||
changeRows: T[],
|
||||
) => {
|
||||
const unRowsKeys = getSelectedRowsKey(changeRows);
|
||||
_selectedRowKeys.value = selected
|
||||
? [...getSetRowKey(selectedRows)]
|
||||
: _selectedRowKeys.value
|
||||
.concat(unRowsKeys)
|
||||
.filter((item) => !unRowsKeys.includes(item));
|
||||
};
|
||||
|
||||
const getSelectedRowsKey = (selectedRows: T[]) =>
|
||||
selectedRows.map((item) => item?.id).filter((i) => !!i);
|
||||
|
||||
const getSetRowKey = (selectedRows: T[]) =>
|
||||
new Set([..._selectedRowKeys.value, ...getSelectedRowsKey(selectedRows)]);
|
||||
|
||||
const cancelSelect = () => {
|
||||
_selectedRowKeys.value = [];
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
checkLable.value = updateSelect(_selectedRowKeys.value);
|
||||
emit('update:modelValue', _selectedRowKeys.value);
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const updateSelect = (selectedRowKeys: T[]) =>
|
||||
selectedRowKeys
|
||||
.map((item) => checkAllDataMap.has(item) && checkAllDataMap.get(item))
|
||||
.toString();
|
||||
|
||||
const onVisible = () => {
|
||||
!props.data.view && (visible.value = true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
cancelSelect();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryDetailListNoPaging({ ...defaultParams, paging: false }).then(
|
||||
(resp: T) => {
|
||||
if (resp.status === 200) {
|
||||
checkAllData = resp.result.map((item: T) => {
|
||||
checkAllDataMap.set(item.id, item.name);
|
||||
return item.id;
|
||||
});
|
||||
if (props.data.id) {
|
||||
checkLable.value = updateSelect(props.data.deviceId);
|
||||
emit('update:modelValue', props.data.deviceId);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => _selectedRowKeys.value,
|
||||
(val) => {
|
||||
Object.assign(state, {
|
||||
checkedList: val,
|
||||
indeterminate: !!val.length && val.length < checkAllData.length,
|
||||
checkAll:
|
||||
!!checkAllData.length && val.length === checkAllData.length,
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.disabled {
|
||||
pointer-events: auto !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,217 @@
|
|||
<template lang="">
|
||||
<a-modal
|
||||
:title="data.id ? '查看' : '新增' + '任务'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
:visible="true"
|
||||
width="700px"
|
||||
:confirm-loading="loading"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<a-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
ref="formRef"
|
||||
:rules="rules"
|
||||
>
|
||||
<a-row :gutter="[24, 0]">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="任务名称" name="name">
|
||||
<a-input
|
||||
placeholder="请输入任务名称"
|
||||
v-model:value="formData.name"
|
||||
:disabled="view"
|
||||
/></a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24"
|
||||
><a-form-item label="推送方式" name="mode">
|
||||
<a-select
|
||||
v-model:value="formData.mode"
|
||||
:options="[
|
||||
{ label: '平台推送', value: 'push' },
|
||||
{ label: '设备拉取', value: 'pull' },
|
||||
]"
|
||||
placeholder="请选择推送方式"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="changeMode"
|
||||
:disabled="view"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12" v-if="formData.mode === 'push'"
|
||||
><a-form-item
|
||||
label="响应超时时间"
|
||||
name="responseTimeoutSeconds"
|
||||
>
|
||||
<a-input-number
|
||||
placeholder="请输入响应超时时间(秒)"
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="99999"
|
||||
:disabled="view"
|
||||
v-model:value="
|
||||
formData.responseTimeoutSeconds
|
||||
" /></a-form-item
|
||||
></a-col>
|
||||
<a-col
|
||||
:span="formData.mode === 'push' ? 12 : 24"
|
||||
v-if="formData.mode === 'push' || formData.mode === 'pull'"
|
||||
><a-form-item label="升级超时时间" name="timeoutSeconds">
|
||||
<a-input-number
|
||||
placeholder="请输入升级超时时间(秒)"
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
:max="99999"
|
||||
:disabled="view"
|
||||
v-model:value="
|
||||
formData.timeoutSeconds
|
||||
" /></a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12" v-if="!!formData.mode"
|
||||
><a-form-item label="升级设备" name="releaseType">
|
||||
<a-radio-group
|
||||
v-model:value="formData.releaseType"
|
||||
button-style="solid"
|
||||
@change="changeShareCluster"
|
||||
:disabled="view"
|
||||
>
|
||||
<a-radio value="all">所有设备</a-radio>
|
||||
<a-radio value="part">选择设备</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" v-if="formData.releaseType === 'part'">
|
||||
<a-form-item label="选择设备" name="deviceId">
|
||||
<SelectDevices
|
||||
v-model:modelValue="formData.deviceId"
|
||||
:data="data"
|
||||
></SelectDevices> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="说明" name="description">
|
||||
<a-textarea
|
||||
placeholder="请输入说明"
|
||||
v-model:value="formData.description"
|
||||
:maxlength="200"
|
||||
:rows="3"
|
||||
showCount
|
||||
:disabled="view"
|
||||
/> </a-form-item
|
||||
></a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup name="TaskPage">
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { queryProduct, saveTask } from '@/api/device/firmware';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import SelectDevices from './SelectDevices.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const productOptions = ref([]);
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const firmwareId = route.query.id;
|
||||
const productId = route.query.productId;
|
||||
const view = props.data.view;
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
mode: undefined,
|
||||
responseTimeoutSeconds: '',
|
||||
timeoutSeconds: '',
|
||||
releaseType: 'all',
|
||||
deviceId: undefined,
|
||||
description: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入任务名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
mode: [{ required: true, message: '请选择推送方式' }],
|
||||
responseTimeoutSeconds: [{ required: true, message: '请输入响应超时时间' }],
|
||||
timeoutSeconds: [{ required: true, message: '请输入升级超时时间' }],
|
||||
releaseType: [{ required: true }],
|
||||
deviceId: [{ required: true, message: '请选择设备' }],
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
};
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const params = await formRef.value?.validate();
|
||||
loading.value = true;
|
||||
const resp = await saveTask({
|
||||
...params,
|
||||
firmwareId,
|
||||
productId,
|
||||
});
|
||||
loading.value = false;
|
||||
resp.success && emit('change', true);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
return view ? emit('change', false) : onSubmit();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
};
|
||||
|
||||
const changeShareCluster = () => {
|
||||
formData.value.deviceId = undefined;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
queryProduct({
|
||||
paging: false,
|
||||
terms: [{ column: 'state', value: 1 }],
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}).then((resp) => {
|
||||
productOptions.value = resp.result.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
});
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (value.id) {
|
||||
formData.value = {
|
||||
...value,
|
||||
releaseType: value?.deviceId ? 'part' : 'all',
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form {
|
||||
.form-submit {
|
||||
background-color: @primary-color !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,248 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search :columns="columns" target="search" @search="handleSearch" />
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
model="TABLE"
|
||||
:columns="columns"
|
||||
:request="task"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: defaultParams,
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><AIcon type="PlusOutlined" />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #mode="slotProps">
|
||||
<span>{{ slotProps.mode.text }}</span>
|
||||
</template>
|
||||
<template #progress="slotProps">
|
||||
<span>{{ slotProps.progress }}%</span>
|
||||
</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>
|
||||
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="TaskPage">
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import { task, startTask, stopTask } from '@/api/device/firmware';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Save from './Save/index.vue';
|
||||
import { useMenuStore } from 'store/menu';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const visible = ref(false);
|
||||
const current = ref({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
fixed: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '推送方式',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '设备拉取',
|
||||
value: 'pull',
|
||||
},
|
||||
{
|
||||
label: '平台推送',
|
||||
value: 'push',
|
||||
},
|
||||
],
|
||||
},
|
||||
scopedSlots: true,
|
||||
width: 200,
|
||||
},
|
||||
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '完成比例',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
ellipsis: true,
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultParams = [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'firmwareId',
|
||||
value: route.query.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stop = data.waiting > 0 && data?.state?.value === 'processing';
|
||||
const pause = data?.state?.value === 'canceled';
|
||||
|
||||
const Actions = [
|
||||
{
|
||||
key: 'details',
|
||||
text: '详情',
|
||||
tooltip: {
|
||||
title: '详情',
|
||||
},
|
||||
icon: 'icon-details',
|
||||
onClick: async () => {
|
||||
handlDetails(data.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'eye',
|
||||
text: '查看',
|
||||
tooltip: {
|
||||
title: '查看',
|
||||
},
|
||||
icon: 'EyeOutlined',
|
||||
onClick: async () => {
|
||||
handlEye(data);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (stop) {
|
||||
Actions.push({
|
||||
key: 'actions',
|
||||
text: '停止',
|
||||
tooltip: {
|
||||
title: '停止',
|
||||
},
|
||||
onClick: async () => {
|
||||
const res = await stopTask(data.id);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
},
|
||||
icon: 'StopOutlined',
|
||||
});
|
||||
} else if (pause) {
|
||||
Actions.push({
|
||||
key: 'actions',
|
||||
text: '继续升级',
|
||||
tooltip: {
|
||||
title: '继续升级',
|
||||
},
|
||||
onClick: async () => {
|
||||
const res = await startTask(data.id, ['canceled']);
|
||||
if (res.success) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
},
|
||||
icon: 'ControlOutlined',
|
||||
});
|
||||
}
|
||||
|
||||
return Actions;
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlEye = (data: object) => {
|
||||
current.value = toRaw({ ...data, view: true });
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlDetails = (id: string) => {
|
||||
// menuStory.jumpPage('device/Firmware/Task/Detail', { id });
|
||||
};
|
||||
const saveChange = (value: boolean) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
message.success('操作成功');
|
||||
tableRef.value.reload();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -66,14 +66,17 @@
|
|||
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="CertificatePage">
|
||||
<script lang="ts" setup name="FirmwarePage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
// import { save, query, remove } from '@/api/link/certificate';
|
||||
import { query, queryProduct, remove } from '@/api/device/firmware';
|
||||
import { message } from 'ant-design-vue';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import Save from './Save/index.vue';
|
||||
import { useMenuStore } from 'store/menu';
|
||||
import type { FormDataType } from './type';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
|
@ -178,7 +181,7 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
|||
},
|
||||
icon: 'FileTextOutlined',
|
||||
onClick: async () => {
|
||||
handlUpdate(data.id);
|
||||
handlUpdate(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -208,23 +211,27 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
|
|||
];
|
||||
};
|
||||
|
||||
const handlUpdate = (id: string) => {
|
||||
// router.push({
|
||||
// path: `/iot/link/certificate/detail/${id}`,
|
||||
// query: { view: true },
|
||||
// });
|
||||
const handlUpdate = (data: FormDataType) => {
|
||||
menuStory.jumpPage(
|
||||
'device/Firmware/Task',
|
||||
{},
|
||||
{
|
||||
id: data.id,
|
||||
productId: data.productId,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
const handlEdit = (data: object) => {
|
||||
const handlEdit = (data: FormDataType) => {
|
||||
current.value = _.cloneDeep(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const saveChange = (value: object) => {
|
||||
const saveChange = (value: FormDataType) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import AIcon from "@/components/AIcon";
|
||||
import { useInstanceStore } from "@/store/instance";
|
||||
import { useMenuStore } from "@/store/menu";
|
||||
import { Button, Descriptions, Modal } from "ant-design-vue"
|
||||
import styles from './index.module.less'
|
||||
|
||||
|
@ -14,6 +16,10 @@ const ManualInspection = defineComponent({
|
|||
|
||||
const { data } = props
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const dataRender = () => {
|
||||
if (data.type === 'device' || data.type === 'product') {
|
||||
return (
|
||||
|
@ -207,7 +213,13 @@ const ManualInspection = defineComponent({
|
|||
emit('save', data)
|
||||
}}
|
||||
onCancel={() => {
|
||||
// TODO 跳转设备和产品
|
||||
if (data.type === 'device') {
|
||||
instanceStore.tabActiveKey = 'Info'
|
||||
} else if (data.type === 'product') {
|
||||
menuStory.jumpPage('device/Product/Detail', { id: data.productId, tab: 'access' });
|
||||
} else {
|
||||
menuStory.jumpPage('link/AccessConfig/Detail', { id: data.configuration?.id });
|
||||
}
|
||||
}}>
|
||||
<div style={{ display: 'flex' }}>{dataRender()}</div>
|
||||
</Modal>
|
||||
|
|
|
@ -11,6 +11,8 @@ import _ from "lodash"
|
|||
import DiagnosticAdvice from './DiagnosticAdvice'
|
||||
import ManualInspection from './ManualInspection'
|
||||
import { deployDevice } from "@/api/initHome"
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue'
|
||||
import { useMenuStore } from "@/store/menu"
|
||||
|
||||
type TypeProps = 'network' | 'child-device' | 'media' | 'cloud' | 'channel'
|
||||
|
||||
|
@ -41,6 +43,7 @@ const Status = defineComponent({
|
|||
const diagnoseData = ref<Partial<Record<string, any>>>()
|
||||
|
||||
const bindParentVisible = ref<boolean>(false)
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const configuration = reactive<{
|
||||
product: Record<string, any>,
|
||||
|
@ -57,19 +60,8 @@ const Status = defineComponent({
|
|||
artificialData.value = params
|
||||
}
|
||||
|
||||
// TODO
|
||||
const jumpAccessConfig = () => {
|
||||
// const purl = getMenuPathByCode(MENUS_CODE['device/Product/Detail']);
|
||||
// if (purl) {
|
||||
// history.push(
|
||||
// `${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], device.productId)}`,
|
||||
// {
|
||||
// tab: 'access',
|
||||
// },
|
||||
// );
|
||||
// } else {
|
||||
// message.error('规则可能有加密处理,请联系管理员');
|
||||
// }
|
||||
menuStory.jumpPage('device/Product/Detail', { id: unref(device).productId, tab: 'access' });
|
||||
};
|
||||
|
||||
const jumpDeviceConfig = () => {
|
||||
|
@ -123,34 +115,40 @@ const Status = defineComponent({
|
|||
<Badge
|
||||
status="default"
|
||||
text={
|
||||
<span>网络组件已禁用,请先<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const res = await startNetwork(
|
||||
unref(gateway)?.channelId,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'network',
|
||||
name: '网络组件',
|
||||
desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm></span>
|
||||
<span>网络组件已禁用,请先
|
||||
<PermissionButton
|
||||
type="link"
|
||||
hasPermission="link/Type:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const res = await startNetwork(
|
||||
unref(gateway)?.channelId,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'network',
|
||||
name: '网络组件',
|
||||
desc: '诊断网络组件配置是否正确,配置错误将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
启用
|
||||
</PermissionButton>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
) : (
|
||||
<div>
|
||||
<div class={styles.infoItem}>
|
||||
|
@ -287,28 +285,31 @@ const Status = defineComponent({
|
|||
<Badge
|
||||
status="default"
|
||||
text={<span>设备接入网关已禁用,请先
|
||||
<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const resp = await startGateway(unref(device).accessId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: desc,
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
<PermissionButton
|
||||
hasPermission="link/Type:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const resp = await startGateway(unref(device).accessId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: desc,
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm>
|
||||
启用
|
||||
</PermissionButton>
|
||||
</span>}
|
||||
/>
|
||||
</div>
|
||||
|
@ -411,28 +412,32 @@ const Status = defineComponent({
|
|||
status="default"
|
||||
text={
|
||||
<span>
|
||||
设备接入网关已禁用,请先<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const resp = await startGateway(unref(device).accessId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: desc,
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
设备接入网关已禁用,请先
|
||||
<PermissionButton
|
||||
hasPermission="link/AccessConfig:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const resp = await startGateway(unref(device).accessId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'gateway',
|
||||
name: '设备接入网关',
|
||||
desc: desc,
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm>
|
||||
启用
|
||||
</PermissionButton>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
@ -519,28 +524,32 @@ const Status = defineComponent({
|
|||
status="default"
|
||||
text={
|
||||
<span>
|
||||
网关父设备已禁用,请先<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const resp = await _deploy(response?.result?.id || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'parent-device',
|
||||
name: '网关父设备',
|
||||
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
网关父设备已禁用,请先
|
||||
<PermissionButton
|
||||
hasPermission="device/Product:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const resp = await _deploy(response?.result?.id || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'parent-device',
|
||||
name: '网关父设备',
|
||||
desc: '诊断网关父设备状态是否正常,禁用或离线将导致连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm>
|
||||
启用
|
||||
</PermissionButton>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
@ -623,28 +632,32 @@ const Status = defineComponent({
|
|||
status="default"
|
||||
text={
|
||||
<span>
|
||||
产品已禁用,请<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const resp = await _deployProduct(unref(device).productId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
产品已禁用,请
|
||||
<PermissionButton
|
||||
hasPermission="device/Product:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const resp = await _deployProduct(unref(device).productId || '');
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'product',
|
||||
name: '产品状态',
|
||||
desc: '诊断产品状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm>
|
||||
启用
|
||||
</PermissionButton>
|
||||
产品
|
||||
</span>
|
||||
}
|
||||
|
@ -695,29 +708,34 @@ const Status = defineComponent({
|
|||
status="default"
|
||||
text={
|
||||
<span>
|
||||
设备已禁用,请<Popconfirm
|
||||
title="确认启用"
|
||||
onConfirm={async () => {
|
||||
const resp = await _deploy(unref(device)?.id || '');
|
||||
if (resp.status === 200) {
|
||||
instanceStore.current.state = { value: 'offline', text: '离线' }
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
设备已禁用,请
|
||||
<PermissionButton
|
||||
hasPermission="device/Instance:action"
|
||||
popConfirm={{
|
||||
title: '确认启用',
|
||||
onConfirm: async () => {
|
||||
const resp = await _deploy(unref(device)?.id || '');
|
||||
if (resp.status === 200) {
|
||||
instanceStore.current.state = { value: 'offline', text: '离线' }
|
||||
message.success('操作成功!');
|
||||
list.value = modifyArrayList(
|
||||
list.value,
|
||||
{
|
||||
key: 'device',
|
||||
name: '设备状态',
|
||||
desc: '诊断设备状态是否正常,禁用状态将导致设备连接失败',
|
||||
status: 'success',
|
||||
text: '正常',
|
||||
info: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="link" style="padding: 0">启用</Button>
|
||||
</Popconfirm>设备
|
||||
启用
|
||||
</PermissionButton>
|
||||
设备
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
:columns="columns"
|
||||
:request="_getEventList"
|
||||
model="TABLE"
|
||||
:bodyStyle="{padding: '0 24px'}"
|
||||
:bodyStyle="{ padding: '0 24px' }"
|
||||
>
|
||||
<template #timestamp="slotProps">
|
||||
{{ moment(slotProps.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
|
@ -19,18 +19,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import moment from 'moment'
|
||||
import { getEventList } from '@/api/device/instance'
|
||||
import { useInstanceStore } from '@/store/instance'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import moment from 'moment';
|
||||
import { getEventList } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
const events = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const instanceStore = useInstanceStore()
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const columns = ref<Record<string, any>>([
|
||||
{
|
||||
|
@ -38,43 +38,52 @@ const columns = ref<Record<string, any>>([
|
|||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
}
|
||||
])
|
||||
const params = ref<Record<string, any>>({})
|
||||
},
|
||||
]);
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const _getEventList = () => getEventList(instanceStore.current.id || '', events.data.id || '', params.value)
|
||||
const _getEventList = () =>
|
||||
getEventList(
|
||||
instanceStore.current.id || '',
|
||||
events.data.id || '',
|
||||
params.value,
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if(events.data?.valueType?.type === 'object'){
|
||||
if (events.data?.valueType?.type === 'object') {
|
||||
(events.data.valueType?.properties || []).map((i: any) => {
|
||||
columns.value.splice(0, 0, {
|
||||
key: i.id,
|
||||
title: i.name,
|
||||
dataIndex: `${i.id}_format`
|
||||
})
|
||||
})
|
||||
key: i.id,
|
||||
title: i.name,
|
||||
dataIndex: `${i.id}_format`,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
columns.value.splice(0, 0, {
|
||||
title: '数据',
|
||||
dataIndex: 'value',
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const detail = () => {
|
||||
Modal.info({
|
||||
title: () => '详情',
|
||||
width: 850,
|
||||
content: () => h('div', {}, [
|
||||
h('p', '暂未开发'),
|
||||
]),
|
||||
okText: '关闭'
|
||||
});
|
||||
}
|
||||
content: () => h('div', {}, [h('p', '暂未开发')]),
|
||||
okText: '关闭',
|
||||
});
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,54 @@
|
|||
<!-- 坐标点拾取组件 -->
|
||||
<template>
|
||||
<div style="width: 100%; height: 400px">
|
||||
<div style="position: relative">
|
||||
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="start">开始动画</a-button>
|
||||
<a-button type="primary" @click="stop">停止动画</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<el-amap :center="center" :zooms="[3, 20]" @init="initMap" ref="map"></el-amap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { initAMapApiLoader } from '@vuemap/vue-amap';
|
||||
import AMapUI from '@vuemap/vue-amap'
|
||||
import '@vuemap/vue-amap/dist/style.css';
|
||||
|
||||
initAMapApiLoader({
|
||||
key: 'a0415acfc35af15f10221bfa5a6850b4',
|
||||
securityJsCode: 'cae6108ec3dd222f946d1a7237c78be0',
|
||||
});
|
||||
|
||||
interface EmitProps {
|
||||
(e: 'update:points', data: string): void;
|
||||
}
|
||||
const props = defineProps({
|
||||
points: { type: Array, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits<EmitProps>();
|
||||
|
||||
// 地图拾取的坐标点(经纬度字符串)
|
||||
const mapPoint = ref('');
|
||||
|
||||
const map = ref(null);
|
||||
|
||||
const center = ref([106.55, 29.56]);
|
||||
const marker = ref(null);
|
||||
|
||||
/**
|
||||
* 地图初始化
|
||||
* @param e
|
||||
*/
|
||||
const initMap = (e: any) => {
|
||||
console.log(e)
|
||||
// map = e;
|
||||
// const pointStr = mapPoint.value as string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="chart" ref="chart"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const { proxy } = <any>getCurrentInstance();
|
||||
|
||||
const props = defineProps({
|
||||
// 图表数据
|
||||
options:{
|
||||
type:Object,
|
||||
default:()=>{}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 绘制图表
|
||||
*/
|
||||
const createChart = () => {
|
||||
nextTick(() => {
|
||||
const myChart = echarts.init(proxy.$refs.chart);
|
||||
myChart.setOption(props.options);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => createChart(),
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div>
|
||||
<a-space>
|
||||
<div>
|
||||
统计周期:
|
||||
<a-select v-model:value="cycle" style="width: 120px">
|
||||
<a-select-option value="*" v-if="_type"
|
||||
>实际值</a-select-option
|
||||
>
|
||||
<a-select-option value="1m">按分钟统计</a-select-option>
|
||||
<a-select-option value="1h">按小时统计</a-select-option>
|
||||
<a-select-option value="1d">按天统计</a-select-option>
|
||||
<a-select-option value="1w">按周统计</a-select-option>
|
||||
<a-select-option value="1M">按月统计</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div v-if="cycle !== '*' && _type">
|
||||
统计规则:
|
||||
<a-select v-model:value="agg" style="width: 120px">
|
||||
<a-select-option value="AVG">平均值</a-select-option>
|
||||
<a-select-option value="MAX">最大值</a-select-option>
|
||||
<a-select-option value="MIN">最小值</a-select-option>
|
||||
<a-select-option value="COUNT">总数</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="width: 100%; height: 500px">
|
||||
<Chart :options="options" v-if="chartsList.length" />
|
||||
<JEmpty v-else />
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getPropertiesInfo, getPropertiesList } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import Chart from './Chart.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const list = ['int', 'float', 'double', 'long'];
|
||||
|
||||
const prop = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
time: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const cycle = ref<string>('*');
|
||||
const agg = ref<string>('AVG');
|
||||
const loading = ref<boolean>(false);
|
||||
const chartsList = ref<any[]>([]);
|
||||
const instanceStore = useInstanceStore();
|
||||
const options = ref({});
|
||||
|
||||
const _type = computed(() => {
|
||||
const flag = list.includes(prop.data?.valueType?.type || '')
|
||||
cycle.value = flag ? '*' : '1m'
|
||||
return flag
|
||||
});
|
||||
|
||||
const queryChartsAggList = async () => {
|
||||
loading.value = true;
|
||||
const resp = await getPropertiesInfo(instanceStore.current.id, {
|
||||
columns: [
|
||||
{
|
||||
property: prop.data.id,
|
||||
alias: prop.data.id,
|
||||
agg: agg.value,
|
||||
},
|
||||
],
|
||||
query: {
|
||||
interval: cycle.value,
|
||||
format: 'yyyy-MM-dd HH:mm:ss',
|
||||
from: prop.time[0],
|
||||
to: prop.time[1],
|
||||
},
|
||||
});
|
||||
loading.value = false;
|
||||
if (resp.status === 200) {
|
||||
const dataList: any[] = [
|
||||
{
|
||||
year: prop.time[1],
|
||||
value: undefined,
|
||||
type: prop.data?.name || '',
|
||||
},
|
||||
];
|
||||
(resp.result as any[]).forEach((i: any) => {
|
||||
dataList.push({
|
||||
...i,
|
||||
year: i.time,
|
||||
value: Number(i[prop.data.id || '']),
|
||||
type: prop.data?.name || '',
|
||||
});
|
||||
});
|
||||
dataList.push({
|
||||
year: prop.time[0],
|
||||
value: undefined,
|
||||
type: prop.data?.name || '',
|
||||
});
|
||||
chartsList.value = (dataList || []).reverse();
|
||||
}
|
||||
};
|
||||
|
||||
const queryChartsList = async () => {
|
||||
loading.value = true;
|
||||
const resp = await getPropertiesList(
|
||||
instanceStore.current.id,
|
||||
prop.data.id,
|
||||
{
|
||||
paging: false,
|
||||
terms: [
|
||||
{
|
||||
column: 'timestamp$BTW',
|
||||
value:
|
||||
prop.time[0] && prop.time[1]
|
||||
? [prop.time[0], prop.time[1]]
|
||||
: [],
|
||||
type: 'and',
|
||||
},
|
||||
],
|
||||
sorts: [{ name: 'timestamp', order: 'asc' }],
|
||||
},
|
||||
);
|
||||
loading.value = false;
|
||||
if (resp.status === 200) {
|
||||
const dataList: any[] = [
|
||||
{
|
||||
year: prop.time[0],
|
||||
value: undefined,
|
||||
type: prop.data?.name || '',
|
||||
},
|
||||
];
|
||||
(resp.result as any)?.data?.forEach((i: any) => {
|
||||
dataList.push({
|
||||
...i,
|
||||
year: i.timestamp,
|
||||
value: i.value,
|
||||
type: prop.data?.name || '',
|
||||
});
|
||||
});
|
||||
dataList.push({
|
||||
year: prop.time[1],
|
||||
value: undefined,
|
||||
type: prop.data?.name || '',
|
||||
});
|
||||
chartsList.value = dataList || [];
|
||||
}
|
||||
};
|
||||
|
||||
const getOptions = (arr: any[]) => {
|
||||
options.value = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: arr.map((item) => {
|
||||
return echarts.format.formatTime(
|
||||
'yyyy-MM-dd\nhh:mm:ss',
|
||||
item.year,
|
||||
false,
|
||||
);
|
||||
}),
|
||||
name: '时间',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: arr[0]?.type,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 10,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt: any) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: arr.map((i: any) => i.value),
|
||||
type: 'line',
|
||||
areaStyle: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [cycle, agg],
|
||||
([newCycle, newAgg]) => {
|
||||
if (newCycle.value === '*' && _type.value) {
|
||||
queryChartsList();
|
||||
} else {
|
||||
queryChartsAggList();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if (chartsList.value.length) {
|
||||
getOptions(chartsList.value);
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div style="position: relative">
|
||||
<div style="position: absolute; right: 0; top: 5px; z-index: 999">
|
||||
<a-space>
|
||||
<a-button type="primary">开始动画</a-button>
|
||||
<a-button type="primary">停止动画</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<AMap :points="geoList" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getPropertyData } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import AMap from './AMap.vue';
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const prop = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
time: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const geoList = ref<any[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const query = async () => {
|
||||
loading.value = true;
|
||||
const resp = await getPropertyData(
|
||||
instanceStore.current.id,
|
||||
encodeQuery({
|
||||
paging: false,
|
||||
terms: {
|
||||
property: prop.data.id,
|
||||
timestamp$BTW: prop.time[0] && prop.time[1] ? prop.time : [],
|
||||
},
|
||||
sorts: { timestamp: 'asc' },
|
||||
}),
|
||||
);
|
||||
loading.value = false;
|
||||
if (resp.status === 200) {
|
||||
const list: any[] = [];
|
||||
((resp.result as any)?.data || []).forEach((item: any) => {
|
||||
list.push([item.value.lon, item.value.lat]);
|
||||
});
|
||||
geoList.value = list
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [prop.data.id, prop.time],
|
||||
([newVal]) => {
|
||||
if (newVal) {
|
||||
query();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,194 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
size="small"
|
||||
rowKey="id"
|
||||
:dataSource="dataSource?.data"
|
||||
@change="onChange"
|
||||
:pagination="{
|
||||
current: (dataSource?.pageIndex || 0) + 1,
|
||||
pageSize: dataSource?.pageSize || 10,
|
||||
showSizeChanger: true,
|
||||
total: dataSource?.total || 0,
|
||||
pageSizeOptions: [5, 10, 20, 50],
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'timestamp'">
|
||||
{{ moment(record.timestamp).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
<template v-if="column.key === 'value'">
|
||||
<ValueRender
|
||||
type="table"
|
||||
:data="_props.data"
|
||||
:value="{ formatValue: record.value }"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="
|
||||
showLoad ||
|
||||
(!getType(record?.value) &&
|
||||
data?.valueType?.fileType === 'base64')
|
||||
"
|
||||
type="link"
|
||||
@click="_download(record)"
|
||||
><AIcon type="DownloadOutlined"
|
||||
/></a-button>
|
||||
<a-button type="link" @click="showDetail(record)"
|
||||
><AIcon type="SearchOutlined"
|
||||
/></a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<a-modal
|
||||
title="详情"
|
||||
:visible="visible"
|
||||
@ok="visible = false"
|
||||
@cancel="visible = false"
|
||||
>
|
||||
<div>自定义属性</div>
|
||||
<JsonViewer
|
||||
v-if="
|
||||
data?.valueType?.type === 'object' ||
|
||||
data?.valueType?.type === 'array'
|
||||
"
|
||||
:expand-depth="5"
|
||||
:value="current.formatValue"
|
||||
/>
|
||||
<a-textarea
|
||||
v-else-if="data?.valueType?.type === 'file'"
|
||||
:value="current.formatValue"
|
||||
:row="3"
|
||||
/>
|
||||
<a-input v-else disabled :value="current.formatValue" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getPropertyData } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import moment from 'moment';
|
||||
import { getType } from '../index';
|
||||
import ValueRender from '../ValueRender.vue';
|
||||
import JsonViewer from 'vue-json-viewer';
|
||||
|
||||
const _props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
time: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const instanceStore = useInstanceStore();
|
||||
const dataSource = ref({});
|
||||
const current = ref<any>({});
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
const columns = computed(() => {
|
||||
const arr: any[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: _props.data?.name || '',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
ellipsis: true,
|
||||
},
|
||||
];
|
||||
if (_props.data?.valueType?.type != 'geoPoint') {
|
||||
arr.push({
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
});
|
||||
|
||||
const showLoad = computed(() => {
|
||||
return (
|
||||
_props.data.valueType?.type === 'file' &&
|
||||
_props.data?.valueType?.fileType === 'Binary(二进制)'
|
||||
);
|
||||
});
|
||||
|
||||
const showDetail = (item: any) => {
|
||||
visible.value = true;
|
||||
current.value = item;
|
||||
};
|
||||
|
||||
const queryPropertyData = async (params: any) => {
|
||||
const resp = await getPropertyData(
|
||||
instanceStore.current.id,
|
||||
encodeQuery({
|
||||
...params,
|
||||
terms: {
|
||||
property: _props.data.id,
|
||||
timestamp$BTW: _props.time,
|
||||
},
|
||||
sorts: { timestamp: 'desc' },
|
||||
}),
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
dataSource.value = resp.result as any;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [_props.data.id, _props.time],
|
||||
([newVal]) => {
|
||||
if (newVal) {
|
||||
queryPropertyData({
|
||||
pageSize: _props.data.valueType?.type === 'file' ? 5 : 10,
|
||||
pageIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const onChange = (page: any) => {
|
||||
queryPropertyData({
|
||||
pageSize: page.pageSize,
|
||||
pageIndex: Number(page.current) - 1 || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const _download = (record: any) => {
|
||||
const downNode = document.createElement('a');
|
||||
downNode.download = `${instanceStore.current.name}-${
|
||||
_props.data.name
|
||||
}${moment(new Date().getTime()).format('YYYY-MM-DD-HH-mm-ss')}.txt`;
|
||||
downNode.style.display = 'none';
|
||||
//字符串内容转成Blob地址
|
||||
const blob = new Blob([record.value]);
|
||||
downNode.href = URL.createObjectURL(blob);
|
||||
//触发点击
|
||||
document.body.appendChild(downNode);
|
||||
downNode.click();
|
||||
//移除
|
||||
document.body.removeChild(downNode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.ant-pagination-item) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<a-space>
|
||||
<a-radio-group
|
||||
:value="radioValue"
|
||||
button-style="solid"
|
||||
@change="onRadioChange"
|
||||
>
|
||||
<a-radio-button value="today">今日</a-radio-button>
|
||||
<a-radio-button value="week">近一周</a-radio-button>
|
||||
<a-radio-button value="month">近一月</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-range-picker
|
||||
show-time
|
||||
v-model:value="dateValue"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
@change="onRangeChange"
|
||||
:allowClear="false"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
type Props = [Dayjs, Dayjs] | undefined
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Props>,
|
||||
default: undefined
|
||||
},
|
||||
});
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:modelValue', data: Props): void;
|
||||
};
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const radioValue = ref<string>('today');
|
||||
const dateValue = ref<Props>();
|
||||
|
||||
const onRangeChange = (value: Props) => {
|
||||
emit('update:modelValue', value);
|
||||
radioValue.value = '';
|
||||
}
|
||||
|
||||
const getTime = (type: string): Props => {
|
||||
let st: number = 0;
|
||||
const et = new Date().getTime();
|
||||
if (type === 'today') {
|
||||
st = dayjs().startOf('day').valueOf();
|
||||
} else if (type === 'week') {
|
||||
st = dayjs().subtract(6, 'days').valueOf();
|
||||
} else if (type === 'month') {
|
||||
st = dayjs().subtract(29, 'days').valueOf();
|
||||
}
|
||||
return [dayjs(st), dayjs(et)]
|
||||
}
|
||||
|
||||
const onRadioChange = (e: any) => {
|
||||
const value: string = e.target.value;
|
||||
radioValue.value = value;
|
||||
emit('update:modelValue', getTime(value));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
radioValue.value = 'today'
|
||||
emit('update:modelValue', getTime('today'));
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal: Props) => {
|
||||
dateValue.value = newVal
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<a-modal title="详情" visible width="50vw" @ok="onCancel" @cancel="onCancel">
|
||||
<div style="margin-bottom: 10px"><TimeComponent v-model="dateValue" /></div>
|
||||
<div>
|
||||
<a-tabs v-model:activeKey="activeKey" style="max-height: 600px; overflow-y: auto">
|
||||
<a-tab-pane key="table" tab="列表">
|
||||
<Table :data="props.data" :time="_getTimes" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="charts" tab="图表">
|
||||
<Charts :data="props.data" :time="_getTimes" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="geo" tab="轨迹" v-if="data?.valueType?.type === 'geoPoint'">
|
||||
<PropertyAMap :data="props.data" :time="_getTimes" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import TimeComponent from './TimeComponent.vue'
|
||||
import Charts from './Charts.vue'
|
||||
import PropertyAMap from './PropertyAMap.vue'
|
||||
import Table from './Table.vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const _emits = defineEmits(['close'])
|
||||
|
||||
const activeKey = ref<'table' | 'charts' | 'geo'>('table')
|
||||
|
||||
const dateValue = ref<[Dayjs, Dayjs]>();
|
||||
|
||||
const _getTimes = computed(() => {
|
||||
if(dateValue.value){
|
||||
return [dateValue.value[0].valueOf(), dateValue.value[1].valueOf()]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const onCancel = () => {
|
||||
_emits('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<a-card :hoverable="true" class="card-box">
|
||||
<!-- <a-spin :spinning="loading"> -->
|
||||
<div class="card-container">
|
||||
<div class="header">
|
||||
<div class="title">{{ _props.data.name }}</div>
|
||||
<div class="extra">
|
||||
<a-space :size="16">
|
||||
<div class="card-container">
|
||||
<div class="header">
|
||||
<div class="title">{{ _props.data.name }}</div>
|
||||
<div class="extra">
|
||||
<a-space :size="16">
|
||||
<template v-for="i in actions" :key="i.key">
|
||||
<a-tooltip
|
||||
v-for="i in actions"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
v-if="i.key !== 'edit'"
|
||||
>
|
||||
<a-button
|
||||
style="padding: 0; margin: 0"
|
||||
|
@ -17,26 +17,48 @@
|
|||
:disabled="i.disabled"
|
||||
@click="i.onClick && i.onClick(data)"
|
||||
>
|
||||
<AIcon :type="i.icon" style="color: #323130; font-size: 12px" />
|
||||
<AIcon
|
||||
:type="i.icon"
|
||||
style="color: #323130; font-size: 12px"
|
||||
/>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<ValueRender :data="data" :value="_props.data" type="card" />
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div style="color: rgba(0, 0, 0, .65); font-size: 12px">更新时间</div>
|
||||
<div class="time-value">{{_props?.data?.timeString || '--'}}</div>
|
||||
<PermissionButton
|
||||
:disabled="i.disabled"
|
||||
v-else
|
||||
:popConfirm="i.popConfirm"
|
||||
:tooltip="i.tooltip"
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
type="link"
|
||||
style="padding: 0px"
|
||||
:hasPermission="'device/Instance:update'"
|
||||
>
|
||||
<template #icon
|
||||
><AIcon :type="i.icon" style="color: #323130; font-size: 12px"
|
||||
/></template>
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<ValueRender :data="data" :value="_props.data" type="card" />
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div style="color: rgba(0, 0, 0, 0.65); font-size: 12px">
|
||||
更新时间
|
||||
</div>
|
||||
<div class="time-value">
|
||||
{{ _props?.data?.timeString || '--' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </a-spin> -->
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ValueRender from './ValueRender.vue'
|
||||
import ValueRender from './ValueRender.vue';
|
||||
const _props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
|
@ -44,7 +66,7 @@ const _props = defineProps({
|
|||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
// const loading = ref<boolean>(true);
|
||||
|
@ -101,6 +123,6 @@ const _props = defineProps({
|
|||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -13,23 +13,18 @@
|
|||
<a-image :src="value?.formatValue" />
|
||||
</template>
|
||||
<template v-else-if="['.flv', '.m3u8', '.mp4'].includes(type)">
|
||||
<!-- TODO 视频组件缺失 -->
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- <json-viewer
|
||||
:value="{
|
||||
'id': '123'
|
||||
}"
|
||||
copyable
|
||||
boxed
|
||||
sort
|
||||
></json-viewer> -->
|
||||
<JsonViewer
|
||||
:expand-depth="5"
|
||||
:value="value?.formatValue"
|
||||
/>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import JsonViewer from 'vue3-json-viewer';
|
||||
import JsonViewer from 'vue-json-viewer';
|
||||
|
||||
const _data = defineProps({
|
||||
type: {
|
||||
|
@ -46,9 +41,6 @@ const handleCancel = () => {
|
|||
_emit('close');
|
||||
};
|
||||
|
||||
// watchEffect(() => {
|
||||
// console.log(_data.value?.formatValue)
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="value">
|
||||
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div>
|
||||
<div v-else-if="data?.valueType?.type === 'file'">
|
||||
<div v-else-if="_data.data?.valueType?.type === 'file'">
|
||||
<template v-if="data?.valueType?.fileType === 'base64'">
|
||||
<div :class="valueClass" v-if="!!getType(value?.formatValue)">
|
||||
<img :src="imgMap.get(_type)" @error="onError" />
|
||||
|
@ -36,10 +36,10 @@
|
|||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
|
||||
<div v-else-if="_data.data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
|
||||
<img :src="imgMap.get('obj')" />
|
||||
</div>
|
||||
<div v-else-if="data?.valueType?.type === 'geoPoint' || data?.valueType?.type === 'array'" :class="valueClass">
|
||||
<div v-else-if="_data.data?.valueType?.type === 'geoPoint' || _data.data?.valueType?.type === 'array'" :class="valueClass">
|
||||
{{JSON.stringify(value?.formatValue)}}
|
||||
</div>
|
||||
<div v-else :class="valueClass">
|
||||
|
@ -53,6 +53,7 @@
|
|||
import { getImage } from "@/utils/comm";
|
||||
import { message } from "ant-design-vue";
|
||||
import ValueDetail from './ValueDetail.vue'
|
||||
import {getType, imgMap, imgList, videoList, fileList} from './index'
|
||||
|
||||
const _data = defineProps({
|
||||
data: {
|
||||
|
@ -73,47 +74,12 @@ const valueClass = computed(() => {
|
|||
return _data.type === 'card' ? 'cardValue' : 'otherValue'
|
||||
})
|
||||
|
||||
const imgMap = new Map<any, any>();
|
||||
imgMap.set('txt', getImage('/running/txt.png'));
|
||||
imgMap.set('doc', getImage('/running/doc.png'));
|
||||
imgMap.set('xls', getImage('/running/xls.png'));
|
||||
imgMap.set('ppt', getImage('/running/ppt.png'));
|
||||
imgMap.set('docx', getImage('/running/docx.png'));
|
||||
imgMap.set('xlsx', getImage('/running/xlsx.png'));
|
||||
imgMap.set('pptx', getImage('/running/pptx.png'));
|
||||
imgMap.set('pdf', getImage('/running/pdf.png'));
|
||||
imgMap.set('img', getImage('/running/img.png'));
|
||||
imgMap.set('error', getImage('/running/error.png'));
|
||||
imgMap.set('video', getImage('/running/video.png'));
|
||||
imgMap.set('other', getImage('/running/other.png'));
|
||||
imgMap.set('obj', getImage('/running/obj.png'));
|
||||
|
||||
const imgList = ['.jpg', '.png', '.swf', '.tiff'];
|
||||
const videoList = ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'];
|
||||
const fileList = ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'];
|
||||
|
||||
const isHttps = document.location.protocol === 'https:';
|
||||
|
||||
const _types = ref<string>('')
|
||||
const visible = ref<boolean>(false)
|
||||
const temp = ref<boolean>(false)
|
||||
|
||||
const getType = (url: string) => {
|
||||
let t: string = '';
|
||||
[...imgList, ...videoList, ...fileList].map((item) => {
|
||||
const str = item.slice(1, item.length);
|
||||
if (url && String(url).indexOf(str) !== -1) {
|
||||
if (imgList.includes(item)) {
|
||||
t = 'img';
|
||||
} else if (videoList.includes(item)) {
|
||||
t = 'video';
|
||||
} else {
|
||||
t = str;
|
||||
}
|
||||
}
|
||||
});
|
||||
return t;
|
||||
};
|
||||
|
||||
const onError = (e: any) => {
|
||||
e.target.src = imgMap.get('other')
|
||||
|
@ -149,7 +115,6 @@ const getDetail = (_type: string) => {
|
|||
_types.value = flag
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { getImage } from "@/utils/comm";
|
||||
|
||||
export const imgMap = new Map<any, any>();
|
||||
imgMap.set('txt', getImage('/running/txt.png'));
|
||||
imgMap.set('doc', getImage('/running/doc.png'));
|
||||
imgMap.set('xls', getImage('/running/xls.png'));
|
||||
imgMap.set('ppt', getImage('/running/ppt.png'));
|
||||
imgMap.set('docx', getImage('/running/docx.png'));
|
||||
imgMap.set('xlsx', getImage('/running/xlsx.png'));
|
||||
imgMap.set('pptx', getImage('/running/pptx.png'));
|
||||
imgMap.set('pdf', getImage('/running/pdf.png'));
|
||||
imgMap.set('img', getImage('/running/img.png'));
|
||||
imgMap.set('error', getImage('/running/error.png'));
|
||||
imgMap.set('video', getImage('/running/video.png'));
|
||||
imgMap.set('other', getImage('/running/other.png'));
|
||||
imgMap.set('obj', getImage('/running/obj.png'));
|
||||
|
||||
export const imgList = ['.jpg', '.png', '.swf', '.tiff'];
|
||||
export const videoList = ['.m3u8', '.flv', '.mp4', '.rmvb', '.mvb'];
|
||||
export const fileList = ['.txt', '.doc', '.xls', '.pdf', '.ppt', '.docx', '.xlsx', '.pptx'];
|
||||
|
||||
export const getType = (url: string) => {
|
||||
let t: string = '';
|
||||
[...imgList, ...videoList, ...fileList].map((item) => {
|
||||
const str = item.slice(1, item.length);
|
||||
if (url && String(url).indexOf(str) !== -1) {
|
||||
if (imgList.includes(item)) {
|
||||
t = 'img';
|
||||
} else if (videoList.includes(item)) {
|
||||
t = 'video';
|
||||
} else {
|
||||
t = str;
|
||||
}
|
||||
}
|
||||
});
|
||||
return t;
|
||||
};
|
|
@ -32,20 +32,30 @@
|
|||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps)"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
<template v-for="i in getActions(slotProps)" :key="i.key">
|
||||
<a-tooltip v-bind="i.tooltip" v-if="i.key !== 'edit'">
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
:disabled="i.disabled"
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
>
|
||||
<AIcon :type="i.icon" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<PermissionButton
|
||||
:disabled="i.disabled"
|
||||
v-else
|
||||
:popConfirm="i.popConfirm"
|
||||
:tooltip="i.tooltip"
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
type="link"
|
||||
style="padding: 0px"
|
||||
:hasPermission="'device/Instance:update'"
|
||||
>
|
||||
<AIcon :type="i.icon" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template #icon><AIcon :type="i.icon" /></template>
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #paginationRender>
|
||||
|
@ -76,14 +86,20 @@
|
|||
@close="indicatorVisible = false"
|
||||
:data="currentInfo"
|
||||
/>
|
||||
<Detail
|
||||
v-if="detailVisible"
|
||||
:data="currentInfo"
|
||||
@close="detailVisible = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import _, { groupBy, throttle, toArray } from 'lodash-es';
|
||||
import _, { groupBy, toArray } from 'lodash-es';
|
||||
import { PropertyData } from '../../../typings';
|
||||
import PropertyCard from './PropertyCard.vue';
|
||||
import ValueRender from './ValueRender.vue';
|
||||
import Save from './Save.vue';
|
||||
import Detail from './Detail/index.vue';
|
||||
import Indicators from './Indicators.vue';
|
||||
import { getProperty } from '@/api/device/instance';
|
||||
import { useInstanceStore } from '@/store/instance';
|
||||
|
@ -238,11 +254,15 @@ const subscribeProperty = () => {
|
|||
?.pipe(map((res: any) => res.payload))
|
||||
.subscribe((payload) => {
|
||||
list.value = [...list.value, payload];
|
||||
unref(list).sort((a: any, b: any) => a.timestamp - b.timestamp)
|
||||
.forEach((item: any) => {
|
||||
const { value } = item;
|
||||
propertyValue.value[value?.property] = { ...item, ...value };
|
||||
});
|
||||
unref(list)
|
||||
.sort((a: any, b: any) => a.timestamp - b.timestamp)
|
||||
.forEach((item: any) => {
|
||||
const { value } = item;
|
||||
propertyValue.value[value?.property] = {
|
||||
...item,
|
||||
...value,
|
||||
};
|
||||
});
|
||||
// list.value = [...list.value, payload];
|
||||
// throttle(valueChange(list.value), 500);
|
||||
});
|
||||
|
@ -335,8 +355,8 @@ const onSearch = () => {
|
|||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
subRef.value && subRef.value?.unsubscribe()
|
||||
})
|
||||
subRef.value && subRef.value?.unsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<a-tabs
|
||||
tab-position="left"
|
||||
style="height: 600px"
|
||||
v-if="tabList.length"
|
||||
v-model:activeKey="activeKey"
|
||||
:tabBarStyle="{ width: '200px' }"
|
||||
@change="tabChange"
|
||||
|
@ -22,6 +23,7 @@
|
|||
:tab="i.tab"
|
||||
/>
|
||||
</a-tabs>
|
||||
<JEmpty v-else style="margin: 250px 0" />
|
||||
</div>
|
||||
<div class="property-box-right">
|
||||
<Event v-if="type === 'event'" :data="data" />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<page-container
|
||||
:tabList="list"
|
||||
@back="onBack"
|
||||
:tabActiveKey="instanceStore.active"
|
||||
:tabActiveKey="instanceStore.tabActiveKey"
|
||||
@tabChange="onTabChange"
|
||||
>
|
||||
<template #title>
|
||||
|
|
|
@ -61,14 +61,15 @@
|
|||
showSearch
|
||||
v-model:value="modelRef.productId"
|
||||
placeholder="请选择所属产品"
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
:value="item.id"
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
:label="item.name"
|
||||
:disabled="!!props.data.id"
|
||||
></a-select-option>
|
||||
>{{item.name}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="describe">
|
||||
|
@ -110,6 +111,10 @@ const modelRef = reactive({
|
|||
photoUrl: getImage('/device/instance/device-card.png'),
|
||||
});
|
||||
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
};
|
||||
|
||||
const vailId = async (_: Record<string, any>, value: string) => {
|
||||
if (!props?.data?.id && value) {
|
||||
const resp = await isExists(value);
|
||||
|
|
|
@ -140,7 +140,6 @@
|
|||
:value="slotProps"
|
||||
@click="handleClick"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:active="_selectedRowKeys.includes(slotProps.id)"
|
||||
:status="slotProps.state?.value"
|
||||
:statusText="slotProps.state?.text"
|
||||
|
@ -151,22 +150,17 @@
|
|||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
:src="
|
||||
getImage('/device/instance/device-card.png')
|
||||
"
|
||||
/>
|
||||
</slot>
|
||||
<img
|
||||
:src="getImage('/device/instance/device-card.png')"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3
|
||||
class="card-item-content-title"
|
||||
@click.stop="handleView(slotProps.id)"
|
||||
>
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<a-row>
|
||||
<Ellipsis style="width: calc(100% - 100px)">
|
||||
<span style="font-size: 16px; font-weight: 600" @click.stop="handleView(slotProps.id)">
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
<a-row style="margin-top: 20px">
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
|
@ -177,7 +171,9 @@
|
|||
<div class="card-item-content-text">
|
||||
产品名称
|
||||
</div>
|
||||
<div>{{ slotProps.productName }}</div>
|
||||
<Ellipsis style="width: 100%">
|
||||
{{ slotProps.productName }}
|
||||
</Ellipsis>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
@ -292,7 +288,7 @@ const operationVisible = ref<boolean>(false);
|
|||
const api = ref<string>('');
|
||||
const type = ref<string>('');
|
||||
|
||||
const menuStory = useMenuStore()
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const statusMap = new Map();
|
||||
statusMap.set('online', 'success');
|
||||
|
@ -538,7 +534,7 @@ const handleAdd = () => {
|
|||
* 查看
|
||||
*/
|
||||
const handleView = (id: string) => {
|
||||
menuStory.jumpPage('device/Instance/Detail', {id})
|
||||
menuStory.jumpPage('device/Instance/Detail', { id });
|
||||
};
|
||||
|
||||
const getActions = (
|
||||
|
|
|
@ -1,167 +1,180 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search
|
||||
:columns="query.columns"
|
||||
target="product-manage"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
:columns="columns"
|
||||
:request="queryProductList"
|
||||
ref="tableRef"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="add"
|
||||
><plus-outlined />新增</a-button
|
||||
>
|
||||
<a-upload
|
||||
name="file"
|
||||
accept=".json"
|
||||
:showUploadList="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<a-button>导入</a-button>
|
||||
</a-upload>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #deviceType="slotProps">
|
||||
<div>{{ slotProps.deviceType.text }}</div>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
@click="handleClick"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:active="_selectedRowKeys.includes(slotProps.id)"
|
||||
:status="slotProps.state"
|
||||
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:statusNames="{
|
||||
1: 'success',
|
||||
0: 'error',
|
||||
}"
|
||||
<Search
|
||||
:columns="query.columns"
|
||||
target="product-manage"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
:columns="columns"
|
||||
:request="queryProductList"
|
||||
ref="tableRef"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="add"
|
||||
><plus-outlined />新增</a-button
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/device-product.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3
|
||||
<a-upload
|
||||
name="file"
|
||||
accept=".json"
|
||||
:showUploadList="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<a-button>导入</a-button>
|
||||
</a-upload>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #deviceType="slotProps">
|
||||
<div>{{ slotProps.deviceType.text }}</div>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
@click="handleClick"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:active="_selectedRowKeys.includes(slotProps.id)"
|
||||
:status="slotProps.state"
|
||||
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:statusNames="{
|
||||
1: 'success',
|
||||
0: 'error',
|
||||
}"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img
|
||||
:src="
|
||||
slotProps.photoUrl ||
|
||||
getImage('/device-product.png')
|
||||
"
|
||||
class="productImg"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<Ellipsis
|
||||
><span
|
||||
@click.stop="handleView(slotProps.id)"
|
||||
style="font-weight: 600"
|
||||
style="font-weight: 600; font-size: 16px"
|
||||
>
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
</div>
|
||||
<div>直连设备</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<a-tooltip
|
||||
v-bind="item.tooltip"
|
||||
:title="item.disabled && item.tooltip.title"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="item.popConfirm"
|
||||
v-bind="item.popConfirm"
|
||||
:disabled="item.disabled"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
</span></Ellipsis
|
||||
>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
</div>
|
||||
<div>{{ slotProps?.deviceType?.text }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
接入方式
|
||||
</div>
|
||||
<Ellipsis
|
||||
><div>
|
||||
{{ slotProps?.accessName }}
|
||||
</div></Ellipsis
|
||||
>
|
||||
<a-button :disabled="item.disabled">
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item?.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<template v-else>
|
||||
<a-button
|
||||
:disabled="item.disabled"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item?.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
<template #state="slotProps">
|
||||
<a-badge
|
||||
:text="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:status="statusMap.get(slotProps.state)"
|
||||
/>
|
||||
</template>
|
||||
<template #id="slotProps">
|
||||
<a>{{ slotProps.id }}</a>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps)"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
v-bind="item.tooltip"
|
||||
:title="item.disabled && item.tooltip.title"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="i.popConfirm"
|
||||
v-bind="i.popConfirm"
|
||||
v-if="item.popConfirm"
|
||||
v-bind="item.popConfirm"
|
||||
:disabled="item.disabled"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
<a-button :disabled="item.disabled">
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item?.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<template v-else>
|
||||
<a-button
|
||||
:disabled="item.disabled"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<AIcon
|
||||
type="DeleteOutlined"
|
||||
v-if="item.key === 'delete'"
|
||||
/>
|
||||
<template v-else>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item?.text }}</span>
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
<template #state="slotProps">
|
||||
<a-badge
|
||||
:text="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:status="statusMap.get(slotProps.state)"
|
||||
/>
|
||||
</template>
|
||||
<template #id="slotProps">
|
||||
<a>{{ slotProps.id }}</a>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps)"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="i.popConfirm"
|
||||
v-bind="i.popConfirm"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
v-else
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
<!-- 新增、编辑 -->
|
||||
<Save
|
||||
ref="saveRef"
|
||||
:isAdd="isAdd"
|
||||
:title="title"
|
||||
@success="refresh"
|
||||
/>
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
v-else
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
<!-- 新增、编辑 -->
|
||||
<Save ref="saveRef" :isAdd="isAdd" :title="title" @success="refresh" />
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
|
@ -193,11 +206,11 @@ import { isNoCommunity, downloadObject } from '@/utils/utils';
|
|||
import { omit } from 'lodash-es';
|
||||
import { typeOptions } from '@/components/Search/util';
|
||||
import Save from './Save/index.vue';
|
||||
import { useMenuStore } from 'store/menu'
|
||||
import { useMenuStore } from 'store/menu';
|
||||
/**
|
||||
* 表格数据
|
||||
*/
|
||||
const menuStory = useMenuStore()
|
||||
const menuStory = useMenuStore();
|
||||
const router = useRouter();
|
||||
const isAdd = ref<number>(0);
|
||||
const title = ref<string>('');
|
||||
|
@ -425,7 +438,7 @@ const beforeUpload = (file: any) => {
|
|||
* 查看
|
||||
*/
|
||||
const handleView = (id: string) => {
|
||||
menuStory.jumpPage('device/Product/Detail',{id})
|
||||
menuStory.jumpPage('device/Product/Detail', { id });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -643,4 +656,13 @@ const handleSearch = (e: any) => {
|
|||
padding: 20px;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.productImg {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
}
|
||||
.productName {
|
||||
white-space: nowrap; /*强制在同一行内显示所有文本,直到文本结束或者遭遇br标签对象才换行。*/
|
||||
overflow: hidden; /*超出部分隐藏*/
|
||||
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
><AIcon type="PlusOutlined" />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
|
@ -188,7 +188,7 @@
|
|||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="AccessConfigPage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import {
|
||||
list,
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
><AIcon type="PlusOutlined" />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #type="slotProps">
|
||||
|
@ -59,8 +59,8 @@
|
|||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="CertificatePage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { save, query, remove } from '@/api/link/certificate';
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import { query, remove } from '@/api/link/certificate';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
|
@ -72,6 +72,9 @@ const columns = [
|
|||
title: '证书标准',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
fixed: 'left',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
|
@ -87,6 +90,7 @@ const columns = [
|
|||
title: '证书名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
|
@ -95,6 +99,7 @@ const columns = [
|
|||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
><AIcon type="PlusOutlined" />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
|
@ -31,7 +31,9 @@
|
|||
</template>
|
||||
<template #content>
|
||||
<div class="card-item-content">
|
||||
<h3 class="card-item-content-title card-item-content-title-a">
|
||||
<h3
|
||||
class="card-item-content-title card-item-content-title-a"
|
||||
>
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<a-row class="card-item-content-box">
|
||||
|
@ -148,7 +150,7 @@
|
|||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="AccessConfigPage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import type { ActionsType } from '@/components/Table/index';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { list, remove } from '@/api/link/protocol';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
@ -156,7 +158,6 @@ import Save from './Save/index.vue';
|
|||
import _ from 'lodash';
|
||||
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const visible = ref(false);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handlAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
><AIcon type="PlusOutlined" />新增</a-button
|
||||
>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
|
@ -186,7 +186,7 @@
|
|||
</page-container>
|
||||
</template>
|
||||
<script lang="ts" setup name="TypePage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import type { ActionsType } from '@/components/Table/index'
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { supports, query, remove, start, shutdown } from '@/api/link/type';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<!-- Modal 弹窗,用于新增、修改数据 -->
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="_vis"
|
||||
:title="!!formData.id ? '编辑' : '新增'"
|
||||
width="650px"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" layout="vertical">
|
||||
<a-row :gutter="10">
|
||||
<a-col :span="12">
|
||||
<a-form-item name="channelId">
|
||||
<template #label>
|
||||
通道ID
|
||||
<a-tooltip title="若不填写,系统将自动生成唯一ID">
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="formData.channelId" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="name"
|
||||
label="通道名称"
|
||||
:rules="{ required: true, message: '请输入通道名称' }"
|
||||
>
|
||||
<a-input v-model:value="formData.name" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
name="media_url"
|
||||
:rules="{ required: true, message: '请输入视频地址' }"
|
||||
>
|
||||
<template #label>
|
||||
视频地址
|
||||
<a-tooltip
|
||||
title="不同厂家的RTSP固定地址规则不同,请按对应厂家的规则填写"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model:value="formData.others.media_url" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="media_username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="formData.others.media_username"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="media_password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="formData.others.media_password"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item name="address" label="安装地址">
|
||||
<a-input v-model:value="formData.address" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item name="description" label="说明">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
:rows="4"
|
||||
:maxlength="200"
|
||||
showCount
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import templateApi from '@/api/notice/template';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:visible', data: boolean): void;
|
||||
(e: 'submit'): void;
|
||||
};
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
channelData: {
|
||||
type: Object as PropType<Partial<Record<string, any>>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const _vis = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val),
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
id: '',
|
||||
address: '',
|
||||
channelId: '',
|
||||
description: '',
|
||||
deviceId: '',
|
||||
name: '',
|
||||
others: {
|
||||
media_password: '',
|
||||
media_url: '',
|
||||
media_username: '',
|
||||
},
|
||||
});
|
||||
// const formRules = ref({});
|
||||
|
||||
/**
|
||||
* 提交
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
emit('submit');
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log('err: ', err);
|
||||
});
|
||||
};
|
||||
const handleCancel = () => {
|
||||
_vis.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,250 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<Search
|
||||
type="simple"
|
||||
:columns="columns"
|
||||
target="product"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<JTable
|
||||
ref="listRef"
|
||||
:columns="columns"
|
||||
:request="(e:any) => ChannelApi.list(e, route?.query.id as string)"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'notifyTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
model="table"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-tooltip
|
||||
v-if="route?.query.type === 'gb28181-2016'"
|
||||
title="接入方式为GB/T28281时,不支持新增"
|
||||
>
|
||||
<a-button type="primary" disabled> 新增 </a-button>
|
||||
</a-tooltip>
|
||||
<a-button type="primary" @click="handleAdd" v-else>
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
<template #status="slotProps">
|
||||
<a-space>
|
||||
<a-badge
|
||||
:status="
|
||||
slotProps.status.value === 'online'
|
||||
? 'success'
|
||||
: 'error'
|
||||
"
|
||||
:text="slotProps.status.text"
|
||||
></a-badge>
|
||||
</a-space>
|
||||
</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>
|
||||
|
||||
<Save
|
||||
v-model:visible="saveVis"
|
||||
:channelData="channelData"
|
||||
@submit="listRef.reload()"
|
||||
/>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChannelApi from '@/api/media/channel';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { useMenuStore } from 'store/menu';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Save from './Save.vue';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
const route = useRoute();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '通道ID',
|
||||
dataIndex: 'channelId',
|
||||
key: 'channelId',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '厂商',
|
||||
dataIndex: 'manufacturer',
|
||||
key: 'manufacturer',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '安装地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '已连接', value: 'online' },
|
||||
{ label: '未连接', value: 'offline' },
|
||||
],
|
||||
handleValue: (v: any) => {
|
||||
return v;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
// console.log('handleSearch e:', e);
|
||||
params.value = e;
|
||||
// console.log('params.value: ', params.value);
|
||||
};
|
||||
|
||||
const saveVis = ref(false);
|
||||
const handleAdd = () => {
|
||||
saveVis.value = true;
|
||||
};
|
||||
|
||||
const listRef = ref();
|
||||
const playVis = ref(false);
|
||||
const channelData = ref();
|
||||
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
const actions = [
|
||||
{
|
||||
key: 'edit',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
saveVis.value = true;
|
||||
channelData.value = data;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'play',
|
||||
text: '播放',
|
||||
tooltip: {
|
||||
title: '播放',
|
||||
},
|
||||
icon: 'VideoCameraOutlined',
|
||||
onClick: () => {
|
||||
playVis.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'backPlay',
|
||||
text: '回放',
|
||||
tooltip: {
|
||||
title: '回放',
|
||||
},
|
||||
icon: 'HistoryOutlined',
|
||||
onClick: () => {
|
||||
menuStory.jumpPage(
|
||||
'media/Device/Playback',
|
||||
{},
|
||||
{
|
||||
id: route.query.id,
|
||||
type: route.query.type,
|
||||
channelId: data.channelId,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
tooltip: {
|
||||
title: '删除',
|
||||
},
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
onConfirm: async () => {
|
||||
const resp = await ChannelApi.del(data.id);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
listRef.value?.reload();
|
||||
} else {
|
||||
message.error('操作失败!');
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
return route?.query.type === 'gb28181-2016'
|
||||
? actions.filter((f) => f.key !== 'delete')
|
||||
: actions;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,114 @@
|
|||
export type CatalogItemType = {
|
||||
district?: string;
|
||||
device?: string;
|
||||
platform?: string;
|
||||
user?: string;
|
||||
platform_outer?: string;
|
||||
ext?: string;
|
||||
};
|
||||
|
||||
export interface CatalogItem {
|
||||
id: string;
|
||||
channelId: string;
|
||||
deviceId: string;
|
||||
name: string;
|
||||
type: CatalogItemType;
|
||||
createTime: number;
|
||||
modifyTime: number;
|
||||
children?: CatalogItem[];
|
||||
}
|
||||
|
||||
export type ChannelStatusType =
|
||||
| 'online'
|
||||
| 'lost'
|
||||
| 'defect'
|
||||
| 'add'
|
||||
| 'delete'
|
||||
| 'update'
|
||||
| 'offline';
|
||||
|
||||
export type PtzType = 'unknown' | 'ball' | 'hemisphere' | 'fixed' | 'remoteControl';
|
||||
|
||||
export type CatalogType = keyof CatalogItemType;
|
||||
|
||||
export type ChannelType =
|
||||
| 'dv_no_storage'
|
||||
| 'dv_has_storage'
|
||||
| 'dv_decoder'
|
||||
| 'networking_monitor_server'
|
||||
| 'media_proxy'
|
||||
| 'web_access_server'
|
||||
| 'video_management_server'
|
||||
| 'network_matrix'
|
||||
| 'network_controller'
|
||||
| 'network_alarm_machine'
|
||||
| 'dvr'
|
||||
| 'video_server'
|
||||
| 'encoder'
|
||||
| 'decoder'
|
||||
| 'video_switching_matrix'
|
||||
| 'audio_switching_matrix'
|
||||
| 'alarm_controller'
|
||||
| 'nvr'
|
||||
| 'hvr'
|
||||
| 'camera'
|
||||
| 'ipc'
|
||||
| 'display'
|
||||
| 'alarm_input'
|
||||
| 'alarm_output'
|
||||
| 'audio_input'
|
||||
| 'audio_output'
|
||||
| 'mobile_trans'
|
||||
| 'other_outer'
|
||||
| 'center_server'
|
||||
| 'web_server'
|
||||
| 'media_dispatcher'
|
||||
| 'proxy_server'
|
||||
| 'secure_server'
|
||||
| 'alarm_server'
|
||||
| 'database_server'
|
||||
| 'gis_server'
|
||||
| 'management_server'
|
||||
| 'gateway_server'
|
||||
| 'media_storage_server'
|
||||
| 'signaling_secure_gateway'
|
||||
| 'business_group'
|
||||
| 'virtual_group'
|
||||
| 'center_user'
|
||||
| 'end_user'
|
||||
| 'media_iap'
|
||||
| 'media_ops'
|
||||
| 'district'
|
||||
| 'other';
|
||||
|
||||
export interface ChannelItem {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
channelId: string;
|
||||
name: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
address: string;
|
||||
provider: string;
|
||||
status: {
|
||||
value: string;
|
||||
text: string;
|
||||
};
|
||||
others: object;
|
||||
description: string;
|
||||
parentChannelId: string;
|
||||
subCount: integer;
|
||||
civilCode: string;
|
||||
ptzType: PtzType;
|
||||
catalogType: CatalogType;
|
||||
channelType: ChannelType;
|
||||
catalogCode: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
createTime: number;
|
||||
modifyTime: number;
|
||||
parentId: string;
|
||||
gb28181ProxyStream: boolean;
|
||||
gb28181ChannelId: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<page-container> 回放 </page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,15 @@
|
|||
export type recordsItemType = {
|
||||
channelId: string;
|
||||
deviceId: string;
|
||||
endTime: number;
|
||||
fileSize: number;
|
||||
name: string;
|
||||
secrecy: string;
|
||||
startTime: number;
|
||||
mediaEndTime: number;
|
||||
mediaStartTime: number;
|
||||
filePath: string;
|
||||
type: string;
|
||||
id: string;
|
||||
isServer?: boolean;
|
||||
};
|
|
@ -51,7 +51,7 @@
|
|||
v-bind="validateInfos.productId"
|
||||
>
|
||||
<a-row :gutter="[0, 10]">
|
||||
<a-col :span="22">
|
||||
<a-col :span="!!route.query.id ? 24 : 22">
|
||||
<a-select
|
||||
v-model:value="formData.productId"
|
||||
placeholder="请选择所属产品"
|
||||
|
@ -66,7 +66,7 @@
|
|||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="2">
|
||||
<a-col :span="2" v-if="!route.query.id">
|
||||
<a-button
|
||||
type="link"
|
||||
@click="saveProductVis = true"
|
||||
|
@ -132,12 +132,11 @@
|
|||
placeholder="请输入说明"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 0, span: 3 }">
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="btnLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
|
@ -356,6 +355,8 @@ const getDetail = async () => {
|
|||
// formData.value = res.result;
|
||||
Object.assign(formData.value, res.result);
|
||||
formData.value.channel = res.result.provider;
|
||||
|
||||
console.log('formData.value: ', formData.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -367,6 +368,7 @@ onMounted(() => {
|
|||
*/
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const handleSubmit = () => {
|
||||
// console.log('formData.value: ', formData.value);
|
||||
validate()
|
||||
.then(async () => {
|
||||
btnLoading.value = true;
|
||||
|
|
|
@ -261,9 +261,13 @@ const getActions = (
|
|||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
menuStory.jumpPage('media/Device/Save', {
|
||||
id: data.id,
|
||||
});
|
||||
menuStory.jumpPage(
|
||||
'media/Device/Save',
|
||||
{},
|
||||
{
|
||||
id: data.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -277,10 +281,14 @@ const getActions = (
|
|||
// router.push(
|
||||
// `/media/device/Channel?id=${data.id}&type=${data.provider}`,
|
||||
// );
|
||||
menuStory.jumpPage('media/Device/Channel', {
|
||||
id: data.id,
|
||||
type: data.provider,
|
||||
});
|
||||
menuStory.jumpPage(
|
||||
'media/Device/Channel',
|
||||
{},
|
||||
{
|
||||
id: data.id,
|
||||
type: data.provider,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -333,23 +333,9 @@ watch(
|
|||
msgType.value = MSG_TYPE[val];
|
||||
|
||||
formData.value.provider =
|
||||
formData.value.provider !== ':id'
|
||||
route.params.id !== ':id'
|
||||
? formData.value.provider
|
||||
: msgType.value[0].value;
|
||||
|
||||
// formData.value.configuration =
|
||||
// CONFIG_FIELD_MAP[val][formData.value.provider];
|
||||
|
||||
// clearValid();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formData.value.provider,
|
||||
(val) => {
|
||||
// formData.value.configuration =
|
||||
// CONFIG_FIELD_MAP[formData.value.type][val];
|
||||
// clearValid();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -421,12 +407,6 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
|||
formRules.value,
|
||||
);
|
||||
|
||||
const clearValid = () => {
|
||||
setTimeout(() => {
|
||||
clearValidate();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const getDetail = async () => {
|
||||
if (route.params.id === ':id') return;
|
||||
const res = await configApi.detail(route.params.id as string);
|
||||
|
@ -444,7 +424,7 @@ const handleTypeChange = () => {
|
|||
setTimeout(() => {
|
||||
formData.value.configuration =
|
||||
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
|
||||
// resetPublicFiles();
|
||||
resetPublicFiles();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
|
@ -454,7 +434,48 @@ const handleTypeChange = () => {
|
|||
const handleProviderChange = () => {
|
||||
formData.value.configuration =
|
||||
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
|
||||
// resetPublicFiles();
|
||||
resetPublicFiles();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置字段值
|
||||
*/
|
||||
const resetPublicFiles = () => {
|
||||
switch (formData.value.provider) {
|
||||
case 'dingTalkMessage':
|
||||
formData.value.configuration.appKey = '';
|
||||
formData.value.configuration.appSecret = '';
|
||||
break;
|
||||
case 'dingTalkRobotWebHook':
|
||||
formData.value.configuration.url = '';
|
||||
break;
|
||||
case 'corpMessage':
|
||||
formData.value.configuration.corpId = '';
|
||||
formData.value.configuration.corpSecret = '';
|
||||
break;
|
||||
case 'embedded':
|
||||
formData.value.configuration.host = '';
|
||||
formData.value.configuration.port = 25;
|
||||
formData.value.configuration.ssl = false;
|
||||
formData.value.configuration.sender = '';
|
||||
formData.value.configuration.username = '';
|
||||
formData.value.configuration.password = '';
|
||||
break;
|
||||
case 'aliyun':
|
||||
formData.value.configuration.regionId = '';
|
||||
formData.value.configuration.accessKeyId = '';
|
||||
formData.value.configuration.secret = '';
|
||||
break;
|
||||
case 'aliyunSms':
|
||||
formData.value.configuration.regionId = '';
|
||||
formData.value.configuration.accessKeyId = '';
|
||||
formData.value.configuration.secret = '';
|
||||
break;
|
||||
case 'http':
|
||||
formData.value.configuration.url = undefined;
|
||||
formData.value.configuration.headers = [];
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@cancel="_vis = false"
|
||||
width="80%"
|
||||
>
|
||||
<a-row :gutter="10">
|
||||
<a-row :gutter="10" class="model-body">
|
||||
<a-col :span="4">
|
||||
<a-input
|
||||
v-model:value="deptName"
|
||||
|
@ -40,6 +40,7 @@
|
|||
:dataSource="dataSource"
|
||||
:loading="tableLoading"
|
||||
model="table"
|
||||
noPagination
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handleAutoBind">
|
||||
|
@ -273,14 +274,24 @@ const getActions = (
|
|||
* 自动绑定
|
||||
*/
|
||||
const handleAutoBind = () => {
|
||||
configApi.dingTalkBindUser([], props.data.id).then(() => {
|
||||
const arr = dataSource.value
|
||||
.filter((item: any) => item.userId && item.status.value === 'error')
|
||||
.map((i: any) => {
|
||||
return {
|
||||
userId: i.userId,
|
||||
providerName: i.userName,
|
||||
thirdPartyUserId: i.thirdPartyUserId,
|
||||
};
|
||||
});
|
||||
// console.log('arr: ', arr);
|
||||
configApi.dingTalkBindUser(arr, props.data.id).then(() => {
|
||||
message.success('操作成功');
|
||||
getTableData();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取钉钉部门用户
|
||||
* 获取钉钉/微信部门用户
|
||||
*/
|
||||
const getDeptUsers = async () => {
|
||||
let res = null;
|
||||
|
@ -304,14 +315,24 @@ const getBindUsers = async () => {
|
|||
return res?.result;
|
||||
};
|
||||
/**
|
||||
* 获取所有用户
|
||||
* 获取所有用户未绑定的用户
|
||||
*/
|
||||
const allUserList = ref([]);
|
||||
const getAllUsers = async () => {
|
||||
const { result } = await configApi.getPlatformUsers();
|
||||
const params = {
|
||||
paging: false,
|
||||
terms: [
|
||||
{
|
||||
column: `id$user-third$${props.data.type}_${props.data.provider}$not`,
|
||||
value: props.data.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = await configApi.getPlatformUsers(params);
|
||||
allUserList.value = result.map((m: any) => ({
|
||||
label: m.name,
|
||||
value: m.id,
|
||||
...m,
|
||||
}));
|
||||
return result;
|
||||
};
|
||||
|
@ -326,31 +347,36 @@ const getTableData = () => {
|
|||
Promise.all<any>([getDeptUsers(), getBindUsers(), getAllUsers()]).then(
|
||||
(res) => {
|
||||
dataSource.value = [];
|
||||
const [deptUsers, bindUsers, allUsers] = res;
|
||||
(deptUsers || []).forEach((item: any) => {
|
||||
const [deptUsers, bindUsers, unBindUsers] = res;
|
||||
(deptUsers || []).forEach((deptUser: any) => {
|
||||
// 未绑定的用户
|
||||
let unBindUser = unBindUsers.find(
|
||||
(f: any) => f.name === deptUser?.name,
|
||||
);
|
||||
// 绑定的用户
|
||||
const bindUser = bindUsers.find(
|
||||
(f: any) => f.thirdPartyUserId === item.id,
|
||||
);
|
||||
// 平台用户
|
||||
const allUser = allUsers.find(
|
||||
(f: any) => f.id === bindUser?.userId,
|
||||
(f: any) => f.thirdPartyUserId === deptUser.id,
|
||||
);
|
||||
if (bindUser) {
|
||||
unBindUser = unBindUsers.find(
|
||||
(f: any) => f.id === bindUser.userId,
|
||||
);
|
||||
}
|
||||
dataSource.value.push({
|
||||
thirdPartyUserId: item.id,
|
||||
thirdPartyUserName: item.name,
|
||||
userId: bindUser?.userId,
|
||||
userName: allUser
|
||||
? `${allUser.name}(${allUser.username})`
|
||||
thirdPartyUserId: deptUser.id,
|
||||
thirdPartyUserName: deptUser.name,
|
||||
bindId: bindUser?.userId,
|
||||
userId: unBindUser?.id,
|
||||
userName: unBindUser
|
||||
? `${unBindUser.name}(${unBindUser.username})`
|
||||
: '',
|
||||
status: {
|
||||
text: bindUser?.providerName ? '已绑定' : '未绑定',
|
||||
value: bindUser?.providerName ? 'success' : 'error',
|
||||
},
|
||||
bindId: bindUser?.id,
|
||||
});
|
||||
});
|
||||
console.log('dataSource.value: ', dataSource.value);
|
||||
// console.log('dataSource.value: ', dataSource.value);
|
||||
},
|
||||
);
|
||||
tableLoading.value = false;
|
||||
|
@ -369,7 +395,11 @@ watch(
|
|||
*/
|
||||
const bindVis = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const formData = ref({ userId: '' });
|
||||
const formData = ref({
|
||||
userId: '',
|
||||
thirdPartyUserId: '',
|
||||
thirdPartyUserName: '',
|
||||
});
|
||||
const formRules = ref({
|
||||
userId: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||
});
|
||||
|
@ -381,7 +411,8 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
|||
|
||||
const handleBind = (row: any) => {
|
||||
bindVis.value = true;
|
||||
formData.value = row;
|
||||
// formData.value = row;
|
||||
Object.assign(formData.value, row);
|
||||
getAllUsers();
|
||||
};
|
||||
|
||||
|
@ -402,8 +433,8 @@ const filterOption = (input: string, option: any) => {
|
|||
const handleBindSubmit = () => {
|
||||
validate().then(async () => {
|
||||
const params = {
|
||||
// providerName: formData.value.thirdPartyUserName,
|
||||
// thirdPartyUserId: formData.value.thirdPartyUserId,
|
||||
providerName: formData.value.thirdPartyUserName,
|
||||
thirdPartyUserId: formData.value.thirdPartyUserId,
|
||||
userId: formData.value.userId,
|
||||
};
|
||||
confirmLoading.value = true;
|
||||
|
@ -434,8 +465,13 @@ const handleBindSubmit = () => {
|
|||
};
|
||||
const handleCancel = () => {
|
||||
bindVis.value = false;
|
||||
resetFields()
|
||||
resetFields();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.model-body {
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<a-button>导入</a-button>
|
||||
</a-upload>
|
||||
<a-popconfirm
|
||||
title="确认导出当前页数据?"
|
||||
title="确认导出?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleExport"
|
||||
|
@ -308,7 +308,7 @@ const beforeUpload = (file: any) => {
|
|||
* 导出
|
||||
*/
|
||||
const handleExport = () => {
|
||||
downloadObject(configRef.value.dataSource, `通知配置`);
|
||||
downloadObject(configRef.value._dataSource, `通知配置`);
|
||||
};
|
||||
|
||||
const syncVis = ref(false);
|
||||
|
|
|
@ -92,10 +92,7 @@ const handleChange = (info: UploadChangeParam, id: string | undefined) => {
|
|||
const targetFileIdx = fileList.value.findIndex((f) => f.id === id);
|
||||
fileList.value[targetFileIdx].name = info.file.name;
|
||||
fileList.value[targetFileIdx].location = info.file.response?.result;
|
||||
emit(
|
||||
'update:attachments',
|
||||
fileList.value.map(({ name, location }) => ({ name, location })),
|
||||
);
|
||||
emitEvents();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -107,6 +104,7 @@ const handleDelete = (id: string | undefined) => {
|
|||
const idx = fileList.value.findIndex((f) => f.id === id);
|
||||
|
||||
fileList.value.splice(idx, 1);
|
||||
emitEvents();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -118,6 +116,15 @@ const handleAdd = () => {
|
|||
name: '',
|
||||
location: '',
|
||||
});
|
||||
|
||||
emitEvents();
|
||||
};
|
||||
|
||||
const emitEvents = () => {
|
||||
emit(
|
||||
'update:attachments',
|
||||
fileList.value.map(({ name, location }) => ({ name, location })),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -106,7 +106,8 @@
|
|||
<a-form-item label="收信部门">
|
||||
<ToOrg
|
||||
v-model:toParty="
|
||||
formData.template.toParty
|
||||
formData.template
|
||||
.departmentIdList
|
||||
"
|
||||
:type="formData.type"
|
||||
:config-id="formData.configId"
|
||||
|
@ -132,7 +133,7 @@
|
|||
</template>
|
||||
<ToUser
|
||||
v-model:toUser="
|
||||
formData.template.toUser
|
||||
formData.template.userIdList
|
||||
"
|
||||
:type="formData.type"
|
||||
:config-id="formData.configId"
|
||||
|
@ -669,33 +670,38 @@
|
|||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item
|
||||
<template
|
||||
v-if="
|
||||
formData.type !== 'webhook' &&
|
||||
formData.type !== 'voice'
|
||||
"
|
||||
v-bind="validateInfos['template.message']"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
模版内容
|
||||
<a-tooltip title="发送的内容,支持录入变量">
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-textarea
|
||||
v-model:value="formData.template.message"
|
||||
:maxlength="200"
|
||||
:rows="5"
|
||||
:disabled="formData.type === 'sms'"
|
||||
placeholder="变量格式:${name};
|
||||
<a-form-item
|
||||
v-bind="validateInfos['template.message']"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
模版内容
|
||||
<a-tooltip
|
||||
title="发送的内容,支持录入变量"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<a-textarea
|
||||
v-model:value="formData.template.message"
|
||||
:maxlength="200"
|
||||
:rows="5"
|
||||
:disabled="formData.type === 'sms'"
|
||||
placeholder="变量格式:${name};
|
||||
示例:尊敬的${name},${time}有设备触发告警,请注意处理"
|
||||
/>
|
||||
</a-form-item>
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item
|
||||
label="变量列表"
|
||||
v-if="
|
||||
|
@ -800,26 +806,61 @@ const formData = ref<TemplateFormData>({
|
|||
});
|
||||
|
||||
/**
|
||||
* 重置公用字段值
|
||||
* 重置字段值
|
||||
*/
|
||||
const resetPublicFiles = () => {
|
||||
formData.value.template.message = '';
|
||||
formData.value.configId = undefined;
|
||||
|
||||
if (
|
||||
formData.value.provider === 'dingTalkMessage' ||
|
||||
formData.value.type === 'weixin'
|
||||
) {
|
||||
formData.value.template.toTag = undefined;
|
||||
formData.value.template.toUser = undefined;
|
||||
formData.value.template.agentId = undefined;
|
||||
formData.value.template = {};
|
||||
switch (formData.value.provider) {
|
||||
case 'dingTalkMessage':
|
||||
formData.value.template.agentId = '';
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.departmentIdList = '';
|
||||
formData.value.template.userIdList = '';
|
||||
break;
|
||||
case 'dingTalkRobotWebHook':
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.messageType = 'markdown';
|
||||
formData.value.template.markdown = { text: '', title: '' };
|
||||
break;
|
||||
case 'corpMessage':
|
||||
formData.value.template.agentId = '';
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.toParty = '';
|
||||
formData.value.template.toUser = '';
|
||||
formData.value.template.toTag = '';
|
||||
break;
|
||||
case 'embedded':
|
||||
formData.value.template.subject = '';
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.text = '';
|
||||
formData.value.template.sendTo = [];
|
||||
formData.value.template.attachments = [];
|
||||
break;
|
||||
case 'aliyun':
|
||||
formData.value.template.templateType = 'tts';
|
||||
formData.value.template.templateCode = '';
|
||||
formData.value.template.ttsCode = '';
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.playTimes = 1;
|
||||
formData.value.template.calledShowNumbers = '';
|
||||
formData.value.template.calledNumber = '';
|
||||
break;
|
||||
case 'aliyunSms':
|
||||
formData.value.template.code = '';
|
||||
formData.value.template.message = '';
|
||||
formData.value.template.phoneNumber = '';
|
||||
formData.value.template.signName = '';
|
||||
break;
|
||||
case 'http':
|
||||
formData.value.template.contextAsBody = true;
|
||||
formData.value.template.body = '';
|
||||
break;
|
||||
}
|
||||
if (formData.value.type === 'weixin')
|
||||
formData.value.template.toParty = undefined;
|
||||
if (formData.value.type === 'email')
|
||||
formData.value.template.toParty = undefined;
|
||||
// formData.value.description = '';
|
||||
|
||||
formData.value.configId = undefined;
|
||||
formData.value.variableDefinitions = [];
|
||||
handleMessageTypeChange();
|
||||
// console.log('formData.value.template: ', formData.value.template);
|
||||
};
|
||||
|
||||
// 根据通知方式展示对应的字段
|
||||
|
@ -831,15 +872,8 @@ watch(
|
|||
route.params.id !== ':id'
|
||||
? formData.value.provider
|
||||
: msgType.value[0].value;
|
||||
// formData.value.provider = formData.value.provider || msgType.value[0].value;
|
||||
// console.log('formData.value.template: ', formData.value.template);
|
||||
|
||||
// formData.value.template =
|
||||
// TEMPLATE_FIELD_MAP[val][formData.value.provider];
|
||||
|
||||
if (val !== 'email') getConfigList();
|
||||
// clearValid();
|
||||
// console.log('formData.value: ', formData.value);
|
||||
|
||||
if (val === 'sms') {
|
||||
getTemplateList();
|
||||
|
@ -848,15 +882,6 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
// watch(
|
||||
// () => formData.value.provider,
|
||||
// (val) => {
|
||||
// formData.value.template = TEMPLATE_FIELD_MAP[formData.value.type][val];
|
||||
|
||||
// clearValid();
|
||||
// },
|
||||
// );
|
||||
|
||||
// 验证规则
|
||||
const formRules = ref({
|
||||
type: [{ required: true, message: '请选择通知方式' }],
|
||||
|
@ -917,7 +942,7 @@ watch(
|
|||
() => formData.value.template.markdown?.title,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
variableReg(val);
|
||||
variableReg();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
@ -926,7 +951,7 @@ watch(
|
|||
() => formData.value.template.link?.title,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
variableReg(val);
|
||||
variableReg();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
@ -935,7 +960,7 @@ watch(
|
|||
() => formData.value.template.subject,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
variableReg(val);
|
||||
variableReg();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
@ -945,7 +970,7 @@ watch(
|
|||
() => formData.value.template.message,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
variableReg(val);
|
||||
variableReg();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
@ -954,21 +979,42 @@ watch(
|
|||
() => formData.value.template.body,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
variableReg(val);
|
||||
variableReg();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* 将需要提取变量的字段值拼接为一个字符串, 用于统一提取变量
|
||||
*/
|
||||
const spliceStr = () => {
|
||||
let variableFieldsStr = formData.value.template.message;
|
||||
if (formData.value.provider === 'dingTalkRobotWebHook') {
|
||||
if (formData.value.template.messageType === 'markdown')
|
||||
variableFieldsStr += formData.value.template.markdown
|
||||
?.title as string;
|
||||
if (formData.value.template.messageType === 'link')
|
||||
variableFieldsStr += formData.value.template.link?.title as string;
|
||||
}
|
||||
if (formData.value.provider === 'embedded')
|
||||
variableFieldsStr += formData.value.template.subject as string;
|
||||
if (formData.value.provider === 'http')
|
||||
variableFieldsStr += formData.value.template.body as string;
|
||||
// console.log('variableFieldsStr: ', variableFieldsStr);
|
||||
return variableFieldsStr || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据字段输入内容, 提取变量
|
||||
* @param value
|
||||
*/
|
||||
const variableReg = (value: string) => {
|
||||
const variableReg = () => {
|
||||
const _val = spliceStr();
|
||||
// 已经存在的变量
|
||||
const oldKey = formData.value.variableDefinitions?.map((m) => m.id);
|
||||
// 正则提取${}里面的值
|
||||
const pattern = /(?<=\$\{).*?(?=\})/g;
|
||||
const titleList = value.match(pattern)?.filter((f) => f);
|
||||
const titleList = _val.match(pattern)?.filter((f) => f);
|
||||
const newKey = [...new Set(titleList)];
|
||||
const result = newKey?.map((m) =>
|
||||
oldKey.includes(m)
|
||||
|
@ -980,28 +1026,37 @@ const variableReg = (value: string) => {
|
|||
format: '%s',
|
||||
},
|
||||
);
|
||||
formData.value.variableDefinitions = [
|
||||
...new Set([
|
||||
...formData.value.variableDefinitions,
|
||||
...(result as IVariableDefinitions[]),
|
||||
]),
|
||||
];
|
||||
formData.value.variableDefinitions = result as IVariableDefinitions[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 钉钉机器人 消息类型选择改变
|
||||
*/
|
||||
const handleMessageTypeChange = () => {
|
||||
delete formData.value.template.markdown;
|
||||
delete formData.value.template.link;
|
||||
delete formData.value.template.text;
|
||||
if (formData.value.template.messageType === 'link') {
|
||||
formData.value.template.link = {
|
||||
title: '',
|
||||
picUrl: '',
|
||||
messageUrl: '',
|
||||
text: formData.value.template.message as string,
|
||||
};
|
||||
}
|
||||
if (formData.value.template.messageType === 'markdown') {
|
||||
formData.value.template.markdown = {
|
||||
title: '',
|
||||
text: formData.value.template.message as string,
|
||||
};
|
||||
}
|
||||
if (formData.value.template.messageType === 'text') {
|
||||
formData.value.template.text = {
|
||||
content: formData.value.template.message as string,
|
||||
};
|
||||
}
|
||||
formData.value.variableDefinitions = [];
|
||||
formData.value.template.message = '';
|
||||
if (formData.value.template.link) {
|
||||
formData.value.template.link.title = '';
|
||||
formData.value.template.link.picUrl = '';
|
||||
formData.value.template.link.messageUrl = '';
|
||||
}
|
||||
if (formData.value.template.markdown) {
|
||||
formData.value.template.markdown.title = '';
|
||||
}
|
||||
// formData.value.template.message = '';
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1047,6 +1102,8 @@ const handleTypeChange = () => {
|
|||
const handleProviderChange = () => {
|
||||
formData.value.template =
|
||||
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
|
||||
// console.log('formData.value: ', formData.value);
|
||||
// console.log('formData.value.template: ', formData.value.template);
|
||||
getConfigList();
|
||||
resetPublicFiles();
|
||||
};
|
||||
|
@ -1112,8 +1169,9 @@ const handleSubmit = () => {
|
|||
setTimeout(() => {
|
||||
validate()
|
||||
.then(async () => {
|
||||
formData.value.template.ttsCode =
|
||||
formData.value.template.templateCode;
|
||||
if (formData.value.provider === 'ttsCode')
|
||||
formData.value.template.ttsCode =
|
||||
formData.value.template.templateCode;
|
||||
btnLoading.value = true;
|
||||
let res;
|
||||
if (!formData.value.id) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<a-button>导入</a-button>
|
||||
</a-upload>
|
||||
<a-popconfirm
|
||||
title="确认导出当前页数据?"
|
||||
title="确认导出?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleExport"
|
||||
|
@ -314,7 +314,7 @@ const beforeUpload = (file: any) => {
|
|||
* 导出
|
||||
*/
|
||||
const handleExport = () => {
|
||||
downloadObject(configRef.value.dataSource, `通知配置`);
|
||||
downloadObject(configRef.value._dataSource, `通知配置`);
|
||||
};
|
||||
|
||||
const syncVis = ref(false);
|
||||
|
|
|
@ -27,16 +27,22 @@ interface ILink {
|
|||
messageUrl: string;
|
||||
text: string;
|
||||
}
|
||||
interface IText {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type TemplateFormData = {
|
||||
template: {
|
||||
// 钉钉消息
|
||||
agentId?: string;
|
||||
message?: string;
|
||||
departmentIdList?: string;
|
||||
userIdList?: string;
|
||||
// 钉钉机器人
|
||||
messageType?: string;
|
||||
markdown?: IMarkDown;
|
||||
link?: ILink;
|
||||
text?: IText;
|
||||
// 微信
|
||||
// agentId?: string;
|
||||
// message?: string;
|
||||
|
|
|
@ -147,10 +147,12 @@ export const TEMPLATE_FIELD_MAP = {
|
|||
dingTalkMessage: {
|
||||
agentId: '',
|
||||
message: '',
|
||||
departmentIdList: '',
|
||||
userIdList: ''
|
||||
},
|
||||
dingTalkRobotWebHook: {
|
||||
message: '',
|
||||
messageType: '',
|
||||
messageType: 'markdown',
|
||||
markdown: {
|
||||
text: '',
|
||||
title: '',
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-form layout="vertical" :rules="rule" :model="form" ref="formRef">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input
|
||||
placeholder="请输入名称"
|
||||
v-model:value="form.name"
|
||||
></a-input> </a-form-item
|
||||
></a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="类型" name="targetType">
|
||||
<a-select
|
||||
:options="options"
|
||||
v-model:value="form.targetType"
|
||||
:disabled="selectDisable"
|
||||
></a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="级别" name="level">
|
||||
<a-radio-group v-model:value="form.level">
|
||||
<a-radio-button
|
||||
v-for="(item, index) in levelOption"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 15px;
|
||||
width: 90%;
|
||||
"
|
||||
>
|
||||
<img
|
||||
:src="getImage(`/alarm/alarm${index + 1}.png`)"
|
||||
style="height: 40px"
|
||||
alt=""
|
||||
/>{{ item.label }}
|
||||
</div>
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="description">
|
||||
<a-textarea v-model:value="form.description"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-button type="primary" @click="handleSave" :loading="loading"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getTargetTypes, save, detail } from '@/api/rule-engine/configuration';
|
||||
import { queryLevel } from '@/api/rule-engine/config';
|
||||
import { query } from '@/api/rule-engine/scene';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Store } from 'jetlinks-store';
|
||||
const route = useRoute();
|
||||
const id = route.query?.id;
|
||||
let selectDisable = ref(false);
|
||||
const queryData = () => {
|
||||
if (id) {
|
||||
detail(id).then((res) => {
|
||||
if (res.status === 200) {
|
||||
form.level = res?.result?.level;
|
||||
form.name = res?.result?.name;
|
||||
form.targetType = res?.result?.targetType;
|
||||
form.description = res?.result?.description;
|
||||
Store.set('configuration-data', res.result);
|
||||
query({
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'id',
|
||||
termType: 'alarm-bind-rule',
|
||||
value: id,
|
||||
},
|
||||
],
|
||||
type: 'and',
|
||||
},
|
||||
],
|
||||
sorts: [
|
||||
{
|
||||
name: 'createTime',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
}).then((resq) => {
|
||||
if (resq.status === 200) {
|
||||
selectDisable.value = !!resq.result.data?.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const rule = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入名称',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最多输入64个字符',
|
||||
},
|
||||
],
|
||||
targetType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择类型',
|
||||
},
|
||||
],
|
||||
level: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择级别',
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{
|
||||
max: 200,
|
||||
message: '最多可输入200个字符',
|
||||
},
|
||||
],
|
||||
};
|
||||
let form = reactive({
|
||||
level: '',
|
||||
targetType: '',
|
||||
name: '',
|
||||
description: '',
|
||||
});
|
||||
let options = ref();
|
||||
let levelOption = ref();
|
||||
let loading = ref(false);
|
||||
const formRef = ref();
|
||||
const menuStory = useMenuStore();
|
||||
const getSupports = async () => {
|
||||
let res = await getTargetTypes();
|
||||
if (res.status === 200) {
|
||||
options.value = res.result.map(
|
||||
(item: { id: string; name: string }) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
getSupports();
|
||||
const getLevel = () => {
|
||||
queryLevel().then((res) => {
|
||||
if (res.status === 200) {
|
||||
levelOption.value = res.result?.levels
|
||||
?.filter((i: any) => i?.level && i?.title)
|
||||
.map((item: { level: number; title: string }) => ({
|
||||
label: item.title,
|
||||
value: item.level,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
getLevel();
|
||||
const handleSave = async () => {
|
||||
loading.value = true;
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
const res = await save(form);
|
||||
loading.value = false;
|
||||
if (res.status === 200) {
|
||||
message.success('操作成功');
|
||||
menuStory.jumpPage(
|
||||
'rule-engine/Alarm/Configuration/Save',
|
||||
{},
|
||||
{ id: res.result?.id },
|
||||
);
|
||||
if (!id) {
|
||||
Store.set('configuration-data', res.result);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
loading.value = false;
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
queryData();
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.ant-radio-button-wrapper {
|
||||
margin: 10px 15px 0 0;
|
||||
width: 125px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>123</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<a-card>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1" tab="基础配置">
|
||||
<Base/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="关联场景联动">
|
||||
<Scene></Scene>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="告警记录"></a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Base from './Base/index.vue';
|
||||
import Scene from './Scene/index.vue'
|
||||
const activeKey = ref('2');
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
<page-container>
|
||||
<div>
|
||||
<Search
|
||||
:columns="query.columns"
|
||||
:columns="columns"
|
||||
target="device-instance"
|
||||
@search="handleSearch"
|
||||
></Search>
|
||||
|
@ -13,6 +13,7 @@
|
|||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-space>
|
||||
|
@ -41,23 +42,27 @@
|
|||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3 style="font-weight: 600">
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<Ellipsis>
|
||||
<span style="font-weight: 600; font-size: 16px">
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="content-des-title">
|
||||
关联场景联动
|
||||
</div>
|
||||
<div class="rule-desc">
|
||||
{{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }}
|
||||
</div>
|
||||
<Ellipsis
|
||||
><div>
|
||||
{{ (slotProps?.scene || []).map((item: any) => item?.name).join(',') || '' }}
|
||||
</div></Ellipsis
|
||||
>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="content-des-title">
|
||||
告警级别
|
||||
</div>
|
||||
<div class="rule-desc">
|
||||
<div>
|
||||
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
|
||||
slotProps.level }}
|
||||
</div>
|
||||
|
@ -200,50 +205,124 @@ import {
|
|||
_disable,
|
||||
remove,
|
||||
_execute,
|
||||
getScene,
|
||||
} from '@/api/rule-engine/configuration';
|
||||
import { queryLevel } from '@/api/rule-engine/config';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
const params = ref<Record<string, any>>({});
|
||||
let isAdd = ref<number>(0);
|
||||
let title = ref<string>('');
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const menuStory = useMenuStore();
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'targetType',
|
||||
key: 'targetType',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '产品',
|
||||
value: 'product',
|
||||
},
|
||||
{
|
||||
label: '设备',
|
||||
value: 'device',
|
||||
},
|
||||
{
|
||||
label: '组织',
|
||||
value: 'org',
|
||||
},
|
||||
{
|
||||
label: '其他',
|
||||
value: 'other',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '告警级别',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const res = await queryLevel();
|
||||
if (res.status === 200) {
|
||||
return (res?.result?.levels || [])
|
||||
.filter((i: any) => i?.level && i?.title)
|
||||
.map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.level,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联场景联动',
|
||||
dataIndex: 'sceneId',
|
||||
wdith: 250,
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: async () => {
|
||||
const res = await getScene(
|
||||
encodeQuery({
|
||||
sorts: { createTime: 'desc' },
|
||||
}),
|
||||
);
|
||||
if(res.status === 200){
|
||||
return res.result.map((item:any) => ({label:item.name, value:item.id}))
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
value: 'enabled',
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 'disabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
search:{
|
||||
type:'string',
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -259,44 +338,6 @@ const map = {
|
|||
org: '组织',
|
||||
other: '其他',
|
||||
};
|
||||
const query = {
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
value: 'enabled',
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 'disabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
key: 'description',
|
||||
dataIndex: 'description',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
|
@ -355,9 +396,7 @@ const getActions = (
|
|||
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
title.value = '编辑';
|
||||
isAdd.value = 2;
|
||||
nextTick(() => {});
|
||||
menuStory.jumpPage('rule-engine/Alarm/Configuration/Save',{},{id:data.id});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -421,14 +460,12 @@ const getActions = (
|
|||
return actions.filter((i: ActionsType) => i.key !== 'view');
|
||||
return actions;
|
||||
};
|
||||
const add = () => {
|
||||
menuStory.jumpPage('rule-engine/Alarm/Configuration/Save');
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.content-des-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
.rule-desc {
|
||||
white-space: nowrap; /*强制在同一行内显示所有文本,直到文本结束或者遭遇br标签对象才换行。*/
|
||||
overflow: hidden; /*超出部分隐藏*/
|
||||
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
</style>
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div>
|
||||
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
|
||||
<Search
|
||||
:columns="query.columns"
|
||||
target="device-instance"
|
||||
@search="handleSearch"
|
||||
></Search>
|
||||
<JTable
|
||||
:columns="columns"
|
||||
:request="queryList"
|
||||
|
@ -14,7 +18,7 @@
|
|||
<template #headerTitle>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="add"
|
||||
><plus-outlined/>新增</a-button
|
||||
><plus-outlined />新增</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
|
@ -36,14 +40,18 @@
|
|||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3 style="font-weight: 600">
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<Ellipsis>
|
||||
<span style="font-weight: 600; font-size: 16px">
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="rule-desc">
|
||||
{{ slotProps.description }}
|
||||
</div>
|
||||
<Ellipsis>
|
||||
<div>
|
||||
{{ slotProps.description }}
|
||||
</div>
|
||||
</Ellipsis>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
@ -154,7 +162,12 @@
|
|||
<script lang="ts" setup>
|
||||
import JTable from '@/components/Table';
|
||||
import type { InstanceItem } from './typings';
|
||||
import { queryList , startRule , stopRule , deleteRule} from '@/api/rule-engine/instance';
|
||||
import {
|
||||
queryList,
|
||||
startRule,
|
||||
stopRule,
|
||||
deleteRule,
|
||||
} from '@/api/rule-engine/instance';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
@ -266,7 +279,10 @@ const getActions = (
|
|||
tooltip: {
|
||||
title: data.state?.value !== 'disable' ? '禁用' : '启用',
|
||||
},
|
||||
icon: data.state?.value !== 'disable' ? 'StopOutlined' : 'CheckCircleOutlined',
|
||||
icon:
|
||||
data.state?.value !== 'disable'
|
||||
? 'StopOutlined'
|
||||
: 'CheckCircleOutlined',
|
||||
popConfirm: {
|
||||
title: `确认${data.state !== 'disable' ? '禁用' : '启用'}?`,
|
||||
onConfirm: async () => {
|
||||
|
@ -332,9 +348,4 @@ const handleSearch = (e: any) => {
|
|||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.rule-desc {
|
||||
white-space: nowrap; /*强制在同一行内显示所有文本,直到文本结束或者遭遇br标签对象才换行。*/
|
||||
overflow: hidden; /*超出部分隐藏*/
|
||||
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,7 @@
|
|||
@click='save'
|
||||
@cancel='cancel'
|
||||
>
|
||||
<a-steps :current='addModel.stepNumber'>
|
||||
<a-steps :current='addModel.stepNumber' @change='stepChange'>
|
||||
<a-step>
|
||||
<template #title>选择产品</template>
|
||||
</a-step>
|
||||
|
@ -17,19 +17,28 @@
|
|||
<template #title>触发类型</template>
|
||||
</a-step>
|
||||
</a-steps>
|
||||
<a-divider style='margin-bottom: 0px' />
|
||||
<div class='steps-content'>
|
||||
<Product :rowKey='addModel.productId' />
|
||||
<Product v-if='addModel.stepNumber === 0' v-model:rowKey='addModel.productId' v-model:detail='addModel.productDetail' />
|
||||
<DeviceSelect
|
||||
v-else-if='addModel.stepNumber === 1'
|
||||
:productId='addModel.productId'
|
||||
v-model:deviceKeys='addModel.deviceKeys'
|
||||
v-model:orgId='addModel.orgId'
|
||||
v-model:selector='addModel.selector'
|
||||
v-model:selectorValues='addModel.selectorValues'
|
||||
/>
|
||||
<Type
|
||||
v-else-if='addModel.stepNumber === 2'
|
||||
:metadata='addModel.metadata'
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class='steps-action'>
|
||||
<template>
|
||||
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
|
||||
<a-button v-else>上一步</a-button>
|
||||
</template>
|
||||
<template>
|
||||
<a-button type='primary' v-if='addModel.stepNumber < 2'>下一步</a-button>
|
||||
<a-button type='primary' v-else>确定</a-button>
|
||||
</template>
|
||||
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
|
||||
<a-button v-else @click='prev'>上一步</a-button>
|
||||
<a-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</a-button>
|
||||
<a-button type='primary' v-else @click='saveClick'>确定</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
@ -37,10 +46,12 @@
|
|||
|
||||
<script setup lang='ts' name='AddModel'>
|
||||
import type { PropType } from 'vue'
|
||||
import { TriggerDevice } from '@/views/rule-engine/Scene/typings'
|
||||
import type { metadataType, TriggerDevice } from '@/views/rule-engine/Scene/typings'
|
||||
import { onlyMessage } from '@/utils/comm'
|
||||
import { detail as deviceDetail } from '@/api/device/instance'
|
||||
import Product from './Product.vue'
|
||||
import DeviceSelect from './DeviceSelect.vue'
|
||||
import Type from './Type.vue'
|
||||
|
||||
type Emit = {
|
||||
(e: 'cancel'): void
|
||||
|
@ -54,11 +65,7 @@ interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
|
|||
orgId: Array<{ label: string, value: string }>
|
||||
productDetail: any
|
||||
selectorValues: Array<{ label: string, value: string }>
|
||||
metadata: {
|
||||
properties?: any[]
|
||||
functions?: any[]
|
||||
events?: any[]
|
||||
}
|
||||
metadata: metadataType
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
@ -97,39 +104,56 @@ const handleOptions = () => {
|
|||
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
addModel.stepNumber = addModel.stepNumber - 1
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emit("cancel")
|
||||
}
|
||||
|
||||
const handleMetadata = (metadata: string) => {
|
||||
const handleMetadata = (metadata?: string) => {
|
||||
try {
|
||||
addModel.metadata = JSON.parse(metadata)
|
||||
addModel.metadata = JSON.parse(metadata || "{}")
|
||||
} catch (e) {
|
||||
console.warn('handleMetadata: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (addModel.stepNumber === 0) {
|
||||
const save = async (step?: number) => {
|
||||
let _step = step !== undefined ? step : addModel.stepNumber
|
||||
if (_step === 0) {
|
||||
addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error')
|
||||
} else if (addModel.stepNumber === 1) {
|
||||
} else if (_step === 1) {
|
||||
const isFixed = addModel.selector === 'fixed' // 是否选择方式为设备
|
||||
if ((['fixed', 'org'].includes(addModel.selector) ) && addModel.selectorValues?.length) {
|
||||
if ((['fixed', 'org'].includes(addModel.selector) ) && !addModel.selectorValues?.length) {
|
||||
return onlyMessage(isFixed ? '请选择设备' : '请选择部门', 'error')
|
||||
}
|
||||
// 选择方式为设备且仅选中一个设备时,物模型取该设备
|
||||
if (isFixed && addModel.selectorValues?.length === 1) {
|
||||
const resp = await deviceDetail(addModel.selectorValues[0].value)
|
||||
addModel.metadata
|
||||
handleMetadata(resp.result.metadata)
|
||||
} else {
|
||||
|
||||
handleMetadata(addModel.productDetail?.metadata)
|
||||
}
|
||||
//
|
||||
addModel.stepNumber = 2
|
||||
} else {
|
||||
|
||||
}
|
||||
// handleOptions()
|
||||
// emit('update:value', {})
|
||||
}
|
||||
|
||||
const saveClick = () => save()
|
||||
|
||||
const stepChange = (step: number) => {
|
||||
if (step !== 0) {
|
||||
save(step - 1)
|
||||
} else {
|
||||
addModel.stepNumber = 0
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
<template>
|
||||
<Search
|
||||
:columns="columns"
|
||||
type='simple'
|
||||
@search="handleSearch"
|
||||
class='search'
|
||||
target="scene-triggrt-device-device"
|
||||
/>
|
||||
<a-divider style='margin: 0' />
|
||||
<j-table
|
||||
ref='actionRef'
|
||||
model='CARD'
|
||||
:columns='columns'
|
||||
:request='deviceQuery'
|
||||
:gridColumn='2'
|
||||
:params='params'
|
||||
:bodyStyle='{
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0
|
||||
}'
|
||||
>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value='slotProps'
|
||||
:active="deviceRowKeys.includes(slotProps.id)"
|
||||
:status="slotProps.state?.value"
|
||||
:statusText="slotProps.state?.text"
|
||||
:statusNames="{
|
||||
online: 'success',
|
||||
offline: 'error',
|
||||
notActive: 'warning',
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<Ellipsis style='width: calc(100% - 100px)'>
|
||||
<span style="font-size: 16px;font-weight: 600" >
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
设备类型
|
||||
</div>
|
||||
<div>{{ slotProps.deviceType?.text }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
产品名称
|
||||
</div>
|
||||
<div>{{ slotProps.productName }}</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
</j-table>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='DeviceSelectList'>
|
||||
import type { PropType } from 'vue'
|
||||
import { getImage } from '@/utils/comm'
|
||||
import { query } from '@/api/device/instance'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update', data: Array<{ name: string, value: string}>): void
|
||||
}
|
||||
|
||||
const actionRef = ref()
|
||||
const params = ref({})
|
||||
const context = inject('SceneDeviceAddModel')
|
||||
const props = defineProps({
|
||||
rowKeys: {
|
||||
type: Array as PropType<Array<{ name: string, value: string}>>,
|
||||
default: () => ([])
|
||||
},
|
||||
productId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const deviceRowKeys = computed(() => {
|
||||
return props.rowKeys.map(item => item.value)
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
first: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 200,
|
||||
search: {
|
||||
type: 'date'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
width: 90,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '禁用', value: 'notActive' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '在线', value: 'online' },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const handleSearch = (p: any) => {
|
||||
params.value = p
|
||||
}
|
||||
|
||||
const deviceQuery = (p: any) => {
|
||||
const sorts: any = [];
|
||||
|
||||
if (props.rowKeys) {
|
||||
props.rowKeys.forEach(rowKey => {
|
||||
sorts.push({
|
||||
name: 'id',
|
||||
value: rowKey,
|
||||
});
|
||||
})
|
||||
}
|
||||
sorts.push({ name: 'createTime', order: 'desc' });
|
||||
const terms = [
|
||||
...p.terms,
|
||||
{ terms: [{ column: "productId", value: props.productId }]}
|
||||
]
|
||||
return query({ ...p, terms, sorts })
|
||||
}
|
||||
|
||||
const handleClick = (detail: any) => {
|
||||
const cloneRowKeys = cloneDeep(props.rowKeys)
|
||||
const indexOf = cloneRowKeys.findIndex(item => item.value === detail.id)
|
||||
if (indexOf !== -1) {
|
||||
cloneRowKeys.splice(indexOf, 1)
|
||||
} else {
|
||||
cloneRowKeys.push({
|
||||
name: detail.name,
|
||||
value: detail.id
|
||||
})
|
||||
}
|
||||
console.log('cloneRowKeys', cloneRowKeys)
|
||||
emit('update', cloneRowKeys)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search {
|
||||
margin-bottom: 0;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class='device-select'>
|
||||
<TopCard :options='typeList' v-model:value='selectorModel' @select='select' />
|
||||
<DeviceList v-if='selectorModel === "fixed"' :productId='productId' :row-keys='devices' @update='updateDevice' />
|
||||
<OrgList v-else-if='selectorModel === "org"' :productId='productId' :row-keys='orgIds' @update='updateOrg' />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
|
||||
import DeviceList from './DeviceList.vue'
|
||||
import OrgList from './OrgList.vue'
|
||||
import { getImage } from '@/utils/comm'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type ItemType = {
|
||||
name: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:selector', data: string): void
|
||||
(e: 'update:selectorValues', data: ItemType[]): void
|
||||
(e: 'update:deviceKeys', data: ItemType[]): void
|
||||
(e: 'update:orgId', data: ItemType[]): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const props = defineProps({
|
||||
productId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selector: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
device: {
|
||||
type: Array as PropType<ItemType[]>,
|
||||
default: () => []
|
||||
},
|
||||
orgId: {
|
||||
type: Array as PropType<ItemType[]>,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const selectorModel = ref(props.selector)
|
||||
const devices = ref(props.device)
|
||||
const orgIds = ref(props.orgId)
|
||||
|
||||
const typeList = [
|
||||
{ label: '自定义', value: 'fixed', tip: '自定义选择当前产品下的任意设备', img: getImage('/scene/device-custom.png')},
|
||||
{ label: '全部', value: 'all', tip: '产品下的所有设备', img: getImage('/scene/trigger-device-all.png')},
|
||||
{ label: '按组织', value: 'org', tip: '选择产品下归属于具体组织的设备', img: getImage('/scene/trigger-device-org.png')},
|
||||
]
|
||||
|
||||
const select = (s: string) => {
|
||||
selectorModel.value = s
|
||||
emit('update:selector', s)
|
||||
emit('update:selectorValues', [])
|
||||
}
|
||||
|
||||
const updateDevice = (d: any[]) => {
|
||||
devices.value = d
|
||||
emit('update:deviceKeys', d)
|
||||
emit('update:selectorValues', d)
|
||||
}
|
||||
|
||||
const updateOrg = (d: any[]) => {
|
||||
orgIds.value = d
|
||||
emit('update:orgId', d)
|
||||
emit('update:selectorValues', d)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.device-select{
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<Search
|
||||
:columns="columns"
|
||||
type='simple'
|
||||
@search="handleSearch"
|
||||
class='search'
|
||||
target="scene-triggrt-device-category"
|
||||
/>
|
||||
<a-divider style='margin: 0' />
|
||||
<JTable
|
||||
ref="instanceRef"
|
||||
model='TABLE'
|
||||
type='TREE'
|
||||
:columns="columns"
|
||||
:request="query"
|
||||
:scroll="{
|
||||
y: 350
|
||||
}"
|
||||
:expandable='{
|
||||
expandedRowKeys: openKeys,
|
||||
onExpandedRowsChange: expandedRowChange,
|
||||
}'
|
||||
:rowSelection='{
|
||||
type: "radio",
|
||||
selectedRowKeys: orgRowKeys,
|
||||
onChange: selectedRowChange
|
||||
}'
|
||||
:onChange='tableChange'
|
||||
>
|
||||
|
||||
</JTable>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='OrgList'>
|
||||
import type { PropType } from 'vue'
|
||||
import { getExpandedRowById } from './util'
|
||||
import { getTreeData_api } from '@/api/system/department'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update', data: Array<{ name: string, value: string}>): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
rowKeys: {
|
||||
type: Array as PropType<Array<{ name: string, value: string}>>,
|
||||
default: () => ([])
|
||||
},
|
||||
productId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const params = ref()
|
||||
const openKeys = ref<string[]>([])
|
||||
const selectedRowKeys = ref(props.rowKeys.map(item => item.value))
|
||||
const sortParam = ref<{ name:string, order: string }>({ name: 'sortIndex', order: 'asc' })
|
||||
const iniPage = ref(true)
|
||||
|
||||
const orgRowKeys = computed(() => {
|
||||
return props.rowKeys.map(item => item.value)
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sortIndex',
|
||||
sorter: true,
|
||||
},
|
||||
]
|
||||
|
||||
const handleSearch = (p: any) => {
|
||||
params.value = p
|
||||
}
|
||||
|
||||
const tableChange = (_: any, f: any, sorter: any) => {
|
||||
if (sorter.order) {
|
||||
sortParam.value = { name: sorter.columnKey, order: (sorter.order as string).replace('end', ''), }
|
||||
} else {
|
||||
sortParam.value = { name: 'sortIndex', order: 'asc' }
|
||||
}
|
||||
}
|
||||
|
||||
const query = async (p: any) => {
|
||||
const _params: any = {
|
||||
paging: false,
|
||||
sorts: [sortParam.value],
|
||||
}
|
||||
|
||||
if (p.terms && p.terms.length) {
|
||||
_params.terms = p.terms
|
||||
}
|
||||
|
||||
const resp = await getTreeData_api(_params)
|
||||
|
||||
if (iniPage.value && props.rowKeys.length) {
|
||||
iniPage.value = false
|
||||
openKeys.value = getExpandedRowById(props.rowKeys[0]?.value, resp.result as any[])
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
const selectedRowChange = (_: any, selectedRows: any[]) => {
|
||||
const item = selectedRows[0]
|
||||
console.log(selectedRows)
|
||||
emit('update', item ? [{ name: item.name, value: item.id }] : [])
|
||||
}
|
||||
|
||||
const expandedRowChange = (keys: string[]) => {
|
||||
openKeys.value = keys
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search {
|
||||
margin-bottom: 0;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
</style>
|
|
@ -4,18 +4,25 @@
|
|||
type='simple'
|
||||
@search="handleSearch"
|
||||
class='search'
|
||||
target="scene-triggrt-device-device"
|
||||
/>
|
||||
<a-divider style='margin: 0' />
|
||||
<j-table
|
||||
:columns='columns'
|
||||
ref='actionRef'
|
||||
model='CARD'
|
||||
:columns='columns'
|
||||
:params='params'
|
||||
:request='productQuery'
|
||||
:gridColumn='2'
|
||||
model='CARD'
|
||||
:bodyStyle='{
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0
|
||||
}'
|
||||
>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value='slotProps'
|
||||
:active="selectedRowKeys.includes(slotProps.id)"
|
||||
:active="rowKey === slotProps.id"
|
||||
:status="slotProps.state"
|
||||
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
|
||||
:statusNames="{ 1: 'success', 0: 'error', }"
|
||||
|
@ -23,13 +30,17 @@
|
|||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/device-product.png')" />
|
||||
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<h3 style="font-weight: 600" >
|
||||
{{ slotProps.name }}
|
||||
</h3>
|
||||
<div style='width: calc(100% - 100px)'>
|
||||
<Ellipsis>
|
||||
<span style="font-size: 16px;font-weight: 600" >
|
||||
{{ slotProps.name }}
|
||||
</span>
|
||||
</Ellipsis>
|
||||
</div>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
|
@ -51,16 +62,25 @@ import { getTreeData_api } from '@/api/system/department'
|
|||
import { isNoCommunity } from '@/utils/utils'
|
||||
import { getImage } from '@/utils/comm'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:rowKey', data: string): void
|
||||
(e: 'update:detail', data: string): void
|
||||
}
|
||||
|
||||
const actionRef = ref()
|
||||
const params = ref({})
|
||||
const props = defineProps({
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
detail: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const selectedRowKeys = ref(props.rowKey)
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -69,12 +89,19 @@ const columns = [
|
|||
width: 300,
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
first: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '网关类型',
|
||||
|
@ -199,7 +226,6 @@ const columns = [
|
|||
|
||||
const handleSearch = (p: any) => {
|
||||
params.value = p
|
||||
actionRef.value.required()
|
||||
}
|
||||
|
||||
const productQuery = (p: any) => {
|
||||
|
@ -217,12 +243,8 @@ const productQuery = (p: any) => {
|
|||
}
|
||||
|
||||
const handleClick = (detail: any) => {
|
||||
const _selected = new Set(selectedRowKeys.value)
|
||||
if (_selected.has(detail.id)) {
|
||||
_selected.delete(detail.id)
|
||||
} else {
|
||||
_selected.add(detail.id)
|
||||
}
|
||||
emit('update:rowKey', detail.id)
|
||||
emit('update:detail', detail)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -230,5 +252,7 @@ const handleClick = (detail: any) => {
|
|||
<style scoped lang='less'>
|
||||
.search {
|
||||
margin-bottom: 0;
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<a-row :gutter='[24]'>
|
||||
<a-col :span='10'>
|
||||
<a-form-item
|
||||
name='readProperties'
|
||||
:rules="[{ required: true, message: '请选择属性' }]"
|
||||
>
|
||||
<a-select
|
||||
show-search
|
||||
mode='multiple'
|
||||
max-tag-count='responsive'
|
||||
placeholder='请选择属性'
|
||||
style='width: 100%'
|
||||
v-model:value='readProperties'
|
||||
:options='properties'
|
||||
:filter-option='filterSelectNode'
|
||||
@change='change'
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span='14'>
|
||||
<a-form-item>定时读取所选属性值</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='ReadProperties'>
|
||||
import { filterSelectNode } from '@/utils/comm'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: Array<string>): void
|
||||
(e: 'update:action', data: string): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
default: () => []
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const readProperties = ref<string[]>(props.value)
|
||||
|
||||
const change = (values: string[], optionItems: any[]) => {
|
||||
console.log(values, optionItems)
|
||||
const names = optionItems.map((item) => item.name);
|
||||
let extraStr = '';
|
||||
let isLimit = false;
|
||||
let indexOf = 0;
|
||||
extraStr = names.reduce((_prev, next, index) => {
|
||||
if (_prev.length <= 30) {
|
||||
indexOf = index;
|
||||
return index === 0 ? next : _prev + '、' + next;
|
||||
} else {
|
||||
isLimit = true;
|
||||
}
|
||||
return _prev;
|
||||
}, '');
|
||||
|
||||
if (isLimit && names.length - 1 > indexOf) {
|
||||
extraStr += `等${optionItems.length}个属性`;
|
||||
}
|
||||
emit('update:value', values)
|
||||
emit('update:action', `读取 ${extraStr}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<a-row :futter='[24]'>
|
||||
<a-col :span='10'>
|
||||
<a-select
|
||||
showSearch
|
||||
max-tag-count='responsive'
|
||||
style='width: 100%'
|
||||
placeholder='placeholder'
|
||||
v-model:value='reportKey'
|
||||
:options='properties'
|
||||
:filter-option='filterSelectNode'
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span='14'>定时调用所选属性</a-col>
|
||||
<a-col :span='24' v-if='showTable'>
|
||||
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='ReportEvent'>
|
||||
import { filterSelectNode } from '@/utils/comm'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: Array<string>): void
|
||||
(e: 'update:action', data: string): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
default: () => []
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const reportKey = ref<Array<string>>([])
|
||||
const callData = ref<Array<{}>>()
|
||||
|
||||
const showTable = computed(() => {
|
||||
return !!reportKey.value
|
||||
})
|
||||
|
||||
watch([props.value, props.properties], () => {
|
||||
if (props.value && props.properties?.length) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<div class='type'>
|
||||
<a-form ref='typeForm' :model='formModel' layout='vertical' :colon='false'>
|
||||
<a-form-item
|
||||
required
|
||||
label='触发类型'
|
||||
>
|
||||
<TopCard
|
||||
:label-bottom='true'
|
||||
:options='topOptions'
|
||||
v-model:value='formModel.operator'
|
||||
/>
|
||||
</a-form-item>
|
||||
<Timer v-if='showTimer' v-model:value='formModel.timer' />
|
||||
<ReadProperties v-if='showReadProperty' v-model:value='formModel.readProperties' v-model:action='optionCache.action' :properties='readProperties' />
|
||||
<a-form-item
|
||||
v-if='showWriteProperty'
|
||||
name='writeProperties'
|
||||
:rules="[{ required: true, message: '请输入修改值' }]"
|
||||
>
|
||||
<WriteProperty v-model:value='formModel.writeProperties' v-model:action='optionCache.action' :properties='writeProperties' />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
|
||||
import { TopCard, Timer } from '@/views/rule-engine/Scene/Save/components'
|
||||
import { getImage } from '@/utils/comm'
|
||||
import { metadataType } from '@/views/rule-engine/Scene/typings'
|
||||
import type { PropType } from 'vue'
|
||||
import { TypeEnum } from '@/views/rule-engine/Scene/Save/Device/util'
|
||||
import ReadProperties from './ReadProperties.vue'
|
||||
import ReportEvent from './ReportEvent.vue'
|
||||
import WriteProperty from './WriteProperty.vue'
|
||||
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
type: Object as PropType<metadataType>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const formModel = reactive({
|
||||
operator: 'online',
|
||||
timer: {},
|
||||
readProperties: [],
|
||||
writeProperties: {}
|
||||
})
|
||||
|
||||
const optionCache = reactive({
|
||||
action: ''
|
||||
})
|
||||
|
||||
const readProperties = ref<any[]>([])
|
||||
const writeProperties = ref<any[]>([])
|
||||
|
||||
const topOptions = computed(() => {
|
||||
const baseOptions = [
|
||||
{
|
||||
label: '设备上线',
|
||||
value: 'online',
|
||||
img: getImage('/scene/online.png'),
|
||||
},
|
||||
{
|
||||
label: '设备离线',
|
||||
value: 'offline',
|
||||
img: getImage('/scene/offline.png'),
|
||||
},
|
||||
]
|
||||
|
||||
if (props.metadata.events?.length) {
|
||||
baseOptions.push(TypeEnum.reportEvent)
|
||||
}
|
||||
|
||||
if (props.metadata.properties?.length) {
|
||||
const _properties = props.metadata.properties
|
||||
readProperties.value = _properties.filter((item: any) => item.expands.type?.includes('read')).map(item => ({...item, label: item.name, value: item.id }))
|
||||
writeProperties.value = _properties.filter((item: any) => item.expands.type?.includes('write')).map(item => ({...item, label: item.name, value: item.id }))
|
||||
const reportProperties = _properties.filter((item: any) => item.expands.type?.includes('report')).map(item => ({...item, label: item.name, value: item.id }))
|
||||
|
||||
if (readProperties.value.length) {
|
||||
baseOptions.push(TypeEnum.readProperty)
|
||||
}
|
||||
|
||||
if (writeProperties.value.length) {
|
||||
baseOptions.push(TypeEnum.writeProperty)
|
||||
}
|
||||
|
||||
if (reportProperties.length) {
|
||||
baseOptions.push(TypeEnum.reportProperty)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (props.metadata.functions?.length) {
|
||||
baseOptions.push(TypeEnum.invokeFunction)
|
||||
}
|
||||
|
||||
return baseOptions
|
||||
})
|
||||
|
||||
const showReadProperty = computed(() => {
|
||||
return formModel.operator === TypeEnum.readProperty.value
|
||||
})
|
||||
|
||||
const showWriteProperty = computed(() => {
|
||||
return formModel.operator === TypeEnum.writeProperty.value
|
||||
})
|
||||
|
||||
const showReportEvent = computed(() => {
|
||||
return formModel.operator === TypeEnum.reportEvent.value
|
||||
})
|
||||
|
||||
const showInvokeFunction = computed(() => {
|
||||
return formModel.operator === TypeEnum.invokeFunction.value
|
||||
})
|
||||
|
||||
const showTimer = computed(() => {
|
||||
return [
|
||||
TypeEnum.readProperty.value,
|
||||
TypeEnum.writeProperty.value,
|
||||
TypeEnum.invokeFunction.value
|
||||
].includes(formModel.operator)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.type {
|
||||
max-height: calc(100vh - 350px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<a-row :futter='[24, 24]'>
|
||||
<a-col :span='10'>
|
||||
<a-select
|
||||
showSearch
|
||||
style='width: 100%'
|
||||
placeholder='请选择属性'
|
||||
v-model:value='reportKey'
|
||||
:options='properties'
|
||||
:filter-option='filterSelectNode'
|
||||
@change='change'
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span='14'>
|
||||
<span style='line-height: 32px;padding-left: 24px'>
|
||||
定时调用所选属性
|
||||
</span>
|
||||
</a-col>
|
||||
<a-col :span='24' v-if='showTable'>
|
||||
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='WriteProperties'>
|
||||
import { filterSelectNode } from '@/utils/comm'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: Record<string, any>): void
|
||||
(e: 'update:action', data: string): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => []
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const reportKey = ref<string>()
|
||||
const callData = ref<Array<Record<string, any>>>()
|
||||
|
||||
const callDataOptions = computed(() => {
|
||||
const _valueKeys = Object.keys(props.value)
|
||||
if (_valueKeys.length) {
|
||||
return _valueKeys.map(key => {
|
||||
const item: any = props.properties.find((p: any) => p.id === key)
|
||||
if (item) {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.valueType ? item.valueType.type : '-',
|
||||
format: item.valueType ? item.valueType.format : undefined,
|
||||
options: item.valueType ? item.valueType.element : undefined,
|
||||
value: props.value[key]
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: key,
|
||||
name: key,
|
||||
type: '',
|
||||
format: undefined,
|
||||
options: undefined,
|
||||
value: props.value[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const showTable = computed(() => {
|
||||
return !!reportKey.value
|
||||
})
|
||||
|
||||
const change = (v: string, option: any) => {
|
||||
console.log(v, option)
|
||||
const _data = {
|
||||
[v]: undefined
|
||||
}
|
||||
callData.value = [_data]
|
||||
emit('update:value', _data)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -26,7 +26,8 @@ import AddButton from '../components/AddButton.vue'
|
|||
import Title from '../components/Title.vue'
|
||||
|
||||
const sceneStore = useSceneStore()
|
||||
const { data } = storeToRefs(sceneStore)
|
||||
const { data } = storeToRefs<any>(sceneStore)
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
const rules = [{
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { getImage } from '@/utils/comm'
|
||||
|
||||
export const TypeName = {
|
||||
online: '设备上线',
|
||||
offline: '设备离线',
|
||||
reportEvent: '事件上报',
|
||||
reportProperty: '属性上报',
|
||||
readProperty: '读取属性',
|
||||
writeProperty: '修改属性',
|
||||
invokeFunction: '功能调用',
|
||||
};
|
||||
|
||||
export const TypeEnum = {
|
||||
reportProperty: {
|
||||
label: '属性上报',
|
||||
value: 'reportProperty',
|
||||
img: getImage('/scene/reportProperty.png'),
|
||||
},
|
||||
reportEvent: {
|
||||
label: '事件上报',
|
||||
value: 'reportEvent',
|
||||
img: getImage('/scene/reportProperty.png'),
|
||||
},
|
||||
readProperty: {
|
||||
label: '读取属性',
|
||||
value: 'readProperty',
|
||||
img: getImage('/scene/readProperty.png'),
|
||||
},
|
||||
writeProperty: {
|
||||
label: '修改属性',
|
||||
value: 'writeProperty',
|
||||
img: getImage('/scene/writeProperty.png'),
|
||||
},
|
||||
invokeFunction: {
|
||||
label: '功能调用',
|
||||
value: 'invokeFunction',
|
||||
img: getImage('/scene/invokeFunction.png'),
|
||||
},
|
||||
};
|
||||
|
||||
export const getExpandedRowById = (id: string, data: any[]): string[] => {
|
||||
const expandedKeys:string[] = []
|
||||
const dataMap = new Map()
|
||||
|
||||
const flatMapData = (flatData: any[]) => {
|
||||
flatData.forEach(item => {
|
||||
dataMap.set(item.id, { pid: item.parentId })
|
||||
if (item.children && item.children.length) {
|
||||
flatMapData(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getExp = (_id: string) => {
|
||||
const item = dataMap.get(_id)
|
||||
if (item) {
|
||||
expandedKeys.push(_id)
|
||||
if (dataMap.has(dataMap)) {
|
||||
getExp(item.pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flatMapData(data)
|
||||
|
||||
getExp(id)
|
||||
|
||||
return expandedKeys
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<a-table
|
||||
:data-source='dataSource'
|
||||
:columns='columns'
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if='column.key'>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='FunctionCall'>
|
||||
|
||||
const dataSource = []
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '参数名称',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '参数名称',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
align: 'center',
|
||||
width: 260
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div class='timer-when-warp'>
|
||||
<div :class='["when-item-option", allActive ? "active" : ""]' @click='() => change(0)'>每天</div>
|
||||
<div
|
||||
v-for='item in timeOptions'
|
||||
:class='["when-item-option", rowKeys.includes(item.value) ? "active" : ""]'
|
||||
@click='() => change(item.value)'
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='WhenOption'>
|
||||
import type { PropType } from 'vue'
|
||||
import { numberToString } from './util'
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: Array<number>):void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array as PropType<Array<number>>,
|
||||
default: []
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const timeOptions = ref<Array<{ label: string, value: number}>>([])
|
||||
const rowKeys = ref<Array<number>>(props.value)
|
||||
|
||||
const change = (number: number) => {
|
||||
const _keys = new Set(rowKeys.value)
|
||||
if (number === 0) { // 全选
|
||||
_keys.clear()
|
||||
} else {
|
||||
if (_keys.has(number)) {
|
||||
_keys.delete(number)
|
||||
} else {
|
||||
_keys.add(number)
|
||||
}
|
||||
}
|
||||
rowKeys.value = [..._keys.values()]
|
||||
emit('update:value', rowKeys.value)
|
||||
}
|
||||
|
||||
const allActive = computed(() => {
|
||||
return !rowKeys.value.length
|
||||
})
|
||||
|
||||
watch(() => props.type, () => {
|
||||
const isMonth = props.type === 'month'
|
||||
const day = isMonth ? 31 : 7
|
||||
change(0)
|
||||
timeOptions.value = new Array(day)
|
||||
.fill(1)
|
||||
.map((_, index) => {
|
||||
const _value = index + 1
|
||||
return {
|
||||
label: isMonth ? `${_value}号` : numberToString[_value],
|
||||
value: _value
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.timer-when-warp {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.when-item-option {
|
||||
width: 76px;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #233dd7;
|
||||
border-color: #233dd7;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import Timer from './index.vue'
|
||||
|
||||
export default Timer
|
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<a-form
|
||||
ref='timerForm'
|
||||
:model='formModel'
|
||||
layout='vertical'
|
||||
:colon='false'
|
||||
>
|
||||
<a-form-item name='trigger'>
|
||||
<a-radio-group
|
||||
v-model:value='formModel.trigger'
|
||||
:options='[
|
||||
{ label: "按周", value: "week" },
|
||||
{ label: "按月", value: "month" },
|
||||
{ label: "cron表达式", value: "cron" },
|
||||
]'
|
||||
option-type='button'
|
||||
button-style='solid'
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if='showCron' name='cron' :rules="[
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
{
|
||||
validator: async (_, v) => {
|
||||
if (v) {
|
||||
if (!isCron(v)) {
|
||||
return Promise.reject(new Error('请输入正确的cron表达式'));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error('请输入cron表达式'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]">
|
||||
<a-input placeholder='corn表达式' v-model:value='formModel.cron' />
|
||||
</a-form-item>
|
||||
<template v-else>
|
||||
<a-form-item name='when'>
|
||||
<WhenOption v-model:value='formModel.when' :type='formModel.trigger' />
|
||||
</a-form-item>
|
||||
<a-form-item name='mod'>
|
||||
<a-radio-group
|
||||
v-model:value='formModel.mod'
|
||||
:options='[
|
||||
{ label: "周期执行", value: "period" },
|
||||
{ label: "执行一次", value: "once" },
|
||||
]'
|
||||
option-type='button'
|
||||
button-style='solid'
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-space v-if='showOnce' style='display: flex;gap: 24px'>
|
||||
<a-form-item :name="['once', 'time']">
|
||||
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%'
|
||||
format='HH:mm:ss' />
|
||||
</a-form-item>
|
||||
<a-form-item> 执行一次</a-form-item>
|
||||
</a-space>
|
||||
<a-space v-if='showPeriod' style='display: flex;gap: 24px'>
|
||||
<a-form-item>
|
||||
<a-time-range-picker
|
||||
valueFormat='HH:mm:ss'
|
||||
:value='[
|
||||
formModel.period.from,
|
||||
formModel.period.to,
|
||||
]'
|
||||
@change='(v) => {
|
||||
formModel.period.from = v[0]
|
||||
formModel.period.to = v[1]
|
||||
}'
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>每</a-form-item>
|
||||
<a-form-item
|
||||
:name='["period", "every"]'
|
||||
:rules='[{ required: true, message: "请输入时间" }]'
|
||||
>
|
||||
<a-input-number
|
||||
placeholder='请输入时间'
|
||||
style='max-width: 170px'
|
||||
:precision='0'
|
||||
:min='1'
|
||||
:max='59'
|
||||
v-model:value='formModel.period.every'
|
||||
>
|
||||
<template #addonAfter>
|
||||
<a-select
|
||||
v-model:value='formModel.period.unit'
|
||||
:options='[
|
||||
{ label: "秒", value: "seconds" },
|
||||
{ label: "分", value: "minutes" },
|
||||
{ label: "小时", value: "hours" },
|
||||
]'
|
||||
/>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>执行一次</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='Timer'>
|
||||
import type { PropType } from 'vue'
|
||||
import moment from 'moment'
|
||||
import WhenOption from './WhenOption.vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type { OperationTimer } from '../../../typings'
|
||||
import { isCron } from '@/utils/regular'
|
||||
|
||||
type NameType = string[] | string
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: OperationTimer): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: [String, Array] as PropType<NameType>,
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const formModel = reactive<OperationTimer>({
|
||||
trigger: 'week',
|
||||
when: [],
|
||||
mod: 'period',
|
||||
cron: undefined,
|
||||
once: {
|
||||
time: moment(new Date()).format('HH:mm:ss')
|
||||
},
|
||||
period: {
|
||||
from: moment(new Date()).startOf('day').format('HH:mm:ss'),
|
||||
to: moment(new Date()).endOf('day').format('HH:mm:ss'),
|
||||
every: 1,
|
||||
unit: 'seconds'
|
||||
}
|
||||
})
|
||||
|
||||
Object.assign(formModel, props.value)
|
||||
|
||||
const showCron = computed(() => {
|
||||
return formModel.trigger === 'cron'
|
||||
})
|
||||
|
||||
const showOnce = computed(() => {
|
||||
return formModel.trigger !== 'cron' && formModel.mod === 'once'
|
||||
})
|
||||
|
||||
const showPeriod = computed(() => {
|
||||
return formModel.trigger !== 'cron' && formModel.mod === 'period'
|
||||
})
|
||||
|
||||
watch(() => formModel, () => {
|
||||
const cloneValue = cloneDeep(formModel)
|
||||
if (cloneValue.trigger === 'cron') {
|
||||
delete cloneValue.when
|
||||
} else {
|
||||
delete cloneValue.cron
|
||||
}
|
||||
|
||||
if (cloneValue.mod === 'period') {
|
||||
delete cloneValue.once
|
||||
} else {
|
||||
delete cloneValue.period
|
||||
}
|
||||
emit('update:value', cloneValue)
|
||||
}, { deep: true })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
export const numberToString = {
|
||||
1: '星期一',
|
||||
2: '星期二',
|
||||
3: '星期三',
|
||||
4: '星期四',
|
||||
5: '星期五',
|
||||
6: '星期六',
|
||||
7: '星期日',
|
||||
};
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div :class='classNames'>
|
||||
<div
|
||||
v-for='item in options'
|
||||
:key='item.value'
|
||||
:class='[
|
||||
"trigger-way-item",
|
||||
value === item.value ? "active" : "",
|
||||
labelBottom ? "label-bottom" : ""
|
||||
]'
|
||||
@click='() => click(item.value)'
|
||||
>
|
||||
<div class='way-item-title'>
|
||||
<span class='label'>{{ item.label }}</span>
|
||||
<a-popover v-if='item.tip' :content='item.tip'>
|
||||
<AIcon type='QuestionCircleOutlined' class='icon' />
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class='way-item-image'>
|
||||
<img
|
||||
width='48'
|
||||
v-bind='item.imgProps'
|
||||
:src='item.img'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts' name='TopCard'>
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type optionsType = {
|
||||
label: string
|
||||
value: string
|
||||
img?: string
|
||||
tip?: string
|
||||
imgProps: Record<string, any>
|
||||
}
|
||||
|
||||
type Emit = {
|
||||
(e: 'update:value', data: string): void
|
||||
(e: 'select', data: string): void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array as PropType<optionsType[]>,
|
||||
default: () => ([])
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
labelBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const classNames = computed(() => {
|
||||
return [
|
||||
props.class,
|
||||
'trigger-way-warp',
|
||||
props.disabled ? 'disabled' : ''
|
||||
]
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const click = (value: string) => {
|
||||
emit('update:value', value)
|
||||
emit('select', value)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang='less'>
|
||||
.trigger-way-warp {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 24px;
|
||||
width: 100%;
|
||||
|
||||
.trigger-way-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 237px;
|
||||
//width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e4e8;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.way-item-title {
|
||||
span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.label {
|
||||
padding-right: 6px;
|
||||
color: rgba(#000, 0.64);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: rgba(#000, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.way-item-image {
|
||||
margin: 0 !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//color: @primary-color-hover;
|
||||
.way-item-image {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: @primary-color-active;
|
||||
.way-item-image {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.label-bottom {
|
||||
flex-direction: column-reverse;
|
||||
grid-gap: 16px;
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
padding: 8px 16px;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.trigger-way-item {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: initial;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export { default as Timer } from './Timer'
|
||||
export { default as TopCard } from './TopCard.vue'
|
||||
export { default as TriggerWay } from './TriggerWay.vue'
|
|
@ -9,6 +9,11 @@ type State = {
|
|||
text: string;
|
||||
};
|
||||
|
||||
export type optionItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type Action = {
|
||||
executor: string;
|
||||
configuration: Record<string, unknown>;
|
||||
|
@ -88,7 +93,7 @@ export enum ActionAlarmMode {
|
|||
export interface OperationTimerPeriod {
|
||||
from: string;
|
||||
to: string;
|
||||
every: string[];
|
||||
every: number;
|
||||
unit: keyof typeof TimeUnit;
|
||||
}
|
||||
|
||||
|
@ -311,3 +316,9 @@ export interface FormModelType {
|
|||
options?: Record<string, any>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type metadataType = {
|
||||
properties?: any[]
|
||||
functions?: any[]
|
||||
events?: any[]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="form-label-container">
|
||||
<span class="text">{{ props.text }}</span>
|
||||
<span class="required">*</span>
|
||||
<span class="required" v-show="props.required">*</span>
|
||||
<a-tooltip>
|
||||
<template #title>{{ props.tooltip }}</template>
|
||||
<AIcon type="QuestionCircleOutlined" style="color: #00000073;cursor: inherit;" />
|
||||
<AIcon type="QuestionCircleOutlined" class="icon" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -24,11 +24,15 @@ const props = defineProps<{
|
|||
|
||||
.required {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
font-family: SimSun, sans-serif;
|
||||
line-height: 1;
|
||||
}
|
||||
.icon {
|
||||
color: #00000073;
|
||||
cursor: inherit;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div class="request-table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'key'">
|
||||
<a-input v-model:value="record.label" />
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'value'">
|
||||
<a-input
|
||||
v-model:value="record.value"
|
||||
v-if="props.valueType === 'input'"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="props.valueType === 'select'"
|
||||
v-model:value="record.value"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in props.valueOptions"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-button
|
||||
type="link"
|
||||
@click="removeRow((current - 1) * 10 + index)"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-pagination
|
||||
v-show="props.value.length > 10"
|
||||
v-model:current="current"
|
||||
:page-size="10"
|
||||
:total="props.value.length"
|
||||
show-less-items
|
||||
/>
|
||||
<a-button type="dashed" @click="addRow" class="add-btn">
|
||||
<AIcon type="PlusOutlined" />新增
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { optionsType } from '../typing';
|
||||
|
||||
const emits = defineEmits(['update:value']);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: optionsType;
|
||||
valueType?: 'input' | 'select';
|
||||
valueOptions?: optionsType;
|
||||
}>(),
|
||||
{
|
||||
valueType: 'input',
|
||||
},
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
title: 'KEY',
|
||||
dataIndex: 'key',
|
||||
width: '40%'
|
||||
},
|
||||
{
|
||||
title: 'VALUE',
|
||||
dataIndex: 'value',
|
||||
width: '40%'
|
||||
},
|
||||
{
|
||||
title: ' ',
|
||||
dataIndex: 'action',
|
||||
width: '20%'
|
||||
},
|
||||
];
|
||||
|
||||
const current = ref<number>(1);
|
||||
|
||||
const tableData = computed(() => {
|
||||
return props.value.slice((current.value - 1) * 10, current.value * 10);
|
||||
});
|
||||
|
||||
if(props.value.length < 1) addRow()
|
||||
watch(
|
||||
() => props.value,
|
||||
(n, o) => {
|
||||
if (!o || n.length === o.length) return;
|
||||
// 如果是新增行操作
|
||||
else if (n.length > o.length) {
|
||||
// 若新增后会出现新一页,则跳转到最新的一页
|
||||
if (o.length % 10 === 0 && n.length > 10)
|
||||
current.value = current.value + 1;
|
||||
} else {
|
||||
// 如果是删除行操作
|
||||
// 若删除的行是本页的最后一行,且本页不是第一页,则跳转到上一页
|
||||
if (o.length % 10 === 1 && o.length > 10)
|
||||
current.value = current.value - 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
function removeRow(index: number) {
|
||||
const left = props.value.slice(0, index++);
|
||||
const right = props.value.slice(index, props.value.length);
|
||||
emits('update:value', [...left, ...right]);
|
||||
}
|
||||
function addRow() {
|
||||
const newRow = {
|
||||
label: '',
|
||||
value: '',
|
||||
};
|
||||
console.log(111);
|
||||
|
||||
emits('update:value', [...props.value, newRow]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.request-table-container {
|
||||
width: 100%;
|
||||
:deep(.ant-btn-link) {
|
||||
color: #000000d9;
|
||||
|
||||
&:hover {
|
||||
color: #1d39c4;
|
||||
}
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -3,351 +3,9 @@
|
|||
<a-card class="save-container">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="14">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form.data"
|
||||
layout="vertical"
|
||||
class="form"
|
||||
>
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input
|
||||
v-model:value="form.data.name"
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="应用" name="provider">
|
||||
<a-radio-group
|
||||
v-model:value="form.data.provider"
|
||||
class="radio-container"
|
||||
@change="form.data.integrationModes = []"
|
||||
>
|
||||
<a-radio-button value="internal-standalone">
|
||||
<div>
|
||||
<a-image
|
||||
:preview="false"
|
||||
:src="
|
||||
getImage('/apply/provider1.png')
|
||||
"
|
||||
width="64px"
|
||||
height="64px"
|
||||
/>
|
||||
<p>内部独立应用</p>
|
||||
</div>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="internal-integrated">
|
||||
<div>
|
||||
<a-image
|
||||
:preview="false"
|
||||
:src="
|
||||
getImage('/apply/provider2.png')
|
||||
"
|
||||
/>
|
||||
<p>内部集成应用</p>
|
||||
</div>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="wechat-webapp">
|
||||
<div>
|
||||
<a-image
|
||||
:preview="false"
|
||||
:src="
|
||||
getImage('/apply/provider3.png')
|
||||
"
|
||||
/>
|
||||
<p>微信网站应用</p>
|
||||
</div>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="dingtalk-ent-app">
|
||||
<div>
|
||||
<a-image
|
||||
:preview="false"
|
||||
:src="
|
||||
getImage('/apply/provider4.png')
|
||||
"
|
||||
/>
|
||||
<p>钉钉企业内部应用</p>
|
||||
</div>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="third-party">
|
||||
<div>
|
||||
<a-image
|
||||
:preview="false"
|
||||
:src="
|
||||
getImage('/apply/provider5.png')
|
||||
"
|
||||
/>
|
||||
<p>第三方应用</p>
|
||||
</div>
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="接入方式" name="integrationModes">
|
||||
<a-checkbox-group
|
||||
v-model:value="form.data.integrationModes"
|
||||
:options="joinOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-collapse
|
||||
v-model:activeKey="form.integrationModesISO"
|
||||
>
|
||||
<a-collapse-panel
|
||||
key="page"
|
||||
v-show="
|
||||
form.data.integrationModes.includes('page')
|
||||
"
|
||||
header="页面集成"
|
||||
>
|
||||
<a-form-item
|
||||
:name="['page', 'baseUrl']"
|
||||
class="resetLabel"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="接入地址"
|
||||
required
|
||||
tooltip="填写访问其它平台的地址"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.data.page.baseUrl"
|
||||
placeholder="请输入接入地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="路由方式"
|
||||
:name="['page', 'routeType']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.data.page.routeType"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="hash"
|
||||
>hash</a-select-option
|
||||
>
|
||||
<a-select-option value="history"
|
||||
>history</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel
|
||||
key="apiClient"
|
||||
v-show="
|
||||
form.data.integrationModes.includes(
|
||||
'apiClient',
|
||||
)
|
||||
"
|
||||
header="API客户端"
|
||||
>
|
||||
<a-form-item
|
||||
class="resetLabel"
|
||||
:name="['apiClient', 'baseUrl']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="接口地址"
|
||||
required
|
||||
tooltip="访问Api服务的地址"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.baseUrl
|
||||
"
|
||||
placeholder="请输入接入地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="resetLabel"
|
||||
:name="[
|
||||
'apiClient',
|
||||
'authConfig',
|
||||
'oauth2',
|
||||
'authorizationUrl',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="授权地址"
|
||||
required
|
||||
tooltip="认证授权地址"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.authConfig
|
||||
.oauth2.authorizationUrl
|
||||
"
|
||||
placeholder="请输入授权地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="resetLabel"
|
||||
:name="[
|
||||
'apiClient',
|
||||
'authConfig',
|
||||
'oauth2',
|
||||
'tokenUrl',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="token地址"
|
||||
required
|
||||
tooltip="设置token令牌的地址"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.authConfig
|
||||
.oauth2.tokenUrl
|
||||
"
|
||||
placeholder="请输入token地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="回调地址"
|
||||
:name="[
|
||||
'apiClient',
|
||||
'authConfig',
|
||||
'oauth2',
|
||||
'redirectUri',
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="回调地址"
|
||||
tooltip="授权完成后跳转到具体页面的回调地址"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.authConfig
|
||||
.oauth2.redirectUri
|
||||
"
|
||||
placeholder="请输入回调地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="resetLabel"
|
||||
:name="[
|
||||
'apiClient',
|
||||
'authConfig',
|
||||
'oauth2',
|
||||
'clientId',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="appId"
|
||||
required
|
||||
tooltip="第三方应用唯一标识"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.authConfig
|
||||
.oauth2.clientId
|
||||
"
|
||||
placeholder="请输入appId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="resetLabel"
|
||||
:name="[
|
||||
'apiClient',
|
||||
'authConfig',
|
||||
'oauth2',
|
||||
'clientSecret',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #label>
|
||||
<FormLabel
|
||||
text="appKey"
|
||||
required
|
||||
tooltip="第三方应用唯一标识的密钥"
|
||||
/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="
|
||||
form.data.apiClient.authConfig
|
||||
.oauth2.clientSecret
|
||||
"
|
||||
placeholder="请输入appKey"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="请求头"> </a-form-item>
|
||||
<a-form-item label="参数"> </a-form-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel
|
||||
key="apiServer"
|
||||
v-show="
|
||||
form.data.integrationModes.includes(
|
||||
'apiServer',
|
||||
)
|
||||
"
|
||||
header="API服务"
|
||||
>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel
|
||||
key="ssoClient"
|
||||
v-show="
|
||||
form.data.integrationModes.includes(
|
||||
'ssoClient',
|
||||
)
|
||||
"
|
||||
header="单点登录"
|
||||
>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-form-item label="说明" name="description">
|
||||
<a-textarea
|
||||
v-model:value="form.data.description"
|
||||
placeholder="请输入说明"
|
||||
showCount
|
||||
:maxlength="200"
|
||||
:rows="5"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button v-if="!routeQuery.view">保存</a-button>
|
||||
<EditForm @change-apply-type="chengeType" />
|
||||
</a-col>
|
||||
<a-col :span="10"><Does :type="form.data.provider" /></a-col>
|
||||
<a-col :span="10"><Does :type="rightType" /></a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</page-container>
|
||||
|
@ -355,151 +13,11 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import Does from './components/Does.vue';
|
||||
import FormLabel from './components/FormLabel.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import type { applyType, formType } from './typing';
|
||||
const routeQuery = useRoute().query;
|
||||
import EditForm from './components/EditForm.vue';
|
||||
import type { applyType } from './typing';
|
||||
|
||||
const initForm: formType = {
|
||||
name: '',
|
||||
provider: 'internal-standalone',
|
||||
integrationModes: [],
|
||||
config: '',
|
||||
description: '',
|
||||
page: {
|
||||
baseUrl: '',
|
||||
routeType: 'hash',
|
||||
},
|
||||
apiClient: {
|
||||
baseUrl: '',
|
||||
authConfig: {
|
||||
type: '',
|
||||
oauth2: {
|
||||
authorizationUrl: '',
|
||||
tokenUrl: '',
|
||||
redirectUri: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
const rightType = ref<applyType>('internal-standalone');
|
||||
const chengeType = (newType: applyType) => {
|
||||
rightType.value = newType;
|
||||
};
|
||||
const form = reactive({
|
||||
data: { ...initForm },
|
||||
integrationModesISO: [] as string[],
|
||||
});
|
||||
const joinOptions = computed(() => {
|
||||
if (form.data.provider === 'internal-standalone')
|
||||
return [
|
||||
{
|
||||
label: '页面集成',
|
||||
value: 'page',
|
||||
},
|
||||
{
|
||||
label: 'API客户端',
|
||||
value: 'apiClient',
|
||||
},
|
||||
{
|
||||
label: 'API服务',
|
||||
value: 'apiServer',
|
||||
},
|
||||
{
|
||||
label: '单点登录',
|
||||
value: 'ssoClient',
|
||||
},
|
||||
];
|
||||
else if (form.data.provider === 'internal-integrated')
|
||||
return [
|
||||
{
|
||||
label: '页面集成',
|
||||
value: 'page',
|
||||
},
|
||||
{
|
||||
label: 'API客户端',
|
||||
value: 'apiClient',
|
||||
},
|
||||
];
|
||||
else if (form.data.provider === 'wechat-webapp')
|
||||
return [
|
||||
{
|
||||
label: '单点登录',
|
||||
value: 'ssoClient',
|
||||
},
|
||||
];
|
||||
else if (form.data.provider === 'dingtalk-ent-app')
|
||||
return [
|
||||
{
|
||||
label: '单点登录',
|
||||
value: 'ssoClient',
|
||||
},
|
||||
];
|
||||
else if (form.data.provider === 'third-party')
|
||||
return [
|
||||
{
|
||||
label: '页面集成',
|
||||
value: 'page',
|
||||
},
|
||||
{
|
||||
label: 'API客户端',
|
||||
value: 'apiClient',
|
||||
},
|
||||
{
|
||||
label: 'API服务',
|
||||
value: 'apiServer',
|
||||
},
|
||||
{
|
||||
label: '单点登录',
|
||||
value: 'ssoClient',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.save-container {
|
||||
.form {
|
||||
.ant-form-item {
|
||||
&.resetLabel {
|
||||
:deep(.ant-form-item-required) {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.radio-container {
|
||||
.ant-radio-button-wrapper {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
padding: 0 15px;
|
||||
box-sizing: content-box;
|
||||
margin-right: 20px;
|
||||
|
||||
> :last-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
:deep(.ant-image) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,28 +3,93 @@ export type applyType = 'internal-standalone'
|
|||
| 'internal-integrated'
|
||||
| 'dingtalk-ent-app'
|
||||
| 'third-party'
|
||||
export type dictType = {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: dictType
|
||||
}[];
|
||||
|
||||
export type optionsType = {
|
||||
label: string,
|
||||
value: string;
|
||||
disabled?: boolean
|
||||
}[]
|
||||
export type formType = {
|
||||
id?:string,
|
||||
name: string;
|
||||
provider: applyType;
|
||||
integrationModes: string[];
|
||||
config: string;
|
||||
config?: string;
|
||||
description: string;
|
||||
page: {
|
||||
baseUrl:string,
|
||||
routeType:'hash' | 'history'
|
||||
},
|
||||
apiClient: {
|
||||
page: { // 页面集成
|
||||
baseUrl: string,
|
||||
authConfig: {
|
||||
type:string,
|
||||
oauth2 :{
|
||||
authorizationUrl:string,
|
||||
tokenUrl: string,
|
||||
redirectUri:string,
|
||||
clientId:string,
|
||||
clientSecret:string
|
||||
routeType: 'hash' | 'history',
|
||||
parameters: optionsType
|
||||
},
|
||||
apiClient: { // API客户端
|
||||
baseUrl: string, // 接口地址
|
||||
headers: optionsType, // 请求头
|
||||
parameters: optionsType, // 请求参数
|
||||
authConfig: { // 认证配置
|
||||
type: 'none' | 'bearer' | 'oauth2' | 'basic' | 'other', // 类型, 可选值:none, bearer, oauth2, basic, other
|
||||
bearer: { token: string }, // 授权信息
|
||||
basic: { username: string, password: string }, // 基本信息
|
||||
token: string,
|
||||
oauth2: { // OAuth2信息
|
||||
authorizationUrl: string, // 授权地址
|
||||
tokenUrl: string, // token地址
|
||||
redirectUri: string, // 重定向地址
|
||||
clientId: string, // 客户端ID
|
||||
clientSecret: string, // 客户端密钥
|
||||
grantType: 'authorization_code' | 'client_credentials' | '', // 类型
|
||||
accessTokenProperty: string, // token属性名
|
||||
tokenRequestType: 'POST_URI' | 'POST_BODY' | '' // token请求方式, 可选值:POST_URI,POST_BODY
|
||||
}
|
||||
}
|
||||
},
|
||||
apiServer: { // API服务
|
||||
appId: string,
|
||||
secureKey: string, // 密钥
|
||||
redirectUri: string, // 重定向URL
|
||||
roleIdList: string[], // 角色列表
|
||||
orgIdList: string[], // 部门列表
|
||||
ipWhiteList: string, // IP白名单
|
||||
signature?: 'MD5' | 'SHA256' | '', // 签名方式, 可选值:MD5,SHA256
|
||||
enableOAuth2: boolean, // 是否启用OAuth2
|
||||
},
|
||||
sso: { // 统一单点登陆集成
|
||||
configuration: { // 配置
|
||||
oauth2: { // Oauth2单点登录配置
|
||||
type?: string, // 认证方式
|
||||
authorizationUrl: string, // 授权地址
|
||||
redirectUri: string, // 重定向地址
|
||||
clientId: string, // 客户端ID
|
||||
clientSecret: string, // 客户端密钥
|
||||
userInfoUrl: string, // 用户信息接口
|
||||
scope: string, // scope
|
||||
logoUrl?:string, // logo
|
||||
userProperty: { // 用户属性字段信息
|
||||
userId: string, // 用户ID
|
||||
username: string, // 用户名
|
||||
name: string, // 名称
|
||||
avatar: string, // 头像
|
||||
email: string, // 邮箱
|
||||
telephone: string, // 电话
|
||||
description: string, // 说明
|
||||
},
|
||||
grantType: 'authorization_code' | 'client_credentials' | '', // 类型
|
||||
tokenUrl: string, // token地址
|
||||
accessTokenProperty: string, // token属性名
|
||||
tokenRequestType: 'POST_URI' | 'POST_BODY' | '', // token请求方式
|
||||
},
|
||||
appId: string, // 微信单点登录配置
|
||||
appKey: string, // 钉钉单点登录配置
|
||||
appSecret: string, // 钉钉、微信单点登录配置
|
||||
},
|
||||
autoCreateUser: boolean, // 是否自动创建平台用户
|
||||
usernamePrefix: string, // 用户ID前缀
|
||||
roleIdList: string[], // 自动创建平台用户时角色列表
|
||||
orgIdList: string[], // 自动创建平台用户时部门列表
|
||||
defaultPasswd: string, // 默认密码
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<a-modal
|
||||
v-model:visible="dialog.visible"
|
||||
title="集成菜单"
|
||||
width="600px"
|
||||
@ok="dialog.handleOk"
|
||||
@cancel="dialog.cancel"
|
||||
class="edit-dialog-container"
|
||||
:confirmLoading="dialog.loading"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.checkedSystem"
|
||||
@change="(value) => value && getTree(value as string)"
|
||||
style="width: 200px"
|
||||
placeholder="请选择集成系统"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in form.systemList"
|
||||
:value="item.value"
|
||||
>{{ item.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
|
||||
<p style="margin: 20px 0 0 0" v-show="form.menuTree.length > 0">当前集成菜单</p>
|
||||
<a-tree
|
||||
v-model:checkedKeys="form.checkedMenu"
|
||||
v-model:expandedKeys="form.expandedKeys"
|
||||
checkable
|
||||
:tree-data="form.menuTree"
|
||||
:fieldNames="{ key: 'id', title: 'name' }"
|
||||
@check="treeCheck"
|
||||
>
|
||||
<template #title="{ name }">
|
||||
<span>{{ name }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { optionItemType } from '@/views/system/DataSource/typing';
|
||||
import { applyType } from '../Save/typing';
|
||||
import {
|
||||
getOwner_api,
|
||||
getOwnerStandalone_api,
|
||||
getOwnerTree_api,
|
||||
getOwnerTreeStandalone_api,
|
||||
saveOwnerMenu_api,
|
||||
} from '@/api/system/apply';
|
||||
import { CheckInfo } from 'ant-design-vue/lib/vc-tree/props';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getMenuTree_api } from '@/api/system/menu';
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'add' | 'edit';
|
||||
}>();
|
||||
// 弹窗相关
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
|
||||
handleOk: () => {
|
||||
const items = filterTree(form.menuTree, [
|
||||
...form.checkedMenu,
|
||||
...form.half,
|
||||
]);
|
||||
if (form.checkedSystem) {
|
||||
if (items && items.length !== 0) {
|
||||
saveOwnerMenu_api('iot', form.id, items).then((resp) => {
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功');
|
||||
dialog.visible = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
message.warning('请勾选配置菜单');
|
||||
}
|
||||
} else {
|
||||
message.warning('请选择所属系统');
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
if (props.mode === 'add')
|
||||
menuStory.jumpPage('system/Apply/Save', {}, { id: form.id });
|
||||
dialog.visible = false;
|
||||
},
|
||||
changeVisible: (id: string, provider: applyType) => {
|
||||
form.id = id;
|
||||
form.provider = provider;
|
||||
form.checkedSystem = undefined;
|
||||
form.checkedMenu = [];
|
||||
dialog.visible = true;
|
||||
|
||||
if (id) {
|
||||
getSystemList();
|
||||
getMenus();
|
||||
}
|
||||
},
|
||||
});
|
||||
// 将打开弹窗的操作暴露给父组件
|
||||
defineExpose({
|
||||
openDialog: dialog.changeVisible,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
id: '',
|
||||
checkedSystem: '' as undefined | string,
|
||||
checkedMenu: [] as string[],
|
||||
expandedKeys: [] as string[],
|
||||
half: [] as string[],
|
||||
|
||||
provider: '' as applyType,
|
||||
systemList: [] as optionItemType[],
|
||||
menuTree: [] as any[],
|
||||
});
|
||||
/**
|
||||
* 与集成系统关联的菜单
|
||||
* @param params
|
||||
*/
|
||||
function getTree(params: string) {
|
||||
const api =
|
||||
form.provider === 'internal-standalone'
|
||||
? getOwnerTreeStandalone_api(form.id, params)
|
||||
: getOwnerTree_api(params);
|
||||
api.then((resp: any) => {
|
||||
form.menuTree = resp.result;
|
||||
form.expandedKeys = resp.result.map((item: any) => item.id);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取当前用户可访问菜单
|
||||
*/
|
||||
function getMenus() {
|
||||
const params = {
|
||||
terms: [
|
||||
{
|
||||
column: 'appId',
|
||||
value: form.id,
|
||||
},
|
||||
],
|
||||
};
|
||||
getMenuTree_api(params).then((resp: any) => {
|
||||
if (resp.status === 200) {
|
||||
form.menuTree = resp.result;
|
||||
const keys = resp.result.map((item: any) => item.id) as string[];
|
||||
form.expandedKeys = keys;
|
||||
form.checkedMenu = keys;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取集成系统选项
|
||||
*/
|
||||
function getSystemList() {
|
||||
const api =
|
||||
form.provider === 'internal-standalone'
|
||||
? getOwnerStandalone_api(form.id, ['iot'])
|
||||
: getOwner_api(['iot']);
|
||||
|
||||
api.then((resp: any) => {
|
||||
if (resp.status === 200) {
|
||||
form.systemList = resp.result.map((item: string) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
// 树选中事件
|
||||
function treeCheck(checkedKeys: any, e: CheckInfo) {
|
||||
form.checkedMenu = checkedKeys;
|
||||
form.half = e.halfCheckedKeys as string[];
|
||||
}
|
||||
//过滤节点-默认带上父节点
|
||||
function filterTree(nodes: any[], list: any[]) {
|
||||
if (!nodes?.length) {
|
||||
return nodes;
|
||||
}
|
||||
return nodes.filter((it) => {
|
||||
// 不符合条件的直接砍掉
|
||||
if (list.indexOf(it.id) <= -1) {
|
||||
return false;
|
||||
}
|
||||
// 符合条件的保留,并且需要递归处理其子节点
|
||||
it.children = filterTree(it.children, list);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -42,6 +42,7 @@
|
|||
enabled: 'success',
|
||||
disabled: 'error',
|
||||
}"
|
||||
hasMark
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
|
@ -118,6 +119,14 @@
|
|||
</PermissionButton>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<template #mark>
|
||||
<AIcon
|
||||
type="EyeOutlined"
|
||||
style="font-size: 24px"
|
||||
@click="() => table.toSave(slotProps.id, true)"
|
||||
/>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
||||
|
@ -151,11 +160,15 @@
|
|||
</template>
|
||||
</JTable>
|
||||
</div>
|
||||
<div class="dialogs">
|
||||
<MenuDialog ref="dialogRef" mode="edit" />
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Apply">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import MenuDialog from './componenets/MenuDialog.vue';
|
||||
import {
|
||||
getApplyList_api,
|
||||
changeApplyStatus_api,
|
||||
|
@ -251,9 +264,10 @@ const columns = [
|
|||
},
|
||||
];
|
||||
const params = ref({});
|
||||
const search = (newParams: any) => (params.value = {...newParams});
|
||||
const search = (newParams: any) => (params.value = { ...newParams });
|
||||
|
||||
const tableRef = ref();
|
||||
const dialogRef = ref();
|
||||
const table = {
|
||||
refresh: () => {
|
||||
tableRef.value.reload();
|
||||
|
@ -344,7 +358,10 @@ const table = {
|
|||
title: '集成菜单',
|
||||
},
|
||||
icon: 'MenuUnfoldOutlined',
|
||||
onClick: () => {},
|
||||
onClick: () => {
|
||||
dialogRef.value &&
|
||||
dialogRef.value.openDialog(data.id, data.provider);
|
||||
},
|
||||
});
|
||||
// 有api操作权限
|
||||
if (otherServers.includes('apiServer'))
|
||||
|
|
Loading…
Reference in New Issue