Merge branch 'dev' into dev-hub

This commit is contained in:
jackhoo_98 2023-02-17 09:28:45 +08:00
commit f7f0556b00
41 changed files with 3783 additions and 258 deletions

13
src/api/media/device.ts Normal file
View File

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

View File

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

View File

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

36
src/api/system/user.ts Normal file
View File

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

View File

@ -45,7 +45,10 @@ const iconKeys = [
'InfoCircleOutlined',
'SearchOutlined',
'EllipsisOutlined',
'ClockCircleOutlined'
'ClockCircleOutlined',
'PartitionOutlined',
'ShareAltOutlined',
'playCircleOutlined',
]
const Icon = (props: {type: string}) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>1ID</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>1SIP服务器编号/SIP域</h2>
<div>
SIP服务器编号填入该设备所属产品-接入方式页面连接信息的SIP
SIP域通常为SIP服务器编号的前10位
</div>
<div class="image">
<a-image
width="100%"
:src="getImage('/media/doc2.png')"
/>
</div>
<h2>2SIP服务器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>
视频设备通过RTSPRTMP固定地址接入平台分为2步
</div>
<div>1添加视频设备</div>
<div>2添加视频下的通道地址</div>
<div>
当前页面为新增视频设备新增完成后点击设备的通道按钮添加通道
</div>
<h1>2.配置说明</h1>
<h2>1ID</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>

View File

@ -0,0 +1,4 @@
export const PROVIDER_OPTIONS = [
{ label: '固定地址', value: 'fixed-media' },
{ label: 'GB/T28181', value: 'gb28181-2016' },
]

View File

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

24
src/views/media/Device/typings.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, // productdeviceorgother
format: format,
time: time,
// from: 'now-1y', // now-1dnow-1wnow-1Mnow-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>

View File

@ -0,0 +1,5 @@
export type Footer = {
title: string;
value: number | string;
status?: "default" | "error" | "success" | "warning" | "processing"
}

View File

@ -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: '操作',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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