mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 02:22:25 +00:00
refactor:【antd】【iot】代码优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { DeviceTypeEnum, DICT_TYPE, LocationTypeEnum } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
@@ -68,7 +68,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['deviceType'],
|
||||
show: (values) => values.deviceType === 1, // GATEWAY_SUB
|
||||
show: (values) => values.deviceType === DeviceTypeEnum.GATEWAY_SUB,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -134,20 +134,20 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === 3, // MANUAL
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'latitude',
|
||||
label: '设备维度',
|
||||
label: '设备纬度',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入设备维度',
|
||||
placeholder: '请输入设备纬度',
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === 3, // MANUAL
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
@@ -89,9 +89,9 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Page>
|
||||
<DeviceDetailsHeader
|
||||
:device="device"
|
||||
:loading="loading"
|
||||
:product="product"
|
||||
:device="device"
|
||||
@refresh="() => getDeviceData(id)"
|
||||
/>
|
||||
|
||||
@@ -99,8 +99,8 @@ onMounted(async () => {
|
||||
<Tabs.TabPane key="info" tab="设备信息">
|
||||
<DeviceDetailsInfo
|
||||
v-if="activeTab === 'info'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
:product="product"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="model" tab="物模型数据">
|
||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
||||
<Tabs.TabPane key="simulator" tab="模拟设备">
|
||||
<DeviceDetailsSimulator
|
||||
v-if="activeTab === 'simulator'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
:product="product"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
@@ -128,11 +128,11 @@ async function updateDeviceConfig() {
|
||||
<div>
|
||||
<!-- 使用说明提示 -->
|
||||
<Alert
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「配置推送」按钮,设备即可进行远程配置。"
|
||||
type="info"
|
||||
show-icon
|
||||
class="my-4"
|
||||
description="如需编辑文件,请点击下方编辑按钮"
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「配置推送」按钮,设备即可进行远程配置。"
|
||||
show-icon
|
||||
type="info"
|
||||
/>
|
||||
|
||||
<!-- 代码视图 - 只读展示 -->
|
||||
@@ -145,8 +145,8 @@ async function updateDeviceConfig() {
|
||||
v-else
|
||||
v-model:value="configString"
|
||||
:rows="20"
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
class="json-editor"
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
@@ -154,18 +154,18 @@ async function updateDeviceConfig() {
|
||||
<Button v-if="isEditing" @click="handleCancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
:loading="saveLoading"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:loading="saveLoading"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button v-else @click="handleEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
:loading="pushLoading"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
配置推送
|
||||
</Button>
|
||||
|
||||
@@ -86,8 +86,8 @@ function openEditForm(row: IotDeviceApi.Device) {
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
<Button
|
||||
size="small"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
@click="copyToClipboard(product.productKey)"
|
||||
>
|
||||
复制
|
||||
|
||||
@@ -75,7 +75,7 @@ function handleAuthInfoDialogClose() {
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="ep:info-filled" class="mr-2 text-primary" />
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:info-filled" />
|
||||
<span>设备信息</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -124,9 +124,9 @@ function handleAuthInfoDialogClose() {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleAuthInfoDialogOpen"
|
||||
size="small"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
@@ -141,7 +141,7 @@ function handleAuthInfoDialogClose() {
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="ep:location" class="mr-2 text-primary" />
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:location" />
|
||||
<span>设备位置</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
@@ -160,7 +160,7 @@ function handleAuthInfoDialogClose() {
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
|
||||
>
|
||||
<IconifyIcon icon="ep:warning" class="mr-2" />
|
||||
<IconifyIcon class="mr-2" icon="ep:warning" />
|
||||
<span>暂无位置信息</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,9 +171,9 @@ function handleAuthInfoDialogClose() {
|
||||
<!-- 认证信息弹框 -->
|
||||
<Modal
|
||||
v-model:open="authDialogVisible"
|
||||
:footer="null"
|
||||
title="MQTT 连接参数"
|
||||
width="640px"
|
||||
:footer="null"
|
||||
>
|
||||
<Form :label-col="{ span: 6 }">
|
||||
<Form.Item label="clientId">
|
||||
@@ -183,7 +183,7 @@ function handleAuthInfoDialogClose() {
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<Button @click="copyToClipboard(authInfo.clientId)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.clientId)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
@@ -195,7 +195,7 @@ function handleAuthInfoDialogClose() {
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<Button @click="copyToClipboard(authInfo.username)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.username)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
@@ -204,19 +204,19 @@ function handleAuthInfoDialogClose() {
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
v-model:value="authInfo.password"
|
||||
readonly
|
||||
:type="authPasswordVisible ? 'text' : 'password'"
|
||||
readonly
|
||||
style="width: calc(100% - 160px)"
|
||||
/>
|
||||
<Button
|
||||
@click="authPasswordVisible = !authPasswordVisible"
|
||||
type="primary"
|
||||
@click="authPasswordVisible = !authPasswordVisible"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="authPasswordVisible ? 'lucide:eye-off' : 'lucide:eye'"
|
||||
/>
|
||||
</Button>
|
||||
<Button @click="copyToClipboard(authInfo.password)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.password)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
|
||||
@@ -186,9 +186,9 @@ defineExpose({
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Select
|
||||
v-model:value="queryParams.method"
|
||||
allow-clear
|
||||
placeholder="所有方法"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item in methodOptions"
|
||||
@@ -201,9 +201,9 @@ defineExpose({
|
||||
</Select>
|
||||
<Select
|
||||
v-model:value="queryParams.upstream"
|
||||
allow-clear
|
||||
placeholder="上行/下行"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option label="上行" value="true">上行</Select.Option>
|
||||
<Select.Option label="下行" value="false">下行</Select.Option>
|
||||
|
||||
@@ -343,18 +343,18 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
<ContentWrap>
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧:指令调试区域 -->
|
||||
<Col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
|
||||
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
|
||||
<Card class="simulator-tabs h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>指令调试</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<IconifyIcon icon="lucide:chevron-up" v-if="!debugCollapsed" />
|
||||
<IconifyIcon icon="lucide:chevron-down" v-if="debugCollapsed" />
|
||||
<IconifyIcon v-if="!debugCollapsed" icon="lucide:chevron-up" />
|
||||
<IconifyIcon v-if="debugCollapsed" icon="lucide:chevron-down" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -374,11 +374,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:data-source="propertyList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -391,11 +391,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
@@ -418,11 +418,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="eventList"
|
||||
align="center"
|
||||
:columns="eventColumns"
|
||||
:data-source="eventList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -434,19 +434,19 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:rows="3"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入事件参数(JSON格式)"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入事件参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEventPost(record)"
|
||||
>
|
||||
上报事件
|
||||
@@ -496,11 +496,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:data-source="propertyList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -513,11 +513,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
@@ -540,11 +540,11 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:data-source="serviceList"
|
||||
align="center"
|
||||
:columns="serviceColumns"
|
||||
:data-source="serviceList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -553,19 +553,19 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:rows="3"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入服务参数(JSON格式)"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
:rows="3"
|
||||
placeholder="输入服务参数(JSON格式)"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleServiceInvoke(record)"
|
||||
>
|
||||
服务调用
|
||||
@@ -583,23 +583,23 @@ async function handleServiceInvoke(row: ThingModelData) {
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:设备消息区域 -->
|
||||
<Col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
|
||||
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>设备消息</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="messageCollapsed = !messageCollapsed"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="lucide:chevron-down"
|
||||
v-if="!messageCollapsed"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
<IconifyIcon
|
||||
icon="lucide:chevron-down"
|
||||
v-if="messageCollapsed"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<Card :loading="loading" title="子设备管理">
|
||||
<Empty
|
||||
description="暂无子设备数据,此功能待实现"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无子设备数据,此功能待实现"
|
||||
/>
|
||||
<!-- TODO: 实现子设备列表展示和管理功能 -->
|
||||
</Card>
|
||||
|
||||
@@ -191,8 +191,8 @@ defineExpose({
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择事件标识符"
|
||||
allow-clear
|
||||
placeholder="请选择事件标识符"
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
@@ -208,10 +208,10 @@ defineExpose({
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
|
||||
@@ -118,7 +118,6 @@ function formatDateRangeWithTime(dates: [string, string]): [string, string] {
|
||||
return [`${dates[0]} 00:00:00`, `${dates[1]} 23:59:59`];
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
@@ -140,9 +139,8 @@ const tableColumns = computed(() => [
|
||||
dataIndex: 'value',
|
||||
align: 'center' as const,
|
||||
},
|
||||
]);
|
||||
]); // 表格列配置
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = computed(() => ({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@@ -151,7 +149,7 @@ const paginationConfig = computed(() => ({
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
showTotal: (total: number) => `共 ${total} 条数据`,
|
||||
}));
|
||||
})); // 分页配置
|
||||
|
||||
/** 获得设备历史数据 */
|
||||
async function getList() {
|
||||
@@ -429,7 +427,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</div>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<Button @click="handleRefresh" :loading="loading">
|
||||
<Button :loading="loading" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:reload-outlined" />
|
||||
</template>
|
||||
@@ -438,9 +436,9 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<Button
|
||||
@click="handleExport"
|
||||
:loading="exporting"
|
||||
:disabled="list.length === 0"
|
||||
:loading="exporting"
|
||||
@click="handleExport"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:export-outlined" />
|
||||
@@ -451,9 +449,9 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<!-- 视图切换 -->
|
||||
<Button.Group class="ml-auto">
|
||||
<Button
|
||||
:disabled="!canShowChart"
|
||||
:type="viewMode === 'chart' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'chart'"
|
||||
:disabled="!canShowChart"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:line-chart-outlined" />
|
||||
@@ -485,14 +483,14 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</div>
|
||||
|
||||
<!-- 数据展示区域 -->
|
||||
<Spin :spinning="loading" :delay="200">
|
||||
<Spin :delay="200" :spinning="loading">
|
||||
<!-- 图表模式 - 使用 v-show 确保图表组件始终挂载 -->
|
||||
<div v-show="viewMode === 'chart'" class="chart-container">
|
||||
<Empty
|
||||
v-if="list.length === 0"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无数据"
|
||||
class="py-20"
|
||||
:description="$t('common.noData')"
|
||||
/>
|
||||
<div v-show="list.length > 0">
|
||||
<EchartsUI ref="chartRef" height="500px" />
|
||||
@@ -502,8 +500,8 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<!-- 表格模式 -->
|
||||
<div v-show="viewMode === 'list'" class="table-container">
|
||||
<Table
|
||||
:data-source="list"
|
||||
:columns="tableColumns"
|
||||
:data-source="list"
|
||||
:pagination="paginationConfig"
|
||||
:scroll="{ y: 500 }"
|
||||
row-key="updateTime"
|
||||
@@ -531,7 +529,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.property-history-container {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
|
||||
@@ -34,23 +34,15 @@ import DeviceDetailsThingModelPropertyHistory from './thing-model-property-histo
|
||||
|
||||
const props = defineProps<{ deviceId: number }>();
|
||||
|
||||
// TODO @haohao:变量写在 // 列表的加载中 这种注释哈,变量后面;
|
||||
/** 列表的加载中 */
|
||||
const loading = ref(true);
|
||||
/** 显示的列表数据 */
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]);
|
||||
/** 完整的数据列表 */
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]);
|
||||
/** 查询参数 */
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 显示的列表数据
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 完整的数据列表
|
||||
const queryParams = reactive({
|
||||
keyword: '' as string,
|
||||
});
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
/** 视图模式状态 */
|
||||
const viewMode = ref<'card' | 'list'>('card');
|
||||
}); // 查询参数
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 定时器
|
||||
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
@@ -286,15 +278,15 @@ onBeforeUnmount(() => {
|
||||
<div class="flex items-center" style="gap: 16px">
|
||||
<Input
|
||||
v-model:value="queryParams.keyword"
|
||||
placeholder="请输入属性名称、标识符"
|
||||
allow-clear
|
||||
placeholder="请输入属性名称、标识符"
|
||||
style="width: 240px"
|
||||
@press-enter="handleQuery"
|
||||
/>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
class="ml-20px"
|
||||
checked-children="定时刷新"
|
||||
class="ml-20px"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</div>
|
||||
@@ -319,19 +311,19 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<template v-if="viewMode === 'card'">
|
||||
<Row :gutter="16" v-loading="loading">
|
||||
<Row v-loading="loading" :gutter="16">
|
||||
<Col
|
||||
v-for="item in list"
|
||||
:key="item.identifier"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:md="12"
|
||||
:sm="12"
|
||||
:xs="24"
|
||||
class="mb-4"
|
||||
>
|
||||
<Card
|
||||
class="relative h-full overflow-hidden transition-colors"
|
||||
:body-style="{ padding: '0' }"
|
||||
class="relative h-full overflow-hidden transition-colors"
|
||||
>
|
||||
<!-- 添加渐变背景层 -->
|
||||
<div
|
||||
@@ -341,12 +333,12 @@ onBeforeUnmount(() => {
|
||||
<!-- 标题区域 -->
|
||||
<div class="mb-3 flex items-center">
|
||||
<div class="mr-2.5 flex items-center">
|
||||
<IconifyIcon icon="ep:cpu" class="text-lg text-primary" />
|
||||
<IconifyIcon class="text-lg text-primary" icon="ep:cpu" />
|
||||
</div>
|
||||
<div class="flex-1 text-base font-bold">{{ item.name }}</div>
|
||||
<!-- 标识符 -->
|
||||
<div class="mr-2 inline-flex items-center">
|
||||
<Tag size="small" color="blue">
|
||||
<Tag color="blue" size="small">
|
||||
{{ item.identifier }}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -364,8 +356,8 @@ onBeforeUnmount(() => {
|
||||
"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="ep:data-line"
|
||||
class="text-lg text-primary"
|
||||
icon="ep:data-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,8 +205,8 @@ defineExpose({
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择服务标识符"
|
||||
allow-clear
|
||||
placeholder="请选择服务标识符"
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
@@ -222,10 +222,10 @@ defineExpose({
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -43,10 +45,11 @@ defineOptions({ name: 'IoTDevice' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const products = ref<any[]>([]);
|
||||
const deviceGroups = ref<any[]>([]);
|
||||
const products = ref<IotProductApi.Product[]>([]);
|
||||
const deviceGroups = ref<IotDeviceGroupApi.DeviceGroup[]>([]);
|
||||
const viewMode = ref<'card' | 'list'>('card');
|
||||
const cardViewRef = ref();
|
||||
const checkedIds = ref<number[]>([]);
|
||||
|
||||
const [DeviceFormModal, deviceFormModalApi] = useVbenModal({
|
||||
connectedComponent: DeviceForm,
|
||||
@@ -150,7 +153,7 @@ async function handleDelete(row: IotDeviceApi.Device) {
|
||||
});
|
||||
try {
|
||||
await deleteDevice(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.deviceName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
@@ -164,7 +167,7 @@ async function handleDeleteBatch() {
|
||||
return;
|
||||
}
|
||||
const hideLoading = message.loading({
|
||||
content: '正在批量删除...',
|
||||
content: $t('ui.actionMessage.deletingBatch'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
@@ -191,7 +194,6 @@ function handleImport() {
|
||||
deviceImportFormModalApi.open();
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
@@ -235,7 +237,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
/** 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口 */
|
||||
const originalQuery = gridApi.query.bind(gridApi);
|
||||
gridApi.query = async (params?: Record<string, any>) => {
|
||||
if (viewMode.value === 'list') {
|
||||
@@ -348,11 +350,11 @@ onMounted(async () => {
|
||||
</Select>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<IconifyIcon icon="ant-design:search-outlined" class="mr-1" />
|
||||
搜索
|
||||
{{ $t('common.search') }}
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<IconifyIcon icon="ant-design:reload-outlined" class="mr-1" />
|
||||
重置
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -361,21 +363,21 @@ onMounted(async () => {
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
label: $t('ui.actionTitle.create', ['设备']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['iot:device:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '导出',
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['iot:device:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: '导入',
|
||||
label: $t('ui.actionTitle.import'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.UPLOAD,
|
||||
auth: ['iot:device:import'],
|
||||
@@ -391,7 +393,7 @@ onMounted(async () => {
|
||||
onClick: handleAddToGroup,
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
@@ -428,7 +430,7 @@ onMounted(async () => {
|
||||
class="cursor-pointer text-primary"
|
||||
@click="openProductDetail(row.productId)"
|
||||
>
|
||||
{{ products.find((p: any) => p.id === row.productId)?.name || '-' }}
|
||||
{{ products.find((p) => p.id === row.productId)?.name || '-' }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -441,7 +443,7 @@ onMounted(async () => {
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ deviceGroups.find((g: any) => g.id === groupId)?.name }}
|
||||
{{ deviceGroups.find((g) => g.id === groupId)?.name }}
|
||||
</Tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
@@ -452,7 +454,7 @@ onMounted(async () => {
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看',
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
onClick: openDetail.bind(null, row.id!),
|
||||
},
|
||||
@@ -473,7 +475,7 @@ onMounted(async () => {
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
popConfirm: {
|
||||
title: `确认删除设备 ${row.deviceName} 吗?`,
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.deviceName]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
// TODO @haohao:product 的 card-view 的意见,这里看看要不要也改改下。
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel, getDictObj } from '@vben/hooks';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { isValidColor, TinyColor } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -15,10 +12,10 @@ import {
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getDevicePage } from '#/api/iot/device/device';
|
||||
|
||||
interface Props {
|
||||
@@ -53,94 +50,6 @@ const queryParams = ref({
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
/** 默认状态映射 */
|
||||
const DEFAULT_STATUS_MAP: Record<
|
||||
'default' | number,
|
||||
{ bgColor: string; borderColor: string; color: string; text: string }
|
||||
> = {
|
||||
[DeviceStateEnum.ONLINE]: {
|
||||
text: '在线',
|
||||
color: '#52c41a',
|
||||
bgColor: '#f6ffed',
|
||||
borderColor: '#b7eb8f',
|
||||
},
|
||||
[DeviceStateEnum.OFFLINE]: {
|
||||
text: '离线',
|
||||
color: '#faad14',
|
||||
bgColor: '#fffbe6',
|
||||
borderColor: '#ffe58f',
|
||||
},
|
||||
[DeviceStateEnum.INACTIVE]: {
|
||||
text: '未激活',
|
||||
color: '#ff4d4f',
|
||||
bgColor: '#fff1f0',
|
||||
borderColor: '#ffccc7',
|
||||
},
|
||||
default: {
|
||||
text: '未知状态',
|
||||
color: '#595959',
|
||||
bgColor: '#fafafa',
|
||||
borderColor: '#d9d9d9',
|
||||
},
|
||||
};
|
||||
|
||||
/** 颜色类型预设 */
|
||||
const COLOR_TYPE_PRESETS: Record<
|
||||
string,
|
||||
{ bgColor: string; borderColor: string; color: string }
|
||||
> = {
|
||||
success: {
|
||||
color: '#52c41a',
|
||||
bgColor: '#f6ffed',
|
||||
borderColor: '#b7eb8f',
|
||||
},
|
||||
processing: {
|
||||
color: '#1890ff',
|
||||
bgColor: '#e6f7ff',
|
||||
borderColor: '#91d5ff',
|
||||
},
|
||||
warning: {
|
||||
color: '#faad14',
|
||||
bgColor: '#fffbe6',
|
||||
borderColor: '#ffe58f',
|
||||
},
|
||||
error: {
|
||||
color: '#ff4d4f',
|
||||
bgColor: '#fff1f0',
|
||||
borderColor: '#ffccc7',
|
||||
},
|
||||
default: {
|
||||
color: '#595959',
|
||||
bgColor: '#fafafa',
|
||||
borderColor: '#d9d9d9',
|
||||
},
|
||||
};
|
||||
|
||||
/** 规范化颜色类型 */
|
||||
function normalizeColorType(colorType?: string) {
|
||||
switch (colorType) {
|
||||
case 'danger': {
|
||||
return 'error';
|
||||
}
|
||||
case 'default':
|
||||
case 'error':
|
||||
case 'processing':
|
||||
case 'success':
|
||||
case 'warning': {
|
||||
return colorType;
|
||||
}
|
||||
case 'info': {
|
||||
return 'default';
|
||||
}
|
||||
case 'primary': {
|
||||
return 'processing';
|
||||
}
|
||||
default: {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取产品名称 */
|
||||
function getProductName(productId: number) {
|
||||
const product = props.products.find((p: any) => p.id === productId);
|
||||
@@ -169,54 +78,6 @@ function handlePageChange(page: number, pageSize: number) {
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 获取设备类型颜色 */
|
||||
function getDeviceTypeColor(deviceType: number) {
|
||||
const colors: Record<number, string> = {
|
||||
0: 'blue',
|
||||
1: 'cyan',
|
||||
};
|
||||
return colors[deviceType] || 'default';
|
||||
}
|
||||
|
||||
/** 获取设备状态信息 */
|
||||
// TODO @haohao:这里可以简化下么?体感看着有点复杂哈;
|
||||
function getStatusInfo(state: null | number | string | undefined) {
|
||||
const parsedState = Number(state);
|
||||
const hasNumericState = Number.isFinite(parsedState);
|
||||
const fallback = hasNumericState
|
||||
? DEFAULT_STATUS_MAP[parsedState] || DEFAULT_STATUS_MAP.default
|
||||
: DEFAULT_STATUS_MAP.default;
|
||||
const dict = getDictObj(
|
||||
DICT_TYPE.IOT_DEVICE_STATE,
|
||||
hasNumericState ? parsedState : state,
|
||||
);
|
||||
if (dict) {
|
||||
if (!dict.colorType && !dict.cssClass) {
|
||||
return {
|
||||
...fallback,
|
||||
text: dict.label || fallback.text,
|
||||
};
|
||||
}
|
||||
const presetKey = normalizeColorType(dict.colorType);
|
||||
if (isValidColor(dict.cssClass)) {
|
||||
const baseColor = new TinyColor(dict.cssClass);
|
||||
return {
|
||||
text: dict.label || fallback.text,
|
||||
color: baseColor.toHexString(),
|
||||
bgColor: baseColor.clone().setAlpha(0.15).toRgbString(),
|
||||
borderColor: baseColor.clone().lighten(30).toHexString(),
|
||||
};
|
||||
}
|
||||
const preset = COLOR_TYPE_PRESETS[presetKey] || COLOR_TYPE_PRESETS.default;
|
||||
return {
|
||||
text: dict.label || fallback.text,
|
||||
...preset,
|
||||
};
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
reload: getList,
|
||||
search: () => {
|
||||
@@ -260,17 +121,11 @@ onMounted(() => {
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="device-title">{{ item.deviceName }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="status-badge"
|
||||
:style="{
|
||||
color: getStatusInfo(item.state).color,
|
||||
backgroundColor: getStatusInfo(item.state).bgColor,
|
||||
borderColor: getStatusInfo(item.state).borderColor,
|
||||
}"
|
||||
>
|
||||
<span class="status-dot"></span>
|
||||
{{ getStatusInfo(item.state).text }}
|
||||
</div>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||
:value="item.state"
|
||||
class="status-tag"
|
||||
/>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="mb-3">
|
||||
@@ -291,17 +146,11 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">设备类型</span>
|
||||
<Tag
|
||||
:color="getDeviceTypeColor(item.deviceType)"
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
>
|
||||
{{
|
||||
getDictLabel(
|
||||
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||
item.deviceType,
|
||||
)
|
||||
}}
|
||||
</Tag>
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Deviceid</span>
|
||||
@@ -395,7 +244,7 @@ onMounted(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -409,24 +258,9 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 状态徽章
|
||||
.status-badge {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
// 状态标签
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
border: 1px solid;
|
||||
border-radius: 12px;
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: currentcolor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// 信息列表
|
||||
@@ -512,12 +346,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.action-btn-data {
|
||||
color: #722ed1;
|
||||
border-color: #722ed1;
|
||||
color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background: #722ed1;
|
||||
background: #fa8c16;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotDeviceApi.Device>();
|
||||
if (!data || !data.id) {
|
||||
// 新增模式:设置默认值(如果需要)
|
||||
// TODO @haohao:是不是 return 就好啦;不用这里 undefined 啦;
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 编辑模式:加载数据
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
@@ -89,9 +89,9 @@ async function handleDownload() {
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<Upload
|
||||
:before-upload="beforeUpload"
|
||||
:max-count="1"
|
||||
accept=".xls,.xlsx"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<Button type="primary"> 选择 Excel 文件</Button>
|
||||
</Upload>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请输入分类排序',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
precision: 0,
|
||||
},
|
||||
rules: z.number().min(0, '分类排序不能为空'),
|
||||
defaultValue: 0,
|
||||
rules: z.number().min(0, '分类排序不能小于 0'),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
|
||||
@@ -68,13 +68,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
||||
if (!data || !data.id) {
|
||||
// 新增模式:设置默认值
|
||||
// TODO @AI:可以参考部门,进一步简化代码;通过 defaultValue 在 schema 里设置默认值
|
||||
formData.value = undefined;
|
||||
await formApi.setValues({
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 编辑模式:加载数据
|
||||
|
||||
@@ -16,161 +16,6 @@ import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
|
||||
getSimpleProductCategoryList().then((data) => (categoryList = data));
|
||||
|
||||
/** 新增/修改产品的表单 */
|
||||
export function useFormSchema(
|
||||
formApi?: any,
|
||||
generateProductKey?: () => string,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
return !values.id;
|
||||
},
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, 'ProductKey 不能为空')
|
||||
.max(32, 'ProductKey 长度不能超过 32 个字符'),
|
||||
suffix: () => {
|
||||
// 创建时的 ProductKey 字段(带生成按钮)
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
if (generateProductKey) {
|
||||
formApi?.setFieldValue('productKey', generateProductKey());
|
||||
}
|
||||
},
|
||||
},
|
||||
{ default: () => '重新生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'productKey',
|
||||
label: 'ProductKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 ProductKey',
|
||||
disabled: true, // 编辑时的 ProductKey 字段(禁用,无按钮)
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
if(values) {
|
||||
return !!values.id;
|
||||
},
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, 'ProductKey 不能为空')
|
||||
.max(32, 'ProductKey 长度不能超过 32 个字符'),
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '产品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入产品名称',
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, '产品名称不能为空')
|
||||
.max(64, '产品名称长度不能超过 64 个字符'),
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '产品分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductCategoryList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品分类',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'deviceType',
|
||||
label: '设备类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'netType',
|
||||
label: '联网方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_NET_TYPE, 'number'),
|
||||
placeholder: '请选择联网方式',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'codecType',
|
||||
label: '数据格式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '产品状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'icon',
|
||||
label: '产品图标',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '产品图片',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '产品描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入产品描述',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 基础表单字段(不含图标、图片、描述) */
|
||||
export function useBasicFormSchema(
|
||||
formApi?: any,
|
||||
|
||||
@@ -119,8 +119,8 @@ function handleUnpublish(product: IotProductApi.Product) {
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
<Button
|
||||
size="small"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
@click="copyToClipboard(product.productKey || '')"
|
||||
>
|
||||
复制
|
||||
|
||||
@@ -22,7 +22,7 @@ function formatDate(date?: Date | string) {
|
||||
|
||||
<template>
|
||||
<Card title="产品信息">
|
||||
<Descriptions bordered :column="3" size="small">
|
||||
<Descriptions :column="3" bordered size="small">
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ product.name }}
|
||||
</Descriptions.Item>
|
||||
@@ -57,7 +57,7 @@ function formatDate(date?: Date | string) {
|
||||
>
|
||||
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="产品描述" :span="3">
|
||||
<Descriptions.Item :span="3" label="产品描述">
|
||||
{{ product.description || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
@@ -107,19 +107,19 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑产品 */
|
||||
function handleEdit(row: any) {
|
||||
function handleEdit(row: IotProductApi.Product) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除产品 */
|
||||
async function handleDelete(row: any) {
|
||||
async function handleDelete(row: IotProductApi.Product) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteProduct(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
@@ -153,7 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
} as VxeTableGridOptions<IotProductApi.Product>,
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
/** 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口 */
|
||||
const originalQuery = gridApi.query.bind(gridApi);
|
||||
gridApi.query = async (params?: Record<string, any>) => {
|
||||
if (viewMode.value === 'list') {
|
||||
@@ -180,9 +180,9 @@ onMounted(() => {
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入产品名称"
|
||||
allow-clear
|
||||
class="w-[220px]"
|
||||
placeholder="请输入产品名称"
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -191,9 +191,9 @@ onMounted(() => {
|
||||
</Input>
|
||||
<Input
|
||||
v-model:value="queryParams.productKey"
|
||||
placeholder="请输入产品标识"
|
||||
allow-clear
|
||||
class="w-[220px]"
|
||||
placeholder="请输入产品标识"
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -201,11 +201,11 @@ onMounted(() => {
|
||||
</template>
|
||||
</Input>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<IconifyIcon icon="ant-design:search-outlined" class="mr-1" />
|
||||
<IconifyIcon class="mr-1" icon="ant-design:search-outlined" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<IconifyIcon icon="ant-design:reload-outlined" class="mr-1" />
|
||||
<IconifyIcon class="mr-1" icon="ant-design:reload-outlined" />
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
@@ -214,13 +214,13 @@ onMounted(() => {
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增产品',
|
||||
label: $t('ui.actionTitle.create', ['产品']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '导出',
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
onClick: handleExport,
|
||||
@@ -245,12 +245,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Grid table-title="产品列表" v-show="viewMode === 'list'">
|
||||
<Grid v-show="viewMode === 'list'" table-title="产品列表">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
onClick: openProductDetail.bind(null, row.id!),
|
||||
},
|
||||
@@ -272,7 +272,7 @@ onMounted(() => {
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: row.status === ProductStatusEnum.PUBLISHED,
|
||||
popConfirm: {
|
||||
title: `确认删除产品 ${row.name} 吗?`,
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
@@ -288,16 +288,10 @@ onMounted(() => {
|
||||
:category-list="categoryList"
|
||||
:search-params="queryParams"
|
||||
@create="handleCreate"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
@detail="openProductDetail"
|
||||
@edit="handleEdit"
|
||||
@thing-model="openThingModel"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||
:deep(.vxe-grid--form-wrapper) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
@@ -14,10 +13,10 @@ import {
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getProductPage } from '#/api/iot/product/product';
|
||||
|
||||
interface Props {
|
||||
@@ -74,15 +73,6 @@ function handlePageChange(page: number, pageSize: number) {
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 获取设备类型颜色 */
|
||||
function getDeviceTypeColor(deviceType: number) {
|
||||
const colors: Record<number, string> = {
|
||||
0: 'blue',
|
||||
1: 'green',
|
||||
};
|
||||
return colors[deviceType] || 'default';
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
reload: getList,
|
||||
query: () => {
|
||||
@@ -137,18 +127,11 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品类型</span>
|
||||
<!-- TODO @AI:这个要不完全用字典的 dict-tag? -->
|
||||
<Tag
|
||||
:color="getDeviceTypeColor(item.deviceType)"
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
>
|
||||
{{
|
||||
getDictLabel(
|
||||
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||
item.deviceType,
|
||||
)
|
||||
}}
|
||||
</Tag>
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品标识</span>
|
||||
@@ -267,8 +250,7 @@ onMounted(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: white;
|
||||
// TODO @haohao:这里的紫色,和下面的紫色按钮,看看能不能换下。嘿嘿,感觉 AI 比较喜欢用紫色,但是放现有的后台,有点突兀
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -337,8 +319,8 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #667eea;
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
color: #1890ff;
|
||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -378,12 +360,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.action-btn-model {
|
||||
color: #722ed1;
|
||||
border-color: #722ed1;
|
||||
color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background: #722ed1;
|
||||
background: #fa8c16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,8 +401,8 @@ html.dark {
|
||||
}
|
||||
|
||||
.product-image {
|
||||
color: #8b9cff;
|
||||
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
|
||||
color: #69c0ff;
|
||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotProductApi.Product>();
|
||||
if (!data || !data.id) {
|
||||
// 新增:设置默认值(status 通过 schema 中的 defaultValue 自动设置为 0)
|
||||
// 新增:确保 Collapse 折叠,并设置默认值
|
||||
activeKey.value = [];
|
||||
await formApi.setValues({
|
||||
productKey: generateProductKey(),
|
||||
});
|
||||
@@ -117,10 +118,19 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
formData.value = await getProduct(data.id);
|
||||
await formApi.setValues(formData.value);
|
||||
// 设置高级表单(如果已挂载)
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
// 如果存在高级字段数据,自动展开 Collapse
|
||||
if (
|
||||
formData.value?.icon ||
|
||||
formData.value?.picUrl ||
|
||||
formData.value?.description
|
||||
) {
|
||||
activeKey.value = ['advanced'];
|
||||
// 等待 Collapse 展开后表单挂载
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
|
||||
Reference in New Issue
Block a user