feat: 新增属性分组功能
- 在产品详情页面添加属性分组标签页 - 实现属性分组的创建、编辑和删除功能 - 添加属性分组的排序和描述信息 - 优化属性选择界面,支持搜索和过滤功能
This commit is contained in:
parent
f8619047c5
commit
0c0a1994b2
|
@ -1,3 +1,5 @@
|
|||
启动项目pnpm run dev:antd
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
|
|
|
@ -118,6 +118,7 @@ const formRules = {
|
|||
},
|
||||
],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
// 'valueParams.dataType': [
|
||||
// { required: true, message: '请选择数据类型', trigger: 'blur' },
|
||||
// ],
|
||||
|
|
|
@ -155,6 +155,7 @@ const formRules = {
|
|||
},
|
||||
],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 抽屉标题
|
||||
|
|
|
@ -38,6 +38,7 @@ const currentMetadata = ref<any>({
|
|||
properties: [],
|
||||
functions: [],
|
||||
events: [],
|
||||
propertyGroups: [], // 新增属性分组
|
||||
});
|
||||
|
||||
// 标签页切换
|
||||
|
@ -97,6 +98,7 @@ const loadMetadata = async () => {
|
|||
properties: [],
|
||||
functions: [],
|
||||
events: [],
|
||||
propertyGroups: [], // 新增属性分组
|
||||
};
|
||||
try {
|
||||
currentMetadata.value = props.productInfo.metadata
|
||||
|
@ -212,6 +214,7 @@ const handleSave = async () => {
|
|||
metadata.properties.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.functions.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.events.sort((a: any, b: any) => a.sort - b.sort);
|
||||
metadata.propertyGroups.sort((a: any, b: any) => a.sort - b.sort); // 新增属性分组排序
|
||||
await productUpdateById(props.productInfo.id, {
|
||||
id: props.productInfo.id,
|
||||
metadata: JSON.stringify(metadata),
|
||||
|
@ -244,6 +247,11 @@ const handleMetadataChange = (data: any) => {
|
|||
currentMetadata.value.properties = data;
|
||||
break;
|
||||
}
|
||||
case 'propertyGroups': {
|
||||
// 新增属性分组处理
|
||||
currentMetadata.value.propertyGroups = data;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
|
@ -328,6 +336,16 @@ loadMetadata();
|
|||
@refresh="loadMetadata"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="propertyGroups" tab="属性分组">
|
||||
<MetadataTable
|
||||
type="propertyGroups"
|
||||
:product-id="productInfo.id"
|
||||
:metadata="currentMetadata.propertyGroups"
|
||||
:available-properties="currentMetadata.properties"
|
||||
@change="handleMetadataChange"
|
||||
@refresh="loadMetadata"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="functions" tab="功能定义">
|
||||
<MetadataTable
|
||||
type="functions"
|
||||
|
|
|
@ -9,11 +9,13 @@ import { dataTypeOptions, readWriteTypeOptions } from '#/constants/dicts';
|
|||
import EventDrawer from './EventDrawer.vue';
|
||||
import FunctionDrawer from './FunctionDrawer.vue';
|
||||
import PropertyDrawer from './PropertyDrawer.vue';
|
||||
import PropertyGroupDrawer from './PropertyGroupDrawer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
availableProperties?: any[]; // 可用的属性列表(用于属性分组)
|
||||
metadata: any[];
|
||||
productId: string;
|
||||
type: 'events' | 'functions' | 'properties' | 'tags';
|
||||
type: 'events' | 'functions' | 'properties' | 'propertyGroups' | 'tags';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -67,6 +69,18 @@ const columns = computed(() => {
|
|||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
// 属性分组特殊列
|
||||
if (props.type === 'propertyGroups') {
|
||||
baseColumns.splice(3, 0, {
|
||||
title: '属性数',
|
||||
dataIndex: 'properties',
|
||||
key: 'count',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => record.properties?.length || 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 功能定义特殊列
|
||||
if (props.type === 'properties') {
|
||||
baseColumns.splice(
|
||||
|
@ -129,6 +143,7 @@ const getTypeLabel = () => {
|
|||
functions: '功能',
|
||||
events: '事件',
|
||||
tags: '标签',
|
||||
propertyGroups: '分组',
|
||||
};
|
||||
return labels[props.type];
|
||||
};
|
||||
|
@ -319,6 +334,16 @@ onMounted(() => {
|
|||
:data="formData"
|
||||
@save="handleSave"
|
||||
/>
|
||||
|
||||
<!-- 属性分组抽屉 -->
|
||||
<PropertyGroupDrawer
|
||||
v-if="type === 'propertyGroups'"
|
||||
v-model:open="drawerVisible"
|
||||
:is-edit="isEdit"
|
||||
:data="formData"
|
||||
:available-properties="props.availableProperties || []"
|
||||
@save="handleSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -116,6 +116,7 @@ const formRules = {
|
|||
},
|
||||
],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
// 'valueParams.dataType': [
|
||||
// { required: true, message: '请选择数据类型', trigger: 'blur' },
|
||||
// ],
|
||||
|
|
|
@ -0,0 +1,304 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Row,
|
||||
Space,
|
||||
Table,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import PropertySelectionModal from './PropertySelectionModal.vue';
|
||||
|
||||
interface PropertyGroupData {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
sort: number;
|
||||
description: string;
|
||||
properties: any[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
isEdit: boolean;
|
||||
data?: PropertyGroupData;
|
||||
availableProperties: any[]; // 可用的属性列表
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'save', data: PropertyGroupData): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: (value) => emit('update:open', value),
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const saveLoading = ref(false);
|
||||
|
||||
// 属性选择弹窗
|
||||
const propertySelectionVisible = ref(false);
|
||||
|
||||
// 默认数据
|
||||
const defaultPropertyGroupData: PropertyGroupData = {
|
||||
id: '',
|
||||
name: '',
|
||||
sort: 1,
|
||||
description: '',
|
||||
properties: [],
|
||||
};
|
||||
|
||||
const formData = ref<PropertyGroupData>({ ...defaultPropertyGroupData });
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
id: [
|
||||
{ required: true, message: '请输入标识', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-z]\w*$/i,
|
||||
message: '请输入以字母开头,只包含字母、数字和下划线的标识',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 属性表格列配置
|
||||
const propertyColumns = [
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = { ...defaultPropertyGroupData };
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
// 监听数据变化
|
||||
watch(
|
||||
() => props.data,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
formData.value = JSON.parse(JSON.stringify(newData));
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 添加属性
|
||||
const handleAddProperty = () => {
|
||||
propertySelectionVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除属性
|
||||
const handleDeleteProperty = (propertyId: string) => {
|
||||
const index = formData.value.properties.findIndex((p) => p.id === propertyId);
|
||||
if (index !== -1) {
|
||||
formData.value.properties.splice(index, 1);
|
||||
message.success('删除属性成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 属性选择确认
|
||||
const handlePropertySelectionConfirm = (selectedProperties: any[]) => {
|
||||
// 过滤掉已经选择的属性
|
||||
const newProperties = selectedProperties.filter(
|
||||
(prop) => !formData.value.properties.some((p) => p.id === prop.id),
|
||||
);
|
||||
|
||||
if (newProperties.length > 0) {
|
||||
formData.value.properties.push(...newProperties);
|
||||
message.success(`成功添加 ${newProperties.length} 个属性`);
|
||||
} else {
|
||||
message.warning('没有可添加的新属性');
|
||||
}
|
||||
|
||||
propertySelectionVisible.value = false;
|
||||
};
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
emit('save', JSON.parse(JSON.stringify(formData.value)));
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭抽屉
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="isEdit ? '编辑分组' : '添加'"
|
||||
width="800px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<FormItem label="标识" name="id">
|
||||
<Input v-model:value="formData.id" placeholder="请输入" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="名称" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Col :span="24">
|
||||
<FormItem label="属性列表" name="properties">
|
||||
<Table
|
||||
:columns="propertyColumns"
|
||||
:data-source="formData.properties"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteProperty(record.id)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="add-button-container">
|
||||
<a-button
|
||||
@click="handleAddProperty"
|
||||
type="primary"
|
||||
class="add-button"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加属性
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber
|
||||
v-model:value="formData.sort"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入"
|
||||
:rows="3"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="saveLoading" @click="handleSave">
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<!-- 属性选择弹窗 -->
|
||||
<PropertySelectionModal
|
||||
v-model:open="propertySelectionVisible"
|
||||
:available-properties="props.availableProperties"
|
||||
:selected-properties="formData.properties"
|
||||
@confirm="handlePropertySelectionConfirm"
|
||||
/>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.property-section {
|
||||
padding-top: 16px;
|
||||
margin: 24px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-button-container {
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
|
||||
.add-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,231 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Input, Modal, Space, Table, Tag } from 'ant-design-vue';
|
||||
|
||||
import { dataTypeOptions, readWriteTypeOptions } from '#/constants/dicts';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
availableProperties: any[]; // 可用的属性列表
|
||||
selectedProperties: any[]; // 已选择的属性列表
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'confirm', value: any[]): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: (value) => emit('update:open', value),
|
||||
});
|
||||
|
||||
const searchName = ref('');
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'selection',
|
||||
key: 'selection',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
title: '标识',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
dataIndex: 'dataType',
|
||||
key: 'dataType',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '读写类型',
|
||||
dataIndex: 'readWriteType',
|
||||
key: 'readWriteType',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 过滤后的属性列表
|
||||
const filteredProperties = computed(() => {
|
||||
let result = [...props.availableProperties];
|
||||
|
||||
// 过滤掉已选择的属性
|
||||
result = result.filter(
|
||||
(prop) => !props.selectedProperties.some((p) => p.id === prop.id),
|
||||
);
|
||||
|
||||
// 按名称过滤
|
||||
if (searchName.value) {
|
||||
result = result.filter(
|
||||
(prop) =>
|
||||
prop.name.toLowerCase().includes(searchName.value.toLowerCase()) ||
|
||||
prop.id.toLowerCase().includes(searchName.value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 选中的属性
|
||||
const selectedPropertyIds = ref<string[]>([]);
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchName.value = '';
|
||||
};
|
||||
|
||||
// 查询
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已在computed中实现
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
const selectedProperties = filteredProperties.value.filter((prop) =>
|
||||
selectedPropertyIds.value.includes(prop.id),
|
||||
);
|
||||
emit('confirm', selectedProperties);
|
||||
selectedPropertyIds.value = [];
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
selectedPropertyIds.value = [];
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 监听弹窗打开,重置选择
|
||||
watch(
|
||||
() => visible.value,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
selectedPropertyIds.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 获取数据类型标签
|
||||
const getDataTypeLabel = (dataType: string) => {
|
||||
const option = dataTypeOptions.find((item) => item.value === dataType);
|
||||
return option ? option.label : dataType;
|
||||
};
|
||||
|
||||
// 获取读写类型标签
|
||||
const getReadWriteTypeLabel = (type: string) => {
|
||||
const option = readWriteTypeOptions.find((item) => item.value === type);
|
||||
return option ? option.label : type;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="visible"
|
||||
title="选择属性"
|
||||
width="1000px"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleConfirm"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-section">
|
||||
<Space>
|
||||
标识/名称:
|
||||
<Input
|
||||
v-model:value="searchName"
|
||||
placeholder="请输入标识和名称搜索"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
/>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
<Button type="primary" @click="handleSearch">查询</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 属性列表 -->
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="filteredProperties"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedPropertyIds,
|
||||
onChange: (selectedRowKeys: string[]) => {
|
||||
selectedPropertyIds = selectedRowKeys;
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
getDataTypeLabel(record.valueParams?.dataType || record.dataType)
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'readWriteType'">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
getReadWriteTypeLabel(
|
||||
record.expands?.type || record.readWriteType,
|
||||
)
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'description'">
|
||||
{{ record.description || '-' }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 分页信息 -->
|
||||
<div class="pagination-info">
|
||||
<span>
|
||||
第 1-{{ filteredProperties.length }} 条/总共{{
|
||||
filteredProperties.length
|
||||
}}
|
||||
条
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue