This commit is contained in:
xingyu4j
2025-10-10 21:55:50 +08:00
parent 7ef1da4f2c
commit fac8cee297
7 changed files with 94 additions and 99 deletions

View File

@@ -345,7 +345,7 @@ onMounted(async () => {
<Button <Button
type="primary" type="primary"
@click="handleCreate" @click="handleCreate"
v-hasPermi="['iot:device:create']" v-access:code="['iot:device:create']"
> >
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
新增 新增
@@ -353,19 +353,19 @@ onMounted(async () => {
<Button <Button
type="primary" type="primary"
@click="handleExport" @click="handleExport"
v-hasPermi="['iot:device:export']" v-access:code="['iot:device:export']"
> >
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
导出 导出
</Button> </Button>
<Button @click="handleImport" v-hasPermi="['iot:device:import']"> <Button @click="handleImport" v-access:code="['iot:device:import']">
<IconifyIcon icon="ant-design:upload-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:upload-outlined" class="mr-1" />
导入 导入
</Button> </Button>
<Button <Button
v-show="viewMode === 'list'" v-show="viewMode === 'list'"
@click="handleAddToGroup" @click="handleAddToGroup"
v-hasPermi="['iot:device:update']" v-access:code="['iot:device:update']"
> >
<IconifyIcon icon="ant-design:folder-add-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:folder-add-outlined" class="mr-1" />
添加到分组 添加到分组
@@ -374,7 +374,7 @@ onMounted(async () => {
v-show="viewMode === 'list'" v-show="viewMode === 'list'"
danger danger
@click="handleDeleteBatch" @click="handleDeleteBatch"
v-hasPermi="['iot:device:delete']" v-access:code="['iot:device:delete']"
> >
<IconifyIcon icon="ant-design:delete-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:delete-outlined" class="mr-1" />
批量删除 批量删除

View File

@@ -53,13 +53,13 @@ const queryParams = ref({
}); });
// 获取产品名称 // 获取产品名称
const getProductName = (productId: number) => { function getProductName(productId: number) {
const product = props.products.find((p: any) => p.id === productId); const product = props.products.find((p: any) => p.id === productId);
return product?.name || '-'; return product?.name || '-';
}; }
// 获取设备列表 // 获取设备列表
const getList = async () => { async function getList() {
loading.value = true; loading.value = true;
try { try {
const data = await getDevicePage({ const data = await getDevicePage({
@@ -71,26 +71,26 @@ const getList = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
// 处理页码变化 // 处理页码变化
const handlePageChange = (page: number, pageSize: number) => { function handlePageChange(page: number, pageSize: number) {
queryParams.value.pageNo = page; queryParams.value.pageNo = page;
queryParams.value.pageSize = pageSize; queryParams.value.pageSize = pageSize;
getList(); getList();
}; }
// 获取设备类型颜色 // 获取设备类型颜色
const getDeviceTypeColor = (deviceType: number) => { function getDeviceTypeColor(deviceType: number) {
const colors: Record<number, string> = { const colors: Record<number, string> = {
0: 'blue', 0: 'blue',
1: 'cyan', 1: 'cyan',
}; };
return colors[deviceType] || 'default'; return colors[deviceType] || 'default';
}; }
// 获取设备状态信息 // 获取设备状态信息
const getStatusInfo = (state: number) => { function getStatusInfo(state: number) {
if (state === DeviceStateEnum.ONLINE) { if (state === DeviceStateEnum.ONLINE) {
return { return {
text: '在线', text: '在线',
@@ -105,7 +105,7 @@ const getStatusInfo = (state: number) => {
bgColor: '#fff1f0', bgColor: '#fff1f0',
borderColor: '#ffccc7', borderColor: '#ffccc7',
}; };
}; }
onMounted(() => { onMounted(() => {
getList(); getList();
@@ -171,7 +171,7 @@ defineExpose({
<a <a
class="value link" class="value link"
@click=" @click="
(e: MouseEvent) => { (e) => {
e.stopPropagation(); e.stopPropagation();
emit('productDetail', item.productId); emit('productDetail', item.productId);
} }
@@ -209,7 +209,7 @@ defineExpose({
size="small" size="small"
class="action-btn btn-edit" class="action-btn btn-edit"
@click=" @click="
(e: MouseEvent) => { (e) => {
e.stopPropagation(); e.stopPropagation();
emit('edit', item); emit('edit', item);
} }
@@ -289,23 +289,23 @@ defineExpose({
.device-card-view { .device-card-view {
.device-card { .device-card {
height: 100%; height: 100%;
border-radius: 8px;
overflow: hidden; overflow: hidden;
box-shadow:
0 1px 2px 0 rgba(0, 0, 0, 0.03),
0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px 0 rgba(0, 0, 0, 0.02);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid #f0f0f0;
background: #fff; background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
box-shadow:
0 1px 2px 0 rgb(0 0 0 / 3%),
0 1px 6px -1px rgb(0 0 0 / 2%),
0 2px 4px 0 rgb(0 0 0 / 2%);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover { &:hover {
box-shadow:
0 1px 2px -2px rgba(0, 0, 0, 0.16),
0 3px 6px 0 rgba(0, 0, 0, 0.12),
0 5px 12px 4px rgba(0, 0, 0, 0.09);
transform: translateY(-4px);
border-color: #e6e6e6; border-color: #e6e6e6;
box-shadow:
0 1px 2px -2px rgb(0 0 0 / 16%),
0 3px 6px 0 rgb(0 0 0 / 12%),
0 5px 12px 4px rgb(0 0 0 / 9%);
transform: translateY(-4px);
} }
:deep(.ant-card-body) { :deep(.ant-card-body) {
@@ -313,10 +313,10 @@ defineExpose({
} }
.card-content { .card-content {
padding: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
padding: 16px;
} }
// 头部区域 // 头部区域
@@ -327,48 +327,48 @@ defineExpose({
margin-bottom: 16px; margin-bottom: 16px;
.device-icon { .device-icon {
width: 32px;
height: 32px;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; width: 32px;
height: 32px;
font-size: 18px; font-size: 18px;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25); color: #fff;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 6px;
box-shadow: 0 2px 8px rgb(102 126 234 / 25%);
} }
.status-badge { .status-badge {
display: flex;
gap: 4px;
align-items: center;
padding: 2px 10px; padding: 2px 10px;
border-radius: 12px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
border: 1px solid;
line-height: 18px; line-height: 18px;
border: 1px solid;
border-radius: 12px;
.status-dot { .status-dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
background: currentcolor;
border-radius: 50%; border-radius: 50%;
background: currentColor;
} }
} }
} }
// 设备名称 // 设备名称
.device-name { .device-name {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 16px; margin-bottom: 16px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; font-size: 16px;
font-weight: 600;
line-height: 24px; line-height: 24px;
color: #262626;
white-space: nowrap;
} }
// 信息区域 // 信息区域
@@ -378,30 +378,30 @@ defineExpose({
.info-item { .info-item {
display: flex; display: flex;
gap: 8px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 12px; margin-bottom: 12px;
gap: 8px;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.label { .label {
flex-shrink: 0;
font-size: 13px; font-size: 13px;
color: #8c8c8c; color: #8c8c8c;
flex-shrink: 0;
} }
.value { .value {
font-size: 13px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
color: #262626;
text-align: right;
white-space: nowrap;
&.link { &.link {
color: #1890ff; color: #1890ff;
@@ -415,11 +415,10 @@ defineExpose({
&.code { &.code {
font-family: font-family:
'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', 'SF Mono', Monaco, Inconsolata, 'Fira Code', Consolas, monospace;
monospace;
font-size: 12px; font-size: 12px;
color: #595959;
font-weight: 500; font-weight: 500;
color: #595959;
} }
} }
} }
@@ -427,28 +426,28 @@ defineExpose({
// 操作按钮栏 // 操作按钮栏
.action-bar { .action-bar {
position: relative;
z-index: 1;
display: flex; display: flex;
gap: 8px; gap: 8px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid #f5f5f5; border-top: 1px solid #f5f5f5;
position: relative;
z-index: 1;
.action-btn { .action-btn {
flex: 1;
height: 32px;
padding: 4px 8px;
border-radius: 6px;
font-size: 13px;
display: flex; display: flex;
flex: 1;
gap: 4px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 4px; height: 32px;
transition: all 0.2s; padding: 4px 8px;
font-size: 13px;
font-weight: 400; font-weight: 400;
border: 1px solid;
cursor: pointer;
pointer-events: auto; pointer-events: auto;
cursor: pointer;
border: 1px solid;
border-radius: 6px;
transition: all 0.2s;
:deep(.anticon) { :deep(.anticon) {
font-size: 16px; font-size: 16px;

View File

@@ -60,7 +60,7 @@ function goToProductDetail(productId: number | undefined) {
<!-- 右上按钮 --> <!-- 右上按钮 -->
<Button <Button
v-if="product.status === 0" v-if="product.status === 0"
v-hasPermi="['iot:device:update']" v-access:code="['iot:device:update']"
@click="openForm('update', device.id)" @click="openForm('update', device.id)"
> >
编辑 编辑

View File

@@ -32,9 +32,9 @@ const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<div class="mb-4 flex items-start justify-between"> <div class="mb-4 flex items-start justify-between">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<span class="mb-2 text-sm font-medium text-gray-500">{{ <span class="mb-2 text-sm font-medium text-gray-500">
title {{ title }}
}}</span> </span>
<span class="text-3xl font-bold text-gray-800"> <span class="text-3xl font-bold text-gray-800">
<span v-if="value === -1">--</span> <span v-if="value === -1">--</span>
<CountTo v-else :end-val="value" :duration="1000" /> <CountTo v-else :end-val="value" :duration="1000" />
@@ -49,9 +49,9 @@ const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span class="text-gray-400">今日新增</span> <span class="text-gray-400">今日新增</span>
<span v-if="todayCount === -1" class="text-gray-400">--</span> <span v-if="todayCount === -1" class="text-gray-400">--</span>
<span v-else class="font-medium text-green-500" <span v-else class="font-medium text-green-500">
>+{{ todayCount }}</span +{{ todayCount }}
> </span>
</div> </div>
</div> </div>
</div> </div>
@@ -61,18 +61,18 @@ const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
<style scoped> <style scoped>
.stat-card { .stat-card {
height: 160px; height: 160px;
transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
} }
.stat-card:hover { .stat-card:hover {
box-shadow: 0 6px 20px rgb(0 0 0 / 8%);
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
} }
.stat-card :deep(.ant-card-body) { .stat-card :deep(.ant-card-body) {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
} }
</style> </style>

View File

@@ -27,7 +27,7 @@ const hasData = computed(() => {
}); });
/** 初始化图表 */ /** 初始化图表 */
const initChart = () => { function initChart() {
if (!hasData.value) return; if (!hasData.value) return;
nextTick(() => { nextTick(() => {
@@ -94,7 +94,7 @@ const initChart = () => {
], ],
}); });
}); });
}; }
/** 监听数据变化 */ /** 监听数据变化 */
watch( watch(

View File

@@ -80,7 +80,7 @@ const getGaugeOption = (value: number, color: string, title: string): any => {
}; };
/** 初始化图表 */ /** 初始化图表 */
const initCharts = () => { function initCharts() {
if (!hasData.value) return; if (!hasData.value) return;
nextTick(() => { nextTick(() => {
@@ -101,7 +101,7 @@ const initCharts = () => {
), ),
); );
}); });
}; }
/** 监听数据变化 */ /** 监听数据变化 */
watch( watch(

View File

@@ -1,10 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import type { import type { IotStatisticsApi } from '#/api/iot/statistics';
IotStatisticsDeviceMessageReqVO,
IotStatisticsDeviceMessageSummaryByDateRespVO,
} from '#/api/iot/statistics';
import { computed, nextTick, onMounted, reactive, ref } from 'vue'; import { computed, nextTick, onMounted, reactive, ref } from 'vue';
@@ -13,7 +10,7 @@ import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Button, Card, DatePicker, Empty, Space } from 'ant-design-vue'; import { Button, Card, DatePicker, Empty, Space } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { StatisticsApi } from '#/api/iot/statistics'; import { getDeviceMessageSummaryByDate } from '#/api/iot/statistics';
defineOptions({ name: 'MessageTrendCard' }); defineOptions({ name: 'MessageTrendCard' });
@@ -23,11 +20,11 @@ const messageChartRef = ref();
const { renderEcharts } = useEcharts(messageChartRef); const { renderEcharts } = useEcharts(messageChartRef);
const loading = ref(false); const loading = ref(false);
const messageData = ref<IotStatisticsDeviceMessageSummaryByDateRespVO[]>([]); const messageData = ref<IotStatisticsApi.DeviceMessageSummaryByDate[]>([]);
const activeTimeRange = ref('7d'); // 当前选中的时间范围 const activeTimeRange = ref('7d'); // 当前选中的时间范围
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined); const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
const queryParams = reactive<IotStatisticsDeviceMessageReqVO>({ const queryParams = reactive<IotStatisticsApi.DeviceMessageReq>({
interval: 1, // 按天 interval: 1, // 按天
times: [], times: [],
}); });
@@ -38,7 +35,7 @@ const hasData = computed(() => {
}); });
// 设置时间范围 // 设置时间范围
const setTimeRange = (range: string) => { function setTimeRange(range: string) {
activeTimeRange.value = range; activeTimeRange.value = range;
dateRange.value = undefined; // 清空自定义时间选择 dateRange.value = undefined; // 清空自定义时间选择
@@ -73,10 +70,10 @@ const setTimeRange = (range: string) => {
]; ];
fetchMessageData(); fetchMessageData();
}; }
// 处理自定义日期选择 // 处理自定义日期选择
const handleDateChange = () => { function handleDateChange() {
if (dateRange.value && dateRange.value.length === 2) { if (dateRange.value && dateRange.value.length === 2) {
activeTimeRange.value = ''; // 清空快捷选择 activeTimeRange.value = ''; // 清空快捷选择
queryParams.interval = 1; // 按天 queryParams.interval = 1; // 按天
@@ -86,16 +83,15 @@ const handleDateChange = () => {
]; ];
fetchMessageData(); fetchMessageData();
} }
}; }
// 获取消息统计数据 // 获取消息统计数据
const fetchMessageData = async () => { async function fetchMessageData() {
if (!queryParams.times || queryParams.times.length !== 2) return; if (!queryParams.times || queryParams.times.length !== 2) return;
loading.value = true; loading.value = true;
try { try {
messageData.value = messageData.value = await getDeviceMessageSummaryByDate(queryParams);
await StatisticsApi.getDeviceMessageSummaryByDate(queryParams);
await nextTick(); await nextTick();
initChart(); initChart();
} catch (error) { } catch (error) {
@@ -104,10 +100,10 @@ const fetchMessageData = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
// 初始化图表 // 初始化图表
const initChart = () => { function initChart() {
if (!hasData.value) return; if (!hasData.value) return;
const times = messageData.value.map((item) => item.time); const times = messageData.value.map((item) => item.time);
@@ -181,7 +177,7 @@ const initChart = () => {
}, },
], ],
}); });
}; }
// 组件挂载时查询数据 // 组件挂载时查询数据
onMounted(() => { onMounted(() => {