refactor:【antd】【iot】设备管理跟后端对齐,必要的 ReqVO、RespVO,子设备管理实现
This commit is contained in:
@@ -3,39 +3,76 @@ import type { PageParam, PageResult } from '@vben/request';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace IotDeviceApi {
|
||||
// TODO @haohao:需要跟后端对齐,必要的 ReqVO、RespVO
|
||||
/** 设备 */
|
||||
export interface Device {
|
||||
id?: number; // 设备 ID,主键,自增
|
||||
/** 设备新增/修改 Request VO */
|
||||
export interface DeviceSaveReqVO {
|
||||
id?: number; // 设备编号
|
||||
deviceName: string; // 设备名称
|
||||
nickname?: string; // 备注名称
|
||||
serialNumber?: string; // 设备序列号
|
||||
picUrl?: string; // 设备图片
|
||||
groupIds?: number[]; // 设备分组编号数组
|
||||
productId: number; // 产品编号(必填)
|
||||
gatewayId?: number; // 网关设备 ID
|
||||
config?: string; // 设备配置
|
||||
locationType: number; // 定位类型(必填)
|
||||
latitude?: number; // 设备位置的纬度
|
||||
longitude?: number; // 设备位置的经度
|
||||
}
|
||||
|
||||
/** 设备更新分组 Request VO */
|
||||
export interface DeviceUpdateGroupReqVO {
|
||||
ids: number[]; // 设备编号列表(必填)
|
||||
groupIds: number[]; // 分组编号列表(必填)
|
||||
}
|
||||
|
||||
/** 设备分页 Request VO */
|
||||
export interface DevicePageReqVO extends PageParam {
|
||||
deviceName?: string; // 设备名称
|
||||
nickname?: string; // 备注名称
|
||||
productId?: number; // 产品编号
|
||||
deviceType?: number; // 设备类型
|
||||
status?: number; // 设备状态
|
||||
groupId?: number; // 设备分组编号
|
||||
gatewayId?: number; // 网关设备 ID
|
||||
}
|
||||
|
||||
/** 设备 Response VO */
|
||||
export interface DeviceRespVO {
|
||||
id: number; // 设备编号
|
||||
deviceName: string; // 设备名称
|
||||
nickname?: string; // 设备备注名称
|
||||
serialNumber?: string; // 设备序列号
|
||||
picUrl?: string; // 设备图片
|
||||
groupIds?: number[]; // 设备分组编号数组
|
||||
productId: number; // 产品编号
|
||||
productKey?: string; // 产品标识
|
||||
deviceType?: number; // 设备类型
|
||||
nickname?: string; // 设备备注名称
|
||||
gatewayId?: number; // 网关设备 ID
|
||||
state?: number; // 设备状态
|
||||
status?: number; // 设备状态(兼容字段)
|
||||
onlineTime?: Date; // 最后上线时间
|
||||
offlineTime?: Date; // 最后离线时间
|
||||
activeTime?: Date; // 设备激活时间
|
||||
createTime?: Date; // 创建时间
|
||||
ip?: string; // 设备的 IP 地址
|
||||
firmwareVersion?: string; // 设备的固件版本
|
||||
deviceSecret?: string; // 设备密钥,用于设备认证,需安全存储
|
||||
mqttClientId?: string; // MQTT 客户端 ID
|
||||
mqttUsername?: string; // MQTT 用户名
|
||||
mqttPassword?: string; // MQTT 密码
|
||||
authType?: string; // 认证类型
|
||||
locationType?: number; // 定位类型
|
||||
deviceSecret?: string; // 设备密钥,用于设备认证
|
||||
authType?: string; // 认证类型(如一机一密、动态注册)
|
||||
config?: string; // 设备配置
|
||||
locationType?: number; // 定位方式
|
||||
latitude?: number; // 设备位置的纬度
|
||||
longitude?: number; // 设备位置的经度
|
||||
areaId?: number; // 地区编码
|
||||
address?: string; // 设备详细地址
|
||||
serialNumber?: string; // 设备序列号
|
||||
config?: string; // 设备配置
|
||||
groupIds?: number[]; // 添加分组 ID
|
||||
picUrl?: string; // 设备图片
|
||||
location?: string; // 位置信息(格式:经度,纬度)
|
||||
createTime?: Date; // 创建时间
|
||||
}
|
||||
|
||||
/** 设备认证信息 Response VO */
|
||||
export interface DeviceAuthInfoRespVO {
|
||||
clientId: string; // 客户端 ID
|
||||
username: string; // 用户名
|
||||
password: string; // 密码
|
||||
}
|
||||
|
||||
/** 设备导入 Response VO */
|
||||
export interface DeviceImportRespVO {
|
||||
createDeviceNames?: string[]; // 创建成功的设备名称列表
|
||||
updateDeviceNames?: string[]; // 更新成功的设备名称列表
|
||||
failureDeviceNames?: Record<string, string>; // 失败的设备名称及原因
|
||||
}
|
||||
|
||||
/** IoT 设备属性详细 VO */
|
||||
@@ -56,30 +93,17 @@ export namespace IotDeviceApi {
|
||||
updateTime: Date; // 更新时间
|
||||
}
|
||||
|
||||
/** 设备认证参数 VO */
|
||||
export interface DeviceAuthInfo {
|
||||
clientId: string; // 客户端 ID
|
||||
username: string; // 用户名
|
||||
password: string; // 密码
|
||||
}
|
||||
|
||||
/** 设备发送消息 Request VO */
|
||||
export interface DeviceMessageSendReq {
|
||||
deviceId: number; // 设备编号
|
||||
method: string; // 请求方法
|
||||
params?: any; // 请求参数
|
||||
}
|
||||
|
||||
/** 设备分组更新请求 */
|
||||
export interface DeviceGroupUpdateReq {
|
||||
ids: number[]; // 设备 ID 列表
|
||||
groupIds: number[]; // 分组 ID 列表
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询设备分页 */
|
||||
export function getDevicePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotDeviceApi.Device>>(
|
||||
export function getDevicePage(params: IotDeviceApi.DevicePageReqVO) {
|
||||
return requestClient.get<PageResult<IotDeviceApi.DeviceRespVO>>(
|
||||
'/iot/device/page',
|
||||
{ params },
|
||||
);
|
||||
@@ -87,38 +111,40 @@ export function getDevicePage(params: PageParam) {
|
||||
|
||||
/** 查询设备详情 */
|
||||
export function getDevice(id: number) {
|
||||
return requestClient.get<IotDeviceApi.Device>(`/iot/device/get?id=${id}`);
|
||||
return requestClient.get<IotDeviceApi.DeviceRespVO>(
|
||||
`/iot/device/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增设备 */
|
||||
export function createDevice(data: IotDeviceApi.Device) {
|
||||
return requestClient.post('/iot/device/create', data);
|
||||
export function createDevice(data: IotDeviceApi.DeviceSaveReqVO) {
|
||||
return requestClient.post<number>('/iot/device/create', data);
|
||||
}
|
||||
|
||||
/** 修改设备 */
|
||||
export function updateDevice(data: IotDeviceApi.Device) {
|
||||
return requestClient.put('/iot/device/update', data);
|
||||
export function updateDevice(data: IotDeviceApi.DeviceSaveReqVO) {
|
||||
return requestClient.put<boolean>('/iot/device/update', data);
|
||||
}
|
||||
|
||||
/** 修改设备分组 */
|
||||
export function updateDeviceGroup(data: IotDeviceApi.DeviceGroupUpdateReq) {
|
||||
return requestClient.put('/iot/device/update-group', data);
|
||||
export function updateDeviceGroup(data: IotDeviceApi.DeviceUpdateGroupReqVO) {
|
||||
return requestClient.put<boolean>('/iot/device/update-group', data);
|
||||
}
|
||||
|
||||
/** 删除单个设备 */
|
||||
export function deleteDevice(id: number) {
|
||||
return requestClient.delete(`/iot/device/delete?id=${id}`);
|
||||
return requestClient.delete<boolean>(`/iot/device/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 删除多个设备 */
|
||||
export function deleteDeviceList(ids: number[]) {
|
||||
return requestClient.delete('/iot/device/delete-list', {
|
||||
return requestClient.delete<boolean>('/iot/device/delete-list', {
|
||||
params: { ids: ids.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
/** 导出设备 */
|
||||
export function exportDeviceExcel(params: any) {
|
||||
export function exportDeviceExcel(params: IotDeviceApi.DevicePageReqVO) {
|
||||
return requestClient.download('/iot/device/export-excel', { params });
|
||||
}
|
||||
|
||||
@@ -129,16 +155,22 @@ export function getDeviceCount(productId: number) {
|
||||
|
||||
/** 获取设备的精简信息列表 */
|
||||
export function getSimpleDeviceList(deviceType?: number, productId?: number) {
|
||||
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
|
||||
params: { deviceType, productId },
|
||||
});
|
||||
return requestClient.get<IotDeviceApi.DeviceRespVO[]>(
|
||||
'/iot/device/simple-list',
|
||||
{
|
||||
params: { deviceType, productId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 根据产品编号,获取设备的精简信息列表 */
|
||||
export function getDeviceListByProductId(productId: number) {
|
||||
return requestClient.get<IotDeviceApi.Device[]>('/iot/device/simple-list', {
|
||||
params: { productId },
|
||||
});
|
||||
return requestClient.get<IotDeviceApi.DeviceRespVO[]>(
|
||||
'/iot/device/simple-list',
|
||||
{
|
||||
params: { productId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取导入模板 */
|
||||
@@ -148,10 +180,13 @@ export function importDeviceTemplate() {
|
||||
|
||||
/** 导入设备 */
|
||||
export function importDevice(file: File, updateSupport: boolean) {
|
||||
return requestClient.upload('/iot/device/import', {
|
||||
file,
|
||||
updateSupport,
|
||||
});
|
||||
return requestClient.upload<IotDeviceApi.DeviceImportRespVO>(
|
||||
'/iot/device/import',
|
||||
{
|
||||
file,
|
||||
updateSupport,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取设备属性最新数据 */
|
||||
@@ -172,7 +207,7 @@ export function getHistoryDevicePropertyList(params: any) {
|
||||
|
||||
/** 获取设备认证信息 */
|
||||
export function getDeviceAuthInfo(id: number) {
|
||||
return requestClient.get<IotDeviceApi.DeviceAuthInfo>(
|
||||
return requestClient.get<IotDeviceApi.DeviceAuthInfoRespVO>(
|
||||
'/iot/device/get-auth-info',
|
||||
{ params: { id } },
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
/** 基础表单字段 */
|
||||
export function useBasicFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
@@ -36,6 +36,14 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'deviceType',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deviceName',
|
||||
label: 'DeviceName',
|
||||
@@ -62,7 +70,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleDeviceList(DeviceTypeEnum.GATEWAY),
|
||||
labelField: 'nickname',
|
||||
labelField: 'deviceName',
|
||||
valueField: 'id',
|
||||
placeholder: '子设备可选择父设备',
|
||||
},
|
||||
@@ -71,6 +79,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
show: (values) => values.deviceType === DeviceTypeEnum.GATEWAY_SUB,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 高级设置表单字段(更多设置) */
|
||||
export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '备注名称',
|
||||
@@ -89,6 +103,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '设备图片',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
{
|
||||
fieldName: 'groupIds',
|
||||
label: '设备分组',
|
||||
@@ -278,6 +297,14 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '备注名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '设备图片',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '所属产品',
|
||||
|
||||
@@ -31,7 +31,7 @@ const router = useRouter();
|
||||
const id = Number(route.params.id);
|
||||
const loading = ref(true);
|
||||
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
|
||||
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
|
||||
const device = ref<IotDeviceApi.DeviceRespVO>({} as IotDeviceApi.DeviceRespVO);
|
||||
const activeTab = ref('info');
|
||||
const thingModelList = ref<ThingModelData[]>([]);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
defineOptions({ name: 'DeviceDetailConfig' });
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.Device;
|
||||
device: IotDeviceApi.DeviceRespVO;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -114,7 +114,7 @@ async function updateDeviceConfig() {
|
||||
await updateDevice({
|
||||
id: props.device.id,
|
||||
config: JSON.stringify(config.value),
|
||||
} as IotDeviceApi.Device);
|
||||
} as IotDeviceApi.DeviceSaveReqVO);
|
||||
message.success({ content: '更新成功!' });
|
||||
// 触发 success 事件
|
||||
emit('success');
|
||||
|
||||
@@ -12,7 +12,7 @@ import DeviceForm from '../../modules/form.vue';
|
||||
|
||||
interface Props {
|
||||
product: IotProductApi.Product;
|
||||
device: IotDeviceApi.Device;
|
||||
device: IotDeviceApi.DeviceRespVO;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function goToProductDetail(productId: number | undefined) {
|
||||
}
|
||||
|
||||
/** 打开编辑表单 */
|
||||
function openEditForm(row: IotDeviceApi.Device) {
|
||||
function openEditForm(row: IotDeviceApi.DeviceRespVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getDeviceAuthInfo } from '#/api/iot/device/device';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
interface Props {
|
||||
device: IotDeviceApi.Device;
|
||||
device: IotDeviceApi.DeviceRespVO;
|
||||
product: IotProductApi.Product;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ const props = defineProps<Props>();
|
||||
|
||||
const authDialogVisible = ref(false);
|
||||
const authPasswordVisible = ref(false);
|
||||
const authInfo = ref<IotDeviceApi.DeviceAuthInfo>(
|
||||
{} as IotDeviceApi.DeviceAuthInfo,
|
||||
const authInfo = ref<IotDeviceApi.DeviceAuthInfoRespVO>(
|
||||
{} as IotDeviceApi.DeviceAuthInfoRespVO,
|
||||
);
|
||||
|
||||
/** 控制地图显示的标志 */
|
||||
@@ -75,8 +75,7 @@ function handleAuthInfoDialogClose() {
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:info-filled" />
|
||||
<IconifyIcon class="mr-2 text-primary" icon="lucide:info" />
|
||||
<span>设备信息</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -142,8 +141,7 @@ function handleAuthInfoDialogClose() {
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:location" />
|
||||
<IconifyIcon class="mr-2 text-primary" icon="lucide:map-pin" />
|
||||
<span>设备位置</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
@@ -162,8 +160,7 @@ function handleAuthInfoDialogClose() {
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
|
||||
>
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2" icon="ep:warning" />
|
||||
<IconifyIcon class="mr-2" icon="lucide:alert-triangle" />
|
||||
<span>暂无位置信息</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ import DataDefinition from '../../../../thingmodel/modules/components/data-defin
|
||||
import DeviceDetailsMessage from './message.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.Device;
|
||||
device: IotDeviceApi.DeviceRespVO;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
@@ -1,45 +1,223 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { Card, Empty } from 'ant-design-vue';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// TODO @haohao:这里要实现一把么?
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Input, Select, Space } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDevicePage } from '#/api/iot/device/device';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
|
||||
interface Props {
|
||||
deviceId: number;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const subDevices = ref<any[]>([]);
|
||||
/** 产品列表 */
|
||||
const products = ref<IotProductApi.Product[]>([]);
|
||||
|
||||
/** 获取子设备列表 */
|
||||
async function getSubDeviceList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// TODO: 实现获取子设备列表的API调用
|
||||
// const data = await getSubDevicesByGatewayId(deviceId);
|
||||
// subDevices.value = data || [];
|
||||
subDevices.value = [];
|
||||
} catch (error) {
|
||||
console.error('获取子设备列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
deviceName: '',
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'deviceName',
|
||||
title: 'DeviceName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '备注名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '所属产品',
|
||||
minWidth: 120,
|
||||
slots: { default: 'product' },
|
||||
},
|
||||
{
|
||||
field: 'state',
|
||||
title: '设备状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.IOT_DEVICE_STATE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'onlineTime',
|
||||
title: '最后上线时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSubDeviceList();
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDevicePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
gatewayId: props.deviceId,
|
||||
deviceType: DeviceTypeEnum.GATEWAY_SUB,
|
||||
deviceName: queryParams.deviceName || undefined,
|
||||
status: queryParams.status,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 搜索操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置搜索 */
|
||||
function resetQuery() {
|
||||
queryParams.deviceName = '';
|
||||
queryParams.status = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取产品名称 */
|
||||
function getProductName(productId: number) {
|
||||
const product = products.value.find((p) => p.id === productId);
|
||||
return product?.name || '-';
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function openDetail(id: number) {
|
||||
router.push({ name: 'IoTDeviceDetail', params: { id } });
|
||||
}
|
||||
|
||||
/** 监听设备ID变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取产品列表
|
||||
products.value = await getSimpleProductList();
|
||||
|
||||
// 如果设备ID存在,则查询列表
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :loading="loading" title="子设备管理">
|
||||
<Empty
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无子设备数据,此功能待实现"
|
||||
/>
|
||||
<!-- TODO: 实现子设备列表展示和管理功能 -->
|
||||
</Card>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
v-model:value="queryParams.deviceName"
|
||||
placeholder="请输入设备名称"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@press-enter="handleQuery"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="请选择设备状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<IconifyIcon icon="ep:refresh-right" class="mr-5px" />
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 子设备列表 -->
|
||||
<Grid>
|
||||
<!-- 所属产品列 -->
|
||||
<template #product="{ row }">
|
||||
{{ getProductName(row.productId) }}
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看详情',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: openDetail.bind(null, row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<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';
|
||||
@@ -51,6 +50,9 @@ const viewMode = ref<'card' | 'list'>('card');
|
||||
const cardViewRef = ref();
|
||||
const checkedIds = ref<number[]>([]);
|
||||
|
||||
/** 判断是否为列表视图 */
|
||||
const isListView = () => viewMode.value === 'list';
|
||||
|
||||
const [DeviceFormModal, deviceFormModalApi] = useVbenModal({
|
||||
connectedComponent: DeviceForm,
|
||||
destroyOnClose: true,
|
||||
@@ -66,13 +68,13 @@ const [DeviceImportFormModal, deviceImportFormModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const queryParams = ref({
|
||||
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
|
||||
deviceName: '',
|
||||
nickname: '',
|
||||
productId: undefined as number | undefined,
|
||||
deviceType: undefined as number | undefined,
|
||||
status: undefined as number | undefined,
|
||||
groupId: undefined as number | undefined,
|
||||
productId: undefined,
|
||||
deviceType: undefined,
|
||||
status: undefined,
|
||||
groupId: undefined,
|
||||
}); // 搜索参数
|
||||
|
||||
/** 搜索 */
|
||||
@@ -112,7 +114,11 @@ async function handleViewModeChange(mode: 'card' | 'list') {
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDeviceExcel(queryParams.value);
|
||||
const data = await exportDeviceExcel({
|
||||
...queryParams.value,
|
||||
pageNo: 1,
|
||||
pageSize: 999999,
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
downloadFileFromBlobPart({ fileName: '物联网设备.xls', source: data });
|
||||
}
|
||||
|
||||
@@ -141,12 +147,12 @@ function handleCreate() {
|
||||
}
|
||||
|
||||
/** 编辑设备 */
|
||||
function handleEdit(row: IotDeviceApi.Device) {
|
||||
function handleEdit(row: IotDeviceApi.DeviceRespVO) {
|
||||
deviceFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除设备 */
|
||||
async function handleDelete(row: IotDeviceApi.Device) {
|
||||
async function handleDelete(row: IotDeviceApi.DeviceRespVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.deviceName]),
|
||||
duration: 0,
|
||||
@@ -197,12 +203,12 @@ function handleImport() {
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: IotDeviceApi.Device[];
|
||||
records: IotDeviceApi.DeviceRespVO[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.DeviceRespVO>({
|
||||
gridOptions: {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
@@ -213,12 +219,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
return await getDevicePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...queryParams.value,
|
||||
});
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -230,7 +240,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<IotDeviceApi.Device>,
|
||||
},
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
@@ -388,7 +398,7 @@ onMounted(async () => {
|
||||
type: 'primary',
|
||||
icon: 'ant-design:folder-add-outlined',
|
||||
auth: ['iot:device:update'],
|
||||
ifShow: () => viewMode === 'list',
|
||||
ifShow: isListView,
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleAddToGroup,
|
||||
},
|
||||
@@ -398,7 +408,7 @@ onMounted(async () => {
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['iot:device:delete'],
|
||||
ifShow: () => viewMode === 'list',
|
||||
ifShow: isListView,
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
@@ -490,7 +500,14 @@ onMounted(async () => {
|
||||
ref="cardViewRef"
|
||||
:products="products"
|
||||
:device-groups="deviceGroups"
|
||||
:search-params="queryParams"
|
||||
:search-params="{
|
||||
deviceName: queryParams.deviceName || '',
|
||||
nickname: queryParams.nickname || '',
|
||||
productId: queryParams.productId,
|
||||
deviceType: queryParams.deviceType,
|
||||
status: queryParams.status,
|
||||
groupId: queryParams.groupId,
|
||||
}"
|
||||
@create="handleCreate"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
Card,
|
||||
Col,
|
||||
Empty,
|
||||
Image,
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Row,
|
||||
@@ -43,9 +46,9 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<any[]>([]);
|
||||
const list = ref<IotDeviceApi.DeviceRespVO[]>([]);
|
||||
const total = ref(0);
|
||||
const queryParams = ref({
|
||||
const queryParams = ref<Partial<IotDeviceApi.DevicePageReqVO>>({
|
||||
pageNo: 1,
|
||||
pageSize: 12,
|
||||
});
|
||||
@@ -63,7 +66,7 @@ async function getList() {
|
||||
const data = await getDevicePage({
|
||||
...queryParams.value,
|
||||
...props.searchParams,
|
||||
});
|
||||
} as IotDeviceApi.DevicePageReqVO);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
} finally {
|
||||
@@ -128,8 +131,8 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="mb-3">
|
||||
<div class="info-list">
|
||||
<div class="mb-3 flex items-start">
|
||||
<div class="info-list flex-1">
|
||||
<div class="info-item">
|
||||
<span class="info-label">所属产品</span>
|
||||
<a
|
||||
@@ -154,13 +157,27 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Deviceid</span>
|
||||
<Tooltip :title="item.Deviceid || item.id" placement="top">
|
||||
<Tooltip :title="String(item.id)" placement="top">
|
||||
<span class="info-value device-id cursor-pointer">
|
||||
{{ item.Deviceid || item.id }}
|
||||
{{ item.id }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备图片 -->
|
||||
<div class="device-image">
|
||||
<Image
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
:preview="true"
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else
|
||||
icon="lucide:image"
|
||||
class="text-2xl opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="action-buttons">
|
||||
@@ -263,6 +280,19 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 设备图片
|
||||
.device-image {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #1890ff;
|
||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// 信息列表
|
||||
.info-list {
|
||||
.info-item {
|
||||
@@ -385,6 +415,11 @@ html.dark {
|
||||
color: rgb(255 255 255 / 75%);
|
||||
}
|
||||
}
|
||||
|
||||
.device-image {
|
||||
color: #69c0ff;
|
||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Collapse, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'IoTDeviceForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<IotDeviceApi.Device>();
|
||||
const formData = ref<IotDeviceApi.DeviceRespVO>();
|
||||
const products = ref<IotProductApi.Product[]>([]);
|
||||
const activeKey = ref<string[]>([]);
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['设备'])
|
||||
@@ -31,10 +36,54 @@ const [Form, formApi] = useVbenForm({
|
||||
},
|
||||
wrapperClass: 'grid-cols-1',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
schema: useBasicFormSchema(),
|
||||
showDefaultActions: false,
|
||||
handleValuesChange: async (values, changedFields) => {
|
||||
// 当产品 ProductId 变化时,自动设置设备类型
|
||||
if (changedFields.includes('productId')) {
|
||||
const productId = values.productId;
|
||||
if (!productId) {
|
||||
await formApi.setFieldValue('deviceType', undefined);
|
||||
return;
|
||||
}
|
||||
// 从产品列表中查找产品
|
||||
const product = products.value.find((p) => p.id === productId);
|
||||
if (product?.deviceType !== undefined) {
|
||||
await formApi.setFieldValue('deviceType', product.deviceType);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1',
|
||||
layout: 'horizontal',
|
||||
schema: useAdvancedFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 获取高级表单的值(如果表单未挂载,则从 formData 中获取) */
|
||||
async function getAdvancedFormValues() {
|
||||
if (advancedFormApi.isMounted) {
|
||||
return await advancedFormApi.getValues();
|
||||
}
|
||||
// 表单未挂载(折叠状态),从 formData 中获取
|
||||
return {
|
||||
nickname: formData.value?.nickname,
|
||||
picUrl: formData.value?.picUrl,
|
||||
groupIds: formData.value?.groupIds,
|
||||
serialNumber: formData.value?.serialNumber,
|
||||
locationType: formData.value?.locationType,
|
||||
longitude: formData.value?.longitude,
|
||||
latitude: formData.value?.latitude,
|
||||
};
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
@@ -42,8 +91,13 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as IotDeviceApi.Device;
|
||||
// 合并两个表单的值(字段不冲突,可直接合并)
|
||||
const basicValues = await formApi.getValues();
|
||||
const advancedValues = await getAdvancedFormValues();
|
||||
const data = {
|
||||
...basicValues,
|
||||
...advancedValues,
|
||||
} as IotDeviceApi.DeviceSaveReqVO;
|
||||
try {
|
||||
await (formData.value?.id ? updateDevice(data) : createDevice(data));
|
||||
// 关闭并提示
|
||||
@@ -57,11 +111,14 @@ const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
activeKey.value = [];
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<IotDeviceApi.Device>();
|
||||
const data = modalApi.getData<IotDeviceApi.DeviceRespVO>();
|
||||
if (!data || !data.id) {
|
||||
// 新增:确保 Collapse 折叠
|
||||
activeKey.value = [];
|
||||
return;
|
||||
}
|
||||
// 编辑模式:加载数据
|
||||
@@ -69,15 +126,43 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
formData.value = await getDevice(data.id);
|
||||
await formApi.setValues(formData.value);
|
||||
// 如果存在高级字段数据,自动展开 Collapse
|
||||
if (
|
||||
formData.value?.nickname ||
|
||||
formData.value?.picUrl ||
|
||||
formData.value?.groupIds?.length ||
|
||||
formData.value?.serialNumber ||
|
||||
formData.value?.locationType !== undefined
|
||||
) {
|
||||
activeKey.value = ['advanced'];
|
||||
// 等待 Collapse 展开后表单挂载
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
if (advancedFormApi.isMounted) {
|
||||
await advancedFormApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 初始化产品列表 */
|
||||
onMounted(async () => {
|
||||
products.value = await getSimpleProductList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<Collapse v-model:active-key="activeKey" class="mt-4">
|
||||
<Collapse.Panel key="advanced" header="更多设置">
|
||||
<AdvancedForm />
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
@@ -41,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await updateDeviceGroup({
|
||||
ids: deviceIds.value,
|
||||
groupIds: data.groupIds as number[],
|
||||
});
|
||||
} as IotDeviceApi.DeviceUpdateGroupReqVO);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
@@ -38,7 +39,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
const result = await importDevice(data.file, data.updateSupport);
|
||||
// 处理导入结果提示
|
||||
const importData = result.data || result;
|
||||
const importData = result as IotDeviceApi.DeviceImportRespVO;
|
||||
if (importData) {
|
||||
let text = `上传成功数量:${importData.createDeviceNames?.length || 0};`;
|
||||
if (importData.createDeviceNames?.length) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { DeviceRespVO, IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { OtaTask } from '#/api/iot/ota/task';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
@@ -57,7 +57,7 @@ const formRules = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const devices = ref<IotDeviceApi.Device[]>([]);
|
||||
const devices = ref<IotDeviceApi.DeviceRespVO[]>([]);
|
||||
|
||||
/** 设备选项 */
|
||||
const deviceOptions = computed(() => {
|
||||
|
||||
@@ -236,6 +236,15 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '产品状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.IOT_PRODUCT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
|
||||
@@ -115,6 +115,11 @@ onMounted(() => {
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<div class="product-title">{{ item.name }}</div>
|
||||
</div>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_STATUS"
|
||||
:value="item.status"
|
||||
class="status-tag"
|
||||
/>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="mb-3 flex items-start">
|
||||
@@ -264,6 +269,11 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 状态标签
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 信息列表
|
||||
.info-list {
|
||||
.info-item {
|
||||
|
||||
Reference in New Issue
Block a user