feat: 新增产品分类功能,抽离本地全局字典

- 添加产品分类相关 API接口
- 实现产品分类列表和 drawers 组件
- 移除冗余的字典定义,统一字典管理
- 优化网络、协议等模块的代码结构
This commit is contained in:
fhysy 2025-08-07 17:18:32 +08:00
parent ff6669eb17
commit da0cb82255
9 changed files with 606 additions and 22 deletions

View File

@ -0,0 +1,57 @@
import type {
ProductCategoryForm,
ProductCategoryQuery,
ProductCategoryVO,
} from './model';
import type { ID, IDS } from '#/api/common';
import { requestClient } from '#/api/request';
/**
*
* @param params
* @returns
*/
export function productCategoryList(params?: ProductCategoryQuery) {
return requestClient.get<ProductCategoryVO[]>(
`/device/productCategory/list`,
{ params },
);
}
/**
*
* @param id id
* @returns
*/
export function productCategoryInfo(id: ID) {
return requestClient.get<ProductCategoryVO>(`/device/productCategory/${id}`);
}
/**
*
* @param data
* @returns void
*/
export function productCategoryAdd(data: ProductCategoryForm) {
return requestClient.postWithMsg<void>('/device/productCategory', data);
}
/**
*
* @param data
* @returns void
*/
export function productCategoryUpdate(data: ProductCategoryForm) {
return requestClient.putWithMsg<void>('/device/productCategory', data);
}
/**
*
* @param id id
* @returns void
*/
export function productCategoryRemove(id: ID | IDS) {
return requestClient.deleteWithMsg<void>(`/device/productCategory/${id}`);
}

View File

@ -0,0 +1,147 @@
import type { BaseEntity } from '#/api/common';
export interface ProductCategoryVO {
/**
*
*/
id: number | string;
/**
*
*/
categoryName: string;
/**
* key
*/
categoryKey: string;
/**
* 0
*/
enabled: string;
/**
*
*/
categoryParam: string;
/**
*
*/
description: string;
/**
*
*/
metadata: string;
/**
*
*/
sortNo: number;
/**
* id
*/
parentId: number | string;
/**
*
*/
children: ProductCategoryVO[];
}
export interface ProductCategoryForm extends BaseEntity {
/**
*
*/
id?: number | string;
/**
*
*/
categoryName?: string;
/**
* key
*/
categoryKey?: string;
/**
* 0
*/
enabled?: string;
/**
*
*/
categoryParam?: string;
/**
*
*/
description?: string;
/**
*
*/
metadata?: string;
/**
*
*/
sortNo?: number;
/**
* id
*/
parentId?: number | string;
}
export interface ProductCategoryQuery {
/**
*
*/
categoryName?: string;
/**
* key
*/
categoryKey?: string;
/**
* 0
*/
enabled?: string;
/**
*
*/
categoryParam?: string;
/**
*
*/
description?: string;
/**
*
*/
metadata?: string;
/**
*
*/
sortNo?: number;
/**
* id
*/
parentId?: number | string;
/**
*
*/
params?: any;
}

View File

@ -0,0 +1,8 @@
/* 本地字典 */
export * from './operations';
// 启用状态
export const enabledOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
];

View File

@ -0,0 +1,105 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
export const querySchema: FormSchemaGetter = () => [
// {
// component: 'Input',
// fieldName: 'categoryKey',
// label: '分类key',
// },
{
component: 'Input',
fieldName: 'categoryName',
label: '分类名称',
},
];
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
// export const columns: () => VxeGridProps['columns'] = () => [
export const columns: VxeGridProps['columns'] = [
// {
// title: '编号',
// field: 'id',
// treeNode: true,
// },
{
title: '分类名称',
field: 'categoryName',
treeNode: true,
},
// {
// title: '分类key',
// field: 'categoryKey',
// },
{
title: '描述',
field: 'description',
},
{
title: '排序号',
field: 'sortNo',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
},
];
export const drawerSchema: FormSchemaGetter = () => [
{
label: '编号',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '上级分类',
fieldName: 'parentId',
component: 'TreeSelect',
},
{
label: '分类名称',
fieldName: 'categoryName',
component: 'Input',
rules: 'required',
},
// {
// label: '分类key',
// fieldName: 'categoryKey',
// component: 'Input',
// rules: 'required',
// },
{
label: '启用状态',
fieldName: 'enabled',
component: 'Input',
rules: 'required',
defaultValue: '1',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '排序号',
fieldName: 'sortNo',
component: 'InputNumber',
rules: 'required',
defaultValue: 10,
},
{
label: '描述',
fieldName: 'description',
component: 'Textarea',
componentProps: {
placeholder: '请输入描述',
rows: 3,
},
},
];

View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { ProductCategoryForm } from '#/api/device/productCategory/model';
import { nextTick } from 'vue';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
productCategoryList,
productCategoryRemove,
} from '#/api/device/productCategory';
import { columns, querySchema } from './data';
import productCategoryDrawer from './productCategory-drawer.vue';
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: querySchema(),
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
};
const gridOptions: VxeGridProps = {
// 使i18ngetter
// columns: columns(),
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_, formValues = {}) => {
const resp = await productCategoryList({
...formValues,
});
return { rows: resp };
},
//
querySuccess: () => {
nextTick(() => {
expandAll();
});
},
},
},
rowConfig: {
keyField: 'id',
},
/**
* 虚拟滚动开关 默认关闭
* 数据量小可以选择关闭
* 如果遇到样式问题(空白错位 滚动等)可以选择关闭虚拟滚动
*/
scrollY: {
enabled: false,
gt: 0,
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
// tree vxe
transform: true,
},
//
id: 'device-productCategory-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
const [ProductCategoryDrawer, drawerApi] = useVbenDrawer({
connectedComponent: productCategoryDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(row: Required<ProductCategoryForm>) {
drawerApi.setData({ id: row.id });
drawerApi.open();
}
async function handleDelete(row: Required<ProductCategoryForm>) {
await productCategoryRemove(row.id);
await tableApi.query();
}
function expandAll() {
tableApi.grid?.setAllTreeExpand(true);
}
function collapseAll() {
tableApi.grid?.setAllTreeExpand(false);
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="产品分类列表">
<template #toolbar-tools>
<Space>
<a-button @click="collapseAll">
{{ $t('pages.common.collapse') }}
</a-button>
<a-button @click="expandAll">
{{ $t('pages.common.expand') }}
</a-button>
<a-button
type="primary"
v-access:code="['device:productCategory:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<ghost-button
v-access:code="['device:productCategory:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['device:productCategory:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
<ProductCategoryDrawer @reload="tableApi.query()" />
</Page>
</template>

View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep, getPopupContainer, listToTree } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import {
productCategoryAdd,
productCategoryInfo,
productCategoryList,
productCategoryUpdate,
} from '#/api/device/productCategory';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
formItemClass: 'col-span-2',
// label px
labelWidth: 80,
//
componentProps: {
class: 'w-full',
},
},
schema: drawerSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
async function setupProductCategorySelect() {
const listData = await productCategoryList();
const treeData = listToTree(listData, { id: 'id', pid: 'parentId' });
formApi.updateSchema([
{
fieldName: 'parentId',
componentProps: {
treeData,
treeLine: { showLeafIcon: false },
fieldNames: { label: 'categoryName', value: 'id' },
treeDefaultExpandAll: true,
getPopupContainer,
},
},
]);
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
//
class: 'w-[550px]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await productCategoryInfo(id);
await formApi.setValues(record);
}
await setupProductCategorySelect();
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value
? productCategoryUpdate(data)
: productCategoryAdd(data));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title">
<BasicForm />
</BasicDrawer>
</template>

View File

@ -5,8 +5,7 @@ import { h } from 'vue';
import { networkList } from '#/api/operations/network';
import { protocolList } from '#/api/operations/protocol';
import { enabledOptions, networkTypeOptions } from '../user.data';
import { enabledOptions, networkTypeOptions } from '#/constants/dicts';
export const providerOptions = [
...networkTypeOptions,
@ -189,7 +188,6 @@ export const drawerSchema: FormSchemaGetter = () => [
showSearch: true,
class: 'w-full',
onChange: async (value: string) => {
console.log('抽屉表单接入方式变化:', value);
if (networkOptionsUpdateCallback) {
await networkOptionsUpdateCallback(value);
}

View File

@ -1,16 +1,7 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { networkTypeOptions,enabledOptions } from '../user.data';
// const networkTypeOptions = [
// { label: 'MQTT客户端', value: 'MQTT_CLIENT' },
// { label: 'HTTP服务', value: 'HTTP_SERVER' },
// ];
// const enabledOptions = [
// { label: '启用', value: '1' },
// { label: '禁用', value: '0' },
// ];
import { enabledOptions, networkTypeOptions } from '#/constants/dicts';
// HTTP服务配置字段
const httpServerFields = [

View File

@ -1,10 +1,7 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
const enabledOptions = [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
];
import { enabledOptions } from '#/constants/dicts';
const protocolTypeOptions = [
{ label: 'local', value: 'local' },
@ -120,14 +117,13 @@ export const drawerSchema: FormSchemaGetter = () => [
{
label: '启用状态',
fieldName: 'enabled',
component: 'Select',
component: 'RadioGroup',
componentProps: {
allowClear: true,
filterOption: true,
buttonStyle: 'solid',
options: enabledOptions,
placeholder: '请选择',
showSearch: true,
optionType: 'button',
},
defaultValue: '1',
rules: 'required',
},
{