feat:字典详情

This commit is contained in:
rashstarfish 2025-03-12 20:49:17 +08:00
parent 89709d8058
commit 1cf9024154
26 changed files with 1236 additions and 896 deletions

View File

@ -291,3 +291,282 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}
/* 由于后端响应主键为不是id, 而前端框架操作主键为id, 所以需要在此处把值传给id */
export function useCustomPKTable<A extends NaiveUI.TableApiFn>(pkName: string, config: NaiveUI.NaiveTableConfig<A>) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const { apiFn, apiParams, immediate } = config;
const showTotal = config.showTotal || true;
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams: resetSearchParams0
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
searchParams: config.searchParams,
columns: config.columns,
transformer: res => {
const { data: records = [], page: current = 1, size = 10, total = 0 } = res.data || {};
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
const pageSize = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => {
return {
...item,
// 这里把pkName赋值给id,用于table监听选框
id: item[pkName],
index: (current - 1) * pageSize + index + 1
};
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title as string,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
if (isMobile.value && column.fixed) {
column.fixed = undefined;
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate
});
const pagination: PaginationProps = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30],
onUpdatePage: async (page: number) => {
pagination.page = page;
updateSearchParams({
page,
size: pagination.pageSize!
});
getData();
},
onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
updateSearchParams({
page: pagination.page,
size: pageSize
});
getData();
},
...(showTotal
? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
};
return p;
});
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
/**
* get data by page number
*
* @param pageNum the page number. default is 1
*/
async function getDataByPage(pageNum: number = 1) {
updatePagination({
page: pageNum
});
updateSearchParams({
page: pageNum,
size: pagination.pageSize!
});
await getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
const resetSearchParams = () => {
resetSearchParams0();
getData();
};
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
};
}
export function useCustomPKTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
openDrawer();
}
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
function handleEdit(pkVal: any) {
operateType.value = 'edit';
const findItem = data.value.find(item => item.id === pkVal) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
function handleCopy(pkVal: any) {
operateType.value = 'add';
const findItem = data.value.find(item => item.id === pkVal) || null;
editingData.value = jsonClone(findItem);
delete editingData.value?.id;
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = ref<number[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
window.$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
}
/** the hook after the delete operation is completed */
async function onDeleted() {
window.$message?.success($t('common.deleteSuccess'));
await getData();
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
handleCopy,
checkedRowKeys,
onBatchDeleted,
onDeleted
};
}

View File

@ -351,7 +351,9 @@ const local: App.I18n.Schema = {
workflow_form_add: '新增工作流',
job: '数采任务',
job_task: '任务管理',
job_batch: '执行批次'
job_batch: '执行批次',
dictionary_type: '字典管理',
dictionary_data: '字典数据'
},
page: {
common: {
@ -506,15 +508,16 @@ const local: App.I18n.Schema = {
addCategory: '新增分类',
editCategory: '编辑分类'
},
dictionary: {
title:'字典',
dictionId:'字典编号',
dictionaryType: {
title: '字典管理',
detail: '字典详情',
dictionaryId: '字典编号',
dictionaryName: '字典名称',
dictionaryType: '字典类型',
dictionaryStatus: '状态',
remark:'备注',
remark: '备注',
createTime: '创建时间',
form:{
form: {
dictionaryNamePlaceHolder: '请输入字典名称',
dictionaryName: '字典名称',
dictionaryTypePlaceHolder: '请输入字典名称',
@ -525,7 +528,22 @@ const local: App.I18n.Schema = {
},
addDictionary: '新增字典',
editDictionary: '编辑字典'
},
dictionaryData: {
title: '字典数据',
detail: '字典数据详情',
dictionaryCode: '字典编码',
dictionaryLabel: '字典标签',
dictionaryValue: '字典键值',
createTime: '创建时间',
dictionaryType: '所属类型',
form: {
dictLabel: '字典标签',
dictValue: '字典键值',
dictType: '所属类型'
},
addDictionary: '新增字典条目',
editDictionary: '编辑字典条目'
},
namespace: {
title: '命名空间',

View File

@ -23,7 +23,8 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
login: () => import("@/views/_builtin/login/index.vue"),
about: () => import("@/views/about/index.vue"),
category: () => import("@/views/category/index.vue"),
dictionary: () => import("@/views/dictionary/index.vue"),
dictionary_data: () => import("@/views/dictionary/data/index.vue"),
dictionary_type: () => import("@/views/dictionary/type/index.vue"),
group: () => import("@/views/group/index.vue"),
home: () => import("@/views/home/index.vue"),
job_batch: () => import("@/views/job/batch/index.vue"),

View File

@ -62,13 +62,36 @@ export const generatedRoutes: GeneratedRoute[] = [
{
name: 'dictionary',
path: '/dictionary',
component: 'layout.base$view.dictionary',
component: 'layout.base',
meta: {
title: 'dictionary',
i18nKey: 'route.dictionary',
icon: 'material-symbols:dictionary-outline',
order: 14
}
order: 14,
keepAlive: false
},
children: [
{
name: 'dictionary_data',
path: '/dictionary/data',
component: 'view.dictionary_data',
meta: {
title: 'dictionary_data',
i18nKey: 'route.dictionary_data',
icon: 'carbon:batch-job'
}
},
{
name: 'dictionary_type',
path: '/dictionary/type',
component: 'view.dictionary_type',
meta: {
title: 'dictionary_type',
i18nKey: 'route.dictionary_type',
icon: 'octicon:tasklist'
}
}
]
},
{
name: 'group',

View File

@ -0,0 +1,60 @@
import { request } from '../request';
const prefix = '/dict/data';
/** get dictType list */
export function fetchGetDictionaryDataList(params?: Api.DictionaryData.DictionaryDataSearchParams) {
return request<Api.DictionaryData.DictionaryDataList>({
url: `${prefix}/list`,
method: 'get',
params
});
}
export function fetchGetDictionaryDataByCode(dictCode: number) {
return request<Api.DictionaryData.DictionaryData>({
url: `${prefix}/${dictCode}`,
method: 'get'
});
}
export function fetchGetDictionaryDataListByDictType(dictType: string) {
return request<Api.DictionaryData.DictionaryDataList>({
url: `${prefix}/type/${dictType}`,
method: 'get'
});
}
/** add dictType */
export function fetchAddDictionaryData(data: Api.DictionaryData.DictionaryDataRequestVO) {
return request<boolean>({
url: prefix,
method: 'post',
data
});
}
/** edit dictType */
export function fetchEditDictionaryData(data: Api.DictionaryData.DictionaryDataRequestVO) {
return request<boolean>({
url: prefix,
method: 'put',
data
});
}
/** delete dictType by id */
export function fetchDeleteDictionaryData(dictCode: number) {
return request<boolean>({
url: `${prefix}/${dictCode}`,
method: 'delete'
});
}
/** batch delete dictType by id */
export function fetchBatchDeleteDictionaryData(dictCodes: number[]) {
return request<boolean>({
url: `${prefix}/${dictCodes}`,
method: 'delete'
});
}

View File

@ -0,0 +1,46 @@
import { request } from '../request';
const prefix = '/dict/type';
/** get dictType list */
export function fetchGetDictionaryTypeList(params?: Api.DictionaryType.DictionaryTypeSearchParams) {
return request<Api.DictionaryType.DictionaryTypeList>({
url: `${prefix}/list`,
method: 'get',
params
});
}
/** add dictType */
export function fetchAddDictionaryType(data: Api.DictionaryType.DictionaryTypeRequestVO) {
return request<boolean>({
url: prefix,
method: 'post',
data
});
}
/** edit dictType */
export function fetchEditDictionaryType(data: Api.DictionaryType.DictionaryTypeRequestVO) {
return request<boolean>({
url: prefix,
method: 'put',
data
});
}
/** delete dictType by id */
export function fetchDeleteDictionaryType(dictId: number) {
return request<boolean>({
url: `${prefix}/${dictId}`,
method: 'delete'
});
}
/** batch delete dictType by id */
export function fetchBatchDeleteDictionaryType(ids: number[]) {
return request<boolean>({
url: `${prefix}/${ids}`,
method: 'delete'
});
}

View File

@ -1,69 +0,0 @@
import { request } from '../request';
/** get dictConfig list */
export function fetchGetDictionaryConfigList(params?: Api.DictionaryConfig.DictionaryConfigSearchParams) {
return request<Api.DictionaryConfig.DictionaryConfigList>({
url: '/dict/list',
method: 'get',
params
});
}
export function fetchGetAllDictionaryNameList(params?: Api.DictionaryConfig.DictionaryConfigSearchParams) {
return request<string[]>({
url: '/dict/all/dict-name/list',
method: 'get',
params
});
}
/** add dictConfig */
export function fetchAddDictionaryConfig(data: Api.DictionaryConfig.DictionaryConfigRequestVO) {
return request<boolean>({
url: '/dict',
method: 'post',
data
});
}
/** edit dictConfig */
export function fetchEditDictionaryConfig(data: Api.DictionaryConfig.DictionaryConfigRequestVO) {
return request<boolean>({
url: '/dict',
method: 'put',
data
});
}
export function fetchUpdateDictionaryStatus(data: Api.DictionaryConfig.DictionaryConfigRequestVO) {
return request<boolean>({
url: '/dict/status',
method: 'put',
data
});
}
/** get partition table list */
// export function fetchGetPartitionTableList() {
// return request<number[]>({
// url: '/dict/partition-table/list',
// method: 'get'
// });
// }
/** get all dict config list */
export function fetchGetAllDictionaryConfigList(data: string[]) {
return request<Api.DictionaryConfig.DictionaryConfig[]>({
url: '/dict/all/dict-config/list',
method: 'post',
data
});
}
/** delete dict by id */
export function fetchDeleteDictionary(dictName: string) {
return request<boolean>({
url: `/dict/${dictName}`,
method: 'delete'
});
}

View File

@ -14,4 +14,5 @@ export * from './job';
export * from './job-batch';
export * from './user';
export * from './category';
export * from './dictionary';
export * from './dictionary-type';
export * from './dictionary-data';

93
src/typings/api.d.ts vendored
View File

@ -1394,30 +1394,75 @@ declare namespace Api {
data: RowData[];
}
}
namespace DictionaryConfig {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
type DictionaryConfig = Common.CommonRecord<{
dictName: string;
dictType: string;
dictStatus: string;
createTime: string;
}>;
type DictionaryConfigSearchParams = CommonType.RecordNullable<
Pick<Api.DictionaryConfig.DictionaryConfig, 'dictName' | 'dictType' | 'dictStatus' | 'createTime'> &
CommonSearchParams
>;
type DictionaryConfigList = {
dictId: number;
dictName: string;
dictType: string;
dictStatus: string;
createTime: string;
};
type DictionaryConfigRequestVO = {
dictName: string;
dictType: string;
dictStatus: string;
createTime: string;
namespace Common {
type DateRangeParams = {
datetimeRange?: [string, string];
};
}
// 字典类型
namespace DictionaryType {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
// 此处可以自定义类型,供对象属性使用
// type DictStatus = 1 | 2 | 3;
// 包含dict对象的所有属性
type DictionaryType = Common.CommonRecord<{
dictId?: number;
dictName: string;
dictType: string;
createTime: string;
updateTime: string;
}>;
// 搜索参数使用Pick从DictionaryType中取出需要的属性RecordNullable将其设置为可选
type DictionaryTypeSearchParams = CommonType.RecordNullable<
Pick<Api.DictionaryType.DictionaryType, 'dictName' | 'dictType'> & CommonSearchParams
>;
// 请求响应对象
type DictionaryTypeList = Common.PaginatingQueryRecord<DictionaryType>;
// 请求(新增/修改)参数对象
type DictionaryTypeRequestVO = {
dictId?: number;
dictName?: string;
dictType?: string;
};
// 导出json参数对象
type ExportDictionaryType = CommonType.RecordNullable<
Pick<Api.DictionaryType.DictionaryType, 'dictName' | 'dictType'> & CommonSearchParams
> & { dictIds: number[] };
}
// 字典类型详细数据
namespace DictionaryData {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
// 此处可以自定义类型,供对象属性使用
// type DictStatus = 1 | 2 | 3;
// 包含dict对象的所有属性
type DictionaryData = Common.CommonRecord<{
dictCode: number;
dictLabel: string;
dictValue: string;
dictType: string;
createTime: string;
updateTime: string;
}>;
// 搜索参数使用Pick从DictionaryData中取出需要的属性RecordNullable将其设置为可选
type DictionaryDataSearchParams = CommonType.RecordNullable<
Pick<Api.DictionaryData.DictionaryData, 'dictLabel' | 'dictType' | 'dictValue'> &
CommonSearchParams &
Common.DateRangeParams
>;
// 请求响应对象
type DictionaryDataList = Common.PaginatingQueryRecord<DictionaryData>;
// 请求(新增/修改)参数对象
type DictionaryDataRequestVO = {
dictCode?: number;
dictLabel?: string;
dictValue?: string;
dictType?: string;
};
// 导出json参数对象
type ExportDictionaryData = CommonType.RecordNullable<
Pick<Api.DictionaryData.DictionaryData, 'dictLabel' | 'dictType' | 'dictValue'> & CommonSearchParams
> & { dictCodes: number[] };
}
}

45
src/typings/app.d.ts vendored
View File

@ -678,14 +678,51 @@ declare namespace App {
categoryParentName: string;
categoryType: string;
createTime: string;
},
};
type: {
website: string;
literature: string;
},
};
addCategory: string;
editCategory: string;
},
};
dictionaryType: {
title: string;
detail: string;
dictionaryId: string;
dictionaryName: string;
dictionaryType: string;
dictionaryStatus: string;
remark: string;
createTime: string;
form: {
dictionaryNamePlaceHolder: string;
dictionaryName: string;
dictionaryTypePlaceHolder: string;
dictionaryType: string;
dictionaryStatusPlaceHolder: string;
dictionaryStatus: string;
createTime: string;
};
addDictionary: string;
editDictionary: string;
};
dictionaryData: {
title: string;
detail: string;
dictionaryCode: string;
dictionaryLabel: string;
dictionaryValue: string;
createTime: string;
dictionaryType: string;
form: {
dictLabel: string;
dictValue: string;
dictType: string;
};
addDictionary: string;
editDictionary: string;
};
pods: {
title: string;
nodeType: string;
@ -752,7 +789,7 @@ declare namespace App {
government: string;
usMilitary: string;
usNavy: string;
},
};
idMode: {
idWorker: string;
segment: string;

70
src/utils/dict.ts Normal file
View File

@ -0,0 +1,70 @@
import { fetchGetDictionaryDataList, fetchGetDictionaryTypeList } from '@/service/api';
// export dict
// {
// type:{
// sys_sex:[
// {label:"男",
// value: "1"},
// {label:"女",
// value: "2"}
// ]
// }
// }
type indexAbleDictData = {
label: string;
value: string;
};
type indexAbleDictType = {
[key: string]: indexAbleDictData[];
};
type indexAbleDict = {
type: indexAbleDictType;
};
async function init() {
// const dictTypeList = reactive<Api.DictionaryType.DictionaryType[]>([]);
// const dict: indexAbleDict = {
// type: {
// fruits: [
// { label: '苹果', value: 'apple' },
// { label: '香蕉', value: 'banana' },
// { label: '橙子', value: 'orange' }
// ],
// colors: [
// { label: '红色', value: 'red' },
// { label: '绿色', value: 'green' },
// { label: '蓝色', value: 'blue' }
// ],
// animals: [
// { label: '狗', value: 'dog' },
// { label: '猫', value: 'cat' },
// { label: '鸟', value: 'bird' }
// ]
// }
// };
const typeList: string[] = (await fetchGetDictionaryTypeList()).data?.data.map(item => item.dictType) || [];
const dataList: any[] =
(await fetchGetDictionaryDataList()).data?.data.map(item => ({
dictType: item.dictType,
dictLabel: item.dictLabel,
dictValue: item.dictValue
})) || [];
const dict: indexAbleDict = {
type: Array.from(typeList).reduce((acc, dictType) => {
// 筛选出当前 dictType 对应的数据
const items = dataList.filter(item => item.dictType === dictType);
// 将数据转换为 indexAbleDictData 类型
acc[dictType] = items.map(item => ({
label: item.dictLabel,
value: item.dictValue
}));
return acc;
}, {} as indexAbleDictType)
};
return dict;
}
const dict = init();
export default dict;

View File

@ -1,11 +1,11 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { NButton, NDivider, NPopconfirm } from 'naive-ui';
import { ref } from 'vue';
import { useBoolean } from '@sa/hooks';
import { fetchBatchDeleteDictionary, fetchDeleteDictionary, fetchGetDictionaryTypeList } from '@/service/api';
import { fetchBatchDeleteDictionaryData, fetchDeleteDictionaryData, fetchGetDictionaryDataList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useDictTable, useDictTableOperate } from '@/hooks/common/table';
import { useCustomPKTable, useCustomPKTableOperate } from '@/hooks/common/table';
import { useAuth } from '@/hooks/business/auth';
// import type { Api } from '@/typings/api';
import { downloadFetch } from '@/utils/download';
@ -16,18 +16,19 @@ const { hasAuth } = useAuth();
const appStore = useAppStore();
/** 详情页属性数据 */
const detailData = ref<Api.DictionaryType.DictionaryType | null>();
const detailData = ref<Api.DictionaryData.DictionaryData | null>();
/** 详情页可见状态 */
const { bool: detailVisible, setTrue: openDetail } = useBoolean(false);
const dictType = history.state.dictType || '';
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } =
useDictTable({
apiFn: fetchGetDictionaryTypeList,
useCustomPKTable('dictCode', {
apiFn: fetchGetDictionaryDataList,
apiParams: {
page: 1,
size: 10,
dictName: '',
dictType: null
dictLabel: '',
dictType
},
columns: () => [
{
@ -36,14 +37,14 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
width: 48
},
{
key: 'dictId',
title: $t('page.dictionaryType.dictionaryId'),
key: 'dictCode',
title: $t('page.dictionaryData.dictionaryCode'),
align: 'center',
width: 64
},
{
key: 'dictName',
title: $t('page.dictionaryType.dictionaryName' as App.I18n.I18nKey),
key: 'dictLabel',
title: $t('page.dictionaryData.dictionaryLabel' as App.I18n.I18nKey),
align: 'left',
width: 200,
render: row => {
@ -54,14 +55,26 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
return (
<n-button text tag="a" type="primary" onClick={showDetailDrawer} class="ws-normal">
{row.dictName}
{row.dictLabel}
</n-button>
);
}
},
{
key: 'dictValue',
title: $t('page.dictionaryData.dictionaryValue'),
align: 'left',
width: 200,
render: row => {
if (row.dictType === null) {
return null;
}
return <p> {row.dictValue} </p>;
}
},
{
key: 'dictType',
title: $t('page.dictionaryType.dictionaryType'),
title: $t('page.dictionaryData.dictionaryType'),
align: 'left',
width: 200,
render: row => {
@ -82,11 +95,11 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
}
return (
<div class="flex-center gap-8px">
<NButton type="primary" text ghost size="small" onClick={() => edit(row.dictId!)}>
<NButton type="primary" text ghost size="small" onClick={() => edit(row.dictCode!)}>
{$t('common.edit')}
</NButton>
<n-divider vertical />
<NPopconfirm onPositiveClick={() => handleDelete(row.dictId!)}>
<NDivider vertical />
<NPopconfirm onPositiveClick={() => handleDelete(row.dictCode!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
@ -113,34 +126,34 @@ const {
onDeleted,
onBatchDeleted
// closeDrawer
} = useDictTableOperate(data, getData);
} = useCustomPKTableOperate(data, getData);
function edit(dictId: number) {
handleEdit(dictId);
}
async function handleBatchDelete() {
const { error } = await fetchBatchDeleteDictionary(checkedRowKeys.value);
const { error } = await fetchBatchDeleteDictionaryData(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(dictId: number) {
const { error } = await fetchDeleteDictionary(dictId);
const { error } = await fetchDeleteDictionaryData(dictId);
if (error) return;
onDeleted();
}
function body(): Api.DictionaryType.ExportDictionaryType {
function body(): Api.DictionaryData.ExportDictionaryData {
return {
dictIds: checkedRowKeys.value,
dictName: searchParams.dictName,
dictCodes: checkedRowKeys.value,
dictLabel: searchParams.dictLabel,
dictType: searchParams.dictType
};
}
function handleExport() {
downloadFetch('/dict/type/export', body(), $t('page.dictionaryType.title'));
downloadFetch('/dict/type/export', body(), $t('page.dictionaryData.title'));
}
</script>
@ -149,7 +162,7 @@ function handleExport() {
<DictionarySearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<DeleteAlert />
<NCard
:title="$t('page.dictionaryType.title')"
:title="$t('page.dictionaryData.title')"
:bordered="false"
size="small"
header-class="view-card-header"
@ -202,6 +215,7 @@ function handleExport() {
/>
<DictionaryOperateDrawer
v-model:visible="drawerVisible"
v-model:search-params="searchParams"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"

View File

@ -7,7 +7,7 @@ defineOptions({
interface Props {
/** row data */
rowData?: Api.DictionaryType.DictionaryType | null;
rowData?: Api.DictionaryData.DictionaryData | null;
}
defineProps<Props>();
@ -18,22 +18,19 @@ const visible = defineModel<boolean>('visible', {
</script>
<template>
<OperateDrawer v-model="visible" :title="$t('page.dictionaryType.detail')">
<OperateDrawer v-model="visible" :title="$t('page.dictionaryData.detail')">
<NDescriptions label-placement="top" bordered :column="2">
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryId')" :span="2">
{{ rowData?.dictId }}
<NDescriptionsItem :label="$t('page.dictionaryData.dictionaryCode')" :span="2">
{{ rowData?.dictCode }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryName')" :span="2">
{{ rowData?.dictName }}
<NDescriptionsItem :label="$t('page.dictionaryData.dictionaryLabel')" :span="2">
{{ rowData?.dictLabel }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryType')" :span="2">
<NDescriptionsItem :label="$t('page.dictionaryData.dictionaryType')" :span="2">
{{ rowData?.dictType }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.createTime')" :span="2">
{{ rowData?.createTime }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.remark')" :span="2">
<NDescriptionsItem :label="$t('page.dictionaryData.createTime')" :span="2">
{{ rowData?.createTime }}
</NDescriptionsItem>
</NDescriptions>

View File

@ -3,22 +3,24 @@ import { computed, reactive, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
// import {
// dictionaryTypeIdModeOptions,
// dictionaryTypeStatusOptions,
// dictionaryTypeTypeOptions,
// dictionaryTypeYesOrNoOptions
// dictionaryDataIdModeOptions,
// dictionaryDataStatusOptions,
// dictionaryDataTypeOptions,
// dictionaryDataYesOrNoOptions
// } from '@/constants/business';
import { fetchAddDictionaryType, fetchEditDictionaryType } from '@/service/api/dictionary';
import { fetchAddDictionaryData, fetchEditDictionaryData } from '@/service/api/dictionary-data';
defineOptions({
name: 'DictionaryOperateDrawer'
});
const searchParams = defineModel<Api.DictionaryType.DictionaryTypeSearchParams>('searchParams', { required: true });
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.DictionaryType.DictionaryType | null;
rowData?: Api.DictionaryData.DictionaryData | null;
}
const props = defineProps<Props>();
@ -38,27 +40,29 @@ const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.dictionaryType.addDictionary'),
edit: $t('page.dictionaryType.editDictionary')
add: $t('page.dictionaryData.addDictionary'),
edit: $t('page.dictionaryData.editDictionary')
};
return titles[props.operateType];
});
type Model = Pick<Api.DictionaryType.DictionaryType, 'dictId' | 'dictName' | 'dictType'>;
type Model = Pick<Api.DictionaryData.DictionaryData, 'dictCode' | 'dictLabel' | 'dictValue' | 'dictType'>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
dictId: 0,
dictName: '',
dictType: ''
dictCode: 0,
dictLabel: '',
dictValue: '',
dictType: searchParams.value.dictType || ''
};
}
type RuleKey = Extract<keyof Model, 'dictId' | 'dictName' | 'dictType' | 'dictStatus'>;
type RuleKey = Extract<keyof Model, 'dictCode' | 'dictLabel' | 'dictValue' | 'dictType'>;
const rules = {
dictId: [defaultRequiredRule],
dictName: [defaultRequiredRule],
dictCode: [defaultRequiredRule],
dictLabel: [defaultRequiredRule],
dictValue: [defaultRequiredRule],
dictType: [defaultRequiredRule]
} satisfies Record<RuleKey, App.Global.FormRule[]>;
@ -81,18 +85,21 @@ async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { dictName, dictType } = model;
const { error } = await fetchAddDictionaryType({
dictName,
const { dictCode, dictLabel, dictValue, dictType } = model;
const { error } = await fetchAddDictionaryData({
dictCode,
dictLabel,
dictValue,
dictType
});
if (error) return;
window.$message?.success($t('common.addSuccess'));
} else {
const { dictId, dictName, dictType } = model;
const { error } = await fetchEditDictionaryType({
dictId,
dictName,
const { dictCode, dictLabel, dictValue, dictType } = model;
const { error } = await fetchEditDictionaryData({
dictCode,
dictLabel,
dictValue,
dictType
});
if (error) return;
@ -116,21 +123,30 @@ watch(visible, () => {
<OperateDrawer v-model="visible" :title="title" @submitted="handleSubmit">
<NForm ref="formRef" :model="model" :rules="rules">
<NCollapse :default-expanded-names="['1', '2']">
<NFormItem :label="$t('page.dictionaryType.dictionaryName')" path="dictName">
<NInput
v-model:value="model.dictName"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryType.form.dictionaryName')"
/>
</NFormItem>
<NFormItem :label="$t('page.dictionaryType.dictionaryType')" path="dictType">
<NFormItem :label="$t('page.dictionaryData.dictionaryType')" path="dictType">
<NInput
v-model:value="model.dictType"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryType.form.dictionaryType')"
:placeholder="$t('page.dictionaryData.dictionaryType')"
:disabled="true"
/>
</NFormItem>
<NFormItem :label="$t('page.dictionaryData.dictionaryLabel')" path="dictLabel">
<NInput
v-model:value="model.dictLabel"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryData.dictionaryLabel')"
/>
</NFormItem>
<NFormItem :label="$t('page.dictionaryData.dictionaryValue')" path="dictValue">
<NInput
v-model:value="model.dictValue"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryData.dictionaryValue')"
/>
</NFormItem>
</NCollapse>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, reactive } from 'vue';
import { NButton, NCard, NForm, NFormItemGi, NGrid, NInput, NSpace } from 'naive-ui';
import { fetchGetDictionaryTypeList } from '@/service/api/dictionary-type';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
@ -9,12 +10,31 @@ const model = defineModel<Api.DictionaryType.DictionaryTypeSearchParams>('model'
const btnSpan = computed(() => (appStore.isMobile ? '7' : '7'));
const dictTypeList = reactive<Api.DictionaryType.DictionaryType[]>([]);
updateOptions();
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
async function updateOptions() {
const res = await fetchGetDictionaryTypeList();
Object.assign(dictTypeList, res.data?.data);
if (res.data?.data?.length !== 0 && res.data?.data?.[0].dictType !== null) {
model.value.dictType = '';
}
}
function translateOptions(dictTypes: Api.DictionaryType.DictionaryType[]) {
return dictTypes.map(dictType => ({
value: dictType.dictType,
label: dictType.dictName
}));
}
function search() {
emit('search');
}
@ -30,19 +50,17 @@ function reset() {
<NGrid responsive="screen" cols="21" item-responsive :y-gap="12">
<!-- 字典名称 -->
<NFormItemGi span="m:7" :label="$t('page.dictionaryType.form.dictionaryName')" path="dictName" class="pr-0px">
<NInput
v-model:value="model.dictName"
<NSelect
v-model:value="model.dictType"
filterable
class="max-w-180px"
:options="translateOptions(dictTypeList)"
:placeholder="$t('page.dictionaryType.form.dictionaryName')"
clearable
/>
</NFormItemGi>
<!-- 字典类型 -->
<NFormItemGi span="m:7" :label="$t('page.dictionaryType.form.dictionaryType')" path="dictType" class="pr-0px">
<NInput
v-model:value="model.dictType"
:placeholder="$t('page.dictionaryType.form.dictionaryType')"
clearable
/>
<NFormItemGi span="m:7" :label="$t('page.dictionaryData.form.dictLabel')" path="dictType" class="pr-0px">
<NInput v-model:value="model.dictName" :placeholder="$t('page.dictionaryData.form.dictLabel')" clearable />
</NFormItemGi>
<!-- 按钮 -->

View File

@ -1,243 +0,0 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { ref } from 'vue';
import { useBoolean } from '@sa/hooks';
import { fetchDeleteDictionary, fetchGetDictionaryConfigList, fetchUpdateDictionaryStatus } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useAuth } from '@/hooks/business/auth';
import { downloadFetch } from '@/utils/download';
import DictionaryOperateDrawer from './modules/dictionary-operate-drawer.vue';
import DictionaryDetailDrawer from './modules/dictionary-detail-drawer.vue';
import DictionarySearch from './modules/dictionary-search.vue';
const { hasAuth } = useAuth();
const appStore = useAppStore();
/** 详情页属性数据 */
const detailData = ref<Api.DictionaryConfig.DictionaryConfig | null>();
/** 详情页可见状态 */
const { bool: detailVisible, setTrue: openDetail } = useBoolean(false);
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } = useTable({
apiFn: fetchGetDictionaryConfigList,
apiParams: {
page: 1,
size: 10,
dictName: '',
dictStatus: '',
dictType: '',
createTime: ''
},
columns: () => [
{
key: 'dictId',
title: $t('page.dictionary.dictionId'),
align: 'center',
width: 64
},
{
key: 'dictName',
title: $t('page.dictionary.dictionaryName'),
align: 'left',
width: 200,
render: row => {
function showDetailDrawer() {
detailData.value = row || null;
openDetail();
}
return (
<n-button text tag="a" type="primary" onClick={showDetailDrawer} class="ws-normal">
{row.groupName}
</n-button>
);
}
},
{
key: 'dictType',
title: $t('page.dictionary.dictionaryType'),
align: 'left',
width: 200,
render: row => {
if (row.type === null) {
return null;
}
// const label = $t(groupConfigTypeRecord[row.type!]);
return <p>政府部门/美国军方/美国海军</p>;
}
},
{
key: 'groupStatus',
title: $t('page.groupConfig.groupStatus'),
align: 'center',
width: 200,
render: row => {
const fetchFn = async (groupStatus: Api.Common.EnableStatusNumber, callback: (flag: boolean) => void) => {
const status = row.groupStatus === 1 ? 0 : 1;
const { error } = await fetchUpdateDictionaryStatus({ groupName: row.groupName, groupStatus: status });
if (!error) {
row.groupStatus = groupStatus;
window.$message?.success($t('common.updateSuccess'));
}
callback(!error);
};
return (
<StatusSwitch v-model:value={row.groupStatus} onSubmitted={fetchFn} disabled={hasAuth('R_USER') as boolean} />
);
}
},
{
key: 'idGeneratorMode',
title: $t('page.dictionary.remark'),
align: 'center',
width: 200,
render: row => {
if (row.idGeneratorMode === null) {
return null;
}
const label = $t(groupConfigIdModeRecord[row.idGeneratorMode!]);
// return <NTag type="primary">{label}</NTag>;
return '用户性别列表';
}
},
{
key: 'groupPartition',
title: $t('page.dictionary.createTime'),
align: 'center',
minWidth: 60
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 300,
render: row => {
if (hasAuth('R_USER')) {
return <></>;
}
return (
<div class="flex-center gap-8px">
<NButton type="primary" text ghost size="small" onClick={() => edit(row.id!)}>
{$t('common.edit')}
</NButton>
<n-divider vertical />
<NPopconfirm onPositiveClick={() => handleDelete(row.groupName!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" text ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
);
}
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
function edit(id: string) {
handleEdit(id);
}
async function handleDelete(groupName: string) {
const { error } = await fetchDeleteDictionary(groupName);
if (error) return;
onDeleted();
}
function body(): Api.DictionaryConfig.ExportDictionaryConfig {
return {
groupName: searchParams.groupName,
groupStatus: searchParams.groupStatus,
groupIds: checkedRowKeys.value
};
}
function handleExport() {
downloadFetch('/group/export', body(), $t('page.groupConfig.title'));
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<DictionarySearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<DeleteAlert />
<NCard
:title="$t('page.groupConfig.title')"
:bordered="false"
size="small"
header-class="view-card-header"
class="sm:flex-1-hidden card-wrapper"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-add="hasAuth('R_ADMIN')"
:show-delete="false"
@add="handleAdd"
@refresh="getData"
>
<template #addAfter>
<FileUpload v-if="hasAuth('R_ADMIN')" action="/group/import" accept="application/json" @refresh="getData" />
<NPopconfirm @positive-click="handleExport">
<template #trigger>
<NButton size="small" ghost type="primary" :disabled="checkedRowKeys.length === 0 && hasAuth('R_USER')">
<template #icon>
<IconPajamasExport class="text-icon" />
</template>
{{ $t('common.export') }}
</NButton>
</template>
<template #default>
{{
checkedRowKeys.length === 0
? $t('common.exportAll')
: $t('common.exportPar', { num: checkedRowKeys.length })
}}
</template>
</NPopconfirm>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<DictionaryOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
<DictionaryDetailDrawer v-model:visible="detailVisible" :row-data="detailData" />
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -1,51 +0,0 @@
<script setup lang="ts">
import { groupConfigIdModeRecord, groupConfigStatusRecord, yesOrNoRecord } from '@/constants/business';
import { $t } from '@/locales';
import { tagColor } from '@/utils/common';
defineOptions({
name: 'GroupDetailDrawer'
});
interface Props {
/** row data */
rowData?: Api.GroupConfig.GroupConfig | null;
}
defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
</script>
<template>
<OperateDrawer v-model="visible" :title="$t('page.groupConfig.detail')">
<NDescriptions label-placement="top" bordered :column="2">
<NDescriptionsItem :label="$t('page.groupConfig.groupName')" :span="2">
{{ rowData?.groupName }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.token')" :span="2">{{ rowData?.token }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.groupStatus')" :span="1">
<NTag :type="tagColor(rowData?.groupStatus!)">{{ $t(groupConfigStatusRecord[rowData?.groupStatus!]) }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.idGeneratorMode')" :span="1">
<NTag :type="tagColor(rowData?.idGeneratorMode!)">
{{ $t(groupConfigIdModeRecord[rowData?.idGeneratorMode!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.groupPartition')" :span="1">
{{ rowData?.groupPartition }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.initScene')" :span="1">
<NTag :type="tagColor(rowData?.initScene!)">{{ $t(yesOrNoRecord[rowData?.initScene!]) }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.groupConfig.description')" :span="2">
{{ rowData?.description }}
</NDescriptionsItem>
</NDescriptions>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -1,81 +0,0 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'CategoryHeaderOperation'
});
interface Props {
itemAlign?: NaiveUI.Align;
disabledDelete?: boolean;
loading?: boolean;
showDelete?: boolean;
showAdd?: boolean;
}
withDefaults(defineProps<Props>(), {
showDelete: true,
showAdd: true
});
interface Emits {
(e: 'add'): void;
(e: 'delete'): void;
(e: 'refresh'): void;
}
const emit = defineEmits<Emits>();
function add() {
emit('add');
}
function batchDelete() {
emit('delete');
}
function refresh() {
emit('refresh');
}
</script>
<template>
<NSpace :align="itemAlign" wrap justify="end" class="lt-sm:w-200px">
<slot name="prefix"></slot>
<slot name="default">
<NButton v-if="showAdd" size="small" ghost type="primary" @click="add">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</NButton>
<slot name="addAfter"></slot>
<NPopconfirm v-if="showDelete" @positive-click="batchDelete">
<template #trigger>
<NButton size="small" ghost type="error" :disabled="disabledDelete">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
{{ $t('common.batchDelete') }}
</NButton>
</template>
{{ $t('common.confirmDelete') }}
</NPopconfirm>
</slot>
<NButton size="small" @click="refresh">
<template #icon>
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
</template>
{{ $t('common.refresh') }}
</NButton>
<slot name="suffix"></slot>
</NSpace>
</template>
<style scoped></style>

View File

@ -1,189 +0,0 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { useClipboard } from '@vueuse/core';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import OperateDrawer from '@/components/common/operate-drawer.vue';
import { $t } from '@/locales';
import { fetchAddNamespace, fetchEditNamespace } from '@/service/api';
import { useAuthStore } from '@/store/modules/auth';
import { translateOptions } from '@/utils/common';
import { fetchAddCategory } from '@/service/api'
// import { categoryTypeOptions } from '@/constants/business';
//
defineOptions({
name: 'CategoryOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Category.Category | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
// 使
const { copy, isSupported } = useClipboard();
// 使
const authStore = useAuthStore();
//
const visible = defineModel<boolean>('visible', {
default: false
});
//
// restoreValidation
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
//
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.category.addCategory'),
edit: $t('page.category.editCategory')
};
return titles[props.operateType];
});
const model: Api.Category.Model = reactive(createDefaultModel());
function updateModel(categoryName: string, categoryType: string, parentId: number) {
model.name = categoryName;
model.type = categoryType;
model.parent = parentId.toString(); //
}
function createDefaultModel(): Api.Category.Model {
return {
name: '',
type: '',
parent: ''
};
}
//
type RuleKey = Extract<keyof Api.Category.Model, App.Global.FormRule>;
const rules: Record<RuleKey, App.Global.FormRule> = {
name: defaultRequiredRule,
type: defaultRequiredRule,
parent: defaultRequiredRule
};
//
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
const parentId = props.rowData!.parentId;
updateModel('', '网站', parentId);
return;
}
if (props.operateType === 'edit' && props.rowData) {
const { categoryName, categoryType, parentId } = props.rowData;
updateModel(categoryName, categoryType, parentId);
console.log(model);
}
}
//
function closeDrawer() {
visible.value = false;
}
//
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const res = await fetchAddCategory(model);
console.log(res);
if (res.error) return;
window.$message?.success($t('common.addSuccess'));
}
// if (props.operateType === 'edit') {
// const { type, name, parentId } = model;
// const { error } = await fetchEditNamespace({ type, name, parentId });
// if (error) return;
// window.$message?.success($t('common.updateSuccess'));
// }
// await authStore.getUserInfo();
closeDrawer();
emit('submitted');
}
// ,
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
async function handleCopy(source: string) {
if (!isSupported) {
window.$message?.error('您的浏览器不支持 Clipboard API');
return;
}
if (!source) {
return;
}
await copy(source);
window.$message?.success('复制成功');
}
const categoryTypeOptions = [
{ label: '1', value: '网站' },
{ label: '2', value: '文献' }
];
</script>
<template>
<OperateDrawer v-model="visible" :title="title" @submitted="handleSubmit">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.category.form.categoryName')" path="name">
<NInputGroup>
<NInput v-model:value="model.name" :placeholder="$t('page.category.form.name')" />
<NTooltip v-if="props.operateType === 'edit'" trigger="hover">
<template #trigger>
<NButton type="default" ghost @click="handleCopy(model.name)">
<icon-ic:round-content-copy class="text-icon" />
</NButton>
</template>
复制
</NTooltip>
</NInputGroup>
</NFormItem>
<NFormItem :label="$t('page.category.form.categoryType')" path="type">
<NSelect v-model:value="model.type" :placeholder="$t('page.category.form.categoryType')"
:options="categoryTypeOptions" clearable />
</NFormItem>
<!-- <NFormItem :label="$t('page.category.form.categoryType')" path="type">
<NSelect v-model:value="model.type" :placeholder="$t('page.category.form.categoryType')"
:options="translateOptions(categoryTypeOptions)" clearable />
</NFormItem> -->
<NFormItem :label="$t('page.category.form.categoryParentName')" path="parent">
<NInput v-model:value="model.parent" :placeholder="$t('page.category.form.categoryType')" :disabled="true" />
</NFormItem>
</NForm>
<!-- 按钮 -->
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.save') }}</NButton>
</NSpace>
</template>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -1,96 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { NButton, NCard, NForm, NFormItemGi, NGrid, NInput, NSelect, NSpace } from 'naive-ui';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { translateOptions } from '@/utils/common';
import { categoryTypeOptions } from '@/constants/business';
const appStore = useAppStore();
const model = ref({
categoryName: '',
categoryStatus: null
});
const btnSpan = computed(() => (appStore.isMobile ? '7' : '7'));
interface Emits {
(e: 'update'): void;
}
const emit = defineEmits<Emits>();
function search() {
console.log('搜索:', model.value.categoryName);
//
emit('update');
}
function reset() {
model.value.categoryName = '';
model.value.categoryStatus = null;
//
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NForm :model="model" label-placement="left" :label-width="80" :show-feedback="appStore.isMobile">
<NGrid responsive="screen" cols="36" item-responsive :y-gap="12">
<!-- 搜索 -->
<NFormItemGi span="m:7" :label="$t('page.dictionary.form.dictionaryName')" path="categoryName" class="pr-0px">
<NInput
v-model:value="model.categoryName"
:placeholder="$t('page.dictionary.form.dictionaryName')"
clearable
/>
</NFormItemGi>
<!-- 搜索 -->
<NFormItemGi span="m:7" :label="$t('page.dictionary.form.dictionaryType')" path="categoryName" class="pr-0px">
<NInput
v-model:value="model.categoryName"
:placeholder="$t('page.dictionary.form.dictionaryType')"
clearable
/>
</NFormItemGi>
<!-- 下拉 -->
<NFormItemGi
span="m:7"
:label="$t('page.dictionary.form.dictionaryStatus')"
path="categoryStatus"
class="pr-0px"
>
<NSelect
v-model:value="model.categoryStatus"
:placeholder="$t('page.dictionary.form.dictionaryStatus')"
:options="translateOptions(categoryTypeOptions)"
clearable
/>
</NFormItemGi>
<!-- 搜索 -->
<NFormItemGi span="m:7" :label="$t('page.dictionary.form.createTime')" path="categoryName" class="pr-0px">
<NInput v-model:value="model.categoryName" :placeholder="$t('page.dictionary.form.createTime')" clearable />
</NFormItemGi>
<!-- 按钮 -->
<NFormItemGi :y-gap="8" :span="btnSpan" class="pr-0px lg:p-t-0 md:p-t-16px">
<NSpace class="min-w-100px w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCard>
</template>
<style scoped></style>

View File

@ -1,47 +0,0 @@
import { ref, Ref, Reactive } from 'vue';
import { jsonClone } from '@sa/utils';
import { useBoolean } from '@sa/hooks';
import { $t } from '@/locales';
export function useFormOperate<T extends Record<string, any>>(config: {
getData: () => Promise<void>;
saveData: (data: T) => Promise<void>;
}) {
const { getData, saveData } = config;
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType: Ref<NaiveUI.TableOperateType> = ref('add');
const editingData: Ref<Api.Category.Category | null> = ref(null);
// 打开添加表单
function handleAdd(parentId: number) {
operateType.value = 'add';
editingData.value = {
id: 0,
parentId: parentId,
categoryName: "",
level: "",
createTime: "",
categoryType: "网站"
};
openDrawer();
}
// 打开编辑表单
function handleEdit(data: Api.Category.Category) {
operateType.value = 'edit';
editingData.value = data;
openDrawer();
}
return {
drawerVisible, // 抽屉是否可见
operateType, // 操作类型
editingData, // 传入侧边栏的数据
openDrawer, // 打开抽屉
closeDrawer, // 关闭抽屉
handleAdd, // 添加操作
handleEdit, // 编辑操作
};
}

View File

@ -0,0 +1,232 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { ref } from 'vue';
import { useBoolean } from '@sa/hooks';
import {
fetchBatchDeleteDictionaryType,
fetchDeleteDictionaryType,
fetchGetDictionaryTypeList
} from '@/service/api/dictionary-type';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useCustomPKTable, useCustomPKTableOperate } from '@/hooks/common/table';
import { useAuth } from '@/hooks/business/auth';
// import type { Api } from '@/typings/api';
import { downloadFetch } from '@/utils/download';
import { useRouterPush } from '@/hooks/common/router';
import DictionaryOperateDrawer from './modules/dictionary-type-operate-drawer.vue';
import DictionaryDetailDrawer from './modules/dictionary-type-detail-drawer.vue';
import DictionarySearch from './modules/dictionary-type-search.vue';
const { hasAuth } = useAuth();
const appStore = useAppStore();
const { routerPushByKey } = useRouterPush();
/** 详情页属性数据 */
const detailData = ref<Api.DictionaryType.DictionaryType | null>();
/** 详情页可见状态 */
const { bool: detailVisible, setTrue: openDetail } = useBoolean(false);
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } =
useCustomPKTable('dictId', {
apiFn: fetchGetDictionaryTypeList,
apiParams: {
page: 1,
size: 10,
dictName: '',
dictType: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'dictId',
title: $t('page.dictionaryType.dictionaryId'),
align: 'center',
width: 64
},
{
key: 'dictName',
title: $t('page.dictionaryType.dictionaryName' as App.I18n.I18nKey),
align: 'left',
width: 200,
render: row => {
function showDetailDrawer() {
detailData.value = row || null;
openDetail();
}
return (
<n-button text tag="a" type="primary" onClick={showDetailDrawer} class="ws-normal">
{row.dictName}
</n-button>
);
}
},
{
key: 'dictType',
title: $t('page.dictionaryType.dictionaryType'),
align: 'left',
width: 200,
render: row => {
if (row.dictType === null) {
return null;
}
// return <p> {row.dictType} </p>;
// routerPushByKey
return (
<n-button
text
tag="a"
type="primary"
onClick={() => routerPushByKey('dictionary_data', { state: { dictType: row.dictType } })}
class="ws-normal"
>
{row.dictType}
</n-button>
);
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 300,
render: row => {
if (hasAuth('R_USER')) {
return <></>;
}
return (
<div class="flex-center gap-8px">
<NButton type="primary" text ghost size="small" onClick={() => edit(row.dictId!)}>
{$t('common.edit')}
</NButton>
<n-divider vertical />
<NPopconfirm onPositiveClick={() => handleDelete(row.dictId!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" text ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
);
}
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onDeleted,
onBatchDeleted
// closeDrawer
} = useCustomPKTableOperate(data, getData);
function edit(dictId: number) {
handleEdit(dictId);
}
async function handleBatchDelete() {
const { error } = await fetchBatchDeleteDictionaryType(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(dictId: number) {
const { error } = await fetchDeleteDictionaryType(dictId);
if (error) return;
onDeleted();
}
function body(): Api.DictionaryType.ExportDictionaryType {
return {
dictIds: checkedRowKeys.value,
dictName: searchParams.dictName,
dictType: searchParams.dictType
};
}
function handleExport() {
downloadFetch('/dict/type/export', body(), $t('page.dictionaryType.title'));
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<DictionarySearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<DeleteAlert />
<NCard
:title="$t('page.dictionaryType.title')"
:bordered="false"
size="small"
header-class="view-card-header"
class="sm:flex-1-hidden card-wrapper"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-add="hasAuth('R_ADMIN')"
:show-delete="hasAuth('R_ADMIN')"
:disabled-delete="checkedRowKeys.length === 0"
@add="handleAdd"
@refresh="getData"
@delete="handleBatchDelete"
>
<template #addAfter>
<FileUpload action="/job/import" accept="application/json" @refresh="getData" />
<NPopconfirm @positive-click="handleExport">
<template #trigger>
<NButton size="small" ghost type="primary" :disabled="checkedRowKeys.length === 0 && hasAuth('R_USER')">
<template #icon>
<IconPajamasExport class="text-icon" />
</template>
{{ $t('common.export') }}
</NButton>
</template>
<template #default>
{{
checkedRowKeys.length === 0
? $t('common.exportAll')
: $t('common.exportPar', { num: checkedRowKeys.length })
}}
</template>
</NPopconfirm>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<DictionaryOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
<DictionaryDetailDrawer v-model:visible="detailVisible" :row-data="detailData" />
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'GroupDetailDrawer'
});
interface Props {
/** row data */
rowData?: Api.DictionaryType.DictionaryType | null;
}
defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
</script>
<template>
<OperateDrawer v-model="visible" :title="$t('page.dictionaryType.detail')">
<NDescriptions label-placement="top" bordered :column="2">
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryId')" :span="2">
{{ rowData?.dictId }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryName')" :span="2">
{{ rowData?.dictName }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.dictionaryType')" :span="2">
{{ rowData?.dictType }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.createTime')" :span="2">
{{ rowData?.createTime }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.dictionaryType.remark')" :span="2">
{{ rowData?.createTime }}
</NDescriptionsItem>
</NDescriptions>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
// import {
// dictionaryTypeIdModeOptions,
// dictionaryTypeStatusOptions,
// dictionaryTypeTypeOptions,
// dictionaryTypeYesOrNoOptions
// } from '@/constants/business';
import { fetchAddDictionaryType, fetchEditDictionaryType } from '@/service/api/dictionary-type';
defineOptions({
name: 'DictionaryOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.DictionaryType.DictionaryType | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.dictionaryType.addDictionary'),
edit: $t('page.dictionaryType.editDictionary')
};
return titles[props.operateType];
});
type Model = Pick<Api.DictionaryType.DictionaryType, 'dictId' | 'dictName' | 'dictType'>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
dictId: 0,
dictName: '',
dictType: ''
};
}
type RuleKey = Extract<keyof Model, 'dictId' | 'dictName' | 'dictType' | 'dictStatus'>;
const rules = {
dictId: [defaultRequiredRule],
dictName: [defaultRequiredRule],
dictType: [defaultRequiredRule]
} satisfies Record<RuleKey, App.Global.FormRule[]>;
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { dictName, dictType } = model;
const { error } = await fetchAddDictionaryType({
dictName,
dictType
});
if (error) return;
window.$message?.success($t('common.addSuccess'));
} else {
const { dictId, dictName, dictType } = model;
const { error } = await fetchEditDictionaryType({
dictId,
dictName,
dictType
});
if (error) return;
window.$message?.success($t('common.updateSuccess'));
}
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
// getAllPartitions(); // drawerkeepaliveonMounteddrawer
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<OperateDrawer v-model="visible" :title="title" @submitted="handleSubmit">
<NForm ref="formRef" :model="model" :rules="rules">
<NCollapse :default-expanded-names="['1', '2']">
<NFormItem :label="$t('page.dictionaryType.dictionaryName')" path="dictName">
<NInput
v-model:value="model.dictName"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryType.form.dictionaryName')"
/>
</NFormItem>
<NFormItem :label="$t('page.dictionaryType.dictionaryType')" path="dictType">
<NInput
v-model:value="model.dictType"
:maxlength="64"
show-count
:placeholder="$t('page.dictionaryType.form.dictionaryType')"
/>
</NFormItem>
</NCollapse>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.save') }}</NButton>
</NSpace>
</template>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed } from 'vue';
import { NButton, NCard, NForm, NFormItemGi, NGrid, NInput, NSpace } from 'naive-ui';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const model = defineModel<Api.DictionaryType.DictionaryTypeSearchParams>('model', { required: true });
const btnSpan = computed(() => (appStore.isMobile ? '7' : '7'));
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
function search() {
emit('search');
}
function reset() {
emit('reset');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NForm :model="model" label-placement="left" :label-width="80" :show-feedback="appStore.isMobile">
<NGrid responsive="screen" cols="21" item-responsive :y-gap="12">
<!-- 字典名称 -->
<NFormItemGi span="m:7" :label="$t('page.dictionaryType.form.dictionaryName')" path="dictName" class="pr-0px">
<NInput
v-model:value="model.dictName"
:placeholder="$t('page.dictionaryType.form.dictionaryName')"
clearable
/>
</NFormItemGi>
<!-- 字典类型 -->
<NFormItemGi span="m:7" :label="$t('page.dictionaryType.form.dictionaryType')" path="dictType" class="pr-0px">
<NInput
v-model:value="model.dictType"
:placeholder="$t('page.dictionaryType.form.dictionaryType')"
clearable
/>
</NFormItemGi>
<!-- 按钮 -->
<NFormItemGi :y-gap="8" :span="btnSpan" class="pr-0px lg:p-t-0 md:p-t-16px">
<NSpace class="min-w-100px w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCard>
</template>
<style scoped></style>

View File

@ -246,7 +246,6 @@ function getGradientColor(color: CardData['color']) {
</div>
</DefineGradientBg>
<!-- define component end: GradientBg -->
<NGrid :cols="gridCol" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in cardData" :key="item.key" class="home-card">
<NSpin :show="false">