Merge branch 'dev' into dev-hub
This commit is contained in:
commit
f7f0556b00
|
@ -0,0 +1,13 @@
|
|||
import server from '@/utils/request'
|
||||
|
||||
export default {
|
||||
// 列表
|
||||
list: (data: any) => server.post(`/media/device/_query/`, data),
|
||||
// 详情
|
||||
detail: (id: string): any => server.get(`/media/device/${id}`),
|
||||
// 新增
|
||||
save: (data: any) => server.post(`/media/device/${data.channel}`, data),
|
||||
// 修改
|
||||
update: (data: any) => server.put(`/media/device/${data.channel}/${data.id}`, data),
|
||||
del: (id: string) => server.remove(`/media/device/${id}`),
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import server from '@/utils/request';
|
||||
/**
|
||||
* 获取今日及当月告警数量
|
||||
*/
|
||||
export const dashboard = (data:Record<string,any[]>)=> server.post('/dashboard/_multi',data);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const getAlarm = (params:Record<string,any[]>) => server.get('/alarm/record/_query',params);
|
||||
|
||||
/**
|
||||
* 获取告警数量
|
||||
*/
|
||||
export const getAlarmConfigCount = (data:Record<string,any>) => server.post('/alarm/config/_count',data);
|
||||
|
||||
/**
|
||||
* 获取报警等级
|
||||
*/
|
||||
export const getAlarmLevel = () => server.get('/alarm/config/default/level');
|
|
@ -10,12 +10,14 @@ export const updateDepartment_api = (data: object) => server.patch(`/organizatio
|
|||
export const delDepartment_api = (id: string) => server.remove(`/organization/${id}`);
|
||||
|
||||
|
||||
// 获取所属产品列表
|
||||
export const getDeviceProduct_api = (data: object) => server.get(`/device/product/_query/no-paging`, data);
|
||||
// 获取产品列表
|
||||
export const getDeviceOrProductList_api = (data: object) => server.post(`/device-product/_query`, data);
|
||||
// 获取设备列表
|
||||
export const getDeviceList_api = (data: object) => server.post(`/device/instance/_query`, data);
|
||||
// 根据产品的id获取产品的权限
|
||||
export const getPermission_api = (type:'device' | 'product',ids: object, id: string) => server.post(`/assets/bindings/${type}/org/${id}/_query`, ids);
|
||||
export const getPermission_api = (type: 'device' | 'product', ids: object, id: string) => server.post(`/assets/bindings/${type}/org/${id}/_query`, ids);
|
||||
// 获取产品的权限字典
|
||||
export const getPermissionDict_api = () => server.get(`/assets/bindings/product/permissions`);
|
||||
|
||||
|
@ -25,3 +27,12 @@ export const bindDeviceOrProductList_api = (type: 'device' | 'product', data: ob
|
|||
export const unBindDeviceOrProduct_api = (type: 'device' | 'product', data: object) => server.post(`/assets/unbind/${type}`, data);
|
||||
// 批量更新权限
|
||||
export const updatePermission_api = (type: 'device' | 'product', parentId: string, data: object) => server.put(`/assets/permission/${type}/org/${parentId}/_batch`, data);
|
||||
|
||||
|
||||
// 用户相关
|
||||
// 获取绑定用户列表
|
||||
export const getBindUserList_api = (data: object) => server.post(`/user/_query`, data);
|
||||
// 绑定用户
|
||||
export const bindUser_api = (parentId:string,data: object) => server.post(`/organization/${parentId}/users/_bind`, data);
|
||||
// 解绑用户
|
||||
export const unBindUser_api = (parentId:string,data: object) => server.post(`/organization/${parentId}/users/_unbind`, data);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import server from '@/utils/request';
|
||||
|
||||
// 获取用户类型
|
||||
export const getUserType_api = () => server.get(`/user/detail/types`);
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserList_api = (data: object) => server.post(`/user/detail/_query`, data);
|
||||
|
||||
// 校验字段合法性
|
||||
export const validateField_api = (type: 'username' | 'password', name: string) => server.post(`/user/${type}/_validate`, name, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
// 获取角色列表
|
||||
export const getRoleList_api = () => server.get(`/role/_query/no-paging?paging=false`);
|
||||
// 获取组织列表
|
||||
export const getDepartmentList_api = () => server.get(`/organization/_all/tree?paging=false`);
|
||||
|
||||
// 获取用户信息
|
||||
export const getUser_api = (id: string) => server.get(`/user/detail/${id}`);
|
||||
// 添加用户
|
||||
export const addUser_api = (data: object) => server.post(`/user/detail/_create`, data);
|
||||
// 更新用户
|
||||
export const updateUser_api = (data: any) => server.put(`/user/detail/${data.id}/_update`, data);
|
||||
// 更新密码
|
||||
export const updatePassword_api = (data: { id: string, password: string }) => server.post(`/user/${data.id}/password/_reset`, data.password, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
});
|
||||
// 修改用户状态
|
||||
export const changeUserStatus_api = (data: object) => server.patch(`/user`,data);
|
||||
// 删除用户
|
||||
export const deleteUser_api = (id: string) => server.remove(`/user/${id}`);
|
|
@ -45,7 +45,10 @@ const iconKeys = [
|
|||
'InfoCircleOutlined',
|
||||
'SearchOutlined',
|
||||
'EllipsisOutlined',
|
||||
'ClockCircleOutlined'
|
||||
'ClockCircleOutlined',
|
||||
'PartitionOutlined',
|
||||
'ShareAltOutlined',
|
||||
'playCircleOutlined',
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
|||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(workerId, label) {
|
||||
getWorker(_, label) {
|
||||
if (label === 'json') {
|
||||
return new jsonWorker();
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ self.MonacoEnvironment = {
|
|||
if (label === 'html') {
|
||||
return new htmlWorker();
|
||||
}
|
||||
if (label === 'ts') {
|
||||
if (['typescript', 'javascript'].includes(label)) {
|
||||
return new tsWorker();
|
||||
}
|
||||
return new editorWorker();
|
||||
|
@ -33,6 +33,7 @@ self.MonacoEnvironment = {
|
|||
const props = defineProps({
|
||||
modelValue: [String, Number],
|
||||
theme: { type: String, default: 'vs-dark' },
|
||||
language: { type: String, default: 'json' },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
@ -42,10 +43,10 @@ const dom = ref();
|
|||
let instance;
|
||||
|
||||
onMounted(() => {
|
||||
const jsonModel = monaco.editor.createModel(props.modelValue, 'json');
|
||||
const _model = monaco.editor.createModel(props.modelValue, props.language);
|
||||
|
||||
instance = monaco.editor.create(dom.value, {
|
||||
model: jsonModel,
|
||||
model: _model,
|
||||
tabSize: 2,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<template v-if="isPermission">
|
||||
<template v-if="popConfirm">
|
||||
<a-popconfirm v-bind="popConfirm" @confirm="conform" :disabled="!isPermission || props.disabled">
|
||||
<a-popconfirm v-bind="popConfirm" :disabled="!isPermission || props.disabled">
|
||||
<a-tooltip v-if="tooltip" v-bind="tooltip">
|
||||
<slot v-if="noButton"></slot>
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
|
||||
<slot></slot>
|
||||
<template #icon>
|
||||
<slot name="icon"></slot>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
|
||||
<slot></slot>
|
||||
<template #icon>
|
||||
<slot name="icon"></slot>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<template v-else-if="tooltip">
|
||||
<a-tooltip v-bind="tooltip">
|
||||
<slot v-if="noButton"></slot>
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
|
||||
<slot></slot>
|
||||
<template #icon>
|
||||
<slot name="icon"></slot>
|
||||
|
@ -32,7 +32,7 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<slot v-if="noButton"></slot>
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
|
||||
<slot></slot>
|
||||
<template #icon>
|
||||
<slot name="icon"></slot>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</template>
|
||||
<a-tooltip v-else title="没有权限">
|
||||
<slot v-if="noButton"></slot>
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" @click="handleClick">
|
||||
<a-button v-else v-bind="_buttonProps" :disabled="_isPermission" >
|
||||
<slot></slot>
|
||||
<template #icon>
|
||||
<slot name="icon"></slot>
|
||||
|
@ -56,11 +56,11 @@ import { TooltipProps, PopconfirmProps } from 'ant-design-vue/es'
|
|||
import { buttonProps } from 'ant-design-vue/es/button/button'
|
||||
import { usePermissionStore } from '@/store/permission';
|
||||
|
||||
interface PermissionButtonEmits {
|
||||
(e: 'click', data: MouseEvent): void;
|
||||
}
|
||||
// interface PermissionButtonEmits {
|
||||
// (e: 'click', data: MouseEvent): void;
|
||||
// }
|
||||
|
||||
const emits = defineEmits<PermissionButtonEmits>()
|
||||
// const emits = defineEmits<PermissionButtonEmits>()
|
||||
|
||||
// interface PermissionButtonProps extends ButtonProps {
|
||||
// tooltip?: TooltipProps;
|
||||
|
@ -100,18 +100,15 @@ const isPermission = computed(() => {
|
|||
})
|
||||
const _isPermission = computed(() =>
|
||||
'hasPermission' in props && isPermission.value
|
||||
? 'disabled' in buttonProps
|
||||
? buttonProps.disabled as boolean
|
||||
? 'disabled' in _buttonProps
|
||||
? _buttonProps.disabled as boolean
|
||||
: false
|
||||
: true
|
||||
)
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
emits('click', e)
|
||||
}
|
||||
|
||||
const conform = (e: MouseEvent) => {
|
||||
props.popConfirm?.onConfirm?.(e)
|
||||
}
|
||||
// const conform = (e: MouseEvent) => {
|
||||
// props.popConfirm?.onConfirm?.(e)
|
||||
// }
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
:key="index"
|
||||
@click="myValue = item.value"
|
||||
>
|
||||
<img class="img" :src="item.logo" alt="" />
|
||||
<img v-if="item.logo" class="img" :src="item.logo" alt="" />
|
||||
<span>{{ item.label }}</span>
|
||||
<div
|
||||
:class="[
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<Charts :options="TodayDevOptions"></Charts> </TopCard
|
||||
></a-col>
|
||||
</a-row>
|
||||
<a-row :span="24">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<div class="message-card">
|
||||
<Guide title="设备消息">
|
||||
|
@ -452,6 +452,7 @@ const getEcharts = (data: any) => {
|
|||
_time = '1M';
|
||||
format = 'yyyy年-M月';
|
||||
}
|
||||
|
||||
dashboard([
|
||||
{
|
||||
dashboard: 'device',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<div class="wrapper">
|
||||
<div class="card-header">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="tools">
|
||||
|
@ -13,18 +13,28 @@
|
|||
<a-radio-button value="month">近一月</a-radio-button>
|
||||
<a-radio-button value="year">近一年</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-range-picker v-model:value="dateRange" />
|
||||
<a-range-picker
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
valueFormat="x"
|
||||
v-model:value="dateRange"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div v-if="chartData.length" class="chart" ref="chartRef"></div>
|
||||
<a-empty v-else class="no-data" description="暂无数据"></a-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts';
|
||||
import moment from 'moment';
|
||||
|
||||
// const { proxy } = <any>getCurrentInstance();
|
||||
type Emits = {
|
||||
(e: 'change', data: any): void;
|
||||
};
|
||||
const emits = defineEmits<Emits>();
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
|
@ -34,7 +44,10 @@ const props = defineProps({
|
|||
|
||||
// 统计时间维度
|
||||
const dimension = ref('week');
|
||||
const dateRange = ref<any>([]);
|
||||
const dateRange = ref<any>([
|
||||
moment().subtract(1, 'week').format('x'),
|
||||
moment().format('x'),
|
||||
]);
|
||||
|
||||
/**
|
||||
* 绘制图表
|
||||
|
@ -92,6 +105,7 @@ const createChart = () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
myChart.setOption(options);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
|
@ -101,24 +115,73 @@ const createChart = () => {
|
|||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
() => createChart(),
|
||||
(val) => {
|
||||
console.log('createChart', val);
|
||||
|
||||
createChart();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
watch(
|
||||
() => dateRange.value,
|
||||
(val) => {
|
||||
emits('change', {
|
||||
time: {
|
||||
start: val[0],
|
||||
end: val[1],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
watch(
|
||||
() => dimension.value,
|
||||
(val) => {
|
||||
if (val === 'today') {
|
||||
dateRange[0] = moment().startOf('day').format('x');
|
||||
}
|
||||
if (val === 'week') {
|
||||
dateRange[0] = moment().subtract(1, 'week').format('x');
|
||||
}
|
||||
if (val === 'month') {
|
||||
dateRange[0] = moment().subtract(1, 'month').format('x');
|
||||
}
|
||||
if (val === 'year') {
|
||||
dateRange[0] = moment().subtract(1, 'year').format('x');
|
||||
}
|
||||
dateRange[1] = moment().format('x');
|
||||
emits('change', {
|
||||
time: {
|
||||
start: dateRange[0],
|
||||
end: dateRange[1],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page-container {
|
||||
.wrapper {
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.chart {
|
||||
.chart,
|
||||
.no-data {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 430px);
|
||||
}
|
||||
.no-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,8 +34,12 @@
|
|||
:value="aggPlayingTotal"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<Card title="播放数量(人次)" :chartData="chartData" />
|
||||
<a-col :span="24" class="dash-board-bottom">
|
||||
<Card
|
||||
title="播放数量(人次)"
|
||||
:chartData="chartData"
|
||||
@change="getPlayCount"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
@ -50,6 +54,7 @@ import dashboardApi from '@/api/media/dashboard';
|
|||
import type { Footer } from '@/views/media/DashBoard/typings';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import { timestampFormat } from '@/utils/utils';
|
||||
import moment from 'moment';
|
||||
|
||||
// 设备
|
||||
const deviceFooter = ref<Footer[]>([]);
|
||||
|
@ -114,10 +119,12 @@ const aggTotal = ref(0);
|
|||
const getAggData = () => {
|
||||
dashboardApi.agg().then((res) => {
|
||||
aggTotal.value = res.result.total;
|
||||
aggFooter.value.push({
|
||||
aggFooter.value = [
|
||||
{
|
||||
title: '总时长',
|
||||
value: timestampFormat(res.result.duration),
|
||||
});
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
getAggData();
|
||||
|
@ -128,10 +135,12 @@ const aggPlayingTotal = ref(0);
|
|||
const getAggPlayingData = () => {
|
||||
dashboardApi.aggPlaying().then((res) => {
|
||||
aggTotal.value = res.result.playingTotal;
|
||||
aggPlayingFooter.value.push({
|
||||
aggPlayingFooter.value = [
|
||||
{
|
||||
title: '播放人数',
|
||||
value: res.result.playerTotal,
|
||||
});
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
getAggPlayingData();
|
||||
|
@ -140,9 +149,44 @@ getAggPlayingData();
|
|||
* 获取播放数量(人次)
|
||||
*/
|
||||
const chartData = ref([]);
|
||||
const getPlayCount = async () => {
|
||||
const params = {};
|
||||
dashboardApi.getPlayCount(params).then((res) => {
|
||||
const getPlayCount = async (params: any) => {
|
||||
let _time = '1h';
|
||||
let _limit = 12;
|
||||
const dt = params.time.end - params.time.start;
|
||||
const hour = 60 * 60 * 1000;
|
||||
const day = hour * 24;
|
||||
const month = day * 30;
|
||||
const year = 365 * day;
|
||||
if (dt <= day) {
|
||||
_limit = Math.abs(Math.ceil(dt / hour));
|
||||
} else if (dt > day && dt < year) {
|
||||
_limit = Math.abs(Math.ceil(dt / day));
|
||||
_time = '1d';
|
||||
} else if (dt >= year) {
|
||||
_limit = Math.abs(Math.floor(dt / month));
|
||||
_time = '1M';
|
||||
}
|
||||
dashboardApi
|
||||
.getPlayCount([
|
||||
{
|
||||
dashboard: 'media_stream',
|
||||
object: 'play_count',
|
||||
measurement: 'quantity',
|
||||
dimension: 'agg',
|
||||
group: 'playCount',
|
||||
params: {
|
||||
time: _time,
|
||||
from: moment(Number(params.time.start)).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
to: moment(Number(params.time.end)).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
limit: _limit,
|
||||
},
|
||||
},
|
||||
])
|
||||
.then((res) => {
|
||||
let result: any = [];
|
||||
res.result.forEach((item: any) => {
|
||||
result = [...result, ...item.data];
|
||||
|
@ -153,11 +197,13 @@ const getPlayCount = async () => {
|
|||
}));
|
||||
});
|
||||
};
|
||||
getPlayCount();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
.dash-board-bottom {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,6 @@ export type AggPlaying = {
|
|||
export type Footer = {
|
||||
title: string;
|
||||
value: number | string;
|
||||
status?: "default" | "error" | "success" | "warning" | "processing"
|
||||
status?: "default" | "error" | "success" | "warning" | "processing" | ""
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
.doc {
|
||||
height: 1050px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
color: rgba(#000, 0.8);
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
|
||||
.url {
|
||||
padding: 8px 16px;
|
||||
color: #2f54eb;
|
||||
background-color: rgba(#a7bdf7, 0.2);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 16px 0;
|
||||
color: rgba(#000, 0.85);
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 6px 0;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
<!-- 通知模板详情 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<a-card>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item
|
||||
label="接入方式"
|
||||
v-bind="validateInfos.channel"
|
||||
>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:options="PROVIDER_OPTIONS"
|
||||
:checkStyle="true"
|
||||
:disabled="!!formData.id"
|
||||
v-model="formData.channel"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<!-- <div class="upload-image-warp-logo">
|
||||
<div class="upload-image-border-logo">
|
||||
<a-upload
|
||||
name="file"
|
||||
:action="FILE_UPLOAD"
|
||||
:headers="{
|
||||
[TOKEN_KEY]:
|
||||
LocalStore.get(TOKEN_KEY),
|
||||
}"
|
||||
:showUploadList="false"
|
||||
accept="image/jpeg', 'image/png"
|
||||
>
|
||||
<div
|
||||
class="upload-image-content-logo"
|
||||
>
|
||||
<div
|
||||
class="loading-logo"
|
||||
v-if="form.logoLoading"
|
||||
>
|
||||
<LoadingOutlined
|
||||
style="font-size: 28px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="upload-image"
|
||||
style="height: 100%"
|
||||
v-if="formValue.logo"
|
||||
:style="
|
||||
formValue.logo
|
||||
? `background-image: url(${formValue.logo});`
|
||||
: ''
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
v-if="formValue.logo"
|
||||
class="upload-image-mask"
|
||||
>
|
||||
点击修改
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="form.logoLoading"
|
||||
>
|
||||
<LoadingOutlined
|
||||
style="
|
||||
font-size: 28px;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<PlusOutlined
|
||||
style="
|
||||
font-size: 28px;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div v-if="form.logoLoading">
|
||||
<div class="upload-loading-mask">
|
||||
<LoadingOutlined
|
||||
style="font-size: 28px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="ID"
|
||||
v-bind="validateInfos.id"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.id"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="设备名称"
|
||||
v-bind="validateInfos.name"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item
|
||||
label="所属产品"
|
||||
v-bind="validateInfos.productId"
|
||||
>
|
||||
<div>
|
||||
<a-select
|
||||
v-model:value="formData.productId"
|
||||
placeholder="请选择所属产品"
|
||||
>
|
||||
<!-- <a-select-option
|
||||
v-for="(item, index) in NOTICE_METHOD"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option> -->
|
||||
</a-select>
|
||||
<AIcon type="PlusCircleOutlined" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="接入密码"
|
||||
v-bind="validateInfos['others.access_pwd']"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="formData.others.access_pwd"
|
||||
placeholder="请输入接入密码"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="说明">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
show-count
|
||||
:maxlength="200"
|
||||
:rows="5"
|
||||
placeholder="请输入说明"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 0, span: 3 }">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="btnLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div v-if="1" class="doc" style="height: 800">
|
||||
<h1>1.概述</h1>
|
||||
<div>
|
||||
视频设备通过GB/T28181接入平台整体分为2部分,包括平台端配置和设备端配置,不同的设备端配置的路径或页面存在差异,但配置项基本大同小异。
|
||||
</div>
|
||||
<h1>2.配置说明</h1>
|
||||
<h1>平台端配置</h1>
|
||||
<h2>1、ID</h2>
|
||||
<div>设备唯一标识,请填写设备端配置的设备编号。</div>
|
||||
<h2>2、所属产品</h2>
|
||||
<div>
|
||||
只能选择接入方式为GB/T28281的产品,若当前无对应产品,可点击右侧快速添加按钮,填写产品名称和选择GB/T28181类型的网关完成产品创建
|
||||
</div>
|
||||
<h2>3、接入密码</h2>
|
||||
<div>
|
||||
配置接入密码,设备端配置的密码需与该密码一致。该字段可在产品-设备接入页面进行统一配置,配置后所有设备将继承产品配置。设备单独修改后将脱离继承关系。
|
||||
</div>
|
||||
<h1>设备端配置</h1>
|
||||
<div>
|
||||
各个厂家、不同设备型号的设备端配置页面布局存在差异,但配置项基本大同小异,此处以大华摄像头为例作为接入配置示例
|
||||
</div>
|
||||
<div class="image">
|
||||
<a-image
|
||||
width="100%"
|
||||
:src="getImage('/media/doc1.png')"
|
||||
/>
|
||||
</div>
|
||||
<h2>1、SIP服务器编号/SIP域</h2>
|
||||
<div>
|
||||
SIP服务器编号填入该设备所属产品-接入方式页面“连接信息”的SIP。
|
||||
SIP域通常为SIP服务器编号的前10位。
|
||||
</div>
|
||||
<div class="image">
|
||||
<a-image
|
||||
width="100%"
|
||||
:src="getImage('/media/doc2.png')"
|
||||
/>
|
||||
</div>
|
||||
<h2>2、SIP服务器IP/端口</h2>
|
||||
<div>
|
||||
SIP服务器IP/端口填入该设备所属产品-接入方式页面中“连接信息”的IP/端口。
|
||||
</div>
|
||||
<div class="image">
|
||||
<a-image
|
||||
width="100%"
|
||||
:src="getImage('/media/doc3.png')"
|
||||
/>
|
||||
</div>
|
||||
<h2>3、设备编号</h2>
|
||||
<div>
|
||||
设备编号为设备唯一性标识,物联网平台的设备接入没有校验该字段,输入任意数字均不影响设备接入平台。
|
||||
</div>
|
||||
<h2>4、注册密码</h2>
|
||||
<div>
|
||||
填入该设备所属产品-接入方式页面中“GB28281配置”处的接入密码
|
||||
</div>
|
||||
<div class="image">
|
||||
<a-image
|
||||
width="100%"
|
||||
:src="getImage('/media/doc4.png')"
|
||||
/>
|
||||
</div>
|
||||
<h2>5、其他字段</h2>
|
||||
<div>不影响设备接入平台,可保持设备初始化值。</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="doc" style="height: 600">
|
||||
<h1>1.概述</h1>
|
||||
<div>
|
||||
视频设备通过RTSP、RTMP固定地址接入平台分为2步。
|
||||
</div>
|
||||
<div>1、添加视频设备</div>
|
||||
<div>2、添加视频下的通道地址。</div>
|
||||
<div>
|
||||
注:当前页面为新增视频设备,新增完成后点击设备的“通道”按钮,添加通道。
|
||||
</div>
|
||||
<h1>2.配置说明</h1>
|
||||
<h2>1、ID</h2>
|
||||
<div>
|
||||
设备唯一标识,若不填写,系统将自动生成唯一标识。
|
||||
</div>
|
||||
<h2>2、所属产品</h2>
|
||||
<div>
|
||||
只能选择接入方式为固定地址的产品,若当前无对应产品,可点击右侧快速添加按钮,填写产品名称和选择固定地址类型的网关完成产品创建。
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import templateApi from '@/api/notice/template';
|
||||
|
||||
import { FILE_UPLOAD } from '@/api/comm';
|
||||
import { LocalStore } from '@/utils/comm';
|
||||
import { TOKEN_KEY } from '@/utils/variable';
|
||||
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const useForm = Form.useForm;
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
channel: 'gb28181-2016',
|
||||
photoUrl: '',
|
||||
productId: '',
|
||||
others: {
|
||||
access_pwd: '',
|
||||
},
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 验证规则
|
||||
const formRules = ref({
|
||||
id: [
|
||||
{ required: true, message: '请输入ID' },
|
||||
{ max: 64, message: '最多输入64个字符' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_\-]+$/,
|
||||
message: '请输入英文或者数字或者-或者_',
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称' },
|
||||
{ max: 64, message: '最多可输入64个字符' },
|
||||
],
|
||||
productId: [{ required: true, message: '请选择所属产品' }],
|
||||
channel: [{ required: true, message: '请选择接入方式' }],
|
||||
'others.access_pwd': [{ required: true, message: '请输入接入密码' }],
|
||||
description: [{ max: 200, message: '最多可输入200个字符' }],
|
||||
});
|
||||
|
||||
const { resetFields, validate, validateInfos, clearValidate } = useForm(
|
||||
formData.value,
|
||||
formRules.value,
|
||||
);
|
||||
|
||||
const clearValid = () => {
|
||||
setTimeout(() => {
|
||||
formData.value.variableDefinitions = [];
|
||||
clearValidate();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
const getDetail = async () => {
|
||||
const res = await templateApi.detail(route.params.id as string);
|
||||
// console.log('res: ', res);
|
||||
formData.value = res.result;
|
||||
// console.log('formData.value: ', formData.value);
|
||||
};
|
||||
// getDetail();
|
||||
|
||||
/**
|
||||
* 表单提交
|
||||
*/
|
||||
const btnLoading = ref<boolean>(false);
|
||||
const handleSubmit = () => {
|
||||
// console.log('formData.value: ', formData.value);
|
||||
validate()
|
||||
.then(async () => {
|
||||
btnLoading.value = true;
|
||||
let res;
|
||||
if (!formData.value.id) {
|
||||
res = await templateApi.save(formData.value);
|
||||
} else {
|
||||
res = await templateApi.update(formData.value);
|
||||
}
|
||||
// console.log('res: ', res);
|
||||
if (res?.success) {
|
||||
message.success('保存成功');
|
||||
router.back();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err: ', err);
|
||||
})
|
||||
.finally(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './index.less';
|
||||
.page-container {
|
||||
background: #f0f2f5;
|
||||
padding: 24px;
|
||||
.upload-image-warp-logo {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
.upload-image-border-logo {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px dashed #d9d9d9;
|
||||
transition: all 0.3s;
|
||||
width: 160px;
|
||||
height: 150px;
|
||||
&:hover {
|
||||
border: 1px dashed #1890ff;
|
||||
display: flex;
|
||||
}
|
||||
.upload-image-content-logo {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160px;
|
||||
height: 150px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
.loading-logo {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
}
|
||||
.loading-icon {
|
||||
position: absolute;
|
||||
}
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: cover;
|
||||
}
|
||||
.upload-image-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: inherit;
|
||||
}
|
||||
.upload-image-mask {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
&:hover .upload-image-mask {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,4 @@
|
|||
export const PROVIDER_OPTIONS = [
|
||||
{ label: '固定地址', value: 'fixed-media' },
|
||||
{ label: 'GB/T28181', value: 'gb28181-2016' },
|
||||
]
|
|
@ -0,0 +1,332 @@
|
|||
<template>
|
||||
<div class="page-container">
|
||||
<Search
|
||||
:columns="columns"
|
||||
target="notice-config"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<JTable
|
||||
ref="listRef"
|
||||
:columns="columns"
|
||||
:request="DeviceApi.list"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="handleAdd"> 新增 </a-button>
|
||||
</template>
|
||||
<template #card="slotProps">
|
||||
<CardBox
|
||||
:value="slotProps"
|
||||
:actions="getActions(slotProps, 'card')"
|
||||
v-bind="slotProps"
|
||||
:showStatus="true"
|
||||
:status="
|
||||
slotProps.state.value === 'online' ? 'success' : 'error'
|
||||
"
|
||||
:statusText="slotProps.state.text"
|
||||
:statusNames="{ success: 'success', error: 'error' }"
|
||||
>
|
||||
<template #img>
|
||||
<slot name="img">
|
||||
<img :src="getImage('/device-media.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>{{ slotProps.manufacturer }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
通道数量
|
||||
</div>
|
||||
<div>{{ slotProps.channelNumber }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">型号</div>
|
||||
<div>{{ slotProps.model }}</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="card-item-content-text">
|
||||
接入方式
|
||||
</div>
|
||||
<div>
|
||||
{{ providerType[slotProps.provider] }}
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<template #actions="item">
|
||||
<a-tooltip
|
||||
v-bind="item.tooltip"
|
||||
:title="item.disabled && item.tooltip.title"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="item.popConfirm"
|
||||
v-bind="item.popConfirm"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<a-button :disabled="item.disabled">
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<template v-else>
|
||||
<a-button
|
||||
:disabled="item.disabled"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<AIcon :type="item.icon" />
|
||||
<span>{{ item.text }}</span>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip
|
||||
v-for="i in getActions(slotProps, 'table')"
|
||||
:key="i.key"
|
||||
v-bind="i.tooltip"
|
||||
>
|
||||
<a-popconfirm
|
||||
v-if="i.popConfirm"
|
||||
v-bind="i.popConfirm"
|
||||
:disabled="i.disabled"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
v-else
|
||||
@click="i.onClick && i.onClick(slotProps)"
|
||||
>
|
||||
<a-button
|
||||
:disabled="i.disabled"
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
><AIcon :type="i.icon"
|
||||
/></a-button>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DeviceApi from '@/api/media/device';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
|
||||
|
||||
const providerType = {
|
||||
'gb28181-2016': 'GB/T28181',
|
||||
'fixed-media': '固定地址',
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const listRef = ref<Record<string, any>>({});
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '接入方式',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: PROVIDER_OPTIONS,
|
||||
handleValue: (v: any) => {
|
||||
return '123';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '通道数量',
|
||||
dataIndex: 'channelNumber',
|
||||
key: 'channelNumber',
|
||||
},
|
||||
{
|
||||
title: '厂商',
|
||||
dataIndex: 'manufacturer',
|
||||
key: 'manufacturer',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'productId',
|
||||
key: 'productId',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '固定地址', value: 'fixed-media' },
|
||||
{ label: 'GB/T28181', value: 'gb28181-2016' },
|
||||
],
|
||||
handleValue: (v: any) => {
|
||||
return '123';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
scopedSlots: true,
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '禁用', value: 'notActive' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '在线', value: 'online' },
|
||||
],
|
||||
handleValue: (v: any) => {
|
||||
return '123';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 250,
|
||||
scopedSlots: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
// console.log('handleSearch:', e);
|
||||
params.value = e;
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
router.push(`/media/device/Save`);
|
||||
};
|
||||
|
||||
const getActions = (
|
||||
data: Partial<Record<string, any>>,
|
||||
type: 'card' | 'table',
|
||||
): ActionsType[] => {
|
||||
if (!data) return [];
|
||||
const actions = [
|
||||
{
|
||||
key: 'edit',
|
||||
text: '编辑',
|
||||
tooltip: {
|
||||
title: '编辑',
|
||||
},
|
||||
icon: 'EditOutlined',
|
||||
onClick: () => {
|
||||
router.push(`/media/device/Save?id=${data.id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
text: '查看通道',
|
||||
tooltip: {
|
||||
title: '查看通道',
|
||||
},
|
||||
icon: 'PartitionOutlined',
|
||||
onClick: () => {
|
||||
router.push(
|
||||
`/media/device/Channel?id=${data.id}&type=${data.provider}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'debug',
|
||||
text: '更新通道',
|
||||
tooltip: {
|
||||
title:
|
||||
data.provider === 'fixed-media'
|
||||
? '固定地址无法更新通道'
|
||||
: data.state.value === 'offline'
|
||||
? '设备已离线'
|
||||
: data.state.value === 'notActive'
|
||||
? '设备已禁用'
|
||||
: '',
|
||||
},
|
||||
disabled:
|
||||
data.state.value === 'offline' ||
|
||||
data.state.value === 'notActive' ||
|
||||
data.provider === 'fixed-media',
|
||||
icon: 'SyncOutlined',
|
||||
onClick: () => {
|
||||
// updateChannel()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
text: '删除',
|
||||
tooltip: {
|
||||
title: '在线设备无法删除',
|
||||
},
|
||||
disabled: data.state.value === 'online',
|
||||
popConfirm: {
|
||||
title: '确认删除?',
|
||||
onConfirm: async () => {
|
||||
const resp = await DeviceApi.del(data.id);
|
||||
if (resp.status === 200) {
|
||||
message.success('操作成功!');
|
||||
listRef.value?.reload();
|
||||
} else {
|
||||
message.error('操作失败!');
|
||||
}
|
||||
},
|
||||
},
|
||||
icon: 'DeleteOutlined',
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.page-container {
|
||||
background: #f0f2f5;
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
type BaseItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
type State = {
|
||||
value: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type DeviceItem = {
|
||||
photoUrl?: string;
|
||||
channelNumber: number;
|
||||
createTime: number;
|
||||
firmware: string;
|
||||
gatewayId: string;
|
||||
host: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
port: number;
|
||||
provider: string;
|
||||
state: State;
|
||||
streamMode: string;
|
||||
transport: string;
|
||||
} & BaseItem;
|
|
@ -36,10 +36,10 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
const getData = () => {
|
||||
homeApi.deviceCount().then((resp) => {
|
||||
homeApi.deviceCount({}).then((resp) => {
|
||||
deviceCount.value = resp.result;
|
||||
});
|
||||
homeApi.channelCount().then((resp) => {
|
||||
homeApi.channelCount({}).then((resp) => {
|
||||
channelCount.value = resp.result;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="chart" ref="chart"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const { proxy } = <any>getCurrentInstance();
|
||||
|
||||
const props = defineProps({
|
||||
// 图表数据
|
||||
options:{
|
||||
type:Object,
|
||||
default:()=>{}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 绘制图表
|
||||
*/
|
||||
const createChart = () => {
|
||||
nextTick(() => {
|
||||
const myChart = echarts.init(proxy.$refs.chart);
|
||||
myChart.setOption(props.options);
|
||||
window.addEventListener('resize', function () {
|
||||
myChart.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => createChart(),
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="home-title">
|
||||
<div v-if="title">{{ title }}</div>
|
||||
<div v-else class="title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="extra-text">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="home-title-english">{{ english }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Guide">
|
||||
interface guideProps {
|
||||
title?: string;
|
||||
english?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<guideProps>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.home-title {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 18px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: @primary-color;
|
||||
border: 1px solid #b4c0da;
|
||||
transform: translateY(-50%);
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.extra-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.title{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-title-english {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div class="new-alarm">
|
||||
<div class="title">最新警告</div>
|
||||
<div v-if="alarmList.length" class="new-alarm-items">
|
||||
<ul>
|
||||
<li v-for="item in alarmList.slice(0, 3)" :key="item">
|
||||
<div class="new-alarm-item">
|
||||
<div class="new-alarm-item-time">
|
||||
<img
|
||||
:src="getImage('/alarm/bashboard.png')"
|
||||
alt=""
|
||||
/>{{
|
||||
moment(item.alarmTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="new-alarm-item-content">
|
||||
<a-tooltip
|
||||
:title="item.alarmName"
|
||||
placement="topLeft"
|
||||
>
|
||||
<a>{{ item.alarmName }}</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="new-alarm-item-state">
|
||||
<a-badge
|
||||
:status="
|
||||
item.state?.value === 'warning'
|
||||
? 'error'
|
||||
: 'default'
|
||||
"
|
||||
>
|
||||
</a-badge>
|
||||
<span
|
||||
:class="
|
||||
item.state?.value === 'warning'
|
||||
? 'error'
|
||||
: 'default'
|
||||
"
|
||||
>
|
||||
{{ item.state?.text }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'new-alarm-item-level',
|
||||
`level-${item.level}`,
|
||||
]"
|
||||
>
|
||||
{{ item.levelName }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="empty-body">
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE"></a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Empty } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import moment from 'moment';
|
||||
const props = defineProps({
|
||||
alarmList: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.new-alarm {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e4e8;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.new-alarm-items {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.new-alarm-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 18px 0;
|
||||
font-size: 12px;
|
||||
.new-alarm-item-time {
|
||||
width: 180px;
|
||||
font-size: 14px;
|
||||
|
||||
> img {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.new-alarm-item-content {
|
||||
width: ~'calc(100% - 360px)';
|
||||
}
|
||||
.new-alarm-item-state {
|
||||
width: 90px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
.error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
.default {
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
.new-alarm-item-level {
|
||||
width: 52px;
|
||||
padding: 2px 8px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
|
||||
&.level-1 {
|
||||
background-color: #e50012;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
background-color: #ff9457;
|
||||
}
|
||||
|
||||
&.level-3 {
|
||||
background-color: #fabd47;
|
||||
}
|
||||
|
||||
&.level-4 {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
&.level-5 {
|
||||
background-color: #bbb;
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty-body {
|
||||
height: 142px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-radio-group
|
||||
v-if="quickBtn"
|
||||
default-value="today"
|
||||
button-style="solid"
|
||||
v-model:value="radioValue"
|
||||
@change="(e) => handleBtnChange(e.target.value)"
|
||||
>
|
||||
<a-radio-button
|
||||
v-for="item in quickBtnList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-range-picker
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
style="margin-left: 12px"
|
||||
@change="rangeChange"
|
||||
v-model:value="rangeVal"
|
||||
:allowClear="false"
|
||||
>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import moment from 'moment';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
interface BtnOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EmitProps {
|
||||
(e: 'change', data: Record<string, any>): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<EmitProps>();
|
||||
|
||||
const props = defineProps({
|
||||
// 显示快捷按钮
|
||||
quickBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 快捷按钮列表
|
||||
quickBtnList: {
|
||||
type: Array as PropType<BtnOptions[]>,
|
||||
default: [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '近一周', value: 'week' },
|
||||
{ label: '近一月', value: 'month' },
|
||||
{ label: '近一年', value: 'year' },
|
||||
],
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'today',
|
||||
},
|
||||
});
|
||||
|
||||
const radioValue = ref(props.type || 'week' || undefined);
|
||||
const rangeVal = ref<[string, string]>();
|
||||
|
||||
const rangeChange = (val: any) => {
|
||||
radioValue.value = undefined;
|
||||
emit('change', {
|
||||
start: moment(val[0]).valueOf(),
|
||||
end: moment(val[1]).valueOf(),
|
||||
type: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const getTimeByType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'hour':
|
||||
return moment().subtract(1, 'hours').valueOf();
|
||||
case 'week':
|
||||
return moment().subtract(6, 'days').valueOf();
|
||||
case 'month':
|
||||
return moment().subtract(29, 'days').valueOf();
|
||||
case 'year':
|
||||
return moment().subtract(365, 'days').valueOf();
|
||||
default:
|
||||
return moment().startOf('day').valueOf();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBtnChange = (val: string) => {
|
||||
radioValue.value = val;
|
||||
let endTime = moment(new Date()).valueOf();
|
||||
let startTime = getTimeByType(val);
|
||||
if (val === 'yesterday') {
|
||||
startTime = moment().subtract(1, 'days').startOf('day').valueOf();
|
||||
endTime = moment().subtract(1, 'days').endOf('day').valueOf();
|
||||
}
|
||||
rangeVal.value = [
|
||||
moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
];
|
||||
emit('change', {
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
type: val,
|
||||
});
|
||||
};
|
||||
handleBtnChange(radioValue.value);
|
||||
watch(
|
||||
() => radioValue.value,
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="top-card">
|
||||
<div class="top-card-content">
|
||||
<div class="content-left">
|
||||
<div class="content-left-title">
|
||||
<span>{{ title }}</span>
|
||||
<a-tooltip placement="top" v-if="tooltip">
|
||||
<template #title>
|
||||
<span>{{ tooltip }}</span>
|
||||
</template>
|
||||
<AIcon type="QuestionCircleOutlined" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="content-left-value">{{ value }}</div>
|
||||
</div>
|
||||
<div class="content-right" v-if="img">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
<div class="content-right-echart" v-else>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-card-footer">
|
||||
<template v-for="(item, index) in footer" :key="index">
|
||||
<span v-if="!item.status">{{ item.title }}</span>
|
||||
<a-badge v-else :text="item.title" :status="item.status" />
|
||||
<div class="footer-item-value">{{ item.value }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import type { Footer } from '@/views/device/DashBoard/typings'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
tooltip: { type: String, default: '' },
|
||||
img: { type: String, default: '' },
|
||||
footer: { type: Array as PropType<Footer[]>, default: '' },
|
||||
value: { type: Number, default: 0 },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.top-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// height: 200px;
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e4e8;
|
||||
border-radius: 2px;
|
||||
.top-card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
.content-left {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
&-title {
|
||||
color: rgba(0, 0, 0, 0.64);
|
||||
}
|
||||
&-value {
|
||||
padding: 12px 0;
|
||||
color: #323130;
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
.content-right {
|
||||
width: 0;
|
||||
height: 123px;
|
||||
display: flex;
|
||||
flex-grow: .7;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.content-right-echart{
|
||||
height: 123px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.top-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
.footer-item-value {
|
||||
color: #323130;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,547 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<div class="DashBoardBox">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="今日告警"
|
||||
:value="state.today"
|
||||
:footer="currentMonAlarm"
|
||||
>
|
||||
<Charts :options="state.fifteenOptions"></Charts>
|
||||
</TopCard>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<TopCard
|
||||
title="告警配置"
|
||||
:value="state.config"
|
||||
:footer="alarmState"
|
||||
:img="getImage('/device/device-number.png')"
|
||||
></TopCard>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<NewAlarm :alarm-list="state.alarmList"></NewAlarm>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<div class="alarm-card">
|
||||
<Guide>
|
||||
<template #title>
|
||||
<span style="margin-right: 24px">告警统计</span>
|
||||
<a-select
|
||||
style="width: 40%"
|
||||
v-model:value="queryCodition.targetType"
|
||||
:options="
|
||||
isNoCommunity ? selectOpt1 : selectOpt2
|
||||
"
|
||||
@change="selectChange"
|
||||
></a-select>
|
||||
</template>
|
||||
<template #extra>
|
||||
<TimeSelect
|
||||
key="flow-static"
|
||||
:type="'week'"
|
||||
:quickBtnList="quickBtnList"
|
||||
@change="initQueryTime"
|
||||
/>
|
||||
</template>
|
||||
</Guide>
|
||||
<div class="alarmBox">
|
||||
<div class="alarmStatistics-chart">
|
||||
<Charts
|
||||
:options="alarmStatisticsOption"
|
||||
></Charts>
|
||||
</div>
|
||||
<div class="alarmRank">
|
||||
<h4>告警排名</h4>
|
||||
<ul v-if="state.ranking.length" class="rankingList">
|
||||
<li v-for="(item,i) in state.ranking" :key="item.targetId">
|
||||
<img :src="getImage(`/rule-engine/dashboard/ranking/${i+1}.png`)" alt="">
|
||||
<span class="rankingItemTitle" :title="item.targetName">{{item.targetName}}</span>
|
||||
<span class="rankingItemValue">{{item.count}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="empty-body">
|
||||
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE"></a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Empty } from 'ant-design-vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import Charts from './components/Charts.vue';
|
||||
import TopCard from './components/TopCard.vue';
|
||||
import NewAlarm from './components/NewAlarm.vue';
|
||||
import TimeSelect from './components/TimeSelect.vue';
|
||||
import Guide from './components/Guide.vue';
|
||||
import encodeQuery from '@/utils/encodeQuery';
|
||||
import type { SelectTypes } from 'ant-design-vue/es/select';
|
||||
import type { Footer } from '@/views/rule-engine/DashBoard/typings';
|
||||
import { isNoCommunity } from '@/utils/utils';
|
||||
import {
|
||||
dashboard,
|
||||
getAlarm,
|
||||
getAlarmConfigCount,
|
||||
getAlarmLevel,
|
||||
} from '@/api/rule-engine/dashboard';
|
||||
import moment from 'moment';
|
||||
let currentMonAlarm = ref<Footer[]>([
|
||||
{
|
||||
title: '当月告警',
|
||||
value: 0,
|
||||
status: 'success',
|
||||
},
|
||||
]);
|
||||
let alarmState = ref<Footer[]>([
|
||||
{
|
||||
title: '正常',
|
||||
value: 0,
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
title: '禁用',
|
||||
value: 0,
|
||||
status: 'error',
|
||||
},
|
||||
]);
|
||||
const selectOpt1 = ref<Object[]>([
|
||||
{ label: '设备', value: 'device' },
|
||||
{ label: '产品', value: 'product' },
|
||||
{ label: '组织', value: 'org' },
|
||||
{ label: '其它', value: 'other' },
|
||||
]);
|
||||
const selectOpt2 = ref<SelectTypes['options']>([
|
||||
{ label: '设备', value: 'device' },
|
||||
{ label: '产品', value: 'product' },
|
||||
{ label: '其它', value: 'other' },
|
||||
]);
|
||||
let queryCodition = reactive({
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
targetType: 'device',
|
||||
});
|
||||
let alarmStatisticsOption = ref<any>({});
|
||||
const quickBtnList = [
|
||||
{ label: '昨日', value: 'yesterday' },
|
||||
{ label: '近一周', value: 'week' },
|
||||
{ label: '近一月', value: 'month' },
|
||||
{ label: '近一年', value: 'year' },
|
||||
];
|
||||
type DashboardItem = {
|
||||
group: string;
|
||||
data: Record<string, any>;
|
||||
};
|
||||
let state = reactive<{
|
||||
today: number;
|
||||
thisMonth: number;
|
||||
config: number;
|
||||
enabledConfig: number;
|
||||
disabledConfig: number;
|
||||
alarmList: any[];
|
||||
ranking: { targetId: string; targetName: string; count: number }[];
|
||||
fifteenOptions: any;
|
||||
}>({
|
||||
today: 0,
|
||||
thisMonth: 0,
|
||||
config: 0,
|
||||
enabledConfig: 0,
|
||||
disabledConfig: 0,
|
||||
alarmList: [],
|
||||
ranking: [],
|
||||
fifteenOptions: {},
|
||||
});
|
||||
// 今日告警
|
||||
const today = {
|
||||
dashboard: 'alarm',
|
||||
object: 'record',
|
||||
measurement: 'trend',
|
||||
dimension: 'agg',
|
||||
group: 'today',
|
||||
params: {
|
||||
time: '1d',
|
||||
// targetType: 'device',
|
||||
format: 'HH:mm:ss',
|
||||
from: moment(new Date(new Date().setHours(0, 0, 0, 0))).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
to: 'now',
|
||||
// limit: 24,
|
||||
},
|
||||
};
|
||||
// 当月告警
|
||||
const thisMonth = {
|
||||
dashboard: 'alarm',
|
||||
object: 'record',
|
||||
measurement: 'trend',
|
||||
dimension: 'agg',
|
||||
group: 'thisMonth',
|
||||
params: {
|
||||
time: '1M',
|
||||
// targetType: 'device',
|
||||
format: 'yyyy-MM',
|
||||
limit: 1,
|
||||
from: 'now-1M',
|
||||
},
|
||||
};
|
||||
|
||||
const fifteen = {
|
||||
dashboard: 'alarm',
|
||||
object: 'record',
|
||||
measurement: 'trend',
|
||||
dimension: 'agg',
|
||||
group: '15day',
|
||||
params: {
|
||||
time: '1d',
|
||||
format: 'yyyy-MM-dd',
|
||||
// targetType: 'product',
|
||||
from: 'now-15d',
|
||||
to: 'now',
|
||||
limit: 15,
|
||||
},
|
||||
};
|
||||
const getDashBoard = () => {
|
||||
dashboard([today, thisMonth, fifteen]).then((res) => {
|
||||
if (res.status == 200) {
|
||||
const _data = res.result as DashboardItem[];
|
||||
state.today = _data.find(
|
||||
(item) => item.group === 'today',
|
||||
)?.data.value;
|
||||
state.thisMonth = _data.find(
|
||||
(item) => item.group === 'thisMonth',
|
||||
)?.data.value;
|
||||
currentMonAlarm.value[0].value = state.thisMonth;
|
||||
const fifteenData = _data
|
||||
.filter((item) => item.group === '15day')
|
||||
.map((item) => item.data)
|
||||
.sort((a, b) => b.timestamp - a.timestamp);
|
||||
state.fifteenOptions = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: fifteenData.map((item) => item.timeString),
|
||||
show: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
top: '2%',
|
||||
bottom: 0,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数',
|
||||
data: fifteenData.map((item) => item.value),
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: '#2F54EB',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
getDashBoard();
|
||||
const getAlarmConfig = async () => {
|
||||
const countRes = await getAlarmConfigCount({});
|
||||
const enabeldRes = await getAlarmConfigCount({
|
||||
terms: [
|
||||
{
|
||||
column: 'state',
|
||||
value: 'enabled',
|
||||
},
|
||||
],
|
||||
});
|
||||
const disableRes = await getAlarmConfigCount({
|
||||
terms: [
|
||||
{
|
||||
column: 'state',
|
||||
value: 'disabled',
|
||||
},
|
||||
],
|
||||
});
|
||||
if (countRes.status == 200) {
|
||||
state.config = countRes.result;
|
||||
}
|
||||
if (enabeldRes.status == 200) {
|
||||
state.enabledConfig = enabeldRes.result;
|
||||
alarmState.value[0].value = state.enabledConfig;
|
||||
}
|
||||
if (disableRes.status == 200) {
|
||||
state.disabledConfig = disableRes.result;
|
||||
alarmState.value[1].value = state.disabledConfig;
|
||||
}
|
||||
};
|
||||
getAlarmConfig();
|
||||
const getCurrentAlarm = async () => {
|
||||
const alarmLevel: any = await getAlarmLevel();
|
||||
const sorts = { alarmTime: 'desc' };
|
||||
const currentAlarm: any = await getAlarm(encodeQuery({ sorts }));
|
||||
if (currentAlarm.status === 200) {
|
||||
if (alarmLevel.status === 200) {
|
||||
const levels = alarmLevel.result.levels;
|
||||
state.alarmList = currentAlarm.result?.data
|
||||
.filter((i: any) => i?.state?.value === 'warning')
|
||||
.map((item: { level: any }) => ({
|
||||
...item,
|
||||
levelName: levels.find((l: any) => l.level === item.level)
|
||||
?.title,
|
||||
}));
|
||||
} else {
|
||||
state.alarmList = currentAlarm.result?.data.filter(
|
||||
(item: any) => item?.state?.value === 'warning',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
getCurrentAlarm();
|
||||
//初始化查询条件
|
||||
const initQueryTime = (data: any) => {
|
||||
queryCodition.startTime = data.start;
|
||||
queryCodition.endTime = data.end;
|
||||
console.log(queryCodition);
|
||||
selectChange();
|
||||
};
|
||||
const selectChange = () => {
|
||||
let time = '1h';
|
||||
let format = 'HH';
|
||||
let limit = 12;
|
||||
const dt = queryCodition.endTime - queryCodition.startTime;
|
||||
const hour = 60 * 60 * 1000;
|
||||
const day = hour * 24;
|
||||
const month = day * 30;
|
||||
const year = 365 * day;
|
||||
if (dt <= day) {
|
||||
limit = Math.abs(Math.ceil(dt / hour));
|
||||
} else if (dt > day && dt < year) {
|
||||
limit = Math.abs(Math.ceil(dt / day)) + 1;
|
||||
time = '1d';
|
||||
format = 'M月dd日';
|
||||
} else if (dt >= year) {
|
||||
limit = Math.abs(Math.floor(dt / month));
|
||||
time = '1M';
|
||||
format = 'yyyy年-M月';
|
||||
}
|
||||
|
||||
// 告警趋势
|
||||
const chartData = {
|
||||
dashboard: 'alarm',
|
||||
object: 'record',
|
||||
measurement: 'trend',
|
||||
dimension: 'agg',
|
||||
group: 'alarmTrend',
|
||||
params: {
|
||||
targetType: queryCodition.targetType, // product、device、org、other
|
||||
format: format,
|
||||
time: time,
|
||||
// from: 'now-1y', // now-1d、now-1w、now-1M、now-1y
|
||||
// to: 'now',
|
||||
limit: limit, // 12
|
||||
// time: params.time.type === 'today' ? '1h' : '1d',
|
||||
from: moment(queryCodition.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
to: moment(queryCodition.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
// limit: 30,
|
||||
},
|
||||
};
|
||||
|
||||
// 告警排名
|
||||
const order = {
|
||||
dashboard: 'alarm',
|
||||
object: 'record',
|
||||
measurement: 'rank',
|
||||
dimension: 'agg',
|
||||
group: 'alarmRank',
|
||||
params: {
|
||||
// time: '1h',
|
||||
time: time,
|
||||
targetType: queryCodition.targetType,
|
||||
from: moment(queryCodition.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
to: moment(queryCodition.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
limit: 9,
|
||||
},
|
||||
};
|
||||
let tip = '其它';
|
||||
if (queryCodition.targetType === 'device') {
|
||||
tip = '设备';
|
||||
} else if (queryCodition.targetType === 'product') {
|
||||
tip = '产品';
|
||||
} else if (queryCodition.targetType === 'org') {
|
||||
tip = '组织';
|
||||
}
|
||||
// 网络请求
|
||||
dashboard([chartData, order]).then((res) => {
|
||||
if (res.status == 200) {
|
||||
const xData: string[] = [];
|
||||
const sData: number[] = [];
|
||||
res.result
|
||||
.filter((item: any) => item.group === 'alarmTrend')
|
||||
.forEach((item: any) => {
|
||||
xData.push(item.data.timeString);
|
||||
sData.push(item.data.value);
|
||||
});
|
||||
alarmStatisticsOption.value = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData.reverse(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// axisPointer: {
|
||||
// type: 'shadow',
|
||||
// },
|
||||
},
|
||||
grid: {
|
||||
top: '2%',
|
||||
bottom: '5%',
|
||||
left: '24px',
|
||||
right: '48px',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: tip,
|
||||
data: sData.reverse(),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: '#685DEB',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#685DEB', // 100% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#FFFFFF', // 0% 处的颜色
|
||||
},
|
||||
],
|
||||
global: false, // 缺省为 false
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
state.ranking = res.result
|
||||
?.filter((item: any) => item.group === 'alarmRank')
|
||||
.map((d: { data: { value: any } }) => d.data?.value)
|
||||
.sort(
|
||||
(a: { count: number }, b: { count: number }) =>
|
||||
b.count - a.count,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.alarm-card {
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.alarmBox {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
.alarmStatistics-chart {
|
||||
width: 70%;
|
||||
height: 500px;
|
||||
}
|
||||
.alarmRank {
|
||||
position: relative;
|
||||
width: 30%;
|
||||
padding-left: 48px;
|
||||
}
|
||||
}
|
||||
.rankingList {
|
||||
margin: 25px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
zoom: 1;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: table;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
span {
|
||||
//color: red;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.rankingItemNumber {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 1.5px;
|
||||
margin-right: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
background-color: #edf0f3;
|
||||
border-radius: 20px;
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: #314659;
|
||||
}
|
||||
}
|
||||
|
||||
.rankingItemTitle {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
padding-left: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty-body {
|
||||
height: 490px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
// height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,5 @@
|
|||
export type Footer = {
|
||||
title: string;
|
||||
value: number | string;
|
||||
status?: "default" | "error" | "success" | "warning" | "processing"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<page-container>
|
||||
<a-card>
|
||||
<div>
|
||||
<Search :columns="query.columns" target="device-instance" @search="handleSearch"></Search>
|
||||
<JTable
|
||||
:columns="columns"
|
||||
|
@ -147,7 +147,7 @@
|
|||
:title="title"
|
||||
@success="refresh"
|
||||
/>
|
||||
</a-card>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
|
@ -209,8 +209,8 @@ const columns = [
|
|||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'describe',
|
||||
key: 'describe',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<a-checkbox-group v-model:value="bulkList" :options="options" />
|
||||
</div>
|
||||
|
||||
<Search :columns="query.columns" @search="query.search" />
|
||||
<Search :columns="props.queryColumns" @search="query.search" />
|
||||
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
|
@ -118,6 +118,7 @@ import { message } from 'ant-design-vue';
|
|||
|
||||
const emits = defineEmits(['confirm']);
|
||||
const props = defineProps<{
|
||||
queryColumns: any[];
|
||||
parentId: string;
|
||||
allPermission: dictType;
|
||||
assetType: 'product' | 'device';
|
||||
|
@ -139,7 +140,6 @@ const dialog = {
|
|||
permission: item.selectPermissions,
|
||||
}));
|
||||
|
||||
// console.log(params);
|
||||
dialog.loading.value = true;
|
||||
bindDeviceOrProductList_api(props.assetType, params)
|
||||
.then(() => {
|
||||
|
@ -334,6 +334,14 @@ const table: any = {
|
|||
data.forEach((item) => {
|
||||
item.permissionList = permissionObj[item.id];
|
||||
item.selectPermissions = ['read'];
|
||||
|
||||
// 产品的状态进行转换处理
|
||||
if(props.assetType === 'product') {
|
||||
item.state = {
|
||||
value: item.state === 1 ? 'online': item.state === 0 ? 'offline': '',
|
||||
text: item.state === 1 ? '正常': item.state === 0 ? '禁用': ''
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resolve({
|
||||
|
|
|
@ -117,8 +117,8 @@ const form = reactive({
|
|||
form.loading = true;
|
||||
const api = form.data.id ? updateDepartment_api : addDepartment_api;
|
||||
api(form.data)
|
||||
.then(() => {
|
||||
emits('refresh');
|
||||
.then((resp:any) => {
|
||||
emits('refresh',resp.result.id);
|
||||
dialog.changeVisible(false);
|
||||
})
|
||||
.finally(() => (form.loading = false));
|
||||
|
|
|
@ -46,7 +46,6 @@ const dialog = {
|
|||
},
|
||||
// 控制弹窗的打开与关闭
|
||||
changeVisible: (ids: string[], permissionList: string[]) => {
|
||||
console.log(ids, permissionList);
|
||||
form.permission = [...permissionList];
|
||||
form.assetIdList = ids;
|
||||
options.value = setOptions(permissionList);
|
||||
|
|
|
@ -10,9 +10,17 @@
|
|||
<search-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button type="primary" @click="openDialog" class="add-btn">
|
||||
<div class="add-btn">
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
class="add-btn"
|
||||
:uhasPermission="`${permission}:add`"
|
||||
@click="openDialog()"
|
||||
>
|
||||
新增
|
||||
</a-button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
|
||||
<a-tree
|
||||
:tree-data="treeData"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
|
@ -21,14 +29,14 @@
|
|||
>
|
||||
<template #title="{ name, data }">
|
||||
<span>{{ name }}</span>
|
||||
<span class="func-btns">
|
||||
<a-tooltip>
|
||||
<span class="func-btns" @click="(e) => e.stopPropagation()">
|
||||
<!-- <a-tooltip>
|
||||
<template #title>编辑</template>
|
||||
<a-button style="padding: 0" type="link">
|
||||
<edit-outlined @click="openDialog(data)" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
</a-tooltip> -->
|
||||
<!-- <a-tooltip>
|
||||
<template #title>新增子组织</template>
|
||||
<a-button style="padding: 0" type="link">
|
||||
<plus-circle-outlined
|
||||
|
@ -42,9 +50,9 @@
|
|||
"
|
||||
/>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-tooltip> -->
|
||||
|
||||
<a-popconfirm
|
||||
<!-- <a-popconfirm
|
||||
title="确认删除"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
|
@ -56,7 +64,45 @@
|
|||
<delete-outlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</a-popconfirm> -->
|
||||
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:update`"
|
||||
type="link"
|
||||
:tooltip="{
|
||||
title: '新增子组织',
|
||||
}"
|
||||
@click="openDialog(data)"
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:add`"
|
||||
type="link"
|
||||
:tooltip="{
|
||||
title: '新增子组织',
|
||||
}"
|
||||
@click="
|
||||
openDialog({
|
||||
...data,
|
||||
id: '',
|
||||
parentId: data.id,
|
||||
})
|
||||
"
|
||||
>
|
||||
<AIcon type="PlusCircleOutlined" />
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
type="link"
|
||||
:uhasPermission="`${permission}:delete`"
|
||||
:tooltip="{ title: '删除' }"
|
||||
:popConfirm="{
|
||||
title: `确定要删除吗`,
|
||||
onConfirm: () => delDepartment(data.id),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</PermissionButton>
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
|
@ -65,38 +111,32 @@
|
|||
<EditDepartmentDialog
|
||||
:tree-data="sourceTree"
|
||||
ref="editDialogRef"
|
||||
@refresh="getTree"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import { getTreeData_api, delDepartment_api } from '@/api/system/department';
|
||||
import { debounce, cloneDeep, omit } from 'lodash-es';
|
||||
import { ArrayToTree } from '@/utils/utils';
|
||||
import EditDepartmentDialog from './EditDepartmentDialog.vue';
|
||||
|
||||
import {
|
||||
SearchOutlined,
|
||||
EditOutlined,
|
||||
PlusCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { SearchOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/Department';
|
||||
|
||||
const save = useRoute().query.save;
|
||||
const emits = defineEmits(['change']);
|
||||
const searchValue = ref('');// 搜索内容
|
||||
const searchValue = ref(''); // 搜索内容
|
||||
const loading = ref<boolean>(false); // 数据加载状态
|
||||
const sourceTree = ref<any[]>([]); // 源数据
|
||||
const treeMap = new Map(); // 数据的map版本
|
||||
const treeData = ref<any[]>([]); // 展示的数据
|
||||
const selectedKeys = ref<string[]>([]); // 当前选中的项
|
||||
|
||||
getTree();
|
||||
watch(selectedKeys, (n) => {
|
||||
emits('change', n[0]);
|
||||
});
|
||||
|
||||
function getTree() {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
|
@ -119,7 +159,7 @@ function getTree() {
|
|||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
const search = debounce(() => {
|
||||
const key = searchValue.value;
|
||||
const treeArray = new Map();
|
||||
|
@ -167,14 +207,30 @@ function delDepartment(id: string) {
|
|||
getTree();
|
||||
});
|
||||
}
|
||||
|
||||
function refresh(id: string) {
|
||||
// @ts-ignore
|
||||
window?.onSaveSuccess && window.onSaveSuccess(id);
|
||||
window.close();
|
||||
getTree();
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const editDialogRef = ref(); // 新增弹窗实例
|
||||
const openDialog = (row: any = {}) => {
|
||||
editDialogRef.value.openDialog(true, row);
|
||||
};
|
||||
|
||||
init();
|
||||
function init() {
|
||||
getTree();
|
||||
watch(selectedKeys, (n) => {
|
||||
emits('change', n[0]);
|
||||
});
|
||||
if (save) {
|
||||
nextTick(() => {
|
||||
openDialog();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -183,8 +239,11 @@ const openDialog = (row: any = {}) => {
|
|||
|
||||
.add-btn {
|
||||
margin: 24px 0;
|
||||
|
||||
:deep(.ant-btn-primary) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tree-treenode) {
|
||||
width: 100%;
|
||||
|
@ -198,8 +257,9 @@ const openDialog = (row: any = {}) => {
|
|||
.func-btns {
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
.ant-btn {
|
||||
height: 22px;
|
||||
.ant-btn-link {
|
||||
padding: 0 4px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="绑定"
|
||||
width="520px"
|
||||
@ok="handleOk"
|
||||
class="edit-dialog-container"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
>
|
||||
是否继续分配产品下的具体设备
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emits = defineEmits(['confirm']);
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const handleOk = () => {
|
||||
emits('confirm');
|
||||
changeVisible();
|
||||
};
|
||||
// 控制弹窗的打开与关闭
|
||||
const changeVisible = () => {
|
||||
visible.value = !visible.value;
|
||||
};
|
||||
// 将打开弹窗的操作暴露给父组件
|
||||
defineExpose({
|
||||
openDialog: changeVisible,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -14,29 +14,38 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="table.clickAdd">
|
||||
<plus-outlined />资产分配
|
||||
</a-button>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
type="primary"
|
||||
@click="table.clickAdd"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />资产分配
|
||||
</PermissionButton>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button>批量操作</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a-popconfirm
|
||||
title="是否批量解除绑定"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickUnBind()"
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否批量解除绑定`,
|
||||
onConfirm: () =>
|
||||
table.clickUnBind(),
|
||||
}"
|
||||
>
|
||||
<a-button>
|
||||
<DisconnectOutlined /> 批量解绑
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<AIcon
|
||||
type="DisconnectOutlined"
|
||||
/>批量解绑
|
||||
</PermissionButton>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-button @click="table.clickEdit()">
|
||||
<EditOutlined /> 批量编辑
|
||||
</a-button>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
@click="table.clickEdit()"
|
||||
>
|
||||
<AIcon type="EditOutlined" />批量编辑
|
||||
</PermissionButton>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
@ -102,21 +111,22 @@
|
|||
</a-row>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
@click="table.clickEdit(slotProps)"
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="是否解除绑定"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickUnBind(slotProps)"
|
||||
><a-button>
|
||||
</PermissionButton>
|
||||
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否解除绑定`,
|
||||
onConfirm: () => table.clickUnBind(slotProps),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DisconnectOutlined" />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
@ -125,6 +135,7 @@
|
|||
<div class="dialogs">
|
||||
<AddDeviceOrProductDialog
|
||||
ref="addDialogRef"
|
||||
:query-columns="query.columns"
|
||||
:parent-id="props.parentId"
|
||||
:all-permission="table.permissionList.value"
|
||||
asset-type="device"
|
||||
|
@ -142,11 +153,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts" name="device">
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DisconnectOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
|
||||
import AddDeviceOrProductDialog from '../components/AddDeviceOrProductDialog.vue';
|
||||
import EditPermissionDialog from '../components/EditPermissionDialog.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
|
@ -155,14 +163,19 @@ import {
|
|||
getPermission_api,
|
||||
getPermissionDict_api,
|
||||
unBindDeviceOrProduct_api,
|
||||
getDeviceProduct_api,
|
||||
} from '@/api/system/department';
|
||||
import { intersection } from 'lodash-es';
|
||||
|
||||
import { dictType } from '../typing.d.ts';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/Department';
|
||||
|
||||
const emits = defineEmits(['update:bindBool']);
|
||||
const props = defineProps<{
|
||||
parentId: string;
|
||||
bindBool: boolean;
|
||||
}>();
|
||||
const query = {
|
||||
columns: [
|
||||
|
@ -186,6 +199,41 @@ const query = {
|
|||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属产品',
|
||||
dataIndex: 'productId$product-info',
|
||||
key: 'productId$product-info',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
const params = {
|
||||
paging: false,
|
||||
'sorts[0].name': 'createTime',
|
||||
'sorts[0].order': 'desc',
|
||||
};
|
||||
getDeviceProduct_api(params).then((resp: any) => {
|
||||
const result = resp.result.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
resolve(result);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'registryTime',
|
||||
key: 'registryTime',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
|
@ -279,13 +327,16 @@ const table = {
|
|||
const { pageIndex, pageSize, total, data } =
|
||||
resp.result as resultType;
|
||||
const ids = data.map((item) => item.id);
|
||||
getPermission_api('device',ids, parentId).then((perResp: any) => {
|
||||
getPermission_api('device', ids, parentId).then(
|
||||
(perResp: any) => {
|
||||
const permissionObj = {};
|
||||
perResp.result.forEach((item: any) => {
|
||||
permissionObj[item.assetId] = item.grantedPermissions;
|
||||
permissionObj[item.assetId] =
|
||||
item.grantedPermissions;
|
||||
});
|
||||
data.forEach(
|
||||
(item) => (item.permission = permissionObj[item.id]),
|
||||
(item) =>
|
||||
(item.permission = permissionObj[item.id]),
|
||||
);
|
||||
|
||||
resolve({
|
||||
|
@ -298,7 +349,8 @@ const table = {
|
|||
},
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}),
|
||||
// 整理参数并获取数据
|
||||
|
@ -393,6 +445,10 @@ const addDialogRef = ref();
|
|||
const editDialogRef = ref();
|
||||
|
||||
table.init();
|
||||
nextTick(() => {
|
||||
props.bindBool && table.clickAdd();
|
||||
emits('update:bindBool', false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -406,7 +462,7 @@ table.init();
|
|||
}
|
||||
}
|
||||
.card-tools {
|
||||
.ant-btn {
|
||||
span {
|
||||
color: #252525;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,19 @@
|
|||
<div class="right">
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="product" tab="产品">
|
||||
<Product :parentId="departmentId" />
|
||||
<Product
|
||||
:parentId="departmentId"
|
||||
@open-device-bind="openDeviceBind"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="device" tab="设备">
|
||||
<Device :parentId="departmentId" />
|
||||
<Device
|
||||
:parentId="departmentId"
|
||||
v-model:bindBool="bindBool"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="user" tab="用户">
|
||||
<User />
|
||||
<User :parentId="departmentId" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
@ -30,6 +36,12 @@ import User from './user/index.vue';
|
|||
const activeKey = ref<'product' | 'device' | 'user'>('product');
|
||||
|
||||
const departmentId = ref<string>('');
|
||||
|
||||
const bindBool = ref<boolean>(false);
|
||||
const openDeviceBind = () => {
|
||||
bindBool.value = true;
|
||||
activeKey.value = 'device';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -43,8 +55,9 @@ const departmentId = ref<string>('');
|
|||
flex-basis: 300px;
|
||||
}
|
||||
.right {
|
||||
flex: 1 1 auto;
|
||||
.ant-tabs-nav {
|
||||
width: calc(100% - 300px);
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,29 +14,38 @@
|
|||
>
|
||||
<template #headerTitle>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="table.clickAdd">
|
||||
<plus-outlined />资产分配
|
||||
</a-button>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
type="primary"
|
||||
@click="table.clickAdd"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />资产分配
|
||||
</PermissionButton>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button>批量操作</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a-popconfirm
|
||||
title="是否批量解除绑定"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickUnBind()"
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否批量解除绑定`,
|
||||
onConfirm: () =>
|
||||
table.clickUnBind(),
|
||||
}"
|
||||
>
|
||||
<a-button>
|
||||
<DisconnectOutlined /> 批量解绑
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<AIcon
|
||||
type="DisconnectOutlined"
|
||||
/>批量解绑
|
||||
</PermissionButton>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-button @click="table.clickEdit()">
|
||||
<EditOutlined /> 批量编辑
|
||||
</a-button>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
@click="()=>table.clickEdit()"
|
||||
>
|
||||
<AIcon type="EditOutlined" />批量编辑
|
||||
</PermissionButton>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
@ -102,21 +111,22 @@
|
|||
</a-row>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button
|
||||
@click="table.clickEdit(slotProps)"
|
||||
style="margin-right: 10px"
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:assert`"
|
||||
@click="() => table.clickEdit(slotProps)"
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="是否解除绑定"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickUnBind(slotProps)"
|
||||
><a-button>
|
||||
</PermissionButton>
|
||||
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否解除绑定`,
|
||||
onConfirm: () => table.clickUnBind(slotProps),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DisconnectOutlined" />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</PermissionButton>
|
||||
</template>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
@ -125,10 +135,11 @@
|
|||
<div class="dialogs">
|
||||
<AddDeviceOrProductDialog
|
||||
ref="addDialogRef"
|
||||
:query-columns="query.columns"
|
||||
:parent-id="props.parentId"
|
||||
:all-permission="table.permissionList.value"
|
||||
asset-type="product"
|
||||
@confirm="table.refresh"
|
||||
@confirm="table.addConfirm"
|
||||
/>
|
||||
<EditPermissionDialog
|
||||
ref="editDialogRef"
|
||||
|
@ -137,18 +148,20 @@
|
|||
asset-type="product"
|
||||
@confirm="table.refresh"
|
||||
/>
|
||||
<NextDialog
|
||||
ref="nextDialogRef"
|
||||
@confirm="emits('openDeviceBind')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="product">
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DisconnectOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
|
||||
import AddDeviceOrProductDialog from '../components/AddDeviceOrProductDialog.vue';
|
||||
import EditPermissionDialog from '../components/EditPermissionDialog.vue';
|
||||
import NextDialog from '../components/NextDialog.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import {
|
||||
getDeviceOrProductList_api,
|
||||
|
@ -161,6 +174,9 @@ import { intersection } from 'lodash-es';
|
|||
import { dictType } from '../typing.d.ts';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/Department';
|
||||
|
||||
const emits = defineEmits(['openDeviceBind']);
|
||||
const props = defineProps<{
|
||||
parentId: string;
|
||||
}>();
|
||||
|
@ -196,16 +212,12 @@ const query = {
|
|||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '在线',
|
||||
value: 'online',
|
||||
},
|
||||
{
|
||||
label: '离线',
|
||||
value: 'offline',
|
||||
label: '正常',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 'notActive',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -279,14 +291,30 @@ const table = {
|
|||
const { pageIndex, pageSize, total, data } =
|
||||
resp.result as resultType;
|
||||
const ids = data.map((item) => item.id);
|
||||
getPermission_api('product', ids, parentId).then((perResp: any) => {
|
||||
getPermission_api('product', ids, parentId).then(
|
||||
(perResp: any) => {
|
||||
const permissionObj = {};
|
||||
perResp.result.forEach((item: any) => {
|
||||
permissionObj[item.assetId] = item.grantedPermissions;
|
||||
permissionObj[item.assetId] =
|
||||
item.grantedPermissions;
|
||||
});
|
||||
data.forEach((item) => {
|
||||
item.permission = permissionObj[item.id];
|
||||
item.state = {
|
||||
value:
|
||||
item.state === 1
|
||||
? 'online'
|
||||
: item.state === 0
|
||||
? 'offline'
|
||||
: '',
|
||||
text:
|
||||
item.state === 1
|
||||
? '正常'
|
||||
: item.state === 0
|
||||
? '禁用'
|
||||
: '',
|
||||
};
|
||||
});
|
||||
data.forEach(
|
||||
(item) => (item.permission = permissionObj[item.id]),
|
||||
);
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
|
@ -298,7 +326,8 @@ const table = {
|
|||
},
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}),
|
||||
// 整理参数并获取数据
|
||||
|
@ -346,10 +375,13 @@ const table = {
|
|||
}
|
||||
},
|
||||
clickAdd: () => {
|
||||
console.log(222)
|
||||
console.log(addDialogRef.value)
|
||||
addDialogRef.value && addDialogRef.value.openDialog();
|
||||
},
|
||||
clickEdit: (row?: any) => {
|
||||
const ids = row ? [row.id] : [...table._selectedRowKeys.value];
|
||||
|
||||
if (row || table.selectedRows.length === 1) {
|
||||
const permissionList =
|
||||
row?.permission || table.selectedRows[0].permission;
|
||||
|
@ -387,11 +419,15 @@ const table = {
|
|||
tableRef.value.reload();
|
||||
});
|
||||
},
|
||||
addConfirm: () => {
|
||||
table.refresh();
|
||||
nextDialogRef.value && nextDialogRef.value.openDialog();
|
||||
},
|
||||
};
|
||||
|
||||
const addDialogRef = ref();
|
||||
const editDialogRef = ref();
|
||||
|
||||
const nextDialogRef = ref();
|
||||
table.init();
|
||||
</script>
|
||||
|
||||
|
@ -406,7 +442,7 @@ table.init();
|
|||
}
|
||||
}
|
||||
.card-tools {
|
||||
.ant-btn {
|
||||
span {
|
||||
color: #252525;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<a-modal
|
||||
class="add-bind-user-dialog-container"
|
||||
title="绑定"
|
||||
width="1440px"
|
||||
@ok="dialog.handleOk"
|
||||
centered
|
||||
:confirmLoading="dialog.loading.value"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
v-model:visible="dialog.visible.value"
|
||||
>
|
||||
<Search :columns="query.columns" @search="query.search" />
|
||||
<div class="table">
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
:columns="table.columns"
|
||||
:request="table.requestFun"
|
||||
:params="query.params"
|
||||
:rowSelection="{
|
||||
selectedRowKeys: table._selectedRowKeys,
|
||||
onChange: table.onSelectChange,
|
||||
}"
|
||||
@cancelSelect="table.cancelSelect"
|
||||
model="TABLE"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { bindUser_api, getBindUserList_api } from '@/api/system/department';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const emits = defineEmits(['confirm']);
|
||||
|
||||
const props = defineProps({
|
||||
parentId: String,
|
||||
});
|
||||
// 弹窗相关
|
||||
const dialog = {
|
||||
loading: ref<boolean>(false),
|
||||
visible: ref<boolean>(false),
|
||||
handleOk: () => {
|
||||
if (table._selectedRowKeys.length && props.parentId) {
|
||||
bindUser_api(props.parentId, table._selectedRowKeys).then(() => {
|
||||
emits('confirm');
|
||||
message.success('操作成功');
|
||||
dialog.changeVisible();
|
||||
});
|
||||
} else {
|
||||
dialog.changeVisible();
|
||||
}
|
||||
},
|
||||
// 控制弹窗的打开与关闭
|
||||
changeVisible: () => {
|
||||
if (!dialog.visible.value) query.search({});
|
||||
dialog.visible.value = !dialog.visible.value;
|
||||
},
|
||||
};
|
||||
// 将打开弹窗的操作暴露给父组件
|
||||
defineExpose({
|
||||
openDialog: dialog.changeVisible,
|
||||
});
|
||||
|
||||
const query = {
|
||||
columns: [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
params: ref({}),
|
||||
search: (params: any) => {
|
||||
query.params.value = params;
|
||||
},
|
||||
};
|
||||
const table = reactive({
|
||||
columns: [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
],
|
||||
_selectedRowKeys: [] as string[],
|
||||
|
||||
requestFun: async (oParams: any) => {
|
||||
table.cancelSelect();
|
||||
if (props.parentId) {
|
||||
const params = {
|
||||
...oParams,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [
|
||||
...oParams.terms,
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'id$in-dimension$org$not',
|
||||
value: props.parentId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const resp: any = await getBindUserList_api(params);
|
||||
return {
|
||||
code: resp.status,
|
||||
result: resp.result,
|
||||
status: resp.status,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 200,
|
||||
result: {
|
||||
data: [],
|
||||
pageIndex: 0,
|
||||
pageSize: 0,
|
||||
total: 0,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
},
|
||||
onSelectChange: (keys: string[]) => {
|
||||
table._selectedRowKeys = keys;
|
||||
},
|
||||
cancelSelect: () => {
|
||||
table._selectedRowKeys = [];
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.add-bind-user-dialog-container) {
|
||||
.table {
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,230 @@
|
|||
<template>
|
||||
<div>
|
||||
用户
|
||||
<Search :columns="query.columns" @search="query.search" />
|
||||
|
||||
<JTable
|
||||
ref="tableRef"
|
||||
:columns="table.columns"
|
||||
:request="table.requestFun"
|
||||
:params="query.params"
|
||||
:rowSelection="{
|
||||
selectedRowKeys: table._selectedRowKeys,
|
||||
onChange: table.onSelectChange,
|
||||
}"
|
||||
@cancelSelect="table.cancelSelect"
|
||||
model="TABLE"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
:uhasPermission="`${permission}:bind-user`"
|
||||
@click="table.openDialog"
|
||||
style="margin-right: 15px;"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />绑定用户
|
||||
</PermissionButton>
|
||||
<div style="display: inline-block;width: 12px;height: 1px;"></div>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否解除绑定`,
|
||||
onConfirm: () => table.unBind(),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DisconnectOutlined" />批量解绑
|
||||
</PermissionButton>
|
||||
</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">
|
||||
<PermissionButton
|
||||
type="link"
|
||||
:uhasPermission="`${permission}:bind`"
|
||||
:popConfirm="{
|
||||
title: `是否解除绑定`,
|
||||
onConfirm: () => table.unBind(slotProps),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DisconnectOutlined" />
|
||||
</PermissionButton>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
|
||||
<div class="dialogs">
|
||||
<AddBindUserDialog
|
||||
ref="addDialogRef"
|
||||
:parent-id="props.parentId"
|
||||
@confirm="table.refresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" name="user">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import AddBindUserDialog from './components/addBindUserDialog.vue';
|
||||
import { getBindUserList_api, unBindUser_api } from '@/api/system/department';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/Department';
|
||||
|
||||
const addDialogRef = ref();
|
||||
|
||||
const props = defineProps<{
|
||||
parentId: string;
|
||||
}>();
|
||||
|
||||
const query = {
|
||||
columns: [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
params: ref({}),
|
||||
search: (params: any) => {
|
||||
query.params.value = params;
|
||||
},
|
||||
};
|
||||
|
||||
// 表格
|
||||
const tableRef = ref<Record<string, any>>({}); // 表格实例
|
||||
const table = reactive({
|
||||
columns: [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
},
|
||||
],
|
||||
_selectedRowKeys: [] as string[],
|
||||
|
||||
requestFun: async (oParams: any) => {
|
||||
table.cancelSelect();
|
||||
if (props.parentId) {
|
||||
const params = {
|
||||
...oParams,
|
||||
sorts: [{ name: 'createTime', order: 'desc' }],
|
||||
terms: [
|
||||
...oParams.terms,
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'id$in-dimension$org',
|
||||
value: props.parentId,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const resp: any = await getBindUserList_api(params);
|
||||
return {
|
||||
code: resp.status,
|
||||
result: resp.result,
|
||||
status: resp.status,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 200,
|
||||
result: {
|
||||
data: [],
|
||||
pageIndex: 0,
|
||||
pageSize: 0,
|
||||
total: 0,
|
||||
},
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
},
|
||||
unBind: (row?: any) => {
|
||||
const ids = row ? [row.id] : table._selectedRowKeys;
|
||||
if (ids.length < 1) return message.warning('请勾选需要解绑的数据');
|
||||
|
||||
unBindUser_api(props.parentId, ids).then(() => {
|
||||
message.success('操作成功');
|
||||
table.refresh();
|
||||
});
|
||||
},
|
||||
// 打开编辑弹窗
|
||||
openDialog: () => {
|
||||
addDialogRef.value && addDialogRef.value.openDialog();
|
||||
},
|
||||
onSelectChange: (keys: string[]) => {
|
||||
table._selectedRowKeys = keys;
|
||||
},
|
||||
cancelSelect: () => {
|
||||
table._selectedRowKeys = [];
|
||||
},
|
||||
// 刷新列表
|
||||
refresh: () => {
|
||||
tableRef.value.reload();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
import { FormInstance, message } from 'ant-design-vue';
|
||||
import { saveRole_api } from '@/api/system/role';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
// 弹窗相关
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
|
@ -56,12 +57,17 @@ const dialog = reactive({
|
|||
if (resp.status === 200) {
|
||||
message.success('操作成功');
|
||||
dialog.visible = false;
|
||||
router.push(`/system/Role/detail/${resp.result.id}`);
|
||||
|
||||
if (route.query.save) {
|
||||
// @ts-ignore
|
||||
window?.onSaveSuccess && window.onSaveSuccess(resp.result.id);
|
||||
window.close();
|
||||
} else router.push(`/system/Role/detail/${resp.result.id}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 控制弹窗的打开与关闭
|
||||
changeVisible: (status: boolean, defaultForm: object={}) => {
|
||||
changeVisible: (status: boolean, defaultForm: object = {}) => {
|
||||
dialog.visible = status;
|
||||
form.data = { name: '', description: '', ...defaultForm };
|
||||
},
|
||||
|
@ -76,12 +82,10 @@ const form = reactive({
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 将打开弹窗的操作暴露给父组件
|
||||
defineExpose({
|
||||
openDialog: dialog.changeVisible
|
||||
})
|
||||
openDialog: dialog.changeVisible,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -10,36 +10,38 @@
|
|||
:params="query.params"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<a-button type="primary" @click="table.clickAdd"
|
||||
><plus-outlined />新增</a-button
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
:uhasPermission="`${permission}:add`"
|
||||
@click="table.clickAdd"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />新增
|
||||
</PermissionButton>
|
||||
</template>
|
||||
|
||||
<template #action="slotProps">
|
||||
<a-space :size="16">
|
||||
<a-tooltip>
|
||||
<template #title>编辑</template>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
<PermissionButton
|
||||
:uhasPermission="`${permission}:update`"
|
||||
type="link"
|
||||
:tooltip="{
|
||||
title: '编辑',
|
||||
}"
|
||||
@click="table.clickEdit(slotProps)"
|
||||
>
|
||||
<edit-outlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm
|
||||
title="确定要删除吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickDel(slotProps)"
|
||||
<AIcon type="EditOutlined" />
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
type="link"
|
||||
:uhasPermission="`${permission}:delete`"
|
||||
:tooltip="{ title: '删除' }"
|
||||
:popConfirm="{
|
||||
title: `确定要删除吗`,
|
||||
onConfirm: () => table.clickDel(slotProps),
|
||||
}"
|
||||
>
|
||||
<a-tooltip>
|
||||
<template #title>删除</template>
|
||||
<a-button style="padding: 0" type="link">
|
||||
<delete-outlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</PermissionButton>
|
||||
</a-space>
|
||||
</template>
|
||||
</JTable>
|
||||
|
@ -51,14 +53,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts" name="Role">
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import AddDialog from './components/AddDialog.vue';
|
||||
import { getRoleList_api, delRole_api } from '@/api/system/role';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/Role';
|
||||
|
||||
const addDialogRef = ref(); // 新增弹窗实例
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
@ -143,4 +144,14 @@ nextTick(() => {
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.role-container {
|
||||
|
||||
:deep(.ant-table-cell) {
|
||||
.ant-btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,433 @@
|
|||
<template>
|
||||
<a-modal
|
||||
v-model:visible="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="675px"
|
||||
@ok="dialog.handleOk"
|
||||
class="edit-dialog-container"
|
||||
:confirmLoading="dialog.loading"
|
||||
cancelText="取消"
|
||||
okText="确定"
|
||||
>
|
||||
<a-form ref="formRef" :model="form.data" layout="vertical">
|
||||
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="name"
|
||||
label="姓名"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入姓名' },
|
||||
{
|
||||
max: 64,
|
||||
message: '最多可输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.data.name"
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="username"
|
||||
label="用户名"
|
||||
:rules="[
|
||||
{ required: true },
|
||||
{
|
||||
validator: form.rules.checkUserName,
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.data.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="dialog.type === 'edit'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="form.IsShow('add', 'reset')">
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
name="password"
|
||||
label="密码"
|
||||
:rules="[
|
||||
{ required: true },
|
||||
{
|
||||
validator: form.rules.checkPassword,
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="form.data.password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<!-- <Progress
|
||||
:percent="20"
|
||||
:steps="5"
|
||||
:strokeColor="{ from: '#ff5500', to: '#ff9300' }"
|
||||
/> -->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="form.IsShow('add', 'reset')">
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
name="confirmPassword"
|
||||
label="确认密码"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入8~64位的密码' },
|
||||
{
|
||||
validator: form.rules.checkAgainPassword,
|
||||
trigger: 'change',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model:value="form.data.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
:maxlength="64"
|
||||
/>
|
||||
<!-- <Progress
|
||||
:percent="60"
|
||||
:steps="5"
|
||||
:strokeColor="{ from: '#ff5500', to: '#ff9300' }"
|
||||
/> -->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 还差页面权限 -->
|
||||
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
|
||||
<a-col :span="12">
|
||||
<a-form-item name="roleIdList" label="角色" class="flex">
|
||||
<a-select
|
||||
v-model:value="form.data.roleIdList"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="请选择角色"
|
||||
:options="form.roleOptions"
|
||||
></a-select>
|
||||
|
||||
<PermissionButton
|
||||
:uhasPermission="`${rolePermission}:update`"
|
||||
@click="form.clickAddItem('roleIdList', 'Role')"
|
||||
class="add-item"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />
|
||||
</PermissionButton>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="orgIdList" label="组织" class="flex">
|
||||
<a-tree-select
|
||||
v-model:value="form.data.orgIdList"
|
||||
show-search
|
||||
style="width: 100%"
|
||||
placeholder="请选择组织"
|
||||
multiple
|
||||
:tree-data="form.departmentOptions"
|
||||
:fieldNames="{ label: 'name', value: 'id' }"
|
||||
>
|
||||
<template #title="{ name }">
|
||||
{{ name }}
|
||||
</template>
|
||||
</a-tree-select>
|
||||
<PermissionButton
|
||||
:uhasPermission="`${deptPermission}:update`"
|
||||
@click="form.clickAddItem('roleIdList', 'Role')"
|
||||
class="add-item"
|
||||
>
|
||||
<AIcon type="PlusOutlined" />
|
||||
</PermissionButton>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24" v-if="form.IsShow('add', 'edit')">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="telephone"
|
||||
label="手机号"
|
||||
:rules="[
|
||||
{
|
||||
pattern: /^1[3456789]\d{9}$/,
|
||||
message: '请输入正确的手机号',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.data.telephone"
|
||||
placeholder="请输入手机号"
|
||||
:maxlength="64"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
:rules="[
|
||||
{
|
||||
pattern:
|
||||
/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
|
||||
message: '请输入正确的邮箱',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.data.email"
|
||||
placeholder="请输入邮箱"
|
||||
:maxlength="64"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import { FormInstance, message } from 'ant-design-vue';
|
||||
// import Progress from './Progress.vue';
|
||||
import {
|
||||
validateField_api,
|
||||
getRoleList_api,
|
||||
getDepartmentList_api,
|
||||
addUser_api,
|
||||
updateUser_api,
|
||||
updatePassword_api,
|
||||
getUser_api,
|
||||
} from '@/api/system/user';
|
||||
import { Rule } from 'ant-design-vue/es/form';
|
||||
import { DefaultOptionType } from 'ant-design-vue/es/vc-tree-select/TreeSelect';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
const deptPermission = 'system/Department';
|
||||
const rolePermission = 'system/Role';
|
||||
|
||||
const emits = defineEmits(['confirm']);
|
||||
// 弹窗相关
|
||||
const dialog = reactive({
|
||||
title: '',
|
||||
visible: false,
|
||||
type: '' as modalType,
|
||||
loading: false,
|
||||
|
||||
handleOk: () => {
|
||||
formRef.value?.validate().then(() => {
|
||||
form.submit(() => {
|
||||
dialog.changeVisible('', {} as any);
|
||||
emits('confirm');
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 设置表单类型
|
||||
* @param type 弹窗类型
|
||||
* @param defaultForm 表单回显对象
|
||||
*/
|
||||
changeVisible: (type: modalType, defaultForm: formType) => {
|
||||
dialog.setTitle(type);
|
||||
form.getUserInfo(defaultForm.id || '', type);
|
||||
dialog.type = type;
|
||||
dialog.visible = type !== '';
|
||||
},
|
||||
setTitle: (type: modalType) => {
|
||||
if (type === 'add') dialog.title = '新增';
|
||||
else if (type === 'edit') dialog.title = '编辑';
|
||||
else if (type === 'reset') dialog.title = '重置密码';
|
||||
else dialog.title = '';
|
||||
},
|
||||
});
|
||||
// 将打开弹窗的操作暴露给父组件
|
||||
defineExpose({
|
||||
openDialog: dialog.changeVisible,
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
data: {} as formType,
|
||||
|
||||
rules: {
|
||||
checkUserName: (_rule: Rule, value: string): Promise<any> =>
|
||||
new Promise((resolve, reject) => {
|
||||
console.log(_rule);
|
||||
if (dialog.type === 'edit') return resolve('');
|
||||
|
||||
if (!value) return reject('请输入用户名');
|
||||
else if (value.length > 64) return reject('最多可输入64个字符');
|
||||
validateField_api('username', value).then((resp: any): any => {
|
||||
resp.result.passed
|
||||
? resolve('')
|
||||
: reject(resp.result.reason);
|
||||
});
|
||||
}),
|
||||
checkPassword: (_rule: Rule, value: string): Promise<any> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!value) return reject('请输入8~64位的密码');
|
||||
else if (value.length > 64) return reject('最多可输入64个字符');
|
||||
else if (value.length < 8) return reject('密码不能少于8位');
|
||||
validateField_api('password', value).then((resp: any) => {
|
||||
resp.result.passed
|
||||
? resolve('')
|
||||
: reject(resp.result.reason);
|
||||
});
|
||||
}),
|
||||
checkAgainPassword: (_rule: Rule, value: string): Promise<any> => {
|
||||
if (!value) return Promise.reject('');
|
||||
return value === form.data.password
|
||||
? Promise.resolve()
|
||||
: Promise.reject('两次密码输入不一致');
|
||||
},
|
||||
//
|
||||
},
|
||||
|
||||
roleOptions: [] as optionType[],
|
||||
departmentOptions: [] as DefaultOptionType[],
|
||||
|
||||
init: () => {
|
||||
form.getDepartmentList();
|
||||
form.getRoleList();
|
||||
},
|
||||
getUserInfo: (id: string, type: modalType) => {
|
||||
if (type === 'add') form.data = {} as formType;
|
||||
else if (type === 'reset') form.data = { id } as formType;
|
||||
else if (type === 'edit') {
|
||||
getUser_api(id).then((resp: any) => {
|
||||
form.data = {
|
||||
...(resp.result as formType),
|
||||
orgIdList: resp.result.orgList.map(
|
||||
(item: dictType) => item.id,
|
||||
),
|
||||
roleIdList: resp.result.roleList.map(
|
||||
(item: dictType) => item.id,
|
||||
),
|
||||
};
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
submit: (cb?: Function) => {
|
||||
let api: axiosFunType;
|
||||
let params = {};
|
||||
switch (dialog.type) {
|
||||
case 'add': {
|
||||
api = addUser_api;
|
||||
params = {
|
||||
user: form.data,
|
||||
orgIdList: form.data.orgIdList,
|
||||
roleIdList: form.data.roleIdList,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
api = updateUser_api;
|
||||
params = {
|
||||
id: form.data.id,
|
||||
user: form.data,
|
||||
orgIdList: form.data.orgIdList,
|
||||
roleIdList: form.data.roleIdList,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'reset': {
|
||||
api = updatePassword_api;
|
||||
params = {
|
||||
id: form.data.id,
|
||||
password: form.data.password,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
console.log(params);
|
||||
|
||||
api(params).then(() => {
|
||||
message.success('操作成功');
|
||||
cb && cb();
|
||||
});
|
||||
},
|
||||
getRoleList: () => {
|
||||
getRoleList_api().then((resp: any) => {
|
||||
form.roleOptions = resp.result.map((item: dictType) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
});
|
||||
},
|
||||
getDepartmentList: () => {
|
||||
getDepartmentList_api().then((resp: any) => {
|
||||
form.departmentOptions = resp.result;
|
||||
});
|
||||
},
|
||||
IsShow: (...typeList: modalType[]) => typeList.includes(dialog.type),
|
||||
clickAddItem: (prop: 'roleIdList' | 'orgIdList', target: string) => {
|
||||
const tab: any = window.open(`${origin}/#/system/${target}?save=true`);
|
||||
tab.onSaveSuccess = (value: string) => {
|
||||
form.data[prop] = [...(form.data[prop] || []), value];
|
||||
if (prop === 'roleIdList') form.getRoleList();
|
||||
else form.getDepartmentList();
|
||||
};
|
||||
},
|
||||
});
|
||||
form.init();
|
||||
|
||||
interface AxiosResponseRewrite<T = any[]> extends AxiosResponse<T, any> {
|
||||
result: T;
|
||||
success: boolean;
|
||||
}
|
||||
type axiosFunType = (data: any) => Promise<AxiosResponseRewrite<unknown>>;
|
||||
type modalType = '' | 'add' | 'edit' | 'reset';
|
||||
type formType = {
|
||||
id?: string;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
roleIdList: string[];
|
||||
orgIdList: string[];
|
||||
telephone: string;
|
||||
email: string;
|
||||
};
|
||||
type dictType = {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: dictType;
|
||||
};
|
||||
type optionType = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.edit-dialog-container {
|
||||
.ant-form-item {
|
||||
&.flex {
|
||||
:deep(.ant-form-item-control-input-content) {
|
||||
display: flex;
|
||||
.ant-select {
|
||||
flex: 1;
|
||||
}
|
||||
.ant-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #1d39c4;
|
||||
color: #1d39c4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div class="progress-container">
|
||||
<div class="value" :style="valueStyle"></div>
|
||||
<div
|
||||
class="split"
|
||||
v-for="leftValue in valueArr"
|
||||
:style="{ left: leftValue + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
steps?: number;
|
||||
strokeColor?:
|
||||
| {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
| string;
|
||||
percent: number;
|
||||
}>(),
|
||||
{
|
||||
steps: 1,
|
||||
strokeColor: '#108ee9',
|
||||
},
|
||||
);
|
||||
|
||||
const valueStyle = computed(() => {
|
||||
let background = '';
|
||||
if (typeof props.strokeColor === 'string') {
|
||||
background = props.strokeColor;
|
||||
} else {
|
||||
background = `-webkit-linear-gradient(
|
||||
left,
|
||||
${props.strokeColor.from},
|
||||
${props.strokeColor.to}
|
||||
)`;
|
||||
}
|
||||
|
||||
return {
|
||||
background,
|
||||
'clip-path': `polygon(0px 0px, ${props.percent}% 0px, ${props.percent}% 100%, 0px 100%)`,
|
||||
};
|
||||
});
|
||||
|
||||
const valueArr = computed(() => {
|
||||
const result = [];
|
||||
for (let i = 1; i < props.steps; i++) result.push((100 / props.steps) * i);
|
||||
return result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: #e0e0e0;
|
||||
height: 8px;
|
||||
margin: 3px 0;
|
||||
|
||||
.split {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.value {
|
||||
transition: all 0.35s ease-in-out 0s;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,369 @@
|
|||
<template>
|
||||
<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
|
||||
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>
|
||||
<template #title>编辑</template>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
@click="table.openDialog('edit', slotProps)"
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</a-button>
|
||||
</a-tooltip> -->
|
||||
<!-- <a-popconfirm
|
||||
:title="`确定${slotProps.status ? '禁用' : '启用'}吗?`"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.changeStatus(slotProps)"
|
||||
>
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.status ? '禁用' : '启用'
|
||||
}}</template>
|
||||
<a-button style="padding: 0" type="link">
|
||||
<stop-outlined v-if="slotProps.status" />
|
||||
<play-circle-outlined v-else />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-popconfirm> -->
|
||||
<!-- <a-tooltip>
|
||||
<template #title>重置密码</template>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
@click="table.openDialog('reset', slotProps)"
|
||||
>
|
||||
<AIcon type="icon-zhongzhimima" />
|
||||
</a-button>
|
||||
</a-tooltip> -->
|
||||
<!-- <a-popconfirm
|
||||
title="确认删除"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="table.clickDel(slotProps)"
|
||||
:disabled="slotProps.status"
|
||||
>
|
||||
<a-tooltip>
|
||||
<template #title>{{
|
||||
slotProps.status ? '请先禁用,再删除' : '删除'
|
||||
}}</template>
|
||||
<a-button
|
||||
style="padding: 0"
|
||||
type="link"
|
||||
:disabled="slotProps.status"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
</a-button>
|
||||
</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>
|
||||
|
||||
<div class="dialogs">
|
||||
<EditUserDialog ref="editDialogRef" @confirm="table.refresh" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="UserMange">
|
||||
import PermissionButton from '@/components/PermissionButton/index.vue';
|
||||
import EditUserDialog from './components/EditUserDialog.vue';
|
||||
import {
|
||||
getUserType_api,
|
||||
getUserList_api,
|
||||
changeUserStatus_api,
|
||||
deleteUser_api,
|
||||
} from '@/api/system/user';
|
||||
import { StopOutlined, PlayCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const permission = 'system/User';
|
||||
|
||||
const query = {
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'select',
|
||||
options: () =>
|
||||
new Promise((resolve) => {
|
||||
getUserType_api().then((resp: any) => {
|
||||
resolve(
|
||||
resp.result.map((item: dictType) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
ellipsis: true,
|
||||
search: {
|
||||
rename: 'status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '启用',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'telephone',
|
||||
key: 'telephone',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
params: ref({}),
|
||||
|
||||
search: (params: object) => {
|
||||
query.params.value = params;
|
||||
},
|
||||
};
|
||||
|
||||
const editDialogRef = ref(); // 弹窗实例
|
||||
const tableRef = ref<Record<string, any>>({}); // 表格实例
|
||||
const table = {
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
scopedSlots: true,
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'telephone',
|
||||
key: 'telephone',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
search: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
scopedSlots: true,
|
||||
},
|
||||
],
|
||||
|
||||
// 打开编辑弹窗
|
||||
openDialog: (type: modalType, row?: any) => {
|
||||
editDialogRef.value.openDialog(type, row || {});
|
||||
},
|
||||
changeStatus: (row: any) => {
|
||||
const params = {
|
||||
id: row.id,
|
||||
status: row.status === 0 ? 1 : 0,
|
||||
};
|
||||
changeUserStatus_api(params).then(() => {
|
||||
message.success('操作成功');
|
||||
table.refresh();
|
||||
});
|
||||
},
|
||||
// 删除
|
||||
clickDel: (row: any) => {
|
||||
deleteUser_api(row.id).then(() => {
|
||||
message.success('操作成功');
|
||||
table.refresh();
|
||||
});
|
||||
},
|
||||
// 刷新列表
|
||||
refresh: () => {
|
||||
tableRef.value.reload();
|
||||
},
|
||||
};
|
||||
|
||||
type dictType = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
type modalType = '' | 'add' | 'edit' | 'reset';
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-container {
|
||||
padding: 24px;
|
||||
|
||||
:deep(.ant-table-tbody) {
|
||||
.ant-table-cell {
|
||||
.ant-space-item {
|
||||
.ant-btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue