Initial commit: OneOS frontend based on yudao-ui-admin-vben
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
229
apps/web-tdesign/src/adapter/component/index.ts
Normal file
229
apps/web-tdesign/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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 { notification } from '#/adapter/tdesign';
|
||||
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('tdesign-vue-next/es/button'));
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/checkbox'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('tdesign-vue-next/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/date-picker'),
|
||||
);
|
||||
const Divider = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/divider'),
|
||||
);
|
||||
const Input = defineAsyncComponent(() => import('tdesign-vue-next/es/input'));
|
||||
const InputNumber = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/input-number'),
|
||||
);
|
||||
// const InputPassword = defineAsyncComponent(() =>
|
||||
// import('tdesign-vue-next/es/input').then((res) => res.InputPassword),
|
||||
// );
|
||||
// const Mentions = defineAsyncComponent(
|
||||
// () => import('tdesign-vue-next/es/mentions'),
|
||||
// );
|
||||
const Radio = defineAsyncComponent(() => import('tdesign-vue-next/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('tdesign-vue-next/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('tdesign-vue-next/es/date-picker').then((res) => res.DateRangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('tdesign-vue-next/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('tdesign-vue-next/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('tdesign-vue-next/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('tdesign-vue-next/es/switch'));
|
||||
const Textarea = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/textarea'),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/time-picker'),
|
||||
);
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('tdesign-vue-next/es/tree-select'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('tdesign-vue-next/es/upload'));
|
||||
|
||||
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'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
// | 'InputPassword'
|
||||
// | 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| '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: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, theme: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
// InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
// Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
let ghost = false;
|
||||
let variant = props.variant;
|
||||
if (props.variant === 'ghost') {
|
||||
ghost = true;
|
||||
variant = 'base';
|
||||
}
|
||||
return h(
|
||||
Button,
|
||||
{ ...props, ghost, variant, attrs, theme: 'primary' },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
RangePicker,
|
||||
{ ...props, modelValue: props.modelValue ?? [], attrs },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
title,
|
||||
content,
|
||||
placement: 'bottom-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
49
apps/web-tdesign/src/adapter/form.ts
Normal file
49
apps/web-tdesign/src/adapter/form.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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';
|
||||
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// tdesign组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
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;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
5
apps/web-tdesign/src/adapter/tdesign.ts
Normal file
5
apps/web-tdesign/src/adapter/tdesign.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
DialogPlugin as dialog,
|
||||
MessagePlugin as message,
|
||||
NotifyPlugin as notification,
|
||||
} from 'tdesign-vue-next';
|
||||
229
apps/web-tdesign/src/adapter/vxe-table.ts
Normal file
229
apps/web-tdesign/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import {
|
||||
AsyncVxeColumn,
|
||||
AsyncVxeTable,
|
||||
createRequiredValidation,
|
||||
setupVbenVxeTable,
|
||||
useVbenVxeGrid,
|
||||
} from '@vben/plugins/vxe-table';
|
||||
import {
|
||||
erpCountInputFormatter,
|
||||
erpNumberFormatter,
|
||||
fenToYuan,
|
||||
formatFileSize,
|
||||
formatPast2,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { Button, Image, ImageViewer, Switch, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
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 { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellImages', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
if (column && column.field && row[column.field]) {
|
||||
return h(ImageViewer, {}, () => {
|
||||
return row[column.field].map((item: any) =>
|
||||
h(Image, { src: item }),
|
||||
);
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', variant: 'text' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellTag' },
|
||||
vxeUI.renderer.add('CellTag', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Tag, { color: props?.color }, () => row[column.field]);
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellTags', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
if (!row[column.field] || row[column.field].length === 0) {
|
||||
return '';
|
||||
}
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'flex items-center justify-center' },
|
||||
{
|
||||
default: () =>
|
||||
row[column.field].map((item: any) =>
|
||||
h(Tag, { color: props?.color }, { default: () => item }),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} },
|
||||
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 = {
|
||||
checkedChildren: $t('common.enabled'),
|
||||
checkedValue: 1,
|
||||
unCheckedChildren: $t('common.disabled'),
|
||||
unCheckedValue: 0,
|
||||
...props,
|
||||
checked: row[column.field],
|
||||
loading: row[loadingKey] ?? false,
|
||||
'onUpdate:checked': 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(Switch, finallyProps);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
|
||||
vxeUI.formats.add('formatPast2', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
return formatPast2(cellValue);
|
||||
},
|
||||
});
|
||||
|
||||
// add by 星语:数量格式化,保留 3 位
|
||||
vxeUI.formats.add('formatAmount3', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
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)}`;
|
||||
},
|
||||
});
|
||||
|
||||
// add by 星语:文件大小格式化
|
||||
vxeUI.formats.add('formatFileSize', {
|
||||
tableCellFormatMethod({ cellValue }, digits = 2) {
|
||||
return formatFileSize(cellValue, digits);
|
||||
},
|
||||
});
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { createRequiredValidation, useVbenVxeGrid };
|
||||
|
||||
export const [VxeTable, VxeColumn] = [AsyncVxeTable, AsyncVxeColumn];
|
||||
|
||||
export * from '#/components/table-action';
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
161
apps/web-tdesign/src/api/core/auth.ts
Normal file
161
apps/web-tdesign/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,
|
||||
);
|
||||
}
|
||||
1
apps/web-tdesign/src/api/core/index.ts
Normal file
1
apps/web-tdesign/src/api/core/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
1
apps/web-tdesign/src/api/index.ts
Normal file
1
apps/web-tdesign/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
44
apps/web-tdesign/src/api/infra/api-access-log/index.ts
Normal file
44
apps/web-tdesign/src/api/infra/api-access-log/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraApiAccessLogApi {
|
||||
/** API 访问日志信息 */
|
||||
export interface ApiAccessLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
requestParams: string;
|
||||
responseBody: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
operateModule: string;
|
||||
operateName: string;
|
||||
operateType: number;
|
||||
beginTime: string;
|
||||
endTime: string;
|
||||
duration: number;
|
||||
resultCode: number;
|
||||
resultMsg: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 API 访问日志列表 */
|
||||
export function getApiAccessLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraApiAccessLogApi.ApiAccessLog>>(
|
||||
'/infra/api-access-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出 API 访问日志 */
|
||||
export function exportApiAccessLog(params: any) {
|
||||
return requestClient.download('/infra/api-access-log/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
55
apps/web-tdesign/src/api/infra/api-error-log/index.ts
Normal file
55
apps/web-tdesign/src/api/infra/api-error-log/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraApiErrorLogApi {
|
||||
/** API 错误日志信息 */
|
||||
export interface ApiErrorLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
requestParams: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
exceptionTime: string;
|
||||
exceptionName: string;
|
||||
exceptionMessage: string;
|
||||
exceptionRootCauseMessage: string;
|
||||
exceptionStackTrace: string;
|
||||
exceptionClassName: string;
|
||||
exceptionFileName: string;
|
||||
exceptionMethodName: string;
|
||||
exceptionLineNumber: number;
|
||||
processUserId: number;
|
||||
processStatus: number;
|
||||
processTime: string;
|
||||
resultCode: number;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 API 错误日志列表 */
|
||||
export function getApiErrorLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraApiErrorLogApi.ApiErrorLog>>(
|
||||
'/infra/api-error-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 更新 API 错误日志的处理状态 */
|
||||
export function updateApiErrorLogStatus(id: number, processStatus: number) {
|
||||
return requestClient.put(
|
||||
`/infra/api-error-log/update-status?id=${id}&processStatus=${processStatus}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出 API 错误日志 */
|
||||
export function exportApiErrorLog(params: any) {
|
||||
return requestClient.download('/infra/api-error-log/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
168
apps/web-tdesign/src/api/infra/codegen/index.ts
Normal file
168
apps/web-tdesign/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-tdesign/src/api/infra/config/index.ts
Normal file
67
apps/web-tdesign/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-tdesign/src/api/infra/data-source-config/index.ts
Normal file
53
apps/web-tdesign/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-tdesign/src/api/infra/demo/demo01/index.ts
Normal file
61
apps/web-tdesign/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-tdesign/src/api/infra/demo/demo02/index.ts
Normal file
48
apps/web-tdesign/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-tdesign/src/api/infra/demo/demo03/erp/index.ts
Normal file
168
apps/web-tdesign/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-tdesign/src/api/infra/demo/demo03/inner/index.ts
Normal file
96
apps/web-tdesign/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-tdesign/src/api/infra/demo/demo03/normal/index.ts
Normal file
96
apps/web-tdesign/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}`,
|
||||
);
|
||||
}
|
||||
84
apps/web-tdesign/src/api/infra/file-config/index.ts
Normal file
84
apps/web-tdesign/src/api/infra/file-config/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
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;
|
||||
region?: string;
|
||||
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-tdesign/src/api/infra/file/index.ts
Normal file
78
apps/web-tdesign/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 });
|
||||
}
|
||||
41
apps/web-tdesign/src/api/infra/job-log/index.ts
Normal file
41
apps/web-tdesign/src/api/infra/job-log/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraJobLogApi {
|
||||
/** 任务日志信息 */
|
||||
export interface JobLog {
|
||||
id?: number;
|
||||
jobId: number;
|
||||
handlerName: string;
|
||||
handlerParam: string;
|
||||
cronExpression: string;
|
||||
executeIndex: string;
|
||||
beginTime: Date;
|
||||
endTime: Date;
|
||||
duration: string;
|
||||
status: number;
|
||||
createTime?: string;
|
||||
result: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询任务日志列表 */
|
||||
export function getJobLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraJobLogApi.JobLog>>(
|
||||
'/infra/job-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询任务日志详情 */
|
||||
export function getJobLog(id: number) {
|
||||
return requestClient.get<InfraJobLogApi.JobLog>(
|
||||
`/infra/job-log/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出定时任务日志 */
|
||||
export function exportJobLog(params: any) {
|
||||
return requestClient.download('/infra/job-log/export-excel', { params });
|
||||
}
|
||||
77
apps/web-tdesign/src/api/infra/job/index.ts
Normal file
77
apps/web-tdesign/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}`);
|
||||
}
|
||||
190
apps/web-tdesign/src/api/infra/redis/index.ts
Normal file
190
apps/web-tdesign/src/api/infra/redis/index.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraRedisApi {
|
||||
/** Redis 信息 */
|
||||
export interface RedisInfo {
|
||||
io_threaded_reads_processed: string;
|
||||
tracking_clients: string;
|
||||
uptime_in_seconds: string;
|
||||
cluster_connections: string;
|
||||
current_cow_size: string;
|
||||
maxmemory_human: string;
|
||||
aof_last_cow_size: string;
|
||||
master_replid2: string;
|
||||
mem_replication_backlog: string;
|
||||
aof_rewrite_scheduled: string;
|
||||
total_net_input_bytes: string;
|
||||
rss_overhead_ratio: string;
|
||||
hz: string;
|
||||
current_cow_size_age: string;
|
||||
redis_build_id: string;
|
||||
errorstat_BUSYGROUP: string;
|
||||
aof_last_bgrewrite_status: string;
|
||||
multiplexing_api: string;
|
||||
client_recent_max_output_buffer: string;
|
||||
allocator_resident: string;
|
||||
mem_fragmentation_bytes: string;
|
||||
aof_current_size: string;
|
||||
repl_backlog_first_byte_offset: string;
|
||||
tracking_total_prefixes: string;
|
||||
redis_mode: string;
|
||||
redis_git_dirty: string;
|
||||
aof_delayed_fsync: string;
|
||||
allocator_rss_bytes: string;
|
||||
repl_backlog_histlen: string;
|
||||
io_threads_active: string;
|
||||
rss_overhead_bytes: string;
|
||||
total_system_memory: string;
|
||||
loading: string;
|
||||
evicted_keys: string;
|
||||
maxclients: string;
|
||||
cluster_enabled: string;
|
||||
redis_version: string;
|
||||
repl_backlog_active: string;
|
||||
mem_aof_buffer: string;
|
||||
allocator_frag_bytes: string;
|
||||
io_threaded_writes_processed: string;
|
||||
instantaneous_ops_per_sec: string;
|
||||
used_memory_human: string;
|
||||
total_error_replies: string;
|
||||
role: string;
|
||||
maxmemory: string;
|
||||
used_memory_lua: string;
|
||||
rdb_current_bgsave_time_sec: string;
|
||||
used_memory_startup: string;
|
||||
used_cpu_sys_main_thread: string;
|
||||
lazyfree_pending_objects: string;
|
||||
aof_pending_bio_fsync: string;
|
||||
used_memory_dataset_perc: string;
|
||||
allocator_frag_ratio: string;
|
||||
arch_bits: string;
|
||||
used_cpu_user_main_thread: string;
|
||||
mem_clients_normal: string;
|
||||
expired_time_cap_reached_count: string;
|
||||
unexpected_error_replies: string;
|
||||
mem_fragmentation_ratio: string;
|
||||
aof_last_rewrite_time_sec: string;
|
||||
master_replid: string;
|
||||
aof_rewrite_in_progress: string;
|
||||
lru_clock: string;
|
||||
maxmemory_policy: string;
|
||||
run_id: string;
|
||||
latest_fork_usec: string;
|
||||
tracking_total_items: string;
|
||||
total_commands_processed: string;
|
||||
expired_keys: string;
|
||||
errorstat_ERR: string;
|
||||
used_memory: string;
|
||||
module_fork_in_progress: string;
|
||||
errorstat_WRONGPASS: string;
|
||||
aof_buffer_length: string;
|
||||
dump_payload_sanitizations: string;
|
||||
mem_clients_slaves: string;
|
||||
keyspace_misses: string;
|
||||
server_time_usec: string;
|
||||
executable: string;
|
||||
lazyfreed_objects: string;
|
||||
db0: string;
|
||||
used_memory_peak_human: string;
|
||||
keyspace_hits: string;
|
||||
rdb_last_cow_size: string;
|
||||
aof_pending_rewrite: string;
|
||||
used_memory_overhead: string;
|
||||
active_defrag_hits: string;
|
||||
tcp_port: string;
|
||||
uptime_in_days: string;
|
||||
used_memory_peak_perc: string;
|
||||
current_save_keys_processed: string;
|
||||
blocked_clients: string;
|
||||
total_reads_processed: string;
|
||||
expire_cycle_cpu_milliseconds: string;
|
||||
sync_partial_err: string;
|
||||
used_memory_scripts_human: string;
|
||||
aof_current_rewrite_time_sec: string;
|
||||
aof_enabled: string;
|
||||
process_supervised: string;
|
||||
master_repl_offset: string;
|
||||
used_memory_dataset: string;
|
||||
used_cpu_user: string;
|
||||
rdb_last_bgsave_status: string;
|
||||
tracking_total_keys: string;
|
||||
atomicvar_api: string;
|
||||
allocator_rss_ratio: string;
|
||||
client_recent_max_input_buffer: string;
|
||||
clients_in_timeout_table: string;
|
||||
aof_last_write_status: string;
|
||||
mem_allocator: string;
|
||||
used_memory_scripts: string;
|
||||
used_memory_peak: string;
|
||||
process_id: string;
|
||||
master_failover_state: string;
|
||||
errorstat_NOAUTH: string;
|
||||
used_cpu_sys: string;
|
||||
repl_backlog_size: string;
|
||||
connected_slaves: string;
|
||||
current_save_keys_total: string;
|
||||
gcc_version: string;
|
||||
total_system_memory_human: string;
|
||||
sync_full: string;
|
||||
connected_clients: string;
|
||||
module_fork_last_cow_size: string;
|
||||
total_writes_processed: string;
|
||||
allocator_active: string;
|
||||
total_net_output_bytes: string;
|
||||
pubsub_channels: string;
|
||||
current_fork_perc: string;
|
||||
active_defrag_key_hits: string;
|
||||
rdb_changes_since_last_save: string;
|
||||
instantaneous_input_kbps: string;
|
||||
used_memory_rss_human: string;
|
||||
configured_hz: string;
|
||||
expired_stale_perc: string;
|
||||
active_defrag_misses: string;
|
||||
used_cpu_sys_children: string;
|
||||
number_of_cached_scripts: string;
|
||||
sync_partial_ok: string;
|
||||
used_memory_lua_human: string;
|
||||
rdb_last_save_time: string;
|
||||
pubsub_patterns: string;
|
||||
slave_expires_tracked_keys: string;
|
||||
redis_git_sha1: string;
|
||||
used_memory_rss: string;
|
||||
rdb_last_bgsave_time_sec: string;
|
||||
os: string;
|
||||
mem_not_counted_for_evict: string;
|
||||
active_defrag_running: string;
|
||||
rejected_connections: string;
|
||||
aof_rewrite_buffer_length: string;
|
||||
total_forks: string;
|
||||
active_defrag_key_misses: string;
|
||||
allocator_allocated: string;
|
||||
aof_base_size: string;
|
||||
instantaneous_output_kbps: string;
|
||||
second_repl_offset: string;
|
||||
rdb_bgsave_in_progress: string;
|
||||
used_cpu_user_children: string;
|
||||
total_connections_received: string;
|
||||
migrate_cached_sockets: string;
|
||||
}
|
||||
|
||||
/** Redis 命令统计 */
|
||||
export interface RedisCommandStats {
|
||||
command: string;
|
||||
calls: number;
|
||||
usec: number;
|
||||
}
|
||||
|
||||
/** Redis 监控信息 */
|
||||
export interface RedisMonitorInfo {
|
||||
info: RedisInfo;
|
||||
dbSize: number;
|
||||
commandStats: RedisCommandStats[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取 Redis 监控信息 */
|
||||
export function getRedisMonitorInfo() {
|
||||
return requestClient.get<InfraRedisApi.RedisMonitorInfo>(
|
||||
'/infra/redis/get-monitor-info',
|
||||
);
|
||||
}
|
||||
186
apps/web-tdesign/src/api/request.ts
Normal file
186
apps/web-tdesign/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/tdesign';
|
||||
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;
|
||||
},
|
||||
});
|
||||
24
apps/web-tdesign/src/api/system/area/index.ts
Normal file
24
apps/web-tdesign/src/api/system/area/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemAreaApi {
|
||||
/** 地区信息 */
|
||||
export interface Area {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
parentId?: number;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得地区树 */
|
||||
export function getAreaTree() {
|
||||
return requestClient.get<SystemAreaApi.Area[]>('/system/area/tree');
|
||||
}
|
||||
|
||||
/** 获得 IP 对应的地区名 */
|
||||
export function getAreaByIp(ip: string) {
|
||||
return requestClient.get<string>(`/system/area/get-by-ip?ip=${ip}`);
|
||||
}
|
||||
52
apps/web-tdesign/src/api/system/dept/index.ts
Normal file
52
apps/web-tdesign/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-tdesign/src/api/system/dict/data/index.ts
Normal file
68
apps/web-tdesign/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-tdesign/src/api/system/dict/type/index.ts
Normal file
64
apps/web-tdesign/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 });
|
||||
}
|
||||
33
apps/web-tdesign/src/api/system/login-log/index.ts
Normal file
33
apps/web-tdesign/src/api/system/login-log/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemLoginLogApi {
|
||||
/** 登录日志信息 */
|
||||
export interface LoginLog {
|
||||
id: number;
|
||||
logType: number;
|
||||
traceId: number;
|
||||
userId: number;
|
||||
userType: number;
|
||||
username: string;
|
||||
result: number;
|
||||
status: number;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询登录日志列表 */
|
||||
export function getLoginLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemLoginLogApi.LoginLog>>(
|
||||
'/system/login-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出登录日志 */
|
||||
export function exportLoginLog(params: any) {
|
||||
return requestClient.download('/system/login-log/export-excel', { params });
|
||||
}
|
||||
64
apps/web-tdesign/src/api/system/mail/account/index.ts
Normal file
64
apps/web-tdesign/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-tdesign/src/api/system/mail/log/index.ts
Normal file
36
apps/web-tdesign/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-tdesign/src/api/system/mail/template/index.ts
Normal file
71
apps/web-tdesign/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-tdesign/src/api/system/menu/index.ts
Normal file
59
apps/web-tdesign/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-tdesign/src/api/system/notice/index.ts
Normal file
59
apps/web-tdesign/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}`);
|
||||
}
|
||||
65
apps/web-tdesign/src/api/system/notify/message/index.ts
Normal file
65
apps/web-tdesign/src/api/system/notify/message/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemNotifyMessageApi {
|
||||
/** 站内信消息信息 */
|
||||
export interface NotifyMessage {
|
||||
id: number;
|
||||
userId: number;
|
||||
userType: number;
|
||||
templateId: number;
|
||||
templateCode: string;
|
||||
templateNickname: string;
|
||||
templateContent: string;
|
||||
templateType: number;
|
||||
templateParams: string;
|
||||
readStatus: boolean;
|
||||
readTime: Date;
|
||||
createTime: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询站内信消息列表 */
|
||||
export function getNotifyMessagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNotifyMessageApi.NotifyMessage>>(
|
||||
'/system/notify-message/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得我的站内信分页 */
|
||||
export function getMyNotifyMessagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNotifyMessageApi.NotifyMessage>>(
|
||||
'/system/notify-message/my-page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 批量标记已读 */
|
||||
export function updateNotifyMessageRead(ids: number[]) {
|
||||
return requestClient.put(
|
||||
'/system/notify-message/update-read',
|
||||
{},
|
||||
{
|
||||
params: { ids },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 标记所有站内信为已读 */
|
||||
export function updateAllNotifyMessageRead() {
|
||||
return requestClient.put('/system/notify-message/update-all-read');
|
||||
}
|
||||
|
||||
/** 获取当前用户的最新站内信列表 */
|
||||
export function getUnreadNotifyMessageList() {
|
||||
return requestClient.get<SystemNotifyMessageApi.NotifyMessage[]>(
|
||||
'/system/notify-message/get-unread-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得当前用户的未读站内信数量 */
|
||||
export function getUnreadNotifyMessageCount() {
|
||||
return requestClient.get<number>('/system/notify-message/get-unread-count');
|
||||
}
|
||||
79
apps/web-tdesign/src/api/system/notify/template/index.ts
Normal file
79
apps/web-tdesign/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-tdesign/src/api/system/oauth2/client/index.ts
Normal file
64
apps/web-tdesign/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(',')}`,
|
||||
);
|
||||
}
|
||||
58
apps/web-tdesign/src/api/system/oauth2/open/index.ts
Normal file
58
apps/web-tdesign/src/api/system/oauth2/open/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** OAuth2.0 授权信息响应 */
|
||||
export namespace SystemOAuth2ClientApi {
|
||||
/** 授权信息 */
|
||||
export interface AuthorizeInfoRespVO {
|
||||
client: {
|
||||
logo: string;
|
||||
name: string;
|
||||
};
|
||||
scopes: {
|
||||
key: string;
|
||||
value: boolean;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得授权信息 */
|
||||
export function getAuthorize(clientId: string) {
|
||||
return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoRespVO>(
|
||||
`/system/oauth2/authorize?clientId=${clientId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 发起授权 */
|
||||
export function authorize(
|
||||
responseType: string,
|
||||
clientId: string,
|
||||
redirectUri: string,
|
||||
state: string,
|
||||
autoApprove: boolean,
|
||||
checkedScopes: string[],
|
||||
uncheckedScopes: string[],
|
||||
) {
|
||||
// 构建 scopes
|
||||
const scopes: Record<string, boolean> = {};
|
||||
for (const scope of checkedScopes) {
|
||||
scopes[scope] = true;
|
||||
}
|
||||
for (const scope of uncheckedScopes) {
|
||||
scopes[scope] = false;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
return requestClient.post<string>('/system/oauth2/authorize', null, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
params: {
|
||||
response_type: responseType,
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
auto_approve: autoApprove,
|
||||
scope: JSON.stringify(scopes),
|
||||
},
|
||||
});
|
||||
}
|
||||
34
apps/web-tdesign/src/api/system/oauth2/token/index.ts
Normal file
34
apps/web-tdesign/src/api/system/oauth2/token/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemOAuth2TokenApi {
|
||||
/** OAuth2.0 令牌信息 */
|
||||
export interface OAuth2Token {
|
||||
id?: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
clientId: string;
|
||||
createTime?: Date;
|
||||
expiresTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 OAuth2.0 令牌列表 */
|
||||
export function getOAuth2TokenPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemOAuth2TokenApi.OAuth2Token>>(
|
||||
'/system/oauth2-token/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除 OAuth2.0 令牌 */
|
||||
export function deleteOAuth2Token(accessToken: string) {
|
||||
return requestClient.delete(
|
||||
`/system/oauth2-token/delete?accessToken=${accessToken}`,
|
||||
);
|
||||
}
|
||||
39
apps/web-tdesign/src/api/system/operate-log/index.ts
Normal file
39
apps/web-tdesign/src/api/system/operate-log/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemOperateLogApi {
|
||||
/** 操作日志信息 */
|
||||
export interface OperateLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userType: number;
|
||||
userId: number;
|
||||
userName: string;
|
||||
type: string;
|
||||
subType: string;
|
||||
bizId: number;
|
||||
action: string;
|
||||
extra: string;
|
||||
requestMethod: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
creator: string;
|
||||
creatorName: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询操作日志列表 */
|
||||
export function getOperateLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemOperateLogApi.OperateLog>>(
|
||||
'/system/operate-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出操作日志 */
|
||||
export function exportOperateLog(params: any) {
|
||||
return requestClient.download('/system/operate-log/export-excel', { params });
|
||||
}
|
||||
57
apps/web-tdesign/src/api/system/permission/index.ts
Normal file
57
apps/web-tdesign/src/api/system/permission/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemPermissionApi {
|
||||
/** 分配用户角色请求 */
|
||||
export interface AssignUserRoleReqVO {
|
||||
userId: number;
|
||||
roleIds: number[];
|
||||
}
|
||||
|
||||
/** 分配角色菜单请求 */
|
||||
export interface AssignRoleMenuReqVO {
|
||||
roleId: number;
|
||||
menuIds: number[];
|
||||
}
|
||||
|
||||
/** 分配角色数据权限请求 */
|
||||
export interface AssignRoleDataScopeReqVO {
|
||||
roleId: number;
|
||||
dataScope: number;
|
||||
dataScopeDeptIds: number[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询角色拥有的菜单权限 */
|
||||
export async function getRoleMenuList(roleId: number) {
|
||||
return requestClient.get(
|
||||
`/system/permission/list-role-menus?roleId=${roleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 赋予角色菜单权限 */
|
||||
export async function assignRoleMenu(
|
||||
data: SystemPermissionApi.AssignRoleMenuReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-role-menu', data);
|
||||
}
|
||||
|
||||
/** 赋予角色数据权限 */
|
||||
export async function assignRoleDataScope(
|
||||
data: SystemPermissionApi.AssignRoleDataScopeReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-role-data-scope', data);
|
||||
}
|
||||
|
||||
/** 查询用户拥有的角色数组 */
|
||||
export async function getUserRoleList(userId: number) {
|
||||
return requestClient.get(
|
||||
`/system/permission/list-user-roles?userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 赋予用户角色 */
|
||||
export async function assignUserRole(
|
||||
data: SystemPermissionApi.AssignUserRoleReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-user-role', data);
|
||||
}
|
||||
63
apps/web-tdesign/src/api/system/post/index.ts
Normal file
63
apps/web-tdesign/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-tdesign/src/api/system/role/index.ts
Normal file
63
apps/web-tdesign/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-tdesign/src/api/system/sms/channel/index.ts
Normal file
67
apps/web-tdesign/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 });
|
||||
}
|
||||
45
apps/web-tdesign/src/api/system/sms/log/index.ts
Normal file
45
apps/web-tdesign/src/api/system/sms/log/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSmsLogApi {
|
||||
/** 短信日志信息 */
|
||||
export interface SmsLog {
|
||||
id?: number;
|
||||
channelId?: number;
|
||||
channelCode: string;
|
||||
templateId?: number;
|
||||
templateCode: string;
|
||||
templateType?: number;
|
||||
templateContent: string;
|
||||
templateParams?: Record<string, any>;
|
||||
apiTemplateId: string;
|
||||
mobile: string;
|
||||
userId?: number;
|
||||
userType?: number;
|
||||
sendStatus?: number;
|
||||
sendTime?: string;
|
||||
apiSendCode: string;
|
||||
apiSendMsg: string;
|
||||
apiRequestId: string;
|
||||
apiSerialNo: string;
|
||||
receiveStatus?: number;
|
||||
receiveTime?: string;
|
||||
apiReceiveCode: string;
|
||||
apiReceiveMsg: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询短信日志列表 */
|
||||
export function getSmsLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSmsLogApi.SmsLog>>(
|
||||
'/system/sms-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出短信日志 */
|
||||
export function exportSmsLog(params: any) {
|
||||
return requestClient.download('/system/sms-log/export-excel', { params });
|
||||
}
|
||||
77
apps/web-tdesign/src/api/system/sms/template/index.ts
Normal file
77
apps/web-tdesign/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);
|
||||
}
|
||||
56
apps/web-tdesign/src/api/system/social/client/index.ts
Normal file
56
apps/web-tdesign/src/api/system/social/client/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
publicKey?: 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(',')}`,
|
||||
);
|
||||
}
|
||||
66
apps/web-tdesign/src/api/system/social/user/index.ts
Normal file
66
apps/web-tdesign/src/api/system/social/user/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSocialUserApi {
|
||||
/** 社交用户信息 */
|
||||
export interface SocialUser {
|
||||
id?: number;
|
||||
type: number;
|
||||
openid: string;
|
||||
token: string;
|
||||
rawTokenInfo: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
rawUserInfo: string;
|
||||
code: string;
|
||||
state: string;
|
||||
createTime?: Date;
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
/** 社交绑定请求 */
|
||||
export interface SocialUserBindReqVO {
|
||||
type: number;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
/** 取消社交绑定请求 */
|
||||
export interface SocialUserUnbindReqVO {
|
||||
type: number;
|
||||
openid: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询社交用户列表 */
|
||||
export function getSocialUserPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSocialUserApi.SocialUser>>(
|
||||
'/system/social-user/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询社交用户详情 */
|
||||
export function getSocialUser(id: number) {
|
||||
return requestClient.get<SystemSocialUserApi.SocialUser>(
|
||||
`/system/social-user/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 社交绑定,使用 code 授权码 */
|
||||
export function socialBind(data: SystemSocialUserApi.SocialUserBindReqVO) {
|
||||
return requestClient.post<boolean>('/system/social-user/bind', data);
|
||||
}
|
||||
|
||||
/** 取消社交绑定 */
|
||||
export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReqVO) {
|
||||
return requestClient.delete<boolean>('/system/social-user/unbind', { data });
|
||||
}
|
||||
|
||||
/** 获得绑定社交用户列表 */
|
||||
export function getBindSocialUserList() {
|
||||
return requestClient.get<SystemSocialUserApi.SocialUser[]>(
|
||||
'/system/social-user/get-bind-list',
|
||||
);
|
||||
}
|
||||
64
apps/web-tdesign/src/api/system/tenant-package/index.ts
Normal file
64
apps/web-tdesign/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-tdesign/src/api/system/tenant/index.ts
Normal file
76
apps/web-tdesign/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-tdesign/src/api/system/user/index.ts
Normal file
88
apps/web-tdesign/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');
|
||||
}
|
||||
56
apps/web-tdesign/src/api/system/user/profile/index.ts
Normal file
56
apps/web-tdesign/src/api/system/user/profile/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemUserProfileApi {
|
||||
/** 用户个人中心信息 */
|
||||
export interface UserProfileRespVO {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
sex?: number;
|
||||
avatar?: string;
|
||||
loginIp: string;
|
||||
loginDate: string;
|
||||
createTime: string;
|
||||
roles: any[];
|
||||
dept: any;
|
||||
posts: any[];
|
||||
}
|
||||
|
||||
/** 更新密码请求 */
|
||||
export interface UpdatePasswordReqVO {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/** 更新个人信息请求 */
|
||||
export interface UpdateProfileReqVO {
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
sex?: number;
|
||||
avatar?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取登录用户信息 */
|
||||
export function getUserProfile() {
|
||||
return requestClient.get<SystemUserProfileApi.UserProfileRespVO>(
|
||||
'/system/user/profile/get',
|
||||
);
|
||||
}
|
||||
|
||||
/** 修改用户个人信息 */
|
||||
export function updateUserProfile(
|
||||
data: SystemUserProfileApi.UpdateProfileReqVO,
|
||||
) {
|
||||
return requestClient.put('/system/user/profile/update', data);
|
||||
}
|
||||
|
||||
/** 修改用户个人密码 */
|
||||
export function updateUserPassword(
|
||||
data: SystemUserProfileApi.UpdatePasswordReqVO,
|
||||
) {
|
||||
return requestClient.put('/system/user/profile/update-password', data);
|
||||
}
|
||||
36
apps/web-tdesign/src/app.vue
Normal file
36
apps/web-tdesign/src/app.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GlobalConfigProvider } from 'tdesign-vue-next';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
|
||||
import { merge } from 'es-toolkit/compat';
|
||||
import { ConfigProvider } from 'tdesign-vue-next';
|
||||
import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
const { isDark } = usePreferences();
|
||||
|
||||
watch(
|
||||
() => isDark.value,
|
||||
(dark) => {
|
||||
document.documentElement.setAttribute('theme-mode', dark ? 'dark' : '');
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const customConfig: GlobalConfigProvider = {
|
||||
// 可以在此处定义更多自定义配置,具体可配置内容参看 API 文档
|
||||
calendar: {},
|
||||
table: {},
|
||||
pagination: {},
|
||||
};
|
||||
const globalConfig = merge(zhConfig, customConfig);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :global-config="globalConfig">
|
||||
<RouterView />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
79
apps/web-tdesign/src/bootstrap.ts
Normal file
79
apps/web-tdesign/src/bootstrap.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
// import '@vben/styles/antd';
|
||||
// 引入组件库的少量全局样式变量
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 1020,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const routeTitle = router.currentRoute.value.meta?.title;
|
||||
const pageTitle =
|
||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||
useTitle(pageTitle);
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
export { bootstrap };
|
||||
966
apps/web-tdesign/src/components/cron-tab/cron-tab.vue
Normal file
966
apps/web-tdesign/src/components/cron-tab/cron-tab.vue
Normal file
@@ -0,0 +1,966 @@
|
||||
<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 {
|
||||
Button,
|
||||
Dialog,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Tabs,
|
||||
} from 'tdesign-vue-next';
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
v-model="defaultValue"
|
||||
class="input-with-select"
|
||||
v-bind="$attrs"
|
||||
@input="inputChange"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<Select v-model="select" placeholder="生成器" class="w-36">
|
||||
<Select.Option value="0 * * * * ?">每分钟</Select.Option>
|
||||
<Select.Option value="0 0 * * * ?">每小时</Select.Option>
|
||||
<Select.Option value="0 0 0 * * ?">每天零点</Select.Option>
|
||||
<Select.Option value="0 0 0 1 * ?">每月一号零点</Select.Option>
|
||||
<Select.Option value="0 0 0 L * ?">每月最后一天零点</Select.Option>
|
||||
<Select.Option value="0 0 0 ? * 1">每周星期日零点</Select.Option>
|
||||
<Select.Option
|
||||
v-for="(item, index) in shortcuts"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.text }}
|
||||
</Select.Option>
|
||||
<Select.Option value="custom">自定义</Select.Option>
|
||||
</Select>
|
||||
</template>
|
||||
</Input>
|
||||
|
||||
<Dialog
|
||||
v-model:open="dialogVisible"
|
||||
:width="720"
|
||||
destroy-on-close
|
||||
title="cron规则生成器"
|
||||
>
|
||||
<div class="sc-cron">
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="second">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>秒</h2>
|
||||
<h4>{{ value_second }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.second.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.second.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.second.range.start"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.second.range.end"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.second.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.second.loop.start"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
秒开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.second.loop.end"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
秒执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.second.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.second.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.second"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="minute">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>分钟</h2>
|
||||
<h4>{{ value_minute }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.minute.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.minute.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.minute.range.start"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.minute.range.end"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.minute.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.minute.loop.start"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
分钟开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.minute.loop.end"
|
||||
:max="59"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
分钟执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.minute.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.minute.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.minute"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="hour">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>小时</h2>
|
||||
<h4>{{ value_hour }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.hour.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.hour.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.hour.range.start"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.hour.range.end"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.hour.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.hour.loop.start"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
小时开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.hour.loop.end"
|
||||
:max="23"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
/>
|
||||
小时执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.hour.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.hour.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.hour"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="day">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>日</h2>
|
||||
<h4>{{ value_day }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.day.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
<RadioButton value="4">本月最后一天</RadioButton>
|
||||
<RadioButton value="5">不指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.day.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.day.range.start"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.day.range.end"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.day.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.day.loop.start"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
号开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.day.loop.end"
|
||||
:max="31"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
天执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.day.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.day.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.day"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="month">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>月</h2>
|
||||
<h4>{{ value_month }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.month.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.month.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.month.range.start"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.month.range.end"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.month.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.month.loop.start"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
月开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.month.loop.end"
|
||||
:max="12"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
月执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.month.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.month.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.month"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="week">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>周</h2>
|
||||
<h4>{{ value_week }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.week.type">
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
<RadioButton value="4">本月最后一周</RadioButton>
|
||||
<RadioButton value="5">不指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.week.type === '1'" label="范围">
|
||||
<Select v-model="cronValue.week.range.start">
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.week"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</Select>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<Select v-model="cronValue.week.range.end">
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.week"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.week.type === '2'" label="间隔">
|
||||
第
|
||||
<InputNumber
|
||||
v-model="cronValue.week.loop.start"
|
||||
:max="4"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
周的星期
|
||||
<Select v-model="cronValue.week.loop.end">
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.week"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</Select>
|
||||
执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.week.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.week.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.week"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.week.type === '4'" label="最后一周">
|
||||
<Select v-model="cronValue.week.last">
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.week"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="year">
|
||||
<template #tab>
|
||||
<div class="sc-cron-num">
|
||||
<h2>年</h2>
|
||||
<h4>{{ value_year }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
<Form>
|
||||
<Form.Item label="类型">
|
||||
<RadioGroup v-model="cronValue.year.type">
|
||||
<RadioButton value="-1">忽略</RadioButton>
|
||||
<RadioButton value="0">任意值</RadioButton>
|
||||
<RadioButton value="1">范围</RadioButton>
|
||||
<RadioButton value="2">间隔</RadioButton>
|
||||
<RadioButton value="3">指定</RadioButton>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.year.type === '1'" label="范围">
|
||||
<InputNumber
|
||||
v-model="cronValue.year.range.start"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="padding: 0 15px">-</span>
|
||||
<InputNumber
|
||||
v-model="cronValue.year.range.end"
|
||||
controls-position="right"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.year.type === '2'" label="间隔">
|
||||
<InputNumber
|
||||
v-model="cronValue.year.loop.start"
|
||||
controls-position="right"
|
||||
/>
|
||||
年开始,每
|
||||
<InputNumber
|
||||
v-model="cronValue.year.loop.end"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
年执行一次
|
||||
</Form.Item>
|
||||
<Form.Item v-if="cronValue.year.type === '3'" label="指定">
|
||||
<Select
|
||||
v-model="cronValue.year.appoint"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="(item, index) in data.year"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button @click="dialogVisible = false">取 消</Button>
|
||||
<Button theme="primary" @click="submit()">确 认</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sc-cron :deep(.ant-tabs-tab) {
|
||||
height: auto;
|
||||
padding: 0 7px;
|
||||
line-height: 1;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.sc-cron-num {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sc-cron-num h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.sc-cron-num h4 {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 15px;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
background: hsl(var(--primary) / 10%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sc-cron :deep(.ant-tabs-tab.ant-tabs-tab-active) .sc-cron-num h4 {
|
||||
color: #fff;
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
[data-theme='dark'] .sc-cron-num h4 {
|
||||
background: hsl(var(--white));
|
||||
}
|
||||
|
||||
.input-with-select .ant-input-group-addon {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
</style>
|
||||
1
apps/web-tdesign/src/components/cron-tab/index.ts
Normal file
1
apps/web-tdesign/src/components/cron-tab/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CronTab } from './cron-tab.vue';
|
||||
266
apps/web-tdesign/src/components/cron-tab/types.ts
Normal file
266
apps/web-tdesign/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(),
|
||||
};
|
||||
125
apps/web-tdesign/src/components/cropper/cropper-avatar.vue
Normal file
125
apps/web-tdesign/src/components/cropper/cropper-avatar.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import type { CropperAvatarProps } from './typing';
|
||||
|
||||
import { computed, ref, unref, watch, watchEffect } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button } from 'tdesign-vue-next';
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
import cropperModal from './cropper-modal.vue';
|
||||
|
||||
defineOptions({ name: 'CropperAvatar' });
|
||||
|
||||
const props = withDefaults(defineProps<CropperAvatarProps>(), {
|
||||
width: 200,
|
||||
value: '',
|
||||
showBtn: true,
|
||||
btnProps: () => ({}),
|
||||
btnText: '',
|
||||
uploadApi: () => Promise.resolve(),
|
||||
size: 5,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
|
||||
const sourceValue = ref(props.value || '');
|
||||
const [CropperModal, modalApi] = useVbenModal({
|
||||
connectedComponent: cropperModal,
|
||||
});
|
||||
|
||||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
||||
|
||||
const getIconWidth = computed(
|
||||
() => `${Number.parseInt(`${props.width}`.replace(/px/, '')) / 2}px`,
|
||||
);
|
||||
|
||||
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
|
||||
|
||||
const getImageWrapperStyle = computed(
|
||||
(): CSSProperties => ({ height: unref(getWidth), width: unref(getWidth) }),
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
sourceValue.value = props.value || '';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sourceValue.value,
|
||||
(v: string) => {
|
||||
emit('update:value', v);
|
||||
},
|
||||
);
|
||||
|
||||
function handleUploadSuccess({ data, source }: any) {
|
||||
sourceValue.value = source;
|
||||
emit('change', { data, source });
|
||||
message.success($t('ui.cropper.uploadSuccess'));
|
||||
}
|
||||
|
||||
const closeModal = () => modalApi.close();
|
||||
const openModal = () => modalApi.open();
|
||||
|
||||
defineExpose({
|
||||
closeModal,
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 头像容器 -->
|
||||
<div class="inline-block text-center" :style="getStyle">
|
||||
<!-- 图片包装器 -->
|
||||
<div
|
||||
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-card"
|
||||
:style="getImageWrapperStyle"
|
||||
@click="openModal"
|
||||
>
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
:style="getImageWrapperStyle"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="lucide:cloud-upload"
|
||||
class="m-auto text-gray-400"
|
||||
:style="{
|
||||
...getImageWrapperStyle,
|
||||
width: getIconWidth,
|
||||
height: getIconWidth,
|
||||
lineHeight: getIconWidth,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<!-- 头像图片 -->
|
||||
<img
|
||||
v-if="sourceValue"
|
||||
:src="sourceValue"
|
||||
alt="avatar"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
<Button
|
||||
v-if="showBtn"
|
||||
class="mx-auto mt-2"
|
||||
@click="openModal"
|
||||
v-bind="btnProps"
|
||||
>
|
||||
{{ btnText ? btnText : $t('ui.cropper.selectImage') }}
|
||||
</Button>
|
||||
|
||||
<CropperModal
|
||||
:size="size"
|
||||
:src="sourceValue"
|
||||
:upload-api="uploadApi"
|
||||
@upload-success="handleUploadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
304
apps/web-tdesign/src/components/cropper/cropper-modal.vue
Normal file
304
apps/web-tdesign/src/components/cropper/cropper-modal.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile } from 'tdesign-vue-next';
|
||||
|
||||
import type { CropendResult, CropperModalProps, CropperType } from './typing';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { dataURLtoBlob, isFunction, isNumber } from '@vben/utils';
|
||||
|
||||
import { Avatar, Button, Space, Tooltip, Upload } from 'tdesign-vue-next';
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
import CropperImage from './cropper.vue';
|
||||
|
||||
defineOptions({ name: 'CropperModal' });
|
||||
|
||||
const props = withDefaults(defineProps<CropperModalProps>(), {
|
||||
circled: true,
|
||||
size: 0,
|
||||
src: '',
|
||||
uploadApi: () => Promise.resolve(),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
|
||||
|
||||
let filename = '';
|
||||
const src = ref(props.src || '');
|
||||
const previewSource = ref('');
|
||||
const cropper = ref<CropperType>();
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm: handleOk,
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading(通过 handleReady)
|
||||
modalLoading(true);
|
||||
const img = new Image();
|
||||
img.src = src.value;
|
||||
img.addEventListener('load', () => {
|
||||
modalLoading(false);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
modalLoading(false);
|
||||
});
|
||||
} else {
|
||||
// 关闭时,清空右侧预览
|
||||
previewSource.value = '';
|
||||
modalLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function modalLoading(loading: boolean) {
|
||||
modalApi.setState({ confirmLoading: loading, loading });
|
||||
}
|
||||
|
||||
// Block upload
|
||||
function handleBeforeUpload(file: UploadFile) {
|
||||
if (
|
||||
file &&
|
||||
props.size > 0 &&
|
||||
isNumber(file.size) &&
|
||||
file.size > 1024 * 1024 * props.size
|
||||
) {
|
||||
emit('uploadError', { msg: $t('ui.cropper.imageTooBig') });
|
||||
return false;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.raw as Blob);
|
||||
src.value = '';
|
||||
previewSource.value = '';
|
||||
reader.addEventListener('load', (e) => {
|
||||
src.value = (e.target?.result as string) ?? '';
|
||||
filename = file.name ?? '';
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleCropend({ imgBase64 }: CropendResult) {
|
||||
previewSource.value = imgBase64;
|
||||
}
|
||||
|
||||
function handleReady(cropperInstance: CropperType) {
|
||||
cropper.value = cropperInstance;
|
||||
// 画布加载完毕 关闭 loading
|
||||
modalLoading(false);
|
||||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||
}
|
||||
if (event === 'scaleY') {
|
||||
scaleY = arg = scaleY === -1 ? 1 : -1;
|
||||
}
|
||||
(cropper?.value as any)?.[event]?.(arg);
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
const uploadApi = props.uploadApi;
|
||||
if (uploadApi && isFunction(uploadApi)) {
|
||||
if (!previewSource.value) {
|
||||
message.warning('未选择图片');
|
||||
return;
|
||||
}
|
||||
const blob = dataURLtoBlob(previewSource.value);
|
||||
try {
|
||||
modalLoading(true);
|
||||
const url = await uploadApi({ file: blob, filename, name: 'file' });
|
||||
emit('uploadSuccess', { data: url, source: previewSource.value });
|
||||
await modalApi.close();
|
||||
} finally {
|
||||
modalLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-bind="$attrs"
|
||||
:confirm-text="$t('ui.cropper.okText')"
|
||||
:fullscreen-button="false"
|
||||
:title="$t('ui.cropper.modalTitle')"
|
||||
class="w-2/3"
|
||||
>
|
||||
<div class="flex h-96">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="h-full w-3/5">
|
||||
<!-- 裁剪器容器 -->
|
||||
<div
|
||||
class="relative h-[300px] bg-gradient-to-b from-neutral-50 to-neutral-200"
|
||||
>
|
||||
<CropperImage
|
||||
v-if="src"
|
||||
:circled="circled"
|
||||
:src="src"
|
||||
height="300px"
|
||||
@cropend="handleCropend"
|
||||
@ready="handleReady"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<Upload
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="[]"
|
||||
accept="image/*"
|
||||
>
|
||||
<Tooltip :title="$t('ui.cropper.selectImage')" placement="bottom">
|
||||
<Button size="small" theme="primary">
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="lucide:upload" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Space>
|
||||
<Tooltip :title="$t('ui.cropper.btn_reset')" placement="bottom">
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('reset')"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="lucide:rotate-ccw" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
:title="$t('ui.cropper.btn_rotate_left')"
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('rotate', -45)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="ant-design:rotate-left-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
:title="$t('ui.cropper.btn_rotate_right')"
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('rotate', 45)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="ant-design:rotate-right-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip :title="$t('ui.cropper.btn_scale_x')" placement="bottom">
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('scaleX')"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="vaadin:arrows-long-h" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip :title="$t('ui.cropper.btn_scale_y')" placement="bottom">
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('scaleY')"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="vaadin:arrows-long-v" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip :title="$t('ui.cropper.btn_zoom_in')" placement="bottom">
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('zoom', 0.1)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="lucide:zoom-in" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip :title="$t('ui.cropper.btn_zoom_out')" placement="bottom">
|
||||
<Button
|
||||
:disabled="!src"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="handlerToolbar('zoom', -0.1)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<IconifyIcon icon="lucide:zoom-out" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="h-full w-2/5">
|
||||
<!-- 预览区域 -->
|
||||
<div
|
||||
class="mx-auto h-56 w-56 overflow-hidden rounded-full border border-gray-200"
|
||||
>
|
||||
<img
|
||||
v-if="previewSource"
|
||||
:alt="$t('ui.cropper.preview')"
|
||||
:src="previewSource"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 头像组合预览 -->
|
||||
<template v-if="previewSource">
|
||||
<div
|
||||
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
|
||||
>
|
||||
<Avatar :src="previewSource" size="large" />
|
||||
<Avatar size="48" :src="previewSource" />
|
||||
<Avatar size="64" :src="previewSource" />
|
||||
<Avatar size="80" :src="previewSource" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
171
apps/web-tdesign/src/components/cropper/cropper.vue
Normal file
171
apps/web-tdesign/src/components/cropper/cropper.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import type { CropperProps } from './typing';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, unref, useAttrs } from 'vue';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import Cropper from 'cropperjs';
|
||||
|
||||
import { defaultOptions } from './typing';
|
||||
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
|
||||
defineOptions({ name: 'CropperImage' });
|
||||
|
||||
const props = withDefaults(defineProps<CropperProps>(), {
|
||||
src: '',
|
||||
alt: '',
|
||||
circled: false,
|
||||
realTimePreview: true,
|
||||
height: '360px',
|
||||
crossorigin: undefined,
|
||||
imageStyle: () => ({}),
|
||||
options: () => ({}),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError']);
|
||||
const attrs = useAttrs();
|
||||
|
||||
type ElRef<T extends HTMLElement = HTMLDivElement> = null | T;
|
||||
const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||
const cropper = ref<Cropper | null>();
|
||||
const isReady = ref(false);
|
||||
|
||||
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
||||
|
||||
const getImageStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: props.height,
|
||||
maxWidth: '100%',
|
||||
...props.imageStyle,
|
||||
};
|
||||
});
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
attrs.class,
|
||||
{
|
||||
'cropper-image--circled': props.circled,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const getWrapperStyle = computed((): CSSProperties => {
|
||||
return { height: `${`${props.height}`.replace(/px/, '')}px` };
|
||||
});
|
||||
|
||||
onMounted(init);
|
||||
|
||||
onUnmounted(() => {
|
||||
cropper.value?.destroy();
|
||||
});
|
||||
|
||||
async function init() {
|
||||
const imgEl = unref(imgElRef);
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
cropper.value = new Cropper(imgEl, {
|
||||
...defaultOptions,
|
||||
ready: () => {
|
||||
isReady.value = true;
|
||||
realTimeCropped();
|
||||
emit('ready', cropper.value);
|
||||
},
|
||||
crop() {
|
||||
debounceRealTimeCropped();
|
||||
},
|
||||
zoom() {
|
||||
debounceRealTimeCropped();
|
||||
},
|
||||
cropmove() {
|
||||
debounceRealTimeCropped();
|
||||
},
|
||||
...props.options,
|
||||
});
|
||||
}
|
||||
|
||||
// Real-time display preview
|
||||
function realTimeCropped() {
|
||||
props.realTimePreview && cropped();
|
||||
}
|
||||
|
||||
// event: return base64 and width and height information after cropping
|
||||
function cropped() {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
const imgInfo = cropper.value.getData();
|
||||
const canvas = props.circled
|
||||
? getRoundedCanvas()
|
||||
: cropper.value.getCroppedCanvas();
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
const fileReader: FileReader = new FileReader();
|
||||
fileReader.readAsDataURL(blob);
|
||||
fileReader.onloadend = (e) => {
|
||||
emit('cropend', {
|
||||
imgBase64: e.target?.result ?? '',
|
||||
imgInfo,
|
||||
});
|
||||
};
|
||||
fileReader.addEventListener('error', () => {
|
||||
emit('cropendError');
|
||||
});
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
// Get a circular picture canvas
|
||||
function getRoundedCanvas() {
|
||||
const sourceCanvas = cropper.value!.getCroppedCanvas();
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d')!;
|
||||
const width = sourceCanvas.width;
|
||||
const height = sourceCanvas.height;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.drawImage(sourceCanvas, 0, 0, width, height);
|
||||
context.globalCompositeOperation = 'destination-in';
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
width / 2,
|
||||
height / 2,
|
||||
Math.min(width, height) / 2,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true,
|
||||
);
|
||||
context.fill();
|
||||
return canvas;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="getClass" :style="getWrapperStyle">
|
||||
<img
|
||||
v-show="isReady"
|
||||
ref="imgElRef"
|
||||
:alt="alt"
|
||||
:crossorigin="crossorigin"
|
||||
:src="src"
|
||||
:style="getImageStyle"
|
||||
class="h-auto max-w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.cropper-image {
|
||||
&--circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
apps/web-tdesign/src/components/cropper/index.ts
Normal file
3
apps/web-tdesign/src/components/cropper/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CropperAvatar } from './cropper-avatar.vue';
|
||||
export { default as CropperImage } from './cropper.vue';
|
||||
export type { CropperType } from './typing';
|
||||
68
apps/web-tdesign/src/components/cropper/typing.ts
Normal file
68
apps/web-tdesign/src/components/cropper/typing.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type Cropper from 'cropperjs';
|
||||
import type { ButtonProps } from 'tdesign-vue-next';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
export interface apiFunParams {
|
||||
file: Blob;
|
||||
filename: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CropendResult {
|
||||
imgBase64: string;
|
||||
imgInfo: Cropper.Data;
|
||||
}
|
||||
|
||||
export interface CropperProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
circled?: boolean;
|
||||
realTimePreview?: boolean;
|
||||
height?: number | string;
|
||||
crossorigin?: '' | 'anonymous' | 'use-credentials' | undefined;
|
||||
imageStyle?: CSSProperties;
|
||||
options?: Cropper.Options;
|
||||
}
|
||||
|
||||
export interface CropperAvatarProps {
|
||||
width?: number | string;
|
||||
value?: string;
|
||||
showBtn?: boolean;
|
||||
btnProps?: ButtonProps;
|
||||
btnText?: string;
|
||||
uploadApi?: (params: apiFunParams) => Promise<any>;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface CropperModalProps {
|
||||
circled?: boolean;
|
||||
uploadApi?: (params: apiFunParams) => Promise<any>;
|
||||
src?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const defaultOptions: Cropper.Options = {
|
||||
aspectRatio: 1,
|
||||
zoomable: true,
|
||||
zoomOnTouch: true,
|
||||
zoomOnWheel: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
highlight: true,
|
||||
center: true,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: true,
|
||||
checkOrientation: true,
|
||||
scalable: true,
|
||||
modal: true,
|
||||
guides: true,
|
||||
movable: true,
|
||||
rotatable: true,
|
||||
};
|
||||
|
||||
export type { Cropper as CropperType };
|
||||
195
apps/web-tdesign/src/components/description/description.vue
Normal file
195
apps/web-tdesign/src/components/description/description.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="tsx">
|
||||
import type { DescriptionsProps } from 'tdesign-vue-next';
|
||||
|
||||
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 { Card, Descriptions } from 'tdesign-vue-next';
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
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 DescriptionsProps;
|
||||
});
|
||||
|
||||
// 防止换行
|
||||
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 (
|
||||
<Descriptions.Item
|
||||
key={field}
|
||||
label={renderLabel(item)}
|
||||
span={span}
|
||||
>
|
||||
{() => {
|
||||
if (item.slot) {
|
||||
return getSlot(slots, item.slot, data);
|
||||
}
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
}
|
||||
const style: CSSProperties = {
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <div style={style}>{getContent()}</div>;
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
);
|
||||
})
|
||||
.filter((item) => !!item);
|
||||
}
|
||||
|
||||
function renderDesc() {
|
||||
return (
|
||||
<Descriptions
|
||||
class={`${prefixCls}`}
|
||||
{...(unref(getDescriptionsProps) as any)}
|
||||
>
|
||||
{renderItem()}
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card
|
||||
bodyStyle={{ padding: '8px 0' }}
|
||||
headerStyle={{
|
||||
padding: '8px 16px',
|
||||
fontSize: '14px',
|
||||
minHeight: '24px',
|
||||
}}
|
||||
style={{ margin: 0 }}
|
||||
title={title}
|
||||
>
|
||||
{{
|
||||
default: () => content,
|
||||
extra: () => extraSlot && <div>{extraSlot}</div>,
|
||||
}}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return () => (unref(useWrapper) ? renderCard() : renderDesc());
|
||||
},
|
||||
});
|
||||
</script>
|
||||
3
apps/web-tdesign/src/components/description/index.ts
Normal file
3
apps/web-tdesign/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';
|
||||
45
apps/web-tdesign/src/components/description/typing.ts
Normal file
45
apps/web-tdesign/src/components/description/typing.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { DescriptionsProps as TdDescriptionsProps } from 'tdesign-vue-next';
|
||||
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 TdDescriptionsProps {
|
||||
// 是否包含卡片组件
|
||||
useCard?: boolean;
|
||||
// 描述项配置
|
||||
schema: DescriptionItemSchema[];
|
||||
// 数据
|
||||
data: Recordable<any>;
|
||||
// 标题
|
||||
title?: string;
|
||||
// 是否包含边框
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
82
apps/web-tdesign/src/components/dict-tag/dict-tag.vue
Normal file
82
apps/web-tdesign/src/components/dict-tag/dict-tag.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDictObj } from '@vben/hooks';
|
||||
import { isValidColor, TinyColor } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'tdesign-vue-next';
|
||||
|
||||
interface DictTagProps {
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 字典值
|
||||
*/
|
||||
value: any;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<DictTagProps>();
|
||||
|
||||
/** 获取字典标签 */
|
||||
const dictTag = computed(() => {
|
||||
// 校验参数有效性
|
||||
if (!props.type || props.value === undefined || props.value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取字典对象
|
||||
const dict = getDictObj(props.type, String(props.value));
|
||||
if (!dict) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理颜色类型
|
||||
let colorType = dict.colorType;
|
||||
switch (colorType) {
|
||||
case 'danger': {
|
||||
colorType = 'danger';
|
||||
break;
|
||||
}
|
||||
case 'info': {
|
||||
colorType = 'success';
|
||||
break;
|
||||
}
|
||||
case 'primary': {
|
||||
colorType = 'primary';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (!colorType) {
|
||||
colorType = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidColor(dict.cssClass)) {
|
||||
colorType = new TinyColor(dict.cssClass).toHexString();
|
||||
}
|
||||
|
||||
return {
|
||||
label: dict.label || '',
|
||||
theme: colorType as
|
||||
| 'danger'
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warning',
|
||||
cssClass: dict.cssClass,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tag v-if="dictTag" :theme="dictTag.theme" :color="dictTag.cssClass">
|
||||
{{ dictTag.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
1
apps/web-tdesign/src/components/dict-tag/index.ts
Normal file
1
apps/web-tdesign/src/components/dict-tag/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DictTag } from './dict-tag.vue';
|
||||
14
apps/web-tdesign/src/components/table-action/icons.ts
Normal file
14
apps/web-tdesign/src/components/table-action/icons.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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',
|
||||
CLOSE: 'lucide:x',
|
||||
};
|
||||
4
apps/web-tdesign/src/components/table-action/index.ts
Normal file
4
apps/web-tdesign/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';
|
||||
282
apps/web-tdesign/src/components/table-action/table-action.vue
Normal file
282
apps/web-tdesign/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<!-- add by 星语:参考 vben2 的方式,增加 TableAction 组件 -->
|
||||
<script setup lang="ts">
|
||||
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 { $t } from '@vben/locales';
|
||||
import { isBoolean, isFunction } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from 'tdesign-vue-next';
|
||||
|
||||
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(() => {
|
||||
const dropDownActions = props.dropDownActions || [];
|
||||
return dropDownActions.filter((action: ActionItem) => isIfShow(action));
|
||||
});
|
||||
|
||||
/** Space 组件的 size */
|
||||
const spaceSize = computed(() => {
|
||||
const actions = unref(getActions);
|
||||
return actions?.some(
|
||||
(item: ActionItem) => item.type === 'primary' && item.variant === 'text',
|
||||
)
|
||||
? 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.onConfirm = popConfirm.confirm;
|
||||
}
|
||||
if (popConfirm.cancel && isFunction(popConfirm.cancel)) {
|
||||
attrs.onCancel = popConfirm.cancel;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/** 获取 Button 属性 */
|
||||
function getButtonProps(action: ActionItem) {
|
||||
return {
|
||||
theme: action.type || 'primary',
|
||||
variant: action.variant || 'text',
|
||||
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(e: any) {
|
||||
const action = getDropdownList.value[e.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();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听props变化,强制重新计算 */
|
||||
watch(
|
||||
() => [props.actions, props.dropDownActions],
|
||||
() => {
|
||||
// 这里不需要额外处理,computed会自动重新计算
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-actions">
|
||||
<Space :size="spaceSize">
|
||||
<template
|
||||
v-for="(action, index) in getActions"
|
||||
:key="getActionKey(action, index)"
|
||||
>
|
||||
<Popconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
>
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<Tooltip v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button
|
||||
v-bind="getButtonProps(action)"
|
||||
@click="handleButtonClick(action)"
|
||||
>
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Space>
|
||||
|
||||
<Dropdown v-if="getDropdownList.length > 0" trigger="hover">
|
||||
<slot name="more">
|
||||
<Button theme="primary" variant="text">
|
||||
<template #icon>
|
||||
{{ $t('page.action.more') }}
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
v-for="(action, index) in getDropdownList"
|
||||
:key="index"
|
||||
:disabled="action.disabled"
|
||||
@click="!action.popConfirm && handleMenuClick({ key: index })"
|
||||
>
|
||||
<template v-if="action.popConfirm">
|
||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<IconifyIcon v-if="action.icon" :icon="action.icon" />
|
||||
<span :class="action.icon ? 'ml-1' : ''">
|
||||
{{ action.label }}
|
||||
</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<IconifyIcon v-if="action.icon" :icon="action.icon" />
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</template>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.table-actions {
|
||||
.ant-btn-link {
|
||||
padding: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ant-btn > .iconify + span,
|
||||
.ant-btn > span + .iconify {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.iconify {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
vertical-align: -0.125em;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-popconfirm {
|
||||
.ant-popconfirm-buttons {
|
||||
.ant-btn {
|
||||
margin-inline-start: 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
apps/web-tdesign/src/components/table-action/typing.ts
Normal file
32
apps/web-tdesign/src/components/table-action/typing.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { TdButtonProps, TooltipProps } from 'tdesign-vue-next';
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: () => void;
|
||||
cancel?: () => void;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionItem {
|
||||
onClick?: () => void;
|
||||
type?: TdButtonProps['theme'];
|
||||
label?: string;
|
||||
icon?: string;
|
||||
color?: 'error' | 'success' | 'warning';
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||
tooltip?: string | TooltipProps;
|
||||
loading?: boolean;
|
||||
size?: TdButtonProps['size'];
|
||||
shape?: TdButtonProps['shape'];
|
||||
variant?: TdButtonProps['variant'];
|
||||
danger?: boolean;
|
||||
}
|
||||
344
apps/web-tdesign/src/components/tinymce/editor.vue
Normal file
344
apps/web-tdesign/src/components/tinymce/editor.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';
|
||||
import type { Editor as EditorType } from 'tinymce/tinymce';
|
||||
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
ref,
|
||||
unref,
|
||||
useAttrs,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { buildShortUUID, isNumber } from '@vben/utils';
|
||||
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
|
||||
import { useUpload } from '#/components/upload/use-upload';
|
||||
|
||||
import { bindHandlers } from './helper';
|
||||
import ImgUpload from './img-upload.vue';
|
||||
import {
|
||||
plugins as defaultPlugins,
|
||||
toolbar as defaultToolbar,
|
||||
} from './tinymce';
|
||||
|
||||
type InitOptions = IPropTypes['init'];
|
||||
|
||||
defineOptions({ name: 'Tinymce', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(defineProps<TinymacProps>(), {
|
||||
height: 400,
|
||||
width: 'auto',
|
||||
options: () => ({}),
|
||||
plugins: defaultPlugins,
|
||||
toolbar: defaultToolbar,
|
||||
showImageUpload: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
interface TinymacProps {
|
||||
options?: Partial<InitOptions>;
|
||||
toolbar?: string;
|
||||
plugins?: string;
|
||||
height?: number | string;
|
||||
width?: number | string;
|
||||
showImageUpload?: boolean;
|
||||
}
|
||||
|
||||
/** 外部使用 v-model 绑定值 */
|
||||
const modelValue = defineModel('modelValue', { default: '', type: String });
|
||||
|
||||
/** TinyMCE 自托管:https://www.jianshu.com/p/59a9c3802443 */
|
||||
const tinymceScriptSrc = `${import.meta.env.VITE_BASE}tinymce/tinymce.min.js`;
|
||||
|
||||
const attrs = useAttrs();
|
||||
const editorRef = ref<EditorType>();
|
||||
const fullscreen = ref(false); // 图片上传,是否放到全屏的位置
|
||||
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
|
||||
const elRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const containerWidth = computed(() => {
|
||||
const width = props.width;
|
||||
if (isNumber(width)) {
|
||||
return `${width}px`;
|
||||
}
|
||||
return width;
|
||||
});
|
||||
|
||||
/** 主题皮肤 */
|
||||
const { isDark } = usePreferences();
|
||||
const skinName = computed(() => {
|
||||
return isDark.value ? 'oxide-dark' : 'oxide';
|
||||
});
|
||||
|
||||
const contentCss = computed(() => {
|
||||
return isDark.value ? 'dark' : 'default';
|
||||
});
|
||||
|
||||
/** 国际化:需要在 langs 目录下,放好语言包 */
|
||||
const { locale } = usePreferences();
|
||||
const langName = computed(() => {
|
||||
if (locale.value === 'en-US') {
|
||||
return 'en';
|
||||
}
|
||||
return 'zh_CN';
|
||||
});
|
||||
|
||||
/** 监听 mode、locale 进行主题、语言切换 */
|
||||
const init = ref(true);
|
||||
watch(
|
||||
() => [preferences.theme.mode, preferences.app.locale],
|
||||
async () => {
|
||||
if (!editorRef.value) {
|
||||
return;
|
||||
}
|
||||
// 通过 init + v-if 来挂载/卸载组件
|
||||
destroy();
|
||||
init.value = false;
|
||||
await nextTick();
|
||||
init.value = true;
|
||||
// 等待加载完成
|
||||
await nextTick();
|
||||
setEditorMode();
|
||||
},
|
||||
);
|
||||
|
||||
const initOptions = computed((): InitOptions => {
|
||||
const { height, options, plugins, toolbar } = props;
|
||||
return {
|
||||
height,
|
||||
toolbar,
|
||||
menubar: 'file edit view insert format tools table help',
|
||||
plugins,
|
||||
language: langName.value,
|
||||
branding: false, // 禁止显示,右下角的“使用 TinyMCE 构建”
|
||||
default_link_target: '_blank',
|
||||
link_title: false,
|
||||
object_resizing: true, // 和 vben2.0 不同,它默认是 false
|
||||
auto_focus: undefined, // 和 vben2.0 不同,它默认是 true
|
||||
skin: skinName.value,
|
||||
content_css: contentCss.value,
|
||||
content_style:
|
||||
'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
|
||||
contextmenu: 'link image table',
|
||||
image_advtab: true, // 图片高级选项
|
||||
image_caption: true,
|
||||
importcss_append: true,
|
||||
noneditable_class: 'mceNonEditable',
|
||||
paste_data_images: true, // 允许粘贴图片,默认 base64 格式,images_upload_handler 启用时为上传
|
||||
quickbars_selection_toolbar:
|
||||
'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
toolbar_mode: 'sliding',
|
||||
...options,
|
||||
images_upload_handler: (blobInfo: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = blobInfo.blob() as File;
|
||||
const { httpRequest } = useUpload();
|
||||
httpRequest(file)
|
||||
.then((url) => {
|
||||
resolve(url);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('tinymce 上传图片失败:', error);
|
||||
reject(error.message);
|
||||
});
|
||||
});
|
||||
},
|
||||
setup: (editor: EditorType) => {
|
||||
editorRef.value = editor;
|
||||
editor.on('init', (e: any) => initSetup(e));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/** 监听 options.readonly 是否只读 */
|
||||
const disabled = computed(() => props.options.readonly ?? false);
|
||||
watch(
|
||||
() => props.options,
|
||||
(options) => {
|
||||
const getDisabled = options && Reflect.get(options, 'readonly');
|
||||
const editor = unref(editorRef);
|
||||
if (editor) {
|
||||
editor.mode.set(getDisabled ? 'readonly' : 'design');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (!initOptions.value.inline) {
|
||||
tinymceId.value = buildShortUUID('tiny-vue');
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
initEditor();
|
||||
setEditorMode();
|
||||
}, 30);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroy();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
destroy();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
setEditorMode();
|
||||
});
|
||||
|
||||
function setEditorMode() {
|
||||
const editor = unref(editorRef);
|
||||
if (editor) {
|
||||
const mode = props.options.readonly ? 'readonly' : 'design';
|
||||
editor.mode.set(mode);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
const editor = unref(editorRef);
|
||||
editor?.destroy();
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
const el = unref(elRef);
|
||||
if (el) {
|
||||
el.style.visibility = '';
|
||||
}
|
||||
}
|
||||
|
||||
function initSetup(e: any) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const value = modelValue.value || '';
|
||||
|
||||
editor.setContent(value);
|
||||
bindModelHandlers(editor);
|
||||
bindHandlers(e, attrs, unref(editorRef));
|
||||
}
|
||||
|
||||
function setValue(editor: Record<string, any>, val?: string, prevVal?: string) {
|
||||
if (
|
||||
editor &&
|
||||
typeof val === 'string' &&
|
||||
val !== prevVal &&
|
||||
val !== editor.getContent({ format: attrs.outputFormat })
|
||||
) {
|
||||
editor.setContent(val);
|
||||
}
|
||||
}
|
||||
|
||||
function bindModelHandlers(editor: any) {
|
||||
const modelEvents = attrs.modelEvents ?? null;
|
||||
const normalizedEvents = Array.isArray(modelEvents)
|
||||
? modelEvents.join(' ')
|
||||
: modelEvents;
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
(val, prevVal) => {
|
||||
setValue(editor, val, prevVal);
|
||||
},
|
||||
);
|
||||
|
||||
editor.on(normalizedEvents || 'change keyup undo redo', () => {
|
||||
const content = editor.getContent({ format: attrs.outputFormat });
|
||||
emit('change', content);
|
||||
});
|
||||
|
||||
editor.on('FullscreenStateChanged', (e: any) => {
|
||||
fullscreen.value = e.state;
|
||||
});
|
||||
}
|
||||
|
||||
function getUploadingImgName(name: string) {
|
||||
return `[uploading:${name}]`;
|
||||
}
|
||||
|
||||
function handleImageUploading(name: string) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
|
||||
const content = editor?.getContent() ?? '';
|
||||
setValue(editor, content);
|
||||
}
|
||||
|
||||
function handleDone(name: string, url: string) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const content = editor?.getContent() ?? '';
|
||||
const val =
|
||||
content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
|
||||
setValue(editor, val);
|
||||
}
|
||||
|
||||
function handleError(name: string) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const content = editor?.getContent() ?? '';
|
||||
const val = content?.replace(getUploadingImgName(name), '') ?? '';
|
||||
setValue(editor, val);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ width: containerWidth }" class="app-tinymce">
|
||||
<ImgUpload
|
||||
v-if="showImageUpload"
|
||||
v-show="editorRef"
|
||||
:disabled="disabled"
|
||||
:fullscreen="fullscreen"
|
||||
@done="handleDone"
|
||||
@error="handleError"
|
||||
@uploading="handleImageUploading"
|
||||
/>
|
||||
<Editor
|
||||
v-if="!initOptions.inline && init"
|
||||
v-model="modelValue"
|
||||
:init="initOptions"
|
||||
:style="{ visibility: 'hidden', zIndex: 3000 }"
|
||||
:tinymce-script-src="tinymceScriptSrc"
|
||||
license-key="gpl"
|
||||
/>
|
||||
<slot v-else></slot>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.tox.tox-silver-sink.tox-tinymce-aux {
|
||||
z-index: 2025; /* 由于 vben modal/drawer 的 zIndex 为 2000,需要调整 z-index(默认 1300)超过它,避免遮挡 */
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-tinymce {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
|
||||
:deep(.textarea) {
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏右上角 tinymce upgrade 按钮 */
|
||||
:deep(.tox-promotion) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
85
apps/web-tdesign/src/components/tinymce/helper.ts
Normal file
85
apps/web-tdesign/src/components/tinymce/helper.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
const validEvents = new Set([
|
||||
'onActivate',
|
||||
'onAddUndo',
|
||||
'onBeforeAddUndo',
|
||||
'onBeforeExecCommand',
|
||||
'onBeforeGetContent',
|
||||
'onBeforePaste',
|
||||
'onBeforeRenderUI',
|
||||
'onBeforeSetContent',
|
||||
'onBlur',
|
||||
'onChange',
|
||||
'onClearUndos',
|
||||
'onClick',
|
||||
'onContextMenu',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDblclick',
|
||||
'onDeactivate',
|
||||
'onDirty',
|
||||
'onDrag',
|
||||
'onDragDrop',
|
||||
'onDragEnd',
|
||||
'onDragGesture',
|
||||
'onDragOver',
|
||||
'onDrop',
|
||||
'onExecCommand',
|
||||
'onFocus',
|
||||
'onFocusIn',
|
||||
'onFocusOut',
|
||||
'onGetContent',
|
||||
'onHide',
|
||||
'onInit',
|
||||
'onKeyDown',
|
||||
'onKeyPress',
|
||||
'onKeyUp',
|
||||
'onLoadContent',
|
||||
'onMouseDown',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
'onMouseMove',
|
||||
'onMouseOut',
|
||||
'onMouseOver',
|
||||
'onMouseUp',
|
||||
'onNodeChange',
|
||||
'onObjectResized',
|
||||
'onObjectResizeStart',
|
||||
'onObjectSelected',
|
||||
'onPaste',
|
||||
'onPostProcess',
|
||||
'onPostRender',
|
||||
'onPreProcess',
|
||||
'onProgressState',
|
||||
'onRedo',
|
||||
'onRemove',
|
||||
'onReset',
|
||||
'onSaveContent',
|
||||
'onSelectionChange',
|
||||
'onSetAttrib',
|
||||
'onSetContent',
|
||||
'onShow',
|
||||
'onSubmit',
|
||||
'onUndo',
|
||||
'onVisualAid',
|
||||
]);
|
||||
|
||||
const isValidKey = (key: string) => validEvents.has(key);
|
||||
|
||||
export const bindHandlers = (
|
||||
initEvent: Event,
|
||||
listeners: any,
|
||||
editor: any,
|
||||
): void => {
|
||||
Object.keys(listeners)
|
||||
.filter((element) => isValidKey(element))
|
||||
.forEach((key: string) => {
|
||||
const handler = listeners[key];
|
||||
if (typeof handler === 'function') {
|
||||
if (key === 'onInit') {
|
||||
handler(initEvent, editor);
|
||||
} else {
|
||||
editor.on(key.slice(2), (e: any) => handler(e, editor));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
111
apps/web-tdesign/src/components/tinymce/img-upload.vue
Normal file
111
apps/web-tdesign/src/components/tinymce/img-upload.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RequestMethodResponse, UploadFile } from 'tdesign-vue-next';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, Upload } from 'tdesign-vue-next';
|
||||
|
||||
import { useUpload } from '#/components/upload/use-upload';
|
||||
|
||||
defineOptions({ name: 'TinymceImageUpload' });
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
fullscreen: {
|
||||
// 图片上传,是否放到全屏的位置
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['uploading', 'done', 'error']);
|
||||
|
||||
const uploading = ref(false);
|
||||
|
||||
const getButtonProps = computed(() => {
|
||||
const { disabled } = props;
|
||||
return {
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
|
||||
async function customRequest(
|
||||
info: UploadFile | UploadFile[],
|
||||
): Promise<RequestMethodResponse> {
|
||||
// 处理单个文件上传
|
||||
const uploadFile = Array.isArray(info) ? info[0] : info;
|
||||
|
||||
if (!uploadFile) {
|
||||
return {
|
||||
status: 'fail',
|
||||
error: 'No file provided',
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
|
||||
// 1. emit 上传中
|
||||
const file = uploadFile.raw as File;
|
||||
const name = file?.name;
|
||||
if (!uploading.value) {
|
||||
emit('uploading', name);
|
||||
uploading.value = true;
|
||||
}
|
||||
|
||||
// 2. 执行上传
|
||||
const { httpRequest } = useUpload();
|
||||
try {
|
||||
const url = await httpRequest(file);
|
||||
emit('done', name, url);
|
||||
uploadFile.onSuccess?.(url);
|
||||
return {
|
||||
status: 'success',
|
||||
response: {
|
||||
url,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
emit('error', name);
|
||||
uploadFile.onError?.(error);
|
||||
return {
|
||||
status: 'fail',
|
||||
error: error instanceof Error ? error.message : 'Upload failed',
|
||||
response: {},
|
||||
};
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :class="[{ fullscreen }]" class="tinymce-image-upload">
|
||||
<Upload
|
||||
:show-upload-list="false"
|
||||
accept=".jpg,.jpeg,.gif,.png,.webp"
|
||||
multiple
|
||||
:request-method="customRequest"
|
||||
>
|
||||
<Button theme="primary" v-bind="{ ...getButtonProps }">
|
||||
{{ $t('ui.upload.imgUpload') }}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tinymce-image-upload {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
z-index: 20;
|
||||
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
apps/web-tdesign/src/components/tinymce/index.ts
Normal file
1
apps/web-tdesign/src/components/tinymce/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Tinymce } from './editor.vue';
|
||||
17
apps/web-tdesign/src/components/tinymce/tinymce.ts
Normal file
17
apps/web-tdesign/src/components/tinymce/tinymce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Any plugins you want to setting has to be imported
|
||||
// Detail plugins list see https://www.tiny.cloud/docs/plugins/
|
||||
// Custom builds see https://www.tiny.cloud/download/custom-builds/
|
||||
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
|
||||
|
||||
export const plugins =
|
||||
'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help emoticons accordion';
|
||||
|
||||
// 和 vben2.0 不同,从 https://www.tiny.cloud/ 拷贝 Vue 部分,然后去掉 importword exportword exportpdf | math 部分,并额外增加最后一行(来自 vben2.0 差异的部分)
|
||||
export const toolbar =
|
||||
'undo redo | accordion accordionremove | \\\n' +
|
||||
' blocks fontfamily fontsize | bold italic underline strikethrough | \\\n' +
|
||||
' align numlist bullist | link image | table media | \\\n' +
|
||||
' lineheight outdent indent | forecolor backcolor removeformat | \\\n' +
|
||||
' charmap emoticons | code fullscreen preview | save print | \\\n' +
|
||||
' pagebreak anchor codesample | ltr rtl | \\\n' +
|
||||
' hr searchreplace alignleft aligncenter alignright blockquote subscript superscript';
|
||||
386
apps/web-tdesign/src/components/upload/file-upload.vue
Normal file
386
apps/web-tdesign/src/components/upload/file-upload.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
RequestMethodResponse,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
} from 'tdesign-vue-next';
|
||||
|
||||
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,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { Button, Upload } from 'tdesign-vue-next';
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
import { UploadResultStatus } from './typing';
|
||||
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 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<UploadProps['files']>([]);
|
||||
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 {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['files'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
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: UploadFile) {
|
||||
emit('preview', file);
|
||||
}
|
||||
|
||||
// 处理文件数量超限
|
||||
function handleExceed() {
|
||||
message.error($t('ui.upload.maxNumber', [maxNumber.value]));
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error('上传失败');
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
async function beforeUpload(file: UploadFile) {
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
|
||||
// 检查文件数量限制
|
||||
if (
|
||||
isNumber(fileList.value!.length) &&
|
||||
fileList.value!.length >= props.maxNumber
|
||||
) {
|
||||
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkFileType(file.raw!, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
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 Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(
|
||||
info: UploadFile | UploadFile[],
|
||||
): Promise<RequestMethodResponse> {
|
||||
// 处理单个文件上传
|
||||
const uploadFile = Array.isArray(info) ? info[0] : info;
|
||||
|
||||
if (!uploadFile) {
|
||||
return {
|
||||
status: 'fail' as const,
|
||||
error: 'No file provided',
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
uploadFile.onProgress?.({ percent });
|
||||
};
|
||||
const res = await api?.(uploadFile.raw!, progressEvent);
|
||||
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res!, uploadFile.raw as File);
|
||||
|
||||
uploadFile.onSuccess!(res);
|
||||
message.success($t('ui.upload.uploadSuccess'));
|
||||
|
||||
// 提取 URL,兼容不同的返回格式
|
||||
const fileUrl = (res as any)?.url || (res as any)?.data || res;
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
response: {
|
||||
url: fileUrl,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
uploadFile.onError!(error);
|
||||
handleUploadError(error);
|
||||
return {
|
||||
status: 'fail' as const,
|
||||
error: error instanceof Error ? error.message : 'Upload failed',
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
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({
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
uid: file.name + Date.now(),
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
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 === UploadResultStatus.SUCCESS)
|
||||
.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(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:request-method="customRequest"
|
||||
:disabled="disabled"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
list-type="text"
|
||||
:progress="{ showInfo: true }"
|
||||
:show-upload-list="{
|
||||
showPreviewIcon: true,
|
||||
showRemoveIcon: true,
|
||||
showDownloadIcon: true,
|
||||
}"
|
||||
@remove="handleRemove"
|
||||
@preview="handlePreview"
|
||||
@reject="handleExceed"
|
||||
>
|
||||
<div v-if="drag" class="upload-drag-area">
|
||||
<p class="upload-drag-icon">
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
</p>
|
||||
<p class="upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p class="upload-hint">
|
||||
支持{{ accept.join('/') }}格式文件,不超过{{ maxSize }}MB
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="fileList && fileList.length < maxNumber">
|
||||
<Button>
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
{{ $t('ui.upload.upload') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDescription && !drag"
|
||||
class="mt-2 flex flex-wrap items-center"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-drag-area {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: var(--td-bg-color-container, #fafafa);
|
||||
border: 2px dashed var(--td-border-level-2-color, #d9d9d9);
|
||||
border-radius: var(--td-radius-default, 8px);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.upload-drag-area:hover {
|
||||
border-color: var(--td-brand-color, #0052d9);
|
||||
}
|
||||
|
||||
.upload-drag-icon {
|
||||
margin-bottom: 16px;
|
||||
font-size: 48px;
|
||||
color: var(--td-text-color-placeholder, #d9d9d9);
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
color: var(--td-text-color-secondary, #666);
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 14px;
|
||||
color: var(--td-text-color-placeholder, #999);
|
||||
}
|
||||
</style>
|
||||
368
apps/web-tdesign/src/components/upload/image-upload.vue
Normal file
368
apps/web-tdesign/src/components/upload/image-upload.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps } from 'tdesign-vue-next';
|
||||
|
||||
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,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { Dialog, Upload } from 'tdesign-vue-next';
|
||||
|
||||
import { message } from '#/adapter/tdesign';
|
||||
|
||||
import { UploadResultStatus } from './typing';
|
||||
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 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<UploadProps['files']>([]);
|
||||
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 {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['files'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => {
|
||||
resolve(reader.result as T);
|
||||
});
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file.url && !file.preview) {
|
||||
// TDesign 使用 raw 而不是 originFileObj
|
||||
file.preview = await getBase64<string>(file.raw!);
|
||||
}
|
||||
previewImage.value = file.url || file.preview || '';
|
||||
previewOpen.value = true;
|
||||
previewTitle.value =
|
||||
file.name ||
|
||||
previewImage.value.slice(
|
||||
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
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 handleCancel() {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
async function beforeUpload(file: UploadFile) {
|
||||
// 检查文件数量限制
|
||||
if (
|
||||
isNumber(fileList.value!.length) &&
|
||||
fileList.value!.length >= props.maxNumber
|
||||
) {
|
||||
message.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = isImage(file.raw!.name, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
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 Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadFile | UploadFile[]) {
|
||||
// 处理单个文件上传
|
||||
const uploadFile = Array.isArray(info) ? info[0] : info;
|
||||
|
||||
if (!uploadFile) {
|
||||
return {
|
||||
status: 'fail' as const,
|
||||
error: 'No file provided',
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
uploadFile.onProgress!({ percent });
|
||||
};
|
||||
// TDesign 使用 raw 而不是 file
|
||||
const res = await api?.(uploadFile.raw as File, progressEvent);
|
||||
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res, uploadFile.raw as File);
|
||||
|
||||
uploadFile.onSuccess!(res);
|
||||
message.success($t('ui.upload.uploadSuccess'));
|
||||
|
||||
// 提取 URL,兼容不同的返回格式
|
||||
const fileUrl = (res as any)?.url || (res as any)?.data || res;
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
response: {
|
||||
url: fileUrl,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
uploadFile.onError!(error);
|
||||
handleUploadError(error);
|
||||
return {
|
||||
status: 'fail' as const,
|
||||
error: error instanceof Error ? error.message : 'Upload failed',
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
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({
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
uid: file.name + Date.now(),
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
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);
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.SUCCESS)
|
||||
.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(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:request-method="customRequest"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
:progress="{ showInfo: true }"
|
||||
@preview="handlePreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div
|
||||
v-if="fileList && fileList.length < maxNumber"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-sm"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="mx-1 font-bold text-primary">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
<Dialog
|
||||
:footer="false"
|
||||
:visible="previewOpen"
|
||||
:header="previewTitle"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<img :src="previewImage" alt="" class="w-full" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* TDesign 上传组件样式 */
|
||||
:deep(.t-upload__card-item) {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
</style>
|
||||
3
apps/web-tdesign/src/components/upload/index.ts
Normal file
3
apps/web-tdesign/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';
|
||||
74
apps/web-tdesign/src/components/upload/input-upload.vue
Normal file
74
apps/web-tdesign/src/components/upload/input-upload.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputProps, TextareaProps } from 'tdesign-vue-next';
|
||||
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Input, Row, Textarea } from 'tdesign-vue-next';
|
||||
|
||||
import FileUpload from './file-upload.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: number | string;
|
||||
fileUploadProps?: FileUploadProps;
|
||||
inputProps?: InputProps;
|
||||
inputType?: 'input' | 'textarea';
|
||||
modelValue?: number | string;
|
||||
textareaProps?: TextareaProps;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'change', payload: number | string): void;
|
||||
(e: 'update:value', payload: number | string): void;
|
||||
(e: 'update:modelValue', payload: number | 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 inputProps = computed(() => {
|
||||
return {
|
||||
...props.inputProps,
|
||||
value: modelValue.value,
|
||||
};
|
||||
});
|
||||
|
||||
const textareaProps = computed(() => {
|
||||
return {
|
||||
...props.textareaProps,
|
||||
value: modelValue.value,
|
||||
};
|
||||
});
|
||||
|
||||
const fileUploadProps = computed(() => {
|
||||
return {
|
||||
...props.fileUploadProps,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Row>
|
||||
<Col :span="18">
|
||||
<Input readonly v-if="inputType === 'input'" v-bind="inputProps" />
|
||||
<Textarea readonly v-else :row="4" v-bind="textareaProps" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<FileUpload
|
||||
class="ml-4"
|
||||
v-bind="fileUploadProps"
|
||||
@return-text="handleReturnText"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</template>
|
||||
39
apps/web-tdesign/src/components/upload/typing.ts
Normal file
39
apps/web-tdesign/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 {
|
||||
ERROR = 'error',
|
||||
PROGRESS = 'progress',
|
||||
SUCCESS = 'success',
|
||||
WAITING = 'waiting',
|
||||
}
|
||||
|
||||
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-tdesign/src/components/upload/use-upload.ts
Normal file
168
apps/web-tdesign/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;
|
||||
}
|
||||
23
apps/web-tdesign/src/layouts/auth.vue
Normal file
23
apps/web-tdesign/src/layouts/auth.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<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);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
</AuthPageLayout>
|
||||
</template>
|
||||
206
apps/web-tdesign/src/layouts/basic.vue
Normal file
206
apps/web-tdesign/src/layouts/basic.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
LockScreen,
|
||||
Notification,
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
date: '3小时前',
|
||||
isRead: true,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '收到了 14 份新周报',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '刚刚',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '朱偏右 回复了你',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '2024-01-01',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '曲丽丽 评论了你',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '代办提醒',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '跳转Workspace示例',
|
||||
link: '/workspace',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '跳转外部链接示例',
|
||||
link: 'https://doc.vben.pro',
|
||||
},
|
||||
]);
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||
const showDot = computed(() =>
|
||||
notifications.value.some((item) => !item.isRead),
|
||||
);
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
handler: () => {
|
||||
router.push({ name: 'Profile' });
|
||||
},
|
||||
icon: 'lucide:user',
|
||||
text: $t('page.auth.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: () => {
|
||||
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: CircleHelp,
|
||||
text: $t('ui.widgets.qa'),
|
||||
},
|
||||
]);
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
notifications.value = [];
|
||||
}
|
||||
|
||||
function markRead(id: number | string) {
|
||||
const item = notifications.value.find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.isRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(id: number | string) {
|
||||
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||
}
|
||||
|
||||
function handleMakeAll() {
|
||||
notifications.value.forEach((item) => (item.isRead = true));
|
||||
}
|
||||
watch(
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
content: preferences.app.watermarkContent,
|
||||
}),
|
||||
async ({ enable, content }) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content:
|
||||
content ||
|
||||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||
<template #user-dropdown>
|
||||
<UserDropdown
|
||||
:avatar
|
||||
:menus
|
||||
:text="userStore.userInfo?.realName"
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
<Notification
|
||||
:dot="showDot"
|
||||
:notifications="notifications"
|
||||
@clear="handleNoticeClear"
|
||||
@read="(item) => item.id && markRead(item.id)"
|
||||
@remove="(item) => item.id && remove(item.id)"
|
||||
@make-all="handleMakeAll"
|
||||
/>
|
||||
</template>
|
||||
<template #extra>
|
||||
<AuthenticationLoginExpiredModal
|
||||
v-model:open="accessStore.loginExpired"
|
||||
:avatar
|
||||
>
|
||||
<LoginForm />
|
||||
</AuthenticationLoginExpiredModal>
|
||||
</template>
|
||||
<template #lock-screen>
|
||||
<LockScreen :avatar @to-login="handleLogout" />
|
||||
</template>
|
||||
</BasicLayout>
|
||||
</template>
|
||||
6
apps/web-tdesign/src/layouts/index.ts
Normal file
6
apps/web-tdesign/src/layouts/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const BasicLayout = () => import('./basic.vue');
|
||||
const AuthPageLayout = () => import('./auth.vue');
|
||||
|
||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||
|
||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||
3
apps/web-tdesign/src/locales/README.md
Normal file
3
apps/web-tdesign/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
77
apps/web-tdesign/src/locales/index.ts
Normal file
77
apps/web-tdesign/src/locales/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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-tdesign/src/locales/langs/en-US/page.json
Normal file
34
apps/web-tdesign/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"
|
||||
}
|
||||
}
|
||||
14
apps/web-tdesign/src/locales/langs/en-US/utils.json
Normal file
14
apps/web-tdesign/src/locales/langs/en-US/utils.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rangePicker": {
|
||||
"today": "Today",
|
||||
"last7Days": "Last 7 Days",
|
||||
"last30Days": "Last 30 Days",
|
||||
"yesterday": "Yesterday",
|
||||
"thisWeek": "This Week",
|
||||
"thisMonth": "This Month",
|
||||
"lastWeek": "Last Week",
|
||||
"lastMonth": "Last Month",
|
||||
"beginTime": "Begin Time",
|
||||
"endTime": "End Time"
|
||||
}
|
||||
}
|
||||
34
apps/web-tdesign/src/locales/langs/zh-CN/page.json
Normal file
34
apps/web-tdesign/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": "切换租户成功"
|
||||
}
|
||||
}
|
||||
14
apps/web-tdesign/src/locales/langs/zh-CN/utils.json
Normal file
14
apps/web-tdesign/src/locales/langs/zh-CN/utils.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rangePicker": {
|
||||
"today": "今天",
|
||||
"last7Days": "最近 7 天",
|
||||
"last30Days": "最近 30 天",
|
||||
"yesterday": "昨天",
|
||||
"thisWeek": "本周",
|
||||
"thisMonth": "本月",
|
||||
"lastWeek": "上周",
|
||||
"lastMonth": "上月",
|
||||
"beginTime": "开始时间",
|
||||
"endTime": "结束时间"
|
||||
}
|
||||
}
|
||||
31
apps/web-tdesign/src/main.ts
Normal file
31
apps/web-tdesign/src/main.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
async function initApplication() {
|
||||
// name用于指定项目唯一标识
|
||||
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
// 启动应用并挂载
|
||||
// vue应用主要逻辑及视图
|
||||
const { bootstrap } = await import('./bootstrap');
|
||||
await bootstrap(namespace);
|
||||
|
||||
// 移除并销毁loading
|
||||
unmountGlobalLoading();
|
||||
}
|
||||
|
||||
initApplication();
|
||||
25
apps/web-tdesign/src/preferences.ts
Normal file
25
apps/web-tdesign/src/preferences.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||
*/
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
/** 后端路由模式 */
|
||||
accessMode: 'backend',
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
enableRefreshToken: true,
|
||||
},
|
||||
footer: {
|
||||
/** 默认关闭 footer 页脚,因为有一定遮挡 */
|
||||
enable: false,
|
||||
fixed: false,
|
||||
},
|
||||
copyright: {
|
||||
companyName: import.meta.env.VITE_APP_TITLE,
|
||||
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
|
||||
},
|
||||
});
|
||||
41
apps/web-tdesign/src/router/access.ts
Normal file
41
apps/web-tdesign/src/router/access.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
AppRouteRecordRaw,
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
// 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示
|
||||
// 补充说明:accessStore.accessMenus 一开始是 AppRouteRecordRaw 类型(后端加载),后面被赋值成 MenuRecordRaw 类型(前端转换)
|
||||
const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[];
|
||||
return convertServerMenuToRouteRecordStringComponent(accessMenus);
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
||||
155
apps/web-tdesign/src/router/guard.ts
Normal file
155
apps/web-tdesign/src/router/guard.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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/tdesign';
|
||||
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({
|
||||
content: `${$t('common.loadingMenu')}...`,
|
||||
});
|
||||
try {
|
||||
const authPermissionInfo = await authStore.fetchUserInfo();
|
||||
if (authPermissionInfo) {
|
||||
userInfo = authPermissionInfo.user;
|
||||
}
|
||||
} finally {
|
||||
message.close(loading);
|
||||
}
|
||||
}
|
||||
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 };
|
||||
40
apps/web-tdesign/src/router/index.ts
Normal file
40
apps/web-tdesign/src/router/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import { resetStaticRoutes } from '@vben/utils';
|
||||
|
||||
import { createRouterGuard } from './guard';
|
||||
import { routes } from './routes';
|
||||
import { setupBaiduTongJi } from './tongji';
|
||||
|
||||
/**
|
||||
* @zh_CN 创建vue-router实例
|
||||
*/
|
||||
const router = createRouter({
|
||||
history:
|
||||
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||
: createWebHistory(import.meta.env.VITE_BASE),
|
||||
// 应该添加到路由的初始路由列表。
|
||||
routes,
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||
},
|
||||
// 是否应该禁止尾部斜杠。
|
||||
// strict: true,
|
||||
});
|
||||
|
||||
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||
|
||||
// 创建路由守卫
|
||||
createRouterGuard(router);
|
||||
// 设置百度统计
|
||||
setupBaiduTongJi(router);
|
||||
|
||||
export { resetRoutes, router };
|
||||
114
apps/web-tdesign/src/router/routes/core.ts
Normal file
114
apps/web-tdesign/src/router/routes/core.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const BasicLayout = () => import('#/layouts/basic.vue');
|
||||
const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||
/** 全局404页面 */
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
},
|
||||
name: 'FallbackNotFound',
|
||||
path: '/:path(.*)*',
|
||||
};
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: preferences.app.defaultHomePath,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
meta: {
|
||||
hideInTab: true,
|
||||
title: 'Authentication',
|
||||
},
|
||||
name: 'Authentication',
|
||||
path: '/auth',
|
||||
redirect: LOGIN_PATH,
|
||||
children: [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CodeLogin',
|
||||
path: 'code-login',
|
||||
component: () => import('#/views/_core/authentication/code-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.codeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'QrCodeLogin',
|
||||
path: 'qrcode-login',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/qrcode-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.qrcodeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ForgetPassword',
|
||||
path: 'forget-password',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/forget-password.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.forgetPassword'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Register',
|
||||
path: 'register',
|
||||
component: () => import('#/views/_core/authentication/register.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.register'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SocialLogin',
|
||||
path: 'social-login',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/social-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SSOLogin',
|
||||
path: 'sso-login',
|
||||
component: () => import('#/views/_core/authentication/sso-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
47
apps/web-tdesign/src/router/routes/index.ts
Normal file
47
apps/web-tdesign/src/router/routes/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||
|
||||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
|
||||
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45
|
||||
const componentKeys: string[] = Object.keys(
|
||||
import.meta.glob('../../views/**/*.vue'),
|
||||
)
|
||||
.filter((item) => !item.includes('/modules/'))
|
||||
.map((v) => {
|
||||
const path = v.replace('../../views/', '/');
|
||||
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||
});
|
||||
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||
38
apps/web-tdesign/src/router/routes/modules/dashboard.ts
Normal file
38
apps/web-tdesign/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-tdesign/src/router/routes/modules/infra.ts
Normal file
30
apps/web-tdesign/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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user