feat: 批量新增字典数据
This commit is contained in:
@@ -45,6 +45,11 @@ export function createDictData(data: SystemDictDataApi.DictData) {
|
||||
return requestClient.post('/system/dict-data/create', data);
|
||||
}
|
||||
|
||||
// 批量新增字典数据
|
||||
export function createDictDataList(data: SystemDictDataApi.DictData[]) {
|
||||
return requestClient.post('/system/dict-data/create-list', data);
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export function updateDictData(data: SystemDictDataApi.DictData) {
|
||||
return requestClient.put('/system/dict-data/update', data);
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal as AntModal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
TextArea,
|
||||
} from 'antdv-next';
|
||||
|
||||
import { createDictDataList } from '#/api/system/dict/data';
|
||||
import { getSimpleDictTypeList } from '#/api/system/dict/type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDataFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
type DictDataBatchItem = Omit<
|
||||
SystemDictDataApi.DictData,
|
||||
'createTime' | 'id'
|
||||
> & {
|
||||
key: number;
|
||||
};
|
||||
|
||||
type DictTypeOption = {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '数据标签', dataIndex: 'label', required: true },
|
||||
{ title: '数据键值', dataIndex: 'value', required: true },
|
||||
{ title: '显示排序', dataIndex: 'sort', required: true, width: 120 },
|
||||
{ title: '状态', dataIndex: 'status', required: true, width: 120 },
|
||||
{ title: '颜色类型', dataIndex: 'colorType', width: 140 },
|
||||
{ title: 'CSS Class', dataIndex: 'cssClass', width: 150 },
|
||||
{ title: '备注', dataIndex: 'remark', width: 180 },
|
||||
{ title: '操作', dataIndex: 'action', width: 80 },
|
||||
];
|
||||
|
||||
const dictType = ref<string>();
|
||||
const tableData = ref<DictDataBatchItem[]>([]);
|
||||
const dictTypeOptions = ref<DictTypeOption[]>([]);
|
||||
const rowKey = ref(0);
|
||||
const statusOptions = getDictOptions(DICT_TYPE.COMMON_STATUS, 'number');
|
||||
const colorOptions = useDataFormSchema()
|
||||
.find((item) => item.fieldName === 'colorType')
|
||||
?.componentProps?.options;
|
||||
|
||||
function buildDefaultRow(sort: number): DictDataBatchItem {
|
||||
return {
|
||||
key: rowKey.value++,
|
||||
dictType: dictType.value || '',
|
||||
label: '',
|
||||
value: '',
|
||||
sort,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
colorType: '',
|
||||
cssClass: '',
|
||||
remark: '',
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDictTypeOptions() {
|
||||
if (!isEmpty(dictTypeOptions.value)) {
|
||||
return;
|
||||
}
|
||||
dictTypeOptions.value = await getSimpleDictTypeList();
|
||||
}
|
||||
|
||||
function handleAddRow() {
|
||||
tableData.value.push(buildDefaultRow(tableData.value.length + 1));
|
||||
}
|
||||
|
||||
function handleDeleteRow(index: number) {
|
||||
tableData.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 快速粘贴相关 */
|
||||
const pasteModalVisible = ref(false);
|
||||
const pasteText = ref('');
|
||||
|
||||
function handleOpenPasteModal() {
|
||||
pasteText.value = '';
|
||||
pasteModalVisible.value = true;
|
||||
}
|
||||
|
||||
function handlePasteConfirm() {
|
||||
const rawText = pasteText.value.trim();
|
||||
if (!rawText) {
|
||||
message.warning('请输入内容');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = rawText.split('\n').filter((line) => line.trim() !== '');
|
||||
if (lines.length === 0) {
|
||||
message.warning('未识别到有效数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const newRows: DictDataBatchItem[] = [];
|
||||
|
||||
for (const [index, line] of lines.entries()) {
|
||||
const trimmedLine = line.trim();
|
||||
const rowNumber = index + 1;
|
||||
|
||||
// 检查是否包含非法分隔符(只允许空格)
|
||||
if (/[\t,;,;||]/.test(trimmedLine)) {
|
||||
message.error(`第 ${rowNumber} 行解析失败:只能使用空格作为分隔符`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用空格分割:第一部分为值,剩余部分为标签
|
||||
const parts = trimmedLine.split(/\s+/);
|
||||
if (parts.length < 2) {
|
||||
message.error(
|
||||
`第 ${rowNumber} 行解析失败:格式不正确,需要"值 标签"(空格分隔)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = parts[0];
|
||||
const label = parts.slice(1).join(' ');
|
||||
|
||||
newRows.push({
|
||||
key: rowKey.value++,
|
||||
dictType: dictType.value || '',
|
||||
label,
|
||||
value,
|
||||
sort: tableData.value.length + newRows.length,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
colorType: '',
|
||||
cssClass: '',
|
||||
remark: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (newRows.length === 0) {
|
||||
message.warning('未识别到有效数据');
|
||||
return;
|
||||
}
|
||||
|
||||
tableData.value = newRows;
|
||||
pasteModalVisible.value = false;
|
||||
pasteText.value = '';
|
||||
message.success(`成功识别 ${newRows.length} 条数据`);
|
||||
}
|
||||
|
||||
function handlePasteCancel() {
|
||||
pasteModalVisible.value = false;
|
||||
pasteText.value = '';
|
||||
}
|
||||
|
||||
function resetRows() {
|
||||
rowKey.value = 0;
|
||||
tableData.value = [buildDefaultRow(1), buildDefaultRow(2), buildDefaultRow(3)];
|
||||
}
|
||||
|
||||
function validateRows() {
|
||||
if (!dictType.value) {
|
||||
message.warning('请选择字典类型');
|
||||
return false;
|
||||
}
|
||||
if (isEmpty(tableData.value)) {
|
||||
message.warning('请至少添加一条字典数据');
|
||||
return false;
|
||||
}
|
||||
const valueSet = new Set<string>();
|
||||
for (const [index, row] of tableData.value.entries()) {
|
||||
const rowNumber = index + 1;
|
||||
if (!row.label) {
|
||||
message.warning(`第 ${rowNumber} 行数据标签不能为空`);
|
||||
return false;
|
||||
}
|
||||
if (!row.value) {
|
||||
message.warning(`第 ${rowNumber} 行数据键值不能为空`);
|
||||
return false;
|
||||
}
|
||||
if (row.sort === undefined || row.sort === null) {
|
||||
message.warning(`第 ${rowNumber} 行显示排序不能为空`);
|
||||
return false;
|
||||
}
|
||||
if (row.status === undefined || row.status === null) {
|
||||
message.warning(`第 ${rowNumber} 行状态不能为空`);
|
||||
return false;
|
||||
}
|
||||
if (valueSet.has(row.value)) {
|
||||
message.warning(`第 ${rowNumber} 行数据键值重复`);
|
||||
return false;
|
||||
}
|
||||
valueSet.add(row.value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!validateRows()) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const submitData = tableData.value.map((item) => ({
|
||||
dictType: dictType.value,
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
sort: item.sort,
|
||||
status: item.status,
|
||||
colorType: item.colorType,
|
||||
cssClass: item.cssClass,
|
||||
remark: item.remark,
|
||||
})) as SystemDictDataApi.DictData[];
|
||||
await createDictDataList(submitData);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
dictType.value = undefined;
|
||||
tableData.value = [];
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ dictType?: string }>();
|
||||
dictType.value = data?.dictType;
|
||||
await loadDictTypeOptions();
|
||||
resetRows();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/3" title="批量新增字典数据">
|
||||
<div class="mx-4 space-y-4">
|
||||
<Form layout="inline">
|
||||
<FormItem label="字典类型" required>
|
||||
<Select
|
||||
v-model:value="dictType"
|
||||
allow-clear
|
||||
class="w-64"
|
||||
:field-names="{ label: 'name', value: 'type' }"
|
||||
:options="dictTypeOptions"
|
||||
placeholder="请选择字典类型"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleAddRow">新增一行</Button>
|
||||
<Button
|
||||
shape="circle"
|
||||
title="快速粘贴"
|
||||
@click="handleOpenPasteModal"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="ant-design:snippets-outlined"
|
||||
class="size-4"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</Space>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
bordered
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="false"
|
||||
row-key="key"
|
||||
size="small"
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<span v-if="column.required" class="text-red-500">*</span>
|
||||
<span>{{ column.title }}</span>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<Input
|
||||
v-if="column.dataIndex === 'label'"
|
||||
v-model:value="record.label"
|
||||
placeholder="请输入数据标签"
|
||||
/>
|
||||
<Input
|
||||
v-else-if="column.dataIndex === 'value'"
|
||||
v-model:value="record.value"
|
||||
placeholder="请输入数据键值"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else-if="column.dataIndex === 'sort'"
|
||||
v-model:value="record.sort"
|
||||
class="!w-full"
|
||||
placeholder="排序"
|
||||
/>
|
||||
<Select
|
||||
v-else-if="column.dataIndex === 'status'"
|
||||
v-model:value="record.status"
|
||||
class="w-full"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<Select
|
||||
v-else-if="column.dataIndex === 'colorType'"
|
||||
v-model:value="record.colorType"
|
||||
allow-clear
|
||||
class="w-full"
|
||||
:options="colorOptions"
|
||||
placeholder="颜色类型"
|
||||
/>
|
||||
<Input
|
||||
v-else-if="column.dataIndex === 'cssClass'"
|
||||
v-model:value="record.cssClass"
|
||||
placeholder="CSS Class"
|
||||
/>
|
||||
<Input
|
||||
v-else-if="column.dataIndex === 'remark'"
|
||||
v-model:value="record.remark"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="column.dataIndex === 'action'"
|
||||
danger
|
||||
type="link"
|
||||
@click="handleDeleteRow(index)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 快速粘贴弹窗 -->
|
||||
<AntModal
|
||||
v-model:open="pasteModalVisible"
|
||||
:destroy-on-close="true"
|
||||
ok-text="确认添加"
|
||||
title="快速粘贴"
|
||||
@ok="handlePasteConfirm"
|
||||
@cancel="handlePasteCancel"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="text-gray-500">
|
||||
每行一条数据,格式:<span class="font-medium text-black">值 标签</span>(空格分隔)
|
||||
</div>
|
||||
<TextArea
|
||||
v-model:value="pasteText"
|
||||
:auto-size="{ minRows: 8, maxRows: 16 }"
|
||||
placeholder="请粘贴数据,例如: 010 生成 040 待下发 045 下发中 050 已下发 060 执行中 061 已取货搬运中"
|
||||
/>
|
||||
<div class="text-xs text-gray-400">
|
||||
只允许使用空格作为分隔符,不支持 Tab、逗号、分号等其他分隔符
|
||||
</div>
|
||||
</div>
|
||||
</AntModal>
|
||||
</template>
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDataGridColumns, useDataGridFormSchema } from '../data';
|
||||
import DataBatchForm from './data-batch-form.vue';
|
||||
import DataForm from './data-form.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,6 +34,11 @@ const [DataFormModal, dataFormModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DataBatchFormModal, dataBatchFormModalApi] = useVbenModal({
|
||||
connectedComponent: DataBatchForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
@@ -49,6 +55,11 @@ function handleCreate() {
|
||||
dataFormModalApi.setData({ dictType: props.dictType }).open();
|
||||
}
|
||||
|
||||
/** 批量创建字典数据 */
|
||||
function handleCreateBatch() {
|
||||
dataBatchFormModalApi.setData({ dictType: props.dictType }).open();
|
||||
}
|
||||
|
||||
/** 编辑字典数据 */
|
||||
function handleEdit(row: SystemDictDataApi.DictData) {
|
||||
dataFormModalApi.setData(row).open();
|
||||
@@ -144,18 +155,26 @@ watch(
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<DataFormModal @success="handleRefresh" />
|
||||
<DataBatchFormModal @success="handleRefresh" />
|
||||
|
||||
<Grid table-title="字典数据列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['字典数据']),
|
||||
label: $t('ui.actionTitle.create', ['数据']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['system:dict:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: '批量新增',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['system:dict:create'],
|
||||
onClick: handleCreateBatch,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
|
||||
Reference in New Issue
Block a user