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

This commit is contained in:
xiongqian 2023-02-07 11:36:55 +08:00
commit a8af608299
13 changed files with 1284 additions and 446 deletions

View File

@ -217,3 +217,28 @@ export const queryMetric = (deviceId: string, propertyId: string) => server.get(
* @returns
*/
export const saveMetric = (deviceId: string, propertyId: string, data: Record<string, any>) => server.patch(`/device-instance/${deviceId}/metric/property/${propertyId}`, data)
/**
*
* @param deviceId id
* @param childrenId id
* @param data
* @returns
*/
export const unbindDevice = (deviceId: string, childrenId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/unbind/${childrenId}`, data)
/**
*
* @param deviceId id
* @param data
* @returns
*/
export const unbindBatchDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/unbind`, data)
/**
*
* @param deviceId id
* @param data
* @returns
*/
export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data)

View File

@ -6,10 +6,14 @@ export const getRoleList_api = (data: any): Promise<any> => server.post(`/role/_
export const delRole_api = (id: string): Promise<any> => server.remove(`/role/${id}`);
// 保存角色
export const saveRole_api = (data: any): Promise<any> => server.post(`/role`, data);
// 更新角色信息
export const updateRole_api = (data: any): Promise<any> => server.patch(`/role`, data);
// 获取角色详细信息
export const getRoleDetails_api = (id: string): Promise<any> => server.get(`/role/${id}`);
// 获取角色对应的权限树
export const getPrimissTree_api = (id: string): Promise<any> => server.get(`/menu/role/${id}/_grant/tree`);
// 更新角色对应的权限树
export const updatePrimissTree_api = (id: string, data:object): Promise<any> => server.put(`/menu/role/${id}/_grant`,data);
// 获取用户列表

View File

@ -0,0 +1,195 @@
<!-- 绑定设备 -->
<template>
<a-modal
:maskClosable="false"
width="1000px"
:visible="true"
title="绑定子设备"
okText="确定"
cancelText="取消"
@ok="handleOk"
@cancel="handleCancel"
:confirmLoading="btnLoading"
>
<div style="margin-top: 10px">
<Search
:columns="columns"
target="child-device-bind"
@search="handleSearch"
type="simple"
/>
<JTable
ref="bindDeviceRef"
:columns="columns"
:request="query"
model="TABLE"
:defaultParams="{
terms: [
{
terms: [
{ column: 'parentId$isnull', value: '1' },
{
column: 'parentId$not',
value: detail.id,
type: 'or',
},
],
},
{
terms: [
{
column: 'id$not',
value: detail.id,
type: 'and',
},
],
},
{
terms: [
{
termType: 'eq',
column: 'deviceType',
value: 'childrenDevice',
},
],
type: 'and',
},
],
}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? moment(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
</JTable>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { query, bindDevice } from '@/api/device/instance';
import moment from 'moment';
import { message } from 'ant-design-vue';
import { useInstanceStore } from '@/store/instance';
import { storeToRefs } from 'pinia';
const instanceStore = useInstanceStore();
const { detail } = storeToRefs(instanceStore);
const emit = defineEmits(['change']);
const bindDeviceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const btnLoading = ref<boolean>(false);
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '所属产品',
dataIndex: 'productName',
key: 'productName',
search: {
type: 'string',
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
];
const handleSearch = (e: any) => {
params.value = e;
};
const onSelectChange = (keys: string[], rows: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleOk = () => {
if (_selectedRowKeys.value.length === 0) {
message.warning('请选择需要绑定的设备');
return;
}
btnLoading.value = true;
bindDevice(detail.value.id, _selectedRowKeys.value)
.then((resp) => {
emit('change', true);
cancelSelect();
message.success('操作成功');
})
.finally(() => {
btnLoading.value = false;
});
};
const handleCancel = () => {
emit('change', false);
};
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,265 @@
<template>
<a-card>
<Search
:columns="columns"
target="child-device"
@search="handleSearch"
class="child-device-search"
/>
<JTable
ref="childDeviceRef"
:columns="columns"
:request="query"
:defaultParams="{
terms: [
{
column: 'parentId',
value: detail?.id || '',
termType: 'eq',
},
],
}"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange,
}"
@cancelSelect="cancelSelect"
:params="params"
:model="'TABLE'"
>
<template #headerTitle>
<a-space>
<a-button type="primary"> 新增并绑定 </a-button>
<a-button type="primary" @click="visible = true">
绑定
</a-button>
<a-popconfirm title="确认解绑吗?" @confirm="handleUnBind">
<a-button type="primary"> 批量解绑 </a-button>
</a-popconfirm>
</a-space>
</template>
<template #registryTime="slotProps">
{{
slotProps.registryTime
? moment(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}
</template>
<template #state="slotProps">
<a-badge
:text="slotProps.state.text"
:status="statusMap.get(slotProps.state.value)"
/>
</template>
<template #action="slotProps">
<a-space :size="16">
<a-tooltip
v-for="i in getActions(slotProps)"
:key="i.key"
v-bind="i.tooltip"
>
<a-popconfirm v-if="i.popConfirm" v-bind="i.popConfirm">
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-popconfirm>
<a-button
style="padding: 0"
type="link"
v-else
@click="i.onClick && i.onClick(slotProps)"
>
<a-button
:disabled="i.disabled"
style="padding: 0"
type="link"
><AIcon :type="i.icon"
/></a-button>
</a-button>
</a-tooltip>
</a-space>
</template>
</JTable>
<BindChildDevice v-if="visible" @change="closeBindDevice" />
</a-card>
</template>
<script setup lang="ts">
import moment from 'moment';
import type { ActionsType } from '@/components/Table';
import { query, unbindDevice, unbindBatchDevice } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import { storeToRefs } from 'pinia';
import { message } from 'ant-design-vue';
import BindChildDevice from './BindChildDevice/index.vue';
const instanceStore = useInstanceStore();
const { detail } = storeToRefs(instanceStore);
const router = useRouter();
const statusMap = new Map();
statusMap.set('online', 'success');
statusMap.set('offline', 'error');
statusMap.set('notActive', 'warning');
const childDeviceRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const _selectedRowKeys = ref<string[]>([]);
const visible = ref<boolean>(false);
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '所属产品',
dataIndex: 'productName',
key: 'productName',
search: {
type: 'string',
},
},
{
title: '注册时间',
dataIndex: 'registryTime',
key: 'registryTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '状态',
dataIndex: 'state',
key: 'state',
scopedSlots: true,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
],
},
},
{
title: '说明',
dataIndex: 'describe',
key: 'describe',
ellipsis: true,
search: {
type: 'string',
},
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 200,
scopedSlots: true,
},
];
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) return [];
return [
{
key: 'view',
text: '查看',
tooltip: {
title: '查看',
},
icon: 'EyeOutlined',
onClick: () => {
router.push('/iot/device/instance/detail/' + data.id);
},
},
{
key: 'unbind',
text: '解绑',
tooltip: {
title: '解绑',
},
icon: 'DisconnectOutlined',
popConfirm: {
title: '确认解绑吗?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
const resp = await unbindDevice(
detail.value.id,
data.id,
{},
);
if (resp.status === 200) {
childDeviceRef.value?.reload();
message.success('操作成功!');
}
},
},
},
];
};
const handleSearch = (e: any) => {
params.value = e;
};
const onSelectChange = (keys: string[]) => {
_selectedRowKeys.value = [...keys];
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleUnBind = async () => {
if (_selectedRowKeys.value.length) {
const resp = await unbindBatchDevice(
detail.value.id,
_selectedRowKeys.value,
);
if (resp.status === 200) {
message.success('操作成功!');
cancelSelect();
childDeviceRef.value?.reload();
}
} else {
message.warning('请勾选需要解绑的数据');
}
};
const closeBindDevice = (val: boolean) => {
visible.value = false;
if (val) {
childDeviceRef.value?.reload();
}
};
</script>
<style scoped lang="less">
.child-device-search {
border-bottom: 1px solid #f0f0f0;
}
:deep(._jtable-body_1eyxz_1 ._jtable-body-header_1eyxz_6) {
justify-content: flex-end;
}
</style>

View File

@ -43,6 +43,7 @@ import { useInstanceStore } from '@/store/instance';
import Info from './Info/index.vue';
import Running from './Running/index.vue'
import Metadata from '../../components/Metadata/index.vue';
import ChildDevice from './ChildDevice/index.vue';
import { _deploy, _disconnect } from '@/api/device/instance'
import { message } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
@ -67,13 +68,18 @@ const list = [
{
key: 'Metadata',
tab: '物模型'
},
{
key: 'ChildDevice',
tab: '子设备'
}
]
const tabs = {
Info,
Metadata,
Running
Running,
ChildDevice,
}
watch(

View File

@ -17,6 +17,7 @@
}"
@cancelSelect="cancelSelect"
:params="params"
:gridColumn="3"
>
<template #headerTitle>
<a-space>

View File

@ -70,6 +70,7 @@
<Guide title="流量统计">
<template #extra>
<TimeSelect
key="flow-static"
:type="'week'"
:quickBtnList="quickBtnList"
@change="getEcharts"
@ -93,6 +94,7 @@
<Guide title="流量使用TOP10">
<template #extra>
<TimeSelect
key="flow-top10"
:quickBtn="false"
:type="'week'"
@change="getTopRang"

View File

@ -1,85 +1,90 @@
<!-- 物联卡-首页 -->
<template>
<page-container>
<a-row :gutter="24">
<a-col :span="14">
<div class="home-guide">
<Guide title="物联卡引导"></Guide>
<div
class="home-guide-items"
:style="`grid-template-columns: repeat(${
guideList ? guideList.length : 1
}, 1fr);`"
>
<div
v-for="(item, index) in guideList"
:key="index"
class="home-guide-item step-bar arrow-2 pointer"
@click="jumpPage(item)"
>
<div class="item-english">{{ item.english }}</div>
<div class="item-title">{{ item.name }}</div>
<div class="item-index">
<img :src="Image[index + 1]" />
</div>
</div>
</div>
</div>
</a-col>
<a-col :span="10">
<div class="home-statistics">
<Guide title="基础统计">
<template #extra>
<span class="extra-text">详情</span>
</template>
</Guide>
<div class="home-statistics-body">
<div class="home-guide-item">
<div class="item-english">昨日流量统计</div>
<div class="item-title">{{ currentSource }} M</div>
<div
class="item-index-echarts"
style="height: 75px; width: 110px"
>
<div class="chart" ref="todayFlowChart"></div>
</div>
</div>
<div class="home-guide-item">
<div class="item-english">物联卡</div>
<div class="item-content">
<div
v-for="iten in pieChartData"
:key="iten.key"
class="item-node"
>
<div class="item-node-text">
{{ iten.value }}
</div>
<div :class="`state ${iten.className}`">
{{ iten.name }}
</div>
<page-container>
<a-row :gutter="24">
<a-col :span="14">
<div class="home-guide">
<Guide title="物联卡引导"></Guide>
<div
class="home-guide-items"
:style="`grid-template-columns: repeat(${
guideList ? guideList.length : 1
}, 1fr);`"
>
<div
v-for="(item, index) in guideList"
:key="index"
class="home-guide-item step-bar arrow-2 pointer"
@click="jumpPage(item)"
>
<div class="item-english">{{ item.english }}</div>
<div class="item-title">{{ item.name }}</div>
<div class="item-index">
<img :src="Image[index + 1]" />
</div>
</div>
</div>
</div>
</div>
<div
class="item-index-echarts"
style="height: 75px; width: 110px"
>
<div class="chart" ref="iotCardChart"></div>
</div>
</div>
</div>
</div>
</a-col>
<a-col :span="24" style="min-height: 580px">
<div class="home-body">
<Guide title="平台架构图" english="PLATFORM ARCHITECTURE DIAGRAM" />
<div class="home-body-img">
<img :src="getImage('/iot-card/iotcard-home.png')" />
</div>
</div>
</a-col>
</a-row>
</page-container>
</a-col>
<a-col :span="10">
<div class="home-statistics">
<Guide title="基础统计">
<template #extra>
<span class="extra-text" @click="jumpDashboard"
>详情</span
>
</template>
</Guide>
<div class="home-statistics-body">
<div class="home-guide-item">
<div class="item-english">昨日流量统计</div>
<div class="item-title">{{ currentSource }} M</div>
<div
class="item-index-echarts"
style="height: 75px; width: 110px"
>
<div class="chart" ref="todayFlowChart"></div>
</div>
</div>
<div class="home-guide-item">
<div class="item-english">物联卡</div>
<div class="item-content">
<div
v-for="iten in pieChartData"
:key="iten.key"
class="item-node"
>
<div class="item-node-text">
{{ iten.value }}
</div>
<div :class="`state ${iten.className}`">
{{ iten.name }}
</div>
</div>
</div>
<div
class="item-index-echarts"
style="height: 75px; width: 110px"
>
<div class="chart" ref="iotCardChart"></div>
</div>
</div>
</div>
</div>
</a-col>
<a-col :span="24" style="min-height: 580px">
<div class="home-body">
<Guide
title="平台架构图"
english="PLATFORM ARCHITECTURE DIAGRAM"
/>
<div class="home-body-img">
<img :src="getImage('/iot-card/iotcard-home.png')" />
</div>
</div>
</a-col>
</a-row>
</page-container>
</template>
<script setup lang="ts">
@ -89,220 +94,245 @@ import { message } from 'ant-design-vue';
import moment from 'moment';
import { queryFlow, list } from '@/api/iot-card/home';
import * as echarts from 'echarts';
import { useMenuStore } from '@/store/menu';
import { usePermissionStore } from '@/store/permission';
const router = useRouter();
const { proxy } = <any>getCurrentInstance();
interface GuideItemProps {
key: string;
name: string;
english: string;
url: string;
param?: Record<string, any>;
index?: number;
auth: boolean;
key: string;
name: string;
english: string;
url: string;
param?: Record<string, any>;
index?: number;
auth: boolean;
}
const menuHasPermission = useMenuStore().hasPermission;
const btnHasPermission = usePermissionStore().hasPermission;
//
const dashBoardUrl = menuHasPermission('/iot-card/Dashboard');
const platformUrl = menuHasPermission('/iot-card/Platform/Detail');
const recordUrl = menuHasPermission('/iot-card/Record');
const cardUrl = menuHasPermission('/iot-card/CardManagement');
//
const paltformPermission = btnHasPermission(`/iot-card/Platform:add`);
const cardPermission = btnHasPermission(`/iot-card/CardManagement:add`);
const Image = {
1: getImage('/home/1.png'),
2: getImage('/home/2.png'),
3: getImage('/home/3.png'),
1: getImage('/home/1.png'),
2: getImage('/home/2.png'),
3: getImage('/home/3.png'),
};
const guideList = [
{
key: 'EQUIPMENT',
name: '平台对接',
english: 'STEP1',
auth: '',
url: '',
},
{
key: 'SCREEN',
name: '物联卡管理',
english: 'STEP2',
auth: '',
url: '',
param: { save: true },
},
{
key: 'CASCADE',
name: '操作记录',
english: 'STEP3',
auth: '',
url: '',
},
{
key: 'EQUIPMENT',
name: '平台对接',
english: 'STEP1',
auth: paltformPermission,
url: platformUrl,
},
{
key: 'SCREEN',
name: '物联卡管理',
english: 'STEP2',
auth: !!cardPermission,
url: cardUrl,
param: { save: true },
},
{
key: 'CASCADE',
name: '操作记录',
english: 'STEP3',
auth: !!recordUrl,
url: recordUrl,
},
];
const currentSource = ref<number>(0);
const barChartData = ref<any[]>([]);
const pieChartData = ref<any[]>([
{
key: 'using',
name: '正常',
value: 0,
className: 'normal',
},
{
key: 'toBeActivated',
name: '未激活',
value: 0,
className: 'notActive',
},
{
key: 'deactivate',
name: '停用',
value: 0,
className: 'stopped',
},
{
key: 'using',
name: '正常',
value: 0,
className: 'normal',
},
{
key: 'toBeActivated',
name: '未激活',
value: 0,
className: 'notActive',
},
{
key: 'deactivate',
name: '停用',
value: 0,
className: 'stopped',
},
]);
const jumpPage = (data: GuideItemProps) => {
if (data.url && data.auth) {
router.push(`${data.url}`, data.param);
} else {
message.warning('暂无权限,请联系管理员');
}
if (data.url && data.auth) {
router.push({ path: `${data.url}`, ...data.param });
} else {
message.warning('暂无权限,请联系管理员');
}
};
const jumpDashboard = () => {
if (dashBoardUrl) {
router.push(`${dashBoardUrl}`);
} else {
message.warning('暂无权限,请联系管理员');
}
};
/**
* 获取昨日流量消耗
*/
const getTodayFlow = async () => {
const beginTime = moment().subtract(1, 'days').startOf('day').valueOf();
const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
const resp: any = await queryFlow(beginTime, endTime, { orderBy: 'date' });
resp.result.map((item: any) => {
currentSource.value += parseFloat(item.value.toFixed(2));
});
const beginTime = moment().subtract(1, 'days').startOf('day').valueOf();
const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
const resp: any = await queryFlow(beginTime, endTime, { orderBy: 'date' });
resp.result.map((item: any) => {
currentSource.value += parseFloat(item.value.toFixed(2));
});
};
/**
* 获取最近15天流量消耗统计图数据
*/
const get15DaysTrafficConsumption = async () => {
const beginTime = moment().subtract(15, 'days').startOf('day').valueOf();
const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
const resp: any = await queryFlow(beginTime, endTime, { orderBy: 'date' });
barChartData.value = resp.result
.map((item: any) => ({
...item,
}))
.reverse();
createBarChart();
const beginTime = moment().subtract(15, 'days').startOf('day').valueOf();
const endTime = moment().subtract(1, 'days').endOf('day').valueOf();
const resp: any = await queryFlow(beginTime, endTime, { orderBy: 'date' });
barChartData.value = resp.result
.map((item: any) => ({
...item,
}))
.reverse();
createBarChart();
};
/**
* 获取物联卡状态数据
*/
const getStateCard = async () => {
Promise.all(
pieChartData.value.map((item) => {
const params = {
terms: [
{
terms: [
{
column: 'cardStateType',
termType: 'eq',
value: item.key,
},
],
},
],
};
return list(params);
}),
)
.then((resp) => {
resp.forEach((i: any, index) => {
if (i.success) {
pieChartData.value[index].value = i.result.total;
}
});
createPieChart();
})
.catch((err) => {
console.log(err);
});
Promise.all(
pieChartData.value.map((item) => {
const params = {
terms: [
{
terms: [
{
column: 'cardStateType',
termType: 'eq',
value: item.key,
},
],
},
],
};
return list(params);
}),
)
.then((resp) => {
resp.forEach((i: any, index) => {
if (i.success) {
pieChartData.value[index].value = i.result.total;
}
});
createPieChart();
})
.catch((err) => {
console.log(err);
});
};
/**
* 最近15天流量消耗统计图
*/
const createBarChart = () => {
const myChart = echarts.init(proxy.$refs.todayFlowChart);
const myChart = echarts.init(proxy.$refs.todayFlowChart);
const options = {
tooltip: {},
xAxis: {
show: false,
data: barChartData.value.map((m) => m.date),
},
yAxis: {
show: false,
},
series: [
{
name: '流量消耗',
type: 'bar',
color: '#FACD89',
// barWidth: '5%', //
showBackground: true, //
data: barChartData.value.map((m) => parseFloat(m.value.toFixed(2))),
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
const options = {
tooltip: {},
xAxis: {
show: false,
data: barChartData.value.map((m) => m.date),
},
yAxis: {
show: false,
},
series: [
{
name: '流量消耗',
type: 'bar',
color: '#FACD89',
// barWidth: '5%', //
showBackground: true, //
data: barChartData.value.map((m) =>
parseFloat(m.value.toFixed(2)),
),
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
};
/**
* 物联卡饼图
*/
const createPieChart = () => {
nextTick(() => {
const myChart = echarts.init(proxy.$refs.iotCardChart);
nextTick(() => {
const myChart = echarts.init(proxy.$refs.iotCardChart);
const options = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
color: ['#85a5ff', '#f29b55', '#c4c4c4'],
series: [
{
name: '',
type: 'pie',
avoidLabelOverlap: true, //
radius: ['50%', '90%'],
center: ['50%', '50%'],
itemStyle: {
borderColor: 'rgba(255,255,255,1)',
borderWidth: 2,
},
label: {
normal: {
show: false,
const options = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
},
data: pieChartData.value,
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
color: ['#85a5ff', '#f29b55', '#c4c4c4'],
series: [
{
name: '',
type: 'pie',
avoidLabelOverlap: true, //
radius: ['50%', '90%'],
center: ['50%', '50%'],
itemStyle: {
borderColor: 'rgba(255,255,255,1)',
borderWidth: 2,
},
label: {
normal: {
show: false,
},
},
data: pieChartData.value,
},
],
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
});
});
};
watch(
barChartData.value,
() => {
createBarChart();
},
{ deep: true },
barChartData.value,
() => {
createBarChart();
},
{ deep: true },
);
getTodayFlow();
@ -312,230 +342,230 @@ getStateCard();
<style scoped lang="less">
.home-base {
position: relative;
padding: 24px 16px;
background-color: #fff;
position: relative;
padding: 24px 16px;
background-color: #fff;
}
.home-guide {
margin-bottom: 24px;
padding: 24px 16px;
background-color: #fff;
margin-bottom: 24px;
padding: 24px 16px;
background-color: #fff;
.home-guide-items {
display: grid;
grid-column-gap: 56px;
}
.home-guide-items {
display: grid;
grid-column-gap: 56px;
}
}
.home-guide-item {
position: relative;
padding: 16px;
background: linear-gradient(
135.62deg,
#f6f7fd 22.27%,
rgba(255, 255, 255, 0.86) 91.82%
);
border-radius: 2px;
box-shadow: 0 4px 18px #efefef;
.state {
position: relative;
padding-left: 8px;
&::before {
position: absolute;
top: 7px;
left: 0;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 2px;
content: '';
padding: 16px;
background: linear-gradient(
135.62deg,
#f6f7fd 22.27%,
rgba(255, 255, 255, 0.86) 91.82%
);
border-radius: 2px;
box-shadow: 0 4px 18px #efefef;
.state {
position: relative;
padding-left: 8px;
&::before {
position: absolute;
top: 7px;
left: 0;
display: inline-block;
width: 6px;
height: 6px;
margin-right: 2px;
content: '';
}
&.normal::before {
background: #85a5ff;
}
&.notActive::before {
background: #f29b55;
}
&.stopped::before {
background: #c4c4c4;
}
}
&.normal::before {
background: #85a5ff;
&.pointer {
cursor: pointer;
}
&.notActive::before {
background: #f29b55;
.item-english {
color: #4f4f4f;
}
&.stopped::before {
background: #c4c4c4;
.item-content {
display: flex;
margin-top: 15px;
width: 80%;
}
}
&.pointer {
cursor: pointer;
}
.item-node {
min-width: 58px;
margin-right: 8px;
z-index: 1;
.item-english {
color: #4f4f4f;
}
.item-content {
display: flex;
margin-top: 15px;
width: 80%;
}
.item-node {
min-width: 58px;
margin-right: 8px;
z-index: 1;
.item-node-text {
font-size: 14px;
font-weight: bold;
.item-node-text {
font-size: 14px;
font-weight: bold;
}
}
}
.item-title {
margin: 20px 0;
color: @text-color;
font-weight: 700;
font-size: 20px;
}
.item-index {
position: absolute;
right: 10%;
bottom: 0;
}
.item-index-echarts {
.item-index;
right: 12px;
bottom: 5%;
z-index: 0;
width: 50%;
.chart {
width: 100%;
height: 100%;
.item-title {
margin: 20px 0;
color: @text-color;
font-weight: 700;
font-size: 20px;
}
.item-index {
position: absolute;
right: 10%;
bottom: 0;
}
.item-index-echarts {
.item-index;
right: 12px;
bottom: 5%;
z-index: 0;
width: 50%;
.chart {
width: 100%;
height: 100%;
}
}
}
}
.home-body {
.home-base;
.home-base;
min-height: 444px;
margin-bottom: 24px;
// padding-bottom: 26.5%;
padding-bottom: 30%;
overflow: hidden;
border-bottom: 1px solid #2f54eb;
min-height: 444px;
margin-bottom: 24px;
// padding-bottom: 26.5%;
padding-bottom: 30%;
overflow: hidden;
border-bottom: 1px solid #2f54eb;
.home-body-img {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
.home-body-img {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
> img {
width: 100%;
height: 100%;
> img {
width: 100%;
height: 100%;
}
}
}
}
.home-statistics {
.home-base;
.home-base;
.extra-text {
cursor: pointer;
color: @primary-color;
}
.extra-text {
cursor: pointer;
color: @primary-color;
}
.home-statistics-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.home-statistics-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
}
.step-item-after {
position: absolute;
top: 50%;
right: -60px;
width: 60px;
height: 40px;
transform: translateY(-50%);
content: ' ';
position: absolute;
top: 50%;
right: -60px;
width: 60px;
height: 40px;
transform: translateY(-50%);
content: ' ';
}
.home-step {
.home-base;
.home-base;
.home-step-items {
display: grid;
grid-column-gap: 66px;
.home-step-items {
display: grid;
grid-column-gap: 66px;
.step-item {
display: flex;
flex-direction: column;
.step-item {
display: flex;
flex-direction: column;
.step-item-title {
position: relative;
padding: 16px 24px;
color: #333;
font-weight: bold;
font-size: 14px;
background-color: #f8f9fd;
cursor: pointer;
.step-item-title {
position: relative;
padding: 16px 24px;
color: #333;
font-weight: bold;
font-size: 14px;
background-color: #f8f9fd;
cursor: pointer;
.step-item-img {
position: absolute;
top: 0;
right: 0;
z-index: 1;
height: 100%;
.step-item-img {
position: absolute;
top: 0;
right: 0;
z-index: 1;
height: 100%;
img {
height: 100%;
}
img {
height: 100%;
}
}
> span {
position: relative;
z-index: 2;
}
}
.step-item-content {
flex-grow: 1;
height: auto;
padding: 24px;
border-right: 1px solid #e5edf4;
border-bottom: 1px solid #e5edf4;
border-left: 1px solid #e5edf4;
}
}
> span {
position: relative;
z-index: 2;
}
}
.step-item-content {
flex-grow: 1;
height: auto;
padding: 24px;
border-right: 1px solid #e5edf4;
border-bottom: 1px solid #e5edf4;
border-left: 1px solid #e5edf4;
}
}
}
}
.step-bar {
position: relative;
position: relative;
&.arrow-1 {
&:not(:last-child) {
&::after {
.step-item-after;
&.arrow-1 {
&:not(:last-child) {
&::after {
.step-item-after;
background: url('/images/home/arrow-1.png') no-repeat center;
}
background: url('/images/home/arrow-1.png') no-repeat center;
}
}
}
}
&.arrow-2 {
&:not(:last-child) {
&::after {
.step-item-after;
&.arrow-2 {
&:not(:last-child) {
&::after {
.step-item-after;
background: url('/images/home/arrow-2.png') no-repeat center;
}
background: url('/images/home/arrow-2.png') no-repeat center;
}
}
}
}
}
</style>

View File

@ -12,6 +12,7 @@
:request="queryList"
:defaultParams="{ sorts: [{ name: 'createTime', order: 'desc' }] }"
:params="params"
:gridColumn="3"
>
<template #headerTitle>
<a-space>

View File

@ -115,6 +115,7 @@ const columns = [
title: '支付URL',
dataIndex: 'url',
key: 'url',
ellipsis: true,
},
{
title: '订单时间',

View File

@ -26,7 +26,7 @@
<a-card>
<h5>权限分配</h5>
<PermissTree />
<PermissTree v-model:select-items="form.menus" />
<a-button
type="primary"
@ -39,12 +39,17 @@
</template>
<script setup lang="ts" name="RolePermiss">
import { FormInstance } from 'ant-design-vue';
import { FormInstance, message } from 'ant-design-vue';
import PermissTree from '../components/PermissTree.vue';
import { getRoleDetails_api } from '@/api/system/role';
import {
getRoleDetails_api,
updateRole_api,
updatePrimissTree_api,
} from '@/api/system/role';
const route = useRoute();
const router = useRouter();
const roleId = route.params.id as string;
//
@ -55,6 +60,7 @@ const form = reactive({
name: '',
description: '',
},
menus: [],
getForm: () => {
getRoleDetails_api(roleId).then((resp) => {
if (resp.status) {
@ -62,7 +68,15 @@ const form = reactive({
}
});
},
clickSave: () => {},
clickSave: () => {
const updateRole = updateRole_api(form.data);
const updateTree = updatePrimissTree_api(roleId, { menu: form.menus });
Promise.all([updateRole, updateTree]).then((resp) => {
message.success('操作成功');
router.push('/system/Role');
});
},
});
form.getForm();

View File

@ -1,64 +1,343 @@
<template>
<div class="permiss-tree-container">
<a-table :data-source="dataSource">
<a-table-column key="menu" data-index="menu">
<template #title><span style="">菜单权限</span></template>
</a-table-column>
<a-table-column key="action" title="操作权限" data-index="action" />
<a-table-column key="data" data-index="data">
<template #title>
<a-table
:columns="columns"
:data-source="tableData"
:pagination="false"
:rowKey="'id'"
>
<!-- 自定义表头 -->
<template #headerCell="{ column }">
<div v-if="column.key === 'menu'">
<a-checkbox
v-model:checked="selectedAll"
:indeterminate="indeterminate"
@change="selectAllChange"
>菜单权限</a-checkbox
>
</div>
<div v-else-if="column.key === 'data'">
<span style="">数据权限</span>
<a-checkbox v-model:checked="checked">批量设置</a-checkbox>
<a-checkbox
v-model:checked="bulkShow"
@change="bulkValue = ''"
>批量设置</a-checkbox
>
<a-select
v-show="checked"
v-model:value="selectValue"
v-show="bulkShow"
v-model:value="bulkValue"
:size="'middle'"
style="width: 200px"
:options="options"
:options="bulkOptions"
@change="bulkChange"
></a-select>
</template>
</a-table-column>
</div>
<div v-else>
<span>{{ column.title }}</span>
</div>
</template>
<!-- 自定义表格内容 -->
<template #bodyCell="{ column, record }">
<div v-if="column.key === 'menu'">
<a-checkbox
v-model:checked="record.granted"
:indeterminate="record.indeterminate"
@change="menuChange(record)"
>{{ record.name }}</a-checkbox
>
</div>
<div v-else-if="column.key === 'action'">
<div v-if="record.buttons && record.buttons.length > 0">
<a-checkbox
v-for="button in record.buttons"
v-model:checked="button.granted"
@change="actionChange(record)"
>{{ button.name }}</a-checkbox
>
</div>
</div>
<div v-else-if="column.key === 'data'">
<span v-if="record.accessSupport === undefined">
不支持数据权限配置默认可查看全部数据
</span>
<div v-else-if="record.accessSupport.value === 'support'">
<a-radio-group v-model:value="record.selectAccesses">
<a-radio
:value="asset.supportId"
v-for="asset in record.assetAccesses"
>{{ asset.name }}</a-radio
>
</a-radio-group>
</div>
<span
v-else-if="
record.accessSupport.value === 'indirect' ||
record.accessSupport.value === 'unsupported'
"
>{{ record.accessDescription }}</span
>
</div>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import { getPrimissTree_api } from '@/api/system/role';
const emits = defineEmits(['update:selectItems']);
const route = useRoute();
const props = defineProps({
selectItems: Array,
});
const dataSource = ref([]);
const checked = ref<boolean>(false);
const options = [
const columns = [
{
title: '菜单权限',
dataIndex: 'menu',
key: 'menu',
width: '260px',
},
{
title: '操作权限',
dataIndex: 'action',
key: 'action',
width: '260px',
},
{
title: '数据权限',
dataIndex: 'data',
key: 'data',
width: '50%',
},
];
const tableData = ref<tableItemType[]>([]);
// -
const selectedAll = ref<boolean>(false);
const indeterminate = ref<boolean>(false);
const selectAllChange = () => {
flatTableData.forEach((item) => {
item.granted = selectedAll.value;
item.buttons?.forEach((button) => {
button.granted = selectedAll.value;
});
});
indeterminate.value = false;
};
// -
const bulkShow = ref<boolean>(false);
const bulkOptions = [
{
label: '全部数据',
value: '1',
value: 'ignore',
},
{
label: '所在组织及下级组织',
value: '2',
value: 'org-include-children',
},
{
label: '所在组织',
value: '3',
value: 'org',
},
{
label: '自己创建的',
value: '4',
value: 'creator',
},
];
const selectValue = ref<string>('');
const getAllPermiss = () => {
const id = route.params.id as string;
getPrimissTree_api(id).then((resp) => {
console.log(resp);
const bulkValue = ref<string>('');
const bulkChange = () => {
if (!bulkValue) return;
flatTableData.forEach((item) => {
if (item.accessSupport && item.accessSupport.value === 'support') {
item.selectAccesses = bulkValue.value;
}
});
};
// ------------------------------
const flatTableData: tableItemType[] = []; // -- 便
getAllPermiss();
const init = () => {
getAllPermiss();
watch(tableData, () => {
const selected = cloneDeep(flatTableData).filter((item) => (item.granted || item.indeterminate) && !item.parentId);
selected.forEach((item) => {
if (
item.accessSupport &&
item.accessSupport.value === 'support' &&
item.selectAccesses
) {
item.selectAccesses = bulkValue.value;
item.assetAccesses?.forEach((asset) => {
if (asset.supportId === item.selectAccesses) {
asset.granted = true;
} else {
asset.granted = false;
}
});
delete item.selectAccesses;
}
delete item.indeterminate
});
emits(
'update:selectItems',
selected,
);
});
};
init();
function getAllPermiss() {
const id = route.params.id as string;
getPrimissTree_api(id).then((resp) => {
tableData.value = resp.result;
treeToSimple(resp.result); //
const selectList = flatTableData.filter((item) => item.granted); //
emits('update:selectItems', selectList); //
// /
if (selectList.length === flatTableData.length) {
selectedAll.value = true;
indeterminate.value = false;
} else if (selectList.length > 0) {
indeterminate.value = true;
selectedAll.value = false;
}
});
}
/**
* 菜单权限改变事件
* @param row 触发的项
* @param setButtonBool 是否改变对应的操作权限
*/
function menuChange(
row: tableItemType,
setButtonBool: boolean = true,
): undefined {
if (setButtonBool) {
if (row.buttons && row.buttons.length > 0)
row.buttons.forEach((button) => {
button.granted = row.granted;
});
row.children && setChildrenChecked(row.children, row.granted);
}
//
const selectList = flatTableData.filter((item) => item.granted); //
if (row.parentId) {
// //
const parent = flatTableData.find(
(item) => item.id === row.parentId,
) as tableItemType;
const selectLen = parent.children?.filter((item) => item.granted)
.length as number; //
if (selectLen === parent.children?.length) {
parent.granted = true;
parent.indeterminate = false;
} else if (selectLen > 0) {
parent.granted = false;
parent.indeterminate = true;
} else {
parent.granted = false;
parent.indeterminate = false;
}
if (parent.parentId) {
return menuChange(parent, false);
}
}
//
if (selectList.length === flatTableData.length) {
selectedAll.value = true;
indeterminate.value = false;
} else if (selectList.length > 0) {
indeterminate.value = true;
selectedAll.value = false;
} else {
selectedAll.value = false;
indeterminate.value = false;
}
emits('update:selectItems', selectList); //
}
/**
* 操作权限改变事件
* @param row 触发的项
*/
function actionChange(row: tableItemType) {
const selectLen = row.buttons?.filter((item) => item.granted)
.length as number;
if (selectLen === row.buttons?.length) {
row.granted = true;
row.indeterminate = false;
} else if (selectLen > 0) {
row.granted = false;
row.indeterminate = true;
} else {
row.granted = false;
row.indeterminate = false;
}
menuChange(row, false);
}
/**
* 将树形结构的表格数据拍扁为一个普通数组
* @param treeData
*/
function treeToSimple(treeData: tableItemType[]) {
treeData.forEach((item) => {
if (item.accessSupport && item.accessSupport.value === 'support') {
const select =
item.assetAccesses?.find((assetItem) => assetItem.granted) ||
{};
item.selectAccesses = select.supportId || '';
}
flatTableData.push(item);
item.children && treeToSimple(item.children);
});
}
/**
* 设置子节点的状态
* @param childrens
* @param value
*/
function setChildrenChecked(childrens: tableItemType[], value: boolean) {
if (childrens.length < 1) return;
childrens.forEach((item) => {
item.granted = value;
if (item.buttons && item.buttons.length > 0)
item.buttons.forEach((button) => {
button.granted = value;
});
item.children && setChildrenChecked(item.children, value);
});
}
type buttonItemType = {
supportId: string;
name: string;
granted: boolean;
};
type tableItemType = {
id: string;
granted: boolean;
name: string;
indeterminate?: boolean;
parentId?: string;
children?: tableItemType[];
accessSupport?: any;
buttons?: buttonItemType[];
accessDescription?: string;
selectAccesses?: string;
assetAccesses?: any[];
};
</script>
<style lang="less" scoped>

View File

@ -15,4 +15,19 @@ const route = useRoute();
const activeKey = ref('1');
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.details-container {
:deep(.ant-tabs-nav-wrap) {
background-color: #fff;
padding: 24px 0 0 24px;
}
.role-permiss-container {
padding: 24px;
}
}
</style>