refactor.dashboard: 重构工作台页面

- 移除欢迎信息和分割线
- 添加产品数、设备数等统计卡片
- 新增链式图表组件
- 更新数据面板布局和样式
- 添加设备上报记录功能
-优化地图组件
- 调整产品属性表格
This commit is contained in:
Kven 2025-03-02 17:53:03 +08:00
parent ea1df9d55f
commit 64d2079779
9 changed files with 564 additions and 196 deletions

22
src/api/dashboard.ts Normal file
View File

@ -0,0 +1,22 @@
import axios from 'axios';
// 获取设备信息
export function getDeviceInfo() {
return axios.get('/api/rest/device/status');
}
// 获取消息信息
export function getMessageInfo() {
return axios.get('/api/rest/device/record/status');
}
// 获取告警信息
export function getAlarmInfo() {
return axios.get('/api/rest/device/data/status');
}
// 获取产品信息
export function getProductInfo() {
return axios.get('/api/rest/product/status');
}

View File

@ -32,11 +32,12 @@ export interface DeviceUpdateRecord extends DeviceCreateRecord {
export interface DeviceEventRecord {
id: number;
clientId: string;
clientId: number;
serveName: string;
params: string;
}
// 分页查询
export function queryDeviceList(data: DeviceRecord) {
return axios({
@ -74,11 +75,11 @@ export function deleteDevice(id: number) {
}
// 查询上报
export function queryDeviceReport(clientId: number) {
export function queryDeviceRecord(data: DeviceRecord) {
return axios({
url: `/api/rest/device/record/photo`,
url: `/api/rest/device/record/data`,
method: 'get',
params: clientId,
params: data,
});
}
@ -95,3 +96,5 @@ export function triggerEvent(data: DeviceEventRecord) {
data,
});
}

View File

@ -1,11 +1,11 @@
<template>
<a-col class="banner">
<a-col :span="8">
<a-typography-title :heading="5" style="margin-top: 0">
{{ $t('workplace.welcome') }} {{ userInfo.name }}
</a-typography-title>
</a-col>
<a-divider class="panel-border" />
<!-- <a-col :span="8">-->
<!-- <a-typography-title :heading="5" style="margin-top: 0">-->
<!-- {{ $t('workplace.welcome') }} {{ userInfo.name }}-->
<!-- </a-typography-title>-->
<!-- </a-col>-->
<!-- <a-divider class="panel-border" />-->
</a-col>
</template>
@ -25,7 +25,7 @@
.banner {
width: 100%;
padding: 20px 20px 0 20px;
background-color: var(--color-bg-2);
//background-color: var(--color-bg-2);
border-radius: 4px 4px 0 0;
}

View File

@ -0,0 +1,264 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card :bordered="false" :style="cardStyle">
<div class="content-wrap">
<div class="content">
<a-statistic
:title="title"
:value="renderData.count"
:value-from="0"
animation
show-group-separator
/>
<div class="desc">
<a-typography-text type="secondary" class="label">
较昨日
</a-typography-text>
<a-typography-text type="danger">
{{ renderData.growth }}
<icon-arrow-rise />
</a-typography-text>
</div>
</div>
<div class="chart">
<Chart v-if="!loading" :option="chartOption" />
</div>
</div>
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, PropType, CSSProperties } from 'vue';
import useLoading from '@/hooks/loading';
import useChartOption from '@/hooks/chart-option';
const barChartOptionsFactory = () => {
const data = ref<any>([]);
const { chartOption } = useChartOption(() => {
return {
grid: {
left: 0,
right: 0,
top: 10,
bottom: 0,
},
xAxis: {
type: 'category',
show: false,
},
yAxis: {
show: false,
},
tooltip: {
show: true,
trigger: 'axis',
},
series: {
name: 'total',
data,
type: 'bar',
barWidth: 7,
itemStyle: {
borderRadius: 2,
},
},
};
});
return {
data,
chartOption,
};
};
const lineChartOptionsFactory = () => {
const data = ref<number[][]>([[], []]);
const { chartOption } = useChartOption(() => {
return {
grid: {
left: 0,
right: 0,
top: 10,
bottom: 0,
},
xAxis: {
type: 'category',
show: false,
},
yAxis: {
show: false,
},
tooltip: {
show: true,
trigger: 'axis',
},
series: [
{
name: '2001',
data: data.value[0],
type: 'line',
showSymbol: false,
smooth: true,
lineStyle: {
color: '#165DFF',
width: 3,
},
},
{
name: '2002',
data: data.value[1],
type: 'line',
showSymbol: false,
smooth: true,
lineStyle: {
color: '#6AA1FF',
width: 3,
type: 'dashed',
},
},
],
};
});
return {
data,
chartOption,
};
};
const pieChartOptionsFactory = () => {
const data = ref<any>([]);
const { chartOption } = useChartOption(() => {
return {
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
legend: {
show: true,
top: 'center',
right: '0',
orient: 'vertical',
icon: 'circle',
itemWidth: 6,
itemHeight: 6,
textStyle: {
color: '#4E5969',
},
},
tooltip: {
show: true,
},
series: [
{
name: '总计',
type: 'pie',
radius: ['50%', '70%'],
label: {
show: false,
},
data,
},
],
};
});
return {
data,
chartOption,
};
};
const props = defineProps({
title: {
type: String,
default: '',
},
quota: {
type: String,
default: '',
},
chartType: {
type: String,
default: '',
},
cardStyle: {
type: Object as PropType<CSSProperties>,
default: () => {
return {};
},
},
});
const { loading, setLoading } = useLoading(true);
const { chartOption: lineChartOption, data: lineData } =
lineChartOptionsFactory();
const { chartOption: barChartOption, data: barData } =
barChartOptionsFactory();
const { chartOption: pieChartOption, data: pieData } =
pieChartOptionsFactory();
const renderData = ref<any>({
count: 0,
growth: 0,
chartData: [],
});
const chartOption = ref({});
try {
// const { data } = renderData.value
// renderData.value = data;
if (props.chartType === 'bar') {
chartOption.value = barChartOption.value;
} else if (props.chartType === 'line') {
chartOption.value = lineChartOption.value;
} else {
chartOption.value = pieChartOption.value;
}
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
</script>
<style scoped lang="less">
:deep(.arco-card) {
border-radius: 4px;
}
:deep(.arco-card-body) {
width: 100%;
height: 134px;
padding: 0;
}
.content-wrap {
width: 100%;
padding: 16px;
white-space: nowrap;
}
:deep(.content) {
float: left;
width: 108px;
height: 102px;
}
:deep(.arco-statistic) {
.arco-statistic-title {
font-size: 16px;
font-weight: bold;
white-space: nowrap;
}
.arco-statistic-content {
margin-top: 10px;
}
}
.chart {
float: right;
width: calc(100% - 108px);
height: 90px;
vertical-align: bottom;
}
.label {
padding-right: 8px;
font-size: 12px;
}
</style>

View File

@ -1,158 +1,203 @@
<template>
<div style="width: 100%">
<a-grid :cols="24" :row-gap="16" class="panel">
<a-grid :cols="24" :row-gap="10" :col-gap="10" class="panel">
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 12, xxl: 6 }"
:span="{ xs: 12, sm: 12, md: 12, lg: 8, xl: 6, xxl: 6 }"
>
<a-space @click="to('PASS')">
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/288b89194e657603ff40db39e8072640.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
title="通过"
:value="formData.pass || 0"
:value-from="0"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
<a-card :bordered="false" class="square-card">
<a-row align="center">
<a-col :span="12">
<a-statistic
title="产品数"
value-style="font-size:36px"
:value="365"
:value-from="0"
animation
>
</a-statistic>
</a-col>
<a-col :span="12">
<a-avatar
:size="80"
image-url="http://iot.le5le.cn/iot/img/%E4%BA%A7%E5%93%81.png"
shape="square"
:style="{ backgroundColor: '#ffffff' }"
/>
<!-- <a-avatar-->
<!-- :size="80"-->
<!-- image-url="http://iot.le5le.cn/iot/img/%E6%95%B0%E6%8D%AE.png"-->
<!-- shape="square"-->
<!-- :style="{ backgroundColor: '#ffffff' }"-->
<!-- />-->
</a-col>
</a-row>
<a-descriptions :data="data" layout="inline-vertical" class="responsive-margin" />
</a-card>
</a-grid-item>
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 12, xxl: 6 }"
:span="{ xs: 12, sm: 12, md: 12, lg: 8, xl: 6, xxl: 6 }"
>
<a-space @click="to('FAILED')">
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/fdc66b07224cdf18843c6076c2587eb5.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
title="未通过"
:value="formData.notPass || 0"
:value-from="0"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
<a-card :bordered="false" class="square-card">
<a-row align="center">
<a-col :span="12">
<a-statistic
title="设备数"
value-style="font-size:36px"
:value="deviceInfo.deviceCount"
:value-from="0"
animation
>
</a-statistic>
</a-col>
<a-col :span="12">
<a-avatar
:size="80"
image-url="http://iot.le5le.cn/iot/img/%E8%AE%BE%E5%A4%87.png"
shape="square"
:style="{ backgroundColor: '#ffffff' }"
/>
</a-col>
</a-row>
<a-descriptions :data="data" layout="inline-vertical" class="responsive-margin" />
</a-card>
</a-grid-item>
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 12, xxl: 6 }"
:span="{ xs: 12, sm: 12, md: 12, lg: 8, xl: 6, xxl: 6 }"
>
<a-space @click="to('EXAMINE')">
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77d74c9a245adeae1ec7fb5d4539738d.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
title="未审计"
:value="formData.notAudit || 0"
:value-from="0"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
<a-card :bordered="false" class="square-card">
<a-row align="center">
<a-col :span="12">
<a-statistic
title="产品数"
value-style="font-size:36px"
:value="365"
:value-from="0"
animation
>
</a-statistic>
</a-col>
<a-col :span="12">
<a-avatar
:size="80"
image-url="http://iot.le5le.cn/iot/img/%E5%91%8A%E8%AD%A6.png"
shape="square"
:style="{ backgroundColor: '#ffffff' }"
/>
</a-col>
</a-row>
<a-descriptions :data="data" layout="inline-vertical" class="responsive-margin" />
</a-card>
</a-grid-item>
<a-grid-item
v-if="userStore.permissions !== 'auditor'"
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 12, xxl: 6 }"
style="border-right: none"
:span="{ xs: 12, sm: 12, md: 12, lg: 8, xl: 6, xxl: 6 }"
>
<a-space @click="to('SUBMIT')">
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
v-model="formData.notFiled"
title="未归档"
:value="formData.notFiled || 0"
:value-from="0"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
<Announcement />
</a-grid-item>
<a-grid-item :span="24">
<a-divider class="panel-border" />
</a-grid-item>
</a-grid>
<a-grid :span="24" :row-gap="10" :col-gap="10" class="panel">
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 8, xxl: 8 }">
<ChainItem
title="分享总量"
quota="share"
chart-type="pie"
:card-style="{
background: isDark
? 'linear-gradient(180deg, #312565 0%, #201936 100%)'
: 'linear-gradient(180deg, #F7F7FF 0%, #ECECFF 100%)',
}"
/>
</a-grid-item>
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 8, xxl: 8 }">
<ChainItem
title="分享总量"
quota="share"
chart-type="pie"
:card-style="{
background: isDark
? 'linear-gradient(180deg, #312565 0%, #201936 100%)'
: 'linear-gradient(180deg, #F7F7FF 0%, #ECECFF 100%)',
}"
/>
</a-grid-item>
<a-grid-item
class="panel-col"
:span="{ xs: 12, sm: 12, md: 12, lg: 12, xl: 8, xxl: 8 }">
<ChainItem
title="分享总量"
quota="share"
chart-type="pie"
:card-style="{
background: isDark
? 'linear-gradient(180deg, #312565 0%, #201936 100%)'
: 'linear-gradient(180deg, #F7F7FF 0%, #ECECFF 100%)',
}"
/>
</a-grid-item>
<a-grid-item :span="24">
<a-divider class="panel-border" />
</a-grid-item>
</a-grid>
<a-card :bordered="false">
<a-space id="TicketEcharts" style="width: 98%; height: 400px"></a-space>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { useUserStore } from '@/store';
import Announcement from '@/views/dashboard/workplace/components/announcement.vue';
import ChainItem from '@/views/dashboard/workplace/components/chain-item.vue';
import useThemes from '@/hooks/themes';
import { getDeviceInfo } from '@/api/dashboard';
import { onMounted, ref } from 'vue';
import * as echarts from 'echarts';
import router from '@/router';
const { t } = useI18n();
const userStore = useUserStore();
const formData = ref({
notAudit: undefined,
notFiled: undefined,
notPass: undefined,
pass: undefined,
});
const getHomeData = async (params: {
auditorId: number | undefined;
userId: number | string | undefined;
}) => {
if (userStore.permissions === 'admin') {
params.userId = '';
} else if (userStore.permissions === 'auditor') {
params.auditorId = userStore.id;
} else if (userStore.permissions === 'user') {
params.userId = userStore.id;
}
};
const to = (key: string) => {
router.push({
name: 'TicketManage',
query: {
status: key,
},
});
};
getHomeData({});
const data = [{
label: 'Name',
value: 'Socrates',
}, {
label: 'Mobile',
value: '123',
}, {
label: 'Residence',
value: 'Beijing'
}];
const deviceInfo = ref({
deviceCount: 0,
onlineCount: 0,
offlineCount: 0,
alarmCount: 0,
productCount: 0,
})
const fetchData = async () => {
const res = await getDeviceInfo();
deviceInfo.value = res.data;
}
const { isDark } = useThemes();
onMounted(() => {
fetchData();
})
</script>
<style lang="less" scoped>
.arco-grid.panel {
margin-bottom: 0;
padding: 16px 20px 0 20px;
padding: 10px 20px 0 0;
}
.panel-col {
padding-left: 43px;
padding-left: 20px; // : 使
border-right: 1px solid rgb(var(--gray-2));
}
.col-avatar {
@ -170,4 +215,14 @@
:deep(.panel-border) {
margin: 4px 0 0 0;
}
.square-card {
width: 100%;
//aspect-ratio: 4 / 3; // : 4:3
padding: 15px 20px 13px 20px;
}
.responsive-margin {
margin-top: 20px; //
}
</style>

View File

@ -16,21 +16,23 @@
</a-grid-item>
</a-grid>
</div>
<div class="right-side">
<a-grid :cols="24" :row-gap="16">
<a-grid-item :span="24">
<div class="panel moduler-wrap">
<QuickOperation />
<RecentlyVisited />
</div>
</a-grid-item>
<a-grid-item class="panel" :span="24"> </a-grid-item>
<a-grid-item class="panel" :span="24">
<Announcement />
</a-grid-item>
<a-grid-item class="panel" :span="24"> </a-grid-item>
</a-grid>
</div>
<!-- <div class="right-side">-->
<!-- <a-grid :cols="24" :row-gap="16">-->
<!-- <a-grid-item :span="24">-->
<!-- <div class="panel moduler-wrap">-->
<!--&lt;!&ndash; <QuickOperation />&ndash;&gt;-->
<!--&lt;!&ndash; <RecentlyVisited />&ndash;&gt;-->
<!-- </div>-->
<!-- </a-grid-item>-->
<!-- <a-grid-item class="panel" :span="24">-->
<!-- <Announcement />-->
<!-- </a-grid-item>-->
<!-- <a-grid-item class="panel" :span="24">-->
<!-- <Announcement />-->
<!-- </a-grid-item>-->
<!-- <a-grid-item class="panel" :span="24"> </a-grid-item>-->
<!-- </a-grid>-->
<!-- </div>-->
</div>
</template>
@ -50,7 +52,7 @@
<style lang="less" scoped>
.container {
background-color: var(--color-fill-2);
//background-color: var(--color-fill-2);
padding: 16px 20px;
padding-bottom: 0;
display: flex;
@ -67,7 +69,7 @@
}
.panel {
background-color: var(--color-bg-2);
//background-color: var(--color-bg-2);
border-radius: 4px;
overflow: auto;
}
@ -77,7 +79,7 @@
}
.moduler-wrap {
border-radius: 4px;
background-color: var(--color-bg-2);
//background-color: var(--color-bg-2);
:deep(.text) {
font-size: 12px;
text-align: center;

View File

@ -63,6 +63,9 @@
<a-tab-pane key="3" tab="执行服务" title="执行服务">
执行服务内容
</a-tab-pane>
<a-tab-pane key="4" tab="上报记录" title="上报记录">
<a-table :columns="deviceReportColumns" :data="deviceReportData" />
</a-tab-pane>
</a-tabs>
</a-card>
</div>
@ -72,7 +75,7 @@
import dayjs from 'dayjs';
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
import { queryDeviceDetail } from '@/api/device';
import { queryDeviceDetail, queryDeviceRecord } from '@/api/device';
const route = useRoute();
const id = Number(route.params.id);
@ -98,11 +101,29 @@
slotName: 'type',
},
];
const deviceReportColumns = [
{
title: '上报时间',
dataIndex: 'recordTime',
slotName: 'recordTime',
},
{
title: '内容',
dataIndex: 'content',
slotName: 'content',
},
];
const activeKey = ref('1');
const renderData = ref([]);
const deviceReportData = ref([]);
const fetchData = async (Id: number) => {
const res = await queryDeviceDetail(Id);
renderData.value = res.data;
const res1 = await queryDeviceRecord({
size: 10,
current: 1
});
deviceReportData.value = res1.data;
};
const handleMenuClick = (e: any) => {
activeKey.value = e;

View File

@ -6,13 +6,6 @@
</a-layout-sider>
<a-layout-content>
<div id="container"></div>
<!-- 标记点信息弹出框 -->
<a-modal v-model:visible="infoVisible" title="设备信息" :footer="null">
<p>ID: {{ selectedDevice.id }}</p>
<p>名称: {{ selectedDevice.name }}</p>
<p>状态: {{ selectedDevice.state }}</p>
<p>在线状态: {{ selectedDevice.online ? '是' : '否' }}</p>
</a-modal>
</a-layout-content>
</a-layout>
</template>
@ -24,43 +17,46 @@
import "@amap/amap-jsapi-types";
let map: any = null;
const selectedDevice = ref<DeviceRecord | null>(null);
const infoVisible = ref(false);
const deviceList = ref<DeviceRecord[]>([]);
const markerContent = `<div class="custom-content-marker">
<img src="//a.amap.com/jsapi_demos/static/demo-center/icons/dir-via-marker.png">
<div class="close-btn">X</div>
</div>`;
AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
version: '2.0', // JSAPI 1.4.15
plugins: [], // 使'AMap.Scale'
})
.then((AMap) => {
map = new AMap.Map('container', {
// id
viewMode: '3D', // 3D
zoom: 11, //
center: [116.397428, 39.90923], //
});
map.on('click', (e: any) => {
//
const lngLat = e.lnglat;
const content = document.createElement('div');
content.className = 'custom-content-marker';
content.innerHTML = `
<img src="https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png" alt="标记点" />
<div class="close-btn">×</div>`;
//
const marker = new AMap.Marker({
position: lngLat,
content,
});
//
map.add(marker);
//
const closeBtn = marker.getContent().querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
map.remove(marker); //
});
});
})
.catch((e) => {
console.log(e);
});
onMounted(() => {
AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
version: '2.0', // JSAPI 1.4.15
plugins: [], // 使'AMap.Scale'
})
.then((AMap) => {
map = new AMap.Map('container', {
// id
viewMode: '3D', // 3D
zoom: 11, //
center: [116.397428, 39.90923], //
});
//
const position = new AMap.LngLat(116.397428, 39.90923);
const marker = new AMap.Marker({
position,
content: markerContent,
offset: new AMap.Pixel(-13, -30),
});
map.add(marker);
})
.catch((e) => {
console.log(e);
});
//
queryDeviceList({
current: 1,
@ -74,9 +70,9 @@
});
});
onUnmounted(() => {
map?.destroy();
});
// onUnmounted(() => {
// map?.destroy();
// });
</script>

View File

@ -23,7 +23,11 @@
<template #icon><icon-plus /></template>
新建
</a-button>
<a-table :columns="columns" :data="propertyData">
<a-table
:columns="columns"
:data="propertyData"
:pagination="true"
>
<template #operation="{ record }">
<a-button
v-permission="['iot:property:delete']"
@ -426,6 +430,7 @@
} from '@/api/tsl';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import { FormInstance } from '@arco-design/web-vue/es/form';
import usePagination from '@/hooks/pagination';
import { Message } from '@arco-design/web-vue';
const { visible, setVisible } = useVisible();
@ -579,7 +584,7 @@
const fetchData = async (Id: number) => {
const params = {
size: 10,
size: 50,
current: 1,
productId: Id,
};