refactor: 升级框架

This commit is contained in:
caiyuchao
2025-07-09 11:28:52 +08:00
parent bbe6d7e76e
commit 258c0e2934
310 changed files with 11060 additions and 8152 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vben/web-antd", "name": "@vben/web-antd",
"version": "5.5.6", "version": "5.5.7",
"homepage": "https://vben.pro", "homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": { "repository": {
@@ -53,7 +53,8 @@
"pinia": "catalog:", "pinia": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-dompurify-html": "catalog:", "vue-dompurify-html": "catalog:",
"vue-router": "catalog:" "vue-router": "catalog:",
"vue3-signature": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "catalog:" "@types/crypto-js": "catalog:"

View File

@@ -11,6 +11,7 @@ import { $t } from '@vben/locales';
/** 手机号正则表达式(中国) */ /** 手机号正则表达式(中国) */
const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/; const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/;
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({ setupVbenForm<ComponentType>({
config: { config: {
// ant design vue组件库默认都是 v-model:value // ant design vue组件库默认都是 v-model:value
@@ -20,7 +21,6 @@ setupVbenForm<ComponentType>({
modelPropNameMap: { modelPropNameMap: {
Checkbox: 'checked', Checkbox: 'checked',
Radio: 'checked', Radio: 'checked',
RichTextarea: 'modelValue',
Switch: 'checked', Switch: 'checked',
Upload: 'fileList', Upload: 'fileList',
}, },
@@ -61,10 +61,11 @@ setupVbenForm<ComponentType>({
}, },
}, },
}); });
}
const useVbenForm = useForm<ComponentType>; const useVbenForm = useForm<ComponentType>;
export { useVbenForm, z }; export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>; export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps }; export type { VbenFormProps };

View File

@@ -1,4 +1,4 @@
/* 来自 @vben/plugins/vxe-table style.css TODO @puhui999可以写下目的哈 */ /* 来自 @vben/plugins/vxe-table style.css,覆盖 vxe-table 原有的样式定义,使用 vben 的样式主题 */
:root { :root {
--vxe-ui-font-color: hsl(var(--foreground)); --vxe-ui-font-color: hsl(var(--foreground));
--vxe-ui-font-primary-color: hsl(var(--primary)); --vxe-ui-font-primary-color: hsl(var(--primary));

View File

@@ -1,3 +1,4 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import { h } from 'vue'; import { h } from 'vue';
@@ -9,9 +10,22 @@ import {
setupVbenVxeTable, setupVbenVxeTable,
useVbenVxeGrid, useVbenVxeGrid,
} from '@vben/plugins/vxe-table'; } from '@vben/plugins/vxe-table';
import { isFunction, isString } from '@vben/utils'; import {
erpCountInputFormatter,
erpNumberFormatter,
formatPast2,
isFunction,
isString,
} from '@vben/utils';
import { Button, Image, Popconfirm, Switch } from 'ant-design-vue'; import {
Button,
Image,
ImagePreviewGroup,
Popconfirm,
Switch,
Tag,
} from 'ant-design-vue';
import { DictTag, DictTagGroup } from '#/components/dict-tag'; import { DictTag, DictTagGroup } from '#/components/dict-tag';
import { $t } from '#/locales'; import { $t } from '#/locales';
@@ -63,7 +77,7 @@ setupVbenVxeTable({
round: true, round: true,
showOverflow: true, showOverflow: true,
size: 'small', size: 'small',
}, } as VxeTableGridOptions,
}); });
// 表格配置项可以用 cellRender: { name: 'CellImage' }, // 表格配置项可以用 cellRender: { name: 'CellImage' },
@@ -74,6 +88,20 @@ setupVbenVxeTable({
}, },
}); });
vxeUI.renderer.add('CellImages', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
if (column && column.field && row[column.field]) {
return h(ImagePreviewGroup, {}, () => {
return row[column.field].map((item: any) =>
h(Image, { src: item }),
);
});
}
return '';
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' }, // 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', { vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) { renderTableDefault(renderOpts) {
@@ -86,6 +114,35 @@ setupVbenVxeTable({
}, },
}); });
// 表格配置项可以用 cellRender: { name: 'CellTag' },
vxeUI.renderer.add('CellTag', {
renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params;
return h(Tag, { color: props?.color }, () => row[column.field]);
},
});
vxeUI.renderer.add('CellTags', {
renderTableDefault(renderOpts, params) {
const { props } = renderOpts;
const { column, row } = params;
if (!row[column.field] || row[column.field].length === 0) {
return '';
}
return h(
'div',
{ class: 'flex items-center justify-center' },
{
default: () =>
row[column.field].map((item: any) =>
h(Tag, { color: props?.color }, { default: () => item }),
),
},
);
},
});
// 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} }, // 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} },
vxeUI.renderer.add('CellDict', { vxeUI.renderer.add('CellDict', {
renderTableDefault(renderOpts, params) { renderTableDefault(renderOpts, params) {
@@ -283,20 +340,23 @@ setupVbenVxeTable({
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化 // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add // vxeUI.formats.add
vxeUI.formats.add('formatPast2', {
tableCellFormatMethod({ cellValue }) {
return formatPast2(cellValue);
},
});
// add by 星语:数量格式化,例如说:金额 // add by 星语:数量格式化,例如说:金额
vxeUI.formats.add('formatAmount', { vxeUI.formats.add('formatNumber', {
cellFormatMethod({ cellValue }, digits = 2) { tableCellFormatMethod({ cellValue }) {
if (cellValue === null || cellValue === undefined) { return erpCountInputFormatter(cellValue);
return ''; },
} });
if (isString(cellValue)) {
cellValue = Number.parseFloat(cellValue); vxeUI.formats.add('formatAmount2', {
} tableCellFormatMethod({ cellValue }, digits = 2) {
// 如果非 number则直接返回空串 return `${erpNumberFormatter(cellValue, digits)}`;
if (Number.isNaN(cellValue)) {
return '';
}
return cellValue.toFixed(digits);
}, },
}); });
}, },
@@ -316,4 +376,5 @@ export type OnActionClickParams<T = Recordable<any>> = {
export type OnActionClickFn<T = Recordable<any>> = ( export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>, params: OnActionClickParams<T>,
) => void; ) => void;
export * from '#/components/table-action';
export type * from '@vben/plugins/vxe-table'; export type * from '@vben/plugins/vxe-table';

View File

@@ -65,13 +65,13 @@ export namespace InfraCodegenApi {
} }
/** 更新代码生成请求 */ /** 更新代码生成请求 */
export interface CodegenUpdateReqVO { export interface CodegenUpdateReq {
table: any | CodegenTable; table: any | CodegenTable;
columns: CodegenColumn[]; columns: CodegenColumn[];
} }
/** 创建代码生成请求 */ /** 创建代码生成请求 */
export interface CodegenCreateListReqVO { export interface CodegenCreateListReq {
dataSourceConfigId?: number; dataSourceConfigId?: number;
tableNames: string[]; tableNames: string[];
} }
@@ -106,32 +106,25 @@ export function getCodegenTable(tableId: number) {
} }
/** 修改代码生成表定义 */ /** 修改代码生成表定义 */
export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReqVO) { export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReq) {
return requestClient.put('/infra/codegen/update', data); return requestClient.put('/infra/codegen/update', data);
} }
/** 基于数据库的表结构,同步数据库的表和字段定义 */ /** 基于数据库的表结构,同步数据库的表和字段定义 */
export function syncCodegenFromDB(tableId: number) { export function syncCodegenFromDB(tableId: number) {
return requestClient.put('/infra/codegen/sync-from-db', { return requestClient.put(`/infra/codegen/sync-from-db?tableId=${tableId}`);
params: { tableId },
});
} }
/** 预览生成代码 */ /** 预览生成代码 */
export function previewCodegen(tableId: number) { export function previewCodegen(tableId: number) {
return requestClient.get<InfraCodegenApi.CodegenPreview[]>( return requestClient.get<InfraCodegenApi.CodegenPreview[]>(
'/infra/codegen/preview', `/infra/codegen/preview?tableId=${tableId}`,
{
params: { tableId },
},
); );
} }
/** 下载生成代码 */ /** 下载生成代码 */
export function downloadCodegen(tableId: number) { export function downloadCodegen(tableId: number) {
return requestClient.download('/infra/codegen/download', { return requestClient.download(`/infra/codegen/download?tableId=${tableId}`);
params: { tableId },
});
} }
/** 获得表定义 */ /** 获得表定义 */
@@ -143,9 +136,7 @@ export function getSchemaTableList(params: any) {
} }
/** 基于数据库的表结构,创建代码生成器的表定义 */ /** 基于数据库的表结构,创建代码生成器的表定义 */
export function createCodegenList( export function createCodegenList(data: InfraCodegenApi.CodegenCreateListReq) {
data: InfraCodegenApi.CodegenCreateListReqVO,
) {
return requestClient.post('/infra/codegen/create-list', data); return requestClient.post('/infra/codegen/create-list', data);
} }
@@ -155,3 +146,10 @@ export function deleteCodegenTable(tableId: number) {
params: { tableId }, params: { tableId },
}); });
} }
/** 批量删除代码生成表定义 */
export function deleteCodegenTableList(tableIds: number[]) {
return requestClient.delete(
`/infra/codegen/delete-list?tableIds=${tableIds.join(',')}`,
);
}

View File

@@ -54,9 +54,14 @@ export function deleteConfig(id: number) {
return requestClient.delete(`/infra/config/delete?id=${id}`); return requestClient.delete(`/infra/config/delete?id=${id}`);
} }
/** 批量删除参数 */
export function deleteConfigList(ids: number[]) {
return requestClient.delete(`/infra/config/delete-list?ids=${ids.join(',')}`);
}
/** 导出参数 */ /** 导出参数 */
export function exportConfig(params: any) { export function exportConfig(params: any) {
return requestClient.download('/infra/config/export', { return requestClient.download('/infra/config/export-excel', {
params, params,
}); });
} }

View File

@@ -9,7 +9,7 @@ export namespace Demo01ContactApi {
export interface Demo01Contact { export interface Demo01Contact {
id: number; // 编号 id: number; // 编号
name?: string; // 名字 name?: string; // 名字
sex?: boolean; // 性别 sex?: number; // 性别
birthday?: Dayjs | string; // 出生年 birthday?: Dayjs | string; // 出生年
description?: string; // 简介 description?: string; // 简介
avatar: string; // 头像 avatar: string; // 头像
@@ -46,6 +46,13 @@ export function deleteDemo01Contact(id: number) {
return requestClient.delete(`/infra/demo01-contact/delete?id=${id}`); return requestClient.delete(`/infra/demo01-contact/delete?id=${id}`);
} }
/** 批量删除示例联系人 */
export function deleteDemo01ContactList(ids: number[]) {
return requestClient.delete(
`/infra/demo01-contact/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出示例联系人 */ /** 导出示例联系人 */
export function exportDemo01Contact(params: any) { export function exportDemo01Contact(params: any) {
return requestClient.download('/infra/demo01-contact/export-excel', params); return requestClient.download('/infra/demo01-contact/export-excel', params);

View File

@@ -34,7 +34,7 @@ export namespace Demo03StudentApi {
/** 查询学生分页 */ /** 查询学生分页 */
export function getDemo03StudentPage(params: PageParam) { export function getDemo03StudentPage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>( return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
'/infra/demo03-student/page', '/infra/demo03-student-erp/page',
{ params }, { params },
); );
} }
@@ -42,28 +42,38 @@ export function getDemo03StudentPage(params: PageParam) {
/** 查询学生详情 */ /** 查询学生详情 */
export function getDemo03Student(id: number) { export function getDemo03Student(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Student>( return requestClient.get<Demo03StudentApi.Demo03Student>(
`/infra/demo03-student/get?id=${id}`, `/infra/demo03-student-erp/get?id=${id}`,
); );
} }
/** 新增学生 */ /** 新增学生 */
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) { export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.post('/infra/demo03-student/create', data); return requestClient.post('/infra/demo03-student-erp/create', data);
} }
/** 修改学生 */ /** 修改学生 */
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) { export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.put('/infra/demo03-student/update', data); return requestClient.put('/infra/demo03-student-erp/update', data);
} }
/** 删除学生 */ /** 删除学生 */
export function deleteDemo03Student(id: number) { export function deleteDemo03Student(id: number) {
return requestClient.delete(`/infra/demo03-student/delete?id=${id}`); return requestClient.delete(`/infra/demo03-student-erp/delete?id=${id}`);
}
/** 批量删除学生 */
export function deleteDemo03StudentList(ids: number[]) {
return requestClient.delete(
`/infra/demo03-student-erp/delete-list?ids=${ids.join(',')}`,
);
} }
/** 导出学生 */ /** 导出学生 */
export function exportDemo03Student(params: any) { export function exportDemo03Student(params: any) {
return requestClient.download('/infra/demo03-student/export-excel', params); return requestClient.download(
'/infra/demo03-student-erp/export-excel',
params,
);
} }
// ==================== 子表(学生课程) ==================== // ==================== 子表(学生课程) ====================
@@ -71,33 +81,44 @@ export function exportDemo03Student(params: any) {
/** 获得学生课程分页 */ /** 获得学生课程分页 */
export function getDemo03CoursePage(params: PageParam) { export function getDemo03CoursePage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Course>>( return requestClient.get<PageResult<Demo03StudentApi.Demo03Course>>(
`/infra/demo03-student/demo03-course/page`, `/infra/demo03-student-erp/demo03-course/page`,
{ { params },
params,
},
); );
} }
/** 新增学生课程 */ /** 新增学生课程 */
export function createDemo03Course(data: Demo03StudentApi.Demo03Course) { export function createDemo03Course(data: Demo03StudentApi.Demo03Course) {
return requestClient.post(`/infra/demo03-student/demo03-course/create`, data); return requestClient.post(
`/infra/demo03-student-erp/demo03-course/create`,
data,
);
} }
/** 修改学生课程 */ /** 修改学生课程 */
export function updateDemo03Course(data: Demo03StudentApi.Demo03Course) { export function updateDemo03Course(data: Demo03StudentApi.Demo03Course) {
return requestClient.put(`/infra/demo03-student/demo03-course/update`, data); return requestClient.put(
`/infra/demo03-student-erp/demo03-course/update`,
data,
);
} }
/** 删除学生课程 */ /** 删除学生课程 */
export function deleteDemo03Course(id: number) { export function deleteDemo03Course(id: number) {
return requestClient.delete( return requestClient.delete(
`/infra/demo03-student/demo03-course/delete?id=${id}`, `/infra/demo03-student-erp/demo03-course/delete?id=${id}`,
);
}
/** 批量删除学生课程 */
export function deleteDemo03CourseList(ids: number[]) {
return requestClient.delete(
`/infra/demo03-student-erp/demo03-course/delete-list?ids=${ids.join(',')}`,
); );
} }
/** 获得学生课程 */ /** 获得学生课程 */
export function getDemo03Course(id: number) { export function getDemo03Course(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Course>( return requestClient.get<Demo03StudentApi.Demo03Course>(
`/infra/demo03-student/demo03-course/get?id=${id}`, `/infra/demo03-student-erp/demo03-course/get?id=${id}`,
); );
} }
@@ -106,32 +127,43 @@ export function getDemo03Course(id: number) {
/** 获得学生班级分页 */ /** 获得学生班级分页 */
export function getDemo03GradePage(params: PageParam) { export function getDemo03GradePage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Grade>>( return requestClient.get<PageResult<Demo03StudentApi.Demo03Grade>>(
`/infra/demo03-student/demo03-grade/page`, `/infra/demo03-student-erp/demo03-grade/page`,
{ { params },
params,
},
); );
} }
/** 新增学生班级 */ /** 新增学生班级 */
export function createDemo03Grade(data: Demo03StudentApi.Demo03Grade) { export function createDemo03Grade(data: Demo03StudentApi.Demo03Grade) {
return requestClient.post(`/infra/demo03-student/demo03-grade/create`, data); return requestClient.post(
`/infra/demo03-student-erp/demo03-grade/create`,
data,
);
} }
/** 修改学生班级 */ /** 修改学生班级 */
export function updateDemo03Grade(data: Demo03StudentApi.Demo03Grade) { export function updateDemo03Grade(data: Demo03StudentApi.Demo03Grade) {
return requestClient.put(`/infra/demo03-student/demo03-grade/update`, data); return requestClient.put(
`/infra/demo03-student-erp/demo03-grade/update`,
data,
);
} }
/** 删除学生班级 */ /** 删除学生班级 */
export function deleteDemo03Grade(id: number) { export function deleteDemo03Grade(id: number) {
return requestClient.delete( return requestClient.delete(
`/infra/demo03-student/demo03-grade/delete?id=${id}`, `/infra/demo03-student-erp/demo03-grade/delete?id=${id}`,
);
}
/** 批量删除学生班级 */
export function deleteDemo03GradeList(ids: number[]) {
return requestClient.delete(
`/infra/demo03-student-erp/demo03-grade/delete-list?ids=${ids.join(',')}`,
); );
} }
/** 获得学生班级 */ /** 获得学生班级 */
export function getDemo03Grade(id: number) { export function getDemo03Grade(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Grade>( return requestClient.get<Demo03StudentApi.Demo03Grade>(
`/infra/demo03-student/demo03-grade/get?id=${id}`, `/infra/demo03-student-erp/demo03-grade/get?id=${id}`,
); );
} }

View File

@@ -1,3 +1,5 @@
import type { Dayjs } from 'dayjs';
import type { PageParam, PageResult } from '@vben/request'; import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
@@ -24,7 +26,7 @@ export namespace Demo03StudentApi {
id: number; // 编号 id: number; // 编号
name?: string; // 名字 name?: string; // 名字
sex?: number; // 性别 sex?: number; // 性别
birthday?: Date; // 出生日期 birthday?: Dayjs | string; // 出生日期
description?: string; // 简介 description?: string; // 简介
demo03courses?: Demo03Course[]; demo03courses?: Demo03Course[];
demo03grade?: Demo03Grade; demo03grade?: Demo03Grade;
@@ -34,7 +36,7 @@ export namespace Demo03StudentApi {
/** 查询学生分页 */ /** 查询学生分页 */
export function getDemo03StudentPage(params: PageParam) { export function getDemo03StudentPage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>( return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
'/infra/demo03-student/page', '/infra/demo03-student-inner/page',
{ params }, { params },
); );
} }
@@ -42,28 +44,38 @@ export function getDemo03StudentPage(params: PageParam) {
/** 查询学生详情 */ /** 查询学生详情 */
export function getDemo03Student(id: number) { export function getDemo03Student(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Student>( return requestClient.get<Demo03StudentApi.Demo03Student>(
`/infra/demo03-student/get?id=${id}`, `/infra/demo03-student-inner/get?id=${id}`,
); );
} }
/** 新增学生 */ /** 新增学生 */
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) { export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.post('/infra/demo03-student/create', data); return requestClient.post('/infra/demo03-student-inner/create', data);
} }
/** 修改学生 */ /** 修改学生 */
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) { export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.put('/infra/demo03-student/update', data); return requestClient.put('/infra/demo03-student-inner/update', data);
} }
/** 删除学生 */ /** 删除学生 */
export function deleteDemo03Student(id: number) { export function deleteDemo03Student(id: number) {
return requestClient.delete(`/infra/demo03-student/delete?id=${id}`); return requestClient.delete(`/infra/demo03-student-inner/delete?id=${id}`);
}
/** 批量删除学生 */
export function deleteDemo03StudentList(ids: number[]) {
return requestClient.delete(
`/infra/demo03-student-inner/delete-list?ids=${ids.join(',')}`,
);
} }
/** 导出学生 */ /** 导出学生 */
export function exportDemo03Student(params: any) { export function exportDemo03Student(params: any) {
return requestClient.download('/infra/demo03-student/export-excel', params); return requestClient.download(
'/infra/demo03-student-inner/export-excel',
params,
);
} }
// ==================== 子表(学生课程) ==================== // ==================== 子表(学生课程) ====================
@@ -71,7 +83,7 @@ export function exportDemo03Student(params: any) {
/** 获得学生课程列表 */ /** 获得学生课程列表 */
export function getDemo03CourseListByStudentId(studentId: number) { export function getDemo03CourseListByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Course[]>( return requestClient.get<Demo03StudentApi.Demo03Course[]>(
`/infra/demo03-student/demo03-course/list-by-student-id?studentId=${studentId}`, `/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=${studentId}`,
); );
} }
@@ -80,6 +92,6 @@ export function getDemo03CourseListByStudentId(studentId: number) {
/** 获得学生班级 */ /** 获得学生班级 */
export function getDemo03GradeByStudentId(studentId: number) { export function getDemo03GradeByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Grade>( return requestClient.get<Demo03StudentApi.Demo03Grade>(
`/infra/demo03-student/demo03-grade/get-by-student-id?studentId=${studentId}`, `/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=${studentId}`,
); );
} }

View File

@@ -36,7 +36,7 @@ export namespace Demo03StudentApi {
/** 查询学生分页 */ /** 查询学生分页 */
export function getDemo03StudentPage(params: PageParam) { export function getDemo03StudentPage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>( return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
'/infra/demo03-student/page', '/infra/demo03-student-normal/page',
{ params }, { params },
); );
} }
@@ -44,28 +44,38 @@ export function getDemo03StudentPage(params: PageParam) {
/** 查询学生详情 */ /** 查询学生详情 */
export function getDemo03Student(id: number) { export function getDemo03Student(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Student>( return requestClient.get<Demo03StudentApi.Demo03Student>(
`/infra/demo03-student/get?id=${id}`, `/infra/demo03-student-normal/get?id=${id}`,
); );
} }
/** 新增学生 */ /** 新增学生 */
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) { export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.post('/infra/demo03-student/create', data); return requestClient.post('/infra/demo03-student-normal/create', data);
} }
/** 修改学生 */ /** 修改学生 */
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) { export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.put('/infra/demo03-student/update', data); return requestClient.put('/infra/demo03-student-normal/update', data);
} }
/** 删除学生 */ /** 删除学生 */
export function deleteDemo03Student(id: number) { export function deleteDemo03Student(id: number) {
return requestClient.delete(`/infra/demo03-student/delete?id=${id}`); return requestClient.delete(`/infra/demo03-student-normal/delete?id=${id}`);
}
/** 批量删除学生 */
export function deleteDemo03StudentList(ids: number[]) {
return requestClient.delete(
`/infra/demo03-student-normal/delete-list?ids=${ids.join(',')}`,
);
} }
/** 导出学生 */ /** 导出学生 */
export function exportDemo03Student(params: any) { export function exportDemo03Student(params: any) {
return requestClient.download('/infra/demo03-student/export-excel', params); return requestClient.download(
'/infra/demo03-student-normal/export-excel',
params,
);
} }
// ==================== 子表(学生课程) ==================== // ==================== 子表(学生课程) ====================
@@ -73,7 +83,7 @@ export function exportDemo03Student(params: any) {
/** 获得学生课程列表 */ /** 获得学生课程列表 */
export function getDemo03CourseListByStudentId(studentId: number) { export function getDemo03CourseListByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Course[]>( return requestClient.get<Demo03StudentApi.Demo03Course[]>(
`/infra/demo03-student/demo03-course/list-by-student-id?studentId=${studentId}`, `/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=${studentId}`,
); );
} }
@@ -82,6 +92,6 @@ export function getDemo03CourseListByStudentId(studentId: number) {
/** 获得学生班级 */ /** 获得学生班级 */
export function getDemo03GradeByStudentId(studentId: number) { export function getDemo03GradeByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Grade>( return requestClient.get<Demo03StudentApi.Demo03Grade>(
`/infra/demo03-student/demo03-grade/get-by-student-id?studentId=${studentId}`, `/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=${studentId}`,
); );
} }

View File

@@ -69,6 +69,13 @@ export function deleteFileConfig(id: number) {
return requestClient.delete(`/infra/file-config/delete?id=${id}`); return requestClient.delete(`/infra/file-config/delete?id=${id}`);
} }
/** 批量删除文件配置 */
export function deleteFileConfigList(ids: number[]) {
return requestClient.delete(
`/infra/file-config/delete-list?ids=${ids.join(',')}`,
);
}
/** 测试文件配置 */ /** 测试文件配置 */
export function testFileConfig(id: number) { export function testFileConfig(id: number) {
return requestClient.get(`/infra/file-config/test?id=${id}`); return requestClient.get(`/infra/file-config/test?id=${id}`);

View File

@@ -19,7 +19,7 @@ export namespace InfraFileApi {
} }
/** 文件预签名地址 */ /** 文件预签名地址 */
export interface FilePresignedUrlRespVO { export interface FilePresignedUrlResp {
configId: number; // 文件配置编号 configId: number; // 文件配置编号
uploadUrl: string; // 文件上传 URL uploadUrl: string; // 文件上传 URL
url: string; // 文件 URL url: string; // 文件 URL
@@ -27,7 +27,7 @@ export namespace InfraFileApi {
} }
/** 上传文件 */ /** 上传文件 */
export interface FileUploadReqVO { export interface FileUploadReq {
file: globalThis.File; file: globalThis.File;
directory?: string; directory?: string;
} }
@@ -45,9 +45,14 @@ export function deleteFile(id: number) {
return requestClient.delete(`/infra/file/delete?id=${id}`); return requestClient.delete(`/infra/file/delete?id=${id}`);
} }
/** 批量删除文件 */
export function deleteFileList(ids: number[]) {
return requestClient.delete(`/infra/file/delete-list?ids=${ids.join(',')}`);
}
/** 获取文件预签名地址 */ /** 获取文件预签名地址 */
export function getFilePresignedUrl(name: string, directory?: string) { export function getFilePresignedUrl(name: string, directory?: string) {
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>( return requestClient.get<InfraFileApi.FilePresignedUrlResp>(
'/infra/file/presigned-url', '/infra/file/presigned-url',
{ {
params: { name, directory }, params: { name, directory },
@@ -62,7 +67,7 @@ export function createFile(data: InfraFileApi.File) {
/** 上传文件 */ /** 上传文件 */
export function uploadFile( export function uploadFile(
data: InfraFileApi.FileUploadReqVO, data: InfraFileApi.FileUploadReq,
onUploadProgress?: AxiosProgressEvent, onUploadProgress?: AxiosProgressEvent,
) { ) {
// 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端 // 特殊:由于 upload 内部封装,即使 directory 为 undefined也会传递给后端

View File

@@ -15,6 +15,7 @@ export namespace InfraJobApi {
retryInterval: number; retryInterval: number;
monitorTimeout: number; monitorTimeout: number;
createTime?: Date; createTime?: Date;
nextTimes?: Date[];
} }
} }
@@ -45,6 +46,11 @@ export function deleteJob(id: number) {
return requestClient.delete(`/infra/job/delete?id=${id}`); return requestClient.delete(`/infra/job/delete?id=${id}`);
} }
/** 批量删除定时任务调度 */
export function deleteJobList(ids: number[]) {
return requestClient.delete(`/infra/job/delete-list?ids=${ids.join(',')}`);
}
/** 导出定时任务调度 */ /** 导出定时任务调度 */
export function exportJob(params: any) { export function exportJob(params: any) {
return requestClient.download('/infra/job/export-excel', { params }); return requestClient.download('/infra/job/export-excel', { params });
@@ -56,7 +62,7 @@ export function updateJobStatus(id: number, status: number) {
id, id,
status, status,
}; };
return requestClient.put('/infra/job/update-status', { params }); return requestClient.put('/infra/job/update-status', {}, { params });
} }
/** 定时任务立即执行一次 */ /** 定时任务立即执行一次 */

View File

@@ -45,3 +45,8 @@ export async function updateDept(data: SystemDeptApi.Dept) {
export async function deleteDept(id: number) { export async function deleteDept(id: number) {
return requestClient.delete(`/system/dept/delete?id=${id}`); return requestClient.delete(`/system/dept/delete?id=${id}`);
} }
/** 批量删除部门 */
export async function deleteDeptList(ids: number[]) {
return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`);
}

View File

@@ -48,7 +48,14 @@ export function deleteDictData(id: number) {
return requestClient.delete(`/system/dict-data/delete?id=${id}`); return requestClient.delete(`/system/dict-data/delete?id=${id}`);
} }
// 批量删除字典数据
export function deleteDictDataList(ids: number[]) {
return requestClient.delete(
`/system/dict-data/delete-list?ids=${ids.join(',')}`,
);
}
// 导出字典类型数据 // 导出字典类型数据
export function exportDictData(params: any) { export function exportDictData(params: any) {
return requestClient.download('/system/dict-data/export', { params }); return requestClient.download('/system/dict-data/export-excel', { params });
} }

View File

@@ -42,7 +42,14 @@ export function deleteDictType(id: number) {
return requestClient.delete(`/system/dict-type/delete?id=${id}`); return requestClient.delete(`/system/dict-type/delete?id=${id}`);
} }
// 批量删除字典
export function deleteDictTypeList(ids: number[]) {
return requestClient.delete(
`/system/dict-type/delete-list?ids=${ids.join(',')}`,
);
}
// 导出字典类型 // 导出字典类型
export function exportDictType(params: any) { export function exportDictType(params: any) {
return requestClient.download('/system/dict-type/export', { params }); return requestClient.download('/system/dict-type/export-excel', { params });
} }

View File

@@ -49,6 +49,13 @@ export function deleteMailAccount(id: number) {
return requestClient.delete(`/system/mail-account/delete?id=${id}`); return requestClient.delete(`/system/mail-account/delete?id=${id}`);
} }
/** 批量删除邮箱账号 */
export function deleteMailAccountList(ids: number[]) {
return requestClient.delete(
`/system/mail-account/delete-list?ids=${ids.join(',')}`,
);
}
/** 获得邮箱账号精简列表 */ /** 获得邮箱账号精简列表 */
export function getSimpleMailAccountList() { export function getSimpleMailAccountList() {
return requestClient.get<SystemMailAccountApi.MailAccount[]>( return requestClient.get<SystemMailAccountApi.MailAccount[]>(

View File

@@ -19,7 +19,7 @@ export namespace SystemMailTemplateApi {
} }
/** 邮件发送信息 */ /** 邮件发送信息 */
export interface MailSendReqVO { export interface MailSendReq {
mail: string; mail: string;
templateCode: string; templateCode: string;
templateParams: Record<string, any>; templateParams: Record<string, any>;
@@ -56,7 +56,14 @@ export function deleteMailTemplate(id: number) {
return requestClient.delete(`/system/mail-template/delete?id=${id}`); return requestClient.delete(`/system/mail-template/delete?id=${id}`);
} }
/** 批量删除邮件模板 */
export function deleteMailTemplateList(ids: number[]) {
return requestClient.delete(
`/system/mail-template/delete-list?ids=${ids.join(',')}`,
);
}
/** 发送邮件 */ /** 发送邮件 */
export function sendMail(data: SystemMailTemplateApi.MailSendReqVO) { export function sendMail(data: SystemMailTemplateApi.MailSendReq) {
return requestClient.post('/system/mail-template/send-mail', data); return requestClient.post('/system/mail-template/send-mail', data);
} }

View File

@@ -53,3 +53,8 @@ export async function updateMenu(data: SystemMenuApi.Menu) {
export async function deleteMenu(id: number) { export async function deleteMenu(id: number) {
return requestClient.delete(`/system/menu/delete?id=${id}`); return requestClient.delete(`/system/menu/delete?id=${id}`);
} }
/** 批量删除菜单 */
export async function deleteMenuList(ids: number[]) {
return requestClient.delete(`/system/menu/delete-list?ids=${ids.join(',')}`);
}

View File

@@ -46,6 +46,13 @@ export function deleteNotice(id: number) {
return requestClient.delete(`/system/notice/delete?id=${id}`); return requestClient.delete(`/system/notice/delete?id=${id}`);
} }
/** 批量删除公告 */
export function deleteNoticeList(ids: number[]) {
return requestClient.delete(
`/system/notice/delete-list?ids=${ids.join(',')}`,
);
}
/** 推送公告 */ /** 推送公告 */
export function pushNotice(id: number) { export function pushNotice(id: number) {
return requestClient.post(`/system/notice/push?id=${id}`); return requestClient.post(`/system/notice/push?id=${id}`);

View File

@@ -17,7 +17,7 @@ export namespace SystemNotifyTemplateApi {
} }
/** 发送站内信请求 */ /** 发送站内信请求 */
export interface NotifySendReqVO { export interface NotifySendReq {
userId: number; userId: number;
userType: number; userType: number;
templateCode: string; templateCode: string;
@@ -59,6 +59,13 @@ export function deleteNotifyTemplate(id: number) {
return requestClient.delete(`/system/notify-template/delete?id=${id}`); return requestClient.delete(`/system/notify-template/delete?id=${id}`);
} }
/** 批量删除站内信模板 */
export function deleteNotifyTemplateList(ids: number[]) {
return requestClient.delete(
`/system/notify-template/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出站内信模板 */ /** 导出站内信模板 */
export function exportNotifyTemplate(params: any) { export function exportNotifyTemplate(params: any) {
return requestClient.download('/system/notify-template/export-excel', { return requestClient.download('/system/notify-template/export-excel', {
@@ -67,6 +74,6 @@ export function exportNotifyTemplate(params: any) {
} }
/** 发送站内信 */ /** 发送站内信 */
export function sendNotify(data: SystemNotifyTemplateApi.NotifySendReqVO) { export function sendNotify(data: SystemNotifyTemplateApi.NotifySendReq) {
return requestClient.post('/system/notify-template/send-notify', data); return requestClient.post('/system/notify-template/send-notify', data);
} }

View File

@@ -3,7 +3,7 @@ import { requestClient } from '#/api/request';
/** OAuth2.0 授权信息响应 */ /** OAuth2.0 授权信息响应 */
export namespace SystemOAuth2ClientApi { export namespace SystemOAuth2ClientApi {
/** 授权信息 */ /** 授权信息 */
export interface AuthorizeInfoRespVO { export interface AuthorizeInfoResp {
client: { client: {
logo: string; logo: string;
name: string; name: string;
@@ -17,7 +17,7 @@ export namespace SystemOAuth2ClientApi {
/** 获得授权信息 */ /** 获得授权信息 */
export function getAuthorize(clientId: string) { export function getAuthorize(clientId: string) {
return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoRespVO>( return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoResp>(
`/system/oauth2/authorize?clientId=${clientId}`, `/system/oauth2/authorize?clientId=${clientId}`,
); );
} }

View File

@@ -32,3 +32,10 @@ export function deleteOAuth2Token(accessToken: string) {
`/system/oauth2-token/delete?accessToken=${accessToken}`, `/system/oauth2-token/delete?accessToken=${accessToken}`,
); );
} }
/** 批量删除 OAuth2.0 令牌 */
export function deleteOAuth2TokenList(accessTokens: string[]) {
return requestClient.delete(
`/system/oauth2-token/delete-list?accessTokens=${accessTokens.join(',')}`,
);
}

View File

@@ -2,19 +2,19 @@ import { requestClient } from '#/api/request';
export namespace SystemPermissionApi { export namespace SystemPermissionApi {
/** 分配用户角色请求 */ /** 分配用户角色请求 */
export interface AssignUserRoleReqVO { export interface AssignUserRoleReq {
userId: number; userId: number;
roleIds: number[]; roleIds: number[];
} }
/** 分配角色菜单请求 */ /** 分配角色菜单请求 */
export interface AssignRoleMenuReqVO { export interface AssignRoleMenuReq {
roleId: number; roleId: number;
menuIds: number[]; menuIds: number[];
} }
/** 分配角色数据权限请求 */ /** 分配角色数据权限请求 */
export interface AssignRoleDataScopeReqVO { export interface AssignRoleDataScopeReq {
roleId: number; roleId: number;
dataScope: number; dataScope: number;
dataScopeDeptIds: number[]; dataScopeDeptIds: number[];
@@ -30,14 +30,14 @@ export async function getRoleMenuList(roleId: number) {
/** 赋予角色菜单权限 */ /** 赋予角色菜单权限 */
export async function assignRoleMenu( export async function assignRoleMenu(
data: SystemPermissionApi.AssignRoleMenuReqVO, data: SystemPermissionApi.AssignRoleMenuReq,
) { ) {
return requestClient.post('/system/permission/assign-role-menu', data); return requestClient.post('/system/permission/assign-role-menu', data);
} }
/** 赋予角色数据权限 */ /** 赋予角色数据权限 */
export async function assignRoleDataScope( export async function assignRoleDataScope(
data: SystemPermissionApi.AssignRoleDataScopeReqVO, data: SystemPermissionApi.AssignRoleDataScopeReq,
) { ) {
return requestClient.post('/system/permission/assign-role-data-scope', data); return requestClient.post('/system/permission/assign-role-data-scope', data);
} }
@@ -51,7 +51,7 @@ export async function getUserRoleList(userId: number) {
/** 赋予用户角色 */ /** 赋予用户角色 */
export async function assignUserRole( export async function assignUserRole(
data: SystemPermissionApi.AssignUserRoleReqVO, data: SystemPermissionApi.AssignUserRoleReq,
) { ) {
return requestClient.post('/system/permission/assign-user-role', data); return requestClient.post('/system/permission/assign-user-role', data);
} }

View File

@@ -50,9 +50,14 @@ export function deletePost(id: number) {
return requestClient.delete(`/system/post/delete?id=${id}`); return requestClient.delete(`/system/post/delete?id=${id}`);
} }
/** 批量删除岗位 */
export function deletePostList(ids: number[]) {
return requestClient.delete(`/system/post/delete-list?ids=${ids.join(',')}`);
}
/** 导出岗位 */ /** 导出岗位 */
export function exportPost(params: any) { export function exportPost(params: any) {
return requestClient.download('/system/post/export', { return requestClient.download('/system/post/export-excel', {
params, params,
}); });
} }

View File

@@ -50,6 +50,11 @@ export function deleteRole(id: number) {
return requestClient.delete(`/system/role/delete?id=${id}`); return requestClient.delete(`/system/role/delete?id=${id}`);
} }
/** 批量删除角色 */
export function deleteRoleList(ids: number[]) {
return requestClient.delete(`/system/role/delete-list?ids=${ids.join(',')}`);
}
/** 导出角色 */ /** 导出角色 */
export function exportRole(params: any) { export function exportRole(params: any) {
return requestClient.download('/system/role/export-excel', { return requestClient.download('/system/role/export-excel', {

View File

@@ -54,7 +54,14 @@ export function deleteSmsChannel(id: number) {
return requestClient.delete(`/system/sms-channel/delete?id=${id}`); return requestClient.delete(`/system/sms-channel/delete?id=${id}`);
} }
/** 批量删除短信渠道 */
export function deleteSmsChannelList(ids: number[]) {
return requestClient.delete(
`/system/sms-channel/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出短信渠道 */ /** 导出短信渠道 */
export function exportSmsChannel(params: any) { export function exportSmsChannel(params: any) {
return requestClient.download('/system/sms-channel/export', { params }); return requestClient.download('/system/sms-channel/export-excel', { params });
} }

View File

@@ -20,7 +20,7 @@ export namespace SystemSmsTemplateApi {
} }
/** 发送短信请求 */ /** 发送短信请求 */
export interface SmsSendReqVO { export interface SmsSendReq {
mobile: string; mobile: string;
templateCode: string; templateCode: string;
templateParams: Record<string, any>; templateParams: Record<string, any>;
@@ -57,6 +57,13 @@ export function deleteSmsTemplate(id: number) {
return requestClient.delete(`/system/sms-template/delete?id=${id}`); return requestClient.delete(`/system/sms-template/delete?id=${id}`);
} }
/** 批量删除短信模板 */
export function deleteSmsTemplateList(ids: number[]) {
return requestClient.delete(
`/system/sms-template/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出短信模板 */ /** 导出短信模板 */
export function exportSmsTemplate(params: any) { export function exportSmsTemplate(params: any) {
return requestClient.download('/system/sms-template/export-excel', { return requestClient.download('/system/sms-template/export-excel', {
@@ -65,6 +72,6 @@ export function exportSmsTemplate(params: any) {
} }
/** 发送短信 */ /** 发送短信 */
export function sendSms(data: SystemSmsTemplateApi.SmsSendReqVO) { export function sendSms(data: SystemSmsTemplateApi.SmsSendReq) {
return requestClient.post('/system/sms-template/send-sms', data); return requestClient.post('/system/sms-template/send-sms', data);
} }

View File

@@ -20,14 +20,14 @@ export namespace SystemSocialUserApi {
} }
/** 社交绑定请求 */ /** 社交绑定请求 */
export interface SocialUserBindReqVO { export interface SocialUserBindReq {
type: number; type: number;
code: string; code: string;
state: string; state: string;
} }
/** 取消社交绑定请求 */ /** 取消社交绑定请求 */
export interface SocialUserUnbindReqVO { export interface SocialUserUnbindReq {
type: number; type: number;
openid: string; openid: string;
} }
@@ -49,12 +49,12 @@ export function getSocialUser(id: number) {
} }
/** 社交绑定,使用 code 授权码 */ /** 社交绑定,使用 code 授权码 */
export function socialBind(data: SystemSocialUserApi.SocialUserBindReqVO) { export function socialBind(data: SystemSocialUserApi.SocialUserBindReq) {
return requestClient.post<boolean>('/system/social-user/bind', data); return requestClient.post<boolean>('/system/social-user/bind', data);
} }
/** 取消社交绑定 */ /** 取消社交绑定 */
export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReqVO) { export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReq) {
return requestClient.delete<boolean>('/system/social-user/unbind', { data }); return requestClient.delete<boolean>('/system/social-user/unbind', { data });
} }

View File

@@ -49,6 +49,13 @@ export function deleteTenantPackage(id: number) {
return requestClient.delete(`/system/tenant-package/delete?id=${id}`); return requestClient.delete(`/system/tenant-package/delete?id=${id}`);
} }
/** 批量删除租户套餐 */
export function deleteTenantPackageList(ids: number[]) {
return requestClient.delete(
`/system/tenant-package/delete-list?ids=${ids.join(',')}`,
);
}
/** 获取租户套餐精简信息列表 */ /** 获取租户套餐精简信息列表 */
export function getTenantPackageList() { export function getTenantPackageList() {
return requestClient.get<SystemTenantPackageApi.TenantPackage[]>( return requestClient.get<SystemTenantPackageApi.TenantPackage[]>(

View File

@@ -61,6 +61,13 @@ export function deleteTenant(id: number) {
return requestClient.delete(`/system/tenant/delete?id=${id}`); return requestClient.delete(`/system/tenant/delete?id=${id}`);
} }
/** 批量删除租户 */
export function deleteTenantList(ids: number[]) {
return requestClient.delete(
`/system/tenant/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出租户 */ /** 导出租户 */
export function exportTenant(params: any) { export function exportTenant(params: any) {
return requestClient.download('/system/tenant/export-excel', { return requestClient.download('/system/tenant/export-excel', {

View File

@@ -49,9 +49,14 @@ export function deleteUser(id: number) {
return requestClient.delete(`/system/user/delete?id=${id}`); return requestClient.delete(`/system/user/delete?id=${id}`);
} }
/** 批量删除用户 */
export function deleteUserList(ids: number[]) {
return requestClient.delete(`/system/user/delete-list?ids=${ids.join(',')}`);
}
/** 导出用户 */ /** 导出用户 */
export function exportUser(params: any) { export function exportUser(params: any) {
return requestClient.download('/system/user/export', params); return requestClient.download('/system/user/export-excel', params);
} }
/** 下载用户导入模板 */ /** 下载用户导入模板 */

View File

@@ -2,7 +2,7 @@ import { requestClient } from '#/api/request';
export namespace SystemUserProfileApi { export namespace SystemUserProfileApi {
/** 用户个人中心信息 */ /** 用户个人中心信息 */
export interface UserProfileRespVO { export interface UserProfileResp {
id: number; id: number;
username: string; username: string;
nickname: string; nickname: string;
@@ -19,13 +19,13 @@ export namespace SystemUserProfileApi {
} }
/** 更新密码请求 */ /** 更新密码请求 */
export interface UpdatePasswordReqVO { export interface UpdatePasswordReq {
oldPassword: string; oldPassword: string;
newPassword: string; newPassword: string;
} }
/** 更新个人信息请求 */ /** 更新个人信息请求 */
export interface UpdateProfileReqVO { export interface UpdateProfileReq {
nickname?: string; nickname?: string;
email?: string; email?: string;
mobile?: string; mobile?: string;
@@ -36,21 +36,19 @@ export namespace SystemUserProfileApi {
/** 获取登录用户信息 */ /** 获取登录用户信息 */
export function getUserProfile() { export function getUserProfile() {
return requestClient.get<SystemUserProfileApi.UserProfileRespVO>( return requestClient.get<SystemUserProfileApi.UserProfileResp>(
'/system/user/profile/get', '/system/user/profile/get',
); );
} }
/** 修改用户个人信息 */ /** 修改用户个人信息 */
export function updateUserProfile( export function updateUserProfile(data: SystemUserProfileApi.UpdateProfileReq) {
data: SystemUserProfileApi.UpdateProfileReqVO,
) {
return requestClient.put('/system/user/profile/update', data); return requestClient.put('/system/user/profile/update', data);
} }
/** 修改用户个人密码 */ /** 修改用户个人密码 */
export function updateUserPassword( export function updateUserPassword(
data: SystemUserProfileApi.UpdatePasswordReqVO, data: SystemUserProfileApi.UpdatePasswordReq,
) { ) {
return requestClient.put('/system/user/profile/update-password', data); return requestClient.put('/system/user/profile/update-password', data);
} }

View File

@@ -14,6 +14,7 @@ import { $t, setupI18n } from '#/locales';
import { setupFormCreate } from '#/plugins/form-create'; import { setupFormCreate } from '#/plugins/form-create';
import { initComponentAdapter } from './adapter/component'; import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
@@ -21,6 +22,9 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置 // // 设置弹窗的默认配置
// setDefaultModalProps({ // setDefaultModalProps({
// fullscreenButton: false, // fullscreenButton: false,

View File

@@ -29,14 +29,14 @@ withDefaults(
<Card :body-style="bodyStyle" :title="title" class="mb-4"> <Card :body-style="bodyStyle" :title="title" class="mb-4">
<template v-if="title" #title> <template v-if="title" #title>
<div class="flex items-center"> <div class="flex items-center">
<span class="text-4 font-[700]">{{ title }}</span> <span class="text-base font-bold">{{ title }}</span>
<Tooltip placement="right"> <Tooltip placement="right">
<template #title> <template #title>
<div class="max-w-[200px]">{{ message }}</div> <div class="max-w-[200px]">{{ message }}</div>
</template> </template>
<ShieldQuestion :size="14" class="ml-5px" /> <ShieldQuestion :size="14" class="ml-1" />
</Tooltip> </Tooltip>
<div class="pl-20px flex flex-grow"> <div class="flex flex-grow pl-5">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import type { CropperAvatarProps } from './typing';
import { computed, ref, unref, watch, watchEffect } from 'vue'; import { computed, ref, unref, watch, watchEffect } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { Button, message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
@@ -27,13 +28,10 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
const emit = defineEmits(['update:value', 'change']); const emit = defineEmits(['update:value', 'change']);
const sourceValue = ref(props.value || ''); const sourceValue = ref(props.value || '');
const prefixCls = 'cropper-avatar';
const [CropperModal, modalApi] = useVbenModal({ const [CropperModal, modalApi] = useVbenModal({
connectedComponent: cropperModal, connectedComponent: cropperModal,
}); });
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`); const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
const getIconWidth = computed( const getIconWidth = computed(
@@ -73,28 +71,42 @@ defineExpose({
</script> </script>
<template> <template>
<div :class="getClass" :style="getStyle"> <!-- 头像容器 -->
<div class="inline-block text-center" :style="getStyle">
<!-- 图片包装器 -->
<div <div
:class="`${prefixCls}-image-wrapper`" class="bg-card group relative cursor-pointer overflow-hidden rounded-full border border-gray-200"
:style="getImageWrapperStyle" :style="getImageWrapperStyle"
@click="openModal" @click="openModal"
> >
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> <!-- 遮罩层 -->
<span <div
class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
:style="getImageWrapperStyle"
>
<IconifyIcon
icon="lucide:cloud-upload"
class="m-auto text-gray-400"
:style="{ :style="{
...getImageWrapperStyle, ...getImageWrapperStyle,
width: `${getIconWidth}`, width: getIconWidth,
height: `${getIconWidth}`, height: getIconWidth,
lineHeight: `${getIconWidth}`, lineHeight: getIconWidth,
}" }"
class="icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]" />
></span>
</div> </div>
<img v-if="sourceValue" :src="sourceValue" alt="avatar" /> <!-- 头像图片 -->
<img
v-if="sourceValue"
:src="sourceValue"
alt="avatar"
class="h-full w-full object-cover"
/>
</div> </div>
<!-- 上传按钮 -->
<Button <Button
v-if="showBtn" v-if="showBtn"
:class="`${prefixCls}-upload-btn`" class="mx-auto mt-2"
@click="openModal" @click="openModal"
v-bind="btnProps" v-bind="btnProps"
> >
@@ -109,49 +121,3 @@ defineExpose({
/> />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.cropper-avatar {
display: inline-block;
text-align: center;
&-image-wrapper {
overflow: hidden;
cursor: pointer;
background: #fff;
border: 1px solid #eee;
border-radius: 50%;
img {
width: 100%;
}
}
&-image-mask {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: inherit;
height: inherit;
cursor: pointer;
background: rgb(0 0 0 / 40%);
border: inherit;
border-radius: inherit;
opacity: 0;
transition: opacity 0.4s;
::v-deep(svg) {
margin: auto;
}
}
&-image-mask:hover {
opacity: 40;
}
&-upload-btn {
margin: 10px auto;
}
}
</style>

View File

@@ -4,6 +4,7 @@ import type { CropendResult, CropperModalProps, CropperType } from './typing';
import { ref } from 'vue'; import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { dataURLtoBlob, isFunction } from '@vben/utils'; import { dataURLtoBlob, isFunction } from '@vben/utils';
@@ -36,13 +37,20 @@ const cropper = ref<CropperType>();
let scaleX = 1; let scaleX = 1;
let scaleY = 1; let scaleY = 1;
const prefixCls = 'cropper-am';
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
onConfirm: handleOk, onConfirm: handleOk,
onOpenChange(isOpen) { onOpenChange(isOpen) {
if (isOpen) { if (isOpen) {
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading通过 handleReady // 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading通过 handleReady
modalLoading(true); modalLoading(true);
const img = new Image();
img.src = src.value;
img.addEventListener('load', () => {
modalLoading(false);
});
img.addEventListener('error', () => {
modalLoading(false);
});
} else { } else {
// 关闭时,清空右侧预览 // 关闭时,清空右侧预览
previewSource.value = ''; previewSource.value = '';
@@ -118,11 +126,15 @@ async function handleOk() {
:confirm-text="$t('ui.cropper.okText')" :confirm-text="$t('ui.cropper.okText')"
:fullscreen-button="false" :fullscreen-button="false"
:title="$t('ui.cropper.modalTitle')" :title="$t('ui.cropper.modalTitle')"
class="w-[800px]" class="w-2/3"
>
<div class="flex h-96">
<!-- 左侧区域 -->
<div class="h-full w-3/5">
<!-- 裁剪器容器 -->
<div
class="relative h-[300px] bg-gradient-to-b from-neutral-50 to-neutral-200"
> >
<div :class="prefixCls">
<div :class="`${prefixCls}-left`" class="w-full">
<div :class="`${prefixCls}-cropper`">
<CropperImage <CropperImage
v-if="src" v-if="src"
:circled="circled" :circled="circled"
@@ -133,7 +145,8 @@ async function handleOk() {
/> />
</div> </div>
<div :class="`${prefixCls}-toolbar`"> <!-- 工具栏 -->
<div class="mt-4 flex items-center justify-between">
<Upload <Upload
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:file-list="[]" :file-list="[]"
@@ -143,7 +156,7 @@ async function handleOk() {
<Button size="small" type="primary"> <Button size="small" type="primary">
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[ant-design--upload-outlined]"></span> <IconifyIcon icon="lucide:upload" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -159,7 +172,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[ant-design--reload-outlined]"></span> <IconifyIcon icon="lucide:rotate-ccw" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -176,9 +189,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span <IconifyIcon icon="ant-design:rotate-left-outlined" />
class="icon-[ant-design--rotate-left-outlined]"
></span>
</div> </div>
</template> </template>
</Button> </Button>
@@ -189,16 +200,13 @@ async function handleOk() {
> >
<Button <Button
:disabled="!src" :disabled="!src"
pre-icon="ant-design:rotate-right-outlined"
size="small" size="small"
type="primary" type="primary"
@click="handlerToolbar('rotate', 45)" @click="handlerToolbar('rotate', 45)"
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span <IconifyIcon icon="ant-design:rotate-right-outlined" />
class="icon-[ant-design--rotate-right-outlined]"
></span>
</div> </div>
</template> </template>
</Button> </Button>
@@ -212,7 +220,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[vaadin--arrows-long-h]"></span> <IconifyIcon icon="vaadin:arrows-long-h" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -226,7 +234,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[vaadin--arrows-long-v]"></span> <IconifyIcon icon="vaadin:arrows-long-v" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -240,7 +248,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[ant-design--zoom-in-outlined]"></span> <IconifyIcon icon="lucide:zoom-in" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -254,7 +262,7 @@ async function handleOk() {
> >
<template #icon> <template #icon>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="icon-[ant-design--zoom-out-outlined]"></span> <IconifyIcon icon="lucide:zoom-out" />
</div> </div>
</template> </template>
</Button> </Button>
@@ -262,16 +270,26 @@ async function handleOk() {
</Space> </Space>
</div> </div>
</div> </div>
<div :class="`${prefixCls}-right`">
<div :class="`${prefixCls}-preview`"> <!-- 右侧区域 -->
<div class="h-full w-2/5">
<!-- 预览区域 -->
<div
class="mx-auto h-56 w-56 overflow-hidden rounded-full border border-gray-200"
>
<img <img
v-if="previewSource" v-if="previewSource"
:alt="$t('ui.cropper.preview')" :alt="$t('ui.cropper.preview')"
:src="previewSource" :src="previewSource"
class="h-full w-full object-cover"
/> />
</div> </div>
<!-- 头像组合预览 -->
<template v-if="previewSource"> <template v-if="previewSource">
<div :class="`${prefixCls}-group`"> <div
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
>
<Avatar :src="previewSource" size="large" /> <Avatar :src="previewSource" size="large" />
<Avatar :size="48" :src="previewSource" /> <Avatar :size="48" :src="previewSource" />
<Avatar :size="64" :src="previewSource" /> <Avatar :size="64" :src="previewSource" />
@@ -282,76 +300,3 @@ async function handleOk() {
</div> </div>
</Modal> </Modal>
</template> </template>
<style lang="scss">
.cropper-am {
display: flex;
&-left,
&-right {
height: 340px;
}
&-left {
width: 55%;
}
&-right {
width: 45%;
}
&-cropper {
height: 300px;
background: #eee;
background-image:
linear-gradient(
45deg,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgb(0 0 0 / 25%) 0
),
linear-gradient(
45deg,
rgb(0 0 0 / 25%) 25%,
transparent 0,
transparent 75%,
rgb(0 0 0 / 25%) 0
);
background-position:
0 0,
12px 12px;
background-size: 24px 24px;
}
&-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
&-preview {
width: 220px;
height: 220px;
margin: 0 auto;
overflow: hidden;
border: 1px solid #eee;
border-radius: 50%;
img {
width: 100%;
height: 100%;
}
}
&-group {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 8px;
margin-top: 8px;
border-top: 1px solid #eee;
}
}
</style>

View File

@@ -33,7 +33,6 @@ const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Cropper | null>(); const cropper = ref<Cropper | null>();
const isReady = ref(false); const isReady = ref(false);
const prefixCls = 'cropper-image';
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80); const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
const getImageStyle = computed((): CSSProperties => { const getImageStyle = computed((): CSSProperties => {
@@ -46,10 +45,9 @@ const getImageStyle = computed((): CSSProperties => {
const getClass = computed(() => { const getClass = computed(() => {
return [ return [
prefixCls,
attrs.class, attrs.class,
{ {
[`${prefixCls}--circled`]: props.circled, 'cropper-image--circled': props.circled,
}, },
]; ];
}); });
@@ -115,10 +113,9 @@ function cropped() {
imgInfo, imgInfo,
}); });
}; };
// eslint-disable-next-line unicorn/prefer-add-event-listener fileReader.addEventListener('error', () => {
fileReader.onerror = () => {
emit('cropendError'); emit('cropendError');
}; });
}, 'image/png'); }, 'image/png');
} }
@@ -157,6 +154,7 @@ function getRoundedCanvas() {
:crossorigin="crossorigin" :crossorigin="crossorigin"
:src="src" :src="src"
:style="getImageStyle" :style="getImageStyle"
class="h-auto max-w-full"
/> />
</div> </div>
</template> </template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { isValidColor, TinyColor } from '@vben/utils';
import { Tag } from 'ant-design-vue'; import { Tag } from 'ant-design-vue';
// import { isHexColor } from '@/utils/color' // TODO @芋艿:【可优化】增加 cssClass 的处理 https://gitee.com/yudaocode/yudao-ui-admin-vben/blob/v2.4.1/src/components/DictTag/src/DictTag.vue#L60
import { getDictObj } from '#/utils'; import { getDictObj } from '#/utils';
interface DictTagProps { interface DictTagProps {
@@ -58,15 +59,23 @@ const dictTag = computed(() => {
} }
} }
if (isValidColor(dict.cssClass)) {
colorType = new TinyColor(dict.cssClass).toHexString();
}
return { return {
label: dict.label || '', label: dict.label || '',
colorType, colorType,
cssClass: dict.cssClass,
}; };
}); });
</script> </script>
<template> <template>
<Tag v-if="dictTag" :color="dictTag.colorType"> <Tag
v-if="dictTag"
:color="dictTag.colorType ? dictTag.colorType : dictTag.cssClass"
>
{{ dictTag.label }} {{ dictTag.label }}
</Tag> </Tag>
</template> </template>

View File

@@ -13,7 +13,7 @@ import {
SelectOption, SelectOption,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils'; import { getDictOptions } from '#/utils';
defineOptions({ name: 'DictSelect' }); defineOptions({ name: 'DictSelect' });
@@ -25,17 +25,16 @@ const props = withDefaults(defineProps<DictSelectProps>(), {
const attrs = useAttrs(); const attrs = useAttrs();
// 获得字典配置 // 获得字典配置
// TODO @dhb可以使用 getDictOptions 替代么? const getDictOption = computed(() => {
const getDictOptions = computed(() => {
switch (props.valueType) { switch (props.valueType) {
case 'bool': { case 'bool': {
return getDictObj(props.dictType, 'bool'); return getDictOptions(props.dictType, 'boolean');
} }
case 'int': { case 'int': {
return getIntDictOptions(props.dictType); return getDictOptions(props.dictType, 'number');
} }
case 'str': { case 'str': {
return getStrDictOptions(props.dictType); return getDictOptions(props.dictType, 'string');
} }
default: { default: {
return []; return [];
@@ -45,27 +44,27 @@ const getDictOptions = computed(() => {
</script> </script>
<template> <template>
<Select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs"> <Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
<SelectOption <SelectOption
v-for="(dict, index) in getDictOptions" v-for="(dict, index) in getDictOption"
:key="index" :key="index"
:value="dict.value" :value="dict.value"
> >
{{ dict.label }} {{ dict.label }}
</SelectOption> </SelectOption>
</Select> </Select>
<RadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs"> <RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
<Radio <Radio
v-for="(dict, index) in getDictOptions" v-for="(dict, index) in getDictOption"
:key="index" :key="index"
:value="dict.value" :value="dict.value"
> >
{{ dict.label }} {{ dict.label }}
</Radio> </Radio>
</RadioGroup> </RadioGroup>
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs"> <CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
<Checkbox <Checkbox
v-for="(dict, index) in getDictOptions" v-for="(dict, index) in getDictOption"
:key="index" :key="index"
:value="dict.value" :value="dict.value"
> >

View File

@@ -190,7 +190,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
// fix多写此步是为了解决 multiple 属性问题 // fix多写此步是为了解决 multiple 属性问题
return ( return (
<Select <Select
class="w-1/1" class="w-full"
loading={loading.value} loading={loading.value}
mode="multiple" mode="multiple"
{...attrs} {...attrs}
@@ -210,7 +210,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
} }
return ( return (
<Select <Select
class="w-1/1" class="w-full"
loading={loading.value} loading={loading.value}
{...attrs} {...attrs}
// TODO: @dhb52 remote 对等实现, 还是说没作用 // TODO: @dhb52 remote 对等实现, 还是说没作用
@@ -235,7 +235,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
]; ];
} }
return ( return (
<CheckboxGroup class="w-1/1" {...attrs}> <CheckboxGroup class="w-full" {...attrs}>
{options.value.map( {options.value.map(
(item: { label: any; value: any }, index: any) => ( (item: { label: any; value: any }, index: any) => (
<Checkbox key={index} value={item.value}> <Checkbox key={index} value={item.value}>
@@ -254,7 +254,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
]; ];
} }
return ( return (
<RadioGroup class="w-1/1" {...attrs}> <RadioGroup class="w-full" {...attrs}>
{options.value.map( {options.value.map(
(item: { label: any; value: any }, index: any) => ( (item: { label: any; value: any }, index: any) => (
<Radio key={index} value={item.value}> <Radio key={index} value={item.value}>

View File

@@ -121,7 +121,7 @@ const apiSelectRule = [
field: 'data', field: 'data',
title: '请求参数 JSON 格式', title: '请求参数 JSON 格式',
props: { props: {
autosize: true, autoSize: true,
type: 'textarea', type: 'textarea',
placeholder: '{"type": 1}', placeholder: '{"type": 1}',
}, },
@@ -155,7 +155,7 @@ const apiSelectRule = [
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表 info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
(data: any)=>{ label: string; value: any }[]`, (data: any)=>{ label: string; value: any }[]`,
props: { props: {
autosize: true, autoSize: true,
rows: { minRows: 2, maxRows: 6 }, rows: { minRows: 2, maxRows: 6 },
type: 'textarea', type: 'textarea',
placeholder: ` placeholder: `

View File

@@ -1,2 +1,4 @@
export * from './icons';
export { default as TableAction } from './table-action.vue'; export { default as TableAction } from './table-action.vue';
export * from './typing'; export * from './typing';

View File

@@ -1,12 +1,10 @@
<!-- add by 星语参考 vben2 的方式增加 TableAction 组件 --> <!-- add by 星语参考 vben2 的方式增加 TableAction 组件 -->
<script setup lang="ts"> <script setup lang="ts">
import type { ButtonType } from 'ant-design-vue/es/button';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { ActionItem, PopConfirm } from './typing'; import type { ActionItem, PopConfirm } from './typing';
import { computed, toRaw } from 'vue'; import { computed, unref } from 'vue';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
@@ -43,34 +41,31 @@ const props = defineProps({
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
/** 检查是否显示 */
function isIfShow(action: ActionItem): boolean { function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow; const ifShow = action.ifShow;
let isIfShow = true; let isIfShow = true;
if (isBoolean(ifShow)) { if (isBoolean(ifShow)) {
isIfShow = ifShow; isIfShow = ifShow;
} }
if (isFunction(ifShow)) { if (isFunction(ifShow)) {
isIfShow = ifShow(action); isIfShow = ifShow(action);
} }
if (isIfShow) {
isIfShow =
hasAccessByCodes(action.auth || []) || (action.auth || []).length === 0;
}
return isIfShow; return isIfShow;
} }
/** 处理按钮 actions */
const getActions = computed(() => { const getActions = computed(() => {
return (toRaw(props.actions) || []) return (props.actions || [])
.filter((action) => { .filter((action: ActionItem) => isIfShow(action))
return ( .map((action: ActionItem) => {
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action) => {
const { popConfirm } = action; const { popConfirm } = action;
return { return {
// getPopupContainer: document.body, type: action.type || 'link',
type: 'link' as ButtonType,
...action, ...action,
...popConfirm, ...popConfirm,
onConfirm: popConfirm?.confirm, onConfirm: popConfirm?.confirm,
@@ -80,19 +75,16 @@ const getActions = computed(() => {
}); });
}); });
const getDropdownList = computed((): any[] => { /** 处理下拉菜单 actions */
return (toRaw(props.dropDownActions) || []) const getDropdownList = computed(() => {
.filter((action) => { return (props.dropDownActions || [])
return ( .filter((action: ActionItem) => isIfShow(action))
(hasAccessByCodes(action.auth || []) || .map((action: ActionItem, index: number) => {
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action, index) => {
const { label, popConfirm } = action; const { label, popConfirm } = action;
const processedAction = { ...action };
delete processedAction.icon;
return { return {
...action, ...processedAction,
...popConfirm, ...popConfirm,
onConfirm: popConfirm?.confirm, onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel, onCancel: popConfirm?.cancel,
@@ -103,8 +95,16 @@ const getDropdownList = computed((): any[] => {
}); });
}); });
/** Space 组件的 size */
const spaceSize = computed(() => {
return unref(getActions)?.some((item: ActionItem) => item.type === 'link')
? 0
: 8;
});
/** 获取 PopConfirm 属性 */
function getPopConfirmProps(attrs: PopConfirm) { function getPopConfirmProps(attrs: PopConfirm) {
const originAttrs: any = attrs; const originAttrs: any = { ...attrs };
delete originAttrs.icon; delete originAttrs.icon;
if (attrs.confirm && isFunction(attrs.confirm)) { if (attrs.confirm && isFunction(attrs.confirm)) {
originAttrs.onConfirm = attrs.confirm; originAttrs.onConfirm = attrs.confirm;
@@ -117,31 +117,44 @@ function getPopConfirmProps(attrs: PopConfirm) {
return originAttrs; return originAttrs;
} }
/** 获取 Button 属性 */
function getButtonProps(action: ActionItem) { function getButtonProps(action: ActionItem) {
const res = { return {
type: action.type || 'primary', type: action.type || 'link',
...action, danger: action.danger || false,
disabled: action.disabled,
loading: action.loading,
size: action.size,
}; };
delete res.icon;
return res;
} }
/** 获取 Tooltip 属性 */
function getTooltipProps(tooltip: any | string) {
if (!tooltip) return {};
return typeof tooltip === 'string' ? { title: tooltip } : { ...tooltip };
}
/** 处理菜单点击 */
function handleMenuClick(e: any) { function handleMenuClick(e: any) {
const action = getDropdownList.value[e.key]; const action = getDropdownList.value[e.key];
if (action.onClick && isFunction(action.onClick)) { if (action && action.onClick && isFunction(action.onClick)) {
action.onClick(); action.onClick();
} }
} }
/** 生成稳定的 key */
function getActionKey(action: ActionItem, index: number) {
return `${action.label || ''}-${action.type || ''}-${index}`;
}
</script> </script>
<template> <template>
<div class="m-table-action"> <div class="table-actions">
<Space <Space :size="spaceSize">
:size=" <template
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8 v-for="(action, index) in getActions"
" :key="getActionKey(action, index)"
> >
<template v-for="(action, index) in getActions" :key="index">
<Popconfirm <Popconfirm
v-if="action.popConfirm" v-if="action.popConfirm"
v-bind="getPopConfirmProps(action.popConfirm)" v-bind="getPopConfirmProps(action.popConfirm)"
@@ -149,13 +162,7 @@ function handleMenuClick(e: any) {
<template v-if="action.popConfirm.icon" #icon> <template v-if="action.popConfirm.icon" #icon>
<IconifyIcon :icon="action.popConfirm.icon" /> <IconifyIcon :icon="action.popConfirm.icon" />
</template> </template>
<Tooltip <Tooltip v-bind="getTooltipProps(action.tooltip)">
v-bind="
typeof action.tooltip === 'string'
? { title: action.tooltip }
: { ...action.tooltip }
"
>
<Button v-bind="getButtonProps(action)"> <Button v-bind="getButtonProps(action)">
<template v-if="action.icon" #icon> <template v-if="action.icon" #icon>
<IconifyIcon :icon="action.icon" /> <IconifyIcon :icon="action.icon" />
@@ -164,14 +171,7 @@ function handleMenuClick(e: any) {
</Button> </Button>
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
<Tooltip <Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
v-else
v-bind="
typeof action.tooltip === 'string'
? { title: action.tooltip }
: { ...action.tooltip }
"
>
<Button v-bind="getButtonProps(action)" @click="action.onClick"> <Button v-bind="getButtonProps(action)" @click="action.onClick">
<template v-if="action.icon" #icon> <template v-if="action.icon" #icon>
<IconifyIcon :icon="action.icon" /> <IconifyIcon :icon="action.icon" />
@@ -184,16 +184,21 @@ function handleMenuClick(e: any) {
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']"> <Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
<slot name="more"> <slot name="more">
<Button size="small" type="link"> <Button :type="getDropdownList[0]?.type">
<template #icon> <template #icon>
{{ $t('page.action.more') }} {{ $t('page.action.more') }}
<IconifyIcon class="icon-more" icon="ant-design:more-outlined" /> <IconifyIcon icon="lucide:ellipsis-vertical" />
</template> </template>
</Button> </Button>
</slot> </slot>
<template #overlay> <template #overlay>
<Menu @click="handleMenuClick"> <Menu>
<Menu.Item v-for="(action, index) in getDropdownList" :key="index"> <Menu.Item
v-for="(action, index) in getDropdownList"
:key="index"
:disabled="action.disabled"
@click="!action.popConfirm && handleMenuClick({ key: index })"
>
<template v-if="action.popConfirm"> <template v-if="action.popConfirm">
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)"> <Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
<template v-if="action.popConfirm.icon" #icon> <template v-if="action.popConfirm.icon" #icon>
@@ -207,7 +212,9 @@ function handleMenuClick(e: any) {
" "
> >
<IconifyIcon v-if="action.icon" :icon="action.icon" /> <IconifyIcon v-if="action.icon" :icon="action.icon" />
<span class="ml-1">{{ action.text }}</span> <span :class="action.icon ? 'ml-1' : ''">
{{ action.text }}
</span>
</div> </div>
</Popconfirm> </Popconfirm>
</template> </template>
@@ -229,9 +236,10 @@ function handleMenuClick(e: any) {
</Dropdown> </Dropdown>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.m-table-action { .table-actions {
.ant-btn { .ant-btn-link {
padding: 4px; padding: 4px;
margin-left: 0; margin-left: 0;
} }

View File

@@ -1,4 +1,7 @@
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; import type {
ButtonProps,
ButtonType,
} from 'ant-design-vue/es/button/buttonTypes';
import type { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip'; import type { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
export interface PopConfirm { export interface PopConfirm {
@@ -13,6 +16,7 @@ export interface PopConfirm {
export interface ActionItem extends ButtonProps { export interface ActionItem extends ButtonProps {
onClick?: () => void; onClick?: () => void;
type?: ButtonType;
label?: string; label?: string;
color?: 'error' | 'success' | 'warning'; color?: 'error' | 'success' | 'warning';
icon?: string; icon?: string;

View File

@@ -5,7 +5,7 @@ import type { VxeToolbarInstance } from '#/adapter/vxe-table';
import { ref } from 'vue'; import { ref } from 'vue';
import { useContentMaximize, useRefresh } from '@vben/hooks'; import { useContentMaximize, useRefresh } from '@vben/hooks';
import { Expand, MsRefresh, Search, TMinimize } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button, Tooltip } from 'ant-design-vue'; import { Button, Tooltip } from 'ant-design-vue';
@@ -41,37 +41,39 @@ defineExpose({
<slot></slot> <slot></slot>
<Tooltip placement="bottom"> <Tooltip placement="bottom">
<template #title> <template #title>
<div class="max-w-[200px]">搜索</div> <div class="max-w-52">搜索</div>
</template> </template>
<Button <Button
class="ml-2 font-[8px]" class="ml-2 font-normal"
shape="circle" shape="circle"
@click="onHiddenSearchBar" @click="onHiddenSearchBar"
> >
<Search :size="15" /> <IconifyIcon icon="lucide:search" :size="15" />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip placement="bottom"> <Tooltip placement="bottom">
<template #title> <template #title>
<div class="max-w-[200px]">刷新</div> <div class="max-w-52">刷新</div>
</template> </template>
<Button class="ml-2 font-[8px]" shape="circle" @click="refresh"> <Button class="ml-2 font-medium" shape="circle" @click="refresh">
<MsRefresh :size="15" /> <IconifyIcon icon="lucide:refresh-cw" :size="15" />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip placement="bottom"> <Tooltip placement="bottom">
<template #title> <template #title>
<div class="max-w-[200px]"> <div class="max-w-52">
{{ contentIsMaximize ? '还原' : '全屏' }} {{ contentIsMaximize ? '还原' : '全屏' }}
</div> </div>
</template> </template>
<Button <Button
class="ml-2 font-[8px]" class="ml-2 font-medium"
shape="circle" shape="circle"
@click="toggleMaximizeAndTabbarHidden" @click="toggleMaximizeAndTabbarHidden"
> >
<Expand v-if="!contentIsMaximize" :size="15" /> <IconifyIcon
<TMinimize v-else :size="15" /> :icon="contentIsMaximize ? 'lucide:minimize' : 'lucide:maximize'"
:size="15"
/>
</Button> </Button>
</Tooltip> </Tooltip>
</template> </template>

View File

@@ -2,7 +2,7 @@
import type { UploadFile, UploadProps } from 'ant-design-vue'; import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface'; import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request'; import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file'; import type { AxiosProgressEvent } from '#/api/infra/file';
@@ -20,31 +20,7 @@ import { useUpload, useUploadType } from './use-upload';
defineOptions({ name: 'FileUpload', inheritAttrs: false }); defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults( const props = withDefaults(defineProps<FileUploadProps>(), {
defineProps<{
// 根据后缀,或者其他
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
// 上传的目录
directory?: string;
disabled?: boolean;
helpText?: string;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
// 是否显示下面的描述
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [], value: () => [],
directory: undefined, directory: undefined,
disabled: false, disabled: false,
@@ -56,9 +32,8 @@ const props = withDefaults(
api: undefined, api: undefined,
resultField: '', resultField: '',
showDescription: false, showDescription: false,
}, });
); const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']);
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props); const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({ const { getStringAccept } = useUploadType({
@@ -112,7 +87,7 @@ watch(
}, },
); );
const handleRemove = async (file: UploadFile) => { async function handleRemove(file: UploadFile) {
if (fileList.value) { if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid); const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1); index !== -1 && fileList.value.splice(index, 1);
@@ -122,9 +97,12 @@ const handleRemove = async (file: UploadFile) => {
emit('change', value); emit('change', value);
emit('delete', file); emit('delete', file);
} }
}; }
async function beforeUpload(file: File) {
const fileContent = await file.text();
emit('returnText', fileContent);
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props; const { maxSize, accept } = props;
const isAct = checkFileType(file, accept); const isAct = checkFileType(file, accept);
if (!isAct) { if (!isAct) {
@@ -141,7 +119,7 @@ const beforeUpload = async (file: File) => {
setTimeout(() => (isLtMsg.value = true), 1000); setTimeout(() => (isLtMsg.value = true), 1000);
} }
return (isAct && !isLt) || Upload.LIST_IGNORE; return (isAct && !isLt) || Upload.LIST_IGNORE;
}; }
async function customRequest(info: UploadRequestOption<any>) { async function customRequest(info: UploadRequestOption<any>) {
let { api } = props; let { api } = props;

View File

@@ -1,3 +1,8 @@
/**
* 默认图片类型
*/
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export function checkFileType(file: File, accepts: string[]) { export function checkFileType(file: File, accepts: string[]) {
if (!accepts || accepts.length === 0) { if (!accepts || accepts.length === 0) {
return true; return true;
@@ -7,11 +12,6 @@ export function checkFileType(file: File, accepts: string[]) {
return reg.test(file.name); return reg.test(file.name);
} }
/**
* 默认图片类型
*/
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export function checkImgType( export function checkImgType(
file: File, file: File,
accepts: string[] = defaultImageAccepts, accepts: string[] = defaultImageAccepts,

View File

@@ -2,15 +2,13 @@
import type { UploadFile, UploadProps } from 'ant-design-vue'; import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface'; import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request'; import type { FileUploadProps } from './typing';
import type { UploadListType } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file'; import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue'; import { ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { isFunction, isObject, isString } from '@vben/utils'; import { isFunction, isObject, isString } from '@vben/utils';
@@ -22,32 +20,7 @@ import { useUpload, useUploadType } from './use-upload';
defineOptions({ name: 'ImageUpload', inheritAttrs: false }); defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults( const props = withDefaults(defineProps<FileUploadProps>(), {
defineProps<{
// 根据后缀,或者其他
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
// 上传的目录
directory?: string;
disabled?: boolean;
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
// 是否显示下面的描述
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [], value: () => [],
directory: undefined, directory: undefined,
disabled: false, disabled: false,
@@ -60,8 +33,7 @@ const props = withDefaults(
api: undefined, api: undefined,
resultField: '', resultField: '',
showDescription: true, showDescription: true,
}, });
);
const emit = defineEmits(['change', 'update:value', 'delete']); const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props); const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false); const isInnerOperate = ref<boolean>(false);
@@ -130,7 +102,7 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
}); });
} }
const handlePreview = async (file: UploadFile) => { async function handlePreview(file: UploadFile) {
if (!file.url && !file.preview) { if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!); file.preview = await getBase64<string>(file.originFileObj!);
} }
@@ -141,9 +113,9 @@ const handlePreview = async (file: UploadFile) => {
previewImage.value.slice( previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1), Math.max(0, previewImage.value.lastIndexOf('/') + 1),
); );
}; }
const handleRemove = async (file: UploadFile) => { async function handleRemove(file: UploadFile) {
if (fileList.value) { if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid); const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1); index !== -1 && fileList.value.splice(index, 1);
@@ -153,14 +125,14 @@ const handleRemove = async (file: UploadFile) => {
emit('change', value); emit('change', value);
emit('delete', file); emit('delete', file);
} }
}; }
const handleCancel = () => { function handleCancel() {
previewOpen.value = false; previewOpen.value = false;
previewTitle.value = ''; previewTitle.value = '';
}; }
const beforeUpload = async (file: File) => { async function beforeUpload(file: File) {
const { maxSize, accept } = props; const { maxSize, accept } = props;
const isAct = checkImgType(file, accept); const isAct = checkImgType(file, accept);
if (!isAct) { if (!isAct) {
@@ -177,7 +149,7 @@ const beforeUpload = async (file: File) => {
setTimeout(() => (isLtMsg.value = true), 1000); setTimeout(() => (isLtMsg.value = true), 1000);
} }
return (isAct && !isLt) || Upload.LIST_IGNORE; return (isAct && !isLt) || Upload.LIST_IGNORE;
}; }
async function customRequest(info: UploadRequestOption<any>) { async function customRequest(info: UploadRequestOption<any>) {
let { api } = props; let { api } = props;
@@ -242,13 +214,13 @@ function getValue() {
v-if="fileList && fileList.length < maxNumber" v-if="fileList && fileList.length < maxNumber"
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
> >
<CloudUpload /> <IconifyIcon icon="lucide:cloud-upload" />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div> <div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
</div> </div>
</Upload> </Upload>
<div <div
v-if="showDescription" v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]" class="mt-2 flex flex-wrap items-center text-sm"
> >
请上传不超过 请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div> <div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>

View File

@@ -1,2 +1,3 @@
export { default as FileUpload } from './file-upload.vue'; export { default as FileUpload } from './file-upload.vue';
export { default as ImageUpload } from './image-upload.vue'; export { default as ImageUpload } from './image-upload.vue';
export { default as InputUpload } from './input-upload.vue';

View File

@@ -1,3 +1,7 @@
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api/infra/file';
export enum UploadResultStatus { export enum UploadResultStatus {
DONE = 'done', DONE = 'done',
ERROR = 'error', ERROR = 'error',
@@ -6,3 +10,28 @@ export enum UploadResultStatus {
} }
export type UploadListType = 'picture' | 'picture-card' | 'text'; export type UploadListType = 'picture' | 'picture-card' | 'text';
export interface FileUploadProps {
// 根据后缀,或者其他
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
// 上传的目录
directory?: string;
disabled?: boolean;
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
// 是否显示下面的描述
showDescription?: boolean;
value?: string | string[];
}

View File

@@ -80,17 +80,17 @@ export function useUploadType({
} }
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构 // TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
export const useUpload = (directory?: string) => { export function useUpload(directory?: string) {
// 后端上传地址 // 后端上传地址
const uploadUrl = getUploadUrl(); const uploadUrl = getUploadUrl();
// 是否使用前端直连上传 // 是否使用前端直连上传
const isClientUpload = const isClientUpload =
UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE; UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
// 重写ElUpload上传方法 // 重写ElUpload上传方法
const httpRequest = async ( async function httpRequest(
file: File, file: File,
onUploadProgress?: AxiosProgressEvent, onUploadProgress?: AxiosProgressEvent,
) => { ) {
// 模式一:前端上传 // 模式一:前端上传
if (isClientUpload) { if (isClientUpload) {
// 1.1 生成文件名称 // 1.1 生成文件名称
@@ -114,20 +114,20 @@ export const useUpload = (directory?: string) => {
// 模式二:后端上传 // 模式二:后端上传
return uploadFile({ file, directory }, onUploadProgress); return uploadFile({ file, directory }, onUploadProgress);
} }
}; }
return { return {
uploadUrl, uploadUrl,
httpRequest, httpRequest,
}; };
}; }
/** /**
* 获得上传 URL * 获得上传 URL
*/ */
export const getUploadUrl = (): string => { export function getUploadUrl(): string {
return `${apiURL}/infra/file/upload`; return `${apiURL}/infra/file/upload`;
}; }
/** /**
* 创建文件信息 * 创建文件信息
@@ -135,7 +135,10 @@ export const getUploadUrl = (): string => {
* @param vo 文件预签名信息 * @param vo 文件预签名信息
* @param file 文件 * @param file 文件
*/ */
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) { function createFile0(
vo: InfraFileApi.FilePresignedUrlResp,
file: File,
): InfraFileApi.File {
const fileVO = { const fileVO = {
configId: vo.configId, configId: vo.configId,
url: vo.url, url: vo.url,

View File

@@ -3,7 +3,10 @@ import type { TableToolbar } from '#/components/table-toolbar';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
// TODO @puhui999这里的注释、目的写下 /**
* vxe 原生工具栏挂载封装
* 解决每个组件使用 vxe-table 组件时都需要写一遍的问题
*/
export function useTableToolbar() { export function useTableToolbar() {
const hiddenSearchBar = ref(false); // 隐藏搜索栏 const hiddenSearchBar = ref(false); // 隐藏搜索栏
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>(); const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
@@ -15,7 +18,7 @@ export function useTableToolbar() {
const table = tableRef.value; const table = tableRef.value;
const tableToolbar = tableToolbarRef.value; const tableToolbar = tableToolbarRef.value;
if (table && tableToolbar) { if (table && tableToolbar) {
// TODO @puhui999通过 nexttick 可以解决么? // 延迟 1 秒,确保 toolbar 组件已经挂载
setTimeout(async () => { setTimeout(async () => {
const toolbar = tableToolbar.getToolbarRef(); const toolbar = tableToolbar.getToolbarRef();
if (!toolbar) { if (!toolbar) {
@@ -29,10 +32,9 @@ export function useTableToolbar() {
watch( watch(
() => tableRef.value, () => tableRef.value,
(val) => { async (val) => {
if (!val || isBound.value) return; if (!val || isBound.value) return;
// TODO @puhui999这里要处理下 promise 的告警么? await bindTableToolbar();
bindTableToolbar();
}, },
{ immediate: true }, { immediate: true },
); );

View File

@@ -4,12 +4,19 @@ import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts'; import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
</script> </script>
<template> <template>
<AuthPageLayout :app-name="appName" :logo="logo"> <AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 --> <!-- 自定义工具栏 -->
<!-- <template #toolbar></template> --> <!-- <template #toolbar></template> -->
</AuthPageLayout> </AuthPageLayout>

View File

@@ -1,20 +1,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts'; import type { NotificationItem } from '@vben/layouts';
import type { SystemTenantApi } from '#/api/system/tenant';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useAccess } from '@vben/access';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui'; import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { useWatermark } from '@vben/hooks'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { AntdProfileOutlined } from '@vben/icons'; import { isTenantEnable, useTabs, useWatermark } from '@vben/hooks';
import {
AntdProfileOutlined,
BookOpenText,
CircleHelp,
MdiGithub,
} from '@vben/icons';
import { import {
BasicLayout, BasicLayout,
Help,
LockScreen, LockScreen,
Notification, Notification,
TenantDropdown,
UserDropdown, UserDropdown,
} from '@vben/layouts'; } from '@vben/layouts';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores'; import { useAccessStore, useUserStore } from '@vben/stores';
import { formatDateTime } from '@vben/utils'; import { formatDateTime, openWindow } from '@vben/utils';
import { message } from 'ant-design-vue';
import { import {
getUnreadNotifyMessageCount, getUnreadNotifyMessageCount,
@@ -22,18 +35,18 @@ import {
updateAllNotifyMessageRead, updateAllNotifyMessageRead,
updateNotifyMessageRead, updateNotifyMessageRead,
} from '#/api/system/notify/message'; } from '#/api/system/notify/message';
import { getSimpleTenantList } from '#/api/system/tenant';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { router } from '#/router'; import { router } from '#/router';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue'; import LoginForm from '#/views/_core/authentication/login.vue';
import Help from './components/help.vue';
import TenantDropdown from './components/tenant-dropdown.vue';
const userStore = useUserStore(); const userStore = useUserStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const { hasAccessByCodes } = useAccess();
const { destroyWatermark, updateWatermark } = useWatermark(); const { destroyWatermark, updateWatermark } = useWatermark();
const { closeOtherTabs, refreshTab } = useTabs();
const notifications = ref<NotificationItem[]>([]); const notifications = ref<NotificationItem[]>([]);
const unreadCount = ref(0); const unreadCount = ref(0);
@@ -142,10 +155,41 @@ function handleNotificationOpen(open: boolean) {
handleNotificationGetUnreadCount(); handleNotificationGetUnreadCount();
} }
// 租户列表
const tenants = ref<SystemTenantApi.Tenant[]>([]);
const tenantEnable = computed(
() => hasAccessByCodes(['system:tenant:visit']) && isTenantEnable(),
);
/** 获取租户列表 */
async function handleGetTenantList() {
if (tenantEnable.value) {
tenants.value = await getSimpleTenantList();
}
}
/** 处理租户切换 */
async function handleTenantChange(tenant: SystemTenantApi.Tenant) {
if (!tenant || !tenant.id) {
message.error('切换租户失败');
return;
}
// 设置访问租户 ID
accessStore.setVisitTenantId(tenant.id as number);
// 关闭其他标签页,只保留当前页
await closeOtherTabs();
// 刷新当前页面
await refreshTab();
// 提示切换成功
message.success(`切换当前租户为: ${tenant.name}`);
}
// ========== 初始化 ========== // ========== 初始化 ==========
onMounted(() => { onMounted(() => {
// 首次加载未读数量 // 首次加载未读数量
handleNotificationGetUnreadCount(); handleNotificationGetUnreadCount();
// 获取租户列表
handleGetTenantList();
// 轮询刷新未读数量 // 轮询刷新未读数量
setInterval( setInterval(
() => { () => {
@@ -198,7 +242,14 @@ watch(
/> />
</template> </template>
<template #header-right-1> <template #header-right-1>
<TenantDropdown class="w-30 mr-2" /> <div v-if="tenantEnable">
<TenantDropdown
class="mr-2"
:tenant-list="tenants"
:visit-tenant-id="accessStore.visitTenantId"
@success="handleTenantChange"
/>
</div>
</template> </template>
<template #extra> <template #extra>
<AuthenticationLoginExpiredModal <AuthenticationLoginExpiredModal

View File

@@ -1,10 +1,248 @@
// todo @芋艿:要不要共享 // todo @芋艿:要不要共享
/** /**
* Created by 千通源码 * Created by 芋道源码
* *
* 枚举类 * 枚举类
*/ */
/**
* AI 平台的枚举
*/
export const AiPlatformEnum = {
TONG_YI: 'TongYi', // 阿里
YI_YAN: 'YiYan', // 百度
DEEP_SEEK: 'DeepSeek', // DeepSeek
ZHI_PU: 'ZhiPu', // 智谱 AI
XING_HUO: 'XingHuo', // 讯飞
SiliconFlow: 'SiliconFlow', // 硅基流动
OPENAI: 'OpenAI',
Ollama: 'Ollama',
STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
MIDJOURNEY: 'Midjourney', // Midjourney
SUNO: 'Suno', // Suno AI
};
export const AiModelTypeEnum = {
CHAT: 1, // 聊天
IMAGE: 2, // 图像
VOICE: 3, // 音频
VIDEO: 4, // 视频
EMBEDDING: 5, // 向量
RERANK: 6, // 重排
};
export interface ImageModel {
key: string;
name: string;
image?: string;
}
export const OtherPlatformEnum: ImageModel[] = [
{
key: AiPlatformEnum.TONG_YI,
name: '通义万相',
},
{
key: AiPlatformEnum.YI_YAN,
name: '百度千帆',
},
{
key: AiPlatformEnum.ZHI_PU,
name: '智谱 AI',
},
{
key: AiPlatformEnum.SiliconFlow,
name: '硅基流动',
},
];
/**
* AI 图像生成状态的枚举
*/
export const AiImageStatusEnum = {
IN_PROGRESS: 10, // 进行中
SUCCESS: 20, // 已完成
FAIL: 30, // 已失败
};
/**
* AI 音乐生成状态的枚举
*/
export const AiMusicStatusEnum = {
IN_PROGRESS: 10, // 进行中
SUCCESS: 20, // 已完成
FAIL: 30, // 已失败
};
/**
* AI 写作类型的枚举
*/
export enum AiWriteTypeEnum {
WRITING = 1, // 撰写
REPLY, // 回复
}
// ========== 【图片 UI】相关的枚举 ==========
export const ImageHotWords = [
'中国旗袍',
'古装美女',
'卡通头像',
'机甲战士',
'童话小屋',
'中国长城',
]; // 图片热词
export const ImageHotEnglishWords = [
'Chinese Cheongsam',
'Ancient Beauty',
'Cartoon Avatar',
'Mech Warrior',
'Fairy Tale Cottage',
'The Great Wall of China',
]; // 图片热词(英文)
export const StableDiffusionSamplers: ImageModel[] = [
{
key: 'DDIM',
name: 'DDIM',
},
{
key: 'DDPM',
name: 'DDPM',
},
{
key: 'K_DPMPP_2M',
name: 'K_DPMPP_2M',
},
{
key: 'K_DPMPP_2S_ANCESTRAL',
name: 'K_DPMPP_2S_ANCESTRAL',
},
{
key: 'K_DPM_2',
name: 'K_DPM_2',
},
{
key: 'K_DPM_2_ANCESTRAL',
name: 'K_DPM_2_ANCESTRAL',
},
{
key: 'K_EULER',
name: 'K_EULER',
},
{
key: 'K_EULER_ANCESTRAL',
name: 'K_EULER_ANCESTRAL',
},
{
key: 'K_HEUN',
name: 'K_HEUN',
},
{
key: 'K_LMS',
name: 'K_LMS',
},
];
export const StableDiffusionStylePresets: ImageModel[] = [
{
key: '3d-model',
name: '3d-model',
},
{
key: 'analog-film',
name: 'analog-film',
},
{
key: 'anime',
name: 'anime',
},
{
key: 'cinematic',
name: 'cinematic',
},
{
key: 'comic-book',
name: 'comic-book',
},
{
key: 'digital-art',
name: 'digital-art',
},
{
key: 'enhance',
name: 'enhance',
},
{
key: 'fantasy-art',
name: 'fantasy-art',
},
{
key: 'isometric',
name: 'isometric',
},
{
key: 'line-art',
name: 'line-art',
},
{
key: 'low-poly',
name: 'low-poly',
},
{
key: 'modeling-compound',
name: 'modeling-compound',
},
// neon-punk origami photographic pixel-art tile-texture
{
key: 'neon-punk',
name: 'neon-punk',
},
{
key: 'origami',
name: 'origami',
},
{
key: 'photographic',
name: 'photographic',
},
{
key: 'pixel-art',
name: 'pixel-art',
},
{
key: 'tile-texture',
name: 'tile-texture',
},
];
export const StableDiffusionClipGuidancePresets: ImageModel[] = [
{
key: 'NONE',
name: 'NONE',
},
{
key: 'FAST_BLUE',
name: 'FAST_BLUE',
},
{
key: 'FAST_GREEN',
name: 'FAST_GREEN',
},
{
key: 'SIMPLE',
name: 'SIMPLE',
},
{
key: 'SLOW',
name: 'SLOW',
},
{
key: 'SLOWER',
name: 'SLOWER',
},
{
key: 'SLOWEST',
name: 'SLOWEST',
},
];
// ========== COMMON 模块 ========== // ========== COMMON 模块 ==========
// 全局通用状态枚举 // 全局通用状态枚举
export const CommonStatusEnum = { export const CommonStatusEnum = {
@@ -92,7 +330,136 @@ export const InfraApiErrorLogProcessStatusEnum = {
DONE: 1, // 已处理 DONE: 1, // 已处理
IGNORE: 2, // 已忽略 IGNORE: 2, // 已忽略
}; };
export interface ImageSize {
key: string;
name?: string;
style: string;
width: string;
height: string;
}
export const Dall3SizeList: ImageSize[] = [
{
key: '1024x1024',
name: '1:1',
width: '1024',
height: '1024',
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
},
{
key: '1024x1792',
name: '3:5',
width: '1024',
height: '1792',
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
},
{
key: '1792x1024',
name: '5:3',
width: '1792',
height: '1024',
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
},
];
export const Dall3Models: ImageModel[] = [
{
key: 'dall-e-3',
name: 'DALL·E 3',
image: `/static/imgs/ai/dall2.jpg`,
},
{
key: 'dall-e-2',
name: 'DALL·E 2',
image: `/static/imgs/ai/dall3.jpg`,
},
];
export const Dall3StyleList: ImageModel[] = [
{
key: 'vivid',
name: '清晰',
image: `/static/imgs/ai/qingxi.jpg`,
},
{
key: 'natural',
name: '自然',
image: `/static/imgs/ai/ziran.jpg`,
},
];
export const MidjourneyModels: ImageModel[] = [
{
key: 'midjourney',
name: 'MJ',
image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png',
},
{
key: 'niji',
name: 'NIJI',
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png',
},
];
export const MidjourneyVersions = [
{
value: '6.0',
label: 'v6.0',
},
{
value: '5.2',
label: 'v5.2',
},
{
value: '5.1',
label: 'v5.1',
},
{
value: '5.0',
label: 'v5.0',
},
{
value: '4.0',
label: 'v4.0',
},
];
export const NijiVersionList = [
{
value: '5',
label: 'v5',
},
];
export const MidjourneySizeList: ImageSize[] = [
{
key: '1:1',
width: '1',
height: '1',
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
},
{
key: '3:4',
width: '3',
height: '4',
style: 'width: 30px; height: 40px;background-color: #dcdcdc;',
},
{
key: '4:3',
width: '4',
height: '3',
style: 'width: 40px; height: 30px;background-color: #dcdcdc;',
},
{
key: '9:16',
width: '9',
height: '16',
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
},
{
key: '16:9',
width: '16',
height: '9',
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
},
];
// ========== PAY 模块 ========== // ========== PAY 模块 ==========
/** /**
* 支付渠道枚举 * 支付渠道枚举
@@ -441,30 +808,6 @@ export const ErpBizType = {
// ========== BPM 模块 ========== // ========== BPM 模块 ==========
export const BpmModelType = {
BPMN: 10, // BPMN 设计器
SIMPLE: 20, // 简易设计器
};
export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20, // 业务表单
};
export const BpmProcessInstanceStatus = {
NOT_START: -1, // 未开始
RUNNING: 1, // 审批中
APPROVE: 2, // 审批通过
REJECT: 3, // 审批不通过
CANCEL: 4, // 已取消
};
export const BpmAutoApproveType = {
NONE: 0, // 不自动通过
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
};
// 候选人策略枚举 用于审批节点。抄送节点 ) // 候选人策略枚举 用于审批节点。抄送节点 )
export enum BpmCandidateStrategyEnum { export enum BpmCandidateStrategyEnum {
/** /**
@@ -594,6 +937,40 @@ export enum BpmNodeTypeEnum {
USER_TASK_NODE = 11, USER_TASK_NODE = 11,
} }
/**
* 流程任务操作按钮
*/
export enum BpmTaskOperationButtonTypeEnum {
/**
* 加签
*/
ADD_SIGN = 5,
/**
* 通过
*/
APPROVE = 1,
/**
* 抄送
*/
COPY = 7,
/**
* 委派
*/
DELEGATE = 4,
/**
* 拒绝
*/
REJECT = 2,
/**
* 退回
*/
RETURN = 6,
/**
* 转办
*/
TRANSFER = 3,
}
/** /**
* 任务状态枚举 * 任务状态枚举
*/ */
@@ -667,3 +1044,147 @@ export enum BpmFieldPermissionType {
*/ */
WRITE = '2', WRITE = '2',
} }
/**
* 流程模型类型
*/
export const BpmModelType = {
BPMN: 10, // BPMN 设计器
SIMPLE: 20, // 简易设计器
};
/**
* 流程模型表单类型
*/
export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20, // 业务表单
};
/**
* 流程实例状态
*/
export const BpmProcessInstanceStatus = {
NOT_START: -1, // 未开始
RUNNING: 1, // 审批中
APPROVE: 2, // 审批通过
REJECT: 3, // 审批不通过
CANCEL: 4, // 已取消
};
/**
* 自动审批类型
*/
export const BpmAutoApproveType = {
NONE: 0, // 不自动通过
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
};
/**
* 审批操作按钮名称
*/
export const OPERATION_BUTTON_NAME = new Map<number, string>();
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.APPROVE, '通过');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.REJECT, '拒绝');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.TRANSFER, '转办');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.DELEGATE, '委派');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.ADD_SIGN, '加签');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.RETURN, '退回');
OPERATION_BUTTON_NAME.set(BpmTaskOperationButtonTypeEnum.COPY, '抄送');
/**
* 流程实例的变量枚举
*/
export enum ProcessVariableEnum {
/**
* 流程定义名称
*/
PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME',
/**
* 发起时间
*/
START_TIME = 'PROCESS_START_TIME',
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID',
}
// ========== 【写作 UI】相关的枚举 ==========
/** 写作点击示例时的数据 */
export const WriteExample = {
write: {
prompt: 'vue',
data: 'Vue.js 是一种用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,易于上手,同时也便于与其他库或已有项目整合。\n\nVue.js 的特点包括:\n- 响应式的数据绑定Vue.js 会自动将数据与 DOM 同步,使得状态管理变得更加简单。\n- 组件化Vue.js 允许开发者通过小型、独立和通常可复用的组件构建大型应用。\n- 虚拟 DOMVue.js 使用虚拟 DOM 实现快速渲染,提高了性能。\n\n在 Vue.js 中,一个典型的应用结构可能包括:\n1. 根实例:每个 Vue 应用都需要一个根实例作为入口点。\n2. 组件系统:可以创建自定义的可复用组件。\n3. 指令:特殊的带有前缀 v- 的属性,为 DOM 元素提供特殊的行为。\n4. 插值:用于文本内容,将数据动态地插入到 HTML。\n5. 计算属性和侦听器:用于处理数据的复杂逻辑和响应数据变化。\n6. 条件渲染:根据条件决定元素的渲染。\n7. 列表渲染:用于显示列表数据。\n8. 事件处理:响应用户交互。\n9. 表单输入绑定:处理表单输入和验证。\n10. 组件生命周期钩子:在组件的不同阶段执行特定的函数。\n\nVue.js 还提供了官方的路由器 Vue Router 和状态管理库 Vuex以支持构建复杂的单页应用SPA。\n\n在开发过程中开发者通常会使用 Vue CLI这是一个强大的命令行工具用于快速生成 Vue 项目脚手架,集成了诸如 Babel、Webpack 等现代前端工具,以及热重载、代码检测等开发体验优化功能。\n\nVue.js 的生态系统还包括大量的第三方库和插件,如 VuetifyUI 组件库、Vue Test Utils测试工具这些都极大地丰富了 Vue.js 的开发生态。\n\n总的来说Vue.js 是一个灵活、高效的前端框架,适合从小型项目到大型企业级应用的开发。它的易用性、灵活性和强大的社区支持使其成为许多开发者的首选框架之一。',
},
reply: {
originalContent: '领导,我想请假',
prompt: '不批',
data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务请及时与我联系。\n\n祝工作顺利。\n\n谢谢。',
},
};
// ========== 【思维导图 UI】相关的枚举 ==========
/** 思维导图已有内容生成示例 */
export const MindMapContentExample = `# Java 技术栈
## 核心技术
### Java SE
### Java EE
## 框架
### Spring
#### Spring Boot
#### Spring MVC
#### Spring Data
### Hibernate
### MyBatis
## 构建工具
### Maven
### Gradle
## 版本控制
### Git
### SVN
## 测试工具
### JUnit
### Mockito
### Selenium
## 应用服务器
### Tomcat
### Jetty
### WildFly
## 数据库
### MySQL
### PostgreSQL
### Oracle
### MongoDB
## 消息队列
### Kafka
### RabbitMQ
### ActiveMQ
## 微服务
### Spring Cloud
### Dubbo
## 容器化
### Docker
### Kubernetes
## 云服务
### AWS
### Azure
### Google Cloud
## 开发工具
### IntelliJ IDEA
### Eclipse
### Visual Studio Code`;

View File

@@ -1,7 +1,8 @@
import type { DefaultOptionType } from 'ant-design-vue/es/select';
// TODO @芋艿:后续再优化 // TODO @芋艿:后续再优化
// TODO @芋艿:可以共享么? // TODO @芋艿:可以共享么?
import type { DictItem } from '#/store';
import { isObject } from '@vben/utils'; import { isObject } from '@vben/utils';
import { useDictStore } from '#/store'; import { useDictStore } from '#/store';
@@ -10,33 +11,103 @@ import { useDictStore } from '#/store';
// 先临时移入到方法中 // 先临时移入到方法中
// const dictStore = useDictStore(); // const dictStore = useDictStore();
// TODO @dhb: antd 组件的 color 类型 /** AntD 组件的颜色类型 */
type ColorType = 'error' | 'info' | 'success' | 'warning'; type ColorType = 'error' | 'info' | 'success' | 'warning';
/** 字典值类型 */
type DictValueType = 'boolean' | 'number' | 'string';
/** 基础字典数据类型 */
export interface DictDataType { export interface DictDataType {
dictType: string; dictType?: string;
label: string; label: string;
value: boolean | number | string; value: boolean | number | string;
colorType: ColorType; colorType?: string;
cssClass: string; cssClass?: string;
} }
/** 数字类型字典数据 */
export interface NumberDictDataType extends DictDataType { export interface NumberDictDataType extends DictDataType {
value: number; value: number;
} }
/** 字符串类型字典数据 */
export interface StringDictDataType extends DictDataType { export interface StringDictDataType extends DictDataType {
value: string; value: string;
} }
/** 布尔类型字典数据 */
export interface BooleanDictDataType extends DictDataType {
value: boolean;
}
/** 字典缓存管理器 */
class DictCacheManager {
private cache = new Map<string, DictDataType[]>();
private maxCacheSize = 100; // 最大缓存数量
/** 清空缓存 */
clear(): void {
this.cache.clear();
}
/** 删除指定字典类型的缓存 */
delete(dictType: string): void {
const keysToDelete = [];
for (const key of this.cache.keys()) {
if (key.startsWith(`${dictType}:`)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach((key) => this.cache.delete(key));
}
/** 获取缓存数据 */
get(dictType: string, valueType: DictValueType): DictDataType[] | undefined {
return this.cache.get(this.getCacheKey(dictType, valueType));
}
/** 设置缓存数据 */
set(dictType: string, valueType: DictValueType, data: DictDataType[]): void {
const key = this.getCacheKey(dictType, valueType);
// 如果缓存数量超过限制,删除最早的缓存
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, data);
}
/** 获取缓存键 */
private getCacheKey(dictType: string, valueType: DictValueType): string {
return `${dictType}:${valueType}`;
}
}
/** 字典缓存实例 */
const dictCache = new DictCacheManager();
/** 值转换器映射 */
const valueConverters: Record<
DictValueType,
(value: any) => boolean | number | string
> = {
boolean: (value: any) => `${value}` === 'true',
number: (value: any) => Number.parseInt(`${value}`, 10),
string: (value: any) => `${value}`,
};
/** /**
* 获取字典标签 * 获取字典标签
*
* @param dictType 字典类型 * @param dictType 字典类型
* @param value 字典值 * @param value 字典值
* @returns 字典标签 * @returns 字典标签
*/ */
function getDictLabel(dictType: string, value: any) { function getDictLabel(dictType: string, value: any): string {
const dictStore = useDictStore(); const dictStore = useDictStore();
const dictObj = dictStore.getDictData(dictType, value); const dictObj = dictStore.getDictData(dictType, value);
return isObject(dictObj) ? dictObj.label : ''; return isObject(dictObj) ? dictObj.label : '';
@@ -44,111 +115,80 @@ function getDictLabel(dictType: string, value: any) {
/** /**
* 获取字典对象 * 获取字典对象
*
* @param dictType 字典类型 * @param dictType 字典类型
* @param value 字典值 * @param value 字典值
* @returns 字典对象 * @returns 字典对象
*/ */
function getDictObj(dictType: string, value: any) { function getDictObj(dictType: string, value: any): DictItem | null {
const dictStore = useDictStore(); const dictStore = useDictStore();
const dictObj = dictStore.getDictData(dictType, value); const dictObj = dictStore.getDictData(dictType, value);
return isObject(dictObj) ? dictObj : null; return isObject(dictObj) ? dictObj : null;
} }
/** /**
* 获取字典数组 用于select radio 等 * 获取字典数组 - 优化版本,支持缓存和泛型
*
* @param dictType 字典类型 * @param dictType 字典类型
* @param valueType 字典值类型,默认 string 类型 * @param valueType 字典值类型,默认 string 类型
* @returns 字典数组 * @returns 字典数组
*/ */
// TODO @puhui999貌似可以定义一个类型不使用 any[] function getDictOptions<T extends DictValueType = 'string'>(
function getDictOptions(
dictType: string, dictType: string,
valueType: 'boolean' | 'number' | 'string' = 'string', valueType: T = 'string' as T,
): any[] { ): T extends 'number'
? NumberDictDataType[]
: T extends 'boolean'
? BooleanDictDataType[]
: StringDictDataType[] {
// 检查缓存
const cachedData = dictCache.get(dictType, valueType);
if (cachedData) {
return cachedData as any;
}
const dictStore = useDictStore(); const dictStore = useDictStore();
const dictOpts = dictStore.getDictOptions(dictType); const dictOpts = dictStore.getDictOptions(dictType);
const dictOptions: DefaultOptionType = [];
if (dictOpts.length > 0) { if (dictOpts.length === 0) {
let dictValue: boolean | number | string = ''; return [] as any;
dictOpts.forEach((d) => {
switch (valueType) {
case 'boolean': {
dictValue = `${d.value}` === 'true';
break;
} }
case 'number': {
dictValue = Number.parseInt(`${d.value}`); const converter = valueConverters[valueType];
break; const dictOptions: DictDataType[] = dictOpts.map((d: DictItem) => ({
} value: converter(d.value),
case 'string': {
dictValue = `${d.value}`;
break;
}
// No default
}
dictOptions.push({
value: dictValue,
label: d.label, label: d.label,
}); colorType: d.colorType,
}); cssClass: d.cssClass,
} }));
return dictOptions.length > 0 ? dictOptions : [];
// 缓存结果
dictCache.set(dictType, valueType, dictOptions);
return dictOptions as any;
} }
// TODO @dhb52下面的一系列方法看看能不能复用 getDictOptions 方法 /**
export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { * 清空字典缓存
// 获得通用的 DictDataType 列表 */
const dictOptions = getDictOptions(dictType) as DictDataType[]; export const clearDictCache = (): void => {
// 转换成 number 类型的 NumberDictDataType 类型 dictCache.clear();
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时el-option 的 key 会告警
const dictOption: NumberDictDataType[] = [];
dictOptions.forEach((dict: DictDataType) => {
dictOption.push({
...dict,
value: Number.parseInt(`${dict.value}`),
});
});
return dictOption;
}; };
// TODO @dhb52下面的一系列方法看看能不能复用 getDictOptions 方法 /**
export const getStrDictOptions = (dictType: string) => { * 删除指定字典类型的缓存
// 获得通用的 DictDataType 列表 * @param dictType 字典类型
const dictOptions = getDictOptions(dictType) as DictDataType[]; */
// 转换成 string 类型的 StringDictDataType 类型 export const deleteDictCache = (dictType: string): void => {
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时el-option 的 key 会告警 dictCache.delete(dictType);
const dictOption: StringDictDataType[] = [];
dictOptions.forEach((dict: DictDataType) => {
dictOption.push({
...dict,
value: `${dict.value}`,
});
});
return dictOption;
};
// TODO @dhb52下面的一系列方法看看能不能复用 getDictOptions 方法
export const getBoolDictOptions = (dictType: string) => {
const dictOption: DictDataType[] = [];
const dictOptions = getDictOptions(dictType) as DictDataType[];
dictOptions.forEach((dict: DictDataType) => {
dictOption.push({
...dict,
value: `${dict.value}` === 'true',
});
});
return dictOption;
}; };
/** 字典类型枚举 - 按模块分组和排序 */
enum DICT_TYPE { enum DICT_TYPE {
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式 AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态 AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态 AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
// ========== AI - 人工智能模块 ========== // ========== AI - 人工智能模块 ==========
AI_PLATFORM = 'ai_platform', // AI 平台 AI_PLATFORM = 'ai_platform', // AI 平台
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式 AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言 AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度 AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
@@ -282,4 +322,12 @@ enum DICT_TYPE {
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
USER_TYPE = 'user_type', USER_TYPE = 'user_type',
} }
export { DICT_TYPE, getDictLabel, getDictObj, getDictOptions };
export {
type ColorType,
DICT_TYPE,
type DictValueType,
getDictLabel,
getDictObj,
getDictOptions,
};

View File

@@ -5,14 +5,12 @@
import { isRef } from 'vue'; import { isRef } from 'vue';
// 编码表单 Conf // 编码表单 Conf
export const encodeConf = (designerRef: object) => { export const encodeConf = (designerRef: any) => {
// @ts-ignore designerRef.value is dynamically added by form-create-designer
return JSON.stringify(designerRef.value.getOption()); return JSON.stringify(designerRef.value.getOption());
}; };
// 编码表单 Fields // 编码表单 Fields
export const encodeFields = (designerRef: object) => { export const encodeFields = (designerRef: any) => {
// @ts-ignore designerRef.value is dynamically added by form-create-designer
const rule = JSON.parse(designerRef.value.getJson()); const rule = JSON.parse(designerRef.value.getJson());
const fields: string[] = []; const fields: string[] = [];
rule.forEach((item: unknown) => { rule.forEach((item: unknown) => {
@@ -32,33 +30,29 @@ export const decodeFields = (fields: string[]) => {
// 设置表单的 Conf 和 Fields适用 FcDesigner 场景 // 设置表单的 Conf 和 Fields适用 FcDesigner 场景
export const setConfAndFields = ( export const setConfAndFields = (
designerRef: object, designerRef: any,
conf: string, conf: string,
fields: string, fields: string | string[],
) => { ) => {
// @ts-ignore designerRef.value is dynamically added by form-create-designer
designerRef.value.setOption(JSON.parse(conf)); designerRef.value.setOption(JSON.parse(conf));
// @ts-ignore designerRef.value is dynamically added by form-create-designer // 处理 fields 参数类型,确保传入 decodeFields 的是 string[] 类型
designerRef.value.setRule(decodeFields(fields)); const fieldsArray = Array.isArray(fields) ? fields : [fields];
designerRef.value.setRule(decodeFields(fieldsArray));
}; };
// 设置表单的 Conf 和 Fields适用 form-create 场景 // 设置表单的 Conf 和 Fields适用 form-create 场景
export const setConfAndFields2 = ( export const setConfAndFields2 = (
detailPreview: object, detailPreview: any,
conf: string, conf: string,
fields: string[], fields: string[],
value?: object, value?: any,
) => { ) => {
if (isRef(detailPreview)) { if (isRef(detailPreview)) {
// @ts-ignore detailPreview.value is dynamically added by form-create-designer
detailPreview = detailPreview.value; detailPreview = detailPreview.value;
} }
// @ts-ignore detailPreview properties are dynamically added by form-create-designer
detailPreview.option = JSON.parse(conf); detailPreview.option = JSON.parse(conf);
// @ts-ignore detailPreview properties are dynamically added by form-create-designer
detailPreview.rule = decodeFields(fields); detailPreview.rule = decodeFields(fields);
if (value) { if (value) {
// @ts-ignore detailPreview properties are dynamically added by form-create-designer
detailPreview.value = value; detailPreview.value = value;
} }
}; };

View File

@@ -1,32 +0,0 @@
/**
* 将毫秒转换成时间字符串。例如说xx 分钟
*
* @param ms 毫秒
* @returns {string} 字符串
*/
// TODO @xingyu这个要融合到哪里去 date 么?
export function formatPast2(ms: number): string {
// 定义时间单位常量,便于维护
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
// 计算各时间单位
const day = Math.floor(ms / DAY);
const hour = Math.floor((ms % DAY) / HOUR);
const minute = Math.floor((ms % HOUR) / MINUTE);
const second = Math.floor((ms % MINUTE) / SECOND);
// 根据时间长短返回不同格式
if (day > 0) {
return `${day}${hour} 小时 ${minute} 分钟`;
}
if (hour > 0) {
return `${hour} 小时 ${minute} 分钟`;
}
if (minute > 0) {
return `${minute} 分钟`;
}
return second > 0 ? `${second}` : `${0}`;
}

View File

@@ -1,7 +1,5 @@
export * from './constants'; export * from './constants';
export * from './dict'; export * from './dict';
export * from './formatTime';
export * from './formCreate'; export * from './formCreate';
export * from './rangePickerProps'; export * from './rangePickerProps';
export * from './routerHelper'; export * from './routerHelper';
export * from './validator';

View File

@@ -1,7 +1,7 @@
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
const modules = import.meta.glob('../views/**/*.{vue,tsx}'); const modules = import.meta.glob('../views/**/*.{vue,tsx}');
// TODO @xingyu这个要不要融合到哪个 router util 里?
/** /**
* 注册一个异步组件 * 注册一个异步组件
* @param componentPath 例:/bpm/oa/leave/detail * @param componentPath 例:/bpm/oa/leave/detail

View File

@@ -1,17 +0,0 @@
// 参数校验,对标 Hutool 的 Validator 工具类
/** 手机号正则表达式(中国) */
const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/;
/**
* 验证是否为手机号码(中国)
*
* @param value 值
* @returns 是否为手机号码(中国)
*/
export function isMobile(value?: null | string): boolean {
if (!value) {
return false;
}
return MOBILE_REGEX.test(value);
}

View File

@@ -19,7 +19,7 @@ const authStore = useAuthStore();
const activeName = ref('basicInfo'); const activeName = ref('basicInfo');
/** 加载个人信息 */ /** 加载个人信息 */
const profile = ref<SystemUserProfileApi.UserProfileRespVO>(); const profile = ref<SystemUserProfileApi.UserProfileResp>();
async function loadProfile() { async function loadProfile() {
profile.value = await getUserProfile(); profile.value = await getUserProfile();
} }

View File

@@ -14,7 +14,7 @@ import { updateUserProfile } from '#/api/system/user/profile';
import { DICT_TYPE, getDictOptions } from '#/utils'; import { DICT_TYPE, getDictOptions } from '#/utils';
const props = defineProps<{ const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO; profile?: SystemUserProfileApi.UserProfileResp;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'success'): void; (e: 'success'): void;
@@ -77,7 +77,7 @@ async function handleSubmit(values: Recordable<any>) {
try { try {
formApi.setLoading(true); formApi.setLoading(true);
// 提交表单 // 提交表单
await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReqVO); await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReq);
// 关闭并提示 // 关闭并提示
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success($t('ui.actionMessage.operationSuccess'));
@@ -101,7 +101,7 @@ watch(
</script> </script>
<template> <template>
<div class="mt-16px md:w-full lg:w-1/2 2xl:w-2/5"> <div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
<Form /> <Form />
</div> </div>
</template> </template>

View File

@@ -14,7 +14,7 @@ import { CropperAvatar } from '#/components/cropper';
import { useUpload } from '#/components/upload/use-upload'; import { useUpload } from '#/components/upload/use-upload';
const props = defineProps<{ const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO; profile?: SystemUserProfileApi.UserProfileResp;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -88,7 +88,7 @@ async function handleSubmit(values: Recordable<any>) {
</script> </script>
<template> <template>
<div class="mt-[16px] md:w-full lg:w-1/2 2xl:w-2/5"> <div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
<Form /> <Form />
</div> </div>
</template> </template>

View File

@@ -181,9 +181,7 @@ onMounted(() => {
/> />
<div class="flex flex-1 items-center justify-between"> <div class="flex flex-1 items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
<h4 <h4 class="mb-1 text-sm text-black/85 dark:text-white/85">
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
>
{{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }} {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
</h4> </h4>
<span class="text-black/45 dark:text-white/45"> <span class="text-black/45 dark:text-white/45">
@@ -191,9 +189,9 @@ onMounted(() => {
{{ item.socialUser?.nickname || item.socialUser?.openid }} {{ item.socialUser?.nickname || item.socialUser?.openid }}
</template> </template>
<template v-else> <template v-else>
绑定{{ 绑定
getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
}}账号 账号
</template> </template>
</span> </span>
</div> </div>

View File

@@ -1,13 +1,15 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log'; import type { DescriptionItemSchema } from '#/components/description';
import { useAccess } from '@vben/access'; import { h } from 'vue';
import { JsonViewer } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
return [ return [
@@ -70,24 +72,19 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraApiAccessLogApi.ApiAccessLog>( export function useGridColumns(): VxeTableGridOptions['columns'] {
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
title: '日志编号', title: '日志编号',
minWidth: 100,
}, },
{ {
field: 'userId', field: 'userId',
title: '用户编号', title: '用户编号',
minWidth: 100,
}, },
{ {
field: 'userType', field: 'userType',
title: '用户类型', title: '用户类型',
minWidth: 120,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.USER_TYPE }, props: { type: DICT_TYPE.USER_TYPE },
@@ -96,34 +93,28 @@ export function useGridColumns<T = InfraApiAccessLogApi.ApiAccessLog>(
{ {
field: 'applicationName', field: 'applicationName',
title: '应用名', title: '应用名',
minWidth: 150,
}, },
{ {
field: 'requestMethod', field: 'requestMethod',
title: '请求方法', title: '请求方法',
minWidth: 80,
}, },
{ {
field: 'requestUrl', field: 'requestUrl',
title: '请求地址', title: '请求地址',
minWidth: 300,
}, },
{ {
field: 'beginTime', field: 'beginTime',
title: '请求时间', title: '请求时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'duration', field: 'duration',
title: '执行时长', title: '执行时长',
minWidth: 120, formatter: ({ cellValue }) => `${cellValue} ms`,
formatter: ({ row }) => `${row.duration} ms`,
}, },
{ {
field: 'resultCode', field: 'resultCode',
title: '操作结果', title: '操作结果',
minWidth: 150,
formatter: ({ row }) => { formatter: ({ row }) => {
return row.resultCode === 0 ? '成功' : `失败(${row.resultMsg})`; return row.resultCode === 0 ? '成功' : `失败(${row.resultMsg})`;
}, },
@@ -131,43 +122,122 @@ export function useGridColumns<T = InfraApiAccessLogApi.ApiAccessLog>(
{ {
field: 'operateModule', field: 'operateModule',
title: '操作模块', title: '操作模块',
minWidth: 150,
}, },
{ {
field: 'operateName', field: 'operateName',
title: '操作名', title: '操作名',
minWidth: 220,
}, },
{ {
field: 'operateType', field: 'operateType',
title: '操作类型', title: '操作类型',
minWidth: 120,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.INFRA_OPERATE_TYPE }, props: { type: DICT_TYPE.INFRA_OPERATE_TYPE },
}, },
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 80, width: 80,
align: 'center',
fixed: 'right', fixed: 'right',
cellRender: { slots: { default: 'actions' },
attrs: { },
nameField: 'id', ];
nameTitle: 'API访问日志', }
onClick: onActionClick,
}, /** 详情页的字段 */
name: 'CellOperation', export function useDetailSchema(): DescriptionItemSchema[] {
options: [ return [
{ {
code: 'detail', field: 'id',
text: '详情', label: '日志编号',
show: hasAccessByCodes(['infra:api-access-log:query']), },
}, {
], field: 'traceId',
}, label: '链路追踪',
},
{
field: 'applicationName',
label: '应用名',
},
{
field: 'userId',
label: '用户Id',
},
{
field: 'userType',
label: '用户类型',
content: (data) => {
return h(DictTag, {
type: DICT_TYPE.USER_TYPE,
value: data.userType,
});
},
},
{
field: 'userIp',
label: '用户IP',
},
{
field: 'userAgent',
label: '用户UA',
},
{
field: 'requestMethod',
label: '请求信息',
content: (data) => {
return `${data.requestMethod} ${data.requestUrl}`;
},
},
{
field: 'requestParams',
label: '请求参数',
content: (data) => {
return h(JsonViewer, {
value: data.requestParams,
previewMode: true,
});
},
},
{
field: 'responseBody',
label: '请求结果',
},
{
field: 'beginTime',
label: '请求时间',
content: (data) => {
return `${formatDateTime(data?.beginTime)} ~ ${formatDateTime(data?.endTime)}`;
},
},
{
field: 'duration',
label: '请求耗时',
},
{
field: 'resultCode',
label: '操作结果',
content: (data) => {
return data.resultCode === 0
? '成功'
: `失败 | ${data.resultCode} | ${data.resultMsg}`;
},
},
{
field: 'operateModule',
label: '操作模块',
},
{
field: 'operateName',
label: '操作名',
},
{
field: 'operateType',
label: '操作类型',
content: (data) =>
h(DictTag, {
type: DICT_TYPE.INFRA_OPERATE_TYPE,
value: data?.operateType,
}),
}, },
]; ];
} }

View File

@@ -1,22 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log'; import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
import { Page, useVbenModal } from '@vben/common-ui'; import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
exportApiAccessLog, exportApiAccessLog,
getApiAccessLogPage, getApiAccessLogPage,
} from '#/api/infra/api-access-log'; } from '#/api/infra/api-access-log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@@ -33,35 +26,22 @@ function onRefresh() {
} }
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function handleExport() {
const data = await exportApiAccessLog(await gridApi.formApi.getValues()); const data = await exportApiAccessLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: 'API 访问日志.xls', source: data }); downloadFileFromBlobPart({ fileName: 'API 访问日志.xls', source: data });
} }
/** 查看 API 访问日志详情 */ /** 查看 API 访问日志详情 */
function onDetail(row: InfraApiAccessLogApi.ApiAccessLog) { function handleDetail(row: InfraApiAccessLogApi.ApiAccessLog) {
detailModalApi.setData(row).open(); detailModalApi.setData(row).open();
} }
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraApiAccessLogApi.ApiAccessLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -95,15 +75,30 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" /> <DetailModal @success="onRefresh" />
<Grid table-title="API 访问日志列表"> <Grid table-title="API 访问日志列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
type="primary" :actions="[
class="ml-2" {
@click="onExport" label: $t('ui.actionTitle.export'),
v-access:code="['infra:api-access-log:export']" type: 'primary',
> icon: ACTION_ICON.DOWNLOAD,
<Download class="size-5" /> auth: ['infra:api-access-log:export'],
{{ $t('ui.actionTitle.export') }} onClick: handleExport,
</Button> },
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['infra:api-access-log:query'],
onClick: handleDetail.bind(null, row),
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -4,12 +4,10 @@ import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
import { ref } from 'vue'; import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions } from 'ant-design-vue'; import { useDescription } from '#/components/description';
import { DictTag } from '#/components/dict-tag'; import { useDetailSchema } from '../data';
import { DICT_TYPE } from '#/utils';
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>(); const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
@@ -32,6 +30,15 @@ const [Modal, modalApi] = useVbenModal({
} }
}, },
}); });
const [Description] = useDescription({
componentProps: {
bordered: true,
column: 1,
class: 'mx-4',
},
schema: useDetailSchema(),
});
</script> </script>
<template> <template>
@@ -41,66 +48,6 @@ const [Modal, modalApi] = useVbenModal({
:show-cancel-button="false" :show-cancel-button="false"
:show-confirm-button="false" :show-confirm-button="false"
> >
<Descriptions <Description :data="formData" />
bordered
:column="1"
size="middle"
class="mx-4"
:label-style="{ width: '110px' }"
>
<Descriptions.Item label="日志编号">
{{ formData?.id }}
</Descriptions.Item>
<Descriptions.Item label="链路追踪">
{{ formData?.traceId }}
</Descriptions.Item>
<Descriptions.Item label="应用名">
{{ formData?.applicationName }}
</Descriptions.Item>
<Descriptions.Item label="用户信息">
{{ formData?.userId }}
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
</Descriptions.Item>
<Descriptions.Item label="用户IP">
{{ formData?.userIp }}
</Descriptions.Item>
<Descriptions.Item label="用户UA">
{{ formData?.userAgent }}
</Descriptions.Item>
<Descriptions.Item label="请求信息">
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
</Descriptions.Item>
<Descriptions.Item label="请求参数">
{{ formData?.requestParams }}
</Descriptions.Item>
<Descriptions.Item label="请求结果">
{{ formData?.responseBody }}
</Descriptions.Item>
<Descriptions.Item label="请求时间">
{{ formatDateTime(formData?.beginTime || '') }} ~
{{ formatDateTime(formData?.endTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="请求耗时">
{{ formData?.duration }} ms
</Descriptions.Item>
<Descriptions.Item label="操作结果">
<div v-if="formData?.resultCode === 0">正常</div>
<div v-else-if="formData && formData?.resultCode > 0">
失败 | {{ formData?.resultCode }} | {{ formData?.resultMsg }}
</div>
</Descriptions.Item>
<Descriptions.Item label="操作模块">
{{ formData?.operateModule }}
</Descriptions.Item>
<Descriptions.Item label="操作名">
{{ formData?.operateName }}
</Descriptions.Item>
<Descriptions.Item label="操作类型">
<DictTag
:type="DICT_TYPE.INFRA_OPERATE_TYPE"
:value="formData?.operateType"
/>
</Descriptions.Item>
</Descriptions>
</Modal> </Modal>
</template> </template>

View File

@@ -1,9 +1,13 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log'; import type { DescriptionItemSchema } from '#/components/description';
import { useAccess } from '@vben/access'; import { h } from 'vue';
import { JsonViewer } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { import {
DICT_TYPE, DICT_TYPE,
getDictOptions, getDictOptions,
@@ -11,8 +15,6 @@ import {
InfraApiErrorLogProcessStatusEnum, InfraApiErrorLogProcessStatusEnum,
} from '#/utils'; } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
return [ return [
@@ -71,24 +73,19 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraApiErrorLogApi.ApiErrorLog>( export function useGridColumns(): VxeTableGridOptions['columns'] {
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
title: '日志编号', title: '日志编号',
minWidth: 100,
}, },
{ {
field: 'userId', field: 'userId',
title: '用户编号', title: '用户编号',
minWidth: 100,
}, },
{ {
field: 'userType', field: 'userType',
title: '用户类型', title: '用户类型',
minWidth: 120,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.USER_TYPE }, props: { type: DICT_TYPE.USER_TYPE },
@@ -97,78 +94,135 @@ export function useGridColumns<T = InfraApiErrorLogApi.ApiErrorLog>(
{ {
field: 'applicationName', field: 'applicationName',
title: '应用名', title: '应用名',
minWidth: 150,
}, },
{ {
field: 'requestMethod', field: 'requestMethod',
title: '请求方法', title: '请求方法',
minWidth: 80,
}, },
{ {
field: 'requestUrl', field: 'requestUrl',
title: '请求地址', title: '请求地址',
minWidth: 200,
}, },
{ {
field: 'exceptionTime', field: 'exceptionTime',
title: '异常发生时间', title: '异常发生时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'exceptionName', field: 'exceptionName',
title: '异常名', title: '异常名',
minWidth: 180,
}, },
{ {
field: 'processStatus', field: 'processStatus',
title: '处理状态', title: '处理状态',
minWidth: 120,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS }, props: { type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS },
}, },
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 200, width: 200,
align: 'center',
fixed: 'right', fixed: 'right',
cellRender: { slots: { default: 'actions' },
attrs: {
nameField: 'id',
nameTitle: 'API错误日志',
onClick: onActionClick,
}, },
name: 'CellOperation', ];
options: [ }
/** 详情页的字段 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{ {
code: 'detail', field: 'id',
text: '详情', label: '日志编号',
show: hasAccessByCodes(['infra:api-error-log:query']),
}, },
{ {
code: 'done', field: 'traceId',
text: '已处理', label: '链路追踪',
show: (row: InfraApiErrorLogApi.ApiErrorLog) => { },
return ( {
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT && field: 'applicationName',
hasAccessByCodes(['infra:api-error-log:update-status']) label: '应用名',
); },
{
field: 'userId',
label: '用户Id',
},
{
field: 'userType',
label: '用户类型',
content: (data) => {
return h(DictTag, {
type: DICT_TYPE.USER_TYPE,
value: data.userType,
});
}, },
}, },
{ {
code: 'ignore', field: 'userIp',
text: '已忽略', label: '用户IP',
show: (row: InfraApiErrorLogApi.ApiErrorLog) => { },
return ( {
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT && field: 'userAgent',
hasAccessByCodes(['infra:api-error-log:update-status']) label: '用户UA',
); },
{
field: 'requestMethod',
label: '请求信息',
content: (data) => {
return `${data.requestMethod} ${data.requestUrl}`;
}, },
}, },
], {
field: 'requestParams',
label: '请求参数',
content: (data) => {
return h(JsonViewer, {
value: data.requestParams,
previewMode: true,
});
},
},
{
field: 'exceptionTime',
label: '异常时间',
content: (data) => {
return formatDateTime(data?.exceptionTime) as string;
},
},
{
field: 'exceptionName',
label: '异常名',
},
{
field: 'exceptionStackTrace',
label: '异常堆栈',
content: (data) => {
return h(JsonViewer, {
value: data.exceptionStackTrace,
previewMode: true,
});
},
},
{
field: 'processStatus',
label: '处理状态',
content: (data) => {
return h(DictTag, {
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
value: data?.processStatus,
});
},
},
{
field: 'processUserId',
label: '处理人',
},
{
field: 'processTime',
label: '处理时间',
content: (data) => {
return formatDateTime(data?.processTime) as string;
}, },
}, },
]; ];

View File

@@ -1,23 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log'; import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
import { confirm, Page, useVbenModal } from '@vben/common-ui'; import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
exportApiErrorLog, exportApiErrorLog,
getApiErrorLogPage, getApiErrorLogPage,
updateApiErrorLogStatus, updateApiErrorLogStatus,
} from '#/api/infra/api-error-log'; } from '#/api/infra/api-error-log';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { InfraApiErrorLogProcessStatusEnum } from '#/utils'; import { InfraApiErrorLogProcessStatusEnum } from '#/utils';
@@ -35,18 +30,18 @@ function onRefresh() {
} }
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function handleExport() {
const data = await exportApiErrorLog(await gridApi.formApi.getValues()); const data = await exportApiErrorLog(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: 'API 错误日志.xls', source: data }); downloadFileFromBlobPart({ fileName: 'API 错误日志.xls', source: data });
} }
/** 查看 API 错误日志详情 */ /** 查看 API 错误日志详情 */
function onDetail(row: InfraApiErrorLogApi.ApiErrorLog) { function handleDetail(row: InfraApiErrorLogApi.ApiErrorLog) {
detailModalApi.setData(row).open(); detailModalApi.setData(row).open();
} }
/** 处理已处理 / 已忽略的操作 */ /** 处理已处理 / 已忽略的操作 */
async function onProcess(id: number, processStatus: number) { async function handleProcess(id: number, processStatus: number) {
confirm({ confirm({
content: `确认标记为${InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'}?`, content: `确认标记为${InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'}?`,
}).then(async () => { }).then(async () => {
@@ -57,33 +52,12 @@ async function onProcess(id: number, processStatus: number) {
}); });
} }
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraApiErrorLogApi.ApiErrorLog>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
case 'done': {
onProcess(row.id, InfraApiErrorLogProcessStatusEnum.DONE);
break;
}
case 'ignore': {
onProcess(row.id, InfraApiErrorLogProcessStatusEnum.IGNORE);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -117,15 +91,54 @@ const [Grid, gridApi] = useVbenVxeGrid({
<DetailModal @success="onRefresh" /> <DetailModal @success="onRefresh" />
<Grid table-title="API 错误日志列表"> <Grid table-title="API 错误日志列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
type="primary" :actions="[
class="ml-2" {
@click="onExport" label: $t('ui.actionTitle.export'),
v-access:code="['infra:api-error-log:export']" type: 'primary',
> icon: ACTION_ICON.DOWNLOAD,
<Download class="size-5" /> auth: ['infra:api-error-log:export'],
{{ $t('ui.actionTitle.export') }} onClick: handleExport,
</Button> },
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.detail'),
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['infra:api-error-log:query'],
onClick: handleDetail.bind(null, row),
},
{
label: '已处理',
type: 'link',
auth: ['infra:api-error-log:update-status'],
ifShow:
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
onClick: handleProcess.bind(
null,
row.id,
InfraApiErrorLogProcessStatusEnum.DONE,
),
},
{
label: '已忽略',
type: 'link',
auth: ['infra:api-error-log:update-status'],
ifShow:
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
onClick: handleProcess.bind(
null,
row.id,
InfraApiErrorLogProcessStatusEnum.IGNORE,
),
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -4,12 +4,10 @@ import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
import { ref } from 'vue'; import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions, Input } from 'ant-design-vue'; import { useDescription } from '#/components/description';
import { DictTag } from '#/components/dict-tag'; import { useDetailSchema } from '../data';
import { DICT_TYPE } from '#/utils';
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>(); const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
@@ -32,6 +30,15 @@ const [Modal, modalApi] = useVbenModal({
} }
}, },
}); });
const [Description] = useDescription({
componentProps: {
bordered: true,
column: 1,
class: 'mx-4',
},
schema: useDetailSchema(),
});
</script> </script>
<template> <template>
@@ -41,63 +48,6 @@ const [Modal, modalApi] = useVbenModal({
:show-cancel-button="false" :show-cancel-button="false"
:show-confirm-button="false" :show-confirm-button="false"
> >
<Descriptions <Description :data="formData" />
bordered
:column="1"
size="middle"
class="mx-4"
:label-style="{ width: '110px' }"
>
<Descriptions.Item label="日志编号">
{{ formData?.id }}
</Descriptions.Item>
<Descriptions.Item label="链路追踪">
{{ formData?.traceId }}
</Descriptions.Item>
<Descriptions.Item label="应用名">
{{ formData?.applicationName }}
</Descriptions.Item>
<Descriptions.Item label="用户编号">
{{ formData?.userId }}
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
</Descriptions.Item>
<Descriptions.Item label="用户IP">
{{ formData?.userIp }}
</Descriptions.Item>
<Descriptions.Item label="用户UA">
{{ formData?.userAgent }}
</Descriptions.Item>
<Descriptions.Item label="请求信息">
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
</Descriptions.Item>
<Descriptions.Item label="请求参数">
{{ formData?.requestParams }}
</Descriptions.Item>
<Descriptions.Item label="异常时间">
{{ formatDateTime(formData?.exceptionTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="异常名">
{{ formData?.exceptionName }}
</Descriptions.Item>
<Descriptions.Item v-if="formData?.exceptionStackTrace" label="异常堆栈">
<Input.TextArea
:value="formData?.exceptionStackTrace"
:auto-size="{ maxRows: 20 }"
readonly
/>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<DictTag
:type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
:value="formData?.processStatus"
/>
</Descriptions.Item>
<Descriptions.Item v-if="formData?.processUserId" label="处理人">
{{ formData?.processUserId }}
</Descriptions.Item>
<Descriptions.Item v-if="formData?.processTime" label="处理时间">
{{ formatDateTime(formData?.processTime || '') }}
</Descriptions.Item>
</Descriptions>
</Modal> </Modal>
</template> </template>

View File

@@ -60,34 +60,35 @@ const formData = ref(''); // 表单数据
useFormCreateDesigner(designer); // 表单设计器增强 useFormCreateDesigner(designer); // 表单设计器增强
/** 打开弹窗 */ /** 打开弹窗 */
const openModel = (title: string) => { function openModel(title: string) {
dialogVisible.value = true; dialogVisible.value = true;
dialogTitle.value = title; dialogTitle.value = title;
modalApi.open(); modalApi.open();
}; }
/** 生成 JSON */ /** 生成 JSON */
const showJson = () => { function showJson() {
openModel('生成 JSON'); openModel('生成 JSON');
formType.value = 0; formType.value = 0;
formData.value = designer.value.getRule(); formData.value = designer.value.getRule();
}; }
/** 生成 Options */ /** 生成 Options */
const showOption = () => { function showOption() {
openModel('生成 Options'); openModel('生成 Options');
formType.value = 1; formType.value = 1;
formData.value = designer.value.getOption(); formData.value = designer.value.getOption();
}; }
/** 生成组件 */ /** 生成组件 */
const showTemplate = () => { function showTemplate() {
openModel('生成组件'); openModel('生成组件');
formType.value = 2; formType.value = 2;
formData.value = makeTemplate(); formData.value = makeTemplate();
}; }
const makeTemplate = () => { /** 生成组件 */
function makeTemplate() {
const rule = designer.value.getRule(); const rule = designer.value.getRule();
const opt = designer.value.getOption(); const opt = designer.value.getOption();
return `<template> return `<template>
@@ -111,10 +112,10 @@ const makeTemplate = () => {
} }
init() init()
<\/script>`; <\/script>`;
}; }
/** 复制 */ /** 复制 */
const copy = async (text: string) => { async function copy(text: string) {
const textToCopy = JSON.stringify(text, null, 2); const textToCopy = JSON.stringify(text, null, 2);
const { copy, copied, isSupported } = useClipboard({ source: textToCopy }); const { copy, copied, isSupported } = useClipboard({ source: textToCopy });
if (isSupported) { if (isSupported) {
@@ -125,12 +126,10 @@ const copy = async (text: string) => {
} else { } else {
message.error('复制失败'); message.error('复制失败');
} }
}; }
/** /** 代码高亮 */
* 代码高亮 function highlightedCode(code: string) {
*/
const highlightedCode = (code: string) => {
// 处理语言和代码 // 处理语言和代码
let language = 'json'; let language = 'json';
if (formType.value === 2) { if (formType.value === 2) {
@@ -143,7 +142,7 @@ const highlightedCode = (code: string) => {
// 高亮 // 高亮
const result = hljs.highlight(code, { language, ignoreIllegals: true }); const result = hljs.highlight(code, { language, ignoreIllegals: true });
return result.value || '&nbsp;'; return result.value || '&nbsp;';
}; }
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {

View File

@@ -1,13 +1,12 @@
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { SystemMenuApi } from '#/api/system/menu'; import type { SystemMenuApi } from '#/api/system/menu';
import { h } from 'vue'; import { h } from 'vue';
import { useAccess } from '@vben/access';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { handleTree } from '@vben/utils'; import { handleTree } from '@vben/utils';
@@ -16,8 +15,6 @@ import { getMenuList } from '#/api/system/menu';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 导入数据库表的表单 */ /** 导入数据库表的表单 */
export function useImportTableFormSchema(): VbenFormSchema[] { export function useImportTableFormSchema(): VbenFormSchema[] {
return [ return [
@@ -350,7 +347,7 @@ export function useGenerationInfoSubTableFormSchema(
}, },
{ {
label: '一对一', label: '一对一',
value: 'false', value: false,
}, },
], ],
}, },
@@ -393,83 +390,43 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraCodegenApi.CodegenTable>( export function useGridColumns(
onActionClick: OnActionClickFn<T>,
getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined, getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined,
): VxeTableGridOptions['columns'] { ): VxeTableGridOptions['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'dataSourceConfigId', field: 'dataSourceConfigId',
title: '数据源', title: '数据源',
minWidth: 120, formatter: ({ cellValue }) => getDataSourceConfigName?.(cellValue) || '-',
formatter: (row) => getDataSourceConfigName?.(row.cellValue) || '-',
}, },
{ {
field: 'tableName', field: 'tableName',
title: '表名称', title: '表名称',
minWidth: 200,
}, },
{ {
field: 'tableComment', field: 'tableComment',
title: '表描述', title: '表描述',
minWidth: 200,
}, },
{ {
field: 'className', field: 'className',
title: '实体', title: '实体',
minWidth: 200,
}, },
{ {
field: 'createTime', field: 'createTime',
title: '创建时间', title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'updateTime', field: 'updateTime',
title: '更新时间', title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
width: 300, width: 280,
fixed: 'right', fixed: 'right',
align: 'center', slots: { default: 'actions' },
cellRender: {
attrs: {
nameField: 'tableName',
nameTitle: '代码生成',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'preview',
text: '预览',
show: hasAccessByCodes(['infra:codegen:preview']),
},
{
code: 'edit',
show: hasAccessByCodes(['infra:codegen:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:codegen:delete']),
},
{
code: 'sync',
text: '同步',
show: hasAccessByCodes(['infra:codegen:update']),
},
{
code: 'generate',
text: '生成代码',
show: hasAccessByCodes(['infra:codegen:download']),
},
],
},
}, },
]; ];
} }

View File

@@ -31,7 +31,7 @@ const columnInfoRef = ref<InstanceType<typeof ColumnInfo>>();
const generateInfoRef = ref<InstanceType<typeof GenerationInfo>>(); const generateInfoRef = ref<InstanceType<typeof GenerationInfo>>();
/** 获取详情数据 */ /** 获取详情数据 */
const getDetail = async () => { async function getDetail() {
const id = route.query.id as any; const id = route.query.id as any;
if (!id) { if (!id) {
return; return;
@@ -42,10 +42,10 @@ const getDetail = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 提交表单 */ /** 提交表单 */
const submitForm = async () => { async function submitForm() {
// 表单验证 // 表单验证
const basicInfoValid = await basicInfoRef.value?.validate(); const basicInfoValid = await basicInfoRef.value?.validate();
if (!basicInfoValid) { if (!basicInfoValid) {
@@ -61,7 +61,6 @@ const submitForm = async () => {
// 提交表单 // 提交表单
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.updating'), content: $t('ui.actionMessage.updating'),
duration: 0,
key: 'action_process_msg', key: 'action_process_msg',
}); });
try { try {
@@ -74,32 +73,35 @@ const submitForm = async () => {
columns, columns,
}); });
// 关闭并提示 // 关闭并提示
message.success($t('ui.actionMessage.operationSuccess')); message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_key_msg',
});
close(); close();
} catch (error) { } catch (error) {
console.error('保存失败', error); console.error('保存失败', error);
} finally { } finally {
hideLoading(); hideLoading();
} }
}; }
const tabs = useTabs(); const tabs = useTabs();
/** 返回列表 */ /** 返回列表 */
const close = () => { function close() {
tabs.closeCurrentTab(); tabs.closeCurrentTab();
router.push('/infra/codegen'); router.push('/infra/codegen');
}; }
/** 下一步 */ /** 下一步 */
const nextStep = async () => { function nextStep() {
currentStep.value += 1; currentStep.value += 1;
}; }
/** 上一步 */ /** 上一步 */
const prevStep = () => { function prevStep() {
if (currentStep.value > 0) { if (currentStep.value > 0) {
currentStep.value -= 1; currentStep.value -= 1;
} }
}; }
/** 步骤配置 */ /** 步骤配置 */
const steps = [ const steps = [
@@ -120,13 +122,11 @@ getDetail();
<template> <template>
<Page auto-content-height v-loading="loading"> <Page auto-content-height v-loading="loading">
<div <div class="bg-card flex h-[95%] flex-col rounded-md p-4">
class="flex h-[95%] flex-col rounded-md bg-white p-4 dark:bg-[#1f1f1f] dark:text-gray-300"
>
<Steps <Steps
type="navigation" type="navigation"
v-model:current="currentStep" v-model:current="currentStep"
class="mb-8 rounded shadow-sm dark:bg-[#141414]" class="mb-8 rounded shadow-sm"
> >
<Steps.Step <Steps.Step
v-for="(step, index) in steps" v-for="(step, index) in steps"

View File

@@ -1,28 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config'; import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui'; import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons'; import { isEmpty } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteCodegenTable, deleteCodegenTable,
deleteCodegenTableList,
downloadCodegen, downloadCodegen,
getCodegenTablePage, getCodegenTablePage,
syncCodegenFromDB, syncCodegenFromDB,
} from '#/api/infra/codegen'; } from '#/api/infra/codegen';
import { getDataSourceConfigList } from '#/api/infra/data-source-config'; import { getDataSourceConfigList } from '#/api/infra/data-source-config';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@@ -57,22 +54,22 @@ function onRefresh() {
} }
/** 导入表格 */ /** 导入表格 */
function onImport() { function handleImport() {
importModalApi.open(); importModalApi.open();
} }
/** 预览代码 */ /** 预览代码 */
function onPreview(row: InfraCodegenApi.CodegenTable) { function handlePreview(row: InfraCodegenApi.CodegenTable) {
previewModalApi.setData(row).open(); previewModalApi.setData(row).open();
} }
/** 编辑表格 */ /** 编辑表格 */
function onEdit(row: InfraCodegenApi.CodegenTable) { function handleEdit(row: InfraCodegenApi.CodegenTable) {
router.push({ name: 'InfraCodegenEdit', query: { id: row.id } }); router.push({ name: 'InfraCodegenEdit', query: { id: row.id } });
} }
/** 删除代码生成配置 */ /** 删除代码生成配置 */
async function onDelete(row: InfraCodegenApi.CodegenTable) { async function handleDelete(row: InfraCodegenApi.CodegenTable) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.tableName]), content: $t('ui.actionMessage.deleting', [row.tableName]),
duration: 0, duration: 0,
@@ -87,16 +84,43 @@ async function onDelete(row: InfraCodegenApi.CodegenTable) {
} }
} }
/** 同步数据库 */ const checkedIds = ref<number[]>([]);
async function onSync(row: InfraCodegenApi.CodegenTable) { function handleRowCheckboxChange({
records,
}: {
records: InfraCodegenApi.CodegenTable[];
}) {
checkedIds.value = records.map((item) => item.id);
}
/** 批量删除代码生成配置 */
async function handleDeleteBatch() {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.updating', [row.tableName]), content: $t('ui.actionMessage.deleting'),
duration: 0, duration: 0,
key: 'action_process_msg', key: 'action_process_msg',
}); });
try {
await deleteCodegenTableList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
}
/** 同步数据库 */
async function handleSync(row: InfraCodegenApi.CodegenTable) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.updating', [row.tableName]),
key: 'action_key_msg',
});
try { try {
await syncCodegenFromDB(row.id); await syncCodegenFromDB(row.id);
message.success($t('ui.actionMessage.updateSuccess', [row.tableName])); message.success({
content: $t('ui.actionMessage.updateSuccess', [row.tableName]),
key: 'action_key_msg',
});
onRefresh(); onRefresh();
} finally { } finally {
hideLoading(); hideLoading();
@@ -104,11 +128,10 @@ async function onSync(row: InfraCodegenApi.CodegenTable) {
} }
/** 生成代码 */ /** 生成代码 */
async function onGenerate(row: InfraCodegenApi.CodegenTable) { async function handleGenerate(row: InfraCodegenApi.CodegenTable) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: '正在生成代码...', content: '正在生成代码...',
duration: 0, key: 'action_key_msg',
key: 'action_process_msg',
}); });
try { try {
const res = await downloadCodegen(row.id); const res = await downloadCodegen(row.id);
@@ -119,47 +142,21 @@ async function onGenerate(row: InfraCodegenApi.CodegenTable) {
link.download = `codegen-${row.className}.zip`; link.download = `codegen-${row.className}.zip`;
link.click(); link.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
message.success('代码生成成功'); message.success({
content: '代码生成成功',
key: 'action_key_msg',
});
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraCodegenApi.CodegenTable>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'generate': {
onGenerate(row);
break;
}
case 'preview': {
onPreview(row);
break;
}
case 'sync': {
onSync(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick, getDataSourceConfigName), columns: useGridColumns(getDataSourceConfigName),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -175,12 +172,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: { code: 'query' }, refresh: { code: 'query' },
search: true, search: true,
}, },
} as VxeTableGridOptions<InfraCodegenApi.CodegenTable>, } as VxeTableGridOptions<InfraCodegenApi.CodegenTable>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
/** 获取数据源配置列表 */ /** 获取数据源配置列表 */
@@ -217,14 +219,70 @@ initDataSourceConfig();
<PreviewModal /> <PreviewModal />
<Grid table-title="代码生成列表"> <Grid table-title="代码生成列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
type="primary" :actions="[
@click="onImport" {
v-access:code="['infra:codegen:create']" label: $t('ui.actionTitle.import'),
> type: 'primary',
<Plus class="size-5" /> icon: ACTION_ICON.ADD,
导入 auth: ['infra:codegen:create'],
</Button> onClick: handleImport,
},
{
label: '批量删除',
type: 'primary',
danger: true,
disabled: isEmpty(checkedIds),
icon: ACTION_ICON.DELETE,
auth: ['infra:codegen:delete'],
onClick: handleDeleteBatch,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '预览',
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['infra:codegen:preview'],
onClick: handlePreview.bind(null, row),
},
{
label: '生成代码',
type: 'link',
icon: ACTION_ICON.DOWNLOAD,
auth: ['infra:codegen:download'],
onClick: handleGenerate.bind(null, row),
},
]"
:drop-down-actions="[
{
label: $t('common.edit'),
type: 'link',
auth: ['infra:codegen:update'],
onClick: handleEdit.bind(null, row),
},
{
label: '同步',
type: 'link',
auth: ['infra:codegen:update'],
onClick: handleSync.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
auth: ['infra:codegen:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.tableName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -69,6 +69,8 @@ function updateTreeSchema(): void {
treeFormApi.setState({ treeFormApi.setState({
schema: useGenerationInfoTreeFormSchema(props.columns), schema: useGenerationInfoTreeFormSchema(props.columns),
}); });
// 树表信息回显
treeFormApi.setValues(props.table as any);
} }
/** 更新主子表信息表单 schema */ /** 更新主子表信息表单 schema */
@@ -76,6 +78,8 @@ function updateSubSchema(): void {
subFormApi.setState({ subFormApi.setState({
schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value), schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
}); });
// 主子表信息回显
subFormApi.setValues(props.table as any);
} }
/** 获取合并的表单值 */ /** 获取合并的表单值 */

View File

@@ -21,7 +21,7 @@ const emit = defineEmits<{
(e: 'success'): void; (e: 'success'): void;
}>(); }>();
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({ const formData = reactive<InfraCodegenApi.CodegenCreateListReq>({
dataSourceConfigId: 0, dataSourceConfigId: 0,
tableNames: [], // 已选择的表列表 tableNames: [], // 已选择的表列表
}); });
@@ -96,15 +96,17 @@ const [Modal, modalApi] = useVbenModal({
// 2. 提交请求 // 2. 提交请求
const hideLoading = message.loading({ const hideLoading = message.loading({
content: '导入中...', content: '导入中...',
duration: 0, key: 'action_key_msg',
key: 'import_loading',
}); });
try { try {
await createCodegenList(formData); await createCodegenList(formData);
// 关闭并提示 // 关闭并提示
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess')); message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_key_msg',
});
} finally { } finally {
hideLoading(); hideLoading();
modalApi.unlock(); modalApi.unlock();

View File

@@ -44,7 +44,7 @@ const activeKey = ref<string>('');
/** 代码地图 */ /** 代码地图 */
const codeMap = ref<Map<string, string>>(new Map<string, string>()); const codeMap = ref<Map<string, string>>(new Map<string, string>());
const setCodeMap = (key: string, lang: string, code: string) => { function setCodeMap(key: string, lang: string, code: string) {
// 处理可能的缩进问题特别是对Java文件 // 处理可能的缩进问题特别是对Java文件
const trimmedCode = code.trimStart(); const trimmedCode = code.trimStart();
// 如果已有缓存则不重新构建 // 如果已有缓存则不重新构建
@@ -59,8 +59,10 @@ const setCodeMap = (key: string, lang: string, code: string) => {
} catch { } catch {
codeMap.value.set(key, trimmedCode); codeMap.value.set(key, trimmedCode);
} }
}; }
const removeCodeMapKey = (targetKey: any) => {
/** 删除代码地图 */
function removeCodeMapKey(targetKey: any) {
// 只有一个代码视图时不允许删除 // 只有一个代码视图时不允许删除
if (codeMap.value.size === 1) { if (codeMap.value.size === 1) {
return; return;
@@ -68,10 +70,10 @@ const removeCodeMapKey = (targetKey: any) => {
if (codeMap.value.has(targetKey)) { if (codeMap.value.has(targetKey)) {
codeMap.value.delete(targetKey); codeMap.value.delete(targetKey);
} }
}; }
/** 复制代码 */ /** 复制代码 */
const copyCode = async () => { async function copyCode() {
const { copy } = useClipboard(); const { copy } = useClipboard();
const file = previewFiles.value.find( const file = previewFiles.value.find(
(item) => item.filePath === activeKey.value, (item) => item.filePath === activeKey.value,
@@ -80,10 +82,10 @@ const copyCode = async () => {
await copy(file.code); await copy(file.code);
message.success('复制成功'); message.success('复制成功');
} }
}; }
/** 文件节点点击事件 */ /** 文件节点点击事件 */
const handleNodeClick = (_: any[], e: any) => { function handleNodeClick(_: any[], e: any) {
if (!e.node.isLeaf) return; if (!e.node.isLeaf) return;
activeKey.value = e.node.key; activeKey.value = e.node.key;
@@ -100,10 +102,10 @@ const handleNodeClick = (_: any[], e: any) => {
const lang = file.filePath.split('.').pop() || ''; const lang = file.filePath.split('.').pop() || '';
setCodeMap(activeKey.value, lang, file.code); setCodeMap(activeKey.value, lang, file.code);
}; }
/** 处理文件树 */ /** 处理文件树 */
const handleFiles = (data: InfraCodegenApi.CodegenPreview[]): FileNode[] => { function handleFiles(data: InfraCodegenApi.CodegenPreview[]): FileNode[] {
const exists: Record<string, boolean> = {}; const exists: Record<string, boolean> = {};
const files: FileNode[] = []; const files: FileNode[] = [];
@@ -176,17 +178,17 @@ const handleFiles = (data: InfraCodegenApi.CodegenPreview[]): FileNode[] => {
} }
/** 构建树形结构 */ /** 构建树形结构 */
const buildTree = (parentKey: string): FileNode[] => { function buildTree(parentKey: string): FileNode[] {
return files return files
.filter((file) => file.parentKey === parentKey) .filter((file) => file.parentKey === parentKey)
.map((file) => ({ .map((file) => ({
...file, ...file,
children: buildTree(file.key), children: buildTree(file.key),
})); }));
}; }
return buildTree('/'); return buildTree('/');
}; }
/** 模态框实例 */ /** 模态框实例 */
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({

View File

@@ -1,13 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraConfigApi } from '#/api/infra/config';
import { useAccess } from '@vben/access';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
@@ -122,39 +117,32 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraConfigApi.Config>( export function useGridColumns(): VxeTableGridOptions['columns'] {
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'id', field: 'id',
title: '参数主键', title: '参数主键',
minWidth: 100,
}, },
{ {
field: 'category', field: 'category',
title: '参数分类', title: '参数分类',
minWidth: 120,
}, },
{ {
field: 'name', field: 'name',
title: '参数名称', title: '参数名称',
minWidth: 200,
}, },
{ {
field: 'key', field: 'key',
title: '参数键名', title: '参数键名',
minWidth: 200,
}, },
{ {
field: 'value', field: 'value',
title: '参数键值', title: '参数键值',
minWidth: 150,
}, },
{ {
field: 'visible', field: 'visible',
title: '是否可见', title: '是否可见',
minWidth: 100,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
@@ -163,7 +151,6 @@ export function useGridColumns<T = InfraConfigApi.Config>(
{ {
field: 'type', field: 'type',
title: '系统内置', title: '系统内置',
minWidth: 100,
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { type: DICT_TYPE.INFRA_CONFIG_TYPE }, props: { type: DICT_TYPE.INFRA_CONFIG_TYPE },
@@ -172,38 +159,17 @@ export function useGridColumns<T = InfraConfigApi.Config>(
{ {
field: 'remark', field: 'remark',
title: '备注', title: '备注',
minWidth: 150,
}, },
{ {
field: 'createTime', field: 'createTime',
title: '创建时间', title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 130, width: 160,
align: 'center',
fixed: 'right', fixed: 'right',
cellRender: { slots: { default: 'actions' },
attrs: {
nameField: 'name',
nameTitle: '参数',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:config:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:config:delete']),
},
],
},
}, },
]; ];
} }

View File

@@ -1,18 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { InfraConfigApi } from '#/api/infra/config'; import type { InfraConfigApi } from '#/api/infra/config';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons'; import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteConfig, exportConfig, getConfigPage } from '#/api/infra/config'; import {
deleteConfig,
deleteConfigList,
exportConfig,
getConfigPage,
} from '#/api/infra/config';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@@ -29,51 +32,62 @@ function onRefresh() {
} }
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function handleExport() {
const data = await exportConfig(await gridApi.formApi.getValues()); const data = await exportConfig(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '参数配置.xls', source: data }); downloadFileFromBlobPart({ fileName: '参数配置.xls', source: data });
} }
/** 创建参数 */ /** 创建参数 */
function onCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑参数 */ /** 编辑参数 */
function onEdit(row: InfraConfigApi.Config) { function handleEdit(row: InfraConfigApi.Config) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除参数 */ /** 删除参数 */
async function onDelete(row: InfraConfigApi.Config) { async function handleDelete(row: InfraConfigApi.Config) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
key: 'action_process_msg', key: 'action_key_msg',
}); });
try { try {
await deleteConfig(row.id as number); await deleteConfig(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 表格操作按钮的回调函数 */ const checkedIds = ref<number[]>([]);
function onActionClick({ function handleRowCheckboxChange({
code, records,
row, }: {
}: OnActionClickParams<InfraConfigApi.Config>) { records: InfraConfigApi.Config[];
switch (code) { }) {
case 'delete': { checkedIds.value = records.map((item) => item.id as number);
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
} }
/** 批量删除参数 */
async function handleDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteConfigList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
} }
} }
@@ -82,7 +96,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@@ -98,12 +112,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: { code: 'query' }, refresh: { code: 'query' },
search: true, search: true,
}, },
} as VxeTableGridOptions<InfraConfigApi.Config>, } as VxeTableGridOptions<InfraConfigApi.Config>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
</script> </script>
@@ -112,23 +131,57 @@ const [Grid, gridApi] = useVbenVxeGrid({
<FormModal @success="onRefresh" /> <FormModal @success="onRefresh" />
<Grid table-title="参数列表"> <Grid table-title="参数列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
type="primary" :actions="[
@click="onCreate" {
v-access:code="['infra:config:create']" label: $t('ui.actionTitle.create', ['参数列表']),
> type: 'primary',
<Plus class="size-5" /> icon: ACTION_ICON.ADD,
{{ $t('ui.actionTitle.create', ['参数']) }} auth: ['infra:config:create'],
</Button> onClick: handleCreate,
<Button },
type="primary" {
class="ml-2" label: $t('ui.actionTitle.export'),
@click="onExport" type: 'primary',
v-access:code="['infra:config:export']" icon: ACTION_ICON.DOWNLOAD,
> auth: ['infra:config:export'],
<Download class="size-5" /> onClick: handleExport,
{{ $t('ui.actionTitle.export') }} },
</Button> {
label: '批量删除',
type: 'primary',
danger: true,
disabled: isEmpty(checkedIds),
icon: ACTION_ICON.DELETE,
auth: ['infra:config:delete'],
onClick: handleDeleteBatch,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['system:post:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['system:post:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -1,10 +1,5 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
@@ -58,62 +53,34 @@ export function useFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraDataSourceConfigApi.DataSourceConfig>( export function useGridColumns(): VxeTableGridOptions['columns'] {
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
title: '主键编号', title: '主键编号',
minWidth: 100,
}, },
{ {
field: 'name', field: 'name',
title: '数据源名称', title: '数据源名称',
minWidth: 150,
}, },
{ {
field: 'url', field: 'url',
title: '数据源连接', title: '数据源连接',
minWidth: 300,
}, },
{ {
field: 'username', field: 'username',
title: '用户名', title: '用户名',
minWidth: 120,
}, },
{ {
field: 'createTime', field: 'createTime',
title: '创建时间', title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 130, width: 160,
align: 'center',
fixed: 'right', fixed: 'right',
cellRender: { slots: { default: 'actions' },
attrs: {
nameField: 'name',
nameTitle: '数据源',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:data-source-config:update']),
disabled: (row: any) => row.id === 0,
},
{
code: 'delete',
show: hasAccessByCodes(['infra:data-source-config:delete']),
disabled: (row: any) => row.id === 0,
},
],
},
}, },
]; ];
} }

View File

@@ -1,18 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config'; import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDataSourceConfig, deleteDataSourceConfig,
getDataSourceConfigList, getDataSourceConfigList,
@@ -28,51 +24,37 @@ const [FormModal, formModalApi] = useVbenModal({
}); });
/** 创建数据源 */ /** 创建数据源 */
function onCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑数据源 */ /** 编辑数据源 */
function onEdit(row: InfraDataSourceConfigApi.DataSourceConfig) { function handleEdit(row: InfraDataSourceConfigApi.DataSourceConfig) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除数据源 */ /** 删除数据源 */
async function onDelete(row: InfraDataSourceConfigApi.DataSourceConfig) { async function handleDelete(row: InfraDataSourceConfigApi.DataSourceConfig) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
key: 'action_process_msg', key: 'action_key_msg',
}); });
try { try {
await deleteDataSourceConfig(row.id as number); await deleteDataSourceConfig(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
await handleLoadData(); await handleLoadData();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraDataSourceConfigApi.DataSourceConfig>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
rowConfig: { rowConfig: {
@@ -94,11 +76,6 @@ async function handleLoadData() {
await gridApi.query(); await gridApi.query();
} }
/** 刷新表格 */
async function onRefresh() {
await handleLoadData();
}
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
handleLoadData(); handleLoadData();
@@ -107,17 +84,44 @@ onMounted(() => {
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<FormModal @success="onRefresh" /> <FormModal @success="handleLoadData" />
<Grid table-title="数据源列表"> <Grid table-title="数据源列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
type="primary" :actions="[
@click="onCreate" {
v-access:code="['infra:data-source-config:create']" label: $t('ui.actionTitle.create', ['数据源']),
> type: 'primary',
<Plus class="size-5" /> icon: ACTION_ICON.ADD,
{{ $t('ui.actionTitle.create', ['数据源']) }} auth: ['infra:data-source-config:create'],
</Button> onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['infra:data-source-config:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['infra:data-source-config:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -83,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<Modal :title="getTitle"> <Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" /> <Form class="mx-4" />
</Modal> </Modal>
</template> </template>

View File

@@ -1,13 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
import { useAccess } from '@vben/access';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
@@ -99,10 +95,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns( export function useGridColumns(): VxeTableGridOptions<Demo01ContactApi.Demo01Contact>['columns'] {
onActionClick?: OnActionClickFn<Demo01ContactApi.Demo01Contact>,
): VxeTableGridOptions<Demo01ContactApi.Demo01Contact>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'id', field: 'id',
title: '编号', title: '编号',
@@ -145,32 +140,10 @@ export function useGridColumns(
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 200, width: 160,
align: 'center',
fixed: 'right', fixed: 'right',
// TODO @puhui999headerAlign 要使用 headerAlign: 'center' 么?看着现在分成了 align 和 headerAlign 两种 slots: { default: 'actions' },
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '示例联系人',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:demo01-contact:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:demo01-contact:delete']),
},
],
},
}, },
]; ];
} }

View File

@@ -1,21 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
import { h } from 'vue'; import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons'; import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo01Contact, deleteDemo01Contact,
deleteDemo01ContactList,
exportDemo01Contact, exportDemo01Contact,
getDemo01ContactPage, getDemo01ContactPage,
} from '#/api/infra/demo/demo01'; } from '#/api/infra/demo/demo01';
@@ -35,17 +32,17 @@ function onRefresh() {
} }
/** 创建示例联系人 */ /** 创建示例联系人 */
function onCreate() { function handleCreate() {
formModalApi.setData({}).open(); formModalApi.setData({}).open();
} }
/** 编辑示例联系人 */ /** 编辑示例联系人 */
function onEdit(row: Demo01ContactApi.Demo01Contact) { function handleEdit(row: Demo01ContactApi.Demo01Contact) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 删除示例联系人 */ /** 删除示例联系人 */
async function onDelete(row: Demo01ContactApi.Demo01Contact) { async function handleDelete(row: Demo01ContactApi.Demo01Contact) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, duration: 0,
@@ -55,32 +52,40 @@ async function onDelete(row: Demo01ContactApi.Demo01Contact) {
await deleteDemo01Contact(row.id as number); await deleteDemo01Contact(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 导出表格 */ /** 批量删除示例联系人 */
async function onExport() { async function handleDeleteBatch() {
const data = await exportDemo01Contact(await gridApi.formApi.getValues()); const hideLoading = message.loading({
downloadFileFromBlobPart({ fileName: '示例联系人.xls', source: data }); content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo01ContactList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
} }
/** 表格操作按钮的回调函数 */ const checkedIds = ref<number[]>([]);
function onActionClick({ function handleRowCheckboxChange({
code, records,
row, }: {
}: OnActionClickParams<Demo01ContactApi.Demo01Contact>) { records: Demo01ContactApi.Demo01Contact[];
switch (code) { }) {
case 'delete': { checkedIds.value = records.map((item) => item.id);
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
} }
/** 导出表格 */
async function handleExport() {
const data = await exportDemo01Contact(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '示例联系人.xls', source: data });
} }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
@@ -88,7 +93,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
pagerConfig: { pagerConfig: {
enabled: true, enabled: true,
@@ -113,6 +118,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true, search: true,
}, },
} as VxeTableGridOptions<Demo01ContactApi.Demo01Contact>, } as VxeTableGridOptions<Demo01ContactApi.Demo01Contact>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
</script> </script>
@@ -122,23 +131,57 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Grid table-title="示例联系人列表"> <Grid table-title="示例联系人列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button <TableAction
:icon="h(Plus)" :actions="[
type="primary" {
@click="onCreate" label: $t('ui.actionTitle.create', ['示例联系人']),
v-access:code="['infra:demo01-contact:create']" type: 'primary',
> icon: ACTION_ICON.ADD,
{{ $t('ui.actionTitle.create', ['示例联系人']) }} auth: ['infra:demo01-contact:create'],
</Button> onClick: handleCreate,
<Button },
:icon="h(Download)" {
type="primary" label: $t('ui.actionTitle.export'),
class="ml-2" type: 'primary',
@click="onExport" icon: ACTION_ICON.DOWNLOAD,
v-access:code="['infra:demo01-contact:export']" auth: ['infra:demo01-contact:export'],
> onClick: handleExport,
{{ $t('ui.actionTitle.export') }} },
</Button> {
label: '批量删除',
type: 'primary',
danger: true,
disabled: isEmpty(checkedIds),
icon: ACTION_ICON.DELETE,
auth: ['infra:demo01-contact:delete'],
onClick: handleDeleteBatch,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['infra:demo01-contact:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['infra:demo01-contact:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -1,15 +1,12 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02'; import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import { useAccess } from '@vben/access';
import { handleTree } from '@vben/utils'; import { handleTree } from '@vben/utils';
import { getDemo02CategoryList } from '#/api/infra/demo/demo02'; import { getDemo02CategoryList } from '#/api/infra/demo/demo02';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
@@ -89,9 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns( export function useGridColumns(): VxeTableGridOptions<Demo02CategoryApi.Demo02Category>['columns'] {
onActionClick?: OnActionClickFn<Demo02CategoryApi.Demo02Category>,
): VxeTableGridOptions<Demo02CategoryApi.Demo02Category>['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
@@ -116,39 +111,10 @@ export function useGridColumns(
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
field: 'operation',
title: '操作', title: '操作',
minWidth: 200, width: 220,
align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center', slots: { default: 'actions' },
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '示例分类',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'append',
text: '新增下级',
show: hasAccessByCodes(['infra:demo02-category:create']),
},
{
code: 'edit',
show: hasAccessByCodes(['infra:demo02-category:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:demo02-category:delete']),
disabled: (row: Demo02CategoryApi.Demo02Category) => {
return !!(row.children && row.children.length > 0);
},
},
],
},
}, },
]; ];
} }

View File

@@ -1,19 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { import type { VxeTableGridOptions } from '#/adapter/vxe-table';
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02'; import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import { h, ref } from 'vue'; import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo02Category, deleteDemo02Category,
exportDemo02Category, exportDemo02Category,
@@ -42,69 +38,50 @@ function onRefresh() {
} }
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function handleExport() {
const data = await exportDemo02Category(await gridApi.formApi.getValues()); const data = await exportDemo02Category(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '示例分类.xls', source: data }); downloadFileFromBlobPart({ fileName: '示例分类.xls', source: data });
} }
/** 创建示例分类 */ /** 创建示例分类 */
function onCreate() { function handleCreate() {
formModalApi.setData(null).open(); formModalApi.setData(null).open();
} }
/** 编辑示例分类 */ /** 编辑示例分类 */
function onEdit(row: Demo02CategoryApi.Demo02Category) { function handleEdit(row: Demo02CategoryApi.Demo02Category) {
formModalApi.setData(row).open(); formModalApi.setData(row).open();
} }
/** 新增下级示例分类 */ /** 新增下级示例分类 */
function onAppend(row: Demo02CategoryApi.Demo02Category) { function handleAppend(row: Demo02CategoryApi.Demo02Category) {
formModalApi.setData({ parentId: row.id }).open(); formModalApi.setData({ parentId: row.id }).open();
} }
/** 删除示例分类 */ /** 删除示例分类 */
async function onDelete(row: Demo02CategoryApi.Demo02Category) { async function handleDelete(row: Demo02CategoryApi.Demo02Category) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0, key: 'action_key_msg',
key: 'action_process_msg',
}); });
try { try {
await deleteDemo02Category(row.id as number); await deleteDemo02Category(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<Demo02CategoryApi.Demo02Category>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(),
height: 'auto', height: 'auto',
treeConfig: { treeConfig: {
parentField: 'parentId', parentField: 'parentId',
@@ -141,26 +118,60 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Grid table-title="示例分类列表"> <Grid table-title="示例分类列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button @click="toggleExpand" class="mr-2"> <TableAction
{{ isExpanded ? '收缩' : '展开' }} :actions="[
</Button> {
<Button label: isExpanded ? '收缩' : '展开',
:icon="h(Plus)" type: 'primary',
type="primary" onClick: toggleExpand,
@click="onCreate" },
v-access:code="['infra:demo02-category:create']" {
> label: $t('ui.actionTitle.create', ['菜单']),
{{ $t('ui.actionTitle.create', ['示例分类']) }} type: 'primary',
</Button> icon: ACTION_ICON.ADD,
<Button auth: ['infra:demo02-category:create'],
:icon="h(Download)" onClick: handleCreate,
type="primary" },
class="ml-2" {
@click="onExport" label: $t('ui.actionTitle.export'),
v-access:code="['infra:demo02-category:export']" type: 'primary',
> icon: ACTION_ICON.DOWNLOAD,
{{ $t('ui.actionTitle.export') }} auth: ['infra:demo02-category:export'],
</Button> onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '新增下级',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['infra:demo02-category:create'],
onClick: handleAppend.bind(null, row),
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['infra:demo02-category:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['infra:demo02-category:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -107,6 +107,7 @@ export function useGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>, onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] { ): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'id', field: 'id',
title: '编号', title: '编号',
@@ -149,7 +150,6 @@ export function useGridColumns(
minWidth: 200, minWidth: 200,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false, showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {
@@ -254,6 +254,7 @@ export function useDemo03CourseGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Course>, onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Course>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] { ): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'id', field: 'id',
title: '编号', title: '编号',
@@ -286,7 +287,7 @@ export function useDemo03CourseGridColumns(
minWidth: 200, minWidth: 200,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false, showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {
@@ -391,6 +392,7 @@ export function useDemo03GradeGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Grade>, onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Grade>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Grade>['columns'] { ): VxeTableGridOptions<Demo03StudentApi.Demo03Grade>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'id', field: 'id',
title: '编号', title: '编号',
@@ -423,7 +425,7 @@ export function useDemo03GradeGridColumns(
minWidth: 200, minWidth: 200,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false, showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {

View File

@@ -8,14 +8,15 @@ import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
import { h, ref } from 'vue'; import { h, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons'; import { Download, Plus, Trash2 } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { Button, message, Tabs } from 'ant-design-vue'; import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo03Student, deleteDemo03Student,
deleteDemo03StudentList,
exportDemo03Student, exportDemo03Student,
getDemo03StudentPage, getDemo03StudentPage,
} from '#/api/infra/demo/demo03/erp'; } from '#/api/infra/demo/demo03/erp';
@@ -61,11 +62,36 @@ async function onDelete(row: Demo03StudentApi.Demo03Student) {
await deleteDemo03Student(row.id as number); await deleteDemo03Student(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 批量删除学生 */
async function onDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03StudentList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: Demo03StudentApi.Demo03Grade[];
}) {
checkedIds.value = records.map((item) => item.id);
}
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function onExport() {
const data = await exportDemo03Student(await gridApi.formApi.getValues()); const data = await exportDemo03Student(await gridApi.formApi.getValues());
@@ -124,6 +150,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellClick: ({ row }: { row: Demo03StudentApi.Demo03Student }) => { cellClick: ({ row }: { row: Demo03StudentApi.Demo03Student }) => {
selectDemo03Student.value = row; selectDemo03Student.value = row;
}, },
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
}, },
}); });
</script> </script>
@@ -152,6 +180,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
> >
{{ $t('ui.actionTitle.export') }} {{ $t('ui.actionTitle.export') }}
</Button> </Button>
<Button
:icon="h(Trash2)"
type="primary"
danger
class="ml-2"
:disabled="isEmpty(checkedIds)"
@click="onDeleteBatch"
v-access:code="['infra:demo03-student:delete']"
>
批量删除
</Button>
</template> </template>
</Grid> </Grid>

View File

@@ -5,16 +5,18 @@ import type {
} from '#/adapter/vxe-table'; } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp'; import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
import { h, nextTick, watch } from 'vue'; import { h, nextTick, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons'; import { Plus, Trash2 } from '@vben/icons';
import { isEmpty } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo03Course, deleteDemo03Course,
deleteDemo03CourseList,
getDemo03CoursePage, getDemo03CoursePage,
} from '#/api/infra/demo/demo03/erp'; } from '#/api/infra/demo/demo03/erp';
import { $t } from '#/locales'; import { $t } from '#/locales';
@@ -59,11 +61,36 @@ async function onDelete(row: Demo03StudentApi.Demo03Course) {
await deleteDemo03Course(row.id as number); await deleteDemo03Course(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 批量删除学生课程 */
async function onDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03CourseList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: Demo03StudentApi.Demo03Course[];
}) {
checkedIds.value = records.map((item) => item.id);
}
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ function onActionClick({
code, code,
@@ -115,6 +142,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
isHover: true, isHover: true,
}, },
} as VxeTableGridOptions<Demo03StudentApi.Demo03Course>, } as VxeTableGridOptions<Demo03StudentApi.Demo03Course>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
/** 刷新表格 */ /** 刷新表格 */
@@ -148,6 +179,17 @@ watch(
> >
{{ $t('ui.actionTitle.create', ['学生课程']) }} {{ $t('ui.actionTitle.create', ['学生课程']) }}
</Button> </Button>
<Button
:icon="h(Trash2)"
type="primary"
danger
class="ml-2"
:disabled="isEmpty(checkedIds)"
@click="onDeleteBatch"
v-access:code="['infra:demo03-student:delete']"
>
批量删除
</Button>
</template> </template>
</Grid> </Grid>
</template> </template>

View File

@@ -5,16 +5,18 @@ import type {
} from '#/adapter/vxe-table'; } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp'; import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
import { h, nextTick, watch } from 'vue'; import { h, nextTick, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons'; import { Plus, Trash2 } from '@vben/icons';
import { isEmpty } from '@vben/utils';
import { Button, message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo03Grade, deleteDemo03Grade,
deleteDemo03GradeList,
getDemo03GradePage, getDemo03GradePage,
} from '#/api/infra/demo/demo03/erp'; } from '#/api/infra/demo/demo03/erp';
import { $t } from '#/locales'; import { $t } from '#/locales';
@@ -59,11 +61,36 @@ async function onDelete(row: Demo03StudentApi.Demo03Grade) {
await deleteDemo03Grade(row.id as number); await deleteDemo03Grade(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 批量删除学生班级 */
async function onDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03GradeList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: Demo03StudentApi.Demo03Grade[];
}) {
checkedIds.value = records.map((item) => item.id);
}
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ function onActionClick({
code, code,
@@ -115,6 +142,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
isHover: true, isHover: true,
}, },
} as VxeTableGridOptions<Demo03StudentApi.Demo03Grade>, } as VxeTableGridOptions<Demo03StudentApi.Demo03Grade>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
/** 刷新表格 */ /** 刷新表格 */
@@ -148,6 +179,17 @@ watch(
> >
{{ $t('ui.actionTitle.create', ['学生班级']) }} {{ $t('ui.actionTitle.create', ['学生班级']) }}
</Button> </Button>
<Button
:icon="h(Trash2)"
type="primary"
danger
class="ml-2"
:disabled="isEmpty(checkedIds)"
@click="onDeleteBatch"
v-access:code="['infra:demo03-student:delete']"
>
批量删除
</Button>
</template> </template>
</Grid> </Grid>
</template> </template>

View File

@@ -64,7 +64,6 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined; formData.value = undefined;
return; return;
} }
// 加载数据 // 加载数据
let data = modalApi.getData<Demo03StudentApi.Demo03Student>(); let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
if (!data) { if (!data) {

View File

@@ -107,6 +107,7 @@ export function useGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>, onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] { ): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ type: 'expand', width: 80, slots: { content: 'expand_content' } }, { type: 'expand', width: 80, slots: { content: 'expand_content' } },
{ {
field: 'id', field: 'id',
@@ -150,7 +151,6 @@ export function useGridColumns(
minWidth: 200, minWidth: 200,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false, showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {
@@ -199,7 +199,6 @@ export function useDemo03CourseGridEditColumns(
minWidth: 60, minWidth: 60,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false, showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {

View File

@@ -8,14 +8,15 @@ import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { h, ref } from 'vue'; import { h, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons'; import { Download, Plus, Trash2 } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { Button, message, Tabs } from 'ant-design-vue'; import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDemo03Student, deleteDemo03Student,
deleteDemo03StudentList,
exportDemo03Student, exportDemo03Student,
getDemo03StudentPage, getDemo03StudentPage,
} from '#/api/infra/demo/demo03/inner'; } from '#/api/infra/demo/demo03/inner';
@@ -60,11 +61,36 @@ async function onDelete(row: Demo03StudentApi.Demo03Student) {
await deleteDemo03Student(row.id as number); await deleteDemo03Student(row.id as number);
message.success($t('ui.actionMessage.deleteSuccess', [row.id])); message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
onRefresh(); onRefresh();
} catch { } finally {
hideLoading(); hideLoading();
} }
} }
/** 批量删除学生 */
async function onDeleteBatch() {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03StudentList(checkedIds.value);
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {
hideLoading();
}
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: Demo03StudentApi.Demo03Student[];
}) {
checkedIds.value = records.map((item) => item.id);
}
/** 导出表格 */ /** 导出表格 */
async function onExport() { async function onExport() {
const data = await exportDemo03Student(await gridApi.formApi.getValues()); const data = await exportDemo03Student(await gridApi.formApi.getValues());
@@ -118,6 +144,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true, search: true,
}, },
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>, } as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
}); });
</script> </script>
@@ -155,6 +185,17 @@ const [Grid, gridApi] = useVbenVxeGrid({
> >
{{ $t('ui.actionTitle.export') }} {{ $t('ui.actionTitle.export') }}
</Button> </Button>
<Button
:icon="h(Trash2)"
type="primary"
danger
class="ml-2"
:disabled="isEmpty(checkedIds)"
@click="onDeleteBatch"
v-access:code="['infra:demo03-student:delete']"
>
批量删除
</Button>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -16,18 +16,18 @@ const props = defineProps<{
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useDemo03CourseGridColumns(), columns: useDemo03CourseGridColumns(),
height: 'auto',
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: { pagerConfig: {
enabled: false, enabled: false,
}, },
toolbarConfig: { toolbarConfig: {
enabled: false, enabled: false,
}, },
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>, height: '600px',
rowConfig: {
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Course>,
}); });
/** 刷新表格 */ /** 刷新表格 */

View File

@@ -11,6 +11,13 @@ const props = defineProps<{
}>(); }>();
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal', layout: 'horizontal',
schema: useDemo03GradeFormSchema(), schema: useDemo03GradeFormSchema(),
showDefaultActions: false, showDefaultActions: false,

View File

@@ -16,18 +16,18 @@ const props = defineProps<{
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useDemo03GradeGridColumns(), columns: useDemo03GradeGridColumns(),
height: 'auto',
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: { pagerConfig: {
enabled: false, enabled: false,
}, },
toolbarConfig: { toolbarConfig: {
enabled: false, enabled: false,
}, },
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>, height: '600px',
rowConfig: {
keyField: 'id',
isHover: true,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Grade>,
}); });
/** 刷新表格 */ /** 刷新表格 */

View File

@@ -61,8 +61,8 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单 // 提交表单
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student; const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
// 拼接子表的数据 // 拼接子表的数据
data.demo03courses = demo03CourseFormRef.value?.getData(); data.demo03Courses = demo03CourseFormRef.value?.getData();
data.demo03grade = await demo03GradeFormRef.value?.getValues(); data.demo03Grade = await demo03GradeFormRef.value?.getValues();
try { try {
await (formData.value?.id await (formData.value?.id
? updateDemo03Student(data) ? updateDemo03Student(data)
@@ -80,7 +80,6 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined; formData.value = undefined;
return; return;
} }
// 加载数据 // 加载数据
let data = modalApi.getData<Demo03StudentApi.Demo03Student>(); let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
if (!data) { if (!data) {

Some files were not shown because too many files have changed in this diff Show More