签入版本
This commit is contained in:
@@ -87,7 +87,7 @@ export const sharedConfig = defineConfig({
|
|||||||
socialLinks: [
|
socialLinks: [
|
||||||
{
|
{
|
||||||
icon: "github",
|
icon: "github",
|
||||||
link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue",
|
link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_NaiveVue",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export const sharedConfig = defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
editLink: {
|
editLink: {
|
||||||
pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue",
|
pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_NaiveVue",
|
||||||
text: "对本页提出修改建议",
|
text: "对本页提出修改建议",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
var _hmt = _hmt || [];
|
var _hmt = _hmt || [];
|
||||||
(function() {
|
(function() {
|
||||||
var hm = document.createElement("script");
|
var hm = document.createElement("script");
|
||||||
hm.src = "https://hm.baidu.com/hm.js?f071c8d09d195a85c543afe1e419c5d5";
|
hm.src = "https://hm.baidu.com/hm.js?f1512443b2c18b2e1ec1bfc25d04a604";
|
||||||
var s = document.getElementsByTagName("script")[0];
|
var s = document.getElementsByTagName("script")[0];
|
||||||
s.parentNode.insertBefore(hm, s);
|
s.parentNode.insertBefore(hm, s);
|
||||||
})();
|
})();
|
||||||
@@ -29,7 +29,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
editLink: {
|
editLink: {
|
||||||
pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue",
|
pattern: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_NaiveVue",
|
||||||
text: "对本页提出修改建议",
|
text: "对本页提出修改建议",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
|
|
||||||
nav: [
|
nav: [
|
||||||
{ text: "官网", link: "https://www.djangoadmin.cn" },
|
{ text: "官网", link: "https://www.djangoadmin.cn" },
|
||||||
{ text: "授权", link: "https://www.djangoadmin.cn/goods/detail/2" },
|
{ text: "授权", link: "https://www.djangoadmin.cn/goods/detail/4" },
|
||||||
{
|
{
|
||||||
text: "部署文档",
|
text: "部署文档",
|
||||||
items: [
|
items: [
|
||||||
@@ -207,8 +207,8 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
link: "https://cn.vuejs.org/",
|
link: "https://cn.vuejs.org/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "ElementPlus",
|
text: "NaiveUI",
|
||||||
link: "https://element-plus.org/zh-CN/",
|
link: "https://www.naiveui.com/zh-CN",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -565,7 +565,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// 5.4 前端开发(Vue3 + Element Plus)
|
// 5.4 前端开发(Vue3 + NaiveUI)
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
{
|
{
|
||||||
text: "5.4 前端开发",
|
text: "5.4 前端开发",
|
||||||
@@ -811,7 +811,7 @@ export const zhConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
|
|||||||
{ text: "9.2 免责声明", link: "/zh/other/declare.html" },
|
{ text: "9.2 免责声明", link: "/zh/other/declare.html" },
|
||||||
{
|
{
|
||||||
text: "9.3 开源协议",
|
text: "9.3 开源协议",
|
||||||
link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_EleVue/blob/master/LICENSE",
|
link: "https://gitee.com/djangoadmin/DjangoAdmin_Tornado_NaiveVue/blob/master/LICENSE",
|
||||||
},
|
},
|
||||||
{ text: "9.4 广告赞助", link: "/zh/other/sponsor.html" },
|
{ text: "9.4 广告赞助", link: "/zh/other/sponsor.html" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
layout: home
|
layout: home
|
||||||
|
|
||||||
title: DjangoAdmin文档
|
title: DjangoAdmin文档
|
||||||
titleTemplate: 前后端分离Tornado+ElementPlus版
|
titleTemplate: 前后端分离Tornado+NaiveUI版
|
||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: DjangoAdmin
|
name: DjangoAdmin
|
||||||
text: "企业级中台解决方案Tornado+ElementPlus版"
|
text: "企业级中台解决方案Tornado+NaiveUI版"
|
||||||
tagline: "赋能开发者,助力企业发展。\n企业级数据平台底座 · 让数字化触手可及。"
|
tagline: "赋能开发者,助力企业发展。\n企业级数据平台底座 · 让数字化触手可及。"
|
||||||
image:
|
image:
|
||||||
src: /logo.svg
|
src: /logo.svg
|
||||||
@@ -17,10 +17,10 @@ hero:
|
|||||||
link: /zh/introduction
|
link: /zh/introduction
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: 在线演示
|
text: 在线演示
|
||||||
link: http://manage.tornado.elevue.djangoadmin.cn
|
link: http://manage.tornado.naivevue.djangoadmin.cn
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: 官方授权
|
text: 官方授权
|
||||||
link: https://www.djangoadmin.cn/goods/detail/2
|
link: https://www.djangoadmin.cn/goods/detail/4
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: ❤️ 成为赞助商
|
text: ❤️ 成为赞助商
|
||||||
link: /zh/other/sponsor.html
|
link: /zh/other/sponsor.html
|
||||||
@@ -37,7 +37,7 @@ features:
|
|||||||
linkText: 查看更多
|
linkText: 查看更多
|
||||||
rel: noreferrer
|
rel: noreferrer
|
||||||
- title: 🚀 全新技术栈
|
- title: 🚀 全新技术栈
|
||||||
details: Vue3、TypeScript、Vite、Pinia、Vue Router 等技术栈,采用主流前后端分离架构实现。
|
details: Tornado 6、SQLAlchemy 2.0、Pydantic v2、Vue3、TypeScript、Vite、Pinia、Vue Router 等技术栈,采用主流前后端分离架构实现。
|
||||||
link: /zh/volume3/tech-stack
|
link: /zh/volume3/tech-stack
|
||||||
linkText: 查看更多
|
linkText: 查看更多
|
||||||
rel: noreferrer
|
rel: noreferrer
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- **Vue3 升级**:前端框架从 Vue 2 升级至 Vue3.4+,全面拥抱 Composition API
|
- **Vue3 升级**:前端框架从 Vue 2 升级至 Vue3.4+,全面拥抱 Composition API
|
||||||
- **ElementPlus**:UI 组件库适配 Vue3 生态
|
- **NaiveUI**:UI 组件库适配 Vue3 生态
|
||||||
- **Pinia 状态管理**:替代 Vuex,更轻量、更灵活、TypeScript 支持更完善
|
- **Pinia 状态管理**:替代 Vuex,更轻量、更灵活、TypeScript 支持更完善
|
||||||
- **TypeScript 类型安全**:全量类型定义,IDE 智能提示与编译时类型检查
|
- **TypeScript 类型安全**:全量类型定义,IDE 智能提示与编译时类型检查
|
||||||
- **Vite 构建工具**:替代 Webpack,开发服务器毫秒级启动,HMR 即时生效
|
- **Vite 构建工具**:替代 Webpack,开发服务器毫秒级启动,HMR 即时生效
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ cp .env.example .env
|
|||||||
# 数据库配置
|
# 数据库配置
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=djangoadmin.tornado.elevue2
|
DB_DATABASE=djangoadmin.tornado.naivevue
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=your_password
|
DB_PASSWORD=your_password
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|||||||
```bash
|
```bash
|
||||||
# 1. 登录 MySQL,创建数据库
|
# 1. 登录 MySQL,创建数据库
|
||||||
mysql -uroot -p
|
mysql -uroot -p
|
||||||
CREATE DATABASE `djangoadmin.tornado.elevue2` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
CREATE DATABASE `djangoadmin.tornado.naivevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
# 2. 导入数据库脚本(脚本位于 document/mysql/ 目录下)
|
# 2. 导入数据库脚本(脚本位于 document/mysql/ 目录下)
|
||||||
mysql -uroot -p djangoadmin.tornado.elevue2 < document/mysql/djangoadmin.tornado.elevue2.sql
|
mysql -uroot -p djangoadmin.tornado.naivevue < document/mysql/djangoadmin.tornado.naivevue.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
## 软件说明
|
## 软件说明
|
||||||
|
|
||||||
基于 Tornado、SQLAlchemy、Pydantic、Vue3、TypeScript、Vite、ElementPlus、MySQL 等技术栈构建的企业级前后端分离后台管理系统。
|
基于 Tornado、SQLAlchemy、Pydantic、Vue3、TypeScript、Vite、NaiveUI、MySQL 等技术栈构建的企业级前后端分离后台管理系统。
|
||||||
|
|
||||||
后端采用 Python 语言,基于 Tornado 异步框架、SQLAlchemy ORM、Pydantic v2 数据校验等主流技术栈;前端基于 Vue3、TypeScript、Vite 构建,采用 ElementPlus 框架,遵循 Vue 前后端分离主流设计思想。
|
后端采用 Python 语言,基于 Tornado 异步框架、SQLAlchemy ORM、Pydantic v2 数据校验等主流技术栈;前端基于 Vue3、TypeScript、Vite 构建,采用 NaiveUI 框架,遵循 Vue 前后端分离主流设计思想。
|
||||||
|
|
||||||
系统采用完全前后端分离架构,前端通过 API 调用后端服务实现数据交互。内置完善的 RBAC 权限体系,权限颗粒度精细至按钮级别;支持多主题切换,兼容桌面端和平板端。系统预置了用户、角色、菜单、部门、岗位、职级、日志、字典、配置、参数、定时任务、代码生成器等基础功能模块,开箱即用。
|
系统采用完全前后端分离架构,前端通过 API 调用后端服务实现数据交互。内置完善的 RBAC 权限体系,权限颗粒度精细至按钮级别;支持多主题切换,兼容桌面端和平板端。系统预置了用户、角色、菜单、部门、岗位、职级、日志、字典、配置、参数、定时任务、代码生成器等基础功能模块,开箱即用。
|
||||||
|
|
||||||
@@ -55,12 +55,12 @@
|
|||||||
|
|
||||||
## 软件出处
|
## 软件出处
|
||||||
|
|
||||||
+ 软件名称:小蚂蚁云前后端分离Tornado+ElementPlus版本
|
+ 软件名称:小蚂蚁云前后端分离Tornado+NaiveUI版本
|
||||||
+ 软件作者:@小蚂蚁云团队
|
+ 软件作者:@小蚂蚁云团队
|
||||||
+ 软件协议:Apache-2.0
|
+ 软件协议:Apache-2.0
|
||||||
+ 官网网址:[https://www.djangoadmin.cn](https://www.djangoadmin.cn)
|
+ 官网网址:[https://www.djangoadmin.cn](https://www.djangoadmin.cn)
|
||||||
+ 文档网址:[http://docs.tornado.elevue2.djangoadmin.cn](http://docs.tornado.elevue2.djangoadmin.cn)
|
+ 文档网址:[http://docs.tornado.naivevue.djangoadmin.cn](http://docs.tornado.naivevue.djangoadmin.cn)
|
||||||
+ 演示地址:[http://manage.tornado.elevue2.djangoadmin.cn](http://manage.tornado.elevue2.djangoadmin.cn)
|
+ 演示地址:[http://manage.tornado.naivevue.djangoadmin.cn](http://manage.tornado.naivevue.djangoadmin.cn)
|
||||||
|
|
||||||
## 软件版本
|
## 软件版本
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 进入项目根目录(以实际解压路径为准)
|
# 进入项目根目录(以实际解压路径为准)
|
||||||
cd DjangoAdmin_Tornado_EleVue
|
cd DjangoAdmin_Tornado_NaiveVue
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
@@ -107,7 +107,7 @@ copy .env.example .env # Windows CMD
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# 基础配置
|
# 基础配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
TORNADO_NAME=Tornado+EleVue旗舰版
|
TORNADO_NAME=Tornado+NaiveVue旗舰版
|
||||||
TORNADO_VERSION=v3.0.0
|
TORNADO_VERSION=v3.0.0
|
||||||
TORNADO_HOST=127.0.0.1
|
TORNADO_HOST=127.0.0.1
|
||||||
TORNADO_PORT=8041
|
TORNADO_PORT=8041
|
||||||
@@ -120,7 +120,7 @@ TORNADO_DEBUG=True
|
|||||||
DB_DRIVER=mysql
|
DB_DRIVER=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=djangoadmin.tornado.elevue
|
DB_DATABASE=djangoadmin.tornado.naivevue
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=your_mysql_password # 修改为真实密码
|
DB_PASSWORD=your_mysql_password # 修改为真实密码
|
||||||
DB_PREFIX=tornado_
|
DB_PREFIX=tornado_
|
||||||
@@ -161,10 +161,10 @@ python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|||||||
```bash
|
```bash
|
||||||
# 1. 登录 MySQL,创建数据库
|
# 1. 登录 MySQL,创建数据库
|
||||||
mysql -uroot -p
|
mysql -uroot -p
|
||||||
CREATE DATABASE `djangoadmin.tornado.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
CREATE DATABASE `djangoadmin.tornado.naivevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
# 2. 导入数据库脚本
|
# 2. 导入数据库脚本
|
||||||
mysql -uroot -p djangoadmin.tornado.elevue < document/djangoadmin.tornado.elevue.sql
|
mysql -uroot -p djangoadmin.tornado.naivevue < document/djangoadmin.tornado.naivevue.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方式二:Alembic 迁移(备选)
|
### 方式二:Alembic 迁移(备选)
|
||||||
@@ -220,7 +220,7 @@ python app.py --port=8041
|
|||||||
| |_| || |_| || (_| || | | (__ | || |_) || (_| || | | (_| |
|
| |_| || |_| || (_| || | | (__ | || |_) || (_| || | | (_| |
|
||||||
|____/ \__,_| \__,_||_| \___||_|| .__/ \__,_||_| \__,_|
|
|____/ \__,_| \__,_||_| \___||_|| .__/ \__,_||_| \__,_|
|
||||||
|_|
|
|_|
|
||||||
Tornado + EleVue 企业级管理框架
|
Tornado + NaiveVue 企业级管理框架
|
||||||
|
|
||||||
[INFO] 服务启动成功 → http://127.0.0.1:8041
|
[INFO] 服务启动成功 → http://127.0.0.1:8041
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
**MySQL**:
|
**MySQL**:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE DATABASE `djangoadmin.tornado.elevue`
|
CREATE DATABASE `djangoadmin.tornado.naivevue`
|
||||||
DEFAULT CHARACTER SET utf8mb4
|
DEFAULT CHARACTER SET utf8mb4
|
||||||
COLLATE utf8mb4_general_ci;
|
COLLATE utf8mb4_general_ci;
|
||||||
```
|
```
|
||||||
@@ -30,7 +30,7 @@ CREATE DATABASE `djangoadmin.tornado.elevue`
|
|||||||
**PostgreSQL**:
|
**PostgreSQL**:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE DATABASE "djangoadmin.tornado.elevue"
|
CREATE DATABASE "djangoadmin.tornado.naivevue"
|
||||||
WITH ENCODING 'UTF8'
|
WITH ENCODING 'UTF8'
|
||||||
LC_COLLATE = 'zh_CN.UTF-8'
|
LC_COLLATE = 'zh_CN.UTF-8'
|
||||||
LC_CTYPE = 'zh_CN.UTF-8';
|
LC_CTYPE = 'zh_CN.UTF-8';
|
||||||
@@ -39,7 +39,7 @@ CREATE DATABASE "djangoadmin.tornado.elevue"
|
|||||||
**SQL Server**:
|
**SQL Server**:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE DATABASE [djangoadmin.tornado.elevue];
|
CREATE DATABASE [djangoadmin.tornado.naivevue];
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第二步:导入数据库脚本
|
### 第二步:导入数据库脚本
|
||||||
@@ -51,11 +51,11 @@ MySQL 数据库脚本位于 `document/` 目录下:
|
|||||||
mysql -uroot -p
|
mysql -uroot -p
|
||||||
|
|
||||||
# 导入脚本
|
# 导入脚本
|
||||||
mysql -uroot -p djangoadmin.tornado.elevue < document/djangoadmin.tornado.elevue.sql
|
mysql -uroot -p djangoadmin.tornado.naivevue < document/djangoadmin.tornado.naivevue.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
`document/djangoadmin.tornado.elevue.sql` 是 MySQL 专用脚本。对于 PostgreSQL / SQL Server / SQLite / Oracle,建议使用 Alembic 方式初始化。
|
`document/djangoadmin.tornado.naivevue.sql` 是 MySQL 专用脚本。对于 PostgreSQL / SQL Server / SQLite / Oracle,建议使用 Alembic 方式初始化。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 第三步:导入字典种子数据
|
### 第三步:导入字典种子数据
|
||||||
@@ -132,7 +132,7 @@ DB_DATABASE=/path/to/djangoadmin.db
|
|||||||
DB_DRIVER=postgresql
|
DB_DRIVER=postgresql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_DATABASE=djangoadmin.tornado.elevue
|
DB_DATABASE=djangoadmin.tornado.naivevue
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=your_password
|
DB_PASSWORD=your_password
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -166,9 +166,9 @@ services:
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# 应用服务:构建并运行本项目
|
# 应用服务:构建并运行本项目
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
tornado_elevue:
|
tornado_naivevue:
|
||||||
image: tornado_elevue:latest # 镜像名称
|
image: tornado_naivevue:latest # 镜像名称
|
||||||
container_name: tornado_elevue # 容器名称
|
container_name: tornado_naivevue # 容器名称
|
||||||
build:
|
build:
|
||||||
context: ./ # 构建上下文为项目根目录
|
context: ./ # 构建上下文为项目根目录
|
||||||
dockerfile: Dockerfile # 使用项目根目录的 Dockerfile
|
dockerfile: Dockerfile # 使用项目根目录的 Dockerfile
|
||||||
@@ -270,7 +270,7 @@ volumes:
|
|||||||
DB_DRIVER=mysql
|
DB_DRIVER=mysql
|
||||||
DB_HOST=mysql # Docker 服务名称,不是 127.0.0.1
|
DB_HOST=mysql # Docker 服务名称,不是 127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=djangoadmin.tornado.elevue
|
DB_DATABASE=djangoadmin.tornado.naivevue
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=your_mysql_password # 与 MYSQL_ROOT_PASSWORD 一致
|
DB_PASSWORD=your_mysql_password # 与 MYSQL_ROOT_PASSWORD 一致
|
||||||
DB_PREFIX=tornado_
|
DB_PREFIX=tornado_
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
| 变量名 | 默认值 | 说明 |
|
| 变量名 | 默认值 | 说明 |
|
||||||
|:----|:----|:----|
|
|:----|:----|:----|
|
||||||
| TORNADO_NAME | Tornado+EleVue旗舰版 | 应用名称,用于界面显示和系统标识 |
|
| TORNADO_NAME | Tornado+NaiveVue旗舰版 | 应用名称,用于界面显示和系统标识 |
|
||||||
| TORNADO_VERSION | v3.0.0 | 应用版本号 |
|
| TORNADO_VERSION | v3.0.0 | 应用版本号 |
|
||||||
| TORNADO_HOST | 127.0.0.1 | 应用监听地址。`127.0.0.1` 仅本地访问;容器/生产建议 `0.0.0.0` |
|
| TORNADO_HOST | 127.0.0.1 | 应用监听地址。`127.0.0.1` 仅本地访问;容器/生产建议 `0.0.0.0` |
|
||||||
| TORNADO_PORT | 8041 | 应用监听端口 |
|
| TORNADO_PORT | 8041 | 应用监听端口 |
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
| DB_DRIVER | mysql | 数据库驱动。可选:`mysql` / `postgresql` / `mssql` / `oracle` / `sqlite` |
|
| DB_DRIVER | mysql | 数据库驱动。可选:`mysql` / `postgresql` / `mssql` / `oracle` / `sqlite` |
|
||||||
| DB_HOST | 127.0.0.1 | 数据库地址(`sqlite` 时忽略) |
|
| DB_HOST | 127.0.0.1 | 数据库地址(`sqlite` 时忽略) |
|
||||||
| DB_PORT | 3306 | 数据库端口(`sqlite` 时忽略) |
|
| DB_PORT | 3306 | 数据库端口(`sqlite` 时忽略) |
|
||||||
| DB_DATABASE | djangoadmin.tornado.elevue | 数据库名称。`sqlite` 时作为数据库文件完整路径 |
|
| DB_DATABASE | djangoadmin.tornado.naivevue | 数据库名称。`sqlite` 时作为数据库文件完整路径 |
|
||||||
| DB_USERNAME | root | 数据库账号(`sqlite` 时忽略) |
|
| DB_USERNAME | root | 数据库账号(`sqlite` 时忽略) |
|
||||||
| DB_PASSWORD | (空) | 数据库密码(`sqlite` 除外) |
|
| DB_PASSWORD | (空) | 数据库密码(`sqlite` 除外) |
|
||||||
| DB_PREFIX | tornado_ | 数据表前缀,所有业务表名自动添加此前缀 |
|
| DB_PREFIX | tornado_ | 数据表前缀,所有业务表名自动添加此前缀 |
|
||||||
@@ -131,7 +131,7 @@ python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# 基础配置
|
# 基础配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
TORNADO_NAME=Tornado+EleVue旗舰版
|
TORNADO_NAME=Tornado+NaiveVue旗舰版
|
||||||
TORNADO_VERSION=v3.0.0
|
TORNADO_VERSION=v3.0.0
|
||||||
TORNADO_HOST=127.0.0.1
|
TORNADO_HOST=127.0.0.1
|
||||||
TORNADO_PORT=8041
|
TORNADO_PORT=8041
|
||||||
@@ -145,7 +145,7 @@ TORNADO_DEMO=False
|
|||||||
DB_DRIVER=mysql
|
DB_DRIVER=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=djangoadmin.tornado.elevue
|
DB_DATABASE=djangoadmin.tornado.naivevue
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=your_password
|
DB_PASSWORD=your_password
|
||||||
DB_PREFIX=tornado_
|
DB_PREFIX=tornado_
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 环境准备
|
# 环境准备
|
||||||
|
|
||||||
本章节介绍在本地搭建项目所需的基础环境和软件安装。项目采用 `Python` + `Tornado` 后端、`Vue3` + `ElementPlus` 前端的分离架构,本地开发需准备以下软件。
|
本章节介绍在本地搭建项目所需的基础环境和软件安装。项目采用 `Python` + `Tornado` 后端、`Vue3` + `NaiveUI` 前端的分离架构,本地开发需准备以下软件。
|
||||||
|
|
||||||
安装软件:`Python(3.12+)`、`MySQL(8.0+)`、`Redis(3.2+)`、`Node.js(22+)`、`pnpm` 等。
|
安装软件:`Python(3.12+)`、`MySQL(8.0+)`、`Redis(3.2+)`、`Node.js(22+)`、`pnpm` 等。
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ mysql>
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 创建数据库(UTF-8 编码)
|
-- 创建数据库(UTF-8 编码)
|
||||||
CREATE DATABASE `djangoadmin.tornado.elevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
CREATE DATABASE `djangoadmin.tornado.naivevue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
-- 查看数据库
|
-- 查看数据库
|
||||||
SHOW DATABASES;
|
SHOW DATABASES;
|
||||||
@@ -297,7 +297,7 @@ OK
|
|||||||
|
|
||||||
## 安装 Node.js
|
## 安装 Node.js
|
||||||
|
|
||||||
`Node.js` 是前端项目的运行环境,要求 `22` 或更高版本。前端基于 `Vue3` + `ElementPlus` 构建。
|
`Node.js` 是前端项目的运行环境,要求 `22` 或更高版本。前端基于 `Vue3` + `NaiveUI` 构建。
|
||||||
|
|
||||||
+ 下载 Node.js
|
+ 下载 Node.js
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ mysql -h127.0.0.1 -P3306 -uroot -p
|
|||||||
|
|
||||||
# 如果是权限问题,授权用户
|
# 如果是权限问题,授权用户
|
||||||
mysql -uroot -p
|
mysql -uroot -p
|
||||||
GRANT ALL PRIVILEGES ON `djangoadmin.tornado.elevue2`.* TO 'root'@'%' IDENTIFIED BY 'your_password';
|
GRANT ALL PRIVILEGES ON `djangoadmin.tornado.naivevue`.* TO 'root'@'%' IDENTIFIED BY 'your_password';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@ ls static/fonts/Vera.ttf
|
|||||||
# .env 中的上传配置
|
# .env 中的上传配置
|
||||||
UPLOAD_ALLOWED_EXTS=.jpg,.jpeg,.png,.gif,.bmp,.ico,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar,.7z
|
UPLOAD_ALLOWED_EXTS=.jpg,.jpeg,.png,.gif,.bmp,.ico,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar,.7z
|
||||||
UPLOAD_MAX_SIZE_MB=10
|
UPLOAD_MAX_SIZE_MB=10
|
||||||
TORNADO_FILE_URL=http://file.tornado.elevue
|
TORNADO_FILE_URL=http://file.tornado.naivevue
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 前端启动
|
# 前端启动
|
||||||
|
|
||||||
本章节介绍如何将前端项目安装依赖并启动开发服务器。前端基于 `Vue3` + `ElementPlus` + `TypeScript`+`Vite` 构建,采用组件化开发模式,支持热更新和代理配置。
|
本章节介绍如何将前端项目安装依赖并启动开发服务器。前端基于 `Vue3` + `NaiveUI` + `TypeScript`+`Vite` 构建,采用组件化开发模式,支持热更新和代理配置。
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
启动前端前,请确保已完成 [环境准备](/zh/volume1/environment) 章节中的 `Node.js` 和 `pnpm` 安装,并已启动后端服务(参见 [后端启动](/zh/volume1/backend))。
|
启动前端前,请确保已完成 [环境准备](/zh/volume1/environment) 章节中的 `Node.js` 和 `pnpm` 安装,并已启动后端服务(参见 [后端启动](/zh/volume1/backend))。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 前端页面视图
|
# 前端页面视图
|
||||||
|
|
||||||
前端页面视图是用户直接交互的界面。项目使用 Vue3 + ElementPlus + Vite 构建。一个完整的 CRUD 页面由 4 个文件组成:
|
前端页面视图是用户直接交互的界面。项目使用 Vue3 + NaiveUI + Vite 构建。一个完整的 CRUD 页面由 4 个文件组成:
|
||||||
|
|
||||||
```
|
```
|
||||||
ui/src/views/system/position/
|
ui/src/views/system/position/
|
||||||
@@ -18,69 +18,83 @@ ui/src/views/system/position/
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<PageWrapper>
|
<div>
|
||||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
|
||||||
</el-card>
|
<template #statusSlot="{ model, field }">
|
||||||
<el-card :bordered="false" class="proCard">
|
<n-input v-model:value="model[field]" />
|
||||||
|
</template>
|
||||||
|
</BasicForm>
|
||||||
|
</n-card>
|
||||||
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicTable
|
<BasicTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:request="loadDataTable"
|
:request="loadDataTable"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
ref="tableRef"
|
ref="basicTableRef"
|
||||||
:actionColumn="actionColumn"
|
:actionColumn="actionColumn"
|
||||||
@selection-change="onSelectionChange"
|
@update:checked-row-keys="onCheckedRow"
|
||||||
|
:autoScrollX="true"
|
||||||
>
|
>
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
<n-space>
|
||||||
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<el-icon class="el-input__icon">
|
<n-icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</el-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
添加岗位
|
新建
|
||||||
</el-button>
|
</n-button>
|
||||||
<el-button
|
<n-button
|
||||||
type="danger"
|
type="error"
|
||||||
@click="handleDelete()"
|
@click="handleDelete"
|
||||||
:disabled="!selectionData.length"
|
:disabled="!rowKeys.length"
|
||||||
v-perm="['sys:position:batchDelete']"
|
v-perm="['sys:position:batchDelete']"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<el-icon class="el-input__icon">
|
<n-icon>
|
||||||
<Delete />
|
<DeleteOutlined />
|
||||||
</el-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</el-card>
|
</n-card>
|
||||||
|
|
||||||
<editDialog
|
<editDialog
|
||||||
v-if="editVisible"
|
ref="createModalRef"
|
||||||
:positionId="positionId"
|
:positionId="positionId"
|
||||||
|
v-if="editVisible"
|
||||||
v-model:visible="editVisible"
|
v-model:visible="editVisible"
|
||||||
@success="reloadTable('noRefresh')"
|
@success="reloadTable('noRefresh')"
|
||||||
/>
|
/>
|
||||||
</PageWrapper>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
import { h, nextTick, reactive, ref, unref } from 'vue';
|
||||||
import { ColProps } from 'element-plus';
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
import { schemas } from './querySchemas';
|
|
||||||
import { useForm } from '@/components/Form/index';
|
|
||||||
import { TableAction } from '@/components/Table';
|
import { TableAction } from '@/components/Table';
|
||||||
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { PlusOutlined } from '@vicons/antd';
|
import { PlusOutlined, DeleteOutlined, FormOutlined } from '@vicons/antd';
|
||||||
import { message, confirm } from '@/utils/auth';
|
import editDialog from './edit.vue';
|
||||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
import { schemas } from './querySchemas';
|
||||||
const positionId = ref(0);
|
import { renderIcon } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义参数
|
||||||
|
*/
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const basicTableRef = ref();
|
||||||
|
const createModalRef = ref();
|
||||||
const editVisible = ref(false);
|
const editVisible = ref(false);
|
||||||
const selectionData = ref([]);
|
const positionId = ref(0);
|
||||||
const tableRef = ref();
|
const rowKeys = ref([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义查询参数
|
* 定义查询参数
|
||||||
@@ -95,24 +109,25 @@ ui/src/views/system/position/
|
|||||||
*/
|
*/
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 200,
|
width: 200,
|
||||||
label: '操作',
|
title: '操作',
|
||||||
prop: 'action',
|
align: 'center',
|
||||||
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:position:update'],
|
auth: ['sys:position:update'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:position:delete'],
|
auth: ['sys:position:delete'],
|
||||||
},
|
},
|
||||||
@@ -123,35 +138,38 @@ ui/src/views/system/position/
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载数据列表
|
* 加载数据列表
|
||||||
|
* @param res 参数
|
||||||
*/
|
*/
|
||||||
const loadDataTable = async (res: any) => {
|
const loadDataTable = async (res) => {
|
||||||
|
rowKeys.value = [];
|
||||||
const result = await getPositionList({ ...formParams, ...res });
|
const result = await getPositionList({ ...formParams, ...res });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新数据列表
|
* 数据行选择事件
|
||||||
|
* @param keys 参数
|
||||||
*/
|
*/
|
||||||
function reloadTable(noRefresh = '') {
|
function onCheckedRow(keys) {
|
||||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
rowKeys.value = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册搜索表单
|
* 加载数据列表
|
||||||
|
* @param noRefresh 参数
|
||||||
*/
|
*/
|
||||||
const [register, {}] = useForm({
|
function reloadTable(noRefresh = '') {
|
||||||
labelWidth: 80,
|
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||||
layout: 'horizontal',
|
}
|
||||||
colProps: { span: 6 } as ColProps,
|
|
||||||
submitOnReset: true,
|
|
||||||
schemas,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行提交表单
|
* 执行提交表单
|
||||||
|
* @param values 参数
|
||||||
*/
|
*/
|
||||||
function handleSubmit(values: Recordable) {
|
function handleSubmit(values: Recordable) {
|
||||||
handleReset();
|
for (const key in formParams) {
|
||||||
|
formParams[key] = '';
|
||||||
|
}
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
formParams[key] = values[key];
|
formParams[key] = values[key];
|
||||||
}
|
}
|
||||||
@@ -160,67 +178,82 @@ ui/src/views/system/position/
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行重置
|
* 执行重置
|
||||||
|
* @param values 参数
|
||||||
*/
|
*/
|
||||||
function handleReset() {
|
function handleReset(values: Recordable) {
|
||||||
for (const key in formParams) {
|
for (const key in formParams) {
|
||||||
formParams[key] = '';
|
formParams[key] = '';
|
||||||
}
|
}
|
||||||
|
for (const key in values) {
|
||||||
|
formParams[key] = values[key];
|
||||||
}
|
}
|
||||||
|
reloadTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行注册
|
||||||
|
*/
|
||||||
|
const [register, {}] = useForm({
|
||||||
|
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||||
|
labelWidth: 80,
|
||||||
|
schemas,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行添加
|
* 执行添加
|
||||||
*/
|
*/
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
positionId.value = 0;
|
positionId.value = 0;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行编辑
|
* 执行编辑
|
||||||
*/
|
*/
|
||||||
const handleEdit = async (record: Recordable) => {
|
async function handleEdit(record: Recordable) {
|
||||||
positionId.value = record.row.id;
|
positionId.value = record.id;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
};
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行删除(单条 + 批量)
|
* 执行删除
|
||||||
|
* @param id 参数
|
||||||
*/
|
*/
|
||||||
async function handleDelete(record: Recordable) {
|
async function handleDelete(record) {
|
||||||
let ids = [];
|
dialog.warning({
|
||||||
if (!record) {
|
title: '提示',
|
||||||
ids = selectionData.value.map(({ id }) => id);
|
content: '确定要删除?',
|
||||||
}
|
positiveText: '确定',
|
||||||
await confirm('确定要删除?');
|
negativeText: '取消',
|
||||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
onPositiveClick: async () => {
|
||||||
message('删除成功');
|
record.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
|
||||||
|
message.success('删除成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
|
||||||
* 选项发生变化
|
|
||||||
*/
|
|
||||||
function onSelectionChange(value) {
|
|
||||||
selectionData.value = value;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 代码解析
|
### 代码解析
|
||||||
|
|
||||||
**页面结构**:`PageWrapper` 包裹两个 `el-card`,上方是搜索表单,下方是数据表格。
|
**页面结构**:`div` 包裹两个 `n-card`,上方是搜索表单,下方是数据表格。
|
||||||
|
|
||||||
**搜索表单**:通过 `useForm` 注册,Schema 从 `querySchemas.ts` 导入。`submitOnReset: true` 表示重置时自动触发查询。
|
**搜索表单**:通过 `useForm` 注册,Schema 从 `querySchemas.ts` 导入。`gridProps` 控制响应式列数。
|
||||||
|
|
||||||
**数据表格**:通过 `BasicTable` 组件渲染,`request` 属性绑定数据加载函数,`columns` 从 `columns.ts` 导入。
|
**数据表格**:通过 `BasicTable` 组件渲染,`request` 属性绑定数据加载函数,`columns` 从 `columns.ts` 导入。
|
||||||
|
|
||||||
**操作栏**:通过 `actionColumn` 的 `render` 函数使用 `h()` 渲染 `TableAction`,支持权限控制(`auth`)。
|
**操作栏**:通过 `actionColumn` 的 `render` 函数使用 `h()` 渲染 `TableAction`,支持权限控制(`auth`)。图标通过 `renderIcon` 工具函数包装。
|
||||||
|
|
||||||
**编辑弹窗**:使用 `defineAsyncComponent` 懒加载 `edit.vue`,通过 `v-model:visible` 控制显示,`positionId` 传入编辑记录 ID(0 表示新增)。
|
**编辑弹窗**:使用 `editDialog` 组件,通过 `v-model:visible` 控制显示,`positionId` 传入编辑记录 ID(0 表示新增)。通过 `ref` 调用 `openModal()` 打开弹窗。
|
||||||
|
|
||||||
**删除逻辑**:单条删除传入 `record`,批量删除从 `selectionData` 取已选 ID。共用同一个 `handleDelete` 方法。
|
**删除逻辑**:使用 NaiveUI 的 `useDialog` 弹出确认对话框,单条删除传入 `record`,批量删除从 `rowKeys` 取已选 ID。
|
||||||
|
|
||||||
|
**消息提示**:使用 NaiveUI 的 `useMessage` 组件,调用 `message.success()` 显示成功提示。
|
||||||
|
|
||||||
## 2. 编辑弹窗 `edit.vue`
|
## 2. 编辑弹窗 `edit.vue`
|
||||||
|
|
||||||
@@ -230,53 +263,57 @@ ui/src/views/system/position/
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<basicModal
|
||||||
v-model="props.visible"
|
@register="modalRegister"
|
||||||
:title="props.positionId ? '编辑' : '新增'"
|
ref="modalRef"
|
||||||
width="500"
|
class="basicModal basicFormModal"
|
||||||
:close-on-click-modal="false"
|
@on-ok="handleSubmit"
|
||||||
:before-close="dialogClose"
|
@on-close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
<template #default>
|
||||||
<el-form-item
|
<n-form
|
||||||
|
class="ls-form"
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="85px"
|
||||||
|
>
|
||||||
|
<n-form-item
|
||||||
label="岗位名称"
|
label="岗位名称"
|
||||||
prop="name"
|
path="name"
|
||||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||||
>
|
>
|
||||||
<el-input class="ls-input" v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="岗位状态" prop="status">
|
<n-form-item label="岗位状态" path="code">
|
||||||
<el-radio-group v-model="formData.status" name="status">
|
<n-radio-group v-model:value="formData.status" name="status">
|
||||||
<el-radio :value="1">正常</el-radio>
|
<n-radio :value="1">正常</n-radio>
|
||||||
<el-radio :value="2">停用</el-radio>
|
<n-radio :value="2">停用</n-radio>
|
||||||
</el-radio-group>
|
</n-radio-group>
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="排序" prop="sort">
|
<n-form-item label="排序" path="sort">
|
||||||
<el-input-number v-model="formData.sort" />
|
<n-input-number v-model:value="formData.sort" />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
</el-form>
|
</n-form>
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="dialogClose">取消</el-button>
|
|
||||||
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</basicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { FormInstance } from 'element-plus';
|
|
||||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||||
import { onMounted, reactive, shallowRef } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { message } from '@/utils/auth';
|
import { useMessage } from 'naive-ui';
|
||||||
import { useLockFn } from '@/utils/useLockFn';
|
import { useModal } from '@/components/Modal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义参数
|
||||||
|
*/
|
||||||
const emit = defineEmits(['success', 'update:visible']);
|
const emit = defineEmits(['success', 'update:visible']);
|
||||||
const formRef = shallowRef<FormInstance>();
|
const formRef = ref();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义表单参数
|
* 定义表单参数
|
||||||
*/
|
*/
|
||||||
|
const message = useMessage();
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -301,32 +338,47 @@ ui/src/views/system/position/
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行提交表单
|
* 定义模态
|
||||||
|
*/
|
||||||
|
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||||
|
title: props.positionId ? '编辑岗位' : '添加岗位',
|
||||||
|
subBtuText: '确定',
|
||||||
|
width: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行提交
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await formRef.value?.validate();
|
formRef.value
|
||||||
|
.validate()
|
||||||
|
.then(async () => {
|
||||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||||
message('操作成功');
|
message.success('操作成功');
|
||||||
|
setSubLoading(false);
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('success');
|
emit('success');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubLoading(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭窗体
|
* 关闭窗体
|
||||||
*/
|
*/
|
||||||
const dialogClose = () => {
|
const handleClose = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置表单数据(编辑模式)
|
* 设置表单数据
|
||||||
*/
|
*/
|
||||||
const setFormData = async () => {
|
const setFormData = async () => {
|
||||||
const data = await getPositionDetail(props.positionId);
|
const data = await getPositionDetail(props.positionId);
|
||||||
for (const key in formData) {
|
for (const key in formData) {
|
||||||
if (data[key] != null && data[key] != undefined) {
|
if (data[key] != null && data[key] != undefined) {
|
||||||
|
//@ts-ignore
|
||||||
formData[key] = data[key];
|
formData[key] = data[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,20 +392,35 @@ ui/src/views/system/position/
|
|||||||
setFormData();
|
setFormData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义函数
|
||||||
|
*/
|
||||||
|
defineExpose({
|
||||||
|
openModal,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 代码解析
|
### 代码解析
|
||||||
|
|
||||||
|
**弹窗组件**:使用项目封装的 `basicModal` 组件,通过 `useModal` hook 管理弹窗状态。
|
||||||
|
|
||||||
**Props 接收**:
|
**Props 接收**:
|
||||||
- `visible`:控制弹窗显示/隐藏,通过 `v-model:visible` 双向绑定
|
- `visible`:控制弹窗显示/隐藏,通过 `v-model:visible` 双向绑定
|
||||||
- `positionId`:编辑记录的 ID,为 0 时表示新增模式
|
- `positionId`:编辑记录的 ID,为 0 时表示新增模式
|
||||||
|
|
||||||
**表单数据**:使用 `reactive` 定义,字段与后端 Schema 对应。
|
**表单组件**:使用 NaiveUI 的 `n-form`、`n-form-item`、`n-input`、`n-radio-group`、`n-radio`、`n-input-number` 组件。
|
||||||
|
|
||||||
|
**表单配置**:
|
||||||
|
- `label-placement="left"`:标签在左侧显示
|
||||||
|
- `label-width="85px"`:标签宽度
|
||||||
|
- `path`:表单字段路径,用于校验
|
||||||
|
- `:rule`:校验规则
|
||||||
|
|
||||||
**新增/编辑判断**:`props.positionId ? positionUpdate : positionAdd`,通过 ID 是否为 0 区分。
|
**新增/编辑判断**:`props.positionId ? positionUpdate : positionAdd`,通过 ID 是否为 0 区分。
|
||||||
|
|
||||||
**防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态,防止重复提交。
|
**防重复提交**:`useModal` 的 `setSubLoading` 方法控制提交按钮 loading 状态。
|
||||||
|
|
||||||
**数据回填**:`onMounted` 时判断如果是编辑模式,调用 `getPositionDetail` 获取数据并填充表单。
|
**数据回填**:`onMounted` 时判断如果是编辑模式,调用 `getPositionDetail` 获取数据并填充表单。
|
||||||
|
|
||||||
@@ -361,6 +428,8 @@ ui/src/views/system/position/
|
|||||||
- `emit('update:visible', false)`:关闭弹窗
|
- `emit('update:visible', false)`:关闭弹窗
|
||||||
- `emit('success')`:通知父页面刷新表格
|
- `emit('success')`:通知父页面刷新表格
|
||||||
|
|
||||||
|
**弹窗调用方式**:父页面通过 `ref` 调用 `openModal()` 打开弹窗。
|
||||||
|
|
||||||
## 3. 表格列定义 `columns.ts`
|
## 3. 表格列定义 `columns.ts`
|
||||||
|
|
||||||
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
|
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
|
||||||
@@ -369,52 +438,58 @@ ui/src/views/system/position/
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { ElTag } from 'element-plus';
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{
|
{
|
||||||
type: 'selection',
|
type: 'selection',
|
||||||
|
width: 50,
|
||||||
|
fixed: 'left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ID',
|
title: 'ID',
|
||||||
prop: 'id',
|
key: 'id',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 50,
|
width: 50,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '岗位名称',
|
title: '岗位名称',
|
||||||
prop: 'name',
|
key: 'name',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '岗位状态',
|
title: '岗位状态',
|
||||||
prop: 'status',
|
key: 'status',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(
|
return h(
|
||||||
ElTag,
|
NTag,
|
||||||
{
|
{
|
||||||
type: record.row.status == 1 ? 'success' : 'danger',
|
style: {
|
||||||
|
marginRight: '6px',
|
||||||
|
},
|
||||||
|
bordered: false,
|
||||||
|
type: record.status == 1 ? 'success' : 'error',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => (record.row.status == 1 ? '正常' : '停用'),
|
default: () => (record.status == 1 ? '正常' : '停用'),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '排序',
|
title: '排序',
|
||||||
prop: 'sort',
|
key: 'sort',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建人',
|
title: '创建人',
|
||||||
prop: 'createUser',
|
key: 'createUser',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
title: '创建时间',
|
||||||
prop: 'createTime',
|
key: 'createTime',
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -422,15 +497,18 @@ export const columns = [
|
|||||||
|
|
||||||
### 代码解析
|
### 代码解析
|
||||||
|
|
||||||
**选择列**:`type: 'selection'` 自动添加复选框列,配合 `@selection-change` 事件实现批量选择。
|
**列配置字段**:NaiveUI 表格使用 `title`(列标题)和 `key`(字段名)。
|
||||||
|
|
||||||
|
**选择列**:`type: 'selection'` 自动添加复选框列,配合 `@update:checked-row-keys` 事件实现批量选择。
|
||||||
|
|
||||||
**固定列**:`fixed: 'left'` 固定在左侧,`fixed: 'right'` 固定在右侧(操作列)。
|
**固定列**:`fixed: 'left'` 固定在左侧,`fixed: 'right'` 固定在右侧(操作列)。
|
||||||
|
|
||||||
**自定义渲染**:`render(record)` 使用 Vue 的 `h()` 函数创建虚拟 DOM。状态列将 `1/2` 渲染为绿色/红色标签。
|
**自定义渲染**:`render(record)` 使用 Vue 的 `h()` 函数创建虚拟 DOM。注意 NaiveUI 的 `render` 回调直接接收 `record` 对象,无需通过 `record.row` 访问。
|
||||||
|
|
||||||
|
**标签组件**:使用 NaiveUI 的 `NTag` 组件,`type` 属性支持 `'success'`、`'error'`、`'warning'`、`'info'` 等类型。`bordered: false` 去除边框。
|
||||||
|
|
||||||
**宽度设置**:
|
**宽度设置**:
|
||||||
- `width`:固定宽度
|
- `width`:固定宽度
|
||||||
- `minWidth`:最小宽度,可自适应拉伸
|
|
||||||
|
|
||||||
## 4. 搜索表单 `querySchemas.ts`
|
## 4. 搜索表单 `querySchemas.ts`
|
||||||
|
|
||||||
@@ -443,7 +521,7 @@ import { FormSchema } from '@/components/Form/index';
|
|||||||
export const schemas: FormSchema[] = [
|
export const schemas: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
component: 'Input',
|
component: 'NInput',
|
||||||
label: '岗位名称',
|
label: '岗位名称',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入岗位名称',
|
placeholder: '请输入岗位名称',
|
||||||
@@ -451,7 +529,7 @@ export const schemas: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
@@ -462,7 +540,7 @@ export const schemas: FormSchema[] = [
|
|||||||
value: '1',
|
value: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '禁用',
|
label: '停用',
|
||||||
value: '2',
|
value: '2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -478,9 +556,9 @@ export const schemas: FormSchema[] = [
|
|||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
|:----|:----|
|
|:----|:----|
|
||||||
| `field` | 字段名,与后端查询参数对应 |
|
| `field` | 字段名,与后端查询参数对应 |
|
||||||
| `component` | 表单组件类型:`Input` / `Select` / `DatePicker` 等 |
|
| `component` | 表单组件类型:`NInput` / `NSelect` / `NDatePicker` 等(NaiveUI 组件名) |
|
||||||
| `label` | 标签文本 |
|
| `label` | 标签文本 |
|
||||||
| `componentProps` | 组件属性,透传给 ElementPlus 组件 |
|
| `componentProps` | 组件属性,透传给 NaiveUI 组件 |
|
||||||
|
|
||||||
**Select 组件**:`options` 定义下拉选项,`clearable: true` 允许清空选择。
|
**Select 组件**:`options` 定义下拉选项,`clearable: true` 允许清空选择。
|
||||||
|
|
||||||
@@ -501,12 +579,15 @@ edit.vue(编辑弹窗)
|
|||||||
|
|
||||||
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
|
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
|
||||||
2. **4 个文件各司其职**:主页面、编辑弹窗、列定义、搜索 Schema
|
2. **4 个文件各司其职**:主页面、编辑弹窗、列定义、搜索 Schema
|
||||||
3. **编辑弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面
|
3. **编辑弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面,通过 `useModal` 管理弹窗状态
|
||||||
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、操作按钮
|
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、操作按钮
|
||||||
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段
|
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段,组件名使用 NaiveUI 组件名(`NInput`、`NSelect`)
|
||||||
6. **新增/编辑通过 `positionId` 区分**:0 为新增,非 0 为编辑
|
6. **新增/编辑通过 `positionId` 区分**:0 为新增,非 0 为编辑
|
||||||
7. **防重复提交使用 `useLockFn`**:提交时按钮 loading
|
7. **消息提示使用 `useMessage()`**:调用 `message.success()` / `message.error()` 等
|
||||||
|
8. **确认对话框使用 `useDialog()`**:调用 `dialog.warning()` 等,通过 `onPositiveClick` 回调处理确认
|
||||||
|
9. **表格行选择使用 `@update:checked-row-keys`**:获取选中行的 key 数组
|
||||||
|
10. **列配置使用 `title` 和 `key`**:定义表格列标题和字段名
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。
|
前端页面统一使用 `n-card` + `BasicForm` + `BasicTable` + `TableAction` 组件组合,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。编辑弹窗使用项目封装的 `basicModal` 组件,通过 `useModal` hook 管理弹窗状态和提交按钮 loading。
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ def _normalize_driver(driver: str) -> str:
|
|||||||
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
|
DB_DRIVER = _normalize_driver(os.getenv('DB_DRIVER', 'mysql'))
|
||||||
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
|
DB_HOST = os.getenv('DB_HOST', '127.0.0.1')
|
||||||
DB_PORT = int(os.getenv('DB_PORT', '3306'))
|
DB_PORT = int(os.getenv('DB_PORT', '3306'))
|
||||||
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.tornado.elevue2')
|
DB_DATABASE = os.getenv('DB_DATABASE', 'djangoadmin.tornado.naivevue')
|
||||||
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
|
DB_USERNAME = os.getenv('DB_USERNAME', 'root')
|
||||||
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Browser (Vue3 + ElementPlus) │
|
│ Browser (Vue3 + NaiveUI) │
|
||||||
│ Axios HTTP Request / Vite Dev Proxy │
|
│ Axios HTTP Request / Vite Dev Proxy │
|
||||||
└──────────────────────────────┬──────────────────────────────────────────┘
|
└──────────────────────────────┬──────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
@@ -167,7 +167,7 @@ class PositionAddHandler(BaseHandler):
|
|||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
│ Frontend (Vue3) │
|
│ Frontend (Vue3) │
|
||||||
│ ElementPlus · Pinia · Vue Router · Axios │
|
│ NaiveUI · Pinia · Vue Router · Axios │
|
||||||
└──────────────────────────┬───────────────────────────────────┘
|
└──────────────────────────┬───────────────────────────────────┘
|
||||||
│ HTTP / JSON
|
│ HTTP / JSON
|
||||||
┌──────────────────────────┴───────────────────────────────────┐
|
┌──────────────────────────┴───────────────────────────────────┐
|
||||||
|
|||||||
@@ -60,13 +60,13 @@ SQLAlchemy 是 Python 生态中最成熟、功能最完备的 ORM 框架,2.0
|
|||||||
|
|
||||||
## 前端技术栈
|
## 前端技术栈
|
||||||
|
|
||||||
### Vue3 + Vite + ElementPlus
|
### Vue3 + Vite + NaiveUI
|
||||||
|
|
||||||
| 组件 | 版本 | 选型理由 |
|
| 组件 | 版本 | 选型理由 |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
| **Vue3** | 3.x | Composition API、更好的 TypeScript 支持 |
|
| **Vue3** | 3.x | Composition API、更好的 TypeScript 支持 |
|
||||||
| **Vite** | 5.x | 极速 HMR、ESM 原生支持、开箱即用的 TypeScript 支持 |
|
| **Vite** | 5.x | 极速 HMR、ESM 原生支持、开箱即用的 TypeScript 支持 |
|
||||||
| **ElementPlus** | 2.x | Vue3 生态最成熟的 UI 组件库,企业级后台管理系统首选 |
|
| **NaiveUI** | 2.x | Vue3 生态高质量 UI 组件库,TypeScript 友好,主题定制灵活 |
|
||||||
| **Pinia** | 2.x | Vue3 官方推荐的状态管理方案,替代 Vuex |
|
| **Pinia** | 2.x | Vue3 官方推荐的状态管理方案,替代 Vuex |
|
||||||
| **Vue Router** | 4.x | Vue3 官方路由,支持动态路由、路由守卫 |
|
| **Vue Router** | 4.x | Vue3 官方路由,支持动态路由、路由守卫 |
|
||||||
| **Axios** | 1.x | HTTP 客户端,支持请求/响应拦截器、取消请求 |
|
| **Axios** | 1.x | HTTP 客户端,支持请求/响应拦截器、取消请求 |
|
||||||
@@ -130,4 +130,4 @@ Redis 在系统中承担多个关键职责:JWT 令牌黑名单、登录失败
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
项目的技术选型以「高性能、类型安全、可扩展」为核心原则。Tornado + SQLAlchemy 2.0 + Pydantic v2 构成后端铁三角,Vue3 + Vite + ElementPlus 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。
|
项目的技术选型以「高性能、类型安全、可扩展」为核心原则。Tornado + SQLAlchemy 2.0 + Pydantic v2 构成后端铁三角,Vue3 + Vite + NaiveUI 提供现代化前端体验,Redis 承担缓存与中间件职责。各组件版本经过充分测试验证,形成稳定的技术底座。选型时优先考虑组件的独立性和可替换性,避免强耦合,为未来的架构演进预留空间。
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ PUT /api/v1/config/item/update
|
|||||||
| 用途 | 枚举值(下拉选项) | 系统参数(配置值) |
|
| 用途 | 枚举值(下拉选项) | 系统参数(配置值) |
|
||||||
| 值类型 | 简单值(字符串) | 可存储较长的配置内容 |
|
| 值类型 | 简单值(字符串) | 可存储较长的配置内容 |
|
||||||
| 典型场景 | 状态、性别、类型 | SMTP配置、站点信息 |
|
| 典型场景 | 状态、性别、类型 | SMTP配置、站点信息 |
|
||||||
| 前端用法 | el-select 选项 | 配置表单 |
|
| 前端用法 | NSelect 选项 | 配置表单 |
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
|
|||||||
@@ -137,14 +137,7 @@ timeout = param_service.get_value_by_code('REQUEST_TIMEOUT', '30')
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-select v-model="form.trainingMethod" placeholder="请选择培训方式">
|
<n-select v-model:value="form.trainingMethod" :options="dictOptions" placeholder="请选择培训方式" />
|
||||||
<el-option
|
|
||||||
v-for="item in dictOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -166,17 +159,17 @@ onMounted(async () => {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { ElTag } from 'element-plus';
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{
|
{
|
||||||
label: '培训方式',
|
title: '培训方式',
|
||||||
prop: 'trainingMethod',
|
key: 'trainingMethod',
|
||||||
render(record) {
|
render(record) {
|
||||||
const map = { 1: '线上', 2: '线下', 3: '混合' };
|
const map = { 1: '线上', 2: '线下', 3: '混合' };
|
||||||
const typeMap = { 1: 'success', 2: 'warning', 3: 'info' };
|
const typeMap = { 1: 'success', 2: 'warning', 3: 'info' };
|
||||||
return h(ElTag, { type: typeMap[record.row.trainingMethod] }, {
|
return h(NTag, { bordered: false, type: typeMap[record.trainingMethod] }, {
|
||||||
default: () => map[record.row.trainingMethod] || '-'
|
default: () => map[record.trainingMethod] || '-'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -191,7 +184,7 @@ export const columns = [
|
|||||||
export const schemas: FormSchema[] = [
|
export const schemas: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'trainingMethod',
|
field: 'trainingMethod',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '培训方式',
|
label: '培训方式',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择培训方式',
|
placeholder: '请选择培训方式',
|
||||||
|
|||||||
@@ -175,15 +175,8 @@ const dictStore = useDictStore()
|
|||||||
const statusOptions = dictStore.getDict('sys_status')
|
const statusOptions = dictStore.getDict('sys_status')
|
||||||
// [{label: "正常", value: "1"}, {label: "停用", value: "2"}]
|
// [{label: "正常", value: "1"}, {label: "停用", value: "2"}]
|
||||||
|
|
||||||
// 在 el-select 中使用
|
// 在 NSelect 中使用
|
||||||
<el-select v-model="form.status">
|
<n-select v-model:value="form.status" :options="statusOptions" />
|
||||||
<el-option
|
|
||||||
v-for="item in statusOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常用字典示例
|
## 常用字典示例
|
||||||
@@ -202,7 +195,7 @@ const statusOptions = dictStore.getDict('sys_status')
|
|||||||
```
|
```
|
||||||
1. 两级结构:字典类型(分组)+ 字典项(具体值),清晰管理
|
1. 两级结构:字典类型(分组)+ 字典项(具体值),清晰管理
|
||||||
2. 进程内缓存:dict_util 内置 TTL 缓存(60秒兜底),变更时主动失效
|
2. 进程内缓存:dict_util 内置 TTL 缓存(60秒兜底),变更时主动失效
|
||||||
3. 前端集成:useDictStore 统一管理,el-select 直接绑定
|
3. 前端集成:useDictStore 统一管理,NSelect 直接绑定
|
||||||
4. 值为字符串:字典项 value 统一为字符串,get_dict_label 自动转字符串匹配
|
4. 值为字符串:字典项 value 统一为字符串,get_dict_label 自动转字符串匹配
|
||||||
5. 避免硬编码:枚举值统一走字典,修改无需改代码
|
5. 避免硬编码:枚举值统一走字典,修改无需改代码
|
||||||
6. 三种取值:get_dict_items(列表)、get_dict_map(映射)、get_dict_label(单值翻译)
|
6. 三种取值:get_dict_items(列表)、get_dict_map(映射)、get_dict_label(单值翻译)
|
||||||
|
|||||||
@@ -185,10 +185,10 @@ def permission_required(permission: str):
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- 只有拥有 sys:user:add 权限的用户才能看到此按钮 -->
|
<!-- 只有拥有 sys:user:add 权限的用户才能看到此按钮 -->
|
||||||
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
|
<n-button v-perm="'sys:user:add'" type="primary">新增</n-button>
|
||||||
|
|
||||||
<!-- 多个权限,满足其一即可 -->
|
<!-- 多个权限,满足其一即可 -->
|
||||||
<el-button v-perm="['sys:user:update', 'sys:user:delete']">操作</el-button>
|
<n-button v-perm="['sys:user:update', 'sys:user:delete']">操作</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 路由守卫
|
### 路由守卫
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class BaseSchema(BaseModel):
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::warning 前端空字符串
|
:::warning 前端空字符串
|
||||||
前端某些组件(如 el-select)在未选择时可能传空字符串 `""`,BaseSchema 的 `empty_str_to_none` 验证器会自动将其转为 `None`,避免整数字段解析报错。
|
前端某些组件(如 NSelect)在未选择时可能传空字符串 `""`,BaseSchema 的 `empty_str_to_none` 验证器会自动将其转为 `None`,避免整数字段解析报错。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 唯一性校验
|
## 唯一性校验
|
||||||
|
|||||||
@@ -208,16 +208,21 @@ transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
|||||||
|
|
||||||
// 业务失败(code === 1)
|
// 业务失败(code === 1)
|
||||||
if (code === ResultEnum.ERROR) {
|
if (code === ResultEnum.ERROR) {
|
||||||
ElMessage.error(msg);
|
window.$message.error(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录超时(code === 401)
|
// 登录超时(code === 401)
|
||||||
if (code === ResultEnum.TIMEOUT) {
|
if (code === ResultEnum.TIMEOUT) {
|
||||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { ... })
|
window.$dialog.warning({
|
||||||
.then(() => {
|
title: '提示',
|
||||||
|
content: '登录身份已失效,请重新登录!',
|
||||||
|
positiveText: '确定',
|
||||||
|
showNegative: false,
|
||||||
|
onPositiveClick: () => {
|
||||||
storage.clear();
|
storage.clear();
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
@@ -304,7 +309,7 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
错误处理分两层:
|
错误处理分两层:
|
||||||
|
|
||||||
1. **业务错误**(code !== 0):拦截器统一弹出 `ElMessage.error(msg)`,并 `throw new Error`
|
1. **业务错误**(code !== 0):拦截器统一弹出 `window.$message.error(msg)`,并 `throw new Error`
|
||||||
2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理
|
2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -312,12 +317,12 @@ responseInterceptorsCatch: (error: any) => {
|
|||||||
const { response, code } = error || {};
|
const { response, code } = error || {};
|
||||||
// 超时
|
// 超时
|
||||||
if (code === 'ECONNABORTED') {
|
if (code === 'ECONNABORTED') {
|
||||||
ElMessage.error('接口请求超时,请刷新页面重试!');
|
window.$message.error('接口请求超时,请刷新页面重试!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 网络异常
|
// 网络异常
|
||||||
if (error.toString().includes('Network Error')) {
|
if (error.toString().includes('Network Error')) {
|
||||||
ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', { ... });
|
window.$dialog.warning({ title: '网络异常', content: '请检查您的网络连接是否正常' });
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
// 其他 HTTP 状态码
|
// 其他 HTTP 状态码
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ Vite 按 mode 加载环境变量,后面的文件覆盖前面的:
|
|||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
| `VITE_PORT` | `number` | `8001` | 开发服务器端口 |
|
| `VITE_PORT` | `number` | `8001` | 开发服务器端口 |
|
||||||
| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) |
|
| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) |
|
||||||
| `VITE_GLOB_APP_TITLE` | `string` | `Tornado+ElementPlus版` | 应用标题(注入 HTML) |
|
| `VITE_GLOB_APP_TITLE` | `string` | `Tornado+NaiveUI版` | 应用标题(注入 HTML) |
|
||||||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Tornado+ElementPlus版` | 应用短名(用于生成运行时配置变量名) |
|
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Tornado+NaiveUI版` | 应用短名(用于生成运行时配置变量名) |
|
||||||
| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) |
|
| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) |
|
||||||
| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 |
|
| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 |
|
||||||
| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) |
|
| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) |
|
||||||
@@ -175,7 +175,7 @@ import { defineStore } from 'pinia'; // 自动
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- 构建后的 index.html -->
|
<!-- 构建后的 index.html -->
|
||||||
<title>Tornado+ElementPlus版</title>
|
<title>Tornado+NaiveUI版</title>
|
||||||
<script src="/app.config.js?v=1.0.0-1725000000000"></script>
|
<script src="/app.config.js?v=1.0.0-1725000000000"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -188,8 +188,8 @@ import { defineStore } from 'pinia'; // 自动
|
|||||||
window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__ = {
|
window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__ = {
|
||||||
"VITE_GLOB_API_URL": "",
|
"VITE_GLOB_API_URL": "",
|
||||||
"VITE_GLOB_API_URL_PREFIX": "/api",
|
"VITE_GLOB_API_URL_PREFIX": "/api",
|
||||||
"VITE_GLOB_FILE_URL": "http://file.tornado.elevue2",
|
"VITE_GLOB_FILE_URL": "http://file.tornado.naivevue",
|
||||||
"VITE_GLOB_APP_TITLE": "Tornado+ElementPlus版",
|
"VITE_GLOB_APP_TITLE": "Tornado+NaiveUI版",
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
Object.freeze(window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__);
|
Object.freeze(window.__PRODUCTION_TORNADO_ELEMENTPLUS___CONF__);
|
||||||
@@ -249,7 +249,7 @@ dist/
|
|||||||
├── app.config.js # 运行时配置(VITE_GLOB_* 变量)
|
├── app.config.js # 运行时配置(VITE_GLOB_* 变量)
|
||||||
├── favicon.ico # 网站图标
|
├── favicon.ico # 网站图标
|
||||||
└── assets/
|
└── assets/
|
||||||
├── index-[hash].js # 主包(Vue、ElementPlus、Pinia 等)
|
├── index-[hash].js # 主包(Vue、NaiveUI、Pinia 等)
|
||||||
├── index-[hash].css # 全局样式
|
├── index-[hash].css # 全局样式
|
||||||
├── [name]-[hash].js # 路由懒加载 chunk(按视图分割)
|
├── [name]-[hash].js # 路由懒加载 chunk(按视图分割)
|
||||||
└── ... # 字体、图片等静态资源
|
└── ... # 字体、图片等静态资源
|
||||||
@@ -276,7 +276,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
|||||||
| chunk | 内容 |
|
| chunk | 内容 |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| `vendor-vue.js` | vue + vue-router + pinia |
|
| `vendor-vue.js` | vue + vue-router + pinia |
|
||||||
| `vendor-element.js` | element-plus |
|
| `vendor-naive.js` | naive-ui |
|
||||||
| `vendor-echarts.js` | echarts(如果使用) |
|
| `vendor-echarts.js` | echarts(如果使用) |
|
||||||
| `vendor-*.js` | 其他大型依赖 |
|
| `vendor-*.js` | 其他大型依赖 |
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@element-plus/icons-vue'],
|
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd'],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -300,12 +300,7 @@ optimizeDeps: {
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// 1. 全局注册 ElementPlus 图标
|
// NaiveUI 按需引入,无需全局注册图标
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
||||||
app.component(key, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 全局引入 ElementPlus(中文包)
|
|
||||||
setupElement(app);
|
setupElement(app);
|
||||||
|
|
||||||
// 3. 全局注册自定义组件(BasicTable、BasicForm 等)
|
// 3. 全局注册自定义组件(BasicTable、BasicForm 等)
|
||||||
@@ -332,7 +327,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
| 注册项 | 文件 | 说明 |
|
| 注册项 | 文件 | 说明 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| ElementPlus | `element.ts` | 全局引入 + 中文包 |
|
| NaiveUI | 按需引入 | 组件按需引入,无需全局注册 |
|
||||||
| 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 |
|
| 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 |
|
||||||
| 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` |
|
| 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` |
|
||||||
|
|
||||||
@@ -497,7 +492,7 @@ pnpm report
|
|||||||
**优化手段**:
|
**优化手段**:
|
||||||
|
|
||||||
1. 路由懒加载(已默认启用)
|
1. 路由懒加载(已默认启用)
|
||||||
2. 按需引入 ElementPlus 组件(当前为全量引入)
|
2. 按需引入 NaiveUI 组件(当前为按需引入)
|
||||||
3. 大型依赖(echarts、wangeditor)按需引入
|
3. 大型依赖(echarts、wangeditor)按需引入
|
||||||
4. 开启 gzip 压缩(`VITE_BUILD_COMPRESS=gzip`)
|
4. 开启 gzip 压缩(`VITE_BUILD_COMPRESS=gzip`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 公共组件库
|
# 公共组件库
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 ElementPlus 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 NaiveUI 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 组件总览
|
## 组件总览
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
| CropperImage | `components/Cropper/` | 图片裁剪(圆形/矩形、实时预览) |
|
| CropperImage | `components/Cropper/` | 图片裁剪(圆形/矩形、实时预览) |
|
||||||
| QrCode | `components/Qrcode/` | 二维码生成(canvas/img、内嵌 logo) |
|
| QrCode | `components/Qrcode/` | 二维码生成(canvas/img、内嵌 logo) |
|
||||||
| ChinaArea | `components/ChinaArea/` | 省市区级联(懒加载、二/三级联动) |
|
| ChinaArea | `components/ChinaArea/` | 省市区级联(懒加载、二/三级联动) |
|
||||||
| Icon | `components/icon/` | 图标(支持 ElementPlus 图标 + SVG 图标) |
|
| Icon | `components/icon/` | 图标(支持 @vicons/antd 图标 + SVG 图标) |
|
||||||
| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) |
|
| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) |
|
||||||
| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) |
|
| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) |
|
||||||
| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) |
|
| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) |
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
## BasicTable 表格
|
## BasicTable 表格
|
||||||
|
|
||||||
表格组件是使用频率最高的组件,基于 `el-table` + `el-pagination` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
表格组件是使用频率最高的组件,基于 NaiveUI 的 `n-data-table` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
@@ -129,9 +129,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef">
|
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef">
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
<n-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
||||||
<el-icon><Plus /></el-icon>新增
|
<template #icon><n-icon><Plus /></n-icon></template>
|
||||||
</el-button>
|
新增
|
||||||
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</template>
|
</template>
|
||||||
@@ -299,7 +300,7 @@ const [registerForm] = useForm({
|
|||||||
|
|
||||||
## BasicModal 弹窗
|
## BasicModal 弹窗
|
||||||
|
|
||||||
弹窗组件基于 `el-dialog` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
弹窗组件基于 `basicModal` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
@@ -356,7 +357,7 @@ function handleEdit(record) {
|
|||||||
|
|
||||||
## BasicUpload 上传
|
## BasicUpload 上传
|
||||||
|
|
||||||
文件/图片上传组件,基于 `el-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
|
文件/图片上传组件,基于 NaiveUI 的 `n-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
@@ -698,10 +699,10 @@ async function getDeptOptions() {
|
|||||||
<BasicTable :columns="columns" :request="loadData" />
|
<BasicTable :columns="columns" :request="loadData" />
|
||||||
|
|
||||||
<template #leftFooter>
|
<template #leftFooter>
|
||||||
<el-button @click="handleCancel">取消</el-button>
|
<n-button @click="handleCancel">取消</n-button>
|
||||||
</template>
|
</template>
|
||||||
<template #rightFooter>
|
<template #rightFooter>
|
||||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
<n-button type="primary" @click="handleSubmit">提交</n-button>
|
||||||
</template>
|
</template>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -724,7 +725,7 @@ async function getDeptOptions() {
|
|||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<Authority :value="['sys:user:add']">
|
<Authority :value="['sys:user:add']">
|
||||||
<el-button type="primary">新增用户</el-button>
|
<n-button type="primary">新增用户</n-button>
|
||||||
</Authority>
|
</Authority>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
@@ -732,7 +733,7 @@ async function getDeptOptions() {
|
|||||||
:::tip v-perm 指令
|
:::tip v-perm 指令
|
||||||
除了 `Authority` 组件,项目还提供 `v-perm` 指令,功能相同但更简洁:
|
除了 `Authority` 组件,项目还提供 `v-perm` 指令,功能相同但更简洁:
|
||||||
```vue
|
```vue
|
||||||
<el-button v-perm="['sys:user:add']">新增用户</el-button>
|
<n-button v-perm="['sys:user:add']">新增用户</n-button>
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -740,7 +741,7 @@ async function getDeptOptions() {
|
|||||||
|
|
||||||
## Icon 图标
|
## Icon 图标
|
||||||
|
|
||||||
统一图标组件,支持 ElementPlus 图标和 SVG 图标。
|
统一图标组件,支持 @vicons/antd 图标和 SVG 图标。
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
@@ -754,8 +755,8 @@ async function getDeptOptions() {
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<!-- ElementPlus 图标 -->
|
<!-- @vicons/antd 图标 -->
|
||||||
<Icon name="el-icon-Plus" size="18px" />
|
<Icon name="PlusOutlined" size="18px" />
|
||||||
<!-- SVG 图标 -->
|
<!-- SVG 图标 -->
|
||||||
<Icon name="svg-icon-user" />
|
<Icon name="svg-icon-user" />
|
||||||
</template>
|
</template>
|
||||||
@@ -836,4 +837,4 @@ setupCustomComponents(app); // 注册所有自定义组件
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
公共组件库基于 ElementPlus 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
|
公共组件库基于 NaiveUI 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# BasicForm 表单
|
# BasicForm 表单
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 ElementPlus 的 `el-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 NaiveUI 的 `n-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 组件架构
|
## 组件架构
|
||||||
@@ -24,9 +24,9 @@ src/components/Form/
|
|||||||
列表页的搜索区域使用 `useForm` 注册表单:
|
列表页的搜索区域使用 `useForm` 注册表单:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||||
</el-card>
|
</n-card>
|
||||||
```
|
```
|
||||||
|
|
||||||
### querySchemas 定义
|
### querySchemas 定义
|
||||||
@@ -39,7 +39,7 @@ import { FormSchema } from '@/components/Form/index';
|
|||||||
export const schemas: FormSchema[] = [
|
export const schemas: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
component: 'Input',
|
component: 'NInput',
|
||||||
label: '岗位名称',
|
label: '岗位名称',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入岗位名称',
|
placeholder: '请输入岗位名称',
|
||||||
@@ -47,14 +47,14 @@ export const schemas: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
clearable: true,
|
clearable: true,
|
||||||
options: [
|
options: [
|
||||||
{ label: '正常', value: '1' },
|
{ label: '正常', value: '1' },
|
||||||
{ label: '禁用', value: '2' },
|
{ label: '停用', value: '2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -66,26 +66,25 @@ export const schemas: FormSchema[] = [
|
|||||||
```typescript
|
```typescript
|
||||||
import { schemas } from './querySchemas';
|
import { schemas } from './querySchemas';
|
||||||
import { useForm } from '@/components/Form/index';
|
import { useForm } from '@/components/Form/index';
|
||||||
import { ColProps } from 'element-plus';
|
|
||||||
|
|
||||||
const formParams = reactive({ name: '', status: '' });
|
const formParams = reactive({ name: '', status: '' });
|
||||||
|
|
||||||
const [register, {}] = useForm({
|
const [register, {}] = useForm({
|
||||||
|
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||||
labelWidth: 80,
|
labelWidth: 80,
|
||||||
layout: 'horizontal',
|
|
||||||
colProps: { span: 6 } as ColProps,
|
|
||||||
submitOnReset: true,
|
|
||||||
schemas,
|
schemas,
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(values: Recordable) {
|
function handleSubmit(values: Recordable) {
|
||||||
handleReset();
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
for (const key in values) { formParams[key] = values[key]; }
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset(values: Recordable) {
|
||||||
for (const key in formParams) { formParams[key] = ''; }
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
|
reloadTable();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,73 +92,85 @@ function handleReset() {
|
|||||||
|
|
||||||
## 业务表单(编辑弹窗)
|
## 业务表单(编辑弹窗)
|
||||||
|
|
||||||
编辑弹窗中直接使用 `el-form` + `el-form-item` 构建表单:
|
编辑弹窗中直接使用 `n-form` + `n-form-item` 构建表单:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/edit.vue -->
|
<!-- src/views/system/position/edit.vue -->
|
||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="props.visible" :title="props.positionId ? '编辑' : '新增'" width="500">
|
<basicModal
|
||||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
@register="modalRegister"
|
||||||
<el-form-item
|
ref="modalRef"
|
||||||
label="岗位名称"
|
class="basicModal basicFormModal"
|
||||||
prop="name"
|
@on-ok="handleSubmit"
|
||||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
@on-close="handleClose"
|
||||||
>
|
>
|
||||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
<template #default>
|
||||||
</el-form-item>
|
<n-form
|
||||||
<el-form-item label="岗位状态" prop="status">
|
class="ls-form"
|
||||||
<el-radio-group v-model="formData.status">
|
ref="formRef"
|
||||||
<el-radio :value="1">正常</el-radio>
|
:model="formData"
|
||||||
<el-radio :value="2">停用</el-radio>
|
label-placement="left"
|
||||||
</el-radio-group>
|
label-width="85px"
|
||||||
</el-form-item>
|
>
|
||||||
<el-form-item label="排序" prop="sort">
|
<n-form-item
|
||||||
<el-input-number v-model="formData.sort" />
|
label="岗位名称"
|
||||||
</el-form-item>
|
path="name"
|
||||||
</el-form>
|
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||||
</el-dialog>
|
>
|
||||||
|
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="岗位状态" path="code">
|
||||||
|
<n-radio-group v-model:value="formData.status" name="status">
|
||||||
|
<n-radio :value="1">正常</n-radio>
|
||||||
|
<n-radio :value="2">停用</n-radio>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="排序" path="sort">
|
||||||
|
<n-input-number v-model:value="formData.sort" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</template>
|
||||||
|
</basicModal>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
## BasicForm 内部渲染机制
|
## BasicForm 内部渲染机制
|
||||||
|
|
||||||
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 ElementPlus 组件:
|
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 NaiveUI 组件:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- src/components/Form/src/BasicForm.vue 核心逻辑 -->
|
<!-- src/components/Form/src/BasicForm.vue 核心逻辑 -->
|
||||||
<template v-for="schema in getSchema" :key="schema.field">
|
<template v-for="schema in getSchema" :key="schema.field">
|
||||||
<!-- Checkbox -->
|
<!-- Checkbox -->
|
||||||
<template v-if="schema.component === 'Checkbox'">
|
<template v-if="schema.component === 'NCheckbox'">
|
||||||
<el-checkbox-group v-model="formModel[schema.field]">
|
<n-checkbox-group v-model:value="formModel[schema.field]">
|
||||||
<el-checkbox v-for="item in schema.componentProps?.options" :value="item.value">
|
<n-checkbox v-for="item in schema.componentProps?.options" :value="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</el-checkbox>
|
</n-checkbox>
|
||||||
</el-checkbox-group>
|
</n-checkbox-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- RadioGroup -->
|
<!-- RadioGroup -->
|
||||||
<template v-else-if="schema.component === 'RadioGroup'">
|
<template v-else-if="schema.component === 'NRadioGroup'">
|
||||||
<el-radio-group v-model="formModel[schema.field]">
|
<n-radio-group v-model:value="formModel[schema.field]">
|
||||||
<el-radio v-for="item in schema.componentProps?.options" :value="item.value">
|
<n-radio v-for="item in schema.componentProps?.options" :value="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</el-radio>
|
</n-radio>
|
||||||
</el-radio-group>
|
</n-radio-group>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Select -->
|
<!-- Select -->
|
||||||
<template v-else-if="schema.component === 'Select'">
|
<template v-else-if="schema.component === 'NSelect'">
|
||||||
<el-select v-model="formModel[schema.field]">
|
<n-select v-model:value="formModel[schema.field]" :options="schema.componentProps?.options" />
|
||||||
<el-option v-for="item in schema.componentProps?.options" :value="item.value" :label="item.label" />
|
|
||||||
</el-select>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Daterange -->
|
<!-- Daterange -->
|
||||||
<template v-else-if="schema.component === 'Daterange'">
|
<template v-else-if="schema.component === 'NDatePicker'">
|
||||||
<el-date-picker v-model="formModel[schema.field]" type="daterange" value-format="YYYY-MM-DD" />
|
<n-date-picker v-model:value="formModel[schema.field]" type="daterange" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 其他组件:动态渲染 el-{component} -->
|
<!-- 其他组件:动态渲染 N{component} -->
|
||||||
<component v-else :is="`el-${schema.component}`" v-model="formModel[schema.field]" />
|
<component v-else :is="schema.component" v-model:value="formModel[schema.field]" v-bind="schema.componentProps" />
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -169,7 +180,7 @@ function handleReset() {
|
|||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
field: string; // 字段名
|
field: string; // 字段名
|
||||||
label: string; // 标签文本
|
label: string; // 标签文本
|
||||||
component: string; // 组件类型:Input / Select / RadioGroup / Checkbox / DatePicker 等
|
component: string; // 组件类型:NInput / NSelect / NRadioGroup / NCheckbox / NDatePicker 等
|
||||||
componentProps?: any; // 组件属性(placeholder、options、clearable 等)
|
componentProps?: any; // 组件属性(placeholder、options、clearable 等)
|
||||||
required?: boolean; // 是否必填
|
required?: boolean; // 是否必填
|
||||||
rules?: any[]; // 自定义校验规则
|
rules?: any[]; // 自定义校验规则
|
||||||
@@ -202,7 +213,7 @@ Schema 中的 `required: true` 会自动生成必填校验规则,也可以通
|
|||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
label: '岗位名称',
|
label: '岗位名称',
|
||||||
component: 'Input',
|
component: 'NInput',
|
||||||
rules: [
|
rules: [
|
||||||
{ required: true, message: '请输入岗位名称', trigger: 'blur' },
|
{ required: true, message: '请输入岗位名称', trigger: 'blur' },
|
||||||
{ min: 1, max: 150, message: '长度在 1 到 150 个字符', trigger: 'blur' },
|
{ min: 1, max: 150, message: '长度在 1 到 150 个字符', trigger: 'blur' },
|
||||||
@@ -237,4 +248,4 @@ onMounted(() => {
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 ElementPlus 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `el-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
|
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 NaiveUI 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `n-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
|||||||
```
|
```
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
|
<n-button :loading="subLoading" type="primary" @click="submit">确定</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
## useAsync
|
## useAsync
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
# 国际化
|
# 国际化
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
当前项目仅支持中文界面,ElementPlus 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
当前项目仅支持中文界面,NaiveUI 默认使用中文,所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
项目当前为中文单语言版本:
|
项目当前为中文单语言版本:
|
||||||
|
|
||||||
- ElementPlus 全局配置中文语言包:`app.use(ElementPlus, { locale: zhCn })`
|
- NaiveUI 默认中文,无需额外配置
|
||||||
- 所有界面文本直接硬编码在组件中:`<el-button>添加岗位</el-button>`
|
- 所有界面文本直接硬编码在组件中:`<n-button>新建</n-button>`
|
||||||
- 后端返回的 `msg` 字段为中文:`"操作成功"`、`"岗位名称不能重复"`
|
- 后端返回的 `msg` 字段为中文:`"操作成功"`、`"岗位名称不能重复"`
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-button type="primary">添加岗位</el-button>
|
<n-button type="primary">新建</n-button>
|
||||||
<el-table-column label="岗位名称" prop="name" />
|
<!-- 表格列定义在 columns.ts 中 -->
|
||||||
<el-table-column label="创建时间" prop="createTime" />
|
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,8 +64,7 @@ export default i18n;
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-button type="primary">{{ $t('common.add') }}</el-button>
|
<n-button type="primary">{{ $t('common.add') }}</n-button>
|
||||||
<el-table-column :label="$t('position.name')" prop="name" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -75,13 +73,20 @@ import { useI18n } from 'vue-i18n';
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### ElementPlus 国际化
|
### NaiveUI 国际化
|
||||||
|
|
||||||
```typescript
|
NaiveUI 通过 `n-config-provider` 的 `locale` 属性支持国际化:
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
|
||||||
import en from 'element-plus/es/locale/lang/en';
|
|
||||||
|
|
||||||
app.use(ElementPlus, { locale: zhCn });
|
```vue
|
||||||
|
<template>
|
||||||
|
<n-config-provider :locale="zhCN">
|
||||||
|
<App />
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { zhCN, dateZhCN } from 'naive-ui';
|
||||||
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 语言切换组件
|
### 语言切换组件
|
||||||
@@ -89,19 +94,9 @@ app.use(ElementPlus, { locale: zhCn });
|
|||||||
```vue
|
```vue
|
||||||
<!-- src/components/LangSelect/index.vue -->
|
<!-- src/components/LangSelect/index.vue -->
|
||||||
<template>
|
<template>
|
||||||
<el-dropdown @command="handleLangChange">
|
<n-dropdown :options="langOptions" @select="handleLangChange">
|
||||||
<span class="lang-icon">🌐</span>
|
<n-button text>🌐</n-button>
|
||||||
<template #dropdown>
|
</n-dropdown>
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="zh-CN" :disabled="locale === 'zh-CN'">
|
|
||||||
中文
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="en-US" :disabled="locale === 'en-US'">
|
|
||||||
English
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -111,11 +106,16 @@ import { useAppStore } from '/@/store/modules/app';
|
|||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const langOptions = [
|
||||||
|
{ label: '中文', key: 'zh-CN', disabled: locale.value === 'zh-CN' },
|
||||||
|
{ label: 'English', key: 'en-US', disabled: locale.value === 'en-US' },
|
||||||
|
];
|
||||||
|
|
||||||
const handleLangChange = (lang: string) => {
|
const handleLangChange = (lang: string) => {
|
||||||
locale.value = lang;
|
locale.value = lang;
|
||||||
localStorage.setItem('lang', lang);
|
localStorage.setItem('lang', lang);
|
||||||
appStore.setLocale(lang);
|
appStore.setLocale(lang);
|
||||||
// 刷新页面以更新 ElementPlus 语言包
|
// 刷新页面以更新 NaiveUI 语言包
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -137,7 +137,7 @@ const msgMap: Record<number, string> = {
|
|||||||
// Axios 响应拦截器中
|
// Axios 响应拦截器中
|
||||||
if (response.data.code !== 0) {
|
if (response.data.code !== 0) {
|
||||||
const msg = msgMap[response.data.code] || response.data.msg;
|
const msg = msgMap[response.data.code] || response.data.msg;
|
||||||
ElMessage.error(msg);
|
window.$message.error(msg);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,4 +147,4 @@ if (response.data.code !== 0) {
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
当前项目为中文单语言版本,ElementPlus 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 ElementPlus 内置语言包和后端消息 code 实现全链路多语言。语言切换组件可放在顶栏,支持用户手动切换。
|
当前项目为中文单语言版本,NaiveUI 默认使用中文。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 NaiveUI 的 `n-config-provider` 内置语言包和后端消息 code 实现全链路多语言。语言切换组件可放在顶栏,支持用户手动切换。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Modal 弹窗
|
# Modal 弹窗
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
|
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和 `useModal` hook 管理弹窗状态。编辑弹窗通过 `basicModal` 组件实现,支持提交按钮 loading、标题动态设置等能力。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 组件架构
|
## 组件架构
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
```
|
```
|
||||||
src/components/Modal/
|
src/components/Modal/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── BasicModal.vue # 主组件(基于 el-dialog)
|
│ ├── BasicModal.vue # 主组件
|
||||||
│ ├── props.ts # 属性定义
|
│ ├── props.ts # 属性定义
|
||||||
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
|
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
|
||||||
│ └── hooks/
|
│ └── hooks/
|
||||||
@@ -19,12 +19,20 @@ src/components/Modal/
|
|||||||
|
|
||||||
## BasicModal 组件
|
## BasicModal 组件
|
||||||
|
|
||||||
`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力:
|
`BasicModal` 提供全屏切换、自定义头部、提交按钮等能力:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
|
<basicModal
|
||||||
<BasicForm @register="registerForm" />
|
@register="modalRegister"
|
||||||
</BasicModal>
|
ref="modalRef"
|
||||||
|
class="basicModal basicFormModal"
|
||||||
|
@on-ok="handleSubmit"
|
||||||
|
@on-close="handleClose"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<!-- 表单内容 -->
|
||||||
|
</template>
|
||||||
|
</basicModal>
|
||||||
```
|
```
|
||||||
|
|
||||||
核心功能:
|
核心功能:
|
||||||
@@ -36,89 +44,96 @@ src/components/Modal/
|
|||||||
|
|
||||||
## 实际用法(position 模块)
|
## 实际用法(position 模块)
|
||||||
|
|
||||||
以岗位管理为例,实际使用原生 `el-dialog` + `defineAsyncComponent` 懒加载:
|
以岗位管理为例,使用 `basicModal` + `useModal` hook 管理弹窗:
|
||||||
|
|
||||||
### 父组件(index.vue)
|
### 父组件(index.vue)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/index.vue -->
|
<!-- src/views/system/position/index.vue -->
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import editDialog from './edit.vue';
|
||||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
|
||||||
|
|
||||||
|
const createModalRef = ref();
|
||||||
const positionId = ref(0);
|
const positionId = ref(0);
|
||||||
const editVisible = ref(false);
|
const editVisible = ref(false);
|
||||||
|
|
||||||
// 新增
|
// 新增
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
positionId.value = 0;
|
positionId.value = 0;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = async (record: Recordable) => {
|
async function handleEdit(record: Recordable) {
|
||||||
positionId.value = record.row.id;
|
positionId.value = record.id;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
};
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<editDialog
|
<editDialog
|
||||||
v-if="editVisible"
|
ref="createModalRef"
|
||||||
:positionId="positionId"
|
:positionId="positionId"
|
||||||
|
v-if="editVisible"
|
||||||
v-model:visible="editVisible"
|
v-model:visible="editVisible"
|
||||||
@success="reloadTable('noRefresh')"
|
@success="reloadTable('noRefresh')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。
|
|
||||||
|
|
||||||
### 编辑弹窗(edit.vue)
|
### 编辑弹窗(edit.vue)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/edit.vue -->
|
<!-- src/views/system/position/edit.vue -->
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<basicModal
|
||||||
v-model="props.visible"
|
@register="modalRegister"
|
||||||
:title="props.positionId ? '编辑' : '新增'"
|
ref="modalRef"
|
||||||
width="500"
|
class="basicModal basicFormModal"
|
||||||
:close-on-click-modal="false"
|
@on-ok="handleSubmit"
|
||||||
:before-close="dialogClose"
|
@on-close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
<template #default>
|
||||||
<el-form-item
|
<n-form
|
||||||
|
class="ls-form"
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="85px"
|
||||||
|
>
|
||||||
|
<n-form-item
|
||||||
label="岗位名称"
|
label="岗位名称"
|
||||||
prop="name"
|
path="name"
|
||||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||||
>
|
>
|
||||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="岗位状态" prop="status">
|
<n-form-item label="岗位状态" path="code">
|
||||||
<el-radio-group v-model="formData.status">
|
<n-radio-group v-model:value="formData.status" name="status">
|
||||||
<el-radio :value="1">正常</el-radio>
|
<n-radio :value="1">正常</n-radio>
|
||||||
<el-radio :value="2">停用</el-radio>
|
<n-radio :value="2">停用</n-radio>
|
||||||
</el-radio-group>
|
</n-radio-group>
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="排序" prop="sort">
|
<n-form-item label="排序" path="sort">
|
||||||
<el-input-number v-model="formData.sort" />
|
<n-input-number v-model:value="formData.sort" />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
</el-form>
|
</n-form>
|
||||||
<template #footer>
|
|
||||||
<el-button @click="dialogClose">取消</el-button>
|
|
||||||
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</basicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||||
import { useLockFn } from '@/utils/useLockFn';
|
import { useMessage } from 'naive-ui';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
|
||||||
const emit = defineEmits(['success', 'update:visible']);
|
const emit = defineEmits(['success', 'update:visible']);
|
||||||
const formRef = shallowRef<FormInstance>();
|
const formRef = ref();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
|
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
|
||||||
|
|
||||||
@@ -127,20 +142,31 @@ const props = defineProps({
|
|||||||
positionId: { type: Number, required: true, default: 0 },
|
positionId: { type: Number, required: true, default: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 定义模态
|
||||||
|
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||||
|
title: props.positionId ? '编辑岗位' : '添加岗位',
|
||||||
|
subBtuText: '确定',
|
||||||
|
width: 600,
|
||||||
|
});
|
||||||
|
|
||||||
// 提交处理
|
// 提交处理
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await formRef.value?.validate();
|
formRef.value
|
||||||
|
.validate()
|
||||||
|
.then(async () => {
|
||||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||||
message('操作成功');
|
message.success('操作成功');
|
||||||
|
setSubLoading(false);
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('success');
|
emit('success');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubLoading(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
const dialogClose = () => { emit('update:visible', false); };
|
const handleClose = () => { emit('update:visible', false); };
|
||||||
|
|
||||||
// useLockFn 防重复提交,isLock 绑定按钮 loading
|
|
||||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
|
||||||
|
|
||||||
// 编辑时加载数据
|
// 编辑时加载数据
|
||||||
const setFormData = async () => {
|
const setFormData = async () => {
|
||||||
@@ -155,42 +181,49 @@ const setFormData = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.positionId) setFormData();
|
if (props.positionId) setFormData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({ openModal });
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 弹窗数据流
|
## 弹窗数据流
|
||||||
|
|
||||||
```
|
```
|
||||||
父组件点击编辑 → positionId = record.row.id → editVisible = true
|
父组件点击编辑 → positionId = record.id → editVisible = true → openModal()
|
||||||
↓
|
↓
|
||||||
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
|
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
|
||||||
↓
|
↓
|
||||||
用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)
|
用户修改 → handleSubmit → formRef.validate() → positionUpdate(formData)
|
||||||
↓
|
↓
|
||||||
emit('success') → 父组件 reloadTable() → 表格刷新
|
emit('success') → 父组件 reloadTable() → 表格刷新
|
||||||
```
|
```
|
||||||
|
|
||||||
## 确认弹窗
|
## 确认弹窗
|
||||||
|
|
||||||
删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`):
|
删除等危险操作使用 NaiveUI 的 `useDialog`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { message, confirm } from '@/utils/auth';
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
|
|
||||||
async function handleDelete(record: Recordable) {
|
const message = useMessage();
|
||||||
let ids = [];
|
const dialog = useDialog();
|
||||||
if (!record) {
|
|
||||||
ids = selectionData.value.map(({ id }) => id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await confirm('确定要删除?');
|
async function handleDelete(record) {
|
||||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
dialog.warning({
|
||||||
message('删除成功');
|
title: '提示',
|
||||||
|
content: '确定要删除?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
record.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
|
||||||
|
message.success('删除成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`confirm` 内部调用 `ElMessageBox.confirm`,返回 Promise,用户取消时抛出异常中断后续逻辑。
|
`useDialog` 返回 Dialog 实例,通过 `dialog.warning()` / `dialog.error()` 等方法弹出确认框,`onPositiveClick` 回调处理确认逻辑。
|
||||||
|
|
||||||
## v-model:visible 双向绑定
|
## v-model:visible 双向绑定
|
||||||
|
|
||||||
@@ -203,14 +236,14 @@ async function handleDelete(record: Recordable) {
|
|||||||
<!-- 子组件 -->
|
<!-- 子组件 -->
|
||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['update:visible']);
|
const emit = defineEmits(['update:visible']);
|
||||||
const dialogClose = () => { emit('update:visible', false); };
|
const handleClose = () => { emit('update:visible', false); };
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。
|
编辑弹窗使用 `basicModal` 组件,通过 `useModal` hook 管理弹窗状态(标题、宽度、提交按钮 loading)。`setSubLoading(false)` 在提交完成后关闭 loading。`defineExpose({ openModal })` 暴露 `openModal` 方法供父组件调用。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
|
编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态,通过 `v-model:visible` 双向绑定显隐状态,`setSubLoading` 控制提交按钮 loading 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。删除确认使用 NaiveUI 的 `useDialog`。
|
||||||
|
|||||||
@@ -90,15 +90,15 @@ export function usePermission() {
|
|||||||
### v-perm(满足任一权限)
|
### v-perm(满足任一权限)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<el-button v-perm="['sys:position:add']">添加岗位</el-button>
|
<n-button v-perm="['sys:position:add']">添加岗位</n-button>
|
||||||
<el-button v-perm="['sys:position:batchDelete']">删除</el-button>
|
<n-button v-perm="['sys:position:batchDelete']">删除</n-button>
|
||||||
<el-button v-perm="['sys:position:export']">导出</el-button>
|
<n-button v-perm="['sys:position:export']">导出</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### v-perms(满足所有权限)
|
### v-perms(满足所有权限)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<el-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</el-button>
|
<n-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 与 v-if 结合
|
### 与 v-if 结合
|
||||||
@@ -113,7 +113,7 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
|||||||
```
|
```
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<el-button v-if="showAddBtn">添加岗位</el-button>
|
<n-button v-if="showAddBtn">添加岗位</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 实际用法(position 模块)
|
## 实际用法(position 模块)
|
||||||
@@ -123,17 +123,20 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
|||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/index.vue -->
|
<!-- src/views/system/position/index.vue -->
|
||||||
<template>
|
<template>
|
||||||
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef" :actionColumn="actionColumn">
|
<BasicTable :columns="columns" :request="loadDataTable" ref="basicTableRef" :actionColumn="actionColumn"
|
||||||
|
@update:checked-row-keys="onCheckedRow">
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
<n-space>
|
||||||
<template #icon><el-icon><PlusOutlined /></el-icon></template>
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||||
添加岗位
|
<template #icon><n-icon><PlusOutlined /></n-icon></template>
|
||||||
</el-button>
|
新建
|
||||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
</n-button>
|
||||||
|
<n-button type="error" @click="handleDelete" :disabled="!rowKeys.length"
|
||||||
v-perm="['sys:position:batchDelete']">
|
v-perm="['sys:position:batchDelete']">
|
||||||
<template #icon><el-icon><Delete /></el-icon></template>
|
<template #icon><n-icon><DeleteOutlined /></n-icon></template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</template>
|
</template>
|
||||||
@@ -142,25 +145,30 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
|||||||
操作列中的权限控制通过 `auth` 属性实现:
|
操作列中的权限控制通过 `auth` 属性实现:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { renderIcon } from '@/utils';
|
||||||
|
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 200,
|
width: 200,
|
||||||
label: '操作',
|
title: '操作',
|
||||||
|
align: 'center',
|
||||||
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:position:update'], // 权限节点
|
auth: ['sys:position:update'], // 权限节点
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:position:delete'], // 权限节点
|
auth: ['sys:position:delete'], // 权限节点
|
||||||
},
|
},
|
||||||
@@ -202,7 +210,7 @@ class PositionAddHandler(BaseHandler):
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- 前端 -->
|
<!-- 前端 -->
|
||||||
<el-button v-perm="['sys:position:add']">添加岗位</el-button>
|
<n-button v-perm="['sys:position:add']">添加岗位</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 超级管理员
|
## 超级管理员
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 前端环境与结构
|
# 前端环境与结构
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
前端基于 Vue3 + ElementPlus 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
前端基于 Vue3 + NaiveUI 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
| 技术 | 版本 | 说明 |
|
| 技术 | 版本 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API |
|
| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API |
|
||||||
| ElementPlus | 2.x | Vue3 组件库 |
|
| NaiveUI | 2.x | Vue3 组件库 |
|
||||||
| Vite | 3.x | 下一代前端构建工具 |
|
| Vite | 3.x | 下一代前端构建工具 |
|
||||||
| Pinia | 2.x | 状态管理(Vuex 替代方案) |
|
| Pinia | 2.x | 状态管理(Vuex 替代方案) |
|
||||||
| Vue Router | 4.x | 路由管理 |
|
| Vue Router | 4.x | 路由管理 |
|
||||||
@@ -140,24 +140,16 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import './styles/tailwindbase.css';
|
import './styles/tailwindbase.css';
|
||||||
import 'element-plus/dist/index.css';
|
|
||||||
import './styles/index.scss';
|
import './styles/index.scss';
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router, { setupRouter } from './router';
|
import router, { setupRouter } from './router';
|
||||||
import { setupStore } from '@/store';
|
import { setupStore } from '@/store';
|
||||||
import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins';
|
import { setupDirectives, setupCustomComponents } from '@/plugins';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// 注册所有 ElementPlus 图标
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
||||||
app.component(key, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupElement(app); // 全局引入 ElementPlus 组件
|
|
||||||
setupCustomComponents(app); // 注册自定义组件
|
setupCustomComponents(app); // 注册自定义组件
|
||||||
setupDirectives(app); // 注册自定义指令(v-perm 等)
|
setupDirectives(app); // 注册自定义指令(v-perm 等)
|
||||||
setupStore(app); // 挂载 Pinia 状态管理
|
setupStore(app); // 挂载 Pinia 状态管理
|
||||||
@@ -170,7 +162,7 @@ async function bootstrap() {
|
|||||||
void bootstrap();
|
void bootstrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
初始化顺序:样式加载 -> ElementPlus -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
|
初始化顺序:样式加载 -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。NaiveUI 组件按需引入,无需全局注册。
|
||||||
|
|
||||||
## 核心目录说明
|
## 核心目录说明
|
||||||
|
|
||||||
@@ -180,7 +172,7 @@ API 接口定义目录,按模块组织文件。公共接口在 `api/common/`
|
|||||||
|
|
||||||
### components/
|
### components/
|
||||||
|
|
||||||
公共组件目录,基于 ElementPlus 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
|
公共组件目录,基于 NaiveUI 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
|
||||||
|
|
||||||
### directives/
|
### directives/
|
||||||
|
|
||||||
@@ -212,4 +204,4 @@ Pinia 状态管理目录,核心模块:
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
前端项目基于 Vue3 + ElementPlus + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
|
前端项目基于 Vue3 + NaiveUI + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。NaiveUI 组件按需引入,无需全局注册。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 样式与主题
|
# 样式与主题
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
前端样式方案采用 Tailwind CSS + SCSS + ElementPlus 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
前端样式方案采用 Tailwind CSS + SCSS + NaiveUI 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 样式架构
|
## 样式架构
|
||||||
@@ -11,7 +11,7 @@ src/styles/
|
|||||||
├── var.scss # SCSS 全局变量
|
├── var.scss # SCSS 全局变量
|
||||||
├── index.scss # 入口文件,引入所有样式
|
├── index.scss # 入口文件,引入所有样式
|
||||||
├── tailwindbase.css # Tailwind CSS 基础样式
|
├── tailwindbase.css # Tailwind CSS 基础样式
|
||||||
└── element-override.css # ElementPlus 主题覆盖(可选)
|
└── naive-override.css # NaiveUI 主题覆盖(可选)
|
||||||
```
|
```
|
||||||
|
|
||||||
## SCSS 全局变量
|
## SCSS 全局变量
|
||||||
@@ -56,36 +56,22 @@ import './styles/tailwindbase.css';
|
|||||||
<div class="w-full md:w-1/2 lg:w-1/3">响应式宽度</div>
|
<div class="w-full md:w-1/2 lg:w-1/3">响应式宽度</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## ElementPlus 主题
|
## NaiveUI 主题
|
||||||
|
|
||||||
ElementPlus 通过 CSS 变量支持主题定制:
|
NaiveUI 通过 CSS 变量和 `themeOverrides` 支持主题定制:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// main.ts
|
// NaiveUI 按需引入,无需全局引入
|
||||||
import 'element-plus/dist/index.css';
|
// 在组件中直接使用 <n-config-provider :theme-overrides="themeOverrides">
|
||||||
import 'element-plus/theme-chalk/display.css';
|
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css'; // 深色模式 CSS 变量
|
|
||||||
```
|
|
||||||
|
|
||||||
### ElementPlus 全局引入
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/plugins/setupElement.ts
|
|
||||||
import ElementPlus from 'element-plus';
|
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
|
||||||
|
|
||||||
export function setupElement(app) {
|
|
||||||
app.use(ElementPlus, { locale: zhCn, size: 'default' });
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 深色模式
|
## 深色模式
|
||||||
|
|
||||||
通过 ElementPlus 的 `dark/css-vars.css` 和 Tailwind 的 `dark` 模式实现深色主题:
|
通过 NaiveUI 的暗色主题和 Tailwind 的 `dark` 模式实现深色主题:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-switch v-model="isDark" @change="toggleDark" />
|
<n-switch v-model:value="isDark" @update:value="toggleDark" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -100,9 +86,9 @@ const toggleDark = (val) => {
|
|||||||
|
|
||||||
```css
|
```css
|
||||||
.dark {
|
.dark {
|
||||||
--el-bg-color: #1d1e1f;
|
--n-color: #1d1e1f;
|
||||||
--el-text-color-primary: #e5eaf3;
|
--n-text-color: #e5eaf3;
|
||||||
--el-border-color: #4c4d4f;
|
--n-border-color: #4c4d4f;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,14 +115,14 @@ export function useDesignSetting() {
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<PageWrapper>
|
<div>
|
||||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
</el-card>
|
</n-card>
|
||||||
<el-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<!-- 表格区域 -->
|
<!-- 表格区域 -->
|
||||||
</el-card>
|
</n-card>
|
||||||
</PageWrapper>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -150,4 +136,4 @@ export function useDesignSetting() {
|
|||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ ElementPlus 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ NaiveUI 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# BasicTable 表格
|
# BasicTable 表格
|
||||||
|
|
||||||
:::tip 说明
|
:::tip 说明
|
||||||
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 ElementPlus 的 `el-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 NaiveUI 的 `n-data-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 组件架构
|
## 组件架构
|
||||||
@@ -33,12 +33,18 @@ src/components/Table/
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:request="loadDataTable"
|
:request="loadDataTable"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
ref="tableRef"
|
ref="basicTableRef"
|
||||||
:actionColumn="actionColumn"
|
:actionColumn="actionColumn"
|
||||||
@selection-change="onSelectionChange"
|
@update:checked-row-keys="onCheckedRow"
|
||||||
|
:autoScrollX="true"
|
||||||
>
|
>
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">添加岗位</el-button>
|
<n-space>
|
||||||
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||||
|
<template #icon><n-icon><PlusOutlined /></n-icon></template>
|
||||||
|
新建
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
```
|
```
|
||||||
@@ -51,27 +57,31 @@ src/components/Table/
|
|||||||
// src/views/system/position/columns.ts
|
// src/views/system/position/columns.ts
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { ElTag } from 'element-plus';
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{ type: 'selection' },
|
{ type: 'selection', width: 50, fixed: 'left' },
|
||||||
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
|
{ title: 'ID', key: 'id', fixed: 'left', width: 50 },
|
||||||
{ label: '岗位名称', prop: 'name', minWidth: 100 },
|
{ title: '岗位名称', key: 'name', width: 100 },
|
||||||
{
|
{
|
||||||
label: '岗位状态',
|
title: '岗位状态',
|
||||||
prop: 'status',
|
key: 'status',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(
|
return h(
|
||||||
ElTag,
|
NTag,
|
||||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
{
|
||||||
{ default: () => (record.row.status == 1 ? '正常' : '停用') },
|
style: { marginRight: '6px' },
|
||||||
|
bordered: false,
|
||||||
|
type: record.status == 1 ? 'success' : 'error',
|
||||||
|
},
|
||||||
|
{ default: () => (record.status == 1 ? '正常' : '停用') },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: '排序', prop: 'sort', minWidth: 100 },
|
{ title: '排序', key: 'sort', width: 100 },
|
||||||
{ label: '创建人', prop: 'createUser', minWidth: 100 },
|
{ title: '创建人', key: 'createUser', width: 100 },
|
||||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
{ title: '创建时间', key: 'createTime', width: 180 },
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -79,24 +89,25 @@ export const columns = [
|
|||||||
|
|
||||||
| 属性 | 类型 | 说明 |
|
| 属性 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `prop` | string | 字段名 |
|
| `key` | string | 字段名 |
|
||||||
| `label` | string | 列标题 |
|
| `title` | string | 列标题 |
|
||||||
| `width` | number | 固定宽度 |
|
| `width` | number | 固定宽度 |
|
||||||
| `minWidth` | number | 最小宽度 |
|
|
||||||
| `type` | string | 列类型:`selection` / `index` |
|
| `type` | string | 列类型:`selection` / `index` |
|
||||||
| `fixed` | string | 固定列:`left` / `right` |
|
| `fixed` | string | 固定列:`left` / `right` |
|
||||||
| `render` | function | 自定义渲染函数,接收 `{ row, column, index }` |
|
| `render` | function | 自定义渲染函数,直接接收行数据 `record` |
|
||||||
| `isSlot` | boolean | 是否使用插槽渲染 |
|
|
||||||
| `ellipsis` | boolean | 是否溢出省略 |
|
| `ellipsis` | boolean | 是否溢出省略 |
|
||||||
|
|
||||||
`render` 函数中 `record.row` 是行数据,`record.$index` 是行索引。
|
:::warning render 回调说明
|
||||||
|
NaiveUI 表格的 `render` 回调直接接收行数据对象 `record`,无需通过 `record.row` 访问。
|
||||||
|
:::
|
||||||
|
|
||||||
## 数据请求
|
## 数据请求
|
||||||
|
|
||||||
`request` 属性传入数据请求函数,组件自动管理分页和 loading:
|
`request` 属性传入数据请求函数,组件自动管理分页和 loading:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const loadDataTable = async (res: any) => {
|
const loadDataTable = async (res) => {
|
||||||
|
rowKeys.value = [];
|
||||||
const result = await getPositionList({ ...formParams, ...res });
|
const result = await getPositionList({ ...formParams, ...res });
|
||||||
return result; // 返回 { data, count } 结构
|
return result; // 返回 { data, count } 结构
|
||||||
};
|
};
|
||||||
@@ -110,27 +121,30 @@ const loadDataTable = async (res: any) => {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { TableAction } from '@/components/Table';
|
import { TableAction } from '@/components/Table';
|
||||||
|
import { renderIcon } from '@/utils';
|
||||||
|
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 200,
|
width: 200,
|
||||||
label: '操作',
|
title: '操作',
|
||||||
prop: 'action',
|
align: 'center',
|
||||||
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:position:update'], // 权限节点
|
auth: ['sys:position:update'], // 权限节点
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:position:delete'], // 权限节点
|
auth: ['sys:position:delete'], // 权限节点
|
||||||
},
|
},
|
||||||
@@ -140,30 +154,32 @@ const actionColumn = reactive({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
`TableAction` 的 `auth` 属性接受权限数组,内部通过 `usePermission` Hook 检查,无权限时自动隐藏按钮。
|
`TableAction` 的 `auth` 属性接受权限数组,内部通过 `usePermission` Hook 检查,无权限时自动隐藏按钮。图标通过 `renderIcon` 工具函数包装 `@vicons/antd` 图标组件。
|
||||||
|
|
||||||
## 多选
|
## 多选
|
||||||
|
|
||||||
通过 `selection-change` 事件获取选中行:
|
通过 `@update:checked-row-keys` 事件获取选中行:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const selectionData = ref([]);
|
const rowKeys = ref([]);
|
||||||
|
|
||||||
function onSelectionChange(value) {
|
function onCheckedRow(keys) {
|
||||||
selectionData.value = value;
|
rowKeys.value = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量删除
|
// 批量删除
|
||||||
async function handleDelete(record) {
|
async function handleDelete(record) {
|
||||||
let ids = [];
|
dialog.warning({
|
||||||
if (!record) {
|
title: '提示',
|
||||||
ids = selectionData.value.map(({ id }) => id);
|
content: '确定要删除?',
|
||||||
}
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
await confirm('确定要删除?');
|
onPositiveClick: async () => {
|
||||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
record.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
|
||||||
message('删除成功');
|
message.success('删除成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,17 +188,17 @@ async function handleDelete(record) {
|
|||||||
通过 ref 调用 `reload` 或 `reloadTable` 方法:
|
通过 ref 调用 `reload` 或 `reloadTable` 方法:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const tableRef = ref();
|
const basicTableRef = ref();
|
||||||
|
|
||||||
// 刷新到第一页
|
// 刷新到第一页
|
||||||
function reloadTable(noRefresh = '') {
|
function reloadTable(noRefresh = '') {
|
||||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 操作成功后刷新
|
// 操作成功后刷新
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
await positionAdd(data);
|
await positionAdd(data);
|
||||||
message('操作成功');
|
message.success('操作成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -216,27 +232,28 @@ BasicTable 内置右侧工具栏,支持:
|
|||||||
const formParams = reactive({ name: '', status: '' });
|
const formParams = reactive({ name: '', status: '' });
|
||||||
|
|
||||||
const [register, {}] = useForm({
|
const [register, {}] = useForm({
|
||||||
|
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||||
labelWidth: 80,
|
labelWidth: 80,
|
||||||
layout: 'horizontal',
|
|
||||||
colProps: { span: 6 } as ColProps,
|
|
||||||
submitOnReset: true,
|
|
||||||
schemas, // querySchemas.ts 中定义
|
schemas, // querySchemas.ts 中定义
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表单提交:更新查询参数,刷新表格
|
// 表单提交:更新查询参数,刷新表格
|
||||||
function handleSubmit(values: Recordable) {
|
function handleSubmit(values: Recordable) {
|
||||||
handleReset();
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
for (const key in values) { formParams[key] = values[key]; }
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单重置:清空查询参数,刷新表格
|
// 表单重置:清空查询参数,刷新表格
|
||||||
function handleReset() {
|
function handleReset(values: Recordable) {
|
||||||
for (const key in formParams) { formParams[key] = ''; }
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
|
reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据请求:合并表单参数和分页参数
|
// 数据请求:合并表单参数和分页参数
|
||||||
const loadDataTable = async (res: any) => {
|
const loadDataTable = async (res) => {
|
||||||
|
rowKeys.value = [];
|
||||||
const result = await getPositionList({ ...formParams, ...res });
|
const result = await getPositionList({ ...formParams, ...res });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -258,9 +275,9 @@ const loadDataTable = async (res: any) => {
|
|||||||
| `setLoading(loading)` | 设置 loading |
|
| `setLoading(loading)` | 设置 loading |
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
BasicTable 内部自动处理分页参数(pageNo/pageSize),调用方只需提供请求函数。操作列的权限控制通过 `TableAction` 的 `auth` 属性实现,与后端 `@permission_required` 对应。`render` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM。
|
BasicTable 内部自动处理分页参数(pageNo/pageSize),调用方只需提供请求函数。操作列的权限控制通过 `TableAction` 的 `auth` 属性实现,与后端 `@permission_required` 对应。`render` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM。NaiveUI 表格的 `render` 回调直接接收行数据,无需通过 `record.row` 访问。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列,`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。
|
BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列(使用 `title`/`key`),`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。多选事件使用 `@update:checked-row-keys`,删除确认使用 NaiveUI 的 `useDialog`。
|
||||||
|
|||||||
@@ -21,18 +21,24 @@ export function getNormalPath(path: string) {
|
|||||||
return newPath[newPath.length - 1] === '/' ? newPath.slice(0, -1) : newPath;
|
return newPath[newPath.length - 1] === '/' ? newPath.slice(0, -1) : newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认弹窗封装
|
// 确认弹窗封装(使用 NaiveUI 的 useDialog)
|
||||||
export function confirm(msg: string, type: 'warning') {
|
export function confirm(msg: string) {
|
||||||
return ElMessageBox.confirm(msg, '温馨提示', {
|
const dialog = window.$dialog;
|
||||||
confirmButtonText: '确定',
|
return new Promise((resolve, reject) => {
|
||||||
cancelButtonText: '取消',
|
dialog.warning({
|
||||||
type: type,
|
title: '温馨提示',
|
||||||
|
content: msg,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: resolve,
|
||||||
|
onNegativeClick: reject,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息提示封装
|
// 消息提示封装(使用 NaiveUI 的 useMessage)
|
||||||
export function message(msg: string, type = 'success') {
|
export function message(msg: string, type = 'success') {
|
||||||
ElMessage[type](msg);
|
window.$message[type](msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件流下载
|
// 文件流下载
|
||||||
@@ -233,9 +239,9 @@ export enum PageEnum {
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 ElementPlus 的简封装,在业务组件中直接调用即可。
|
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 NaiveUI 的简封装,在业务组件中直接调用即可。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 ElementPlus 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 NaiveUI 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ src/views/
|
|||||||
| 文件 | 职责 | 说明 |
|
| 文件 | 职责 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 |
|
| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 |
|
||||||
| `edit.vue` | 编辑弹窗 | el-dialog + el-form,懒加载 |
|
| `edit.vue` | 编辑弹窗 | basicModal + n-form,懒加载 |
|
||||||
| `columns.ts` | 表格列定义 | 定义列 prop、label、render 函数等 |
|
| `columns.ts` | 表格列定义 | 定义列 key、title、render 函数等 |
|
||||||
| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 |
|
| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 |
|
||||||
|
|
||||||
## index.vue 列表页
|
## index.vue 列表页
|
||||||
@@ -43,69 +43,83 @@ src/views/
|
|||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/index.vue -->
|
<!-- src/views/system/position/index.vue -->
|
||||||
<template>
|
<template>
|
||||||
<PageWrapper>
|
<div>
|
||||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
|
||||||
</el-card>
|
<template #statusSlot="{ model, field }">
|
||||||
<el-card :bordered="false" class="proCard">
|
<n-input v-model:value="model[field]" />
|
||||||
|
</template>
|
||||||
|
</BasicForm>
|
||||||
|
</n-card>
|
||||||
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicTable
|
<BasicTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:request="loadDataTable"
|
:request="loadDataTable"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
ref="tableRef"
|
ref="basicTableRef"
|
||||||
:actionColumn="actionColumn"
|
:actionColumn="actionColumn"
|
||||||
@selection-change="onSelectionChange"
|
@update:checked-row-keys="onCheckedRow"
|
||||||
|
:autoScrollX="true"
|
||||||
>
|
>
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
<n-space>
|
||||||
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<el-icon class="el-input__icon">
|
<n-icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</el-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
添加岗位
|
新建
|
||||||
</el-button>
|
</n-button>
|
||||||
<el-button
|
<n-button
|
||||||
type="danger"
|
type="error"
|
||||||
@click="handleDelete()"
|
@click="handleDelete"
|
||||||
:disabled="!selectionData.length"
|
:disabled="!rowKeys.length"
|
||||||
v-perm="['sys:position:batchDelete']"
|
v-perm="['sys:position:batchDelete']"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<el-icon class="el-input__icon">
|
<n-icon>
|
||||||
<Delete />
|
<DeleteOutlined />
|
||||||
</el-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</el-card>
|
</n-card>
|
||||||
|
|
||||||
<editDialog
|
<editDialog
|
||||||
v-if="editVisible"
|
ref="createModalRef"
|
||||||
:positionId="positionId"
|
:positionId="positionId"
|
||||||
|
v-if="editVisible"
|
||||||
v-model:visible="editVisible"
|
v-model:visible="editVisible"
|
||||||
@success="reloadTable('noRefresh')"
|
@success="reloadTable('noRefresh')"
|
||||||
/>
|
/>
|
||||||
</PageWrapper>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
import { h, nextTick, reactive, ref, unref } from 'vue';
|
||||||
import { ColProps } from 'element-plus';
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
import { schemas } from './querySchemas';
|
|
||||||
import { useForm } from '@/components/Form/index';
|
|
||||||
import { TableAction } from '@/components/Table';
|
import { TableAction } from '@/components/Table';
|
||||||
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { PlusOutlined } from '@vicons/antd';
|
import { PlusOutlined, DeleteOutlined, FormOutlined } from '@vicons/antd';
|
||||||
import { message, confirm } from '@/utils/auth';
|
import editDialog from './edit.vue';
|
||||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
import { schemas } from './querySchemas';
|
||||||
const positionId = ref(0);
|
import { renderIcon } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义参数
|
||||||
|
*/
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
const basicTableRef = ref();
|
||||||
|
const createModalRef = ref();
|
||||||
const editVisible = ref(false);
|
const editVisible = ref(false);
|
||||||
const selectionData = ref([]);
|
const positionId = ref(0);
|
||||||
const tableRef = ref();
|
const rowKeys = ref([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义查询参数
|
* 定义查询参数
|
||||||
@@ -120,24 +134,25 @@ src/views/
|
|||||||
*/
|
*/
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 200,
|
width: 200,
|
||||||
label: '操作',
|
title: '操作',
|
||||||
prop: 'action',
|
align: 'center',
|
||||||
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:position:update'],
|
auth: ['sys:position:update'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:position:delete'],
|
auth: ['sys:position:delete'],
|
||||||
},
|
},
|
||||||
@@ -148,37 +163,34 @@ src/views/
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载数据列表
|
* 加载数据列表
|
||||||
* @param res 参数
|
|
||||||
*/
|
*/
|
||||||
const loadDataTable = async (res: any) => {
|
const loadDataTable = async (res) => {
|
||||||
|
rowKeys.value = [];
|
||||||
const result = await getPositionList({ ...formParams, ...res });
|
const result = await getPositionList({ ...formParams, ...res });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新数据列表
|
* 数据行选择事件
|
||||||
* @param noRefresh 参数
|
|
||||||
*/
|
*/
|
||||||
function reloadTable(noRefresh = '') {
|
function onCheckedRow(keys) {
|
||||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
rowKeys.value = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册
|
* 刷新数据列表
|
||||||
*/
|
*/
|
||||||
const [register, {}] = useForm({
|
function reloadTable(noRefresh = '') {
|
||||||
labelWidth: 80,
|
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||||
layout: 'horizontal',
|
}
|
||||||
colProps: { span: 6 } as ColProps,
|
|
||||||
submitOnReset: true,
|
|
||||||
schemas,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行提交表单
|
* 执行提交表单
|
||||||
*/
|
*/
|
||||||
function handleSubmit(values: Recordable) {
|
function handleSubmit(values: Recordable) {
|
||||||
handleReset();
|
for (const key in formParams) {
|
||||||
|
formParams[key] = '';
|
||||||
|
}
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
formParams[key] = values[key];
|
formParams[key] = values[key];
|
||||||
}
|
}
|
||||||
@@ -188,56 +200,62 @@ src/views/
|
|||||||
/**
|
/**
|
||||||
* 执行重置
|
* 执行重置
|
||||||
*/
|
*/
|
||||||
function handleReset() {
|
function handleReset(values: Recordable) {
|
||||||
for (const key in formParams) {
|
for (const key in formParams) {
|
||||||
formParams[key] = '';
|
formParams[key] = '';
|
||||||
}
|
}
|
||||||
|
for (const key in values) {
|
||||||
|
formParams[key] = values[key];
|
||||||
}
|
}
|
||||||
|
reloadTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行注册
|
||||||
|
*/
|
||||||
|
const [register, {}] = useForm({
|
||||||
|
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||||
|
labelWidth: 80,
|
||||||
|
schemas,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行添加
|
* 执行添加
|
||||||
*/
|
*/
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
positionId.value = 0;
|
positionId.value = 0;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行编辑
|
* 执行编辑
|
||||||
* @param record 参数
|
|
||||||
*/
|
*/
|
||||||
const handleEdit = async (record: Recordable) => {
|
async function handleEdit(record: Recordable) {
|
||||||
positionId.value = record.row.id;
|
positionId.value = record.id;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
};
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行删除
|
* 执行删除
|
||||||
* @param record 参数
|
|
||||||
*/
|
*/
|
||||||
async function handleDelete(record: Recordable) {
|
async function handleDelete(record) {
|
||||||
let ids = [];
|
dialog.warning({
|
||||||
if (!record) {
|
title: '提示',
|
||||||
ids = selectionData.value.map(({ id }) => id);
|
content: '确定要删除?',
|
||||||
}
|
positiveText: '确定',
|
||||||
await confirm('确定要删除?');
|
negativeText: '取消',
|
||||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
onPositiveClick: async () => {
|
||||||
message('删除成功');
|
record.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
|
||||||
|
message.success('删除成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
},
|
||||||
|
});
|
||||||
/**
|
|
||||||
* 选项发生变化
|
|
||||||
* @param value 参数
|
|
||||||
*/
|
|
||||||
function onSelectionChange(value) {
|
|
||||||
selectionData.value = value;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## edit.vue 编辑弹窗
|
## edit.vue 编辑弹窗
|
||||||
@@ -245,52 +263,57 @@ src/views/
|
|||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/edit.vue -->
|
<!-- src/views/system/position/edit.vue -->
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<basicModal
|
||||||
v-model="props.visible"
|
@register="modalRegister"
|
||||||
:title="props.positionId ? '编辑' : '新增'"
|
ref="modalRef"
|
||||||
width="500"
|
class="basicModal basicFormModal"
|
||||||
:close-on-click-modal="false"
|
@on-ok="handleSubmit"
|
||||||
:before-close="dialogClose"
|
@on-close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
<template #default>
|
||||||
<el-form-item
|
<n-form
|
||||||
|
class="ls-form"
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="85px"
|
||||||
|
>
|
||||||
|
<n-form-item
|
||||||
label="岗位名称"
|
label="岗位名称"
|
||||||
prop="name"
|
path="name"
|
||||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||||
>
|
>
|
||||||
<el-input class="ls-input" v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="岗位状态" prop="status">
|
<n-form-item label="岗位状态" path="code">
|
||||||
<el-radio-group v-model="formData.status" name="status">
|
<n-radio-group v-model:value="formData.status" name="status">
|
||||||
<el-radio :value="1">正常</el-radio>
|
<n-radio :value="1">正常</n-radio>
|
||||||
<el-radio :value="2">停用</el-radio>
|
<n-radio :value="2">停用</n-radio>
|
||||||
</el-radio-group>
|
</n-radio-group>
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
<el-form-item label="排序" prop="sort">
|
<n-form-item label="排序" path="sort">
|
||||||
<el-input-number v-model="formData.sort" />
|
<n-input-number v-model:value="formData.sort" />
|
||||||
</el-form-item>
|
</n-form-item>
|
||||||
</el-form>
|
</n-form>
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="dialogClose">取消</el-button>
|
|
||||||
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</basicModal>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { FormInstance } from 'element-plus';
|
|
||||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||||
import { onMounted, reactive, shallowRef } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { message } from '@/utils/auth';
|
import { useMessage } from 'naive-ui';
|
||||||
import { useLockFn } from '@/utils/useLockFn';
|
import { useModal } from '@/components/Modal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义参数
|
||||||
|
*/
|
||||||
const emit = defineEmits(['success', 'update:visible']);
|
const emit = defineEmits(['success', 'update:visible']);
|
||||||
const formRef = shallowRef<FormInstance>();
|
const formRef = ref();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义表单参数
|
* 定义表单参数
|
||||||
*/
|
*/
|
||||||
|
const message = useMessage();
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -315,25 +338,39 @@ src/views/
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行提交表单
|
* 定义模态
|
||||||
|
*/
|
||||||
|
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||||
|
title: props.positionId ? '编辑岗位' : '添加岗位',
|
||||||
|
subBtuText: '确定',
|
||||||
|
width: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行提交
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await formRef.value?.validate();
|
formRef.value
|
||||||
|
.validate()
|
||||||
|
.then(async () => {
|
||||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||||
message('操作成功');
|
message.success('操作成功');
|
||||||
|
setSubLoading(false);
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('success');
|
emit('success');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setSubLoading(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭窗体
|
* 关闭窗体
|
||||||
*/
|
*/
|
||||||
const dialogClose = () => {
|
const handleClose = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置表单数据
|
* 设置表单数据
|
||||||
*/
|
*/
|
||||||
@@ -355,8 +392,13 @@ src/views/
|
|||||||
setFormData();
|
setFormData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义函数
|
||||||
|
*/
|
||||||
|
defineExpose({
|
||||||
|
openModal,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -365,9 +407,9 @@ src/views/
|
|||||||
```
|
```
|
||||||
页面加载 → BasicForm 注册 → BasicTable request 触发 → loadDataTable → API → 渲染表格
|
页面加载 → BasicForm 注册 → BasicTable request 触发 → loadDataTable → API → 渲染表格
|
||||||
用户搜索 → handleSubmit → 更新 formParams → reloadTable → 重新请求
|
用户搜索 → handleSubmit → 更新 formParams → reloadTable → 重新请求
|
||||||
用户新增 → handleAdd → positionId=0 → editVisible=true → edit.vue 加载 → 表单填写 → 提交
|
用户新增 → handleAdd → positionId=0 → editVisible=true → openModal() → 表单填写 → 提交
|
||||||
用户编辑 → handleEdit → positionId=id → editVisible=true → edit.vue 加载详情 → 修改 → 提交
|
用户编辑 → handleEdit → positionId=id → editVisible=true → openModal() → 加载详情 → 修改 → 提交
|
||||||
用户删除 → confirm → API → reloadTable
|
用户删除 → dialog.warning → onPositiveClick → API → reloadTable
|
||||||
```
|
```
|
||||||
|
|
||||||
## columns.ts
|
## columns.ts
|
||||||
@@ -375,52 +417,58 @@ src/views/
|
|||||||
```typescript
|
```typescript
|
||||||
// src/views/system/position/columns.ts
|
// src/views/system/position/columns.ts
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { ElTag } from 'element-plus';
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{
|
{
|
||||||
type: 'selection',
|
type: 'selection',
|
||||||
|
width: 50,
|
||||||
|
fixed: 'left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ID',
|
title: 'ID',
|
||||||
prop: 'id',
|
key: 'id',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 50,
|
width: 50,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '岗位名称',
|
title: '岗位名称',
|
||||||
prop: 'name',
|
key: 'name',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '岗位状态',
|
title: '岗位状态',
|
||||||
prop: 'status',
|
key: 'status',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(
|
return h(
|
||||||
ElTag,
|
NTag,
|
||||||
{
|
{
|
||||||
type: record.row.status == 1 ? 'success' : 'danger',
|
style: {
|
||||||
|
marginRight: '6px',
|
||||||
|
},
|
||||||
|
bordered: false,
|
||||||
|
type: record.status == 1 ? 'success' : 'error',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => (record.row.status == 1 ? '正常' : '停用'),
|
default: () => (record.status == 1 ? '正常' : '停用'),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '排序',
|
title: '排序',
|
||||||
prop: 'sort',
|
key: 'sort',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建人',
|
title: '创建人',
|
||||||
prop: 'createUser',
|
key: 'createUser',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
title: '创建时间',
|
||||||
prop: 'createTime',
|
key: 'createTime',
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -434,7 +482,7 @@ import { FormSchema } from '@/components/Form/index';
|
|||||||
export const schemas: FormSchema[] = [
|
export const schemas: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
component: 'Input',
|
component: 'NInput',
|
||||||
label: '岗位名称',
|
label: '岗位名称',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入岗位名称',
|
placeholder: '请输入岗位名称',
|
||||||
@@ -442,7 +490,7 @@ export const schemas: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
@@ -453,7 +501,7 @@ export const schemas: FormSchema[] = [
|
|||||||
value: '1',
|
value: '1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '禁用',
|
label: '停用',
|
||||||
value: '2',
|
value: '2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -463,9 +511,9 @@ export const schemas: FormSchema[] = [
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::tip 温馨提示
|
:::tip 温馨提示
|
||||||
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。
|
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。NaiveUI 的列配置使用 `title`/`key`,组件名使用 `NInput`/`NSelect` 等。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
页面视图遵循 `index.vue` + `edit.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `defineAsyncComponent` 懒加载 + `useLockFn` 防重复提交。数据流清晰:搜索更新参数 → 刷新表格,操作成功 → emit 事件 → 刷新表格。
|
页面视图遵循 `index.vue` + `edit.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态。数据流清晰:搜索更新参数 → 刷新表格,操作成功 → emit 事件 → 刷新表格。删除确认使用 NaiveUI 的 `useDialog`,消息提示使用 `useMessage`。
|
||||||
|
|||||||
@@ -94,19 +94,21 @@ transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
|||||||
|
|
||||||
// 业务失败(code === 1):弹出错误提示
|
// 业务失败(code === 1):弹出错误提示
|
||||||
if (code === ResultEnum.ERROR) {
|
if (code === ResultEnum.ERROR) {
|
||||||
ElMessage.error(msg);
|
window.$message.error(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录超时(code === 401):弹出确认框,跳转登录页
|
// 登录超时(code === 401):弹出确认框,跳转登录页
|
||||||
if (code === ResultEnum.TIMEOUT) {
|
if (code === ResultEnum.TIMEOUT) {
|
||||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', {
|
window.$dialog.warning({
|
||||||
confirmButtonText: '确定',
|
title: '提示',
|
||||||
showCancelButton: false,
|
content: '登录身份已失效,请重新登录!',
|
||||||
type: 'warning',
|
positiveText: '确定',
|
||||||
}).then(() => {
|
showNegative: false,
|
||||||
|
onPositiveClick: () => {
|
||||||
storage.clear();
|
storage.clear();
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
@@ -140,14 +142,14 @@ const result = await getPositionList(params);
|
|||||||
| code | 说明 | 前端行为 |
|
| code | 说明 | 前端行为 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| 0 | 成功 | 返回 `data` |
|
| 0 | 成功 | 返回 `data` |
|
||||||
| 1 | 业务失败 | `ElMessage.error(msg)` + `throw` |
|
| 1 | 业务失败 | `window.$message.error(msg)` + `throw` |
|
||||||
| 401 | 未登录或 Token 过期 | 弹出确认框 → 跳转登录页 |
|
| 401 | 未登录或 Token 过期 | 弹出确认框 → 跳转登录页 |
|
||||||
|
|
||||||
HTTP 状态码始终为 200,业务成功/失败通过 `code` 字段区分。
|
HTTP 状态码始终为 200,业务成功/失败通过 `code` 字段区分。
|
||||||
|
|
||||||
## 前端错误展示
|
## 前端错误展示
|
||||||
|
|
||||||
业务错误通过 `ElMessage.error` 弹出提示,无需组件手动处理:
|
业务错误通过 `window.$message.error` 弹出提示,无需组件手动处理:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 组件中直接调用,错误由拦截器处理
|
// 组件中直接调用,错误由拦截器处理
|
||||||
@@ -155,7 +157,7 @@ try {
|
|||||||
await positionAdd(formData);
|
await positionAdd(formData);
|
||||||
message('操作成功');
|
message('操作成功');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 拦截器已弹出 ElMessage.error,此处可选处理
|
// 拦截器已弹出 window.$message.error,此处可选处理
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -145,25 +145,25 @@ class ParamService(BaseService(Param]):
|
|||||||
```typescript
|
```typescript
|
||||||
// src/views/system/param/columns.ts
|
// src/views/system/param/columns.ts
|
||||||
{
|
{
|
||||||
label: '参数类型',
|
title: '参数类型',
|
||||||
prop: 'type',
|
key: 'type',
|
||||||
width: 100,
|
width: 100,
|
||||||
// 方式一:直接使用后端返回的 typeText 字段
|
// 方式一:直接使用后端返回的 typeText 字段
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(ElTag,
|
return h(NTag,
|
||||||
{ type: record.row.type == 0 ? 'primary' : 'warning' },
|
{ bordered: false, type: record.type == 0 ? 'info' : 'warning' },
|
||||||
{ default: () => record.row.typeText });
|
{ default: () => record.typeText });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
title: '状态',
|
||||||
prop: 'status',
|
key: 'status',
|
||||||
width: 100,
|
width: 100,
|
||||||
// 方式二:直接使用后端返回的 statusText 字段
|
// 方式二:直接使用后端返回的 statusText 字段
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(ElTag,
|
return h(NTag,
|
||||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
{ bordered: false, type: record.status == 1 ? 'success' : 'error' },
|
||||||
{ default: () => record.row.statusText });
|
{ default: () => record.statusText });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
@@ -309,8 +309,8 @@ const options = [
|
|||||||
// 业务字段为数字
|
// 业务字段为数字
|
||||||
formData.type = 0;
|
formData.type = 0;
|
||||||
|
|
||||||
// el-option 绑定时需要转换
|
// NSelect 的 value 类型与后端一致
|
||||||
<el-option :value="Number(item.value)" />
|
<n-select :options="options" />
|
||||||
```
|
```
|
||||||
|
|
||||||
搜索表单中的 `options` 的 `value` 为字符串 `'0'`/`'1'`,后端 `page_eq_fields` 中的 `type` 字段期望数字。前端 API 传参时自动转换。
|
搜索表单中的 `options` 的 `value` 为字符串 `'0'`/`'1'`,后端 `page_eq_fields` 中的 `type` 字段期望数字。前端 API 传参时自动转换。
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ class PositionAddHandler(BaseHandler):
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- src/views/system/position/index.vue -->
|
<!-- src/views/system/position/index.vue -->
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||||
添加岗位
|
新建
|
||||||
</el-button>
|
</n-button>
|
||||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
<n-button type="error" @click="handleDelete" :disabled="!rowKeys.length"
|
||||||
v-perm="['sys:position:batchDelete']">
|
v-perm="['sys:position:batchDelete']">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</n-button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### TableAction auth 属性
|
### TableAction auth 属性
|
||||||
@@ -65,22 +65,25 @@ class PositionAddHandler(BaseHandler):
|
|||||||
操作列通过 `TableAction` 组件的 `auth` 属性控制:
|
操作列通过 `TableAction` 组件的 `auth` 属性控制:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { renderIcon } from '@/utils';
|
||||||
|
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
const actionColumn = {
|
const actionColumn = {
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:position:update'],
|
auth: ['sys:position:update'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:position:delete'],
|
auth: ['sys:position:delete'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|:----|:----|:----|
|
|:----|:----|:----|
|
||||||
| `UPLOAD_ALLOWED_EXTS` | `.jpg,.jpeg,.png,.gif,...,.zip` | 允许上传的文件扩展名(逗号分隔,带点号) |
|
| `UPLOAD_ALLOWED_EXTS` | `.jpg,.jpeg,.png,.gif,...,.zip` | 允许上传的文件扩展名(逗号分隔,带点号) |
|
||||||
| `UPLOAD_MAX_SIZE_MB` | `10` | 上传文件大小上限(MB) |
|
| `UPLOAD_MAX_SIZE_MB` | `10` | 上传文件大小上限(MB) |
|
||||||
| `TORNADO_FILE_URL` | `http://file.tornado.elevue2` | 文件访问域名 |
|
| `TORNADO_FILE_URL` | `http://file.tornado.naivevue` | 文件访问域名 |
|
||||||
| `TORNADO_UPLOAD_DIR` | `uploads` | 上传文件存储目录 |
|
| `TORNADO_UPLOAD_DIR` | `uploads` | 上传文件存储目录 |
|
||||||
|
|
||||||
## 上传接口
|
## 上传接口
|
||||||
@@ -173,14 +173,14 @@ Service 层的 `BaseService._enrich_file_urls()` 会自动为文件字段补此
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<el-upload
|
<n-upload
|
||||||
:action="uploadUrl"
|
:action="uploadUrl"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:on-success="handleSuccess"
|
:on-success="handleSuccess"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
>
|
>
|
||||||
<el-button type="primary">上传文件</el-button>
|
<n-button type="primary">上传文件</n-button>
|
||||||
</el-upload>
|
</n-upload>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -196,7 +196,7 @@ const headers = {
|
|||||||
function beforeUpload(file) {
|
function beforeUpload(file) {
|
||||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||||
if (!isLt10M) {
|
if (!isLt10M) {
|
||||||
ElMessage.error('文件大小不能超过 10MB');
|
window.$message.error('文件大小不能超过 10MB');
|
||||||
}
|
}
|
||||||
return isLt10M;
|
return isLt10M;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,12 +187,12 @@ pip install -r requirements.txt
|
|||||||
# 干跑:核对源库表和行数
|
# 干跑:核对源库表和行数
|
||||||
python scripts/migrate_db.py --dry-run \
|
python scripts/migrate_db.py --dry-run \
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-user=root --src-pass=root --src-db=djangoadmin.tornado.elevue2
|
--src-user=root --src-pass=root --src-db=djangoadmin.tornado.naivevue
|
||||||
|
|
||||||
# 或使用 Make 命令
|
# 或使用 Make 命令
|
||||||
make db-migrate-dry-run ARGS="\
|
make db-migrate-dry-run ARGS="\
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-user=root --src-pass=root --src-db=djangoadmin.tornado.elevue2"
|
--src-user=root --src-pass=root --src-db=djangoadmin.tornado.naivevue"
|
||||||
```
|
```
|
||||||
|
|
||||||
### MySQL → PostgreSQL
|
### MySQL → PostgreSQL
|
||||||
@@ -200,9 +200,9 @@ make db-migrate-dry-run ARGS="\
|
|||||||
```bash
|
```bash
|
||||||
python scripts/migrate_db.py \
|
python scripts/migrate_db.py \
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \
|
--src-db=djangoadmin.tornado.naivevue --src-user=root --src-pass=root \
|
||||||
--dst-driver=postgresql --dst-host=localhost --dst-port=5432 \
|
--dst-driver=postgresql --dst-host=localhost --dst-port=5432 \
|
||||||
--dst-db=djangoadmin.tornado.elevue2 --dst-user=postgres --dst-pass=你的密码 \
|
--dst-db=djangoadmin.tornado.naivevue --dst-user=postgres --dst-pass=你的密码 \
|
||||||
--drop-target-first
|
--drop-target-first
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -210,12 +210,12 @@ python scripts/migrate_db.py \
|
|||||||
|
|
||||||
```
|
```
|
||||||
==============================================================
|
==============================================================
|
||||||
[迁移开始] 源: mysql:djangoadmin.tornado.elevue2 → 目标: postgresql:djangoadmin.tornado.elevue2
|
[迁移开始] 源: mysql:djangoadmin.tornado.naivevue → 目标: postgresql:djangoadmin.tornado.naivevue
|
||||||
[迁移开始] 待迁移 23 张表
|
[迁移开始] 待迁移 23 张表
|
||||||
==============================================================
|
==============================================================
|
||||||
[·] [1/5] 建目标库...
|
[·] [1/5] 建目标库...
|
||||||
· 正在连接 postgres 维护库...
|
· 正在连接 postgres 维护库...
|
||||||
· 正在创建目标库 "djangoadmin.tornado.elevue2"(UTF8)...
|
· 正在创建目标库 "djangoadmin.tornado.naivevue"(UTF8)...
|
||||||
[✓] [1/5] 建目标库 完成(耗时 947ms)
|
[✓] [1/5] 建目标库 完成(耗时 947ms)
|
||||||
[·] [2/5] 建表结构(23 张表 create_all)...
|
[·] [2/5] 建表结构(23 张表 create_all)...
|
||||||
[✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表
|
[✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表
|
||||||
@@ -248,9 +248,9 @@ python scripts/migrate_db.py \
|
|||||||
```bash
|
```bash
|
||||||
python scripts/migrate_db.py \
|
python scripts/migrate_db.py \
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \
|
--src-db=djangoadmin.tornado.naivevue --src-user=root --src-pass=root \
|
||||||
--dst-driver=mssql --dst-host=localhost --dst-port=1433 \
|
--dst-driver=mssql --dst-host=localhost --dst-port=1433 \
|
||||||
--dst-db=djangoadmin.tornado.elevue2 --dst-user=sa --dst-pass=你的密码 \
|
--dst-db=djangoadmin.tornado.naivevue --dst-user=sa --dst-pass=你的密码 \
|
||||||
--drop-target-first
|
--drop-target-first
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -262,13 +262,13 @@ python scripts/migrate_db.py \
|
|||||||
|
|
||||||
```
|
```
|
||||||
==============================================================
|
==============================================================
|
||||||
[迁移开始] 源: mysql → 目标: mssql:djangoadmin.tornado.elevue2
|
[迁移开始] 源: mysql → 目标: mssql:djangoadmin.tornado.naivevue
|
||||||
[迁移开始] 待迁移 23 张表
|
[迁移开始] 待迁移 23 张表
|
||||||
==============================================================
|
==============================================================
|
||||||
[·] [1/5] 建目标库...
|
[·] [1/5] 建目标库...
|
||||||
· 正在连接 SQL Server(master 维护库)...
|
· 正在连接 SQL Server(master 维护库)...
|
||||||
· 正在删除旧目标库 [djangoadmin.tornado.elevue2](SINGLE_USER + DROP)...
|
· 正在删除旧目标库 [djangoadmin.tornado.naivevue](SINGLE_USER + DROP)...
|
||||||
· 正在创建目标库 [djangoadmin.tornado.elevue2]...
|
· 正在创建目标库 [djangoadmin.tornado.naivevue]...
|
||||||
[✓] [1/5] 建目标库 完成(耗时 304ms)
|
[✓] [1/5] 建目标库 完成(耗时 304ms)
|
||||||
[·] [2/5] 建表结构(23 张表 create_all)...
|
[·] [2/5] 建表结构(23 张表 create_all)...
|
||||||
[✓] [2/5] 建表结构 完成(耗时 743ms),23 张表
|
[✓] [2/5] 建表结构 完成(耗时 743ms),23 张表
|
||||||
@@ -291,8 +291,8 @@ python scripts/migrate_db.py \
|
|||||||
```bash
|
```bash
|
||||||
python scripts/migrate_db.py \
|
python scripts/migrate_db.py \
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \
|
--src-db=djangoadmin.tornado.naivevue --src-user=root --src-pass=root \
|
||||||
--dst-driver=sqlite --dst-db=D:/data/djangoadmin.tornado.elevue2.db \
|
--dst-driver=sqlite --dst-db=D:/data/djangoadmin.tornado.naivevue.db \
|
||||||
--drop-target-first
|
--drop-target-first
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ python scripts/migrate_db.py \
|
|||||||
|
|
||||||
```
|
```
|
||||||
==============================================================
|
==============================================================
|
||||||
[迁移开始] 源: mysql → 目标: sqlite:D:/data/djangoadmin.tornado.elevue2.db
|
[迁移开始] 源: mysql → 目标: sqlite:D:/data/djangoadmin.tornado.naivevue.db
|
||||||
[迁移开始] 待迁移 23 张表
|
[迁移开始] 待迁移 23 张表
|
||||||
==============================================================
|
==============================================================
|
||||||
[·] [1/5] 建目标库...
|
[·] [1/5] 建目标库...
|
||||||
@@ -334,7 +334,7 @@ python scripts/migrate_db.py \
|
|||||||
```bash
|
```bash
|
||||||
python scripts/migrate_db.py \
|
python scripts/migrate_db.py \
|
||||||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||||||
--src-db=djangoadmin.tornado.elevue2 --src-user=root --src-pass=root \
|
--src-db=djangoadmin.tornado.naivevue --src-user=root --src-pass=root \
|
||||||
--dst-driver=oracle --dst-host=localhost --dst-port=1521 \
|
--dst-driver=oracle --dst-host=localhost --dst-port=1521 \
|
||||||
--dst-db=ORCLPDB1 --dst-user=tornado --dst-pass=你的密码 \
|
--dst-db=ORCLPDB1 --dst-user=tornado --dst-pass=你的密码 \
|
||||||
--drop-target-first
|
--drop-target-first
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
**数据库**:
|
**数据库**:
|
||||||
|
|
||||||
- [ ] 安装 MySQL 8.0+
|
- [ ] 安装 MySQL 8.0+
|
||||||
- [ ] 创建数据库:`CREATE DATABASE djangoadmin.tornado.elevue2 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
|
- [ ] 创建数据库:`CREATE DATABASE djangoadmin.tornado.naivevue DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`
|
||||||
- [ ] 创建数据库用户并授权
|
- [ ] 创建数据库用户并授权
|
||||||
- [ ] 修改 `.env` 中的数据库配置
|
- [ ] 修改 `.env` 中的数据库配置
|
||||||
- [ ] 初始化数据库:`python scripts/init_db.py`
|
- [ ] 初始化数据库:`python scripts/init_db.py`
|
||||||
@@ -138,7 +138,7 @@ server {
|
|||||||
|
|
||||||
### 数据库迁移
|
### 数据库迁移
|
||||||
|
|
||||||
- [ ] 备份数据库:`mysqldump -uroot -p djangoadmin.tornado.elevue2 > backup.sql`
|
- [ ] 备份数据库:`mysqldump -uroot -p djangoadmin.tornado.naivevue > backup.sql`
|
||||||
- [ ] 执行迁移:`alembic upgrade head`
|
- [ ] 执行迁移:`alembic upgrade head`
|
||||||
- [ ] 验证迁移结果
|
- [ ] 验证迁移结果
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ cp -r /opt/backup/dist /opt/project/ui/dist
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 从备份恢复
|
# 从备份恢复
|
||||||
mysql -uroot -p djangoadmin.tornado.elevue2 < backup.sql
|
mysql -uroot -p djangoadmin.tornado.naivevue < backup.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ Dockerfile 将 `requirements.txt` 和源码分两步 COPY,利用 Docker 层缓
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
tornado_elevue: # 服务名称
|
tornado_naivevue: # 服务名称
|
||||||
image: tornado_elevue # 镜像名称
|
image: tornado_naivevue # 镜像名称
|
||||||
container_name: tornado_elevue # 容器名称
|
container_name: tornado_naivevue # 容器名称
|
||||||
build: # 构建配置
|
build: # 构建配置
|
||||||
context: ./ # 构建上下文路径
|
context: ./ # 构建上下文路径
|
||||||
dockerfile: Dockerfile # Dockerfile 文件名
|
dockerfile: Dockerfile # Dockerfile 文件名
|
||||||
@@ -116,26 +116,26 @@ docker-compose build
|
|||||||
docker-compose ps
|
docker-compose ps
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
docker-compose logs -f tornado_elevue
|
docker-compose logs -f tornado_naivevue
|
||||||
|
|
||||||
# 停止服务
|
# 停止服务
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
|
||||||
# 重启服务
|
# 重启服务
|
||||||
docker-compose restart tornado_elevue
|
docker-compose restart tornado_naivevue
|
||||||
|
|
||||||
# 进入容器
|
# 进入容器
|
||||||
docker exec -it tornado_elevue bash
|
docker exec -it tornado_naivevue bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### 查看容器资源
|
### 查看容器资源
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看容器资源占用
|
# 查看容器资源占用
|
||||||
docker stats tornado_elevue
|
docker stats tornado_naivevue
|
||||||
|
|
||||||
# 查看容器日志大小
|
# 查看容器日志大小
|
||||||
docker inspect --format='{{.LogPath}}' tornado_elevue
|
docker inspect --format='{{.LogPath}}' tornado_naivevue
|
||||||
```
|
```
|
||||||
|
|
||||||
## 环境变量注入
|
## 环境变量注入
|
||||||
@@ -186,7 +186,7 @@ logging:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
tornado_elevue:
|
tornado_naivevue:
|
||||||
# ... 后端配置 ...
|
# ... 后端配置 ...
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@@ -198,7 +198,7 @@ services:
|
|||||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf # Nginx 配置文件
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf # Nginx 配置文件
|
||||||
- ./dist:/usr/share/nginx/html # 前端构建产物
|
- ./dist:/usr/share/nginx/html # 前端构建产物
|
||||||
depends_on: # 依赖服务
|
depends_on: # 依赖服务
|
||||||
- tornado_elevue # 等待后端服务启动
|
- tornado_naivevue # 等待后端服务启动
|
||||||
```
|
```
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|||||||
@@ -249,10 +249,10 @@ python -m memory_profiler app.py
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 查看容器日志
|
# 1. 查看容器日志
|
||||||
docker-compose logs tornado_elevue
|
docker-compose logs tornado_naivevue
|
||||||
|
|
||||||
# 2. 进入容器调试
|
# 2. 进入容器调试
|
||||||
docker run -it --rm tornado_elevue bash
|
docker run -it --rm tornado_naivevue bash
|
||||||
|
|
||||||
# 3. 检查 .env 文件
|
# 3. 检查 .env 文件
|
||||||
cat .env
|
cat .env
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ query = query.filter(self._soft_col() == 0)
|
|||||||
```
|
```
|
||||||
document/ # 补充文档
|
document/ # 补充文档
|
||||||
├── 数据库迁移_runbook.md # 数据库迁移操作手册
|
├── 数据库迁移_runbook.md # 数据库迁移操作手册
|
||||||
└── djangoadmin.tornado.elevue2.sql # 数据库结构 SQL
|
└── djangoadmin.tornado.naivevue.sql # 数据库结构 SQL
|
||||||
|
|
||||||
wiki/ # VitePress 文档站
|
wiki/ # VitePress 文档站
|
||||||
├── zh/ # 中文文档
|
├── zh/ # 中文文档
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
前端基于 Vue3 + ElementPlus + 自定义组件库构建。页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理,无需单独引入分页组件。
|
前端基于 Vue3 + NaiveUI + 自定义组件库构建。页面统一使用 `n-card` + `BasicForm` + `BasicTable` + `TableAction` 组件组合,分页由 `BasicTable` 内置处理,无需单独引入分页组件。
|
||||||
|
|
||||||
:::tip 技术栈
|
:::tip 技术栈
|
||||||
- Vue3(Composition API + `<script setup>`)
|
- Vue3(Composition API + `<script setup>`)
|
||||||
- ElementPlus
|
- NaiveUI
|
||||||
- 自定义组件(BasicTable / BasicForm / TableAction / PageWrapper)
|
- 自定义组件(BasicTable / BasicForm / TableAction / PageWrapper)
|
||||||
- Pinia 状态管理
|
- Pinia 状态管理
|
||||||
- Vue Router 路由管理
|
- Vue Router 路由管理
|
||||||
@@ -56,7 +56,7 @@ ui/src/
|
|||||||
├── utils/ # 工具函数
|
├── utils/ # 工具函数
|
||||||
├── hooks/ # 组合式函数
|
├── hooks/ # 组合式函数
|
||||||
├── styles/ # 全局样式
|
├── styles/ # 全局样式
|
||||||
├── plugins/ # 插件注册(ElementPlus、自定义组件、指令)
|
├── plugins/ # 插件注册(自定义组件、指令)
|
||||||
└── main.ts # 入口文件
|
└── main.ts # 入口文件
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,55 +77,60 @@ ui/src/
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<PageWrapper>
|
<div>
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||||
</el-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicTable
|
<BasicTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:request="loadDataTable"
|
:request="loadDataTable"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
ref="tableRef"
|
ref="basicTableRef"
|
||||||
:actionColumn="actionColumn"
|
:actionColumn="actionColumn"
|
||||||
@selection-change="onSelectionChange"
|
@update:checked-row-keys="onCheckedRow"
|
||||||
|
:autoScrollX="true"
|
||||||
>
|
>
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:param:add']">
|
<n-space>
|
||||||
<el-icon><PlusOutlined /></el-icon>
|
<n-button type="primary" @click="handleAdd" v-perm="['sys:param:add']">
|
||||||
添加参数
|
<template #icon><n-icon><PlusOutlined /></n-icon></template>
|
||||||
</el-button>
|
新建
|
||||||
<el-button
|
</n-button>
|
||||||
type="danger"
|
<n-button
|
||||||
@click="handleDelete()"
|
type="error"
|
||||||
:disabled="!selectionData.length"
|
@click="handleDelete"
|
||||||
|
:disabled="!rowKeys.length"
|
||||||
v-perm="['sys:param:batchDelete']"
|
v-perm="['sys:param:batchDelete']"
|
||||||
>
|
>
|
||||||
<el-icon><Delete /></el-icon>
|
<template #icon><n-icon><DeleteOutlined /></n-icon></template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</n-button>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</el-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 编辑弹窗(懒加载) -->
|
<!-- 编辑弹窗 -->
|
||||||
<editDialog
|
<editDialog
|
||||||
v-if="editVisible"
|
ref="createModalRef"
|
||||||
:paramId="paramId"
|
:paramId="paramId"
|
||||||
|
v-if="editVisible"
|
||||||
v-model:visible="editVisible"
|
v-model:visible="editVisible"
|
||||||
@success="reloadTable('noRefresh')"
|
@success="reloadTable('noRefresh')"
|
||||||
/>
|
/>
|
||||||
</PageWrapper>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning 注意
|
:::warning 注意
|
||||||
- 分页由 `BasicTable` 内置处理,无需引入 `<pagination>` 组件
|
- 分页由 `BasicTable` 内置处理,无需引入 `<pagination>` 组件
|
||||||
- `:request="loadDataTable"` 自动管理分页参数(pageNo / pageSize)和 loading 状态
|
- `:request="loadDataTable"` 自动管理分页参数(pageNo / pageSize)和 loading 状态
|
||||||
- `tableRef.value.reload({ pageNo: 1 })` 用于搜索/刷新时重置到第一页
|
- `basicTableRef.value.reload({ pageNo: 1 })` 用于搜索/刷新时重置到第一页
|
||||||
|
- 表格行选择使用 `@update:checked-row-keys` 获取选中行
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 表格列定义(columns.ts)
|
## 表格列定义(columns.ts)
|
||||||
@@ -133,37 +138,43 @@ ui/src/
|
|||||||
```typescript
|
```typescript
|
||||||
// views/data/param/columns.ts
|
// views/data/param/columns.ts
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { ElTag } from 'element-plus';
|
import { NTag } from 'naive-ui';
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{ type: 'selection' }, // 多选列
|
{ type: 'selection', width: 50, fixed: 'left' }, // 多选列
|
||||||
{ label: 'ID', prop: 'id', width: 50 }, // 固定列
|
{ title: 'ID', key: 'id', width: 50 }, // 固定列
|
||||||
{ label: '参数名称', prop: 'name', minWidth: 200 },
|
{ title: '参数名称', key: 'name', width: 200 },
|
||||||
{ label: '参数编码', prop: 'code', minWidth: 250 },
|
{ title: '参数编码', key: 'code', width: 250 },
|
||||||
{ label: '参数值', prop: 'value', minWidth: 200 },
|
{ title: '参数值', key: 'value', width: 200 },
|
||||||
{
|
{
|
||||||
label: '参数类型',
|
title: '参数类型',
|
||||||
prop: 'type',
|
key: 'type',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(ElTag,
|
return h(NTag,
|
||||||
{ type: record.row.type == 0 ? 'primary' : 'warning' },
|
{
|
||||||
{ default: () => (record.row.type == 0 ? '系统' : '业务') }
|
bordered: false,
|
||||||
|
type: record.type == 0 ? 'info' : 'warning',
|
||||||
|
},
|
||||||
|
{ default: () => (record.type == 0 ? '系统' : '业务') }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
title: '状态',
|
||||||
prop: 'status',
|
key: 'status',
|
||||||
minWidth: 100,
|
width: 100,
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(ElTag,
|
return h(NTag,
|
||||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
{
|
||||||
{ default: () => (record.row.status == 1 ? '正常' : '禁用') }
|
bordered: false,
|
||||||
|
type: record.status == 1 ? 'success' : 'error',
|
||||||
|
},
|
||||||
|
{ default: () => (record.status == 1 ? '正常' : '禁用') }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
{ title: '创建时间', key: 'createTime', width: 180 },
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,17 +183,17 @@ export const columns = [
|
|||||||
```typescript
|
```typescript
|
||||||
// 状态标签
|
// 状态标签
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(ElTag,
|
return h(NTag,
|
||||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
{ bordered: false, type: record.status == 1 ? 'success' : 'error' },
|
||||||
{ default: () => record.row.statusText } // 使用后端 serialize_maps 返回的 xxxText
|
{ default: () => record.statusText } // 使用后端 serialize_maps 返回的 xxxText
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 链接
|
// 链接
|
||||||
render(record) {
|
render(record) {
|
||||||
return h('a',
|
return h('a',
|
||||||
{ href: record.row.url, target: '_blank', class: 'text-blue-500' },
|
{ href: record.url, target: '_blank', class: 'text-blue-500' },
|
||||||
{ default: () => record.row.url }
|
{ default: () => record.url }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -196,13 +207,13 @@ import { FormSchema } from '@/components/Form/index';
|
|||||||
export const schemas: FormSchema[] = [
|
export const schemas: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
component: 'Input',
|
component: 'NInput',
|
||||||
label: '参数名称',
|
label: '参数名称',
|
||||||
componentProps: { placeholder: '请输入参数名称' },
|
componentProps: { placeholder: '请输入参数名称' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'type',
|
field: 'type',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '参数类型',
|
label: '参数类型',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择参数类型',
|
placeholder: '请选择参数类型',
|
||||||
@@ -215,7 +226,7 @@ export const schemas: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
component: 'Select',
|
component: 'NSelect',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
@@ -232,26 +243,30 @@ export const schemas: FormSchema[] = [
|
|||||||
## 操作栏(TableAction)
|
## 操作栏(TableAction)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { renderIcon } from '@/utils';
|
||||||
|
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 200,
|
width: 200,
|
||||||
label: '操作',
|
title: '操作',
|
||||||
prop: 'action',
|
align: 'center',
|
||||||
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(record) {
|
render(record) {
|
||||||
return h(TableAction, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
icon: 'Edit',
|
icon: renderIcon(FormOutlined),
|
||||||
type: 'warning',
|
type: 'info',
|
||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['sys:param:update'],
|
auth: ['sys:param:update'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '删除',
|
label: '删除',
|
||||||
icon: 'Delete',
|
icon: renderIcon(DeleteOutlined),
|
||||||
type: 'danger',
|
type: 'error',
|
||||||
onClick: handleDelete.bind(null, record),
|
onClick: handleDelete.bind(null, record),
|
||||||
auth: ['sys:param:delete'],
|
auth: ['sys:param:delete'],
|
||||||
},
|
},
|
||||||
@@ -296,84 +311,90 @@ export function paramBatchDelete(data: any) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 加载数据(BasicTable 的 :request 回调,自动传入分页参数)
|
// 加载数据(BasicTable 的 :request 回调,自动传入分页参数)
|
||||||
const loadDataTable = async (res: any) => {
|
const loadDataTable = async (res) => {
|
||||||
|
rowKeys.value = [];
|
||||||
const result = await getParamList({ ...formParams, ...res });
|
const result = await getParamList({ ...formParams, ...res });
|
||||||
return result; // 返回 { items, total } 或 R.ok(data=列表, count=总数)
|
return result; // 返回 { items, total } 或 R.ok(data=列表, count=总数)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新表格
|
// 刷新表格
|
||||||
function reloadTable(noRefresh = '') {
|
function reloadTable(noRefresh = '') {
|
||||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索提交
|
// 搜索提交
|
||||||
function handleSubmit(values: Recordable) {
|
function handleSubmit(values: Recordable) {
|
||||||
handleReset();
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
for (const key in values) {
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
formParams[key] = values[key];
|
|
||||||
}
|
|
||||||
reloadTable(); // 重置到第一页
|
reloadTable(); // 重置到第一页
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置搜索
|
// 重置搜索
|
||||||
function handleReset() {
|
function handleReset(values: Recordable) {
|
||||||
for (const key in formParams) {
|
for (const key in formParams) { formParams[key] = ''; }
|
||||||
formParams[key] = '';
|
for (const key in values) { formParams[key] = values[key]; }
|
||||||
}
|
reloadTable();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 编辑弹窗(edit.vue)
|
## 编辑弹窗(edit.vue)
|
||||||
|
|
||||||
编辑弹窗通过 `defineAsyncComponent` 懒加载,使用 `v-model:visible` 控制显示:
|
编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- index.vue 中引用 -->
|
<!-- index.vue 中引用 -->
|
||||||
<editDialog
|
<editDialog
|
||||||
v-if="editVisible"
|
ref="createModalRef"
|
||||||
:paramId="paramId"
|
:paramId="paramId"
|
||||||
|
v-if="editVisible"
|
||||||
v-model:visible="editVisible"
|
v-model:visible="editVisible"
|
||||||
@success="reloadTable('noRefresh')"
|
@success="reloadTable('noRefresh')"
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 懒加载编辑组件
|
|
||||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
|
||||||
|
|
||||||
// 新增
|
// 新增
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
paramId.value = 0;
|
paramId.value = 0;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = async (record: Recordable) => {
|
async function handleEdit(record: Recordable) {
|
||||||
paramId.value = record.row.id;
|
paramId.value = record.id;
|
||||||
await nextTick();
|
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
};
|
await nextTick();
|
||||||
|
createModalRef.value.openModal();
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 删除(单条 + 批量)
|
## 删除(单条 + 批量)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function handleDelete(record: Recordable) {
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
let ids = [];
|
|
||||||
if (!record) {
|
const message = useMessage();
|
||||||
// 批量删除:取多选行的 ID
|
const dialog = useDialog();
|
||||||
ids = selectionData.value.map(({ id }) => id);
|
|
||||||
}
|
async function handleDelete(record) {
|
||||||
await confirm('确定要删除?');
|
dialog.warning({
|
||||||
record ? await paramDelete(record.row.id) : await paramBatchDelete(ids);
|
title: '提示',
|
||||||
message('删除成功');
|
content: '确定要删除?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
record.id ? await paramDelete(record.id) : await paramBatchDelete(rowKeys.value);
|
||||||
|
message.success('删除成功');
|
||||||
reloadTable();
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多选变化
|
// 多选变化
|
||||||
function onSelectionChange(value) {
|
function onCheckedRow(keys) {
|
||||||
selectionData.value = value;
|
rowKeys.value = keys;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -384,8 +405,8 @@ function onSelectionChange(value) {
|
|||||||
| `handle` | 事件处理 | `handleAdd`、`handleEdit`、`handleDelete`、`handleSubmit`、`handleReset` |
|
| `handle` | 事件处理 | `handleAdd`、`handleEdit`、`handleDelete`、`handleSubmit`、`handleReset` |
|
||||||
| `load` | 数据加载 | `loadDataTable` |
|
| `load` | 数据加载 | `loadDataTable` |
|
||||||
| `reload` | 刷新数据 | `reloadTable` |
|
| `reload` | 刷新数据 | `reloadTable` |
|
||||||
| `on` | 组件事件回调 | `onSelectionChange`、`onSuccess`、`onError` |
|
| `on` | 组件事件回调 | `onCheckedRow`、`onSuccess`、`onError` |
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套。分页由 `BasicTable` 内置处理,无需单独引入分页组件。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。API 接口按模块分文件定义在 `api/` 目录下。
|
前端页面统一使用 `n-card` + `BasicForm` + `BasicTable` + `TableAction` 组件组合。分页由 `BasicTable` 内置处理,无需单独引入分页组件。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。API 接口按模块分文件定义在 `api/` 目录下。列配置使用 `title`/`key`,组件使用 NaiveUI 的 `n-` 前缀组件。
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ service.py → 业务逻辑层
|
|||||||
| 层级 | 技术 |
|
| 层级 | 技术 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 后端 | Tornado + SQLAlchemy + Pydantic |
|
| 后端 | Tornado + SQLAlchemy + Pydantic |
|
||||||
| 前端 | Vue3 + ElementPlus + TypeScript + Vite |
|
| 前端 | Vue3 + NaiveUI + TypeScript + Vite |
|
||||||
| 基础设施 | MySQL / PostgreSQL / Redis / Docker |
|
| 基础设施 | MySQL / PostgreSQL / Redis / Docker |
|
||||||
|
|
||||||
详细版本号和技术选型说明见 [技术选型](/zh/volume3/tech-stack)。
|
详细版本号和技术选型说明见 [技术选型](/zh/volume3/tech-stack)。
|
||||||
|
|||||||
Reference in New Issue
Block a user