review:【antd/ele】【iot】代码迁移的 review

This commit is contained in:
YunaiV
2025-12-07 16:36:55 +08:00
parent 250109507f
commit 2a4c774aca
20 changed files with 96 additions and 92 deletions

View File

@@ -63,17 +63,17 @@ const [DeviceImportFormModal, deviceImportFormModalApi] = useVbenModal({
destroyOnClose: true, destroyOnClose: true,
}); });
// 搜索参数 const queryParams = ref({
const searchParams = ref({
deviceName: '', deviceName: '',
nickname: '', nickname: '',
productId: undefined as number | undefined, productId: undefined as number | undefined,
deviceType: undefined as number | undefined, deviceType: undefined as number | undefined,
status: undefined as number | undefined, status: undefined as number | undefined,
groupId: undefined as number | undefined, groupId: undefined as number | undefined,
}); }); // 搜索参数
// 获取字典选项 // 获取字典选项
// TODO @haohao直接使用 getDictOptions 哈,不用包装方法;
const getIntDictOptions = (dictType: string) => { const getIntDictOptions = (dictType: string) => {
return getDictOptions(dictType, 'number'); return getDictOptions(dictType, 'number');
}; };
@@ -81,21 +81,22 @@ const getIntDictOptions = (dictType: string) => {
/** 搜索 */ /** 搜索 */
function handleSearch() { function handleSearch() {
if (viewMode.value === 'list') { if (viewMode.value === 'list') {
gridApi.formApi.setValues(searchParams.value); gridApi.formApi.setValues(queryParams.value);
gridApi.query(); gridApi.query();
} else { } else {
cardViewRef.value?.search(searchParams.value); // todo @haohao改成 query 方法,更统一;
cardViewRef.value?.search(queryParams.value);
} }
} }
/** 重置 */ /** 重置 */
function handleReset() { function handleReset() {
searchParams.value.deviceName = ''; queryParams.value.deviceName = '';
searchParams.value.nickname = ''; queryParams.value.nickname = '';
searchParams.value.productId = undefined; queryParams.value.productId = undefined;
searchParams.value.deviceType = undefined; queryParams.value.deviceType = undefined;
searchParams.value.status = undefined; queryParams.value.status = undefined;
searchParams.value.groupId = undefined; queryParams.value.groupId = undefined;
handleSearch(); handleSearch();
} }
@@ -110,7 +111,7 @@ function handleRefresh() {
/** 导出表格 */ /** 导出表格 */
async function handleExport() { async function handleExport() {
const data = await exportDeviceExcel(searchParams.value); const data = await exportDeviceExcel(queryParams.value);
downloadFileFromBlobPart({ fileName: '物联网设备.xls', source: data }); downloadFileFromBlobPart({ fileName: '物联网设备.xls', source: data });
} }
@@ -212,7 +213,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
return await getDevicePage({ return await getDevicePage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
...searchParams.value, ...queryParams.value,
}); });
}, },
}, },
@@ -238,7 +239,7 @@ onMounted(async () => {
// 处理 productId 参数 // 处理 productId 参数
const { productId } = route.query; const { productId } = route.query;
if (productId) { if (productId) {
searchParams.value.productId = Number(productId); queryParams.value.productId = Number(productId);
// 自动触发搜索 // 自动触发搜索
handleSearch(); handleSearch();
} }
@@ -256,7 +257,7 @@ onMounted(async () => {
<!-- 搜索表单 --> <!-- 搜索表单 -->
<div class="mb-3 flex flex-wrap items-center gap-3"> <div class="mb-3 flex flex-wrap items-center gap-3">
<Select <Select
v-model:value="searchParams.productId" v-model:value="queryParams.productId"
placeholder="请选择产品" placeholder="请选择产品"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@@ -270,21 +271,21 @@ onMounted(async () => {
</Select.Option> </Select.Option>
</Select> </Select>
<Input <Input
v-model:value="searchParams.deviceName" v-model:value="queryParams.deviceName"
placeholder="请输入 DeviceName" placeholder="请输入 DeviceName"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@press-enter="handleSearch" @press-enter="handleSearch"
/> />
<Input <Input
v-model:value="searchParams.nickname" v-model:value="queryParams.nickname"
placeholder="请输入备注名称" placeholder="请输入备注名称"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@press-enter="handleSearch" @press-enter="handleSearch"
/> />
<Select <Select
v-model:value="searchParams.deviceType" v-model:value="queryParams.deviceType"
placeholder="请选择设备类型" placeholder="请选择设备类型"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@@ -298,7 +299,7 @@ onMounted(async () => {
</Select.Option> </Select.Option>
</Select> </Select>
<Select <Select
v-model:value="searchParams.status" v-model:value="queryParams.status"
placeholder="请选择设备状态" placeholder="请选择设备状态"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@@ -312,7 +313,7 @@ onMounted(async () => {
</Select.Option> </Select.Option>
</Select> </Select>
<Select <Select
v-model:value="searchParams.groupId" v-model:value="queryParams.groupId"
placeholder="请选择设备分组" placeholder="请选择设备分组"
allow-clear allow-clear
style="width: 200px" style="width: 200px"
@@ -360,6 +361,7 @@ onMounted(async () => {
auth: ['iot:device:import'], auth: ['iot:device:import'],
onClick: handleImport, onClick: handleImport,
}, },
// TODO @haohao应该是选中后才可用
{ {
label: '添加到分组', label: '添加到分组',
type: 'primary', type: 'primary',
@@ -368,6 +370,7 @@ onMounted(async () => {
ifShow: () => viewMode === 'list', ifShow: () => viewMode === 'list',
onClick: handleAddToGroup, onClick: handleAddToGroup,
}, },
// TODO @haohao应该是选中后才可用然后然后 danger 颜色;
{ {
label: '批量删除', label: '批量删除',
type: 'primary', type: 'primary',
@@ -398,7 +401,7 @@ onMounted(async () => {
</div> </div>
</Card> </Card>
<Grid v-show="viewMode === 'list'"> <Grid table-title="设备列表" v-show="viewMode === 'list'">
<template #toolbar-tools> <template #toolbar-tools>
<div></div> <div></div>
</template> </template>
@@ -469,7 +472,7 @@ onMounted(async () => {
ref="cardViewRef" ref="cardViewRef"
:products="products" :products="products"
:device-groups="deviceGroups" :device-groups="deviceGroups"
:search-params="searchParams" :search-params="queryParams"
@create="handleCreate" @create="handleCreate"
@edit="handleEdit" @edit="handleEdit"
@delete="handleDelete" @delete="handleDelete"

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
// TODO @haohaoproduct 的 card-view 的意见,这里看看要不要也改改下。
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
import { getDictLabel, getDictObj } from '@vben/hooks'; import { getDictLabel, getDictObj } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { isValidColor, TinyColor } from '@vben/utils'; import { isValidColor, TinyColor } from '@vben/utils';
@@ -18,8 +19,6 @@ import {
Tooltip, Tooltip,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { DeviceStateEnum } from '@vben/constants';
import { getDevicePage } from '#/api/iot/device/device'; import { getDevicePage } from '#/api/iot/device/device';
interface Props { interface Props {
@@ -180,6 +179,7 @@ function getDeviceTypeColor(deviceType: number) {
} }
/** 获取设备状态信息 */ /** 获取设备状态信息 */
// TODO @haohao这里可以简化下么体感看着有点复杂哈
function getStatusInfo(state: null | number | string | undefined) { function getStatusInfo(state: null | number | string | undefined) {
const parsedState = Number(state); const parsedState = Number(state);
const hasNumericState = Number.isFinite(parsedState); const hasNumericState = Number.isFinite(parsedState);
@@ -274,7 +274,7 @@ onMounted(() => {
<div class="info-item"> <div class="info-item">
<span class="info-label">所属产品</span> <span class="info-label">所属产品</span>
<a <a
class="info-value text-primary cursor-pointer" class="info-value cursor-pointer text-primary"
@click=" @click="
(e) => { (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -301,10 +301,7 @@ onMounted(() => {
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">Deviceid</span> <span class="info-label">Deviceid</span>
<Tooltip <Tooltip :title="item.Deviceid || item.id" placement="top">
:title="item.Deviceid || item.id"
placement="top"
>
<span class="info-value device-id cursor-pointer"> <span class="info-value device-id cursor-pointer">
{{ item.Deviceid || item.id }} {{ item.Deviceid || item.id }}
</span> </span>
@@ -359,7 +356,7 @@ onMounted(() => {
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div v-if="list.length > 0" class="flex justify-end"> <div v-if="list.length > 0" class="mt-3 flex justify-end">
<Pagination <Pagination
v-model:current="queryParams.pageNo" v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize" v-model:page-size="queryParams.pageSize"

View File

@@ -7,11 +7,10 @@ import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { DeviceTypeEnum } from '@vben/constants';
import { message, Tabs } from 'ant-design-vue'; import { message, Tabs } from 'ant-design-vue';
import { DeviceTypeEnum } from '@vben/constants';
import { getDevice } from '#/api/iot/device/device'; import { getDevice } from '#/api/iot/device/device';
import { getProduct } from '#/api/iot/product/product'; import { getProduct } from '#/api/iot/product/product';
import { getThingModelListByProductId } from '#/api/iot/thingmodel'; import { getThingModelListByProductId } from '#/api/iot/thingmodel';
@@ -36,6 +35,8 @@ const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device);
const activeTab = ref('info'); const activeTab = ref('info');
const thingModelList = ref<ThingModelData[]>([]); const thingModelList = ref<ThingModelData[]>([]);
// TODO @haohao类似 device/detail/index.vue 挪出去哈。
/** 获取设备详情 */ /** 获取设备详情 */
async function getDeviceData(deviceId: number) { async function getDeviceData(deviceId: number) {
loading.value = true; loading.value = true;

View File

@@ -56,14 +56,14 @@ const hasConfigData = computed(() => {
}); });
/** 启用编辑模式的函数 */ /** 启用编辑模式的函数 */
function enableEdit() { function handleEdit() {
isEditing.value = true; isEditing.value = true;
// 重新同步编辑器内容 // 重新同步编辑器内容
configString.value = JSON.stringify(config.value, null, 2); configString.value = JSON.stringify(config.value, null, 2);
} }
/** 取消编辑的函数 */ /** 取消编辑的函数 */
function cancelEdit() { function handleCancelEdit() {
try { try {
config.value = props.device.config ? JSON.parse(props.device.config) : {}; config.value = props.device.config ? JSON.parse(props.device.config) : {};
configString.value = JSON.stringify(config.value, null, 2); configString.value = JSON.stringify(config.value, null, 2);
@@ -84,29 +84,23 @@ async function saveConfig() {
message.error({ content: 'JSON格式错误请修正后再提交' }); message.error({ content: 'JSON格式错误请修正后再提交' });
return; return;
} }
// TODO @haohao这里要不要做个类似下面的 pushLoading 避免重复提交;
await updateDeviceConfig(); await updateDeviceConfig();
isEditing.value = false; isEditing.value = false;
} }
/** 配置推送处理函数 */ /** 配置推送处理函数 */
async function handleConfigPush() { async function handleConfigPush() {
pushLoading.value = true;
try { try {
pushLoading.value = true;
// 调用配置推送接口 // 调用配置推送接口
await sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method, method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
params: config.value, params: config.value,
}); });
// 提示成功
message.success({ content: '配置推送成功!' }); message.success({ content: '配置推送成功!' });
} catch (error) {
if (error !== 'cancel') {
message.error({ content: '配置推送失败!' });
console.error('配置推送错误:', error);
}
} finally { } finally {
pushLoading.value = false; pushLoading.value = false;
} }
@@ -124,8 +118,6 @@ async function updateDeviceConfig() {
message.success({ content: '更新成功!' }); message.success({ content: '更新成功!' });
// 触发 success 事件 // 触发 success 事件
emit('success'); emit('success');
} catch (error) {
console.error(error);
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -143,8 +135,9 @@ async function updateDeviceConfig() {
class="my-4" class="my-4"
description="如需编辑文件,请点击下方编辑按钮" description="如需编辑文件,请点击下方编辑按钮"
/> />
<!-- TODO @haohao应该按钮是在下方可以参考 element-plus 的版本 -->
<div class="mt-5 text-center"> <div class="mt-5 text-center">
<Button v-if="isEditing" @click="cancelEdit">取消</Button> <Button v-if="isEditing" @click="handleCancelEdit">取消</Button>
<Button <Button
v-if="isEditing" v-if="isEditing"
type="primary" type="primary"
@@ -153,7 +146,7 @@ async function updateDeviceConfig() {
> >
保存 保存
</Button> </Button>
<Button v-else @click="enableEdit">编辑</Button> <Button v-else @click="handleEdit">编辑</Button>
<Button <Button
v-if="!isEditing" v-if="!isEditing"
type="primary" type="primary"

View File

@@ -27,6 +27,8 @@ import { getDeviceMessagePage } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag'; import { DictTag } from '#/components/dict-tag';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'; import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
// TODO @haohao看看能不能调整成 Grid 风格~方便 element-plus 的迁移
const props = defineProps<{ const props = defineProps<{
deviceId: number; deviceId: number;
}>(); }>();

View File

@@ -341,6 +341,7 @@ async function handleServiceInvoke(row: ThingModelData) {
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 上方指令调试区域 --> <!-- 上方指令调试区域 -->
<!-- TODO @haohao要不要改成左右 -->
<Card class="simulator-tabs mb-4"> <Card class="simulator-tabs mb-4">
<template #title> <template #title>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -3,6 +3,8 @@ import { onMounted, ref } from 'vue';
import { Card, Empty } from 'ant-design-vue'; import { Card, Empty } from 'ant-design-vue';
// TODO @haohao这里要实现一把么
interface Props { interface Props {
deviceId: number; deviceId: number;
} }

View File

@@ -1,5 +1,6 @@
<!-- 设备事件管理 --> <!-- 设备事件管理 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @haohao看看能不能用 Grid 实现下,方便 element-plus 迁移
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';

View File

@@ -24,9 +24,8 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
import { getHistoryDevicePropertyList } from '#/api/iot/device/device'; import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'; import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
/** IoT 设备属性历史数据详情 */ /** IoT 设备属性历史数据详情 */
@@ -43,16 +42,10 @@ const total = ref(0); // 总数据量
const thingModelDataType = ref<string>(''); // 物模型数据类型 const thingModelDataType = ref<string>(''); // 物模型数据类型
const propertyIdentifier = ref<string>(''); // 属性标识符 const propertyIdentifier = ref<string>(''); // 属性标识符
/** 时间范围(仅日期,不包含时分秒) */
const dateRange = ref<[string, string]>([ const dateRange = ref<[string, string]>([
dayjs().subtract(6, 'day').format('YYYY-MM-DD'), dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD'),
]); ]); // 时间范围(仅日期,不包含时分秒)
/** 将日期范围转换为带时分秒的格式 */
function formatDateRangeWithTime(dates: [string, string]): [string, string] {
return [`${dates[0]} 00:00:00`, `${dates[1]} 23:59:59`];
}
const queryParams = reactive({ const queryParams = reactive({
deviceId: -1, deviceId: -1,
@@ -64,23 +57,21 @@ const queryParams = reactive({
const chartRef = ref<EchartsUIType>(); const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef); const { renderEcharts } = useEcharts(chartRef);
// 判断是否为复杂数据类型struct 或 array
const isComplexDataType = computed(() => { const isComplexDataType = computed(() => {
if (!thingModelDataType.value) return false; if (!thingModelDataType.value) return false;
return [ return [
IoTDataSpecsDataTypeEnum.ARRAY, IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.STRUCT,
].includes(thingModelDataType.value as any); ].includes(thingModelDataType.value as any);
}); }); // 判断是否为复杂数据类型struct 或 array
// 统计数据
const maxValue = computed(() => { const maxValue = computed(() => {
if (isComplexDataType.value || list.value.length === 0) return '-'; if (isComplexDataType.value || list.value.length === 0) return '-';
const values = list.value const values = list.value
.map((item) => Number(item.value)) .map((item) => Number(item.value))
.filter((v) => !Number.isNaN(v)); .filter((v) => !Number.isNaN(v));
return values.length > 0 ? Math.max(...values).toFixed(2) : '-'; return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
}); }); // 统计数据
const minValue = computed(() => { const minValue = computed(() => {
if (isComplexDataType.value || list.value.length === 0) return '-'; if (isComplexDataType.value || list.value.length === 0) return '-';
@@ -100,6 +91,11 @@ const avgValue = computed(() => {
return (sum / values.length).toFixed(2); return (sum / values.length).toFixed(2);
}); });
/** 将日期范围转换为带时分秒的格式 */
function formatDateRangeWithTime(dates: [string, string]): [string, string] {
return [`${dates[0]} 00:00:00`, `${dates[1]} 23:59:59`];
}
// 表格列配置 // 表格列配置
const tableColumns = computed(() => [ const tableColumns = computed(() => [
{ {
@@ -409,7 +405,9 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
<Space :size="12" class="w-full" wrap> <Space :size="12" class="w-full" wrap>
<!-- 时间选择 --> <!-- 时间选择 -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="whitespace-nowrap text-sm text-gray-500">时间范围</span> <span class="whitespace-nowrap text-sm text-gray-500">
时间范围
</span>
<ShortcutDateRangePicker @change="handleDateRangeChange" /> <ShortcutDateRangePicker @change="handleDateRangeChange" />
</div> </div>
@@ -531,7 +529,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
.chart-container, .chart-container,
.table-container { .table-container {
padding: 16px; padding: 16px;
background-color: hsl(var(--card)); background-color: hsl(var(--card)); // TODO @haohao看看这个能不能 fix 下~ idea 爆红了;
border: 1px solid hsl(var(--border) / 60%); border: 1px solid hsl(var(--border) / 60%);
border-radius: 8px; border-radius: 8px;
} }

View File

@@ -1,5 +1,6 @@
<!-- 设备属性管理 --> <!-- 设备属性管理 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @haohao看看能不能用 Grid 实现下,方便 element-plus 迁移
import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';

View File

@@ -1,5 +1,6 @@
<!-- 设备服务调用 --> <!-- 设备服务调用 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @haohao看看能不能调整成 Grid 风格~方便 element-plus 的迁移
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';

View File

@@ -8,11 +8,7 @@ import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
createDevice,
getDevice,
updateDevice,
} from '#/api/iot/device/device';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
@@ -40,7 +36,6 @@ const [Form, formApi] = useVbenForm({
}); });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
/** 提交表单 */
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
@@ -51,6 +46,7 @@ const [Modal, modalApi] = useVbenModal({
const data = (await formApi.getValues()) as IotDeviceApi.Device; const data = (await formApi.getValues()) as IotDeviceApi.Device;
try { try {
await (formData.value?.id ? updateDevice(data) : createDevice(data)); await (formData.value?.id ? updateDevice(data) : createDevice(data));
// 关闭并提示
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
@@ -58,7 +54,6 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock(); modalApi.unlock();
} }
}, },
/** 弹窗打开/关闭 */
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = undefined;
@@ -68,6 +63,7 @@ const [Modal, modalApi] = useVbenModal({
const data = modalApi.getData<IotDeviceApi.Device>(); const data = modalApi.getData<IotDeviceApi.Device>();
if (!data || !data.id) { if (!data || !data.id) {
// 新增模式:设置默认值(如果需要) // 新增模式:设置默认值(如果需要)
// TODO @haohao是不是 return 就好啦;不用这里 undefined 啦;
formData.value = undefined; formData.value = undefined;
return; return;
} }

View File

@@ -29,7 +29,6 @@ const [Form, formApi] = useVbenForm({
}); });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
/** 提交表单 */
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
@@ -43,6 +42,7 @@ const [Modal, modalApi] = useVbenModal({
ids: deviceIds.value, ids: deviceIds.value,
groupIds: data.groupIds as number[], groupIds: data.groupIds as number[],
}); });
// 关闭并提示
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
@@ -50,7 +50,6 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock(); modalApi.unlock();
} }
}, },
/** 弹窗打开/关闭 */
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
deviceIds.value = []; deviceIds.value = [];

View File

@@ -69,6 +69,7 @@ const [Modal, modalApi] = useVbenModal({
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>(); const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
if (!data || !data.id) { if (!data || !data.id) {
// 新增模式:设置默认值 // 新增模式:设置默认值
// TODO @AI可以参考部门进一步简化代码通过 defaultValue 在 schema 里设置默认值
formData.value = undefined; formData.value = undefined;
await formApi.setValues({ await formApi.setValues({
sort: 0, sort: 0,

View File

@@ -21,6 +21,7 @@ async function loadCategoryData() {
} }
// 初始化加载分类数据 // 初始化加载分类数据
// TODO @haohao可以参考 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/system/tenant/data.ts 简洁一点。
loadCategoryData(); loadCategoryData();
/** 新增/修改产品的表单 */ /** 新增/修改产品的表单 */

View File

@@ -7,13 +7,12 @@ import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { ProductStatusEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, Input, message, Space } from 'ant-design-vue'; import { Button, Card, Input, message, Space } from 'ant-design-vue';
import { ProductStatusEnum } from '@vben/constants';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleProductCategoryList } from '#/api/iot/product/category'; import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { import {
@@ -33,7 +32,7 @@ const router = useRouter();
const categoryList = ref<IotProductCategoryApi.ProductCategory[]>([]); const categoryList = ref<IotProductCategoryApi.ProductCategory[]>([]);
const viewMode = ref<'card' | 'list'>('card'); const viewMode = ref<'card' | 'list'>('card');
const cardViewRef = ref(); const cardViewRef = ref();
const searchParams = ref({ const queryParams = ref({
name: '', name: '',
productKey: '', productKey: '',
}); // 搜索参数 }); // 搜索参数
@@ -51,17 +50,18 @@ async function loadCategories() {
/** 搜索产品 */ /** 搜索产品 */
function handleSearch() { function handleSearch() {
if (viewMode.value === 'list') { if (viewMode.value === 'list') {
gridApi.formApi.setValues(searchParams.value); gridApi.formApi.setValues(queryParams.value);
gridApi.query(); gridApi.query();
} else { } else {
cardViewRef.value?.search(searchParams.value); // TODO @haohao要不 search 也改成 query 方法,更统一一点哈。
cardViewRef.value?.search(queryParams.value);
} }
} }
/** 重置搜索 */ /** 重置搜索 */
function handleReset() { function handleReset() {
searchParams.value.name = ''; queryParams.value.name = '';
searchParams.value.productKey = ''; queryParams.value.productKey = '';
handleSearch(); handleSearch();
} }
@@ -76,7 +76,7 @@ function handleRefresh() {
/** 导出表格 */ /** 导出表格 */
async function handleExport() { async function handleExport() {
const data = await exportProduct(searchParams.value); const data = await exportProduct(queryParams.value);
downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data }); downloadFileFromBlobPart({ fileName: '产品列表.xls', source: data });
} }
@@ -133,7 +133,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
return await getProductPage({ return await getProductPage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
...searchParams.value, ...queryParams.value,
}); });
}, },
}, },
@@ -164,7 +164,7 @@ onMounted(() => {
<!-- 搜索表单 --> <!-- 搜索表单 -->
<div class="mb-3 flex items-center gap-3"> <div class="mb-3 flex items-center gap-3">
<Input <Input
v-model:value="searchParams.name" v-model:value="queryParams.name"
placeholder="请输入产品名称" placeholder="请输入产品名称"
allow-clear allow-clear
class="w-[220px]" class="w-[220px]"
@@ -175,7 +175,7 @@ onMounted(() => {
</template> </template>
</Input> </Input>
<Input <Input
v-model:value="searchParams.productKey" v-model:value="queryParams.productKey"
placeholder="请输入产品标识" placeholder="请输入产品标识"
allow-clear allow-clear
class="w-[220px]" class="w-[220px]"
@@ -230,7 +230,7 @@ onMounted(() => {
</div> </div>
</Card> </Card>
<Grid v-show="viewMode === 'list'"> <Grid table-title="产品列表" v-show="viewMode === 'list'">
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[
@@ -271,14 +271,13 @@ onMounted(() => {
v-show="viewMode === 'card'" v-show="viewMode === 'card'"
ref="cardViewRef" ref="cardViewRef"
:category-list="categoryList" :category-list="categoryList"
:search-params="searchParams" :search-params="queryParams"
@create="handleCreate" @create="handleCreate"
@edit="handleEdit" @edit="handleEdit"
@delete="handleDelete" @delete="handleDelete"
@detail="openProductDetail" @detail="openProductDetail"
@thing-model="openThingModel" @thing-model="openThingModel"
/> />
</Page> </Page>
</template> </template>
<style scoped> <style scoped>

View File

@@ -137,6 +137,7 @@ onMounted(() => {
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">产品类型</span> <span class="info-label">产品类型</span>
<!-- TODO @AI这个要不完全用字典的 dict-tag -->
<Tag <Tag
:color="getDeviceTypeColor(item.deviceType)" :color="getDeviceTypeColor(item.deviceType)"
class="info-tag m-0" class="info-tag m-0"
@@ -231,7 +232,7 @@ onMounted(() => {
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div v-if="list.length > 0" class="flex justify-end"> <div v-if="list.length > 0" class="mt-3 flex justify-end">
<Pagination <Pagination
v-model:current="queryParams.pageNo" v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize" v-model:page-size="queryParams.pageSize"
@@ -266,6 +267,7 @@ onMounted(() => {
width: 36px; width: 36px;
height: 36px; height: 36px;
color: white; color: white;
// TODO @haohao这里的紫色和下面的紫色按钮看看能不能换下。嘿嘿感觉 AI 比较喜欢用紫色,但是放现有的后台,有点突兀
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px; border-radius: 8px;
} }

View File

@@ -28,6 +28,7 @@ const activeTab = ref('info');
provide('product', product); // 提供产品信息给子组件 provide('product', product); // 提供产品信息给子组件
/** 获取产品详情 */ /** 获取产品详情 */
// TODO @haohao因为 detail 是独立界面,所以不放在 modules 里,应该放在 web-antd/src/views/iot/product/product/detail/index.vue 里,更合理一些哈。
async function getProductData(productId: number) { async function getProductData(productId: number) {
loading.value = true; loading.value = true;
try { try {

View File

@@ -4,11 +4,10 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { ProductStatusEnum } from '@vben/constants';
import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue'; import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
import { ProductStatusEnum } from '@vben/constants';
import { updateProductStatus } from '#/api/iot/product/product'; import { updateProductStatus } from '#/api/iot/product/product';
import Form from '../../form.vue'; import Form from '../../form.vue';

View File

@@ -40,6 +40,7 @@ const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-2', wrapperClass: 'grid-cols-2',
}); });
// TODO @haohao这个要不还是一行一个这样样式好看点哈。
const [AdvancedForm, advancedFormApi] = useVbenForm({ const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: { commonConfig: {
componentProps: { class: 'w-full' }, componentProps: { class: 'w-full' },
@@ -51,10 +52,10 @@ const [AdvancedForm, advancedFormApi] = useVbenForm({
}); });
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */ /** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
// TODO haohao@haohao要不要把 generateProductKey 拿到这个 vue 里,作为参数传递到 useBasicFormSchema 里?
formApi.setState({ schema: useBasicFormSchema(formApi) }); formApi.setState({ schema: useBasicFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
/** 提交表单 */
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
@@ -63,6 +64,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(); modalApi.lock();
// 合并两个表单的值 // 合并两个表单的值
const basicValues = await formApi.getValues(); const basicValues = await formApi.getValues();
// TODO @haohao有 linter 修复下“formData.value?.id”另外这里直接两个表单合并是不是就可以了呀因为 2 个 schema 本身不同,字段就不同,不会冲突。
const advancedValues = activeKey.value.includes('advanced') const advancedValues = activeKey.value.includes('advanced')
? await advancedFormApi.getValues() ? await advancedFormApi.getValues()
: formData.value?.id : formData.value?.id
@@ -85,7 +87,6 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock(); modalApi.unlock();
} }
}, },
/** 弹窗打开/关闭 */
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = undefined;
@@ -96,9 +97,11 @@ const [Modal, modalApi] = useVbenModal({
const data = modalApi.getData<IotProductApi.Product>(); const data = modalApi.getData<IotProductApi.Product>();
if (!data || !data.id) { if (!data || !data.id) {
// 新增:设置默认值 // 新增:设置默认值
// TODO @AI
await formApi.setValues({ await formApi.setValues({
// TODO @haohao要不要把 generateProductKey 拿到这个 vue 里,作为参数传递到 useBasicFormSchema 里?
productKey: generateProductKey(), productKey: generateProductKey(),
status: 0, status: 0, // TODO @haohao通过 defaultValue 即可;
}); });
return; return;
} }
@@ -108,12 +111,15 @@ const [Modal, modalApi] = useVbenModal({
formData.value = await getProduct(data.id); formData.value = await getProduct(data.id);
await formApi.setValues(formData.value); await formApi.setValues(formData.value);
// 设置高级表单(不等待) // 设置高级表单(不等待)
// TODO @haohao直接把 formData 传过去?没关系的哈。因为会 filter 掉不存在的值,可以试试哈。
// TODO @haohao这里是不是要 await 下呀?有黄色的告警;
advancedFormApi.setValues({ advancedFormApi.setValues({
icon: formData.value.icon, icon: formData.value.icon,
picUrl: formData.value.picUrl, picUrl: formData.value.picUrl,
description: formData.value.description, description: formData.value.description,
}); });
// 有高级字段时自动展开 // 有高级字段时自动展开
// TODO @haohao默认不用展开哈。
if ( if (
formData.value.icon || formData.value.icon ||
formData.value.picUrl || formData.value.picUrl ||