feat: 完成 NestJS 后端核心底座开发 (M1-M6) 和 Ant Design Vue 前端迁移
主要更新: 1. 后端核心底座完成 (M1-M6): - 健康检查、指标监控、分布式锁 - 事件总线、队列系统、事务管理 - 安全守卫、多租户隔离、存储适配器 - 审计日志、配置管理、多语言支持 2. 前端迁移到 Ant Design Vue: - 从 Element Plus 迁移到 Ant Design Vue - 完善 system 模块 (role/menu/dept) - 修复依赖和配置问题 3. 文档完善: - AI 开发工作流文档 - 架构约束和开发规范 - 项目进度跟踪 4. 其他改进: - 修复编译错误和类型问题 - 完善测试用例 - 优化项目结构
This commit is contained in:
102
.cursor/rules/ai.mdc
Normal file
102
.cursor/rules/ai.mdc
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
## 智能体工作流程(多智能体协作)
|
||||
|
||||
### 角色定义(按执行顺序标注)
|
||||
- S1 需求分析体(Analyzer): 解析需求、对应 PHP/Nest 规范、输出任务切分与验收标准
|
||||
- S2 架构治理体(Architect): 校验分层/依赖/目录规范,给出重构建议与边界清单
|
||||
- S3 基建接入体(InfraOperator): 接入/校验 Kafka、Redis、队列、事务与配置,提供接入差异与示例
|
||||
- S4 开发执行体(Developer): 按规范编码、编写测试、修复构建
|
||||
- S5 安全基线体(SecurityGuard): 检查守卫、跨租户(site_id)隔离、敏感信息暴露(开发中与提测前各执行一次)
|
||||
- S6 质量门禁体(QualityGate): 聚合 ESLint/TS/覆盖率/e2e 结果,低于阈值阻断合并
|
||||
- S7 规范审计体(Auditor): 按清单逐项核查,出具差异报告与修复项
|
||||
- S8 上线管控体(Release): 构建、变更说明、灰度计划与回滚预案
|
||||
- S9 性能优化体(PerfTuner): 建议缓存/异步化/批处理,识别大对象传输与 N+1(开发后期与上线后持续执行)
|
||||
|
||||
### 串联流程(带顺序)
|
||||
1) S1 Analyzer
|
||||
- 输入: 业务需求/接口变更/对齐 PHP 的说明
|
||||
- 输出: 模块划分、路由表、DTO、实体字段清单、与 DB/ThinkPHP 对照
|
||||
|
||||
2) S2 Architect
|
||||
- 校验: 模块目录、分层(Application/Core/Infrastructure)、依赖方向(App→Common→Core→Vendor)
|
||||
- 输出: 设计说明、端口(Repository/Provider)定义、删除/迁移建议
|
||||
|
||||
3) S3 InfraOperator
|
||||
- 接入: Kafka/Redis/队列/事务的工程化接入与配置
|
||||
- 产物: 接入差异与示例代码(见 integration.md),健康检查/配置项校验清单
|
||||
|
||||
4) S4 Developer
|
||||
- 实现: Controller 仅路由+DTO校验;AppService 编排;Core 规则;Infra 实现;Entity 对齐 DB
|
||||
- 接入: 守卫(RBAC)、Pipes(JSON/Timestamp)、拦截器(请求日志)、事件与队列
|
||||
- 测试: 单测/集成/e2e,构建通过
|
||||
|
||||
5) S5 SecurityGuard(第一次,开发阶段)
|
||||
- 检查: 控制器守卫、site_id 隔离、敏感字段输出、配置权限
|
||||
|
||||
6) S6 QualityGate(CI 阶段)
|
||||
- 指标: ESLint/TS 无报错;覆盖率≥阈值;e2e 关键路径通过
|
||||
- 动作: 不达标阻断合并
|
||||
|
||||
7) S7 Auditor(提测前)
|
||||
- 检查: 规范清单(见 checklists.md),字段/命名/路由/守卫/事务/队列/事件 与 PHP/DB 对齐
|
||||
- 产物: 差异报告与修复任务
|
||||
|
||||
8) S5 SecurityGuard(第二次,提测前)
|
||||
- 复检: 重要接口的鉴权/越权/敏感输出
|
||||
|
||||
9) S9 PerfTuner(并行/持续)
|
||||
- 建议: 缓存、异步化、批量化、索引与查询优化;识别 N+1、大对象传输
|
||||
|
||||
10) S8 Release
|
||||
- 产出: 变更日志、部署步骤、数据迁移脚本、回滚预案
|
||||
|
||||
### 关键约束
|
||||
- 与 PHP 业务/数据100%一致;与 NestJS 规范100%匹配
|
||||
- 禁止创建 DB 不存在字段;`sys_config.value(JSON)` 统一
|
||||
- 管理端路由 `/adminapi`,前台 `/api`;统一守卫与响应格式
|
||||
|
||||
### 基础能力检查点(Kafka / Redis / 队列 / 事务)
|
||||
- 事务: 仅在 Application 开启;多仓储共享同一 EntityManager;Core 不直接操作事务对象
|
||||
- 队列: 用例完成后入队;载荷仅传关键 ID;处理器在 Infrastructure;按队列名分域
|
||||
- 事件: 统一用 DomainEventService;事件名 `domain.aggregate.action`;默认 DB Outbox,可切 Kafka
|
||||
- Redis: 短缓存配置读取、上传限流/防刷(计数器)、幂等(SETNX+TTL)
|
||||
|
||||
### 命名与对齐
|
||||
- PHP 业务命名优先(不违反 Nest/TS 规范前提下),包括服务方法、DTO 字段、配置键
|
||||
- Nest 特有类型按规范命名:`*.module.ts`、`*.controller.ts`、`*.app.service.ts`、`*.core.service.ts`
|
||||
|
||||
### 核心链接
|
||||
- 模块映射: `./mapping.md`
|
||||
- 能力集成: `./integration.md`
|
||||
- 规则与清单: `./rules.md`、`./checklists.md`
|
||||
|
||||
### 执行与验收(CI/PR 建议)
|
||||
- PR 必须通过: build、单测/集成/e2e
|
||||
- 审计体根据 `checklists.md` 自动评论差异(字段/命名/路由/守卫/事务/队列/事件)
|
||||
- 安全基线: 管理端控制器统一 `JwtAuthGuard + RolesGuard`;/adminapi 与 /api 路由前缀
|
||||
|
||||
### 目录职能速查(防误用)
|
||||
- common/(框架通用服务层)
|
||||
- 放可被业务复用的通用功能:用户/权限/菜单/上传/通知/设置等模块
|
||||
- 内部模块按 Controller / Application / Core / Infrastructure / Entities / DTO 分层
|
||||
- 禁止依赖 App 层;允许依赖 core/, config/, vendor/
|
||||
- config/(配置与适配)
|
||||
- 环境变量、数据库/HTTP/安全/队列/第三方等配置模块与注入工厂
|
||||
- 仅存放配置与适配代码,不放业务逻辑
|
||||
- core/(核心基础设施与通用规则)
|
||||
- 通用规则/策略与仓储接口(Core 层),以及全局基础设施(如队列、事件、健康、拦截器)
|
||||
- 不直接依赖业务模块;面向 common/app 提供能力
|
||||
- vendor/(第三方适配层)
|
||||
- 外部服务适配:存储/支付/短信/HTTP/Kafka/Redis 等 Provider
|
||||
- 通过接口注入到 Infrastructure 或 Application,避免在 Controller 直接使用
|
||||
- lang/(多语言)
|
||||
- 多语言资源与语言包,供接口/异常/文案统一输出
|
||||
- 智能体在涉及文案/错误消息时,优先调用多语言键值而非写死文本
|
||||
- test/(测试)
|
||||
- 单元/集成/e2e 测试,包含关键业务与基础能力(事务/队列/事件/权限)覆盖
|
||||
|
||||
- PR 必须通过测试基线,质量门禁体(QualityGate)据此决策
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
**主要数据库文件:**
|
||||
- **WWJCloud 主数据库**:[/g:/wwjcloud-nestjs/sql/wwjcloud.sql](../sql/wwjcloud.sql)
|
||||
- **WWJAuth 认证数据库**:[/g:/wwjcloud-nestjs/sql/wwjauth.sql](../sql/wwjauth.sql)
|
||||
|
||||
**核心表结构:**
|
||||
- `sys_user` - 系统用户表 (uid, username, password, real_name, last_ip, last_time, create_time, login_num, status, delete_time)
|
||||
|
||||
@@ -297,8 +297,8 @@ JSON 字段 → 使用 @Column('json')
|
||||
[ ] 查询语法使用TypeORM操作符
|
||||
✅ 开发后检查
|
||||
[ ] npm run build 无错误
|
||||
[ ] 与PHP项目字段名100%一致
|
||||
[ ] 业务逻辑与PHP保持一致
|
||||
[ ] 与PHP项目字段名100%一致,并列出php的model层,数据库,nestjs字段名清单。
|
||||
[ ] 业务逻辑与PHP保持一致,并列出,php与nestjs的,命名规范对比清单
|
||||
[ ] 类型安全无警告
|
||||
🚀 简化处理步骤
|
||||
三步快速修复法
|
||||
@@ -310,4 +310,5 @@ JSON 字段 → 使用 @Column('json')
|
||||
中优先级:类型不匹配(遵守NestJS规范)
|
||||
低优先级:语法优化(保持代码整洁)
|
||||
一句话总结
|
||||
"用NestJS的语法,写PHP的逻辑,保持数据库的一致性"
|
||||
"
|
||||
用NestJS的语法,写PHP的逻辑,保持数据库的一致性"
|
||||
1
admin/.gitignore
vendored
1
admin/.gitignore
vendored
@@ -49,4 +49,3 @@ vite.config.ts.*
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
|
||||
@@ -140,12 +140,8 @@ pnpm build
|
||||
|
||||
## 貢献者
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -140,12 +140,8 @@ If you think this project is helpful to you, you can help the author buy a cup o
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -140,12 +140,8 @@ pnpm build
|
||||
|
||||
## 贡献者
|
||||
|
||||
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_CODES } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_USERS } from '~/utils/mock-data';
|
||||
import {
|
||||
forbiddenResponse,
|
||||
useResponseError,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { password, username } = await readBody(event);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const refreshToken = getRefreshTokenFromCookie(event);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import {
|
||||
clearRefreshTokenCookie,
|
||||
getRefreshTokenFromCookie,
|
||||
setRefreshTokenCookie,
|
||||
} from '~/utils/cookie-utils';
|
||||
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_USERS } from '~/utils/mock-data';
|
||||
import { verifyRefreshToken } from '~/utils/jwt-utils';
|
||||
import { forbiddenResponse } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { eventHandler, setHeader } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENUS } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { eventHandler, getQuery, setResponseStatus } from 'h3';
|
||||
import { useResponseError } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const { status } = getQuery(event);
|
||||
setResponseStatus(event, Number(status));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const namesMap: Record<string, any> = {};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const pathMap: Record<string, any> = { '/': 0 };
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { eventHandler, getQuery } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
usePageResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
@@ -49,69 +44,30 @@ export default eventHandler(async (event) => {
|
||||
await sleep(600);
|
||||
|
||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
||||
// 规范化分页参数,处理 string[]
|
||||
const pageRaw = Array.isArray(page) ? page[0] : page;
|
||||
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
|
||||
const pageNumber = Math.max(
|
||||
1,
|
||||
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
|
||||
);
|
||||
const pageSizeNumber = Math.min(
|
||||
100,
|
||||
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
|
||||
);
|
||||
const listData = structuredClone(mockData);
|
||||
|
||||
// 规范化 query 入参,兼容 string[]
|
||||
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
|
||||
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
|
||||
// 检查 sortBy 是否是 listData 元素的合法属性键
|
||||
if (
|
||||
typeof sortKeyRaw === 'string' &&
|
||||
listData[0] &&
|
||||
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
|
||||
) {
|
||||
// 定义数组元素的类型
|
||||
type ItemType = (typeof listData)[0];
|
||||
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
|
||||
const isDesc = sortOrderRaw === 'desc';
|
||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
||||
listData.sort((a, b) => {
|
||||
const aValue = a[sortKey] as unknown;
|
||||
const bValue = b[sortKey] as unknown;
|
||||
|
||||
let result = 0;
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
result = aValue - bValue;
|
||||
} else if (aValue instanceof Date && bValue instanceof Date) {
|
||||
result = aValue.getTime() - bValue.getTime();
|
||||
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
||||
if (aValue === bValue) {
|
||||
result = 0;
|
||||
} else {
|
||||
result = aValue ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
const aStr = String(aValue);
|
||||
const bStr = String(bValue);
|
||||
const aNum = Number(aStr);
|
||||
const bNum = Number(bStr);
|
||||
result =
|
||||
Number.isFinite(aNum) && Number.isFinite(bNum)
|
||||
? aNum - bNum
|
||||
: aStr.localeCompare(bStr, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base',
|
||||
});
|
||||
}
|
||||
|
||||
return isDesc ? -result : result;
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(
|
||||
String(pageNumber),
|
||||
String(pageSizeNumber),
|
||||
listData,
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(a[sortBy as string]) -
|
||||
Number.parseFloat(b[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(b[sortBy as string]) -
|
||||
Number.parseFloat(a[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => 'Test get handler');
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => 'Test post handler');
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { eventHandler } from 'h3';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
return `
|
||||
<h1>Hello Vben Admin</h1>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import { deleteCookie, getCookie, setCookie } from 'h3';
|
||||
|
||||
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
|
||||
deleteCookie(event, 'jwt', {
|
||||
httpOnly: true,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import type { UserInfo } from './mock-data';
|
||||
|
||||
import { getHeader } from 'h3';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { MOCK_USERS } from './mock-data';
|
||||
import { UserInfo } from './mock-data';
|
||||
|
||||
// TODO: Replace with your own secret key
|
||||
const ACCESS_TOKEN_SECRET = 'access_token_secret';
|
||||
@@ -34,22 +31,12 @@ export function verifyAccessToken(
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenParts = authHeader.split(' ');
|
||||
if (tokenParts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
const token = tokenParts[1] as string;
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
ACCESS_TOKEN_SECRET,
|
||||
) as unknown as UserPayload;
|
||||
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
|
||||
|
||||
const username = decoded.username;
|
||||
const user = MOCK_USERS.find((item) => item.username === username);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const { password: _pwd, ...userinfo } = user;
|
||||
return userinfo;
|
||||
} catch {
|
||||
@@ -63,12 +50,7 @@ export function verifyRefreshToken(
|
||||
try {
|
||||
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
|
||||
const username = decoded.username;
|
||||
const user = MOCK_USERS.find(
|
||||
(item) => item.username === username,
|
||||
) as UserInfo;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
const user = MOCK_USERS.find((item) => item.username === username);
|
||||
const { password: _pwd, ...userinfo } = user;
|
||||
return userinfo;
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { EventHandlerRequest, H3Event } from 'h3';
|
||||
|
||||
import { setResponseStatus } from 'h3';
|
||||
|
||||
export function useResponseSuccess<T = any>(data: T) {
|
||||
return {
|
||||
code: 0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 端口号
|
||||
VITE_PORT=5777
|
||||
VITE_PORT=5666
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
20
admin/apps/web-antd/__tests__/e2e/auth-login.spec.ts
Normal file
20
admin/apps/web-antd/__tests__/e2e/auth-login.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { authLogin } from './common/auth';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Auth Login Page Tests', () => {
|
||||
test('check title and page elements', async ({ page }) => {
|
||||
// 获取页面标题并断言标题包含 'Vben Admin'
|
||||
const title = await page.title();
|
||||
expect(title).toContain('Vben Admin');
|
||||
});
|
||||
|
||||
// 测试用例: 成功登录
|
||||
test('should successfully login with valid credentials', async ({ page }) => {
|
||||
await authLogin(page);
|
||||
});
|
||||
});
|
||||
46
admin/apps/web-antd/__tests__/e2e/common/auth.ts
Normal file
46
admin/apps/web-antd/__tests__/e2e/common/auth.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
export async function authLogin(page: Page) {
|
||||
// 确保登录表单正常
|
||||
const usernameInput = await page.locator(`input[name='username']`);
|
||||
await expect(usernameInput).toBeVisible();
|
||||
|
||||
const passwordInput = await page.locator(`input[name='password']`);
|
||||
await expect(passwordInput).toBeVisible();
|
||||
|
||||
const sliderCaptcha = await page.locator(`div[name='captcha']`);
|
||||
const sliderCaptchaAction = await page.locator(`div[name='captcha-action']`);
|
||||
await expect(sliderCaptcha).toBeVisible();
|
||||
await expect(sliderCaptchaAction).toBeVisible();
|
||||
|
||||
// 拖动验证码滑块
|
||||
// 获取拖动按钮的位置
|
||||
const sliderCaptchaBox = await sliderCaptcha.boundingBox();
|
||||
if (!sliderCaptchaBox) throw new Error('滑块未找到');
|
||||
|
||||
const actionBoundingBox = await sliderCaptchaAction.boundingBox();
|
||||
if (!actionBoundingBox) throw new Error('要拖动的按钮未找到');
|
||||
|
||||
// 计算起始位置和目标位置
|
||||
const startX = actionBoundingBox.x + actionBoundingBox.width / 2; // div 中心的 x 坐标
|
||||
const startY = actionBoundingBox.y + actionBoundingBox.height / 2; // div 中心的 y 坐标
|
||||
|
||||
const targetX = startX + sliderCaptchaBox.width + actionBoundingBox.width; // 向右拖动容器的宽度
|
||||
const targetY = startY; // y 坐标保持不变
|
||||
|
||||
// 模拟鼠标拖动
|
||||
await page.mouse.move(startX, startY); // 移动到 action 的中心
|
||||
await page.mouse.down(); // 按下鼠标
|
||||
await page.mouse.move(targetX, targetY, { steps: 20 }); // 拖动到目标位置
|
||||
await page.mouse.up(); // 松开鼠标
|
||||
|
||||
// 在拖动后进行断言,检查action是否在预期位置,
|
||||
const newActionBoundingBox = await sliderCaptchaAction.boundingBox();
|
||||
expect(newActionBoundingBox?.x).toBeGreaterThan(actionBoundingBox.x);
|
||||
|
||||
// 到这里已经校验成功,点击进行登录
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByRole('button', { name: 'login' }).click();
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?97352b16ed2df8c3860cf5a1a65fb4dd';
|
||||
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.5.9",
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.8",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "apps/web-ele"
|
||||
"directory": "apps/web-antd"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
@@ -26,6 +26,8 @@
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/vue-query": "catalog:",
|
||||
"@vben-core/menu-ui": "workspace:*",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
@@ -41,13 +43,14 @@
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"ant-design-vue": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"element-plus": "catalog:",
|
||||
"json-bigint": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unplugin-element-plus": "catalog:"
|
||||
"@types/json-bigint": "catalog:"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
207
admin/apps/web-antd/src/adapter/component/index.ts
Normal file
207
admin/apps/web-antd/src/adapter/component/index.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/date-picker'),
|
||||
);
|
||||
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||
const InputNumber = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input-number'),
|
||||
);
|
||||
const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
const Mentions = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/mentions'),
|
||||
);
|
||||
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||
const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/time-picker'),
|
||||
);
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
// const publicApi: Recordable<any> = {};
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
// const instance = getCurrentInstance();
|
||||
// instance?.proxy?.$nextTick(() => {
|
||||
// for (const key in innerRef.value) {
|
||||
// if (typeof innerRef.value[key] === 'function') {
|
||||
// publicApi[key] = innerRef.value[key];
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
@@ -11,18 +11,25 @@ import { $t } from '@vben/locales';
|
||||
async function initSetupVbenForm() {
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
@@ -36,6 +43,5 @@ async function initSetupVbenForm() {
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupVbenForm, useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
297
admin/apps/web-antd/src/adapter/vxe-table.ts
Normal file
297
admin/apps/web-antd/src/adapter/vxe-table.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $te } from '@vben/locales';
|
||||
import {
|
||||
setupVbenVxeTable,
|
||||
useVbenVxeGrid as useGrid,
|
||||
} from '@vben/plugins/vxe-table';
|
||||
import { get, isFunction, isString } from '@vben/utils';
|
||||
|
||||
import { objectOmit } from '@vueuse/core';
|
||||
import { Button, Image, Popconfirm, Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
minHeight: 180,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: '',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 解决vxeTable在热更新时可能会出错的问题
|
||||
*/
|
||||
vxeUI.renderer.forEach((_item, key) => {
|
||||
if (key.startsWith('Cell')) {
|
||||
vxeUI.renderer.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 单元格渲染: Tag
|
||||
vxeUI.renderer.add('CellTag', {
|
||||
renderTableDefault({ options, props }, { column, row }) {
|
||||
const value = get(row, column.field);
|
||||
const tagOptions = options ?? [
|
||||
{ color: 'success', label: $t('common.enabled'), value: 1 },
|
||||
{ color: 'error', label: $t('common.disabled'), value: 0 },
|
||||
];
|
||||
const tagItem = tagOptions.find((item) => item.value === value);
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
...props,
|
||||
...objectOmit(tagItem ?? {}, ['label']),
|
||||
},
|
||||
{ default: () => tagItem?.label ?? value },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellSwitch', {
|
||||
renderTableDefault({ attrs, props }, { column, row }) {
|
||||
const loadingKey = `__loading_${column.field}`;
|
||||
const finallyProps = {
|
||||
checkedChildren: $t('common.enabled'),
|
||||
checkedValue: 1,
|
||||
unCheckedChildren: $t('common.disabled'),
|
||||
unCheckedValue: 0,
|
||||
...props,
|
||||
checked: row[column.field],
|
||||
loading: row[loadingKey] ?? false,
|
||||
'onUpdate:checked': onChange,
|
||||
};
|
||||
async function onChange(newVal: any) {
|
||||
row[loadingKey] = true;
|
||||
try {
|
||||
const result = await attrs?.beforeChange?.(newVal, row);
|
||||
if (result !== false) {
|
||||
row[column.field] = newVal;
|
||||
}
|
||||
} finally {
|
||||
row[loadingKey] = false;
|
||||
}
|
||||
}
|
||||
return h(Switch, finallyProps);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 注册表格的操作按钮渲染器
|
||||
*/
|
||||
vxeUI.renderer.add('CellOperation', {
|
||||
renderTableDefault({ attrs, options, props }, { column, row }) {
|
||||
const defaultProps = { size: 'small', type: 'link', ...props };
|
||||
let align = 'end';
|
||||
switch (column.align) {
|
||||
case 'center': {
|
||||
align = 'center';
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
align = 'start';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
align = 'end';
|
||||
break;
|
||||
}
|
||||
}
|
||||
const presets: Recordable<Recordable<any>> = {
|
||||
delete: {
|
||||
danger: true,
|
||||
text: $t('common.delete'),
|
||||
},
|
||||
edit: {
|
||||
text: $t('common.edit'),
|
||||
},
|
||||
};
|
||||
const operations: Array<Recordable<any>> = (
|
||||
options || ['edit', 'delete']
|
||||
)
|
||||
.map((opt) => {
|
||||
if (isString(opt)) {
|
||||
return presets[opt]
|
||||
? { code: opt, ...presets[opt], ...defaultProps }
|
||||
: {
|
||||
code: opt,
|
||||
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
|
||||
...defaultProps,
|
||||
};
|
||||
} else {
|
||||
return { ...defaultProps, ...presets[opt.code], ...opt };
|
||||
}
|
||||
})
|
||||
.map((opt) => {
|
||||
const optBtn: Recordable<any> = {};
|
||||
Object.keys(opt).forEach((key) => {
|
||||
optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
|
||||
});
|
||||
return optBtn;
|
||||
})
|
||||
.filter((opt) => opt.show !== false);
|
||||
|
||||
function renderBtn(opt: Recordable<any>, listen = true) {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
...props,
|
||||
...opt,
|
||||
icon: undefined,
|
||||
onClick: listen
|
||||
? () =>
|
||||
attrs?.onClick?.({
|
||||
code: opt.code,
|
||||
row,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
const content = [];
|
||||
if (opt.icon) {
|
||||
content.push(
|
||||
h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
|
||||
);
|
||||
}
|
||||
content.push(opt.text);
|
||||
return content;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function renderConfirm(opt: Recordable<any>) {
|
||||
let viewportWrapper: HTMLElement | null = null;
|
||||
return h(
|
||||
Popconfirm,
|
||||
{
|
||||
/**
|
||||
* 当popconfirm用在固定列中时,将固定列作为弹窗的容器时可能会因为固定列较窄而无法容纳弹窗
|
||||
* 将表格主体区域作为弹窗容器时又会因为固定列的层级较高而遮挡弹窗
|
||||
* 将body或者表格视口区域作为弹窗容器时又会导致弹窗无法跟随表格滚动。
|
||||
* 鉴于以上各种情况,一种折中的解决方案是弹出层展示时,禁止操作表格的滚动条。
|
||||
* 这样既解决了弹窗的遮挡问题,又不至于让弹窗随着表格的滚动而跑出视口区域。
|
||||
*/
|
||||
getPopupContainer(el) {
|
||||
viewportWrapper = el.closest('.vxe-table--viewport-wrapper');
|
||||
return document.body;
|
||||
},
|
||||
placement: 'topLeft',
|
||||
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
|
||||
...props,
|
||||
...opt,
|
||||
icon: undefined,
|
||||
onOpenChange: (open: boolean) => {
|
||||
// 当弹窗打开时,禁止表格的滚动
|
||||
if (open) {
|
||||
viewportWrapper?.style.setProperty('pointer-events', 'none');
|
||||
} else {
|
||||
viewportWrapper?.style.removeProperty('pointer-events');
|
||||
}
|
||||
},
|
||||
onConfirm: () => {
|
||||
attrs?.onClick?.({
|
||||
code: opt.code,
|
||||
row,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => renderBtn({ ...opt }, false),
|
||||
description: () =>
|
||||
h(
|
||||
'div',
|
||||
{ class: 'truncate' },
|
||||
$t('ui.actionMessage.deleteConfirm', [
|
||||
row[attrs?.nameField || 'name'],
|
||||
]),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const btns = operations.map((opt) =>
|
||||
opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
|
||||
);
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex table-operations',
|
||||
style: { justifyContent: align },
|
||||
},
|
||||
btns,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export const useVbenVxeGrid = <T extends Record<string, any>>(
|
||||
...rest: Parameters<typeof useGrid<T, ComponentType>>
|
||||
) => useGrid<T, ComponentType>(...rest);
|
||||
|
||||
export type OnActionClickParams<T = Recordable<any>> = {
|
||||
code: string;
|
||||
row: T;
|
||||
};
|
||||
export type OnActionClickFn<T = Recordable<any>> = (
|
||||
params: OnActionClickParams<T>,
|
||||
) => void;
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
@@ -22,23 +22,29 @@ export namespace AuthApi {
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data, {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>(
|
||||
'/auth/refresh',
|
||||
null,
|
||||
{
|
||||
withCredentials: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/auth/logout', {
|
||||
return baseRequestClient.post('/auth/logout', null, {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
28
admin/apps/web-antd/src/api/examples/download.ts
Normal file
28
admin/apps/web-antd/src/api/examples/download.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RequestResponse } from '@vben/request';
|
||||
|
||||
import { requestClient } from '../request';
|
||||
|
||||
/**
|
||||
* 下载文件,获取Blob
|
||||
* @returns Blob
|
||||
*/
|
||||
async function downloadFile1() {
|
||||
return requestClient.download<Blob>(
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件,获取完整的Response
|
||||
* @returns RequestResponse<Blob>
|
||||
*/
|
||||
async function downloadFile2() {
|
||||
return requestClient.download<RequestResponse<Blob>>(
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
{
|
||||
responseReturn: 'raw',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { downloadFile1, downloadFile2 };
|
||||
2
admin/apps/web-antd/src/api/examples/index.ts
Normal file
2
admin/apps/web-antd/src/api/examples/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './status';
|
||||
export * from './table';
|
||||
10
admin/apps/web-antd/src/api/examples/json-bigint.ts
Normal file
10
admin/apps/web-antd/src/api/examples/json-bigint.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
*/
|
||||
async function getBigIntData() {
|
||||
return requestClient.get('/demo/bigint');
|
||||
}
|
||||
|
||||
export { getBigIntData };
|
||||
19
admin/apps/web-antd/src/api/examples/params.ts
Normal file
19
admin/apps/web-antd/src/api/examples/params.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 发起数组请求
|
||||
*/
|
||||
async function getParamsData(
|
||||
params: Recordable<any>,
|
||||
type: 'brackets' | 'comma' | 'indices' | 'repeat',
|
||||
) {
|
||||
return requestClient.get('/status', {
|
||||
params,
|
||||
paramsSerializer: type,
|
||||
responseReturn: 'raw',
|
||||
});
|
||||
}
|
||||
|
||||
export { getParamsData };
|
||||
10
admin/apps/web-antd/src/api/examples/status.ts
Normal file
10
admin/apps/web-antd/src/api/examples/status.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 模拟任意状态码
|
||||
*/
|
||||
async function getMockStatusApi(status: string) {
|
||||
return requestClient.get('/status', { params: { status } });
|
||||
}
|
||||
|
||||
export { getMockStatusApi };
|
||||
18
admin/apps/web-antd/src/api/examples/table.ts
Normal file
18
admin/apps/web-antd/src/api/examples/table.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace DemoTableApi {
|
||||
export interface PageFetchParams {
|
||||
[key: string]: any;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取示例表格数据
|
||||
*/
|
||||
async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
|
||||
return requestClient.get('/table/list', { params });
|
||||
}
|
||||
|
||||
export { getExampleTableApi };
|
||||
25
admin/apps/web-antd/src/api/examples/upload.ts
Normal file
25
admin/apps/web-antd/src/api/examples/upload.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
interface UploadFileParams {
|
||||
file: File;
|
||||
onError?: (error: Error) => void;
|
||||
onProgress?: (progress: { percent: number }) => void;
|
||||
onSuccess?: (data: any, file: File) => void;
|
||||
}
|
||||
export async function upload_file({
|
||||
file,
|
||||
onError,
|
||||
onProgress,
|
||||
onSuccess,
|
||||
}: UploadFileParams) {
|
||||
try {
|
||||
onProgress?.({ percent: 0 });
|
||||
|
||||
const data = await requestClient.upload('/upload', { file });
|
||||
|
||||
onProgress?.({ percent: 100 });
|
||||
onSuccess?.(data, file);
|
||||
} catch (error) {
|
||||
onError?.(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
3
admin/apps/web-antd/src/api/index.ts
Normal file
3
admin/apps/web-antd/src/api/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './core';
|
||||
export * from './examples';
|
||||
export * from './system';
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
import type { AxiosResponseHeaders, RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { message } from 'ant-design-vue';
|
||||
import JSONBigInt from 'json-bigint';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
@@ -25,6 +27,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
transformResponse: (data: any, header: AxiosResponseHeaders) => {
|
||||
// storeAsString指示将BigInt存储为字符串,设为false则会存储为内置的BigInt类型
|
||||
return header.getContentType()?.toString().includes('application/json')
|
||||
? cloneDeep(
|
||||
JSONBigInt({ storeAsString: true, strict: true }).parse(data),
|
||||
)
|
||||
: data;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -99,7 +109,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||||
// 如果没有错误信息,则会根据状态码进行提示
|
||||
ElMessage.error(errorMessage || msg);
|
||||
message.error(errorMessage || msg);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -111,3 +121,9 @@ export const requestClient = createRequestClient(apiURL, {
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
||||
export interface PageFetchParams {
|
||||
[key: string]: any;
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
54
admin/apps/web-antd/src/api/system/dept.ts
Normal file
54
admin/apps/web-antd/src/api/system/dept.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemDeptApi {
|
||||
export interface SystemDept {
|
||||
[key: string]: any;
|
||||
children?: SystemDept[];
|
||||
id: string;
|
||||
name: string;
|
||||
remark?: string;
|
||||
status: 0 | 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表数据
|
||||
*/
|
||||
async function getDeptList() {
|
||||
return requestClient.get<Array<SystemDeptApi.SystemDept>>(
|
||||
'/system/dept/list',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
* @param data 部门数据
|
||||
*/
|
||||
async function createDept(
|
||||
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.post('/system/dept', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
*
|
||||
* @param id 部门 ID
|
||||
* @param data 部门数据
|
||||
*/
|
||||
async function updateDept(
|
||||
id: string,
|
||||
data: Omit<SystemDeptApi.SystemDept, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/dept/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param id 部门 ID
|
||||
*/
|
||||
async function deleteDept(id: string) {
|
||||
return requestClient.delete(`/system/dept/${id}`);
|
||||
}
|
||||
|
||||
export { createDept, deleteDept, getDeptList, updateDept };
|
||||
3
admin/apps/web-antd/src/api/system/index.ts
Normal file
3
admin/apps/web-antd/src/api/system/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './dept';
|
||||
export * from './menu';
|
||||
export * from './role';
|
||||
158
admin/apps/web-antd/src/api/system/menu.ts
Normal file
158
admin/apps/web-antd/src/api/system/menu.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemMenuApi {
|
||||
/** 徽标颜色集合 */
|
||||
export const BadgeVariants = [
|
||||
'default',
|
||||
'destructive',
|
||||
'primary',
|
||||
'success',
|
||||
'warning',
|
||||
] as const;
|
||||
/** 徽标类型集合 */
|
||||
export const BadgeTypes = ['dot', 'normal'] as const;
|
||||
/** 菜单类型集合 */
|
||||
export const MenuTypes = [
|
||||
'catalog',
|
||||
'menu',
|
||||
'embedded',
|
||||
'link',
|
||||
'button',
|
||||
] as const;
|
||||
/** 系统菜单 */
|
||||
export interface SystemMenu {
|
||||
[key: string]: any;
|
||||
/** 后端权限标识 */
|
||||
authCode: string;
|
||||
/** 子级 */
|
||||
children?: SystemMenu[];
|
||||
/** 组件 */
|
||||
component?: string;
|
||||
/** 菜单ID */
|
||||
id: string;
|
||||
/** 菜单元数据 */
|
||||
meta?: {
|
||||
/** 激活时显示的图标 */
|
||||
activeIcon?: string;
|
||||
/** 作为路由时,需要激活的菜单的Path */
|
||||
activePath?: string;
|
||||
/** 固定在标签栏 */
|
||||
affixTab?: boolean;
|
||||
/** 在标签栏固定的顺序 */
|
||||
affixTabOrder?: number;
|
||||
/** 徽标内容(当徽标类型为normal时有效) */
|
||||
badge?: string;
|
||||
/** 徽标类型 */
|
||||
badgeType?: (typeof BadgeTypes)[number];
|
||||
/** 徽标颜色 */
|
||||
badgeVariants?: (typeof BadgeVariants)[number];
|
||||
/** 在菜单中隐藏下级 */
|
||||
hideChildrenInMenu?: boolean;
|
||||
/** 在面包屑中隐藏 */
|
||||
hideInBreadcrumb?: boolean;
|
||||
/** 在菜单中隐藏 */
|
||||
hideInMenu?: boolean;
|
||||
/** 在标签栏中隐藏 */
|
||||
hideInTab?: boolean;
|
||||
/** 菜单图标 */
|
||||
icon?: string;
|
||||
/** 内嵌Iframe的URL */
|
||||
iframeSrc?: string;
|
||||
/** 是否缓存页面 */
|
||||
keepAlive?: boolean;
|
||||
/** 外链页面的URL */
|
||||
link?: string;
|
||||
/** 同一个路由最大打开的标签数 */
|
||||
maxNumOfOpenTab?: number;
|
||||
/** 无需基础布局 */
|
||||
noBasicLayout?: boolean;
|
||||
/** 是否在新窗口打开 */
|
||||
openInNewWindow?: boolean;
|
||||
/** 菜单排序 */
|
||||
order?: number;
|
||||
/** 额外的路由参数 */
|
||||
query?: Recordable<any>;
|
||||
/** 菜单标题 */
|
||||
title?: string;
|
||||
};
|
||||
/** 菜单名称 */
|
||||
name: string;
|
||||
/** 路由路径 */
|
||||
path: string;
|
||||
/** 父级ID */
|
||||
pid: string;
|
||||
/** 重定向 */
|
||||
redirect?: string;
|
||||
/** 菜单类型 */
|
||||
type: (typeof MenuTypes)[number];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单数据列表
|
||||
*/
|
||||
async function getMenuList() {
|
||||
return requestClient.get<Array<SystemMenuApi.SystemMenu>>(
|
||||
'/system/menu/list',
|
||||
);
|
||||
}
|
||||
|
||||
async function isMenuNameExists(
|
||||
name: string,
|
||||
id?: SystemMenuApi.SystemMenu['id'],
|
||||
) {
|
||||
return requestClient.get<boolean>('/system/menu/name-exists', {
|
||||
params: { id, name },
|
||||
});
|
||||
}
|
||||
|
||||
async function isMenuPathExists(
|
||||
path: string,
|
||||
id?: SystemMenuApi.SystemMenu['id'],
|
||||
) {
|
||||
return requestClient.get<boolean>('/system/menu/path-exists', {
|
||||
params: { id, path },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建菜单
|
||||
* @param data 菜单数据
|
||||
*/
|
||||
async function createMenu(
|
||||
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.post('/system/menu', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单
|
||||
*
|
||||
* @param id 菜单 ID
|
||||
* @param data 菜单数据
|
||||
*/
|
||||
async function updateMenu(
|
||||
id: string,
|
||||
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/menu/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param id 菜单 ID
|
||||
*/
|
||||
async function deleteMenu(id: string) {
|
||||
return requestClient.delete(`/system/menu/${id}`);
|
||||
}
|
||||
|
||||
export {
|
||||
createMenu,
|
||||
deleteMenu,
|
||||
getMenuList,
|
||||
isMenuNameExists,
|
||||
isMenuPathExists,
|
||||
updateMenu,
|
||||
};
|
||||
55
admin/apps/web-antd/src/api/system/role.ts
Normal file
55
admin/apps/web-antd/src/api/system/role.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemRoleApi {
|
||||
export interface SystemRole {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
remark?: string;
|
||||
status: 0 | 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色列表数据
|
||||
*/
|
||||
async function getRoleList(params: Recordable<any>) {
|
||||
return requestClient.get<Array<SystemRoleApi.SystemRole>>(
|
||||
'/system/role/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
* @param data 角色数据
|
||||
*/
|
||||
async function createRole(data: Omit<SystemRoleApi.SystemRole, 'id'>) {
|
||||
return requestClient.post('/system/role', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param data 角色数据
|
||||
*/
|
||||
async function updateRole(
|
||||
id: string,
|
||||
data: Omit<SystemRoleApi.SystemRole, 'id'>,
|
||||
) {
|
||||
return requestClient.put(`/system/role/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param id 角色 ID
|
||||
*/
|
||||
async function deleteRole(id: string) {
|
||||
return requestClient.delete(`/system/role/${id}`);
|
||||
}
|
||||
|
||||
export { createRole, deleteRole, getRoleList, updateRole };
|
||||
39
admin/apps/web-antd/src/app.vue
Normal file
39
admin/apps/web-antd/src/app.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useAntdDesignTokens } from '@vben/hooks';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
|
||||
import { App, ConfigProvider, theme } from 'ant-design-vue';
|
||||
|
||||
import { antdLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const { tokens } = useAntdDesignTokens();
|
||||
|
||||
const tokenTheme = computed(() => {
|
||||
const algorithm = isDark.value
|
||||
? [theme.darkAlgorithm]
|
||||
: [theme.defaultAlgorithm];
|
||||
|
||||
// antd 紧凑模式算法
|
||||
if (preferences.app.compact) {
|
||||
algorithm.push(theme.compactAlgorithm);
|
||||
}
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
token: tokens,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||
<App>
|
||||
<RouterView />
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
@@ -5,17 +5,16 @@ import { registerLoadingDirective } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/ele';
|
||||
import '@vben/styles/antd';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { ElLoading } from 'element-plus';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
@@ -24,22 +23,20 @@ async function bootstrap(namespace: string) {
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 2000,
|
||||
// zIndex: 1020,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
// 注册Vben提供的v-loading和v-spinning指令
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
@@ -59,6 +56,10 @@ async function bootstrap(namespace: string) {
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置@tanstack/vue-query
|
||||
const { VueQueryPlugin } = await import('@tanstack/vue-query');
|
||||
app.use(VueQueryPlugin);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
@@ -8,6 +8,7 @@ import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
const clickLogo = () => {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,6 +17,7 @@ const logo = computed(() => preferences.logo.source);
|
||||
:logo="logo"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
:click-logo="clickLogo"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
@@ -14,13 +14,26 @@ import {
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { useAccessStore, useTabbarStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const { setMenuList } = useTabbarStore();
|
||||
setMenuList([
|
||||
'close',
|
||||
'affix',
|
||||
'maximize',
|
||||
'reload',
|
||||
'open-in-new-window',
|
||||
'close-left',
|
||||
'close-right',
|
||||
'close-other',
|
||||
'close-all',
|
||||
]);
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
@@ -105,6 +118,9 @@ function handleNoticeClear() {
|
||||
function handleMakeAll() {
|
||||
notifications.value.forEach((item) => (item.isRead = true));
|
||||
}
|
||||
|
||||
function handleClickLogo() {}
|
||||
|
||||
watch(
|
||||
() => preferences.app.watermark,
|
||||
async (enable) => {
|
||||
@@ -120,10 +136,19 @@ watch(
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (preferences.app.watermark) {
|
||||
destroyWatermark();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||
<BasicLayout
|
||||
@clear-preferences-and-logout="handleLogout"
|
||||
@click-logo="handleClickLogo"
|
||||
>
|
||||
<template #user-dropdown>
|
||||
<UserDropdown
|
||||
:avatar
|
||||
@@ -131,6 +156,7 @@ watch(
|
||||
:text="userStore.userInfo?.realName"
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
trigger="both"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Language } from 'element-plus/es/locale';
|
||||
import type { Locale } from 'ant-design-vue/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import enLocale from 'element-plus/es/locale/lang/en';
|
||||
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
|
||||
|
||||
const elementLocale = ref<Language>(defaultLocale);
|
||||
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||
|
||||
const modules = import.meta.glob('./langs/**/*.json');
|
||||
|
||||
@@ -43,7 +43,7 @@ async function loadMessages(lang: SupportedLanguagesType) {
|
||||
* @param lang
|
||||
*/
|
||||
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||
await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]);
|
||||
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,17 +74,17 @@ async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载element-plus的语言包
|
||||
* 加载antd的语言包
|
||||
* @param lang
|
||||
*/
|
||||
async function loadElementLocale(lang: SupportedLanguagesType) {
|
||||
async function loadAntdLocale(lang: SupportedLanguagesType) {
|
||||
switch (lang) {
|
||||
case 'en-US': {
|
||||
elementLocale.value = enLocale;
|
||||
antdLocale.value = antdEnLocale;
|
||||
break;
|
||||
}
|
||||
case 'zh-CN': {
|
||||
elementLocale.value = defaultLocale;
|
||||
antdLocale.value = antdDefaultLocale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -99,4 +99,4 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export { $t, elementLocale, setupI18n };
|
||||
export { $t, antdLocale, setupI18n };
|
||||
70
admin/apps/web-antd/src/locales/langs/en-US/demos.json
Normal file
70
admin/apps/web-antd/src/locales/langs/en-US/demos.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"access": {
|
||||
"frontendPermissions": "Frontend Permissions",
|
||||
"backendPermissions": "Backend Permissions",
|
||||
"pageAccess": "Page Access",
|
||||
"buttonControl": "Button Control",
|
||||
"menuVisible403": "Menu Visible(403)",
|
||||
"superVisible": "Visible to Super",
|
||||
"adminVisible": "Visible to Admin",
|
||||
"userVisible": "Visible to User"
|
||||
},
|
||||
"nested": {
|
||||
"title": "Nested Menu",
|
||||
"menu1": "Menu 1",
|
||||
"menu2": "Menu 2",
|
||||
"menu2_1": "Menu 2-1",
|
||||
"menu3": "Menu 3",
|
||||
"menu3_1": "Menu 3-1",
|
||||
"menu3_2": "Menu 3-2",
|
||||
"menu3_2_1": "Menu 3-2-1"
|
||||
},
|
||||
"outside": {
|
||||
"title": "External Pages",
|
||||
"embedded": "Embedded",
|
||||
"externalLink": "External Link"
|
||||
},
|
||||
"badge": {
|
||||
"title": "Menu Badge",
|
||||
"dot": "Dot Badge",
|
||||
"text": "Text Badge",
|
||||
"color": "Badge Color"
|
||||
},
|
||||
"activeIcon": {
|
||||
"title": "Active Menu Icon",
|
||||
"children": "Children Active Icon"
|
||||
},
|
||||
"fallback": {
|
||||
"title": "Fallback Page"
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"hideChildrenInMenu": "Hide Menu Children",
|
||||
"loginExpired": "Login Expired",
|
||||
"icons": "Icons",
|
||||
"watermark": "Watermark",
|
||||
"tabs": "Tabs",
|
||||
"tabDetail": "Tab Detail Page",
|
||||
"fullScreen": "FullScreen",
|
||||
"clipboard": "Clipboard",
|
||||
"menuWithQuery": "Menu With Query",
|
||||
"openInNewWindow": "Open in New Window",
|
||||
"fileDownload": "File Download"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "Breadcrumb Navigation",
|
||||
"lateral": "Lateral Mode",
|
||||
"lateralDetail": "Lateral Mode Detail",
|
||||
"level": "Level Mode",
|
||||
"levelDetail": "Level Mode Detail"
|
||||
},
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
"document": "Document",
|
||||
"antdv": "Ant Design Vue Version",
|
||||
"naive-ui": "Naive UI Version",
|
||||
"element-plus": "Element Plus Version"
|
||||
}
|
||||
}
|
||||
76
admin/apps/web-antd/src/locales/langs/en-US/examples.json
Normal file
76
admin/apps/web-antd/src/locales/langs/en-US/examples.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "Examples",
|
||||
"modal": {
|
||||
"title": "Modal"
|
||||
},
|
||||
"drawer": {
|
||||
"title": "Drawer"
|
||||
},
|
||||
"ellipsis": {
|
||||
"title": "EllipsisText"
|
||||
},
|
||||
"form": {
|
||||
"title": "Form",
|
||||
"basic": "Basic Form",
|
||||
"layout": "Custom Layout",
|
||||
"query": "Query Form",
|
||||
"rules": "Form Rules",
|
||||
"dynamic": "Dynamic Form",
|
||||
"custom": "Custom Component",
|
||||
"api": "Api",
|
||||
"merge": "Merge Form",
|
||||
"scrollToError": "Scroll to Error Field",
|
||||
"upload-error": "Partial file upload failed",
|
||||
"upload-urls": "Urls after file upload",
|
||||
"file": "file",
|
||||
"upload-image": "Click to upload image"
|
||||
},
|
||||
"vxeTable": {
|
||||
"title": "Vxe Table",
|
||||
"basic": "Basic Table",
|
||||
"remote": "Remote Load",
|
||||
"tree": "Tree Table",
|
||||
"fixed": "Fixed Header/Column",
|
||||
"virtual": "Virtual Scroll",
|
||||
"editCell": "Edit Cell",
|
||||
"editRow": "Edit Row",
|
||||
"custom-cell": "Custom Cell",
|
||||
"form": "Form Table"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "Captcha",
|
||||
"pointSelection": "Point Selection Captcha",
|
||||
"sliderCaptcha": "Slider Captcha",
|
||||
"sliderRotateCaptcha": "Rotate Captcha",
|
||||
"sliderTranslateCaptcha": "Translate Captcha",
|
||||
"captchaCardTitle": "Please complete the security verification",
|
||||
"pageDescription": "Verify user identity by clicking on specific locations in the image.",
|
||||
"pageTitle": "Captcha Component Example",
|
||||
"basic": "Basic Usage",
|
||||
"titlePlaceholder": "Captcha Title Text",
|
||||
"captchaImageUrlPlaceholder": "Captcha Image (supports img tag src attribute value)",
|
||||
"hintImage": "Hint Image",
|
||||
"hintText": "Hint Text",
|
||||
"hintImagePlaceholder": "Hint Image (supports img tag src attribute value)",
|
||||
"hintTextPlaceholder": "Hint Text",
|
||||
"showConfirm": "Show Confirm",
|
||||
"hideConfirm": "Hide Confirm",
|
||||
"widthPlaceholder": "Captcha Image Width Default 300px",
|
||||
"heightPlaceholder": "Captcha Image Height Default 220px",
|
||||
"paddingXPlaceholder": "Horizontal Padding Default 12px",
|
||||
"paddingYPlaceholder": "Vertical Padding Default 16px",
|
||||
"index": "Index:",
|
||||
"timestamp": "Timestamp:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"resize": {
|
||||
"title": "Resize"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "ColPage Layout"
|
||||
},
|
||||
"button-group": {
|
||||
"title": "Button Group"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password"
|
||||
"forgetPassword": "Forget Password",
|
||||
"sendingCode": "SMS Code is sending...",
|
||||
"codeSentTo": "Code has been sent to {0}"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
65
admin/apps/web-antd/src/locales/langs/en-US/system.json
Normal file
65
admin/apps/web-antd/src/locales/langs/en-US/system.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"title": "System Management",
|
||||
"dept": {
|
||||
"name": "Department",
|
||||
"title": "Department Management",
|
||||
"deptName": "Department Name",
|
||||
"status": "Status",
|
||||
"createTime": "Create Time",
|
||||
"remark": "Remark",
|
||||
"operation": "Operation",
|
||||
"parentDept": "Parent Department"
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu Management",
|
||||
"parent": "Parent Menu",
|
||||
"menuTitle": "Title",
|
||||
"menuName": "Menu Name",
|
||||
"name": "Menu",
|
||||
"type": "Type",
|
||||
"typeCatalog": "Catalog",
|
||||
"typeMenu": "Menu",
|
||||
"typeButton": "Button",
|
||||
"typeLink": "Link",
|
||||
"typeEmbedded": "Embedded",
|
||||
"icon": "Icon",
|
||||
"activeIcon": "Active Icon",
|
||||
"activePath": "Active Path",
|
||||
"path": "Route Path",
|
||||
"component": "Component",
|
||||
"status": "Status",
|
||||
"authCode": "Auth Code",
|
||||
"badge": "Badge",
|
||||
"operation": "Operation",
|
||||
"linkSrc": "Link Address",
|
||||
"affixTab": "Affix In Tabs",
|
||||
"keepAlive": "Keep Alive",
|
||||
"hideInMenu": "Hide In Menu",
|
||||
"hideInTab": "Hide In Tabbar",
|
||||
"hideChildrenInMenu": "Hide Children In Menu",
|
||||
"hideInBreadcrumb": "Hide In Breadcrumb",
|
||||
"advancedSettings": "Other Settings",
|
||||
"activePathMustExist": "The path could not find a valid menu",
|
||||
"activePathHelp": "When jumping to the current route, \nthe menu path that needs to be activated must be specified when it does not display in the navigation menu.",
|
||||
"badgeType": {
|
||||
"title": "Badge Type",
|
||||
"dot": "Dot",
|
||||
"normal": "Text",
|
||||
"none": "None"
|
||||
},
|
||||
"badgeVariants": "Badge Style"
|
||||
},
|
||||
"role": {
|
||||
"title": "Role Management",
|
||||
"list": "Role List",
|
||||
"name": "Role",
|
||||
"roleName": "Role Name",
|
||||
"id": "Role ID",
|
||||
"status": "Status",
|
||||
"remark": "Remark",
|
||||
"createTime": "Creation Time",
|
||||
"operation": "Operation",
|
||||
"permissions": "Permissions",
|
||||
"setPermissions": "Permissions"
|
||||
}
|
||||
}
|
||||
71
admin/apps/web-antd/src/locales/langs/zh-CN/demos.json
Normal file
71
admin/apps/web-antd/src/locales/langs/zh-CN/demos.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"access": {
|
||||
"frontendPermissions": "前端权限",
|
||||
"backendPermissions": "后端权限",
|
||||
"pageAccess": "页面访问",
|
||||
"buttonControl": "按钮控制",
|
||||
"menuVisible403": "菜单可见(403)",
|
||||
"superVisible": "Super 可见",
|
||||
"adminVisible": "Admin 可见",
|
||||
"userVisible": "User 可见"
|
||||
},
|
||||
"nested": {
|
||||
"title": "嵌套菜单",
|
||||
"menu1": "菜单 1",
|
||||
"menu2": "菜单 2",
|
||||
"menu2_1": "菜单 2-1",
|
||||
"menu3": "菜单 3",
|
||||
"menu3_1": "菜单 3-1",
|
||||
"menu3_2": "菜单 3-2",
|
||||
"menu3_2_1": "菜单 3-2-1"
|
||||
},
|
||||
"outside": {
|
||||
"title": "外部页面",
|
||||
"embedded": "内嵌",
|
||||
"externalLink": "外链"
|
||||
},
|
||||
"badge": {
|
||||
"title": "菜单徽标",
|
||||
"dot": "点徽标",
|
||||
"text": "文本徽标",
|
||||
"color": "徽标颜色"
|
||||
},
|
||||
"activeIcon": {
|
||||
"title": "菜单激活图标",
|
||||
"children": "子级激活图标"
|
||||
},
|
||||
"fallback": {
|
||||
"title": "缺省页"
|
||||
},
|
||||
"features": {
|
||||
"title": "功能",
|
||||
"hideChildrenInMenu": "隐藏子菜单",
|
||||
"loginExpired": "登录过期",
|
||||
"icons": "图标",
|
||||
"watermark": "水印",
|
||||
"tabs": "标签页",
|
||||
"tabDetail": "标签详情页",
|
||||
"fullScreen": "全屏",
|
||||
"clipboard": "剪贴板",
|
||||
"menuWithQuery": "带参菜单",
|
||||
"openInNewWindow": "新窗口打开",
|
||||
"fileDownload": "文件下载",
|
||||
"requestParamsSerializer": "参数序列化"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"navigation": "面包屑导航",
|
||||
"lateral": "平级模式",
|
||||
"level": "层级模式",
|
||||
"levelDetail": "层级模式详情",
|
||||
"lateralDetail": "平级模式详情"
|
||||
},
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
"document": "文档",
|
||||
"antdv": "Ant Design Vue 版本",
|
||||
"naive-ui": "Naive UI 版本",
|
||||
"element-plus": "Element Plus 版本"
|
||||
}
|
||||
}
|
||||
76
admin/apps/web-antd/src/locales/langs/zh-CN/examples.json
Normal file
76
admin/apps/web-antd/src/locales/langs/zh-CN/examples.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"title": "示例",
|
||||
"modal": {
|
||||
"title": "弹窗"
|
||||
},
|
||||
"drawer": {
|
||||
"title": "抽屉"
|
||||
},
|
||||
"ellipsis": {
|
||||
"title": "文本省略"
|
||||
},
|
||||
"resize": {
|
||||
"title": "拖动调整"
|
||||
},
|
||||
"form": {
|
||||
"title": "表单",
|
||||
"basic": "基础表单",
|
||||
"layout": "自定义布局",
|
||||
"query": "查询表单",
|
||||
"rules": "表单校验",
|
||||
"dynamic": "动态表单",
|
||||
"custom": "自定义组件",
|
||||
"api": "Api",
|
||||
"merge": "合并表单",
|
||||
"scrollToError": "滚动到错误字段",
|
||||
"upload-error": "部分文件上传失败",
|
||||
"upload-urls": "文件上传后的网址",
|
||||
"file": "文件",
|
||||
"upload-image": "点击上传图片"
|
||||
},
|
||||
"vxeTable": {
|
||||
"title": "Vxe 表格",
|
||||
"basic": "基础表格",
|
||||
"remote": "远程加载",
|
||||
"tree": "树形表格",
|
||||
"fixed": "固定表头/列",
|
||||
"virtual": "虚拟滚动",
|
||||
"editCell": "单元格编辑",
|
||||
"editRow": "行编辑",
|
||||
"custom-cell": "自定义单元格",
|
||||
"form": "搜索表单"
|
||||
},
|
||||
"captcha": {
|
||||
"title": "验证码",
|
||||
"pointSelection": "点选验证",
|
||||
"sliderCaptcha": "滑块验证",
|
||||
"sliderRotateCaptcha": "旋转验证",
|
||||
"sliderTranslateCaptcha": "拼图滑块验证",
|
||||
"captchaCardTitle": "请完成安全验证",
|
||||
"pageDescription": "通过点击图片中的特定位置来验证用户身份。",
|
||||
"pageTitle": "验证码组件示例",
|
||||
"basic": "基本使用",
|
||||
"titlePlaceholder": "验证码标题文案",
|
||||
"captchaImageUrlPlaceholder": "验证码图片(支持img标签src属性值)",
|
||||
"hintImage": "提示图片",
|
||||
"hintText": "提示文本",
|
||||
"hintImagePlaceholder": "提示图片(支持img标签src属性值)",
|
||||
"hintTextPlaceholder": "提示文本",
|
||||
"showConfirm": "展示确认",
|
||||
"hideConfirm": "隐藏确认",
|
||||
"widthPlaceholder": "验证码图片宽度 默认300px",
|
||||
"heightPlaceholder": "验证码图片高度 默认220px",
|
||||
"paddingXPlaceholder": "水平内边距 默认12px",
|
||||
"paddingYPlaceholder": "垂直内边距 默认16px",
|
||||
"index": "索引:",
|
||||
"timestamp": "时间戳:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "双列布局"
|
||||
},
|
||||
"button-group": {
|
||||
"title": "按钮组"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码"
|
||||
"forgetPassword": "忘记密码",
|
||||
"sendingCode": "正在发送验证码",
|
||||
"codeSentTo": "验证码已发送至{0}"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
67
admin/apps/web-antd/src/locales/langs/zh-CN/system.json
Normal file
67
admin/apps/web-antd/src/locales/langs/zh-CN/system.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"dept": {
|
||||
"list": "部门列表",
|
||||
"createTime": "创建时间",
|
||||
"deptName": "部门名称",
|
||||
"name": "部门",
|
||||
"operation": "操作",
|
||||
"parentDept": "上级部门",
|
||||
"remark": "备注",
|
||||
"status": "状态",
|
||||
"title": "部门管理"
|
||||
},
|
||||
"menu": {
|
||||
"list": "菜单列表",
|
||||
"activeIcon": "激活图标",
|
||||
"activePath": "激活路径",
|
||||
"activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径",
|
||||
"activePathMustExist": "该路径未能找到有效的菜单",
|
||||
"advancedSettings": "其它设置",
|
||||
"affixTab": "固定在标签",
|
||||
"authCode": "权限标识",
|
||||
"badge": "徽章内容",
|
||||
"badgeVariants": "徽标样式",
|
||||
"badgeType": {
|
||||
"dot": "点",
|
||||
"none": "无",
|
||||
"normal": "文字",
|
||||
"title": "徽标类型"
|
||||
},
|
||||
"component": "页面组件",
|
||||
"hideChildrenInMenu": "隐藏子菜单",
|
||||
"hideInBreadcrumb": "在面包屑中隐藏",
|
||||
"hideInMenu": "隐藏菜单",
|
||||
"hideInTab": "在标签栏中隐藏",
|
||||
"icon": "图标",
|
||||
"keepAlive": "缓存标签页",
|
||||
"linkSrc": "链接地址",
|
||||
"menuName": "菜单名称",
|
||||
"menuTitle": "标题",
|
||||
"name": "菜单",
|
||||
"operation": "操作",
|
||||
"parent": "上级菜单",
|
||||
"path": "路由地址",
|
||||
"status": "状态",
|
||||
"title": "菜单管理",
|
||||
"type": "类型",
|
||||
"typeButton": "按钮",
|
||||
"typeCatalog": "目录",
|
||||
"typeEmbedded": "内嵌",
|
||||
"typeLink": "外链",
|
||||
"typeMenu": "菜单"
|
||||
},
|
||||
"role": {
|
||||
"title": "角色管理",
|
||||
"list": "角色列表",
|
||||
"name": "角色",
|
||||
"roleName": "角色名称",
|
||||
"id": "角色ID",
|
||||
"status": "状态",
|
||||
"remark": "备注",
|
||||
"createTime": "创建时间",
|
||||
"operation": "操作",
|
||||
"permissions": "权限",
|
||||
"setPermissions": "授权"
|
||||
},
|
||||
"title": "系统管理"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getAllMenusApi } from '#/api';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
@@ -25,9 +25,9 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
ElMessage({
|
||||
duration: 1500,
|
||||
message: `${$t('common.loadingMenu')}...`,
|
||||
message.loading({
|
||||
content: `${$t('common.loadingMenu')}...`,
|
||||
duration: 1.5,
|
||||
});
|
||||
return await getAllMenusApi();
|
||||
},
|
||||
@@ -30,7 +30,6 @@ function setupCommonGuard(router: Router) {
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
@@ -49,7 +48,6 @@ function setupAccessGuard(router: Router) {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
@@ -107,11 +105,16 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === preferences.app.defaultHomePath
|
||||
? userInfo.homePath || preferences.app.defaultHomePath
|
||||
: to.fullPath)) as string;
|
||||
|
||||
let redirectPath: string;
|
||||
if (from.query.redirect) {
|
||||
redirectPath = from.query.redirect as string;
|
||||
} else if (to.path === preferences.app.defaultHomePath) {
|
||||
redirectPath = preferences.app.defaultHomePath;
|
||||
} else if (userInfo.homePath && to.path === userInfo.homePath) {
|
||||
redirectPath = userInfo.homePath;
|
||||
} else {
|
||||
redirectPath = to.fullPath;
|
||||
}
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
replace: true,
|
||||
@@ -34,4 +34,14 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
||||
const componentKeys: string[] = Object.keys(
|
||||
import.meta.glob('../../views/**/*.vue'),
|
||||
)
|
||||
.filter((item) => !item.includes('/modules/'))
|
||||
.map((v) => {
|
||||
const path = v.replace('../../views/', '/');
|
||||
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||
});
|
||||
|
||||
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||
594
admin/apps/web-antd/src/router/routes/modules/demos.ts
Normal file
594
admin/apps/web-antd/src/router/routes/modules/demos.ts
Normal file
@@ -0,0 +1,594 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('demos.title'),
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
// 权限控制
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:shield-key-outline',
|
||||
title: $t('demos.access.frontendPermissions'),
|
||||
},
|
||||
name: 'AccessDemos',
|
||||
path: '/demos/access',
|
||||
children: [
|
||||
{
|
||||
name: 'AccessPageControlDemo',
|
||||
path: '/demos/access/page-control',
|
||||
component: () => import('#/views/demos/access/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:page-previous-outline',
|
||||
title: $t('demos.access.pageAccess'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessButtonControlDemo',
|
||||
path: '/demos/access/button-control',
|
||||
component: () => import('#/views/demos/access/button-control.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.buttonControl'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessMenuVisible403Demo',
|
||||
path: '/demos/access/menu-visible-403',
|
||||
component: () =>
|
||||
import('#/views/demos/access/menu-visible-403.vue'),
|
||||
meta: {
|
||||
authority: ['no-body'],
|
||||
icon: 'mdi:button-cursor',
|
||||
menuVisibleWithForbidden: true,
|
||||
title: $t('demos.access.menuVisible403'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessSuperVisibleDemo',
|
||||
path: '/demos/access/super-visible',
|
||||
component: () => import('#/views/demos/access/super-visible.vue'),
|
||||
meta: {
|
||||
authority: ['super'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.superVisible'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessAdminVisibleDemo',
|
||||
path: '/demos/access/admin-visible',
|
||||
component: () => import('#/views/demos/access/admin-visible.vue'),
|
||||
meta: {
|
||||
authority: ['admin'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.adminVisible'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'AccessUserVisibleDemo',
|
||||
path: '/demos/access/user-visible',
|
||||
component: () => import('#/views/demos/access/user-visible.vue'),
|
||||
meta: {
|
||||
authority: ['user'],
|
||||
icon: 'mdi:button-cursor',
|
||||
title: $t('demos.access.userVisible'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 功能
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:feature-highlight',
|
||||
title: $t('demos.features.title'),
|
||||
},
|
||||
name: 'FeaturesDemos',
|
||||
path: '/demos/features',
|
||||
children: [
|
||||
{
|
||||
name: 'LoginExpiredDemo',
|
||||
path: '/demos/features/login-expired',
|
||||
component: () =>
|
||||
import('#/views/demos/features/login-expired/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:encryption-expiration',
|
||||
title: $t('demos.features.loginExpired'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IconsDemo',
|
||||
path: '/demos/features/icons',
|
||||
component: () => import('#/views/demos/features/icons/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:annoyed',
|
||||
title: $t('demos.features.icons'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WatermarkDemo',
|
||||
path: '/demos/features/watermark',
|
||||
component: () =>
|
||||
import('#/views/demos/features/watermark/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:tags',
|
||||
title: $t('demos.features.watermark'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FeatureTabsDemo',
|
||||
path: '/demos/features/tabs',
|
||||
component: () => import('#/views/demos/features/tabs/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:app-window',
|
||||
title: $t('demos.features.tabs'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FeatureTabDetailDemo',
|
||||
path: '/demos/features/tabs/detail/:id',
|
||||
component: () =>
|
||||
import('#/views/demos/features/tabs/tab-detail.vue'),
|
||||
meta: {
|
||||
activePath: '/demos/features/tabs',
|
||||
hideInMenu: true,
|
||||
maxNumOfOpenTab: 3,
|
||||
title: $t('demos.features.tabDetail'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'HideChildrenInMenuParentDemo',
|
||||
path: '/demos/features/hide-menu-children',
|
||||
meta: {
|
||||
hideChildrenInMenu: true,
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.features.hideChildrenInMenu'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'HideChildrenInMenuDemo',
|
||||
path: '',
|
||||
component: () =>
|
||||
import(
|
||||
'#/views/demos/features/hide-menu-children/parent.vue'
|
||||
),
|
||||
meta: {
|
||||
// hideInMenu: true,
|
||||
title: $t('demos.features.hideChildrenInMenu'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'HideChildrenInMenuChildrenDemo',
|
||||
path: '/demos/features/hide-menu-children/children',
|
||||
component: () =>
|
||||
import(
|
||||
'#/views/demos/features/hide-menu-children/children.vue'
|
||||
),
|
||||
meta: {
|
||||
activePath: '/demos/features/hide-menu-children',
|
||||
title: $t('demos.features.hideChildrenInMenu'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'FullScreenDemo',
|
||||
path: '/demos/features/full-screen',
|
||||
component: () =>
|
||||
import('#/views/demos/features/full-screen/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:fullscreen',
|
||||
title: $t('demos.features.fullScreen'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FileDownloadDemo',
|
||||
path: '/demos/features/file-download',
|
||||
component: () =>
|
||||
import('#/views/demos/features/file-download/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:hard-drive-download',
|
||||
title: $t('demos.features.fileDownload'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ClipboardDemo',
|
||||
path: '/demos/features/clipboard',
|
||||
component: () =>
|
||||
import('#/views/demos/features/clipboard/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copy',
|
||||
title: $t('demos.features.clipboard'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MenuQueryDemo',
|
||||
path: '/demos/menu-query',
|
||||
component: () =>
|
||||
import('#/views/demos/features/menu-query/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:curly-braces',
|
||||
query: {
|
||||
id: 1,
|
||||
},
|
||||
title: $t('demos.features.menuWithQuery'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NewWindowDemo',
|
||||
path: '/demos/new-window',
|
||||
component: () =>
|
||||
import('#/views/demos/features/new-window/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:app-window',
|
||||
openInNewWindow: true,
|
||||
title: $t('demos.features.openInNewWindow'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueQueryDemo',
|
||||
path: '/demos/features/vue-query',
|
||||
component: () =>
|
||||
import('#/views/demos/features/vue-query/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:git-pull-request-arrow',
|
||||
title: 'Tanstack Query',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RequestParamsSerializerDemo',
|
||||
path: '/demos/features/request-params-serializer',
|
||||
component: () =>
|
||||
import(
|
||||
'#/views/demos/features/request-params-serializer/index.vue'
|
||||
),
|
||||
meta: {
|
||||
icon: 'lucide:git-pull-request-arrow',
|
||||
title: $t('demos.features.requestParamsSerializer'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BigIntDemo',
|
||||
path: '/demos/features/json-bigint',
|
||||
component: () =>
|
||||
import('#/views/demos/features/json-bigint/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:grape',
|
||||
title: 'JSON BigInt',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 面包屑导航
|
||||
{
|
||||
name: 'BreadcrumbDemos',
|
||||
path: '/demos/breadcrumb',
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.navigation'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BreadcrumbLateralDemo',
|
||||
path: '/demos/breadcrumb/lateral',
|
||||
component: () => import('#/views/demos/breadcrumb/lateral.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.lateral'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BreadcrumbLateralDetailDemo',
|
||||
path: '/demos/breadcrumb/lateral-detail',
|
||||
component: () =>
|
||||
import('#/views/demos/breadcrumb/lateral-detail.vue'),
|
||||
meta: {
|
||||
activePath: '/demos/breadcrumb/lateral',
|
||||
hideInMenu: true,
|
||||
title: $t('demos.breadcrumb.lateralDetail'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BreadcrumbLevelDemo',
|
||||
path: '/demos/breadcrumb/level',
|
||||
meta: {
|
||||
icon: 'lucide:navigation',
|
||||
title: $t('demos.breadcrumb.level'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'BreadcrumbLevelDetailDemo',
|
||||
path: '/demos/breadcrumb/level/detail',
|
||||
component: () =>
|
||||
import('#/views/demos/breadcrumb/level-detail.vue'),
|
||||
meta: {
|
||||
title: $t('demos.breadcrumb.levelDetail'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 缺省页
|
||||
{
|
||||
meta: {
|
||||
icon: 'mdi:lightbulb-error-outline',
|
||||
title: $t('demos.fallback.title'),
|
||||
},
|
||||
name: 'FallbackDemos',
|
||||
path: '/demos/fallback',
|
||||
children: [
|
||||
{
|
||||
name: 'Fallback403Demo',
|
||||
path: '/demos/fallback/403',
|
||||
component: () => import('#/views/_core/fallback/forbidden.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:do-not-disturb-alt',
|
||||
title: '403',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fallback404Demo',
|
||||
path: '/demos/fallback/404',
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:table-off',
|
||||
title: '404',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fallback500Demo',
|
||||
path: '/demos/fallback/500',
|
||||
component: () =>
|
||||
import('#/views/_core/fallback/internal-error.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:server-network-off',
|
||||
title: '500',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FallbackOfflineDemo',
|
||||
path: '/demos/fallback/offline',
|
||||
component: () => import('#/views/_core/fallback/offline.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:offline',
|
||||
title: $t('ui.fallback.offline'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 菜单徽标
|
||||
{
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'lucide:circle-dot',
|
||||
title: $t('demos.badge.title'),
|
||||
},
|
||||
name: 'BadgeDemos',
|
||||
path: '/demos/badge',
|
||||
children: [
|
||||
{
|
||||
name: 'BadgeDotDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/dot',
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.dot'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BadgeTextDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/text',
|
||||
meta: {
|
||||
badge: '10',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.text'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BadgeColorDemo',
|
||||
component: () => import('#/views/demos/badge/index.vue'),
|
||||
path: '/demos/badge/color',
|
||||
meta: {
|
||||
badge: 'Hot',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'lucide:square-dot',
|
||||
title: $t('demos.badge.color'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 菜单激活图标
|
||||
{
|
||||
meta: {
|
||||
activeIcon: 'fluent-emoji:radioactive',
|
||||
icon: 'bi:radioactive',
|
||||
title: $t('demos.activeIcon.title'),
|
||||
},
|
||||
name: 'ActiveIconDemos',
|
||||
path: '/demos/active-icon',
|
||||
children: [
|
||||
{
|
||||
name: 'ActiveIconDemo',
|
||||
component: () => import('#/views/demos/active-icon/index.vue'),
|
||||
path: '/demos/active-icon/children',
|
||||
meta: {
|
||||
activeIcon: 'fluent-emoji:radioactive',
|
||||
icon: 'bi:radioactive',
|
||||
title: $t('demos.activeIcon.children'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 外部链接
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-settings-input-composite',
|
||||
title: $t('demos.outside.title'),
|
||||
},
|
||||
name: 'OutsideDemos',
|
||||
path: '/demos/outside',
|
||||
children: [
|
||||
{
|
||||
name: 'IframeDemos',
|
||||
path: '/demos/outside/iframe',
|
||||
meta: {
|
||||
icon: 'mdi:newspaper-variant-outline',
|
||||
title: $t('demos.outside.embedded'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'VueDocumentDemo',
|
||||
path: '/demos/outside/iframe/vue-document',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vue',
|
||||
iframeSrc: 'https://cn.vuejs.org/',
|
||||
keepAlive: true,
|
||||
title: 'Vue',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TailwindcssDemo',
|
||||
path: '/demos/outside/iframe/tailwindcss',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'devicon:tailwindcss',
|
||||
iframeSrc: 'https://tailwindcss.com/',
|
||||
// keepAlive: true,
|
||||
title: 'Tailwindcss',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ExternalLinkDemos',
|
||||
path: '/demos/outside/external-link',
|
||||
meta: {
|
||||
icon: 'mdi:newspaper-variant-multiple-outline',
|
||||
title: $t('demos.outside.externalLink'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ViteDemo',
|
||||
path: '/demos/outside/external-link/vite',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vitejs',
|
||||
link: 'https://vitejs.dev/',
|
||||
title: 'Vite',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueUseDemo',
|
||||
path: '/demos/outside/external-link/vue-use',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
icon: 'logos:vueuse',
|
||||
link: 'https://vueuse.org',
|
||||
title: 'VueUse',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 嵌套菜单
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.title'),
|
||||
},
|
||||
name: 'NestedDemos',
|
||||
path: '/demos/nested',
|
||||
children: [
|
||||
{
|
||||
name: 'Menu1Demo',
|
||||
path: '/demos/nested/menu1',
|
||||
component: () => import('#/views/demos/nested/menu-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu1'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Menu2Demo',
|
||||
path: '/demos/nested/menu2',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu2'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu21Demo',
|
||||
path: '/demos/nested/menu2/menu2-1',
|
||||
component: () => import('#/views/demos/nested/menu-2-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu2_1'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Menu3Demo',
|
||||
path: '/demos/nested/menu3',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.menu3'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu31Demo',
|
||||
path: '/demos/nested/menu3/menu3-1',
|
||||
component: () => import('#/views/demos/nested/menu-3-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu3_1'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Menu32Demo',
|
||||
path: '/demos/nested/menu3/menu3-2',
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
title: $t('demos.nested.menu3_2'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Menu321Demo',
|
||||
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
|
||||
component: () =>
|
||||
import('#/views/demos/nested/menu-3-2-1.vue'),
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
keepAlive: true,
|
||||
title: $t('demos.nested.menu3_2_1'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
335
admin/apps/web-antd/src/router/routes/modules/examples.ts
Normal file
335
admin/apps/web-antd/src/router/routes/modules/examples.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ion:layers-outline',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('examples.title'),
|
||||
},
|
||||
name: 'Examples',
|
||||
path: '/examples',
|
||||
children: [
|
||||
{
|
||||
name: 'FormExample',
|
||||
path: '/examples/form',
|
||||
meta: {
|
||||
icon: 'mdi:form-select',
|
||||
title: $t('examples.form.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'FormBasicExample',
|
||||
path: '/examples/form/basic',
|
||||
component: () => import('#/views/examples/form/basic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.basic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormQueryExample',
|
||||
path: '/examples/form/query',
|
||||
component: () => import('#/views/examples/form/query.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.query'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormRulesExample',
|
||||
path: '/examples/form/rules',
|
||||
component: () => import('#/views/examples/form/rules.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.rules'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormDynamicExample',
|
||||
path: '/examples/form/dynamic',
|
||||
component: () => import('#/views/examples/form/dynamic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.dynamic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormLayoutExample',
|
||||
path: '/examples/form/custom-layout',
|
||||
component: () => import('#/views/examples/form/custom-layout.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.layout'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormCustomExample',
|
||||
path: '/examples/form/custom',
|
||||
component: () => import('#/views/examples/form/custom.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.custom'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormApiExample',
|
||||
path: '/examples/form/api',
|
||||
component: () => import('#/views/examples/form/api.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.api'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormMergeExample',
|
||||
path: '/examples/form/merge',
|
||||
component: () => import('#/views/examples/form/merge.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.merge'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FormScrollToErrorExample',
|
||||
path: '/examples/form/scroll-to-error-test',
|
||||
component: () =>
|
||||
import('#/views/examples/form/scroll-to-error-test.vue'),
|
||||
meta: {
|
||||
title: $t('examples.form.scrollToError'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VxeTableExample',
|
||||
path: '/examples/vxe-table',
|
||||
meta: {
|
||||
icon: 'lucide:table',
|
||||
title: $t('examples.vxeTable.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'VxeTableBasicExample',
|
||||
path: '/examples/vxe-table/basic',
|
||||
component: () => import('#/views/examples/vxe-table/basic.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.basic'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableRemoteExample',
|
||||
path: '/examples/vxe-table/remote',
|
||||
component: () => import('#/views/examples/vxe-table/remote.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.remote'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableTreeExample',
|
||||
path: '/examples/vxe-table/tree',
|
||||
component: () => import('#/views/examples/vxe-table/tree.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.tree'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableFixedExample',
|
||||
path: '/examples/vxe-table/fixed',
|
||||
component: () => import('#/views/examples/vxe-table/fixed.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.fixed'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableCustomCellExample',
|
||||
path: '/examples/vxe-table/custom-cell',
|
||||
component: () =>
|
||||
import('#/views/examples/vxe-table/custom-cell.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.custom-cell'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableFormExample',
|
||||
path: '/examples/vxe-table/form',
|
||||
component: () => import('#/views/examples/vxe-table/form.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.form'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableEditCellExample',
|
||||
path: '/examples/vxe-table/edit-cell',
|
||||
component: () => import('#/views/examples/vxe-table/edit-cell.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.editCell'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableEditRowExample',
|
||||
path: '/examples/vxe-table/edit-row',
|
||||
component: () => import('#/views/examples/vxe-table/edit-row.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.editRow'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VxeTableVirtualExample',
|
||||
path: '/examples/vxe-table/virtual',
|
||||
component: () => import('#/views/examples/vxe-table/virtual.vue'),
|
||||
meta: {
|
||||
title: $t('examples.vxeTable.virtual'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'CaptchaExample',
|
||||
path: '/examples/captcha',
|
||||
meta: {
|
||||
icon: 'logos:recaptcha',
|
||||
title: $t('examples.captcha.title'),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'DragVerifyExample',
|
||||
path: '/examples/captcha/slider',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RotateVerifyExample',
|
||||
path: '/examples/captcha/slider-rotate',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-rotate-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderRotateCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TranslateVerifyExample',
|
||||
path: '/examples/captcha/slider-translate',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/slider-translate-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.sliderTranslateCaptcha'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CaptchaPointSelectionExample',
|
||||
path: '/examples/captcha/point-selection',
|
||||
component: () =>
|
||||
import('#/views/examples/captcha/point-selection-captcha.vue'),
|
||||
meta: {
|
||||
title: $t('examples.captcha.pointSelection'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ModalExample',
|
||||
path: '/examples/modal',
|
||||
component: () => import('#/views/examples/modal/index.vue'),
|
||||
meta: {
|
||||
icon: 'system-uicons:window-content',
|
||||
keepAlive: true,
|
||||
title: $t('examples.modal.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'DrawerExample',
|
||||
path: '/examples/drawer',
|
||||
component: () => import('#/views/examples/drawer/index.vue'),
|
||||
meta: {
|
||||
icon: 'iconoir:drawer',
|
||||
keepAlive: true,
|
||||
title: $t('examples.drawer.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EllipsisExample',
|
||||
path: '/examples/ellipsis',
|
||||
component: () => import('#/views/examples/ellipsis/index.vue'),
|
||||
meta: {
|
||||
icon: 'ion:ellipsis-horizontal',
|
||||
title: $t('examples.ellipsis.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VueResizeDemo',
|
||||
path: '/demos/resize/basic',
|
||||
component: () => import('#/views/examples/resize/basic.vue'),
|
||||
meta: {
|
||||
icon: 'material-symbols:resize',
|
||||
title: $t('examples.resize.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ColPageDemo',
|
||||
path: '/examples/layout/col-page',
|
||||
component: () => import('#/views/examples/layout/col-page.vue'),
|
||||
meta: {
|
||||
badge: 'Alpha',
|
||||
badgeVariants: 'destructive',
|
||||
icon: 'material-symbols:horizontal-distribute',
|
||||
title: $t('examples.layout.col-page'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'TippyDemo',
|
||||
path: '/examples/tippy',
|
||||
component: () => import('#/views/examples/tippy/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:message-settings-outline',
|
||||
title: 'Tippy',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'JsonViewer',
|
||||
path: '/examples/json-viewer',
|
||||
component: () => import('#/views/examples/json-viewer/index.vue'),
|
||||
meta: {
|
||||
icon: 'tabler:json',
|
||||
title: 'JsonViewer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Motion',
|
||||
path: '/examples/motion',
|
||||
component: () => import('#/views/examples/motion/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:animation-play',
|
||||
title: 'Motion',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CountTo',
|
||||
path: '/examples/count-to',
|
||||
component: () => import('#/views/examples/count-to/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:animation-play',
|
||||
title: 'CountTo',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Loading',
|
||||
path: '/examples/loading',
|
||||
component: () => import('#/views/examples/loading/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:circle-double',
|
||||
title: 'Loading',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ButtonGroup',
|
||||
path: '/examples/button-group',
|
||||
component: () => import('#/views/examples/button-group/index.vue'),
|
||||
meta: {
|
||||
icon: 'mdi:check-circle',
|
||||
title: $t('examples.button-group.title'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
46
admin/apps/web-antd/src/router/routes/modules/system.ts
Normal file
46
admin/apps/web-antd/src/router/routes/modules/system.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ion:settings-outline',
|
||||
order: 9997,
|
||||
title: $t('system.title'),
|
||||
},
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
path: '/system/role',
|
||||
name: 'SystemRole',
|
||||
meta: {
|
||||
icon: 'mdi:account-group',
|
||||
title: $t('system.role.title'),
|
||||
},
|
||||
component: () => import('#/views/system/role/list.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/menu',
|
||||
name: 'SystemMenu',
|
||||
meta: {
|
||||
icon: 'mdi:menu',
|
||||
title: $t('system.menu.title'),
|
||||
},
|
||||
component: () => import('#/views/system/menu/list.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/dept',
|
||||
name: 'SystemDept',
|
||||
meta: {
|
||||
icon: 'charm:organisation',
|
||||
title: $t('system.dept.title'),
|
||||
},
|
||||
component: () => import('#/views/system/dept/list.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -3,6 +3,7 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import {
|
||||
VBEN_ANT_PREVIEW_URL,
|
||||
VBEN_DOC_URL,
|
||||
VBEN_ELE_PREVIEW_URL,
|
||||
VBEN_GITHUB_URL,
|
||||
VBEN_LOGO_URL,
|
||||
VBEN_NAIVE_PREVIEW_URL,
|
||||
@@ -43,6 +44,17 @@ const routes: RouteRecordRaw[] = [
|
||||
title: 'Github',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenAntdv',
|
||||
path: '/vben-admin/antdv',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgAntdvLogoIcon,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('demos.vben.antdv'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenNaive',
|
||||
path: '/vben-admin/naive',
|
||||
@@ -55,27 +67,27 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenAntd',
|
||||
path: '/vben-admin/antd',
|
||||
name: 'VbenElementPlus',
|
||||
path: '/vben-admin/ele',
|
||||
component: IFrameView,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: SvgAntdvLogoIcon,
|
||||
link: VBEN_ANT_PREVIEW_URL,
|
||||
title: $t('demos.vben.antdv'),
|
||||
icon: 'logos:element',
|
||||
link: VBEN_ELE_PREVIEW_URL,
|
||||
title: $t('demos.vben.element-plus'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
order: 9999,
|
||||
title: $t('demos.vben.about'),
|
||||
},
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||
@@ -24,6 +24,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
* @param onSuccess 成功之后的回调函数
|
||||
*/
|
||||
async function authLogin(
|
||||
params: Recordable<any>,
|
||||
@@ -37,7 +38,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(accessToken);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
@@ -62,10 +62,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
ElNotification({
|
||||
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
title: $t('authentication.loginSuccess'),
|
||||
type: 'success',
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
} catch {
|
||||
// 不做任何处理
|
||||
}
|
||||
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
@@ -2,16 +2,36 @@
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
|
||||
function sendCodeApi(phoneNumber: string) {
|
||||
message.loading({
|
||||
content: $t('page.auth.sendingCode'),
|
||||
duration: 0,
|
||||
key: 'sending-code',
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
message.success({
|
||||
content: $t('page.auth.codeSentTo', [phoneNumber]),
|
||||
duration: 3,
|
||||
key: 'sending-code',
|
||||
});
|
||||
resolve({ code: '123456', phoneNumber });
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
@@ -39,6 +59,25 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
handleSendCode: async () => {
|
||||
// 模拟发送验证码
|
||||
// Simulate sending verification code
|
||||
loading.value = true;
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
loading.value = false;
|
||||
throw new Error('formApi is not ready');
|
||||
}
|
||||
await formApi.validateField('phoneNumber');
|
||||
const isPhoneReady = await formApi.isFieldValid('phoneNumber');
|
||||
if (!isPhoneReady) {
|
||||
loading.value = false;
|
||||
throw new Error('Phone number is not Ready');
|
||||
}
|
||||
const { phoneNumber } = await formApi.getValues();
|
||||
await sendCodeApi(phoneNumber);
|
||||
loading.value = false;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
@@ -62,6 +101,7 @@ async function handleLogin(values: Recordable<any>) {
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
@@ -28,7 +27,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
function handleSubmit(value: Record<string, any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
import type { BasicOption, Recordable } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed, markRaw, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -32,6 +32,23 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
// componentProps(_values, form) {
|
||||
// return {
|
||||
// 'onUpdate:modelValue': (value: string) => {
|
||||
// const findItem = MOCK_USER_OPTIONS.find(
|
||||
// (item) => item.value === value,
|
||||
// );
|
||||
// if (findItem) {
|
||||
// form.setValues({
|
||||
// password: '123456',
|
||||
// username: findItem.label,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// options: MOCK_USER_OPTIONS,
|
||||
// placeholder: $t('authentication.selectAccount'),
|
||||
// };
|
||||
// },
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
@@ -87,12 +104,29 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
|
||||
|
||||
async function onSubmit(params: Recordable<any>) {
|
||||
authStore.authLogin(params).catch(() => {
|
||||
// 登陆失败,刷新验证码的演示
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
// 重置验证码组件的值
|
||||
formApi?.setFieldValue('captcha', false, false);
|
||||
// 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
|
||||
formApi
|
||||
?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
|
||||
?.resume();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="authStore.authLogin"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user