!48 基于 dev-v5 分支重构优化

Merge pull request !48 from chenminjie/dev-v5_cmj
pull/49/head
xingyu 2024-11-26 02:40:05 +00:00 committed by Gitee
commit 36ffbbbb95
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
73 changed files with 2812 additions and 978 deletions

View File

@ -3,14 +3,11 @@ VITE_APP_TITLE=芋道管理系统
# 应用命名空间用于缓存、store等功能的前缀确保隔离 # 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=yudao-vben-antd VITE_APP_NAMESPACE=yudao-vben-antd
# 是否开启模拟数据
VITE_NITRO_MOCK=false
# 租户开关 # 租户开关
VITE_APP_TENANT_ENABLE=true VITE_APP_TENANT_ENABLE=true
# 验证码的开关 # 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false VITE_APP_CAPTCHA_ENABLE=true
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT=芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME=admin
VITE_APP_DEFAULT_LOGIN_PASSWORD=admin123

View File

@ -5,12 +5,15 @@ VITE_BASE=/
# 接口地址 # 接口地址
VITE_GLOB_API_URL=/admin-api VITE_GLOB_API_URL=/admin-api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false
# 是否打开 devtoolstrue 为打开false 为关闭 # 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false VITE_DEVTOOLS=false
# 是否注入全局loading # 是否注入全局loading
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
# 默认租户名称
VITE_APP_DEFAULT_TENANT_NAME=芋道源码
# 默认登录用户名
VITE_APP_DEFAULT_USERNAME=admin
# 默认登录密码
VITE_APP_DEFAULT_PASSWORD=admin123

View File

@ -42,13 +42,9 @@
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "catalog:",
"ant-design-vue": "catalog:", "ant-design-vue": "catalog:",
"crypto-js": "^4.2.0",
"dayjs": "catalog:", "dayjs": "catalog:",
"pinia": "catalog:", "pinia": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-router": "catalog:" "vue-router": "catalog:"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2"
} }
} }

View File

@ -5,6 +5,8 @@
import type { BaseFormComponentType } from '@vben/common-ui'; import type { BaseFormComponentType } from '@vben/common-ui';
import type { CustomComponentType } from '#/components/form/types';
import type { Component, SetupContext } from 'vue'; import type { Component, SetupContext } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
@ -36,6 +38,8 @@ import {
Upload, Upload,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { registerComponent as registerCustomFormComponent } from '#/components/form/component-map';
const withDefaultPlaceholder = <T extends Component>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,
type: 'input' | 'select', type: 'input' | 'select',
@ -70,7 +74,8 @@ export type ComponentType =
| 'TimePicker' | 'TimePicker'
| 'TreeSelect' | 'TreeSelect'
| 'Upload' | 'Upload'
| BaseFormComponentType; | BaseFormComponentType
| CustomComponentType;
async function initComponentAdapter() { async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = { const components: Partial<Record<ComponentType, Component>> = {
@ -108,6 +113,9 @@ async function initComponentAdapter() {
Upload, Upload,
}; };
// 注册自定义组件
registerCustomFormComponent(components);
// 将组件注册到全局共享状态中 // 将组件注册到全局共享状态中
globalShareState.setComponents(components); globalShareState.setComponents(components);

View File

@ -20,6 +20,17 @@ setupVbenVxeTable({
// 全局禁用vxe-table的表单配置使用formOptions // 全局禁用vxe-table的表单配置使用formOptions
enabled: false, enabled: false,
}, },
toolbarConfig: {
import: true,
export: true,
refresh: true,
print: true,
zoom: true,
custom: true,
},
customConfig: {
mode: 'modal',
},
proxyConfig: { proxyConfig: {
autoLoad: true, autoLoad: true,
response: { response: {
@ -29,6 +40,12 @@ setupVbenVxeTable({
showActiveMsg: true, showActiveMsg: true,
showResponseMsg: false, showResponseMsg: false,
}, },
pagerConfig: {
enabled: true,
},
sortConfig: {
multiple: true,
},
round: true, round: true,
showOverflow: true, showOverflow: true,
size: 'small', size: 'small',

View File

@ -1,7 +1,6 @@
import type { YudaoUserInfo } from '#/types'; import type { AuthPermissionInfo } from '@vben/types';
import { baseRequestClient, requestClient } from '#/api/request'; import { baseRequestClient, requestClient } from '#/api/request';
import { getRefreshToken } from '#/utils';
export namespace AuthApi { export namespace AuthApi {
/** 登录接口参数 */ /** 登录接口参数 */
@ -13,40 +12,47 @@ export namespace AuthApi {
/** 登录接口返回值 */ /** 登录接口返回值 */
export interface LoginResult { export interface LoginResult {
userId: number | string;
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
userId: number;
expiresTime: number; expiresTime: number;
} }
export interface RefreshTokenResult { export interface TenantSimple {
data: string; id: number;
status: number; name: string;
}
export interface SmsCodeVO {
mobile: string;
scene: number;
}
export interface SmsLoginVO {
mobile: string;
code: string;
} }
} }
/** /**
* *
*/ */
export function loginApi(data: AuthApi.LoginParams) { export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data); return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
} }
/** /**
* accessToken * accessToken
*/ */
export function refreshTokenApi() { export async function refreshTokenApi(refreshToken: string) {
return requestClient.post<AuthApi.LoginResult>( return requestClient.post<AuthApi.LoginResult>(
`/system/auth/refresh-token?refreshToken=${getRefreshToken()}`, `/system/auth/refresh-token?refreshToken=${refreshToken}`,
);
}
/**
* 退
*/
export async function logoutApi() {
return requestClient.post('/system/auth/logout');
}
/**
*
*/
export function getAuthPermissionInfoApi() {
return requestClient.get<AuthPermissionInfo>(
'/system/auth/get-permission-info',
); );
} }
@ -55,7 +61,7 @@ export function refreshTokenApi() {
* @param name * @param name
* @returns * @returns
*/ */
export function getTenantIdByName(name: string) { export async function getTenantIdByName(name: string) {
return requestClient.get<number>( return requestClient.get<number>(
`/system/tenant/get-id-by-name?name=${name}`, `/system/tenant/get-id-by-name?name=${name}`,
); );
@ -66,68 +72,18 @@ export function getTenantIdByName(name: string) {
* @param website * @param website
* @returns * @returns
*/ */
export function getTenantByWebsite(website: string) { export async function getTenantByWebsite(website: string) {
return requestClient.get(`/system/tenant/get-by-website?website=${website}`); return requestClient.get<AuthApi.TenantSimple>(
} `/system/tenant/get-by-website?website=${website}`,
/**
* 退
*/
export function logoutApi() {
return requestClient.post('/system/auth/logout');
}
/**
*
*/
export function getUserInfo() {
return requestClient.get<YudaoUserInfo>('/system/auth/get-permission-info');
}
/**
*
*/
export function sendSmsCode(data: AuthApi.SmsCodeVO) {
return requestClient.post('/system/auth/send-sms-code', data);
}
/**
*
*/
export function smsLogin(data: AuthApi.SmsLoginVO) {
return requestClient.post('/system/auth/sms-login', data);
}
/**
* 使 code
*/
export function socialLogin(type: string, code: string, state: string) {
return requestClient.post('/system/auth/social-login', {
type,
code,
state,
});
}
/**
*
*/
export function socialAuthRedirect(type: number, redirectUri: string) {
return requestClient.get(
`/system/auth/social-auth-redirect?type=${type}&redirectUri=${redirectUri}`,
); );
} }
/** // 获取验证图片 以及token
* token export async function getCaptcha(data: any) {
*/
export function getCaptcha(data: any) {
return baseRequestClient.post('/system/captcha/get', data); return baseRequestClient.post('/system/captcha/get', data);
} }
/** // 滑动或者点选验证
* export async function checkCaptcha(data: any) {
*/
export function checkCaptcha(data: any) {
return baseRequestClient.post('/system/captcha/check', data); return baseRequestClient.post('/system/captcha/check', data);
} }

View File

@ -0,0 +1,312 @@
import { type PageParam, requestClient } from '#/api/request';
export namespace CodegenApi {
/**
* Response VO
*/
export interface CodegenColumnRespVO {
/** 编号 */
id: number;
/** 表编号 */
tableId: number;
/** 字段名 */
columnName: string;
/** 字段类型 */
dataType: string;
/** 字段描述 */
columnComment: string;
/** 是否允许为空 */
nullable: boolean;
/** 是否主键 */
primaryKey: boolean;
/** 排序 */
ordinalPosition: number;
/** Java 属性类型 */
javaType: string;
/** Java 属性名 */
javaField: string;
/** 字典类型 */
dictType?: string;
/** 数据示例 */
example?: string;
/** 是否为 Create 创建操作的字段 */
createOperation: boolean;
/** 是否为 Update 更新操作的字段 */
updateOperation: boolean;
/** 是否为 List 查询操作的字段 */
listOperation: boolean;
/** List 查询操作的条件类型 */
listOperationCondition: string;
/** 是否为 List 查询操作的返回字段 */
listOperationResult: boolean;
/** 显示类型 */
htmlType: string;
/** 创建时间 */
createTime: string; // 假设为 ISO 字符串格式的 LocalDateTime
}
/**
* / Request VO
*/
export interface CodegenColumnSaveReqVO {
/** 编号 */
id: number;
/** 表编号 */
tableId: number;
/** 字段名 */
columnName: string;
/** 字段类型 */
dataType: string;
/** 字段描述 */
columnComment: string;
/** 是否允许为空 */
nullable: boolean;
/** 是否主键 */
primaryKey: boolean;
/** 排序 */
ordinalPosition: number;
/** Java 属性类型 */
javaType: string;
/** Java 属性名 */
javaField: string;
/** 字典类型 */
dictType?: string;
/** 数据示例 */
example?: string;
/** 是否为 Create 创建操作的字段 */
createOperation: boolean;
/** 是否为 Update 更新操作的字段 */
updateOperation: boolean;
/** 是否为 List 查询操作的字段 */
listOperation: boolean;
/** List 查询操作的条件类型 */
listOperationCondition: string;
/** 是否为 List 查询操作的返回字段 */
listOperationResult: boolean;
/** 显示类型 */
htmlType: string;
}
/**
* Request VO
*/
export interface CodegenTablePageReqVO {
/** 表名称,模糊匹配 */
tableName?: string;
/** 表描述,模糊匹配 */
tableComment?: string;
/** 实体,模糊匹配 */
className?: string;
/** 创建时间 */
createTime?: [string, string]; // 假设为 ISO 字符串格式的 LocalDateTime
}
/**
* Response VO
*/
export interface CodegenTableRespVO {
/** 编号 */
id: number;
/** 生成场景 */
scene: number;
/** 表名称 */
tableName: string;
/** 表描述 */
tableComment: string;
/** 备注 */
remark?: string;
/** 模块名 */
moduleName: string;
/** 业务名 */
businessName: string;
/** 类名称 */
className: string;
/** 类描述 */
classComment: string;
/** 作者 */
author: string;
/** 模板类型 */
templateType: number;
/** 前端类型 */
frontType: number;
/** 父菜单编号 */
parentMenuId?: number;
/** 主表的编号 */
masterTableId?: number;
/** 子表关联主表的字段编号 */
subJoinColumnId?: number;
/** 主表与子表是否一对多 */
subJoinMany?: boolean;
/** 树表的父字段编号 */
treeParentColumnId?: number;
/** 树表的名字字段编号 */
treeNameColumnId?: number;
/** 主键编号 */
dataSourceConfigId: number;
/** 创建时间 */
createTime: string; // 假设为 ISO 字符串格式的 LocalDateTime
/** 更新时间 */
updateTime: string; // 假设为 ISO 字符串格式的 LocalDateTime
}
/**
* / Response VO
*/
export interface CodegenTableSaveReqVO {
/** 编号 */
id: number;
/** 生成场景 */
scene: number;
/** 表名称 */
tableName: string;
/** 表描述 */
tableComment: string;
/** 备注 */
remark?: string;
/** 模块名 */
moduleName: string;
/** 业务名 */
businessName: string;
/** 类名称 */
className: string;
/** 类描述 */
classComment: string;
/** 作者 */
author: string;
/** 模板类型 */
templateType: number;
/** 前端类型 */
frontType: number;
/** 父菜单编号 */
parentMenuId?: number;
/** 主表的编号 */
masterTableId?: number;
/** 子表关联主表的字段编号 */
subJoinColumnId?: number;
/** 主表与子表是否一对多 */
subJoinMany?: boolean;
/** 树表的父字段编号 */
treeParentColumnId?: number;
/** 树表的名字字段编号 */
treeNameColumnId?: number;
}
/**
* Response VO
*/
export interface DatabaseTableRespVO {
/** 表名称 */
name: string;
/** 表描述 */
comment: string;
}
/**
* Request VO
*/
export interface CodegenCreateListReqVO {
/** 数据源配置的编号 */
dataSourceConfigId: number;
/** 表名数组 */
tableNames: string[];
}
/**
* Response VO
*/
export interface CodegenDetailRespVO {
/** 表定义 */
table: CodegenTableRespVO;
/** 字段定义 */
columns: CodegenColumnRespVO[];
}
/**
* Response VO
*/
export interface CodegenPreviewRespVO {
/** 文件路径 */
filePath: string;
/** 代码 */
code: string;
}
/**
* Request VO
*/
export interface CodegenUpdateReqVO {
/** 表定义 */
table: CodegenTableSaveReqVO;
/** 字段定义 */
columns: CodegenColumnSaveReqVO[];
}
}
// 查询列表代码生成表定义
export const getCodegenTableList = (dataSourceConfigId: number) => {
return requestClient.get<CodegenApi.CodegenTableRespVO[]>(
`/infra/codegen/table/list?dataSourceConfigId=${dataSourceConfigId}`,
);
};
// 查询列表代码生成表定义
export const getCodegenTablePage = (params: PageParam) => {
return requestClient.get<CodegenApi.CodegenTableRespVO[]>(
'/infra/codegen/table/page',
{ params },
);
};
// 查询详情代码生成表定义
export const getCodegenTable = (id: number) => {
return requestClient.get<CodegenApi.CodegenDetailRespVO>(
`/infra/codegen/detail?tableId=${id}`,
);
};
// 新增代码生成表定义
export const createCodegenTable = (data: CodegenApi.CodegenCreateListReqVO) => {
return requestClient.post('/infra/codegen/create', data);
};
// 修改代码生成表定义
export const updateCodegenTable = (data: CodegenApi.CodegenUpdateReqVO) => {
return requestClient.put('/infra/codegen/update', data);
};
// 基于数据库的表结构,同步数据库的表和字段定义
export const syncCodegenFromDB = (id: number) => {
return requestClient.put(`/infra/codegen/sync-from-db?tableId=${id}`);
};
// 预览生成代码
export const previewCodegen = (id: number) => {
return requestClient.get(`/infra/codegen/preview?tableId=${id}`);
};
// 下载生成代码
export const downloadCodegen = (id: number) => {
return requestClient.download(`/infra/codegen/download?tableId=${id}`);
};
// 获得表定义
export const getSchemaTableList = (params: {
comment?: string;
dataSourceConfigId: number;
name?: string;
}) => {
return requestClient.get<CodegenApi.DatabaseTableRespVO[]>(
'/infra/codegen/db/table/list',
{ params },
);
};
// 基于数据库的表结构,创建代码生成器的表定义
export const createCodegenList = (data: CodegenApi.CodegenCreateListReqVO) => {
return requestClient.post('/infra/codegen/create-list', data);
};
// 删除代码生成表定义
export const deleteCodegenTable = (id: number) => {
return requestClient.delete(`/infra/codegen/delete?tableId=${id}`);
};

View File

@ -0,0 +1,51 @@
import { requestClient } from '#/api/request';
export namespace DataSourceConfigApi {
export interface DataSourceConfigRespVO {
id: number;
name: string;
url: string;
username: string;
createTime?: Date;
}
export interface DataSourceConfigSaveReqVO {
id?: number;
name: string;
url: string;
username: string;
password: string;
}
}
// 新增数据源配置
export const createDataSourceConfig = (
data: DataSourceConfigApi.DataSourceConfigSaveReqVO,
) => {
return requestClient.post('/infra/data-source-config/create', data);
};
// 修改数据源配置
export const updateDataSourceConfig = (
data: DataSourceConfigApi.DataSourceConfigSaveReqVO,
) => {
return requestClient.put('/infra/data-source-config/update', data);
};
// 删除数据源配置
export const deleteDataSourceConfig = (id: number) => {
return requestClient.delete(`/infra/data-source-config/delete?id=${id}`);
};
// 查询数据源配置详情
export const getDataSourceConfig = (id: number) => {
return requestClient.get<DataSourceConfigApi.DataSourceConfigRespVO>(
`/infra/data-source-config/get?id=${id}`,
);
};
// 查询数据源配置列表
export const getDataSourceConfigList = () => {
return requestClient.get<DataSourceConfigApi.DataSourceConfigRespVO[]>(
'/infra/data-source-config/list',
);
};

View File

@ -10,19 +10,15 @@ import {
errorMessageResponseInterceptor, errorMessageResponseInterceptor,
RequestClient, RequestClient,
} from '@vben/request'; } from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore, useTenantStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { getTenantId } from '#/utils';
import { refreshTokenApi } from './core'; import { refreshTokenApi } from './core';
const { apiURL, tenantEnable } = useAppConfig( const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
import.meta.env,
import.meta.env.PROD,
);
function createRequestClient(baseURL: string) { function createRequestClient(baseURL: string) {
const client = new RequestClient({ const client = new RequestClient({
@ -52,9 +48,12 @@ function createRequestClient(baseURL: string) {
*/ */
async function doRefreshToken() { async function doRefreshToken() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const resp = await refreshTokenApi(); const resp = await refreshTokenApi(accessStore.refreshToken ?? '');
const newToken = resp.refreshToken; const newToken = resp.accessToken;
const newRefreshToken = resp.refreshToken;
accessStore.setAccessToken(newToken); accessStore.setAccessToken(newToken);
accessStore.setRefreshToken(newRefreshToken);
return newToken; return newToken;
} }
@ -66,11 +65,11 @@ function createRequestClient(baseURL: string) {
client.addRequestInterceptor({ client.addRequestInterceptor({
fulfilled: async (config) => { fulfilled: async (config) => {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const tenantId = getTenantId(); const tenantStore = useTenantStore();
config.headers.Authorization = formatToken(accessStore.accessToken); config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale; config.headers['Accept-Language'] = preferences.app.locale;
config.headers['tenant-id'] = config.headers['tenant-id'] = tenantStore.tenantId ?? undefined;
tenantEnable && tenantId ? tenantId : undefined;
return config; return config;
}, },
}); });
@ -78,32 +77,13 @@ function createRequestClient(baseURL: string) {
// response数据解构 // response数据解构
client.addResponseInterceptor<HttpResponse>({ client.addResponseInterceptor<HttpResponse>({
fulfilled: (response) => { fulfilled: (response) => {
// const { config, data: responseData, status, request } = response; const { data: responseData, status } = response;
const { data: responseData, request } = response;
// 这个判断的目的是excel 导出等情况下,系统执行异常,此时返回的是 json而不是二进制数据
if (
(request.responseType === 'blob' ||
request.responseType === 'arraybuffer') &&
responseData?.code === undefined
) {
return responseData;
}
const { code, data: result } = responseData; const { code, data } = responseData;
if (responseData && Reflect.has(responseData, 'code') && code === 0) { if (status >= 200 && status < 400 && code === 0) {
return result; return data;
} }
switch (code) {
case 401: {
response.status = 401;
throw Object.assign({}, response, { response }); throw Object.assign({}, response, { response });
}
default: {
response.status = code;
throw Object.assign({}, response, { response });
}
}
}, },
}); });
@ -124,7 +104,8 @@ function createRequestClient(baseURL: string) {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message // 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {}; const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? ''; const errorMessage =
responseData?.error ?? responseData?.message ?? responseData.msg ?? '';
// 如果没有错误信息,则会根据状态码进行提示 // 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg); message.error(errorMessage || msg);
}), }),
@ -133,11 +114,8 @@ function createRequestClient(baseURL: string) {
return client; return client;
} }
export type PageParam = {
pageNo?: number;
pageSize?: number;
};
export const requestClient = createRequestClient(apiURL); export const requestClient = createRequestClient(apiURL);
export const baseRequestClient = new RequestClient({ baseURL: apiURL }); export const baseRequestClient = new RequestClient({ baseURL: apiURL });
export type * from '@vben/request';

View File

@ -0,0 +1,89 @@
import { type PageParam, requestClient } from '#/api/request';
export namespace DictDataApi {
/**
* Response VO
*/
export type DictDataRespVO = {
colorType?: string;
createTime?: Date;
cssClass?: string;
dictType: string;
id?: number;
label: string;
remark?: string;
sort?: number;
status?: number;
value: string;
};
/**
* Request VO
*/
export interface DictDataPageReqVO extends PageParam {
dictType?: string;
label?: string;
status?: number;
}
/**
* / Request VO
*/
export interface DictDataSaveReqVO {
colorType?: string;
cssClass?: string;
dictType: string;
id?: number;
label: string;
remark?: string;
sort?: number;
status?: number;
value: string;
}
/**
* ) Response VO
*/
export interface DictDataSimpleRespVO {
colorType?: string;
cssClass?: string;
dictType: string;
label: string;
value: string;
}
}
// 查询字典数据(精简)列表
export const getSimpleDictDataList = () => {
return requestClient.get('/system/dict-data/simple-list');
};
// 查询字典数据列表
export const getDictDataPage = (params: PageParam) => {
return requestClient.get('/system/dict-data/page', { params });
};
// 查询字典数据详情
export const getDictData = (id: number) => {
return requestClient.get(`/system/dict-data/get?id=${id}`);
};
// 新增字典数据
export const createDictData = (data: DictDataApi.DictDataSaveReqVO) => {
return requestClient.post('/system/dict-data/create', data);
};
// 修改字典数据
export const updateDictData = (data: DictDataApi.DictDataSaveReqVO) => {
return requestClient.put('/system/dict-data/update', data);
};
// 删除字典数据
export const deleteDictData = (id: number) => {
return requestClient.delete(`/system/dict-data/delete?id=${id}`);
};
// 导出字典类型数据
export const exportDictData = (params: DictDataApi.DictDataPageReqVO) => {
return requestClient.download('/system/dict-data/export', { params });
};

View File

@ -0,0 +1 @@
export namespace DictTypeApi {}

View File

@ -1,49 +0,0 @@
import { requestClient } from '#/api/request';
export type DictDataVO = {
colorType: string;
createTime: Date;
cssClass: string;
dictType: string;
id: number | undefined;
label: string;
remark: string;
sort: number | undefined;
status: number;
value: string;
};
// 查询字典数据(精简)列表
export function getSimpleDictDataList() {
return requestClient.get('/system/dict-data/simple-list');
}
// 查询字典数据列表
export function getDictDataPage(params: any) {
return requestClient.get('/system/dict-data/page', params);
}
// 查询字典数据详情
export function getDictData(id: number) {
return requestClient.get(`/system/dict-data/get?id=${id}`);
}
// 新增字典数据
export function createDictData(data: DictDataVO) {
return requestClient.post('/system/dict-data/create', data);
}
// 修改字典数据
export function updateDictData(data: DictDataVO) {
return requestClient.put('/system/dict-data/update', data);
}
// 删除字典数据
export function deleteDictData(id: number) {
return requestClient.delete(`/system/dict-data/delete?id=${id}`);
}
// 导出字典类型数据
export function exportDictData(params: any) {
return requestClient.download('/system/dict-data/export', params);
}

View File

@ -1,44 +0,0 @@
import { requestClient } from '#/api/request';
export type DictTypeVO = {
createTime: Date;
id: number | undefined;
name: string;
remark: string;
status: number;
type: string;
};
// 查询字典(精简)列表
export function getSimpleDictTypeList() {
return requestClient.get('/system/dict-type/list-all-simple');
}
// 查询字典列表
export function getDictTypePage(params: any) {
return requestClient.get('/system/dict-type/page', params);
}
// 查询字典详情
export function getDictType(id: number) {
return requestClient.get(`/system/dict-type/get?id=${id}`);
}
// 新增字典
export function createDictType(data: DictTypeVO) {
return requestClient.post('/system/dict-type/create', data);
}
// 修改字典
export function updateDictType(data: DictTypeVO) {
return requestClient.put('/system/dict-type/update', data);
}
// 删除字典
export function deleteDictType(id: number) {
return requestClient.delete(`/system/dict-type/delete?id=${id}`);
}
// 导出字典类型
export function exportDictType(params: any) {
return requestClient.download('/system/dict-type/export', params);
}

View File

@ -1 +0,0 @@
export { default as Verify } from './src/Verify.vue';

View File

@ -1,162 +0,0 @@
<script type="text/babel">
/**
* Verify 验证码组件
* @description 分发验证码使用
*/
import { computed, ref, toRefs, watchEffect } from 'vue';
// import { $t } from '@vben/locales';
import VerifyPoints from './Verify/VerifyPoints.vue';
import VerifySlide from './Verify/VerifySlide.vue';
import './style/verify.css';
export default {
name: 'Vue3Verify',
components: {
VerifyPoints,
VerifySlide,
},
props: {
arith: {
default: 0,
type: Number,
},
barSize: {
default: () => {
return {
height: '40px',
width: '310px',
};
},
type: Object,
},
blockSize: {
default() {
return {
height: '50px',
width: '50px',
};
},
type: Object,
},
captchaType: {
required: true,
type: String,
},
explain: {
default: '',
type: String,
},
figure: {
default: 0,
type: Number,
},
imgSize: {
default() {
return {
height: '155px',
width: '310px',
};
},
type: Object,
},
mode: {
default: 'pop',
type: String,
},
vSpace: {
default: 5,
type: Number,
},
},
setup(props) {
const { captchaType, mode } = toRefs(props);
const clickShow = ref(false);
const verifyType = ref(undefined);
const componentType = ref(undefined);
const instance = ref({});
const showBox = computed(() => {
return mode.value === 'pop' ? clickShow.value : true;
});
/**
* refresh
* @description 刷新
*/
const refresh = () => {
if (instance.value.refresh) instance.value.refresh();
};
const closeBox = () => {
clickShow.value = false;
refresh();
};
const show = () => {
if (mode.value === 'pop') clickShow.value = true;
};
watchEffect(() => {
switch (captchaType.value) {
case 'blockPuzzle': {
verifyType.value = '2';
componentType.value = 'VerifySlide';
break;
}
case 'clickWord': {
verifyType.value = '';
componentType.value = 'VerifyPoints';
break;
}
}
});
return {
clickShow,
closeBox,
componentType,
instance,
show,
showBox,
verifyType,
};
},
};
</script>
<template>
<div v-show="showBox" :class="mode === 'pop' ? 'mask' : ''">
<div
:class="mode === 'pop' ? 'verifybox' : ''"
:style="{ 'max-width': `${parseInt(imgSize.width) + 20}px` }"
>
<div v-if="mode === 'pop'" class="verifybox-top">
请完成安全验证
<span class="verifybox-close" @click="closeBox">
<i class="iconfont icon-close"></i>
</span>
</div>
<div
:style="{ padding: mode === 'pop' ? '10px' : '0' }"
class="verifybox-bottom"
>
<!-- 验证码容器 -->
<component
:is="componentType"
v-if="componentType"
ref="instance"
:arith="arith"
:bar-size="barSize"
:block-size="blockSize"
:captcha-type="captchaType"
:explain="explain"
:figure="figure"
:img-size="imgSize"
:mode="mode"
:type="verifyType"
:v-space="vSpace"
/>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import type { DescItem } from './types';
import type { PropType } from 'vue';
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
import { componentMap } from '#/components/view/component-map';
defineProps({
title: { type: String, default: '' },
bordered: { type: Boolean, default: true },
size: {
type: String as PropType<'default' | 'middle' | 'small'>,
default: undefined,
},
column: {
type: [Number, Object],
default: () => {
// return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
return 12;
},
},
labelStyle: {
type: Object,
default() {
return {
width: '120px',
};
},
},
contentStyle: {
type: Object,
default() {
return {
width: '0px',
};
},
},
schema: {
type: Array as PropType<DescItem[]>,
default: () => [],
},
data: { type: Object, default: undefined },
});
</script>
<template>
<Descriptions
:bordered="bordered"
:column="column"
:content-style="contentStyle"
:label-style="labelStyle"
:size="size"
:title="title ? title : undefined"
>
<template v-for="item in schema" :key="item.field">
<DescriptionsItem :label="item.label" :span="item.span">
<component
:is="(componentMap as Map<String, any>).get(item.component)"
v-if="(componentMap as Map<String, any>).has(item.component)"
:value="data?.[item.field]"
v-bind="{ ...item.componentProps }"
/>
<component
:is="item.render(data?.[item.field], data)"
v-else-if="
!(componentMap as Map<String, any>).has(item.component) &&
item.render
"
:value="data?.[item.field]"
v-bind="{ ...item.componentProps }"
/>
<template v-else>{{ data?.[item.field] }}</template>
</DescriptionsItem>
</template>
</Descriptions>
</template>

View File

@ -0,0 +1,2 @@
export { default as Description } from './description.vue';
export type * from './types';

View File

@ -0,0 +1,54 @@
import type { CollapseContainerOptions } from '@/components/Container';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { CSSProperties, VNode } from 'vue';
export interface DescItem {
labelMinWidth?: number;
contentMinWidth?: number;
labelStyle?: CSSProperties;
field: string;
label: JSX.Element | string | VNode;
// Merge column
span?: number;
show?: (...arg: any) => boolean;
// render
render?: (
val: any,
data: Recordable,
) => Element | JSX.Element | number | string | undefined | VNode;
component: string;
componentProps?: any;
children?: DescItem[];
}
export interface DescriptionProps extends DescriptionsProps {
// Whether to include the collapse component
useCollapse?: boolean;
/**
* item configuration
* @type DescItem
*/
schema: DescItem[];
/**
*
* @type object
*/
data: Recordable;
/**
* Built-in CollapseContainer component configuration
* @type CollapseContainerOptions
*/
collapseOptions?: CollapseContainerOptions;
}
export interface DescInstance {
setDescProps(descProps: Partial<DescriptionProps>): void;
}
export type Register = (descInstance: DescInstance) => void;
/**
* @description:
*/
export type UseDescReturnType = [Register, DescInstance];

View File

@ -0,0 +1,37 @@
import type { CustomComponentType } from './types';
import type { Component } from 'vue';
import { kebabToCamelCase } from '@vben/utils';
const componentMap = new Map<CustomComponentType | string, Component>();
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob('./components/**/*.vue', { eager: true });
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
if (!key.includes('-ignore')) {
const mod = (modules as any)[key].default || {};
// ./components/ApiDict.vue
// 获取ApiDict
const compName = key.replace('./components/', '').replace('.vue', '');
componentMap.set(kebabToCamelCase(compName), mod);
}
});
export function add(compName: string, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: string) {
componentMap.delete(compName);
}
/**
*
* @param components
*/
export const registerComponent = (components: any) => {
componentMap.forEach((value, key) => {
components[key] = value as Component;
});
};
export { componentMap };

View File

@ -0,0 +1,137 @@
<script setup lang="ts">
import type { CheckboxValueType } from 'ant-design-vue/es/checkbox/interface';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { CheckboxGroup, Spin } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [Array] as PropType<CheckboxValueType[]>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
requestMethod: {
//
type: String,
default: 'post',
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
</script>
<template>
<Spin :spinning="loading" style="margin-left: 20px">
<CheckboxGroup
v-bind="$attrs"
v-model:value="mValue"
:options="getOptions"
class="w-full"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</CheckboxGroup>
</Spin>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed, type PropType } from 'vue';
import { requestClient } from '#/api/request';
import { ApiCheckboxGroup, ApiRadioGroup, ApiSelect } from '..';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
renderType: {
type: String as PropType<'CheckboxGroup' | 'RadioGroup' | 'Select'>,
default: 'Select',
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
code: {
type: String,
default: undefined,
},
});
const DictComponent = computed(() => {
if (props.renderType === 'RadioGroup') {
return ApiRadioGroup;
} else if (props.renderType === 'CheckboxGroup') {
return ApiCheckboxGroup;
}
return ApiSelect;
});
const fetch = () => {
return requestClient.post('/sys/dict/getByDictType', {
dictType: props.code,
...props.params,
});
};
</script>
<template>
<DictComponent :api="props.code ? fetch : props.api" />
</template>

View File

@ -0,0 +1,143 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { RadioGroup, Spin } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
isBtn: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
</script>
<template>
<Spin :spinning="loading" style="margin-left: 20px">
<RadioGroup
v-bind="$attrs"
v-model:value="mValue"
:button-style="isBtn ? 'solid' : 'outline'"
:option-type="isBtn ? 'button' : 'default'"
:options="getOptions"
class="w-full"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</RadioGroup>
</Spin>
</template>

View File

@ -0,0 +1,150 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getNestedValue, isFunction } from '@vben/utils';
import { objectOmit, useVModel } from '@vueuse/core';
import { Select } from 'ant-design-vue';
import { requestClient } from '#/api/request';
type OptionsItem = { disabled?: boolean; label: string; value: string };
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
numberToString: {
type: Boolean,
default: false,
},
api: {
type: [Function, String] as PropType<
(arg?: any) => Promise<OptionsItem[]> | String
>,
default: null,
},
requestMethod: {
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'optionsChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const res: OptionsItem[] = [];
options.value.forEach((item: any) => {
const value = item[valueField];
res.push({
...objectOmit(item, [labelField, valueField]),
label: item[labelField],
value: numberToString ? `${value}` : value,
disabled: item.disabled || false,
});
});
return res;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
options.value = res;
emits('optionsChange', options.value);
} else {
options.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('optionsChange', options.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
async function handleFetch() {
if (!props.immediate && isFirstLoad.value) {
await fetch();
isFirstLoad.value = false;
}
}
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
</script>
<template>
<Select
v-model:value="mValue"
:options="getOptions"
class="w-full"
@dropdown-visible-change="handleFetch"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loading" #suffixIcon>
<IconifyIcon icon="ant-design:loading-outlined" spin />
</template>
<template v-if="loading" #notFoundContent>
<span>
<IconifyIcon icon="ant-design:loading-outlined" spin />
请等待数据加载完成
</span>
</template>
</Select>
</template>

View File

@ -0,0 +1,140 @@
<script setup lang="ts">
import type { SelectValue } from 'ant-design-vue/es/select';
import { computed, type PropType, ref, watch, watchEffect } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getNestedValue, isFunction } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { TreeSelect } from 'ant-design-vue';
import { requestClient } from '#/api/request';
const props = defineProps({
value: {
type: [String, Number, Array] as PropType<SelectValue>,
default: undefined,
},
api: {
type: [Function, String] as PropType<(arg?: any) => Promise<any> | String>,
default: null,
},
requestMethod: {
//
type: String,
default: 'post',
},
// api params
params: {
type: Object as PropType<any>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: {
type: String,
default: '',
},
labelField: {
type: String,
default: 'title',
},
valueField: {
type: String,
default: 'value',
},
childrenField: {
type: String,
default: 'children',
},
immediate: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['update:value', 'treeDataChange']);
const mValue = useVModel(props, 'value', emits, {
defaultValue: props.value,
passive: true,
});
const treeData = ref<any>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const fieldNames = computed(() => {
return {
label: props.labelField,
value: props.valueField,
children: props.childrenField,
};
});
const getTreeData = computed(() => {
return treeData.value;
});
const fetch = async () => {
const api: any =
typeof props.api === 'string' && props.api
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const params =
props.requestMethod === 'get' ? { params: props.params } : props.params;
const res = await api(params);
if (Array.isArray(res)) {
treeData.value = res;
emits('treeDataChange', treeData.value);
} else {
treeData.value = props.resultField
? getNestedValue(res, props.resultField)
: [];
emits('treeDataChange', treeData.value);
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
};
async function handleFetch() {
if (!props.immediate && isFirstLoad.value) {
await fetch();
isFirstLoad.value = false;
}
}
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!isFirstLoad.value && fetch();
},
{ deep: true },
);
</script>
<template>
<TreeSelect
v-model:value="mValue"
:field-names="fieldNames"
:tree-data="getTreeData"
:tree-node-filter-prop="labelField"
class="w-full"
@dropdown-visible-change="handleFetch"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loading" #suffixIcon>
<IconifyIcon icon="ant-design:loading-outlined" spin />
</template>
</TreeSelect>
</template>

View File

@ -0,0 +1,5 @@
export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue';
export { default as ApiDict } from './components/api-dict.vue';
export { default as ApiRadioGroup } from './components/api-radio-group.vue';
export { default as ApiSelect } from './components/api-select.vue';
export { default as ApiTreeSelect } from './components/api-tree-select.vue';

View File

@ -0,0 +1,6 @@
export type CustomComponentType =
| 'ApiCheckboxGroup'
| 'ApiDict'
| 'ApiRadioGroup'
| 'ApiSelect'
| 'ApiTreeSelect';

View File

@ -0,0 +1,27 @@
import type { Component } from 'vue';
import { toPascalCase } from '#/util/tool';
const componentMap = new Map<string, Component>();
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob('./components/**/*.vue', { eager: true });
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
if (!key.includes('-ignore')) {
const mod = (modules as any)[key].default || {};
// ./components/ApiDict.vue
// 获取ApiDict
const compName = key.replace('./components/', '').replace('.vue', '');
componentMap.set(toPascalCase(compName), mod);
}
});
export function add(compName: string, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: string) {
componentMap.delete(compName);
}
export { componentMap };

View File

@ -0,0 +1,6 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed } from 'vue';
import { type DictItem, useDictStore } from '@vben/stores';
const props = defineProps({
code: {
type: String,
default: undefined,
},
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
split: {
//
type: String,
default: ',',
},
join: {
//
type: String,
default: ',',
},
});
const dictStore = useDictStore();
const cValue = computed(() => {
if (!props.value && props.value !== 0) {
return '';
}
const arr: Array<any> = [];
if (Array.isArray(props.value)) {
arr.push(...props.value);
} else {
arr.push(...props.value.toString().split(props.split));
}
const dictData = dictStore.getDictData(props.code as string) as DictItem[];
const res: Array<any> = [];
arr.forEach((item) => {
for (let i = 0; i < dictData.length; i++) {
if (dictData[i]?.value?.toString() === item?.toString()) {
res.push(dictData[i]?.label);
break;
}
if (i === dictData.length - 1) {
res.push(item);
}
}
});
return res.join(props.join);
});
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -0,0 +1,6 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

View File

@ -0,0 +1,154 @@
<script setup lang="ts">
import { computed, onMounted, type PropType, watch } from 'vue';
import { type DictItem, useDictStore } from '@vben/stores';
import { requestClient } from '#/api/request';
const props = defineProps({
code: {
type: String,
default: undefined,
},
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
split: {
//
type: String,
default: ',',
},
join: {
//
type: String,
default: ',',
},
api: {
//
type: [Function, String] as PropType<
((...arg: any) => Promise<any>) | String
>,
default() {
return () => {
return new Promise((resolve) => {
resolve([]);
});
};
},
},
params: {
type: Object,
default() {
return {};
},
},
cacheKey: {
type: String,
default: '',
},
requestMethod: {
type: String,
default: 'post',
},
});
const dictStore = useDictStore();
/**
* 获取包含的id
*/
const getIncludeIds = () => {
if (!props.value && props.value !== 0) {
return [];
}
const arr: Array<any> = [];
if (Array.isArray(props.value)) {
arr.push(...props.value);
} else {
arr.push(...props.value.toString().split(props.split));
}
return arr;
};
/**
* 获取缓存key
*/
const getCacheKey = () => {
let cacheKey = props.cacheKey;
if (typeof props.api === 'string' && !cacheKey) {
cacheKey = props.api as string;
}
return cacheKey;
};
const cValue = computed(() => {
if (!props.value && props.value !== 0) {
return '';
}
const arr: Array<any> = getIncludeIds();
const cacheKey = getCacheKey();
const dictData = dictStore.getDictData(cacheKey) as DictItem[];
const res: Array<any> = [];
arr.forEach((item) => {
for (let i = 0; i < dictData.length; i++) {
if (dictData[i]?.value?.toString() === item?.toString()) {
res.push(dictData[i]?.label);
break;
}
if (i === dictData.length - 1) {
res.push(item);
}
}
});
return res.join(props.join);
});
const requestData = () => {
const api: (...arg: any) => Promise<any> =
typeof props.api === 'string'
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: (props.api as (...arg: any) => Promise<any>);
const cacheKey = getCacheKey();
const params =
props.requestMethod === 'get'
? {
params: {
...props.params,
dictType: cacheKey,
includeType: 2,
includeIds: getIncludeIds(),
},
}
: {
data: {
...props.params,
dictType: cacheKey,
includeType: 2,
includeIds: getIncludeIds(),
},
};
dictStore.setDictCacheByApi(api, params);
};
onMounted(() => {
requestData();
});
watch(
() => props.value,
() => {
requestData();
},
);
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import { computed, onMounted, type PropType, ref } from 'vue';
import { requestClient } from '#/api/request';
const props = defineProps({
data: {
type: Object,
default() {
return {};
},
},
value: {
//
type: [String, Number, Array],
default: undefined,
},
api: {
//
type: [Function, String] as PropType<
((...arg: any) => Promise<any>) | String
>,
default() {
return () => {
return new Promise((resolve) => {
resolve([]);
});
};
},
},
params: {
type: Object,
default() {
return {};
},
},
cacheKey: {
type: String,
default: '',
},
requestMethod: {
type: String,
default: 'post',
},
valueField: {
type: String,
default: 'id',
},
labelField: {
type: String,
default: 'name',
},
multiple: {
type: Boolean,
default: false,
},
});
const currentData = ref({});
const cValue = computed(() => {
return (currentData.value as any)[props.labelField] || props.value;
});
onMounted(() => {
const api: (...arg: any) => Promise<any> =
typeof props.api === 'string'
? (params: any) => {
return (requestClient as any)[props.requestMethod](
props.api as any,
params,
);
}
: (props.api as (...arg: any) => Promise<any>);
const searchType = props.multiple ? 'IN' : 'EQ';
const params =
props.requestMethod === 'get'
? {
params: {
...props.params,
[`m_${searchType}_${props.valueField}`]: props.value,
},
}
: {
...props.params,
[`m_${searchType}_${props.valueField}`]: props.value,
};
api(params).then((res) => {
if (res.length > 0) {
currentData.value = res[0];
}
});
});
</script>
<template>
<div>{{ cValue }}</div>
</template>
<style lang="less" scoped></style>

View File

@ -101,7 +101,7 @@ const menus = computed(() => [
]); ]);
const avatar = computed(() => { const avatar = computed(() => {
return userStore.userInfo?.user.avatar ?? preferences.app.defaultAvatar; return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
}); });
async function handleLogout() { async function handleLogout() {
@ -138,7 +138,7 @@ watch(
<UserDropdown <UserDropdown
:avatar :avatar
:menus :menus
:text="userStore.userInfo?.user.nickname" :text="userStore.userInfo?.nickname"
tag-text="Admin" tag-text="Admin"
@logout="handleLogout" @logout="handleLogout"
/> />

View File

@ -8,9 +8,9 @@ import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({ export const overridesPreferences = defineOverridesPreferences({
// overrides // overrides
app: { app: {
name: import.meta.env.VITE_APP_TITLE,
/** 后端路由模式 */ /** 后端路由模式 */
accessMode: 'backend', accessMode: 'backend',
name: import.meta.env.VITE_APP_TITLE,
enableRefreshToken: true, enableRefreshToken: true,
}, },
}); });

View File

@ -1,16 +1,14 @@
import type { import type {
ComponentRecordType, ComponentRecordType,
GenerateMenuAndRoutesOptions, GenerateMenuAndRoutesOptions,
RouteRecordStringComponent,
} from '@vben/types'; } from '@vben/types';
import { generateAccessible } from '@vben/access'; import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import { cloneDeep } from '@vben/utils';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { getAuthPermissionInfoApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts'; import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales'; import { $t } from '#/locales';
@ -18,75 +16,6 @@ import { buildMenus } from './helper';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
/**
* base
*/
const baseMenus: RouteRecordStringComponent[] = [
{
component: 'BasicLayout',
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: 'page.dashboard.analytics',
},
},
{
name: 'Workspace',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
},
},
{
name: 'VbenAbout',
path: '/about',
component: '/_core/about/index.vue',
meta: {
icon: 'lucide:copyright',
title: 'demos.vben.about',
},
},
],
},
{
component: 'BasicLayout',
meta: {
icon: 'ant-design:user-outlined',
order: -1,
title: '个人中心',
hideInMenu: true,
},
name: 'profile',
path: '/profile',
children: [
{
name: 'UserProfile',
path: '/profile/index',
component: '/_core/profile/profile.vue',
meta: {
icon: 'ant-design:user-outlined',
title: '个人中心',
hideInMenu: true,
},
},
],
},
];
async function generateAccess(options: GenerateMenuAndRoutesOptions) { async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
@ -102,10 +31,10 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
content: `${$t('common.loadingMenu')}...`, content: `${$t('common.loadingMenu')}...`,
duration: 1.5, duration: 1.5,
}); });
const userStore = useUserStore(); const authPermissionInfo = await getAuthPermissionInfoApi();
const menus = userStore.userInfo?.menus; const menus = authPermissionInfo.menus;
const routes = buildMenus(menus); const routes = buildMenus(menus);
const menuList = [...cloneDeep(baseMenus), ...routes]; const menuList = [...routes];
return menuList; return menuList;
}, },
// 可以指定没有权限跳转403页面 // 可以指定没有权限跳转403页面

View File

@ -87,9 +87,11 @@ function setupAccessGuard(router: Router) {
// 生成路由表 // 生成路由表
// 当前登录用户拥有的角色标识列表 // 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); let userRoles = userStore.userRoles;
const userRoles = userInfo.roles ?? []; if (!userRoles) {
const authPermissionInfo = await authStore.getAuthPermissionInfo();
userRoles = authPermissionInfo?.roles ?? [];
}
// 生成菜单和路由 // 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({ const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles, roles: userRoles,

View File

@ -1,6 +1,7 @@
import type { RouteRecordStringComponent } from '@vben/types'; import type {
AppRouteRecordRaw,
import type { AppRouteRecordRaw } from '#/types'; RouteRecordStringComponent,
} from '@vben/types';
import { isHttpUrl } from '@vben/utils'; import { isHttpUrl } from '@vben/utils';

View File

@ -1,6 +1,4 @@
import type { Recordable } from '@vben/types'; import type { AuthPermissionInfo, Recordable } from '@vben/types';
import type { YudaoUserInfo } from '#/types';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -11,16 +9,12 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getUserInfo, loginApi, logoutApi } from '#/api'; import { getAuthPermissionInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { setAccessToken, setRefreshToken } from '#/utils';
import { useDictStore } from './dict';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const userStore = useUserStore(); const userStore = useUserStore();
const dictStore = useDictStore();
const router = useRouter(); const router = useRouter();
const loginLoading = ref(false); const loginLoading = ref(false);
@ -35,52 +29,45 @@ export const useAuthStore = defineStore('auth', () => {
onSuccess?: () => Promise<void> | void, onSuccess?: () => Promise<void> | void,
) { ) {
// 异步处理用户登录操作并获取 accessToken // 异步处理用户登录操作并获取 accessToken
let userInfo: null | YudaoUserInfo = null; let authPermissionInfo: AuthPermissionInfo | null = null;
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken, expiresTime, refreshToken } = await loginApi(params); const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken); accessStore.setRefreshToken(refreshToken);
setAccessToken(accessToken, expiresTime);
setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中 authPermissionInfo = await getAuthPermissionInfo();
const fetchUserInfoResult = await fetchUserInfo();
userInfo = fetchUserInfoResult;
if (userInfo) {
if (userInfo.roles) {
userStore.setUserRoles(userInfo.roles);
}
// userStore.setMenus(userInfo.menus);
accessStore.setAccessCodes(userInfo.permissions);
if (accessStore.loginExpired) { if (accessStore.loginExpired) {
accessStore.setLoginExpired(false); accessStore.setLoginExpired(false);
} else { } else {
onSuccess // 执行成功回调
? await onSuccess?.() await onSuccess?.();
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH); // 跳转首页
await router.push(authPermissionInfo.homePath || DEFAULT_HOME_PATH);
} }
dictStore.setDictMap(); if (
authPermissionInfo?.user.realName ||
if (userInfo?.realName) { authPermissionInfo.user.nickname
) {
notification.success({ notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, description: `${$t('authentication.loginSuccessDesc')}:${authPermissionInfo?.user.realName ?? authPermissionInfo?.user.nickname}`,
duration: 3, duration: 3,
message: $t('authentication.loginSuccess'), message: $t('authentication.loginSuccess'),
}); });
} }
} }
}
} finally { } finally {
loginLoading.value = false; loginLoading.value = false;
} }
return { return {
userInfo, authPermissionInfo,
}; };
} }
@ -104,11 +91,13 @@ export const useAuthStore = defineStore('auth', () => {
}); });
} }
async function fetchUserInfo() { async function getAuthPermissionInfo() {
let userInfo: null | YudaoUserInfo = null; let authPermissionInfo: AuthPermissionInfo | null = null;
userInfo = await getUserInfo(); authPermissionInfo = await getAuthPermissionInfoApi();
userStore.setUserInfo(userInfo); userStore.setUserInfo(authPermissionInfo.user);
return userInfo; userStore.setUserRoles(authPermissionInfo.roles);
accessStore.setAccessCodes(authPermissionInfo.permissions);
return authPermissionInfo;
} }
function $reset() { function $reset() {
@ -118,7 +107,7 @@ export const useAuthStore = defineStore('auth', () => {
return { return {
$reset, $reset,
authLogin, authLogin,
fetchUserInfo, getAuthPermissionInfo,
loginLoading, loginLoading,
logout, logout,
}; };

View File

@ -1,83 +0,0 @@
import { StorageManager } from '@vben/utils';
import { acceptHMRUpdate, defineStore } from 'pinia';
import { getSimpleDictDataList } from '#/api/system/dict/dict.data';
const DICT_STORAGE_KEY = 'DICT_STORAGE__';
interface DictValueType {
value: any;
label: string;
colorType?: string;
cssClass?: string;
}
// interface DictTypeType {
// dictType: string;
// dictValue: DictValueType[];
// }
interface DictState {
dictMap: Map<string, DictValueType[]>;
isSetDict: boolean;
}
const storage = new StorageManager({
prefix: import.meta.env.VITE_APP_NAMESPACE,
storageType: 'sessionStorage',
});
export const useDictStore = defineStore('dict', {
actions: {
async setDictMap() {
try {
const dataRes = await getSimpleDictDataList();
const dictDataMap = new Map<string, DictValueType[]>();
dataRes.forEach((item: any) => {
let dictTypeArray = dictDataMap.get(item.dictType);
if (!dictTypeArray) {
dictTypeArray = [];
}
dictTypeArray.push({
value: item.value,
label: item.label,
colorType: item.colorType,
cssClass: item.cssClass,
});
dictDataMap.set(item.dictType, dictTypeArray);
});
this.dictMap = dictDataMap;
this.isSetDict = true;
// 将字典数据存储到 sessionStorage 中
storage.setItem(DICT_STORAGE_KEY, dictDataMap, 60);
} catch (error) {
console.error('Failed to set dictionary values:', error);
}
},
},
getters: {
getDictMap: (state) => state.dictMap,
getDictData: (state) => (dictType: string) => {
return state.dictMap.get(dictType);
},
getDictOptions: (state) => (dictType: string) => {
return state.dictMap.get(dictType);
},
},
persist: [{ pick: ['dictMap', 'isSetDict'] }],
state: (): DictState => ({
dictMap: new Map<string, DictValueType[]>(),
isSetDict: false,
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useDictStore, hot));
}

View File

@ -1,2 +1 @@
export * from './auth'; export * from './auth';
export * from './dict';

View File

@ -1,2 +0,0 @@
export * from './menus';
export * from './user';

View File

@ -1,22 +0,0 @@
import type { BasicUserInfo } from '@vben/types';
import type { AppRouteRecordRaw } from '#/types';
/** 用户信息 */
type ExBasicUserInfo = {
deptId: number;
} & BasicUserInfo;
/** 用户信息 */
interface YudaoUserInfo extends ExBasicUserInfo {
permissions: string[];
menus: AppRouteRecordRaw[];
/**
*
*/
homePath: string;
roles: string[];
user: ExBasicUserInfo;
}
export type { ExBasicUserInfo, YudaoUserInfo };

View File

@ -1,45 +0,0 @@
import { StorageManager } from '@vben/utils';
// token key
const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN__';
const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN__';
const TENANT_ID_KEY = 'TENANT_ID__';
const storage = new StorageManager({
prefix: import.meta.env.VITE_APP_NAMESPACE,
storageType: 'sessionStorage',
});
function getAccessToken(): null | string {
return storage.getItem(ACCESS_TOKEN_KEY);
}
function setAccessToken(value: string, unix: number) {
return storage.setItem(ACCESS_TOKEN_KEY, value, unix - Date.now());
}
function getRefreshToken(): null | string {
return storage.getItem(REFRESH_TOKEN_KEY);
}
function setRefreshToken(value: string) {
return storage.setItem(REFRESH_TOKEN_KEY, value);
}
function getTenantId(): null | number {
return storage.getItem(TENANT_ID_KEY);
}
function setTenantId(value: number) {
return storage.setItem(TENANT_ID_KEY, value);
}
export {
getAccessToken,
getRefreshToken,
getTenantId,
setAccessToken,
setRefreshToken,
setTenantId,
};

View File

@ -1 +0,0 @@
export * from './auth';

View File

@ -1,129 +1,132 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, reactive, ref } from 'vue'; import { computed, ref, watchEffect } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui'; import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useDictStore, useTenantStore } from '@vben/stores';
import { getTenantByWebsite, getTenantIdByName } from '#/api/core/auth'; import {
import { Verify } from '#/components/Verification'; checkCaptcha,
getCaptcha,
getTenantByWebsite,
getTenantIdByName,
} from '#/api';
import { getSimpleDictDataList } from '#/api/system/dict-data';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import { setTenantId } from '#/utils';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const authStore = useAuthStore();
/**
* 初始化验证码
* blockPuzzle 滑块
* clickWord 点击文字
*/
const verify = ref();
const captchaType = ref('blockPuzzle');
const { tenantEnable, captchaEnable } = useAppConfig( const { tenantEnable, captchaEnable } = useAppConfig(
import.meta.env, import.meta.env,
import.meta.env.PROD, import.meta.env.PROD,
); );
const authStore = useAuthStore();
const tenantStore = useTenantStore();
const dictStore = useDictStore();
const captchaType = 'blockPuzzle';
const loginData = ref<Recordable<any>>({});
const verifyRef = ref();
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
return [ return [
{ {
component: 'VbenInput', component: 'VbenInput',
componentProps: { componentProps: {
placeholder: $t('page.auth.tenantNameTip'), placeholder: $t('authentication.tenantName'),
}, },
fieldName: 'tenantName', fieldName: 'tenantName',
label: $t('page.auth.tenantname'), label: $t('authentication.tenantName'),
rules: z.string().min(1, { message: $t('page.auth.tenantNameTip') }), rules: z
defaultValue: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '', .string()
.min(1, { message: $t('authentication.tenantNameTip') })
.default(import.meta.env.VITE_APP_DEFAULT_TENANT_NAME),
dependencies: {
triggerFields: ['tenantName'],
if: tenantEnable && !tenantStore.tenantId,
trigger: (values) => {
tenantStore.setTenantName(values.tenantName);
},
},
}, },
{ {
component: 'VbenInput', component: 'VbenInput',
componentProps: { componentProps: {
placeholder: $t('page.auth.usernameTip'), placeholder: $t('authentication.usernameTip'),
}, },
fieldName: 'username', fieldName: 'username',
label: $t('page.auth.username'), label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('page.auth.usernameTip') }), rules: z
defaultValue: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '', .string()
.min(1, { message: $t('authentication.usernameTip') })
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
}, },
{ {
component: 'VbenInputPassword', component: 'VbenInputPassword',
componentProps: { componentProps: {
placeholder: $t('page.auth.passwordTip'), placeholder: $t('authentication.password'),
}, },
fieldName: 'password', fieldName: 'password',
label: $t('page.auth.password'), label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('page.auth.passwordTip') }), rules: z
defaultValue: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '', .string()
.min(1, { message: $t('authentication.passwordTip') })
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
}, },
]; ];
}); });
const loginData = reactive({
loginForm: {
username: '',
password: '',
tenantName: '',
},
});
const captchaVerification = ref('');
//
async function getCode(params: any) {
if (params) {
loginData.loginForm = params;
}
try {
await getTenant();
if (captchaEnable) {
//
//
verify.value.show();
} else {
//
await handleLogin({});
}
} catch (error) {
console.error('Error in getCode:', error);
}
}
// && ID /**
async function getTenant() { * 处理登录
*/
const handleLogin = async (values: any) => {
//
if (tenantEnable && !tenantStore.tenantId) {
const tenantId = await getTenantIdByName(values.tenantName);
if (tenantId) {
tenantStore.setTenantId(tenantId);
}
}
//
if (captchaEnable) {
loginData.value = values;
verifyRef.value.show();
} else {
authStore.authLogin(values);
}
};
const handleVerifySuccess = async ({ captchaVerification }: any) => {
await authStore.authLogin(
{
...loginData.value,
captchaVerification,
},
() => {
//
dictStore.setDictCacheByApi(getSimpleDictDataList, 'label', 'value');
},
);
};
watchEffect(async () => {
if (tenantEnable) { if (tenantEnable) {
const website = location.host; const website = window.location.hostname;
try {
const tenant = await getTenantByWebsite(website); const tenant = await getTenantByWebsite(website);
if (tenant) { if (tenant) {
loginData.loginForm.tenantName = tenant.name; tenantStore.setTenant({
setTenantId(tenant.id); tenantId: tenant.id,
} else { tenantName: tenant.name,
const res = await getTenantIdByName(loginData.loginForm.tenantName);
setTenantId(res);
}
} catch (error) {
console.error('Error in getTenant:', error);
}
}
}
async function handleLogin(params: any) {
if (!params.captchaVerification && captchaEnable) {
console.error('Captcha verification is required');
return;
}
captchaVerification.value = params.captchaVerification;
try {
await authStore.authLogin({
...loginData.loginForm,
captchaVerification: captchaVerification.value,
}); });
} catch (error) {
console.error('Error in handleLogin:', error);
} }
} }
});
</script> </script>
<template> <template>
@ -131,14 +134,16 @@ async function handleLogin(params: any) {
<AuthenticationLogin <AuthenticationLogin
:form-schema="formSchema" :form-schema="formSchema"
:loading="authStore.loginLoading" :loading="authStore.loginLoading"
@submit="getCode" @submit="handleLogin"
/> />
<Verify <Verification
ref="verify" ref="verifyRef"
:captcha-type="captchaType" :captcha-type="captchaType"
:check-captcha-api="checkCaptcha"
:get-captcha-api="getCaptcha"
:img-size="{ width: '400px', height: '200px' }" :img-size="{ width: '400px', height: '200px' }"
mode="pop" mode="pop"
@success="handleLogin" @on-success="handleVerifySuccess"
/> />
</div> </div>
</template> </template>

View File

@ -0,0 +1,98 @@
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { CodegenApi } from '#/api/infra/codegen';
export namespace CodegenDefaultData {
/**
*
*/
export const tableColumns: VxeGridProps<CodegenApi.CodegenTableRespVO>['columns'] =
[
{
type: 'checkbox',
width: 50,
},
{
type: 'seq',
width: 50,
},
{ field: 'id', title: '编号', width: 100 },
{ field: 'tableName', title: '表名' },
{ field: 'tableComment', title: '表描述' },
{ field: 'className', title: '实体类名' },
{ field: 'createTime', title: '创建时间', formatter: 'formatDateTime' },
{ field: 'updateTime', title: '更新时间', formatter: 'formatDateTime' },
{
title: '操作',
width: 'auto',
fixed: 'right',
slots: { default: 'action' },
},
];
/**
*
*/
export const formSchema: VbenFormProps['schema'] = [
{
label: '表名称',
fieldName: 'tableName',
component: 'Input',
},
{
label: '表描述',
fieldName: 'tableComment',
component: 'Input',
},
{
label: '创建时间',
fieldName: 'createTime',
component: 'DatePicker',
componentProps: {
type: 'daterange',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
},
];
}
//* ****************************** ImportTableModal.vue *******************************//
export namespace CodegenImportTableModalData {
/**
*
*/
export const tableColumns: VxeGridProps<CodegenApi.DatabaseTableRespVO>['columns'] =
[
{ type: 'checkbox', width: 50 },
{ type: 'seq', title: '序号', width: 50 },
{ field: 'name', title: '表名' },
{ field: 'comment', title: '表描述' },
];
/**
*
*/
export const formSchema: VbenFormProps['schema'] = [
{
label: '数据源',
fieldName: 'dataSourceConfigId',
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '请选择数据源',
},
},
{
label: '表名称',
fieldName: 'name',
component: 'Input',
},
{
label: '表描述',
fieldName: 'comment',
component: 'Input',
},
];
}

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useVbenModal, type VbenFormProps } from '@vben/common-ui';
import {
useVbenVxeGrid,
type VxeGridListeners,
type VxeGridProps,
} from '@vben/plugins/vxe-table';
import { getSchemaTableList } from '#/api/infra/codegen';
import {
type DataSourceConfigApi,
getDataSourceConfigList,
} from '#/api/infra/data-source-config';
import { CodegenImportTableModalData } from '../codegen.data';
// checked
const checkedStatus = ref<boolean>(false);
const dataSourceConfigList =
ref<DataSourceConfigApi.DataSourceConfigRespVO[]>();
/**
* 表格查询表单配置
*/
const formOptions = reactive<any>({
//
collapsed: false,
schema: CodegenImportTableModalData.formSchema,
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
//
submitOnEnter: false,
} as VbenFormProps);
/**
* 表格配置
*/
const gridOptions = reactive<any>({
columns: CodegenImportTableModalData.tableColumns,
toolbarConfig: {
enabled: false,
},
height: 'auto',
proxyConfig: {
autoLoad: false,
ajax: {
query: async (_, values) => {
return await getSchemaTableList(values);
},
},
},
pagerConfig: {
enabled: false,
},
} as VxeGridProps);
const gridEvents = reactive<any>({
checkboxChange: (params) => {
const { checked } = params;
checkedStatus.value = checked;
},
checkboxAll: (params) => {
const { checked } = params;
checkedStatus.value = checked;
},
} as VxeGridListeners);
// 使
const [Grid, gridApi] = useVbenVxeGrid(
reactive({
formOptions,
gridOptions,
gridEvents,
}),
);
const [Modal] = useVbenModal({
class: 'w-[800px] h-[800px]',
onOpenChange: async (isOpen) => {
if (isOpen) {
//
dataSourceConfigList.value = await getDataSourceConfigList();
//
gridApi.formApi.updateSchema([
{
fieldName: 'dataSourceConfigId',
componentProps: {
options: dataSourceConfigList.value?.map((item) => ({
label: item.name,
value: item.id,
})),
},
},
]);
//
gridApi.formApi.setFieldValue(
'dataSourceConfigId',
dataSourceConfigList.value?.[0]?.id,
);
//
gridApi.reload(await gridApi.formApi.getValues());
}
},
});
</script>
<template>
<Modal>
<Grid />
</Modal>
</template>

View File

@ -0,0 +1,194 @@
<script lang="ts" setup>
import { defineAsyncComponent, reactive, ref } from 'vue';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import {
useVbenVxeGrid,
type VxeGridListeners,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { type CodegenApi, getCodegenTablePage } from '#/api/infra/codegen';
import { CodegenDefaultData } from './codegen.data';
// 使
const [ImportTableModal, importTableModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./components/import-table-modal.vue'),
),
});
// checked
const checkedStatus = ref<boolean>(false);
/**
* 查看详情
*/
const handleView = (_row: CodegenApi.CodegenTableRespVO) => {
// console.log('', row);
};
/**
* 编辑
*/
const handleEdit = (_row: CodegenApi.CodegenTableRespVO) => {
// console.log('', row);
};
/**
* 删除
*/
const handleDelete = (_row: CodegenApi.CodegenTableRespVO) => {
// console.log('', row);
};
/**
* 批量删除
*/
const handleBatchDelete = (_rows: CodegenApi.CodegenTableRespVO[]) => {
// console.log('', rows);
};
/**
* 导入表
*/
const handleImportTable = () => {
// console.log('', importTableModalApi);
importTableModalApi.open();
};
/**
* 同步
*/
const handleSync = (_row: CodegenApi.CodegenTableRespVO) => {
// console.log('', row);
};
/**
* 批量同步
*/
const handleBatchSync = (_rows: CodegenApi.CodegenTableRespVO[]) => {
// console.log('', rows);
};
/**
* 生成代码
*/
const handleGenerateCode = (_row: CodegenApi.CodegenTableRespVO) => {
// console.log('', row);
};
/**
* 批量生成代码
*/
const handleBatchGenerateCode = (_rows: CodegenApi.CodegenTableRespVO[]) => {
// console.log('', rows);
};
/**
* 表格查询表单配置
*/
const formOptions = reactive<any>({
//
collapsed: false,
schema: CodegenDefaultData.formSchema,
//
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
//
submitOnEnter: false,
} as VbenFormProps);
/**
* 表格配置
*/
const gridOptions = reactive<any>({
columns: CodegenDefaultData.tableColumns,
toolbarConfig: {
slots: {
buttons: 'toolbar_buttons',
},
},
proxyConfig: {
ajax: {
query: async ({ page }, params) => {
const data = await getCodegenTablePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...params,
});
return data;
},
},
},
} as VxeGridProps);
const gridEvents = reactive<any>({
checkboxChange: (params) => {
const { checked } = params;
checkedStatus.value = checked;
},
checkboxAll: (params) => {
const { checked } = params;
checkedStatus.value = checked;
},
} as VxeGridListeners);
// 使
const [Grid] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
</script>
<template>
<Page auto-content-height>
<Grid>
<template #toolbar_buttons="{ $grid }">
<div class="flex items-center gap-2">
<Button type="primary" @click="handleImportTable"></Button>
<Button
:disabled="!checkedStatus"
type="primary"
@click="handleBatchSync($grid.getCheckboxRecords())"
>
批量同步
</Button>
<Button
:disabled="!checkedStatus"
type="primary"
@click="handleBatchGenerateCode($grid.getCheckboxRecords())"
>
批量生成
</Button>
<Button
:disabled="!checkedStatus"
danger
type="primary"
@click="handleBatchDelete($grid.getCheckboxRecords())"
>
批量删除
</Button>
</div>
</template>
<template #action="{ row }">
<div class="flex w-fit items-center justify-around">
<Button type="link" @click="handleView(row)"></Button>
<Button type="link" @click="handleEdit(row)"></Button>
<Button type="link" @click="handleSync(row)"></Button>
<Button type="link" @click="handleGenerateCode(row)">
生成代码
</Button>
<Button danger type="link" @click="handleDelete(row)"></Button>
</div>
</template>
</Grid>
<ImportTableModal />
</Page>
</template>

View File

@ -31,11 +31,13 @@
"@vben/types": "workspace:*", "@vben/types": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:", "@vueuse/integrations": "catalog:",
"crypto-js": "catalog:",
"qrcode": "catalog:", "qrcode": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-router": "catalog:" "vue-router": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "catalog:",
"@types/qrcode": "catalog:" "@types/qrcode": "catalog:"
} }
} }

View File

@ -1,6 +1,8 @@
export { default as PointSelectionCaptcha } from './point-selection-captcha/index.vue'; export { default as PointSelectionCaptcha } from './point-selection-captcha/index.vue';
export { default as PointSelectionCaptchaCard } from './point-selection-captcha/index.vue';
export { default as PointSelectionCaptchaCard } from './point-selection-captcha/index.vue';
export { default as SliderCaptcha } from './slider-captcha/index.vue'; export { default as SliderCaptcha } from './slider-captcha/index.vue';
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue'; export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue';
export type * from './types'; export type * from './types';
export { default as Verification } from './verification/index.vue';

View File

@ -1,9 +1,8 @@
<script type="text/babel" setup> <script lang="ts" setup>
/** import type { VerificationProps } from '../types';
* VerifyPoints
* @description 点选
*/
import { import {
type ComponentInternalInstance,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
onMounted, onMounted,
@ -14,68 +13,91 @@ import {
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { checkCaptcha, getCaptcha } from '#/api/core/auth'; import { aesEncrypt } from '../utils/ase';
import { resetSize } from '../utils/util';
import { aesEncrypt } from './../utils/ase'; /**
import { resetSize } from './../utils/util'; * VerifyPoints
* @description 点选
*/
const props = defineProps({ // const props = defineProps({
barSize: { // barSize: {
default() { // default() {
return { // return {
height: '40px', // height: '40px',
width: '310px', // width: '310px',
}; // };
}, // },
type: Object, // type: Object,
}, // },
captchaType: { // captchaType: {
default() { // default() {
return 'VerifyPoints'; // return 'VerifyPoints';
}, // },
type: String, // type: String,
}, // },
imgSize: { // imgSize: {
default() { // default() {
return { // return {
height: '155px', // height: '155px',
width: '310px', // width: '310px',
}; // };
}, // },
type: Object, // type: Object,
}, // },
// popfixed // // popfixed
mode: { // mode: {
default: 'fixed', // default: 'fixed',
type: String, // type: String,
}, // },
// // //
vSpace: { // vSpace: {
default: 5, // default: 5,
type: Number, // type: Number,
}, // },
// });
defineOptions({
name: 'VerifyPoints',
}); });
const { captchaType, mode } = toRefs(props); const props = withDefaults(defineProps<VerificationProps>(), {
const { proxy } = getCurrentInstance(); barSize: () => ({
const secretKey = ref(''); // ase height: '40px',
width: '310px',
}),
captchaType: 'clickWord',
imgSize: () => ({
height: '155px',
width: '310px',
}),
mode: 'fixed',
space: 5,
});
const emit = defineEmits(['onSuccess', 'onError', 'onClose', 'onReady']);
const { captchaType, mode, checkCaptchaApi, getCaptchaApi } = toRefs(props);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const secretKey = ref(); // ase
const checkNum = ref(3); // const checkNum = ref(3); //
const fontPos = reactive([]); // const fontPos = reactive<any[]>([]); //
const checkPosArr = reactive([]); // const checkPosArr = reactive<any[]>([]); //
const num = ref(1); // const num = ref(1); //
const pointBackImgBase = ref(''); // const pointBackImgBase = ref(); //
const poinTextList = reactive([]); // const poinTextList = ref<any[]>([]); //
const backToken = ref(''); // token const backToken = ref(); // token
const setSize = reactive({ const setSize = reactive({
barHeight: 0, barHeight: 0,
barWidth: 0, barWidth: 0,
imgHeight: 0, imgHeight: 0,
imgWidth: 0, imgWidth: 0,
}); });
const tempPoints = reactive([]); const tempPoints = reactive<any[]>([]);
const text = ref(''); const text = ref();
const barAreaColor = ref(undefined); const barAreaColor = ref();
const barAreaBorderColor = ref(undefined); const barAreaBorderColor = ref();
const showRefresh = ref(true); const showRefresh = ref(true);
const bindingClick = ref(true); const bindingClick = ref(true);
@ -91,33 +113,34 @@ function init() {
setSize.imgWidth = imgWidth; setSize.imgWidth = imgWidth;
setSize.barHeight = barHeight; setSize.barHeight = barHeight;
setSize.barWidth = barWidth; setSize.barWidth = barWidth;
proxy.$parent.$emit('ready', proxy); emit('onReady', proxy);
}); });
} }
onMounted(() => { onMounted(() => {
// //
init(); init();
proxy.$el.addEventListener('selectstart', () => { proxy?.$el?.addEventListener('selectstart', () => {
return false; return false;
}); });
}); });
const canvas = ref(null); const canvas = ref(null);
// //
const getMousePos = function (obj, e) { const getMousePos = function (obj: any, e: any) {
const x = e.offsetX; const x = e.offsetX;
const y = e.offsetY; const y = e.offsetY;
return { x, y }; return { x, y };
}; };
// //
const createPoint = function (pos) { const createPoint = function (pos: any) {
tempPoints.push(Object.assign({}, pos)); tempPoints.push(Object.assign({}, pos));
return num.value + 1; return num.value + 1;
}; };
// //
const pointTransfrom = function (pointArr, imgSize) { const pointTransfrom = function (pointArr: any, imgSize: any) {
const newPointArr = pointArr.map((p) => { const newPointArr = pointArr.map((p: any) => {
const x = Math.round((310 * p.x) / Number.parseInt(imgSize.imgWidth)); const x = Math.round((310 * p.x) / Number.parseInt(imgSize.imgWidth));
const y = Math.round((155 * p.y) / Number.parseInt(imgSize.imgHeight)); const y = Math.round((155 * p.y) / Number.parseInt(imgSize.imgHeight));
return { x, y }; return { x, y };
@ -137,7 +160,7 @@ const refresh = async function () {
showRefresh.value = true; showRefresh.value = true;
}; };
function canvasClick(e) { function canvasClick(e: any) {
checkPosArr.push(getMousePos(canvas, e)); checkPosArr.push(getMousePos(canvas, e));
if (num.value === checkNum.value) { if (num.value === checkNum.value) {
num.value = createPoint(getMousePos(canvas, e)); num.value = createPoint(getMousePos(canvas, e));
@ -162,25 +185,25 @@ function canvasClick(e) {
: JSON.stringify(checkPosArr), : JSON.stringify(checkPosArr),
token: backToken.value, token: backToken.value,
}; };
checkCaptcha(data).then((response) => { checkCaptchaApi?.value?.(data).then((response: any) => {
const res = response.data; const res = response.data;
if (res.repCode === '0000') { if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c'; barAreaColor.value = '#4cae4c';
barAreaBorderColor.value = '#5cb85c'; barAreaBorderColor.value = '#5cb85c';
text.value = $t('components.captcha.success'); text.value = $t('ui.captcha.success');
bindingClick.value = false; bindingClick.value = false;
if (mode.value === 'pop') { if (mode.value === 'pop') {
setTimeout(() => { setTimeout(() => {
proxy.$parent.clickShow = false; emit('onClose');
refresh(); refresh();
}, 1500); }, 1500);
} }
proxy.$parent.$emit('success', { captchaVerification }); emit('onSuccess', { captchaVerification });
} else { } else {
proxy.$parent.$emit('error', proxy); emit('onError', proxy);
barAreaColor.value = '#d9534f'; barAreaColor.value = '#d9534f';
barAreaBorderColor.value = '#d9534f'; barAreaBorderColor.value = '#d9534f';
text.value = $t('components.captcha.fail'); text.value = $t('ui.captcha.sliderRotateFailTip');
setTimeout(() => { setTimeout(() => {
refresh(); refresh();
}, 700); }, 700);
@ -197,17 +220,22 @@ async function getPictrue() {
const data = { const data = {
captchaType: captchaType.value, captchaType: captchaType.value,
}; };
const res = await getCaptcha(data); const res = await getCaptchaApi?.value?.(data);
if (res.data.repCode === '0000') {
pointBackImgBase.value = res.data.repData.originalImageBase64; if (res?.data?.repCode === '0000') {
pointBackImgBase.value = `data:image/png;base64,${res?.data?.repData?.originalImageBase64}`;
backToken.value = res.data.repData.token; backToken.value = res.data.repData.token;
secretKey.value = res.data.repData.secretKey; secretKey.value = res.data.repData.secretKey;
poinTextList.value = res.data.repData.wordList; poinTextList.value = res.data.repData.wordList;
text.value = `${$t('components.captcha.point')}${poinTextList.value.join(',')}`; text.value = `${$t('ui.captcha.point')}${poinTextList.value.join(',')}`;
} else { } else {
text.value = res.data.repMsg; text.value = res?.data?.repMsg;
} }
} }
defineExpose({
init,
refresh,
});
</script> </script>
<template> <template>
@ -218,7 +246,7 @@ async function getPictrue() {
width: setSize.imgWidth, width: setSize.imgWidth,
height: setSize.imgHeight, height: setSize.imgHeight,
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`, 'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
'margin-bottom': `${vSpace}px`, 'margin-bottom': `${space}px`,
}" }"
class="verify-img-panel" class="verify-img-panel"
> >
@ -232,7 +260,7 @@ async function getPictrue() {
</div> </div>
<img <img
ref="canvas" ref="canvas"
:src="`data:image/png;base64,${pointBackImgBase}`" :src="pointBackImgBase"
alt="" alt=""
style="display: block; width: 100%; height: 100%" style="display: block; width: 100%; height: 100%"
@click="bindingClick ? canvasClick($event) : undefined" @click="bindingClick ? canvasClick($event) : undefined"
@ -251,8 +279,8 @@ async function getPictrue() {
'line-height': '20px', 'line-height': '20px',
'border-radius': '50%', 'border-radius': '50%',
position: 'absolute', position: 'absolute',
top: `${parseInt(tempPoint.y - 10)}px`, top: `${tempPoint.y - 10}px`,
left: `${parseInt(tempPoint.x - 10)}px`, left: `${tempPoint.x - 10}px`,
}" }"
class="point-area" class="point-area"
> >

View File

@ -1,4 +1,6 @@
<script type="text/babel" setup> <script lang="ts" setup>
import type { VerificationProps } from '../types';
/** /**
* VerifySlide * VerifySlide
* @description 滑块 * @description 滑块
@ -11,107 +13,81 @@ import {
reactive, reactive,
ref, ref,
toRefs, toRefs,
watch,
} from 'vue'; } from 'vue';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { checkCaptcha, getCaptcha } from '#/api/core/auth';
import { aesEncrypt } from './../utils/ase'; import { aesEncrypt } from './../utils/ase';
import { resetSize } from './../utils/util'; import { resetSize } from './../utils/util';
const props = defineProps({ const props = withDefaults(defineProps<VerificationProps>(), {
barSize: { barSize: () => ({
default() { height: '40px',
return {
height: '30px',
width: '310px', width: '310px',
}; }),
}, blockSize: () => ({
type: Object,
},
blockSize: {
default() {
return {
height: '50px', height: '50px',
width: '50px', width: '50px',
}; }),
}, captchaType: 'blockPuzzle',
type: Object, explain: '',
}, imgSize: () => ({
captchaType: {
default() {
return 'VerifySlide';
},
type: String,
},
explain: {
default: '',
type: String,
},
imgSize: {
default() {
return {
height: '155px', height: '155px',
width: '310px', width: '310px',
}; }),
}, mode: 'fixed',
type: Object, type: '1',
}, space: 5,
// popfixed
mode: {
default: 'fixed',
type: String,
},
type: {
default: '1',
type: String,
},
vSpace: {
default: 5,
type: Number,
},
}); });
const { blockSize, captchaType, explain, mode, type } = toRefs(props); const emit = defineEmits(['onSuccess', 'onError', 'onClose']);
const { proxy } = getCurrentInstance();
const secretKey = ref(''); // ase const {
const passFlag = ref(''); // blockSize,
const backImgBase = ref(''); // captchaType,
const blockBackImgBase = ref(''); // explain,
const backToken = ref(''); // token mode,
const startMoveTime = ref(''); // checkCaptchaApi,
const endMovetime = ref(''); // getCaptchaApi,
const tipWords = ref(''); } = toRefs(props);
const text = ref('');
const finishText = ref(''); const { proxy } = getCurrentInstance()!;
const secretKey = ref(); // ase
const passFlag = ref(); //
const backImgBase = ref(); //
const blockBackImgBase = ref(); //
const backToken = ref(); // token
const startMoveTime = ref(); //
const endMovetime = ref(); //
const tipWords = ref();
const text = ref();
const finishText = ref();
const setSize = reactive({ const setSize = reactive({
barHeight: 0, barHeight: '0px',
barWidth: 0, barWidth: '0px',
imgHeight: 0, imgHeight: '0px',
imgWidth: 0, imgWidth: '0px',
}); });
const moveBlockLeft = ref(undefined); const moveBlockLeft = ref();
const leftBarWidth = ref(undefined); const leftBarWidth = ref();
// //
const moveBlockBackgroundColor = ref(undefined); const moveBlockBackgroundColor = ref();
const leftBarBorderColor = ref('#ddd'); const leftBarBorderColor = ref('#ddd');
const iconColor = ref(undefined); const iconColor = ref();
const iconClass = ref('icon-right'); const iconClass = ref('icon-right');
const status = ref(false); // const status = ref(false); //
const isEnd = ref(false); // const isEnd = ref(false); //
const showRefresh = ref(true); const showRefresh = ref(true);
const transitionLeft = ref(''); const transitionLeft = ref();
const transitionWidth = ref(''); const transitionWidth = ref();
const startLeft = ref(0); const startLeft = ref(0);
const barArea = computed(() => { const barArea = computed(() => {
return proxy.$el.querySelector('.verify-bar-area'); return proxy?.$el.querySelector('.verify-bar-area');
}); });
function init() { function init() {
text.value = text.value =
explain.value === '' ? $t('components.captcha.slide') : explain.value; explain.value === '' ? $t('ui.captcha.sliderDefaultText') : explain.value;
getPictrue(); getPictrue();
nextTick(() => { nextTick(() => {
@ -120,7 +96,7 @@ function init() {
setSize.imgWidth = imgWidth; setSize.imgWidth = imgWidth;
setSize.barHeight = barHeight; setSize.barHeight = barHeight;
setSize.barWidth = barWidth; setSize.barWidth = barWidth;
proxy.$parent.$emit('ready', proxy); proxy?.$parent?.$emit('ready', proxy);
}); });
window.removeEventListener('touchmove', move); window.removeEventListener('touchmove', move);
@ -137,20 +113,21 @@ function init() {
window.addEventListener('touchend', end); window.addEventListener('touchend', end);
window.addEventListener('mouseup', end); window.addEventListener('mouseup', end);
} }
watch(type, () => {
init();
});
onMounted(() => { onMounted(() => {
// //
init(); init();
proxy.$el.addEventListener('selectstart', () => { proxy?.$el.addEventListener('selectstart', () => {
return false; return false;
}); });
}); });
// //
function start(e) { function start(e: MouseEvent | TouchEvent) {
e = e || window.event; const x =
const x = e.touches ? e.touches[0].pageX : e.clientX; ((e as TouchEvent).touches
? (e as TouchEvent).touches[0]?.pageX
: (e as MouseEvent).clientX) || 0;
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left); startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left);
startMoveTime.value = Date.now(); // startMoveTime.value = Date.now(); //
if (isEnd.value === false) { if (isEnd.value === false) {
@ -163,27 +140,25 @@ function start(e) {
} }
} }
// //
function move(e) { function move(e: MouseEvent | TouchEvent) {
e = e || window.event;
if (status.value && isEnd.value === false) { if (status.value && isEnd.value === false) {
const x = e.touches ? e.touches[0].pageX : e.clientX; const x =
((e as TouchEvent).touches
? (e as TouchEvent).touches[0]?.pageX
: (e as MouseEvent).clientX) || 0;
const bar_area_left = barArea.value.getBoundingClientRect().left; const bar_area_left = barArea.value.getBoundingClientRect().left;
let move_block_left = x - bar_area_left; // left let move_block_left = x - bar_area_left; // left
if ( if (
move_block_left >= move_block_left >=
barArea.value.offsetWidth - barArea.value.offsetWidth - Number.parseInt(blockSize.value.width) / 2 - 2
Number.parseInt(Number.parseInt(blockSize.value.width) / 2) -
2
) )
move_block_left = move_block_left =
barArea.value.offsetWidth - barArea.value.offsetWidth -
Number.parseInt(Number.parseInt(blockSize.value.width) / 2) - Number.parseInt(blockSize.value.width) / 2 -
2; 2;
if (move_block_left <= 0) if (move_block_left <= 0)
move_block_left = Number.parseInt( move_block_left = Number.parseInt(blockSize.value.width) / 2;
Number.parseInt(blockSize.value.width) / 2,
);
// left // left
moveBlockLeft.value = `${move_block_left - startLeft.value}px`; moveBlockLeft.value = `${move_block_left - startLeft.value}px`;
@ -211,7 +186,7 @@ function end() {
: JSON.stringify({ x: moveLeftDistance, y: 5 }), : JSON.stringify({ x: moveLeftDistance, y: 5 }),
token: backToken.value, token: backToken.value,
}; };
checkCaptcha(data).then((response) => { checkCaptchaApi?.value?.(data).then((response) => {
const res = response.data; const res = response.data;
if (res.repCode === '0000') { if (res.repCode === '0000') {
moveBlockBackgroundColor.value = '#5cb85c'; moveBlockBackgroundColor.value = '#5cb85c';
@ -222,13 +197,13 @@ function end() {
isEnd.value = true; isEnd.value = true;
if (mode.value === 'pop') { if (mode.value === 'pop') {
setTimeout(() => { setTimeout(() => {
proxy.$parent.clickShow = false; emit('onClose');
refresh(); refresh();
}, 1500); }, 1500);
} }
passFlag.value = true; passFlag.value = true;
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s
${$t('components.captcha.success')}`; ${$t('ui.captcha.title')}`;
const captchaVerification = secretKey.value const captchaVerification = secretKey.value
? aesEncrypt( ? aesEncrypt(
`${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`, `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`,
@ -237,8 +212,8 @@ function end() {
: `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`; : `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`;
setTimeout(() => { setTimeout(() => {
tipWords.value = ''; tipWords.value = '';
proxy.$parent.closeBox(); emit('onSuccess', { captchaVerification });
proxy.$parent.$emit('success', { captchaVerification }); emit('onClose');
}, 1000); }, 1000);
} else { } else {
moveBlockBackgroundColor.value = '#d9534f'; moveBlockBackgroundColor.value = '#d9534f';
@ -249,8 +224,8 @@ function end() {
setTimeout(() => { setTimeout(() => {
refresh(); refresh();
}, 1000); }, 1000);
proxy.$parent.$emit('error', proxy); emit('onError', proxy);
tipWords.value = $t('components.captcha.fail'); tipWords.value = $t('ui.captcha.sliderRotateFailTip');
setTimeout(() => { setTimeout(() => {
tipWords.value = ''; tipWords.value = '';
}, 1000); }, 1000);
@ -289,23 +264,28 @@ async function getPictrue() {
const data = { const data = {
captchaType: captchaType.value, captchaType: captchaType.value,
}; };
const res = await getCaptcha(data); const res = await getCaptchaApi?.value?.(data);
if (res.data.repCode === '0000') {
backImgBase.value = res.data.repData.originalImageBase64; if (res?.data?.repCode === '0000') {
blockBackImgBase.value = `data:image/png;base64,${res.data.repData.jigsawImageBase64}`; backImgBase.value = `data:image/png;base64,${res?.data?.repData?.originalImageBase64}`;
blockBackImgBase.value = `data:image/png;base64,${res?.data?.repData?.jigsawImageBase64}`;
backToken.value = res.data.repData.token; backToken.value = res.data.repData.token;
secretKey.value = res.data.repData.secretKey; secretKey.value = res.data.repData.secretKey;
} else { } else {
tipWords.value = res.data.repMsg; tipWords.value = res?.data?.repMsg;
} }
} }
defineExpose({
init,
refresh,
});
</script> </script>
<template> <template>
<div style="position: relative"> <div style="position: relative">
<div <div
v-if="type === '2'" v-if="type === '2'"
:style="{ height: `${parseInt(setSize.imgHeight) + vSpace}px` }" :style="{ height: `${Number.parseInt(setSize.imgHeight) + space}px` }"
class="verify-img-out" class="verify-img-out"
> >
<div <div
@ -313,7 +293,7 @@ async function getPictrue() {
class="verify-img-panel" class="verify-img-panel"
> >
<img <img
:src="`data:image/png;base64,${backImgBase}`" :src="backImgBase"
alt="" alt=""
style="display: block; width: 100%; height: 100%" style="display: block; width: 100%; height: 100%"
/> />
@ -346,7 +326,7 @@ async function getPictrue() {
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height, width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
height: barSize.height, height: barSize.height,
'border-color': leftBarBorderColor, 'border-color': leftBarBorderColor,
transaction: transitionWidth, transition: transitionWidth,
}" }"
class="verify-left-bar" class="verify-left-bar"
> >
@ -371,9 +351,9 @@ async function getPictrue() {
<div <div
v-if="type === '2'" v-if="type === '2'"
:style="{ :style="{
width: `${Math.floor((parseInt(setSize.imgWidth) * 47) / 310)}px`, width: `${Math.floor((Number.parseInt(setSize.imgWidth) * 47) / 310)}px`,
height: setSize.imgHeight, height: setSize.imgHeight,
top: `-${parseInt(setSize.imgHeight) + vSpace}px`, top: `-${Number.parseInt(setSize.imgHeight) + space}px`,
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`, 'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
}" }"
class="verify-sub-block" class="verify-sub-block"

View File

@ -0,0 +1,150 @@
<script setup lang="ts">
/**
* Verify 验证码组件
* @description 分发验证码使用
*/
import type { VerificationProps } from './types';
import { defineAsyncComponent, markRaw, ref, toRefs, watchEffect } from 'vue';
import './style/verify.css';
defineOptions({
name: 'Verification',
});
const props = withDefaults(defineProps<VerificationProps>(), {
arith: 0,
barSize: () => ({
height: '40px',
width: '310px',
}),
blockSize: () => ({
height: '50px',
width: '50px',
}),
captchaType: 'blockPuzzle',
explain: '',
figure: 0,
imgSize: () => ({
height: '155px',
width: '310px',
}),
mode: 'fixed',
space: 5,
});
const emit = defineEmits(['onSuccess', 'onError', 'onClose', 'onReady']);
const VerifyPoints = defineAsyncComponent(
() => import('./Verify/VerifyPoints.vue'),
);
const VerifySlide = defineAsyncComponent(
() => import('./Verify/VerifySlide.vue'),
);
const { captchaType, mode, checkCaptchaApi, getCaptchaApi } = toRefs(props);
const verifyType = ref();
const componentType = ref();
const instance = ref<InstanceType<typeof VerifyPoints | typeof VerifySlide>>();
const showBox = ref(false);
/**
* refresh
* @description 刷新
*/
const refresh = () => {
if (instance.value && instance.value.refresh) instance.value.refresh();
};
const show = () => {
if (mode.value === 'pop') showBox.value = true;
};
const onError = (proxy: any) => {
emit('onError', proxy);
refresh();
};
const onReady = (proxy: any) => {
emit('onReady', proxy);
refresh();
};
const onClose = () => {
emit('onClose');
showBox.value = false;
};
const onSuccess = (data: any) => {
emit('onSuccess', data);
};
watchEffect(() => {
switch (captchaType.value) {
case 'blockPuzzle': {
verifyType.value = '2';
componentType.value = markRaw(VerifySlide);
break;
}
case 'clickWord': {
verifyType.value = '';
componentType.value = markRaw(VerifyPoints);
break;
}
}
});
defineExpose({
onClose,
onError,
onReady,
onSuccess,
show,
refresh,
});
</script>
<template>
<div v-show="showBox">
<div
:class="mode === 'pop' ? 'verifybox' : ''"
:style="{ 'max-width': `${parseInt(imgSize.width) + 20}px` }"
>
<div v-if="mode === 'pop'" class="verifybox-top">
{{ $t('ui.captcha.title') }}
<span class="verifybox-close" @click="onClose">
<i class="iconfont icon-close"></i>
</span>
</div>
<div
:style="{ padding: mode === 'pop' ? '10px' : '0' }"
class="verifybox-bottom"
>
<component
:is="componentType"
v-if="componentType"
ref="instance"
:arith="arith"
:bar-size="barSize"
:block-size="blockSize"
:captcha-type="captchaType"
:check-captcha-api="checkCaptchaApi"
:explain="explain"
:figure="figure"
:get-captcha-api="getCaptchaApi"
:img-size="imgSize"
:mode="mode"
:space="space"
:type="verifyType"
@on-close="onClose"
@on-error="onError"
@on-ready="onReady"
@on-success="onSuccess"
/>
</div>
</div>
</div>
</template>

View File

@ -1,5 +1,5 @@
.verifybox { .verifybox {
position: relative; position: absolute;
top: 25%; top: 25%;
left: 50%; left: 50%;
box-sizing: border-box; box-sizing: border-box;

View File

@ -0,0 +1,25 @@
interface VerificationProps {
arith?: number;
barSize?: {
height: string;
width: string;
};
blockSize?: {
height: string;
width: string;
};
captchaType?: 'blockPuzzle' | 'clickWord';
explain?: string;
figure?: number;
imgSize?: {
height: string;
width: string;
};
mode?: 'fixed' | 'pop';
space?: number;
type?: '1' | '2';
checkCaptchaApi?: (data: any) => Promise<any>;
getCaptchaApi?: (data: any) => Promise<any>;
}
export type { VerificationProps };

View File

@ -16,6 +16,7 @@ import {
VxeInput, VxeInput,
VxeLoading, VxeLoading,
VxeModal, VxeModal,
VxeNumberInput,
VxePager, VxePager,
// VxeList, // VxeList,
// VxeModal, // VxeModal,
@ -70,6 +71,7 @@ export function initVxeTable() {
VxeUI.component(VxeGrid); VxeUI.component(VxeGrid);
VxeUI.component(VxeToolbar); VxeUI.component(VxeToolbar);
VxeUI.component(VxeNumberInput);
VxeUI.component(VxeButton); VxeUI.component(VxeButton);
// VxeUI.component(VxeButtonGroup); // VxeUI.component(VxeButtonGroup);
VxeUI.component(VxeCheckbox); VxeUI.component(VxeCheckbox);

View File

@ -22,9 +22,11 @@
"dependencies": { "dependencies": {
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/utils": "workspace:*", "@vben/utils": "workspace:*",
"axios": "catalog:" "axios": "catalog:",
"qs": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@types/qs": "catalog:",
"axios-mock-adapter": "catalog:" "axios-mock-adapter": "catalog:"
} }
} }

View File

@ -20,9 +20,9 @@ export const authenticateResponseInterceptor = ({
}): ResponseInterceptorConfig => { }): ResponseInterceptorConfig => {
return { return {
rejected: async (error) => { rejected: async (error) => {
const { config, response } = error; const { config, response, data: responseData } = error;
// 如果不是 401 错误,直接抛出异常 // 如果不是 401 错误,直接抛出异常
if (response?.status !== 401) { if (response?.status !== 401 && responseData.code !== 401) {
throw error; throw error;
} }
// 判断是否启用了 refreshToken 功能 // 判断是否启用了 refreshToken 功能
@ -92,7 +92,7 @@ export const errorMessageResponseInterceptor = (
} }
let errorMessage = ''; let errorMessage = '';
const status = error?.response?.status; const status = error?.response?.data?.code || error?.response?.status;
switch (status) { switch (status) {
case 400: { case 400: {

View File

@ -8,6 +8,7 @@ import type {
import { bindMethods, merge } from '@vben/utils'; import { bindMethods, merge } from '@vben/utils';
import axios from 'axios'; import axios from 'axios';
import qs from 'qs';
import { FileDownloader } from './modules/downloader'; import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor'; import { InterceptorManager } from './modules/interceptor';
@ -39,6 +40,10 @@ class RequestClient {
}, },
// 默认超时时间 // 默认超时时间
timeout: 10_000, timeout: 10_000,
// 处理请求参数 默认使用qs库处理
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
}; };
const { ...axiosConfig } = options; const { ...axiosConfig } = options;
const requestConfig = merge(axiosConfig, defaultConfig); const requestConfig = merge(axiosConfig, defaultConfig);

View File

@ -39,12 +39,25 @@ interface HttpResponse<T = any> {
*/ */
code: number; code: number;
data: T; data: T;
message: string; msg: string;
}
interface PageParam {
[key: string]: any;
pageNo: number;
pageSize: number;
}
interface PageResult<T> {
list: T[];
total: number;
} }
export type { export type {
HttpResponse, HttpResponse,
MakeErrorMessageFn, MakeErrorMessageFn,
PageParam,
PageResult,
RequestClientOptions, RequestClientOptions,
RequestContentType, RequestContentType,
RequestInterceptorConfig, RequestInterceptorConfig,

View File

@ -0,0 +1,63 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
export interface DictItem {
colorType?: string;
cssClass?: string;
label: string;
value: string;
}
export type Dict = Record<string, DictItem[]>;
interface DictState {
dictCache: Dict;
}
export const useDictStore = defineStore('dict', {
actions: {
getDictData(dictType: string, value?: string) {
const dict = this.dictCache[dictType];
if (!dict) {
return undefined;
}
return value ? dict.find((d) => d.value === value) : dict;
},
setDictCache(dicts: Dict) {
this.dictCache = dicts;
},
setDictCacheByApi(
api: (params: Record<string, any>) => Promise<Record<string, any>[]>,
params: Record<string, any>,
labelField: string = 'label',
valueField: string = 'value',
) {
api(params).then((dicts) => {
const dictCacheData: Dict = {};
dicts.forEach((dict) => {
dictCacheData[dict.dictType] = dicts
.filter((d) => d.dictType === dict.dictType)
.map((d) => ({
colorType: d.colorType,
cssClass: d.cssClass,
label: d[labelField],
value: d[valueField],
}));
});
this.setDictCache(dictCacheData);
});
},
},
persist: {
// 持久化
pick: ['dictCache'],
},
state: (): DictState => ({
dictCache: {},
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useDictStore, hot));
}

View File

@ -1,4 +1,6 @@
export * from './access'; export * from './access';
export * from './dict';
export * from './lock'; export * from './lock';
export * from './tabbar'; export * from './tabbar';
export * from './tenant';
export * from './user'; export * from './user';

View File

@ -0,0 +1,33 @@
import { defineStore } from 'pinia';
export interface TenantState {
tenantId?: number;
tenantName?: string;
}
export const useTenantStore = defineStore('tenant', {
actions: {
$reset() {
this.tenantId = undefined;
this.tenantName = undefined;
},
setTenant(tenant: TenantState) {
this.tenantId = tenant.tenantId;
this.tenantName = tenant.tenantName;
},
setTenantId(id: number) {
this.tenantId = id;
},
setTenantName(name: string) {
this.tenantName = name;
},
},
persist: {
// 持久化
pick: ['tenantId', 'tenantName'],
},
state: (): TenantState => ({
tenantId: undefined,
tenantName: undefined,
}),
});

View File

@ -51,6 +51,10 @@ export const useUserStore = defineStore('core-user', {
this.userRoles = roles; this.userRoles = roles;
}, },
}, },
persist: {
// 持久化
pick: ['userInfo', 'userRoles'],
},
state: (): AccessState => ({ state: (): AccessState => ({
userInfo: null, userInfo: null,
userRoles: [], userRoles: [],

View File

@ -1,2 +1,3 @@
export type * from './menu';
export type * from './user'; export type * from './user';
export type * from '@vben-core/typings'; export type * from '@vben-core/typings';

View File

@ -1,20 +1,18 @@
import type { BasicUserInfo } from '@vben-core/typings'; import type { BasicUserInfo } from '@vben-core/typings';
/** 用户信息 */ import type { AppRouteRecordRaw } from './menu';
interface UserInfo extends BasicUserInfo {
/**
*
*/
desc: string;
/**
*
*/
homePath: string;
/** interface ExUserInfo extends BasicUserInfo {
* accessToken deptId: number;
*/ nickname: string;
token: string;
} }
export type { UserInfo }; interface AuthPermissionInfo {
permissions: string[];
menus: AppRouteRecordRaw[];
roles: string[];
homePath: string;
user: ExUserInfo;
}
export type { AuthPermissionInfo, ExUserInfo };

View File

@ -29,7 +29,7 @@ async function generateRoutesByBackend(
const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap); const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
return routes; return [...options.routes, ...routes];
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return []; return [];

View File

@ -78,6 +78,9 @@ catalogs:
'@types/archiver': '@types/archiver':
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/eslint': '@types/eslint':
specifier: ^9.6.1 specifier: ^9.6.1
version: 9.6.1 version: 9.6.1
@ -102,6 +105,9 @@ catalogs:
'@types/qrcode': '@types/qrcode':
specifier: ^1.5.5 specifier: ^1.5.5
version: 1.5.5 version: 1.5.5
'@types/qs':
specifier: ^6.9.17
version: 6.9.17
'@types/sortablejs': '@types/sortablejs':
specifier: ^1.15.8 specifier: ^1.15.8
version: 1.15.8 version: 1.15.8
@ -174,6 +180,9 @@ catalogs:
cross-env: cross-env:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
crypto-js:
specifier: ^4.2.0
version: 4.2.0
cspell: cspell:
specifier: ^8.16.0 specifier: ^8.16.0
version: 8.16.0 version: 8.16.0
@ -348,6 +357,9 @@ catalogs:
qrcode: qrcode:
specifier: ^1.5.4 specifier: ^1.5.4
version: 1.5.4 version: 1.5.4
qs:
specifier: ^6.13.1
version: 6.13.1
radix-vue: radix-vue:
specifier: ^1.9.10 specifier: ^1.9.10
version: 1.9.10 version: 1.9.10
@ -661,9 +673,6 @@ importers:
ant-design-vue: ant-design-vue:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.2.6(vue@3.5.13(typescript@5.7.2)) version: 4.2.6(vue@3.5.13(typescript@5.7.2))
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs: dayjs:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.11.13 version: 1.11.13
@ -676,10 +685,6 @@ importers:
vue-router: vue-router:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.4.5(vue@3.5.13(typescript@5.7.2)) version: 4.4.5(vue@3.5.13(typescript@5.7.2))
devDependencies:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
apps/web-ele: apps/web-ele:
dependencies: dependencies:
@ -1494,6 +1499,9 @@ importers:
'@vueuse/integrations': '@vueuse/integrations':
specifier: 'catalog:' specifier: 'catalog:'
version: 11.3.0(async-validator@4.2.5)(axios@1.7.7)(change-case@5.4.4)(focus-trap@7.6.2)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.4)(vue@3.5.13(typescript@5.7.2)) version: 11.3.0(async-validator@4.2.5)(axios@1.7.7)(change-case@5.4.4)(focus-trap@7.6.2)(nprogress@0.2.0)(qrcode@1.5.4)(sortablejs@1.15.4)(vue@3.5.13(typescript@5.7.2))
crypto-js:
specifier: 'catalog:'
version: 4.2.0
qrcode: qrcode:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.5.4 version: 1.5.4
@ -1504,6 +1512,9 @@ importers:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.4.5(vue@3.5.13(typescript@5.7.2)) version: 4.4.5(vue@3.5.13(typescript@5.7.2))
devDependencies: devDependencies:
'@types/crypto-js':
specifier: 'catalog:'
version: 4.2.2
'@types/qrcode': '@types/qrcode':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.5.5 version: 1.5.5
@ -1651,7 +1662,13 @@ importers:
axios: axios:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.7.7 version: 1.7.7
qs:
specifier: 'catalog:'
version: 6.13.1
devDependencies: devDependencies:
'@types/qs':
specifier: 'catalog:'
version: 6.9.17
axios-mock-adapter: axios-mock-adapter:
specifier: 'catalog:' specifier: 'catalog:'
version: 2.1.0(axios@1.7.7) version: 2.1.0(axios@1.7.7)
@ -4349,6 +4366,9 @@ packages:
'@types/qrcode@1.5.5': '@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/qs@6.9.17':
resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
'@types/readdir-glob@1.1.5': '@types/readdir-glob@1.1.5':
resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
@ -8597,6 +8617,10 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
hasBin: true hasBin: true
qs@6.13.1:
resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -13095,6 +13119,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.9.3 '@types/node': 22.9.3
'@types/qs@6.9.17': {}
'@types/readdir-glob@1.1.5': '@types/readdir-glob@1.1.5':
dependencies: dependencies:
'@types/node': 22.9.3 '@types/node': 22.9.3
@ -17799,6 +17825,10 @@ snapshots:
pngjs: 5.0.0 pngjs: 5.0.0
yargs: 15.4.1 yargs: 15.4.1
qs@6.13.1:
dependencies:
side-channel: 1.0.6
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
queue-tick@1.0.1: {} queue-tick@1.0.1: {}

View File

@ -39,6 +39,7 @@ catalog:
'@tanstack/vue-query': ^5.61.3 '@tanstack/vue-query': ^5.61.3
'@tanstack/vue-store': ^0.5.7 '@tanstack/vue-store': ^0.5.7
'@types/archiver': ^6.0.3 '@types/archiver': ^6.0.3
'@types/crypto-js': ^4.2.2
'@types/eslint': ^9.6.1 '@types/eslint': ^9.6.1
'@types/html-minifier-terser': ^7.0.2 '@types/html-minifier-terser': ^7.0.2
'@types/jsonwebtoken': ^9.0.7 '@types/jsonwebtoken': ^9.0.7
@ -47,6 +48,7 @@ catalog:
'@types/nprogress': ^0.2.3 '@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3 '@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5 '@types/qrcode': ^1.5.5
'@types/qs': ^6.9.17
'@types/sortablejs': ^1.15.8 '@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.15.0 '@typescript-eslint/eslint-plugin': ^8.15.0
'@typescript-eslint/parser': ^8.15.0 '@typescript-eslint/parser': ^8.15.0
@ -73,6 +75,7 @@ catalog:
commitlint-plugin-function-rules: ^4.0.1 commitlint-plugin-function-rules: ^4.0.1
consola: ^3.2.3 consola: ^3.2.3
cross-env: ^7.0.3 cross-env: ^7.0.3
crypto-js: ^4.2.0
cspell: ^8.16.0 cspell: ^8.16.0
cssnano: ^7.0.6 cssnano: ^7.0.6
cz-git: ^1.11.0 cz-git: ^1.11.0
@ -132,6 +135,7 @@ catalog:
prettier-plugin-tailwindcss: ^0.6.9 prettier-plugin-tailwindcss: ^0.6.9
publint: ^0.2.12 publint: ^0.2.12
qrcode: ^1.5.4 qrcode: ^1.5.4
qs: ^6.13.1
radix-vue: ^1.9.10 radix-vue: ^1.9.10
resolve.exports: ^2.0.2 resolve.exports: ^2.0.2
rimraf: ^6.0.1 rimraf: ^6.0.1