Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
xieyonghong 2023-02-24 18:22:56 +08:00
commit bba6ef3c30
94 changed files with 7212 additions and 2374 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"

BIN
public/images/apply/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/images/apply/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/images/apply/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
public/images/apply/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
public/images/apply/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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)

View File

@ -175,3 +175,13 @@ export const saveDevice = (data:any) => server.post('/device-product',data)
* ()
*/
export const updateDevice = (data:any) => server.patch('/device-product',data)
/**
*
*/
export const getOperator = () => server.get<OperatorItem>('/property-calculate-rule/description')
/**
*
*/
export const getStreamingAggType = () => server.get<Record<string, string>[]>('/dictionary/streaming-agg-type/items')

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

@ -0,0 +1,37 @@
import server from '@/utils/request';
/**
*
*/
export const queryList = (data:any) => server.post('/alarm/config/detail/_query',data);
/**
*
*/
export const _enable = (id:string) => server.post(`/alarm/config/${id}/_enable`);
/**
*
*/
export const _disable = (id:string) => server.post(`/alarm/config/${id}/_disable`);
/**
*
*/
export const remove = (id:string) => server.remove(`/alarm/config/${id}`);
/**
*
*/
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);

14
src/api/system/apply.ts Normal file
View File

@ -0,0 +1,14 @@
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 delApply_api = (id:string) => server.remove(`/application/${id}`)
// 获取组织列表
export const getDepartmentList_api = () => server.get(`/organization/_all/tree`);

View File

@ -52,7 +52,10 @@ const iconKeys = [
'RightOutlined',
'FileTextOutlined',
'UploadOutlined',
'ArrowLeftOutlined'
'LikeOutlined',
'ArrowLeftOutlined',
'DownloadOutlined',
'PauseOutlined'
]
const Icon = (props: {type: string}) => {

View File

@ -3,6 +3,7 @@
<div class="advance-box">
<div class="left">
<Editor
ref="editor"
mode="advance"
key="advance"
v-model:value="_value"
@ -16,7 +17,7 @@
/>
</div>
<div class="right">
<Operator :id="id" />
<Operator :id="id" @add-operator-value="addOperatorValue"/>
</div>
</div>
</a-modal>
@ -44,10 +45,15 @@ const handleCancel = () => {
emit('change', 'simple')
}
const handleOk = () => {
console.log(_value.value)
emit('update:value', _value.value)
emit('change', 'simple')
}
const editor = ref()
const addOperatorValue = (val: string) => {
editor.value.addOperatorValue(val)
}
</script>
<style lang="less" scoped>

View File

@ -3,7 +3,7 @@
<div class="top">
<div class="left">
<span v-for="item in symbolList.filter((t: SymbolType, i: number) => i <= 3)" :key="item.key"
@click="handleInsertCode(item.value)">
@click="addOperatorValue(item.value)">
{{ item.value }}
</span>
<span>
@ -12,7 +12,7 @@
<template #overlay>
<a-menu>
<a-menu-item v-for="item in symbolList.filter((t: SymbolType, i: number) => i > 6)" :key="item.key"
@click="handleInsertCode(item.value)">
@click="addOperatorValue(item.value)">
{{ item.value }}
</a-menu-item>
</a-menu>
@ -149,7 +149,7 @@ onMounted(() => {
}, 100);
})
const handleInsertCode = (val: string) => {
const addOperatorValue = (val: string) => {
editor.value?.insert(val)
}
@ -159,6 +159,10 @@ const fullscreenClick = () => {
}
}
defineExpose({
addOperatorValue
})
</script>
<style lang="less" scoped>
.editor-box {

View File

@ -1,51 +1,64 @@
<template>
<div class="operator-box">
<a-input-search @search="search" allow-clear placeholder="搜索关键字" />
<a-tree class="tree" @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
<template #title="node">
<div class="node">
<div>{{ node.name }}</div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<a-popover v-if="node.type === 'property'" placement="right" title="请选择使用值" @visibleChange="setVisible">
<template #content>
<a-space direction="vertical">
<a-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<a-button type="text" @click="recentClick(node)">
$recent实时值
</a-button>
</a-tooltip>
<a-tooltip placement="right" title="实时值的上一有效值">
<a-button @click="lastClick(node)" type="text">
上一值
</a-button>
</a-tooltip>
</a-space>
</template>
<a @click="setVisible(true)">添加</a>
</a-popover>
<div class="tree">
<a-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
<template #title="node">
<div class="node">
<div>{{ node.name }}</div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<a-popover v-if="node.type === 'property'" placement="right" title="请选择使用值">
<template #content>
<a-space direction="vertical">
<a-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<a-button type="text" @click="recentClick(node)">
$recent实时值
</a-button>
</a-tooltip>
<a-tooltip placement="right" title="实时值的上一有效值">
<a-button @click="lastClick(node)" type="text">
上一值
</a-button>
</a-tooltip>
</a-space>
</template>
<a>添加</a>
</a-popover>
<a v-else @click="addClick(node)">
添加
</a>
<a v-else @click="addClick(node)">
添加
</a>
</div>
</div>
</div>
</template>
</a-tree>
</template>
</a-tree>
</div>
<div class="explain">
<ReactMarkdown>{{ item?.description || '' }}</ReactMarkdown>
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
</template>
<script setup lang="ts" name="Operator">
import { useProductStore } from '@/store/product';
import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree'
import { Store } from 'jetlinks-store';
import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product'
import Markdown from 'vue3-markdown-it'
const props = defineProps({
id: String
})
interface Emits {
(e: 'addOperatorValue', data: string): void;
}
const emit = defineEmits<Emits>();
const item = ref<Partial<OperatorItem>>()
const data = ref<OperatorItem[]>([])
const dataRef = ref<OperatorItem[]>([])
const visible = ref(false)
const search = (value: string) => {
if (value) {
@ -60,22 +73,52 @@ const selectTree = (k: any, info: any) => {
item.value = info.node as unknown as OperatorItem;
}
const setVisible = (_visible: boolean) => {
visible.value = !!visible
}
const recentClick = (node: OperatorItem) => {
Store.set('add-operator-value', `$recent("${node.id}")`);
setVisible(!visible.value);
emit('addOperatorValue', `$recent("${node.id}")`)
}
const lastClick = (node: OperatorItem) => {
Store.set('add-operator-value', `$lastState("${node.id}")`);
setVisible(!visible.value);
emit('addOperatorValue', `$lastState("${node.id}")`)
}
const addClick = (node: OperatorItem) => {
Store.set('add-operator-value', node.code);
setVisible(true);
emit('addOperatorValue', node.code)
}
const productStore = useProductStore()
const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}';
console.log('metadata', metadata)
const _properties = JSON.parse(metadata).properties || [] as PropertyMetadata[]
const properties = {
id: 'property',
name: '属性',
description: '',
code: '',
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
id: p.id,
name: p.name,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `,
type: 'property',
})),
};
const response = await getOperator();
if (response.status === 200) {
data.value = [properties, ...response.result];
dataRef.value = [properties, ...response.result];
}
};
watch(() => props.id,
(val) => {
getData(val)
},
{ immediate: true }
)
</script>
<style lang="less" scoped>
.border {

View File

@ -1,7 +1,7 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
{{ ruleEditorStore.state.model }}
<Advance v-if="ruleEditorStore.state.model === 'advance'" :model="ruleEditorStore.state.model"
{{ _value }}
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
</template>
<script setup lang="ts" name="FRuleEditor">

View File

@ -87,7 +87,7 @@ const handleEdit = (index: number) => {
}
const handleDelete = (index: number) => {
editIndex.value = -1
_value.value.slice(index, 1)
_value.value.splice(index, 1)
}
const handleClose = () => {
editIndex.value = -1

View File

@ -1,11 +1,40 @@
<template>
<a-form-item :name="name.concat(['script'])">
<f-rule-editor v-model:value="value.script" :id="id" ></f-rule-editor>
<f-rule-editor v-model:value="value.script" :id="id"></f-rule-editor>
</a-form-item>
<template v-if="showWindow">
<a-form-item label="规则配置" :name="name.concat(['isVirtualRule'])">
<a-switch v-model:checked="value.isVirtualRule" :checked-value="true" :un-checked-value="false"
@change="changeWindow"></a-switch>
</a-form-item>
<template v-if="value.isVirtualRule">
<a-form-item label="窗口" :name="name.concat(['windowType'])" :rules="[
{ required: true, message: '请选择窗口' },
]">
<a-select v-model:value="value.windowType" :options="windowTypeEnum" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item label="聚合函数" :name="name.concat(['aggType'])" :rules="[
{ required: true, message: '请选择聚合函数' },
]">
<a-select v-model:value="value.aggType" :options="aggTypeOptions" size="small" allow-clear></a-select>
</a-form-item>
<a-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
{ required: true, message: '请输入窗口长度' },
]">
<a-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
<a-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
{ required: true, message: '请输入步长' },
]">
<a-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></a-input-number>
</a-form-item>
</template>
</template>
</template>
<script setup lang="ts" name="VirtualRuleParam">
import { PropType } from 'vue';
import FRuleEditor from '@/components/FRuleEditor/index.vue'
import { getStreamingAggType } from '@/api/device/product'
const props = defineProps({
value: {
@ -18,7 +47,11 @@ const props = defineProps({
type: Array as PropType<string[]>,
default: () => ([])
},
id: String
id: String,
showWindow: {
type: Boolean,
default: false
}
})
interface Emits {
@ -32,5 +65,54 @@ onMounted(() => {
type: 'script'
})
})
const aggTypeOptions = ref()
const getAggTypeList = async () => {
aggTypeOptions.value = await getStreamingAggType().then((resp) =>
resp.result.map((item: any) => ({
label: `${item.value}(${item.text})`,
value: item.value,
})),
);
}
getAggTypeList()
const changeWindow = (val: boolean | string | number) => {
if (val as boolean) {
props.value.type = 'window'
if (!props.value.window) {
props.value.window = {}
}
} else {
delete props.value.type
}
}
const windowTypeEnum = [
{ label: '时间窗口', value: 'time' },
{ label: '次数窗口', value: 'num' },
]
const spanLabel = computed(() => {
switch(props.value.windowType) {
case 'time':
return '窗口长度(秒)';
case 'num':
return '窗口长度(次)';
default:
return '窗口长度'
}
})
const everyLabel = computed(() => {
switch(props.value.windowType) {
case 'time':
return '步长(秒)';
case 'num':
return '步长(次)';
default:
return '步长'
}
})
</script>
<style lang="less" scoped></style>

View File

@ -90,6 +90,11 @@ const insert = (val) => {
]);
}
watch(() => props.value,
(val) => {
instance.setValue(val)
})
defineExpose({
editorFormat,
insert,

View File

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

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

10
src/utils/validate.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* +
* @param value
* @returns {boolean}
*/
export const phoneRegEx = (value: string) => {
const phone = new RegExp('^(((\\+86)|(\\+86-))|((86)|(86\\-))|((0086)|(0086\\-)))?1[3|5|7|8]\\d{9}$')
const mobile = /(0[0-9]{2,3})([2-9][0-9]{6,7})+([0-9]{8,11})?$/
return phone.test(value) || mobile.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,272 @@
<template lang="">
<a-input
placeholder="请选择设备"
v-model:value="checkLable"
:disabled="true"
>
<template #addonAfter>
<AIcon 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 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 = _selectedRowKeys.value
.map((item) => checkAllDataMap.has(item) && checkAllDataMap.get(item))
.toString();
emit('update:modelValue', _selectedRowKeys.value);
visible.value = false;
};
const onVisible = () => {
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;
});
}
},
);
});
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>
.form {
.form-submit {
background-color: @primary-color !important;
}
}
</style>

View File

@ -0,0 +1,277 @@
<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"
>
<a-row :gutter="[24, 0]">
<a-col :span="24">
<a-form-item label="任务名称" v-bind="validateInfos.name">
<a-input
placeholder="请输入任务名称"
v-model:value="formData.name"
/></a-form-item>
</a-col>
<a-col :span="24"
><a-form-item label="推送方式" v-bind="validateInfos.mode">
<a-select
v-model:value="formData.mode"
:options="[
{ label: '平台推送', value: 'push' },
{ label: '设备拉取', value: 'pull' },
]"
placeholder="请选择推送方式"
allowClear
show-search
:filter-option="filterOption"
@change="changeMode"
/> </a-form-item
></a-col>
<a-col :span="12" v-if="formData.mode === 'push'"
><a-form-item
label="响应超时时间"
v-bind="validateInfos.responseTimeoutSeconds"
>
<a-input-number
placeholder="请输入响应超时时间(秒)"
style="width: 100%"
:min="1"
:max="99999"
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="升级超时时间"
v-bind="validateInfos.timeoutSeconds"
>
<a-input-number
placeholder="请输入升级超时时间(秒)"
style="width: 100%"
:min="1"
:max="99999"
v-model:value="
formData.timeoutSeconds
" /></a-form-item
></a-col>
<a-col :span="12" v-if="!!formData.mode"
><a-form-item
label="升级设备"
v-bind="validateInfos.releaseType"
>
<a-radio-group
v-model:value="formData.releaseType"
button-style="solid"
@change="changeShareCluster"
>
<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="选择设备"
v-bind="validateInfos.deviceId"
>
<SelectDevices
v-model:modelValue="formData.deviceId"
:data="devicesData"
></SelectDevices> </a-form-item
></a-col>
<a-col :span="24">
<a-form-item
label="说明"
v-bind="validateInfos.description"
>
<a-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/> </a-form-item
></a-col>
</a-row>
</a-form>
</a-modal>
<!-- <SelectDevices v-if="visible" @change="saveChange"></SelectDevices> -->
</template>
<script lang="ts" setup name="TaskPage">
import { message, Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import { save, update, queryProduct } from '@/api/device/firmware';
import type { FormInstance } from 'ant-design-vue';
import type { Properties } from '../../type';
import SelectDevices from './SelectDevices.vue';
const route = useRoute();
console.log(111, route.query);
const loading = ref(false);
const useForm = Form.useForm;
const productOptions = ref([]);
const visible = ref(false);
const devicesData = ref()
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['change']);
const id = props.data.id;
const formData = ref({
name: '',
mode: undefined,
responseTimeoutSeconds: '',
timeoutSeconds: '',
releaseType: 'all',
deviceId: [],
description: '',
});
const extraValue = ref({});
const validatorSign = async (_: Record<string, any>, value: string) => {
// const { releaseType, url } = formData.value;
// if (value && !!releaseType && !!url && !extraValue.value) {
// return extraValue.value[releaseType] !== value
// ? Promise.reject('')
// : Promise.resolve();
// } else {
// return Promise.resolve();
// }
};
const { resetFields, validate, validateInfos } = useForm(
formData,
reactive({
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: '请选择设备' },
{ validator: validatorSign },
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),
);
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const changeMode = (value) => {
console.log(111, value, formData.value);
};
const onChange = () => {
visible.value = true;
};
const saveChange = () => {
visible.value = false;
};
const onSubmit = async () => {
validate()
.then(async (res) => {
// const product = productOptions.value.find(
// (item) => item.value === res.mode,
// );
// const productName = product.label || props.data?.url;
// const size = extraValue.value.length || props.data?.size;
// const params = {
// ...toRaw(formData.value),
// properties: !!properties ? properties : [],
// productName,
// size,
// };
// loading.value = true;
// const response = !id
// ? await save(params)
// : await update({ ...props.data, ...params });
// if (response.status === 200) {
// message.success('');
// emit('change', true);
// }
// loading.value = false;
})
.catch((err) => {
loading.value = false;
});
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const changeSignMethod = () => {
formData.value.deviceId = '';
formData.value.url = '';
};
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;
}
},
{ immediate: true, deep: true },
);
watch(
() => extraValue.value,
() => validate('deviceId'),
{ deep: true },
);
</script>
<style lang="less" scoped>
.form {
.form-submit {
background-color: @primary-color !important;
}
}
</style>

View File

@ -0,0 +1,256 @@
<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="CertificatePage">
import type { ActionsType } from '@/components/Table/index.vue';
import { task, startTask, stopTask } from '@/api/device/firmware';
import { message } from 'ant-design-vue';
import Save from './Save/index.vue'
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',
},
// scopedSlots: true,
},
{
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: 'edit',
text: '详情',
tooltip: {
title: '详情',
},
icon: 'EditOutlined',
onClick: async () => {
handlEdit(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: 'PauseOutlined',
});
}
return Actions;
};
const handlAdd = () => {
current.value = {};
visible.value = true;
};
const handlEye = (data: object) => {
current.value = toRaw(data);
visible.value = true;
};
const handlEdit = (id: string) => {
// router.push({
// path: `/iot/link/certificate/detail/${id}`,
// query: { view: false },
// });
};
const saveChange = (value: FormDataType) => {
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>
.a {
// display: none;
visibility: 0;
}
</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,169 +1,180 @@
<template>
<page-container>
<a-card class="device-product">
<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"
/>
</a-card>
><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>
@ -195,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>('');
@ -427,7 +438,7 @@ const beforeUpload = (file: any) => {
* 查看
*/
const handleView = (id: string) => {
menuStory.jumpPage('device/Product/Detail',{id})
menuStory.jumpPage('device/Product/Detail', { id });
};
/**
@ -645,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

@ -4,7 +4,7 @@
]">
<a-select v-model:value="_value.source" :options="PropertySource" size="small" :disabled="metadataStore.model.action === 'edit'"></a-select>
</a-form-item>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule" :name="name.concat(['virtualRule'])" :id="id"></virtual-rule-param>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule" :name="name.concat(['virtualRule'])" :id="id" :showWindow="_value.source === 'rule'"></virtual-rule-param>
<a-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择读写类型' },
]">

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

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

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

View File

@ -0,0 +1,174 @@
<template>
<page-container>
<Search
type="simple"
:columns="columns"
target="product"
@search="handleSearch"
/>
<JTable
ref="instanceRef"
:columns="columns"
:request="(e:any) => templateApi.getHistory(e, data.id)"
:defaultParams="{
sorts: [{ name: 'notifyTime', order: 'desc' }],
terms: [{ column: 'notifyType$IN', value: data.type }],
}"
:params="params"
model="table"
>
<template #notifyTime="slotProps">
{{ moment(slotProps.notifyTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
<template #state="slotProps">
<a-space>
<a-badge
:status="slotProps.state.value"
:text="slotProps.state.text"
></a-badge>
<AIcon
v-if="slotProps.state.value === 'error'"
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleError(slotProps.errorStack)"
/>
</a-space>
</template>
<template #action="slotProps">
<AIcon
type="ExclamationCircleOutlined"
style="color: #1d39c4; cursor: pointer"
@click="handleDetail(slotProps.context)"
/>
</template>
</JTable>
</page-container>
</template>
<script setup lang="ts">
import templateApi from '@/api/notice/template';
import { PropType } from 'vue';
import moment from 'moment';
import { Modal } from 'ant-design-vue';
type Emits = {
(e: 'update:visible', data: boolean): void;
};
// const emit = defineEmits<Emits>();
const props = defineProps({
visible: { type: Boolean, default: false },
data: {
type: Object as PropType<Partial<Record<string, any>>>,
default: () => ({}),
},
});
// const _vis = computed({
// get: () => props.visible,
// set: (val) => emit('update:visible', val),
// });
// watch(
// () => _vis.value,
// (val) => {
// if (val) handleSearch({ terms: [] });
// },
// );
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
search: {
type: 'string',
},
},
{
title: '发送时间',
dataIndex: 'notifyTime',
key: 'notifyTime',
scopedSlots: true,
search: {
type: 'date',
handleValue: (v: any) => {
return v;
},
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'error' },
],
handleValue: (v: any) => {
return v;
},
},
},
{
title: '操作',
key: 'action',
scopedSlots: true,
},
];
const params = ref<Record<string, any>>({});
/**
* 搜索
* @param params
*/
const handleSearch = (e: any) => {
// console.log('handleSearch e:', e);
params.value = e;
// console.log('params.value: ', params.value);
};
/**
* 查看错误信息
*/
const handleError = (e: any) => {
Modal.info({
title: '错误信息',
content: h(
'p',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
},
},
JSON.stringify(e),
),
});
};
/**
* 查看详情
*/
const handleDetail = (e: any) => {
Modal.info({
title: '详情信息',
content: h(
'p',
{
style: {
maxHeight: '300px',
overflowY: 'auto',
},
},
JSON.stringify(e),
),
});
};
</script>
<style lang="less" scoped></style>

View File

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

View File

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

View File

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

View File

@ -258,12 +258,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>
@ -334,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();
},
);
@ -422,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);
@ -445,7 +424,7 @@ const handleTypeChange = () => {
setTimeout(() => {
formData.value.configuration =
CONFIG_FIELD_MAP[formData.value.type][formData.value.provider];
// resetPublicFiles();
resetPublicFiles();
}, 0);
};
@ -455,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"
@ -157,6 +158,7 @@
formData.template.messageType
"
placeholder="请选择消息类型"
@change="handleMessageTypeChange"
>
<a-select-option
v-for="(
@ -480,7 +482,11 @@
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-form-item
v-bind="
validateInfos['template.calledShowNumbers']
"
>
<template #label>
<span>
被叫显号
@ -713,12 +719,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>
@ -757,6 +762,8 @@ import ToTag from './components/ToTag.vue';
import { FILE_UPLOAD } from '@/api/comm';
import { LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { phoneRegEx } from '@/utils/validate';
import type { Rule } from 'ant-design-vue/es/form';
const router = useRouter();
const route = useRoute();
@ -794,25 +801,59 @@ const formData = ref<TemplateFormData>({
});
/**
* 重置公用字段值
* 重置字段值
*/
const resetPublicFiles = () => {
formData.value.template.message = '';
formData.value.configId = undefined;
if (
formData.value.provider === 'dingTalkMessage' ||
formData.value.type === 'weixin'
) {
formData.value.template.toTag = undefined;
formData.value.template.toUser = undefined;
formData.value.template.agentId = undefined;
switch (formData.value.provider) {
case 'dingTalkMessage':
formData.value.template.agentId = '';
formData.value.template.message = '';
formData.value.template.departmentIdList = '';
formData.value.template.userIdList = '';
break;
case 'dingTalkRobotWebHook':
formData.value.template.message = '';
formData.value.template.messageType = 'markdown';
formData.value.template.markdown = { text: '', title: '' };
break;
case 'corpMessage':
formData.value.template.agentId = '';
formData.value.template.message = '';
formData.value.template.toParty = '';
formData.value.template.toUser = '';
formData.value.template.toTag = '';
break;
case 'embedded':
formData.value.template.subject = '';
formData.value.template.message = '';
formData.value.template.text = '';
formData.value.template.sendTo = [];
formData.value.template.attachments = [];
break;
case 'aliyun':
formData.value.template.templateType = 'tts';
formData.value.template.templateCode = '';
formData.value.template.ttsCode = '';
formData.value.template.message = '';
formData.value.template.playTimes = 1;
formData.value.template.calledShowNumbers = '';
formData.value.template.calledNumber = '';
break;
case 'aliyunSms':
formData.value.template.code = '';
formData.value.template.message = '';
formData.value.template.phoneNumber = '';
formData.value.template.signName = '';
break;
case 'http':
formData.value.template.contextAsBody = true;
formData.value.template.body = '';
break;
}
if (formData.value.type === 'weixin')
formData.value.template.toParty = undefined;
if (formData.value.type === 'email')
formData.value.template.toParty = undefined;
// formData.value.description = '';
formData.value.configId = undefined;
formData.value.variableDefinitions = [];
handleMessageTypeChange();
};
//
@ -824,15 +865,8 @@ watch(
route.params.id !== ':id'
? formData.value.provider
: msgType.value[0].value;
// formData.value.provider = formData.value.provider || msgType.value[0].value;
// console.log('formData.value.template: ', formData.value.template);
// formData.value.template =
// TEMPLATE_FIELD_MAP[val][formData.value.provider];
if (val !== 'email') getConfigList();
// clearValid();
// console.log('formData.value: ', formData.value);
if (val === 'sms') {
getTemplateList();
@ -841,15 +875,6 @@ watch(
},
);
// watch(
// () => formData.value.provider,
// (val) => {
// formData.value.template = TEMPLATE_FIELD_MAP[formData.value.type][val];
// clearValid();
// },
// );
//
const formRules = ref({
type: [{ required: true, message: '请选择通知方式' }],
@ -862,7 +887,14 @@ const formRules = ref({
//
'template.agentId': [{ required: true, message: '请输入agentId' }],
'template.messageType': [{ required: true, message: '请选择消息类型' }],
'template.markdown.title': [{ required: true, message: '请输入标题' }],
'template.markdown.title': [
{ required: true, message: '请输入标题' },
{ max: 64, message: '最多可输入64个字符' },
],
'template.link.title': [
{ required: true, message: '请输入标题' },
{ max: 64, message: '最多可输入64个字符' },
],
// 'template.url': [{ required: true, message: 'WebHook' }],
//
// 'template.agentId': [{ required: true, message: 'agentId' }],
@ -876,7 +908,21 @@ const formRules = ref({
'template.signName': [{ required: true, message: '请输入签名' }],
// webhook
description: [{ max: 200, message: '最多可输入200个字符' }],
'template.message': [{ required: true, message: '请输入' }],
'template.message': [
{ required: true, message: '请输入' },
{ max: 500, message: '最多可输入500个字符' },
],
'template.calledShowNumbers': [
{
trigger: 'blur',
validator(_rule: Rule, value: string) {
if (!phoneRegEx(value)) {
return Promise.reject('请输入有效号码');
}
return Promise.resolve();
},
},
],
});
const { resetFields, validate, validateInfos, clearValidate } = useForm(
@ -884,39 +930,127 @@ const { resetFields, validate, validateInfos, clearValidate } = useForm(
formRules.value,
);
// markdown
watch(
() => formData.value.template.message,
() => formData.value.template.markdown?.title,
(val) => {
if (!val) return;
//
const oldKey = formData.value.variableDefinitions?.map((m) => m.id);
// ${}
const pattern = /(?<=\$\{).*?(?=\})/g;
const titleList = val.match(pattern)?.filter((f) => f);
const newKey = [...new Set(titleList)];
const result = newKey?.map((m) =>
oldKey.includes(m)
? formData.value.variableDefinitions.find(
(item) => item.id === m,
)
: {
id: m,
name: '',
type: 'string',
format: '%s',
},
);
formData.value.variableDefinitions = result as IVariableDefinitions[];
variableReg();
},
{ deep: true },
);
// link
watch(
() => formData.value.template.link?.title,
(val) => {
if (!val) return;
variableReg();
},
{ deep: true },
);
//
watch(
() => formData.value.template.subject,
(val) => {
if (!val) return;
variableReg();
},
{ deep: true },
);
// const clearValid = () => {
// setTimeout(() => {
// formData.value.variableDefinitions = [];
// clearValidate();
// }, 200);
// };
//
watch(
() => formData.value.template.message,
(val) => {
if (!val) return;
variableReg();
},
{ deep: true },
);
// webhook
watch(
() => formData.value.template.body,
(val) => {
if (!val) return;
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 = () => {
const _val = spliceStr();
//
const oldKey = formData.value.variableDefinitions?.map((m) => m.id);
// ${}
const pattern = /(?<=\$\{).*?(?=\})/g;
const titleList = _val.match(pattern)?.filter((f) => f);
const newKey = [...new Set(titleList)];
const result = newKey?.map((m) =>
oldKey.includes(m)
? formData.value.variableDefinitions.find((item) => item.id === m)
: {
id: m,
name: '',
type: 'string',
format: '%s',
},
);
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 = '';
};
/**
* 获取详情
@ -951,6 +1085,7 @@ const handleTypeChange = () => {
setTimeout(() => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value.template: ', formData.value.template);
resetPublicFiles();
}, 0);
};
@ -961,6 +1096,8 @@ const handleTypeChange = () => {
const handleProviderChange = () => {
formData.value.template =
TEMPLATE_FIELD_MAP[formData.value.type][formData.value.provider];
// console.log('formData.value: ', formData.value);
// console.log('formData.value.template: ', formData.value.template);
getConfigList();
resetPublicFiles();
};
@ -1026,8 +1163,9 @@ const handleSubmit = () => {
setTimeout(() => {
validate()
.then(async () => {
formData.value.template.ttsCode =
formData.value.template.templateCode;
if (formData.value.provider === 'ttsCode')
formData.value.template.ttsCode =
formData.value.template.templateCode;
btnLoading.value = true;
let res;
if (!formData.value.id) {

View File

@ -29,7 +29,7 @@
<a-button>导入</a-button>
</a-upload>
<a-popconfirm
title="确认导出当前页数据"
title="确认导出"
ok-text="确定"
cancel-text="取消"
@confirm="handleExport"
@ -114,6 +114,14 @@
</template>
</CardBox>
</template>
<template #bodyCell="{ column, text, record }">
<span v-if="column.dataIndex === 'type'">
{{ getMethodTxt(record.type) }}
</span>
<span v-if="column.dataIndex === 'provider'">
{{ getProviderTxt(record.type, record.provider) }}
</span>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
@ -254,6 +262,14 @@ const getLogo = (type: string, provider: string) => {
const getMethodTxt = (type: string) => {
return NOTICE_METHOD.find((f) => f.value === type)?.label;
};
/**
* 根据类型展示对应文案
* @param type
* @param provider
*/
const getProviderTxt = (type: string, provider: string) => {
return MSG_TYPE[type].find((f: any) => f.value === provider)?.label;
};
/**
* 新增
@ -298,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

@ -0,0 +1,471 @@
<template>
<page-container>
<div>
<Search
:columns="columns"
target="device-instance"
@search="handleSearch"
></Search>
<JTable
:columns="columns"
:request="queryList"
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-space>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img
:src="getImage('/alarm/alarm-config.png')"
/>
</slot>
</template>
<template #content>
<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>
<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>
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level }}
</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
v-if="
item.key != 'trigger' ||
slotProps.sceneTriggerType == 'manual'
"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
okText="确定"
cancelText="取消"
>
<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 #targetType="slotProps">
<span>{{ map[slotProps.targetType] }}</span>
</template>
<template #level="slotProps">
<a-tooltip
placement="topLeft"
:title="(Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level"
>
<div class="ellipsis">
{{ (Store.get('default-level') || []).find((item: any) => item?.level === slotProps.level)?.title ||
slotProps.level }}
</div>
</a-tooltip>
</template>
<template #sceneId="slotProps">
<span
>{{(slotProps?.scene || []).map((item: any) => item?.name).join(',') || ''}}</span
>
</template>
<template #state="slotProps">
<a-badge
:text="
slotProps.state?.value === 'enabled'
? '正常'
: '禁用'
"
:status="
slotProps.state?.value === 'enabled'
? 'success'
: 'error'
"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<span
v-if="
i.key != 'trigger' ||
slotProps.sceneTriggerType == 'manual'
"
>
<a-popconfirm
v-if="i.popConfirm"
v-bind="i.popConfirm"
okText="确定"
cancelText="取消"
>
<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>
</span>
</a-tooltip>
</a-space>
</template>
</JTable>
</div>
</page-container>
</template>
<script lang="ts" setup>
import JTable from '@/components/Table';
import {
queryList,
_enable,
_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: '操作',
key: 'action',
fixed: 'right',
width: 150,
scopedSlots: true,
},
];
const map = {
product: '产品',
device: '设备',
org: '组织',
other: '其他',
};
const handleSearch = (e: any) => {
params.value = e;
};
const queryDefaultLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
Store.set('default-level', res.result?.levels || []);
}
});
};
queryDefaultLevel();
const getActions = (
data: Partial<Record<string, any>>,
type?: 'card' | 'table',
): ActionsType[] => {
if (!data) {
return [];
}
const actions = [
{
key: 'trigger',
text: '手动触发',
disabled: data?.state?.value === 'disabled',
tooltip: {
title:
data?.state?.value === 'disabled'
? '未启用,不能手动触发'
: '手动触发',
},
popConfirm: {
title: '确定手动触发?',
onConfirm: async () => {
const scene = (data.scene || [])
.filter((item: any) => item?.triggerType === 'manual')
.map((i) => {
return { id: i?.id };
});
_execute(scene).then((res) => {
if (res.status === 200) {
message.success('操作成功');
tableRef.value?.reload();
} else {
message.error('操作失败');
}
});
},
},
icon: 'LikeOutlined',
},
{
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => {
menuStory.jumpPage('rule-engine/Alarm/Configuration/Save',{},{id:data.id});
},
},
{
key: 'action',
text: data.state?.value !== 'disabled' ? '禁用' : '启用',
tooltip: {
title: data.state?.value !== 'disabled' ? '禁用' : '启用',
},
icon:
data.state?.value !== 'disabled'
? 'StopOutlined'
: 'CheckCircleOutlined',
popConfirm: {
title: `${
data.state?.value !== 'disabled'
? '禁用告警不会影响关联的场景状态,确定要禁用吗'
: '确认启用'
}?`,
onConfirm: async () => {
let response = undefined;
if (data.state?.value === 'disabled') {
response = await _enable(data.id);
} else {
response = await _disable(data.id);
}
if (response && response.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
},
{
key: 'delete',
text: '删除',
disabled: data?.state?.value !== 'disabled',
tooltip: {
title:
data?.state?.value !== 'disabled'
? '请先禁用该告警,再删除'
: '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: async () => {
const resp = await remove(data.id);
if (resp.status === 200) {
message.success('操作成功!');
tableRef.value?.reload();
} else {
message.error('操作失败!');
}
},
},
icon: 'DeleteOutlined',
},
];
if (type === 'card')
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;
}
</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';
@ -193,6 +206,14 @@ const query = {
],
},
},
{
title: '说明',
key: 'description',
dataIndex: 'description',
search: {
type: 'string',
},
},
],
};
const columns = [
@ -258,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 () => {
@ -320,14 +344,8 @@ const refresh = () => {
tableRef.value?.reload();
};
const handleSearch = (e: any) => {
console.log(e);
params.value = e;
};
</script>
<style scoped>
.rule-desc {
white-space: nowrap; /*强制在同一行内显示所有文本直到文本结束或者遭遇br标签对象才换行。*/
overflow: hidden; /*超出部分隐藏*/
text-overflow: ellipsis; /*隐藏部分以省略号代替*/
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<div class="does-container">
<div v-show="props.type === 'internal-standalone'">
<h1>1.概述</h1>
<div>
内部独立应用适用于将<span>官方开发</span>的其他应用与<span
>物联网平台相互集成</span
>
例如将可视化平台集成至物联网平台或者将物联网平台集成至可视化平台以实现多处访问集中管控的业务场景
</div>
<div>
内部独立应用的<span>后端服务</span>相互<span>独立运行</span>互不影响
</div>
<div class="image">
<a-image width="100%" :src="img1" />
</div>
<h1>2.接入方式说明</h1>
<div>1页面集成</div>
<div>
集成其他应用的<span>前端页面</span>至物联网平台中为实现应用与物联网平台数据互联互通
<span>通常还需要配置API服务</span>
</div>
<div>2API客户端</div>
<div>
<span>物联网平台</span>请求<span>其他应用</span>
的接口以实现将物联网平台集成至其他应用系统如需实现<span
>其他应用</span
>
登录后可以访问<span>物联网平台</span>页面<span>还需要配置单点登录</span>
</div>
<div>3API服务</div>
<div>
<span>外部应用</span
>请求<span>物联网平台</span>的接口实现物联网平台的服务调用能力
<span>通常还需要配置页面集成</span>
</div>
<div>
配置API服务后系统将<span>自动创建</span>对应的<span>第三方应用用户</span>用户的
<span>账号/密码</span>分别对应appid/secureKey
</div>
<div>
第三方用户<span>可调用的API服务</span>在其应用管理卡片的<span
>其他-{'>'}赋权</span
>
页面进行<span>自定义配置</span>
</div>
<div>4单点登录</div>
<div>通过<span>第三方平台账号</span>登录到物联网平台</div>
</div>
<div v-show="props.type === 'internal-integrated'">
<h1>1.概述</h1>
<div>
内部集成应用适用于将<span>官方开发</span>的其他应用与<span
>物联网平台相互集成</span
>
例如将可视化平台集成至物联网平台或者将物联网平台集成至可视化平台以实现多处访问集中管控的业务场景
</div>
<div>内部独立应用的<span>后端服务在同一个环境运行</span></div>
<div class="image">
<a-image width="100%" :src="img2" />
</div>
<h1>2.接入方式说明</h1>
<div>1页面集成</div>
<div>
集成其他应用的<span>前端页面</span>
至物联网平台中集成后系统顶部将新增对应的应用管理菜单
</div>
<div>2API客户端</div>
<div>
<span>物联网平台</span
>去请求其他应用的接口以实现将物联网平台集成至其他应用
</div>
</div>
<div v-show="props.type === 'dingtalk-ent-app'">
<div class="url">
钉钉开放平台
<a
href="https://open-dev.dingtalk.com"
target="_blank"
rel="noopener noreferrer"
>
https://open-dev.dingtalk.com
</a>
</div>
<h1>1.概述</h1>
<div>钉钉企业内部应用适用于通过钉钉登录<span>物联网平台</span></div>
<div class="image">
<a-image width="100%" :src="img4" />
</div>
<h1>2.接入方式说明</h1>
<div>1单点登录</div>
<div>通过钉钉账号登录到物联网平台</div>
</div>
<div v-show="props.type === 'wechat-webapp'">
<div class="url">
微信开放平台
<a
href="https://open.weixin.qq.com"
target="_blank"
rel="noopener noreferrer"
>
https://open.weixin.qq.com
</a>
</div>
<h1>1.概述</h1>
<div>微信网站应用适用于通过微信授权登录<span>物联网平台</span></div>
<div class="image">
<a-image width="100%" :src="img3" />
</div>
<h1>2.接入方式说明</h1>
<div>1单点登录</div>
<div>通过微信账号登录到物联网平台</div>
</div>
<div v-show="props.type === 'third-party'">
<h1>1. 概述</h1>
<div>
第三方应用适用于<span>第三方应用</span><span
>物联网平台相互集成</span
>
例如将公司业务管理系统集成至物联网平台或者将物联网平台集成至业务管理系统以实现多处访问集中管控的业务场景
</div>
<div class="image">
<a-image width="100%" :src="img5" />
</div>
<h1>2.接入方式说明</h1>
<div>1页面集成</div>
<div>
集成其他应用的<span>前端页面</span>至物联网平台中为实现应用与物联网平台数据互联互通
<span>还需要配置API服务</span>
</div>
<div>2API客户端</div>
<div>
<span>物联网平台</span>请求<span>第三方应用</span>
的接口以实现将物联网平台集成至其他应用如需实现<span>第三方应用</span>登录后可以访问
<span>物联网平台</span>页面<span>还需要配置单点登录</span>
</div>
<div>3API服务</div>
<div>
<span>第三方应用</span
>通过API服务配置请求物联网平台接口实现<span
>物联网平台</span
>
的服务调用能力<span>通常还需要配置页面集成</span>
</div>
<div>
配置API服务后系统将<span>自动创建</span>对应的<span>第三方应用用户</span>用户的
<span>账号/密码</span>分别对应appid/secureKey
</div>
<div>
第三方用户<span>可调用的API服务</span>在其应用管理卡片的<span
>其他-{'>'}赋权</span
>
页面进行<span>自定义配置</span>
</div>
<div>4单点登录</div>
<div>通过<span>第三方平台账号</span>登录到物联网平台</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import type { applyType } from '../typing';
const props = defineProps<{
type: applyType;
}>();
const img1 = getImage('/apply/1.png');
const img2 = getImage('/apply/2.png');
const img3 = getImage('/apply/3.png');
const img4 = getImage('/apply/4.png');
const img5 = getImage('/apply/5.png');
</script>
<style lang="less" scoped>
.does-container {
padding: 24px;
overflow-y: auto;
color: rgba(#000, 0.8);
font-size: 14px;
background-color: #fafafa;
.url {
padding: 8px 16px;
color: #2f54eb;
background-color: rgba(#a7bdf7, 0.2);
}
h1 {
margin: 16px 0;
color: rgba(#000, 0.85);
font-weight: bold;
font-size: 14px;
&:first-child {
margin-top: 0;
}
}
h2 {
margin: 6px 0;
color: rgba(0, 0, 0, 0.8);
font-size: 14px;
}
.image {
margin: 16px 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
<template>
<div class="form-label-container">
<span class="text">{{ props.text }}</span>
<span class="required" v-show="props.required">*</span>
<a-tooltip>
<template #title>{{ props.tooltip }}</template>
<AIcon type="QuestionCircleOutlined" class="icon" />
</a-tooltip>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
text: string;
tooltip: string;
required?: boolean;
}>();
</script>
<style lang="less" scoped>
.form-label-container {
display: flex;
align-items: center;
.required {
display: inline-block;
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,121 @@
<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" />
</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 = defineProps<{
value: optionsType;
}>();
const columns = [
{
title: 'KEY',
dataIndex: 'key',
},
{
title: 'VALUE',
dataIndex: 'value',
},
{
title: ' ',
dataIndex: 'action',
},
];
const current = ref<number>(1);
const tableData = computed(() => {
return props.value.slice((current.value - 1) * 10, current.value * 10);
});
watch(
() => props.value,
(n, o) => {
if (!o || n.length === o.length) return;
//
else if (n.length > o.length) {
//
if (o.length % 10 === 0 && n.length > 10)
current.value = current.value + 1;
} else {
//
//
if (o.length % 10 === 1 && o.length > 10)
current.value = current.value - 1;
}
},
{
immediate: true,
},
);
function removeRow(index: number) {
const left = props.value.slice(0, index++);
const right = props.value.slice(index, props.value.length);
emits('update:value', [...left, ...right]);
}
function addRow() {
const newRow = {
label: '',
value: '',
};
console.log(111);
emits('update:value', [...props.value, newRow]);
}
</script>
<style lang="less" scoped>
.request-table-container {
width: 100%;
:deep(.ant-btn-link) {
color: #000000d9;
&:hover {
color: #1d39c4;
}
}
.add-btn {
width: 100%;
display: block;
margin-top: 10px;
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<page-container>
<a-card class="save-container">
<a-row :gutter="24">
<a-col :span="14">
<EditForm @change-apply-type="chengeType" />
</a-col>
<a-col :span="10"><Does :type="rightType" /></a-col>
</a-row>
</a-card>
</page-container>
</template>
<script setup lang="ts">
import Does from './components/Does.vue';
import EditForm from './components/EditForm.vue';
import type { applyType } from './typing';
const rightType = ref<applyType>('internal-standalone');
const chengeType = (newType: applyType) => {
rightType.value = newType;
};
</script>

91
src/views/system/Apply/Save/typing.d.ts vendored Normal file
View File

@ -0,0 +1,91 @@
export type applyType = 'internal-standalone'
| 'wechat-webapp'
| '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 = {
name: string;
provider: applyType;
integrationModes: string[];
config: string;
description: string;
page: { // 页面集成
baseUrl: 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 }, // 基本信息
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单点登录配置
authorizationUrl: string, // 授权地址
redirectUri: string, // 重定向地址
clientId: string, // 客户端ID
clientSecret: string, // 客户端密钥
userInfoUrl: string, // 用户信息接口
scope: string, // scope
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

@ -1,13 +1,399 @@
<template>
<div>
应用管理
</div>
<page-container class="apply-container">
<div class="apply-container">
<Search :columns="columns" @search="search" />
<JTable
ref="tableRef"
:columns="columns"
:request="getApplyList_api"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
:params="params"
:gridColumn="3"
>
<template #headerTitle>
<div style="display: flex; align-items: center">
<PermissionButton
:uhasPermission="`${permission}:add`"
type="primary"
@click="() => table.toSave()"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<p style="margin: 0 0 0 30px; color: #0000008c">
<AIcon
type="ExclamationCircleOutlined"
style="margin-right: 12px"
/>
应用管理将多个应用系统的登录简化为一次登录实现多处访问集中管控的业务场景
</p>
</div>
</template>
<template #card="slotProps">
<CardBox
:value="slotProps"
:actions="table.getActions(slotProps, 'card')"
v-bind="slotProps"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<template #img>
<slot name="img">
<img :src="getImage('/apply.png')" />
</slot>
</template>
<template #content>
<h3 class="card-item-content-title">
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
类型
</div>
<div>
{{
table.getTypeLabel(
slotProps.provider,
)
}}
</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
说明
</div>
<div>{{ slotProps.description }}</div>
</a-col>
</a-row>
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
>
<a-dropdown
placement="bottomRight"
v-if="item.key === 'others'"
>
<a-button>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
v-for="(o, i) in item.children"
:key="i"
>
<a-button
type="link"
@click="o.onClick"
>
<AIcon :type="o.icon" />
<span>{{ o.text }}</span>
</a-button>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<PermissionButton
v-else
:uhasPermission="item.permission"
:tooltip="item.tooltip"
:pop-confirm="item.popConfirm"
@click="item.onClick"
:disabled="item.disabled"
>
<AIcon :type="item.icon" />
<span v-if="item.key !== 'delete'">{{
item.text
}}</span>
</PermissionButton>
</a-tooltip>
</template>
</CardBox>
</template>
<template #provider="slotProps">
{{ table.getTypeLabel(slotProps.provider) }}
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:text="slotProps.state.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
v-for="i in table.getActions(slotProps, 'table')"
:uhasPermission="i.permission"
type="link"
:tooltip="i.tooltip"
:pop-confirm="i.popConfirm"
@click="i.onClick"
:disabled="i.disabled"
>
<AIcon :type="i.icon" />
</PermissionButton>
</a-space>
</template>
</JTable>
</div>
</page-container>
</template>
<script setup lang="ts">
<script setup lang="ts" name="Apply">
import PermissionButton from '@/components/PermissionButton/index.vue';
import {
getApplyList_api,
changeApplyStatus_api,
delApply_api,
} from '@/api/system/apply';
import { ActionsType } from '@/components/Table';
import { getImage } from '@/utils/comm';
import { useMenuStore } from '@/store/menu';
import { message } from 'ant-design-vue';
const menuStory = useMenuStore();
const permission = 'system/User';
const typeOptions = [
{
label: '内部独立应用',
value: 'internal-standalone',
},
{
label: '微信网站应用',
value: 'wechat-webapp',
},
{
label: '内部集成应用',
value: 'internal-integrated',
},
{
label: '钉钉企业内部应用',
value: 'dingtalk-ent-app',
},
{
label: '第三方应用',
value: 'third-party',
},
];
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '类型',
dataIndex: 'provider',
key: 'provider',
ellipsis: true,
fixed: 'left',
search: {
type: 'select',
options: typeOptions,
},
scopedSlots: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
ellipsis: true,
search: {
rename: 'status',
type: 'select',
options: [
{
label: '正常',
value: 'enabled',
},
{
label: '禁用',
value: 'disabled',
},
],
},
scopedSlots: true,
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
];
const params = ref({});
const search = (newParams: any) => (params.value = {...newParams});
const tableRef = ref();
const table = {
refresh: () => {
tableRef.value.reload();
},
toSave: (id?: string, view = false) => {
if (id) menuStory.jumpPage('system/Apply/Save', {}, { id, view });
else menuStory.jumpPage('system/Apply/Save');
},
changeStatus: (row: any) => {
const state = row.state.value === 'enabled' ? 'disabled' : 'enabled';
changeApplyStatus_api(row.id, { state }).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功');
table.refresh();
}
});
},
clickDel: (row: any) => {
delApply_api(row.id).then((resp: any) => {
if (resp.status === 200) {
message.success('操作成功');
table.refresh();
}
});
},
getActions: (
data: Partial<Record<string, any>>,
type: 'card' | 'table',
) => {
if (!data) return [];
const disabled = data.state.value === 'enabled';
const result = [
{
permission: true,
key: 'edit',
text: '编辑',
tooltip: {
title: '编辑',
},
icon: 'EditOutlined',
onClick: () => table.toSave(data.id),
},
{
permission: true,
key: 'action',
text: disabled ? '禁用' : '启用',
tooltip: {
title: disabled ? '禁用' : '启用',
},
popConfirm: {
title: `确认${disabled ? '禁用' : '启用'}`,
onConfirm: () => table.changeStatus(data),
},
icon: disabled ? 'StopOutlined' : 'PlayCircleOutlined',
},
{
permission: true,
key: 'delete',
text: '删除',
tooltip: {
title: disabled ? '请先禁用再删除' : '删除',
},
popConfirm: {
title: '确认删除?',
onConfirm: () => table.clickDel(data),
},
disabled,
icon: 'DeleteOutlined',
},
] as ActionsType[];
const otherServers = data.integrationModes.map(
(item: any) => item.value as string,
);
const others = {
key: 'others',
text: '其他',
icon: 'EllipsisOutlined',
children: [] as ActionsType[],
};
//
if (otherServers.includes('page'))
others.children?.push({
permission: true,
key: 'page',
text: '集成菜单',
tooltip: {
title: '集成菜单',
},
icon: 'MenuUnfoldOutlined',
onClick: () => {},
});
// api
if (otherServers.includes('apiServer'))
others.children?.push(
{
permission: true,
key: 'empowerment',
text: '赋权',
tooltip: {
title: '赋权',
},
icon: 'icon-fuquan',
onClick: () => {},
},
{
permission: true,
key: 'viewApi',
text: '查看API',
tooltip: {
title: '查看API',
},
icon: 'icon-chakanAPI',
onClick: () => {},
},
);
//
if (others.children.length > 0) {
if (type === 'card') {
result.splice(result.length - 1, 0, others);
} else {
result.splice(result.length - 1, 0, ...others.children);
}
}
return result;
},
getTypeLabel: (val: string) => {
if (!val) return '';
return typeOptions.find((item) => item.value === val)?.label;
},
};
</script>
<style scoped>
</style>
<style lang="less" scoped>
.apply-container {
:deep(.ant-table-cell) {
.ant-btn-link {
padding: 0;
}
}
}
</style>

View File

@ -1,124 +1,134 @@
<template>
<div class="data-source-container">
<Search :columns="query.columns" @search="query.search" />
<page-container>
<div class="data-source-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getDataSourceList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog({})"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
</BadgeStatus>
</template>
<template #typeId="slotProps">
{{
(table.typeOptions.value.length &&
table.getTypeLabel(slotProps.typeId)) ||
''
}}
</template>
<template #action="slotProps">
<a-space :size="16">
<JTable
ref="tableRef"
:columns="table.columns"
:request="getDataSourceList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
>
<template #headerTitle>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog({})"
>
<AIcon type="EditOutlined" />
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:manage`"
type="link"
:tooltip="{
title:
slotProps?.typeId === 'rabbitmq'
? '暂不支持管理功能'
: table.getRowStatus(slotProps)
? '管理'
: '请先启用数据源',
}"
@click="
() =>
router.push(
`/system/DataSource/Management?id=${slotProps.id}`,
)
"
:disabled="slotProps?.typeId === 'rabbitmq' || !table.getRowStatus(slotProps)"
>
<AIcon type="icon-ziyuankuguanli" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:popConfirm="{
title: `确定要${
table.getRowStatus(slotProps) ? '禁用' : '启用'
}`,
onConfirm: () => table.clickChangeStatus(slotProps),
}"
:tooltip="{
title: table.getRowStatus(slotProps)
? '禁用'
: '启用',
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state?.value"
:text="slotProps.state?.text"
:statusNames="{
enabled: 'success',
disabled: 'error',
}"
>
<AIcon
:type="
table.getRowStatus(slotProps)
? 'StopOutlined'
: 'PlayCircleOutlined'
</BadgeStatus>
</template>
<template #typeId="slotProps">
{{
(table.typeOptions.value.length &&
table.getTypeLabel(slotProps.typeId)) ||
''
}}
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:manage`"
type="link"
:tooltip="{
title:
slotProps?.typeId === 'rabbitmq'
? '暂不支持管理功能'
: table.getRowStatus(slotProps)
? '管理'
: '请先启用数据源',
}"
@click="
() =>
router.push(
`/system/DataSource/Management?id=${slotProps.id}`,
)
"
/>
<!-- <AIcon type="PlayCircleOutlined" /> -->
</PermissionButton>
:disabled="
slotProps?.typeId === 'rabbitmq' ||
!table.getRowStatus(slotProps)
"
>
<AIcon type="icon-ziyuankuguanli" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:popConfirm="{
title: `确定要${
table.getRowStatus(slotProps)
? '禁用'
: '启用'
}`,
onConfirm: () =>
table.clickChangeStatus(slotProps),
}"
:tooltip="{
title: table.getRowStatus(slotProps)
? '禁用'
: '启用',
}"
>
<AIcon
:type="
table.getRowStatus(slotProps)
? 'StopOutlined'
: 'PlayCircleOutlined'
"
/>
<!-- <AIcon type="PlayCircleOutlined" /> -->
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{
title: table.getRowStatus(slotProps)
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="table.getRowStatus(slotProps)"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{
title: table.getRowStatus(slotProps)
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="table.getRowStatus(slotProps)"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<div class="dialogs">
<EditDialog ref="editDialogRef" @confirm="table.refresh" />
<div class="dialogs">
<EditDialog ref="editDialogRef" @confirm="table.refresh" />
</div>
</div>
</div>
</page-container>
</template>
<script setup lang="ts" name="DataSource">
@ -132,7 +142,7 @@ import {
getDataSourceList_api,
getDataTypeDict_api,
changeStatus_api,
delDataSource_api
delDataSource_api,
} from '@/api/system/dataSource';
import { message } from 'ant-design-vue';
@ -243,7 +253,7 @@ const table = {
key: 'action',
scopedSlots: true,
width: '200px',
fixed:'right'
fixed: 'right',
},
],
@ -298,7 +308,6 @@ table.getTypeOption();
<style lang="less" scoped>
.data-source-container {
padding: 24px;
:deep(.ant-table-cell) {
.ant-btn-link {
padding: 0;

View File

@ -1,30 +1,32 @@
<template>
<div class="department-container">
<a-card class="department-content">
<div class="left">
<LeftTree @change="(id) => (departmentId = id)" />
</div>
<div class="right">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="product" tab="产品">
<Product
:parentId="departmentId"
@open-device-bind="openDeviceBind"
/>
</a-tab-pane>
<a-tab-pane key="device" tab="设备">
<Device
:parentId="departmentId"
v-model:bindBool="bindBool"
/>
</a-tab-pane>
<a-tab-pane key="user" tab="用户">
<User :parentId="departmentId" />
</a-tab-pane>
</a-tabs>
</div>
</a-card>
</div>
<page-container>
<div class="department-container">
<a-card class="department-content">
<div class="left">
<LeftTree @change="(id) => (departmentId = id)" />
</div>
<div class="right">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="product" tab="产品">
<Product
:parentId="departmentId"
@open-device-bind="openDeviceBind"
/>
</a-tab-pane>
<a-tab-pane key="device" tab="设备">
<Device
:parentId="departmentId"
v-model:bindBool="bindBool"
/>
</a-tab-pane>
<a-tab-pane key="user" tab="用户">
<User :parentId="departmentId" />
</a-tab-pane>
</a-tabs>
</div>
</a-card>
</div>
</page-container>
</template>
<script setup lang="ts" name="Department">
@ -46,7 +48,6 @@ const openDeviceBind = () => {
<style lang="less" scoped>
.department-container {
padding: 24px;
.department-content {
:deep(.ant-card-body) {
display: flex;

View File

@ -1,12 +1,16 @@
<template>
<div class="menu-detail-container">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="basic" tab="基本信息"> <BasicInfo /> </a-tab-pane>
<a-tab-pane key="button" tab="按钮管理">
<ButtonMange />
</a-tab-pane>
</a-tabs>
</div>
<page-container>
<div class="menu-detail-container">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="basic" tab="基本信息">
<BasicInfo />
</a-tab-pane>
<a-tab-pane key="button" tab="按钮管理">
<ButtonMange />
</a-tab-pane>
</a-tabs>
</div>
</page-container>
</template>
<script setup lang="ts">
@ -24,7 +28,7 @@ const activeKey = ref('basic');
}
.ant-tabs-tabpane {
background-color: #f0f2f5;
padding: 24px;
padding-top: 24px;
}
}
</style>

View File

@ -1,73 +1,79 @@
<template>
<div class="menu-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="table.getList"
model="TABLE"
:params="query.params"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.toDetails({})"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<a-button
style="margin-left: 12px"
@click="router.push('/system/Menu/Setting')"
>菜单配置</a-button
>
<!-- <PermissionButton
:uhasPermission="true"
@click="router.push('/system/Menu/Setting')"
>
菜单配置
</PermissionButton> -->
</template>
<template #createTime="slotProps">
{{ moment(slotProps.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip>
<template #title>查看</template>
<a-button
style="padding: 0"
type="link"
@click="table.toDetails(slotProps)"
>
<search-outlined />
</a-button>
</a-tooltip>
<page-container>
<div class="menu-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="table.getList"
model="TABLE"
:params="query.params"
>
<template #headerTitle>
<PermissionButton
type="link"
type="primary"
:uhasPermission="`${permission}:add`"
:tooltip="{ title: '新增子菜单' }"
@click="table.addChildren(slotProps)"
@click="table.toDetails({})"
>
<AIcon type="PlusCircleOutlined" />
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `是否删除该菜单`,
onConfirm: () => table.clickDel(slotProps),
}"
<a-button
style="margin-left: 12px"
@click="router.push('/system/Menu/Setting')"
>菜单配置</a-button
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
</div>
<!-- <PermissionButton
:uhasPermission="true"
@click="router.push('/system/Menu/Setting')"
>
菜单配置
</PermissionButton> -->
</template>
<template #createTime="slotProps">
{{
moment(slotProps.createTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip>
<template #title>查看</template>
<a-button
style="padding: 0"
type="link"
@click="table.toDetails(slotProps)"
>
<search-outlined />
</a-button>
</a-tooltip>
<PermissionButton
type="link"
:uhasPermission="`${permission}:add`"
:tooltip="{ title: '新增子菜单' }"
@click="table.addChildren(slotProps)"
>
<AIcon type="PlusCircleOutlined" />
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `是否删除该菜单`,
onConfirm: () => table.clickDel(slotProps),
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
</div>
</page-container>
</template>
<script setup lang="ts">
@ -269,8 +275,6 @@ const table = reactive({
<style lang="less" scoped>
.menu-container {
padding: 24px;
:deep(.ant-table-cell) {
.ant-btn-link {
padding: 0;

View File

@ -1,120 +1,127 @@
<template>
<div class="permission-container">
<Search :columns="query.columns" @search="query.search" />
<page-container>
<div class="permission-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getPermission_api"
model="TABLE"
:params="query.params"
:defaultParams="{ sorts: [{ name: 'id', order: 'asc' }] }"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog(undefined)"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<a-dropdown trigger="hover">
<a-button>批量操作</a-button>
<template #overlay>
<a-menu>
<a-menu-item>
<a-upload
name="file"
action="#"
accept=".json"
:showUploadList="false"
:before-upload="table.clickImport"
:disabled="
!hasPermission(`${permission}:import`)
"
>
<PermissionButton
:hasPermission="`${permission}:import`"
<JTable
ref="tableRef"
:columns="table.columns"
:request="getPermission_api"
model="TABLE"
:params="query.params"
:defaultParams="{ sorts: [{ name: 'id', order: 'asc' }] }"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog(undefined)"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<a-dropdown trigger="hover">
<a-button>批量操作</a-button>
<template #overlay>
<a-menu>
<a-menu-item>
<a-upload
name="file"
action="#"
accept=".json"
:showUploadList="false"
:before-upload="table.clickImport"
:disabled="
!hasPermission(
`${permission}:import`,
)
"
>
导入
<PermissionButton
:hasPermission="`${permission}:import`"
>
导入
</PermissionButton>
</a-upload>
</a-menu-item>
<a-menu-item>
<PermissionButton
:uhasPermission="`${permission}:export`"
:popConfirm="{
title: `确认导出?`,
onConfirm: () =>
table.clickExport(),
}"
>
导出
</PermissionButton>
</a-upload>
</a-menu-item>
<a-menu-item>
<PermissionButton
:uhasPermission="`${permission}:export`"
:popConfirm="{
title: `确认导出?`,
onConfirm: () => table.clickExport(),
}"
>
导出
</PermissionButton>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<template #status="slotProps">
<StatusLabel :status-value="slotProps.status" />
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<template #status="slotProps">
<StatusLabel :status-value="slotProps.status" />
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:popConfirm="{
title: `确定要${
slotProps.status ? '禁用' : '启用'
}`,
onConfirm: () => table.changeStatus(slotProps),
}"
:tooltip="{ title: slotProps.status ? '禁用' : '启用' }"
>
<AIcon
:type="
slotProps.status
? 'StopOutlined'
: 'PlayCircleOutlined '
"
/>
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:popConfirm="{
title: `确定要${
slotProps.status ? '禁用' : '启用'
}`,
onConfirm: () => table.changeStatus(slotProps),
}"
:tooltip="{
title: slotProps.status ? '禁用' : '启用',
}"
>
<AIcon
:type="
slotProps.status
? 'StopOutlined'
: 'PlayCircleOutlined '
"
/>
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{
title: slotProps.status
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{
title: slotProps.status
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<div class="dialogs">
<EditDialog ref="editDialogRef" @refresh="table.refresh" />
<div class="dialogs">
<EditDialog ref="editDialogRef" @refresh="table.refresh" />
</div>
</div>
</div>
</page-container>
</template>
<script setup lang="ts">
@ -281,7 +288,6 @@ const table = reactive({
<style lang="less" scoped>
.permission-container {
padding: 24px;
.ant-dropdown-trigger {
margin-left: 12px;

View File

@ -1,5 +1,8 @@
<template>
<a-card class="api-page-container">
<p>
<AIcon type="ExclamationCircleOutlined" style="margin-right: 12px;" />配置系统支持API赋权的范围
</p>
<a-row :gutter="24">
<a-col :span="5">
<LeftTree @select="treeSelect" :mode="props.mode" />
@ -117,7 +120,6 @@ function init() {
<style scoped>
.api-page-container {
padding: 24px;
height: 100%;
background-color: transparent;
}

View File

@ -1,7 +1,7 @@
<template>
<div>
<page-container>
<Api mode="api" />
</div>
</page-container>
</template>
<script setup lang="ts" name="Platforms">

View File

@ -1,55 +1,59 @@
<template>
<div class="relationship-container">
<Search :columns="query.columns" @search="query.search" />
<page-container>
<div class="relationship-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getRelationshipList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog(undefined)"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #action="slotProps">
<a-space :size="16">
<JTable
ref="tableRef"
:columns="table.columns"
:request="getRelationshipList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
>
<template #headerTitle>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.openDialog(undefined)"
>
<AIcon type="EditOutlined" />
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<PermissionButton
:uhasPermission="`${permission}:delete`"
type="link"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<EditDialog ref="editDialogRef" @refresh="table.refresh" />
</div>
<EditDialog ref="editDialogRef" @refresh="table.refresh" />
</div>
</page-container>
</template>
<script setup lang="ts" name="Relationship">
@ -181,7 +185,6 @@ const table = {
<style lang="less" scoped>
.relationship-container {
padding: 24px;
:deep(.ant-table-cell) {
.ant-btn-link {
padding: 0;

View File

@ -1,10 +1,12 @@
<template>
<div class="details-container">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="权限分配"><Permiss /></a-tab-pane>
<a-tab-pane key="2" tab="用户管理"><User /></a-tab-pane>
</a-tabs>
</div>
<page-container>
<div class="details-container">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="权限分配"><Permiss /></a-tab-pane>
<a-tab-pane key="2" tab="用户管理"><User /></a-tab-pane>
</a-tabs>
</div>
</page-container>
</template>
<script setup lang="ts" name="Detail">
@ -17,17 +19,9 @@ const activeKey = ref('1');
<style lang="less" scoped>
.details-container {
:deep(.ant-tabs-nav-wrap) {
background-color: #fff;
padding: 24px 0 0 24px;
}
:deep(.ant-tabs-content-holder) {
padding: 24px;
padding-left: 24px;
}
}
</style>

View File

@ -1,55 +1,57 @@
<template>
<a-card class="role-container">
<Search :columns="query.columns" />
<page-container>
<a-card class="role-container">
<Search :columns="query.columns" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getRoleList_api"
model="TABLE"
:params="query.params"
>
<template #headerTitle>
<PermissionButton
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.clickAdd"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #action="slotProps">
<a-space :size="16">
<JTable
ref="tableRef"
:columns="table.columns"
:request="getRoleList_api"
model="TABLE"
:params="query.params"
>
<template #headerTitle>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.clickEdit(slotProps)"
type="primary"
:uhasPermission="`${permission}:add`"
@click="table.clickAdd"
>
<AIcon type="EditOutlined" />
<AIcon type="PlusOutlined" />新增
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确定要删除吗`,
onConfirm: () => table.clickDel(slotProps),
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
</template>
<div class="dialogs">
<AddDialog ref="addDialogRef" />
</div>
</a-card>
<template #action="slotProps">
<a-space :size="16">
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.clickEdit(slotProps)"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{ title: '删除' }"
:popConfirm="{
title: `确定要删除吗`,
onConfirm: () => table.clickDel(slotProps),
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<div class="dialogs">
<AddDialog ref="addDialogRef" />
</div>
</a-card>
</page-container>
</template>
<script setup lang="ts" name="Role">
@ -146,12 +148,10 @@ nextTick(() => {
<style lang="less" scoped>
.role-container {
:deep(.ant-table-cell) {
.ant-btn-link {
padding: 0;
}
}
}
</style>

View File

@ -1,46 +1,49 @@
<template>
<div class="user-container">
<Search :columns="query.columns" @search="query.search" />
<page-container>
<div class="user-container">
<Search :columns="query.columns" @search="query.search" />
<JTable
ref="tableRef"
:columns="table.columns"
:request="getUserList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
>
<template #headerTitle>
<!-- <a-button
<JTable
ref="tableRef"
:columns="table.columns"
:request="getUserList_api"
model="TABLE"
:params="query.params.value"
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
>
<template #headerTitle>
<!-- <a-button
type="primary"
@click="table.openDialog('add')"
style="margin-right: 10px"
><AIcon type="PlusOutlined" />新增</a-button
> -->
<PermissionButton
:uhasPermission="`${permission}:add`"
type="primary"
@click="table.openDialog('add')"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #type="slotProps">
{{ slotProps.type.name }}
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.status"
:text="slotProps.status ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<!-- <a-tooltip>
<PermissionButton
:uhasPermission="`${permission}:add`"
type="primary"
@click="table.openDialog('add')"
>
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #type="slotProps">
{{ slotProps.type.name }}
</template>
<template #status="slotProps">
<BadgeStatus
:status="slotProps.status"
:text="slotProps.status ? '正常' : '禁用'"
:statusNames="{
1: 'success',
0: 'error',
}"
></BadgeStatus>
</template>
<template #action="slotProps">
<a-space :size="16">
<!-- <a-tooltip>
<template #title>编辑</template>
<a-button
style="padding: 0"
@ -50,7 +53,7 @@
<AIcon type="EditOutlined" />
</a-button>
</a-tooltip> -->
<!-- <a-popconfirm
<!-- <a-popconfirm
:title="`确定${slotProps.status ? '禁用' : '启用'}吗?`"
ok-text="确定"
cancel-text="取消"
@ -66,7 +69,7 @@
</a-button>
</a-tooltip>
</a-popconfirm> -->
<!-- <a-tooltip>
<!-- <a-tooltip>
<template #title>重置密码</template>
<a-button
style="padding: 0"
@ -76,7 +79,7 @@
<AIcon type="icon-zhongzhimima" />
</a-button>
</a-tooltip> -->
<!-- <a-popconfirm
<!-- <a-popconfirm
title="确认删除"
ok-text="确定"
cancel-text="取消"
@ -97,66 +100,67 @@
</a-tooltip>
</a-popconfirm> -->
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog('edit')"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:tooltip="{
title: `${slotProps.status ? '禁用' : '启用'}`,
}"
:popConfirm="{
title: `确定${
slotProps.status ? '禁用' : '启用'
}`,
onConfirm: () => table.changeStatus(slotProps),
}"
>
<stop-outlined v-if="slotProps.status" />
<play-circle-outlined v-else />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '重置密码',
}"
@click="table.openDialog('reset', slotProps)"
>
<AIcon type="icon-zhongzhimima" />
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{
title: slotProps.status
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '编辑',
}"
@click="table.openDialog('edit')"
>
<AIcon type="EditOutlined" />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:action`"
type="link"
:tooltip="{
title: `${slotProps.status ? '禁用' : '启用'}`,
}"
:popConfirm="{
title: `确定${
slotProps.status ? '禁用' : '启用'
}`,
onConfirm: () => table.changeStatus(slotProps),
}"
>
<stop-outlined v-if="slotProps.status" />
<play-circle-outlined v-else />
</PermissionButton>
<PermissionButton
:uhasPermission="`${permission}:update`"
type="link"
:tooltip="{
title: '重置密码',
}"
@click="table.openDialog('reset', slotProps)"
>
<AIcon type="icon-zhongzhimima" />
</PermissionButton>
<PermissionButton
type="link"
:uhasPermission="`${permission}:delete`"
:tooltip="{
title: slotProps.status
? '请先禁用,再删除'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: () => table.clickDel(slotProps),
}"
:disabled="slotProps.status"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</a-space>
</template>
</JTable>
<div class="dialogs">
<EditUserDialog ref="editDialogRef" @confirm="table.refresh" />
<div class="dialogs">
<EditUserDialog ref="editDialogRef" @confirm="table.refresh" />
</div>
</div>
</div>
</page-container>
</template>
<script setup lang="ts" name="UserMange">
@ -354,8 +358,6 @@ type modalType = '' | 'add' | 'edit' | 'reset';
<style lang="less" scoped>
.user-container {
padding: 24px;
:deep(.ant-table-tbody) {
.ant-table-cell {
.ant-space-item {

1555
yarn.lock

File diff suppressed because it is too large Load Diff