refactor: 项目结构优化调整
This commit is contained in:
38
README.md
38
README.md
@@ -50,11 +50,9 @@
|
||||
<a target="_blank" href="https://vue.youlai.tech">🖥️ 在线预览</a> | <a target="_blank" href="https://app.youlai.tech">📲 移动端预览</a> | <a target="_blank" href="https://juejin.cn/post/7228990409909108793">📑 阅读文档</a>| <a target="_blank" href="https://www.youlai.tech//vue3-element-admin">🌐 官网</a> | <a href="./README.en-US.md">💬 English
|
||||
</div>
|
||||
|
||||
|
||||
## 项目简介
|
||||
|
||||
[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) 基于 Vue3、Vite7、TypeScript 和 Element-Plus 搭建的极简开箱即用企业级后台管理前端模板。 配套 Java 后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) 和 Node 后端 [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) 。 提供开发简版[vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) 和 JS 版本[vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin) 供开发者快速开发。
|
||||
|
||||
[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) 基于 Vue3、Vite7、TypeScript 和 Element-Plus 搭建的极简开箱即用企业级后台管理前端模板。 配套 Java 后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot)、多租户后端 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) 和 Node 后端 [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) 。 提供开发简版[vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) 和 JS 版本[vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin) 供开发者快速开发。
|
||||
|
||||
## 项目特色
|
||||
|
||||
@@ -64,10 +62,11 @@
|
||||
- **系统功能:** 提供用户管理、角色管理、菜单管理、部门管理、字典管理、系统配置、通知公告等功能模块。
|
||||
- **权限管理:** 支持动态路由、按钮权限、角色权限和数据权限等多种权限管理方式。
|
||||
|
||||
- **多租户:** 支持多租户模式与租户隔离。
|
||||
|
||||
- **基础设施:** 提供国际化、多布局、暗黑模式、全屏、水印、接口文档和代码生成器等功能。
|
||||
- **持续更新**:项目持续开源更新,实时更新工具和依赖。
|
||||
|
||||
|
||||
## 项目截图
|
||||
|
||||
🖥️ **控制台**
|
||||
@@ -84,20 +83,20 @@
|
||||
|
||||
## 项目源码
|
||||
|
||||
| 项目 | Gitee | Github | GitCode|
|
||||
| ---- | ----| ---- | ---- |
|
||||
| vue3-element-admin ✅| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) |
|
||||
| vue3-element-admin JS版| [vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin-js) | [vue3-element-admin-js](https://github.com/youlaitech/vue3-element-admin-js) | [vue3-element-admin-js](https://gitcode.com/youlai/vue3-element-admin-js) |
|
||||
| vue3-element-admin 精简版 | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | [vue3-element-template](https://github.com/youlaitech/vue3-element-template) |[vue3-element-template](https://gitcode.com/youlai/vue3-element-template)|
|
||||
| vue-uniapp-admin 移动版 | [vue-uniapp-admin](https://gitee.com/youlaiorg/vue-uniapp-admin) | [vue-uniapp-admin](https://github.com/youlaitech/vue-uniapp-admin) |[vue-uniapp-admin](https://gitcode.com/youlai/vue-uniapp-admin)|
|
||||
| Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) |[youlai-boot](https://gitcode.com/youlai/youlai-boot.git)|
|
||||
| Node 后端 | [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | [youlai-nest](https://github.com/haoxianrui/youlai-nest.git) |[youlai-nest](https://gitcode.com/youlai/youlai-nest.git)|
|
||||
|
||||
| 项目 | Gitee | Github | GitCode |
|
||||
| ------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| vue3-element-admin ✅ | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) |
|
||||
| vue3-element-admin JS版 | [vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin-js) | [vue3-element-admin-js](https://github.com/youlaitech/vue3-element-admin-js) | [vue3-element-admin-js](https://gitcode.com/youlai/vue3-element-admin-js) |
|
||||
| vue3-element-admin 精简版 | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | [vue3-element-template](https://github.com/youlaitech/vue3-element-template) | [vue3-element-template](https://gitcode.com/youlai/vue3-element-template) |
|
||||
| vue-uniapp-admin 移动版 | [vue-uniapp-admin](https://gitee.com/youlaiorg/vue-uniapp-admin) | [vue-uniapp-admin](https://github.com/youlaitech/vue-uniapp-admin) | [vue-uniapp-admin](https://gitcode.com/youlai/vue-uniapp-admin) |
|
||||
| Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) | [youlai-boot](https://gitcode.com/youlai/youlai-boot.git) |
|
||||
| Java 多租户后端 | [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) | - | - |
|
||||
| Node 后端 | [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | [youlai-nest](https://github.com/haoxianrui/youlai-nest.git) | [youlai-nest](https://gitcode.com/youlai/youlai-nest.git) |
|
||||
|
||||
## 开发指南
|
||||
|
||||
| 名称 | 地址 |
|
||||
|---------------|--------------------|
|
||||
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 视频教程 | [https://www.bilibili.com/video/BV1eFUuYyEFj](https://www.bilibili.com/video/BV1eFUuYyEFj) |
|
||||
| 项目搭建 | [基于 Vue3 + Vite + TypeScript + Element-Plus 从0到1搭建后台管理系统](https://blog.csdn.net/u013737132/article/details/130191394) |
|
||||
| 官方文档 | [https://www.youlai.tech/vue3-element-admin](https://www.youlai.tech/vue3-element-admin/) |
|
||||
@@ -105,19 +104,16 @@
|
||||
| 提交规范 | [Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范](https://youlai.blog.csdn.net/article/details/145615236) |
|
||||
| 接口文档 | [https://www.apifox.cn](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
|
||||
|
||||
|
||||
|
||||
## 项目启动
|
||||
|
||||
- **环境准备**
|
||||
|
||||
| 环境类型 | 版本要求 | 备注 |
|
||||
|---------|---------|------|
|
||||
| ------------ | ------------------------------------------------------------ | --------------------------------- |
|
||||
| **Node.js** | `^20.19.0` 或 `>=22.12.0` | 推荐使用 LTS 版本(主版本为偶数) |
|
||||
| **包管理器** | `pnpm >= 8.0.0` | 项目使用 pnpm 作为包管理器 |
|
||||
| **开发工具** | [Visual Studio Code](https://code.visualstudio.com/Download) | 推荐安装 Vue、TypeScript 相关插件 |
|
||||
|
||||
|
||||
- **快速开始**
|
||||
|
||||
```bash
|
||||
@@ -140,7 +136,6 @@ pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
|
||||
## 项目部署
|
||||
|
||||
执行 `pnpm run build` 命令后,项目将被打包并生成 `dist` 目录。接下来,将 `dist` 目录下的文件上传到服务器 `/usr/share/nginx/html` 目录下,并配置 Nginx 进行反向代理。
|
||||
@@ -183,7 +178,6 @@ server {
|
||||
2. 根据后端工程的说明文档 [README.md](https://gitee.com/youlaiorg/youlai-boot#%E9%A1%B9%E7%9B%AE%E8%BF%90%E8%A1%8C) 完成本地启动。
|
||||
3. 修改 `.env.development` 文件中的 `VITE_APP_API_URL` 的值,将其从 https://api.youlai.tech 更改为 http://localhost:8989 即可。
|
||||
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **自动导入插件自动生成默认关闭**
|
||||
@@ -208,25 +202,21 @@ server {
|
||||
|
||||
如果有其他问题或者建议,建议 [ISSUE](https://gitee.com/youlaiorg/vue3-element-admin/issues/new)
|
||||
|
||||
|
||||
## 提交规范
|
||||
|
||||
执行 `pnpm run commit` 唤起 git commit 交互,根据提示完成信息的输入和选择。
|
||||
|
||||

|
||||
|
||||
|
||||
## 项目统计
|
||||
|
||||

|
||||
|
||||
|
||||
Thanks to all the contributors!
|
||||
感谢所有的贡献者!
|
||||
|
||||
[](https://github.com/youlaitech/vue3-element-admin/graphs/contributors)
|
||||
|
||||
|
||||
## 特别感谢
|
||||
|
||||
- 感谢 [GitCode](https://gitcode.com/) 官方的 [G-Star](https://gitcode.com/g-star) 认证
|
||||
|
||||
118
mock/ai.mock.ts
Normal file
118
mock/ai.mock.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { defineMock } from "./base";
|
||||
|
||||
export default defineMock([
|
||||
{
|
||||
url: "ai/assistant/parse",
|
||||
method: ["POST"],
|
||||
body: ({ body }) => {
|
||||
return {
|
||||
code: "00000",
|
||||
data: {
|
||||
parseLogId: "10001",
|
||||
success: true,
|
||||
functionCalls: [
|
||||
{
|
||||
name: "navigate",
|
||||
arguments: {
|
||||
path: "/system/user",
|
||||
},
|
||||
},
|
||||
],
|
||||
explanation: `Mock: 已解析命令:${body?.command ?? ""}`,
|
||||
confidence: 0.92,
|
||||
},
|
||||
msg: "一切ok",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "ai/assistant/execute",
|
||||
method: ["POST"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
success: true,
|
||||
message: "Mock: 执行成功",
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "ai/assistant/records",
|
||||
method: ["GET"],
|
||||
body: ({ query }) => {
|
||||
const pageNum = Number(query?.pageNum ?? 1);
|
||||
const pageSize = Number(query?.pageSize ?? 10);
|
||||
const total = 2;
|
||||
|
||||
return {
|
||||
code: "00000",
|
||||
data: {
|
||||
list: [
|
||||
{
|
||||
id: "10001",
|
||||
userId: 1,
|
||||
username: "admin",
|
||||
originalCommand: "跳转到用户管理",
|
||||
aiProvider: "qwen",
|
||||
aiModel: "qwen-plus",
|
||||
parseStatus: 1,
|
||||
functionCalls: JSON.stringify(
|
||||
[
|
||||
{
|
||||
name: "navigate",
|
||||
arguments: { path: "/system/user" },
|
||||
},
|
||||
],
|
||||
null,
|
||||
0
|
||||
),
|
||||
explanation: "Mock: 识别到跳转用户管理",
|
||||
confidence: 0.92,
|
||||
parseDurationMs: 128,
|
||||
functionName: "navigate",
|
||||
functionArguments: JSON.stringify({ path: "/system/user" }),
|
||||
executeStatus: 1,
|
||||
ipAddress: "127.0.0.1",
|
||||
createTime: "2025-12-17 15:00:00",
|
||||
updateTime: "2025-12-17 15:00:00",
|
||||
},
|
||||
{
|
||||
id: "10002",
|
||||
userId: 1,
|
||||
username: "admin",
|
||||
originalCommand: "获取姓名为张三的用户信息",
|
||||
aiProvider: "qwen",
|
||||
aiModel: "qwen-plus",
|
||||
parseStatus: 0,
|
||||
functionCalls: "[]",
|
||||
explanation: "Mock: 解析失败示例",
|
||||
confidence: 0.2,
|
||||
parseErrorMessage: "Mock: 无法匹配函数",
|
||||
parseDurationMs: 256,
|
||||
executeStatus: 0,
|
||||
ipAddress: "127.0.0.1",
|
||||
createTime: "2025-12-17 15:01:00",
|
||||
updateTime: "2025-12-17 15:01:00",
|
||||
},
|
||||
].slice((pageNum - 1) * pageSize, pageNum * pageSize),
|
||||
total,
|
||||
},
|
||||
msg: "一切ok",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "ai/assistant/records/:ids",
|
||||
method: ["DELETE"],
|
||||
body: ({ params }) => {
|
||||
return {
|
||||
code: "00000",
|
||||
data: {
|
||||
ids: params?.ids,
|
||||
},
|
||||
msg: "一切ok",
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -164,44 +164,4 @@ export default defineMock([
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "logs/visit-trend",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
dates: [
|
||||
"2024-06-30",
|
||||
"2024-07-01",
|
||||
"2024-07-02",
|
||||
"2024-07-03",
|
||||
"2024-07-04",
|
||||
"2024-07-05",
|
||||
"2024-07-06",
|
||||
"2024-07-07",
|
||||
],
|
||||
pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003],
|
||||
uvList: null,
|
||||
ipList: [207, 566, 565, 631, 579, 496, 222, 152],
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
url: "logs/visit-stats",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
todayPvCount: 1629,
|
||||
totalPvCount: 286086,
|
||||
pvGrowthRate: -0.65,
|
||||
todayIpCount: 169,
|
||||
totalIpCount: 19985,
|
||||
ipGrowthRate: -0.57,
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
43
mock/statistics.mock.ts
Normal file
43
mock/statistics.mock.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineMock } from "./base";
|
||||
|
||||
export default defineMock([
|
||||
{
|
||||
url: "statistics/visits/trend",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
dates: [
|
||||
"2024-06-30",
|
||||
"2024-07-01",
|
||||
"2024-07-02",
|
||||
"2024-07-03",
|
||||
"2024-07-04",
|
||||
"2024-07-05",
|
||||
"2024-07-06",
|
||||
"2024-07-07",
|
||||
],
|
||||
pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003],
|
||||
uvList: null,
|
||||
ipList: [207, 566, 565, 631, 579, 496, 222, 152],
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "statistics/visits/overview",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
todayUvCount: 169,
|
||||
totalUvCount: 19985,
|
||||
uvGrowthRate: -0.57,
|
||||
todayPvCount: 1629,
|
||||
totalPvCount: 286086,
|
||||
pvGrowthRate: -0.65,
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
]);
|
||||
65
mock/tenant.mock.ts
Normal file
65
mock/tenant.mock.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { defineMock } from "./base";
|
||||
|
||||
export default defineMock([
|
||||
{
|
||||
url: "tenants",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: "默认租户",
|
||||
domain: "default",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "演示租户",
|
||||
domain: "demo",
|
||||
},
|
||||
],
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "tenants/current",
|
||||
method: ["GET"],
|
||||
body: {
|
||||
code: "00000",
|
||||
data: {
|
||||
id: 1,
|
||||
name: "默认租户",
|
||||
domain: "default",
|
||||
},
|
||||
msg: "一切ok",
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "tenants/:tenantId/switch",
|
||||
method: ["POST"],
|
||||
body({ params }) {
|
||||
const tenantId = Number(params.tenantId);
|
||||
|
||||
const allTenants = [
|
||||
{
|
||||
id: 1,
|
||||
name: "默认租户",
|
||||
domain: "default",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "演示租户",
|
||||
domain: "demo",
|
||||
},
|
||||
];
|
||||
|
||||
const tenant = allTenants.find((t) => t.id === tenantId) || null;
|
||||
|
||||
return {
|
||||
code: tenant ? "00000" : "A0400",
|
||||
data: tenant,
|
||||
msg: tenant ? "切换租户成功" : "租户不存在",
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -1,180 +1,73 @@
|
||||
import request from "@/utils/request";
|
||||
import type {
|
||||
AiCommandRequest,
|
||||
AiCommandResponse,
|
||||
AiExecuteRequest,
|
||||
AiExecuteResponse,
|
||||
AiCommandRecordPageQuery,
|
||||
AiCommandRecordVo,
|
||||
} from "@/types/api";
|
||||
|
||||
/**
|
||||
* AI 命令请求参数
|
||||
*/
|
||||
export interface AiCommandRequest {
|
||||
/** 用户输入的自然语言命令 */
|
||||
command: string;
|
||||
/** 当前页面路由(用于上下文) */
|
||||
currentRoute?: string;
|
||||
/** 当前激活的组件名称 */
|
||||
currentComponent?: string;
|
||||
/** 额外上下文信息 */
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数调用参数
|
||||
*/
|
||||
export interface FunctionCall {
|
||||
/** 函数名称 */
|
||||
name: string;
|
||||
/** 函数描述 */
|
||||
description?: string;
|
||||
/** 参数对象 */
|
||||
arguments: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 命令解析响应
|
||||
*/
|
||||
export interface AiCommandResponse {
|
||||
/** 解析日志ID(用于关联执行记录) */
|
||||
parseLogId?: string;
|
||||
/** 是否成功解析 */
|
||||
success: boolean;
|
||||
/** 解析后的函数调用列表 */
|
||||
functionCalls: FunctionCall[];
|
||||
/** AI 的理解和说明 */
|
||||
explanation?: string;
|
||||
/** 置信度 (0-1) */
|
||||
confidence?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 原始 LLM 响应(用于调试) */
|
||||
rawResponse?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 命令执行请求
|
||||
*/
|
||||
export interface AiExecuteRequest {
|
||||
/** 关联的解析日志ID */
|
||||
parseLogId?: string;
|
||||
/** 原始命令(用于审计) */
|
||||
originalCommand?: string;
|
||||
/** 要执行的函数调用 */
|
||||
functionCall: FunctionCall;
|
||||
/** 确认模式:auto=自动执行, manual=需要用户确认 */
|
||||
confirmMode?: "auto" | "manual";
|
||||
/** 用户确认标志 */
|
||||
userConfirmed?: boolean;
|
||||
/** 幂等性令牌(防止重复执行) */
|
||||
idempotencyKey?: string;
|
||||
/** 当前页面路由 */
|
||||
currentRoute?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 命令执行响应
|
||||
*/
|
||||
export interface AiExecuteResponse {
|
||||
/** 是否执行成功 */
|
||||
success: boolean;
|
||||
/** 执行结果数据 */
|
||||
data?: any;
|
||||
/** 执行结果说明 */
|
||||
message?: string;
|
||||
/** 影响的记录数 */
|
||||
affectedRows?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 记录ID(用于追踪) */
|
||||
recordId?: string;
|
||||
/** 需要用户确认 */
|
||||
requiresConfirmation?: boolean;
|
||||
/** 确认提示信息 */
|
||||
confirmationPrompt?: string;
|
||||
}
|
||||
|
||||
export interface AiCommandRecordPageQuery extends PageQuery {
|
||||
keywords?: string;
|
||||
executeStatus?: number;
|
||||
parseStatus?: number;
|
||||
userId?: number;
|
||||
aiProvider?: string;
|
||||
aiModel?: string;
|
||||
functionName?: string;
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
export interface AiCommandRecordVO {
|
||||
id: string;
|
||||
userId: number;
|
||||
username: string;
|
||||
originalCommand: string;
|
||||
aiProvider?: string;
|
||||
aiModel?: string;
|
||||
parseStatus?: number;
|
||||
functionCalls?: string;
|
||||
explanation?: string;
|
||||
confidence?: number;
|
||||
parseErrorMessage?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
parseDurationMs?: number;
|
||||
functionName?: string;
|
||||
functionArguments?: string;
|
||||
executeStatus?: number;
|
||||
executeErrorMessage?: string;
|
||||
ipAddress?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
const AI_BASE_URL = "/api/v1/ai/assistant";
|
||||
|
||||
/**
|
||||
* AI 命令 API
|
||||
*/
|
||||
class AiCommandApi {
|
||||
const AiCommandApi = {
|
||||
/**
|
||||
* 解析自然语言命令
|
||||
* 解析 AI 命令
|
||||
*
|
||||
* @param data 命令请求参数
|
||||
* @returns 解析结果
|
||||
* @param data AI 命令请求参数
|
||||
* @returns AI 命令解析响应
|
||||
*/
|
||||
static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> {
|
||||
parseCommand(data: AiCommandRequest) {
|
||||
return request<any, AiCommandResponse>({
|
||||
url: "/api/v1/ai/command/parse",
|
||||
url: `${AI_BASE_URL}/parse`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行已解析的命令
|
||||
* 执行 AI 命令
|
||||
*
|
||||
* @param data 执行请求参数
|
||||
* @returns 执行结果数据(成功时返回,失败时抛出异常)
|
||||
* @param data AI 命令执行请求
|
||||
* @returns AI 命令执行响应
|
||||
*/
|
||||
static executeCommand(data: AiExecuteRequest): Promise<any> {
|
||||
return request<any, any>({
|
||||
url: "/api/v1/ai/command/execute",
|
||||
executeCommand(data: AiExecuteRequest) {
|
||||
return request<any, AiExecuteResponse>({
|
||||
url: `${AI_BASE_URL}/execute`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取命令记录分页列表
|
||||
* 获取 AI 命令记录分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @returns AI 命令记录分页列表
|
||||
*/
|
||||
static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
|
||||
return request<any, PageResult<AiCommandRecordVO[]>>({
|
||||
url: "/api/v1/ai/command/records",
|
||||
getPage(queryParams: AiCommandRecordPageQuery) {
|
||||
return request<any, PageResult<AiCommandRecordVo[]>>({
|
||||
url: `${AI_BASE_URL}/records`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 撤销命令执行(如果支持)
|
||||
* 删除 AI 命令记录
|
||||
*
|
||||
* @param ids 记录ID,多个以逗号分隔
|
||||
* @returns 删除结果
|
||||
*/
|
||||
static rollbackCommand(logId: string) {
|
||||
deleteByIds(ids: string) {
|
||||
return request({
|
||||
url: `/api/v1/ai/command/rollback/${logId}`,
|
||||
method: "post",
|
||||
url: `${AI_BASE_URL}/records/${ids}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default AiCommandApi;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { GeneratorPreviewVo, TablePageQuery, TablePageVo, GenConfigForm } from "@/api/types";
|
||||
|
||||
const GENERATOR_BASE_URL = "/api/v1/codegen";
|
||||
|
||||
const GeneratorAPI = {
|
||||
/** 获取数据表分页列表 */
|
||||
getTablePage(params: TablePageQuery) {
|
||||
return request<any, PageResult<TablePageVO[]>>({
|
||||
return request<any, PageResult<TablePageVo[]>>({
|
||||
url: `${GENERATOR_BASE_URL}/table/page`,
|
||||
method: "get",
|
||||
params,
|
||||
@@ -31,7 +32,7 @@ const GeneratorAPI = {
|
||||
|
||||
/** 获取代码生成预览数据 */
|
||||
getPreviewData(tableName: string, pageType?: "classic" | "curd") {
|
||||
return request<any, GeneratorPreviewVO[]>({
|
||||
return request<any, GeneratorPreviewVo[]>({
|
||||
url: `${GENERATOR_BASE_URL}/${tableName}/preview`,
|
||||
method: "get",
|
||||
params: pageType ? { pageType } : undefined,
|
||||
@@ -58,15 +59,26 @@ const GeneratorAPI = {
|
||||
params: pageType ? { pageType } : undefined,
|
||||
responseType: "blob",
|
||||
}).then((response) => {
|
||||
const fileName = decodeURI(
|
||||
response.headers["content-disposition"].split(";")[1].split("=")[1]
|
||||
);
|
||||
const contentDisposition = response?.headers?.["content-disposition"] as string | undefined;
|
||||
let fileName = `${tableName}.zip`;
|
||||
if (contentDisposition) {
|
||||
// content-disposition: attachment; filename=xxx.zip
|
||||
const match = /filename\*?=(?:UTF-8''|")?([^;"]+)/i.exec(contentDisposition);
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
fileName = decodeURIComponent(match[1]);
|
||||
} catch {
|
||||
fileName = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: "application/zip" });
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
});
|
||||
@@ -76,124 +88,3 @@ const GeneratorAPI = {
|
||||
export default GeneratorAPI;
|
||||
|
||||
/** 代码生成预览对象 */
|
||||
export interface GeneratorPreviewVO {
|
||||
/** 文件生成路径 */
|
||||
path: string;
|
||||
/** 文件名称 */
|
||||
fileName: string;
|
||||
/** 文件内容 */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** 数据表分页查询参数 */
|
||||
export interface TablePageQuery extends PageQuery {
|
||||
/** 关键字(表名) */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/** 数据表分页对象 */
|
||||
export interface TablePageVO {
|
||||
/** 表名称 */
|
||||
tableName: string;
|
||||
|
||||
/** 表描述 */
|
||||
tableComment: string;
|
||||
|
||||
/** 存储引擎 */
|
||||
engine: string;
|
||||
|
||||
/** 字符集排序规则 */
|
||||
tableCollation: string;
|
||||
|
||||
/** 创建时间 */
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/** 代码生成配置表单 */
|
||||
export interface GenConfigForm {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
|
||||
/** 表名 */
|
||||
tableName?: string;
|
||||
|
||||
/** 业务名 */
|
||||
businessName?: string;
|
||||
|
||||
/** 模块名 */
|
||||
moduleName?: string;
|
||||
|
||||
/** 包名 */
|
||||
packageName?: string;
|
||||
|
||||
/** 实体名 */
|
||||
entityName?: string;
|
||||
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
|
||||
/** 上级菜单 */
|
||||
parentMenuId?: string;
|
||||
|
||||
/** 后端应用名 */
|
||||
backendAppName?: string;
|
||||
/** 前端应用名 */
|
||||
frontendAppName?: string;
|
||||
|
||||
/** 字段配置列表 */
|
||||
fieldConfigs?: FieldConfig[];
|
||||
|
||||
/** 页面类型 classic|curd */
|
||||
pageType?: "classic" | "curd";
|
||||
|
||||
/** 要移除的表前缀,如 sys_ */
|
||||
removeTablePrefix?: string;
|
||||
}
|
||||
|
||||
/** 字段配置 */
|
||||
export interface FieldConfig {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
|
||||
/** 列名 */
|
||||
columnName?: string;
|
||||
|
||||
/** 列类型 */
|
||||
columnType?: string;
|
||||
|
||||
/** 字段名 */
|
||||
fieldName?: string;
|
||||
|
||||
/** 字段类型 */
|
||||
fieldType?: string;
|
||||
|
||||
/** 字段描述 */
|
||||
fieldComment?: string;
|
||||
|
||||
/** 是否在列表显示 */
|
||||
isShowInList?: number;
|
||||
|
||||
/** 是否在表单显示 */
|
||||
isShowInForm?: number;
|
||||
|
||||
/** 是否在查询条件显示 */
|
||||
isShowInQuery?: number;
|
||||
|
||||
/** 是否必填 */
|
||||
isRequired?: number;
|
||||
|
||||
/** 表单类型 */
|
||||
formType?: number;
|
||||
|
||||
/** 查询类型 */
|
||||
queryType?: number;
|
||||
|
||||
/** 字段长度 */
|
||||
maxLength?: number;
|
||||
|
||||
/** 字段排序 */
|
||||
fieldSort?: number;
|
||||
|
||||
/** 字典类型 */
|
||||
dictType?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from "@/utils/request";
|
||||
import type { FileInfo } from "@/types/api";
|
||||
|
||||
const FileAPI = {
|
||||
/** 上传文件 (传入 FormData,上传进度回调) */
|
||||
@@ -57,8 +58,3 @@ const FileAPI = {
|
||||
};
|
||||
|
||||
export default FileAPI;
|
||||
|
||||
export interface FileInfo {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { ConfigPageQuery, ConfigForm, ConfigPageVo } from "@/types/api";
|
||||
|
||||
const CONFIG_BASE_URL = "/api/v1/configs";
|
||||
|
||||
const ConfigAPI = {
|
||||
/** 获取配置分页数据 */
|
||||
getPage(queryParams?: ConfigPageQuery) {
|
||||
return request<any, PageResult<ConfigPageVO[]>>({
|
||||
return request<any, PageResult<ConfigPageVo[]>>({
|
||||
url: `${CONFIG_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -37,34 +38,3 @@ const ConfigAPI = {
|
||||
};
|
||||
|
||||
export default ConfigAPI;
|
||||
|
||||
export interface ConfigPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
export interface ConfigForm {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 配置名称 */
|
||||
configName?: string;
|
||||
/** 配置键 */
|
||||
configKey?: string;
|
||||
/** 配置值 */
|
||||
configValue?: string;
|
||||
/** 描述、备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ConfigPageVO {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 配置名称 */
|
||||
configName?: string;
|
||||
/** 配置键 */
|
||||
configKey?: string;
|
||||
/** 配置值 */
|
||||
configValue?: string;
|
||||
/** 描述、备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { DeptQuery, DeptVo, DeptForm } from "@/types/api";
|
||||
|
||||
const DEPT_BASE_URL = "/api/v1/depts";
|
||||
|
||||
const DeptAPI = {
|
||||
/** 获取部门树形列表 */
|
||||
getList(queryParams?: DeptQuery) {
|
||||
return request<any, DeptVO[]>({ url: `${DEPT_BASE_URL}`, method: "get", params: queryParams });
|
||||
return request<any, DeptVo[]>({ url: `${DEPT_BASE_URL}`, method: "get", params: queryParams });
|
||||
},
|
||||
/** 获取部门下拉数据源 */
|
||||
getOptions() {
|
||||
@@ -30,46 +31,3 @@ const DeptAPI = {
|
||||
};
|
||||
|
||||
export default DeptAPI;
|
||||
|
||||
export interface DeptQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 状态 */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface DeptVO {
|
||||
/** 子部门 */
|
||||
children?: DeptVO[];
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 部门ID */
|
||||
id?: string;
|
||||
/** 部门名称 */
|
||||
name?: string;
|
||||
/** 部门编号 */
|
||||
code?: string;
|
||||
/** 父部门ID */
|
||||
parentid?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 修改时间 */
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
export interface DeptForm {
|
||||
/** 部门ID(新增不填) */
|
||||
id?: string;
|
||||
/** 部门名称 */
|
||||
name?: string;
|
||||
/** 部门编号 */
|
||||
code?: string;
|
||||
/** 父部门ID */
|
||||
parentId: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import request from "@/utils/request";
|
||||
import type {
|
||||
DictPageQuery,
|
||||
DictPageVo,
|
||||
DictForm,
|
||||
DictItemPageQuery,
|
||||
DictItemPageVo,
|
||||
DictItemForm,
|
||||
DictItemOption,
|
||||
} from "@/types/api";
|
||||
|
||||
const DICT_BASE_URL = "/api/v1/dicts";
|
||||
|
||||
const DictAPI = {
|
||||
/** 字典分页列表 */
|
||||
getPage(queryParams: DictPageQuery) {
|
||||
return request<any, PageResult<DictPageVO[]>>({
|
||||
return request<any, PageResult<DictPageVo[]>>({
|
||||
url: `${DICT_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -34,7 +43,7 @@ const DictAPI = {
|
||||
|
||||
/** 获取字典项分页列表 */
|
||||
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
|
||||
return request<any, PageResult<DictItemPageVO[]>>({
|
||||
return request<any, PageResult<DictItemPageVo[]>>({
|
||||
url: `${DICT_BASE_URL}/${dictCode}/items/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -69,77 +78,3 @@ const DictAPI = {
|
||||
};
|
||||
|
||||
export default DictAPI;
|
||||
|
||||
export interface DictPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
}
|
||||
export interface DictPageVO {
|
||||
/** 字典ID */
|
||||
id: string;
|
||||
/** 字典名称 */
|
||||
name: string;
|
||||
/** 字典编码 */
|
||||
dictCode: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status: number;
|
||||
}
|
||||
export interface DictForm {
|
||||
/** 字典ID(新增不填) */
|
||||
id?: string;
|
||||
/** 字典名称 */
|
||||
name?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
export interface DictItemPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
}
|
||||
export interface DictItemPageVO {
|
||||
/** 字典项ID */
|
||||
id: string;
|
||||
/** 字典编码 */
|
||||
dictCode: string;
|
||||
/** 字典项值 */
|
||||
value: string;
|
||||
/** 字典项标签 */
|
||||
label: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status: number;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
}
|
||||
export interface DictItemForm {
|
||||
/** 字典项ID(新增不填) */
|
||||
id?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
/** 字典项值 */
|
||||
value?: string;
|
||||
/** 字典项标签 */
|
||||
label?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 标签类型 */
|
||||
tagType?: "success" | "warning" | "info" | "primary" | "danger" | "";
|
||||
}
|
||||
export interface DictItemOption {
|
||||
/** 值 */
|
||||
value: number | string;
|
||||
/** 标签 */
|
||||
label: string;
|
||||
/** 标签类型 */
|
||||
tagType?: "" | "success" | "info" | "warning" | "danger";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { LogPageQuery, LogPageVo } from "@/types/api";
|
||||
|
||||
const LOG_BASE_URL = "/api/v1/logs";
|
||||
|
||||
const LogAPI = {
|
||||
/** 获取日志分页列表 */
|
||||
getPage(queryParams: LogPageQuery) {
|
||||
return request<any, PageResult<LogPageVO[]>>({
|
||||
return request<any, PageResult<LogPageVo[]>>({
|
||||
url: `${LOG_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -14,35 +15,3 @@ const LogAPI = {
|
||||
};
|
||||
|
||||
export default LogAPI;
|
||||
|
||||
export interface LogPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 操作时间 */
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
export interface LogPageVO {
|
||||
/** 主键 */
|
||||
id: string;
|
||||
/** 日志模块 */
|
||||
module: string;
|
||||
/** 日志内容 */
|
||||
content: string;
|
||||
/** 请求路径 */
|
||||
requestUri: string;
|
||||
/** 请求方法 */
|
||||
method: string;
|
||||
/** IP 地址 */
|
||||
ip: string;
|
||||
/** 地区 */
|
||||
region: string;
|
||||
/** 浏览器 */
|
||||
browser: string;
|
||||
/** 终端系统 */
|
||||
os: string;
|
||||
/** 执行时间(毫秒) */
|
||||
executionTime: number;
|
||||
/** 操作人 */
|
||||
operator: string;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import request from "@/utils/request";
|
||||
import type { MenuTypeEnum } from "@/enums/business";
|
||||
import type { MenuQuery, MenuVo, MenuForm, MenuOption, RouteVo, Meta } from "@/types/api";
|
||||
|
||||
const MENU_BASE_URL = "/api/v1/menus";
|
||||
|
||||
const MenuAPI = {
|
||||
/** 获取当前用户的路由列表 */
|
||||
getRoutes() {
|
||||
return request<any, RouteVO[]>({ url: `${MENU_BASE_URL}/routes`, method: "get" });
|
||||
return request<any, RouteVo[]>({ url: `${MENU_BASE_URL}/routes`, method: "get" });
|
||||
},
|
||||
/** 获取菜单树形列表 */
|
||||
getList(queryParams: MenuQuery) {
|
||||
return request<any, MenuVO[]>({ url: `${MENU_BASE_URL}`, method: "get", params: queryParams });
|
||||
return request<any, MenuVo[]>({ url: `${MENU_BASE_URL}`, method: "get", params: queryParams });
|
||||
},
|
||||
/** 获取菜单下拉数据源 */
|
||||
getOptions(onlyParent?: boolean) {
|
||||
@@ -39,98 +40,3 @@ const MenuAPI = {
|
||||
};
|
||||
|
||||
export default MenuAPI;
|
||||
|
||||
export interface MenuQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
export interface MenuVO {
|
||||
/** 子菜单 */
|
||||
children?: MenuVO[];
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 菜单ID */
|
||||
id?: string;
|
||||
/** 菜单名称 */
|
||||
name?: string;
|
||||
/** 父菜单ID */
|
||||
parentId?: string;
|
||||
/** 按钮权限标识 */
|
||||
perm?: string;
|
||||
/** 跳转路径 */
|
||||
redirect?: string;
|
||||
/** 路由名称 */
|
||||
routeName?: string;
|
||||
/** 路由相对路径 */
|
||||
routePath?: string;
|
||||
/** 菜单排序(数字越小排名越靠前) */
|
||||
sort?: number;
|
||||
/** 菜单类型 */
|
||||
type?: MenuTypeEnum;
|
||||
/** 是否可见(1:显示;0:隐藏) */
|
||||
visible?: number;
|
||||
}
|
||||
export interface MenuForm {
|
||||
/** 菜单ID */
|
||||
id?: string;
|
||||
/** 父菜单ID */
|
||||
parentId?: string;
|
||||
/** 菜单名称 */
|
||||
name?: string;
|
||||
/** 是否可见(1-是 0-否) */
|
||||
visible: number;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 路由名称 */
|
||||
routeName?: string;
|
||||
/** 路由路径 */
|
||||
routePath?: string;
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** 跳转路由路径 */
|
||||
redirect?: string;
|
||||
/** 菜单类型 */
|
||||
type?: MenuTypeEnum;
|
||||
/** 权限标识 */
|
||||
perm?: string;
|
||||
/** 【菜单】是否开启页面缓存 */
|
||||
keepAlive?: number;
|
||||
/** 【目录】只有一个子路由是否始终显示 */
|
||||
alwaysShow?: number;
|
||||
/** 其他参数 */
|
||||
params?: KeyValue[];
|
||||
}
|
||||
interface KeyValue {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
export interface RouteVO {
|
||||
/** 子路由列表 */
|
||||
children: RouteVO[];
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** 路由属性 */
|
||||
meta?: Meta;
|
||||
/** 路由名称 */
|
||||
name?: string;
|
||||
/** 路由路径 */
|
||||
path?: string;
|
||||
/** 跳转链接 */
|
||||
redirect?: string;
|
||||
}
|
||||
export interface Meta {
|
||||
/** 【目录】只有一个子路由是否始终显示 */
|
||||
alwaysShow?: boolean;
|
||||
/** 是否隐藏(true-是 false-否) */
|
||||
hidden?: boolean;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 【菜单】是否开启页面缓存 */
|
||||
keepAlive?: boolean;
|
||||
/** 路由title */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { NoticePageQuery, NoticeForm, NoticePageVo, NoticeDetailVo } from "@/types/api";
|
||||
|
||||
const NOTICE_BASE_URL = "/api/v1/notices";
|
||||
|
||||
const NoticeAPI = {
|
||||
/** 获取通知公告分页数据 */
|
||||
getPage(queryParams?: NoticePageQuery) {
|
||||
return request<any, PageResult<NoticePageVO[]>>({
|
||||
return request<any, PageResult<NoticePageVo[]>>({
|
||||
url: `${NOTICE_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -37,7 +38,7 @@ const NoticeAPI = {
|
||||
},
|
||||
/** 查看通知 */
|
||||
getDetail(id: string) {
|
||||
return request<any, NoticeDetailVO>({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: "get" });
|
||||
return request<any, NoticeDetailVo>({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: "get" });
|
||||
},
|
||||
/** 全部已读 */
|
||||
readAll() {
|
||||
@@ -45,7 +46,7 @@ const NoticeAPI = {
|
||||
},
|
||||
/** 获取我的通知分页列表 */
|
||||
getMyNoticePage(queryParams?: NoticePageQuery) {
|
||||
return request<any, PageResult<NoticePageVO[]>>({
|
||||
return request<any, PageResult<NoticePageVo[]>>({
|
||||
url: `${NOTICE_BASE_URL}/my`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -54,68 +55,3 @@ const NoticeAPI = {
|
||||
};
|
||||
|
||||
export default NoticeAPI;
|
||||
|
||||
export interface NoticePageQuery extends PageQuery {
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 发布状态(0:草稿;1:已发布;2:已撤回) */
|
||||
publishStatus?: number;
|
||||
/** 是否已读(1:是;0:否) */
|
||||
isRead?: number;
|
||||
}
|
||||
export interface NoticeForm {
|
||||
/** 通知ID(新增不填) */
|
||||
id?: string;
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 内容 */
|
||||
content?: string;
|
||||
/** 类型 */
|
||||
type?: number;
|
||||
/** 优先级/级别 */
|
||||
level?: string;
|
||||
/** 目标类型 */
|
||||
targetType?: number;
|
||||
/** 目标用户ID(多个以英文逗号(,)分割) */
|
||||
targetUserIds?: string;
|
||||
}
|
||||
export interface NoticePageVO {
|
||||
/** 通知ID */
|
||||
id: string;
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 内容 */
|
||||
content?: string;
|
||||
/** 类型 */
|
||||
type?: number;
|
||||
/** 发布人ID */
|
||||
publisherId?: bigint;
|
||||
/** 优先级 */
|
||||
priority?: number;
|
||||
/** 目标类型 */
|
||||
targetType?: number;
|
||||
/** 发布状态 */
|
||||
publishStatus?: number;
|
||||
/** 发布时间 */
|
||||
publishTime?: Date;
|
||||
/** 撤回时间 */
|
||||
revokeTime?: Date;
|
||||
}
|
||||
export interface NoticeDetailVO {
|
||||
/** 通知ID */
|
||||
id?: string;
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 内容 */
|
||||
content?: string;
|
||||
/** 类型 */
|
||||
type?: number;
|
||||
/** 发布人名称 */
|
||||
publisherName?: string;
|
||||
/** 优先级/级别 */
|
||||
level?: string;
|
||||
/** 发布时间 */
|
||||
publishTime?: Date;
|
||||
/** 发布状态 */
|
||||
publishStatus?: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { RolePageQuery, RolePageVo, RoleForm } from "@/types/api";
|
||||
|
||||
const ROLE_BASE_URL = "/api/v1/roles";
|
||||
|
||||
const RoleAPI = {
|
||||
/** 获取角色分页数据 */
|
||||
getPage(queryParams?: RolePageQuery) {
|
||||
return request<any, PageResult<RolePageVO[]>>({
|
||||
return request<any, PageResult<RolePageVo[]>>({
|
||||
url: `${ROLE_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -42,38 +43,3 @@ const RoleAPI = {
|
||||
};
|
||||
|
||||
export default RoleAPI;
|
||||
|
||||
export interface RolePageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
export interface RolePageVO {
|
||||
/** 角色ID */
|
||||
id?: string;
|
||||
/** 角色编码 */
|
||||
code?: string;
|
||||
/** 角色名称 */
|
||||
name?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 角色状态 */
|
||||
status?: number;
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 修改时间 */
|
||||
updateTime?: Date;
|
||||
}
|
||||
export interface RoleForm {
|
||||
/** 角色ID */
|
||||
id?: string;
|
||||
/** 角色编码 */
|
||||
code?: string;
|
||||
/** 数据权限 */
|
||||
dataScope?: number;
|
||||
/** 角色名称 */
|
||||
name?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 角色状态(1-正常;0-停用) */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import request from "@/utils/request";
|
||||
import type { VisitTrendQuery, VisitTrendVo, VisitStatsVo } from "@/types/api";
|
||||
|
||||
const STATISTICS_BASE_URL = "/api/v1/statistics";
|
||||
|
||||
const StatisticsAPI = {
|
||||
/** 获取访问趋势统计 */
|
||||
getVisitTrend(queryParams: VisitTrendQuery) {
|
||||
return request<any, VisitTrendVO>({
|
||||
return request<any, VisitTrendVo>({
|
||||
url: `${STATISTICS_BASE_URL}/visits/trend`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -13,7 +14,7 @@ const StatisticsAPI = {
|
||||
},
|
||||
/** 获取访问概览统计 */
|
||||
getVisitOverview() {
|
||||
return request<any, VisitStatsVO>({
|
||||
return request<any, VisitStatsVo>({
|
||||
url: `${STATISTICS_BASE_URL}/visits/overview`,
|
||||
method: "get",
|
||||
});
|
||||
@@ -21,36 +22,3 @@ const StatisticsAPI = {
|
||||
};
|
||||
|
||||
export default StatisticsAPI;
|
||||
|
||||
export interface VisitTrendQuery {
|
||||
/** 开始日期 */
|
||||
startDate: string;
|
||||
/** 结束日期 */
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export interface VisitTrendVO {
|
||||
/** 日期列表 */
|
||||
dates: string[];
|
||||
/** 浏览量(PV) */
|
||||
pvList: number[];
|
||||
/** 访客数(UV) */
|
||||
uvList: number[];
|
||||
/** IP数 */
|
||||
ipList: number[];
|
||||
}
|
||||
|
||||
export interface VisitStatsVO {
|
||||
/** 今日访客数(UV) */
|
||||
todayUvCount: number;
|
||||
/** 总访客数 */
|
||||
totalUvCount: number;
|
||||
/** 访客数同比增长率(相对于昨天同一时间段的增长率) */
|
||||
uvGrowthRate: number;
|
||||
/** 今日浏览量(PV) */
|
||||
todayPvCount: number;
|
||||
/** 总浏览量 */
|
||||
totalPvCount: number;
|
||||
/** 同比增长率(相对于昨天同一时间段的增长率) */
|
||||
pvGrowthRate: number;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
import request from "@/utils/request";
|
||||
import type {
|
||||
TenantCreateForm,
|
||||
TenantCreateResultVo,
|
||||
TenantForm,
|
||||
TenantInfo,
|
||||
TenantPageQuery,
|
||||
TenantPageVo,
|
||||
} from "@/types/api";
|
||||
|
||||
const TENANT_BASE_URL = "/api/v1/tenants";
|
||||
|
||||
/**
|
||||
* 租户信息
|
||||
*/
|
||||
export interface TenantInfo {
|
||||
/** 租户ID */
|
||||
id: number;
|
||||
/** 租户名称 */
|
||||
name: string;
|
||||
/** 租户编码 */
|
||||
code?: string;
|
||||
/** 租户状态(1-正常 0-禁用) */
|
||||
status?: number;
|
||||
/** 联系人姓名 */
|
||||
contactName?: string;
|
||||
/** 联系人电话 */
|
||||
contactPhone?: string;
|
||||
/** 联系人邮箱 */
|
||||
contactEmail?: string;
|
||||
/** 租户域名 */
|
||||
domain?: string;
|
||||
/** 租户Logo */
|
||||
logo?: string;
|
||||
/** 是否默认租户 */
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户 API
|
||||
*/
|
||||
const TenantAPI = {
|
||||
/**
|
||||
* 获取当前用户的租户列表
|
||||
* 获取当前用户可访问的租户列表
|
||||
*/
|
||||
getTenantList() {
|
||||
return request<any, TenantInfo[]>({
|
||||
@@ -63,6 +46,58 @@ const TenantAPI = {
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
|
||||
/** 获取租户分页数据(平台租户管理) */
|
||||
getPage(queryParams?: TenantPageQuery) {
|
||||
return request<any, PageResult<TenantPageVo[]>>({
|
||||
url: `${TENANT_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
});
|
||||
},
|
||||
|
||||
/** 获取租户表单数据 */
|
||||
getFormData(tenantId: string) {
|
||||
return request<any, TenantForm>({
|
||||
url: `${TENANT_BASE_URL}/${tenantId}/form`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
/** 新增租户并初始化默认数据 */
|
||||
create(data: TenantCreateForm) {
|
||||
return request<any, TenantCreateResultVo>({
|
||||
url: `${TENANT_BASE_URL}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
/** 修改租户 */
|
||||
update(tenantId: string, data: TenantForm) {
|
||||
return request({
|
||||
url: `${TENANT_BASE_URL}/${tenantId}`,
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
},
|
||||
|
||||
/** 删除租户(批量) */
|
||||
deleteByIds(ids: string) {
|
||||
return request({
|
||||
url: `${TENANT_BASE_URL}/${ids}`,
|
||||
method: "delete",
|
||||
});
|
||||
},
|
||||
|
||||
/** 修改租户状态 */
|
||||
updateStatus(tenantId: string, status: number) {
|
||||
return request({
|
||||
url: `${TENANT_BASE_URL}/${tenantId}/status`,
|
||||
method: "put",
|
||||
params: { status },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default TenantAPI;
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import request from "@/utils/request";
|
||||
import type {
|
||||
UserInfo,
|
||||
UserPageQuery,
|
||||
UserPageVo,
|
||||
UserForm,
|
||||
UserProfileVo,
|
||||
UserProfileForm,
|
||||
PasswordChangeForm,
|
||||
MobileUpdateForm,
|
||||
EmailUpdateForm,
|
||||
} from "@/types/api";
|
||||
|
||||
const USER_BASE_URL = "/api/v1/users";
|
||||
|
||||
@@ -21,7 +32,7 @@ const UserAPI = {
|
||||
* @param queryParams 查询参数
|
||||
*/
|
||||
getPage(queryParams: UserPageQuery) {
|
||||
return request<any, PageResult<UserPageVO[]>>({
|
||||
return request<any, PageResult<UserPageVo[]>>({
|
||||
url: `${USER_BASE_URL}/page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
@@ -139,7 +150,7 @@ const UserAPI = {
|
||||
|
||||
/** 获取个人中心用户信息 */
|
||||
getProfile() {
|
||||
return request<any, UserProfileVO>({
|
||||
return request<any, UserProfileVo>({
|
||||
url: `${USER_BASE_URL}/profile`,
|
||||
method: "get",
|
||||
});
|
||||
@@ -211,174 +222,3 @@ const UserAPI = {
|
||||
};
|
||||
|
||||
export default UserAPI;
|
||||
|
||||
/** 登录用户信息 */
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId?: string;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 角色 */
|
||||
roles: string[];
|
||||
|
||||
/** 权限 */
|
||||
perms: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户分页查询对象
|
||||
*/
|
||||
export interface UserPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
|
||||
/** 用户状态 */
|
||||
status?: number;
|
||||
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
|
||||
/** 开始时间 */
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
/** 用户分页对象 */
|
||||
export interface UserPageVO {
|
||||
/** 用户ID */
|
||||
id: string;
|
||||
/** 用户头像URL */
|
||||
avatar?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||
roleNames?: string;
|
||||
/** 用户状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 用户表单类型 */
|
||||
export interface UserForm {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
/** 用户头像 */
|
||||
avatar?: string;
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
/** 角色ID集合 */
|
||||
roleIds?: number[];
|
||||
/** 用户状态(1:正常;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息 */
|
||||
export interface UserProfileVO {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
|
||||
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||
roleNames?: string;
|
||||
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息表单 */
|
||||
export interface UserProfileForm {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/** 修改密码表单 */
|
||||
export interface PasswordChangeForm {
|
||||
/** 原密码 */
|
||||
oldPassword?: string;
|
||||
/** 新密码 */
|
||||
newPassword?: string;
|
||||
/** 确认新密码 */
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
/** 修改手机表单 */
|
||||
export interface MobileUpdateForm {
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** 修改邮箱表单 */
|
||||
export interface EmailUpdateForm {
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
1
src/api/types.ts
Normal file
1
src/api/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@/types/api";
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -3,16 +3,28 @@
|
||||
<div class="ai-assistant">
|
||||
<!-- AI 助手图标按钮 -->
|
||||
<el-button
|
||||
v-if="!dialogVisible"
|
||||
v-if="!dialogVisible && !fabCollapsed"
|
||||
class="ai-fab-button"
|
||||
type="primary"
|
||||
circle
|
||||
size="large"
|
||||
:style="fabStyle"
|
||||
@contextmenu.prevent="fabCollapsed = true"
|
||||
@click="handleOpen"
|
||||
>
|
||||
<div class="i-svg:ai ai-icon" />
|
||||
</el-button>
|
||||
|
||||
<!-- 收缩态:贴边小标签,避免遮挡表单控件 -->
|
||||
<div
|
||||
v-if="!dialogVisible && fabCollapsed"
|
||||
class="ai-fab-tab"
|
||||
:style="fabStyle"
|
||||
@click="fabCollapsed = false"
|
||||
>
|
||||
AI
|
||||
</div>
|
||||
|
||||
<!-- AI 对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -107,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { onBeforeUnmount, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
@@ -151,6 +163,111 @@ const command = ref("");
|
||||
const loading = ref(false);
|
||||
const response = ref<AiResponse | null>(null);
|
||||
|
||||
const fabCollapsed = useStorage<boolean>("vea:ui:ai_assistant_fab_collapsed", false);
|
||||
|
||||
const fabRight = ref(30);
|
||||
const fabBottom = ref(80);
|
||||
const fabStyle = computed(() => ({
|
||||
right: `${fabRight.value}px`,
|
||||
bottom: `${fabBottom.value}px`,
|
||||
}));
|
||||
|
||||
const isElementVisible = (el: Element) => {
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.display === "none" || style.visibility === "hidden") {
|
||||
return false;
|
||||
}
|
||||
return (el as HTMLElement).getClientRects().length > 0;
|
||||
};
|
||||
|
||||
const getActiveRightDrawerWidth = (): number => {
|
||||
const drawers = Array.from(document.querySelectorAll(".el-drawer"));
|
||||
for (let i = drawers.length - 1; i >= 0; i--) {
|
||||
const drawer = drawers[i] as HTMLElement;
|
||||
if (!isElementVisible(drawer)) {
|
||||
continue;
|
||||
}
|
||||
const rect = drawer.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.right >= window.innerWidth - 1) {
|
||||
return rect.width;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const updateFabPosition = () => {
|
||||
const safeMargin = 24;
|
||||
const drawerWidth = getActiveRightDrawerWidth() || 0;
|
||||
const baseRight = drawerWidth + 30;
|
||||
|
||||
// base position
|
||||
const nextRight = baseRight;
|
||||
let nextBottom = 80;
|
||||
|
||||
// Avoid Element Plus popper overlays (select dropdown, icon picker, date picker, etc.)
|
||||
// If the FAB would overlap any visible popper, push it upward.
|
||||
const fabSize = fabCollapsed.value ? 42 : 60;
|
||||
const computeFabRect = (rightPx: number, bottomPx: number) => {
|
||||
const right = window.innerWidth - rightPx;
|
||||
const left = right - fabSize;
|
||||
const bottom = window.innerHeight - bottomPx;
|
||||
const top = bottom - fabSize;
|
||||
return { left, right, top, bottom };
|
||||
};
|
||||
|
||||
const intersects = (
|
||||
a: { left: number; right: number; top: number; bottom: number },
|
||||
b: DOMRect
|
||||
) => {
|
||||
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
|
||||
};
|
||||
|
||||
const poppers = Array.from(document.querySelectorAll(".el-popper"));
|
||||
for (const popper of poppers) {
|
||||
if (!isElementVisible(popper)) {
|
||||
continue;
|
||||
}
|
||||
const rect = (popper as HTMLElement).getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateFabRect = computeFabRect(nextRight, nextBottom);
|
||||
if (intersects(candidateFabRect, rect)) {
|
||||
const requiredBottom = Math.ceil(window.innerHeight - rect.top + safeMargin);
|
||||
nextBottom = Math.max(nextBottom, requiredBottom);
|
||||
}
|
||||
}
|
||||
|
||||
// clamp so the button doesn't get pushed off-screen
|
||||
const maxBottom = window.innerHeight - fabSize - safeMargin;
|
||||
nextBottom = Math.min(nextBottom, Math.max(0, maxBottom));
|
||||
|
||||
fabRight.value = nextRight + (drawerWidth > 0 ? safeMargin : 0);
|
||||
fabBottom.value = nextBottom;
|
||||
};
|
||||
|
||||
watch(
|
||||
fabCollapsed,
|
||||
() => {
|
||||
updateFabPosition();
|
||||
},
|
||||
{ flush: "post" }
|
||||
);
|
||||
|
||||
let domObserver: MutationObserver | null = null;
|
||||
let rafId: number | null = null;
|
||||
|
||||
const scheduleUpdateFabPosition = () => {
|
||||
if (rafId != null) {
|
||||
return;
|
||||
}
|
||||
rafId = window.requestAnimationFrame(() => {
|
||||
rafId = null;
|
||||
updateFabPosition();
|
||||
});
|
||||
};
|
||||
|
||||
// 快捷命令示例
|
||||
const examples = [
|
||||
"修改test用户的姓名为测试人员",
|
||||
@@ -550,7 +667,32 @@ const executeAction = async (action: AiAction) => {
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onMounted(() => {
|
||||
updateFabPosition();
|
||||
window.addEventListener("resize", updateFabPosition);
|
||||
|
||||
domObserver = new MutationObserver(() => {
|
||||
scheduleUpdateFabPosition();
|
||||
});
|
||||
domObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style"],
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updateFabPosition);
|
||||
if (domObserver) {
|
||||
domObserver.disconnect();
|
||||
domObserver = null;
|
||||
}
|
||||
if (rafId != null) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
rafId = null;
|
||||
}
|
||||
|
||||
if (navigationTimer) {
|
||||
clearTimeout(navigationTimer);
|
||||
navigationTimer = null;
|
||||
@@ -566,8 +708,6 @@ onBeforeUnmount(() => {
|
||||
.ai-assistant {
|
||||
.ai-fab-button {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
bottom: 80px;
|
||||
z-index: 9999;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
@@ -584,6 +724,24 @@ onBeforeUnmount(() => {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-fab-tab {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 4px 12px rgba(2, 119, 252, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assistant-dialog {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleTenantSwitch">
|
||||
<div class="tenant-select">
|
||||
<el-icon class="tenant-select__icon"><OfficeBuilding /></el-icon>
|
||||
<span class="tenant-select__name">{{ currentTenantName }}</span>
|
||||
<el-icon class="tenant-select__arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:command="tenant.id"
|
||||
:class="{ 'is-active': tenant.id === currentTenantId }"
|
||||
>
|
||||
<div class="tenant-item">
|
||||
<span class="tenant-item__name">{{ tenant.name }}</span>
|
||||
<el-icon v-if="tenant.id === currentTenantId" class="tenant-item__check">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { OfficeBuilding, ArrowDown, Check } from "@element-plus/icons-vue";
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
// 当前租户名称
|
||||
const currentTenantName = computed(() => {
|
||||
if (tenantStore.currentTenant?.name) {
|
||||
return tenantStore.currentTenant.name;
|
||||
}
|
||||
// 如果当前租户信息不存在,尝试从租户列表中查找
|
||||
if (tenantStore.currentTenantId) {
|
||||
const tenant = tenantStore.tenantList.find((t) => t.id === tenantStore.currentTenantId);
|
||||
if (tenant) {
|
||||
return tenant.name;
|
||||
}
|
||||
}
|
||||
return "未选择租户";
|
||||
});
|
||||
|
||||
// 当前租户ID
|
||||
const currentTenantId = computed(() => tenantStore.currentTenantId);
|
||||
|
||||
// 租户列表
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*/
|
||||
async function handleTenantSwitch(tenantId: number) {
|
||||
if (tenantId === currentTenantId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tenantStore.switchTenant(tenantId);
|
||||
ElMessage.success("切换租户成功");
|
||||
// 刷新页面以重新加载菜单和权限
|
||||
window.location.reload();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:获取租户列表
|
||||
onMounted(() => {
|
||||
tenantStore.fetchTenantList().catch((error) => {
|
||||
console.error("获取租户列表失败:", error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&__icon {
|
||||
margin-right: 6px;
|
||||
font-size: 18px;
|
||||
color: inherit;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&__name {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
|
||||
.tenant-select__icon,
|
||||
.tenant-select__name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tenant-select__arrow {
|
||||
color: var(--el-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__check {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
</style>
|
||||
@@ -1,187 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择租户"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<div v-if="loading" class="tenant-dialog-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载租户列表...</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tenantList.length === 0" class="tenant-dialog-empty">
|
||||
<el-empty description="暂无可用租户" />
|
||||
</div>
|
||||
|
||||
<el-radio-group v-else v-model="selectedTenantId" class="tenant-radio-group">
|
||||
<el-radio
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:label="tenant.id"
|
||||
class="tenant-radio-item"
|
||||
>
|
||||
<div class="tenant-radio-content">
|
||||
<div class="tenant-radio-content__name">{{ tenant.name }}</div>
|
||||
<div v-if="tenant.code" class="tenant-radio-content__code">{{ tenant.code }}</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="switching" @click="handleConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
confirm: [];
|
||||
}>();
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const switching = ref(false);
|
||||
const selectedTenantId = ref<number | null>(null);
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
// 监听对话框打开,加载租户列表
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadTenantList();
|
||||
// 默认选择当前租户或第一个租户
|
||||
selectedTenantId.value = tenantStore.currentTenantId || tenantList.value[0]?.id || null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载租户列表
|
||||
*/
|
||||
async function loadTenantList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await tenantStore.fetchTenantList();
|
||||
// 如果列表为空,自动关闭对话框
|
||||
if (tenantList.value.length === 0) {
|
||||
visible.value = false;
|
||||
ElMessage.warning("您暂无可用租户");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "获取租户列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
async function handleConfirm() {
|
||||
if (!selectedTenantId.value) {
|
||||
ElMessage.warning("请选择租户");
|
||||
return;
|
||||
}
|
||||
|
||||
switching.value = true;
|
||||
try {
|
||||
await tenantStore.switchTenant(selectedTenantId.value);
|
||||
ElMessage.success("切换租户成功");
|
||||
visible.value = false;
|
||||
emit("confirm");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
} finally {
|
||||
switching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
// 如果用户没有租户,不允许取消
|
||||
if (tenantList.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-dialog-loading {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.tenant-dialog-empty {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tenant-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tenant-radio-item {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-radio__input.is-checked) {
|
||||
.el-radio__inner {
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-radio-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__code {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<el-dropdown v-if="showTenantSelector" trigger="click" @command="handleSwitchTenant">
|
||||
<div class="tenant-selector">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span class="tenant-name">{{ currentTenantName }}</span>
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:command="tenant.id"
|
||||
:disabled="tenant.id === currentTenantId"
|
||||
>
|
||||
<div class="tenant-item">
|
||||
<span>{{ tenant.name }}</span>
|
||||
<el-icon v-if="tenant.id === currentTenantId" class="check-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { OfficeBuilding, ArrowDown, Check } from "@element-plus/icons-vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
/**
|
||||
* 租户切换器组件
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前租户名称
|
||||
* - 下拉列表展示所有可访问的租户
|
||||
* - 点击切换租户
|
||||
* - 切换后刷新页面以重新加载数据
|
||||
*
|
||||
* 使用条件:
|
||||
* - 需要在 .env 中设置 VITE_APP_TENANT_ENABLED=true
|
||||
* - 后端需要启用多租户功能
|
||||
* - 用户至少属于一个租户
|
||||
*/
|
||||
|
||||
// 多租户开关
|
||||
const TENANT_ENABLED = import.meta.env.VITE_APP_TENANT_ENABLED === "true";
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
// 当前租户ID
|
||||
const currentTenantId = computed(() => tenantStore.currentTenantId);
|
||||
|
||||
// 当前租户名称
|
||||
const currentTenantName = computed(() => {
|
||||
return tenantStore.currentTenant?.name || "未选择租户";
|
||||
});
|
||||
|
||||
// 租户列表
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
// 是否显示租户切换器(多租户开关启用 且 有租户列表)
|
||||
const showTenantSelector = computed(() => {
|
||||
return TENANT_ENABLED && tenantList.value.length > 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*/
|
||||
const handleSwitchTenant = async (tenantId: number) => {
|
||||
if (tenantId === currentTenantId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tenantStore.switchTenant(tenantId);
|
||||
ElMessage.success("切换租户成功");
|
||||
|
||||
// 刷新页面以重新加载数据(确保所有数据都基于新租户)
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || "切换租户失败");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tenant-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.tenant-name {
|
||||
margin: 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 150px;
|
||||
|
||||
.check-icon {
|
||||
margin-left: 10px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,33 @@
|
||||
<template>
|
||||
<el-select
|
||||
<el-dropdown
|
||||
v-if="tenantList.length > 0"
|
||||
v-model="currentTenantIdRef"
|
||||
placeholder="选择租户"
|
||||
style="width: 180px"
|
||||
@change="onChange"
|
||||
class="tenant-switcher"
|
||||
trigger="click"
|
||||
:hide-on-click="true"
|
||||
@command="onCommand"
|
||||
>
|
||||
<el-option v-for="item in tenantList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<div class="tenant-switcher__trigger">
|
||||
<span class="tenant-switcher__label">{{ currentTenantName }}</span>
|
||||
<el-icon class="tenant-switcher__icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="tenant-switcher__menu">
|
||||
<el-dropdown-item
|
||||
v-for="item in tenantList"
|
||||
:key="item.id"
|
||||
:command="item.id"
|
||||
:class="{ 'is-active': item.id === currentTenantIdRef }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -29,7 +45,62 @@ const currentTenantIdRef = computed<number | null>({
|
||||
},
|
||||
});
|
||||
|
||||
function onChange(tenantId: number) {
|
||||
const currentTenantName = computed(() => {
|
||||
const currentId = currentTenantIdRef.value;
|
||||
const fromList = tenantList.value.find((t) => t.id === currentId)?.name;
|
||||
return fromList || tenantStore.currentTenant?.name || "切换租户";
|
||||
});
|
||||
|
||||
function onCommand(tenantId: number) {
|
||||
if (tenantId === currentTenantIdRef.value) {
|
||||
return;
|
||||
}
|
||||
emit("change", tenantId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tenant-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
&__label {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tenant-switcher__trigger {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/**
|
||||
* 配置统一导出
|
||||
*/
|
||||
|
||||
export * from "./storage";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 本地存储键名配置
|
||||
*
|
||||
* @description
|
||||
* 统一管理 localStorage/sessionStorage 的键名
|
||||
* 命名规范:{prefix}:{namespace}:{key}
|
||||
*/
|
||||
|
||||
export const APP_PREFIX = "vea";
|
||||
|
||||
/**
|
||||
* 存储键名常量
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
// ===== 认证相关 =====
|
||||
ACCESS_TOKEN: `${APP_PREFIX}:auth:access_token`,
|
||||
REFRESH_TOKEN: `${APP_PREFIX}:auth:refresh_token`,
|
||||
REMEMBER_ME: `${APP_PREFIX}:auth:remember_me`,
|
||||
|
||||
// ===== 租户相关 =====
|
||||
TENANT_ID: `${APP_PREFIX}:tenant:id`,
|
||||
TENANT_INFO: `${APP_PREFIX}:tenant:info`,
|
||||
|
||||
// ===== 系统相关 =====
|
||||
DICT_CACHE: `${APP_PREFIX}:system:dict_cache`,
|
||||
|
||||
// ===== UI 设置 =====
|
||||
SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`,
|
||||
SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`,
|
||||
SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`,
|
||||
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`,
|
||||
LAYOUT: `${APP_PREFIX}:ui:layout`,
|
||||
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`,
|
||||
THEME: `${APP_PREFIX}:ui:theme`,
|
||||
THEME_COLOR: `${APP_PREFIX}:ui:theme_color`,
|
||||
|
||||
// ===== 应用状态 =====
|
||||
DEVICE: `${APP_PREFIX}:app:device`,
|
||||
SIZE: `${APP_PREFIX}:app:size`,
|
||||
LANGUAGE: `${APP_PREFIX}:app:language`,
|
||||
SIDEBAR_STATUS: `${APP_PREFIX}:app:sidebar_status`,
|
||||
ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 认证相关键名(便于批量操作)
|
||||
*/
|
||||
export const AUTH_KEYS = {
|
||||
ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN,
|
||||
REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN,
|
||||
REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 租户相关键名(便于批量操作)
|
||||
*/
|
||||
export const TENANT_KEYS = {
|
||||
TENANT_ID: STORAGE_KEYS.TENANT_ID,
|
||||
TENANT_INFO: STORAGE_KEYS.TENANT_INFO,
|
||||
} as const;
|
||||
108
src/constants/index.ts
Normal file
108
src/constants/index.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 常量统一导出
|
||||
*
|
||||
* @description
|
||||
* 包含应用中所有的常量定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 超级管理员角色标识
|
||||
*
|
||||
* @description
|
||||
* 拥有系统最高权限,可以访问所有资源
|
||||
*/
|
||||
export const ROLE_ROOT = "ROOT";
|
||||
|
||||
/**
|
||||
* 应用存储前缀
|
||||
*/
|
||||
export const APP_PREFIX = "vea";
|
||||
|
||||
/**
|
||||
* 存储命名空间
|
||||
*/
|
||||
const NAMESPACES = {
|
||||
AUTH: "auth",
|
||||
TENANT: "tenant",
|
||||
SYSTEM: "system",
|
||||
UI: "ui",
|
||||
APP: "app",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 存储键名常量
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
// ===== 认证相关 =====
|
||||
ACCESS_TOKEN: `${APP_PREFIX}:${NAMESPACES.AUTH}:access_token`,
|
||||
REFRESH_TOKEN: `${APP_PREFIX}:${NAMESPACES.AUTH}:refresh_token`,
|
||||
REMEMBER_ME: `${APP_PREFIX}:${NAMESPACES.AUTH}:remember_me`,
|
||||
|
||||
// ===== 租户相关 =====
|
||||
TENANT_ID: `${APP_PREFIX}:${NAMESPACES.TENANT}:id`,
|
||||
TENANT_INFO: `${APP_PREFIX}:${NAMESPACES.TENANT}:info`,
|
||||
|
||||
// ===== 系统相关 =====
|
||||
DICT_CACHE: `${APP_PREFIX}:${NAMESPACES.SYSTEM}:dict_cache`,
|
||||
|
||||
// ===== UI 设置 =====
|
||||
SHOW_TAGS_VIEW: `${APP_PREFIX}:${NAMESPACES.UI}:show_tags_view`,
|
||||
SHOW_APP_LOGO: `${APP_PREFIX}:${NAMESPACES.UI}:show_app_logo`,
|
||||
SHOW_WATERMARK: `${APP_PREFIX}:${NAMESPACES.UI}:show_watermark`,
|
||||
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:${NAMESPACES.UI}:enable_ai_assistant`,
|
||||
LAYOUT: `${APP_PREFIX}:${NAMESPACES.UI}:layout`,
|
||||
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:${NAMESPACES.UI}:sidebar_color_scheme`,
|
||||
THEME: `${APP_PREFIX}:${NAMESPACES.UI}:theme`,
|
||||
THEME_COLOR: `${APP_PREFIX}:${NAMESPACES.UI}:theme_color`,
|
||||
|
||||
// ===== 应用状态 =====
|
||||
DEVICE: `${APP_PREFIX}:${NAMESPACES.APP}:device`,
|
||||
SIZE: `${APP_PREFIX}:${NAMESPACES.APP}:size`,
|
||||
LANGUAGE: `${APP_PREFIX}:${NAMESPACES.APP}:language`,
|
||||
SIDEBAR_STATUS: `${APP_PREFIX}:${NAMESPACES.APP}:sidebar_status`,
|
||||
ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:${NAMESPACES.APP}:active_top_menu_path`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 认证相关键名(便于批量操作)
|
||||
*/
|
||||
export const AUTH_KEYS = {
|
||||
ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN,
|
||||
REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN,
|
||||
REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 租户相关键名(便于批量操作)
|
||||
*/
|
||||
export const TENANT_KEYS = {
|
||||
TENANT_ID: STORAGE_KEYS.TENANT_ID,
|
||||
TENANT_INFO: STORAGE_KEYS.TENANT_INFO,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* UI设置相关键名
|
||||
*/
|
||||
export const UI_KEYS = {
|
||||
SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW,
|
||||
SHOW_APP_LOGO: STORAGE_KEYS.SHOW_APP_LOGO,
|
||||
SHOW_WATERMARK: STORAGE_KEYS.SHOW_WATERMARK,
|
||||
ENABLE_AI_ASSISTANT: STORAGE_KEYS.ENABLE_AI_ASSISTANT,
|
||||
LAYOUT: STORAGE_KEYS.LAYOUT,
|
||||
SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
|
||||
THEME: STORAGE_KEYS.THEME,
|
||||
THEME_COLOR: STORAGE_KEYS.THEME_COLOR,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 应用状态相关键名
|
||||
*/
|
||||
export const APP_KEYS = {
|
||||
DEVICE: STORAGE_KEYS.DEVICE,
|
||||
SIZE: STORAGE_KEYS.SIZE,
|
||||
LANGUAGE: STORAGE_KEYS.LANGUAGE,
|
||||
SIDEBAR_STATUS: STORAGE_KEYS.SIDEBAR_STATUS,
|
||||
ACTIVE_TOP_MENU_PATH: STORAGE_KEYS.ACTIVE_TOP_MENU_PATH,
|
||||
} as const;
|
||||
|
||||
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
import { useUserStore } from "@/store";
|
||||
import { ROLE_ROOT } from "@/enums";
|
||||
import { ROLE_ROOT } from "@/constants";
|
||||
|
||||
/**
|
||||
* 按钮权限
|
||||
|
||||
@@ -25,23 +25,3 @@ export enum UserGender {
|
||||
/** 女 */
|
||||
FEMALE = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员角色标识
|
||||
*
|
||||
* @description
|
||||
* 拥有系统最高权限,可以访问所有资源
|
||||
*/
|
||||
export const ROLE_ROOT = "ROOT";
|
||||
|
||||
/**
|
||||
* 角色类型枚举
|
||||
*/
|
||||
export enum RoleType {
|
||||
/** 超级管理员 */
|
||||
ROOT = "ROOT",
|
||||
/** 管理员 */
|
||||
ADMIN = "ADMIN",
|
||||
/** 普通用户 */
|
||||
USER = "USER",
|
||||
}
|
||||
|
||||
@@ -279,13 +279,18 @@ function handleSettingsClick() {
|
||||
}
|
||||
|
||||
// 租户选择器在白色文字模式下的样式
|
||||
:deep(.tenant-select) {
|
||||
::v-deep(.tenant-switcher__trigger) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover {
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger:hover) {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,18 +315,23 @@ function handleSettingsClick() {
|
||||
}
|
||||
|
||||
// 租户选择器在深色文字模式下的样式
|
||||
:deep(.tenant-select) {
|
||||
::v-deep(.tenant-switcher__trigger) {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
|
||||
&:hover {
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger:hover) {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保下拉菜单中的图标不受影响
|
||||
:deep(.el-dropdown-menu) {
|
||||
::v-deep(.el-dropdown-menu) {
|
||||
[class^="i-svg:"] {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
import en from "element-plus/es/locale/lang/en";
|
||||
import { store } from "@/store";
|
||||
import { DeviceEnum, SidebarStatus } from "@/enums";
|
||||
import { STORAGE_KEYS } from "@/config/storage";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
// 设备类型
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { store } from "@/store";
|
||||
import DictAPI, { type DictItemOption } from "@/api/system/dict";
|
||||
import { STORAGE_KEYS } from "@/config/storage";
|
||||
import DictAPI from "@/api/system/dict";
|
||||
import type { DictItemOption } from "@/types/api";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
export const useDictStore = defineStore("dict", () => {
|
||||
// 字典数据缓存
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defaultSettings } from "@/settings";
|
||||
import { SidebarColor, ThemeMode } from "@/enums";
|
||||
import type { LayoutMode } from "@/enums";
|
||||
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
|
||||
import { STORAGE_KEYS } from "@/config/storage";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
// 🎯 设置项类型定义
|
||||
interface SettingsState {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { store } from "@/store";
|
||||
import TenantAPI, { type TenantInfo } from "@/api/system/tenant";
|
||||
import { STORAGE_KEYS } from "@/config/storage";
|
||||
import TenantAPI from "@/api/system/tenant";
|
||||
import type { TenantInfo } from "@/types/api";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
|
||||
/**
|
||||
* 租户 Store
|
||||
@@ -63,28 +64,52 @@ export const useTenantStore = defineStore("tenant", () => {
|
||||
* 此方法由路由守卫调用,仅在启用多租户时执行
|
||||
*/
|
||||
async function loadTenant() {
|
||||
restoreTenant();
|
||||
|
||||
// 1. 获取租户列表
|
||||
await fetchTenantList();
|
||||
|
||||
// 2. 如果已有租户列表且未设置当前租户
|
||||
if (tenantList.value.length > 0 && !currentTenantId.value) {
|
||||
// 2. 校验本地恢复的租户是否仍然可用(避免 tenantId 不在列表导致无默认选中)
|
||||
if (
|
||||
currentTenantId.value &&
|
||||
tenantList.value.length > 0 &&
|
||||
!tenantList.value.some((t) => t.id === currentTenantId.value)
|
||||
) {
|
||||
console.debug("[Tenant] 本地租户已不可用,清除并重新选择:", currentTenantId.value);
|
||||
currentTenantId.value = null;
|
||||
currentTenant.value = null;
|
||||
localStorage.removeItem(STORAGE_KEYS.TENANT_ID);
|
||||
localStorage.removeItem(STORAGE_KEYS.TENANT_INFO);
|
||||
}
|
||||
|
||||
// 3. 如果已有租户列表,则保证一定有一个默认租户被选中
|
||||
if (tenantList.value.length > 0) {
|
||||
// 3.1 优先后端当前租户
|
||||
if (!currentTenantId.value) {
|
||||
try {
|
||||
// 尝试从后端获取当前租户
|
||||
const currentTenantInfo = await TenantAPI.getCurrentTenant();
|
||||
if (currentTenantInfo) {
|
||||
setCurrentTenant(currentTenantInfo);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("[Tenant] 获取当前租户失败,尝试自动选择:", error);
|
||||
console.debug("[Tenant] 获取当前租户失败,尝试本地/默认选择:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果只有一个租户,自动选中
|
||||
if (tenantList.value.length === 1) {
|
||||
// 3.2 本地已有 tenantId,但 currentTenant 为空时,从列表补全 tenantInfo(保持展示名称一致)
|
||||
if (currentTenantId.value && !currentTenant.value) {
|
||||
const matched = tenantList.value.find((t) => t.id === currentTenantId.value);
|
||||
if (matched) {
|
||||
setCurrentTenant(matched);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3.3 兜底:默认选中第一个(即使有多个租户,也保证 TenantSwitcher 有默认选中)
|
||||
if (!currentTenantId.value) {
|
||||
setCurrentTenant(tenantList.value[0]);
|
||||
console.debug("[Tenant] 自动选中唯一租户:", tenantList.value[0].name);
|
||||
} else {
|
||||
console.debug("[Tenant] 多个租户可用,等待用户选择");
|
||||
console.debug("[Tenant] 默认选中第一个租户:", tenantList.value[0].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,16 +76,3 @@ $sidebar-width: 210px; // 侧边栏宽度
|
||||
$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度
|
||||
$navbar-height: 50px; // 导航栏高度
|
||||
$tags-view-height: 34px; // TagsView 高度
|
||||
|
||||
/* 供 JS/TS 侧按需读取的变量导出 */
|
||||
/* stylelint-disable property-no-unknown */
|
||||
:export {
|
||||
sidebar-width: $sidebar-width;
|
||||
navbar-height: $navbar-height;
|
||||
tags-view-height: $tags-view-height;
|
||||
menu-background: $menu-background;
|
||||
menu-text: $menu-text;
|
||||
menu-active-text: $menu-active-text;
|
||||
menu-hover: $menu-hover;
|
||||
}
|
||||
/* stylelint-enable property-no-unknown */
|
||||
|
||||
147
src/types/api/ai.ts
Normal file
147
src/types/api/ai.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* AI 模块类型定义
|
||||
*/
|
||||
|
||||
/** AI命令请求参数 */
|
||||
export interface AiCommandRequest {
|
||||
/** 用户输入的自然语言命令 */
|
||||
command: string;
|
||||
/** 当前页面路由(用于上下文) */
|
||||
currentRoute?: string;
|
||||
/** 当前激活的组件名称 */
|
||||
currentComponent?: string;
|
||||
/** 额外上下文信息 */
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
|
||||
/** 函数调用参数 */
|
||||
export interface FunctionCall {
|
||||
/** 函数名称 */
|
||||
name: string;
|
||||
/** 函数描述 */
|
||||
description?: string;
|
||||
/** 参数对象 */
|
||||
arguments: Record<string, any>;
|
||||
}
|
||||
|
||||
/** AI命令解析响应 */
|
||||
export interface AiCommandResponse {
|
||||
/** 解析日志ID(用于关联执行记录) */
|
||||
parseLogId?: string;
|
||||
/** 是否成功解析 */
|
||||
success: boolean;
|
||||
/** 解析后的函数调用列表 */
|
||||
functionCalls: FunctionCall[];
|
||||
/** AI的理解和说明 */
|
||||
explanation?: string;
|
||||
/** 置信度(0-1) */
|
||||
confidence?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 原始LLM响应(用于调试) */
|
||||
rawResponse?: string;
|
||||
}
|
||||
|
||||
/** AI命令执行请求 */
|
||||
export interface AiExecuteRequest {
|
||||
/** 关联的解析日志ID */
|
||||
parseLogId?: string;
|
||||
/** 原始命令(用于审计) */
|
||||
originalCommand?: string;
|
||||
/** 要执行的函数调用 */
|
||||
functionCall: FunctionCall;
|
||||
/** 确认模式:auto=自动执行, manual=需要用户确认 */
|
||||
confirmMode?: "auto" | "manual";
|
||||
/** 用户确认标志 */
|
||||
userConfirmed?: boolean;
|
||||
/** 幂等性令牌(防止重复执行) */
|
||||
idempotencyKey?: string;
|
||||
/** 当前页面路由 */
|
||||
currentRoute?: string;
|
||||
}
|
||||
|
||||
/** AI命令执行响应 */
|
||||
export interface AiExecuteResponse {
|
||||
/** 是否执行成功 */
|
||||
success: boolean;
|
||||
/** 执行结果数据 */
|
||||
data?: any;
|
||||
/** 执行结果说明 */
|
||||
message?: string;
|
||||
/** 影响的记录数 */
|
||||
affectedRows?: number;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 记录ID(用于追踪) */
|
||||
recordId?: string;
|
||||
/** 需要用户确认 */
|
||||
requiresConfirmation?: boolean;
|
||||
/** 确认提示信息 */
|
||||
confirmationPrompt?: string;
|
||||
}
|
||||
|
||||
/** AI命令记录分页查询参数 */
|
||||
export interface AiCommandRecordPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 执行状态 */
|
||||
executeStatus?: number;
|
||||
/** 解析状态 */
|
||||
parseStatus?: number;
|
||||
/** 用户ID */
|
||||
userId?: number;
|
||||
/** AI提供商 */
|
||||
aiProvider?: string;
|
||||
/** AI模型 */
|
||||
aiModel?: string;
|
||||
/** 函数名称 */
|
||||
functionName?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
/** AI命令记录视图对象 */
|
||||
export interface AiCommandRecordVo {
|
||||
/** 记录ID */
|
||||
id: string;
|
||||
/** 用户ID */
|
||||
userId: number;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 原始命令 */
|
||||
originalCommand: string;
|
||||
/** AI提供商 */
|
||||
aiProvider?: string;
|
||||
/** AI模型 */
|
||||
aiModel?: string;
|
||||
/** 解析状态 */
|
||||
parseStatus?: number;
|
||||
/** 函数调用列表 */
|
||||
functionCalls?: string;
|
||||
/** 解析说明 */
|
||||
explanation?: string;
|
||||
/** 置信度 */
|
||||
confidence?: number;
|
||||
/** 解析错误信息 */
|
||||
parseErrorMessage?: string;
|
||||
/** 输入Token数 */
|
||||
inputTokens?: number;
|
||||
/** 输出Token数 */
|
||||
outputTokens?: number;
|
||||
/** 解析耗时(毫秒) */
|
||||
parseDurationMs?: number;
|
||||
/** 函数名称 */
|
||||
functionName?: string;
|
||||
/** 函数参数 */
|
||||
functionArguments?: string;
|
||||
/** 执行状态 */
|
||||
executeStatus?: number;
|
||||
/** 执行错误信息 */
|
||||
executeErrorMessage?: string;
|
||||
/** IP地址 */
|
||||
ipAddress?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
/** 更新时间 */
|
||||
updateTime?: string;
|
||||
}
|
||||
99
src/types/api/codegen.ts
Normal file
99
src/types/api/codegen.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* CodeGen 代码生成类型定义
|
||||
*/
|
||||
|
||||
/** 代码生成预览对象 */
|
||||
export interface GeneratorPreviewVo {
|
||||
/** 文件生成路径 */
|
||||
path: string;
|
||||
/** 文件名称 */
|
||||
fileName: string;
|
||||
/** 文件内容 */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** 数据表分页查询参数 */
|
||||
export interface TablePageQuery extends PageQuery {
|
||||
/** 搜索关键字(表名) */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/** 数据表分页对象 */
|
||||
export interface TablePageVo {
|
||||
/** 表名称 */
|
||||
tableName: string;
|
||||
/** 表描述 */
|
||||
tableComment: string;
|
||||
/** 是否已配置(1:是;0:否) */
|
||||
isConfigured?: number;
|
||||
/** 存储引擎 */
|
||||
engine: string;
|
||||
/** 字符集排序规则 */
|
||||
tableCollation: string;
|
||||
/** 创建时间 */
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/** 代码生成配置表单 */
|
||||
export interface GenConfigForm {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 表名 */
|
||||
tableName?: string;
|
||||
/** 业务名 */
|
||||
businessName?: string;
|
||||
/** 模块名 */
|
||||
moduleName?: string;
|
||||
/** 包名 */
|
||||
packageName?: string;
|
||||
/** 实体名 */
|
||||
entityName?: string;
|
||||
/** 作者 */
|
||||
author?: string;
|
||||
/** 上级菜单 */
|
||||
parentMenuId?: string;
|
||||
/** 后端应用名 */
|
||||
backendAppName?: string;
|
||||
/** 前端应用名 */
|
||||
frontendAppName?: string;
|
||||
/** 字段配置列表 */
|
||||
fieldConfigs?: FieldConfig[];
|
||||
/** 页面类型 classic|curd */
|
||||
pageType?: "classic" | "curd";
|
||||
/** 要移除的表前缀,如 sys_ */
|
||||
removeTablePrefix?: string;
|
||||
}
|
||||
|
||||
/** 字段配置 */
|
||||
export interface FieldConfig {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 列名 */
|
||||
columnName?: string;
|
||||
/** 列类型 */
|
||||
columnType?: string;
|
||||
/** 字段名 */
|
||||
fieldName?: string;
|
||||
/** 字段类型 */
|
||||
fieldType?: string;
|
||||
/** 字段描述 */
|
||||
fieldComment?: string;
|
||||
/** 是否在列表显示 */
|
||||
isShowInList?: number;
|
||||
/** 是否在表单显示 */
|
||||
isShowInForm?: number;
|
||||
/** 是否在查询条件显示 */
|
||||
isShowInQuery?: number;
|
||||
/** 是否必填 */
|
||||
isRequired?: number;
|
||||
/** 表单类型 */
|
||||
formType?: number;
|
||||
/** 查询类型 */
|
||||
queryType?: number;
|
||||
/** 字段长度 */
|
||||
maxLength?: number;
|
||||
/** 字段排序 */
|
||||
fieldSort?: number;
|
||||
/** 字典类型 */
|
||||
dictType?: string;
|
||||
}
|
||||
@@ -2,31 +2,50 @@
|
||||
* 通用 API 类型定义
|
||||
*/
|
||||
|
||||
/** API 响应结构 */
|
||||
export interface ApiResponse<T = any> {
|
||||
/** 响应码 */
|
||||
code: string;
|
||||
/** 响应数据 */
|
||||
data: T;
|
||||
/** 响应消息 */
|
||||
msg: string;
|
||||
}
|
||||
|
||||
/** 分页查询参数 */
|
||||
export interface PageQuery {
|
||||
/** 页码 */
|
||||
pageNum: number;
|
||||
/** 每页记录数 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 分页响应结构 */
|
||||
export interface PageResult<T> {
|
||||
/** 数据列表 */
|
||||
list: T;
|
||||
/** 总记录数 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 下拉选项 */
|
||||
export interface OptionType {
|
||||
/** 选项值 */
|
||||
value: string | number;
|
||||
/** 选项标签 */
|
||||
label: string;
|
||||
/** 子选项 */
|
||||
children?: OptionType[];
|
||||
}
|
||||
|
||||
/** Excel 导入结果 */
|
||||
export interface ExcelResult {
|
||||
/** 响应码 */
|
||||
code: string;
|
||||
/** 无效数据数量 */
|
||||
invalidCount: number;
|
||||
/** 有效数据数量 */
|
||||
validCount: number;
|
||||
/** 错误信息列表 */
|
||||
messageList: string[];
|
||||
}
|
||||
|
||||
35
src/types/api/config.ts
Normal file
35
src/types/api/config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Config 配置类型定义
|
||||
*/
|
||||
|
||||
/** 配置分页查询参数 */
|
||||
export interface ConfigPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/** 配置表单对象 */
|
||||
export interface ConfigForm {
|
||||
/** 配置ID */
|
||||
id?: string;
|
||||
/** 配置名称 */
|
||||
configName?: string;
|
||||
/** 配置键 */
|
||||
configKey?: string;
|
||||
/** 配置值 */
|
||||
configValue?: string;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/** 配置分页对象 */
|
||||
export interface ConfigPageVo {
|
||||
/** 配置ID */
|
||||
id?: string;
|
||||
/** 配置名称 */
|
||||
configName?: string;
|
||||
/** 配置键 */
|
||||
configKey?: string;
|
||||
/** 配置值 */
|
||||
configValue?: string;
|
||||
}
|
||||
49
src/types/api/dept.ts
Normal file
49
src/types/api/dept.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Dept 部门类型定义
|
||||
*/
|
||||
|
||||
/** 部门查询参数 */
|
||||
export interface DeptQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 状态 */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/** 部门视图对象 */
|
||||
export interface DeptVo {
|
||||
/** 子部门 */
|
||||
children?: DeptVo[];
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 部门ID */
|
||||
id?: string;
|
||||
/** 部门名称 */
|
||||
name?: string;
|
||||
/** 父部门ID */
|
||||
parentId?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 父节点ID路径 */
|
||||
treePath?: string;
|
||||
/** 修改时间 */
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
/** 部门表单对象 */
|
||||
export interface DeptForm {
|
||||
/** 部门ID */
|
||||
id?: string;
|
||||
/** 部门名称 */
|
||||
name?: string;
|
||||
/** 部门编号 */
|
||||
code?: string;
|
||||
/** 父部门ID */
|
||||
parentId?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
}
|
||||
90
src/types/api/dict.ts
Normal file
90
src/types/api/dict.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Dict 字典类型定义
|
||||
*/
|
||||
|
||||
/** 字典分页查询参数 */
|
||||
|
||||
export interface DictPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/** 字典分页对象 */
|
||||
export interface DictPageVo {
|
||||
/** 字典ID */
|
||||
id: string;
|
||||
/** 字典名称 */
|
||||
name: string;
|
||||
/** 字典编码 */
|
||||
dictCode: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status: number;
|
||||
}
|
||||
|
||||
/** 字典表单对象 */
|
||||
export interface DictForm {
|
||||
/** 字典ID */
|
||||
id?: string;
|
||||
/** 字典名称 */
|
||||
name?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/** 字典项分页查询参数 */
|
||||
export interface DictItemPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
}
|
||||
|
||||
/** 字典项分页对象 */
|
||||
export interface DictItemPageVo {
|
||||
/** 字典项ID */
|
||||
id: string;
|
||||
/** 字典编码 */
|
||||
dictCode: string;
|
||||
/** 字典项标签 */
|
||||
label: string;
|
||||
/** 字典项值 */
|
||||
value: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status: number;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
}
|
||||
|
||||
/** 字典项表单对象 */
|
||||
export interface DictItemForm {
|
||||
/** 字典项ID */
|
||||
id?: string;
|
||||
/** 字典编码 */
|
||||
dictCode?: string;
|
||||
/** 字典项标签 */
|
||||
label?: string;
|
||||
/** 字典项值 */
|
||||
value?: string;
|
||||
/** 状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 标签类型 */
|
||||
tagType?: "success" | "warning" | "info" | "primary" | "danger" | "";
|
||||
}
|
||||
|
||||
/** 字典项选项 */
|
||||
export interface DictItemOption {
|
||||
/** 字典项值 */
|
||||
value: number | string;
|
||||
/** 字典项标签 */
|
||||
label: string;
|
||||
/** 标签类型 */
|
||||
tagType?: "success" | "warning" | "info" | "primary" | "danger" | "";
|
||||
}
|
||||
11
src/types/api/file.ts
Normal file
11
src/types/api/file.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* File 文件上传类型定义
|
||||
*/
|
||||
|
||||
/** 文件信息 */
|
||||
export interface FileInfo {
|
||||
/** 文件名称 */
|
||||
name: string;
|
||||
/** 文件URL */
|
||||
url: string;
|
||||
}
|
||||
@@ -2,5 +2,22 @@
|
||||
* API 类型统一导出
|
||||
*/
|
||||
|
||||
export * from "./common";
|
||||
export * from "./auth";
|
||||
export * from "./common";
|
||||
|
||||
// System 模块
|
||||
export * from "./user";
|
||||
export * from "./role";
|
||||
export * from "./menu";
|
||||
export * from "./dept";
|
||||
export * from "./dict";
|
||||
export * from "./config";
|
||||
export * from "./log";
|
||||
export * from "./statistics";
|
||||
export * from "./notice";
|
||||
export * from "./tenant";
|
||||
|
||||
// 其他模块
|
||||
export * from "./ai";
|
||||
export * from "./file";
|
||||
export * from "./codegen";
|
||||
|
||||
37
src/types/api/log.ts
Normal file
37
src/types/api/log.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Log 日志类型定义
|
||||
*/
|
||||
|
||||
/** 日志分页查询参数 */
|
||||
export interface LogPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 操作时间 */
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
/** 日志分页对象 */
|
||||
export interface LogPageVo {
|
||||
/** 日志ID */
|
||||
id: string;
|
||||
/** 日志模块 */
|
||||
module: string;
|
||||
/** 日志内容 */
|
||||
content: string;
|
||||
/** 请求路径 */
|
||||
requestUri: string;
|
||||
/** 请求方法 */
|
||||
method: string;
|
||||
/** IP地址 */
|
||||
ip: string;
|
||||
/** 地区 */
|
||||
region: string;
|
||||
/** 浏览器 */
|
||||
browser: string;
|
||||
/** 终端系统 */
|
||||
os: string;
|
||||
/** 执行时间(毫秒) */
|
||||
executionTime: number;
|
||||
/** 操作人 */
|
||||
operator: string;
|
||||
}
|
||||
103
src/types/api/menu.ts
Normal file
103
src/types/api/menu.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Menu 菜单类型定义
|
||||
*/
|
||||
|
||||
/** 菜单查询参数 */
|
||||
export interface MenuQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/** 菜单视图对象 */
|
||||
export interface MenuVo {
|
||||
/** 子菜单 */
|
||||
children?: MenuVo[];
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 菜单ID */
|
||||
id?: string;
|
||||
/** 菜单名称 */
|
||||
name?: string;
|
||||
/** 父菜单ID */
|
||||
parentId?: string;
|
||||
/** 路由路径 */
|
||||
path?: string;
|
||||
/** 按钮权限标识 */
|
||||
perm?: string;
|
||||
/** 跳转路径 */
|
||||
redirect?: string;
|
||||
/** 菜单排序(数字越小排名越靠前) */
|
||||
sort?: number;
|
||||
/** 菜单类型(C-目录 M-菜单 B-按钮) */
|
||||
type?: string;
|
||||
/** 菜单是否可见(1:显示;0:隐藏) */
|
||||
visible?: number;
|
||||
}
|
||||
|
||||
/** 菜单表单对象 */
|
||||
export interface MenuForm {
|
||||
/** 菜单ID */
|
||||
id?: string;
|
||||
/** 父菜单ID */
|
||||
parentId?: string;
|
||||
/** 菜单名称 */
|
||||
name?: string;
|
||||
/** 菜单类型(C-目录 M-菜单 B-按钮) */
|
||||
type?: string;
|
||||
/** 路由路径 */
|
||||
path?: string;
|
||||
/** 跳转路径 */
|
||||
redirect?: string;
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 菜单是否可见 */
|
||||
visible?: number;
|
||||
/** 按钮权限标识 */
|
||||
perm?: string;
|
||||
}
|
||||
|
||||
/** 菜单选项 */
|
||||
export interface MenuOption {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** 路由对象 */
|
||||
export interface RouteVo {
|
||||
/** 子路由列表 */
|
||||
children: RouteVo[];
|
||||
/** 组件路径 */
|
||||
component?: string;
|
||||
/** 路由名称 */
|
||||
name?: string;
|
||||
/** 路由路径 */
|
||||
path?: string;
|
||||
/** 路由属性 */
|
||||
meta?: Meta;
|
||||
/** 跳转链接 */
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
/** 路由属性 */
|
||||
export interface Meta {
|
||||
/** 【目录】只有一个子路由是否始终显示 */
|
||||
alwaysShow?: boolean;
|
||||
/** 是否隐藏(true-是 false-否) */
|
||||
hidden?: boolean;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 【菜单】是否开启页面缓存 */
|
||||
keepAlive?: boolean;
|
||||
/** 路由参数 */
|
||||
params?: Record<string, any>;
|
||||
/** 角色集合 */
|
||||
roles?: string[];
|
||||
/** 路由title */
|
||||
title?: string;
|
||||
}
|
||||
71
src/types/api/notice.ts
Normal file
71
src/types/api/notice.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Notice 通知类型定义
|
||||
*/
|
||||
|
||||
/** 通知分页查询参数 */
|
||||
export interface NoticePageQuery extends PageQuery {
|
||||
/** 通知标题 */
|
||||
title?: string;
|
||||
/** 发布状态(0:草稿;1:已发布;2:已撤回) */
|
||||
publishStatus?: number;
|
||||
/** 是否已读(1:是;0:否) */
|
||||
isRead?: number;
|
||||
}
|
||||
|
||||
/** 通知表单对象 */
|
||||
export interface NoticeForm {
|
||||
/** 通知ID */
|
||||
id?: string;
|
||||
/** 通知标题 */
|
||||
title?: string;
|
||||
/** 通知内容 */
|
||||
content?: string;
|
||||
/** 通知类型 */
|
||||
type?: number;
|
||||
/** 通知等级 */
|
||||
level?: string;
|
||||
/** 发布状态(0:草稿;1:已发布;2:已撤回) */
|
||||
publishStatus?: number;
|
||||
/** 目标用户ID(多个以英文逗号(,)分割) */
|
||||
targetUserIds?: string;
|
||||
}
|
||||
|
||||
/** 通知分页对象 */
|
||||
export interface NoticePageVo {
|
||||
/** 通知ID */
|
||||
id: string;
|
||||
/** 通知标题 */
|
||||
title: string;
|
||||
/** 通知内容 */
|
||||
content: string;
|
||||
/** 通知类型 */
|
||||
type: number;
|
||||
/** 通知等级 */
|
||||
level: string;
|
||||
/** 发布状态 */
|
||||
publishStatus: number;
|
||||
/** 是否已读 */
|
||||
isRead: number;
|
||||
/** 发布时间 */
|
||||
publishTime?: Date;
|
||||
/** 撤回时间 */
|
||||
revokeTime?: Date;
|
||||
}
|
||||
|
||||
/** 通知详情对象 */
|
||||
export interface NoticeDetailVo {
|
||||
/** 通知ID */
|
||||
id?: string;
|
||||
/** 通知标题 */
|
||||
title?: string;
|
||||
/** 通知内容 */
|
||||
content?: string;
|
||||
/** 通知类型 */
|
||||
type?: number;
|
||||
/** 通知等级 */
|
||||
level?: string;
|
||||
/** 发布状态 */
|
||||
publishStatus?: number;
|
||||
/** 目标用户ID */
|
||||
targetUserIds?: string;
|
||||
}
|
||||
45
src/types/api/role.ts
Normal file
45
src/types/api/role.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Role 角色类型定义
|
||||
*/
|
||||
|
||||
/** 角色分页查询参数 */
|
||||
export interface RolePageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/** 角色分页对象 */
|
||||
export interface RolePageVo {
|
||||
/** 角色ID */
|
||||
id?: string;
|
||||
/** 角色编码 */
|
||||
code?: string;
|
||||
/** 角色名称 */
|
||||
name?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 角色状态 */
|
||||
status?: number;
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 修改时间 */
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
/** 角色表单对象 */
|
||||
export interface RoleForm {
|
||||
/** 角色ID */
|
||||
id?: string;
|
||||
/** 角色编码 */
|
||||
code?: string;
|
||||
/** 角色名称 */
|
||||
name?: string;
|
||||
/** 排序 */
|
||||
sort?: number;
|
||||
/** 数据权限 */
|
||||
dataScope?: number;
|
||||
/** 角色状态 */
|
||||
status?: number;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
}
|
||||
39
src/types/api/statistics.ts
Normal file
39
src/types/api/statistics.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Statistics 统计类型定义
|
||||
*/
|
||||
|
||||
/** 访问趋势查询参数 */
|
||||
export interface VisitTrendQuery {
|
||||
/** 开始日期 */
|
||||
startDate: string;
|
||||
/** 结束日期 */
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
/** 访问趋势视图对象 */
|
||||
export interface VisitTrendVo {
|
||||
/** 日期列表 */
|
||||
dates: string[];
|
||||
/** 浏览量(PV)列表 */
|
||||
pvList: number[];
|
||||
/** 访客数(UV)列表 */
|
||||
uvList: number[];
|
||||
/** IP数列表 */
|
||||
ipList: number[];
|
||||
}
|
||||
|
||||
/** 访问量统计视图对象 */
|
||||
export interface VisitStatsVo {
|
||||
/** 今日独立访客数(UV) */
|
||||
todayUvCount: number;
|
||||
/** 累计独立访客数(UV) */
|
||||
totalUvCount: number;
|
||||
/** 独立访客增长率 */
|
||||
uvGrowthRate: number;
|
||||
/** 今日页面浏览量(PV) */
|
||||
todayPvCount: number;
|
||||
/** 累计页面浏览量(PV) */
|
||||
totalPvCount: number;
|
||||
/** 页面浏览量增长率 */
|
||||
pvGrowthRate: number;
|
||||
}
|
||||
79
src/types/api/tenant.ts
Normal file
79
src/types/api/tenant.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Tenant 租户类型定义
|
||||
*/
|
||||
|
||||
import type { PageQuery } from "./common";
|
||||
|
||||
/** 租户信息 */
|
||||
export interface TenantInfo {
|
||||
/** 租户ID */
|
||||
id: number;
|
||||
/** 租户名称 */
|
||||
name: string;
|
||||
/** 租户域名 */
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
/** 租户分页查询参数 */
|
||||
export interface TenantPageQuery extends PageQuery {
|
||||
/** 关键字(租户名称/租户编码/域名) */
|
||||
keywords?: string;
|
||||
/** 租户状态(1-正常 0-禁用) */
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/** 租户分页对象 */
|
||||
export interface TenantPageVo {
|
||||
id?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
contactEmail?: string;
|
||||
domain?: string;
|
||||
logo?: string;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
expireTime?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/** 租户表单对象(编辑) */
|
||||
export interface TenantForm {
|
||||
id?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
contactEmail?: string;
|
||||
domain?: string;
|
||||
logo?: string;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
expireTime?: string;
|
||||
}
|
||||
|
||||
/** 新增租户表单对象 */
|
||||
export interface TenantCreateForm {
|
||||
name?: string;
|
||||
code?: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
contactEmail?: string;
|
||||
domain?: string;
|
||||
logo?: string;
|
||||
remark?: string;
|
||||
expireTime?: string;
|
||||
adminUsername?: string;
|
||||
}
|
||||
|
||||
/** 新增租户结果 */
|
||||
export interface TenantCreateResultVo {
|
||||
tenantId?: string;
|
||||
tenantCode?: string;
|
||||
tenantName?: string;
|
||||
adminUsername?: string;
|
||||
adminInitialPassword?: string;
|
||||
adminRoleCode?: string;
|
||||
}
|
||||
149
src/types/api/user.ts
Normal file
149
src/types/api/user.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* User 用户类型定义
|
||||
*/
|
||||
|
||||
/** 登录用户信息 */
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId?: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
/** 角色集合 */
|
||||
roles: string[];
|
||||
/** 权限集合 */
|
||||
perms: string[];
|
||||
}
|
||||
|
||||
/** 用户分页查询参数 */
|
||||
export interface UserPageQuery extends PageQuery {
|
||||
/** 搜索关键字 */
|
||||
keywords?: string;
|
||||
/** 用户状态 */
|
||||
status?: number;
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: [string, string];
|
||||
}
|
||||
|
||||
/** 用户分页对象 */
|
||||
export interface UserPageVo {
|
||||
/** 用户ID */
|
||||
id: string;
|
||||
/** 用户头像地址 */
|
||||
avatar?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 角色名称,多个使用英文逗号(,)分割 */
|
||||
roleNames?: string;
|
||||
/** 用户状态(1:启用;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 用户表单对象 */
|
||||
export interface UserForm {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
/** 用户头像 */
|
||||
avatar?: string;
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
/** 用户邮箱 */
|
||||
email?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 角色ID集合 */
|
||||
roleIds?: number[];
|
||||
/** 用户状态(1:正常;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息 */
|
||||
export interface UserProfileVo {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 角色名称 */
|
||||
roleNames?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** 个人中心用户信息表单 */
|
||||
export interface UserProfileForm {
|
||||
/** 用户ID */
|
||||
id?: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 用户昵称 */
|
||||
nickname?: string;
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
/** 性别 */
|
||||
gender?: number;
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/** 修改密码表单 */
|
||||
export interface PasswordChangeForm {
|
||||
/** 原密码 */
|
||||
oldPassword?: string;
|
||||
/** 新密码 */
|
||||
newPassword?: string;
|
||||
/** 确认新密码 */
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
/** 修改手机表单 */
|
||||
export interface MobileUpdateForm {
|
||||
/** 手机号 */
|
||||
mobile?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** 修改邮箱表单 */
|
||||
export interface EmailUpdateForm {
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 验证码 */
|
||||
code?: string;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Storage } from "./storage";
|
||||
import { AUTH_KEYS } from "@/config/storage";
|
||||
import { ROLE_ROOT } from "@/enums";
|
||||
import { AUTH_KEYS } from "@/constants";
|
||||
import { ROLE_ROOT } from "@/constants";
|
||||
import { useUserStoreHook } from "@/store/modules/user-store";
|
||||
import router from "@/router";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axio
|
||||
import qs from "qs";
|
||||
import { ApiCodeEnum } from "@/enums/api";
|
||||
import { AuthStorage, redirectToLogin } from "@/utils/auth";
|
||||
import { STORAGE_KEYS } from "@/constants";
|
||||
import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
|
||||
import { authConfig } from "@/settings";
|
||||
|
||||
@@ -28,8 +29,14 @@ httpRequest.interceptors.request.use(
|
||||
// 如果 Authorization 设置为 no-auth,则不携带 Token
|
||||
if (config.headers.Authorization !== "no-auth" && accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
|
||||
const tenantId = localStorage.getItem(STORAGE_KEYS.TENANT_ID);
|
||||
if (tenantId) {
|
||||
config.headers["tenant-id"] = tenantId;
|
||||
}
|
||||
} else {
|
||||
delete config.headers.Authorization;
|
||||
delete config.headers["tenant-id"];
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STORAGE_KEYS, APP_PREFIX } from "@/config/storage";
|
||||
import { STORAGE_KEYS, APP_PREFIX } from "@/constants";
|
||||
|
||||
/**
|
||||
* 存储工具类
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<div class="filter-section">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item prop="keywords" label="关键字">
|
||||
<el-input
|
||||
@@ -83,13 +83,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card shadow="hover" class="data-table">
|
||||
<el-card shadow="hover" class="table-section">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="pageData"
|
||||
highlight-current-row
|
||||
border
|
||||
class="data-table__content"
|
||||
class="table-section__content"
|
||||
>
|
||||
<el-table-column label="创建时间" prop="createTime" width="180" />
|
||||
<el-table-column label="用户名" prop="username" width="120" />
|
||||
@@ -263,7 +263,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import AiCommandApi, { AiCommandRecordVO, AiCommandRecordPageQuery } from "@/api/ai";
|
||||
import AiCommandApi from "@/api/ai";
|
||||
import type { AiCommandRecordVo, AiCommandRecordPageQuery } from "@/types/api";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
const queryFormRef = ref();
|
||||
@@ -282,10 +283,10 @@ const queryParams = reactive<AiCommandRecordPageQuery>({
|
||||
createTime: ["", ""],
|
||||
});
|
||||
|
||||
const pageData = ref<AiCommandRecordVO[]>([]);
|
||||
const pageData = ref<AiCommandRecordVo[]>([]);
|
||||
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRow = ref<AiCommandRecordVO>();
|
||||
const currentRow = ref<AiCommandRecordVo>();
|
||||
|
||||
function getExecuteStatusText(status: number): string {
|
||||
switch (status) {
|
||||
@@ -315,7 +316,7 @@ function getExecuteStatusTagType(status: number): "info" | "success" | "danger"
|
||||
|
||||
function fetchData() {
|
||||
loading.value = true;
|
||||
AiCommandApi.getCommandRecordPage(queryParams)
|
||||
AiCommandApi.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
@@ -336,7 +337,7 @@ function handleResetQuery() {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleViewDetail(row: AiCommandRecordVO) {
|
||||
function handleViewDetail(row: AiCommandRecordVo) {
|
||||
currentRow.value = row;
|
||||
detailDialogVisible.value = true;
|
||||
}
|
||||
@@ -363,7 +364,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -524,12 +524,8 @@ import type { EditorConfiguration } from "codemirror";
|
||||
|
||||
import { FormTypeEnum, QueryTypeEnum } from "@/enums/codegen";
|
||||
|
||||
import GeneratorAPI, {
|
||||
TablePageVO,
|
||||
GenConfigForm,
|
||||
TablePageQuery,
|
||||
FieldConfig,
|
||||
} from "@/api/codegen";
|
||||
import GeneratorAPI from "@/api/codegen";
|
||||
import type { FieldConfig, GenConfigForm, TablePageQuery, TablePageVo } from "@/api/types";
|
||||
import { ElLoading } from "element-plus";
|
||||
|
||||
import DictAPI from "@/api/system/dict";
|
||||
@@ -586,7 +582,7 @@ const queryParams = reactive<TablePageQuery>({
|
||||
const loading = ref(false);
|
||||
const loadingText = ref("loading...");
|
||||
|
||||
const pageData = ref<TablePageVO[]>([]);
|
||||
const pageData = ref<TablePageVo[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
const formTypeOptions: Record<string, OptionType> = FormTypeEnum;
|
||||
@@ -630,7 +626,7 @@ watch(
|
||||
);
|
||||
|
||||
const { copy, copied } = useClipboard();
|
||||
const code = ref();
|
||||
const code = ref<string>("");
|
||||
const cmRef = ref<CmComponentRef>();
|
||||
const cmOptions: EditorConfiguration = {
|
||||
mode: "text/javascript",
|
||||
@@ -640,7 +636,7 @@ const prevBtnText = ref("");
|
||||
const nextBtnText = ref("下一步,字段配置");
|
||||
const active = ref(0);
|
||||
const currentTableName = ref("");
|
||||
const sortFlag = ref<object>();
|
||||
const sortFlag = ref<Sortable | null>(null);
|
||||
|
||||
// ================= 本地写盘(可选) =================
|
||||
const supportsFSAccess = typeof (window as any).showDirectoryPicker === "function";
|
||||
@@ -677,12 +673,24 @@ watch(active, (val) => {
|
||||
} else if (val === 1) {
|
||||
prevBtnText.value = "上一步,基础配置";
|
||||
nextBtnText.value = "下一步,确认生成";
|
||||
nextTick(() => {
|
||||
initSort();
|
||||
});
|
||||
} else if (val === 2) {
|
||||
prevBtnText.value = "上一步,字段配置";
|
||||
nextBtnText.value = "下载代码";
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => dialog.visible,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
destroySort();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage.success("复制成功");
|
||||
@@ -696,7 +704,8 @@ watch(
|
||||
if (
|
||||
fieldConfig.fieldType &&
|
||||
fieldConfig.fieldType.includes("Date") &&
|
||||
fieldConfig.isShowInQuery === 1
|
||||
fieldConfig.isShowInQuery === 1 &&
|
||||
fieldConfig.queryType == null
|
||||
) {
|
||||
fieldConfig.queryType = QueryTypeEnum.BETWEEN.value as number;
|
||||
}
|
||||
@@ -705,12 +714,17 @@ watch(
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
function destroySort() {
|
||||
if (!sortFlag.value) return;
|
||||
sortFlag.value.destroy();
|
||||
sortFlag.value = null;
|
||||
}
|
||||
|
||||
const initSort = () => {
|
||||
if (sortFlag.value) {
|
||||
return;
|
||||
}
|
||||
if (sortFlag.value) return;
|
||||
const table = document.querySelector(".elTableCustom .el-table__body-wrapper tbody");
|
||||
sortFlag.value = Sortable.create(<HTMLElement>table, {
|
||||
if (!table) return;
|
||||
sortFlag.value = Sortable.create(table as HTMLElement, {
|
||||
group: "shared",
|
||||
animation: 150,
|
||||
ghostClass: "sortable-ghost", //拖拽样式
|
||||
@@ -724,20 +738,6 @@ const initSort = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setNodeSort = (oldIndex: number, newIndex: number) => {
|
||||
// 使用arr复制一份表格数组数据
|
||||
const arr = Object.assign([], genConfigFormData.value.fieldConfigs);
|
||||
const currentRow = arr.splice(oldIndex, 1)[0];
|
||||
arr.splice(newIndex, 0, currentRow);
|
||||
arr.forEach((item: FieldConfig, index) => {
|
||||
item.fieldSort = index + 1;
|
||||
});
|
||||
genConfigFormData.value.fieldConfigs = [];
|
||||
nextTick(async () => {
|
||||
genConfigFormData.value.fieldConfigs = arr;
|
||||
});
|
||||
};
|
||||
|
||||
/** 上一步 */
|
||||
function handlePrevClick() {
|
||||
if (active.value === 2) {
|
||||
@@ -755,7 +755,6 @@ function handlePrevClick() {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
initSort();
|
||||
}
|
||||
if (active.value-- <= 0) active.value = 0;
|
||||
}
|
||||
@@ -770,7 +769,6 @@ function handleNextClick() {
|
||||
ElMessage.error("表名、业务名、包名、模块名、实体名不能为空");
|
||||
return;
|
||||
}
|
||||
initSort();
|
||||
}
|
||||
if (active.value === 1) {
|
||||
// 保存生成配置
|
||||
@@ -783,7 +781,7 @@ function handleNextClick() {
|
||||
loadingText.value = "代码生成中,请稍后...";
|
||||
GeneratorAPI.saveGenConfig(tableName, genConfigFormData.value)
|
||||
.then(() => {
|
||||
handlePreview(tableName);
|
||||
return handlePreview(tableName);
|
||||
})
|
||||
.then(() => {
|
||||
if (active.value++ >= 2) active.value = 2;
|
||||
@@ -833,36 +831,34 @@ function handleResetQuery() {
|
||||
async function handleOpenDialog(tableName: string) {
|
||||
dialog.visible = true;
|
||||
active.value = 0;
|
||||
|
||||
menuOptions.value = await MenuAPI.getOptions(true);
|
||||
|
||||
currentTableName.value = tableName;
|
||||
// 获取字典数据
|
||||
DictAPI.getList().then((data) => {
|
||||
dictOptions.value = data;
|
||||
loading.value = true;
|
||||
GeneratorAPI.getGenConfig(tableName)
|
||||
.then((data) => {
|
||||
try {
|
||||
const [menuList, dictList, config] = await Promise.all([
|
||||
MenuAPI.getOptions(true),
|
||||
DictAPI.getList(),
|
||||
GeneratorAPI.getGenConfig(tableName),
|
||||
]);
|
||||
|
||||
menuOptions.value = menuList;
|
||||
dictOptions.value = dictList;
|
||||
dialog.title = `${tableName} 代码生成`;
|
||||
genConfigFormData.value = data;
|
||||
genConfigFormData.value = config;
|
||||
|
||||
checkAllSelected("isShowInQuery", isCheckAllQuery);
|
||||
checkAllSelected("isShowInList", isCheckAllList);
|
||||
checkAllSelected("isShowInForm", isCheckAllForm);
|
||||
|
||||
// 如果已经配置过,直接跳转到预览页面
|
||||
if (genConfigFormData.value.id) {
|
||||
active.value = 2;
|
||||
handlePreview(tableName);
|
||||
} else {
|
||||
// 如果没有配置过,跳转到基础配置页面
|
||||
active.value = 0;
|
||||
await handlePreview(tableName);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
} catch {
|
||||
ElMessage.error("获取生成配置失败");
|
||||
dialog.visible = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置配置 */
|
||||
@@ -896,27 +892,26 @@ const checkAllSelected = (key: keyof FieldConfig, isCheckAllRef: any) => {
|
||||
};
|
||||
|
||||
/** 获取生成预览 */
|
||||
function handlePreview(tableName: string) {
|
||||
async function handlePreview(tableName: string) {
|
||||
treeData.value = [];
|
||||
GeneratorAPI.getPreviewData(tableName, (genConfigFormData.value.pageType as any) || "classic")
|
||||
.then((data) => {
|
||||
try {
|
||||
const data = await GeneratorAPI.getPreviewData(
|
||||
tableName,
|
||||
(genConfigFormData.value.pageType as any) || "classic"
|
||||
);
|
||||
dialog.title = `代码生成 ${tableName}`;
|
||||
// 组装树形结构完善代码
|
||||
const tree = buildTree(data);
|
||||
// 缓存原始数据用于写盘
|
||||
lastPreviewFiles.value = data || [];
|
||||
// 去掉根节点“前后端代码”,直接展示其 children 作为一级目录
|
||||
treeData.value = tree?.children ? [...tree.children] : [];
|
||||
|
||||
// 默认选中第一个叶子节点并设置 code 值
|
||||
const firstLeafNode = findFirstLeafNode(tree);
|
||||
if (firstLeafNode) {
|
||||
code.value = firstLeafNode.content || "";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
} catch {
|
||||
active.value = 0;
|
||||
});
|
||||
throw new Error("preview_failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1065,27 +1060,11 @@ const pickBackendDir = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureDir(root: any, path: string[], force = true) {
|
||||
async function ensureDir(root: any, path: string[], create = true) {
|
||||
let current = root;
|
||||
for (const segment of path) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
current = await current.getDirectoryHandle(segment, { create: true });
|
||||
} catch (err: any) {
|
||||
// 若同名文件阻塞目录创建,尝试强制删除后重建
|
||||
if (force && err?.name === "TypeMismatchError") {
|
||||
try {
|
||||
// @ts-ignore
|
||||
await current.removeEntry(segment, { recursive: true });
|
||||
// @ts-ignore
|
||||
current = await current.getDirectoryHandle(segment, { create: true });
|
||||
} catch {
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
current = await current.getDirectoryHandle(segment, { create });
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -1103,15 +1082,8 @@ async function writeFile(dirHandle: any, filePath: string, content: string) {
|
||||
fileHandle = await targetDir.getFileHandle(fileName, { create: true });
|
||||
} catch (err: any) {
|
||||
if (err?.name === "TypeMismatchError") {
|
||||
// 存在同名目录,尝试删除后重建文件
|
||||
try {
|
||||
// @ts-ignore
|
||||
await targetDir.removeEntry(fileName, { recursive: true });
|
||||
// @ts-ignore
|
||||
fileHandle = await targetDir.getFileHandle(fileName, { create: true });
|
||||
} catch {
|
||||
// 存在同名目录(或其它类型冲突),为安全起见不自动删除
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
@@ -1319,6 +1291,10 @@ async function confirmWrite() {
|
||||
/** 组件挂载后执行 */
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cmRef.value?.destroy();
|
||||
destroySort();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -90,14 +90,17 @@
|
||||
<el-dialog
|
||||
v-model="tenantDialogVisible"
|
||||
title="选择登录租户"
|
||||
width="500px"
|
||||
:width="isSmallScreen ? '92vw' : '500px'"
|
||||
:fullscreen="isSmallScreen"
|
||||
append-to-body
|
||||
:teleported="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<div class="tenant-select-content">
|
||||
<div class="tenant-select-content" :style="tenantDialogBodyStyle">
|
||||
<p class="tenant-select-tip">检测到你的账号属于多个租户,请选择登录租户:</p>
|
||||
<TenantSwitcher @change="(id: number) => (selectedTenantId = id)" />
|
||||
<TenantSwitcher @change="handleTenantSwitcherChange" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="tenantDialogVisible = false">取消</el-button>
|
||||
@@ -154,6 +157,7 @@ const loginFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
// 是否大写锁定
|
||||
const isCapsLock = ref(false);
|
||||
const isSmallScreen = useMediaQuery("(max-width: 768px)");
|
||||
// 验证码图片Base64字符串
|
||||
const captchaBase64 = ref();
|
||||
// 记住我
|
||||
@@ -162,6 +166,26 @@ const rememberMe = AuthStorage.getRememberMe();
|
||||
const tenantDialogVisible = ref(false);
|
||||
const selectedTenantId = ref<number | null>(null);
|
||||
|
||||
function handleTenantSwitcherChange(id: number) {
|
||||
selectedTenantId.value = id;
|
||||
tenantStore.currentTenantId = id;
|
||||
const matched = tenantStore.tenantList?.find((t) => t.id === id) || null;
|
||||
tenantStore.currentTenant = matched;
|
||||
}
|
||||
|
||||
const tenantDialogBodyStyle = computed(() => {
|
||||
if (isSmallScreen.value) {
|
||||
return {
|
||||
maxHeight: "calc(100vh - 160px)",
|
||||
overflow: "auto",
|
||||
};
|
||||
}
|
||||
return {
|
||||
maxHeight: "60vh",
|
||||
overflow: "auto",
|
||||
};
|
||||
});
|
||||
|
||||
const loginFormData = ref<LoginRequest>({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
@@ -232,10 +256,19 @@ async function handleLoginSubmit() {
|
||||
await router.push(decodeURIComponent(redirectPath));
|
||||
} catch (error: any) {
|
||||
// 检查是否是 choose_tenant 响应
|
||||
if (error?.code === ApiCodeEnum.CHOOSE_TENANT && error?.data?.tenants) {
|
||||
if (
|
||||
error?.code === ApiCodeEnum.CHOOSE_TENANT &&
|
||||
Array.isArray(error?.data) &&
|
||||
error.data.length > 0
|
||||
) {
|
||||
// 需要选择租户
|
||||
tenantStore.setTenantList(error.data.tenants);
|
||||
selectedTenantId.value = error.data.tenants[0]?.id || null;
|
||||
tenantStore.setTenantList(error.data);
|
||||
selectedTenantId.value = error.data[0]?.id || null;
|
||||
if (selectedTenantId.value) {
|
||||
tenantStore.currentTenantId = selectedTenantId.value;
|
||||
tenantStore.currentTenant =
|
||||
error.data.find((t: any) => t.id === selectedTenantId.value) || null;
|
||||
}
|
||||
tenantDialogVisible.value = true;
|
||||
return; // 等待用户选择租户
|
||||
}
|
||||
@@ -263,7 +296,10 @@ async function handleTenantSelected() {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 使用选中的租户ID重新登录(将 tenantId 设置到表单数据中)
|
||||
const loginData = { ...loginFormData.value, tenantId: selectedTenantId.value };
|
||||
const loginData = {
|
||||
...loginFormData.value,
|
||||
tenantId: selectedTenantId.value,
|
||||
};
|
||||
await userStore.login(loginData);
|
||||
// 登录成功,关闭对话框并跳转
|
||||
tenantDialogVisible.value = false;
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<span>⦿</span>
|
||||
统一身份认证与权限管理
|
||||
</li>
|
||||
<li>
|
||||
<span>⦿</span>
|
||||
支持多租户模式与租户隔离
|
||||
</li>
|
||||
<li>
|
||||
<span>⦿</span>
|
||||
数据安全与操作审计
|
||||
@@ -49,9 +53,14 @@
|
||||
<div class="auth-panel__title-row">
|
||||
<span class="auth-panel__title">{{ defaultSettings.title }}</span>
|
||||
</div>
|
||||
<div v-if="defaultSettings.version" class="auth-panel__version-row">
|
||||
<span class="auth-panel__version-label">Version</span>
|
||||
<span class="auth-panel__version-pill">v{{ defaultSettings.version }}</span>
|
||||
<div v-if="defaultSettings.version || tenantEnabled" class="auth-panel__version-row">
|
||||
<el-text size="small" type="info">VERSION</el-text>
|
||||
<el-tag v-if="defaultSettings.version" size="small" effect="light" round>
|
||||
{{ `v${defaultSettings.version}` }}
|
||||
</el-tag>
|
||||
<el-tag v-if="tenantEnabled" type="success" size="small" effect="light" round>
|
||||
多租户
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +91,8 @@ type LayoutMap = "login" | "register" | "resetPwd";
|
||||
const { t } = useI18n();
|
||||
const component = ref<LayoutMap>("login");
|
||||
|
||||
const tenantEnabled = import.meta.env.VITE_APP_TENANT_ENABLED === "true";
|
||||
|
||||
const formComponents = {
|
||||
login: defineAsyncComponent(() => import("./components/Login.vue")),
|
||||
register: defineAsyncComponent(() => import("./components/Register.vue")),
|
||||
@@ -425,21 +436,6 @@ onBeforeUnmount(() => {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.auth-panel__version-label {
|
||||
color: var(--el-text-color-placeholder);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.auth-panel__version-pill {
|
||||
padding: 0.1rem 0.55rem;
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
background: linear-gradient(135deg, rgba(22, 93, 255, 0.12), rgba(64, 150, 255, 0.18));
|
||||
border: 1px solid rgba(22, 93, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.auth-panel__form {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -141,7 +141,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import ConfigAPI, { ConfigPageVO, ConfigForm, ConfigPageQuery } from "@/api/system/config";
|
||||
import ConfigAPI from "@/api/system/config";
|
||||
import type { ConfigPageVo, ConfigForm, ConfigPageQuery } from "@/api/types";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
@@ -159,7 +160,7 @@ const queryParams = reactive<ConfigPageQuery>({
|
||||
});
|
||||
|
||||
// 系统配置表格数据
|
||||
const pageData = ref<ConfigPageVO[]>([]);
|
||||
const pageData = ref<ConfigPageVo[]>([]);
|
||||
|
||||
const dialog = reactive({
|
||||
title: "",
|
||||
|
||||
@@ -163,7 +163,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/system/dept";
|
||||
import DeptAPI from "@/api/system/dept";
|
||||
import type { DeptVo, DeptForm, DeptQuery } from "@/api/types";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const deptFormRef = ref();
|
||||
@@ -177,7 +178,7 @@ const dialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const deptList = ref<DeptVO[]>();
|
||||
const deptList = ref<DeptVo[]>();
|
||||
const deptOptions = ref<OptionType[]>();
|
||||
const formData = reactive<DeptForm>({
|
||||
status: 1,
|
||||
|
||||
@@ -155,7 +155,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TagProps } from "element-plus";
|
||||
import DictAPI, { DictItemPageQuery, DictItemPageVO, DictItemForm } from "@/api/system/dict";
|
||||
import DictAPI from "@/api/system/dict";
|
||||
import type { DictItemPageQuery, DictItemPageVo, DictItemForm } from "@/api/types";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -173,7 +174,7 @@ const queryParams = reactive<DictItemPageQuery>({
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const tableData = ref<DictItemPageVO[]>();
|
||||
const tableData = ref<DictItemPageVo[]>();
|
||||
|
||||
const dialog = reactive({
|
||||
title: "",
|
||||
@@ -226,7 +227,7 @@ function handleSelectionChange(selection: any) {
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
function handleOpenDialog(row?: DictItemPageVO) {
|
||||
function handleOpenDialog(row?: DictItemPageVo) {
|
||||
dialog.visible = true;
|
||||
dialog.title = row ? "编辑字典项" : "新增字典项";
|
||||
|
||||
|
||||
@@ -137,7 +137,8 @@ defineOptions({
|
||||
inherititems: false,
|
||||
});
|
||||
|
||||
import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/system/dict";
|
||||
import DictAPI from "@/api/system/dict";
|
||||
import type { DictPageQuery, DictPageVo, DictForm } from "@/api/types";
|
||||
|
||||
import router from "@/router";
|
||||
|
||||
@@ -153,7 +154,7 @@ const queryParams = reactive<DictPageQuery>({
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const tableData = ref<DictPageVO[]>();
|
||||
const tableData = ref<DictPageVo[]>();
|
||||
|
||||
const dialog = reactive({
|
||||
title: "",
|
||||
@@ -285,7 +286,7 @@ function handleDelete(id?: number) {
|
||||
}
|
||||
|
||||
// 打开字典项
|
||||
function handleOpenDictData(row: DictPageVO) {
|
||||
function handleOpenDictData(row: DictPageVo) {
|
||||
router.push({
|
||||
path: "/system/dict-item",
|
||||
query: { dictCode: row.dictCode, title: "【" + row.name + "】字典数据" },
|
||||
|
||||
@@ -68,7 +68,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import LogAPI, { LogPageVO, LogPageQuery } from "@/api/system/log";
|
||||
import LogAPI from "@/api/system/log";
|
||||
import type { LogPageVo, LogPageQuery } from "@/api/types";
|
||||
|
||||
const queryFormRef = ref();
|
||||
|
||||
@@ -83,7 +84,7 @@ const queryParams = reactive<LogPageQuery>({
|
||||
});
|
||||
|
||||
// 日志表格数据
|
||||
const pageData = ref<LogPageVO[]>();
|
||||
const pageData = ref<LogPageVo[]>();
|
||||
|
||||
/** 获取数据 */
|
||||
function fetchData() {
|
||||
|
||||
@@ -47,15 +47,19 @@
|
||||
>
|
||||
<el-table-column label="菜单名称" min-width="200">
|
||||
<template #default="scope">
|
||||
<div class="menu-name-cell">
|
||||
<span class="menu-name-cell__icon">
|
||||
<template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')">
|
||||
<el-icon style="vertical-align: -0.15em">
|
||||
<component :is="scope.row.icon.replace('el-icon-', '')" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else-if="scope.row.icon">
|
||||
<div :class="`i-svg:${scope.row.icon}`" />
|
||||
<span :class="`i-svg:${scope.row.icon}`" />
|
||||
</template>
|
||||
{{ scope.row.name }}
|
||||
</span>
|
||||
<span class="menu-name-cell__text">{{ scope.row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -340,7 +344,8 @@
|
||||
import { useAppStore } from "@/store/modules/app-store";
|
||||
import { DeviceEnum } from "@/enums/settings";
|
||||
|
||||
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu";
|
||||
import MenuAPI from "@/api/system/menu";
|
||||
import type { MenuQuery, MenuForm, MenuVo } from "@/api/types";
|
||||
import { MenuTypeEnum } from "@/enums/business";
|
||||
|
||||
defineOptions({
|
||||
@@ -363,7 +368,7 @@ const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600
|
||||
// 查询参数
|
||||
const queryParams = reactive<MenuQuery>({});
|
||||
// 菜单表格数据
|
||||
const menuTableData = ref<MenuVO[]>([]);
|
||||
const menuTableData = ref<MenuVo[]>([]);
|
||||
// 顶级菜单下拉选项
|
||||
const menuOptions = ref<OptionType[]>([]);
|
||||
// 初始菜单表单数据
|
||||
@@ -432,7 +437,7 @@ function handleResetQuery() {
|
||||
}
|
||||
|
||||
// 行点击事件
|
||||
function handleRowClick(row: MenuVO) {
|
||||
function handleRowClick(row: MenuVo) {
|
||||
selectedMenuId.value = row.id;
|
||||
}
|
||||
|
||||
@@ -562,3 +567,26 @@ onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-name-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.menu-name-cell__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
min-width: 18px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.menu-name-cell__text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -260,12 +260,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import NoticeAPI, {
|
||||
NoticePageVO,
|
||||
NoticeForm,
|
||||
NoticePageQuery,
|
||||
NoticeDetailVO,
|
||||
} from "@/api/system/notice";
|
||||
import NoticeAPI from "@/api/system/notice";
|
||||
import type { NoticePageVo, NoticeForm, NoticePageQuery, NoticeDetailVo } from "@/api/types";
|
||||
import UserAPI from "@/api/system/user";
|
||||
|
||||
const queryFormRef = ref();
|
||||
@@ -282,7 +278,7 @@ const queryParams = reactive<NoticePageQuery>({
|
||||
|
||||
const userOptions = ref<OptionType[]>([]);
|
||||
// 通知公告表格数据
|
||||
const pageData = ref<NoticePageVO[]>([]);
|
||||
const pageData = ref<NoticePageVo[]>([]);
|
||||
|
||||
// 弹窗
|
||||
const dialog = reactive({
|
||||
@@ -319,7 +315,7 @@ const rules = reactive({
|
||||
const detailDialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
const currentNotice = ref<NoticeDetailVO>({});
|
||||
const currentNotice = ref<NoticeDetailVo>({});
|
||||
|
||||
// 查询通知公告
|
||||
function handleQuery() {
|
||||
|
||||
@@ -215,7 +215,8 @@
|
||||
import { useAppStore } from "@/store/modules/app-store";
|
||||
import { DeviceEnum } from "@/enums/settings";
|
||||
|
||||
import RoleAPI, { RolePageVO, RoleForm, RolePageQuery } from "@/api/system/role";
|
||||
import RoleAPI from "@/api/system/role";
|
||||
import type { RolePageVo, RoleForm, RolePageQuery } from "@/api/types";
|
||||
import MenuAPI from "@/api/system/menu";
|
||||
|
||||
defineOptions({
|
||||
@@ -239,7 +240,7 @@ const queryParams = reactive<RolePageQuery>({
|
||||
});
|
||||
|
||||
// 角色表格数据
|
||||
const roleList = ref<RolePageVO[]>();
|
||||
const roleList = ref<RolePageVo[]>();
|
||||
// 菜单权限下拉
|
||||
const menuPermOptions = ref<OptionType[]>([]);
|
||||
|
||||
@@ -389,7 +390,7 @@ function handleDelete(roleId?: number) {
|
||||
}
|
||||
|
||||
// 打开分配菜单权限弹窗
|
||||
async function handleOpenAssignPermDialog(row: RolePageVO) {
|
||||
async function handleOpenAssignPermDialog(row: RolePageVo) {
|
||||
const roleId = row.id;
|
||||
if (roleId) {
|
||||
assignPermDialogVisible.value = true;
|
||||
|
||||
411
src/views/system/tenant/index.vue
Normal file
411
src/views/system/tenant/index.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="filter-section">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item prop="keywords" label="关键字">
|
||||
<el-input
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="租户名称/租户编码/域名"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="hover" class="table-section">
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:tenant:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog()"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['sys:tenant:delete']"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="ids.length === 0"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="pageData"
|
||||
highlight-current-row
|
||||
border
|
||||
class="table-section__content"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="租户名称" prop="name" min-width="160" />
|
||||
<el-table-column label="租户编码" prop="code" width="140" />
|
||||
<el-table-column label="域名" prop="domain" min-width="160" />
|
||||
<el-table-column label="联系人" prop="contactName" width="120" />
|
||||
<el-table-column label="电话" prop="contactPhone" width="140" />
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-if="hasPermChangeStatus"
|
||||
v-model="scope.row.status"
|
||||
inline-prompt
|
||||
active-text="正常"
|
||||
inactive-text="禁用"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="
|
||||
(val) => {
|
||||
pageData.length > 0 && handleChangeStatus(scope.row.id, Number(val));
|
||||
}
|
||||
"
|
||||
/>
|
||||
<el-tag v-else :type="scope.row.status === 1 ? 'success' : 'info'">
|
||||
{{ scope.row.status === 1 ? "正常" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" prop="expireTime" width="180" />
|
||||
<el-table-column label="创建时间" prop="createTime" width="180" />
|
||||
<el-table-column fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['sys:tenant:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog(scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['sys:tenant:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="fetchData"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 租户表单弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="600px"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="租户名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入租户名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="租户编码" prop="code">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入租户编码"
|
||||
:disabled="!!formData.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="域名" prop="domain">
|
||||
<el-input v-model="formData.domain" placeholder="demo.youlai.tech(可选)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系人" prop="contactName">
|
||||
<el-input v-model="formData.contactName" placeholder="可选" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="formData.contactPhone" placeholder="可选" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="联系邮箱" prop="contactEmail">
|
||||
<el-input v-model="formData.contactEmail" placeholder="可选" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="过期时间" prop="expireTime">
|
||||
<el-date-picker
|
||||
v-model="formData.expireTime"
|
||||
type="datetime"
|
||||
placeholder="不填表示永不过期"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!formData.id" label="管理员账号" prop="adminUsername">
|
||||
<el-input v-model="formData.adminUsername" placeholder="为空则系统生成" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.id" label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Tenant",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { hasPerm } from "@/utils/auth";
|
||||
|
||||
import TenantAPI from "@/api/system/tenant";
|
||||
import type { TenantCreateForm, TenantForm, TenantPageQuery, TenantPageVo } from "@/api/types";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
|
||||
const loading = ref(false);
|
||||
const ids = ref<number[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
const queryParams = reactive<TenantPageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
});
|
||||
|
||||
const pageData = ref<TenantPageVo[]>([]);
|
||||
|
||||
const dialog = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const formData = reactive<TenantForm & TenantCreateForm>({
|
||||
id: undefined,
|
||||
name: "",
|
||||
code: "",
|
||||
domain: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
contactEmail: "",
|
||||
remark: "",
|
||||
expireTime: undefined,
|
||||
status: 1,
|
||||
adminUsername: "",
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入租户名称", trigger: "blur" }],
|
||||
code: [{ required: true, message: "请输入租户编码", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const hasPermChangeStatus = computed(() => hasPerm("sys:tenant:change-status"));
|
||||
|
||||
function fetchData() {
|
||||
loading.value = true;
|
||||
TenantAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleResetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.pageNum = 1;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleSelectionChange(selection: any) {
|
||||
ids.value = selection.map((item: any) => Number(item.id));
|
||||
}
|
||||
|
||||
async function handleOpenDialog(tenantId?: string) {
|
||||
dialog.visible = true;
|
||||
if (tenantId) {
|
||||
dialog.title = "修改租户";
|
||||
const data = await TenantAPI.getFormData(tenantId);
|
||||
Object.assign(formData, data);
|
||||
formData.adminUsername = "";
|
||||
} else {
|
||||
dialog.title = "新增租户";
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
name: "",
|
||||
code: "",
|
||||
domain: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
contactEmail: "",
|
||||
remark: "",
|
||||
expireTime: undefined,
|
||||
status: 1,
|
||||
adminUsername: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
dialog.visible = false;
|
||||
dataFormRef.value?.resetFields();
|
||||
dataFormRef.value?.clearValidate();
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
name: "",
|
||||
code: "",
|
||||
domain: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
contactEmail: "",
|
||||
remark: "",
|
||||
expireTime: undefined,
|
||||
status: 1,
|
||||
adminUsername: "",
|
||||
});
|
||||
}
|
||||
|
||||
const handleSubmit = useDebounceFn(async () => {
|
||||
const valid = await dataFormRef.value?.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const tenantId = formData.id;
|
||||
if (tenantId) {
|
||||
const payload: TenantForm = {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
domain: formData.domain,
|
||||
contactName: formData.contactName,
|
||||
contactPhone: formData.contactPhone,
|
||||
contactEmail: formData.contactEmail,
|
||||
remark: formData.remark,
|
||||
expireTime: formData.expireTime,
|
||||
status: formData.status,
|
||||
};
|
||||
await TenantAPI.update(String(tenantId), payload);
|
||||
ElMessage.success("修改成功");
|
||||
} else {
|
||||
const payload: TenantCreateForm = {
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
domain: formData.domain,
|
||||
contactName: formData.contactName,
|
||||
contactPhone: formData.contactPhone,
|
||||
contactEmail: formData.contactEmail,
|
||||
remark: formData.remark,
|
||||
expireTime: formData.expireTime,
|
||||
adminUsername: formData.adminUsername,
|
||||
};
|
||||
const result = await TenantAPI.create(payload);
|
||||
ElMessage.success(`新增成功:管理员账号 ${result?.adminUsername || ""}`);
|
||||
}
|
||||
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch {
|
||||
ElMessage.error(formData.id ? "修改失败" : "新增失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
function handleDelete(tenantId?: string) {
|
||||
const tenantIds = tenantId ? tenantId : ids.value.join(",");
|
||||
if (!tenantIds) {
|
||||
ElMessage.warning("请勾选删除项");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm("确认删除选中的租户吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await TenantAPI.deleteByIds(tenantIds);
|
||||
ElMessage.success("删除成功");
|
||||
handleResetQuery();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
});
|
||||
}
|
||||
|
||||
async function handleChangeStatus(id: string | undefined, status: number) {
|
||||
if (!id) return;
|
||||
try {
|
||||
await TenantAPI.updateStatus(String(id), status);
|
||||
ElMessage.success("状态更新成功");
|
||||
} catch {
|
||||
ElMessage.error("状态更新失败");
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -253,7 +253,7 @@ import { useDebounceFn } from "@vueuse/core";
|
||||
import { ElMessage, ElMessageBox, type FormInstance } from "element-plus";
|
||||
|
||||
// ==================== 3. 类型定义 ====================
|
||||
import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user";
|
||||
import type { UserForm, UserPageQuery, UserPageVo } from "@/api/types";
|
||||
|
||||
// ==================== 3.5 工具函数 ====================
|
||||
import { downloadFile } from "@/utils";
|
||||
@@ -299,7 +299,7 @@ const queryParams = reactive<UserPageQuery>({
|
||||
});
|
||||
|
||||
// 列表数据
|
||||
const userList = ref<UserPageVO[]>([]);
|
||||
const userList = ref<UserPageVo[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -363,7 +363,7 @@ async function fetchUserList(): Promise<void> {
|
||||
}
|
||||
|
||||
// ==================== 表格选择 ====================
|
||||
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
|
||||
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVo>();
|
||||
|
||||
// ==================== 查询操作 ====================
|
||||
|
||||
@@ -390,7 +390,7 @@ function handleResetQuery(): void {
|
||||
* 重置用户密码
|
||||
* @param row 用户数据
|
||||
*/
|
||||
function handleResetPassword(row: UserPageVO): void {
|
||||
function handleResetPassword(row: UserPageVo): void {
|
||||
ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
|
||||
Reference in New Issue
Block a user