feat: 添加完整的前端管理系统 (VbenAdmin)
- 添加基于 VbenAdmin + Vue3 + Element Plus 的前端管理系统 - 包含完整的 UI 组件库和工具链 - 支持多应用架构 (web-ele, backend-mock, playground) - 包含完整的开发规范和配置 - 修复 admin 目录的子模块问题,确保正确提交
This commit is contained in:
331
admin/apps/web-ele/src/adapter/component/index.ts
Normal file
331
admin/apps/web-ele/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
|
||||
const ElButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/button/index'),
|
||||
import('element-plus/es/components/button/style/css'),
|
||||
]).then(([res]) => res.ElButton),
|
||||
);
|
||||
const ElCheckbox = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox/style/css'),
|
||||
]).then(([res]) => res.ElCheckbox),
|
||||
);
|
||||
const ElCheckboxButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-button/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxButton),
|
||||
);
|
||||
const ElCheckboxGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-group/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxGroup),
|
||||
);
|
||||
const ElDatePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/date-picker/index'),
|
||||
import('element-plus/es/components/date-picker/style/css'),
|
||||
]).then(([res]) => res.ElDatePicker),
|
||||
);
|
||||
const ElDivider = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/divider/index'),
|
||||
import('element-plus/es/components/divider/style/css'),
|
||||
]).then(([res]) => res.ElDivider),
|
||||
);
|
||||
const ElInput = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input/index'),
|
||||
import('element-plus/es/components/input/style/css'),
|
||||
]).then(([res]) => res.ElInput),
|
||||
);
|
||||
const ElInputNumber = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input-number/index'),
|
||||
import('element-plus/es/components/input-number/style/css'),
|
||||
]).then(([res]) => res.ElInputNumber),
|
||||
);
|
||||
const ElRadio = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio/style/css'),
|
||||
]).then(([res]) => res.ElRadio),
|
||||
);
|
||||
const ElRadioButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-button/style/css'),
|
||||
]).then(([res]) => res.ElRadioButton),
|
||||
);
|
||||
const ElRadioGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-group/style/css'),
|
||||
]).then(([res]) => res.ElRadioGroup),
|
||||
);
|
||||
const ElSelectV2 = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/select-v2/index'),
|
||||
import('element-plus/es/components/select-v2/style/css'),
|
||||
]).then(([res]) => res.ElSelectV2),
|
||||
);
|
||||
const ElSpace = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/space/index'),
|
||||
import('element-plus/es/components/space/style/css'),
|
||||
]).then(([res]) => res.ElSpace),
|
||||
);
|
||||
const ElSwitch = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/switch/index'),
|
||||
import('element-plus/es/components/switch/style/css'),
|
||||
]).then(([res]) => res.ElSwitch),
|
||||
);
|
||||
const ElTimePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/time-picker/index'),
|
||||
import('element-plus/es/components/time-picker/style/css'),
|
||||
]).then(([res]) => res.ElTimePicker),
|
||||
);
|
||||
const ElTreeSelect = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/tree-select/index'),
|
||||
import('element-plus/es/components/tree-select/style/css'),
|
||||
]).then(([res]) => res.ElTreeSelect),
|
||||
);
|
||||
const ElUpload = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/upload/index'),
|
||||
import('element-plus/es/components/upload/style/css'),
|
||||
]).then(([res]) => res.ElUpload),
|
||||
);
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: ElTreeSelect,
|
||||
props: { label: 'label', children: 'children' },
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'loading',
|
||||
optionsPropName: 'data',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options, isButton } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(isButton ? ElCheckboxButton : ElCheckbox, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'append',
|
||||
modelValueProp: 'model-value',
|
||||
inputComponent: ElInput,
|
||||
}),
|
||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? ElRadioButton : ElRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Select: (props, { attrs, slots }) => {
|
||||
return h(ElSelectV2, { ...props, attrs }, slots);
|
||||
},
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, isRange } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (isRange) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElTimePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
DatePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, type } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (type && type.includes('range')) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElDatePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
||||
Upload: ElUpload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
ElNotification({
|
||||
title,
|
||||
message: content,
|
||||
position: 'bottom-right',
|
||||
duration: 0,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
41
admin/apps/web-ele/src/adapter/form.ts
Normal file
41
admin/apps/web-ele/src/adapter/form.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
70
admin/apps/web-ele/src/adapter/vxe-table.ts
Normal file
70
admin/apps/web-ele/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { ElButton, ElImage } from 'element-plus';
|
||||
|
||||
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,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
const src = row[column.field];
|
||||
return h(ElImage, { src, previewSrcList: [src] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
ElButton,
|
||||
{ size: 'small', link: true },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
334
admin/apps/web-ele/src/api/common/auth.ts
Normal file
334
admin/apps/web-ele/src/api/common/auth.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 用户相关接口
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
status: number;
|
||||
roles: Role[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateUserDto {
|
||||
username: string;
|
||||
realname: string;
|
||||
password: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
role_ids: string[];
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface UpdateUserDto {
|
||||
realname?: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
role_ids?: string[];
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface UserListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
username?: string;
|
||||
realname?: string;
|
||||
status?: number;
|
||||
role_id?: string;
|
||||
}
|
||||
|
||||
// 角色相关接口
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
sort: number;
|
||||
remark?: string;
|
||||
status: number;
|
||||
permissions: Permission[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateRoleDto {
|
||||
name: string;
|
||||
code: string;
|
||||
sort: number;
|
||||
remark?: string;
|
||||
status: number;
|
||||
permission_ids: string[];
|
||||
}
|
||||
|
||||
export interface UpdateRoleDto {
|
||||
name?: string;
|
||||
code?: string;
|
||||
sort?: number;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
permission_ids?: string[];
|
||||
}
|
||||
|
||||
export interface RoleListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// 权限相关接口
|
||||
export interface Permission {
|
||||
id: string;
|
||||
parent_id?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: 'button' | 'menu';
|
||||
path?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
status: number;
|
||||
children?: Permission[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 菜单相关接口
|
||||
export interface Menu {
|
||||
id: string;
|
||||
parent_id?: string;
|
||||
type: 'button' | 'directory' | 'menu';
|
||||
name: string;
|
||||
title: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
is_link: boolean;
|
||||
link_url?: string;
|
||||
is_show: boolean;
|
||||
permission?: string;
|
||||
status: number;
|
||||
children?: Menu[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateMenuDto {
|
||||
parent_id?: string;
|
||||
type: 'button' | 'directory' | 'menu';
|
||||
name: string;
|
||||
title: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
sort: number;
|
||||
is_link: boolean;
|
||||
link_url?: string;
|
||||
is_show: boolean;
|
||||
permission?: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface UpdateMenuDto {
|
||||
parent_id?: string;
|
||||
type?: 'button' | 'directory' | 'menu';
|
||||
name?: string;
|
||||
title?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
sort?: number;
|
||||
is_link?: boolean;
|
||||
link_url?: string;
|
||||
is_show?: boolean;
|
||||
permission?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface MenuListParams {
|
||||
name?: string;
|
||||
title?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// API 接口定义
|
||||
|
||||
// 用户管理接口
|
||||
export const getUserListApi = (params: UserListParams) => {
|
||||
return requestClient.get<{
|
||||
limit: number;
|
||||
list: User[];
|
||||
page: number;
|
||||
total: number;
|
||||
}>('/api/system/users', { params });
|
||||
};
|
||||
|
||||
export const getUserByIdApi = (id: string) => {
|
||||
return requestClient.get<User>(`/api/system/users/${id}`);
|
||||
};
|
||||
|
||||
export const createUserApi = (data: CreateUserDto) => {
|
||||
return requestClient.post<User>('/api/system/users', data);
|
||||
};
|
||||
|
||||
export const updateUserApi = (id: string, data: UpdateUserDto) => {
|
||||
return requestClient.put<User>(`/api/system/users/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteUserApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/users/${id}`);
|
||||
};
|
||||
|
||||
export const batchDeleteAdminApi = (ids: string[]) => {
|
||||
return requestClient.delete('/admin/user/batch-delete', { data: { ids } });
|
||||
};
|
||||
|
||||
// 管理员相关接口
|
||||
export interface AdminUser {
|
||||
id: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
status: number;
|
||||
roles: Role[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateAdminParams {
|
||||
username: string;
|
||||
realname: string;
|
||||
password: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
role_ids: string[];
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface UpdateAdminParams {
|
||||
realname?: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
role_ids?: string[];
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const getAdminListApi = (params: UserListParams) => {
|
||||
return requestClient.get('/admin/user/list', {
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const createAdminApi = (data: CreateAdminParams) => {
|
||||
return requestClient.post('/admin/user/create', data);
|
||||
};
|
||||
|
||||
export const updateAdminApi = (id: string, data: UpdateAdminParams) => {
|
||||
return requestClient.put(`/admin/user/update/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteAdminApi = (id: string) => {
|
||||
return requestClient.delete(`/admin/user/delete/${id}`);
|
||||
};
|
||||
|
||||
export const setAdminRolesApi = (id: string, role_ids: string[]) => {
|
||||
return requestClient.put(`/admin/user/roles/${id}`, { role_ids });
|
||||
};
|
||||
|
||||
export const getAdminRolesApi = (id: string) => {
|
||||
return requestClient.get(`/admin/user/roles/${id}`);
|
||||
};
|
||||
|
||||
export const lockUserApi = (id: string) => {
|
||||
return requestClient.post(`/api/system/users/${id}/lock`);
|
||||
};
|
||||
|
||||
export const unlockUserApi = (id: string) => {
|
||||
return requestClient.post(`/api/system/users/${id}/unlock`);
|
||||
};
|
||||
|
||||
export const resetPasswordApi = (id: string, password: string) => {
|
||||
return requestClient.post(`/api/system/users/${id}/reset-password`, {
|
||||
password,
|
||||
});
|
||||
};
|
||||
|
||||
// 角色管理接口
|
||||
export const getRoleListApi = (params: RoleListParams) => {
|
||||
return requestClient.get<{
|
||||
limit: number;
|
||||
list: Role[];
|
||||
page: number;
|
||||
total: number;
|
||||
}>('/api/system/roles', { params });
|
||||
};
|
||||
|
||||
export const getAllRolesApi = () => {
|
||||
return requestClient.get<Role[]>('/api/system/roles/all');
|
||||
};
|
||||
|
||||
export const getRoleByIdApi = (id: string) => {
|
||||
return requestClient.get<Role>(`/api/system/roles/${id}`);
|
||||
};
|
||||
|
||||
export const createRoleApi = (data: CreateRoleDto) => {
|
||||
return requestClient.post<Role>('/api/system/roles', data);
|
||||
};
|
||||
|
||||
export const updateRoleApi = (id: string, data: UpdateRoleDto) => {
|
||||
return requestClient.put<Role>(`/api/system/roles/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteRoleApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/roles/${id}`);
|
||||
};
|
||||
|
||||
// 权限管理接口
|
||||
export const getPermissionListApi = () => {
|
||||
return requestClient.get<Permission[]>('/api/system/permissions');
|
||||
};
|
||||
|
||||
export const getPermissionTreeApi = () => {
|
||||
return requestClient.get<Permission[]>('/api/system/permissions/tree');
|
||||
};
|
||||
|
||||
// 菜单管理接口
|
||||
export const getMenuListApi = (params: MenuListParams) => {
|
||||
return requestClient.get<Menu[]>('/api/system/menus', { params });
|
||||
};
|
||||
|
||||
export const getMenuTreeApi = () => {
|
||||
return requestClient.get<Menu[]>('/api/system/menus/tree');
|
||||
};
|
||||
|
||||
export const getMenuByIdApi = (id: string) => {
|
||||
return requestClient.get<Menu>(`/api/system/menus/${id}`);
|
||||
};
|
||||
|
||||
export const createMenuApi = (data: CreateMenuDto) => {
|
||||
return requestClient.post<Menu>('/api/system/menus', data);
|
||||
};
|
||||
|
||||
export const updateMenuApi = (id: string, data: UpdateMenuDto) => {
|
||||
return requestClient.put<Menu>(`/api/system/menus/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteMenuApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/menus/${id}`);
|
||||
};
|
||||
|
||||
// 获取用户菜单(用于导航)
|
||||
export const getUserMenusApi = () => {
|
||||
return requestClient.get<Menu[]>('/api/system/menus/user');
|
||||
};
|
||||
350
admin/apps/web-ele/src/api/common/common.ts
Normal file
350
admin/apps/web-ele/src/api/common/common.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 文件上传相关接口
|
||||
export interface UploadFile {
|
||||
id: string;
|
||||
original_name: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
url: string;
|
||||
mime_type: string;
|
||||
size: number;
|
||||
driver: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
file: UploadFile;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 字典相关接口
|
||||
export interface Dictionary {
|
||||
id: string;
|
||||
parent_id?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
value?: string;
|
||||
sort: number;
|
||||
status: number;
|
||||
remark?: string;
|
||||
children?: Dictionary[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateDictionaryDto {
|
||||
parent_id?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
value?: string;
|
||||
sort: number;
|
||||
status: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface UpdateDictionaryDto {
|
||||
parent_id?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
value?: string;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface DictionaryListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
parent_id?: string;
|
||||
}
|
||||
|
||||
// 系统日志相关接口
|
||||
export interface SystemLog {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
username?: string;
|
||||
action: string;
|
||||
method: string;
|
||||
url: string;
|
||||
ip: string;
|
||||
user_agent: string;
|
||||
request_data?: any;
|
||||
response_data?: any;
|
||||
status_code: number;
|
||||
duration: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface LogListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
user_id?: string;
|
||||
username?: string;
|
||||
action?: string;
|
||||
method?: string;
|
||||
status_code?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
// 系统信息接口
|
||||
export interface SystemInfo {
|
||||
server: {
|
||||
arch: string;
|
||||
memory_usage: {
|
||||
external: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
rss: number;
|
||||
};
|
||||
node_version: string;
|
||||
os: string;
|
||||
uptime: number;
|
||||
};
|
||||
database: {
|
||||
size: string;
|
||||
type: string;
|
||||
version: string;
|
||||
};
|
||||
redis?: {
|
||||
connected_clients: number;
|
||||
memory: string;
|
||||
version: string;
|
||||
};
|
||||
application: {
|
||||
environment: string;
|
||||
name: string;
|
||||
timezone: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// API 接口定义
|
||||
|
||||
// 文件上传接口
|
||||
export const uploadFileApi = (file: File, type?: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (type) {
|
||||
formData.append('type', type);
|
||||
}
|
||||
return requestClient.post<UploadResponse>('/api/system/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadMultipleFilesApi = (files: File[], type?: string) => {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
if (type) {
|
||||
formData.append('type', type);
|
||||
}
|
||||
return requestClient.post<UploadResponse[]>(
|
||||
'/api/system/upload/multiple',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const getFileListApi = (params: {
|
||||
filename?: string;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
type?: string;
|
||||
}) => {
|
||||
return requestClient.get<{
|
||||
limit: number;
|
||||
list: UploadFile[];
|
||||
page: number;
|
||||
total: number;
|
||||
}>('/api/system/files', { params });
|
||||
};
|
||||
|
||||
export const deleteFileApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/files/${id}`);
|
||||
};
|
||||
|
||||
// 字典管理接口
|
||||
export const getDictionaryListApi = (params: DictionaryListParams) => {
|
||||
return requestClient.get<{
|
||||
limit: number;
|
||||
list: Dictionary[];
|
||||
page: number;
|
||||
total: number;
|
||||
}>('/api/system/dictionaries', { params });
|
||||
};
|
||||
|
||||
export const getDictionaryTreeApi = (code?: string) => {
|
||||
return requestClient.get<Dictionary[]>('/api/system/dictionaries/tree', {
|
||||
params: { code },
|
||||
});
|
||||
};
|
||||
|
||||
export const getDictionaryByIdApi = (id: string) => {
|
||||
return requestClient.get<Dictionary>(`/api/system/dictionaries/${id}`);
|
||||
};
|
||||
|
||||
export const getDictionaryByCodeApi = (code: string) => {
|
||||
return requestClient.get<Dictionary[]>(
|
||||
`/api/system/dictionaries/code/${code}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const createDictionaryApi = (data: CreateDictionaryDto) => {
|
||||
return requestClient.post<Dictionary>('/api/system/dictionaries', data);
|
||||
};
|
||||
|
||||
export const updateDictionaryApi = (id: string, data: UpdateDictionaryDto) => {
|
||||
return requestClient.put<Dictionary>(`/api/system/dictionaries/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteDictionaryApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/dictionaries/${id}`);
|
||||
};
|
||||
|
||||
// 系统日志接口
|
||||
export const getSystemLogListApi = (params: LogListParams) => {
|
||||
return requestClient.get<{
|
||||
limit: number;
|
||||
list: SystemLog[];
|
||||
page: number;
|
||||
total: number;
|
||||
}>('/api/system/logs', { params });
|
||||
};
|
||||
|
||||
export const getSystemLogByIdApi = (id: string) => {
|
||||
return requestClient.get<SystemLog>(`/api/system/logs/${id}`);
|
||||
};
|
||||
|
||||
export const clearSystemLogsApi = (days?: number) => {
|
||||
return requestClient.delete('/api/system/logs/clear', {
|
||||
params: { days },
|
||||
});
|
||||
};
|
||||
|
||||
export const exportSystemLogsApi = (params: LogListParams) => {
|
||||
return requestClient.get('/api/system/logs/export', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
// 系统信息接口
|
||||
export const getSystemInfoApi = () => {
|
||||
return requestClient.get<SystemInfo>('/api/system/info');
|
||||
};
|
||||
|
||||
// 健康检查接口
|
||||
export const getHealthCheckApi = () => {
|
||||
return requestClient.get<{
|
||||
checks: {
|
||||
database: { responseTime?: number; status: 'down' | 'up' };
|
||||
disk: { status: 'critical' | 'ok' | 'warning'; usage: number };
|
||||
memory: { status: 'critical' | 'ok' | 'warning'; usage: number };
|
||||
redis?: { responseTime?: number; status: 'down' | 'up' };
|
||||
};
|
||||
status: 'error' | 'ok';
|
||||
timestamp: string;
|
||||
uptime: number;
|
||||
}>('/api/system/health');
|
||||
};
|
||||
|
||||
// 缓存管理接口
|
||||
export const getCacheInfoApi = () => {
|
||||
return requestClient.get<{
|
||||
hits: number;
|
||||
keys: number;
|
||||
memory: string;
|
||||
misses: number;
|
||||
}>('/api/system/cache/info');
|
||||
};
|
||||
|
||||
export const clearCacheApi = (pattern?: string) => {
|
||||
return requestClient.delete('/api/system/cache/clear', {
|
||||
params: { pattern },
|
||||
});
|
||||
};
|
||||
|
||||
// 系统配置接口
|
||||
export const getSystemConfigApi = () => {
|
||||
return requestClient.get<Record<string, any>>('/api/system/config');
|
||||
};
|
||||
|
||||
export const updateSystemConfigApi = (data: Record<string, any>) => {
|
||||
return requestClient.put('/api/system/config', data);
|
||||
};
|
||||
|
||||
// 数据备份接口
|
||||
export const createBackupApi = () => {
|
||||
return requestClient.post<{
|
||||
created_at: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}>('/api/system/backup');
|
||||
};
|
||||
|
||||
export const getBackupListApi = () => {
|
||||
return requestClient.get<
|
||||
{
|
||||
created_at: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}[]
|
||||
>('/api/system/backup/list');
|
||||
};
|
||||
|
||||
export const downloadBackupApi = (filename: string) => {
|
||||
return requestClient.get(`/api/system/backup/download/${filename}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteBackupApi = (filename: string) => {
|
||||
return requestClient.delete(`/api/system/backup/${filename}`);
|
||||
};
|
||||
|
||||
// 系统通知接口
|
||||
export interface SystemNotification {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: 'error' | 'info' | 'success' | 'warning';
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const getNotificationListApi = (params: {
|
||||
is_read?: boolean;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}) => {
|
||||
return requestClient.get<{
|
||||
list: SystemNotification[];
|
||||
total: number;
|
||||
unread_count: number;
|
||||
}>('/api/system/notifications', { params });
|
||||
};
|
||||
|
||||
export const markNotificationReadApi = (id: string) => {
|
||||
return requestClient.post(`/api/system/notifications/${id}/read`);
|
||||
};
|
||||
|
||||
export const markAllNotificationsReadApi = () => {
|
||||
return requestClient.post('/api/system/notifications/read-all');
|
||||
};
|
||||
|
||||
export const deleteNotificationApi = (id: string) => {
|
||||
return requestClient.delete(`/api/system/notifications/${id}`);
|
||||
};
|
||||
141
admin/apps/web-ele/src/api/common/email.ts
Normal file
141
admin/apps/web-ele/src/api/common/email.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 邮件配置接口
|
||||
*/
|
||||
export interface EmailConfig {
|
||||
// SMTP配置
|
||||
smtp_host?: string;
|
||||
smtp_port?: number;
|
||||
smtp_username?: string;
|
||||
smtp_password?: string;
|
||||
smtp_from_email?: string;
|
||||
smtp_from_name?: string;
|
||||
smtp_encryption?: string;
|
||||
smtp_enabled?: boolean;
|
||||
|
||||
// 邮件模板配置
|
||||
register_subject?: string;
|
||||
register_content?: string;
|
||||
register_enabled?: boolean;
|
||||
reset_subject?: string;
|
||||
reset_content?: string;
|
||||
reset_enabled?: boolean;
|
||||
notify_subject?: string;
|
||||
notify_content?: string;
|
||||
notify_enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件模板预览数据
|
||||
*/
|
||||
export interface EmailTemplatePreview {
|
||||
subject: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件配置
|
||||
* @param type 配置类型 smtp | template
|
||||
*/
|
||||
export function getEmailConfigApi(type: string) {
|
||||
return requestClient.get<EmailConfig>(`/api/common/email/config/${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新邮件配置
|
||||
* @param type 配置类型 smtp | template
|
||||
* @param data 配置数据
|
||||
*/
|
||||
export function updateEmailConfigApi(type: string, data: Partial<EmailConfig>) {
|
||||
return requestClient.put(`/api/common/email/config/${type}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试邮件发送
|
||||
* @param email 收件人邮箱
|
||||
* @param type 邮件类型 register | reset | notify
|
||||
*/
|
||||
export function testEmailApi(email: string, type: string) {
|
||||
return requestClient.post('/api/common/email/test', {
|
||||
email,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览邮件模板
|
||||
* @param type 模板类型 register | reset | notify
|
||||
*/
|
||||
export function previewEmailTemplateApi(type: string) {
|
||||
return requestClient.get<EmailTemplatePreview>(`/api/common/email/template/preview/${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置邮件配置
|
||||
* @param type 配置类型 smtp | template
|
||||
*/
|
||||
export function resetEmailConfigApi(type: string) {
|
||||
return requestClient.post(`/api/common/email/config/reset/${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件发送统计
|
||||
*/
|
||||
export function getEmailStatsApi() {
|
||||
return requestClient.get('/api/common/email/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件发送日志
|
||||
* @param params 查询参数
|
||||
*/
|
||||
export function getEmailLogsApi(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
type?: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}) {
|
||||
return requestClient.get('/api/common/email/logs', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空邮件发送队列
|
||||
*/
|
||||
export function clearEmailQueueApi() {
|
||||
return requestClient.post('/api/common/email/queue/clear');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试失败的邮件
|
||||
* @param id 邮件ID
|
||||
*/
|
||||
export function retryEmailApi(id: number) {
|
||||
return requestClient.post(`/api/common/email/retry/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重试失败的邮件
|
||||
* @param ids 邮件ID数组
|
||||
*/
|
||||
export function batchRetryEmailApi(ids: number[]) {
|
||||
return requestClient.post('/api/common/email/batch-retry', { ids });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除邮件日志
|
||||
* @param id 邮件ID
|
||||
*/
|
||||
export function deleteEmailLogApi(id: number) {
|
||||
return requestClient.delete(`/api/common/email/log/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除邮件日志
|
||||
* @param ids 邮件ID数组
|
||||
*/
|
||||
export function batchDeleteEmailLogApi(ids: number[]) {
|
||||
return requestClient.delete('/api/common/email/logs', { data: { ids } });
|
||||
}
|
||||
10
admin/apps/web-ele/src/api/common/index.ts
Normal file
10
admin/apps/web-ele/src/api/common/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// 统一导出系统管理相关的所有API接口
|
||||
|
||||
// 认证授权相关
|
||||
export * from './auth';
|
||||
|
||||
// 通用功能相关
|
||||
export * from './common';
|
||||
|
||||
// 系统设置相关
|
||||
export * from './settings';
|
||||
211
admin/apps/web-ele/src/api/common/login.ts
Normal file
211
admin/apps/web-ele/src/api/common/login.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { requestClient } from '#/api/request'
|
||||
|
||||
// 登录配置接口
|
||||
export interface LoginConfig {
|
||||
methods?: {
|
||||
username_enabled: boolean
|
||||
mobile_enabled: boolean
|
||||
email_enabled: boolean
|
||||
sms_enabled: boolean
|
||||
oauth_enabled: boolean
|
||||
guest_enabled: boolean
|
||||
}
|
||||
security?: {
|
||||
captcha_enabled: boolean
|
||||
captcha_type: 'image' | 'slide' | 'click'
|
||||
max_fail_attempts: number
|
||||
lock_duration: number
|
||||
password_strength_enabled: boolean
|
||||
password_min_length: number
|
||||
password_complexity: string[]
|
||||
password_expire_enabled: boolean
|
||||
password_expire_days: number
|
||||
single_sign_on_enabled: boolean
|
||||
}
|
||||
oauth?: {
|
||||
wechat_enabled: boolean
|
||||
wechat_app_id: string
|
||||
wechat_secret: string
|
||||
wechat_redirect_uri: string
|
||||
qq_enabled: boolean
|
||||
qq_app_id: string
|
||||
qq_secret: string
|
||||
qq_redirect_uri: string
|
||||
github_enabled: boolean
|
||||
github_client_id: string
|
||||
github_secret: string
|
||||
github_redirect_uri: string
|
||||
}
|
||||
register?: {
|
||||
register_enabled: boolean
|
||||
register_methods: string[]
|
||||
register_verification: 'none' | 'email' | 'sms' | 'manual'
|
||||
register_captcha_enabled: boolean
|
||||
agreement_required: boolean
|
||||
agreement_content: string
|
||||
default_role: string
|
||||
reward_enabled: boolean
|
||||
reward_points: number
|
||||
reward_balance: number
|
||||
}
|
||||
}
|
||||
|
||||
// 登录统计接口
|
||||
export interface LoginStats {
|
||||
total_logins: number
|
||||
today_logins: number
|
||||
failed_logins: number
|
||||
locked_accounts: number
|
||||
oauth_logins: number
|
||||
guest_logins: number
|
||||
}
|
||||
|
||||
// 登录记录接口
|
||||
export interface LoginRecord {
|
||||
id: number
|
||||
user_id: number
|
||||
username: string
|
||||
login_type: 'username' | 'mobile' | 'email' | 'sms' | 'oauth' | 'guest'
|
||||
login_ip: string
|
||||
login_location: string
|
||||
user_agent: string
|
||||
login_time: string
|
||||
logout_time?: string
|
||||
status: 'success' | 'failed' | 'locked'
|
||||
fail_reason?: string
|
||||
}
|
||||
|
||||
// 在线用户接口
|
||||
export interface OnlineUser {
|
||||
id: number
|
||||
user_id: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
login_ip: string
|
||||
login_location: string
|
||||
login_time: string
|
||||
last_activity: string
|
||||
device_type: string
|
||||
browser: string
|
||||
os: string
|
||||
}
|
||||
|
||||
// 获取登录配置
|
||||
export function getLoginConfigApi() {
|
||||
return requestClient.get<LoginConfig>('/api/common/login/config')
|
||||
}
|
||||
|
||||
// 更新登录配置
|
||||
export function updateLoginConfigApi(data: {
|
||||
type: 'methods' | 'security' | 'oauth' | 'register'
|
||||
config: any
|
||||
}) {
|
||||
return requestClient.put('/api/common/login/config', data)
|
||||
}
|
||||
|
||||
// 重置登录配置
|
||||
export function resetLoginConfigApi(type: 'methods' | 'security' | 'oauth' | 'register') {
|
||||
return requestClient.post('/api/common/login/config/reset', { type })
|
||||
}
|
||||
|
||||
// 测试登录配置
|
||||
export function testLoginConfigApi(data: {
|
||||
type: 'captcha' | 'oauth' | 'sms'
|
||||
config: any
|
||||
}) {
|
||||
return requestClient.post('/api/common/login/config/test', data)
|
||||
}
|
||||
|
||||
// 获取登录统计
|
||||
export function getLoginStatsApi() {
|
||||
return requestClient.get<LoginStats>('/api/common/login/stats')
|
||||
}
|
||||
|
||||
// 获取登录记录
|
||||
export function getLoginRecordsApi(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
user_id?: number
|
||||
login_type?: string
|
||||
status?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
list: LoginRecord[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>('/api/common/login/records', { params })
|
||||
}
|
||||
|
||||
// 获取在线用户
|
||||
export function getOnlineUsersApi(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
username?: string
|
||||
device_type?: string
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
list: OnlineUser[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>('/api/common/login/online', { params })
|
||||
}
|
||||
|
||||
// 强制下线用户
|
||||
export function forceLogoutApi(userId: number) {
|
||||
return requestClient.post('/api/common/login/force-logout', { user_id: userId })
|
||||
}
|
||||
|
||||
// 批量强制下线用户
|
||||
export function batchForceLogoutApi(userIds: number[]) {
|
||||
return requestClient.post('/api/common/login/batch-force-logout', { user_ids: userIds })
|
||||
}
|
||||
|
||||
// 解锁账户
|
||||
export function unlockAccountApi(userId: number) {
|
||||
return requestClient.post('/api/common/login/unlock-account', { user_id: userId })
|
||||
}
|
||||
|
||||
// 批量解锁账户
|
||||
export function batchUnlockAccountApi(userIds: number[]) {
|
||||
return requestClient.post('/api/common/login/batch-unlock-account', { user_ids: userIds })
|
||||
}
|
||||
|
||||
// 清理登录记录
|
||||
export function cleanLoginRecordsApi(data: {
|
||||
days?: number
|
||||
status?: string
|
||||
}) {
|
||||
return requestClient.post('/api/common/login/clean-records', data)
|
||||
}
|
||||
|
||||
// 导出登录记录
|
||||
export function exportLoginRecordsApi(params?: {
|
||||
user_id?: number
|
||||
login_type?: string
|
||||
status?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
}) {
|
||||
return requestClient.get('/api/common/login/export-records', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证登录配置
|
||||
export function validateLoginConfigApi(data: {
|
||||
type: 'methods' | 'security' | 'oauth' | 'register'
|
||||
config: any
|
||||
}) {
|
||||
return requestClient.post('/api/common/login/config/validate', data)
|
||||
}
|
||||
|
||||
// 获取登录配置模板
|
||||
export function getLoginConfigTemplateApi(type: 'methods' | 'security' | 'oauth' | 'register') {
|
||||
return requestClient.get(`/api/common/login/config/template/${type}`)
|
||||
}
|
||||
220
admin/apps/web-ele/src/api/common/payment.ts
Normal file
220
admin/apps/web-ele/src/api/common/payment.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { requestClient } from '#/api/request'
|
||||
|
||||
// 支付配置接口
|
||||
export interface PaymentConfig {
|
||||
// 支付宝配置
|
||||
alipay?: {
|
||||
alipay_app_id?: string
|
||||
alipay_gateway_url?: string
|
||||
alipay_private_key?: string
|
||||
alipay_public_key?: string
|
||||
alipay_sign_type?: string
|
||||
alipay_charset?: string
|
||||
alipay_notify_url?: string
|
||||
alipay_return_url?: string
|
||||
alipay_enabled?: boolean
|
||||
}
|
||||
// 微信支付配置
|
||||
wechat?: {
|
||||
wechat_app_id?: string
|
||||
wechat_mch_id?: string
|
||||
wechat_key?: string
|
||||
wechat_secret?: string
|
||||
wechat_cert_path?: string
|
||||
wechat_key_path?: string
|
||||
wechat_notify_url?: string
|
||||
wechat_trade_type?: string
|
||||
wechat_enabled?: boolean
|
||||
}
|
||||
// 通用配置
|
||||
general?: {
|
||||
default_method?: string
|
||||
timeout?: number
|
||||
min_amount?: number
|
||||
max_amount?: number
|
||||
success_url?: string
|
||||
fail_url?: string
|
||||
balance_enabled?: boolean
|
||||
points_enabled?: boolean
|
||||
points_ratio?: number
|
||||
}
|
||||
}
|
||||
|
||||
// 支付统计接口
|
||||
export interface PaymentStats {
|
||||
total_amount: number
|
||||
total_count: number
|
||||
success_count: number
|
||||
fail_count: number
|
||||
pending_count: number
|
||||
today_amount: number
|
||||
today_count: number
|
||||
alipay_amount: number
|
||||
alipay_count: number
|
||||
wechat_amount: number
|
||||
wechat_count: number
|
||||
balance_amount: number
|
||||
balance_count: number
|
||||
}
|
||||
|
||||
// 支付记录接口
|
||||
export interface PaymentRecord {
|
||||
id: number
|
||||
order_id: string
|
||||
user_id: number
|
||||
method: string
|
||||
amount: number
|
||||
status: string
|
||||
trade_no?: string
|
||||
transaction_id?: string
|
||||
subject: string
|
||||
body?: string
|
||||
notify_data?: any
|
||||
created_at: string
|
||||
updated_at: string
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
nickname?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 退款记录接口
|
||||
export interface RefundRecord {
|
||||
id: number
|
||||
payment_id: number
|
||||
refund_no: string
|
||||
amount: number
|
||||
reason: string
|
||||
status: string
|
||||
refund_data?: any
|
||||
created_at: string
|
||||
updated_at: string
|
||||
payment?: PaymentRecord
|
||||
}
|
||||
|
||||
// 测试支付参数
|
||||
export interface TestPaymentParams {
|
||||
method: string
|
||||
amount: number
|
||||
subject: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
// 测试支付结果
|
||||
export interface TestPaymentResult {
|
||||
order_id: string
|
||||
payUrl?: string
|
||||
qrCode?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
// 获取支付配置
|
||||
export function getPaymentConfigApi() {
|
||||
return requestClient.get<PaymentConfig>('/admin/settings/payment')
|
||||
}
|
||||
|
||||
// 更新支付配置
|
||||
export function updatePaymentConfigApi(data: {
|
||||
type: 'alipay' | 'wechat' | 'general'
|
||||
config: any
|
||||
}) {
|
||||
return requestClient.put('/admin/settings/payment', data)
|
||||
}
|
||||
|
||||
// 测试支付
|
||||
export function testPaymentApi(data: TestPaymentParams) {
|
||||
return requestClient.post<TestPaymentResult>('/admin/payment/test', data)
|
||||
}
|
||||
|
||||
// 重置支付配置
|
||||
export function resetPaymentConfigApi(type: 'alipay' | 'wechat' | 'general') {
|
||||
return requestClient.delete(`/admin/settings/payment/${type}`)
|
||||
}
|
||||
|
||||
// 获取支付统计
|
||||
export function getPaymentStatsApi(params?: {
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
method?: string
|
||||
}) {
|
||||
return requestClient.get<PaymentStats>('/admin/payment/stats', { params })
|
||||
}
|
||||
|
||||
// 获取支付记录
|
||||
export function getPaymentRecordsApi(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
method?: string
|
||||
status?: string
|
||||
user_id?: number
|
||||
order_id?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
data: PaymentRecord[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>('/admin/payment/records', { params })
|
||||
}
|
||||
|
||||
// 获取退款记录
|
||||
export function getRefundRecordsApi(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
status?: string
|
||||
payment_id?: number
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
data: RefundRecord[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}>('/admin/payment/refunds', { params })
|
||||
}
|
||||
|
||||
// 处理退款
|
||||
export function processRefundApi(data: {
|
||||
payment_id: number
|
||||
amount: number
|
||||
reason: string
|
||||
}) {
|
||||
return requestClient.post('/admin/payment/refund', data)
|
||||
}
|
||||
|
||||
// 查询支付状态
|
||||
export function queryPaymentStatusApi(orderId: string) {
|
||||
return requestClient.get(`/admin/payment/query/${orderId}`)
|
||||
}
|
||||
|
||||
// 同步支付状态
|
||||
export function syncPaymentStatusApi(paymentId: number) {
|
||||
return requestClient.post(`/admin/payment/sync/${paymentId}`)
|
||||
}
|
||||
|
||||
// 批量同步支付状态
|
||||
export function batchSyncPaymentStatusApi(paymentIds: number[]) {
|
||||
return requestClient.post('/admin/payment/batch-sync', { payment_ids: paymentIds })
|
||||
}
|
||||
|
||||
// 关闭支付订单
|
||||
export function closePaymentOrderApi(orderId: string) {
|
||||
return requestClient.post(`/admin/payment/close/${orderId}`)
|
||||
}
|
||||
|
||||
// 验证支付配置
|
||||
export function validatePaymentConfigApi(data: {
|
||||
type: 'alipay' | 'wechat'
|
||||
config: any
|
||||
}) {
|
||||
return requestClient.post('/admin/payment/validate', data)
|
||||
}
|
||||
|
||||
// 获取支付方式模板
|
||||
export function getPaymentMethodTemplateApi(method: 'alipay' | 'wechat') {
|
||||
return requestClient.get(`/admin/payment/template/${method}`)
|
||||
}
|
||||
229
admin/apps/web-ele/src/api/common/security.ts
Normal file
229
admin/apps/web-ele/src/api/common/security.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 安全配置接口
|
||||
export interface SecurityConfig {
|
||||
password?: {
|
||||
enablePasswordStrength: boolean;
|
||||
minPasswordLength: number;
|
||||
requireLowercase: boolean;
|
||||
requireUppercase: boolean;
|
||||
requireNumbers: boolean;
|
||||
requireSpecialChars: boolean;
|
||||
forbidCommonPasswords: boolean;
|
||||
passwordExpireDays: number;
|
||||
passwordHistoryLimit: number;
|
||||
forcePasswordChange: boolean;
|
||||
};
|
||||
login?: {
|
||||
maxLoginAttempts: number;
|
||||
lockoutDuration: number;
|
||||
enableLoginCaptcha: boolean;
|
||||
captchaTriggerAttempts: number;
|
||||
enableTwoFactor: boolean;
|
||||
forceTwoFactor: boolean;
|
||||
sessionTimeout: number;
|
||||
enableSingleSignOn: boolean;
|
||||
recordLoginLog: boolean;
|
||||
};
|
||||
ip?: {
|
||||
enableIpControl: boolean;
|
||||
accessMode: 'whitelist' | 'blacklist';
|
||||
ipWhitelist: string[];
|
||||
ipBlacklist: string[];
|
||||
adminIpWhitelist: string[];
|
||||
};
|
||||
audit?: {
|
||||
enableAudit: boolean;
|
||||
auditLoginLogout: boolean;
|
||||
auditUserManagement: boolean;
|
||||
auditRoleManagement: boolean;
|
||||
auditPermissionManagement: boolean;
|
||||
auditSystemConfig: boolean;
|
||||
auditDataExport: boolean;
|
||||
auditFileUpload: boolean;
|
||||
auditSensitiveOperations: boolean;
|
||||
auditLogRetention: number;
|
||||
enableSecondaryConfirm: boolean;
|
||||
confirmDeleteUser: boolean;
|
||||
confirmResetPassword: boolean;
|
||||
confirmModifyRole: boolean;
|
||||
confirmSystemBackup: boolean;
|
||||
confirmSystemRestore: boolean;
|
||||
confirmClearData: boolean;
|
||||
enableAnomalyDetection: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 安全统计接口
|
||||
export interface SecurityStats {
|
||||
totalLoginAttempts: number;
|
||||
failedLoginAttempts: number;
|
||||
lockedAccounts: number;
|
||||
activeAuditLogs: number;
|
||||
blockedIpCount: number;
|
||||
securityEvents: {
|
||||
date: string;
|
||||
loginAttempts: number;
|
||||
failedLogins: number;
|
||||
securityAlerts: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 安全日志接口
|
||||
export interface SecurityLog {
|
||||
id: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
action: string;
|
||||
resource: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
result: 'success' | 'failed' | 'blocked';
|
||||
riskLevel: 'low' | 'medium' | 'high';
|
||||
details: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// IP测试结果接口
|
||||
export interface IpTestResult {
|
||||
ip: string;
|
||||
allowed: boolean;
|
||||
reason: string;
|
||||
matchedRule?: string;
|
||||
}
|
||||
|
||||
// 更新安全配置参数
|
||||
export interface UpdateSecurityConfigParams {
|
||||
type: 'password' | 'login' | 'ip' | 'audit';
|
||||
config: any;
|
||||
}
|
||||
|
||||
// 获取安全配置
|
||||
export function getSecurityConfigApi() {
|
||||
return requestClient.get<SecurityConfig>('/api/common/security/config');
|
||||
}
|
||||
|
||||
// 更新安全配置
|
||||
export function updateSecurityConfigApi(data: UpdateSecurityConfigParams) {
|
||||
return requestClient.put('/api/common/security/config', data);
|
||||
}
|
||||
|
||||
// 重置安全配置
|
||||
export function resetSecurityConfigApi(type: string) {
|
||||
return requestClient.post(`/api/common/security/config/reset/${type}`);
|
||||
}
|
||||
|
||||
// 测试安全配置
|
||||
export function testSecurityConfigApi(type: string, config: any) {
|
||||
return requestClient.post(`/api/common/security/config/test/${type}`, { config });
|
||||
}
|
||||
|
||||
// 获取安全统计
|
||||
export function getSecurityStatsApi(params?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
type?: string;
|
||||
}) {
|
||||
return requestClient.get<SecurityStats>('/api/common/security/stats', { params });
|
||||
}
|
||||
|
||||
// 获取安全日志
|
||||
export function getSecurityLogsApi(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
userId?: string;
|
||||
action?: string;
|
||||
result?: string;
|
||||
riskLevel?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
ip?: string;
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
list: SecurityLog[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}>('/api/common/security/logs', { params });
|
||||
}
|
||||
|
||||
// 清理安全日志
|
||||
export function cleanSecurityLogsApi(params: {
|
||||
beforeDate: string;
|
||||
logType?: string;
|
||||
}) {
|
||||
return requestClient.delete('/api/common/security/logs/clean', { data: params });
|
||||
}
|
||||
|
||||
// 导出安全日志
|
||||
export function exportSecurityLogsApi(params?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
format?: 'excel' | 'csv';
|
||||
userId?: string;
|
||||
action?: string;
|
||||
}) {
|
||||
return requestClient.post('/api/common/security/logs/export', params, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
// 测试IP访问
|
||||
export function testIpAccessApi() {
|
||||
return requestClient.get<IpTestResult>('/api/common/security/ip/test');
|
||||
}
|
||||
|
||||
// 解锁账户
|
||||
export function unlockAccountApi(userId: string) {
|
||||
return requestClient.post(`/api/common/security/account/unlock/${userId}`);
|
||||
}
|
||||
|
||||
// 批量解锁账户
|
||||
export function batchUnlockAccountApi(userIds: string[]) {
|
||||
return requestClient.post('/api/common/security/account/unlock/batch', { userIds });
|
||||
}
|
||||
|
||||
// 强制下线用户
|
||||
export function forceLogoutUserApi(userId: string) {
|
||||
return requestClient.post(`/api/common/security/session/logout/${userId}`);
|
||||
}
|
||||
|
||||
// 批量强制下线用户
|
||||
export function batchForceLogoutUserApi(userIds: string[]) {
|
||||
return requestClient.post('/api/common/security/session/logout/batch', { userIds });
|
||||
}
|
||||
|
||||
// 获取在线用户
|
||||
export function getOnlineUsersApi(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
username?: string;
|
||||
ip?: string;
|
||||
}) {
|
||||
return requestClient.get('/api/common/security/session/online', { params });
|
||||
}
|
||||
|
||||
// 验证安全配置
|
||||
export function validateSecurityConfigApi(type: string, config: any) {
|
||||
return requestClient.post(`/api/common/security/config/validate/${type}`, { config });
|
||||
}
|
||||
|
||||
// 获取安全配置模板
|
||||
export function getSecurityConfigTemplateApi(type: string) {
|
||||
return requestClient.get(`/api/common/security/config/template/${type}`);
|
||||
}
|
||||
|
||||
// 安全扫描
|
||||
export function securityScanApi() {
|
||||
return requestClient.post('/api/common/security/scan');
|
||||
}
|
||||
|
||||
// 获取安全建议
|
||||
export function getSecuritySuggestionsApi() {
|
||||
return requestClient.get('/api/common/security/suggestions');
|
||||
}
|
||||
|
||||
// 应用安全建议
|
||||
export function applySecuritySuggestionApi(suggestionId: string) {
|
||||
return requestClient.post(`/api/common/security/suggestions/apply/${suggestionId}`);
|
||||
}
|
||||
314
admin/apps/web-ele/src/api/common/settings.ts
Normal file
314
admin/apps/web-ele/src/api/common/settings.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 基础设置接口
|
||||
export interface BasicSettings {
|
||||
site_name: string;
|
||||
site_title: string;
|
||||
site_keywords: string;
|
||||
site_description: string;
|
||||
site_logo: string;
|
||||
site_icon: string;
|
||||
site_icp: string;
|
||||
site_copyright: string;
|
||||
site_status: boolean;
|
||||
site_close_reason: string;
|
||||
}
|
||||
|
||||
// 邮件设置接口
|
||||
export interface EmailSettings {
|
||||
enabled: boolean;
|
||||
driver: 'mailgun' | 'sendmail' | 'ses' | 'smtp';
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
smtp_username: string;
|
||||
smtp_password: string;
|
||||
smtp_encryption: 'none' | 'ssl' | 'tls';
|
||||
from_email: string;
|
||||
from_name: string;
|
||||
}
|
||||
|
||||
// 短信设置接口
|
||||
export interface SmsSettings {
|
||||
enabled: boolean;
|
||||
driver: 'aliyun' | 'huawei' | 'qiniu' | 'tencent';
|
||||
access_key_id: string;
|
||||
access_key_secret: string;
|
||||
sign_name: string;
|
||||
template_code: string;
|
||||
code_length: number;
|
||||
code_expire: number;
|
||||
rate_limit: number;
|
||||
test_mobile: string;
|
||||
}
|
||||
|
||||
// 存储设置接口
|
||||
export interface StorageSettings {
|
||||
default_driver: 'aliyun_oss' | 'aws_s3' | 'local' | 'qiniu' | 'tencent_cos';
|
||||
max_file_size: number;
|
||||
allowed_extensions: string[];
|
||||
local: {
|
||||
base_url: string;
|
||||
storage_path: string;
|
||||
};
|
||||
aliyun_oss: {
|
||||
access_key_id: string;
|
||||
access_key_secret: string;
|
||||
bucket: string;
|
||||
custom_domain?: string;
|
||||
endpoint: string;
|
||||
};
|
||||
tencent_cos: {
|
||||
bucket: string;
|
||||
custom_domain?: string;
|
||||
region: string;
|
||||
secret_id: string;
|
||||
secret_key: string;
|
||||
};
|
||||
qiniu: {
|
||||
access_key: string;
|
||||
bucket: string;
|
||||
domain: string;
|
||||
secret_key: string;
|
||||
};
|
||||
aws_s3: {
|
||||
access_key_id: string;
|
||||
bucket: string;
|
||||
custom_domain?: string;
|
||||
region: string;
|
||||
secret_access_key: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 支付设置接口
|
||||
export interface PaymentSettings {
|
||||
alipay: {
|
||||
app_id: string;
|
||||
enabled: boolean;
|
||||
mode: 'production' | 'sandbox';
|
||||
notify_url?: string;
|
||||
private_key: string;
|
||||
public_key: string;
|
||||
return_url?: string;
|
||||
};
|
||||
wechat: {
|
||||
api_key: string;
|
||||
app_id: string;
|
||||
cert_path?: string;
|
||||
enabled: boolean;
|
||||
key_path?: string;
|
||||
mch_id: string;
|
||||
mode: 'production' | 'sandbox';
|
||||
notify_url?: string;
|
||||
};
|
||||
unionpay: {
|
||||
cert_password: string;
|
||||
cert_path: string;
|
||||
enabled: boolean;
|
||||
mer_id: string;
|
||||
mode: 'production' | 'sandbox';
|
||||
notify_url?: string;
|
||||
return_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 登录设置接口
|
||||
export interface LoginSettings {
|
||||
login_methods: {
|
||||
email: boolean;
|
||||
github: boolean;
|
||||
mobile: boolean;
|
||||
qq: boolean;
|
||||
username: boolean;
|
||||
wechat: boolean;
|
||||
};
|
||||
captcha: {
|
||||
enabled: boolean;
|
||||
expire: number;
|
||||
length: number;
|
||||
type: 'email' | 'image' | 'sms';
|
||||
};
|
||||
password_policy: {
|
||||
min_length: number;
|
||||
require_lowercase: boolean;
|
||||
require_numbers: boolean;
|
||||
require_symbols: boolean;
|
||||
require_uppercase: boolean;
|
||||
};
|
||||
session: {
|
||||
max_sessions: number;
|
||||
remember_me: boolean;
|
||||
timeout: number;
|
||||
};
|
||||
security: {
|
||||
force_logout_on_password_change: boolean;
|
||||
lockout_duration: number;
|
||||
max_login_attempts: number;
|
||||
};
|
||||
}
|
||||
|
||||
// API 接口定义
|
||||
|
||||
// 基础设置
|
||||
export const getBasicSettingsApi = () => {
|
||||
return requestClient.get<BasicSettings>('/api/system/settings/basic');
|
||||
};
|
||||
|
||||
export const updateBasicSettingsApi = (data: Partial<BasicSettings>) => {
|
||||
return requestClient.put<BasicSettings>('/api/system/settings/basic', data);
|
||||
};
|
||||
|
||||
// 邮件设置
|
||||
export const getEmailSettingsApi = () => {
|
||||
return requestClient.get<EmailSettings>('/api/system/settings/email');
|
||||
};
|
||||
|
||||
export const updateEmailSettingsApi = (data: Partial<EmailSettings>) => {
|
||||
return requestClient.put<EmailSettings>('/api/system/settings/email', data);
|
||||
};
|
||||
|
||||
export const testEmailApi = (email: string) => {
|
||||
return requestClient.post('/api/system/settings/email/test', { email });
|
||||
};
|
||||
|
||||
// 短信设置
|
||||
export const getSmsSettingsApi = () => {
|
||||
return requestClient.get<SmsSettings>('/api/system/settings/sms');
|
||||
};
|
||||
|
||||
export const updateSmsSettingsApi = (data: Partial<SmsSettings>) => {
|
||||
return requestClient.put<SmsSettings>('/api/system/settings/sms', data);
|
||||
};
|
||||
|
||||
export const testSmsApi = (mobile: string) => {
|
||||
return requestClient.post('/api/system/settings/sms/test', { mobile });
|
||||
};
|
||||
|
||||
// 存储设置
|
||||
export const getStorageSettingsApi = () => {
|
||||
return requestClient.get<StorageSettings>('/api/system/settings/storage');
|
||||
};
|
||||
|
||||
export const updateStorageSettingsApi = (data: Partial<StorageSettings>) => {
|
||||
return requestClient.put<StorageSettings>(
|
||||
'/api/system/settings/storage',
|
||||
data,
|
||||
);
|
||||
};
|
||||
|
||||
export const testStorageConnectionApi = (driver: string) => {
|
||||
return requestClient.post('/api/system/settings/storage/test', { driver });
|
||||
};
|
||||
|
||||
// 支付设置
|
||||
export const getPaymentSettingsApi = () => {
|
||||
return requestClient.get<PaymentSettings>('/api/system/settings/payment');
|
||||
};
|
||||
|
||||
export const updatePaymentSettingsApi = (data: Partial<PaymentSettings>) => {
|
||||
return requestClient.put<PaymentSettings>(
|
||||
'/api/system/settings/payment',
|
||||
data,
|
||||
);
|
||||
};
|
||||
|
||||
// 登录设置
|
||||
export const getLoginSettingsApi = () => {
|
||||
return requestClient.get<LoginSettings>('/api/system/settings/login');
|
||||
};
|
||||
|
||||
export const updateLoginSettingsApi = (data: Partial<LoginSettings>) => {
|
||||
return requestClient.put<LoginSettings>('/api/system/settings/login', data);
|
||||
};
|
||||
|
||||
// 获取所有设置
|
||||
export const getAllSettingsApi = () => {
|
||||
return requestClient.get<{
|
||||
basic: BasicSettings;
|
||||
email: EmailSettings;
|
||||
login: LoginSettings;
|
||||
payment: PaymentSettings;
|
||||
sms: SmsSettings;
|
||||
storage: StorageSettings;
|
||||
}>('/api/system/settings');
|
||||
};
|
||||
|
||||
// 重置设置到默认值
|
||||
export const resetSettingsApi = (
|
||||
type: 'basic' | 'email' | 'login' | 'payment' | 'sms' | 'storage',
|
||||
) => {
|
||||
return requestClient.post(`/api/system/settings/${type}/reset`);
|
||||
};
|
||||
|
||||
// 导出设置
|
||||
export const exportSettingsApi = () => {
|
||||
return requestClient.get('/api/system/settings/export', {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
// 导入设置
|
||||
export const importSettingsApi = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return requestClient.post('/api/system/settings/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 通知设置接口
|
||||
export interface NotificationSettings {
|
||||
email: {
|
||||
enabled: boolean;
|
||||
types: string[];
|
||||
adminEmails: string[];
|
||||
rateLimit: number;
|
||||
retryTimes: number;
|
||||
queueDelay: number;
|
||||
};
|
||||
sms: {
|
||||
enabled: boolean;
|
||||
types: string[];
|
||||
adminPhones: string[];
|
||||
rateLimit: number;
|
||||
retryTimes: number;
|
||||
queueDelay: number;
|
||||
};
|
||||
system: {
|
||||
enabled: boolean;
|
||||
types: string[];
|
||||
retentionDays: number;
|
||||
maxPerUser: number;
|
||||
autoRead: boolean;
|
||||
};
|
||||
wechat: {
|
||||
enabled: boolean;
|
||||
types: string[];
|
||||
templateIds: Record<string, string>;
|
||||
retryTimes: number;
|
||||
queueDelay: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateNotificationSettingsParams {
|
||||
type: 'email' | 'sms' | 'system' | 'wechat';
|
||||
settings: Partial<NotificationSettings[keyof NotificationSettings]>;
|
||||
}
|
||||
|
||||
// 通知设置API
|
||||
export const getNotificationSettingsApi = () => {
|
||||
return requestClient.get<NotificationSettings>('/api/system/settings/notification');
|
||||
};
|
||||
|
||||
export const updateNotificationSettingsApi = (data: UpdateNotificationSettingsParams) => {
|
||||
return requestClient.put<NotificationSettings>('/api/system/settings/notification', data);
|
||||
};
|
||||
|
||||
export const resetNotificationSettingsApi = (type: 'email' | 'sms' | 'system' | 'wechat') => {
|
||||
return requestClient.post(`/api/system/settings/notification/${type}/reset`);
|
||||
};
|
||||
|
||||
export const testNotificationApi = (type: 'email' | 'sms' | 'system' | 'wechat') => {
|
||||
return requestClient.post(`/api/system/settings/notification/${type}/test`);
|
||||
};
|
||||
184
admin/apps/web-ele/src/api/common/sms.ts
Normal file
184
admin/apps/web-ele/src/api/common/sms.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 短信配置接口
|
||||
export interface SmsConfig {
|
||||
provider: 'aliyun' | 'tencent';
|
||||
access_key_id?: string;
|
||||
access_key_secret?: string;
|
||||
sign_name?: string;
|
||||
region?: string;
|
||||
secret_id?: string;
|
||||
secret_key?: string;
|
||||
app_id?: string;
|
||||
sign_content?: string;
|
||||
enabled: boolean;
|
||||
debug_mode: boolean;
|
||||
templates: SmsTemplate[];
|
||||
rate_limit: {
|
||||
per_minute: number;
|
||||
per_hour: number;
|
||||
per_day: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 短信模板接口
|
||||
export interface SmsTemplate {
|
||||
type: 'verify_code' | 'notification' | 'marketing';
|
||||
template_id: string;
|
||||
content: string;
|
||||
variables?: string;
|
||||
}
|
||||
|
||||
// 短信统计接口
|
||||
export interface SmsStats {
|
||||
total_sent: number;
|
||||
success_count: number;
|
||||
failed_count: number;
|
||||
today_sent: number;
|
||||
this_month_sent: number;
|
||||
}
|
||||
|
||||
// 短信日志接口
|
||||
export interface SmsLog {
|
||||
id: number;
|
||||
mobile: string;
|
||||
content: string;
|
||||
template_type: string;
|
||||
status: 'pending' | 'sent' | 'failed';
|
||||
error_message?: string;
|
||||
provider: string;
|
||||
created_at: string;
|
||||
sent_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信配置
|
||||
*/
|
||||
export async function getSmsConfigApi(): Promise<SmsConfig> {
|
||||
return requestClient.get('/api/admin/settings/sms');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新短信配置
|
||||
*/
|
||||
export async function updateSmsConfigApi(data: SmsConfig): Promise<void> {
|
||||
return requestClient.put('/api/admin/settings/sms', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试短信发送
|
||||
*/
|
||||
export async function testSmsApi(data: {
|
||||
mobile: string;
|
||||
template_type: string;
|
||||
content: string;
|
||||
}): Promise<void> {
|
||||
return requestClient.post('/api/admin/settings/sms/test', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置短信配置
|
||||
*/
|
||||
export async function resetSmsConfigApi(): Promise<void> {
|
||||
return requestClient.post('/api/admin/settings/sms/reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信统计
|
||||
*/
|
||||
export async function getSmsStatsApi(): Promise<SmsStats> {
|
||||
return requestClient.get('/api/admin/settings/sms/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信日志
|
||||
*/
|
||||
export async function getSmsLogsApi(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
mobile?: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}): Promise<{
|
||||
data: SmsLog[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}> {
|
||||
return requestClient.get('/api/admin/settings/sms/logs', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空短信队列
|
||||
*/
|
||||
export async function clearSmsQueueApi(): Promise<void> {
|
||||
return requestClient.post('/api/admin/settings/sms/clear-queue');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试发送短信
|
||||
*/
|
||||
export async function retrySmsApi(id: number): Promise<void> {
|
||||
return requestClient.post(`/api/admin/settings/sms/retry/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重试发送短信
|
||||
*/
|
||||
export async function batchRetrySmsApi(ids: number[]): Promise<void> {
|
||||
return requestClient.post('/api/admin/settings/sms/batch-retry', { ids });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除短信日志
|
||||
*/
|
||||
export async function deleteSmsLogApi(id: number): Promise<void> {
|
||||
return requestClient.delete(`/api/admin/settings/sms/logs/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除短信日志
|
||||
*/
|
||||
export async function batchDeleteSmsLogApi(ids: number[]): Promise<void> {
|
||||
return requestClient.post('/api/admin/settings/sms/logs/batch-delete', { ids });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信模板预览
|
||||
*/
|
||||
export async function previewSmsTemplateApi(data: {
|
||||
template_id: string;
|
||||
variables: Record<string, any>;
|
||||
}): Promise<{
|
||||
content: string;
|
||||
preview: string;
|
||||
}> {
|
||||
return requestClient.post('/api/admin/settings/sms/template/preview', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信配置
|
||||
*/
|
||||
export async function validateSmsConfigApi(data: Partial<SmsConfig>): Promise<{
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}> {
|
||||
return requestClient.post('/api/admin/settings/sms/validate', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信服务商配置模板
|
||||
*/
|
||||
export async function getSmsProviderTemplateApi(provider: string): Promise<{
|
||||
fields: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
}>;
|
||||
}> {
|
||||
return requestClient.get(`/api/admin/settings/sms/provider/${provider}/template`);
|
||||
}
|
||||
207
admin/apps/web-ele/src/api/common/storage.ts
Normal file
207
admin/apps/web-ele/src/api/common/storage.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// 存储配置接口
|
||||
export interface StorageConfig {
|
||||
driver: 'local' | 'oss' | 'cos' | 'qiniu' | 'upyun' | 's3';
|
||||
// 本地存储配置
|
||||
local_path?: string;
|
||||
local_domain?: string;
|
||||
// 阿里云OSS配置
|
||||
oss_access_key_id?: string;
|
||||
oss_access_key_secret?: string;
|
||||
oss_bucket?: string;
|
||||
oss_region?: string;
|
||||
oss_domain?: string;
|
||||
oss_is_private?: boolean;
|
||||
// 腾讯云COS配置
|
||||
cos_secret_id?: string;
|
||||
cos_secret_key?: string;
|
||||
cos_bucket?: string;
|
||||
cos_region?: string;
|
||||
// 七牛云配置
|
||||
qiniu_access_key?: string;
|
||||
qiniu_secret_key?: string;
|
||||
qiniu_bucket?: string;
|
||||
qiniu_domain?: string;
|
||||
// 又拍云配置
|
||||
upyun_username?: string;
|
||||
upyun_password?: string;
|
||||
upyun_bucket?: string;
|
||||
upyun_domain?: string;
|
||||
// AWS S3配置
|
||||
s3_access_key_id?: string;
|
||||
s3_secret_access_key?: string;
|
||||
s3_bucket?: string;
|
||||
s3_region?: string;
|
||||
s3_endpoint?: string;
|
||||
// 上传限制
|
||||
max_size: number;
|
||||
allowed_types: string[];
|
||||
// 图片处理
|
||||
thumbnail_enabled: boolean;
|
||||
thumbnail_width?: number;
|
||||
thumbnail_height?: number;
|
||||
// 其他设置
|
||||
enabled: boolean;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
// 存储统计接口
|
||||
export interface StorageStats {
|
||||
total_files: number;
|
||||
total_size: number;
|
||||
used_space: string;
|
||||
available_space: string;
|
||||
files_by_type: Record<string, number>;
|
||||
upload_trend: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
size: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 文件信息接口
|
||||
export interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: string;
|
||||
mime_type: string;
|
||||
driver: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 上传结果接口
|
||||
export interface UploadResult {
|
||||
success: boolean;
|
||||
url: string;
|
||||
path: string;
|
||||
size: number;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储配置
|
||||
*/
|
||||
export function getStorageConfigApi() {
|
||||
return requestClient.get<StorageConfig>('/admin/settings/storage');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新存储配置
|
||||
*/
|
||||
export function updateStorageConfigApi(data: StorageConfig) {
|
||||
return requestClient.put('/admin/settings/storage', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试存储配置
|
||||
*/
|
||||
export function testStorageApi(formData: FormData) {
|
||||
return requestClient.post<UploadResult>('/admin/settings/storage/test', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置存储配置
|
||||
*/
|
||||
export function resetStorageConfigApi() {
|
||||
return requestClient.post('/admin/settings/storage/reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储统计信息
|
||||
*/
|
||||
export function getStorageStatsApi() {
|
||||
return requestClient.get<StorageStats>('/admin/settings/storage/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*/
|
||||
export function getFileListApi(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
type?: string;
|
||||
driver?: string;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<{
|
||||
list: FileInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}>('/admin/files', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
export function deleteFileApi(id: string) {
|
||||
return requestClient.delete(`/admin/files/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
*/
|
||||
export function batchDeleteFilesApi(ids: string[]) {
|
||||
return requestClient.delete('/admin/files/batch', { data: { ids } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export function uploadFileApi(formData: FormData) {
|
||||
return requestClient.post<UploadResult>('/admin/files/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件详情
|
||||
*/
|
||||
export function getFileDetailApi(id: string) {
|
||||
return requestClient.get<FileInfo>(`/admin/files/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理无效文件
|
||||
*/
|
||||
export function cleanInvalidFilesApi() {
|
||||
return requestClient.post('/admin/files/clean');
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步文件信息
|
||||
*/
|
||||
export function syncFilesApi() {
|
||||
return requestClient.post('/admin/files/sync');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证存储配置
|
||||
*/
|
||||
export function validateStorageConfigApi(data: Partial<StorageConfig>) {
|
||||
return requestClient.post<{ valid: boolean; message?: string }>(
|
||||
'/admin/settings/storage/validate',
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储驱动模板配置
|
||||
*/
|
||||
export function getStorageDriverTemplateApi(driver: string) {
|
||||
return requestClient.get<Partial<StorageConfig>>(
|
||||
`/admin/settings/storage/template/${driver}`
|
||||
);
|
||||
}
|
||||
105
admin/apps/web-ele/src/api/common/system.ts
Normal file
105
admin/apps/web-ele/src/api/common/system.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 系统配置接口
|
||||
*/
|
||||
export interface SystemConfig {
|
||||
id?: number;
|
||||
site_id?: number;
|
||||
config_key: string;
|
||||
value: string;
|
||||
status?: number;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
addon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统信息接口
|
||||
*/
|
||||
export interface SystemInfo {
|
||||
os: string;
|
||||
server: string;
|
||||
php_version: string;
|
||||
mysql_version: string;
|
||||
redis_version: string;
|
||||
node_version: string;
|
||||
memory_usage: string;
|
||||
disk_usage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基本信息配置
|
||||
*/
|
||||
export interface BasicConfig {
|
||||
site_name: string;
|
||||
site_title: string;
|
||||
site_description: string;
|
||||
site_keywords: string;
|
||||
site_icp: string;
|
||||
site_copyright: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统配置
|
||||
*/
|
||||
export interface ConfigSettings {
|
||||
site_status: string;
|
||||
site_close_reason?: string;
|
||||
timezone: string;
|
||||
default_language: string;
|
||||
page_size: number;
|
||||
cache_enabled: boolean;
|
||||
debug_enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
* @param type 配置类型
|
||||
*/
|
||||
export async function getSystemConfigApi(type: 'basic' | 'config'): Promise<any> {
|
||||
return requestClient.get(`/api/system/config/${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
* @param type 配置类型
|
||||
* @param data 配置数据
|
||||
*/
|
||||
export async function updateSystemConfigApi(
|
||||
type: 'basic' | 'config',
|
||||
data: BasicConfig | ConfigSettings,
|
||||
): Promise<void> {
|
||||
return requestClient.put(`/api/system/config/${type}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
export async function getSystemInfoApi(): Promise<SystemInfo> {
|
||||
return requestClient.get('/api/system/info');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出系统信息
|
||||
*/
|
||||
export async function exportSystemInfoApi(): Promise<void> {
|
||||
return requestClient.get('/api/system/info/export', {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置系统配置
|
||||
* @param type 配置类型
|
||||
*/
|
||||
export async function resetSystemConfigApi(type: 'basic' | 'config'): Promise<void> {
|
||||
return requestClient.post(`/api/system/config/${type}/reset`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除系统缓存
|
||||
*/
|
||||
export async function clearSystemCacheApi(): Promise<void> {
|
||||
return requestClient.post('/api/system/cache/clear');
|
||||
}
|
||||
51
admin/apps/web-ele/src/api/core/auth.ts
Normal file
51
admin/apps/web-ele/src/api/core/auth.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/auth/logout', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/auth/codes');
|
||||
}
|
||||
3
admin/apps/web-ele/src/api/core/index.ts
Normal file
3
admin/apps/web-ele/src/api/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
10
admin/apps/web-ele/src/api/core/menu.ts
Normal file
10
admin/apps/web-ele/src/api/core/menu.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
}
|
||||
10
admin/apps/web-ele/src/api/core/user.ts
Normal file
10
admin/apps/web-ele/src/api/core/user.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
}
|
||||
1
admin/apps/web-ele/src/api/index.ts
Normal file
1
admin/apps/web-ele/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
113
admin/apps/web-ele/src/api/request.ts
Normal file
113
admin/apps/web-ele/src/api/request.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
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 resp = await refreshTokenApi();
|
||||
const newToken = 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;
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
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 ?? '';
|
||||
// 如果没有错误信息,则会根据状态码进行提示
|
||||
ElMessage.error(errorMessage || msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
17
admin/apps/web-ele/src/app.vue
Normal file
17
admin/apps/web-ele/src/app.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { useElementPlusDesignTokens } from '@vben/hooks';
|
||||
|
||||
import { ElConfigProvider } from 'element-plus';
|
||||
|
||||
import { elementLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
useElementPlusDesignTokens();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="elementLocale">
|
||||
<RouterView />
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
79
admin/apps/web-ele/src/bootstrap.ts
Normal file
79
admin/apps/web-ele/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';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/ele';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { ElLoading } from 'element-plus';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 2000,
|
||||
// });
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
// 注册Vben提供的v-loading和v-spinning指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
|
||||
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 };
|
||||
23
admin/apps/web-ele/src/layouts/auth.vue
Normal file
23
admin/apps/web-ele/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>
|
||||
157
admin/apps/web-ele/src/layouts/basic.vue
Normal file
157
admin/apps/web-ele/src/layouts/basic.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } 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[]>([
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
date: '3小时前',
|
||||
isRead: true,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '收到了 14 份新周报',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '刚刚',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '朱偏右 回复了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '2024-01-01',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '曲丽丽 评论了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '代办提醒',
|
||||
},
|
||||
]);
|
||||
|
||||
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: () => {
|
||||
openWindow(VBEN_DOC_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: BookOpenText,
|
||||
text: $t('ui.widgets.document'),
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(VBEN_GITHUB_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: MdiGithub,
|
||||
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 handleMakeAll() {
|
||||
notifications.value.forEach((item) => (item.isRead = true));
|
||||
}
|
||||
watch(
|
||||
() => preferences.app.watermark,
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
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"
|
||||
@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
admin/apps/web-ele/src/layouts/index.ts
Normal file
6
admin/apps/web-ele/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
admin/apps/web-ele/src/locales/README.md
Normal file
3
admin/apps/web-ele/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
102
admin/apps/web-ele/src/locales/index.ts
Normal file
102
admin/apps/web-ele/src/locales/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Language } from 'element-plus/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
$t,
|
||||
setupI18n as coreSetup,
|
||||
loadLocalesMapFromDir,
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import enLocale from 'element-plus/es/locale/lang/en';
|
||||
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
|
||||
|
||||
const elementLocale = ref<Language>(defaultLocale);
|
||||
|
||||
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 Promise.all([loadElementLocale(lang), 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载element-plus的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadElementLocale(lang: SupportedLanguagesType) {
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
elementLocale.value = enLocale;
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
elementLocale.value = defaultLocale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
await coreSetup(app, {
|
||||
defaultLocale: preferences.app.locale,
|
||||
loadMessages,
|
||||
missingWarn: !import.meta.env.PROD,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, elementLocale, setupI18n };
|
||||
13
admin/apps/web-ele/src/locales/langs/en-US/demos.json
Normal file
13
admin/apps/web-ele/src/locales/langs/en-US/demos.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "Form",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
"document": "Document",
|
||||
"antdv": "Ant Design Vue Version",
|
||||
"naive-ui": "Naive UI Version",
|
||||
"element-plus": "Element Plus Version"
|
||||
}
|
||||
}
|
||||
14
admin/apps/web-ele/src/locales/langs/en-US/page.json
Normal file
14
admin/apps/web-ele/src/locales/langs/en-US/page.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"analytics": "Analytics",
|
||||
"workspace": "Workspace"
|
||||
}
|
||||
}
|
||||
13
admin/apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal file
13
admin/apps/web-ele/src/locales/langs/zh-CN/demos.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "表单演示",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
"document": "文档",
|
||||
"antdv": "Ant Design Vue 版本",
|
||||
"naive-ui": "Naive UI 版本",
|
||||
"element-plus": "Element Plus 版本"
|
||||
}
|
||||
}
|
||||
14
admin/apps/web-ele/src/locales/langs/zh-CN/page.json
Normal file
14
admin/apps/web-ele/src/locales/langs/zh-CN/page.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
"analytics": "分析页",
|
||||
"workspace": "工作台"
|
||||
}
|
||||
}
|
||||
31
admin/apps/web-ele/src/main.ts
Normal file
31
admin/apps/web-ele/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();
|
||||
13
admin/apps/web-ele/src/preferences.ts
Normal file
13
admin/apps/web-ele/src/preferences.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||
*/
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
});
|
||||
42
admin/apps/web-ele/src/router/access.ts
Normal file
42
admin/apps/web-ele/src/router/access.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { getAllMenusApi } from '#/api';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
ElMessage({
|
||||
duration: 1500,
|
||||
message: `${$t('common.loadingMenu')}...`,
|
||||
});
|
||||
return await getAllMenusApi();
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
||||
133
admin/apps/web-ele/src/router/guard.ts
Normal file
133
admin/apps/web-ele/src/router/guard.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
|
||||
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();
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
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;
|
||||
}
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
roles: userRoles,
|
||||
router,
|
||||
// 则会在菜单中显示,但是访问会被重定向到403
|
||||
routes: accessRoutes,
|
||||
});
|
||||
|
||||
// 保存菜单信息和路由信息
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
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 };
|
||||
37
admin/apps/web-ele/src/router/index.ts
Normal file
37
admin/apps/web-ele/src/router/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import { resetStaticRoutes } from '@vben/utils';
|
||||
|
||||
import { createRouterGuard } from './guard';
|
||||
import { routes } from './routes';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
export { resetRoutes, router };
|
||||
97
admin/apps/web-ele/src/router/routes/core.ts
Normal file
97
admin/apps/web-ele/src/router/routes/core.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
37
admin/apps/web-ele/src/router/routes/index.ts
Normal file
37
admin/apps/web-ele/src/router/routes/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
174
admin/apps/web-ele/src/router/routes/modules/common.ts
Normal file
174
admin/apps/web-ele/src/router/routes/modules/common.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/common',
|
||||
name: 'Common',
|
||||
component: () => import('#/layouts').then((m) => m.BasicLayout),
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
icon: 'ep:setting',
|
||||
order: 1000,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'CommonUser',
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'ep:user',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'admin',
|
||||
name: 'CommonUserAdmin',
|
||||
component: () => import('#/views/common/user/admin/index.vue'),
|
||||
meta: {
|
||||
title: '管理员',
|
||||
icon: 'ep:user-filled',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
name: 'CommonUserMember',
|
||||
component: () => import('#/views/common/user/member/index.vue'),
|
||||
meta: {
|
||||
title: '会员用户',
|
||||
icon: 'ep:avatar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'rbac',
|
||||
name: 'CommonRbac',
|
||||
meta: {
|
||||
title: '权限管理',
|
||||
icon: 'ep:lock',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'role',
|
||||
name: 'CommonRbacRole',
|
||||
component: () => import('#/views/common/rbac/role/index.vue'),
|
||||
meta: {
|
||||
title: '角色管理',
|
||||
icon: 'ep:user-filled',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'CommonRbacMenu',
|
||||
component: () => import('#/views/common/rbac/menu/index.vue'),
|
||||
meta: {
|
||||
title: '菜单管理',
|
||||
icon: 'ep:menu',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'permission',
|
||||
name: 'CommonRbacPermission',
|
||||
component: () => import('#/views/common/rbac/permission/index.vue'),
|
||||
meta: {
|
||||
title: '权限管理',
|
||||
icon: 'ep:key',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'CommonSettings',
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'ep:tools',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'email',
|
||||
name: 'CommonSettingsEmail',
|
||||
component: () => import('#/views/common/settings/email/index.vue'),
|
||||
meta: {
|
||||
title: '邮件设置',
|
||||
icon: 'ep:message',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'sms',
|
||||
name: 'CommonSettingsSms',
|
||||
component: () => import('#/views/common/settings/sms/index.vue'),
|
||||
meta: {
|
||||
title: '短信设置',
|
||||
icon: 'ep:chat-dot-round',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'storage',
|
||||
name: 'CommonSettingsStorage',
|
||||
component: () => import('#/views/common/settings/storage/index.vue'),
|
||||
meta: {
|
||||
title: '存储设置',
|
||||
icon: 'ep:folder',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'payment',
|
||||
name: 'CommonSettingsPayment',
|
||||
component: () => import('#/views/common/settings/payment/index.vue'),
|
||||
meta: {
|
||||
title: '支付设置',
|
||||
icon: 'ep:credit-card',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
name: 'CommonSettingsLogin',
|
||||
component: () => import('#/views/common/settings/login/index.vue'),
|
||||
meta: {
|
||||
title: '登录设置',
|
||||
icon: 'ep:unlock',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
name: 'CommonSettingsSystem',
|
||||
component: () => import('#/views/common/settings/system/index.vue'),
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'ep:setting',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'CommonSettingsSecurity',
|
||||
component: () => import('#/views/common/settings/security/index.vue'),
|
||||
meta: {
|
||||
title: '安全设置',
|
||||
icon: 'ep:lock',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'notification',
|
||||
name: 'CommonSettingsNotification',
|
||||
component: () => import('#/views/common/settings/notification/index.vue'),
|
||||
meta: {
|
||||
title: '通知设置',
|
||||
icon: 'ep:bell',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
name: 'CommonFile',
|
||||
component: () => import('#/views/common/file/index.vue'),
|
||||
meta: {
|
||||
title: '文件管理',
|
||||
icon: 'ep:folder',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
38
admin/apps/web-ele/src/router/routes/modules/dashboard.ts
Normal file
38
admin/apps/web-ele/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;
|
||||
36
admin/apps/web-ele/src/router/routes/modules/demos.ts
Normal file
36
admin/apps/web-ele/src/router/routes/modules/demos.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('demos.title'),
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.elementPlus'),
|
||||
},
|
||||
name: 'NaiveDemos',
|
||||
path: '/demos/element',
|
||||
component: () => import('#/views/demos/element/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'BasicForm',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
82
admin/apps/web-ele/src/router/routes/modules/vben.ts
Normal file
82
admin/apps/web-ele/src/router/routes/modules/vben.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import {
|
||||
VBEN_ANT_PREVIEW_URL,
|
||||
VBEN_DOC_URL,
|
||||
VBEN_GITHUB_URL,
|
||||
VBEN_LOGO_URL,
|
||||
VBEN_NAIVE_PREVIEW_URL,
|
||||
} from '@vben/constants';
|
||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
||||
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9998,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
link: VBEN_DOC_URL,
|
||||
title: $t('demos.vben.document'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenGithub',
|
||||
path: '/vben-admin/github',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'mdi:github',
|
||||
link: VBEN_GITHUB_URL,
|
||||
title: 'Github',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenNaive',
|
||||
path: '/vben-admin/naive',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'logos:naiveui',
|
||||
link: VBEN_NAIVE_PREVIEW_URL,
|
||||
title: $t('demos.vben.naive-ui'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenAntd',
|
||||
path: '/vben-admin/antd',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgAntdvLogoIcon,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('demos.vben.antdv'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
119
admin/apps/web-ele/src/store/auth.ts
Normal file
119
admin/apps/web-ele/src/store/auth.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { Recordable, UserInfo } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
*/
|
||||
async function authLogin(
|
||||
params: Recordable<any>,
|
||||
onSuccess?: () => Promise<void> | void,
|
||||
) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { accessToken } = await loginApi(params);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
fetchUserInfo(),
|
||||
getAccessCodesApi(),
|
||||
]);
|
||||
|
||||
userInfo = fetchUserInfoResult;
|
||||
|
||||
userStore.setUserInfo(userInfo);
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(
|
||||
userInfo.homePath || preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
ElNotification({
|
||||
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
title: $t('authentication.loginSuccess'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登录页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
let userInfo: null | UserInfo = null;
|
||||
userInfo = await getUserInfoApi();
|
||||
userStore.setUserInfo(userInfo);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
1
admin/apps/web-ele/src/store/index.ts
Normal file
1
admin/apps/web-ele/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
3
admin/apps/web-ele/src/views/_core/README.md
Normal file
3
admin/apps/web-ele/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
admin/apps/web-ele/src/views/_core/about/index.vue
Normal file
9
admin/apps/web-ele/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
98
admin/apps/web-ele/src/views/_core/authentication/login.vue
Normal file
98
admin/apps/web-ele/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: markRaw(SliderCaptcha),
|
||||
fieldName: 'captcha',
|
||||
rules: z.boolean().refine((value) => value, {
|
||||
message: $t('authentication.verifyRequiredTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'VbenCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'vben-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
9
admin/apps/web-ele/src/views/_core/fallback/offline.vue
Normal file
9
admin/apps/web-ele/src/views/_core/fallback/offline.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
1248
admin/apps/web-ele/src/views/common/file/index.vue
Normal file
1248
admin/apps/web-ele/src/views/common/file/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
662
admin/apps/web-ele/src/views/common/rbac/menu/index.vue
Normal file
662
admin/apps/web-ele/src/views/common/rbac/menu/index.vue
Normal file
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<Page>
|
||||
<div class="p-4">
|
||||
<!-- 搜索表单 -->
|
||||
<el-card class="mb-4">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="菜单名称">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入菜单名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="显示" :value="1" />
|
||||
<el-option label="隐藏" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择类型" clearable>
|
||||
<el-option label="目录" value="catalog" />
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="按钮" value="button" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="ep:search" class="mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-card class="mb-4">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增菜单
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleExpandAll">
|
||||
<Icon icon="ep:d-arrow-right" class="mr-1" />
|
||||
展开全部
|
||||
</el-button>
|
||||
<el-button type="info" @click="handleCollapseAll">
|
||||
<Icon icon="ep:d-arrow-left" class="mr-1" />
|
||||
收起全部
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
row-key="menuId"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
ref="tableRef"
|
||||
>
|
||||
<el-table-column prop="title" label="菜单名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="menu-title">
|
||||
<Icon :icon="row.icon || 'ep:folder'" class="mr-2" />
|
||||
<span>{{ row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.type)">
|
||||
{{ getTypeText(row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="component" label="组件路径" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="permission" label="权限标识" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="handleAddChild(row)">
|
||||
新增子菜单
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="800px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上级菜单" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="formData.parentId"
|
||||
:data="menuTreeData"
|
||||
:props="treeSelectProps"
|
||||
placeholder="请选择上级菜单"
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<el-radio-group v-model="formData.type" @change="handleTypeChange">
|
||||
<el-radio label="catalog">目录</el-radio>
|
||||
<el-radio label="menu">菜单</el-radio>
|
||||
<el-radio label="button">按钮</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单图标" prop="icon">
|
||||
<el-input v-model="formData.icon" placeholder="请输入图标名称">
|
||||
<template #prepend>
|
||||
<Icon :icon="formData.icon || 'ep:folder'" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" v-if="formData.type !== 'button'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="路由路径" prop="path">
|
||||
<el-input v-model="formData.path" placeholder="请输入路由路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 'menu'">
|
||||
<el-form-item label="组件路径" prop="component">
|
||||
<el-input v-model="formData.component" placeholder="请输入组件路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="权限标识" prop="permission">
|
||||
<el-input v-model="formData.permission" placeholder="请输入权限标识" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" v-if="formData.type !== 'button'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否隐藏" prop="hidden">
|
||||
<el-radio-group v-model="formData.hidden">
|
||||
<el-radio :label="0">显示</el-radio>
|
||||
<el-radio :label="1">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否缓存" prop="keepAlive">
|
||||
<el-radio-group v-model="formData.keepAlive">
|
||||
<el-radio :label="1">缓存</el-radio>
|
||||
<el-radio :label="0">不缓存</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 1. Vue 相关导入
|
||||
import { ref, reactive, onMounted, computed, type FormInstance } from 'vue';
|
||||
|
||||
// 2. Element Plus 组件导入
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
ElTreeSelect,
|
||||
type ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
// 3. 图标组件导入
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 4. Vben 组件导入
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 5. 项目内部导入
|
||||
import {
|
||||
getMenuListApi,
|
||||
getMenuTreeApi,
|
||||
createMenuApi,
|
||||
updateMenuApi,
|
||||
deleteMenuApi,
|
||||
type Menu,
|
||||
type CreateMenuParams,
|
||||
type UpdateMenuParams,
|
||||
type MenuTreeNode,
|
||||
} from '#/api/common/rbac';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const tableData = ref<Menu[]>([]);
|
||||
const menuTreeData = ref<MenuTreeNode[]>([]);
|
||||
const currentParent = ref<Menu | null>(null);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined,
|
||||
type: undefined as string | undefined,
|
||||
});
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const isAddChild = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreateMenuParams & { menuId?: number }>({
|
||||
parentId: 0,
|
||||
title: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
sort: 0,
|
||||
hidden: 0,
|
||||
keepAlive: 1,
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 树形选择器配置
|
||||
const treeSelectProps = {
|
||||
value: 'menuId',
|
||||
label: 'title',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '菜单名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择菜单类型', trigger: 'change' },
|
||||
],
|
||||
path: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.type !== 'button' && !value) {
|
||||
callback(new Error('请输入路由路径'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
component: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.type === 'menu' && !value) {
|
||||
callback(new Error('请输入组件路径'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
permission: [
|
||||
{ required: true, message: '请输入权限标识', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => {
|
||||
if (isAddChild.value) {
|
||||
return `新增子菜单 - ${currentParent.value?.title}`;
|
||||
}
|
||||
return isEdit.value ? '编辑菜单' : '新增菜单';
|
||||
});
|
||||
|
||||
// 方法
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const map = {
|
||||
catalog: '目录',
|
||||
menu: '菜单',
|
||||
button: '按钮',
|
||||
};
|
||||
return map[type as keyof typeof map] || type;
|
||||
};
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const map = {
|
||||
catalog: 'warning',
|
||||
menu: 'primary',
|
||||
button: 'success',
|
||||
};
|
||||
return map[type as keyof typeof map] || 'info';
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status,
|
||||
type: searchForm.type,
|
||||
};
|
||||
const result = await getMenuListApi(params);
|
||||
tableData.value = result;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenuTree = async () => {
|
||||
try {
|
||||
const result = await getMenuTreeApi();
|
||||
// 添加根节点
|
||||
menuTreeData.value = [
|
||||
{
|
||||
menuId: 0,
|
||||
title: '根目录',
|
||||
children: result,
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
ElMessage.error('加载菜单树失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined;
|
||||
searchForm.type = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const table = tableRef.value;
|
||||
if (table) {
|
||||
const expandAll = (data: Menu[]) => {
|
||||
data.forEach(row => {
|
||||
table.toggleRowExpansion(row, true);
|
||||
if (row.children) {
|
||||
expandAll(row.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
expandAll(tableData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
const table = tableRef.value;
|
||||
if (table) {
|
||||
const collapseAll = (data: Menu[]) => {
|
||||
data.forEach(row => {
|
||||
table.toggleRowExpansion(row, false);
|
||||
if (row.children) {
|
||||
collapseAll(row.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
collapseAll(tableData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
isEdit.value = false;
|
||||
isAddChild.value = false;
|
||||
currentParent.value = null;
|
||||
await loadMenuTree();
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleAddChild = async (row: Menu) => {
|
||||
isEdit.value = false;
|
||||
isAddChild.value = true;
|
||||
currentParent.value = row;
|
||||
await loadMenuTree();
|
||||
resetForm();
|
||||
formData.parentId = row.menuId;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = async (row: Menu) => {
|
||||
isEdit.value = true;
|
||||
isAddChild.value = false;
|
||||
currentParent.value = null;
|
||||
await loadMenuTree();
|
||||
Object.assign(formData, {
|
||||
menuId: row.menuId,
|
||||
parentId: row.parentId,
|
||||
title: row.title,
|
||||
type: row.type,
|
||||
path: row.path,
|
||||
component: row.component,
|
||||
icon: row.icon,
|
||||
permission: row.permission,
|
||||
sort: row.sort,
|
||||
hidden: row.hidden,
|
||||
keepAlive: row.keepAlive,
|
||||
status: row.status,
|
||||
remark: row.remark,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (row: Menu) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除菜单 "${row.title}" 吗?删除后子菜单也会被删除!`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await deleteMenuApi(row.menuId);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypeChange = (type: string) => {
|
||||
// 根据类型清空相关字段
|
||||
if (type === 'button') {
|
||||
formData.path = '';
|
||||
formData.component = '';
|
||||
formData.hidden = 0;
|
||||
formData.keepAlive = 0;
|
||||
} else if (type === 'catalog') {
|
||||
formData.component = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
const updateData: UpdateMenuParams = {
|
||||
menuId: formData.menuId!,
|
||||
parentId: formData.parentId,
|
||||
title: formData.title,
|
||||
type: formData.type,
|
||||
path: formData.path,
|
||||
component: formData.component,
|
||||
icon: formData.icon,
|
||||
permission: formData.permission,
|
||||
sort: formData.sort,
|
||||
hidden: formData.hidden,
|
||||
keepAlive: formData.keepAlive,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
};
|
||||
await updateMenuApi(updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
const createData: CreateMenuParams = {
|
||||
parentId: formData.parentId,
|
||||
title: formData.title,
|
||||
type: formData.type,
|
||||
path: formData.path,
|
||||
component: formData.component,
|
||||
icon: formData.icon,
|
||||
permission: formData.permission,
|
||||
sort: formData.sort,
|
||||
hidden: formData.hidden,
|
||||
keepAlive: formData.keepAlive,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
};
|
||||
await createMenuApi(createData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
menuId: undefined,
|
||||
parentId: 0,
|
||||
title: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
permission: '',
|
||||
sort: 0,
|
||||
hidden: 0,
|
||||
keepAlive: 1,
|
||||
status: 1,
|
||||
remark: '',
|
||||
});
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
660
admin/apps/web-ele/src/views/common/rbac/permission/index.vue
Normal file
660
admin/apps/web-ele/src/views/common/rbac/permission/index.vue
Normal file
@@ -0,0 +1,660 @@
|
||||
<template>
|
||||
<Page>
|
||||
<div class="p-4">
|
||||
<!-- 搜索表单 -->
|
||||
<el-card class="mb-4">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="权限名称">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入权限名称或标识"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择类型" clearable>
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="按钮" value="button" />
|
||||
<el-option label="接口" value="api" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="ep:search" class="mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-card class="mb-4">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-1" />
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleSyncFromMenu">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
从菜单同步
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="permissionId" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="权限名称" min-width="150" />
|
||||
<el-table-column prop="code" label="权限标识" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="type" label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.type)">
|
||||
{{ getTypeText(row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="resource" label="资源路径" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="method" label="请求方法" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.method" :type="getMethodTagType(row.method)" size="small">
|
||||
{{ row.method }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="menuId" label="关联菜单" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.menuId" type="info" size="small">
|
||||
{{ getMenuName(row.menuId) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="700px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="权限名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入权限名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="权限标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入权限标识" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="权限类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择类型" @change="handleTypeChange">
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="按钮" value="button" />
|
||||
<el-option label="接口" value="api" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="关联菜单" prop="menuId">
|
||||
<el-select v-model="formData.menuId" placeholder="请选择菜单" clearable filterable>
|
||||
<el-option
|
||||
v-for="menu in menuOptions"
|
||||
:key="menu.menuId"
|
||||
:label="menu.title"
|
||||
:value="menu.menuId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资源路径" prop="resource">
|
||||
<el-input v-model="formData.resource" placeholder="请输入资源路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 'api'">
|
||||
<el-form-item label="请求方法" prop="method">
|
||||
<el-select v-model="formData.method" placeholder="请选择请求方法">
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
<el-option label="DELETE" value="DELETE" />
|
||||
<el-option label="PATCH" value="PATCH" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入权限描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 1. Vue 相关导入
|
||||
import { ref, reactive, onMounted, computed, type FormInstance } from 'vue';
|
||||
|
||||
// 2. Element Plus 组件导入
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
// 3. 图标组件导入
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 4. Vben 组件导入
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 5. 项目内部导入
|
||||
import {
|
||||
getPermissionListApi,
|
||||
createPermissionApi,
|
||||
updatePermissionApi,
|
||||
deletePermissionApi,
|
||||
batchDeletePermissionApi,
|
||||
syncPermissionFromMenuApi,
|
||||
type Permission,
|
||||
type CreatePermissionParams,
|
||||
type UpdatePermissionParams,
|
||||
} from '#/api/rbac';
|
||||
import { getMenuListApi, type Menu } from '#/api/rbac';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const tableData = ref<Permission[]>([]);
|
||||
const selectedRows = ref<Permission[]>([]);
|
||||
const menuOptions = ref<Menu[]>([]);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
type: undefined as string | undefined,
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreatePermissionParams & { permissionId?: number }>({
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'button',
|
||||
resource: '',
|
||||
method: '',
|
||||
menuId: undefined,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入权限名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '权限名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入权限标识', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '权限标识长度在 2 到 100 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9_:.-]*$/, message: '权限标识格式不正确', trigger: 'blur' },
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择权限类型', trigger: 'change' },
|
||||
],
|
||||
resource: [
|
||||
{ max: 200, message: '资源路径不能超过 200 个字符', trigger: 'blur' },
|
||||
],
|
||||
method: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.type === 'api' && !value) {
|
||||
callback(new Error('接口类型权限必须选择请求方法'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{ max: 500, message: '描述不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑权限' : '新增权限'));
|
||||
|
||||
// 方法
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const map = {
|
||||
menu: '菜单',
|
||||
button: '按钮',
|
||||
api: '接口',
|
||||
};
|
||||
return map[type as keyof typeof map] || type;
|
||||
};
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const map = {
|
||||
menu: 'primary',
|
||||
button: 'success',
|
||||
api: 'warning',
|
||||
};
|
||||
return map[type as keyof typeof map] || 'info';
|
||||
};
|
||||
|
||||
const getMethodTagType = (method: string) => {
|
||||
const map = {
|
||||
GET: 'primary',
|
||||
POST: 'success',
|
||||
PUT: 'warning',
|
||||
DELETE: 'danger',
|
||||
PATCH: 'info',
|
||||
};
|
||||
return map[method as keyof typeof map] || 'info';
|
||||
};
|
||||
|
||||
const getMenuName = (menuId: number) => {
|
||||
const menu = menuOptions.value.find(m => m.menuId === menuId);
|
||||
return menu ? menu.title : `菜单${menuId}`;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
type: searchForm.type,
|
||||
status: searchForm.status,
|
||||
};
|
||||
const result = await getPermissionListApi(params);
|
||||
tableData.value = result.list;
|
||||
pagination.total = result.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenuOptions = async () => {
|
||||
try {
|
||||
const result = await getMenuListApi({});
|
||||
// 扁平化菜单树
|
||||
const flattenMenu = (menus: Menu[]): Menu[] => {
|
||||
let result: Menu[] = [];
|
||||
menus.forEach(menu => {
|
||||
result.push(menu);
|
||||
if (menu.children) {
|
||||
result = result.concat(flattenMenu(menu.children));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
menuOptions.value = flattenMenu(result);
|
||||
} catch (error) {
|
||||
ElMessage.error('加载菜单选项失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.type = undefined;
|
||||
searchForm.status = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.limit = size;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selection: Permission[]) => {
|
||||
selectedRows.value = selection;
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
isEdit.value = false;
|
||||
await loadMenuOptions();
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = async (row: Permission) => {
|
||||
isEdit.value = true;
|
||||
await loadMenuOptions();
|
||||
Object.assign(formData, {
|
||||
permissionId: row.permissionId,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
type: row.type,
|
||||
resource: row.resource,
|
||||
method: row.method,
|
||||
menuId: row.menuId,
|
||||
sort: row.sort,
|
||||
status: row.status,
|
||||
description: row.description,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (row: Permission) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除权限 "${row.name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await deletePermissionApi(row.permissionId);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 个权限吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
const permissionIds = selectedRows.value.map(row => row.permissionId);
|
||||
await batchDeletePermissionApi(permissionIds);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncFromMenu = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要从菜单同步权限吗?这将根据菜单自动创建对应的权限。',
|
||||
'确认同步',
|
||||
{
|
||||
type: 'info',
|
||||
}
|
||||
);
|
||||
const result = await syncPermissionFromMenuApi();
|
||||
ElMessage.success(`同步成功,新增 ${result.created} 个权限,更新 ${result.updated} 个权限`);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('同步失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypeChange = (type: string) => {
|
||||
// 根据类型清空相关字段
|
||||
if (type !== 'api') {
|
||||
formData.method = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
const updateData: UpdatePermissionParams = {
|
||||
permissionId: formData.permissionId!,
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
type: formData.type,
|
||||
resource: formData.resource,
|
||||
method: formData.method,
|
||||
menuId: formData.menuId,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
description: formData.description,
|
||||
};
|
||||
await updatePermissionApi(updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
const createData: CreatePermissionParams = {
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
type: formData.type,
|
||||
resource: formData.resource,
|
||||
method: formData.method,
|
||||
menuId: formData.menuId,
|
||||
sort: formData.sort,
|
||||
status: formData.status,
|
||||
description: formData.description,
|
||||
};
|
||||
await createPermissionApi(createData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
permissionId: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'button',
|
||||
resource: '',
|
||||
method: '',
|
||||
menuId: undefined,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
description: '',
|
||||
});
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
826
admin/apps/web-ele/src/views/common/rbac/role/index.vue
Normal file
826
admin/apps/web-ele/src/views/common/rbac/role/index.vue
Normal file
@@ -0,0 +1,826 @@
|
||||
<template>
|
||||
<Page
|
||||
description="管理系统角色权限信息"
|
||||
title="角色管理"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- 搜索表单 -->
|
||||
<el-card>
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="角色名称">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入角色名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="ep:search" class="mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮和数据表格 -->
|
||||
<el-card>
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-1" />
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="roleId" label="ID" width="80" />
|
||||
<el-table-column prop="roleName" label="角色名称" min-width="150" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="rules" label="权限规则" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.rules === '*'" type="danger">超级管理员</el-tag>
|
||||
<el-tag v-else-if="!row.rules" type="info">无权限</el-tag>
|
||||
<el-tooltip v-else :content="row.rules" placement="top">
|
||||
<el-tag type="primary">{{ getPermissionCount(row.rules) }}个权限</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="handleSetPermission(row)">
|
||||
设置权限
|
||||
</el-button>
|
||||
<el-button type="info" size="small" @click="handleViewUsers(row)">
|
||||
查看用户
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
:disabled="row.roleId === 1"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="formData.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">正常</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 权限设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="permissionDialogVisible"
|
||||
title="设置权限"
|
||||
width="800px"
|
||||
@close="handlePermissionDialogClose"
|
||||
>
|
||||
<div class="permission-content">
|
||||
<div class="permission-header">
|
||||
<span>为角色 "{{ currentRole?.roleName }}" 设置权限</span>
|
||||
<div class="permission-actions">
|
||||
<el-button size="small" @click="handleExpandAll">展开全部</el-button>
|
||||
<el-button size="small" @click="handleCollapseAll">收起全部</el-button>
|
||||
<el-button size="small" @click="handleCheckAll">全选</el-button>
|
||||
<el-button size="small" @click="handleUncheckAll">取消全选</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTreeData"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="checkedPermissions"
|
||||
:default-expand-all="false"
|
||||
class="permission-tree"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
<Icon :icon="data.icon || 'ep:folder'" class="mr-2" />
|
||||
<span>{{ data.title }}</span>
|
||||
<el-tag v-if="data.type" size="small" class="ml-2">
|
||||
{{ getNodeTypeText(data.type) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="permissionDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSavePermission" :loading="permissionLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户列表对话框 -->
|
||||
<el-dialog
|
||||
v-model="userDialogVisible"
|
||||
title="角色用户列表"
|
||||
width="800px"
|
||||
>
|
||||
<div class="user-content">
|
||||
<div class="user-header">
|
||||
<span>角色 "{{ currentRole?.roleName }}" 的用户列表</span>
|
||||
<el-button type="primary" size="small" @click="handleAddUser">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
添加用户
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-loading="userLoading" :data="roleUsers">
|
||||
<el-table-column prop="userId" label="用户ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="realName" label="真实姓名" min-width="120" />
|
||||
<el-table-column prop="mobile" label="手机号" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="150" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleRemoveUser(row)"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="addUserDialogVisible"
|
||||
title="添加用户到角色"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="addUserForm" label-width="100px">
|
||||
<el-form-item label="选择用户">
|
||||
<el-select
|
||||
v-model="addUserForm.userIds"
|
||||
multiple
|
||||
placeholder="请选择用户"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchUsers"
|
||||
:loading="searchUserLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in availableUsers"
|
||||
:key="user.userId"
|
||||
:label="`${user.username} (${user.realName})`"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addUserDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveAddUser" :loading="addUserLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type ElTree } from 'element-plus';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
getRoleListApi,
|
||||
createRoleApi,
|
||||
updateRoleApi,
|
||||
deleteRoleApi,
|
||||
batchDeleteRolesApi,
|
||||
getRoleUsersApi,
|
||||
addRoleUserApi,
|
||||
removeRoleUserApi,
|
||||
getPermissionTreeApi,
|
||||
type Role,
|
||||
type CreateRoleParams,
|
||||
type UpdateRoleParams,
|
||||
type PermissionTreeNode,
|
||||
} from '#/api/rbac';
|
||||
import { getAdminListApi, type AdminUser } from '#/api/user';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const permissionLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const addUserLoading = ref(false);
|
||||
const searchUserLoading = ref(false);
|
||||
const tableData = ref<Role[]>([]);
|
||||
const selectedRows = ref<Role[]>([]);
|
||||
const currentRole = ref<Role | null>(null);
|
||||
const roleUsers = ref<AdminUser[]>([]);
|
||||
const availableUsers = ref<AdminUser[]>([]);
|
||||
const permissionTreeData = ref<PermissionTreeNode[]>([]);
|
||||
const checkedPermissions = ref<string[]>([]);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false);
|
||||
const permissionDialogVisible = ref(false);
|
||||
const userDialogVisible = ref(false);
|
||||
const addUserDialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const permissionTreeRef = ref<InstanceType<typeof ElTree>>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreateRoleParams & { roleId?: number }>({
|
||||
roleName: '',
|
||||
description: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 添加用户表单
|
||||
const addUserForm = reactive({
|
||||
userIds: [] as number[],
|
||||
});
|
||||
|
||||
// 树形组件配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'title',
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
roleName: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '角色名称长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
description: [
|
||||
{ max: 200, message: '描述不能超过 200 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑角色' : '新增角色'));
|
||||
|
||||
// 方法
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getPermissionCount = (rules: string) => {
|
||||
if (!rules || rules === '*') return 0;
|
||||
try {
|
||||
const ruleArray = rules.split(',').filter(Boolean);
|
||||
return ruleArray.length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getNodeTypeText = (type: string) => {
|
||||
const map = {
|
||||
menu: '菜单',
|
||||
button: '按钮',
|
||||
api: '接口',
|
||||
};
|
||||
return map[type as keyof typeof map] || type;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status,
|
||||
};
|
||||
const result = await getRoleListApi(params);
|
||||
tableData.value = result.list;
|
||||
pagination.total = result.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPermissionTree = async () => {
|
||||
try {
|
||||
const result = await getPermissionTreeApi();
|
||||
permissionTreeData.value = result;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载权限树失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadRoleUsers = async (roleId: number) => {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const result = await getRoleUsersApi(roleId);
|
||||
roleUsers.value = result;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载角色用户失败');
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const searchUsers = async (keyword: string) => {
|
||||
if (!keyword) {
|
||||
availableUsers.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
searchUserLoading.value = true;
|
||||
try {
|
||||
const result = await getAdminListApi({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
keyword,
|
||||
});
|
||||
availableUsers.value = result.list;
|
||||
} catch (error) {
|
||||
ElMessage.error('搜索用户失败');
|
||||
} finally {
|
||||
searchUserLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.limit = size;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selection: Role[]) => {
|
||||
selectedRows.value = selection;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: Role) => {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, {
|
||||
roleId: row.roleId,
|
||||
roleName: row.roleName,
|
||||
description: row.description,
|
||||
status: row.status,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (row: Role) => {
|
||||
if (row.roleId === 1) {
|
||||
ElMessage.warning('超级管理员角色不能删除');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色 "${row.roleName}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await deleteRoleApi(row.roleId);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
const canDeleteRoles = selectedRows.value.filter(row => row.roleId !== 1);
|
||||
if (canDeleteRoles.length === 0) {
|
||||
ElMessage.warning('选中的角色中没有可删除的角色');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${canDeleteRoles.length} 个角色吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
const roleIds = canDeleteRoles.map(row => row.roleId);
|
||||
await batchDeleteRoleApi(roleIds);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetPermission = async (row: Role) => {
|
||||
currentRole.value = row;
|
||||
await loadPermissionTree();
|
||||
|
||||
// 解析当前角色的权限
|
||||
if (row.rules && row.rules !== '*') {
|
||||
checkedPermissions.value = row.rules.split(',').filter(Boolean);
|
||||
} else {
|
||||
checkedPermissions.value = [];
|
||||
}
|
||||
|
||||
permissionDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleViewUsers = async (row: Role) => {
|
||||
currentRole.value = row;
|
||||
await loadRoleUsers(row.roleId);
|
||||
userDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleAddUser = () => {
|
||||
addUserForm.userIds = [];
|
||||
availableUsers.value = [];
|
||||
addUserDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (user: AdminUser) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要将用户 "${user.username}" 从角色中移除吗?`,
|
||||
'确认移除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await removeRoleUserApi(currentRole.value!.roleId, user.userId);
|
||||
ElMessage.success('移除成功');
|
||||
loadRoleUsers(currentRole.value!.roleId);
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('移除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAddUser = async () => {
|
||||
if (!addUserForm.userIds.length) {
|
||||
ElMessage.warning('请选择要添加的用户');
|
||||
return;
|
||||
}
|
||||
|
||||
addUserLoading.value = true;
|
||||
try {
|
||||
await addRoleUsersApi(currentRole.value!.roleId, addUserForm.userIds);
|
||||
ElMessage.success('添加成功');
|
||||
addUserDialogVisible.value = false;
|
||||
loadRoleUsers(currentRole.value!.roleId);
|
||||
} catch (error) {
|
||||
ElMessage.error('添加失败');
|
||||
} finally {
|
||||
addUserLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandAll = () => {
|
||||
const tree = permissionTreeRef.value;
|
||||
if (tree) {
|
||||
const expandAll = (nodes: PermissionTreeNode[]) => {
|
||||
nodes.forEach(node => {
|
||||
tree.setExpanded(node.id, true);
|
||||
if (node.children) {
|
||||
expandAll(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
expandAll(permissionTreeData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
const tree = permissionTreeRef.value;
|
||||
if (tree) {
|
||||
const collapseAll = (nodes: PermissionTreeNode[]) => {
|
||||
nodes.forEach(node => {
|
||||
tree.setExpanded(node.id, false);
|
||||
if (node.children) {
|
||||
collapseAll(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
collapseAll(permissionTreeData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckAll = () => {
|
||||
const tree = permissionTreeRef.value;
|
||||
if (tree) {
|
||||
const getAllNodeIds = (nodes: PermissionTreeNode[]): string[] => {
|
||||
let ids: string[] = [];
|
||||
nodes.forEach(node => {
|
||||
ids.push(node.id);
|
||||
if (node.children) {
|
||||
ids = ids.concat(getAllNodeIds(node.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
const allIds = getAllNodeIds(permissionTreeData.value);
|
||||
tree.setCheckedKeys(allIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUncheckAll = () => {
|
||||
const tree = permissionTreeRef.value;
|
||||
if (tree) {
|
||||
tree.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSavePermission = async () => {
|
||||
const tree = permissionTreeRef.value;
|
||||
if (!tree || !currentRole.value) return;
|
||||
|
||||
permissionLoading.value = true;
|
||||
try {
|
||||
const checkedKeys = tree.getCheckedKeys() as string[];
|
||||
const halfCheckedKeys = tree.getHalfCheckedKeys() as string[];
|
||||
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
|
||||
|
||||
await setRolePermissionApi(currentRole.value.roleId, allCheckedKeys);
|
||||
ElMessage.success('权限设置成功');
|
||||
permissionDialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error('权限设置失败');
|
||||
} finally {
|
||||
permissionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
const updateData: UpdateRoleParams = {
|
||||
roleId: formData.roleId!,
|
||||
roleName: formData.roleName,
|
||||
description: formData.description,
|
||||
status: formData.status,
|
||||
};
|
||||
await updateRoleApi(updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
const createData: CreateRoleParams = {
|
||||
roleName: formData.roleName,
|
||||
description: formData.description,
|
||||
status: formData.status,
|
||||
};
|
||||
await createRoleApi(createData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const handlePermissionDialogClose = () => {
|
||||
currentRole.value = null;
|
||||
checkedPermissions.value = [];
|
||||
permissionTreeData.value = [];
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
roleId: undefined,
|
||||
roleName: '',
|
||||
description: '',
|
||||
status: 1,
|
||||
});
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.permission-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permission-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.permission-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.permission-tree {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
559
admin/apps/web-ele/src/views/common/settings/email/index.vue
Normal file
559
admin/apps/web-ele/src/views/common/settings/email/index.vue
Normal file
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<Page>
|
||||
<VbenCard title="邮件设置">
|
||||
<template #extra>
|
||||
<Icon icon="lucide:mail" class="text-lg" />
|
||||
</template>
|
||||
|
||||
<VbenTabs v-model:active-key="activeTab" type="card">
|
||||
<!-- SMTP配置 -->
|
||||
<VbenTabPane key="smtp" tab="SMTP配置">
|
||||
<VbenForm @submit="handleSubmitSmtp">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="smtp_host" label="SMTP服务器">
|
||||
<Input v-model:value="form.smtp_host" placeholder="请输入SMTP服务器地址" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_port" label="端口">
|
||||
<InputNumber
|
||||
v-model:value="form.smtp_port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
placeholder="请输入端口号"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_username" label="用户名">
|
||||
<Input v-model:value="form.smtp_username" placeholder="请输入邮箱用户名" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_password" label="密码">
|
||||
<Input
|
||||
v-model:value="form.smtp_password"
|
||||
type="password"
|
||||
placeholder="请输入邮箱密码或授权码"
|
||||
show-password
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_from_email" label="发件人邮箱">
|
||||
<Input v-model:value="form.smtp_from_email" placeholder="请输入发件人邮箱" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_from_name" label="发件人名称">
|
||||
<Input v-model:value="form.smtp_from_name" placeholder="请输入发件人名称" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_encryption" label="加密方式">
|
||||
<Select v-model:value="form.smtp_encryption" placeholder="请选择加密方式">
|
||||
<template #default>
|
||||
<SelectOption value="">无</SelectOption>
|
||||
<SelectOption value="ssl">SSL</SelectOption>
|
||||
<SelectOption value="tls">TLS</SelectOption>
|
||||
</template>
|
||||
</Select>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="smtp_enabled" label="启用状态">
|
||||
<Switch v-model:checked="form.smtp_enabled" />
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存设置
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handleTestEmail" :loading="testLoading">
|
||||
<Icon icon="lucide:send" class="mr-1" />
|
||||
发送测试邮件
|
||||
</DefaultButton>
|
||||
<DefaultButton @click="handleResetSmtp">
|
||||
<Icon icon="lucide:rotate-ccw" class="mr-1" />
|
||||
重置
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 邮件模板 -->
|
||||
<VbenTabPane key="template" tab="邮件模板">
|
||||
<VbenTabs v-model:active-key="templateTab" type="line">
|
||||
<!-- 注册验证模板 -->
|
||||
<VbenTabPane key="register" tab="注册验证">
|
||||
<VbenForm @submit="handleSubmitTemplate">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="register_subject" label="邮件标题">
|
||||
<Input
|
||||
v-model:value="form.register_subject"
|
||||
placeholder="请输入注册验证邮件标题"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="register_content" label="邮件内容">
|
||||
<Input
|
||||
v-model:value="form.register_content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入注册验证邮件内容,可使用变量:{username}、{code}、{expire}"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="register_enabled" label="启用状态">
|
||||
<Switch v-model:checked="form.register_enabled" />
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存模板
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handlePreviewTemplate('register')">
|
||||
<Icon icon="lucide:eye" class="mr-1" />
|
||||
预览模板
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 找回密码模板 -->
|
||||
<VbenTabPane key="reset" tab="找回密码">
|
||||
<VbenForm @submit="handleSubmitTemplate">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="reset_subject" label="邮件标题">
|
||||
<Input
|
||||
v-model:value="form.reset_subject"
|
||||
placeholder="请输入找回密码邮件标题"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="reset_content" label="邮件内容">
|
||||
<Input
|
||||
v-model:value="form.reset_content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入找回密码邮件内容,可使用变量:{username}、{code}、{expire}"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="reset_enabled" label="启用状态">
|
||||
<Switch v-model:checked="form.reset_enabled" />
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存模板
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handlePreviewTemplate('reset')">
|
||||
<Icon icon="lucide:eye" class="mr-1" />
|
||||
预览模板
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 通知邮件模板 -->
|
||||
<VbenTabPane key="notify" tab="通知邮件">
|
||||
<VbenForm @submit="handleSubmitTemplate">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="notify_subject" label="邮件标题">
|
||||
<Input
|
||||
v-model:value="form.notify_subject"
|
||||
placeholder="请输入通知邮件标题"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="notify_content" label="邮件内容">
|
||||
<Input
|
||||
v-model:value="form.notify_content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入通知邮件内容,可使用变量:{username}、{title}、{content}"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="notify_enabled" label="启用状态">
|
||||
<Switch v-model:checked="form.notify_enabled" />
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存模板
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handlePreviewTemplate('notify')">
|
||||
<Icon icon="lucide:eye" class="mr-1" />
|
||||
预览模板
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
</VbenTabs>
|
||||
</VbenTabPane>
|
||||
</VbenTabs>
|
||||
</VbenCard>
|
||||
|
||||
<!-- 测试邮件对话框 -->
|
||||
<VbenModal v-model:open="testDialogVisible" title="发送测试邮件" width="500px">
|
||||
<VbenForm @submit="handleSendTest">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="test_email" label="收件人邮箱">
|
||||
<Input
|
||||
v-model:value="form.test_email"
|
||||
placeholder="请输入测试邮箱地址"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="test_type" label="邮件类型">
|
||||
<Select v-model:value="form.test_type" placeholder="请选择邮件类型">
|
||||
<template #default>
|
||||
<SelectOption value="register">注册验证</SelectOption>
|
||||
<SelectOption value="reset">找回密码</SelectOption>
|
||||
<SelectOption value="notify">通知邮件</SelectOption>
|
||||
</template>
|
||||
</Select>
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="sendTestLoading">
|
||||
<Icon icon="lucide:send" class="mr-1" />
|
||||
发送
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="testDialogVisible = false">
|
||||
取消
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenModal>
|
||||
|
||||
<!-- 模板预览对话框 -->
|
||||
<VbenModal v-model:open="previewDialogVisible" title="模板预览" width="600px">
|
||||
<div class="template-preview">
|
||||
<div class="preview-item">
|
||||
<label>邮件标题:</label>
|
||||
<div class="preview-content">{{ previewData.subject }}</div>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<label>邮件内容:</label>
|
||||
<div class="preview-content" v-html="previewData.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<DefaultButton @click="previewDialogVisible = false">
|
||||
关闭
|
||||
</DefaultButton>
|
||||
</template>
|
||||
</VbenModal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import {
|
||||
Page,
|
||||
VbenCard,
|
||||
VbenTabs,
|
||||
VbenTabPane,
|
||||
VbenModal
|
||||
} from '@vben/common-ui';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import {
|
||||
getEmailConfigApi,
|
||||
updateEmailConfigApi,
|
||||
testEmailApi,
|
||||
previewEmailTemplateApi
|
||||
} from '#/api/common/email';
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('smtp');
|
||||
const templateTab = ref('register');
|
||||
const submitLoading = ref(false);
|
||||
const testLoading = ref(false);
|
||||
const sendTestLoading = ref(false);
|
||||
const testDialogVisible = ref(false);
|
||||
const previewDialogVisible = ref(false);
|
||||
|
||||
// 预览数据
|
||||
const previewData = reactive({
|
||||
subject: '',
|
||||
content: '',
|
||||
});
|
||||
|
||||
// SMTP配置表单配置
|
||||
const smtpFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'smtp_host',
|
||||
label: 'SMTP服务器',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入SMTP服务器地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'smtp_port',
|
||||
label: '端口',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
placeholder: '请输入端口号',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'smtp_username',
|
||||
label: '用户名',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱用户名',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'smtp_password',
|
||||
label: '密码',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
type: 'password',
|
||||
placeholder: '请输入邮箱密码或授权码',
|
||||
showPassword: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'smtp_from_email',
|
||||
label: '发件人邮箱',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入发件人邮箱',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'smtp_from_name',
|
||||
label: '发件人名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入发件人名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'smtp_encryption',
|
||||
label: '加密方式',
|
||||
componentProps: {
|
||||
placeholder: '请选择加密方式',
|
||||
options: [
|
||||
{ label: '无', value: '' },
|
||||
{ label: 'SSL', value: 'ssl' },
|
||||
{ label: 'TLS', value: 'tls' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'smtp_enabled',
|
||||
label: '启用状态',
|
||||
},
|
||||
];
|
||||
|
||||
// 邮件模板表单配置
|
||||
const templateFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'register_subject',
|
||||
label: '邮件标题',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入注册验证邮件标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'register_content',
|
||||
label: '邮件内容',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 6,
|
||||
placeholder: '请输入注册验证邮件内容,可使用变量:{username}、{code}、{expire}',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'register_enabled',
|
||||
label: '启用状态',
|
||||
},
|
||||
];
|
||||
|
||||
// 测试邮件表单配置
|
||||
const testFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'test_email',
|
||||
label: '收件人邮箱',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入测试邮箱地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'test_type',
|
||||
label: '邮件类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择邮件类型',
|
||||
options: [
|
||||
{ label: '注册验证', value: 'register' },
|
||||
{ label: '找回密码', value: 'reset' },
|
||||
{ label: '通知邮件', value: 'notify' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 创建表单实例
|
||||
const [SmtpForm, smtpFormApi] = useVbenForm({
|
||||
schema: smtpFormSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [TemplateForm, templateFormApi] = useVbenForm({
|
||||
schema: templateFormSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [TestForm, testFormApi] = useVbenForm({
|
||||
schema: testFormSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 处理SMTP配置提交
|
||||
const handleSubmitSmtp = async (values: Record<string, any>) => {
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
await updateEmailConfigApi('smtp', values);
|
||||
// 显示成功消息
|
||||
} catch (error) {
|
||||
console.error('保存SMTP配置失败:', error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理模板提交
|
||||
const handleSubmitTemplate = async (values: Record<string, any>) => {
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
await updateEmailConfigApi('template', values);
|
||||
// 显示成功消息
|
||||
} catch (error) {
|
||||
console.error('保存邮件模板失败:', error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理测试邮件
|
||||
const handleTestEmail = () => {
|
||||
testDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 发送测试邮件
|
||||
const handleSendTest = async (values: Record<string, any>) => {
|
||||
try {
|
||||
sendTestLoading.value = true;
|
||||
await testEmailApi(values.test_email, values.test_type);
|
||||
testDialogVisible.value = false;
|
||||
// 显示成功消息
|
||||
} catch (error) {
|
||||
console.error('发送测试邮件失败:', error);
|
||||
} finally {
|
||||
sendTestLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 预览模板
|
||||
const handlePreviewTemplate = async (type: string) => {
|
||||
try {
|
||||
const data = await previewEmailTemplateApi(type);
|
||||
previewData.subject = data.subject;
|
||||
previewData.content = data.content;
|
||||
previewDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('预览模板失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置SMTP配置
|
||||
const handleResetSmtp = () => {
|
||||
smtpFormApi.resetForm();
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
try {
|
||||
const [smtpData, templateData] = await Promise.all([
|
||||
getEmailConfigApi('smtp'),
|
||||
getEmailConfigApi('template'),
|
||||
]);
|
||||
|
||||
smtpFormApi.setValues(smtpData);
|
||||
templateFormApi.setValues(templateData);
|
||||
} catch (error) {
|
||||
console.error('初始化数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化数据
|
||||
onMounted(() => {
|
||||
initData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-preview {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-item label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
621
admin/apps/web-ele/src/views/common/settings/login/index.vue
Normal file
621
admin/apps/web-ele/src/views/common/settings/login/index.vue
Normal file
@@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<VbenTabs v-model:active-key="activeTab" type="card">
|
||||
<!-- 登录方式设置 -->
|
||||
<VbenTabPane key="methods" tab="登录方式">
|
||||
<VbenForm
|
||||
ref="methodsFormRef"
|
||||
:schema="methodsFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveMethods"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveMethods">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleResetMethods">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 安全设置 -->
|
||||
<VbenTabPane key="security" tab="安全设置">
|
||||
<VbenForm
|
||||
ref="securityFormRef"
|
||||
:schema="securityFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveSecurity"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveSecurity">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleResetSecurity">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<VbenTabPane key="oauth" tab="第三方登录">
|
||||
<VbenForm
|
||||
ref="oauthFormRef"
|
||||
:schema="oauthFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveOauth"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveOauth">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleResetOauth">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 注册设置 -->
|
||||
<VbenTabPane key="register" tab="注册设置">
|
||||
<VbenForm
|
||||
ref="registerFormRef"
|
||||
:schema="registerFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveRegister"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveRegister">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleResetRegister">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
</VbenTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { VbenForm, VbenButton, VbenTabs, VbenTabPane } from '@vben/components'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormSchema } from '@vben/types'
|
||||
import {
|
||||
getLoginConfigApi,
|
||||
updateLoginConfigApi,
|
||||
resetLoginConfigApi,
|
||||
type LoginConfig,
|
||||
} from '@/api/common/login'
|
||||
|
||||
const methodsFormRef = ref()
|
||||
const securityFormRef = ref()
|
||||
const oauthFormRef = ref()
|
||||
const registerFormRef = ref()
|
||||
const saveLoading = ref(false)
|
||||
const activeTab = ref('methods')
|
||||
|
||||
// 登录方式表单配置
|
||||
const methodsFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'username_enabled',
|
||||
label: '用户名登录',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '允许用户使用用户名进行登录',
|
||||
},
|
||||
{
|
||||
field: 'mobile_enabled',
|
||||
label: '手机号登录',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '允许用户使用手机号进行登录',
|
||||
},
|
||||
{
|
||||
field: 'email_enabled',
|
||||
label: '邮箱登录',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '允许用户使用邮箱进行登录',
|
||||
},
|
||||
{
|
||||
field: 'sms_enabled',
|
||||
label: '短信验证码登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '允许用户使用短信验证码进行登录',
|
||||
},
|
||||
{
|
||||
field: 'oauth_enabled',
|
||||
label: '第三方登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '启用微信、QQ等第三方登录方式',
|
||||
},
|
||||
{
|
||||
field: 'guest_enabled',
|
||||
label: '游客模式',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '允许游客访问部分功能,无需注册登录',
|
||||
},
|
||||
]
|
||||
|
||||
// 安全设置表单配置
|
||||
const securityFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'captcha_enabled',
|
||||
label: '登录验证码',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '登录时需要输入图形验证码',
|
||||
},
|
||||
{
|
||||
field: 'captcha_type',
|
||||
label: '验证码类型',
|
||||
component: 'RadioGroup',
|
||||
show: ({ values }) => values.captcha_enabled,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '图形验证码', value: 'image' },
|
||||
{ label: '滑动验证码', value: 'slide' },
|
||||
{ label: '点击验证码', value: 'click' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'image',
|
||||
},
|
||||
{
|
||||
field: 'max_fail_attempts',
|
||||
label: '最大登录失败次数',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 3,
|
||||
max: 20,
|
||||
addonAfter: '次',
|
||||
},
|
||||
defaultValue: 5,
|
||||
helpMessage: '超过此次数将锁定账户',
|
||||
},
|
||||
{
|
||||
field: 'lock_duration',
|
||||
label: '账户锁定时间',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 5,
|
||||
max: 1440,
|
||||
addonAfter: '分钟',
|
||||
},
|
||||
defaultValue: 30,
|
||||
helpMessage: '账户被锁定后的解锁时间',
|
||||
},
|
||||
{
|
||||
field: 'password_strength_enabled',
|
||||
label: '密码强度检查',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '强制用户使用强密码',
|
||||
},
|
||||
{
|
||||
field: 'password_min_length',
|
||||
label: '密码最小长度',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.password_strength_enabled,
|
||||
componentProps: {
|
||||
min: 6,
|
||||
max: 20,
|
||||
addonAfter: '位',
|
||||
},
|
||||
defaultValue: 8,
|
||||
},
|
||||
{
|
||||
field: 'password_complexity',
|
||||
label: '密码复杂度要求',
|
||||
component: 'CheckboxGroup',
|
||||
show: ({ values }) => values.password_strength_enabled,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '包含大写字母', value: 'uppercase' },
|
||||
{ label: '包含小写字母', value: 'lowercase' },
|
||||
{ label: '包含数字', value: 'number' },
|
||||
{ label: '包含特殊字符', value: 'special' },
|
||||
],
|
||||
},
|
||||
defaultValue: ['lowercase', 'number'],
|
||||
},
|
||||
{
|
||||
field: 'password_expire_enabled',
|
||||
label: '强制定期修改密码',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '强制用户定期修改密码',
|
||||
},
|
||||
{
|
||||
field: 'password_expire_days',
|
||||
label: '密码有效期',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.password_expire_enabled,
|
||||
componentProps: {
|
||||
min: 30,
|
||||
max: 365,
|
||||
addonAfter: '天',
|
||||
},
|
||||
defaultValue: 90,
|
||||
},
|
||||
{
|
||||
field: 'single_sign_on_enabled',
|
||||
label: '单点登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '同一账户只能在一个设备上登录',
|
||||
},
|
||||
]
|
||||
|
||||
// 第三方登录表单配置
|
||||
const oauthFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'wechat_enabled',
|
||||
label: '启用微信登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
field: 'wechat_app_id',
|
||||
label: '微信应用ID',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.wechat_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_secret',
|
||||
label: '微信应用密钥',
|
||||
component: 'InputPassword',
|
||||
show: ({ values }) => values.wechat_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_redirect_uri',
|
||||
label: '微信回调地址',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.wechat_enabled,
|
||||
helpMessage: '微信登录授权回调地址',
|
||||
},
|
||||
{
|
||||
field: 'qq_enabled',
|
||||
label: '启用QQ登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
field: 'qq_app_id',
|
||||
label: 'QQ应用ID',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.qq_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'qq_secret',
|
||||
label: 'QQ应用密钥',
|
||||
component: 'InputPassword',
|
||||
show: ({ values }) => values.qq_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'qq_redirect_uri',
|
||||
label: 'QQ回调地址',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.qq_enabled,
|
||||
helpMessage: 'QQ登录授权回调地址',
|
||||
},
|
||||
{
|
||||
field: 'github_enabled',
|
||||
label: '启用GitHub登录',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
field: 'github_client_id',
|
||||
label: 'GitHub客户端ID',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.github_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'github_secret',
|
||||
label: 'GitHub客户端密钥',
|
||||
component: 'InputPassword',
|
||||
show: ({ values }) => values.github_enabled,
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'github_redirect_uri',
|
||||
label: 'GitHub回调地址',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.github_enabled,
|
||||
helpMessage: 'GitHub登录授权回调地址',
|
||||
},
|
||||
]
|
||||
|
||||
// 注册设置表单配置
|
||||
const registerFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'register_enabled',
|
||||
label: '开放注册',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
helpMessage: '是否允许用户注册新账户',
|
||||
},
|
||||
{
|
||||
field: 'register_methods',
|
||||
label: '注册方式',
|
||||
component: 'CheckboxGroup',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '用户名注册', value: 'username' },
|
||||
{ label: '手机号注册', value: 'mobile' },
|
||||
{ label: '邮箱注册', value: 'email' },
|
||||
],
|
||||
},
|
||||
defaultValue: ['username', 'mobile'],
|
||||
},
|
||||
{
|
||||
field: 'register_verification',
|
||||
label: '注册验证方式',
|
||||
component: 'RadioGroup',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '无需验证', value: 'none' },
|
||||
{ label: '邮箱验证', value: 'email' },
|
||||
{ label: '短信验证', value: 'sms' },
|
||||
{ label: '人工审核', value: 'manual' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'sms',
|
||||
},
|
||||
{
|
||||
field: 'register_captcha_enabled',
|
||||
label: '注册验证码',
|
||||
component: 'Switch',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
defaultValue: true,
|
||||
helpMessage: '注册时需要输入验证码',
|
||||
},
|
||||
{
|
||||
field: 'agreement_required',
|
||||
label: '用户协议',
|
||||
component: 'Switch',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
defaultValue: true,
|
||||
helpMessage: '用户注册时是否必须同意用户协议',
|
||||
},
|
||||
{
|
||||
field: 'agreement_content',
|
||||
label: '协议内容',
|
||||
component: 'InputTextArea',
|
||||
show: ({ values }) => values.register_enabled && values.agreement_required,
|
||||
componentProps: {
|
||||
rows: 6,
|
||||
placeholder: '请输入用户协议内容',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'default_role',
|
||||
label: '默认用户组',
|
||||
component: 'Select',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '普通用户', value: 'user' },
|
||||
{ label: 'VIP用户', value: 'vip' },
|
||||
{ label: '会员', value: 'member' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'user',
|
||||
},
|
||||
{
|
||||
field: 'reward_enabled',
|
||||
label: '注册奖励',
|
||||
component: 'Switch',
|
||||
show: ({ values }) => values.register_enabled,
|
||||
defaultValue: true,
|
||||
helpMessage: '新用户注册时给予奖励',
|
||||
},
|
||||
{
|
||||
field: 'reward_points',
|
||||
label: '奖励积分',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.register_enabled && values.reward_enabled,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
defaultValue: 100,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'reward_balance',
|
||||
label: '奖励余额',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.register_enabled && values.reward_enabled,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
addonAfter: '元',
|
||||
},
|
||||
defaultValue: 0,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
]
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const data = await getLoginConfigApi()
|
||||
methodsFormRef.value?.setFieldsValue(data.methods || {})
|
||||
securityFormRef.value?.setFieldsValue(data.security || {})
|
||||
oauthFormRef.value?.setFieldsValue(data.oauth || {})
|
||||
registerFormRef.value?.setFieldsValue(data.register || {})
|
||||
} catch (error) {
|
||||
message.error('加载配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存登录方式配置
|
||||
const handleSaveMethods = async () => {
|
||||
try {
|
||||
const values = await methodsFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateLoginConfigApi({ type: 'methods', config: values })
|
||||
message.success('登录方式配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存安全设置配置
|
||||
const handleSaveSecurity = async () => {
|
||||
try {
|
||||
const values = await securityFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateLoginConfigApi({ type: 'security', config: values })
|
||||
message.success('安全设置配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存第三方登录配置
|
||||
const handleSaveOauth = async () => {
|
||||
try {
|
||||
const values = await oauthFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateLoginConfigApi({ type: 'oauth', config: values })
|
||||
message.success('第三方登录配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存注册设置配置
|
||||
const handleSaveRegister = async () => {
|
||||
try {
|
||||
const values = await registerFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateLoginConfigApi({ type: 'register', config: values })
|
||||
message.success('注册设置配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
const handleResetMethods = async () => {
|
||||
try {
|
||||
await resetLoginConfigApi('methods')
|
||||
await loadConfig()
|
||||
message.success('登录方式配置已重置')
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetSecurity = async () => {
|
||||
try {
|
||||
await resetLoginConfigApi('security')
|
||||
await loadConfig()
|
||||
message.success('安全设置配置已重置')
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetOauth = async () => {
|
||||
try {
|
||||
await resetLoginConfigApi('oauth')
|
||||
await loadConfig()
|
||||
message.success('第三方登录配置已重置')
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetRegister = async () => {
|
||||
try {
|
||||
await resetLoginConfigApi('register')
|
||||
await loadConfig()
|
||||
message.success('注册设置配置已重置')
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,905 @@
|
||||
<template>
|
||||
<Page>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<Icon icon="ep:bell" class="mr-2" />
|
||||
<span>通知设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<!-- 邮件通知 -->
|
||||
<el-tab-pane label="邮件通知" name="email">
|
||||
<el-form
|
||||
ref="emailFormRef"
|
||||
:model="emailForm"
|
||||
:rules="emailRules"
|
||||
label-width="150px"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item label="启用邮件通知" prop="enabled">
|
||||
<el-switch
|
||||
v-model="emailForm.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">启用后系统将发送邮件通知</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="emailForm.enabled">
|
||||
<el-form-item label="通知类型" prop="types">
|
||||
<el-checkbox-group v-model="emailForm.types">
|
||||
<el-checkbox label="user_register">用户注册</el-checkbox>
|
||||
<el-checkbox label="user_login">用户登录</el-checkbox>
|
||||
<el-checkbox label="password_reset">密码重置</el-checkbox>
|
||||
<el-checkbox label="order_created">订单创建</el-checkbox>
|
||||
<el-checkbox label="order_paid">订单支付</el-checkbox>
|
||||
<el-checkbox label="order_shipped">订单发货</el-checkbox>
|
||||
<el-checkbox label="order_completed">订单完成</el-checkbox>
|
||||
<el-checkbox label="system_error">系统错误</el-checkbox>
|
||||
<el-checkbox label="security_alert">安全警报</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理员邮箱" prop="adminEmails">
|
||||
<div class="email-list-container">
|
||||
<div class="email-list">
|
||||
<div v-for="(email, index) in emailForm.adminEmails" :key="index" class="email-item">
|
||||
<el-input
|
||||
v-model="emailForm.adminEmails[index]"
|
||||
placeholder="请输入管理员邮箱"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
text
|
||||
@click="removeAdminEmail(index)"
|
||||
class="ml-2"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="addAdminEmail" type="primary" text class="mt-2">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
添加邮箱
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发送频率限制" prop="rateLimit">
|
||||
<el-input-number
|
||||
v-model="emailForm.rateLimit"
|
||||
:min="1"
|
||||
:max="100"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">封/小时</span>
|
||||
<div class="form-item-tip">限制每小时发送的邮件数量</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="重试次数" prop="retryTimes">
|
||||
<el-input-number
|
||||
v-model="emailForm.retryTimes"
|
||||
:min="0"
|
||||
:max="5"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">次</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="队列延迟" prop="queueDelay">
|
||||
<el-input-number
|
||||
v-model="emailForm.queueDelay"
|
||||
:min="0"
|
||||
:max="3600"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">秒</span>
|
||||
<div class="form-item-tip">邮件发送延迟时间</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveEmail" :loading="saveLoading">
|
||||
<Icon icon="ep:check" class="mr-1" />
|
||||
保存设置
|
||||
</el-button>
|
||||
<el-button @click="handleResetEmail">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button @click="handleTestEmail" :loading="testEmailLoading" v-if="emailForm.enabled">
|
||||
<Icon icon="ep:message" class="mr-1" />
|
||||
发送测试邮件
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 短信通知 -->
|
||||
<el-tab-pane label="短信通知" name="sms">
|
||||
<el-form
|
||||
ref="smsFormRef"
|
||||
:model="smsForm"
|
||||
:rules="smsRules"
|
||||
label-width="150px"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item label="启用短信通知" prop="enabled">
|
||||
<el-switch
|
||||
v-model="smsForm.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">启用后系统将发送短信通知</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="smsForm.enabled">
|
||||
<el-form-item label="通知类型" prop="types">
|
||||
<el-checkbox-group v-model="smsForm.types">
|
||||
<el-checkbox label="user_register">用户注册</el-checkbox>
|
||||
<el-checkbox label="login_verify">登录验证</el-checkbox>
|
||||
<el-checkbox label="password_reset">密码重置</el-checkbox>
|
||||
<el-checkbox label="order_status">订单状态变更</el-checkbox>
|
||||
<el-checkbox label="payment_notify">支付通知</el-checkbox>
|
||||
<el-checkbox label="security_alert">安全警报</el-checkbox>
|
||||
<el-checkbox label="marketing">营销推广</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理员手机" prop="adminPhones">
|
||||
<div class="phone-list-container">
|
||||
<div class="phone-list">
|
||||
<div v-for="(phone, index) in smsForm.adminPhones" :key="index" class="phone-item">
|
||||
<el-input
|
||||
v-model="smsForm.adminPhones[index]"
|
||||
placeholder="请输入管理员手机号"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
text
|
||||
@click="removeAdminPhone(index)"
|
||||
class="ml-2"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="addAdminPhone" type="primary" text class="mt-2">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
添加手机号
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发送时间限制">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开始时间" prop="sendTimeStart" label-width="80px">
|
||||
<el-time-picker
|
||||
v-model="smsForm.sendTimeStart"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="选择开始时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结束时间" prop="sendTimeEnd" label-width="80px">
|
||||
<el-time-picker
|
||||
v-model="smsForm.sendTimeEnd"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="选择结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="form-item-tip">限制短信发送的时间段,避免打扰用户</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发送频率限制" prop="rateLimit">
|
||||
<el-input-number
|
||||
v-model="smsForm.rateLimit"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">条/小时</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="同号码限制" prop="phoneLimit">
|
||||
<el-input-number
|
||||
v-model="smsForm.phoneLimit"
|
||||
:min="1"
|
||||
:max="10"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">条/天</span>
|
||||
<div class="form-item-tip">限制同一手机号每天接收的短信数量</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveSms" :loading="saveLoading">
|
||||
<Icon icon="ep:check" class="mr-1" />
|
||||
保存设置
|
||||
</el-button>
|
||||
<el-button @click="handleResetSms">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button @click="handleTestSms" :loading="testSmsLoading" v-if="smsForm.enabled">
|
||||
<Icon icon="ep:chat-dot-round" class="mr-1" />
|
||||
发送测试短信
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 站内通知 -->
|
||||
<el-tab-pane label="站内通知" name="system">
|
||||
<el-form
|
||||
ref="systemFormRef"
|
||||
:model="systemForm"
|
||||
:rules="systemRules"
|
||||
label-width="150px"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item label="启用站内通知" prop="enabled">
|
||||
<el-switch
|
||||
v-model="systemForm.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">启用后系统将发送站内消息通知</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="systemForm.enabled">
|
||||
<el-form-item label="通知类型" prop="types">
|
||||
<el-checkbox-group v-model="systemForm.types">
|
||||
<el-checkbox label="user_register">用户注册</el-checkbox>
|
||||
<el-checkbox label="order_created">订单创建</el-checkbox>
|
||||
<el-checkbox label="order_paid">订单支付</el-checkbox>
|
||||
<el-checkbox label="order_shipped">订单发货</el-checkbox>
|
||||
<el-checkbox label="order_completed">订单完成</el-checkbox>
|
||||
<el-checkbox label="order_refund">订单退款</el-checkbox>
|
||||
<el-checkbox label="user_feedback">用户反馈</el-checkbox>
|
||||
<el-checkbox label="system_maintenance">系统维护</el-checkbox>
|
||||
<el-checkbox label="promotion">促销活动</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="消息保留期" prop="retentionDays">
|
||||
<el-input-number
|
||||
v-model="systemForm.retentionDays"
|
||||
:min="7"
|
||||
:max="365"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">天</span>
|
||||
<div class="form-item-tip">超过保留期的消息将被自动删除</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大消息数" prop="maxMessages">
|
||||
<el-input-number
|
||||
v-model="systemForm.maxMessages"
|
||||
:min="100"
|
||||
:max="10000"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">条</span>
|
||||
<div class="form-item-tip">每个用户最多保留的消息数量</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="自动标记已读" prop="autoMarkRead">
|
||||
<el-switch
|
||||
v-model="systemForm.autoMarkRead"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">用户查看消息后自动标记为已读</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推送到桌面" prop="desktopPush">
|
||||
<el-switch
|
||||
v-model="systemForm.desktopPush"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">支持浏览器桌面通知推送</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="声音提醒" prop="soundAlert">
|
||||
<el-switch
|
||||
v-model="systemForm.soundAlert"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">新消息时播放提示音</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveSystem" :loading="saveLoading">
|
||||
<Icon icon="ep:check" class="mr-1" />
|
||||
保存设置
|
||||
</el-button>
|
||||
<el-button @click="handleResetSystem">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button @click="handleTestSystem" :loading="testSystemLoading" v-if="systemForm.enabled">
|
||||
<Icon icon="ep:bell" class="mr-1" />
|
||||
发送测试通知
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 微信通知 -->
|
||||
<el-tab-pane label="微信通知" name="wechat">
|
||||
<el-form
|
||||
ref="wechatFormRef"
|
||||
:model="wechatForm"
|
||||
:rules="wechatRules"
|
||||
label-width="150px"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item label="启用微信通知" prop="enabled">
|
||||
<el-switch
|
||||
v-model="wechatForm.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
<div class="form-item-tip">启用后系统将发送微信模板消息</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="wechatForm.enabled">
|
||||
<el-form-item label="AppID" prop="appId">
|
||||
<el-input
|
||||
v-model="wechatForm.appId"
|
||||
placeholder="请输入微信公众号AppID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="AppSecret" prop="appSecret">
|
||||
<el-input
|
||||
v-model="wechatForm.appSecret"
|
||||
type="password"
|
||||
placeholder="请输入微信公众号AppSecret"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="通知类型" prop="types">
|
||||
<el-checkbox-group v-model="wechatForm.types">
|
||||
<el-checkbox label="order_created">订单创建</el-checkbox>
|
||||
<el-checkbox label="order_paid">订单支付</el-checkbox>
|
||||
<el-checkbox label="order_shipped">订单发货</el-checkbox>
|
||||
<el-checkbox label="order_completed">订单完成</el-checkbox>
|
||||
<el-checkbox label="order_refund">订单退款</el-checkbox>
|
||||
<el-checkbox label="payment_success">支付成功</el-checkbox>
|
||||
<el-checkbox label="account_change">账户变动</el-checkbox>
|
||||
<el-checkbox label="service_notice">服务通知</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模板消息配置">
|
||||
<div class="template-config">
|
||||
<div v-for="(template, key) in wechatForm.templates" :key="key" class="template-item">
|
||||
<div class="template-label">{{ getTemplateLabel(key) }}</div>
|
||||
<el-input
|
||||
v-model="wechatForm.templates[key]"
|
||||
placeholder="请输入模板ID"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发送频率限制" prop="rateLimit">
|
||||
<el-input-number
|
||||
v-model="wechatForm.rateLimit"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">条/小时</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveWechat" :loading="saveLoading">
|
||||
<Icon icon="ep:check" class="mr-1" />
|
||||
保存设置
|
||||
</el-button>
|
||||
<el-button @click="handleResetWechat">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button @click="handleTestWechat" :loading="testWechatLoading" v-if="wechatForm.enabled">
|
||||
<Icon icon="ep:chat-dot-round" class="mr-1" />
|
||||
发送测试消息
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 1. Vue 相关导入
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
// 2. Element Plus 组件导入
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElCol,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElRow,
|
||||
ElSwitch,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElTimePicker,
|
||||
} from 'element-plus';
|
||||
|
||||
// 3. 图标组件导入
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 4. Vben 组件导入
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 5. 项目内部导入
|
||||
import {
|
||||
getNotificationSettingsApi,
|
||||
updateNotificationSettingsApi,
|
||||
resetNotificationSettingsApi,
|
||||
testNotificationApi,
|
||||
type NotificationSettings,
|
||||
type UpdateNotificationSettingsParams,
|
||||
} from '#/api/settings';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const testEmailLoading = ref(false);
|
||||
const testSmsLoading = ref(false);
|
||||
const testSystemLoading = ref(false);
|
||||
const testWechatLoading = ref(false);
|
||||
const activeTab = ref('email');
|
||||
const emailFormRef = ref<FormInstance>();
|
||||
const smsFormRef = ref<FormInstance>();
|
||||
const systemFormRef = ref<FormInstance>();
|
||||
const wechatFormRef = ref<FormInstance>();
|
||||
|
||||
// 邮件通知表单
|
||||
const emailForm = reactive({
|
||||
enabled: true,
|
||||
types: ['user_register', 'order_created', 'system_error'],
|
||||
adminEmails: ['admin@example.com'],
|
||||
rateLimit: 50,
|
||||
retryTimes: 3,
|
||||
queueDelay: 0,
|
||||
});
|
||||
|
||||
// 短信通知表单
|
||||
const smsForm = reactive({
|
||||
enabled: false,
|
||||
types: ['user_register', 'login_verify', 'password_reset'],
|
||||
adminPhones: [''],
|
||||
sendTimeStart: '08:00',
|
||||
sendTimeEnd: '22:00',
|
||||
rateLimit: 100,
|
||||
phoneLimit: 5,
|
||||
});
|
||||
|
||||
// 站内通知表单
|
||||
const systemForm = reactive({
|
||||
enabled: true,
|
||||
types: ['user_register', 'order_created', 'order_paid'],
|
||||
retentionDays: 30,
|
||||
maxMessages: 1000,
|
||||
autoMarkRead: true,
|
||||
desktopPush: false,
|
||||
soundAlert: true,
|
||||
});
|
||||
|
||||
// 微信通知表单
|
||||
const wechatForm = reactive({
|
||||
enabled: false,
|
||||
appId: '',
|
||||
appSecret: '',
|
||||
types: ['order_created', 'order_paid', 'payment_success'],
|
||||
templates: {
|
||||
order_created: '',
|
||||
order_paid: '',
|
||||
order_shipped: '',
|
||||
order_completed: '',
|
||||
order_refund: '',
|
||||
payment_success: '',
|
||||
account_change: '',
|
||||
service_notice: '',
|
||||
},
|
||||
rateLimit: 100,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const emailRules = {
|
||||
adminEmails: [
|
||||
{ required: true, message: '请输入管理员邮箱', trigger: 'blur' },
|
||||
],
|
||||
rateLimit: [
|
||||
{ required: true, message: '请输入发送频率限制', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 100, message: '发送频率范围为 1-100 封/小时', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const smsRules = {
|
||||
adminPhones: [
|
||||
{ required: true, message: '请输入管理员手机号', trigger: 'blur' },
|
||||
],
|
||||
rateLimit: [
|
||||
{ required: true, message: '请输入发送频率限制', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 1000, message: '发送频率范围为 1-1000 条/小时', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const systemRules = {
|
||||
retentionDays: [
|
||||
{ required: true, message: '请输入消息保留期', trigger: 'blur' },
|
||||
{ type: 'number', min: 7, max: 365, message: '保留期范围为 7-365 天', trigger: 'blur' },
|
||||
],
|
||||
maxMessages: [
|
||||
{ required: true, message: '请输入最大消息数', trigger: 'blur' },
|
||||
{ type: 'number', min: 100, max: 10000, message: '消息数范围为 100-10000 条', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const wechatRules = {
|
||||
appId: [
|
||||
{ required: true, message: '请输入微信AppID', trigger: 'blur' },
|
||||
],
|
||||
appSecret: [
|
||||
{ required: true, message: '请输入微信AppSecret', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 方法
|
||||
const addAdminEmail = () => {
|
||||
emailForm.adminEmails.push('');
|
||||
};
|
||||
|
||||
const removeAdminEmail = (index: number) => {
|
||||
if (emailForm.adminEmails.length > 1) {
|
||||
emailForm.adminEmails.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const addAdminPhone = () => {
|
||||
smsForm.adminPhones.push('');
|
||||
};
|
||||
|
||||
const removeAdminPhone = (index: number) => {
|
||||
if (smsForm.adminPhones.length > 1) {
|
||||
smsForm.adminPhones.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const getTemplateLabel = (key: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
order_created: '订单创建',
|
||||
order_paid: '订单支付',
|
||||
order_shipped: '订单发货',
|
||||
order_completed: '订单完成',
|
||||
order_refund: '订单退款',
|
||||
payment_success: '支付成功',
|
||||
account_change: '账户变动',
|
||||
service_notice: '服务通知',
|
||||
};
|
||||
return labels[key] || key;
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const settings = await getNotificationSettingsApi();
|
||||
Object.assign(emailForm, settings.email || {});
|
||||
Object.assign(smsForm, settings.sms || {});
|
||||
Object.assign(systemForm, settings.system || {});
|
||||
Object.assign(wechatForm, settings.wechat || {});
|
||||
} catch (error) {
|
||||
ElMessage.error('加载通知设置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEmail = async () => {
|
||||
if (!emailFormRef.value) return;
|
||||
|
||||
try {
|
||||
await emailFormRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
// 过滤空的邮箱地址
|
||||
const filteredEmails = emailForm.adminEmails.filter(email => email.trim());
|
||||
|
||||
const updateData: UpdateNotificationSettingsParams = {
|
||||
type: 'email',
|
||||
config: {
|
||||
...emailForm,
|
||||
adminEmails: filteredEmails,
|
||||
},
|
||||
};
|
||||
|
||||
await updateNotificationSettingsApi(updateData);
|
||||
ElMessage.success('邮件通知设置保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveSms = async () => {
|
||||
if (!smsFormRef.value) return;
|
||||
|
||||
try {
|
||||
await smsFormRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
// 过滤空的手机号
|
||||
const filteredPhones = smsForm.adminPhones.filter(phone => phone.trim());
|
||||
|
||||
const updateData: UpdateNotificationSettingsParams = {
|
||||
type: 'sms',
|
||||
config: {
|
||||
...smsForm,
|
||||
adminPhones: filteredPhones,
|
||||
},
|
||||
};
|
||||
|
||||
await updateNotificationSettingsApi(updateData);
|
||||
ElMessage.success('短信通知设置保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveSystem = async () => {
|
||||
if (!systemFormRef.value) return;
|
||||
|
||||
try {
|
||||
await systemFormRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
const updateData: UpdateNotificationSettingsParams = {
|
||||
type: 'system',
|
||||
config: systemForm,
|
||||
};
|
||||
|
||||
await updateNotificationSettingsApi(updateData);
|
||||
ElMessage.success('站内通知设置保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveWechat = async () => {
|
||||
if (!wechatFormRef.value) return;
|
||||
|
||||
try {
|
||||
await wechatFormRef.value.validate();
|
||||
saveLoading.value = true;
|
||||
|
||||
const updateData: UpdateNotificationSettingsParams = {
|
||||
type: 'wechat',
|
||||
config: wechatForm,
|
||||
};
|
||||
|
||||
await updateNotificationSettingsApi(updateData);
|
||||
ElMessage.success('微信通知设置保存成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetEmail = async () => {
|
||||
try {
|
||||
await resetNotificationSettingsApi('email');
|
||||
await loadSettings();
|
||||
ElMessage.success('邮件通知设置已重置');
|
||||
} catch (error) {
|
||||
ElMessage.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetSms = async () => {
|
||||
try {
|
||||
await resetNotificationSettingsApi('sms');
|
||||
await loadSettings();
|
||||
ElMessage.success('短信通知设置已重置');
|
||||
} catch (error) {
|
||||
ElMessage.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetSystem = async () => {
|
||||
try {
|
||||
await resetNotificationSettingsApi('system');
|
||||
await loadSettings();
|
||||
ElMessage.success('站内通知设置已重置');
|
||||
} catch (error) {
|
||||
ElMessage.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetWechat = async () => {
|
||||
try {
|
||||
await resetNotificationSettingsApi('wechat');
|
||||
await loadSettings();
|
||||
ElMessage.success('微信通知设置已重置');
|
||||
} catch (error) {
|
||||
ElMessage.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestEmail = async () => {
|
||||
testEmailLoading.value = true;
|
||||
try {
|
||||
await testNotificationApi('email');
|
||||
ElMessage.success('测试邮件发送成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('发送测试邮件失败');
|
||||
} finally {
|
||||
testEmailLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestSms = async () => {
|
||||
testSmsLoading.value = true;
|
||||
try {
|
||||
await testNotificationApi('sms');
|
||||
ElMessage.success('测试短信发送成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('发送测试短信失败');
|
||||
} finally {
|
||||
testSmsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestSystem = async () => {
|
||||
testSystemLoading.value = true;
|
||||
try {
|
||||
await testNotificationApi('system');
|
||||
ElMessage.success('测试通知发送成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('发送测试通知失败');
|
||||
} finally {
|
||||
testSystemLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestWechat = async () => {
|
||||
testWechatLoading.value = true;
|
||||
try {
|
||||
await testNotificationApi('wechat');
|
||||
ElMessage.success('测试微信消息发送成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('发送测试微信消息失败');
|
||||
} finally {
|
||||
testWechatLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification-settings-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-item-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.email-list-container,
|
||||
.phone-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.email-list,
|
||||
.phone-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.email-item,
|
||||
.phone-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.template-label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox-group .el-checkbox) {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
522
admin/apps/web-ele/src/views/common/settings/payment/index.vue
Normal file
522
admin/apps/web-ele/src/views/common/settings/payment/index.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<VbenTabs v-model:active-key="activeTab" type="card">
|
||||
<!-- 支付宝设置 -->
|
||||
<VbenTabPane key="alipay" tab="支付宝">
|
||||
<VbenForm
|
||||
ref="alipayFormRef"
|
||||
:schema="alipayFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveAlipay"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveAlipay">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleTestAlipay">
|
||||
测试支付
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 微信支付设置 -->
|
||||
<VbenTabPane key="wechat" tab="微信支付">
|
||||
<VbenForm
|
||||
ref="wechatFormRef"
|
||||
:schema="wechatFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveWechat"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveWechat">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleTestWechat">
|
||||
测试支付
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 通用设置 -->
|
||||
<VbenTabPane key="general" tab="通用设置">
|
||||
<VbenForm
|
||||
ref="generalFormRef"
|
||||
:schema="generalFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSaveGeneral"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSaveGeneral">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
</VbenTabs>
|
||||
|
||||
<!-- 测试支付对话框 -->
|
||||
<VbenModal
|
||||
v-model:open="testDialogVisible"
|
||||
title="测试支付"
|
||||
width="500px"
|
||||
@ok="handleCreateTestOrder"
|
||||
>
|
||||
<VbenForm
|
||||
ref="testFormRef"
|
||||
:schema="testFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
/>
|
||||
</VbenModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { VbenForm, VbenButton, VbenModal, VbenTabs, VbenTabPane } from '@vben/components'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormSchema } from '@vben/types'
|
||||
import {
|
||||
getPaymentConfigApi,
|
||||
updatePaymentConfigApi,
|
||||
testPaymentApi,
|
||||
resetPaymentConfigApi,
|
||||
type PaymentConfig,
|
||||
} from '@/api/common/payment'
|
||||
|
||||
const alipayFormRef = ref()
|
||||
const wechatFormRef = ref()
|
||||
const generalFormRef = ref()
|
||||
const testFormRef = ref()
|
||||
const saveLoading = ref(false)
|
||||
const testDialogVisible = ref(false)
|
||||
const activeTab = ref('alipay')
|
||||
|
||||
// 支付宝表单配置
|
||||
const alipayFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'alipay_app_id',
|
||||
label: '应用ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'alipay_gateway_url',
|
||||
label: '网关地址',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '正式环境', value: 'https://openapi.alipay.com/gateway.do' },
|
||||
{ label: '沙箱环境', value: 'https://openapi.alipaydev.com/gateway.do' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'https://openapi.alipaydev.com/gateway.do',
|
||||
},
|
||||
{
|
||||
field: 'alipay_private_key',
|
||||
label: '应用私钥',
|
||||
component: 'InputTextArea',
|
||||
required: true,
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '请输入应用私钥(PKCS8格式)',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'alipay_public_key',
|
||||
label: '支付宝公钥',
|
||||
component: 'InputTextArea',
|
||||
required: true,
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '请输入支付宝公钥',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'alipay_sign_type',
|
||||
label: '签名方式',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: 'RSA2', value: 'RSA2' },
|
||||
{ label: 'RSA', value: 'RSA' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'RSA2',
|
||||
},
|
||||
{
|
||||
field: 'alipay_charset',
|
||||
label: '字符集',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: 'UTF-8', value: 'utf-8' },
|
||||
{ label: 'GBK', value: 'gbk' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'utf-8',
|
||||
},
|
||||
{
|
||||
field: 'alipay_notify_url',
|
||||
label: '异步通知地址',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
helpMessage: '支付结果异步通知地址',
|
||||
},
|
||||
{
|
||||
field: 'alipay_return_url',
|
||||
label: '同步返回地址',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
helpMessage: '支付完成后同步跳转地址',
|
||||
},
|
||||
{
|
||||
field: 'alipay_enabled',
|
||||
label: '启用状态',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 微信支付表单配置
|
||||
const wechatFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'wechat_app_id',
|
||||
label: '应用ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_mch_id',
|
||||
label: '商户号',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_key',
|
||||
label: '商户密钥',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_secret',
|
||||
label: '应用密钥',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'wechat_cert_path',
|
||||
label: '证书路径',
|
||||
component: 'Input',
|
||||
helpMessage: '微信支付证书文件路径',
|
||||
},
|
||||
{
|
||||
field: 'wechat_key_path',
|
||||
label: '私钥路径',
|
||||
component: 'Input',
|
||||
helpMessage: '微信支付私钥文件路径',
|
||||
},
|
||||
{
|
||||
field: 'wechat_notify_url',
|
||||
label: '异步通知地址',
|
||||
component: 'Input',
|
||||
colProps: { span: 12 },
|
||||
helpMessage: '支付结果异步通知地址',
|
||||
},
|
||||
{
|
||||
field: 'wechat_trade_type',
|
||||
label: '支付模式',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
colProps: { span: 12 },
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: 'JSAPI支付', value: 'JSAPI' },
|
||||
{ label: 'Native支付', value: 'NATIVE' },
|
||||
{ label: 'APP支付', value: 'APP' },
|
||||
{ label: 'H5支付', value: 'MWEB' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'JSAPI',
|
||||
},
|
||||
{
|
||||
field: 'wechat_enabled',
|
||||
label: '启用状态',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 通用设置表单配置
|
||||
const generalFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'default_method',
|
||||
label: '默认支付方式',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '余额支付', value: 'balance' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'alipay',
|
||||
},
|
||||
{
|
||||
field: 'timeout',
|
||||
label: '支付超时时间(分钟)',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 1440,
|
||||
},
|
||||
defaultValue: 30,
|
||||
},
|
||||
{
|
||||
field: 'min_amount',
|
||||
label: '最小支付金额(元)',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 0.01,
|
||||
precision: 2,
|
||||
},
|
||||
defaultValue: 0.01,
|
||||
},
|
||||
{
|
||||
field: 'max_amount',
|
||||
label: '最大支付金额(元)',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
precision: 2,
|
||||
},
|
||||
defaultValue: 50000,
|
||||
},
|
||||
{
|
||||
field: 'success_url',
|
||||
label: '支付成功页面',
|
||||
component: 'Input',
|
||||
helpMessage: '支付成功后跳转的页面地址',
|
||||
},
|
||||
{
|
||||
field: 'fail_url',
|
||||
label: '支付失败页面',
|
||||
component: 'Input',
|
||||
helpMessage: '支付失败后跳转的页面地址',
|
||||
},
|
||||
{
|
||||
field: 'balance_enabled',
|
||||
label: '启用余额支付',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
field: 'points_enabled',
|
||||
label: '启用积分抵扣',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
field: 'points_ratio',
|
||||
label: '积分抵扣比例',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.points_enabled,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 1000,
|
||||
addonAfter: '积分 = 1元',
|
||||
},
|
||||
defaultValue: 100,
|
||||
helpMessage: '多少积分等于1元',
|
||||
},
|
||||
]
|
||||
|
||||
// 测试表单配置
|
||||
const testFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'amount',
|
||||
label: '支付金额',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
componentProps: {
|
||||
min: 0.01,
|
||||
precision: 2,
|
||||
},
|
||||
defaultValue: 0.01,
|
||||
},
|
||||
{
|
||||
field: 'subject',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
defaultValue: '测试商品',
|
||||
},
|
||||
{
|
||||
field: 'body',
|
||||
label: '商品描述',
|
||||
component: 'Input',
|
||||
defaultValue: '这是一个测试订单',
|
||||
},
|
||||
]
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const data = await getPaymentConfigApi()
|
||||
alipayFormRef.value?.setFieldsValue(data.alipay || {})
|
||||
wechatFormRef.value?.setFieldsValue(data.wechat || {})
|
||||
generalFormRef.value?.setFieldsValue(data.general || {})
|
||||
} catch (error) {
|
||||
message.error('加载配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存支付宝配置
|
||||
const handleSaveAlipay = async () => {
|
||||
try {
|
||||
const values = await alipayFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updatePaymentConfigApi({ type: 'alipay', config: values })
|
||||
message.success('支付宝配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存微信支付配置
|
||||
const handleSaveWechat = async () => {
|
||||
try {
|
||||
const values = await wechatFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updatePaymentConfigApi({ type: 'wechat', config: values })
|
||||
message.success('微信支付配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存通用配置
|
||||
const handleSaveGeneral = async () => {
|
||||
try {
|
||||
const values = await generalFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updatePaymentConfigApi({ type: 'general', config: values })
|
||||
message.success('通用配置保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试支付宝
|
||||
const handleTestAlipay = () => {
|
||||
testFormRef.value?.setFieldsValue({
|
||||
amount: 0.01,
|
||||
subject: '支付宝测试商品',
|
||||
body: '这是一个支付宝测试订单',
|
||||
})
|
||||
testDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 测试微信支付
|
||||
const handleTestWechat = () => {
|
||||
testFormRef.value?.setFieldsValue({
|
||||
amount: 0.01,
|
||||
subject: '微信支付测试商品',
|
||||
body: '这是一个微信支付测试订单',
|
||||
})
|
||||
testDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 创建测试订单
|
||||
const handleCreateTestOrder = async () => {
|
||||
try {
|
||||
const values = await testFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
const testData = {
|
||||
method: activeTab.value,
|
||||
...values,
|
||||
}
|
||||
|
||||
const result = await testPaymentApi(testData)
|
||||
message.success('测试订单创建成功')
|
||||
|
||||
// 打开支付页面或显示支付二维码
|
||||
if (result.payUrl) {
|
||||
window.open(result.payUrl, '_blank')
|
||||
}
|
||||
|
||||
testDialogVisible.value = false
|
||||
} catch (error) {
|
||||
message.error('创建测试订单失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
688
admin/apps/web-ele/src/views/common/settings/security/index.vue
Normal file
688
admin/apps/web-ele/src/views/common/settings/security/index.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div class="security-settings-page">
|
||||
<div class="settings-container">
|
||||
<Card title="安全设置" :loading="loading">
|
||||
<Tabs v-model:activeKey="activeTab" type="card">
|
||||
<!-- 密码策略 -->
|
||||
<TabPane key="password" tab="密码策略">
|
||||
<BasicForm
|
||||
ref="passwordFormRef"
|
||||
:schemas="passwordSchemas"
|
||||
:model="passwordForm"
|
||||
:label-width="120"
|
||||
:action-col-options="{ span: 24 }"
|
||||
:submit-button-options="{ text: '保存设置', loading: saveLoading }"
|
||||
:reset-button-options="{ text: '重置设置' }"
|
||||
@submit="handleSavePassword"
|
||||
@reset="handleResetPassword"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<!-- 登录安全 -->
|
||||
<TabPane key="login" tab="登录安全">
|
||||
<BasicForm
|
||||
ref="loginFormRef"
|
||||
:schemas="loginSchemas"
|
||||
:model="loginForm"
|
||||
:label-width="120"
|
||||
:action-col-options="{ span: 24 }"
|
||||
:submit-button-options="{ text: '保存设置', loading: saveLoading }"
|
||||
:reset-button-options="{ text: '重置设置' }"
|
||||
@submit="handleSaveLogin"
|
||||
@reset="handleResetLogin"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<!-- IP访问控制 -->
|
||||
<TabPane key="ip" tab="IP访问控制">
|
||||
<BasicForm
|
||||
ref="ipFormRef"
|
||||
:schemas="ipSchemas"
|
||||
:model="ipForm"
|
||||
:label-width="120"
|
||||
:action-col-options="{ span: 24 }"
|
||||
:submit-button-options="{ text: '保存设置', loading: saveLoading }"
|
||||
:reset-button-options="{ text: '重置设置' }"
|
||||
@submit="handleSaveIp"
|
||||
@reset="handleResetIp"
|
||||
>
|
||||
<template #testIpAction>
|
||||
<Button type="primary" :loading="testIpLoading" @click="handleTestIp">
|
||||
测试当前IP
|
||||
</Button>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</TabPane>
|
||||
|
||||
<!-- 操作审计 -->
|
||||
<TabPane key="audit" tab="操作审计">
|
||||
<BasicForm
|
||||
ref="auditFormRef"
|
||||
:schemas="auditSchemas"
|
||||
:model="auditForm"
|
||||
:label-width="120"
|
||||
:action-col-options="{ span: 24 }"
|
||||
:submit-button-options="{ text: '保存设置', loading: saveLoading }"
|
||||
:reset-button-options="{ text: '重置设置' }"
|
||||
@submit="handleSaveAudit"
|
||||
@reset="handleResetAudit"
|
||||
>
|
||||
<template #auditLogAction>
|
||||
<Button type="default" @click="handleViewAuditLog">
|
||||
查看审计日志
|
||||
</Button>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Card, Tabs, TabPane, Button, message } from 'ant-design-vue';
|
||||
import { BasicForm, FormSchema } from '@/components/Form';
|
||||
import {
|
||||
getSecurityConfigApi,
|
||||
updateSecurityConfigApi,
|
||||
resetSecurityConfigApi,
|
||||
testIpAccessApi,
|
||||
type SecurityConfig,
|
||||
} from '@/api/common/security';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const testIpLoading = ref(false);
|
||||
const activeTab = ref('password');
|
||||
|
||||
// 表单引用
|
||||
const passwordFormRef = ref();
|
||||
const loginFormRef = ref();
|
||||
const ipFormRef = ref();
|
||||
const auditFormRef = ref();
|
||||
|
||||
// 表单数据
|
||||
const passwordForm = reactive({
|
||||
enablePasswordStrength: true,
|
||||
minPasswordLength: 8,
|
||||
requireLowercase: true,
|
||||
requireUppercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true,
|
||||
forbidCommonPasswords: true,
|
||||
passwordExpireDays: 90,
|
||||
passwordHistoryLimit: 5,
|
||||
forcePasswordChange: false,
|
||||
});
|
||||
|
||||
const loginForm = reactive({
|
||||
maxLoginAttempts: 5,
|
||||
lockoutDuration: 30,
|
||||
enableLoginCaptcha: true,
|
||||
captchaTriggerAttempts: 3,
|
||||
enableTwoFactor: false,
|
||||
forceTwoFactor: false,
|
||||
sessionTimeout: 120,
|
||||
enableSingleSignOn: false,
|
||||
recordLoginLog: true,
|
||||
});
|
||||
|
||||
const ipForm = reactive({
|
||||
enableIpControl: false,
|
||||
accessMode: 'whitelist',
|
||||
ipWhitelist: [''],
|
||||
ipBlacklist: [''],
|
||||
adminIpWhitelist: [''],
|
||||
});
|
||||
|
||||
const auditForm = reactive({
|
||||
enableAudit: true,
|
||||
auditLoginLogout: true,
|
||||
auditUserManagement: true,
|
||||
auditRoleManagement: true,
|
||||
auditPermissionManagement: true,
|
||||
auditSystemConfig: true,
|
||||
auditDataExport: true,
|
||||
auditFileUpload: true,
|
||||
auditSensitiveOperations: true,
|
||||
auditLogRetention: 365,
|
||||
enableSecondaryConfirm: true,
|
||||
confirmDeleteUser: true,
|
||||
confirmResetPassword: true,
|
||||
confirmModifyRole: true,
|
||||
confirmSystemBackup: true,
|
||||
confirmSystemRestore: true,
|
||||
confirmClearData: true,
|
||||
enableAnomalyDetection: true,
|
||||
});
|
||||
|
||||
// 表单配置
|
||||
const passwordSchemas: FormSchema[] = [
|
||||
{
|
||||
field: 'enablePasswordStrength',
|
||||
label: '启用密码强度检查',
|
||||
component: 'Switch',
|
||||
helpMessage: '开启后将检查密码复杂度',
|
||||
},
|
||||
{
|
||||
field: 'minPasswordLength',
|
||||
label: '最小密码长度',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 6,
|
||||
max: 32,
|
||||
placeholder: '请输入最小密码长度',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入最小密码长度' },
|
||||
{ type: 'number', min: 6, max: 32, message: '密码长度范围为 6-32 位' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'requireLowercase',
|
||||
label: '要求小写字母',
|
||||
component: 'Switch',
|
||||
helpMessage: '密码必须包含小写字母',
|
||||
},
|
||||
{
|
||||
field: 'requireUppercase',
|
||||
label: '要求大写字母',
|
||||
component: 'Switch',
|
||||
helpMessage: '密码必须包含大写字母',
|
||||
},
|
||||
{
|
||||
field: 'requireNumbers',
|
||||
label: '要求数字',
|
||||
component: 'Switch',
|
||||
helpMessage: '密码必须包含数字',
|
||||
},
|
||||
{
|
||||
field: 'requireSpecialChars',
|
||||
label: '要求特殊字符',
|
||||
component: 'Switch',
|
||||
helpMessage: '密码必须包含特殊字符',
|
||||
},
|
||||
{
|
||||
field: 'forbidCommonPasswords',
|
||||
label: '禁止常见密码',
|
||||
component: 'Switch',
|
||||
helpMessage: '禁止使用常见的弱密码',
|
||||
},
|
||||
{
|
||||
field: 'passwordExpireDays',
|
||||
label: '密码有效期(天)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 365,
|
||||
placeholder: '请输入密码有效期',
|
||||
},
|
||||
helpMessage: '0表示永不过期',
|
||||
rules: [
|
||||
{ required: true, message: '请输入密码有效期' },
|
||||
{ type: 'number', min: 0, max: 365, message: '密码有效期范围为 0-365 天' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'passwordHistoryLimit',
|
||||
label: '密码重复使用限制',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 20,
|
||||
placeholder: '请输入历史密码限制数量',
|
||||
},
|
||||
helpMessage: '禁止重复使用最近N个密码,0表示不限制',
|
||||
rules: [
|
||||
{ type: 'number', min: 0, max: 20, message: '历史密码限制范围为 0-20 个' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'forcePasswordChange',
|
||||
label: '强制定期修改密码',
|
||||
component: 'Switch',
|
||||
helpMessage: '强制用户在密码过期前修改密码',
|
||||
},
|
||||
];
|
||||
|
||||
const loginSchemas: FormSchema[] = [
|
||||
{
|
||||
field: 'maxLoginAttempts',
|
||||
label: '最大登录失败次数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 3,
|
||||
max: 20,
|
||||
placeholder: '请输入最大登录失败次数',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入最大登录失败次数' },
|
||||
{ type: 'number', min: 3, max: 20, message: '登录失败次数范围为 3-20 次' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'lockoutDuration',
|
||||
label: '账户锁定时间(分钟)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 5,
|
||||
max: 1440,
|
||||
placeholder: '请输入账户锁定时间',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入账户锁定时间' },
|
||||
{ type: 'number', min: 5, max: 1440, message: '锁定时间范围为 5-1440 分钟' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'enableLoginCaptcha',
|
||||
label: '启用登录验证码',
|
||||
component: 'Switch',
|
||||
helpMessage: '登录时显示验证码',
|
||||
},
|
||||
{
|
||||
field: 'captchaTriggerAttempts',
|
||||
label: '验证码触发条件',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
placeholder: '请输入触发验证码的失败次数',
|
||||
},
|
||||
helpMessage: '登录失败多少次后显示验证码',
|
||||
rules: [
|
||||
{ type: 'number', min: 1, max: 10, message: '触发条件范围为 1-10 次' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'enableTwoFactor',
|
||||
label: '启用双因子认证',
|
||||
component: 'Switch',
|
||||
helpMessage: '启用短信或邮箱二次验证',
|
||||
},
|
||||
{
|
||||
field: 'forceTwoFactor',
|
||||
label: '强制双因子认证',
|
||||
component: 'Switch',
|
||||
helpMessage: '强制所有用户启用双因子认证',
|
||||
},
|
||||
{
|
||||
field: 'sessionTimeout',
|
||||
label: '会话超时时间(分钟)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 30,
|
||||
max: 1440,
|
||||
placeholder: '请输入会话超时时间',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入会话超时时间' },
|
||||
{ type: 'number', min: 30, max: 1440, message: '会话超时时间范围为 30-1440 分钟' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'enableSingleSignOn',
|
||||
label: '启用单点登录',
|
||||
component: 'Switch',
|
||||
helpMessage: '同一账户只能在一个地方登录',
|
||||
},
|
||||
{
|
||||
field: 'recordLoginLog',
|
||||
label: '记录登录日志',
|
||||
component: 'Switch',
|
||||
helpMessage: '记录用户登录和登出日志',
|
||||
},
|
||||
];
|
||||
|
||||
const ipSchemas: FormSchema[] = [
|
||||
{
|
||||
field: 'enableIpControl',
|
||||
label: '启用IP访问控制',
|
||||
component: 'Switch',
|
||||
helpMessage: '启用后将根据IP地址控制访问权限',
|
||||
},
|
||||
{
|
||||
field: 'accessMode',
|
||||
label: '访问控制模式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '白名单模式', value: 'whitelist' },
|
||||
{ label: '黑名单模式', value: 'blacklist' },
|
||||
],
|
||||
},
|
||||
helpMessage: '白名单:仅允许列表中的IP访问;黑名单:禁止列表中的IP访问',
|
||||
},
|
||||
{
|
||||
field: 'ipWhitelist',
|
||||
label: 'IP白名单',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: '请输入IP地址,每行一个\n支持格式:192.168.1.1 或 192.168.1.0/24',
|
||||
},
|
||||
helpMessage: '每行一个IP地址或IP段,支持CIDR格式',
|
||||
show: ({ model }) => model.accessMode === 'whitelist',
|
||||
},
|
||||
{
|
||||
field: 'ipBlacklist',
|
||||
label: 'IP黑名单',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: '请输入IP地址,每行一个\n支持格式:192.168.1.1 或 192.168.1.0/24',
|
||||
},
|
||||
helpMessage: '每行一个IP地址或IP段,支持CIDR格式',
|
||||
show: ({ model }) => model.accessMode === 'blacklist',
|
||||
},
|
||||
{
|
||||
field: 'adminIpWhitelist',
|
||||
label: '管理员IP白名单',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: '请输入管理员IP地址,每行一个\n支持格式:192.168.1.1 或 192.168.1.0/24',
|
||||
},
|
||||
helpMessage: '管理员专用IP白名单,优先级高于普通IP控制',
|
||||
},
|
||||
{
|
||||
field: 'testIpAction',
|
||||
label: '',
|
||||
component: 'Input',
|
||||
slot: 'testIpAction',
|
||||
},
|
||||
];
|
||||
|
||||
const auditSchemas: FormSchema[] = [
|
||||
{
|
||||
field: 'enableAudit',
|
||||
label: '启用操作审计',
|
||||
component: 'Switch',
|
||||
helpMessage: '记录用户的重要操作行为',
|
||||
},
|
||||
{
|
||||
field: 'auditEvents',
|
||||
label: '审计事件类型',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '登录登出', value: 'loginLogout' },
|
||||
{ label: '用户管理', value: 'userManagement' },
|
||||
{ label: '角色管理', value: 'roleManagement' },
|
||||
{ label: '权限管理', value: 'permissionManagement' },
|
||||
{ label: '系统配置', value: 'systemConfig' },
|
||||
{ label: '数据导出', value: 'dataExport' },
|
||||
{ label: '文件上传', value: 'fileUpload' },
|
||||
{ label: '敏感操作', value: 'sensitiveOperations' },
|
||||
],
|
||||
},
|
||||
helpMessage: '选择需要审计的事件类型',
|
||||
},
|
||||
{
|
||||
field: 'auditLogRetention',
|
||||
label: '审计日志保留期(天)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 30,
|
||||
max: 3650,
|
||||
placeholder: '请输入日志保留期',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入审计日志保留期' },
|
||||
{ type: 'number', min: 30, max: 3650, message: '日志保留期范围为 30-3650 天' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'enableSecondaryConfirm',
|
||||
label: '敏感操作二次确认',
|
||||
component: 'Switch',
|
||||
helpMessage: '敏感操作需要二次确认',
|
||||
},
|
||||
{
|
||||
field: 'sensitiveOperations',
|
||||
label: '敏感操作类型',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '删除用户', value: 'deleteUser' },
|
||||
{ label: '重置密码', value: 'resetPassword' },
|
||||
{ label: '修改角色', value: 'modifyRole' },
|
||||
{ label: '系统备份', value: 'systemBackup' },
|
||||
{ label: '系统恢复', value: 'systemRestore' },
|
||||
{ label: '清除数据', value: 'clearData' },
|
||||
],
|
||||
},
|
||||
helpMessage: '选择需要二次确认的敏感操作',
|
||||
show: ({ model }) => model.enableSecondaryConfirm,
|
||||
},
|
||||
{
|
||||
field: 'enableAnomalyDetection',
|
||||
label: '异常行为检测',
|
||||
component: 'Switch',
|
||||
helpMessage: '检测并记录异常的用户行为',
|
||||
},
|
||||
{
|
||||
field: 'auditLogAction',
|
||||
label: '',
|
||||
component: 'Input',
|
||||
slot: 'auditLogAction',
|
||||
},
|
||||
];
|
||||
|
||||
// 方法
|
||||
const loadSettings = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const config = await getSecurityConfigApi();
|
||||
|
||||
// 更新表单数据
|
||||
Object.assign(passwordForm, config.password || {});
|
||||
Object.assign(loginForm, config.login || {});
|
||||
Object.assign(ipForm, {
|
||||
...config.ip,
|
||||
ipWhitelist: config.ip?.ipWhitelist?.join('\n') || '',
|
||||
ipBlacklist: config.ip?.ipBlacklist?.join('\n') || '',
|
||||
adminIpWhitelist: config.ip?.adminIpWhitelist?.join('\n') || '',
|
||||
});
|
||||
|
||||
// 处理审计事件类型
|
||||
const auditEvents = [];
|
||||
if (config.audit?.auditLoginLogout) auditEvents.push('loginLogout');
|
||||
if (config.audit?.auditUserManagement) auditEvents.push('userManagement');
|
||||
if (config.audit?.auditRoleManagement) auditEvents.push('roleManagement');
|
||||
if (config.audit?.auditPermissionManagement) auditEvents.push('permissionManagement');
|
||||
if (config.audit?.auditSystemConfig) auditEvents.push('systemConfig');
|
||||
if (config.audit?.auditDataExport) auditEvents.push('dataExport');
|
||||
if (config.audit?.auditFileUpload) auditEvents.push('fileUpload');
|
||||
if (config.audit?.auditSensitiveOperations) auditEvents.push('sensitiveOperations');
|
||||
|
||||
const sensitiveOperations = [];
|
||||
if (config.audit?.confirmDeleteUser) sensitiveOperations.push('deleteUser');
|
||||
if (config.audit?.confirmResetPassword) sensitiveOperations.push('resetPassword');
|
||||
if (config.audit?.confirmModifyRole) sensitiveOperations.push('modifyRole');
|
||||
if (config.audit?.confirmSystemBackup) sensitiveOperations.push('systemBackup');
|
||||
if (config.audit?.confirmSystemRestore) sensitiveOperations.push('systemRestore');
|
||||
if (config.audit?.confirmClearData) sensitiveOperations.push('clearData');
|
||||
|
||||
Object.assign(auditForm, {
|
||||
...config.audit,
|
||||
auditEvents,
|
||||
sensitiveOperations,
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('加载安全设置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSavePassword = async (values: any) => {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await updateSecurityConfigApi({
|
||||
type: 'password',
|
||||
config: values,
|
||||
});
|
||||
message.success('密码策略保存成功');
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveLogin = async (values: any) => {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await updateSecurityConfigApi({
|
||||
type: 'login',
|
||||
config: values,
|
||||
});
|
||||
message.success('登录安全保存成功');
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveIp = async (values: any) => {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
// 处理IP列表
|
||||
const config = {
|
||||
...values,
|
||||
ipWhitelist: values.ipWhitelist ? values.ipWhitelist.split('\n').filter((ip: string) => ip.trim()) : [],
|
||||
ipBlacklist: values.ipBlacklist ? values.ipBlacklist.split('\n').filter((ip: string) => ip.trim()) : [],
|
||||
adminIpWhitelist: values.adminIpWhitelist ? values.adminIpWhitelist.split('\n').filter((ip: string) => ip.trim()) : [],
|
||||
};
|
||||
|
||||
await updateSecurityConfigApi({
|
||||
type: 'ip',
|
||||
config,
|
||||
});
|
||||
message.success('IP访问控制保存成功');
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAudit = async (values: any) => {
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
// 处理审计事件类型
|
||||
const config = {
|
||||
...values,
|
||||
auditLoginLogout: values.auditEvents?.includes('loginLogout') || false,
|
||||
auditUserManagement: values.auditEvents?.includes('userManagement') || false,
|
||||
auditRoleManagement: values.auditEvents?.includes('roleManagement') || false,
|
||||
auditPermissionManagement: values.auditEvents?.includes('permissionManagement') || false,
|
||||
auditSystemConfig: values.auditEvents?.includes('systemConfig') || false,
|
||||
auditDataExport: values.auditEvents?.includes('dataExport') || false,
|
||||
auditFileUpload: values.auditEvents?.includes('fileUpload') || false,
|
||||
auditSensitiveOperations: values.auditEvents?.includes('sensitiveOperations') || false,
|
||||
confirmDeleteUser: values.sensitiveOperations?.includes('deleteUser') || false,
|
||||
confirmResetPassword: values.sensitiveOperations?.includes('resetPassword') || false,
|
||||
confirmModifyRole: values.sensitiveOperations?.includes('modifyRole') || false,
|
||||
confirmSystemBackup: values.sensitiveOperations?.includes('systemBackup') || false,
|
||||
confirmSystemRestore: values.sensitiveOperations?.includes('systemRestore') || false,
|
||||
confirmClearData: values.sensitiveOperations?.includes('clearData') || false,
|
||||
};
|
||||
|
||||
await updateSecurityConfigApi({
|
||||
type: 'audit',
|
||||
config,
|
||||
});
|
||||
message.success('操作审计保存成功');
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
try {
|
||||
await resetSecurityConfigApi('password');
|
||||
await loadSettings();
|
||||
message.success('密码策略已重置');
|
||||
} catch (error) {
|
||||
message.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetLogin = async () => {
|
||||
try {
|
||||
await resetSecurityConfigApi('login');
|
||||
await loadSettings();
|
||||
message.success('登录安全已重置');
|
||||
} catch (error) {
|
||||
message.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetIp = async () => {
|
||||
try {
|
||||
await resetSecurityConfigApi('ip');
|
||||
await loadSettings();
|
||||
message.success('IP访问控制已重置');
|
||||
} catch (error) {
|
||||
message.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetAudit = async () => {
|
||||
try {
|
||||
await resetSecurityConfigApi('audit');
|
||||
await loadSettings();
|
||||
message.success('操作审计已重置');
|
||||
} catch (error) {
|
||||
message.error('重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestIp = async () => {
|
||||
testIpLoading.value = true;
|
||||
try {
|
||||
const result = await testIpAccessApi();
|
||||
if (result.allowed) {
|
||||
message.success(`当前IP ${result.ip} 允许访问`);
|
||||
} else {
|
||||
message.warning(`当前IP ${result.ip} 被拒绝访问`);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('测试IP失败');
|
||||
} finally {
|
||||
testIpLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewAuditLog = () => {
|
||||
// 跳转到审计日志页面
|
||||
console.log('查看审计日志');
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.security-settings-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
352
admin/apps/web-ele/src/views/common/settings/sms/index.vue
Normal file
352
admin/apps/web-ele/src/views/common/settings/sms/index.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<VbenForm
|
||||
ref="formRef"
|
||||
:schema="formSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSave"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSave">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleTest">
|
||||
测试发送
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleReset">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
|
||||
<!-- 测试短信发送对话框 -->
|
||||
<VbenModal
|
||||
v-model:open="testDialogVisible"
|
||||
title="测试短信发送"
|
||||
width="500px"
|
||||
@ok="handleSendTest"
|
||||
>
|
||||
<VbenForm
|
||||
ref="testFormRef"
|
||||
:schema="testFormSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
/>
|
||||
</VbenModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { VbenForm, VbenButton, VbenModal } from '@vben/components'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormSchema } from '@vben/types'
|
||||
import {
|
||||
getSmsConfigApi,
|
||||
updateSmsConfigApi,
|
||||
testSmsApi,
|
||||
resetSmsConfigApi,
|
||||
type SmsConfig,
|
||||
} from '@/api/common/sms'
|
||||
|
||||
const formRef = ref()
|
||||
const testFormRef = ref()
|
||||
const saveLoading = ref(false)
|
||||
const testDialogVisible = ref(false)
|
||||
|
||||
// 表单配置
|
||||
const formSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'provider',
|
||||
label: '短信服务商',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '阿里云短信', value: 'aliyun' },
|
||||
{ label: '腾讯云短信', value: 'tencent' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'access_key_id',
|
||||
label: 'AccessKey ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'aliyun',
|
||||
},
|
||||
{
|
||||
field: 'access_key_secret',
|
||||
label: 'AccessKey Secret',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'aliyun',
|
||||
},
|
||||
{
|
||||
field: 'sign_name',
|
||||
label: '签名名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'aliyun',
|
||||
},
|
||||
{
|
||||
field: 'region',
|
||||
label: '地域',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'aliyun',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '华东1(杭州)', value: 'cn-hangzhou' },
|
||||
{ label: '华北2(北京)', value: 'cn-beijing' },
|
||||
{ label: '华东2(上海)', value: 'cn-shanghai' },
|
||||
{ label: '华南1(深圳)', value: 'cn-shenzhen' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'secret_id',
|
||||
label: 'SecretId',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'tencent',
|
||||
},
|
||||
{
|
||||
field: 'secret_key',
|
||||
label: 'SecretKey',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'tencent',
|
||||
},
|
||||
{
|
||||
field: 'app_id',
|
||||
label: '应用ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'tencent',
|
||||
},
|
||||
{
|
||||
field: 'sign_content',
|
||||
label: '签名内容',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.provider === 'tencent',
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
label: '启用状态',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
field: 'debug_mode',
|
||||
label: '调试模式',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '开启后将记录详细的发送日志',
|
||||
},
|
||||
{
|
||||
field: 'templates',
|
||||
label: '短信模板',
|
||||
component: 'FormList',
|
||||
componentProps: {
|
||||
copyIconProps: false,
|
||||
deleteIconProps: false,
|
||||
addButtonProps: {
|
||||
text: '添加模板',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
field: 'type',
|
||||
label: '模板类型',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '验证码', value: 'verify_code' },
|
||||
{ label: '通知', value: 'notification' },
|
||||
{ label: '营销', value: 'marketing' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'template_id',
|
||||
label: '模板ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
label: '模板内容',
|
||||
component: 'Textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'variables',
|
||||
label: '可用变量',
|
||||
component: 'Input',
|
||||
helpMessage: '多个变量用逗号分隔,如:code,name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'rate_limit',
|
||||
label: '发送限制',
|
||||
component: 'FormGroup',
|
||||
children: [
|
||||
{
|
||||
field: 'per_minute',
|
||||
label: '每分钟限制',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 10,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'per_hour',
|
||||
label: '每小时限制',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 100,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'per_day',
|
||||
label: '每日限制',
|
||||
component: 'InputNumber',
|
||||
defaultValue: 1000,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 10000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 测试表单配置
|
||||
const testFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机号码',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
rules: [
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的手机号码',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'template_type',
|
||||
label: '模板类型',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '验证码', value: 'verify_code' },
|
||||
{ label: '通知', value: 'notification' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
label: '短信内容',
|
||||
component: 'Textarea',
|
||||
required: true,
|
||||
componentProps: {
|
||||
rows: 3,
|
||||
placeholder: '请输入测试短信内容',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const data = await getSmsConfigApi()
|
||||
formRef.value?.setFieldsValue(data)
|
||||
} catch (error) {
|
||||
message.error('加载配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await formRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateSmsConfigApi(values as SmsConfig)
|
||||
message.success('保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试发送
|
||||
const handleTest = () => {
|
||||
testDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 发送测试短信
|
||||
const handleSendTest = async () => {
|
||||
try {
|
||||
const values = await testFormRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
await testSmsApi(values)
|
||||
message.success('测试短信发送成功')
|
||||
testDialogVisible.value = false
|
||||
} catch (error) {
|
||||
message.error('测试短信发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
await resetSmsConfigApi()
|
||||
message.success('重置成功')
|
||||
await loadConfig()
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
470
admin/apps/web-ele/src/views/common/settings/storage/index.vue
Normal file
470
admin/apps/web-ele/src/views/common/settings/storage/index.vue
Normal file
@@ -0,0 +1,470 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<VbenForm
|
||||
ref="formRef"
|
||||
:schema="formSchema"
|
||||
:form-options="{
|
||||
layout: 'vertical',
|
||||
labelCol: { span: 24 },
|
||||
wrapperCol: { span: 24 },
|
||||
}"
|
||||
@submit="handleSave"
|
||||
>
|
||||
<template #submitButton>
|
||||
<div class="flex gap-2">
|
||||
<VbenButton type="primary" :loading="saveLoading" @click="handleSave">
|
||||
保存配置
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleTest">
|
||||
测试上传
|
||||
</VbenButton>
|
||||
<VbenButton @click="handleReset">
|
||||
重置配置
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
</VbenForm>
|
||||
|
||||
<!-- 测试上传对话框 -->
|
||||
<VbenModal
|
||||
v-model:open="testDialogVisible"
|
||||
title="测试文件上传"
|
||||
width="600px"
|
||||
@ok="handleUploadTest"
|
||||
>
|
||||
<div class="upload-test-container">
|
||||
<VbenUpload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="testFileList"
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<div class="upload-area">
|
||||
<div class="upload-icon">
|
||||
<Icon icon="ep:upload-filled" size="40" />
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<div class="upload-tip">
|
||||
选择一个文件进行上传测试
|
||||
</div>
|
||||
</div>
|
||||
</VbenUpload>
|
||||
|
||||
<div v-if="testFile" class="test-file-info">
|
||||
<h4>文件信息:</h4>
|
||||
<p>文件名:{{ testFile.name }}</p>
|
||||
<p>文件大小:{{ formatFileSize(testFile.size) }}</p>
|
||||
<p>文件类型:{{ testFile.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VbenModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { VbenForm, VbenButton, VbenModal, VbenUpload } from '@vben/components'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { FormSchema } from '@vben/types'
|
||||
import {
|
||||
getStorageConfigApi,
|
||||
updateStorageConfigApi,
|
||||
testStorageApi,
|
||||
resetStorageConfigApi,
|
||||
type StorageConfig,
|
||||
} from '@/api/common/storage'
|
||||
|
||||
const formRef = ref()
|
||||
const uploadRef = ref()
|
||||
const saveLoading = ref(false)
|
||||
const testDialogVisible = ref(false)
|
||||
const testFileList = ref([])
|
||||
const testFile = ref<File | null>(null)
|
||||
|
||||
// 表单配置
|
||||
const formSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'driver',
|
||||
label: '存储驱动',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '本地存储', value: 'local' },
|
||||
{ label: '阿里云OSS', value: 'oss' },
|
||||
{ label: '腾讯云COS', value: 'cos' },
|
||||
{ label: '七牛云', value: 'qiniu' },
|
||||
{ label: '又拍云', value: 'upyun' },
|
||||
{ label: 'AWS S3', value: 's3' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// 本地存储配置
|
||||
{
|
||||
field: 'local_path',
|
||||
label: '存储路径',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'local',
|
||||
helpMessage: '相对于项目根目录的路径',
|
||||
},
|
||||
{
|
||||
field: 'local_domain',
|
||||
label: '访问域名',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'local',
|
||||
helpMessage: '文件访问的完整域名',
|
||||
},
|
||||
// 阿里云OSS配置
|
||||
{
|
||||
field: 'oss_access_key_id',
|
||||
label: 'AccessKey ID',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
},
|
||||
{
|
||||
field: 'oss_access_key_secret',
|
||||
label: 'AccessKey Secret',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
},
|
||||
{
|
||||
field: 'oss_bucket',
|
||||
label: 'Bucket名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
},
|
||||
{
|
||||
field: 'oss_region',
|
||||
label: '地域节点',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '华东1(杭州)', value: 'oss-cn-hangzhou' },
|
||||
{ label: '华东2(上海)', value: 'oss-cn-shanghai' },
|
||||
{ label: '华北1(青岛)', value: 'oss-cn-qingdao' },
|
||||
{ label: '华北2(北京)', value: 'oss-cn-beijing' },
|
||||
{ label: '华南1(深圳)', value: 'oss-cn-shenzhen' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'oss_domain',
|
||||
label: '自定义域名',
|
||||
component: 'Input',
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
helpMessage: '不填写则使用默认域名',
|
||||
},
|
||||
{
|
||||
field: 'oss_is_private',
|
||||
label: '是否私有',
|
||||
component: 'Switch',
|
||||
show: ({ values }) => values.driver === 'oss',
|
||||
defaultValue: false,
|
||||
},
|
||||
// 腾讯云COS配置
|
||||
{
|
||||
field: 'cos_secret_id',
|
||||
label: 'SecretId',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'cos',
|
||||
},
|
||||
{
|
||||
field: 'cos_secret_key',
|
||||
label: 'SecretKey',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'cos',
|
||||
},
|
||||
{
|
||||
field: 'cos_bucket',
|
||||
label: 'Bucket名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'cos',
|
||||
},
|
||||
{
|
||||
field: 'cos_region',
|
||||
label: '地域',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'cos',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '北京', value: 'ap-beijing' },
|
||||
{ label: '上海', value: 'ap-shanghai' },
|
||||
{ label: '广州', value: 'ap-guangzhou' },
|
||||
{ label: '成都', value: 'ap-chengdu' },
|
||||
{ label: '重庆', value: 'ap-chongqing' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// 七牛云配置
|
||||
{
|
||||
field: 'qiniu_access_key',
|
||||
label: 'AccessKey',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'qiniu',
|
||||
},
|
||||
{
|
||||
field: 'qiniu_secret_key',
|
||||
label: 'SecretKey',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'qiniu',
|
||||
},
|
||||
{
|
||||
field: 'qiniu_bucket',
|
||||
label: '存储空间',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'qiniu',
|
||||
},
|
||||
{
|
||||
field: 'qiniu_domain',
|
||||
label: '访问域名',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
show: ({ values }) => values.driver === 'qiniu',
|
||||
},
|
||||
// 上传限制
|
||||
{
|
||||
field: 'max_size',
|
||||
label: '最大文件大小(MB)',
|
||||
component: 'InputNumber',
|
||||
required: true,
|
||||
defaultValue: 10,
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 1024,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'allowed_types',
|
||||
label: '允许的文件类型',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
options: [
|
||||
{ label: '图片 (jpg, jpeg, png, gif, webp)', value: 'image' },
|
||||
{ label: '文档 (pdf, doc, docx, xls, xlsx, ppt, pptx)', value: 'document' },
|
||||
{ label: '视频 (mp4, avi, mov, wmv, flv)', value: 'video' },
|
||||
{ label: '音频 (mp3, wav, flac, aac)', value: 'audio' },
|
||||
{ label: '压缩包 (zip, rar, 7z, tar, gz)', value: 'archive' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// 图片处理
|
||||
{
|
||||
field: 'thumbnail_enabled',
|
||||
label: '自动生成缩略图',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
field: 'thumbnail_width',
|
||||
label: '缩略图宽度',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.thumbnail_enabled,
|
||||
defaultValue: 200,
|
||||
componentProps: {
|
||||
min: 50,
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'thumbnail_height',
|
||||
label: '缩略图高度',
|
||||
component: 'InputNumber',
|
||||
show: ({ values }) => values.thumbnail_enabled,
|
||||
defaultValue: 200,
|
||||
componentProps: {
|
||||
min: 50,
|
||||
max: 1000,
|
||||
},
|
||||
},
|
||||
// 其他设置
|
||||
{
|
||||
field: 'enabled',
|
||||
label: '启用状态',
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
field: 'is_default',
|
||||
label: '默认存储',
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
helpMessage: '设为默认存储驱动',
|
||||
},
|
||||
]
|
||||
|
||||
// 工具函数
|
||||
const formatFileSize = (size: number) => {
|
||||
if (size < 1024) {
|
||||
return size + ' B'
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + ' KB'
|
||||
} else {
|
||||
return (size / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const data = await getStorageConfigApi()
|
||||
formRef.value?.setFieldsValue(data)
|
||||
} catch (error) {
|
||||
message.error('加载配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await formRef.value?.validate()
|
||||
if (!values) return
|
||||
|
||||
saveLoading.value = true
|
||||
await updateStorageConfigApi(values as StorageConfig)
|
||||
message.success('保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试上传
|
||||
const handleTest = () => {
|
||||
testFile.value = null
|
||||
testFileList.value = []
|
||||
testDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (fileList: any[]) => {
|
||||
if (fileList.length > 0) {
|
||||
testFile.value = fileList[0].originFileObj || fileList[0]
|
||||
} else {
|
||||
testFile.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 上传测试
|
||||
const handleUploadTest = async () => {
|
||||
if (!testFile.value) {
|
||||
message.warning('请选择要上传的文件')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', testFile.value)
|
||||
|
||||
const result = await testStorageApi(formData)
|
||||
message.success(`上传测试成功!文件URL: ${result.url}`)
|
||||
testDialogVisible.value = false
|
||||
} catch (error) {
|
||||
message.error('上传测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
await resetStorageConfigApi()
|
||||
message.success('重置成功')
|
||||
await loadConfig()
|
||||
} catch (error) {
|
||||
message.error('重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-test-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-text em {
|
||||
color: #1890ff;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.test-file-info {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.test-file-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.test-file-info p {
|
||||
margin: 5px 0;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
476
admin/apps/web-ele/src/views/common/settings/system/index.vue
Normal file
476
admin/apps/web-ele/src/views/common/settings/system/index.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<Page>
|
||||
<VbenCard title="系统设置">
|
||||
<template #extra>
|
||||
<Icon icon="lucide:settings" class="text-lg" />
|
||||
</template>
|
||||
|
||||
<VbenTabs v-model:active-key="activeTab" type="card">
|
||||
<!-- 基本信息 -->
|
||||
<VbenTabPane key="basic" tab="基本信息">
|
||||
<VbenForm @submit="handleSubmitBasic">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="site_name" label="网站名称">
|
||||
<Input v-model:value="form.site_name" placeholder="请输入网站名称" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="site_title" label="网站标题">
|
||||
<Input v-model:value="form.site_title" placeholder="请输入网站标题" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="site_description" label="网站描述">
|
||||
<Input
|
||||
v-model:value="form.site_description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入网站描述"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="site_keywords" label="网站关键词">
|
||||
<Input
|
||||
v-model:value="form.site_keywords"
|
||||
placeholder="请输入网站关键词,多个关键词用逗号分隔"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="site_icp" label="ICP备案号">
|
||||
<Input v-model:value="form.site_icp" placeholder="请输入ICP备案号" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="site_copyright" label="版权信息">
|
||||
<Input
|
||||
v-model:value="form.site_copyright"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入版权信息"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存设置
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handleResetBasic">
|
||||
<Icon icon="lucide:rotate-ccw" class="mr-1" />
|
||||
重置
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 系统配置 -->
|
||||
<VbenTabPane key="config" tab="系统配置">
|
||||
<VbenForm @submit="handleSubmitConfig">
|
||||
<template #default="{ form }">
|
||||
<VbenFormItem name="site_status" label="系统状态">
|
||||
<RadioGroup v-model:value="form.site_status">
|
||||
<template #default>
|
||||
<Radio value="1">正常运行</Radio>
|
||||
<Radio value="0">维护模式</Radio>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem
|
||||
v-if="form.site_status === '0'"
|
||||
name="site_close_reason"
|
||||
label="维护提示"
|
||||
>
|
||||
<Input
|
||||
v-model:value="form.site_close_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入维护提示信息"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="timezone" label="时区设置">
|
||||
<Select v-model:value="form.timezone" placeholder="请选择时区">
|
||||
<template #default>
|
||||
<SelectOption value="Asia/Shanghai">北京时间 (UTC+8)</SelectOption>
|
||||
<SelectOption value="Asia/Tokyo">东京时间 (UTC+9)</SelectOption>
|
||||
<SelectOption value="America/New_York">纽约时间 (UTC-5)</SelectOption>
|
||||
<SelectOption value="Europe/London">伦敦时间 (UTC+0)</SelectOption>
|
||||
<SelectOption value="Europe/Paris">巴黎时间 (UTC+1)</SelectOption>
|
||||
</template>
|
||||
</Select>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="default_language" label="默认语言">
|
||||
<Select v-model:value="form.default_language" placeholder="请选择默认语言">
|
||||
<template #default>
|
||||
<SelectOption value="zh-CN">简体中文</SelectOption>
|
||||
<SelectOption value="zh-TW">繁体中文</SelectOption>
|
||||
<SelectOption value="en-US">English</SelectOption>
|
||||
<SelectOption value="ja-JP">日本語</SelectOption>
|
||||
<SelectOption value="ko-KR">한국어</SelectOption>
|
||||
</template>
|
||||
</Select>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="page_size" label="分页大小">
|
||||
<InputNumber
|
||||
v-model:value="form.page_size"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="10"
|
||||
placeholder="请输入分页大小"
|
||||
/>
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="cache_enabled" label="缓存启用">
|
||||
<Switch v-model:checked="form.cache_enabled" />
|
||||
</VbenFormItem>
|
||||
|
||||
<VbenFormItem name="debug_enabled" label="调试模式">
|
||||
<Switch v-model:checked="form.debug_enabled" />
|
||||
</VbenFormItem>
|
||||
</template>
|
||||
|
||||
<template #submit>
|
||||
<Space>
|
||||
<PrimaryButton html-type="submit" :loading="submitLoading">
|
||||
<Icon icon="lucide:check" class="mr-1" />
|
||||
保存设置
|
||||
</PrimaryButton>
|
||||
<DefaultButton @click="handleResetConfig">
|
||||
<Icon icon="lucide:rotate-ccw" class="mr-1" />
|
||||
重置
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</template>
|
||||
</VbenForm>
|
||||
</VbenTabPane>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<VbenTabPane key="info" tab="系统信息">
|
||||
<VbenDescriptions title="服务器信息" :column="2" bordered>
|
||||
<VbenDescriptionsItem label="操作系统">
|
||||
{{ systemInfo.os || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="服务器软件">
|
||||
{{ systemInfo.server || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="PHP版本">
|
||||
{{ systemInfo.php_version || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="MySQL版本">
|
||||
{{ systemInfo.mysql_version || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="Redis版本">
|
||||
{{ systemInfo.redis_version || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="Node.js版本">
|
||||
{{ systemInfo.node_version || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="内存使用">
|
||||
{{ systemInfo.memory_usage || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
<VbenDescriptionsItem label="磁盘使用">
|
||||
{{ systemInfo.disk_usage || '-' }}
|
||||
</VbenDescriptionsItem>
|
||||
</VbenDescriptions>
|
||||
|
||||
<div class="mt-4">
|
||||
<Space>
|
||||
<DefaultButton @click="handleRefreshInfo" :loading="refreshLoading">
|
||||
<Icon icon="lucide:refresh-cw" class="mr-1" />
|
||||
刷新信息
|
||||
</DefaultButton>
|
||||
<DefaultButton @click="handleExportInfo" :loading="exportLoading">
|
||||
<Icon icon="lucide:download" class="mr-1" />
|
||||
导出信息
|
||||
</DefaultButton>
|
||||
</Space>
|
||||
</div>
|
||||
</VbenTabPane>
|
||||
</VbenTabs>
|
||||
</VbenCard>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import {
|
||||
Page,
|
||||
VbenCard,
|
||||
VbenTabs,
|
||||
VbenTabPane,
|
||||
VbenDescriptions,
|
||||
VbenDescriptionsItem
|
||||
} from '@vben/common-ui';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import {
|
||||
getSystemConfigApi,
|
||||
updateSystemConfigApi,
|
||||
getSystemInfoApi,
|
||||
exportSystemInfoApi
|
||||
} from '#/api/common/system';
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('basic');
|
||||
const submitLoading = ref(false);
|
||||
const refreshLoading = ref(false);
|
||||
const exportLoading = ref(false);
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = reactive({
|
||||
os: '',
|
||||
server: '',
|
||||
php_version: '',
|
||||
mysql_version: '',
|
||||
redis_version: '',
|
||||
node_version: '',
|
||||
memory_usage: '',
|
||||
disk_usage: '',
|
||||
});
|
||||
|
||||
// 基本信息表单配置
|
||||
const basicFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_name',
|
||||
label: '网站名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入网站名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_title',
|
||||
label: '网站标题',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入网站标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_description',
|
||||
label: '网站描述',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 3,
|
||||
placeholder: '请输入网站描述',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_keywords',
|
||||
label: '网站关键词',
|
||||
componentProps: {
|
||||
placeholder: '请输入网站关键词,多个关键词用逗号分隔',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_icp',
|
||||
label: 'ICP备案号',
|
||||
componentProps: {
|
||||
placeholder: '请输入ICP备案号',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_copyright',
|
||||
label: '版权信息',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 2,
|
||||
placeholder: '请输入版权信息',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 系统配置表单配置
|
||||
const configFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'site_status',
|
||||
label: '系统状态',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '正常运行', value: '1' },
|
||||
{ label: '维护模式', value: '0' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'site_close_reason',
|
||||
label: '维护提示',
|
||||
dependencies: {
|
||||
triggerFields: ['site_status'],
|
||||
show: (values) => values.site_status === '0',
|
||||
},
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 3,
|
||||
placeholder: '请输入维护提示信息',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'timezone',
|
||||
label: '时区设置',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择时区',
|
||||
options: [
|
||||
{ label: '北京时间 (UTC+8)', value: 'Asia/Shanghai' },
|
||||
{ label: '东京时间 (UTC+9)', value: 'Asia/Tokyo' },
|
||||
{ label: '纽约时间 (UTC-5)', value: 'America/New_York' },
|
||||
{ label: '伦敦时间 (UTC+0)', value: 'Europe/London' },
|
||||
{ label: '巴黎时间 (UTC+1)', value: 'Europe/Paris' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'default_language',
|
||||
label: '默认语言',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择默认语言',
|
||||
options: [
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: '繁体中文', value: 'zh-TW' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
{ label: '日本語', value: 'ja-JP' },
|
||||
{ label: '한국어', value: 'ko-KR' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'page_size',
|
||||
label: '分页大小',
|
||||
componentProps: {
|
||||
min: 10,
|
||||
max: 100,
|
||||
step: 10,
|
||||
placeholder: '请输入分页大小',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'cache_enabled',
|
||||
label: '缓存启用',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'debug_enabled',
|
||||
label: '调试模式',
|
||||
},
|
||||
];
|
||||
|
||||
// 创建表单实例
|
||||
const [BasicForm, basicFormApi] = useVbenForm({
|
||||
schema: basicFormSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [ConfigForm, configFormApi] = useVbenForm({
|
||||
schema: configFormSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 处理基本信息提交
|
||||
const handleSubmitBasic = async (values: Record<string, any>) => {
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
await updateSystemConfigApi('basic', values);
|
||||
// 显示成功消息
|
||||
} catch (error) {
|
||||
console.error('保存基本信息失败:', error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理系统配置提交
|
||||
const handleSubmitConfig = async (values: Record<string, any>) => {
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
await updateSystemConfigApi('config', values);
|
||||
// 显示成功消息
|
||||
} catch (error) {
|
||||
console.error('保存系统配置失败:', error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置基本信息
|
||||
const handleResetBasic = () => {
|
||||
basicFormApi.resetForm();
|
||||
};
|
||||
|
||||
// 重置系统配置
|
||||
const handleResetConfig = () => {
|
||||
configFormApi.resetForm();
|
||||
};
|
||||
|
||||
// 刷新系统信息
|
||||
const handleRefreshInfo = async () => {
|
||||
try {
|
||||
refreshLoading.value = true;
|
||||
const data = await getSystemInfoApi();
|
||||
Object.assign(systemInfo, data);
|
||||
} catch (error) {
|
||||
console.error('刷新系统信息失败:', error);
|
||||
} finally {
|
||||
refreshLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导出系统信息
|
||||
const handleExportInfo = async () => {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
await exportSystemInfoApi();
|
||||
} catch (error) {
|
||||
console.error('导出系统信息失败:', error);
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
try {
|
||||
const [basicData, configData, infoData] = await Promise.all([
|
||||
getSystemConfigApi('basic'),
|
||||
getSystemConfigApi('config'),
|
||||
getSystemInfoApi(),
|
||||
]);
|
||||
|
||||
basicFormApi.setValues(basicData);
|
||||
configFormApi.setValues(configData);
|
||||
Object.assign(systemInfo, infoData);
|
||||
} catch (error) {
|
||||
console.error('初始化数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化数据
|
||||
onMounted(() => {
|
||||
initData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
550
admin/apps/web-ele/src/views/common/user/admin/index.vue
Normal file
550
admin/apps/web-ele/src/views/common/user/admin/index.vue
Normal file
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<Page
|
||||
description="管理系统管理员账户信息"
|
||||
title="管理员管理"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- 搜索表单 -->
|
||||
<el-card>
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="ep:search" class="mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮和数据表格 -->
|
||||
<el-card>
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增管理员
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-1" />
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="uid" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="realName" label="真实姓名" min-width="120" />
|
||||
<el-table-column prop="mobile" label="手机号" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="150" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isAdmin" label="超级管理员" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isAdmin === 1 ? 'warning' : 'info'">
|
||||
{{ row.isAdmin === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="handleSetRoles(row)">
|
||||
角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名" prop="realName">
|
||||
<el-input v-model="formData.realName" placeholder="请输入真实姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formData.sex">
|
||||
<el-radio :label="1">男</el-radio>
|
||||
<el-radio :label="2">女</el-radio>
|
||||
<el-radio :label="0">未知</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">正常</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="超级管理员" prop="isAdmin">
|
||||
<el-radio-group v-model="formData.isAdmin">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 角色分配对话框 -->
|
||||
<el-dialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
||||
<el-checkbox-group v-model="selectedRoleIds">
|
||||
<el-checkbox
|
||||
v-for="role in roleList"
|
||||
:key="role.roleId"
|
||||
:label="role.roleId"
|
||||
>
|
||||
{{ role.roleName }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveRoles" :loading="roleLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 1. Vue 相关导入
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
|
||||
// 2. Element Plus 组件导入
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
type FormInstance,
|
||||
} from 'element-plus';
|
||||
|
||||
// 3. 图标组件导入
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 4. Vben 组件导入
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 5. 项目内部导入
|
||||
import {
|
||||
getAdminListApi,
|
||||
createAdminApi,
|
||||
updateAdminApi,
|
||||
deleteAdminApi,
|
||||
batchDeleteAdminApi,
|
||||
setAdminRolesApi,
|
||||
getAdminRolesApi,
|
||||
getAllRolesApi,
|
||||
type AdminUser,
|
||||
type CreateAdminParams,
|
||||
type UpdateAdminParams,
|
||||
type Role,
|
||||
} from '#/api/common/auth';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const roleLoading = ref(false);
|
||||
const tableData = ref<AdminUser[]>([]);
|
||||
const selectedRows = ref<AdminUser[]>([]);
|
||||
const roleList = ref<Role[]>([]);
|
||||
const selectedRoleIds = ref<number[]>([]);
|
||||
const currentUserId = ref<number>(0);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined,
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false);
|
||||
const roleDialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreateAdminParams & { uid?: number }>({
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
sex: 0,
|
||||
status: 1,
|
||||
isAdmin: 0,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
realName: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||
],
|
||||
mobile: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑管理员' : '新增管理员'));
|
||||
|
||||
// 方法
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status,
|
||||
};
|
||||
const result = await getAdminListApi(params);
|
||||
tableData.value = result.list;
|
||||
pagination.total = result.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
roleList.value = await getAllRolesApi();
|
||||
} catch (error) {
|
||||
ElMessage.error('加载角色列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.limit = size;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selection: AdminUser[]) => {
|
||||
selectedRows.value = selection;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: AdminUser) => {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, {
|
||||
uid: row.uid,
|
||||
username: row.username,
|
||||
password: '',
|
||||
realName: row.realName,
|
||||
mobile: row.mobile,
|
||||
email: row.email,
|
||||
sex: row.sex,
|
||||
status: row.status,
|
||||
isAdmin: row.isAdmin,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (row: AdminUser) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除管理员 "${row.username}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await deleteAdminApi(row.uid);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 个管理员吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
const uids = selectedRows.value.map(row => row.uid);
|
||||
await batchDeleteAdminApi(uids);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetRoles = async (row: AdminUser) => {
|
||||
currentUserId.value = row.uid;
|
||||
try {
|
||||
const roles = await getAdminRolesApi(row.uid);
|
||||
selectedRoleIds.value = roles.map(role => role.roleId);
|
||||
roleDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载用户角色失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRoles = async () => {
|
||||
roleLoading.value = true;
|
||||
try {
|
||||
await setAdminRolesApi(currentUserId.value, selectedRoleIds.value);
|
||||
ElMessage.success('角色分配成功');
|
||||
roleDialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error('角色分配失败');
|
||||
} finally {
|
||||
roleLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
const updateData: UpdateAdminParams = {
|
||||
uid: formData.uid!,
|
||||
username: formData.username,
|
||||
realName: formData.realName,
|
||||
mobile: formData.mobile,
|
||||
email: formData.email,
|
||||
sex: formData.sex,
|
||||
status: formData.status,
|
||||
isAdmin: formData.isAdmin,
|
||||
};
|
||||
if (formData.password) {
|
||||
updateData.password = formData.password;
|
||||
}
|
||||
await updateAdminApi(updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
await createAdminApi(formData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
uid: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
sex: 0,
|
||||
status: 1,
|
||||
isAdmin: 0,
|
||||
});
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
loadRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-user-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
722
admin/apps/web-ele/src/views/common/user/member/index.vue
Normal file
722
admin/apps/web-ele/src/views/common/user/member/index.vue
Normal file
@@ -0,0 +1,722 @@
|
||||
<template>
|
||||
<div class="member-user-page">
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入用户名或手机号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员等级">
|
||||
<el-select v-model="searchForm.memberLevel" placeholder="请选择会员等级" clearable>
|
||||
<el-option label="普通会员" :value="1" />
|
||||
<el-option label="银牌会员" :value="2" />
|
||||
<el-option label="金牌会员" :value="3" />
|
||||
<el-option label="钻石会员" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<Icon icon="ep:search" class="mr-1" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="ep:refresh" class="mr-1" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons mb-4">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<Icon icon="ep:plus" class="mr-1" />
|
||||
新增会员
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-1" />
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleExport">
|
||||
<Icon icon="ep:download" class="mr-1" />
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="memberId" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||||
<el-table-column prop="mobile" label="手机号" min-width="120" />
|
||||
<el-table-column prop="sex" label="性别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSexTagType(row.sex)">
|
||||
{{ getSexText(row.sex) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="memberLevel" label="会员等级" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelTagType(row.memberLevel)">
|
||||
{{ getLevelText(row.memberLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="point" label="积分" width="80" />
|
||||
<el-table-column prop="balance" label="余额" width="100">
|
||||
<template #default="{ row }">
|
||||
¥{{ (row.balance / 100).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="注册时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="handleBalance(row)">
|
||||
余额
|
||||
</el-button>
|
||||
<el-button type="info" size="small" @click="handlePoint(row)">
|
||||
积分
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickname">
|
||||
<el-input v-model="formData.nickname" placeholder="请输入昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formData.sex">
|
||||
<el-radio :label="1">男</el-radio>
|
||||
<el-radio :label="2">女</el-radio>
|
||||
<el-radio :label="0">未知</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员等级" prop="memberLevel">
|
||||
<el-select v-model="formData.memberLevel" placeholder="请选择会员等级">
|
||||
<el-option label="普通会员" :value="1" />
|
||||
<el-option label="银牌会员" :value="2" />
|
||||
<el-option label="金牌会员" :value="3" />
|
||||
<el-option label="钻石会员" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="积分" prop="point">
|
||||
<el-input-number v-model="formData.point" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="余额" prop="balance">
|
||||
<el-input-number v-model="formData.balance" :min="0" :precision="2" />
|
||||
<span class="ml-2 text-gray-500">元</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">正常</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 余额调整对话框 -->
|
||||
<el-dialog v-model="balanceDialogVisible" title="余额调整" width="400px">
|
||||
<el-form :model="balanceForm" label-width="100px">
|
||||
<el-form-item label="当前余额">
|
||||
<span>¥{{ (currentMember?.balance || 0) / 100 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整类型">
|
||||
<el-radio-group v-model="balanceForm.changeType">
|
||||
<el-radio label="increase">增加</el-radio>
|
||||
<el-radio label="decrease">减少</el-radio>
|
||||
<el-radio label="set">设置</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整金额">
|
||||
<el-input-number
|
||||
v-model="balanceForm.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
<span class="ml-2 text-gray-500">元</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="balanceForm.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入调整原因"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="balanceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveBalance" :loading="balanceLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 积分调整对话框 -->
|
||||
<el-dialog v-model="pointDialogVisible" title="积分调整" width="400px">
|
||||
<el-form :model="pointForm" label-width="100px">
|
||||
<el-form-item label="当前积分">
|
||||
<span>{{ currentMember?.point || 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整类型">
|
||||
<el-radio-group v-model="pointForm.changeType">
|
||||
<el-radio label="increase">增加</el-radio>
|
||||
<el-radio label="decrease">减少</el-radio>
|
||||
<el-radio label="set">设置</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整积分">
|
||||
<el-input-number
|
||||
v-model="pointForm.amount"
|
||||
:min="0"
|
||||
placeholder="请输入积分"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="pointForm.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入调整原因"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pointDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSavePoint" :loading="pointLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 1. Vue 相关导入
|
||||
import { ref, reactive, onMounted, computed, type FormInstance } from 'vue';
|
||||
|
||||
// 2. Element Plus 组件导入
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
// 3. 图标组件导入
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
// 4. Vben 组件导入
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 5. 项目内部导入
|
||||
import {
|
||||
getMemberListApi,
|
||||
createMemberApi,
|
||||
updateMemberApi,
|
||||
deleteMemberApi,
|
||||
batchDeleteMemberApi,
|
||||
updateMemberBalanceApi,
|
||||
updateMemberPointApi,
|
||||
type Member,
|
||||
type CreateMemberParams,
|
||||
type UpdateMemberParams,
|
||||
} from '#/api/common/user';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const balanceLoading = ref(false);
|
||||
const pointLoading = ref(false);
|
||||
const tableData = ref<Member[]>([]);
|
||||
const selectedRows = ref<Member[]>([]);
|
||||
const currentMember = ref<Member | null>(null);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined,
|
||||
memberLevel: undefined as number | undefined,
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false);
|
||||
const balanceDialogVisible = ref(false);
|
||||
const pointDialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreateMemberParams & { memberId?: number; balance: number }>({
|
||||
username: '',
|
||||
password: '',
|
||||
mobile: '',
|
||||
nickname: '',
|
||||
sex: 0,
|
||||
memberLevel: 1,
|
||||
point: 0,
|
||||
balance: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 余额调整表单
|
||||
const balanceForm = reactive({
|
||||
changeType: 'increase',
|
||||
amount: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 积分调整表单
|
||||
const pointForm = reactive({
|
||||
changeType: 'increase',
|
||||
amount: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||
],
|
||||
nickname: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑会员' : '新增会员'));
|
||||
|
||||
// 方法
|
||||
const formatTime = (timestamp: number) => {
|
||||
if (!timestamp) return '-';
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getSexText = (sex: number) => {
|
||||
const map = { 0: '未知', 1: '男', 2: '女' };
|
||||
return map[sex as keyof typeof map] || '未知';
|
||||
};
|
||||
|
||||
const getSexTagType = (sex: number) => {
|
||||
const map = { 0: 'info', 1: 'primary', 2: 'success' };
|
||||
return map[sex as keyof typeof map] || 'info';
|
||||
};
|
||||
|
||||
const getLevelText = (level: number) => {
|
||||
const map = { 1: '普通', 2: '银牌', 3: '金牌', 4: '钻石' };
|
||||
return map[level as keyof typeof map] || '普通';
|
||||
};
|
||||
|
||||
const getLevelTagType = (level: number) => {
|
||||
const map = { 1: 'info', 2: 'warning', 3: 'success', 4: 'danger' };
|
||||
return map[level as keyof typeof map] || 'info';
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status,
|
||||
};
|
||||
const result = await getMemberListApi(params);
|
||||
tableData.value = result.list;
|
||||
pagination.total = result.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined;
|
||||
searchForm.memberLevel = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.limit = size;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selection: Member[]) => {
|
||||
selectedRows.value = selection;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: Member) => {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, {
|
||||
memberId: row.memberId,
|
||||
username: row.username,
|
||||
password: '',
|
||||
mobile: row.mobile,
|
||||
nickname: row.nickname,
|
||||
sex: row.sex,
|
||||
memberLevel: row.memberLevel,
|
||||
point: row.point,
|
||||
balance: row.balance / 100, // 转换为元
|
||||
status: row.status,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (row: Member) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除会员 "${row.username}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
await deleteMemberApi(row.memberId);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 个会员吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
const memberIds = selectedRows.value.map(row => row.memberId);
|
||||
await batchDeleteMemberApi(memberIds);
|
||||
ElMessage.success('删除成功');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
ElMessage.info('导出功能开发中...');
|
||||
};
|
||||
|
||||
const handleBalance = (row: Member) => {
|
||||
currentMember.value = row;
|
||||
balanceForm.changeType = 'increase';
|
||||
balanceForm.amount = 0;
|
||||
balanceForm.remark = '';
|
||||
balanceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handlePoint = (row: Member) => {
|
||||
currentMember.value = row;
|
||||
pointForm.changeType = 'increase';
|
||||
pointForm.amount = 0;
|
||||
pointForm.remark = '';
|
||||
pointDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSaveBalance = async () => {
|
||||
if (!currentMember.value || !balanceForm.amount) {
|
||||
ElMessage.warning('请输入调整金额');
|
||||
return;
|
||||
}
|
||||
|
||||
balanceLoading.value = true;
|
||||
try {
|
||||
let newBalance = balanceForm.amount * 100; // 转换为分
|
||||
if (balanceForm.changeType === 'decrease') {
|
||||
newBalance = -newBalance;
|
||||
} else if (balanceForm.changeType === 'set') {
|
||||
newBalance = balanceForm.amount * 100;
|
||||
}
|
||||
|
||||
await updateMemberBalanceApi(
|
||||
currentMember.value.memberId,
|
||||
newBalance,
|
||||
balanceForm.changeType,
|
||||
balanceForm.remark
|
||||
);
|
||||
|
||||
ElMessage.success('余额调整成功');
|
||||
balanceDialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error('余额调整失败');
|
||||
} finally {
|
||||
balanceLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSavePoint = async () => {
|
||||
if (!currentMember.value || !pointForm.amount) {
|
||||
ElMessage.warning('请输入调整积分');
|
||||
return;
|
||||
}
|
||||
|
||||
pointLoading.value = true;
|
||||
try {
|
||||
let newPoint = pointForm.amount;
|
||||
if (pointForm.changeType === 'decrease') {
|
||||
newPoint = -newPoint;
|
||||
}
|
||||
|
||||
await updateMemberPointApi(
|
||||
currentMember.value.memberId,
|
||||
newPoint,
|
||||
pointForm.changeType,
|
||||
pointForm.remark
|
||||
);
|
||||
|
||||
ElMessage.success('积分调整成功');
|
||||
pointDialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error('积分调整失败');
|
||||
} finally {
|
||||
pointLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
submitLoading.value = true;
|
||||
|
||||
if (isEdit.value) {
|
||||
const updateData: UpdateMemberParams = {
|
||||
memberId: formData.memberId!,
|
||||
username: formData.username,
|
||||
mobile: formData.mobile,
|
||||
nickname: formData.nickname,
|
||||
sex: formData.sex,
|
||||
memberLevel: formData.memberLevel,
|
||||
point: formData.point,
|
||||
balance: Math.round(formData.balance * 100), // 转换为分
|
||||
status: formData.status,
|
||||
};
|
||||
if (formData.password) {
|
||||
updateData.password = formData.password;
|
||||
}
|
||||
await updateMemberApi(updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
const createData: CreateMemberParams = {
|
||||
...formData,
|
||||
balance: Math.round(formData.balance * 100), // 转换为分
|
||||
};
|
||||
await createMemberApi(createData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
memberId: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
mobile: '',
|
||||
nickname: '',
|
||||
sex: 0,
|
||||
memberLevel: 1,
|
||||
point: 0,
|
||||
balance: 0,
|
||||
status: 1,
|
||||
});
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-user-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
90
admin/apps/web-ele/src/views/dashboard/analytics/index.vue
Normal file
90
admin/apps/web-ele/src/views/dashboard/analytics/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
266
admin/apps/web-ele/src/views/dashboard/workspace/index.vue
Normal file
266
admin/apps/web-ele/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
117
admin/apps/web-ele/src/views/demos/element/index.vue
Normal file
117
admin/apps/web-ele/src/views/demos/element/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElMessage,
|
||||
ElNotification,
|
||||
ElSegmented,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
ElMessage.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
ElMessage.error({
|
||||
duration: 2500,
|
||||
message: 'Once upon a time you dressed so fine',
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
ElMessage.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
ElMessage.success(
|
||||
'Cause you walked hand in hand With another man in my place',
|
||||
);
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
ElNotification({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
const tableData = [
|
||||
{ prop1: '1', prop2: 'A' },
|
||||
{ prop1: '2', prop2: 'B' },
|
||||
{ prop1: '3', prop2: 'C' },
|
||||
{ prop1: '4', prop2: 'D' },
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
|
||||
const segmentedValue = ref('Mon');
|
||||
|
||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<div class="flex flex-wrap gap-5">
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> Segmented </template>
|
||||
<ElSegmented
|
||||
v-model="segmentedValue"
|
||||
:options="segmentedOptions"
|
||||
size="large"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> V-Loading </template>
|
||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
||||
一些演示的内容
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
191
admin/apps/web-ele/src/views/demos/form/basic.vue
Normal file
191
admin/apps/web-ele/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
// wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'IconPicker',
|
||||
fieldName: 'icon',
|
||||
label: 'IconPicker',
|
||||
},
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer();
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<Drawer class="w-[600px]" title="基础表单示例">
|
||||
<Form />
|
||||
</Drawer>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElButton type="primary" @click="drawerApi.open"> 打开抽屉 </ElButton>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user