!290 refactor:【antd】【iot】产品管理问题修复
Merge pull request !290 from haohaoMT/dev
This commit is contained in:
@@ -48,6 +48,12 @@ export enum CodecTypeEnum {
|
|||||||
ALINK = 'Alink', // 阿里云 Alink 协议
|
ALINK = 'Alink', // 阿里云 Alink 协议
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** IOT 产品状态枚举类 */
|
||||||
|
export enum ProductStatusEnum {
|
||||||
|
UNPUBLISHED = 0, // 开发中
|
||||||
|
PUBLISHED = 1, // 已发布
|
||||||
|
}
|
||||||
|
|
||||||
/** 查询产品分页 */
|
/** 查询产品分页 */
|
||||||
export function getProductPage(params: PageParam) {
|
export function getProductPage(params: PageParam) {
|
||||||
return requestClient.get<PageResult<IotProductApi.Product>>(
|
return requestClient.get<PageResult<IotProductApi.Product>>(
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import { deleteDeviceGroup, getDeviceGroupPage } from '#/api/iot/device/group';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import DeviceGroupForm from './modules/device-group-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceGroup' });
|
defineOptions({ name: 'IoTDeviceGroup' });
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: DeviceGroupForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
@@ -17,17 +17,9 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceGroupForm' });
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
// TODO @haohao:web-antd/src/views/iot/product/category/modules/product-category-form.vue 类似问题
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const formData = ref<IotDeviceGroupApi.DeviceGroup>();
|
const formData = ref<IotDeviceGroupApi.DeviceGroup>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
const modalTitle = computed(() => {
|
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['设备分组'])
|
? $t('ui.actionTitle.edit', ['设备分组'])
|
||||||
: $t('ui.actionTitle.create', ['设备分组']);
|
: $t('ui.actionTitle.create', ['设备分组']);
|
||||||
@@ -40,11 +32,9 @@ const [Form, formApi] = useVbenForm({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: useFormSchema(),
|
schema: useFormSchema(),
|
||||||
showCollapseButton: false,
|
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:参考别的 form;1)文件的命名可以简化;2)代码可以在简化下;
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
@@ -52,17 +42,13 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as IotDeviceGroupApi.DeviceGroup;
|
||||||
try {
|
try {
|
||||||
const values = await formApi.getValues();
|
|
||||||
|
|
||||||
await (formData.value?.id
|
await (formData.value?.id
|
||||||
? updateDeviceGroup({
|
? updateDeviceGroup(data)
|
||||||
...values,
|
: createDeviceGroup(data));
|
||||||
id: formData.value.id,
|
// 关闭并提示
|
||||||
} as IotDeviceGroupApi.DeviceGroup)
|
|
||||||
: createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup));
|
|
||||||
|
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
@@ -70,28 +56,20 @@ 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;
|
||||||
await formApi.resetForm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 加载数据
|
||||||
// 重置表单
|
|
||||||
await formApi.resetForm();
|
|
||||||
|
|
||||||
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
|
const data = modalApi.getData<IotDeviceGroupApi.DeviceGroup>();
|
||||||
// 如果没有数据或没有 id,表示是新增
|
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
formData.value = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑模式:加载数据
|
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getDeviceGroup(data.id);
|
formData.value = await getDeviceGroup(data.id);
|
||||||
|
// 设置到 values
|
||||||
await formApi.setValues(formData.value);
|
await formApi.setValues(formData.value);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
@@ -101,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal :title="modalTitle" class="w-2/5">
|
<Modal :title="getTitle">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import ProductCategoryForm from './modules/product-category-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductCategory' });
|
defineOptions({ name: 'IoTProductCategory' });
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: ProductCategoryForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
// TODO @haohao:应该是 form.vue,不用前缀;
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<IotProductCategoryApi.ProductCategory>();
|
const formData = ref<IotProductCategoryApi.ProductCategory>();
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
@@ -40,7 +38,6 @@ const [Form, formApi] = useVbenForm({
|
|||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:参考 apps/web-antd/src/views/system/dept/modules/form.vue 简化 useVbenModal 里的代码;
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
@@ -66,25 +63,19 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = undefined;
|
formData.value = undefined;
|
||||||
formApi.resetForm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 加载数据
|
||||||
// 重置表单
|
|
||||||
await formApi.resetForm();
|
|
||||||
|
|
||||||
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
const data = modalApi.getData<IotProductCategoryApi.ProductCategory>();
|
||||||
// 如果没有数据或没有 id,表示是新增
|
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
formData.value = undefined;
|
|
||||||
// 新增模式:设置默认值
|
// 新增模式:设置默认值
|
||||||
|
formData.value = undefined;
|
||||||
await formApi.setValues({
|
await formApi.setValues({
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑模式:加载数据
|
// 编辑模式:加载数据
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||||
|
|
||||||
import { h, ref } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
@@ -10,7 +11,17 @@ import { Button } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { z } from '#/adapter/form';
|
import { z } from '#/adapter/form';
|
||||||
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
|
||||||
|
/** 产品分类列表缓存 */
|
||||||
|
let categoryList: IotProductCategoryApi.ProductCategory[] = [];
|
||||||
|
|
||||||
|
/** 加载产品分类数据 */
|
||||||
|
async function loadCategoryData() {
|
||||||
|
categoryList = await getSimpleProductCategoryList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载分类数据
|
||||||
|
loadCategoryData();
|
||||||
|
|
||||||
/** 新增/修改产品的表单 */
|
/** 新增/修改产品的表单 */
|
||||||
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||||
@@ -134,7 +145,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
|||||||
label: '产品状态',
|
label: '产品状态',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||||
buttonStyle: 'solid',
|
buttonStyle: 'solid',
|
||||||
optionType: 'button',
|
optionType: 'button',
|
||||||
},
|
},
|
||||||
@@ -283,7 +294,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
|||||||
label: '产品状态',
|
label: '产品状态',
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
options: getDictOptions(DICT_TYPE.IOT_PRODUCT_STATUS, 'number'),
|
||||||
buttonStyle: 'solid',
|
buttonStyle: 'solid',
|
||||||
optionType: 'button',
|
optionType: 'button',
|
||||||
},
|
},
|
||||||
@@ -308,11 +319,10 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
|||||||
{
|
{
|
||||||
fieldName: 'icon',
|
fieldName: 'icon',
|
||||||
label: '产品图标',
|
label: '产品图标',
|
||||||
component: 'IconPicker', // 用这个组件 产品卡片列表 可以根据这个显示 否则就显示默认的
|
component: 'IconPicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择产品图标',
|
placeholder: '请选择产品图标',
|
||||||
prefix: 'carbon',
|
prefix: 'carbon',
|
||||||
autoFetchApi: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -333,31 +343,6 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
|
||||||
// TODO @haohao:貌似用不上?
|
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
fieldName: 'name',
|
|
||||||
label: '产品名称',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入产品名称',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'productKey',
|
|
||||||
label: 'ProductKey',
|
|
||||||
component: 'Input',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入产品标识',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 列表的字段 */
|
/** 列表的字段 */
|
||||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
return [
|
return [
|
||||||
@@ -375,7 +360,8 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'categoryId',
|
field: 'categoryId',
|
||||||
title: '品类',
|
title: '品类',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
slots: { default: 'category' },
|
formatter: ({ cellValue }) =>
|
||||||
|
categoryList.find((c) => c.id === cellValue)?.name || '未分类',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceType',
|
field: 'deviceType',
|
||||||
@@ -390,13 +376,17 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
field: 'icon',
|
field: 'icon',
|
||||||
title: '产品图标',
|
title: '产品图标',
|
||||||
width: 100,
|
width: 100,
|
||||||
slots: { default: 'icon' },
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'picUrl',
|
field: 'picUrl',
|
||||||
title: '产品图片',
|
title: '产品图片',
|
||||||
width: 100,
|
width: 100,
|
||||||
slots: { default: 'picUrl' },
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'createTime',
|
field: 'createTime',
|
||||||
@@ -413,35 +403,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询产品列表 */
|
|
||||||
// TODO @haohao:貌似可以删除?
|
|
||||||
export async function queryProductList({ page }: any, searchParams: any) {
|
|
||||||
return await getProductPage({
|
|
||||||
pageNo: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...searchParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建图片预览状态 */
|
|
||||||
// TODO @haohao:可能不一定用的上;
|
|
||||||
export function useImagePreview() {
|
|
||||||
const previewVisible = ref(false);
|
|
||||||
const previewImage = ref('');
|
|
||||||
|
|
||||||
function handlePreviewImage(url: string) {
|
|
||||||
previewImage.value = url;
|
|
||||||
previewVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
previewVisible,
|
|
||||||
previewImage,
|
|
||||||
handlePreviewImage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:放到对应的 form 里
|
|
||||||
/** 生成 ProductKey(包含大小写字母和数字) */
|
/** 生成 ProductKey(包含大小写字母和数字) */
|
||||||
export function generateProductKey(): string {
|
export function generateProductKey(): string {
|
||||||
const chars =
|
const chars =
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -8,7 +10,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
|||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
import { Button, Card, Image, Input, message, Space } from 'ant-design-vue';
|
import { Button, Card, Input, message, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
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';
|
||||||
@@ -19,14 +21,14 @@ import {
|
|||||||
} from '#/api/iot/product/product';
|
} from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useImagePreview } from './data';
|
import { useGridColumns } from './data';
|
||||||
import ProductCardView from './modules/product-card-view.vue';
|
import ProductCardView from './modules/card-view.vue';
|
||||||
import ProductForm from './modules/product-form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProduct' });
|
defineOptions({ name: 'IoTProduct' });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const categoryList = ref<any[]>([]); // TODO @haohao:category 类型
|
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 searchParams = ref({
|
||||||
@@ -34,10 +36,8 @@ const searchParams = ref({
|
|||||||
productKey: '',
|
productKey: '',
|
||||||
}); // 搜索参数
|
}); // 搜索参数
|
||||||
|
|
||||||
const { previewVisible, previewImage, handlePreviewImage } = useImagePreview();
|
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: ProductForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,13 +46,6 @@ async function loadCategories() {
|
|||||||
categoryList.value = await getSimpleProductCategoryList();
|
categoryList.value = await getSimpleProductCategoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取分类名称 */
|
|
||||||
function getCategoryNameByValue(categoryId: number) {
|
|
||||||
const category = categoryList.value.find((c: any) => c.id === categoryId);
|
|
||||||
return category?.name || '未分类';
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @haohao:要不要改成 handleRefresh,注释改成“刷新表格”,更加统一。
|
|
||||||
/** 搜索产品 */
|
/** 搜索产品 */
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
if (viewMode.value === 'list') {
|
if (viewMode.value === 'list') {
|
||||||
@@ -128,10 +121,6 @@ async function handleDelete(row: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
// TODO @haohao:这个不用,可以删除掉的
|
|
||||||
formOptions: {
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useGridColumns(),
|
columns: useGridColumns(),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -155,7 +144,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
refresh: true,
|
refresh: true,
|
||||||
search: true,
|
search: true,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions, // TODO @haohao:这里有个 <> 泛型
|
} as VxeTableGridOptions<IotProductApi.Product>,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
@@ -172,24 +161,22 @@ onMounted(() => {
|
|||||||
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="mb-3 flex items-center gap-3">
|
<div class="mb-3 flex items-center gap-3">
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.name"
|
v-model:value="searchParams.name"
|
||||||
placeholder="请输入产品名称"
|
placeholder="请输入产品名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 220px"
|
class="w-[220px]"
|
||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="text-gray-400">产品名称</span>
|
<span class="text-gray-400">产品名称</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.productKey"
|
v-model:value="searchParams.productKey"
|
||||||
placeholder="请输入产品标识"
|
placeholder="请输入产品标识"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 220px"
|
class="w-[220px]"
|
||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -207,18 +194,22 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Space :size="12">
|
<TableAction
|
||||||
<Button type="primary" @click="handleCreate">
|
:actions="[
|
||||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.ADD -->
|
{
|
||||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
label: '新增产品',
|
||||||
新增产品
|
type: 'primary',
|
||||||
</Button>
|
icon: ACTION_ICON.ADD,
|
||||||
<Button type="primary" @click="handleExport">
|
onClick: handleCreate,
|
||||||
<!-- TODO @haohao:按钮使用中立的,ACTION_ICON.EXPORT -->
|
},
|
||||||
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
{
|
||||||
导出
|
label: '导出',
|
||||||
</Button>
|
type: 'primary',
|
||||||
</Space>
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
<!-- 视图切换 -->
|
<!-- 视图切换 -->
|
||||||
<Space :size="4">
|
<Space :size="4">
|
||||||
<Button
|
<Button
|
||||||
@@ -238,53 +229,18 @@ onMounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Grid v-show="viewMode === 'list'">
|
<Grid v-show="viewMode === 'list'">
|
||||||
<!-- TODO @haohao:这里貌似可以删除掉 -->
|
|
||||||
<template #toolbar-tools>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
<!-- 产品分类列 -->
|
|
||||||
<!-- TODO @haohao:这里应该可以拿到 data.ts,参考别的模块;类似 apps/web-antd/src/views/ai/image/manager/data.ts 里,里面查询 category ,和自己渲染-->
|
|
||||||
<template #category="{ row }">
|
|
||||||
<span>{{ getCategoryNameByValue(row.categoryId) }}</span>
|
|
||||||
</template>
|
|
||||||
<!-- 产品图标列 -->
|
|
||||||
<!-- TODO @haohao:直接用 Image 组件,就 ok 了呀。在 data.ts 里 -->
|
|
||||||
<template #icon="{ row }">
|
|
||||||
<Button
|
|
||||||
v-if="row.icon"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handlePreviewImage(row.icon)"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<span v-else class="text-gray-400">-</span>
|
|
||||||
</template>
|
|
||||||
<!-- TODO @haohao:直接用 Image 组件,就 ok 了呀。在 data.ts 里 -->
|
|
||||||
<!-- 产品图片列 -->
|
|
||||||
<template #picUrl="{ row }">
|
|
||||||
<Button
|
|
||||||
v-if="row.picUrl"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handlePreviewImage(row.picUrl)"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<span v-else class="text-gray-400">-</span>
|
|
||||||
</template>
|
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '详情',
|
label: '详情',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
onClick: openProductDetail.bind(null, row.id),
|
onClick: openProductDetail.bind(null, row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '物模型',
|
label: '物模型',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
onClick: openThingModel.bind(null, row.id),
|
onClick: openThingModel.bind(null, row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('common.edit'),
|
label: $t('common.edit'),
|
||||||
@@ -320,42 +276,11 @@ onMounted(() => {
|
|||||||
@thing-model="openThingModel"
|
@thing-model="openThingModel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 图片预览 -->
|
|
||||||
<!-- TODO @haohao:tindwind -->
|
|
||||||
<div style="display: none">
|
|
||||||
<!-- TODO @haohao:是不是通过 Image 直接实现预览 -->
|
|
||||||
<Image.PreviewGroup
|
|
||||||
:preview="{
|
|
||||||
visible: previewVisible,
|
|
||||||
onVisibleChange: (visible) => (previewVisible = visible),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Image :src="previewImage" />
|
|
||||||
</Image.PreviewGroup>
|
|
||||||
</div>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/** TODO @haohao:貌似这 2 个 css 没啥用? */
|
|
||||||
:deep(.vxe-toolbar div) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||||
:deep(.vxe-grid--form-wrapper) {
|
:deep(.vxe-grid--form-wrapper) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 控制图片预览的大小 */
|
|
||||||
.ant-image-preview-img {
|
|
||||||
max-width: 80% !important;
|
|
||||||
max-height: 80% !important;
|
|
||||||
object-fit: contain !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-image-preview-operations {
|
|
||||||
background: rgb(0 0 0 / 70%) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Col,
|
Col,
|
||||||
Empty,
|
Empty,
|
||||||
|
Image,
|
||||||
Pagination,
|
Pagination,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
@@ -19,10 +20,13 @@ import {
|
|||||||
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
|
||||||
// TODO @haohao:应该是 card-view.vue;
|
interface Props {
|
||||||
|
categoryList: any[];
|
||||||
// TODO @haohao:命名不太对;可以简化下;
|
searchParams?: {
|
||||||
defineOptions({ name: 'ProductCardView' });
|
name: string;
|
||||||
|
productKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
@@ -34,14 +38,6 @@ const emit = defineEmits<{
|
|||||||
thingModel: [productId: number];
|
thingModel: [productId: number];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
interface Props {
|
|
||||||
categoryList: any[];
|
|
||||||
searchParams?: {
|
|
||||||
name: string;
|
|
||||||
productKey: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const list = ref<any[]>([]);
|
const list = ref<any[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -50,14 +46,13 @@ const queryParams = ref({
|
|||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @haohao:注释的优化;
|
/** 获取分类名称 */
|
||||||
// 获取分类名称
|
|
||||||
function getCategoryName(categoryId: number) {
|
function getCategoryName(categoryId: number) {
|
||||||
const category = props.categoryList.find((c: any) => c.id === categoryId);
|
const category = props.categoryList.find((c: any) => c.id === categoryId);
|
||||||
return category?.name || '未分类';
|
return category?.name || '未分类';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取产品列表
|
/** 获取产品列表 */
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -72,14 +67,14 @@ async function getList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理页码变化
|
/** 处理页码变化 */
|
||||||
function handlePageChange(page: number, pageSize: number) {
|
function handlePageChange(page: number, pageSize: number) {
|
||||||
queryParams.value.pageNo = page;
|
queryParams.value.pageNo = page;
|
||||||
queryParams.value.pageSize = pageSize;
|
queryParams.value.pageSize = pageSize;
|
||||||
getList();
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取设备类型颜色
|
/** 获取设备类型颜色 */
|
||||||
function getDeviceTypeColor(deviceType: number) {
|
function getDeviceTypeColor(deviceType: number) {
|
||||||
const colors: Record<number, string> = {
|
const colors: Record<number, string> = {
|
||||||
0: 'blue',
|
0: 'blue',
|
||||||
@@ -114,17 +109,17 @@ onMounted(() => {
|
|||||||
:sm="12"
|
:sm="12"
|
||||||
:md="12"
|
:md="12"
|
||||||
:lg="6"
|
:lg="6"
|
||||||
class="mb-4"
|
|
||||||
>
|
>
|
||||||
<!-- TODO @haohao:卡片之间的上下距离,太宽了。 -->
|
<Card
|
||||||
<Card :body-style="{ padding: '20px' }" class="product-card h-full">
|
:body-style="{ padding: '16px' }"
|
||||||
|
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-4 flex items-start">
|
<div class="mb-3 flex items-center">
|
||||||
<!-- TODO @haohao:图标太大了;看看是不是参考 vue3 + element-plus 搞小点;然后标题居中。 -->
|
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="item.icon || 'ant-design:inbox-outlined'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-3xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
@@ -132,7 +127,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-4 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="info-list flex-1">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品分类</span>
|
<span class="info-label">产品分类</span>
|
||||||
@@ -156,20 +151,25 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品标识</span>
|
<span class="info-label">产品标识</span>
|
||||||
<!-- TODO @haohao:展示 ?有点奇怪,要不小手? -->
|
|
||||||
<Tooltip :title="item.productKey || item.id" placement="top">
|
<Tooltip :title="item.productKey || item.id" placement="top">
|
||||||
<span class="info-value product-key">
|
<span class="info-value product-key cursor-pointer">
|
||||||
{{ item.productKey || item.id }}
|
{{ item.productKey || item.id }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO @haohao:这里是不是有 image?然后默认 icon -->
|
<!-- 产品图片 -->
|
||||||
<!-- TODO @haohao:高度太高了。建议和左侧(产品分类 + 产品类型 + 产品标识)高度保持一致 -->
|
<div class="product-image">
|
||||||
<div class="product-3d-icon">
|
<Image
|
||||||
|
v-if="item.picUrl"
|
||||||
|
:src="item.picUrl"
|
||||||
|
:preview="true"
|
||||||
|
class="size-full rounded object-cover"
|
||||||
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="ant-design:box-plot-outlined"
|
v-else
|
||||||
class="text-2xl"
|
icon="lucide:image"
|
||||||
|
class="text-2xl opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,8 +180,7 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-edit"
|
class="action-btn action-btn-edit"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<!-- TODO @haohao:按钮尽量用中立的按钮,方便迁移 ele; -->
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
<IconifyIcon icon="ant-design:edit-outlined" class="mr-1" />
|
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -189,7 +188,7 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-detail"
|
class="action-btn action-btn-detail"
|
||||||
@click="emit('detail', item.id)"
|
@click="emit('detail', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -197,23 +196,17 @@ onMounted(() => {
|
|||||||
class="action-btn action-btn-model"
|
class="action-btn action-btn-model"
|
||||||
@click="emit('thingModel', item.id)"
|
@click="emit('thingModel', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||||
icon="ant-design:apartment-outlined"
|
|
||||||
class="mr-1"
|
|
||||||
/>
|
|
||||||
物模型
|
物模型
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip v-if="item.status === 1" title="启用状态的产品不能删除">
|
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
disabled
|
disabled
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="action-btn action-btn-delete !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
icon="ant-design:delete-outlined"
|
|
||||||
class="text-sm"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -226,10 +219,7 @@ onMounted(() => {
|
|||||||
danger
|
danger
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="action-btn action-btn-delete !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
icon="ant-design:delete-outlined"
|
|
||||||
class="text-sm"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,8 +231,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<!-- TODO @haohao:放到最右侧好点 -->
|
<div v-if="list.length > 0" class="flex justify-end">
|
||||||
<div v-if="list.length > 0" class="flex justify-center">
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-model:current="queryParams.pageNo"
|
v-model:current="queryParams.pageNo"
|
||||||
v-model:page-size="queryParams.pageSize"
|
v-model:page-size="queryParams.pageSize"
|
||||||
@@ -258,18 +247,9 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
/** TODO @haohao:看看哪些可以 tindwind 掉 */
|
|
||||||
.product-card-view {
|
.product-card-view {
|
||||||
.product-card {
|
.product-card {
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
:deep(.ant-card-body) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -283,8 +263,8 @@ onMounted(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 48px;
|
width: 36px;
|
||||||
height: 48px;
|
height: 36px;
|
||||||
color: white;
|
color: white;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -294,9 +274,9 @@ onMounted(() => {
|
|||||||
.product-title {
|
.product-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 36px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +285,7 @@ onMounted(() => {
|
|||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -338,7 +318,6 @@ onMounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: help;
|
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,18 +327,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D 图标
|
// 产品图片
|
||||||
.product-3d-icon {
|
.product-image {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100px;
|
width: 80px;
|
||||||
height: 100px;
|
height: 80px;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按钮组
|
// 按钮组
|
||||||
@@ -420,10 +398,6 @@ onMounted(() => {
|
|||||||
html.dark {
|
html.dark {
|
||||||
.product-card-view {
|
.product-card-view {
|
||||||
.product-card {
|
.product-card {
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 16px rgb(0 0 0 / 30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title {
|
.product-title {
|
||||||
color: rgb(255 255 255 / 85%);
|
color: rgb(255 255 255 / 85%);
|
||||||
}
|
}
|
||||||
@@ -442,7 +416,7 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-3d-icon {
|
.product-image {
|
||||||
color: #8b9cff;
|
color: #8b9cff;
|
||||||
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
|
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
|
||||||
}
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<!-- IoT 产品选择器,使用弹窗展示 -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
// TODO @haohao:这个貌似暂时没看到,在哪里用?
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
import { reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { Button, Form, Input, message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductTableSelect' });
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
success: [product: IotProductApi.Product | IotProductApi.Product[]];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
multiple?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
|
||||||
title: '产品选择器',
|
|
||||||
// TODO @haohao:handleConfirm 直接放到这里,不用单独声明
|
|
||||||
onConfirm: handleConfirm,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedProducts = ref<IotProductApi.Product[]>([]);
|
|
||||||
const selectedRowKeys = ref<number[]>([]);
|
|
||||||
|
|
||||||
// 搜索参数
|
|
||||||
const queryParams = reactive({
|
|
||||||
name: '',
|
|
||||||
productKey: '',
|
|
||||||
});
|
|
||||||
// TODO @haohao:是不是 form 应该也在 Grid 里;
|
|
||||||
|
|
||||||
// 配置表格
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
|
||||||
gridOptions: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
type: props.multiple ? 'checkbox' : 'radio',
|
|
||||||
width: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: '产品名称',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'productKey',
|
|
||||||
title: 'ProductKey',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '品类',
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'deviceType',
|
|
||||||
title: '设备类型',
|
|
||||||
minWidth: 100,
|
|
||||||
cellRender: {
|
|
||||||
name: 'CellDict',
|
|
||||||
props: { type: 'iot_product_device_type' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
minWidth: 180,
|
|
||||||
formatter: 'formatDateTime',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
checkboxConfig: {
|
|
||||||
reserve: true,
|
|
||||||
highlight: true,
|
|
||||||
},
|
|
||||||
radioConfig: {
|
|
||||||
reserve: true,
|
|
||||||
highlight: true,
|
|
||||||
},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }: any) => {
|
|
||||||
return await getProductPage({
|
|
||||||
pageNo: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打开选择器
|
|
||||||
async function open() {
|
|
||||||
selectedProducts.value = [];
|
|
||||||
selectedRowKeys.value = [];
|
|
||||||
modalApi.open();
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
function handleSearch() {
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置搜索
|
|
||||||
function handleReset() {
|
|
||||||
queryParams.name = '';
|
|
||||||
queryParams.productKey = '';
|
|
||||||
gridApi.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认选择
|
|
||||||
async function handleConfirm() {
|
|
||||||
const grid = gridApi.grid;
|
|
||||||
if (!grid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.multiple) {
|
|
||||||
const checkboxRecords = grid.getCheckboxRecords();
|
|
||||||
if (checkboxRecords.length === 0) {
|
|
||||||
message.warning('请至少选择一个产品');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
emit('success', checkboxRecords);
|
|
||||||
} else {
|
|
||||||
const radioRecord = grid.getRadioRecord();
|
|
||||||
if (!radioRecord) {
|
|
||||||
message.warning('请选择一个产品');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
emit('success', radioRecord);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal class="!w-[900px]">
|
|
||||||
<div class="mb-4">
|
|
||||||
<Form layout="inline" :model="queryParams">
|
|
||||||
<Form.Item label="产品名称">
|
|
||||||
<Input
|
|
||||||
v-model:value="queryParams.name"
|
|
||||||
placeholder="请输入产品名称"
|
|
||||||
allow-clear
|
|
||||||
class="!w-[200px]"
|
|
||||||
@press-enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="ProductKey">
|
|
||||||
<Input
|
|
||||||
v-model:value="queryParams.productKey"
|
|
||||||
placeholder="请输入产品标识"
|
|
||||||
allow-clear
|
|
||||||
class="!w-[200px]"
|
|
||||||
@press-enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" @click="handleSearch">
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="ant-design:search-outlined" />
|
|
||||||
</template>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
<Button class="ml-2" @click="handleReset">
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="ant-design:reload-outlined" />
|
|
||||||
</template>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Grid />
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
// TODO @haohao:detail 挪到 yudao-ui-admin-vben-v5/apps/web-antd/src/views/iot/product/product/detail 下。独立一个,不放在 modules 里。
|
|
||||||
import { onMounted, provide, ref } from 'vue';
|
import { onMounted, provide, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import { getDeviceCount } from '#/api/iot/device/device';
|
|||||||
import { getProduct } from '#/api/iot/product/product';
|
import { getProduct } from '#/api/iot/product/product';
|
||||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||||
|
|
||||||
import ProductDetailsHeader from './product-details-header.vue';
|
import ProductDetailsHeader from './modules/header.vue';
|
||||||
import ProductDetailsInfo from './product-details-info.vue';
|
import ProductDetailsInfo from './modules/info.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductDetail' });
|
defineOptions({ name: 'IoTProductDetail' });
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 header.vue
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Button, Card, Descriptions, message } from 'ant-design-vue';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { updateProductStatus } from '#/api/iot/product/product';
|
import { Button, Card, Descriptions, message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
import ProductForm from '../product-form.vue';
|
import {
|
||||||
|
ProductStatusEnum,
|
||||||
|
updateProductStatus,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import Form from '../../form.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
product: IotProductApi.Product;
|
product: IotProductApi.Product;
|
||||||
@@ -25,7 +28,11 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const formRef = ref();
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
/** 复制到剪贴板 */
|
/** 复制到剪贴板 */
|
||||||
async function copyToClipboard(text: string) {
|
async function copyToClipboard(text: string) {
|
||||||
@@ -46,59 +53,63 @@ function goToDeviceList(productId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 打开编辑表单 */
|
/** 打开编辑表单 */
|
||||||
function openForm(type: string, id?: number) {
|
function openEditForm(row: IotProductApi.Product) {
|
||||||
formRef.value?.open(type, id);
|
formModalApi.setData(row).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发布产品 */
|
/** 发布产品 */
|
||||||
async function confirmPublish(id: number) {
|
function handlePublish(product: IotProductApi.Product) {
|
||||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
Modal.confirm({
|
||||||
try {
|
title: '确认发布',
|
||||||
await updateProductStatus(id, 1); // TODO @好好】:1 和 0,最好用枚举;
|
content: `确认要发布产品「${product.name}」吗?`,
|
||||||
message.success('发布成功');
|
async onOk() {
|
||||||
emit('refresh');
|
await updateProductStatus(product.id!, ProductStatusEnum.PUBLISHED);
|
||||||
} catch {
|
message.success('发布成功');
|
||||||
message.error('发布失败');
|
emit('refresh');
|
||||||
}
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 撤销发布 */
|
/** 撤销发布 */
|
||||||
async function confirmUnpublish(id: number) {
|
function handleUnpublish(product: IotProductApi.Product) {
|
||||||
// TODO @haohao:最好类似;async function handleDeleteBatch() { 的做法:1)有个 confirm;2)有个 loading
|
Modal.confirm({
|
||||||
try {
|
title: '确认撤销发布',
|
||||||
await updateProductStatus(id, 0);
|
content: `确认要撤销发布产品「${product.name}」吗?`,
|
||||||
message.success('撤销发布成功');
|
async onOk() {
|
||||||
emit('refresh');
|
await updateProductStatus(product.id!, ProductStatusEnum.UNPUBLISHED);
|
||||||
} catch {
|
message.success('撤销发布成功');
|
||||||
message.error('撤销发布失败');
|
emit('refresh');
|
||||||
}
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
<FormModal @success="emit('refresh')" />
|
||||||
|
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold">{{ product.name }}</h2>
|
<h2 class="text-xl font-bold">{{ product.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<Button
|
<Button
|
||||||
:disabled="product.status === 1"
|
:disabled="product.status === ProductStatusEnum.PUBLISHED"
|
||||||
@click="openForm('update', product.id)"
|
@click="openEditForm(product)"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="product.status === 0"
|
v-if="product.status === ProductStatusEnum.UNPUBLISHED"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="confirmPublish(product.id!)"
|
@click="handlePublish(product)"
|
||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="product.status === 1"
|
v-if="product.status === ProductStatusEnum.PUBLISHED"
|
||||||
danger
|
danger
|
||||||
@click="confirmUnpublish(product.id!)"
|
@click="handleUnpublish(product)"
|
||||||
>
|
>
|
||||||
撤销发布
|
撤销发布
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,9 +138,5 @@ async function confirmUnpublish(id: number) {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 表单弹窗 -->
|
|
||||||
<!-- TODO @haohao:弹不出来;另外,应该用 index.vue 里,Form 的声明方式哈。 -->
|
|
||||||
<ProductForm ref="formRef" @success="emit('refresh')" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
// TODO @haohao:放到 detail/modules 里。然后名字就是 info.vue
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
@@ -24,7 +23,6 @@ function formatDate(date?: Date | string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card title="产品信息">
|
<Card title="产品信息">
|
||||||
<!-- TODO @haohao:看看是不是用 description 组件 -->
|
|
||||||
<Descriptions bordered :column="3" size="small">
|
<Descriptions bordered :column="3" size="small">
|
||||||
<Descriptions.Item label="产品名称">
|
<Descriptions.Item label="产品名称">
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
@@ -48,7 +46,7 @@ function formatDate(date?: Date | string) {
|
|||||||
{{ product.codecType || '-' }}
|
{{ product.codecType || '-' }}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="产品状态">
|
<Descriptions.Item label="产品状态">
|
||||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
<DictTag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
v-if="
|
v-if="
|
||||||
142
apps/web-antd/src/views/iot/product/product/modules/form.vue
Normal file
142
apps/web-antd/src/views/iot/product/product/modules/form.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Collapse, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createProduct,
|
||||||
|
getProduct,
|
||||||
|
updateProduct,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateProductKey,
|
||||||
|
useAdvancedFormSchema,
|
||||||
|
useBasicFormSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<IotProductApi.Product>();
|
||||||
|
const activeKey = ref<string[]>([]);
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['产品'])
|
||||||
|
: $t('ui.actionTitle.create', ['产品']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [],
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useAdvancedFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-2',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
|
||||||
|
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
/** 提交表单 */
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 合并两个表单的值
|
||||||
|
const basicValues = await formApi.getValues();
|
||||||
|
const advancedValues = activeKey.value.includes('advanced')
|
||||||
|
? await advancedFormApi.getValues()
|
||||||
|
: formData.value?.id
|
||||||
|
? {
|
||||||
|
icon: formData.value.icon,
|
||||||
|
picUrl: formData.value.picUrl,
|
||||||
|
description: formData.value.description,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
const data = {
|
||||||
|
...basicValues,
|
||||||
|
...advancedValues,
|
||||||
|
} as IotProductApi.Product;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateProduct(data) : createProduct(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 弹窗打开/关闭 */
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
activeKey.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<IotProductApi.Product>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
// 新增:设置默认值
|
||||||
|
await formApi.setValues({
|
||||||
|
productKey: generateProductKey(),
|
||||||
|
status: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑:加载数据
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getProduct(data.id);
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
// 设置高级表单(不等待)
|
||||||
|
advancedFormApi.setValues({
|
||||||
|
icon: formData.value.icon,
|
||||||
|
picUrl: formData.value.picUrl,
|
||||||
|
description: formData.value.description,
|
||||||
|
});
|
||||||
|
// 有高级字段时自动展开
|
||||||
|
if (
|
||||||
|
formData.value.icon ||
|
||||||
|
formData.value.picUrl ||
|
||||||
|
formData.value.description
|
||||||
|
) {
|
||||||
|
activeKey.value = ['advanced'];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-2/5">
|
||||||
|
<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,172 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { Collapse, message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createProduct,
|
|
||||||
getProduct,
|
|
||||||
updateProduct,
|
|
||||||
} from '#/api/iot/product/product';
|
|
||||||
import { $t } from '#/locales';
|
|
||||||
|
|
||||||
import {
|
|
||||||
generateProductKey,
|
|
||||||
useAdvancedFormSchema,
|
|
||||||
useBasicFormSchema,
|
|
||||||
} from '../data';
|
|
||||||
|
|
||||||
// TODO @haohao:应该是 form.vue;
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
|
|
||||||
const CollapsePanel = Collapse.Panel;
|
|
||||||
|
|
||||||
const formData = ref<any>();
|
|
||||||
const getTitle = computed(() => {
|
|
||||||
return formData.value?.id ? '编辑产品' : '新增产品';
|
|
||||||
});
|
|
||||||
const activeKey = ref<string[]>([]); // 折叠面板的激活 key,默认不展开
|
|
||||||
|
|
||||||
// TODO @haohao:每一行一个;
|
|
||||||
const [Form, formApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wrapperClass: 'grid-cols-2',
|
|
||||||
layout: 'horizontal',
|
|
||||||
schema: [],
|
|
||||||
showDefaultActions: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO @haohao:每一行一个;
|
|
||||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
|
||||||
commonConfig: {
|
|
||||||
componentProps: {
|
|
||||||
class: 'w-full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wrapperClass: 'grid-cols-2',
|
|
||||||
layout: 'horizontal',
|
|
||||||
schema: [],
|
|
||||||
showDefaultActions: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO @haohao:看看是不是可以参考别的 form 模块,优化表单这块的逻辑;从 61 到 156 行。体感有点冗余、以及代码风格,不够统一;
|
|
||||||
formApi.setState({ schema: useBasicFormSchema(formApi) });
|
|
||||||
advancedFormApi.setState({ schema: useAdvancedFormSchema() });
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
|
||||||
async onConfirm() {
|
|
||||||
// 只验证基础表单
|
|
||||||
const { valid: basicValid } = await formApi.validate();
|
|
||||||
if (!basicValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
modalApi.lock();
|
|
||||||
try {
|
|
||||||
// 提交表单 - 合并两个表单的值
|
|
||||||
const basicValues = await formApi.getValues();
|
|
||||||
|
|
||||||
// 如果折叠面板展开,则获取高级表单的值,否则保留原有值(编辑时)或使用空值(新增时)
|
|
||||||
let advancedValues: any = {};
|
|
||||||
if (activeKey.value.includes('advanced')) {
|
|
||||||
advancedValues = await advancedFormApi.getValues();
|
|
||||||
} else if (formData.value?.id) {
|
|
||||||
// 编辑时保留原有的高级字段值
|
|
||||||
advancedValues = {
|
|
||||||
icon: formData.value.icon,
|
|
||||||
picUrl: formData.value.picUrl,
|
|
||||||
description: formData.value.description,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
...basicValues,
|
|
||||||
...advancedValues,
|
|
||||||
} as IotProductApi.Product;
|
|
||||||
const data = formData.value?.id
|
|
||||||
? { ...values, id: formData.value.id }
|
|
||||||
: values;
|
|
||||||
|
|
||||||
await (formData.value?.id ? updateProduct(data) : createProduct(data));
|
|
||||||
// 关闭并提示
|
|
||||||
await modalApi.close();
|
|
||||||
emit('success');
|
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
|
||||||
} finally {
|
|
||||||
modalApi.unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onOpenChange(isOpen: boolean) {
|
|
||||||
if (!isOpen) {
|
|
||||||
formData.value = undefined;
|
|
||||||
// 重置折叠面板状态
|
|
||||||
activeKey.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 加载数据
|
|
||||||
const data = modalApi.getData<any>();
|
|
||||||
if (!data || !data.id) {
|
|
||||||
// 设置默认值
|
|
||||||
await formApi.setValues({
|
|
||||||
productKey: generateProductKey(), // 自动生成 ProductKey
|
|
||||||
// deviceType: 0, // 默认直连设备
|
|
||||||
// codecType: 'Alink', // 默认 Alink
|
|
||||||
// dataFormat: 1, // 默认 JSON
|
|
||||||
// validateType: 1, // 默认设备密钥
|
|
||||||
status: 0, // 默认启用
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
formData.value = await getProduct(data.id);
|
|
||||||
// 设置基础表单
|
|
||||||
await formApi.setValues(formData.value);
|
|
||||||
|
|
||||||
// 先设置高级表单的值(不等待)
|
|
||||||
advancedFormApi.setValues({
|
|
||||||
icon: formData.value.icon,
|
|
||||||
picUrl: formData.value.picUrl,
|
|
||||||
description: formData.value.description,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果有图标、图片或描述,自动展开折叠面板以便显示
|
|
||||||
if (
|
|
||||||
formData.value.icon ||
|
|
||||||
formData.value.picUrl ||
|
|
||||||
formData.value.description
|
|
||||||
) {
|
|
||||||
activeKey.value = ['advanced'];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载产品数据失败:', error);
|
|
||||||
message.error('加载产品数据失败,请重试');
|
|
||||||
} finally {
|
|
||||||
modalApi.unlock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal :title="getTitle" class="w-2/5">
|
|
||||||
<div class="mx-4">
|
|
||||||
<Form />
|
|
||||||
<Collapse v-model:active-key="activeKey" class="mt-4">
|
|
||||||
<CollapsePanel key="advanced" header="更多设置">
|
|
||||||
<AdvancedForm />
|
|
||||||
</CollapsePanel>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user