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

This commit is contained in:
wangshuaiswim 2023-02-27 19:24:43 +08:00
commit 76dfb3a797
98 changed files with 8298 additions and 2486 deletions

View File

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

View File

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

View File

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

15
src/api/media/channel.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,13 @@ const iconKeys = [
'FileTextOutlined',
'UploadOutlined',
'LikeOutlined',
'ArrowLeftOutlined'
'ArrowLeftOutlined',
'DownloadOutlined',
'PauseOutlined',
'ControlOutlined',
'RedoOutlined',
'VideoCameraOutlined',
'HistoryOutlined',
]
const Icon = (props: {type: string}) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '请输入签名' },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<template>
<div class="value">
<div v-if="value?.formatValue !== 0 && !value?.formatValue" :class="valueClass">--</div>
<div v-else-if="data?.valueType?.type === 'file'">
<div v-else-if="_data.data?.valueType?.type === 'file'">
<template v-if="data?.valueType?.fileType === 'base64'">
<div :class="valueClass" v-if="!!getType(value?.formatValue)">
<img :src="imgMap.get(_type)" @error="onError" />
@ -36,10 +36,10 @@
</template>
</template>
</div>
<div v-else-if="data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
<div v-else-if="_data.data?.valueType?.type === 'object'" @click="getDetail('obj')" :class="valueClass">
<img :src="imgMap.get('obj')" />
</div>
<div v-else-if="data?.valueType?.type === 'geoPoint' || data?.valueType?.type === 'array'" :class="valueClass">
<div v-else-if="_data.data?.valueType?.type === 'geoPoint' || _data.data?.valueType?.type === 'array'" :class="valueClass">
{{JSON.stringify(value?.formatValue)}}
</div>
<div v-else :class="valueClass">
@ -53,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>

View File

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

View File

@ -32,20 +32,30 @@
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-button
style="padding: 0"
type="link"
<template v-for="i in getActions(slotProps)" :key="i.key">
<a-tooltip v-bind="i.tooltip" v-if="i.key !== 'edit'">
<a-button
style="padding: 0"
type="link"
:disabled="i.disabled"
@click="i.onClick && i.onClick(slotProps)"
>
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
<PermissionButton
:disabled="i.disabled"
v-else
:popConfirm="i.popConfirm"
:tooltip="i.tooltip"
@click="i.onClick && i.onClick(slotProps)"
type="link"
style="padding: 0px"
:hasPermission="'device/Instance:update'"
>
<AIcon :type="i.icon" />
</a-button>
</a-tooltip>
<template #icon><AIcon :type="i.icon" /></template>
</PermissionButton>
</template>
</a-space>
</template>
<template #paginationRender>
@ -76,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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<template>
<page-container> 回放 </page-container>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -106,7 +106,8 @@
<a-form-item label="收信部门">
<ToOrg
v-model:toParty="
formData.template.toParty
formData.template
.departmentIdList
"
:type="formData.type"
:config-id="formData.configId"
@ -132,7 +133,7 @@
</template>
<ToUser
v-model:toUser="
formData.template.toUser
formData.template.userIdList
"
:type="formData.type"
:config-id="formData.configId"
@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

@ -0,0 +1,3 @@
import Timer from './index.vue'
export default Timer

View File

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

View File

@ -0,0 +1,9 @@
export const numberToString = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
7: '星期日',
};

View File

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

View File

@ -0,0 +1,3 @@
export { default as Timer } from './Timer'
export { default as TopCard } from './TopCard.vue'
export { default as TriggerWay } from './TriggerWay.vue'

View File

@ -9,6 +9,11 @@ type State = {
text: string;
};
export type optionItem = {
label: string
value: string
}
type Action = {
executor: string;
configuration: Record<string, unknown>;
@ -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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

1555
yarn.lock

File diff suppressed because it is too large Load Diff