提交新版本
This commit is contained in:
35
apps/web-naive/.env
Normal file
35
apps/web-naive/.env
Normal file
@@ -0,0 +1,35 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=芋道管理系统
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=yudao-vben-naive
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
||||
# 是否开启模拟数据
|
||||
VITE_NITRO_MOCK=false
|
||||
|
||||
# 租户开关
|
||||
VITE_APP_TENANT_ENABLE=true
|
||||
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=false
|
||||
|
||||
# 文档地址的开关
|
||||
VITE_APP_DOCALERT_ENABLE=true
|
||||
|
||||
# 百度统计
|
||||
VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
|
||||
|
||||
# GoView域名
|
||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||
|
||||
# API 加解密
|
||||
VITE_APP_API_ENCRYPT_ENABLE = true
|
||||
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
|
||||
VITE_APP_API_ENCRYPT_ALGORITHM = AES
|
||||
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
|
||||
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
|
||||
# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
|
||||
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
|
||||
50
apps/web-naive/package.json
Normal file
50
apps/web-naive/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.5.9",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "apps/web-naive"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "vben",
|
||||
"email": "ann.vben@gmail.com",
|
||||
"url": "https://github.com/anncwb"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build --mode production",
|
||||
"build:analyze": "pnpm vite build --mode analyze",
|
||||
"dev": "pnpm vite --mode development",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||
},
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
"@vben/hooks": "workspace:*",
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/layouts": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/plugins": "workspace:*",
|
||||
"@vben/preferences": "workspace:*",
|
||||
"@vben/request": "workspace:*",
|
||||
"@vben/stores": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"naive-ui": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
}
|
||||
BIN
apps/web-naive/public/wx-xingyu.png
Normal file
BIN
apps/web-naive/public/wx-xingyu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 495 KiB |
236
apps/web-naive/src/adapter/component/index.ts
Normal file
236
apps/web-naive/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
|
||||
const NButton = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/button').then((res) => res.NButton),
|
||||
);
|
||||
const NCheckbox = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/checkbox').then((res) => res.NCheckbox),
|
||||
);
|
||||
const NCheckboxGroup = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/checkbox').then((res) => res.NCheckboxGroup),
|
||||
);
|
||||
const NDatePicker = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/date-picker').then((res) => res.NDatePicker),
|
||||
);
|
||||
const NDivider = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/divider').then((res) => res.NDivider),
|
||||
);
|
||||
const NInput = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/input').then((res) => res.NInput),
|
||||
);
|
||||
const NInputNumber = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/input-number').then((res) => res.NInputNumber),
|
||||
);
|
||||
const NRadio = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadio),
|
||||
);
|
||||
const NRadioButton = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadioButton),
|
||||
);
|
||||
const NRadioGroup = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadioGroup),
|
||||
);
|
||||
const NSelect = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/select').then((res) => res.NSelect),
|
||||
);
|
||||
const NSpace = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/space').then((res) => res.NSpace),
|
||||
);
|
||||
const NSwitch = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/switch').then((res) => res.NSwitch),
|
||||
);
|
||||
const NTimePicker = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/time-picker').then((res) => res.NTimePicker),
|
||||
);
|
||||
const NTreeSelect = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/tree-select').then((res) => res.NTreeSelect),
|
||||
);
|
||||
const NUpload = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/upload').then((res) => res.NUpload),
|
||||
);
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'FileUpload'
|
||||
| 'IconPicker'
|
||||
| 'ImageUpload'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: NSelect,
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: NTreeSelect,
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'arrow',
|
||||
keyField: 'value',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'options',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () => options.map((option) => h(NCheckbox, option));
|
||||
}
|
||||
}
|
||||
return h(
|
||||
NCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
},
|
||||
DatePicker: NDatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(NButton, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(NButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: NDivider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'suffix',
|
||||
inputComponent: NInput,
|
||||
}),
|
||||
Input: withDefaultPlaceholder(NInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? NRadioButton : NRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
const groupRender = h(
|
||||
NRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
return attrs.isButton
|
||||
? h(NSpace, { vertical: true }, () => groupRender)
|
||||
: groupRender;
|
||||
},
|
||||
Select: withDefaultPlaceholder(NSelect, 'select'),
|
||||
Space: NSpace,
|
||||
Switch: NSwitch,
|
||||
TimePicker: NTimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
|
||||
Upload: NUpload,
|
||||
FileUpload,
|
||||
ImageUpload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
message.success(content || title, {
|
||||
duration: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
66
apps/web-naive/src/adapter/form.ts
Normal file
66
apps/web-naive/src/adapter/form.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isMobile } from '@vben/utils';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Upload: 'fileList',
|
||||
DatePicker: 'formatted-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 手机号非必填
|
||||
mobile: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return true;
|
||||
} else if (!isMobile(value)) {
|
||||
return $t('ui.formRules.mobile', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 手机号必填
|
||||
mobileRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
if (!isMobile(value)) {
|
||||
return $t('ui.formRules.mobile', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
219
apps/web-naive/src/adapter/vxe-table.ts
Normal file
219
apps/web-naive/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
import {
|
||||
erpCountInputFormatter,
|
||||
erpNumberFormatter,
|
||||
fenToYuan,
|
||||
formatFileSize,
|
||||
formatPast2,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { NButton, NImage, NImageGroup, NSwitch, NTag } from 'naive-ui';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import DictTag from '../components/dict-tag/dict-tag.vue';
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
import: false, // 是否导入
|
||||
export: false, // 是否导出
|
||||
refresh: true, // 是否刷新
|
||||
print: false, // 是否打印
|
||||
zoom: true, // 是否缩放
|
||||
custom: true, // 是否自定义配置
|
||||
},
|
||||
customConfig: {
|
||||
mode: 'modal',
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'list',
|
||||
total: 'total',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
sortConfig: {
|
||||
multiple: true,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(NImage, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellImages', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
if (column && column.field && row[column.field]) {
|
||||
return h(NImageGroup, { srcList: row[column.field] });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
NButton,
|
||||
{ size: 'small', type: 'primary', quaternary: true },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellTag' },
|
||||
vxeUI.renderer.add('CellTag', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(NTag, { color: props?.color }, () => row[column.field]);
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellTags', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
if (!row[column.field] || !Array.isArray(row[column.field])) {
|
||||
return '';
|
||||
}
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'flex items-center justify-center' },
|
||||
{
|
||||
default: () =>
|
||||
row[column.field].map((item: any) =>
|
||||
h(NTag, { color: props?.color }, { default: () => item }),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} },
|
||||
vxeUI.renderer.add('CellDict', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
if (!props) {
|
||||
return '';
|
||||
}
|
||||
// 使用 DictTag 组件替代原来的实现
|
||||
return h(DictTag, {
|
||||
type: props.type,
|
||||
value: row[column.field]?.toString(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellSwitch', props: { beforeChange: () => {} } },
|
||||
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/adapter/vxe-table.ts#L97-L123
|
||||
vxeUI.renderer.add('CellSwitch', {
|
||||
renderTableDefault({ attrs, props }, { column, row }) {
|
||||
const loadingKey = `__loading_${column.field}`;
|
||||
const finallyProps = {
|
||||
inlinePrompt: true,
|
||||
checkedValue: 0,
|
||||
uncheckedValue: 1,
|
||||
...props,
|
||||
value: row[column.field],
|
||||
loading: row[loadingKey] ?? false,
|
||||
'onUpdate:value': onChange,
|
||||
};
|
||||
|
||||
async function onChange(newVal: any) {
|
||||
row[loadingKey] = true;
|
||||
try {
|
||||
const result = await attrs?.beforeChange?.(newVal, row);
|
||||
if (result !== false) {
|
||||
row[column.field] = newVal;
|
||||
}
|
||||
} finally {
|
||||
row[loadingKey] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return h(NSwitch, finallyProps, {
|
||||
checked: () => h('p', props?.checkedChildren ?? $t('common.enabled')),
|
||||
unchecked: () =>
|
||||
h('p', props?.uncheckedChildren ?? $t('common.disabled')),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
vxeUI.formats.add('formatPast2', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
return formatPast2(cellValue);
|
||||
},
|
||||
});
|
||||
|
||||
// add by 星语:数量格式化,保留 3 位
|
||||
vxeUI.formats.add('formatAmount3', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
if (cellValue === null || cellValue === undefined) {
|
||||
return '';
|
||||
}
|
||||
return erpCountInputFormatter(cellValue);
|
||||
},
|
||||
});
|
||||
// add by 星语:数量格式化,保留 2 位
|
||||
vxeUI.formats.add('formatAmount2', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
return `${erpNumberFormatter(cellValue, digits)}`;
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.formats.add('formatFenToYuanAmount', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
return `${erpNumberFormatter(fenToYuan(cellValue), digits)}`;
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.formats.add('formatFileSize', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
return formatFileSize(cellValue, digits);
|
||||
},
|
||||
});
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export * from '#/components/table-action';
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
161
apps/web-naive/src/api/core/auth.ts
Normal file
161
apps/web-naive/src/api/core/auth.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { AuthPermissionInfo } from '@vben/types';
|
||||
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
captchaVerification?: string;
|
||||
// 绑定社交登录时,需要传递如下参数
|
||||
socialType?: number;
|
||||
socialCode?: string;
|
||||
socialState?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userId: number;
|
||||
expiresTime: number;
|
||||
}
|
||||
|
||||
/** 租户信息返回值 */
|
||||
export interface TenantResult {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** 手机验证码获取接口参数 */
|
||||
export interface SmsCodeParams {
|
||||
mobile: string;
|
||||
scene: number;
|
||||
}
|
||||
|
||||
/** 手机验证码登录接口参数 */
|
||||
export interface SmsLoginParams {
|
||||
mobile: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** 注册接口参数 */
|
||||
export interface RegisterParams {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaVerification: string;
|
||||
}
|
||||
|
||||
/** 重置密码接口参数 */
|
||||
export interface ResetPasswordParams {
|
||||
password: string;
|
||||
mobile: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** 社交快捷登录接口参数 */
|
||||
export interface SocialLoginParams {
|
||||
type: number;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录 */
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data, {
|
||||
headers: {
|
||||
isEncrypt: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新 accessToken */
|
||||
export async function refreshTokenApi(refreshToken: string) {
|
||||
return baseRequestClient.post(
|
||||
`/system/auth/refresh-token?refreshToken=${refreshToken}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
export async function logoutApi(accessToken: string) {
|
||||
return baseRequestClient.post(
|
||||
'/system/auth/logout',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取权限信息 */
|
||||
export async function getAuthPermissionInfoApi() {
|
||||
return requestClient.get<AuthPermissionInfo>(
|
||||
'/system/auth/get-permission-info',
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取租户列表 */
|
||||
export async function getTenantSimpleList() {
|
||||
return requestClient.get<AuthApi.TenantResult[]>(
|
||||
`/system/tenant/simple-list`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 使用租户域名,获得租户信息 */
|
||||
export async function getTenantByWebsite(website: string) {
|
||||
return requestClient.get<AuthApi.TenantResult>(
|
||||
`/system/tenant/get-by-website?website=${website}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取验证码 */
|
||||
export async function getCaptcha(data: any) {
|
||||
return baseRequestClient.post('/system/captcha/get', data);
|
||||
}
|
||||
|
||||
/** 校验验证码 */
|
||||
export async function checkCaptcha(data: any) {
|
||||
return baseRequestClient.post('/system/captcha/check', data);
|
||||
}
|
||||
|
||||
/** 获取登录验证码 */
|
||||
export async function sendSmsCode(data: AuthApi.SmsCodeParams) {
|
||||
return requestClient.post('/system/auth/send-sms-code', data);
|
||||
}
|
||||
|
||||
/** 短信验证码登录 */
|
||||
export async function smsLogin(data: AuthApi.SmsLoginParams) {
|
||||
return requestClient.post('/system/auth/sms-login', data);
|
||||
}
|
||||
|
||||
/** 注册 */
|
||||
export async function register(data: AuthApi.RegisterParams) {
|
||||
return requestClient.post('/system/auth/register', data);
|
||||
}
|
||||
|
||||
/** 通过短信重置密码 */
|
||||
export async function smsResetPassword(data: AuthApi.ResetPasswordParams) {
|
||||
return requestClient.post('/system/auth/reset-password', data);
|
||||
}
|
||||
|
||||
/** 社交授权的跳转 */
|
||||
export async function socialAuthRedirect(type: number, redirectUri: string) {
|
||||
return requestClient.get('/system/auth/social-auth-redirect', {
|
||||
params: {
|
||||
type,
|
||||
redirectUri,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 社交快捷登录 */
|
||||
export async function socialLogin(data: AuthApi.SocialLoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>(
|
||||
'/system/auth/social-login',
|
||||
data,
|
||||
);
|
||||
}
|
||||
168
apps/web-naive/src/api/infra/codegen/index.ts
Normal file
168
apps/web-naive/src/api/infra/codegen/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraCodegenApi {
|
||||
/** 代码生成表定义 */
|
||||
export interface CodegenTable {
|
||||
id: number;
|
||||
tableId: number;
|
||||
isParentMenuIdValid: boolean;
|
||||
dataSourceConfigId: number;
|
||||
scene: number;
|
||||
tableName: string;
|
||||
tableComment: string;
|
||||
remark: string;
|
||||
moduleName: string;
|
||||
businessName: string;
|
||||
className: string;
|
||||
classComment: string;
|
||||
author: string;
|
||||
createTime: Date;
|
||||
updateTime: Date;
|
||||
templateType: number;
|
||||
parentMenuId: number;
|
||||
}
|
||||
|
||||
/** 代码生成字段定义 */
|
||||
export interface CodegenColumn {
|
||||
id: number;
|
||||
tableId: number;
|
||||
columnName: string;
|
||||
dataType: string;
|
||||
columnComment: string;
|
||||
nullable: number;
|
||||
primaryKey: number;
|
||||
ordinalPosition: number;
|
||||
javaType: string;
|
||||
javaField: string;
|
||||
dictType: string;
|
||||
example: string;
|
||||
createOperation: number;
|
||||
updateOperation: number;
|
||||
listOperation: number;
|
||||
listOperationCondition: string;
|
||||
listOperationResult: number;
|
||||
htmlType: string;
|
||||
}
|
||||
|
||||
/** 数据库表定义 */
|
||||
export interface DatabaseTable {
|
||||
name: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
/** 代码生成详情 */
|
||||
export interface CodegenDetail {
|
||||
table: CodegenTable;
|
||||
columns: CodegenColumn[];
|
||||
}
|
||||
|
||||
/** 代码预览 */
|
||||
export interface CodegenPreview {
|
||||
filePath: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** 更新代码生成请求 */
|
||||
export interface CodegenUpdateReqVO {
|
||||
table: any | CodegenTable;
|
||||
columns: CodegenColumn[];
|
||||
}
|
||||
|
||||
/** 创建代码生成请求 */
|
||||
export interface CodegenCreateListReqVO {
|
||||
dataSourceConfigId?: number;
|
||||
tableNames: string[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询列表代码生成表定义 */
|
||||
export function getCodegenTableList(dataSourceConfigId: number) {
|
||||
return requestClient.get<InfraCodegenApi.CodegenTable[]>(
|
||||
'/infra/codegen/table/list?',
|
||||
{
|
||||
params: { dataSourceConfigId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询列表代码生成表定义 */
|
||||
export function getCodegenTablePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraCodegenApi.CodegenTable>>(
|
||||
'/infra/codegen/table/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询详情代码生成表定义 */
|
||||
export function getCodegenTable(tableId: number) {
|
||||
return requestClient.get<InfraCodegenApi.CodegenDetail>(
|
||||
'/infra/codegen/detail',
|
||||
{
|
||||
params: { tableId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 修改代码生成表定义 */
|
||||
export function updateCodegenTable(data: InfraCodegenApi.CodegenUpdateReqVO) {
|
||||
return requestClient.put('/infra/codegen/update', data);
|
||||
}
|
||||
|
||||
/** 基于数据库的表结构,同步数据库的表和字段定义 */
|
||||
export function syncCodegenFromDB(tableId: number) {
|
||||
return requestClient.put(
|
||||
'/infra/codegen/sync-from-db',
|
||||
{},
|
||||
{
|
||||
params: { tableId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 预览生成代码 */
|
||||
export function previewCodegen(tableId: number) {
|
||||
return requestClient.get<InfraCodegenApi.CodegenPreview[]>(
|
||||
'/infra/codegen/preview',
|
||||
{
|
||||
params: { tableId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 下载生成代码 */
|
||||
export function downloadCodegen(tableId: number) {
|
||||
return requestClient.download('/infra/codegen/download', {
|
||||
params: { tableId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获得表定义 */
|
||||
export function getSchemaTableList(params: any) {
|
||||
return requestClient.get<InfraCodegenApi.DatabaseTable[]>(
|
||||
'/infra/codegen/db/table/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 基于数据库的表结构,创建代码生成器的表定义 */
|
||||
export function createCodegenList(
|
||||
data: InfraCodegenApi.CodegenCreateListReqVO,
|
||||
) {
|
||||
return requestClient.post('/infra/codegen/create-list', data);
|
||||
}
|
||||
|
||||
/** 删除代码生成表定义 */
|
||||
export function deleteCodegenTable(tableId: number) {
|
||||
return requestClient.delete('/infra/codegen/delete', {
|
||||
params: { tableId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除代码生成表定义 */
|
||||
export function deleteCodegenTableList(tableIds: number[]) {
|
||||
return requestClient.delete(
|
||||
`/infra/codegen/delete-list?tableIds=${tableIds.join(',')}`,
|
||||
);
|
||||
}
|
||||
67
apps/web-naive/src/api/infra/config/index.ts
Normal file
67
apps/web-naive/src/api/infra/config/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraConfigApi {
|
||||
/** 参数配置信息 */
|
||||
export interface Config {
|
||||
id?: number;
|
||||
category: string;
|
||||
name: string;
|
||||
key: string;
|
||||
value: string;
|
||||
type: number;
|
||||
visible: boolean;
|
||||
remark: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询参数列表 */
|
||||
export function getConfigPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraConfigApi.Config>>(
|
||||
'/infra/config/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询参数详情 */
|
||||
export function getConfig(id: number) {
|
||||
return requestClient.get<InfraConfigApi.Config>(`/infra/config/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 根据参数键名查询参数值 */
|
||||
export function getConfigKey(configKey: string) {
|
||||
return requestClient.get<string>(
|
||||
`/infra/config/get-value-by-key?key=${configKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增参数 */
|
||||
export function createConfig(data: InfraConfigApi.Config) {
|
||||
return requestClient.post('/infra/config/create', data);
|
||||
}
|
||||
|
||||
/** 修改参数 */
|
||||
export function updateConfig(data: InfraConfigApi.Config) {
|
||||
return requestClient.put('/infra/config/update', data);
|
||||
}
|
||||
|
||||
/** 删除参数 */
|
||||
export function deleteConfig(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/config/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
53
apps/web-naive/src/api/infra/data-source-config/index.ts
Normal file
53
apps/web-naive/src/api/infra/data-source-config/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraDataSourceConfigApi {
|
||||
/** 数据源配置信息 */
|
||||
export interface DataSourceConfig {
|
||||
id?: number;
|
||||
name: string;
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询数据源配置列表 */
|
||||
export function getDataSourceConfigList() {
|
||||
return requestClient.get<InfraDataSourceConfigApi.DataSourceConfig[]>(
|
||||
'/infra/data-source-config/list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询数据源配置详情 */
|
||||
export function getDataSourceConfig(id: number) {
|
||||
return requestClient.get<InfraDataSourceConfigApi.DataSourceConfig>(
|
||||
`/infra/data-source-config/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增数据源配置 */
|
||||
export function createDataSourceConfig(
|
||||
data: InfraDataSourceConfigApi.DataSourceConfig,
|
||||
) {
|
||||
return requestClient.post('/infra/data-source-config/create', data);
|
||||
}
|
||||
|
||||
/** 修改数据源配置 */
|
||||
export function updateDataSourceConfig(
|
||||
data: InfraDataSourceConfigApi.DataSourceConfig,
|
||||
) {
|
||||
return requestClient.put('/infra/data-source-config/update', data);
|
||||
}
|
||||
|
||||
/** 删除数据源配置 */
|
||||
export function deleteDataSourceConfig(id: number) {
|
||||
return requestClient.delete(`/infra/data-source-config/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除数据源配置 */
|
||||
export function deleteDataSourceConfigList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/infra/data-source-config/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
61
apps/web-naive/src/api/infra/demo/demo01/index.ts
Normal file
61
apps/web-naive/src/api/infra/demo/demo01/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace Demo01ContactApi {
|
||||
/** 示例联系人信息 */
|
||||
export interface Demo01Contact {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
sex?: number; // 性别
|
||||
birthday?: Dayjs | string; // 出生年
|
||||
description?: string; // 简介
|
||||
avatar: string; // 头像
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询示例联系人分页 */
|
||||
export function getDemo01ContactPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo01ContactApi.Demo01Contact>>(
|
||||
'/infra/demo01-contact/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询示例联系人详情 */
|
||||
export function getDemo01Contact(id: number) {
|
||||
return requestClient.get<Demo01ContactApi.Demo01Contact>(
|
||||
`/infra/demo01-contact/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增示例联系人 */
|
||||
export function createDemo01Contact(data: Demo01ContactApi.Demo01Contact) {
|
||||
return requestClient.post('/infra/demo01-contact/create', data);
|
||||
}
|
||||
|
||||
/** 修改示例联系人 */
|
||||
export function updateDemo01Contact(data: Demo01ContactApi.Demo01Contact) {
|
||||
return requestClient.put('/infra/demo01-contact/update', data);
|
||||
}
|
||||
|
||||
/** 删除示例联系人 */
|
||||
export function deleteDemo01Contact(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/demo01-contact/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
48
apps/web-naive/src/api/infra/demo/demo02/index.ts
Normal file
48
apps/web-naive/src/api/infra/demo/demo02/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace Demo02CategoryApi {
|
||||
/** 示例分类信息 */
|
||||
export interface Demo02Category {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
parentId?: number; // 父级编号
|
||||
children?: Demo02Category[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询示例分类列表 */
|
||||
export function getDemo02CategoryList(params: any) {
|
||||
return requestClient.get<Demo02CategoryApi.Demo02Category[]>(
|
||||
'/infra/demo02-category/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询示例分类详情 */
|
||||
export function getDemo02Category(id: number) {
|
||||
return requestClient.get<Demo02CategoryApi.Demo02Category>(
|
||||
`/infra/demo02-category/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增示例分类 */
|
||||
export function createDemo02Category(data: Demo02CategoryApi.Demo02Category) {
|
||||
return requestClient.post('/infra/demo02-category/create', data);
|
||||
}
|
||||
|
||||
/** 修改示例分类 */
|
||||
export function updateDemo02Category(data: Demo02CategoryApi.Demo02Category) {
|
||||
return requestClient.put('/infra/demo02-category/update', data);
|
||||
}
|
||||
|
||||
/** 删除示例分类 */
|
||||
export function deleteDemo02Category(id: number) {
|
||||
return requestClient.delete(`/infra/demo02-category/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 导出示例分类 */
|
||||
export function exportDemo02Category(params: any) {
|
||||
return requestClient.download('/infra/demo02-category/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
168
apps/web-naive/src/api/infra/demo/demo03/erp/index.ts
Normal file
168
apps/web-naive/src/api/infra/demo/demo03/erp/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace Demo03StudentApi {
|
||||
/** 学生课程信息 */
|
||||
export interface Demo03Course {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
score?: number; // 分数
|
||||
}
|
||||
|
||||
/** 学生班级信息 */
|
||||
export interface Demo03Grade {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
teacher?: string; // 班主任
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Demo03Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
sex?: number; // 性别
|
||||
birthday?: Dayjs | string; // 出生日期
|
||||
description?: string; // 简介
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getDemo03StudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
|
||||
'/infra/demo03-student-erp/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getDemo03Student(id: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Student>(
|
||||
`/infra/demo03-student-erp/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.post('/infra/demo03-student-erp/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.put('/infra/demo03-student-erp/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteDemo03Student(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/demo03-student-erp/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 获得学生课程分页 */
|
||||
export function getDemo03CoursePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo03StudentApi.Demo03Course>>(
|
||||
`/infra/demo03-student-erp/demo03-course/page`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
/** 新增学生课程 */
|
||||
export function createDemo03Course(data: Demo03StudentApi.Demo03Course) {
|
||||
return requestClient.post(
|
||||
`/infra/demo03-student-erp/demo03-course/create`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 修改学生课程 */
|
||||
export function updateDemo03Course(data: Demo03StudentApi.Demo03Course) {
|
||||
return requestClient.put(
|
||||
`/infra/demo03-student-erp/demo03-course/update`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除学生课程 */
|
||||
export function deleteDemo03Course(id: number) {
|
||||
return requestClient.delete(
|
||||
`/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) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Course>(
|
||||
`/infra/demo03-student-erp/demo03-course/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 获得学生班级分页 */
|
||||
export function getDemo03GradePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo03StudentApi.Demo03Grade>>(
|
||||
`/infra/demo03-student-erp/demo03-grade/page`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
/** 新增学生班级 */
|
||||
export function createDemo03Grade(data: Demo03StudentApi.Demo03Grade) {
|
||||
return requestClient.post(
|
||||
`/infra/demo03-student-erp/demo03-grade/create`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 修改学生班级 */
|
||||
export function updateDemo03Grade(data: Demo03StudentApi.Demo03Grade) {
|
||||
return requestClient.put(
|
||||
`/infra/demo03-student-erp/demo03-grade/update`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除学生班级 */
|
||||
export function deleteDemo03Grade(id: number) {
|
||||
return requestClient.delete(
|
||||
`/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) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Grade>(
|
||||
`/infra/demo03-student-erp/demo03-grade/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
96
apps/web-naive/src/api/infra/demo/demo03/inner/index.ts
Normal file
96
apps/web-naive/src/api/infra/demo/demo03/inner/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace Demo03StudentApi {
|
||||
/** 学生课程信息 */
|
||||
export interface Demo03Course {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
score?: number; // 分数
|
||||
}
|
||||
|
||||
/** 学生班级信息 */
|
||||
export interface Demo03Grade {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
teacher?: string; // 班主任
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Demo03Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
sex?: number; // 性别
|
||||
birthday?: Dayjs | string; // 出生日期
|
||||
description?: string; // 简介
|
||||
demo03courses?: Demo03Course[];
|
||||
demo03grade?: Demo03Grade;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getDemo03StudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
|
||||
'/infra/demo03-student-inner/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getDemo03Student(id: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Student>(
|
||||
`/infra/demo03-student-inner/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.post('/infra/demo03-student-inner/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.put('/infra/demo03-student-inner/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteDemo03Student(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/demo03-student-inner/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 获得学生课程列表 */
|
||||
export function getDemo03CourseListByStudentId(studentId: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Course[]>(
|
||||
`/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=${studentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 获得学生班级 */
|
||||
export function getDemo03GradeByStudentId(studentId: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Grade>(
|
||||
`/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=${studentId}`,
|
||||
);
|
||||
}
|
||||
96
apps/web-naive/src/api/infra/demo/demo03/normal/index.ts
Normal file
96
apps/web-naive/src/api/infra/demo/demo03/normal/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace Demo03StudentApi {
|
||||
/** 学生课程信息 */
|
||||
export interface Demo03Course {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
score?: number; // 分数
|
||||
}
|
||||
|
||||
/** 学生班级信息 */
|
||||
export interface Demo03Grade {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
teacher?: string; // 班主任
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Demo03Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
sex?: number; // 性别
|
||||
birthday?: Dayjs | string; // 出生日期
|
||||
description?: string; // 简介
|
||||
demo03courses?: Demo03Course[];
|
||||
demo03grade?: Demo03Grade;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getDemo03StudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>(
|
||||
'/infra/demo03-student-normal/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getDemo03Student(id: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Student>(
|
||||
`/infra/demo03-student-normal/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.post('/infra/demo03-student-normal/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
|
||||
return requestClient.put('/infra/demo03-student-normal/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteDemo03Student(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/demo03-student-normal/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 获得学生课程列表 */
|
||||
export function getDemo03CourseListByStudentId(studentId: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Course[]>(
|
||||
`/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=${studentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 获得学生班级 */
|
||||
export function getDemo03GradeByStudentId(studentId: number) {
|
||||
return requestClient.get<Demo03StudentApi.Demo03Grade>(
|
||||
`/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=${studentId}`,
|
||||
);
|
||||
}
|
||||
83
apps/web-naive/src/api/infra/file-config/index.ts
Normal file
83
apps/web-naive/src/api/infra/file-config/index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraFileConfigApi {
|
||||
/** 文件客户端配置 */
|
||||
export interface FileClientConfig {
|
||||
basePath: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
mode?: string;
|
||||
endpoint?: string;
|
||||
bucket?: string;
|
||||
accessKey?: string;
|
||||
accessSecret?: string;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
/** 文件配置信息 */
|
||||
export interface FileConfig {
|
||||
id?: number;
|
||||
name: string;
|
||||
storage?: number;
|
||||
master: boolean;
|
||||
visible: boolean;
|
||||
config: FileClientConfig;
|
||||
remark: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询文件配置列表 */
|
||||
export function getFileConfigPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraFileConfigApi.FileConfig>>(
|
||||
'/infra/file-config/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询文件配置详情 */
|
||||
export function getFileConfig(id: number) {
|
||||
return requestClient.get<InfraFileConfigApi.FileConfig>(
|
||||
`/infra/file-config/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 更新文件配置为主配置 */
|
||||
export function updateFileConfigMaster(id: number) {
|
||||
return requestClient.put(`/infra/file-config/update-master?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增文件配置 */
|
||||
export function createFileConfig(data: InfraFileConfigApi.FileConfig) {
|
||||
return requestClient.post('/infra/file-config/create', data);
|
||||
}
|
||||
|
||||
/** 修改文件配置 */
|
||||
export function updateFileConfig(data: InfraFileConfigApi.FileConfig) {
|
||||
return requestClient.put('/infra/file-config/update', data);
|
||||
}
|
||||
|
||||
/** 删除文件配置 */
|
||||
export function deleteFileConfig(id: number) {
|
||||
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) {
|
||||
return requestClient.get(`/infra/file-config/test?id=${id}`);
|
||||
}
|
||||
78
apps/web-naive/src/api/infra/file/index.ts
Normal file
78
apps/web-naive/src/api/infra/file/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { AxiosRequestConfig, PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** Axios 上传进度事件 */
|
||||
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
|
||||
|
||||
export namespace InfraFileApi {
|
||||
/** 文件信息 */
|
||||
export interface File {
|
||||
id?: number;
|
||||
configId?: number;
|
||||
path: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
size?: number;
|
||||
type?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 文件预签名地址 */
|
||||
export interface FilePresignedUrlRespVO {
|
||||
configId: number; // 文件配置编号
|
||||
uploadUrl: string; // 文件上传 URL
|
||||
url: string; // 文件 URL
|
||||
path: string; // 文件路径
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
export interface FileUploadReqVO {
|
||||
file: globalThis.File;
|
||||
directory?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询文件列表 */
|
||||
export function getFilePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraFileApi.File>>('/infra/file/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
export function deleteFile(id: number) {
|
||||
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) {
|
||||
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
|
||||
'/infra/file/presigned-url',
|
||||
{
|
||||
params: { name, directory },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建文件 */
|
||||
export function createFile(data: InfraFileApi.File) {
|
||||
return requestClient.post('/infra/file/create', data);
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
export function uploadFile(
|
||||
data: InfraFileApi.FileUploadReqVO,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) {
|
||||
// 特殊:由于 upload 内部封装,即使 directory 为 undefined,也会传递给后端
|
||||
if (!data.directory) {
|
||||
delete data.directory;
|
||||
}
|
||||
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
|
||||
}
|
||||
77
apps/web-naive/src/api/infra/job/index.ts
Normal file
77
apps/web-naive/src/api/infra/job/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraJobApi {
|
||||
/** 任务信息 */
|
||||
export interface Job {
|
||||
id?: number;
|
||||
name: string;
|
||||
status: number;
|
||||
handlerName: string;
|
||||
handlerParam: string;
|
||||
cronExpression: string;
|
||||
retryCount: number;
|
||||
retryInterval: number;
|
||||
monitorTimeout: number;
|
||||
createTime?: Date;
|
||||
nextTimes?: Date[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询任务列表 */
|
||||
export function getJobPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraJobApi.Job>>('/infra/job/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询任务详情 */
|
||||
export function getJob(id: number) {
|
||||
return requestClient.get<InfraJobApi.Job>(`/infra/job/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增任务 */
|
||||
export function createJob(data: InfraJobApi.Job) {
|
||||
return requestClient.post('/infra/job/create', data);
|
||||
}
|
||||
|
||||
/** 修改定时任务调度 */
|
||||
export function updateJob(data: InfraJobApi.Job) {
|
||||
return requestClient.put('/infra/job/update', data);
|
||||
}
|
||||
|
||||
/** 删除定时任务调度 */
|
||||
export function deleteJob(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/infra/job/export-excel', { params });
|
||||
}
|
||||
|
||||
/** 任务状态修改 */
|
||||
export function updateJobStatus(id: number, status: number) {
|
||||
return requestClient.put('/infra/job/update-status', undefined, {
|
||||
params: {
|
||||
id,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 定时任务立即执行一次 */
|
||||
export function runJob(id: number) {
|
||||
return requestClient.put(`/infra/job/trigger?id=${id}`);
|
||||
}
|
||||
|
||||
/** 获得定时任务的下 n 次执行时间 */
|
||||
export function getJobNextTimes(id: number) {
|
||||
return requestClient.get(`/infra/job/get_next_times?id=${id}`);
|
||||
}
|
||||
186
apps/web-naive/src/api/request.ts
Normal file
186
apps/web-naive/src/api/request.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { isTenantEnable, useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { createApiEncrypt } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const tenantEnable = isTenantEnable();
|
||||
const apiEncrypt = createApiEncrypt(import.meta.env);
|
||||
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
async function doReAuthenticate() {
|
||||
console.warn('Access token or refresh token is invalid or expired. ');
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
accessStore.setAccessToken(null);
|
||||
if (
|
||||
preferences.app.loginExpiredMode === 'modal' &&
|
||||
accessStore.isAccessChecked
|
||||
) {
|
||||
accessStore.setLoginExpired(true);
|
||||
} else {
|
||||
await authStore.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token逻辑
|
||||
*/
|
||||
async function doRefreshToken() {
|
||||
const accessStore = useAccessStore();
|
||||
const refreshToken = accessStore.refreshToken as string;
|
||||
if (!refreshToken) {
|
||||
throw new Error('Refresh token is null!');
|
||||
}
|
||||
const resp = await refreshTokenApi(refreshToken);
|
||||
const newToken = resp?.data?.data?.accessToken;
|
||||
// add by 芋艿:这里一定要抛出 resp.data,从而触发 authenticateResponseInterceptor 中,刷新令牌失败!!!
|
||||
if (!newToken) {
|
||||
throw resp.data;
|
||||
}
|
||||
accessStore.setAccessToken(newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
function formatToken(token: null | string) {
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
// 请求头处理
|
||||
client.addRequestInterceptor({
|
||||
fulfilled: async (config) => {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||
config.headers['Accept-Language'] = preferences.app.locale;
|
||||
// 添加租户编号
|
||||
config.headers['tenant-id'] = tenantEnable
|
||||
? accessStore.tenantId
|
||||
: undefined;
|
||||
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||
config.headers['visit-tenant-id'] = tenantEnable
|
||||
? accessStore.visitTenantId
|
||||
: undefined;
|
||||
|
||||
// 是否 API 加密
|
||||
if ((config.headers || {}).isEncrypt) {
|
||||
try {
|
||||
// 加密请求数据
|
||||
if (config.data) {
|
||||
config.data = apiEncrypt.encryptRequest(config.data);
|
||||
// 设置加密标识头
|
||||
config.headers[apiEncrypt.getEncryptHeader()] = 'true';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求数据加密失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
// API 解密响应拦截器
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: (response) => {
|
||||
// 检查是否需要解密响应数据
|
||||
const encryptHeader = apiEncrypt.getEncryptHeader();
|
||||
const isEncryptResponse =
|
||||
response.headers[encryptHeader] === 'true' ||
|
||||
response.headers[encryptHeader.toLowerCase()] === 'true';
|
||||
if (isEncryptResponse && typeof response.data === 'string') {
|
||||
try {
|
||||
// 解密响应数据
|
||||
response.data = apiEncrypt.decryptResponse(response.data);
|
||||
} catch (error) {
|
||||
console.error('响应数据解密失败:', error);
|
||||
throw new Error(`响应数据解密失败: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
authenticateResponseInterceptor({
|
||||
client,
|
||||
doReAuthenticate,
|
||||
doRefreshToken,
|
||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||
formatToken,
|
||||
}),
|
||||
);
|
||||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string, error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
// 当前mock接口返回的错误字段是 error 或者 message
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const errorMessage =
|
||||
responseData?.error ?? responseData?.message ?? responseData.msg ?? '';
|
||||
// add by 芋艿:特殊:避免 401 “账号未登录”,重复提示。因为,此时会跳转到登录界面,只需提示一次!!!
|
||||
if (error?.data?.code === 401) {
|
||||
return;
|
||||
}
|
||||
// 如果没有错误信息,则会根据状态码进行提示
|
||||
message.error(errorMessage || msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
baseRequestClient.addRequestInterceptor({
|
||||
fulfilled: (config) => {
|
||||
const accessStore = useAccessStore();
|
||||
// 添加租户编号
|
||||
config.headers['tenant-id'] = tenantEnable
|
||||
? accessStore.tenantId
|
||||
: undefined;
|
||||
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||
config.headers['visit-tenant-id'] = tenantEnable
|
||||
? accessStore.visitTenantId
|
||||
: undefined;
|
||||
return config;
|
||||
},
|
||||
});
|
||||
52
apps/web-naive/src/api/system/dept/index.ts
Normal file
52
apps/web-naive/src/api/system/dept/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDeptApi {
|
||||
/** 部门信息 */
|
||||
export interface Dept {
|
||||
id?: number;
|
||||
name: string;
|
||||
parentId?: number;
|
||||
status: number;
|
||||
sort: number;
|
||||
leaderUserId: number;
|
||||
phone: string;
|
||||
email: string;
|
||||
createTime: Date;
|
||||
children?: Dept[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询部门(精简)列表 */
|
||||
export async function getSimpleDeptList() {
|
||||
return requestClient.get<SystemDeptApi.Dept[]>('/system/dept/simple-list');
|
||||
}
|
||||
|
||||
/** 查询部门列表 */
|
||||
export async function getDeptList() {
|
||||
return requestClient.get('/system/dept/list');
|
||||
}
|
||||
|
||||
/** 查询部门详情 */
|
||||
export async function getDept(id: number) {
|
||||
return requestClient.get<SystemDeptApi.Dept>(`/system/dept/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增部门 */
|
||||
export async function createDept(data: SystemDeptApi.Dept) {
|
||||
return requestClient.post('/system/dept/create', data);
|
||||
}
|
||||
|
||||
/** 修改部门 */
|
||||
export async function updateDept(data: SystemDeptApi.Dept) {
|
||||
return requestClient.put('/system/dept/update', data);
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
export async function deleteDept(id: number) {
|
||||
return requestClient.delete(`/system/dept/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除部门 */
|
||||
export async function deleteDeptList(ids: number[]) {
|
||||
return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
68
apps/web-naive/src/api/system/dict/data/index.ts
Normal file
68
apps/web-naive/src/api/system/dict/data/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDictDataApi {
|
||||
/** 字典数据 */
|
||||
export type DictData = {
|
||||
colorType: string;
|
||||
createTime: Date;
|
||||
cssClass: string;
|
||||
dictType: string;
|
||||
id?: number;
|
||||
label: string;
|
||||
remark: string;
|
||||
sort?: number;
|
||||
status: number;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 查询字典数据(精简)列表
|
||||
export function getSimpleDictDataList() {
|
||||
return requestClient.get<SystemDictDataApi.DictData[]>(
|
||||
'/system/dict-data/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
// 查询字典数据列表
|
||||
export function getDictDataPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemDictDataApi.DictData>>(
|
||||
'/system/dict-data/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 查询字典数据详情
|
||||
export function getDictData(id: number) {
|
||||
return requestClient.get<SystemDictDataApi.DictData>(
|
||||
`/system/dict-data/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export function createDictData(data: SystemDictDataApi.DictData) {
|
||||
return requestClient.post('/system/dict-data/create', data);
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export function updateDictData(data: SystemDictDataApi.DictData) {
|
||||
return requestClient.put('/system/dict-data/update', data);
|
||||
}
|
||||
|
||||
// 删除字典数据
|
||||
export function deleteDictData(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/dict-data/export-excel', { params });
|
||||
}
|
||||
64
apps/web-naive/src/api/system/dict/type/index.ts
Normal file
64
apps/web-naive/src/api/system/dict/type/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDictTypeApi {
|
||||
/** 字典类型 */
|
||||
export type DictType = {
|
||||
createTime: Date;
|
||||
id?: number;
|
||||
name: string;
|
||||
remark: string;
|
||||
status: number;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 查询字典(精简)列表
|
||||
export function getSimpleDictTypeList() {
|
||||
return requestClient.get<SystemDictTypeApi.DictType[]>(
|
||||
'/system/dict-type/list-all-simple',
|
||||
);
|
||||
}
|
||||
|
||||
// 查询字典列表
|
||||
export function getDictTypePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemDictTypeApi.DictType>>(
|
||||
'/system/dict-type/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 查询字典详情
|
||||
export function getDictType(id: number) {
|
||||
return requestClient.get<SystemDictTypeApi.DictType>(
|
||||
`/system/dict-type/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 新增字典
|
||||
export function createDictType(data: SystemDictTypeApi.DictType) {
|
||||
return requestClient.post('/system/dict-type/create', data);
|
||||
}
|
||||
|
||||
// 修改字典
|
||||
export function updateDictType(data: SystemDictTypeApi.DictType) {
|
||||
return requestClient.put('/system/dict-type/update', data);
|
||||
}
|
||||
|
||||
// 删除字典
|
||||
export function deleteDictType(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/dict-type/export-excel', { params });
|
||||
}
|
||||
64
apps/web-naive/src/api/system/mail/account/index.ts
Normal file
64
apps/web-naive/src/api/system/mail/account/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemMailAccountApi {
|
||||
/** 邮箱账号 */
|
||||
export interface MailAccount {
|
||||
id: number;
|
||||
mail: string;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
sslEnable: boolean;
|
||||
starttlsEnable: boolean;
|
||||
status: number;
|
||||
createTime: Date;
|
||||
remark: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询邮箱账号列表 */
|
||||
export function getMailAccountPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemMailAccountApi.MailAccount>>(
|
||||
'/system/mail-account/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询邮箱账号详情 */
|
||||
export function getMailAccount(id: number) {
|
||||
return requestClient.get<SystemMailAccountApi.MailAccount>(
|
||||
`/system/mail-account/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增邮箱账号 */
|
||||
export function createMailAccount(data: SystemMailAccountApi.MailAccount) {
|
||||
return requestClient.post('/system/mail-account/create', data);
|
||||
}
|
||||
|
||||
/** 修改邮箱账号 */
|
||||
export function updateMailAccount(data: SystemMailAccountApi.MailAccount) {
|
||||
return requestClient.put('/system/mail-account/update', data);
|
||||
}
|
||||
|
||||
/** 删除邮箱账号 */
|
||||
export function deleteMailAccount(id: number) {
|
||||
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() {
|
||||
return requestClient.get<SystemMailAccountApi.MailAccount[]>(
|
||||
'/system/mail-account/simple-list',
|
||||
);
|
||||
}
|
||||
36
apps/web-naive/src/api/system/mail/log/index.ts
Normal file
36
apps/web-naive/src/api/system/mail/log/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemMailLogApi {
|
||||
/** 邮件日志 */
|
||||
export interface MailLog {
|
||||
id: number;
|
||||
userId: number;
|
||||
userType: number;
|
||||
toMails: string[];
|
||||
ccMails?: string[];
|
||||
bccMails?: string[];
|
||||
accountId: number;
|
||||
fromMail: string;
|
||||
templateId: number;
|
||||
templateCode: string;
|
||||
templateNickname: string;
|
||||
templateTitle: string;
|
||||
templateContent: string;
|
||||
templateParams: string;
|
||||
sendStatus: number;
|
||||
sendTime: string;
|
||||
sendMessageId: string;
|
||||
sendException: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询邮件日志列表 */
|
||||
export function getMailLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemMailLogApi.MailLog>>(
|
||||
'/system/mail-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
71
apps/web-naive/src/api/system/mail/template/index.ts
Normal file
71
apps/web-naive/src/api/system/mail/template/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemMailTemplateApi {
|
||||
/** 邮件模版信息 */
|
||||
export interface MailTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
accountId: number;
|
||||
nickname: string;
|
||||
title: string;
|
||||
content: string;
|
||||
params: string[];
|
||||
status: number;
|
||||
remark: string;
|
||||
createTime: Date;
|
||||
}
|
||||
|
||||
/** 邮件发送信息 */
|
||||
export interface MailSendReqVO {
|
||||
toMails: string[];
|
||||
ccMails?: string[];
|
||||
bccMails?: string[];
|
||||
templateCode: string;
|
||||
templateParams: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询邮件模版列表 */
|
||||
export function getMailTemplatePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemMailTemplateApi.MailTemplate>>(
|
||||
'/system/mail-template/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询邮件模版详情 */
|
||||
export function getMailTemplate(id: number) {
|
||||
return requestClient.get<SystemMailTemplateApi.MailTemplate>(
|
||||
`/system/mail-template/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增邮件模版 */
|
||||
export function createMailTemplate(data: SystemMailTemplateApi.MailTemplate) {
|
||||
return requestClient.post('/system/mail-template/create', data);
|
||||
}
|
||||
|
||||
/** 修改邮件模版 */
|
||||
export function updateMailTemplate(data: SystemMailTemplateApi.MailTemplate) {
|
||||
return requestClient.put('/system/mail-template/update', data);
|
||||
}
|
||||
|
||||
/** 删除邮件模版 */
|
||||
export function deleteMailTemplate(id: number) {
|
||||
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) {
|
||||
return requestClient.post('/system/mail-template/send-mail', data);
|
||||
}
|
||||
59
apps/web-naive/src/api/system/menu/index.ts
Normal file
59
apps/web-naive/src/api/system/menu/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemMenuApi {
|
||||
/** 菜单信息 */
|
||||
export interface Menu {
|
||||
id: number;
|
||||
name: string;
|
||||
permission: string;
|
||||
type: number;
|
||||
sort: number;
|
||||
parentId: number;
|
||||
path: string;
|
||||
icon: string;
|
||||
component: string;
|
||||
componentName?: string;
|
||||
status: number;
|
||||
visible: boolean;
|
||||
keepAlive: boolean;
|
||||
alwaysShow?: boolean;
|
||||
createTime: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询菜单(精简)列表 */
|
||||
export async function getSimpleMenusList() {
|
||||
return requestClient.get<SystemMenuApi.Menu[]>('/system/menu/simple-list');
|
||||
}
|
||||
|
||||
/** 查询菜单列表 */
|
||||
export async function getMenuList(params?: Record<string, any>) {
|
||||
return requestClient.get<SystemMenuApi.Menu[]>('/system/menu/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取菜单详情 */
|
||||
export async function getMenu(id: number) {
|
||||
return requestClient.get<SystemMenuApi.Menu>(`/system/menu/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增菜单 */
|
||||
export async function createMenu(data: SystemMenuApi.Menu) {
|
||||
return requestClient.post('/system/menu/create', data);
|
||||
}
|
||||
|
||||
/** 修改菜单 */
|
||||
export async function updateMenu(data: SystemMenuApi.Menu) {
|
||||
return requestClient.put('/system/menu/update', data);
|
||||
}
|
||||
|
||||
/** 删除菜单 */
|
||||
export async function deleteMenu(id: number) {
|
||||
return requestClient.delete(`/system/menu/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除菜单 */
|
||||
export async function deleteMenuList(ids: number[]) {
|
||||
return requestClient.delete(`/system/menu/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
59
apps/web-naive/src/api/system/notice/index.ts
Normal file
59
apps/web-naive/src/api/system/notice/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemNoticeApi {
|
||||
/** 公告信息 */
|
||||
export interface Notice {
|
||||
id?: number;
|
||||
title: string;
|
||||
type: number;
|
||||
content: string;
|
||||
status: number;
|
||||
remark: string;
|
||||
creator?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询公告列表 */
|
||||
export function getNoticePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNoticeApi.Notice>>(
|
||||
'/system/notice/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询公告详情 */
|
||||
export function getNotice(id: number) {
|
||||
return requestClient.get<SystemNoticeApi.Notice>(
|
||||
`/system/notice/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增公告 */
|
||||
export function createNotice(data: SystemNoticeApi.Notice) {
|
||||
return requestClient.post('/system/notice/create', data);
|
||||
}
|
||||
|
||||
/** 修改公告 */
|
||||
export function updateNotice(data: SystemNoticeApi.Notice) {
|
||||
return requestClient.put('/system/notice/update', data);
|
||||
}
|
||||
|
||||
/** 删除公告 */
|
||||
export function deleteNotice(id: number) {
|
||||
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) {
|
||||
return requestClient.post(`/system/notice/push?id=${id}`);
|
||||
}
|
||||
79
apps/web-naive/src/api/system/notify/template/index.ts
Normal file
79
apps/web-naive/src/api/system/notify/template/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemNotifyTemplateApi {
|
||||
/** 站内信模板信息 */
|
||||
export interface NotifyTemplate {
|
||||
id?: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
code: string;
|
||||
content: string;
|
||||
type?: number;
|
||||
params: string[];
|
||||
status: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
/** 发送站内信请求 */
|
||||
export interface NotifySendReqVO {
|
||||
userId: number;
|
||||
userType: number;
|
||||
templateCode: string;
|
||||
templateParams: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询站内信模板列表 */
|
||||
export function getNotifyTemplatePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNotifyTemplateApi.NotifyTemplate>>(
|
||||
'/system/notify-template/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询站内信模板详情 */
|
||||
export function getNotifyTemplate(id: number) {
|
||||
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplate>(
|
||||
`/system/notify-template/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增站内信模板 */
|
||||
export function createNotifyTemplate(
|
||||
data: SystemNotifyTemplateApi.NotifyTemplate,
|
||||
) {
|
||||
return requestClient.post('/system/notify-template/create', data);
|
||||
}
|
||||
|
||||
/** 修改站内信模板 */
|
||||
export function updateNotifyTemplate(
|
||||
data: SystemNotifyTemplateApi.NotifyTemplate,
|
||||
) {
|
||||
return requestClient.put('/system/notify-template/update', data);
|
||||
}
|
||||
|
||||
/** 删除站内信模板 */
|
||||
export function deleteNotifyTemplate(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/notify-template/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 发送站内信 */
|
||||
export function sendNotify(data: SystemNotifyTemplateApi.NotifySendReqVO) {
|
||||
return requestClient.post('/system/notify-template/send-notify', data);
|
||||
}
|
||||
64
apps/web-naive/src/api/system/oauth2/client/index.ts
Normal file
64
apps/web-naive/src/api/system/oauth2/client/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemOAuth2ClientApi {
|
||||
/** OAuth2.0 客户端信息 */
|
||||
export interface OAuth2Client {
|
||||
id?: number;
|
||||
clientId: string;
|
||||
secret: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
status: number;
|
||||
accessTokenValiditySeconds: number;
|
||||
refreshTokenValiditySeconds: number;
|
||||
redirectUris: string[];
|
||||
autoApprove: boolean;
|
||||
authorizedGrantTypes: string[];
|
||||
scopes: string[];
|
||||
authorities: string[];
|
||||
resourceIds: string[];
|
||||
additionalInformation: string;
|
||||
isAdditionalInformationJson: boolean;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 OAuth2.0 客户端列表 */
|
||||
export function getOAuth2ClientPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemOAuth2ClientApi.OAuth2Client>>(
|
||||
'/system/oauth2-client/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询 OAuth2.0 客户端详情 */
|
||||
export function getOAuth2Client(id: number) {
|
||||
return requestClient.get<SystemOAuth2ClientApi.OAuth2Client>(
|
||||
`/system/oauth2-client/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增 OAuth2.0 客户端 */
|
||||
export function createOAuth2Client(data: SystemOAuth2ClientApi.OAuth2Client) {
|
||||
return requestClient.post('/system/oauth2-client/create', data);
|
||||
}
|
||||
|
||||
/** 修改 OAuth2.0 客户端 */
|
||||
export function updateOAuth2Client(data: SystemOAuth2ClientApi.OAuth2Client) {
|
||||
return requestClient.put('/system/oauth2-client/update', data);
|
||||
}
|
||||
|
||||
/** 删除 OAuth2.0 客户端 */
|
||||
export function deleteOAuth2Client(id: number) {
|
||||
return requestClient.delete(`/system/oauth2-client/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除 OAuth2.0 客户端 */
|
||||
export function deleteOAuth2ClientList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/oauth2-client/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
63
apps/web-naive/src/api/system/post/index.ts
Normal file
63
apps/web-naive/src/api/system/post/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemPostApi {
|
||||
/** 岗位信息 */
|
||||
export interface Post {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
sort: number;
|
||||
status: number;
|
||||
remark: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询岗位列表 */
|
||||
export function getPostPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemPostApi.Post>>(
|
||||
'/system/post/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取岗位精简信息列表 */
|
||||
export function getSimplePostList() {
|
||||
return requestClient.get<SystemPostApi.Post[]>('/system/post/simple-list');
|
||||
}
|
||||
|
||||
/** 查询岗位详情 */
|
||||
export function getPost(id: number) {
|
||||
return requestClient.get<SystemPostApi.Post>(`/system/post/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增岗位 */
|
||||
export function createPost(data: SystemPostApi.Post) {
|
||||
return requestClient.post('/system/post/create', data);
|
||||
}
|
||||
|
||||
/** 修改岗位 */
|
||||
export function updatePost(data: SystemPostApi.Post) {
|
||||
return requestClient.put('/system/post/update', data);
|
||||
}
|
||||
|
||||
/** 删除岗位 */
|
||||
export function deletePost(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/post/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
63
apps/web-naive/src/api/system/role/index.ts
Normal file
63
apps/web-naive/src/api/system/role/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemRoleApi {
|
||||
/** 角色信息 */
|
||||
export interface Role {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
sort: number;
|
||||
status: number;
|
||||
type: number;
|
||||
dataScope: number;
|
||||
dataScopeDeptIds: number[];
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询角色列表 */
|
||||
export function getRolePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemRoleApi.Role>>(
|
||||
'/system/role/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询角色(精简)列表 */
|
||||
export function getSimpleRoleList() {
|
||||
return requestClient.get<SystemRoleApi.Role[]>('/system/role/simple-list');
|
||||
}
|
||||
|
||||
/** 查询角色详情 */
|
||||
export function getRole(id: number) {
|
||||
return requestClient.get<SystemRoleApi.Role>(`/system/role/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增角色 */
|
||||
export function createRole(data: SystemRoleApi.Role) {
|
||||
return requestClient.post('/system/role/create', data);
|
||||
}
|
||||
|
||||
/** 修改角色 */
|
||||
export function updateRole(data: SystemRoleApi.Role) {
|
||||
return requestClient.put('/system/role/update', data);
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
export function deleteRole(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/role/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
67
apps/web-naive/src/api/system/sms/channel/index.ts
Normal file
67
apps/web-naive/src/api/system/sms/channel/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSmsChannelApi {
|
||||
/** 短信渠道信息 */
|
||||
export interface SmsChannel {
|
||||
id?: number;
|
||||
code: string;
|
||||
status: number;
|
||||
signature: string;
|
||||
remark: string;
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
callbackUrl: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询短信渠道列表 */
|
||||
export function getSmsChannelPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSmsChannelApi.SmsChannel>>(
|
||||
'/system/sms-channel/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得短信渠道精简列表 */
|
||||
export function getSimpleSmsChannelList() {
|
||||
return requestClient.get<SystemSmsChannelApi.SmsChannel[]>(
|
||||
'/system/sms-channel/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询短信渠道详情 */
|
||||
export function getSmsChannel(id: number) {
|
||||
return requestClient.get<SystemSmsChannelApi.SmsChannel>(
|
||||
`/system/sms-channel/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增短信渠道 */
|
||||
export function createSmsChannel(data: SystemSmsChannelApi.SmsChannel) {
|
||||
return requestClient.post('/system/sms-channel/create', data);
|
||||
}
|
||||
|
||||
/** 修改短信渠道 */
|
||||
export function updateSmsChannel(data: SystemSmsChannelApi.SmsChannel) {
|
||||
return requestClient.put('/system/sms-channel/update', data);
|
||||
}
|
||||
|
||||
/** 删除短信渠道 */
|
||||
export function deleteSmsChannel(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/sms-channel/export-excel', { params });
|
||||
}
|
||||
77
apps/web-naive/src/api/system/sms/template/index.ts
Normal file
77
apps/web-naive/src/api/system/sms/template/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSmsTemplateApi {
|
||||
/** 短信模板信息 */
|
||||
export interface SmsTemplate {
|
||||
id?: number;
|
||||
type?: number;
|
||||
status: number;
|
||||
code: string;
|
||||
name: string;
|
||||
content: string;
|
||||
remark: string;
|
||||
apiTemplateId: string;
|
||||
channelId?: number;
|
||||
channelCode?: string;
|
||||
params?: string[];
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 发送短信请求 */
|
||||
export interface SmsSendReqVO {
|
||||
mobile: string;
|
||||
templateCode: string;
|
||||
templateParams: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询短信模板列表 */
|
||||
export function getSmsTemplatePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSmsTemplateApi.SmsTemplate>>(
|
||||
'/system/sms-template/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询短信模板详情 */
|
||||
export function getSmsTemplate(id: number) {
|
||||
return requestClient.get<SystemSmsTemplateApi.SmsTemplate>(
|
||||
`/system/sms-template/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增短信模板 */
|
||||
export function createSmsTemplate(data: SystemSmsTemplateApi.SmsTemplate) {
|
||||
return requestClient.post('/system/sms-template/create', data);
|
||||
}
|
||||
|
||||
/** 修改短信模板 */
|
||||
export function updateSmsTemplate(data: SystemSmsTemplateApi.SmsTemplate) {
|
||||
return requestClient.put('/system/sms-template/update', data);
|
||||
}
|
||||
|
||||
/** 删除短信模板 */
|
||||
export function deleteSmsTemplate(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/sms-template/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 发送短信 */
|
||||
export function sendSms(data: SystemSmsTemplateApi.SmsSendReqVO) {
|
||||
return requestClient.post('/system/sms-template/send-sms', data);
|
||||
}
|
||||
55
apps/web-naive/src/api/system/social/client/index.ts
Normal file
55
apps/web-naive/src/api/system/social/client/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSocialClientApi {
|
||||
/** 社交客户端信息 */
|
||||
export interface SocialClient {
|
||||
id?: number;
|
||||
name: string;
|
||||
socialType: number;
|
||||
userType: number;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
agentId?: string;
|
||||
status: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询社交客户端列表 */
|
||||
export function getSocialClientPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSocialClientApi.SocialClient>>(
|
||||
'/system/social-client/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询社交客户端详情 */
|
||||
export function getSocialClient(id: number) {
|
||||
return requestClient.get<SystemSocialClientApi.SocialClient>(
|
||||
`/system/social-client/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增社交客户端 */
|
||||
export function createSocialClient(data: SystemSocialClientApi.SocialClient) {
|
||||
return requestClient.post('/system/social-client/create', data);
|
||||
}
|
||||
|
||||
/** 修改社交客户端 */
|
||||
export function updateSocialClient(data: SystemSocialClientApi.SocialClient) {
|
||||
return requestClient.put('/system/social-client/update', data);
|
||||
}
|
||||
|
||||
/** 删除社交客户端 */
|
||||
export function deleteSocialClient(id: number) {
|
||||
return requestClient.delete(`/system/social-client/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除社交客户端 */
|
||||
export function deleteSocialClientList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/social-client/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
64
apps/web-naive/src/api/system/tenant-package/index.ts
Normal file
64
apps/web-naive/src/api/system/tenant-package/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemTenantPackageApi {
|
||||
/** 租户套餐信息 */
|
||||
export interface TenantPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
status: number;
|
||||
remark: string;
|
||||
creator: string;
|
||||
updater: string;
|
||||
updateTime: string;
|
||||
menuIds: number[];
|
||||
createTime: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 租户套餐列表 */
|
||||
export function getTenantPackagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemTenantPackageApi.TenantPackage>>(
|
||||
'/system/tenant-package/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询租户套餐详情 */
|
||||
export function getTenantPackage(id: number) {
|
||||
return requestClient.get(`/system/tenant-package/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增租户套餐 */
|
||||
export function createTenantPackage(
|
||||
data: SystemTenantPackageApi.TenantPackage,
|
||||
) {
|
||||
return requestClient.post('/system/tenant-package/create', data);
|
||||
}
|
||||
|
||||
/** 修改租户套餐 */
|
||||
export function updateTenantPackage(
|
||||
data: SystemTenantPackageApi.TenantPackage,
|
||||
) {
|
||||
return requestClient.put('/system/tenant-package/update', data);
|
||||
}
|
||||
|
||||
/** 删除租户套餐 */
|
||||
export function deleteTenantPackage(id: number) {
|
||||
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() {
|
||||
return requestClient.get<SystemTenantPackageApi.TenantPackage[]>(
|
||||
'/system/tenant-package/get-simple-list',
|
||||
);
|
||||
}
|
||||
76
apps/web-naive/src/api/system/tenant/index.ts
Normal file
76
apps/web-naive/src/api/system/tenant/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemTenantApi {
|
||||
/** 租户信息 */
|
||||
export interface Tenant {
|
||||
id?: number;
|
||||
name: string;
|
||||
packageId: number;
|
||||
contactName: string;
|
||||
contactMobile: string;
|
||||
accountCount: number;
|
||||
expireTime: Date;
|
||||
websites: string[];
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/** 租户列表 */
|
||||
export function getTenantPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemTenantApi.Tenant>>(
|
||||
'/system/tenant/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取租户精简信息列表 */
|
||||
export function getSimpleTenantList() {
|
||||
return requestClient.get<SystemTenantApi.Tenant[]>(
|
||||
'/system/tenant/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询租户详情 */
|
||||
export function getTenant(id: number) {
|
||||
return requestClient.get<SystemTenantApi.Tenant>(
|
||||
`/system/tenant/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取租户精简信息列表 */
|
||||
export function getTenantList() {
|
||||
return requestClient.get<SystemTenantApi.Tenant[]>(
|
||||
'/system/tenant/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增租户 */
|
||||
export function createTenant(data: SystemTenantApi.Tenant) {
|
||||
return requestClient.post('/system/tenant/create', data);
|
||||
}
|
||||
|
||||
/** 修改租户 */
|
||||
export function updateTenant(data: SystemTenantApi.Tenant) {
|
||||
return requestClient.put('/system/tenant/update', data);
|
||||
}
|
||||
|
||||
/** 删除租户 */
|
||||
export function deleteTenant(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/tenant/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
88
apps/web-naive/src/api/system/user/index.ts
Normal file
88
apps/web-naive/src/api/system/user/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemUserApi {
|
||||
/** 用户信息 */
|
||||
export interface User {
|
||||
id?: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
deptId: number;
|
||||
postIds: string[];
|
||||
email: string;
|
||||
mobile: string;
|
||||
sex: number;
|
||||
avatar: string;
|
||||
loginIp: string;
|
||||
status: number;
|
||||
remark: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询用户管理列表 */
|
||||
export function getUserPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemUserApi.User>>(
|
||||
'/system/user/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询用户详情 */
|
||||
export function getUser(id: number) {
|
||||
return requestClient.get<SystemUserApi.User>(`/system/user/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增用户 */
|
||||
export function createUser(data: SystemUserApi.User) {
|
||||
return requestClient.post('/system/user/create', data);
|
||||
}
|
||||
|
||||
/** 修改用户 */
|
||||
export function updateUser(data: SystemUserApi.User) {
|
||||
return requestClient.put('/system/user/update', data);
|
||||
}
|
||||
|
||||
/** 删除用户 */
|
||||
export function deleteUser(id: number) {
|
||||
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) {
|
||||
return requestClient.download('/system/user/export-excel', { params });
|
||||
}
|
||||
|
||||
/** 下载用户导入模板 */
|
||||
export function importUserTemplate() {
|
||||
return requestClient.download('/system/user/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入用户 */
|
||||
export function importUser(file: File, updateSupport: boolean) {
|
||||
return requestClient.upload('/system/user/import', {
|
||||
file,
|
||||
updateSupport,
|
||||
});
|
||||
}
|
||||
|
||||
/** 用户密码重置 */
|
||||
export function resetUserPassword(id: number, password: string) {
|
||||
return requestClient.put('/system/user/update-password', { id, password });
|
||||
}
|
||||
|
||||
/** 用户状态修改 */
|
||||
export function updateUserStatus(id: number, status: number) {
|
||||
return requestClient.put('/system/user/update-status', { id, status });
|
||||
}
|
||||
|
||||
/** 获取用户精简信息列表 */
|
||||
export function getSimpleUserList() {
|
||||
return requestClient.get<SystemUserApi.User[]>('/system/user/simple-list');
|
||||
}
|
||||
955
apps/web-naive/src/components/cron-tab/cron-tab.vue
Normal file
955
apps/web-naive/src/components/cron-tab/cron-tab.vue
Normal file
@@ -0,0 +1,955 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { CronData, CronValue, ShortcutsType } from './types';
|
||||
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
NButton,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
} from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
import { CronDataDefault, CronValueDefault } from './types';
|
||||
|
||||
defineOptions({ name: 'Crontab' });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '* * * * * ?',
|
||||
},
|
||||
shortcuts: {
|
||||
type: Array as PropType<ShortcutsType[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const defaultValue = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const cronValue = reactive<CronValue>(CronValueDefault);
|
||||
|
||||
const data = reactive<CronData>(CronDataDefault);
|
||||
const value_second = computed(() => {
|
||||
const v = cronValue.second;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_minute = computed(() => {
|
||||
const v = cronValue.minute;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_hour = computed(() => {
|
||||
const v = cronValue.hour;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_day = computed(() => {
|
||||
const v = cronValue.day;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
case '4': {
|
||||
return 'L';
|
||||
}
|
||||
case '5': {
|
||||
return '?';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_month = computed(() => {
|
||||
const v = cronValue.month;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_week = computed(() => {
|
||||
const v = cronValue.week;
|
||||
switch (v.type) {
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.end}#${v.loop.start}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '*';
|
||||
}
|
||||
case '4': {
|
||||
return `${v.last}L`;
|
||||
}
|
||||
case '5': {
|
||||
return '?';
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const value_year = computed(() => {
|
||||
const v = cronValue.year;
|
||||
switch (v.type) {
|
||||
case '-1': {
|
||||
return '';
|
||||
}
|
||||
case '0': {
|
||||
return '*';
|
||||
}
|
||||
case '1': {
|
||||
return `${v.range.start}-${v.range.end}`;
|
||||
}
|
||||
case '2': {
|
||||
return `${v.loop.start}/${v.loop.end}`;
|
||||
}
|
||||
case '3': {
|
||||
return v.appoint.length > 0 ? v.appoint.join(',') : '';
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => cronValue.week.type,
|
||||
(val: string) => {
|
||||
if (val !== '5') {
|
||||
cronValue.day.type = '5';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => cronValue.day.type,
|
||||
(val: string) => {
|
||||
if (val !== '5') {
|
||||
cronValue.week.type = '5';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
defaultValue.value = props.modelValue;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
defaultValue.value = props.modelValue;
|
||||
});
|
||||
|
||||
const select = ref<string>();
|
||||
|
||||
watch(
|
||||
() => select.value,
|
||||
() => {
|
||||
if (select.value === 'custom') {
|
||||
open();
|
||||
} else {
|
||||
defaultValue.value = select.value || '';
|
||||
emit('update:modelValue', defaultValue.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function open() {
|
||||
set();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function set() {
|
||||
defaultValue.value = props.modelValue;
|
||||
let arr = (props.modelValue || '* * * * * ?').split(' ');
|
||||
|
||||
/** 简单检查 */
|
||||
if (arr.length < 6) {
|
||||
message.warning('cron表达式错误,已转换为默认表达式');
|
||||
arr = '* * * * * ?'.split(' ');
|
||||
}
|
||||
|
||||
/** 秒 */
|
||||
if (arr[0] === '*') {
|
||||
cronValue.second.type = '0';
|
||||
} else if (arr[0]?.includes('-')) {
|
||||
cronValue.second.type = '1';
|
||||
cronValue.second.range.start = Number(arr[0].split('-')[0]);
|
||||
cronValue.second.range.end = Number(arr[0].split('-')[1]);
|
||||
} else if (arr[0]?.includes('/')) {
|
||||
cronValue.second.type = '2';
|
||||
cronValue.second.loop.start = Number(arr[0].split('/')[0]);
|
||||
cronValue.second.loop.end = Number(arr[0].split('/')[1]);
|
||||
} else {
|
||||
cronValue.second.type = '3';
|
||||
cronValue.second.appoint = arr[0]?.split(',') || [];
|
||||
}
|
||||
|
||||
/** 分 */
|
||||
if (arr[1] === '*') {
|
||||
cronValue.minute.type = '0';
|
||||
} else if (arr[1]?.includes('-')) {
|
||||
cronValue.minute.type = '1';
|
||||
cronValue.minute.range.start = Number(arr[1].split('-')[0]);
|
||||
cronValue.minute.range.end = Number(arr[1].split('-')[1]);
|
||||
} else if (arr[1]?.includes('/')) {
|
||||
cronValue.minute.type = '2';
|
||||
cronValue.minute.loop.start = Number(arr[1].split('/')[0]);
|
||||
cronValue.minute.loop.end = Number(arr[1].split('/')[1]);
|
||||
} else {
|
||||
cronValue.minute.type = '3';
|
||||
cronValue.minute.appoint = arr[1]?.split(',') || [];
|
||||
}
|
||||
|
||||
/** 小时 */
|
||||
if (arr[2] === '*') {
|
||||
cronValue.hour.type = '0';
|
||||
} else if (arr[2]?.includes('-')) {
|
||||
cronValue.hour.type = '1';
|
||||
cronValue.hour.range.start = Number(arr[2].split('-')[0]);
|
||||
cronValue.hour.range.end = Number(arr[2].split('-')[1]);
|
||||
} else if (arr[2]?.includes('/')) {
|
||||
cronValue.hour.type = '2';
|
||||
cronValue.hour.loop.start = Number(arr[2].split('/')[0]);
|
||||
cronValue.hour.loop.end = Number(arr[2].split('/')[1]);
|
||||
} else {
|
||||
cronValue.hour.type = '3';
|
||||
cronValue.hour.appoint = arr[2]?.split(',') || [];
|
||||
}
|
||||
|
||||
/** 日 */
|
||||
switch (arr[3]) {
|
||||
case '*': {
|
||||
cronValue.day.type = '0';
|
||||
|
||||
break;
|
||||
}
|
||||
case '?': {
|
||||
cronValue.day.type = '5';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'L': {
|
||||
cronValue.day.type = '4';
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (arr[3]?.includes('-')) {
|
||||
cronValue.day.type = '1';
|
||||
cronValue.day.range.start = Number(arr[3].split('-')[0]);
|
||||
cronValue.day.range.end = Number(arr[3].split('-')[1]);
|
||||
} else if (arr[3]?.includes('/')) {
|
||||
cronValue.day.type = '2';
|
||||
cronValue.day.loop.start = Number(arr[3].split('/')[0]);
|
||||
cronValue.day.loop.end = Number(arr[3].split('/')[1]);
|
||||
} else {
|
||||
cronValue.day.type = '3';
|
||||
cronValue.day.appoint = arr[3]?.split(',') || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 月 */
|
||||
if (arr[4] === '*') {
|
||||
cronValue.month.type = '0';
|
||||
} else if (arr[4]?.includes('-')) {
|
||||
cronValue.month.type = '1';
|
||||
cronValue.month.range.start = Number(arr[4].split('-')[0]);
|
||||
cronValue.month.range.end = Number(arr[4].split('-')[1]);
|
||||
} else if (arr[4]?.includes('/')) {
|
||||
cronValue.month.type = '2';
|
||||
cronValue.month.loop.start = Number(arr[4].split('/')[0]);
|
||||
cronValue.month.loop.end = Number(arr[4].split('/')[1]);
|
||||
} else {
|
||||
cronValue.month.type = '3';
|
||||
cronValue.month.appoint = arr[4]?.split(',') || [];
|
||||
}
|
||||
|
||||
/** 周 */
|
||||
if (arr[5] === '*') {
|
||||
cronValue.week.type = '0';
|
||||
} else if (arr[5] === '?') {
|
||||
cronValue.week.type = '5';
|
||||
} else if (arr[5]?.includes('-')) {
|
||||
cronValue.week.type = '1';
|
||||
cronValue.week.range.start = arr[5].split('-')[0] || '';
|
||||
cronValue.week.range.end = arr[5].split('-')[1] || '';
|
||||
} else if (arr[5]?.includes('#')) {
|
||||
cronValue.week.type = '2';
|
||||
cronValue.week.loop.start = Number(arr[5].split('#')[1]);
|
||||
cronValue.week.loop.end = arr[5].split('#')[0] || '';
|
||||
} else if (arr[5]?.includes('L')) {
|
||||
cronValue.week.type = '4';
|
||||
cronValue.week.last = arr[5].split('L')[0] || '';
|
||||
} else {
|
||||
cronValue.week.type = '3';
|
||||
cronValue.week.appoint = arr[5]?.split(',') || [];
|
||||
}
|
||||
|
||||
/** 年 */
|
||||
if (!arr[6]) {
|
||||
cronValue.year.type = '-1';
|
||||
} else if (arr[6] === '*') {
|
||||
cronValue.year.type = '0';
|
||||
} else if (arr[6]?.includes('-')) {
|
||||
cronValue.year.type = '1';
|
||||
cronValue.year.range.start = Number(arr[6].split('-')[0]);
|
||||
cronValue.year.range.end = Number(arr[6].split('-')[1]);
|
||||
} else if (arr[6]?.includes('/')) {
|
||||
cronValue.year.type = '2';
|
||||
cronValue.year.loop.start = Number(arr[6].split('/')[1]);
|
||||
cronValue.year.loop.end = Number(arr[6].split('/')[0]);
|
||||
} else {
|
||||
cronValue.year.type = '3';
|
||||
cronValue.year.appoint = arr[6]?.split(',') || [];
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const year = value_year.value ? ` ${value_year.value}` : '';
|
||||
defaultValue.value = `${value_second.value} ${value_minute.value} ${
|
||||
value_hour.value
|
||||
} ${value_day.value} ${value_month.value} ${value_week.value}${year}`;
|
||||
emit('update:modelValue', defaultValue.value);
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function inputChange() {
|
||||
emit('update:modelValue', defaultValue.value);
|
||||
}
|
||||
|
||||
const shortcutsOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: '每分钟',
|
||||
value: '0 * * * * ?',
|
||||
},
|
||||
{
|
||||
label: '每小时',
|
||||
value: '0 0 * * * ?',
|
||||
},
|
||||
{
|
||||
label: '每天零点',
|
||||
value: '0 0 0 * * ?',
|
||||
},
|
||||
{
|
||||
label: '每月一号零点',
|
||||
value: '0 0 0 1 * ?',
|
||||
},
|
||||
{
|
||||
label: '每月最后一天零点',
|
||||
value: '0 0 0 L * ?',
|
||||
},
|
||||
{
|
||||
label: '每周星期日零点',
|
||||
value: '0 0 0 ? * 1',
|
||||
},
|
||||
...props.shortcuts.map((item) => ({
|
||||
label: item.text,
|
||||
value: item.value,
|
||||
})),
|
||||
{
|
||||
label: '自定义',
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NInput
|
||||
v-model:value="defaultValue"
|
||||
class="input-with-select"
|
||||
v-bind="$attrs"
|
||||
@update:value="inputChange"
|
||||
>
|
||||
<template #suffix>
|
||||
<NSelect
|
||||
v-model:value="select"
|
||||
placeholder="生成器"
|
||||
class="w-36"
|
||||
:options="shortcutsOptions"
|
||||
/>
|
||||
</template>
|
||||
</NInput>
|
||||
|
||||
<NModal
|
||||
v-model:show="dialogVisible"
|
||||
class="w-[720px]"
|
||||
preset="card"
|
||||
destroy-on-close
|
||||
title="cron规则生成器"
|
||||
>
|
||||
<div class="sc-cron">
|
||||
<NTabs>
|
||||
<NTabPane name="second">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>秒</h2>
|
||||
<h4>{{ value_second }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.second.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.second.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.second.range.start as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.second.range.end as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.second.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.second.loop.start as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
秒开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.second.loop.end as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
秒执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.second.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.second.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.second.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="minute">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>分钟</h2>
|
||||
<h4>{{ value_minute }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.minute.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.minute.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.minute.range.start as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.minute.range.end as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.minute.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.minute.loop.start as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
分钟开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.minute.loop.end as number"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
分钟执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.minute.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.minute.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.minute.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="hour">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>小时</h2>
|
||||
<h4>{{ value_hour }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.hour.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.hour.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.hour.range.start as number"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.hour.range.end as number"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.hour.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.hour.loop.start as number"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
小时开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.hour.loop.end as number"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
小时执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.hour.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.hour.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.hour.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="day">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>日</h2>
|
||||
<h4>{{ value_day }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.day.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
<NRadioButton value="4">本月最后一天</NRadioButton>
|
||||
<NRadioButton value="5">不指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.day.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.day.range.start as number"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.day.range.end as number"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.day.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.day.loop.start as number"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
号开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.day.loop.end as number"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
天执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.day.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.day.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.day.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="month">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>月</h2>
|
||||
<h4>{{ value_month }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.month.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.month.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.month.range.start as number"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.month.range.end as number"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.month.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.month.loop.start as number"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
月开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.month.loop.end as number"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
月执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.month.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.month.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.month.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="week">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>周</h2>
|
||||
<h4>{{ value_week }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.week.type">
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
<NRadioButton value="4">本月最后一周</NRadioButton>
|
||||
<NRadioButton value="5">不指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.week.type === '1'" label="范围">
|
||||
<NSelect
|
||||
v-model:value="cronValue.week.range.start"
|
||||
:options="
|
||||
data.week.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NSelect
|
||||
v-model:value="cronValue.week.range.end"
|
||||
:options="
|
||||
data.week.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.week.type === '2'" label="间隔">
|
||||
第
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.week.loop.start as number"
|
||||
:max="4"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
周的星期
|
||||
<NSelect
|
||||
v-model:value="cronValue.week.loop.end"
|
||||
:options="
|
||||
data.week.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.week.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.week.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.week.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.week.type === '4'" label="最后一周">
|
||||
<NSelect
|
||||
v-model:value="cronValue.week.last"
|
||||
:options="
|
||||
data.week.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="year">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>年</h2>
|
||||
<h4>{{ value_year }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NRadioGroup v-model:value="cronValue.year.type">
|
||||
<NRadioButton value="-1">忽略</NRadioButton>
|
||||
<NRadioButton value="0">任意值</NRadioButton>
|
||||
<NRadioButton value="1">范围</NRadioButton>
|
||||
<NRadioButton value="2">间隔</NRadioButton>
|
||||
<NRadioButton value="3">指定</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.year.type === '1'" label="范围">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.year.range.start as number"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="px-4">-</span>
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.year.range.end as number"
|
||||
controls-position="right"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.year.type === '2'" label="间隔">
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.year.loop.start as number"
|
||||
controls-position="right"
|
||||
/>
|
||||
年开始,每
|
||||
<NInputNumber
|
||||
v-model:value="cronValue.year.loop.end as number"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
年执行一次
|
||||
</NFormItem>
|
||||
<NFormItem v-if="cronValue.year.type === '3'" label="指定">
|
||||
<NSelect
|
||||
v-model:value="cronValue.year.appoint"
|
||||
multiple
|
||||
class="w-full"
|
||||
:options="
|
||||
data.year.map((item) => ({
|
||||
label: item.toString(),
|
||||
value: item.toString(),
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<NButton @click="dialogVisible = false">取 消</NButton>
|
||||
<NButton type="primary" @click="submit()">确 认</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sc-cron {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
1
apps/web-naive/src/components/cron-tab/index.ts
Normal file
1
apps/web-naive/src/components/cron-tab/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CronTab } from './cron-tab.vue';
|
||||
266
apps/web-naive/src/components/cron-tab/types.ts
Normal file
266
apps/web-naive/src/components/cron-tab/types.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
export interface ShortcutsType {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CronRange {
|
||||
start: number | string | undefined;
|
||||
end: number | string | undefined;
|
||||
}
|
||||
|
||||
export interface CronLoop {
|
||||
start: number | string | undefined;
|
||||
end: number | string | undefined;
|
||||
}
|
||||
|
||||
export interface CronItem {
|
||||
type: string;
|
||||
range: CronRange;
|
||||
loop: CronLoop;
|
||||
appoint: string[];
|
||||
last?: string;
|
||||
}
|
||||
|
||||
export interface CronValue {
|
||||
second: CronItem;
|
||||
minute: CronItem;
|
||||
hour: CronItem;
|
||||
day: CronItem;
|
||||
month: CronItem;
|
||||
week: CronItem & { last: string };
|
||||
year: CronItem;
|
||||
}
|
||||
|
||||
export interface WeekOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CronData {
|
||||
second: string[];
|
||||
minute: string[];
|
||||
hour: string[];
|
||||
day: string[];
|
||||
month: string[];
|
||||
week: WeekOption[];
|
||||
year: number[];
|
||||
}
|
||||
|
||||
const getYear = (): number[] => {
|
||||
const v: number[] = [];
|
||||
const y = new Date().getFullYear();
|
||||
for (let i = 0; i < 11; i++) {
|
||||
v.push(y + i);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
export const CronValueDefault: CronValue = {
|
||||
second: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
minute: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
hour: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
day: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
month: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
week: {
|
||||
type: '5',
|
||||
range: {
|
||||
start: '2',
|
||||
end: '3',
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: '2',
|
||||
},
|
||||
last: '2',
|
||||
appoint: [],
|
||||
},
|
||||
year: {
|
||||
type: '-1',
|
||||
range: {
|
||||
start: getYear()[0],
|
||||
end: getYear()[1],
|
||||
},
|
||||
loop: {
|
||||
start: getYear()[0],
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CronDataDefault: CronData = {
|
||||
second: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
minute: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
hour: [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
],
|
||||
day: [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
'24',
|
||||
'25',
|
||||
'26',
|
||||
'27',
|
||||
'28',
|
||||
'29',
|
||||
'30',
|
||||
'31',
|
||||
],
|
||||
month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
|
||||
week: [
|
||||
{
|
||||
value: '1',
|
||||
label: '周日',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '周一',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '周二',
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '周三',
|
||||
},
|
||||
{
|
||||
value: '5',
|
||||
label: '周四',
|
||||
},
|
||||
{
|
||||
value: '6',
|
||||
label: '周五',
|
||||
},
|
||||
{
|
||||
value: '7',
|
||||
label: '周六',
|
||||
},
|
||||
],
|
||||
year: getYear(),
|
||||
};
|
||||
198
apps/web-naive/src/components/description/description.vue
Normal file
198
apps/web-naive/src/components/description/description.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="tsx">
|
||||
import type { DescriptionsProps as NDescriptionsProps } from 'naive-ui';
|
||||
|
||||
import type { CSSProperties, PropType, Slots } from 'vue';
|
||||
|
||||
import type { DescriptionItemSchema, DescriptionProps } from './typing';
|
||||
|
||||
import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
|
||||
|
||||
import { get, getNestedValue, isFunction } from '@vben/utils';
|
||||
|
||||
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
const props = {
|
||||
bordered: { default: true, type: Boolean },
|
||||
column: {
|
||||
default: () => {
|
||||
return { lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 };
|
||||
},
|
||||
type: [Number, Object],
|
||||
},
|
||||
data: { type: Object },
|
||||
schema: {
|
||||
default: () => [],
|
||||
type: Array as PropType<DescriptionItemSchema[]>,
|
||||
},
|
||||
size: {
|
||||
default: 'small',
|
||||
type: String,
|
||||
validator: (v: string) =>
|
||||
['default', 'middle', 'small', undefined].includes(v),
|
||||
},
|
||||
title: { default: '', type: String },
|
||||
useCard: { default: true, type: Boolean },
|
||||
labelPlacement: { default: 'left', type: String as PropType<'left' | 'top'> },
|
||||
};
|
||||
|
||||
function getSlot(slots: Slots, slot: string, data?: any) {
|
||||
if (!slots || !Reflect.has(slots, slot)) {
|
||||
return null;
|
||||
}
|
||||
if (!isFunction(slots[slot])) {
|
||||
console.error(`${slot} is not a function!`);
|
||||
return null;
|
||||
}
|
||||
const slotFn = slots[slot];
|
||||
if (!slotFn) return null;
|
||||
return slotFn({ data });
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Description',
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const propsRef = ref<null | Partial<DescriptionProps>>(null);
|
||||
|
||||
const prefixCls = 'description';
|
||||
const attrs = useAttrs();
|
||||
|
||||
// Custom title component: get title
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...(unref(propsRef) as any),
|
||||
} as DescriptionProps;
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt = {
|
||||
...unref(getMergeProps),
|
||||
title: undefined,
|
||||
};
|
||||
return opt as DescriptionProps;
|
||||
});
|
||||
|
||||
const useWrapper = computed(() => !!unref(getMergeProps).title);
|
||||
|
||||
const getDescriptionsProps = computed(() => {
|
||||
return { ...unref(attrs), ...unref(getProps) } as NDescriptionsProps;
|
||||
});
|
||||
|
||||
// 防止换行
|
||||
function renderLabel({
|
||||
label,
|
||||
labelMinWidth,
|
||||
labelStyle,
|
||||
}: DescriptionItemSchema) {
|
||||
if (!labelStyle && !labelMinWidth) {
|
||||
return label;
|
||||
}
|
||||
|
||||
const labelStyles: CSSProperties = {
|
||||
...labelStyle,
|
||||
minWidth: `${labelMinWidth}px `,
|
||||
};
|
||||
return <div style={labelStyles}>{label}</div>;
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { data, schema } = unref(getProps);
|
||||
return unref(schema)
|
||||
.map((item) => {
|
||||
const { contentMinWidth, field, render, show, span } = item;
|
||||
|
||||
if (show && isFunction(show) && !show(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
const _data = unref(getProps)?.data;
|
||||
if (!_data) {
|
||||
return null;
|
||||
}
|
||||
const getField = field.includes('.')
|
||||
? (getNestedValue(_data, field) ?? get(_data, field))
|
||||
: get(_data, field);
|
||||
// if (
|
||||
// getField &&
|
||||
// !Object.prototype.hasOwnProperty.call(toRefs(_data), field)
|
||||
// ) {
|
||||
// return isFunction(render) ? render('', _data) : (getField ?? '');
|
||||
// }
|
||||
return isFunction(render)
|
||||
? render(getField, _data)
|
||||
: (getField ?? '');
|
||||
}
|
||||
|
||||
const width = contentMinWidth;
|
||||
return (
|
||||
<NDescriptionsItem key={field} span={span}>
|
||||
{{
|
||||
label: () => {
|
||||
return renderLabel(item);
|
||||
},
|
||||
default: () => {
|
||||
if (item.slot) {
|
||||
const slotContent = getSlot(slots, item.slot, data);
|
||||
return slotContent;
|
||||
}
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
}
|
||||
const style: CSSProperties = {
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <div style={style}>{getContent()}</div>;
|
||||
},
|
||||
}}
|
||||
</NDescriptionsItem>
|
||||
);
|
||||
})
|
||||
.filter((item) => !!item);
|
||||
}
|
||||
|
||||
function renderDesc() {
|
||||
return (
|
||||
<NDescriptions
|
||||
class={`${prefixCls}`}
|
||||
{...(unref(getDescriptionsProps) as any)}
|
||||
>
|
||||
{renderItem()}
|
||||
</NDescriptions>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCard() {
|
||||
const content = props.useCard ? renderDesc() : <div>{renderDesc()}</div>;
|
||||
// Reduce the dom level
|
||||
if (!props.useCard) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const { title } = unref(getMergeProps);
|
||||
const extraSlot = getSlot(slots, 'extra');
|
||||
|
||||
return (
|
||||
<NCard
|
||||
contentStyle={{ padding: '8px 0' }}
|
||||
headerStyle={{
|
||||
padding: '8px 16px',
|
||||
fontSize: '14px',
|
||||
minHeight: '24px',
|
||||
}}
|
||||
style={{ margin: 0 }}
|
||||
title={title}
|
||||
>
|
||||
{{
|
||||
default: () => content,
|
||||
extra: () => extraSlot && <div>{extraSlot}</div>,
|
||||
}}
|
||||
</NCard>
|
||||
);
|
||||
}
|
||||
|
||||
return () => (unref(useWrapper) ? renderCard() : renderDesc());
|
||||
},
|
||||
});
|
||||
</script>
|
||||
3
apps/web-naive/src/components/description/index.ts
Normal file
3
apps/web-naive/src/components/description/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Description } from './description.vue';
|
||||
export * from './typing';
|
||||
export { useDescription } from './use-description';
|
||||
41
apps/web-naive/src/components/description/typing.ts
Normal file
41
apps/web-naive/src/components/description/typing.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { DescriptionsProps as NDescriptionsProps } from 'naive-ui';
|
||||
import type { JSX } from 'vue/jsx-runtime';
|
||||
|
||||
import type { CSSProperties, VNode } from 'vue';
|
||||
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
export interface DescriptionItemSchema {
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
// 自定义标签样式
|
||||
labelStyle?: CSSProperties;
|
||||
// 对应 data 中的字段名
|
||||
field: string;
|
||||
// 内容的描述
|
||||
label: JSX.Element | string | VNode;
|
||||
// 包含列的数量
|
||||
span?: number;
|
||||
// 是否显示
|
||||
show?: (...arg: any) => boolean;
|
||||
// 插槽名称
|
||||
slot?: string;
|
||||
// 自定义需要展示的内容
|
||||
render?: (
|
||||
val: any,
|
||||
data?: Recordable<any>,
|
||||
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||
}
|
||||
|
||||
export interface DescriptionProps extends NDescriptionsProps {
|
||||
// 是否包含卡片组件
|
||||
useCard?: boolean;
|
||||
// 描述项配置
|
||||
schema: DescriptionItemSchema[];
|
||||
// 数据
|
||||
data: Recordable<any>;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
31
apps/web-naive/src/components/description/use-description.ts
Normal file
31
apps/web-naive/src/components/description/use-description.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { DescInstance, DescriptionProps } from './typing';
|
||||
|
||||
import { h, reactive } from 'vue';
|
||||
|
||||
import Description from './description.vue';
|
||||
|
||||
export function useDescription(options?: Partial<DescriptionProps>) {
|
||||
const propsState = reactive<Partial<DescriptionProps>>(options || {});
|
||||
|
||||
const api: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
||||
Object.assign(propsState, descProps);
|
||||
},
|
||||
};
|
||||
|
||||
// 创建一个包装组件,将 propsState 合并到 props 中
|
||||
const DescriptionWrapper: Component = {
|
||||
name: 'UseDescription',
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => {
|
||||
// @ts-ignore - 避免类型实例化过深
|
||||
return h(Description, { ...propsState, ...attrs }, slots);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return [DescriptionWrapper, api] as const;
|
||||
}
|
||||
83
apps/web-naive/src/components/dict-tag/dict-tag.vue
Normal file
83
apps/web-naive/src/components/dict-tag/dict-tag.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDictObj } from '@vben/hooks';
|
||||
|
||||
import { NTag } from 'naive-ui';
|
||||
|
||||
interface DictTagProps {
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 字典值
|
||||
*/
|
||||
value: any;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<DictTagProps>();
|
||||
|
||||
/** 获取字典标签 */
|
||||
const dictTag = computed(() => {
|
||||
const defaultDict = {
|
||||
label: '',
|
||||
colorType: 'primary',
|
||||
};
|
||||
// 校验参数有效性
|
||||
if (!props.type || props.value === undefined || props.value === null) {
|
||||
return defaultDict;
|
||||
}
|
||||
|
||||
// 获取字典对象
|
||||
const dict = getDictObj(props.type, String(props.value));
|
||||
if (!dict) {
|
||||
return defaultDict;
|
||||
}
|
||||
|
||||
// 处理颜色类型
|
||||
let colorType = dict.colorType;
|
||||
switch (colorType) {
|
||||
case 'danger': {
|
||||
colorType = 'error';
|
||||
break;
|
||||
}
|
||||
case 'info': {
|
||||
colorType = 'info';
|
||||
break;
|
||||
}
|
||||
case 'primary': {
|
||||
colorType = 'primary';
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
colorType = 'success';
|
||||
break;
|
||||
}
|
||||
case 'warning': {
|
||||
colorType = 'warning';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (!colorType) {
|
||||
colorType = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: dict.label || '',
|
||||
colorType,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTag v-if="dictTag.label" :type="dictTag.colorType as any">
|
||||
{{ dictTag.label }}
|
||||
</NTag>
|
||||
</template>
|
||||
13
apps/web-naive/src/components/table-action/icons.ts
Normal file
13
apps/web-naive/src/components/table-action/icons.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const ACTION_ICON = {
|
||||
DOWNLOAD: 'lucide:download',
|
||||
UPLOAD: 'lucide:upload',
|
||||
ADD: 'lucide:plus',
|
||||
EDIT: 'lucide:edit',
|
||||
DELETE: 'lucide:trash-2',
|
||||
REFRESH: 'lucide:refresh-cw',
|
||||
SEARCH: 'lucide:search',
|
||||
FILTER: 'lucide:filter',
|
||||
MORE: 'lucide:ellipsis-vertical',
|
||||
VIEW: 'lucide:eye',
|
||||
COPY: 'lucide:copy',
|
||||
};
|
||||
4
apps/web-naive/src/components/table-action/index.ts
Normal file
4
apps/web-naive/src/components/table-action/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './icons';
|
||||
|
||||
export { default as TableAction } from './table-action.vue';
|
||||
export * from './typing';
|
||||
221
apps/web-naive/src/components/table-action/table-action.vue
Normal file
221
apps/web-naive/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<!-- add by 星语:参考 vben2 的方式,增加 TableAction 组件 -->
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMixedOption } from 'naive-ui/es/dropdown/src/interface';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { ActionItem, PopConfirm } from './typing';
|
||||
|
||||
import { computed, unref, watch } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { isBoolean, isFunction } from '@vben/utils';
|
||||
|
||||
import { NButton, NDropdown, NPopconfirm, NSpace } from 'naive-ui';
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
dropDownActions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 检查是否显示 */
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
let isIfShow = true;
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
if (isIfShow) {
|
||||
isIfShow =
|
||||
hasAccessByCodes(action.auth || []) || (action.auth || []).length === 0;
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
/** 处理按钮 actions */
|
||||
const getActions = computed(() => {
|
||||
const actions = props.actions || [];
|
||||
return actions.filter((action: ActionItem) => isIfShow(action));
|
||||
});
|
||||
|
||||
/** 处理下拉菜单 actions */
|
||||
const getDropdownList = computed((): DropdownMixedOption[] => {
|
||||
const dropDownActions = props.dropDownActions || [];
|
||||
return dropDownActions
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem, index: number) => ({
|
||||
label: action.label || '',
|
||||
onClick: () => action.onClick?.(),
|
||||
key: getActionKey(action, index),
|
||||
disabled: action.disabled,
|
||||
divider: action.divider || false,
|
||||
}));
|
||||
});
|
||||
|
||||
/** Space 组件的 size */
|
||||
const spaceSize = computed(() => {
|
||||
const actions = unref(getActions);
|
||||
return actions?.some((item: ActionItem) => item.type === 'primary') ? 4 : 8;
|
||||
});
|
||||
|
||||
/** 获取 PopConfirm 属性 */
|
||||
function getPopConfirmProps(popConfirm: PopConfirm) {
|
||||
if (!popConfirm) return {};
|
||||
|
||||
const attrs: Record<string, any> = {};
|
||||
|
||||
// 复制基本属性,排除函数
|
||||
Object.keys(popConfirm).forEach((key) => {
|
||||
if (key !== 'confirm' && key !== 'cancel' && key !== 'icon') {
|
||||
attrs[key] = popConfirm[key as keyof PopConfirm];
|
||||
}
|
||||
});
|
||||
|
||||
// 单独处理事件函数
|
||||
if (popConfirm.confirm && isFunction(popConfirm.confirm)) {
|
||||
attrs.positiveConfirm = popConfirm.confirm;
|
||||
}
|
||||
if (popConfirm.cancel && isFunction(popConfirm.cancel)) {
|
||||
attrs.negativeCancel = popConfirm.cancel;
|
||||
}
|
||||
if (popConfirm.okText) {
|
||||
attrs.positiveText = popConfirm.okText;
|
||||
}
|
||||
if (popConfirm.cancelText) {
|
||||
attrs.negativeText = popConfirm.cancelText;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/** 获取 Button 属性 */
|
||||
function getButtonProps(action: ActionItem) {
|
||||
return {
|
||||
type: action.type || 'primary',
|
||||
quaternary: action.quaternary || false,
|
||||
text: action.text || false,
|
||||
disabled: action.disabled,
|
||||
loading: action.loading,
|
||||
size: action.size,
|
||||
};
|
||||
}
|
||||
|
||||
// /** 获取 Tooltip 属性 */
|
||||
// function getTooltipProps(tooltip: any | string) {
|
||||
// if (!tooltip) return {};
|
||||
// return typeof tooltip === 'string' ? { title: tooltip } : { ...tooltip };
|
||||
// }
|
||||
|
||||
/** 处理菜单点击 */
|
||||
function handleMenuClick(key: number) {
|
||||
const action = getDropdownList.value.find((item) => item.key === key);
|
||||
if (action && action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成稳定的 key */
|
||||
function getActionKey(action: ActionItem, index: number) {
|
||||
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||
}
|
||||
|
||||
/** 处理按钮点击 */
|
||||
function handleButtonClick(action: ActionItem) {
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePopconfirmClick(popconfirm: PopConfirm, type: string) {
|
||||
if (type === 'positive') {
|
||||
popconfirm.confirm();
|
||||
} else if (type === 'negative' && popconfirm.cancel) {
|
||||
popconfirm.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听props变化,强制重新计算 */
|
||||
watch(
|
||||
() => [props.actions, props.dropDownActions],
|
||||
() => {
|
||||
// 这里不需要额外处理,computed会自动重新计算
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<NSpace :size="spaceSize">
|
||||
<template
|
||||
v-for="(action, index) in getActions"
|
||||
:key="getActionKey(action, index)"
|
||||
>
|
||||
<NPopconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
@positive-click="handlePopconfirmClick(action.popConfirm, 'positive')"
|
||||
@negative-click="handlePopconfirmClick(action.popConfirm, 'negative')"
|
||||
>
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<template #trigger>
|
||||
<NButton v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</NButton>
|
||||
</template>
|
||||
<span>{{ getPopConfirmProps(action.popConfirm).title }}</span>
|
||||
</NPopconfirm>
|
||||
<NButton
|
||||
v-else
|
||||
v-bind="getButtonProps(action)"
|
||||
@click="handleButtonClick(action)"
|
||||
>
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NSpace>
|
||||
|
||||
<NDropdown
|
||||
v-if="getDropdownList.length > 0"
|
||||
trigger="click"
|
||||
:options="getDropdownList"
|
||||
:show-arrow="true"
|
||||
@select="handleMenuClick"
|
||||
>
|
||||
<NButton type="primary" text>
|
||||
{{ $t('page.action.more') }}
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</div>
|
||||
</template>
|
||||
28
apps/web-naive/src/components/table-action/typing.ts
Normal file
28
apps/web-naive/src/components/table-action/typing.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ButtonProps } from 'naive-ui/es/button/src/Button';
|
||||
import type { TooltipProps } from 'naive-ui/es/tooltip/src/Tooltip';
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: () => void;
|
||||
cancel?: () => void;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionItem extends ButtonProps {
|
||||
onClick?: () => void;
|
||||
type?: ButtonProps['type'];
|
||||
label?: string;
|
||||
color?: 'error' | 'success' | 'warning';
|
||||
icon?: string;
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||
tooltip?: string | TooltipProps;
|
||||
}
|
||||
260
apps/web-naive/src/components/upload/README.md
Normal file
260
apps/web-naive/src/components/upload/README.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Upload Components - Naive UI 版本
|
||||
|
||||
本目录包含已重构为 Naive UI 的上传组件。
|
||||
|
||||
## 组件列表
|
||||
|
||||
### 1. ImageUpload - 图片上传组件
|
||||
|
||||
- **文件**: `image-upload.vue`
|
||||
- **功能**: 专门用于图片上传的组件,支持图片预览
|
||||
- **特性**:
|
||||
- 支持单图/多图上传
|
||||
- 支持图片预览(使用 NModal + NImage)
|
||||
- 支持拖拽上传
|
||||
- 自动校验文件类型和大小
|
||||
- 支持自定义上传 API
|
||||
- 支持进度显示
|
||||
|
||||
### 2. FileUpload - 文件上传组件
|
||||
|
||||
- **文件**: `file-upload.vue`
|
||||
- **功能**: 通用文件上传组件
|
||||
- **特性**:
|
||||
- 支持单文件/多文件上传
|
||||
- 支持拖拽上传区域
|
||||
- 支持文件预览和下载
|
||||
- 自动校验文件类型和大小
|
||||
- 支持自定义上传 API
|
||||
- 支持进度显示
|
||||
- 支持返回文本内容(用于配置文件等)
|
||||
|
||||
### 3. InputUpload - 输入框上传组件
|
||||
|
||||
- **文件**: `input-upload.vue`
|
||||
- **功能**: 结合输入框和文件上传的组件
|
||||
- **特性**:
|
||||
- 支持文本输入框或文本域
|
||||
- 支持通过上传文件自动填充内容
|
||||
- 使用 NGrid 布局,响应式设计
|
||||
|
||||
## 使用示例
|
||||
|
||||
### ImageUpload 图片上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ImageUpload } from '#/components/upload';
|
||||
|
||||
const imageUrl = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ImageUpload
|
||||
v-model="imageUrl"
|
||||
:max-number="1"
|
||||
:max-size="2"
|
||||
:accept="['jpg', 'jpeg', 'png', 'gif']"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### FileUpload 文件上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { FileUpload } from '#/components/upload';
|
||||
|
||||
const fileUrl = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FileUpload
|
||||
v-model="fileUrl"
|
||||
:max-number="3"
|
||||
:max-size="5"
|
||||
:accept="['pdf', 'doc', 'docx']"
|
||||
drag
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### InputUpload 输入框上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { InputUpload } from '#/components/upload';
|
||||
|
||||
const configContent = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputUpload
|
||||
v-model="configContent"
|
||||
input-type="textarea"
|
||||
:file-upload-props="{
|
||||
accept: ['json', 'yaml', 'yml'],
|
||||
maxSize: 1,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Props 说明
|
||||
|
||||
### 通用 Props (FileUploadProps)
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| ---------------- | -------------------- | ------- | ------------------ |
|
||||
| modelValue/value | `string \| string[]` | - | v-model 绑定值 |
|
||||
| accept | `string[]` | `[]` | 接受的文件类型 |
|
||||
| maxSize | `number` | `2` | 文件最大大小(MB) |
|
||||
| maxNumber | `number` | `1` | 最大文件数量 |
|
||||
| multiple | `boolean` | `false` | 是否支持多选 |
|
||||
| disabled | `boolean` | `false` | 是否禁用 |
|
||||
| drag | `boolean` | `false` | 是否支持拖拽上传 |
|
||||
| directory | `string` | - | 上传目录 |
|
||||
| api | `Function` | - | 自定义上传 API |
|
||||
| showDescription | `boolean` | - | 是否显示描述文本 |
|
||||
|
||||
### ImageUpload 特有 Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| listType | `string` | `'picture-card'` | 列表类型 |
|
||||
| accept | `string[]` | `['jpg', 'jpeg', 'png', 'gif', 'webp']` | 接受的图片类型 |
|
||||
| showDescription | `boolean` | `true` | 是否显示描述文本 |
|
||||
|
||||
### InputUpload 特有 Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
| --------------- | ----------------------- | --------- | ---------------- |
|
||||
| inputType | `'input' \| 'textarea'` | `'input'` | 输入框类型 |
|
||||
| inputProps | `InputProps` | - | 输入框属性 |
|
||||
| fileUploadProps | `FileUploadProps` | - | 文件上传组件属性 |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| update:value | `value: string \| string[]` | 值更新事件 |
|
||||
| update:modelValue | `value: string \| string[]` | v-model 更新事件 |
|
||||
| change | `value: string \| string[]` | 值变化事件 |
|
||||
| delete | `file: UploadFileInfo` | 删除文件事件 |
|
||||
| preview | `file: UploadFileInfo` | 预览文件事件(仅 FileUpload) |
|
||||
| returnText | `text: string` | 返回文件文本内容(仅 FileUpload) |
|
||||
|
||||
## 辅助工具
|
||||
|
||||
### useUpload
|
||||
|
||||
- **文件**: `use-upload.ts`
|
||||
- **功能**: 提供上传相关的工具函数
|
||||
- **主要方法**:
|
||||
- `httpRequest`: 统一的文件上传请求方法
|
||||
- `getUploadUrl`: 获取上传 URL
|
||||
|
||||
### useUploadType
|
||||
|
||||
- **功能**: 处理上传类型相关的逻辑
|
||||
- **主要方法**:
|
||||
- `getStringAccept`: 获取 accept 字符串
|
||||
- `getHelpText`: 获取帮助文本
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **UI 框架**: Naive UI
|
||||
- **核心组件**:
|
||||
- NUpload
|
||||
- NImage
|
||||
- NImageGroup
|
||||
- NModal
|
||||
- NButton
|
||||
- NGrid
|
||||
- NInput
|
||||
- **工具库**:
|
||||
- @vueuse/core
|
||||
- @vben/utils
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 文件状态使用 Naive UI 的状态值:`'pending' | 'uploading' | 'finished' | 'error' | 'removed'`
|
||||
2. 所有文件 ID 使用 Naive UI 的 `id` 字段,而不是 `uid`
|
||||
3. 上传前会自动校验文件类型和大小
|
||||
4. 支持两种上传模式:
|
||||
- 客户端直接上传(S3)
|
||||
- 通过后端上传
|
||||
5. 支持自定义上传 API,如果不提供则使用默认的上传接口
|
||||
|
||||
## 迁移指南
|
||||
|
||||
从 Ant Design Vue 迁移到 Naive UI 的主要变化:
|
||||
|
||||
1. **组件导入**:
|
||||
|
||||
```typescript
|
||||
// 旧
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
// 新
|
||||
import { NUpload } from 'naive-ui';
|
||||
```
|
||||
|
||||
2. **文件列表类型**:
|
||||
|
||||
```typescript
|
||||
// 旧
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
|
||||
// 新
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
```
|
||||
|
||||
3. **状态值**:
|
||||
|
||||
```typescript
|
||||
// 旧
|
||||
status: 'done';
|
||||
|
||||
// 新
|
||||
status: 'finished';
|
||||
```
|
||||
|
||||
4. **事件回调**:
|
||||
|
||||
```typescript
|
||||
// 旧
|
||||
@remove="handleRemove"
|
||||
function handleRemove(file: UploadFile) { }
|
||||
|
||||
// 新
|
||||
@remove="handleRemove"
|
||||
function handleRemove(options: { file: UploadFileInfo; fileList: UploadFileInfo[] }) { }
|
||||
```
|
||||
|
||||
5. **自定义上传**:
|
||||
|
||||
```typescript
|
||||
// 旧
|
||||
customRequest(info: UploadRequestOption) {
|
||||
info.onSuccess!(res);
|
||||
}
|
||||
|
||||
// 新
|
||||
customRequest(options: UploadCustomRequestOptions) {
|
||||
options.onFinish();
|
||||
}
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-01-16)
|
||||
|
||||
- ✅ 将所有上传组件从 Ant Design Vue 重构为 Naive UI
|
||||
- ✅ 保持原有功能和 API 兼容性
|
||||
- ✅ 优化代码结构和类型定义
|
||||
- ✅ 修复所有 linter 错误
|
||||
- ✅ 添加完整的文档说明
|
||||
345
apps/web-naive/src/components/upload/file-upload.vue
Normal file
345
apps/web-naive/src/components/upload/file-upload.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
||||
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { checkFileType, isFunction, isObject, isString } from '@vben/utils';
|
||||
|
||||
import { NButton, NUpload, useMessage } from 'naive-ui';
|
||||
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
modelValue: undefined,
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
drag: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'change',
|
||||
'update:value',
|
||||
'update:modelValue',
|
||||
'delete',
|
||||
'returnText',
|
||||
'preview',
|
||||
]);
|
||||
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const message = useMessage();
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
|
||||
const isActMsg = ref<boolean>(true); // 文件类型错误提示
|
||||
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
|
||||
const uploadNumber = ref<number>(0); // 上传文件计数器
|
||||
const uploadList = ref<any[]>([]); // 临时上传列表
|
||||
|
||||
watch(
|
||||
currentValue,
|
||||
(v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string[] = [];
|
||||
if (v) {
|
||||
if (Array.isArray(v)) {
|
||||
value = v;
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
id: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'finished',
|
||||
url: item,
|
||||
} as UploadFileInfo;
|
||||
} else if (item && isObject(item)) {
|
||||
return item as unknown as UploadFileInfo;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item) => item !== null) as UploadFileInfo[];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 移除文件 */
|
||||
function handleRemove(options: {
|
||||
file: UploadFileInfo;
|
||||
fileList: UploadFileInfo[];
|
||||
}) {
|
||||
const file = options.file;
|
||||
const index = fileList.value.findIndex((item) => item.id === file.id);
|
||||
if (index !== -1) {
|
||||
fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理文件预览 */
|
||||
function handlePreview(file: UploadFileInfo) {
|
||||
emit('preview', file);
|
||||
}
|
||||
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/** 上传前校验 */
|
||||
async function beforeUpload(options: {
|
||||
file: UploadFileInfo;
|
||||
fileList: UploadFileInfo[];
|
||||
}) {
|
||||
const file = options.file.file as File;
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
|
||||
// 检查文件数量限制
|
||||
if (fileList.value.length >= props.maxNumber) {
|
||||
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return false;
|
||||
}
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkFileType(file, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('ui.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 自定义上传 */
|
||||
async function customRequest(options: UploadCustomRequestOptions) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
}
|
||||
try {
|
||||
// 上传文件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
options.onProgress?.({ percent });
|
||||
};
|
||||
const res = await api?.(options.file.file as File, progressEvent);
|
||||
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res, options.file);
|
||||
|
||||
options.onFinish();
|
||||
message.success($t('ui.upload.uploadSuccess'));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
options.onError();
|
||||
handleUploadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理上传成功 */
|
||||
function handleUploadSuccess(res: any, file: UploadFileInfo) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
if (index !== -1) {
|
||||
fileList.value?.splice(index!, 1);
|
||||
}
|
||||
|
||||
// 添加到临时上传列表
|
||||
const fileUrl = res?.url || res?.data || res;
|
||||
uploadList.value.push({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: 'finished',
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
if (uploadList.value.length >= uploadNumber.value) {
|
||||
fileList.value?.push(...uploadList.value);
|
||||
uploadList.value = [];
|
||||
uploadNumber.value = 0;
|
||||
|
||||
// 更新值
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取值 */
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === 'finished')
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response;
|
||||
}
|
||||
return item?.url || item?.response?.url || item?.response;
|
||||
});
|
||||
|
||||
// 单个文件的情况,根据输入参数类型决定返回格式
|
||||
if (props.maxNumber === 1) {
|
||||
const singleValue = list.length > 0 ? list[0] : '';
|
||||
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
|
||||
if (
|
||||
isString(props.value) ||
|
||||
(isUsingModelValue.value && isString(props.modelValue))
|
||||
) {
|
||||
return singleValue;
|
||||
}
|
||||
return singleValue;
|
||||
}
|
||||
|
||||
// 多文件情况,根据输入参数类型决定返回格式
|
||||
if (isUsingModelValue.value) {
|
||||
return Array.isArray(props.modelValue) ? list : list.join(',');
|
||||
}
|
||||
|
||||
return Array.isArray(props.value) ? list : list.join(',');
|
||||
}
|
||||
|
||||
/** 处理文件列表变化 */
|
||||
function handleChange() {
|
||||
// 移除操作已经在 handleRemove 中处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NUpload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:directory="drag"
|
||||
:max="maxNumber"
|
||||
:multiple="multiple"
|
||||
:show-download-button="true"
|
||||
:show-preview-button="true"
|
||||
:show-remove-button="true"
|
||||
@before-upload="beforeUpload"
|
||||
@change="handleChange"
|
||||
@preview="handlePreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="drag" class="upload-drag-area">
|
||||
<div class="flex flex-col items-center justify-center p-6">
|
||||
<IconifyIcon
|
||||
icon="lucide:cloud-upload"
|
||||
class="mb-4 text-5xl text-gray-400"
|
||||
/>
|
||||
<p class="mb-2 text-base text-gray-600">点击或拖拽文件到此区域上传</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
支持{{ accept.join('/') }}格式文件,不超过{{ maxSize }}MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<NButton v-else-if="fileList && fileList.length < maxNumber" secondary>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
</template>
|
||||
{{ $t('ui.upload.upload') }}
|
||||
</NButton>
|
||||
</NUpload>
|
||||
<div
|
||||
v-if="showDescription && !drag"
|
||||
class="mt-2 flex flex-wrap items-center text-sm text-gray-600"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-drag-area {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #fafafa;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-drag-area:hover {
|
||||
background-color: #f0f9ff;
|
||||
border-color: #18a058;
|
||||
}
|
||||
</style>
|
||||
341
apps/web-naive/src/components/upload/image-upload.vue
Normal file
341
apps/web-naive/src/components/upload/image-upload.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
||||
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import {
|
||||
defaultImageAccepts,
|
||||
isFunction,
|
||||
isImage,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { NImage, NImageGroup, NModal, NUpload, useMessage } from 'naive-ui';
|
||||
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
modelValue: undefined,
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccepts,
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'change',
|
||||
'update:value',
|
||||
'update:modelValue',
|
||||
'delete',
|
||||
]);
|
||||
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const message = useMessage();
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
|
||||
const previewOpen = ref<boolean>(false); // 是否展示预览
|
||||
const previewImage = ref<string>(''); // 预览图片
|
||||
const previewTitle = ref<string>(''); // 预览标题
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
|
||||
const isActMsg = ref<boolean>(true); // 文件类型错误提示
|
||||
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
|
||||
const uploadNumber = ref<number>(0); // 上传文件计数器
|
||||
const uploadList = ref<any[]>([]); // 临时上传列表
|
||||
|
||||
watch(
|
||||
currentValue,
|
||||
async (v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string | string[] = [];
|
||||
if (v) {
|
||||
if (Array.isArray(v)) {
|
||||
value = v;
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
id: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'finished',
|
||||
url: item,
|
||||
} as UploadFileInfo;
|
||||
} else if (item && isObject(item)) {
|
||||
return item as unknown as UploadFileInfo;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item) => item !== null) as UploadFileInfo[];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 预览图片 */
|
||||
async function handlePreview(file: UploadFileInfo) {
|
||||
previewImage.value = file.url || '';
|
||||
previewOpen.value = true;
|
||||
previewTitle.value = file.name || '';
|
||||
}
|
||||
|
||||
/** 移除文件 */
|
||||
function handleRemove(options: {
|
||||
file: UploadFileInfo;
|
||||
fileList: UploadFileInfo[];
|
||||
}) {
|
||||
const file = options.file;
|
||||
const index = fileList.value.findIndex((item) => item.id === file.id);
|
||||
if (index !== -1) {
|
||||
fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传前校验 */
|
||||
function beforeUpload(options: {
|
||||
file: UploadFileInfo;
|
||||
fileList: UploadFileInfo[];
|
||||
}) {
|
||||
const file = options.file.file as File;
|
||||
|
||||
// 检查文件数量限制
|
||||
if (fileList.value.length >= props.maxNumber) {
|
||||
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return false;
|
||||
}
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = isImage(file.name, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('ui.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 自定义上传 */
|
||||
async function customRequest(options: UploadCustomRequestOptions) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
}
|
||||
try {
|
||||
// 上传文件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
options.onProgress?.({ percent });
|
||||
};
|
||||
const res = await api?.(options.file.file as File, progressEvent);
|
||||
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res, options.file);
|
||||
|
||||
options.onFinish();
|
||||
message.success($t('ui.upload.uploadSuccess'));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
options.onError();
|
||||
handleUploadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理上传成功 */
|
||||
function handleUploadSuccess(res: any, file: UploadFileInfo) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
if (index !== -1) {
|
||||
fileList.value?.splice(index!, 1);
|
||||
}
|
||||
|
||||
// 添加到临时上传列表
|
||||
const fileUrl = res?.url || res?.data || res;
|
||||
uploadList.value.push({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: 'finished',
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
if (uploadList.value.length >= uploadNumber.value) {
|
||||
fileList.value?.push(...uploadList.value);
|
||||
uploadList.value = [];
|
||||
uploadNumber.value = 0;
|
||||
|
||||
// 更新值
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/** 获取值 */
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === 'finished')
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response;
|
||||
}
|
||||
return item?.url || item?.response?.url || item?.response;
|
||||
});
|
||||
|
||||
// 单个文件的情况,根据输入参数类型决定返回格式
|
||||
if (props.maxNumber === 1) {
|
||||
const singleValue = list.length > 0 ? list[0] : '';
|
||||
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
|
||||
if (
|
||||
isString(props.value) ||
|
||||
(isUsingModelValue.value && isString(props.modelValue))
|
||||
) {
|
||||
return singleValue;
|
||||
}
|
||||
return singleValue;
|
||||
}
|
||||
|
||||
// 多文件情况,根据输入参数类型决定返回格式
|
||||
if (isUsingModelValue.value) {
|
||||
return Array.isArray(props.modelValue) ? list : list.join(',');
|
||||
}
|
||||
|
||||
return Array.isArray(props.value) ? list : list.join(',');
|
||||
}
|
||||
|
||||
/** 处理文件列表变化 */
|
||||
function handleChange() {
|
||||
// 移除操作已经在 handleRemove 中处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NUpload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
list-type="image-card"
|
||||
:max="maxNumber"
|
||||
:multiple="multiple"
|
||||
:show-preview-button="true"
|
||||
:show-remove-button="true"
|
||||
@before-upload="beforeUpload"
|
||||
@change="handleChange"
|
||||
@preview="handlePreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div
|
||||
v-if="fileList && fileList.length < maxNumber"
|
||||
class="flex flex-col items-center justify-center p-2"
|
||||
>
|
||||
<IconifyIcon icon="lucide:cloud-upload" class="text-2xl" />
|
||||
<div class="mt-2 text-sm">{{ $t('ui.upload.imgUpload') }}</div>
|
||||
</div>
|
||||
</NUpload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-sm text-gray-600"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
<NModal
|
||||
v-model:show="previewOpen"
|
||||
:title="previewTitle"
|
||||
preset="card"
|
||||
class="w-[600px]"
|
||||
>
|
||||
<NImageGroup>
|
||||
<NImage :src="previewImage" alt="" class="w-full" />
|
||||
</NImageGroup>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-upload-trigger) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
3
apps/web-naive/src/components/upload/index.ts
Normal file
3
apps/web-naive/src/components/upload/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as FileUpload } from './file-upload.vue';
|
||||
export { default as ImageUpload } from './image-upload.vue';
|
||||
export { default as InputUpload } from './input-upload.vue';
|
||||
81
apps/web-naive/src/components/upload/input-upload.vue
Normal file
81
apps/web-naive/src/components/upload/input-upload.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputProps } from 'naive-ui';
|
||||
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { NGrid, NGridItem, NInput } from 'naive-ui';
|
||||
|
||||
import FileUpload from './file-upload.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string;
|
||||
fileUploadProps?: FileUploadProps;
|
||||
inputProps?: InputProps;
|
||||
inputType?: 'input' | 'textarea';
|
||||
modelValue?: string;
|
||||
textareaProps?: InputProps;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'change', payload: string): void;
|
||||
(e: 'update:value', payload: string): void;
|
||||
(e: 'update:modelValue', payload: string): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
defaultValue: props.defaultValue,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
function handleReturnText(text: string) {
|
||||
modelValue.value = text;
|
||||
emits('change', modelValue.value);
|
||||
emits('update:value', modelValue.value);
|
||||
emits('update:modelValue', modelValue.value);
|
||||
}
|
||||
|
||||
const inputPropsComputed = computed(() => {
|
||||
return {
|
||||
...props.inputProps,
|
||||
value: modelValue.value as string,
|
||||
};
|
||||
});
|
||||
|
||||
const textareaPropsComputed = computed(() => {
|
||||
return {
|
||||
...props.textareaProps,
|
||||
value: modelValue.value as string,
|
||||
};
|
||||
});
|
||||
|
||||
const fileUploadProps = computed(() => {
|
||||
return {
|
||||
...props.fileUploadProps,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NGrid :cols="24" :x-gap="12">
|
||||
<NGridItem :span="18">
|
||||
<NInput
|
||||
v-if="inputType === 'input'"
|
||||
readonly
|
||||
v-bind="inputPropsComputed"
|
||||
/>
|
||||
<NInput
|
||||
v-else
|
||||
readonly
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
v-bind="textareaPropsComputed"
|
||||
/>
|
||||
</NGridItem>
|
||||
<NGridItem :span="6">
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</template>
|
||||
39
apps/web-naive/src/components/upload/typing.ts
Normal file
39
apps/web-naive/src/components/upload/typing.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
export enum UploadResultStatus {
|
||||
DONE = 'done',
|
||||
ERROR = 'error',
|
||||
SUCCESS = 'success',
|
||||
UPLOADING = 'uploading',
|
||||
}
|
||||
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export interface FileUploadProps {
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
drag?: boolean; // 是否支持拖拽上传
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
modelValue?: string | string[]; // v-model 支持
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}
|
||||
168
apps/web-naive/src/components/upload/use-upload.ts
Normal file
168
apps/web-naive/src/components/upload/use-upload.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file';
|
||||
|
||||
import { computed, unref } from 'vue';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
|
||||
import { baseRequestClient } from '#/api/request';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
/**
|
||||
* 上传类型
|
||||
*/
|
||||
enum UPLOAD_TYPE {
|
||||
// 客户端直接上传(只支持S3服务)
|
||||
CLIENT = 'client',
|
||||
// 客户端发送到后端上传
|
||||
SERVER = 'server',
|
||||
}
|
||||
|
||||
export function useUploadType({
|
||||
acceptRef,
|
||||
helpTextRef,
|
||||
maxNumberRef,
|
||||
maxSizeRef,
|
||||
}: {
|
||||
acceptRef: Ref<string[]>;
|
||||
helpTextRef: Ref<string>;
|
||||
maxNumberRef: Ref<number>;
|
||||
maxSizeRef: Ref<number>;
|
||||
}) {
|
||||
// 文件类型限制
|
||||
const getAccept = computed(() => {
|
||||
const accept = unref(acceptRef);
|
||||
if (accept && accept.length > 0) {
|
||||
return accept;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const getStringAccept = computed(() => {
|
||||
return unref(getAccept)
|
||||
.map((item) => {
|
||||
return item.indexOf('/') > 0 || item.startsWith('.')
|
||||
? item
|
||||
: `.${item}`;
|
||||
})
|
||||
.join(',');
|
||||
});
|
||||
|
||||
// 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
|
||||
const getHelpText = computed(() => {
|
||||
const helpText = unref(helpTextRef);
|
||||
if (helpText) {
|
||||
return helpText;
|
||||
}
|
||||
const helpTexts: string[] = [];
|
||||
|
||||
const accept = unref(acceptRef);
|
||||
if (accept.length > 0) {
|
||||
helpTexts.push($t('ui.upload.accept', [accept.join(',')]));
|
||||
}
|
||||
|
||||
const maxSize = unref(maxSizeRef);
|
||||
if (maxSize) {
|
||||
helpTexts.push($t('ui.upload.maxSize', [maxSize]));
|
||||
}
|
||||
|
||||
const maxNumber = unref(maxNumberRef);
|
||||
if (maxNumber && maxNumber !== Infinity) {
|
||||
helpTexts.push($t('ui.upload.maxNumber', [maxNumber]));
|
||||
}
|
||||
return helpTexts.join(',');
|
||||
});
|
||||
return { getAccept, getStringAccept, getHelpText };
|
||||
}
|
||||
|
||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||
export function useUpload(directory?: string) {
|
||||
// 后端上传地址
|
||||
const uploadUrl = getUploadUrl();
|
||||
// 是否使用前端直连上传
|
||||
const isClientUpload =
|
||||
UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
|
||||
// 重写ElUpload上传方法
|
||||
async function httpRequest(
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) {
|
||||
// 模式一:前端上传
|
||||
if (isClientUpload) {
|
||||
// 1.1 生成文件名称
|
||||
const fileName = await generateFileName(file);
|
||||
// 1.2 获取文件预签名地址
|
||||
const presignedInfo = await getFilePresignedUrl(fileName, directory);
|
||||
// 1.3 上传文件
|
||||
return baseRequestClient
|
||||
.put(presignedInfo.uploadUrl, file, {
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
// 1.4. 记录文件信息到后端(异步)
|
||||
createFile0(presignedInfo, file);
|
||||
// 通知成功,数据格式保持与后端上传的返回结果一致
|
||||
return { url: presignedInfo.url };
|
||||
});
|
||||
} else {
|
||||
// 模式二:后端上传
|
||||
return uploadFile({ file, directory }, onUploadProgress);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uploadUrl,
|
||||
httpRequest,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得上传 URL
|
||||
*/
|
||||
export function getUploadUrl(): string {
|
||||
return `${apiURL}/infra/file/upload`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件信息
|
||||
*
|
||||
* @param vo 文件预签名信息
|
||||
* @param file 文件
|
||||
*/
|
||||
function createFile0(
|
||||
vo: InfraFileApi.FilePresignedUrlRespVO,
|
||||
file: File,
|
||||
): InfraFileApi.File {
|
||||
const fileVO = {
|
||||
configId: vo.configId,
|
||||
url: vo.url,
|
||||
path: vo.path,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
createFile(fileVO);
|
||||
return fileVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名称(使用算法SHA256)
|
||||
*
|
||||
* @param file 要上传的文件
|
||||
*/
|
||||
async function generateFileName(file: File) {
|
||||
// // 读取文件内容
|
||||
// const data = await file.arrayBuffer();
|
||||
// const wordArray = CryptoJS.lib.WordArray.create(data);
|
||||
// // 计算SHA256
|
||||
// const sha256 = CryptoJS.SHA256(wordArray).toString();
|
||||
// // 拼接后缀
|
||||
// const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
|
||||
// return `${sha256}${ext}`;
|
||||
return file.name;
|
||||
}
|
||||
25
apps/web-naive/src/layouts/auth.vue
Normal file
25
apps/web-naive/src/layouts/auth.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthPageLayout } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:logo-dark="logoDark"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
</AuthPageLayout>
|
||||
</template>
|
||||
271
apps/web-naive/src/layouts/basic.vue
Normal file
271
apps/web-naive/src/layouts/basic.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import type { SystemTenantApi } from '#/api/system/tenant';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { isTenantEnable, useTabs, useWatermark } from '@vben/hooks';
|
||||
import {
|
||||
AntdProfileOutlined,
|
||||
BookOpenText,
|
||||
CircleHelp,
|
||||
SvgGithubIcon,
|
||||
} from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
Help,
|
||||
LockScreen,
|
||||
Notification,
|
||||
TenantDropdown,
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { formatDateTime, openWindow } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
getUnreadNotifyMessageCount,
|
||||
getUnreadNotifyMessageList,
|
||||
updateAllNotifyMessageRead,
|
||||
updateNotifyMessageRead,
|
||||
} from '#/api/system/notify/message';
|
||||
import { getSimpleTenantList } from '#/api/system/tenant';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||
const { closeOtherTabs, refreshTab } = useTabs();
|
||||
|
||||
const notifications = ref<NotificationItem[]>([]);
|
||||
const unreadCount = ref(0);
|
||||
const showDot = computed(() => unreadCount.value > 0);
|
||||
|
||||
const [HelpModal, helpModalApi] = useVbenModal({
|
||||
connectedComponent: Help,
|
||||
});
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
handler: () => {
|
||||
router.push({ name: 'Profile' });
|
||||
},
|
||||
icon: AntdProfileOutlined,
|
||||
text: $t('ui.widgets.profile'),
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(VBEN_DOC_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: BookOpenText,
|
||||
text: $t('ui.widgets.document'),
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(VBEN_GITHUB_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: SvgGithubIcon,
|
||||
text: 'GitHub',
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
helpModalApi.open();
|
||||
},
|
||||
icon: CircleHelp,
|
||||
text: $t('ui.widgets.qa'),
|
||||
},
|
||||
]);
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
/** 获得未读消息数 */
|
||||
async function handleNotificationGetUnreadCount() {
|
||||
unreadCount.value = await getUnreadNotifyMessageCount();
|
||||
}
|
||||
|
||||
/** 获得消息列表 */
|
||||
async function handleNotificationGetList() {
|
||||
const list = await getUnreadNotifyMessageList();
|
||||
notifications.value = list.map((item) => ({
|
||||
avatar: preferences.app.defaultAvatar,
|
||||
date: formatDateTime(item.createTime) as string,
|
||||
isRead: false,
|
||||
id: item.id,
|
||||
message: item.templateContent,
|
||||
title: item.templateNickname,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 跳转我的站内信 */
|
||||
function handleNotificationViewAll() {
|
||||
router.push({
|
||||
name: 'MyNotifyMessage',
|
||||
});
|
||||
}
|
||||
|
||||
/** 标记所有已读 */
|
||||
async function handleNotificationMakeAll() {
|
||||
await updateAllNotifyMessageRead();
|
||||
unreadCount.value = 0;
|
||||
notifications.value = [];
|
||||
}
|
||||
|
||||
/** 清空通知 */
|
||||
async function handleNotificationClear() {
|
||||
await handleNotificationMakeAll();
|
||||
}
|
||||
|
||||
/** 标记单个已读 */
|
||||
async function handleNotificationRead(item: NotificationItem) {
|
||||
if (!item.id) {
|
||||
return;
|
||||
}
|
||||
await updateNotifyMessageRead([item.id]);
|
||||
await handleNotificationGetUnreadCount();
|
||||
notifications.value = notifications.value.filter((n) => n.id !== item.id);
|
||||
}
|
||||
|
||||
/** 处理通知打开 */
|
||||
function handleNotificationOpen(open: boolean) {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
handleNotificationGetList();
|
||||
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(() => {
|
||||
// 首次加载未读数量
|
||||
handleNotificationGetUnreadCount();
|
||||
// 获取租户列表
|
||||
handleGetTenantList();
|
||||
// 轮询刷新未读数量
|
||||
setInterval(
|
||||
() => {
|
||||
if (userStore.userInfo) {
|
||||
handleNotificationGetUnreadCount();
|
||||
}
|
||||
},
|
||||
1000 * 60 * 2,
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
content: preferences.app.watermarkContent,
|
||||
}),
|
||||
async ({ enable, content }) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content:
|
||||
content ||
|
||||
`${userStore.userInfo?.id} - ${userStore.userInfo?.nickname}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||
<template #user-dropdown>
|
||||
<UserDropdown
|
||||
:avatar
|
||||
:menus
|
||||
:text="userStore.userInfo?.nickname"
|
||||
:description="userStore.userInfo?.email"
|
||||
:tag-text="userStore.userInfo?.username"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
<Notification
|
||||
:dot="showDot"
|
||||
:notifications="notifications"
|
||||
@clear="handleNotificationClear"
|
||||
@make-all="handleNotificationMakeAll"
|
||||
@view-all="handleNotificationViewAll"
|
||||
@open="handleNotificationOpen"
|
||||
@read="handleNotificationRead"
|
||||
/>
|
||||
</template>
|
||||
<template #header-right-1>
|
||||
<div v-if="tenantEnable">
|
||||
<TenantDropdown
|
||||
class="mr-2"
|
||||
:tenant-list="tenants"
|
||||
:visit-tenant-id="accessStore.visitTenantId"
|
||||
@success="handleTenantChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<AuthenticationLoginExpiredModal
|
||||
v-model:open="accessStore.loginExpired"
|
||||
:avatar
|
||||
>
|
||||
<LoginForm />
|
||||
</AuthenticationLoginExpiredModal>
|
||||
</template>
|
||||
<template #lock-screen>
|
||||
<LockScreen :avatar @to-login="handleLogout" />
|
||||
</template>
|
||||
</BasicLayout>
|
||||
<HelpModal />
|
||||
</template>
|
||||
78
apps/web-naive/src/locales/index.ts
Normal file
78
apps/web-naive/src/locales/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import {
|
||||
$t,
|
||||
setupI18n as coreSetup,
|
||||
loadLocalesMapFromDir,
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const modules = import.meta.glob('./langs/**/*.json');
|
||||
|
||||
const localesMap = loadLocalesMapFromDir(
|
||||
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
||||
modules,
|
||||
);
|
||||
|
||||
/**
|
||||
* 加载应用特有的语言包
|
||||
* 这里也可以改造为从服务端获取翻译数据
|
||||
* @param lang
|
||||
*/
|
||||
async function loadMessages(lang: SupportedLanguagesType) {
|
||||
const [appLocaleMessages] = await Promise.all([
|
||||
localesMap[lang]?.(),
|
||||
loadThirdPartyMessage(lang),
|
||||
]);
|
||||
return appLocaleMessages?.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载第三方组件库的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||
await loadDayjsLocale(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载dayjs的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
let locale;
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
locale = await import('dayjs/locale/en');
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
locale = await import('dayjs/locale/zh-cn');
|
||||
break;
|
||||
}
|
||||
// 默认使用英语
|
||||
default: {
|
||||
locale = await import('dayjs/locale/en');
|
||||
}
|
||||
}
|
||||
if (locale) {
|
||||
dayjs.locale(locale);
|
||||
} else {
|
||||
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
await coreSetup(app, {
|
||||
defaultLocale: preferences.app.locale,
|
||||
loadMessages,
|
||||
missingWarn: !import.meta.env.PROD,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, setupI18n };
|
||||
34
apps/web-naive/src/locales/langs/en-US/page.json
Normal file
34
apps/web-naive/src/locales/langs/en-US/page.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"analytics": "Analytics",
|
||||
"workspace": "Workspace"
|
||||
},
|
||||
"action": {
|
||||
"action": "Action",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"reset": "Reset",
|
||||
"search": "Search",
|
||||
"more": "More"
|
||||
},
|
||||
"tenant": {
|
||||
"placeholder": "Please select tenant",
|
||||
"success": "Switch tenant success"
|
||||
}
|
||||
}
|
||||
34
apps/web-naive/src/locales/langs/zh-CN/page.json
Normal file
34
apps/web-naive/src/locales/langs/zh-CN/page.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码",
|
||||
"profile": "个人中心"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
"analytics": "分析页",
|
||||
"workspace": "工作台"
|
||||
},
|
||||
"action": {
|
||||
"action": "操作",
|
||||
"add": "新增",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"save": "保存",
|
||||
"import": "导入",
|
||||
"export": "导出",
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"reset": "重置",
|
||||
"search": "搜索",
|
||||
"more": "更多"
|
||||
},
|
||||
"tenant": {
|
||||
"placeholder": "请选择租户",
|
||||
"success": "切换租户成功"
|
||||
}
|
||||
}
|
||||
153
apps/web-naive/src/router/guard.ts
Normal file
153
apps/web-naive/src/router/guard.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useDictStore, useUserStore } from '@vben/stores';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getSimpleDictDataList } from '#/api/system/dict/data';
|
||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { generateAccess } from './access';
|
||||
|
||||
/**
|
||||
* 通用守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function setupCommonGuard(router: Router) {
|
||||
// 记录已经加载的页面
|
||||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach((to) => {
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
if (!to.meta.loaded && preferences.transition.progress) {
|
||||
startProgress();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
stopProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限访问守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function setupAccessGuard(router: Router) {
|
||||
router.beforeEach(async (to, from) => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const dictStore = useDictStore();
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// accessToken 检查
|
||||
if (!accessStore.accessToken) {
|
||||
// 明确声明忽略权限访问权限,则可以访问
|
||||
if (to.meta.ignoreAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 没有访问权限,跳转登录页面
|
||||
if (to.fullPath !== LOGIN_PATH) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query:
|
||||
to.fullPath === preferences.app.defaultHomePath
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessStore.isAccessChecked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 加载字典数据(不阻塞加载)
|
||||
dictStore.setDictCacheByApi(getSimpleDictDataList);
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
let userInfo = userStore.userInfo;
|
||||
if (!userInfo) {
|
||||
// add by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync
|
||||
const loading = message.loading(`${$t('common.loadingMenu')}...`);
|
||||
try {
|
||||
const authPermissionInfo = await authStore.fetchUserInfo();
|
||||
if (authPermissionInfo) {
|
||||
userInfo = authPermissionInfo.user;
|
||||
}
|
||||
} finally {
|
||||
loading.destroy();
|
||||
}
|
||||
}
|
||||
const userRoles = userStore.userRoles ?? [];
|
||||
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
roles: userRoles,
|
||||
router,
|
||||
// 则会在菜单中显示,但是访问会被重定向到403
|
||||
routes: accessRoutes,
|
||||
});
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
userStore.setUserRoles(userRoles);
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === preferences.app.defaultHomePath
|
||||
? userInfo?.homePath || preferences.app.defaultHomePath
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
replace: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目守卫配置
|
||||
* @param router
|
||||
*/
|
||||
function createRouterGuard(router: Router) {
|
||||
/** 通用 */
|
||||
setupCommonGuard(router);
|
||||
/** 权限访问 */
|
||||
setupAccessGuard(router);
|
||||
}
|
||||
|
||||
export { createRouterGuard };
|
||||
38
apps/web-naive/src/router/routes/modules/dashboard.ts
Normal file
38
apps/web-naive/src/router/routes/modules/dashboard.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
path: '/analytics',
|
||||
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||
meta: {
|
||||
affixTab: true,
|
||||
icon: 'lucide:area-chart',
|
||||
title: $t('page.dashboard.analytics'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
path: '/workspace',
|
||||
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||
meta: {
|
||||
icon: 'carbon:workspace',
|
||||
title: $t('page.dashboard.workspace'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
30
apps/web-naive/src/router/routes/modules/infra.ts
Normal file
30
apps/web-naive/src/router/routes/modules/infra.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/infra/job/log',
|
||||
component: () => import('#/views/infra/job/logger/index.vue'),
|
||||
name: 'InfraJobLog',
|
||||
meta: {
|
||||
title: '调度日志',
|
||||
icon: 'ant-design:history-outlined',
|
||||
activePath: '/infra/job',
|
||||
keepAlive: false,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/infra/codegen/edit',
|
||||
component: () => import('#/views/infra/codegen/edit/index.vue'),
|
||||
name: 'InfraCodegenEdit',
|
||||
meta: {
|
||||
title: '生成配置修改',
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
activePath: '/infra/codegen',
|
||||
keepAlive: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
157
apps/web-naive/src/store/auth.ts
Normal file
157
apps/web-naive/src/store/auth.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { AuthPermissionInfo, Recordable, UserInfo } from '@vben/types';
|
||||
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { notification } from '#/adapter/naive';
|
||||
import {
|
||||
getAuthPermissionInfoApi,
|
||||
loginApi,
|
||||
logoutApi,
|
||||
register,
|
||||
smsLogin,
|
||||
socialLogin,
|
||||
} from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param type 登录类型
|
||||
* @param params 登录表单数据
|
||||
* @param onSuccess 登录成功后的回调函数
|
||||
*/
|
||||
async function authLogin(
|
||||
type: 'mobile' | 'register' | 'social' | 'username',
|
||||
params: Recordable<any>,
|
||||
onSuccess?: () => Promise<void> | void,
|
||||
) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
let loginResult: AuthApi.LoginResult;
|
||||
loginLoading.value = true;
|
||||
switch (type) {
|
||||
case 'mobile': {
|
||||
loginResult = await smsLogin(params as AuthApi.SmsLoginParams);
|
||||
break;
|
||||
}
|
||||
case 'register': {
|
||||
loginResult = await register(params as AuthApi.RegisterParams);
|
||||
break;
|
||||
}
|
||||
case 'social': {
|
||||
loginResult = await socialLogin(params as AuthApi.SocialLoginParams);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
loginResult = await loginApi(params);
|
||||
}
|
||||
}
|
||||
const { accessToken, refreshToken } = loginResult;
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
accessStore.setAccessToken(accessToken);
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
|
||||
// 获取用户信息并存储到 userStore、accessStore 中
|
||||
// TODO @芋艿:清理掉 accessCodes 相关的逻辑
|
||||
// const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
// fetchUserInfo(),
|
||||
// // getAccessCodesApi(),
|
||||
// ]);
|
||||
const fetchUserInfoResult = await fetchUserInfo();
|
||||
|
||||
userInfo = fetchUserInfoResult.user;
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(
|
||||
userInfo.homePath || preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (userInfo?.nickname) {
|
||||
notification.success({
|
||||
content: $t('authentication.loginSuccess'),
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.nickname}`,
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
const accessToken = accessStore.accessToken as string;
|
||||
if (accessToken) {
|
||||
await logoutApi(accessToken);
|
||||
}
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登录页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
// 加载
|
||||
let authPermissionInfo: AuthPermissionInfo | null = null;
|
||||
authPermissionInfo = await getAuthPermissionInfoApi();
|
||||
// userStore
|
||||
userStore.setUserInfo(authPermissionInfo.user);
|
||||
userStore.setUserRoles(authPermissionInfo.roles);
|
||||
// accessStore
|
||||
accessStore.setAccessMenus(authPermissionInfo.menus);
|
||||
accessStore.setAccessCodes(authPermissionInfo.permissions);
|
||||
return authPermissionInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
1
apps/web-naive/src/store/index.ts
Normal file
1
apps/web-naive/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
1
apps/web-naive/src/utils/index.ts
Normal file
1
apps/web-naive/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './rangePickerProps';
|
||||
52
apps/web-naive/src/utils/rangePickerProps.ts
Normal file
52
apps/web-naive/src/utils/rangePickerProps.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
/** 时间段选择器拓展 */
|
||||
export function getRangePickerDefaultProps() {
|
||||
return {
|
||||
startPlaceholder: $t('utils.rangePicker.beginTime'),
|
||||
endPlaceholder: $t('utils.rangePicker.endTime'),
|
||||
type: 'datetimerange',
|
||||
format: 'YYYY-MM-dd HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-dd HH:mm:ss',
|
||||
defaultTime: ['00:00:00', '23:59:59'],
|
||||
shortcuts: {
|
||||
[$t('utils.rangePicker.today')]: () =>
|
||||
[
|
||||
dayjs().startOf('day').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.last7Days')]: () =>
|
||||
[
|
||||
dayjs().subtract(7, 'day').startOf('day').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.last30Days')]: () =>
|
||||
[
|
||||
dayjs().subtract(30, 'day').startOf('day').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.yesterday')]: () =>
|
||||
[
|
||||
dayjs().subtract(1, 'day').startOf('day').toDate(),
|
||||
dayjs().subtract(1, 'day').endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.thisWeek')]: () =>
|
||||
[
|
||||
dayjs().startOf('week').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.thisMonth')]: () =>
|
||||
[
|
||||
dayjs().startOf('month').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
[$t('utils.rangePicker.lastWeek')]: () =>
|
||||
[
|
||||
dayjs().subtract(1, 'week').startOf('day').toDate(),
|
||||
dayjs().endOf('day').toDate(),
|
||||
] as const,
|
||||
},
|
||||
};
|
||||
}
|
||||
101
apps/web-naive/src/views/_core/profile/base-setting.vue
Normal file
101
apps/web-naive/src/views/_core/profile/base-setting.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { SystemUserProfileApi } from '#/api/system/user/profile';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting, z } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { updateUserProfile } from '#/api/system/user/profile';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
profile?: SystemUserProfileApi.UserProfileRespVO;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const profileBaseSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
label: '用户昵称',
|
||||
fieldName: 'nickname',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户昵称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '用户手机',
|
||||
fieldName: 'mobile',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户手机',
|
||||
},
|
||||
rules: z.string(),
|
||||
},
|
||||
{
|
||||
label: '用户邮箱',
|
||||
fieldName: 'email',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户邮箱',
|
||||
},
|
||||
rules: z.string().email('请输入正确的邮箱'),
|
||||
},
|
||||
{
|
||||
label: '用户性别',
|
||||
fieldName: 'sex',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number(),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
async function handleSubmit(values: Recordable<any>) {
|
||||
try {
|
||||
profileBaseSettingRef.value.getFormApi().setLoading(true);
|
||||
// 提交表单
|
||||
await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReqVO);
|
||||
// 关闭并提示
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
profileBaseSettingRef.value.getFormApi().setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听 profile 变化 */
|
||||
watch(
|
||||
() => props.profile,
|
||||
(newProfile) => {
|
||||
if (newProfile) {
|
||||
profileBaseSettingRef.value.getFormApi().setValues(newProfile);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<ProfileBaseSetting
|
||||
ref="profileBaseSettingRef"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
49
apps/web-naive/src/views/_core/profile/index.vue
Normal file
49
apps/web-naive/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Profile } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import ProfileBase from './base-setting.vue';
|
||||
import ProfileNotificationSetting from './notification-setting.vue';
|
||||
import ProfilePasswordSetting from './password-setting.vue';
|
||||
import ProfileSecuritySetting from './security-setting.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const tabsValue = ref<string>('basic');
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: '基本设置',
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
label: '安全设置',
|
||||
value: 'security',
|
||||
},
|
||||
{
|
||||
label: '修改密码',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
label: '新消息提醒',
|
||||
value: 'notice',
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<Profile
|
||||
v-model:model-value="tabsValue"
|
||||
title="个人中心"
|
||||
:user-info="userStore.userInfo"
|
||||
:tabs="tabs"
|
||||
>
|
||||
<template #content>
|
||||
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||
</template>
|
||||
</Profile>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '其他用户的消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'systemMessage',
|
||||
label: '系统消息',
|
||||
description: '系统消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'todoTask',
|
||||
label: '待办任务',
|
||||
description: '待办任务将以站内信的形式通知',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||
</template>
|
||||
66
apps/web-naive/src/views/_core/profile/password-setting.vue
Normal file
66
apps/web-naive/src/views/_core/profile/password-setting.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
const profilePasswordSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'oldPassword',
|
||||
label: '旧密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入旧密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'newPassword',
|
||||
label: '新密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请输入新密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'confirmPassword',
|
||||
label: '确认密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请再次输入新密码',
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { newPassword } = values;
|
||||
return z
|
||||
.string({ required_error: '请再次输入新密码' })
|
||||
.min(1, { message: '请再次输入新密码' })
|
||||
.refine((value) => value === newPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
});
|
||||
},
|
||||
triggerFields: ['newPassword'],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
message.success('密码修改成功');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfilePasswordSetting
|
||||
ref="profilePasswordSettingRef"
|
||||
class="w-1/3"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
43
apps/web-naive/src/views/_core/profile/security-setting.vue
Normal file
43
apps/web-naive/src/views/_core/profile/security-setting.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '当前密码强度:强',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityPhone',
|
||||
label: '密保手机',
|
||||
description: '已绑定手机:138****8293',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityQuestion',
|
||||
label: '密保问题',
|
||||
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityEmail',
|
||||
label: '备用邮箱',
|
||||
description: '已绑定邮箱:ant***sign.com',
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
fieldName: 'securityMfa',
|
||||
label: 'MFA 设备',
|
||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].toSorted((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
273
apps/web-naive/src/views/infra/apiAccessLog/data.ts
Normal file
273
apps/web-naive/src/views/infra/apiAccessLog/data.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
clearable: true,
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'applicationName',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'beginTime',
|
||||
label: '请求时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'duration',
|
||||
label: '执行时长',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入执行时长',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'resultCode',
|
||||
label: '结果码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入结果码',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
title: '应用名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
title: '请求方法',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
title: '请求地址',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'beginTime',
|
||||
title: '请求时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
title: '执行时长',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => `${cellValue} ms`,
|
||||
},
|
||||
{
|
||||
field: 'resultCode',
|
||||
title: '操作结果',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return row.resultCode === 0 ? '成功' : `失败(${row.resultMsg})`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operateModule',
|
||||
title: '操作模块',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'operateName',
|
||||
title: '操作名',
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'operateType',
|
||||
title: '操作类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_OPERATE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '日志编号',
|
||||
},
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
label: '应用名',
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
label: '用户Id',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
label: '用户 IP',
|
||||
},
|
||||
{
|
||||
field: 'userAgent',
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'responseBody',
|
||||
label: '请求结果',
|
||||
},
|
||||
{
|
||||
label: '请求时间',
|
||||
field: 'beginTime',
|
||||
render: (val, data) => {
|
||||
if (val && data?.endTime) {
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请求耗时',
|
||||
field: 'duration',
|
||||
render: (val) => {
|
||||
return val ? `${val} ms` : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '操作结果',
|
||||
field: 'resultCode',
|
||||
render: (val, data) => {
|
||||
if (val === 0) {
|
||||
return '正常';
|
||||
} else if (val > 0 && data?.resultMsg) {
|
||||
return `失败 | ${val} | ${data.resultMsg}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operateModule',
|
||||
label: '操作模块',
|
||||
},
|
||||
{
|
||||
field: 'operateName',
|
||||
label: '操作名',
|
||||
},
|
||||
{
|
||||
field: 'operateType',
|
||||
label: '操作类型',
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_OPERATE_TYPE,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
107
apps/web-naive/src/views/infra/apiAccessLog/index.vue
Normal file
107
apps/web-naive/src/views/infra/apiAccessLog/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiAccessLog,
|
||||
getApiAccessLogPage,
|
||||
} from '#/api/infra/api-access-log';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportApiAccessLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 访问日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 访问日志详情 */
|
||||
function handleDetail(row: InfraApiAccessLogApi.ApiAccessLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiAccessLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraApiAccessLogApi.ApiAccessLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="handleRefresh" />
|
||||
<Grid table-title="API 访问日志列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:api-access-log:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:api-access-log:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="API 访问日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
249
apps/web-naive/src/views/infra/apiErrorLog/data.ts
Normal file
249
apps/web-naive/src/views/infra/apiErrorLog/data.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { DICT_TYPE, InfraApiErrorLogProcessStatusEnum } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
clearable: true,
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'applicationName',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'processStatus',
|
||||
label: '处理状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
'number',
|
||||
),
|
||||
clearable: true,
|
||||
placeholder: '请选择处理状态',
|
||||
},
|
||||
defaultValue: InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
title: '应用名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
title: '请求方法',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
title: '请求地址',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
title: '异常发生时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'exceptionName',
|
||||
title: '异常名',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'processStatus',
|
||||
title: '处理状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
minWidth: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '日志编号',
|
||||
},
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
label: '应用名',
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
label: '用户Id',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
label: '用户 IP',
|
||||
},
|
||||
{
|
||||
field: 'userAgent',
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
render: (val) => {
|
||||
return formatDateTime(val) as string;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'exceptionName',
|
||||
label: '异常名',
|
||||
},
|
||||
{
|
||||
field: 'exceptionStackTrace',
|
||||
label: '异常堆栈',
|
||||
show: (val) => !val,
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h('textarea', {
|
||||
value: val,
|
||||
style:
|
||||
'width: 100%; min-height: 200px; max-height: 400px; resize: vertical;',
|
||||
readonly: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processStatus',
|
||||
label: '处理状态',
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processUserId',
|
||||
label: '处理人',
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'processTime',
|
||||
label: '处理时间',
|
||||
show: (val) => !val,
|
||||
render: (val) => {
|
||||
return formatDateTime(val) as string;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
155
apps/web-naive/src/views/infra/apiErrorLog/index.vue
Normal file
155
apps/web-naive/src/views/infra/apiErrorLog/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { InfraApiErrorLogProcessStatusEnum } from '@vben/constants';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiErrorLog,
|
||||
getApiErrorLogPage,
|
||||
updateApiErrorLogStatus,
|
||||
} from '#/api/infra/api-error-log';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportApiErrorLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 错误日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 错误日志详情 */
|
||||
function handleDetail(row: InfraApiErrorLogApi.ApiErrorLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 处理已处理 / 已忽略的操作 */
|
||||
async function handleProcess(id: number, processStatus: number) {
|
||||
await confirm({
|
||||
content: `确认标记为${InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'}?`,
|
||||
});
|
||||
const hideLoading = message.loading('正在处理中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await updateApiErrorLogStatus(id, processStatus);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiErrorLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraApiErrorLogApi.ApiErrorLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="handleRefresh" />
|
||||
<Grid table-title="API 错误日志列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:api-error-log:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:api-error-log:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '已处理',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:api-error-log:update-status'],
|
||||
ifShow:
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
onClick: handleProcess.bind(
|
||||
null,
|
||||
row.id,
|
||||
InfraApiErrorLogProcessStatusEnum.DONE,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '已忽略',
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:api-error-log:update-status'],
|
||||
ifShow:
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
onClick: handleProcess.bind(
|
||||
null,
|
||||
row.id,
|
||||
InfraApiErrorLogProcessStatusEnum.IGNORE,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="API 错误日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
9
apps/web-naive/src/views/infra/build/index.vue
Normal file
9
apps/web-naive/src/views/infra/build/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'InfraBuild' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page> 待完成 </Page>
|
||||
</template>
|
||||
553
apps/web-naive/src/views/infra/codegen/data.ts
Normal file
553
apps/web-naive/src/views/infra/codegen/data.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
|
||||
import { getMenuList } from '#/api/system/menu';
|
||||
import { $t } from '#/locales';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 导入数据库表的表单 */
|
||||
export function useImportTableFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'dataSourceConfigId',
|
||||
label: '数据源',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getDataSourceConfigList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
autoSelect: 'first',
|
||||
placeholder: '请选择数据源',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入表名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'comment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 导入数据库表表格列定义 */
|
||||
export function useImportTableColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{ field: 'name', title: '表名称', minWidth: 200 },
|
||||
{ field: 'comment', title: '表描述', minWidth: 200 },
|
||||
];
|
||||
}
|
||||
|
||||
/** 基本信息表单的 schema */
|
||||
export function useBasicInfoFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'tableName',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入仓库名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'tableComment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'className',
|
||||
label: '实体类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入实体类名称',
|
||||
},
|
||||
rules: 'required',
|
||||
help: '默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。',
|
||||
},
|
||||
{
|
||||
fieldName: 'author',
|
||||
label: '作者',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入作者',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 3,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 生成信息表单基础 schema */
|
||||
export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'templateType',
|
||||
label: '生成模板',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE,
|
||||
'number',
|
||||
),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'frontType',
|
||||
label: '前端类型',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE, 'number'),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'scene',
|
||||
label: '生成场景',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE, 'number'),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'parentMenuId',
|
||||
label: '上级菜单',
|
||||
help: '分配到指定菜单下,例如 系统管理',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
api: async () => {
|
||||
const data = await getMenuList();
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级菜单',
|
||||
} as SystemMenuApi.Menu);
|
||||
return handleTree(data);
|
||||
},
|
||||
class: 'w-full',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级菜单',
|
||||
filterTreeNode(input: string, node: Recordable<any>) {
|
||||
if (!input || input.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const name: string = node.label ?? '';
|
||||
if (!name) return false;
|
||||
return name.includes(input) || $t(name).includes(input);
|
||||
},
|
||||
showSearch: true,
|
||||
treeDefaultExpandedKeys: [0],
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
title({ label, icon }: { icon: string; label: string }) {
|
||||
const components = [];
|
||||
if (!label) return '';
|
||||
if (icon) {
|
||||
components.push(h(IconifyIcon, { class: 'size-4', icon }));
|
||||
}
|
||||
components.push(h('span', { class: '' }, $t(label || '')));
|
||||
return h('div', { class: 'flex items-center gap-1' }, components);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'moduleName',
|
||||
label: '模块名',
|
||||
help: '模块名,即一级目录,例如 system、infra、tool 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'businessName',
|
||||
label: '业务名',
|
||||
help: '业务名,即二级目录,例如 user、permission、dict 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'className',
|
||||
label: '类名称',
|
||||
help: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'classComment',
|
||||
label: '类描述',
|
||||
help: '用作类描述,例如 用户',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 树表信息 schema */
|
||||
export function useGenerationInfoTreeFormSchema(
|
||||
columns: InfraCodegenApi.CodegenColumn[] = [],
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: 'treeDivider',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['树表信息'],
|
||||
};
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'treeParentColumnId',
|
||||
label: '父编号字段',
|
||||
help: '树显示的父编码字段名,例如 parent_Id',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
clearable: true,
|
||||
placeholder: '请选择',
|
||||
options: columns.map((column) => ({
|
||||
label: column.columnName,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'treeNameColumnId',
|
||||
label: '名称字段',
|
||||
help: '树节点显示的名称字段,一般是 name',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
clearable: true,
|
||||
placeholder: '请选择名称字段',
|
||||
options: columns.map((column) => ({
|
||||
label: column.columnName,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 主子表信息 schema */
|
||||
export function useGenerationInfoSubTableFormSchema(
|
||||
columns: InfraCodegenApi.CodegenColumn[] = [],
|
||||
tables: InfraCodegenApi.CodegenTable[] = [],
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: 'subDivider',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['主子表信息'],
|
||||
};
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'masterTableId',
|
||||
label: '关联的主表',
|
||||
help: '关联主表(父表)的表名, 如:system_user',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
clearable: true,
|
||||
placeholder: '请选择',
|
||||
options: tables.map((table) => ({
|
||||
label: `${table.tableName}:${table.tableComment}`,
|
||||
value: table.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'subJoinColumnId',
|
||||
label: '子表关联的字段',
|
||||
help: '子表关联的字段, 如:user_id',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
clearable: true,
|
||||
placeholder: '请选择',
|
||||
options: columns.map((column) => ({
|
||||
label: `${column.columnName}:${column.columnComment}`,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'subJoinMany',
|
||||
label: '关联关系',
|
||||
help: '主表与子表的关联关系',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
clearable: true,
|
||||
placeholder: '请选择',
|
||||
options: [
|
||||
{
|
||||
label: '一对多',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: '一对一',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'tableName',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入表名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'tableComment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'dataSourceConfigId',
|
||||
title: '数据源',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => getDataSourceConfigName?.(cellValue) || '-',
|
||||
},
|
||||
{
|
||||
field: 'tableName',
|
||||
title: '表名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'tableComment',
|
||||
title: '表描述',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'className',
|
||||
title: '实体',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 代码生成表格列定义 */
|
||||
export function useCodegenColumnTableColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ field: 'columnName', title: '字段列名', minWidth: 130 },
|
||||
{
|
||||
field: 'columnComment',
|
||||
title: '字段描述',
|
||||
minWidth: 100,
|
||||
slots: { default: 'columnComment' },
|
||||
},
|
||||
{ field: 'dataType', title: '物理类型', minWidth: 100 },
|
||||
{
|
||||
field: 'javaType',
|
||||
title: 'Java 类型',
|
||||
minWidth: 130,
|
||||
slots: { default: 'javaType' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: 'Long', value: 'Long' },
|
||||
{ label: 'String', value: 'String' },
|
||||
{ label: 'Integer', value: 'Integer' },
|
||||
{ label: 'Double', value: 'Double' },
|
||||
{ label: 'BigDecimal', value: 'BigDecimal' },
|
||||
{ label: 'LocalDateTime', value: 'LocalDateTime' },
|
||||
{ label: 'Boolean', value: 'Boolean' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'javaField',
|
||||
title: 'Java 属性',
|
||||
minWidth: 100,
|
||||
slots: { default: 'javaField' },
|
||||
},
|
||||
{
|
||||
field: 'createOperation',
|
||||
title: '插入',
|
||||
width: 40,
|
||||
slots: { default: 'createOperation' },
|
||||
},
|
||||
{
|
||||
field: 'updateOperation',
|
||||
title: '编辑',
|
||||
width: 40,
|
||||
slots: { default: 'updateOperation' },
|
||||
},
|
||||
{
|
||||
field: 'listOperationResult',
|
||||
title: '列表',
|
||||
width: 40,
|
||||
slots: { default: 'listOperationResult' },
|
||||
},
|
||||
{
|
||||
field: 'listOperation',
|
||||
title: '查询',
|
||||
width: 40,
|
||||
slots: { default: 'listOperation' },
|
||||
},
|
||||
{
|
||||
field: 'listOperationCondition',
|
||||
title: '查询方式',
|
||||
minWidth: 100,
|
||||
slots: { default: 'listOperationCondition' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: '=', value: '=' },
|
||||
{ label: '!=', value: '!=' },
|
||||
{ label: '>', value: '>' },
|
||||
{ label: '>=', value: '>=' },
|
||||
{ label: '<', value: '<' },
|
||||
{ label: '<=', value: '<=' },
|
||||
{ label: 'LIKE', value: 'LIKE' },
|
||||
{ label: 'BETWEEN', value: 'BETWEEN' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nullable',
|
||||
title: '允许空',
|
||||
width: 60,
|
||||
slots: { default: 'nullable' },
|
||||
},
|
||||
{
|
||||
field: 'htmlType',
|
||||
title: '显示类型',
|
||||
width: 130,
|
||||
slots: { default: 'htmlType' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: '文本框', value: 'input' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '复选框', value: 'checkbox' },
|
||||
{ label: '日期控件', value: 'datetime' },
|
||||
{ label: '图片上传', value: 'imageUpload' },
|
||||
{ label: '文件上传', value: 'fileUpload' },
|
||||
{ label: '富文本控件', value: 'editor' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dictType',
|
||||
title: '字典类型',
|
||||
width: 120,
|
||||
slots: { default: 'dictType' },
|
||||
},
|
||||
{
|
||||
field: 'example',
|
||||
title: '示例',
|
||||
minWidth: 100,
|
||||
slots: { default: 'example' },
|
||||
},
|
||||
];
|
||||
}
|
||||
164
apps/web-naive/src/views/infra/codegen/edit/index.vue
Normal file
164
apps/web-naive/src/views/infra/codegen/edit/index.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { ref, unref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { NButton, NStep, NSteps } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getCodegenTable, updateCodegenTable } from '#/api/infra/codegen';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import BasicInfo from '../modules/basic-info.vue';
|
||||
import ColumnInfo from '../modules/column-info.vue';
|
||||
import GenerationInfo from '../modules/generation-info.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const currentStep = ref(1);
|
||||
const formData = ref<InfraCodegenApi.CodegenDetail>({
|
||||
table: {} as InfraCodegenApi.CodegenTable,
|
||||
columns: [],
|
||||
});
|
||||
|
||||
/** 表单引用 */
|
||||
const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
|
||||
const columnInfoRef = ref<InstanceType<typeof ColumnInfo>>();
|
||||
const generateInfoRef = ref<InstanceType<typeof GenerationInfo>>();
|
||||
|
||||
/** 获取详情数据 */
|
||||
async function getDetail() {
|
||||
const id = route.query.id as any;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
formData.value = await getCodegenTable(id);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function submitForm() {
|
||||
// 表单验证
|
||||
const basicInfoValid = await basicInfoRef.value?.validate();
|
||||
if (!basicInfoValid) {
|
||||
message.warning('保存失败,原因:基本信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
const generateInfoValid = await generateInfoRef.value?.validate();
|
||||
if (!generateInfoValid) {
|
||||
message.warning('保存失败,原因:生成信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const hideLoading = message.loading($t('ui.actionMessage.updating'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
// 拼接相关信息
|
||||
const basicInfo = await basicInfoRef.value?.getValues();
|
||||
const columns = columnInfoRef.value?.getData() || unref(formData).columns;
|
||||
const generateInfo = await generateInfoRef.value?.getValues();
|
||||
await updateCodegenTable({
|
||||
table: { ...unref(formData).table, ...basicInfo, ...generateInfo },
|
||||
columns,
|
||||
});
|
||||
// 关闭并提示
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
close();
|
||||
} catch (error) {
|
||||
console.error('保存失败', error);
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表 */
|
||||
const tabs = useTabs();
|
||||
function close() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push({ name: 'InfraCodegen' });
|
||||
}
|
||||
|
||||
/** 下一步 */
|
||||
function nextStep() {
|
||||
currentStep.value += 1;
|
||||
}
|
||||
|
||||
/** 上一步 */
|
||||
function prevStep() {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 步骤配置 */
|
||||
const steps = [
|
||||
{
|
||||
title: '基本信息',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
title: '字段信息',
|
||||
key: 2,
|
||||
},
|
||||
{
|
||||
title: '生成信息',
|
||||
key: 3,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化
|
||||
getDetail();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<div class="flex h-[95%] flex-col rounded-md bg-card p-4">
|
||||
<NSteps :current="currentStep" class="mb-8 rounded shadow-sm">
|
||||
<NStep v-for="step in steps" :key="step.key" :title="step.title" />
|
||||
</NSteps>
|
||||
|
||||
<div class="flex-1 overflow-auto py-4">
|
||||
<!-- 根据当前步骤显示对应的组件 -->
|
||||
<BasicInfo
|
||||
v-show="currentStep === 1"
|
||||
ref="basicInfoRef"
|
||||
:table="formData.table"
|
||||
/>
|
||||
<ColumnInfo
|
||||
v-show="currentStep === 2"
|
||||
ref="columnInfoRef"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
<GenerationInfo
|
||||
v-show="currentStep === 3"
|
||||
ref="generateInfoRef"
|
||||
:table="formData.table"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<NButton :disabled="currentStep === 1" @click="prevStep">
|
||||
上一步
|
||||
</NButton>
|
||||
<NButton :disabled="currentStep === steps.length" @click="nextStep">
|
||||
下一步
|
||||
</NButton>
|
||||
<NButton type="primary" :loading="loading" @click="submitForm">
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
283
apps/web-naive/src/views/infra/codegen/index.vue
Normal file
283
apps/web-naive/src/views/infra/codegen/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCodegenTable,
|
||||
deleteCodegenTableList,
|
||||
downloadCodegen,
|
||||
getCodegenTablePage,
|
||||
syncCodegenFromDB,
|
||||
} from '#/api/infra/codegen';
|
||||
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ImportTable from './modules/import-table.vue';
|
||||
import PreviewCode from './modules/preview-code.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const dataSourceConfigList = ref<InfraDataSourceConfigApi.DataSourceConfig[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
/** 获取数据源名称 */
|
||||
const getDataSourceConfigName = (dataSourceConfigId: number) => {
|
||||
return dataSourceConfigList.value.find(
|
||||
(item) => item.id === dataSourceConfigId,
|
||||
)?.name;
|
||||
};
|
||||
|
||||
const [ImportModal, importModalApi] = useVbenModal({
|
||||
connectedComponent: ImportTable,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [PreviewModal, previewModalApi] = useVbenModal({
|
||||
connectedComponent: PreviewCode,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导入表格 */
|
||||
function handleImport() {
|
||||
importModalApi.open();
|
||||
}
|
||||
|
||||
/** 预览代码 */
|
||||
function handlePreview(row: InfraCodegenApi.CodegenTable) {
|
||||
previewModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 编辑表格 */
|
||||
function handleEdit(row: InfraCodegenApi.CodegenTable) {
|
||||
router.push({ name: 'InfraCodegenEdit', query: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 删除代码生成配置 */
|
||||
async function handleDelete(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading('正在删除...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteCodegenTable(row.id);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.tableName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除代码生成配置 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading('正在删除...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteCodegenTableList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.CodegenTable[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 同步数据库 */
|
||||
async function handleSync(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.updating', [row.tableName]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await syncCodegenFromDB(row.id);
|
||||
message.success($t('ui.actionMessage.updateSuccess', [row.tableName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成代码 */
|
||||
async function handleGenerate(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading('正在生成代码...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
const res = await downloadCodegen(row.id);
|
||||
const blob = new Blob([res], { type: 'application/zip' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `codegen-${row.className}.zip`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
message.success('代码生成成功');
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(getDataSourceConfigName),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCodegenTablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.CodegenTable>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 获取数据源配置列表 */
|
||||
// TODO @芋艿:这种场景的最佳实践;
|
||||
async function initDataSourceConfig() {
|
||||
try {
|
||||
dataSourceConfigList.value = await getDataSourceConfigList();
|
||||
} catch (error) {
|
||||
console.error('获取数据源配置失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
initDataSourceConfig();
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="代码生成(单表)"
|
||||
url="https://doc.iocoder.cn/new-feature/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="代码生成(树表)"
|
||||
url="https://doc.iocoder.cn/new-feature/tree/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="代码生成(主子表)"
|
||||
url="https://doc.iocoder.cn/new-feature/master-sub/"
|
||||
/>
|
||||
<DocAlert title="单元测试" url="https://doc.iocoder.cn/unit-test/" />
|
||||
</template>
|
||||
|
||||
<ImportModal @success="handleRefresh" />
|
||||
<PreviewModal />
|
||||
<Grid table-title="代码生成列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.import'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:codegen:create'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:codegen:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '预览',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:codegen:preview'],
|
||||
onClick: handlePreview.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '生成代码',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:codegen:download'],
|
||||
onClick: handleGenerate.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:codegen:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '同步',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:codegen:update'],
|
||||
onClick: handleSync.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
auth: ['infra:codegen:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.tableName]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
153
apps/web-naive/src/views/infra/codegen/modules/column-info.vue
Normal file
153
apps/web-naive/src/views/infra/codegen/modules/column-info.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { NCheckbox, NInput, NSelect } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSimpleDictTypeList } from '#/api/system/dict/type';
|
||||
|
||||
import { useCodegenColumnTableColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
columns?: InfraCodegenApi.CodegenColumn[];
|
||||
}>();
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useCodegenColumnTableColumns(),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听外部传入的列数据 */
|
||||
watch(
|
||||
() => props.columns,
|
||||
async (columns) => {
|
||||
if (!columns) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
gridApi.grid?.loadData(columns);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): InfraCodegenApi.CodegenColumn[] => gridApi.grid.getData(),
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
const dictTypeOptions = ref<SelectOption[]>([]); // 字典类型选项
|
||||
|
||||
async function initDictTypeOptions() {
|
||||
const res = await getSimpleDictTypeList();
|
||||
dictTypeOptions.value = res.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
};
|
||||
});
|
||||
}
|
||||
onMounted(async () => {
|
||||
await initDictTypeOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<!-- 字段描述 -->
|
||||
<template #columnComment="{ row }">
|
||||
<NInput v-model:value="row.columnComment" />
|
||||
</template>
|
||||
|
||||
<!-- Java 类型 -->
|
||||
<template #javaType="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.javaType"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
<!-- Java 属性 -->
|
||||
<template #javaField="{ row }">
|
||||
<NInput v-model:value="row.javaField" />
|
||||
</template>
|
||||
|
||||
<!-- 插入 -->
|
||||
<template #createOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.createOperation" />
|
||||
</template>
|
||||
<!-- 编辑 -->
|
||||
<template #updateOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.updateOperation" />
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<template #listOperationResult="{ row }">
|
||||
<NCheckbox v-model:checked="row.listOperationResult" />
|
||||
</template>
|
||||
<!-- 查询 -->
|
||||
<template #listOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.listOperation" />
|
||||
</template>
|
||||
|
||||
<!-- 查询方式 -->
|
||||
<template #listOperationCondition="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.listOperationCondition"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 允许空 -->
|
||||
<template #nullable="{ row }">
|
||||
<NCheckbox v-model:checked="row.nullable" />
|
||||
</template>
|
||||
|
||||
<!-- 显示类型 -->
|
||||
<template #htmlType="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.htmlType"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 字典类型 -->
|
||||
<template #dictType="{ row }">
|
||||
<NSelect
|
||||
v-model:value="row.dictType"
|
||||
:options="dictTypeOptions"
|
||||
class="w-full"
|
||||
allow-clear
|
||||
show-search
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 示例 -->
|
||||
<template #example="{ row }">
|
||||
<NInput v-model:value="row.example" />
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { InfraCodegenTemplateTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getCodegenTableList } from '#/api/infra/codegen';
|
||||
|
||||
import {
|
||||
useGenerationInfoBaseFormSchema,
|
||||
useGenerationInfoSubTableFormSchema,
|
||||
useGenerationInfoTreeFormSchema,
|
||||
} from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
columns?: InfraCodegenApi.CodegenColumn[];
|
||||
table?: InfraCodegenApi.CodegenTable;
|
||||
}>();
|
||||
|
||||
const tables = ref<InfraCodegenApi.CodegenTable[]>([]);
|
||||
|
||||
/** 计算当前模板类型 */
|
||||
const currentTemplateType = ref<number>();
|
||||
const isTreeTable = computed(
|
||||
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.TREE,
|
||||
);
|
||||
const isSubTable = computed(
|
||||
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.SUB,
|
||||
);
|
||||
|
||||
/** 基础表单实例 */
|
||||
const [BaseForm, baseFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: useGenerationInfoBaseFormSchema(),
|
||||
handleValuesChange: (values) => {
|
||||
// 监听模板类型变化
|
||||
if (
|
||||
values.templateType !== undefined &&
|
||||
values.templateType !== currentTemplateType.value
|
||||
) {
|
||||
currentTemplateType.value = values.templateType;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 树表信息表单实例 */
|
||||
const [TreeForm, treeFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: [],
|
||||
});
|
||||
|
||||
/** 主子表信息表单实例 */
|
||||
const [SubForm, subFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: [],
|
||||
});
|
||||
|
||||
/** 更新树表信息表单 schema */
|
||||
function updateTreeSchema(): void {
|
||||
treeFormApi.setState({
|
||||
schema: useGenerationInfoTreeFormSchema(props.columns),
|
||||
});
|
||||
// 树表信息回显
|
||||
treeFormApi.setValues(props.table as any);
|
||||
}
|
||||
|
||||
/** 更新主子表信息表单 schema */
|
||||
function updateSubSchema(): void {
|
||||
subFormApi.setState({
|
||||
schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
|
||||
});
|
||||
// 主子表信息回显
|
||||
subFormApi.setValues(props.table as any);
|
||||
}
|
||||
|
||||
/** 获取合并的表单值 */
|
||||
async function getAllFormValues(): Promise<Record<string, any>> {
|
||||
// 基础表单值
|
||||
const baseValues = await baseFormApi.getValues();
|
||||
// 根据模板类型获取对应的额外表单值
|
||||
let extraValues = {};
|
||||
if (isTreeTable.value) {
|
||||
extraValues = await treeFormApi.getValues();
|
||||
} else if (isSubTable.value) {
|
||||
extraValues = await subFormApi.getValues();
|
||||
}
|
||||
// 合并表单值
|
||||
return { ...baseValues, ...extraValues };
|
||||
}
|
||||
|
||||
/** 验证所有表单 */
|
||||
async function validateAllForms() {
|
||||
// 验证基础表单
|
||||
const { valid: baseFormValid } = await baseFormApi.validate();
|
||||
// 根据模板类型验证对应的额外表单
|
||||
let extraValid = true;
|
||||
if (isTreeTable.value) {
|
||||
const { valid: treeFormValid } = await treeFormApi.validate();
|
||||
extraValid = treeFormValid;
|
||||
} else if (isSubTable.value) {
|
||||
const { valid: subFormValid } = await subFormApi.validate();
|
||||
extraValid = subFormValid;
|
||||
}
|
||||
return baseFormValid && extraValid;
|
||||
}
|
||||
|
||||
/** 设置表单值 */
|
||||
function setAllFormValues(values: Record<string, any>): void {
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录模板类型
|
||||
currentTemplateType.value = values.templateType;
|
||||
|
||||
// 设置基础表单值
|
||||
baseFormApi.setValues(values);
|
||||
// 根据模板类型设置对应的额外表单值
|
||||
if (isTreeTable.value) {
|
||||
treeFormApi.setValues(values);
|
||||
} else if (isSubTable.value) {
|
||||
subFormApi.setValues(values);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听表格数据变化 */
|
||||
watch(
|
||||
() => props.table,
|
||||
async (val) => {
|
||||
if (!val || isEmpty(val)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = val as InfraCodegenApi.CodegenTable;
|
||||
// 初始化树表的 schema
|
||||
updateTreeSchema();
|
||||
// 设置表单值
|
||||
setAllFormValues(table);
|
||||
// 获取表数据,用于主子表选择
|
||||
const dataSourceConfigId = table.dataSourceConfigId;
|
||||
if (dataSourceConfigId === undefined) {
|
||||
return;
|
||||
}
|
||||
tables.value = await getCodegenTableList(dataSourceConfigId);
|
||||
// 初始化子表 schema
|
||||
updateSubSchema();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: validateAllForms,
|
||||
getValues: getAllFormValues,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 基础表单 -->
|
||||
<BaseForm />
|
||||
<!-- 树表信息表单 -->
|
||||
<TreeForm v-if="isTreeTable" />
|
||||
<!-- 主子表信息表单 -->
|
||||
<SubForm v-if="isSubTable" />
|
||||
</div>
|
||||
</template>
|
||||
118
apps/web-naive/src/views/infra/codegen/modules/import-table.vue
Normal file
118
apps/web-naive/src/views/infra/codegen/modules/import-table.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
useImportTableColumns,
|
||||
useImportTableFormSchema,
|
||||
} from '#/views/infra/codegen/data';
|
||||
|
||||
/** 定义组件事件 */
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
dataSourceConfigId: 0,
|
||||
tableNames: [], // 已选择的表列表
|
||||
});
|
||||
|
||||
/** 表格实例 */
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useImportTableFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useImportTableColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (formValues.dataSourceConfigId === undefined) {
|
||||
return [];
|
||||
}
|
||||
formData.dataSourceConfigId = formValues.dataSourceConfigId;
|
||||
return await getSchemaTableList({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'name',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
range: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.DatabaseTable>,
|
||||
gridEvents: {
|
||||
checkboxChange: ({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.DatabaseTable[];
|
||||
}) => {
|
||||
formData.tableNames = records.map((item) => item.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 模态框实例 */
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '导入表',
|
||||
class: 'w-1/2',
|
||||
async onConfirm() {
|
||||
modalApi.lock();
|
||||
// 1.1 获取表单值
|
||||
if (formData?.dataSourceConfigId === undefined) {
|
||||
message.error('请选择数据源');
|
||||
return;
|
||||
}
|
||||
// 1.2 校验是否选择了表
|
||||
if (formData.tableNames.length === 0) {
|
||||
message.error('请选择需要导入的表');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 提交请求
|
||||
const hideLoading = message.loading('导入中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await createCodegenList(formData);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal>
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
268
apps/web-naive/src/views/infra/codegen/modules/preview-code.vue
Normal file
268
apps/web-naive/src/views/infra/codegen/modules/preview-code.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
|
||||
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { CodeEditor } from '@vben/plugins/code-editor';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { NButton, NTabPane, NTabs, NTree } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { previewCodegen } from '#/api/infra/codegen';
|
||||
|
||||
/** 文件树类型 */
|
||||
interface FileNode {
|
||||
key: string;
|
||||
title: string;
|
||||
parentKey: string;
|
||||
isLeaf?: boolean;
|
||||
children?: FileNode[];
|
||||
}
|
||||
|
||||
/** 组件状态 */
|
||||
const loading = ref(false);
|
||||
const fileTree = ref<FileNode[]>([]);
|
||||
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
|
||||
const activeKey = ref<string>('');
|
||||
|
||||
/** 代码地图 */
|
||||
const codeMap = ref<Map<string, string>>(new Map<string, string>());
|
||||
function setCodeMap(key: string, code: string) {
|
||||
// 处理可能的缩进问题,特别是对Java文件
|
||||
const trimmedCode = code.trimStart();
|
||||
// 如果已有缓存则不重新构建
|
||||
if (codeMap.value.has(key)) {
|
||||
return;
|
||||
}
|
||||
codeMap.value.set(key, trimmedCode);
|
||||
}
|
||||
|
||||
/** 删除代码地图 */
|
||||
function removeCodeMapKey(targetKey: any) {
|
||||
// 只有一个代码视图时不允许删除
|
||||
if (codeMap.value.size === 1) {
|
||||
return;
|
||||
}
|
||||
if (codeMap.value.has(targetKey)) {
|
||||
codeMap.value.delete(targetKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制代码 */
|
||||
async function copyCode() {
|
||||
const { copy } = useClipboard();
|
||||
const file = previewFiles.value.find(
|
||||
(item) => item.filePath === activeKey.value,
|
||||
);
|
||||
if (file) {
|
||||
await copy(file.code);
|
||||
message.success('复制成功');
|
||||
}
|
||||
}
|
||||
|
||||
/** 文件节点点击事件 */
|
||||
function handleNodeClick({
|
||||
option,
|
||||
}: {
|
||||
option: TreeOption;
|
||||
}): TreeOverrideNodeClickBehaviorReturn {
|
||||
if (!option.isLeaf) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
activeKey.value = option.key?.toString() || '';
|
||||
const file = previewFiles.value.find((item) => {
|
||||
const list = activeKey.value.split('.');
|
||||
// 特殊处理 - 包合并
|
||||
if (list.length > 2) {
|
||||
const lang = list.pop();
|
||||
return item.filePath === `${list.join('/')}.${lang}`;
|
||||
}
|
||||
return item.filePath === activeKey.value;
|
||||
});
|
||||
if (!file) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
setCodeMap(activeKey.value, file.code);
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/** 处理文件树 */
|
||||
function handleFiles(data: InfraCodegenApi.CodegenPreview[]): FileNode[] {
|
||||
const exists: Record<string, boolean> = {};
|
||||
const files: FileNode[] = [];
|
||||
|
||||
// 处理文件路径
|
||||
for (const item of data) {
|
||||
const paths = item.filePath.split('/');
|
||||
let cursor = 0;
|
||||
let fullPath = '';
|
||||
|
||||
while (cursor < paths.length) {
|
||||
const path = paths[cursor] || '';
|
||||
const oldFullPath = fullPath;
|
||||
|
||||
// 处理 Java 包路径特殊情况
|
||||
if (path === 'java' && cursor + 1 < paths.length) {
|
||||
fullPath = fullPath ? `${fullPath}/${path}` : path;
|
||||
cursor++;
|
||||
|
||||
// 合并包路径
|
||||
let packagePath = '';
|
||||
while (cursor < paths.length) {
|
||||
const nextPath = paths[cursor] || '';
|
||||
if (
|
||||
[
|
||||
'controller',
|
||||
'convert',
|
||||
'dal',
|
||||
'dataobject',
|
||||
'enums',
|
||||
'mysql',
|
||||
'service',
|
||||
'vo',
|
||||
].includes(nextPath)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
packagePath = packagePath ? `${packagePath}.${nextPath}` : nextPath;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (packagePath) {
|
||||
const newFullPath = `${fullPath}/${packagePath}`;
|
||||
if (!exists[newFullPath]) {
|
||||
exists[newFullPath] = true;
|
||||
files.push({
|
||||
key: newFullPath,
|
||||
title: packagePath,
|
||||
parentKey: oldFullPath || '/',
|
||||
isLeaf: cursor === paths.length,
|
||||
});
|
||||
}
|
||||
fullPath = newFullPath;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理普通路径
|
||||
fullPath = fullPath ? `${fullPath}/${path}` : path;
|
||||
if (!exists[fullPath]) {
|
||||
exists[fullPath] = true;
|
||||
files.push({
|
||||
key: fullPath,
|
||||
title: path,
|
||||
parentKey: oldFullPath || '/',
|
||||
isLeaf: cursor === paths.length - 1,
|
||||
});
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建树形结构 */
|
||||
function buildTree(parentKey: string): FileNode[] {
|
||||
return files
|
||||
.filter((file) => file.parentKey === parentKey)
|
||||
.map((file) => ({
|
||||
...file,
|
||||
children: buildTree(file.key),
|
||||
}));
|
||||
}
|
||||
|
||||
return buildTree('/');
|
||||
}
|
||||
|
||||
/** 模态框实例 */
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
footer: false,
|
||||
fullscreen: true,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
// 关闭时清除代码视图缓存
|
||||
codeMap.value.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = modalApi.getData<InfraCodegenApi.CodegenTable>();
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载预览数据
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await previewCodegen(row.id);
|
||||
previewFiles.value = data;
|
||||
|
||||
// 构建代码树,并默认选中第一个文件
|
||||
fileTree.value = handleFiles(data);
|
||||
if (data.length > 0) {
|
||||
activeKey.value = data[0]?.filePath || '';
|
||||
const code = data[0]?.code || '';
|
||||
setCodeMap(activeKey.value, code);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="代码预览">
|
||||
<div class="flex h-full" v-loading="loading">
|
||||
<!-- 文件树 -->
|
||||
<div
|
||||
class="h-full w-1/3 overflow-auto border-r border-gray-200 pr-4 dark:border-gray-700"
|
||||
>
|
||||
<NTree
|
||||
v-if="fileTree.length > 0"
|
||||
:data="fileTree"
|
||||
default-expand-all
|
||||
key-field="key"
|
||||
label-field="title"
|
||||
children-field="children"
|
||||
:checked-keys="[activeKey]"
|
||||
:override-default-node-click-behavior="handleNodeClick"
|
||||
/>
|
||||
</div>
|
||||
<!-- 代码预览 -->
|
||||
<div class="h-full w-2/3 overflow-auto pl-4">
|
||||
<NTabs v-model:value="activeKey" type="card" @close="removeCodeMapKey">
|
||||
<NTabPane
|
||||
v-for="key in codeMap.keys()"
|
||||
:name="key"
|
||||
:key="key"
|
||||
:tab="key.split('/').pop()"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-md bg-gray-50 !p-0 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<CodeEditor
|
||||
class="max-h-200"
|
||||
:value="codeMap.get(activeKey)"
|
||||
mode="application/json"
|
||||
:readonly="true"
|
||||
:bordered="true"
|
||||
:auto-format="false"
|
||||
/>
|
||||
</div>
|
||||
</NTabPane>
|
||||
<template #suffix>
|
||||
<NButton type="primary" ghost @click="copyCode">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
复制代码
|
||||
</NButton>
|
||||
</template>
|
||||
</NTabs>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
186
apps/web-naive/src/views/infra/config/data.ts
Normal file
186
apps/web-naive/src/views/infra/config/data.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '参数分类',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数分类',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '参数名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'key',
|
||||
label: '参数键名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
label: '参数键值',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键值',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'visible',
|
||||
label: '是否可见',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
defaultValue: true,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '参数名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'key',
|
||||
label: '参数键名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键名',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '系统内置',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE, 'number'),
|
||||
placeholder: '请选择系统内置',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '参数主键',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
title: '参数分类',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '参数名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'key',
|
||||
title: '参数键名',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '参数键值',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'visible',
|
||||
title: '是否可见',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '系统内置',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_CONFIG_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
183
apps/web-naive/src/views/infra/config/index.vue
Normal file
183
apps/web-naive/src/views/infra/config/index.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteConfig,
|
||||
deleteConfigList,
|
||||
exportConfig,
|
||||
getConfigPage,
|
||||
} from '#/api/infra/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportConfig(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '参数配置.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建参数 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑参数 */
|
||||
function handleEdit(row: InfraConfigApi.Config) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除参数 */
|
||||
async function handleDelete(row: InfraConfigApi.Config) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteConfig(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除参数 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteConfigList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraConfigApi.Config[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraConfigApi.Config>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="参数列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['参数']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:config:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:config:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
81
apps/web-naive/src/views/infra/config/modules/form.vue
Normal file
81
apps/web-naive/src/views/infra/config/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createConfig, getConfig, updateConfig } from '#/api/infra/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraConfigApi.Config>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['参数'])
|
||||
: $t('ui.actionTitle.create', ['参数']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as InfraConfigApi.Config;
|
||||
try {
|
||||
await (formData.value?.id ? updateConfig(data) : createConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraConfigApi.Config>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getConfig(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
92
apps/web-naive/src/views/infra/dataSourceConfig/data.ts
Normal file
92
apps/web-naive/src/views/infra/dataSourceConfig/data.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '数据源名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据源名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'url',
|
||||
label: '数据源连接',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据源连接',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
type: 'password',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '主键编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '数据源名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
title: '数据源连接',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: '用户名',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
159
apps/web-naive/src/views/infra/dataSourceConfig/index.vue
Normal file
159
apps/web-naive/src/views/infra/dataSourceConfig/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDataSourceConfig,
|
||||
deleteDataSourceConfigList,
|
||||
getDataSourceConfigList,
|
||||
} from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建数据源 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑数据源 */
|
||||
function handleEdit(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除数据源 */
|
||||
async function handleDelete(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDataSourceConfig(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除数据源 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDataSourceConfigList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: getDataSourceConfigList,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<InfraDataSourceConfigApi.DataSourceConfig>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="数据源列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['数据源']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:data-source-config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:data-source-config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: 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>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDataSourceConfig,
|
||||
getDataSourceConfig,
|
||||
updateDataSourceConfig,
|
||||
} from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraDataSourceConfigApi.DataSourceConfig>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['数据源'])
|
||||
: $t('ui.actionTitle.create', ['数据源']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as InfraDataSourceConfigApi.DataSourceConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDataSourceConfig(data)
|
||||
: createDataSourceConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraDataSourceConfigApi.DataSourceConfig>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDataSourceConfig(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
149
apps/web-naive/src/views/infra/demo/demo01/data.ts
Normal file
149
apps/web-naive/src/views/infra/demo/demo01/data.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'InputNumber',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
rules: 'required',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'birthday',
|
||||
label: '出生年',
|
||||
rules: 'required',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
type: 'datetime',
|
||||
valueFormat: 'YYYY-MM-dd HH:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
rules: 'required',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
{
|
||||
fieldName: 'avatar',
|
||||
label: '头像',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo01ContactApi.Demo01Contact>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'birthday',
|
||||
title: '出生年',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '简介',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
185
apps/web-naive/src/views/infra/demo/demo01/index.vue
Normal file
185
apps/web-naive/src/views/infra/demo/demo01/index.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo01Contact,
|
||||
deleteDemo01ContactList,
|
||||
exportDemo01Contact,
|
||||
getDemo01ContactPage,
|
||||
} from '#/api/infra/demo/demo01';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo01Contact(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '示例联系人.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建示例联系人 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑示例联系人 */
|
||||
function handleEdit(row: Demo01ContactApi.Demo01Contact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除示例联系人 */
|
||||
async function handleDelete(row: Demo01ContactApi.Demo01Contact) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo01Contact(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除示例联系人 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo01ContactList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo01ContactApi.Demo01Contact[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDemo01ContactPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo01ContactApi.Demo01Contact>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="示例联系人列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['示例联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo01-contact:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo01-contact:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:demo01-contact:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo01-contact:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo01-contact:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
87
apps/web-naive/src/views/infra/demo/demo01/modules/form.vue
Normal file
87
apps/web-naive/src/views/infra/demo/demo01/modules/form.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo01Contact,
|
||||
getDemo01Contact,
|
||||
updateDemo01Contact,
|
||||
} from '#/api/infra/demo/demo01';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo01ContactApi.Demo01Contact>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['示例联系人'])
|
||||
: $t('ui.actionTitle.create', ['示例联系人']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo01ContactApi.Demo01Contact;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo01Contact(data)
|
||||
: createDemo01Contact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<Demo01ContactApi.Demo01Contact>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDemo01Contact(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
120
apps/web-naive/src/views/infra/demo/demo02/data.ts
Normal file
120
apps/web-naive/src/views/infra/demo/demo02/data.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getDemo02CategoryList } from '#/api/infra/demo/demo02';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'InputNumber',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级示例分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
api: async () => {
|
||||
const data = await getDemo02CategoryList({});
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级示例分类',
|
||||
});
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级示例分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '父级编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入父级编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo02CategoryApi.Demo02Category>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'parentId',
|
||||
title: '父级编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
175
apps/web-naive/src/views/infra/demo/demo02/index.vue
Normal file
175
apps/web-naive/src/views/infra/demo/demo02/index.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo02Category,
|
||||
exportDemo02Category,
|
||||
getDemo02CategoryList,
|
||||
} from '#/api/infra/demo/demo02';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo02Category(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '示例分类.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建示例分类 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑示例分类 */
|
||||
function handleEdit(row: Demo02CategoryApi.Demo02Category) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 添加下级示例分类 */
|
||||
function handleAppend(row: Demo02CategoryApi.Demo02Category) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除示例分类 */
|
||||
async function handleDelete(row: Demo02CategoryApi.Demo02Category) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo02Category(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
function handleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
return await getDemo02CategoryList(formValues);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo02CategoryApi.Demo02Category>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="示例分类列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['示例分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo02-category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: isExpanded ? '收缩' : '展开',
|
||||
type: 'primary',
|
||||
onClick: handleExpand,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo02-category:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增下级',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo02-category:create'],
|
||||
onClick: handleAppend.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo02-category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo02-category:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
90
apps/web-naive/src/views/infra/demo/demo02/modules/form.vue
Normal file
90
apps/web-naive/src/views/infra/demo/demo02/modules/form.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo02Category,
|
||||
getDemo02Category,
|
||||
updateDemo02Category,
|
||||
} from '#/api/infra/demo/demo02';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo02CategoryApi.Demo02Category>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['示例分类'])
|
||||
: $t('ui.actionTitle.create', ['示例分类']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as Demo02CategoryApi.Demo02Category;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo02Category(data)
|
||||
: createDemo02Category(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<Demo02CategoryApi.Demo02Category>();
|
||||
if (!data || !data.id) {
|
||||
// 设置上级
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDemo02Category(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user