!328 表单设计器 UserSelect/DeptSelect 支持默认选中当前用户/部门、修复商品 SKU 名称校验失败的问题

Merge pull request !328 from puhui999/master-fix
This commit is contained in:
xingyu
2026-01-29 03:28:24 +00:00
committed by Gitee
14 changed files with 580 additions and 19 deletions

View File

@@ -0,0 +1,210 @@
<!-- 部门选择器 - 树形结构显示 (Ant Design Vue 版本) -->
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { useUserStore } from '@vben/stores';
import { handleTree } from '@vben/utils';
import { TreeSelect } from 'ant-design-vue';
import { requestClient } from '#/api/request';
defineOptions({ name: 'DeptSelect' });
const props = withDefaults(defineProps<Props>(), {
multiple: false,
returnType: 'id',
defaultCurrentDept: false,
disabled: false,
placeholder: '',
});
const emit = defineEmits<{
(
e: 'update:modelValue',
value: number | number[] | string | string[] | undefined,
): void;
}>();
// 部门数据接口
interface DeptVO {
id: number;
name: string;
parentId: number;
sort?: number;
leaderUserId?: number;
phone?: string;
email?: string;
status?: number;
}
// 接受父组件参数
interface Props {
modelValue?: number | number[] | string | string[];
multiple?: boolean;
returnType?: 'id' | 'name';
defaultCurrentDept?: boolean;
disabled?: boolean;
placeholder?: string;
formCreateInject?: any;
}
// 部门树形数据
const deptTree = ref<any[]>([]);
// 原始部门列表(用于 returnType='name' 时查找名称)
const deptList = ref<DeptVO[]>([]);
// 当前选中值
const selectedValue = ref<number | number[] | undefined>();
// 加载部门树形数据
async function loadDeptTree(): Promise<void> {
try {
const data = await requestClient.get<DeptVO[]>('/system/dept/simple-list');
deptList.value = data;
deptTree.value = handleTree(data);
} catch (error) {
console.warn('[DeptSelect] 加载部门数据失败:', error);
deptTree.value = [];
}
}
// 根据 ID 获取部门名称
function getDeptNameById(id: number): string | undefined {
const dept = deptList.value.find((item) => item.id === id);
if (!dept) {
console.warn(`[DeptSelect] 未找到 ID 为 ${id} 的部门`);
}
return dept?.name;
}
// 根据名称获取部门 ID
function getDeptIdByName(name: string): number | undefined {
const dept = deptList.value.find((item) => item.name === name);
return dept?.id;
}
// 处理选中值变化
function handleChange(value: number | number[] | undefined): void {
if (value === undefined || value === null) {
emit('update:modelValue', props.multiple ? [] : undefined);
return;
}
// 根据 returnType 决定返回值类型
if (props.returnType === 'name') {
if (props.multiple && Array.isArray(value)) {
const names = value
.map((id) => getDeptNameById(id))
.filter(Boolean) as string[];
emit('update:modelValue', names);
} else if (!props.multiple && typeof value === 'number') {
const name = getDeptNameById(value);
emit('update:modelValue', name);
}
} else {
emit('update:modelValue', value);
}
}
// 树节点过滤方法(支持搜索过滤)
function filterTreeNode(inputValue: string, treeNode: any): boolean {
if (!inputValue) return true;
return treeNode.name?.toLowerCase().includes(inputValue.toLowerCase());
}
// 同步 modelValue 到内部选中值
function syncSelectedValue(): void {
const newValue = props.modelValue;
if (newValue === undefined || newValue === null) {
selectedValue.value = props.multiple ? [] : undefined;
return;
}
// 如果 returnType 是 'name',需要将名称转换为 ID 用于树选择器显示
if (props.returnType === 'name') {
// 只有在 deptList 加载完成后才能进行转换
if (deptList.value.length === 0) {
return;
}
if (props.multiple && Array.isArray(newValue)) {
const ids = (newValue as string[])
.map((name) => getDeptIdByName(name))
.filter(Boolean) as number[];
selectedValue.value = ids;
} else if (!props.multiple && typeof newValue === 'string') {
const id = getDeptIdByName(newValue);
selectedValue.value = id;
}
} else {
selectedValue.value = newValue as number | number[];
}
}
// 监听 modelValue 变化,同步到内部选中值
watch(() => props.modelValue, syncSelectedValue, { immediate: true });
// 监听 deptList 变化,重新同步选中值(解决数据加载完成后的回显问题)
watch(() => deptList.value, syncSelectedValue);
// 检查是否有有效的预设值
function hasValidPresetValue(): boolean {
const value = props.modelValue;
if (value === undefined || value === null || value === '') {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
// 设置默认值(当前用户部门)
function setDefaultValue(): void {
// 仅当 defaultCurrentDept 为 true 时处理
if (!props.defaultCurrentDept) {
return;
}
// 检查是否已有预设值(预设值优先级高于默认当前部门)
if (hasValidPresetValue()) {
return;
}
// 获取当前用户的部门 ID
const userStore = useUserStore();
const deptId = userStore.userInfo?.deptId as number | undefined;
// 处理 deptId 为空或 0 的边界情况
if (!deptId || deptId === 0) {
return;
}
// 根据多选模式决定默认值格式
const defaultValue = props.multiple ? [deptId] : deptId;
emit('update:modelValue', defaultValue);
}
// 组件挂载时加载数据并设置默认值
onMounted(async () => {
await loadDeptTree();
// 数据加载完成后设置默认值
setDefaultValue();
});
</script>
<template>
<TreeSelect
v-model:value="selectedValue"
class="w-full"
:tree-data="deptTree"
:field-names="{ label: 'name', value: 'id', children: 'children' }"
:multiple="multiple"
:disabled="disabled"
:placeholder="placeholder || '请选择部门'"
:tree-checkable="multiple"
:show-search="true"
:filter-tree-node="filterTreeNode"
:allow-clear="true"
@change="handleChange"
/>
</template>

View File

@@ -2,6 +2,7 @@ import type { ApiSelectProps } from '#/components/form-create/typing';
import { defineComponent, onMounted, ref, useAttrs } from 'vue';
import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils';
import {
@@ -74,12 +75,46 @@ export function useApiSelect(option: ApiSelectProps) {
type: String,
default: 'id',
},
// 是否默认选中当前用户(仅用于 UserSelect
defaultCurrentUser: {
type: Boolean,
default: false,
},
setup(props) {
},
setup(props, { emit }) {
const attrs = useAttrs();
const options = ref<any[]>([]); // 下拉数据
const loading = ref(false); // 是否正在从远程获取数据
const queryParam = ref<any>(); // 当前输入的值
// 检查是否有有效的预设值
function hasValidPresetValue(): boolean {
const value = attrs.modelValue;
if (value === undefined || value === null || value === '') {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
// 设置默认当前用户
function setDefaultCurrentUser(): void {
if (option.name !== 'UserSelect' || !props.defaultCurrentUser) {
return;
}
if (hasValidPresetValue()) {
return;
}
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id;
if (currentUserId) {
const defaultValue = props.multiple ? [currentUserId] : currentUserId;
emit('update:modelValue', defaultValue);
}
}
const getOptions = async () => {
options.value = [];
// 接口选择器
@@ -199,6 +234,8 @@ export function useApiSelect(option: ApiSelectProps) {
onMounted(async () => {
await getOptions();
// 设置默认当前用户(仅用于 UserSelect
setDefaultCurrentUser();
});
const buildSelect = () => {

View File

@@ -189,6 +189,14 @@ export async function useFormCreateDesigner(designer: Ref) {
name: 'UserSelect',
label: '用户选择器',
icon: 'icon-eye',
props: [
{
type: 'switch',
field: 'defaultCurrentUser',
title: '默认选中当前用户',
value: true,
},
],
});
const deptSelectRule = useSelectRule({
name: 'DeptSelect',
@@ -205,6 +213,12 @@ export async function useFormCreateDesigner(designer: Ref) {
{ label: '部门名称', value: 'name' },
],
},
{
type: 'switch',
field: 'defaultCurrentDept',
title: '默认选中当前部门',
value: true,
},
],
});
const dictSelectRule = useDictSelectRule();

View File

@@ -23,13 +23,24 @@ export function useSelectRule(option: SelectRuleOption) {
name,
event: option.event,
rule() {
return {
// 构建基础规则
const baseRule: any = {
type: name,
field: buildUUID(),
title: label,
info: '',
$required: false,
};
// 将自定义 props 的默认值添加到 rule 的 props 中
if (option.props && option.props.length > 0) {
baseRule.props = {};
option.props.forEach((prop: any) => {
if (prop.field && prop.value !== undefined) {
baseRule.props[prop.field] = prop.value;
}
});
}
return baseRule;
},
props(_: any, { t }: any) {
if (!option.props) {

View File

@@ -34,6 +34,7 @@ import {
// ======================= 自定义组件 =======================
import { useApiSelect } from '#/components/form-create';
import DeptSelect from '#/components/form-create/components/dept-select.vue';
import DictSelect from '#/components/form-create/components/dict-select.vue';
import { useImagesUpload } from '#/components/form-create/components/use-images-upload';
import { Tinymce } from '#/components/tinymce';
@@ -45,12 +46,6 @@ const UserSelect = useApiSelect({
valueField: 'id',
url: '/system/user/simple-list',
});
const DeptSelect = useApiSelect({
name: 'DeptSelect',
labelField: 'name',
valueField: 'id',
url: '/system/dept/simple-list',
});
const ApiSelect = useApiSelect({
name: 'ApiSelect',
});

View File

@@ -56,6 +56,7 @@ const tableHeaders = ref<{ label: string; prop: string }[]>([]);
/** 创建空 SKU 数据 */
function createEmptySku(): MallSpuApi.Sku {
return {
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,

View File

@@ -50,6 +50,7 @@ const formData = ref<MallSpuApi.Spu>({
subCommissionType: false,
skus: [
{
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,
@@ -181,6 +182,11 @@ async function handleSubmit() {
.merge(otherFormApi)
.submitAllForm(true);
values.skus = formData.value.skus;
// 校验商品名称不能为空(用于 SKU name
if (!values.name || values.name.trim() === '') {
message.error('商品名称不能为空');
return;
}
if (values.skus) {
try {
// 校验 sku
@@ -190,6 +196,8 @@ async function handleSubmit() {
return;
}
values.skus.forEach((item) => {
// 给 sku name 赋值(使用商品名称作为 SKU 名称)
item.name = values.name;
// 金额转换:元转分
item.price = convertToInteger(item.price);
item.marketPrice = convertToInteger(item.marketPrice);
@@ -277,6 +285,7 @@ function handleChangeSpec() {
// 重置 sku 列表
formData.value.skus = [
{
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,

View File

@@ -0,0 +1,218 @@
<!-- 部门选择器 - 树形结构显示 (Element Plus 版本) -->
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { useUserStore } from '@vben/stores';
import { handleTree } from '@vben/utils';
import { ElTreeSelect } from 'element-plus';
import { requestClient } from '#/api/request';
defineOptions({ name: 'DeptSelect' });
const props = withDefaults(defineProps<Props>(), {
multiple: false,
returnType: 'id',
defaultCurrentDept: false,
disabled: false,
placeholder: '',
});
const emit = defineEmits<{
(
e: 'update:modelValue',
value: number | number[] | string | string[] | undefined,
): void;
}>();
// 部门数据接口
interface DeptVO {
id: number;
name: string;
parentId: number;
sort?: number;
leaderUserId?: number;
phone?: string;
email?: string;
status?: number;
}
// 接受父组件参数
interface Props {
modelValue?: number | number[] | string | string[];
multiple?: boolean;
returnType?: 'id' | 'name';
defaultCurrentDept?: boolean;
disabled?: boolean;
placeholder?: string;
formCreateInject?: any;
}
// Element Plus TreeSelect 的 props 配置
const treeProps = {
label: 'name',
value: 'id',
children: 'children',
};
// 部门树形数据
const deptTree = ref<any[]>([]);
// 原始部门列表(用于 returnType='name' 时查找名称)
const deptList = ref<DeptVO[]>([]);
// 当前选中值
const selectedValue = ref<number | number[] | undefined>();
// 加载部门树形数据
async function loadDeptTree(): Promise<void> {
try {
const data = await requestClient.get<DeptVO[]>('/system/dept/simple-list');
deptList.value = data;
deptTree.value = handleTree(data);
} catch (error) {
console.warn('[DeptSelect] 加载部门数据失败:', error);
deptTree.value = [];
}
}
// 根据 ID 获取部门名称
function getDeptNameById(id: number): string | undefined {
const dept = deptList.value.find((item) => item.id === id);
if (!dept) {
console.warn(`[DeptSelect] 未找到 ID 为 ${id} 的部门`);
}
return dept?.name;
}
// 根据名称获取部门 ID
function getDeptIdByName(name: string): number | undefined {
const dept = deptList.value.find((item) => item.name === name);
return dept?.id;
}
// 处理选中值变化
function handleChange(value: number | number[] | undefined): void {
if (value === undefined || value === null) {
emit('update:modelValue', props.multiple ? [] : undefined);
return;
}
// 根据 returnType 决定返回值类型
if (props.returnType === 'name') {
if (props.multiple && Array.isArray(value)) {
const names = value
.map((id) => getDeptNameById(id))
.filter(Boolean) as string[];
emit('update:modelValue', names);
} else if (!props.multiple && typeof value === 'number') {
const name = getDeptNameById(value);
emit('update:modelValue', name);
}
} else {
emit('update:modelValue', value);
}
}
// 树节点过滤方法(支持搜索过滤)
function filterNode(value: string, data: any): boolean {
if (!value) return true;
return data.name?.toLowerCase().includes(value.toLowerCase());
}
// 同步 modelValue 到内部选中值
function syncSelectedValue(): void {
const newValue = props.modelValue;
if (newValue === undefined || newValue === null) {
selectedValue.value = props.multiple ? [] : undefined;
return;
}
// 如果 returnType 是 'name',需要将名称转换为 ID 用于树选择器显示
if (props.returnType === 'name') {
// 只有在 deptList 加载完成后才能进行转换
if (deptList.value.length === 0) {
return;
}
if (props.multiple && Array.isArray(newValue)) {
const ids = (newValue as string[])
.map((name) => getDeptIdByName(name))
.filter(Boolean) as number[];
selectedValue.value = ids;
} else if (!props.multiple && typeof newValue === 'string') {
const id = getDeptIdByName(newValue);
selectedValue.value = id;
}
} else {
selectedValue.value = newValue as number | number[];
}
}
// 监听 modelValue 变化,同步到内部选中值
watch(() => props.modelValue, syncSelectedValue, { immediate: true });
// 监听 deptList 变化,重新同步选中值(解决数据加载完成后的回显问题)
watch(() => deptList.value, syncSelectedValue);
// 检查是否有有效的预设值
function hasValidPresetValue(): boolean {
const value = props.modelValue;
if (value === undefined || value === null || value === '') {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
// 设置默认值(当前用户部门)
function setDefaultValue(): void {
// 仅当 defaultCurrentDept 为 true 时处理
if (!props.defaultCurrentDept) {
return;
}
// 检查是否已有预设值(预设值优先级高于默认当前部门)
if (hasValidPresetValue()) {
return;
}
// 获取当前用户的部门 ID
const userStore = useUserStore();
const deptId = userStore.userInfo?.deptId as number | undefined;
// 处理 deptId 为空或 0 的边界情况
if (!deptId || deptId === 0) {
return;
}
// 根据多选模式决定默认值格式
const defaultValue = props.multiple ? [deptId] : deptId;
emit('update:modelValue', defaultValue);
}
// 组件挂载时加载数据并设置默认值
onMounted(async () => {
await loadDeptTree();
// 数据加载完成后设置默认值
setDefaultValue();
});
</script>
<template>
<ElTreeSelect
v-model="selectedValue"
class="w-full"
:data="deptTree"
:props="treeProps"
:multiple="multiple"
:disabled="disabled"
:placeholder="placeholder || '请选择部门'"
:check-strictly="true"
:filterable="true"
:filter-node-method="filterNode"
:clearable="true"
node-key="id"
@change="handleChange"
/>
</template>

View File

@@ -2,6 +2,7 @@ import type { ApiSelectProps } from '#/components/form-create/typing';
import { defineComponent, onMounted, ref, useAttrs } from 'vue';
import { useUserStore } from '@vben/stores';
import { isEmpty } from '@vben/utils';
import {
@@ -74,12 +75,46 @@ export function useApiSelect(option: ApiSelectProps) {
type: String,
default: 'id',
},
// 是否默认选中当前用户(仅用于 UserSelect
defaultCurrentUser: {
type: Boolean,
default: false,
},
setup(props) {
},
setup(props, { emit }) {
const attrs = useAttrs();
const options = ref<any[]>([]); // 下拉数据
const loading = ref(false); // 是否正在从远程获取数据
const queryParam = ref<any>(); // 当前输入的值
// 检查是否有有效的预设值
function hasValidPresetValue(): boolean {
const value = attrs.modelValue;
if (value === undefined || value === null || value === '') {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
// 设置默认当前用户
function setDefaultCurrentUser(): void {
if (option.name !== 'UserSelect' || !props.defaultCurrentUser) {
return;
}
if (hasValidPresetValue()) {
return;
}
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id;
if (currentUserId) {
const defaultValue = props.multiple ? [currentUserId] : currentUserId;
emit('update:modelValue', defaultValue);
}
}
const getOptions = async () => {
options.value = [];
// 接口选择器
@@ -199,6 +234,8 @@ export function useApiSelect(option: ApiSelectProps) {
onMounted(async () => {
await getOptions();
// 设置默认当前用户(仅用于 UserSelect
setDefaultCurrentUser();
});
const buildSelect = () => {

View File

@@ -189,6 +189,14 @@ export async function useFormCreateDesigner(designer: Ref) {
name: 'UserSelect',
label: '用户选择器',
icon: 'icon-eye',
props: [
{
type: 'switch',
field: 'defaultCurrentUser',
title: '默认选中当前用户',
value: true,
},
],
});
const deptSelectRule = useSelectRule({
name: 'DeptSelect',
@@ -205,6 +213,12 @@ export async function useFormCreateDesigner(designer: Ref) {
{ label: '部门名称', value: 'name' },
],
},
{
type: 'switch',
field: 'defaultCurrentDept',
title: '默认选中当前部门',
value: true,
},
],
});
const dictSelectRule = useDictSelectRule();

View File

@@ -23,13 +23,24 @@ export function useSelectRule(option: SelectRuleOption) {
name,
event: option.event,
rule() {
return {
// 构建基础规则
const baseRule: any = {
type: name,
field: buildUUID(),
title: label,
info: '',
$required: false,
};
// 将自定义 props 的默认值添加到 rule 的 props 中
if (option.props && option.props.length > 0) {
baseRule.props = {};
option.props.forEach((prop: any) => {
if (prop.field && prop.value !== undefined) {
baseRule.props[prop.field] = prop.value;
}
});
}
return baseRule;
},
props(_: any, { t }: any) {
if (!option.props) {

View File

@@ -34,6 +34,7 @@ import {
// ======================= 自定义组件 =======================
import { useApiSelect } from '#/components/form-create';
import DeptSelect from '#/components/form-create/components/dept-select.vue';
import DictSelect from '#/components/form-create/components/dict-select.vue';
import { useImagesUpload } from '#/components/form-create/components/use-images-upload';
import { Tinymce } from '#/components/tinymce';
@@ -45,12 +46,6 @@ const UserSelect = useApiSelect({
valueField: 'id',
url: '/system/user/simple-list',
});
const DeptSelect = useApiSelect({
name: 'DeptSelect',
labelField: 'name',
valueField: 'id',
url: '/system/dept/simple-list',
});
const ApiSelect = useApiSelect({
name: 'ApiSelect',
});

View File

@@ -62,6 +62,7 @@ const tableHeaders = ref<{ label: string; prop: string }[]>([]);
/** 创建空 SKU 数据 */
function createEmptySku(): MallSpuApi.Sku {
return {
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,

View File

@@ -50,6 +50,7 @@ const formData = ref<MallSpuApi.Spu>({
subCommissionType: false,
skus: [
{
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,
@@ -168,8 +169,8 @@ const [OtherForm, otherFormApi] = useVbenForm({
});
/** tab 切换 */
function handleTabChange(key: string) {
activeTabName.value = key;
function handleTabChange(key: number | string) {
activeTabName.value = key as string;
}
/** 提交表单 */
@@ -181,6 +182,11 @@ async function handleSubmit() {
.merge(otherFormApi)
.submitAllForm(true);
values.skus = formData.value.skus;
// 校验商品名称不能为空(用于 SKU name
if (!values.name || values.name.trim() === '') {
ElMessage.error('商品名称不能为空');
return;
}
if (values.skus) {
try {
// 校验 sku
@@ -190,6 +196,8 @@ async function handleSubmit() {
return;
}
values.skus.forEach((item) => {
// 给 sku name 赋值(使用商品名称作为 SKU 名称)
item.name = values.name;
// 金额转换:元转分
item.price = convertToInteger(item.price);
item.marketPrice = convertToInteger(item.marketPrice);
@@ -277,6 +285,7 @@ function handleChangeSpec() {
// 重置 sku 列表
formData.value.skus = [
{
name: '', // SKU 名称,提交时会自动使用 SPU 名称
price: 0,
marketPrice: 0,
costPrice: 0,
@@ -320,7 +329,6 @@ onMounted(async () => {
<ElCard class="h-full w-full" v-loading="formLoading">
<template #header>
<div class="flex items-center justify-between">
<!-- @puhui999idea 这边会有告警 -->
<ElTabs v-model="activeTabName" @tab-change="handleTabChange">
<ElTabPane label="基础设置" name="info" />
<ElTabPane label="价格库存" name="sku" />