签入版本
This commit is contained in:
@@ -119,7 +119,7 @@ export function exampleBatchDelete(data: any) {
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
@@ -131,16 +131,21 @@ const fetchData = async () => {
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id) => {
|
||||
await ElMessageBox.confirm('确认删除?', '提示', { type: 'warning' });
|
||||
await exampleDelete(id);
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
onOk: async () => {
|
||||
await exampleDelete(id);
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async (ids) => {
|
||||
await exampleBatchDelete({ ids });
|
||||
ElMessage.success('删除成功');
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
# 9.2 目录结构详解
|
||||
|
||||
:::tip 概述
|
||||
本节详细介绍前端项目(独立 EleVue 仓库)的目录结构,帮助开发者快速定位代码和理解项目组织方式。
|
||||
本节详细介绍前端项目(独立 AntdVue 仓库)的目录结构,帮助开发者快速定位代码和理解项目组织方式。
|
||||
:::
|
||||
|
||||
```text
|
||||
ui/src/
|
||||
├── api/ # API 接口层(按业务域分组)
|
||||
│ ├── common/ # 公共接口(登录/菜单/用户信息/城市)
|
||||
│ ├── system/ # 系统管理(用户/角色/菜单/部门/岗位/职级/日志/租户)
|
||||
│ ├── system/ # 系统管理(用户/角色/菜单/部门/岗位/职级/租户)
|
||||
│ ├── content/ # 内容管理(文章/分类/链接)
|
||||
│ ├── data/ # 数据管理(城市/配置/字典/公告/参数)
|
||||
│ ├── dashboard/ # 仪表盘(控制台/消息)
|
||||
│ ├── file/ # 文件管理(文件模板)
|
||||
│ ├── logger/ # 日志管理(登录日志/操作日志)
|
||||
│ ├── monitor/ # 监控管理(定时任务)
|
||||
│ ├── region/ # 行政区划
|
||||
│ ├── oauth2/ # OAuth2 授权
|
||||
│ ├── setting/ # 设置(个人资料/网站配置)
|
||||
│ └── tool/ # 工具(示例/代码生成器)
|
||||
├── components/ # 公共组件库(20+ 个)
|
||||
@@ -30,7 +31,10 @@ ui/src/
|
||||
│ ├── Qrcode/ # 二维码
|
||||
│ └── ...
|
||||
├── directives/ # 自定义指令
|
||||
│ └── permission.ts # v-perm / v-perms 权限指令
|
||||
│ ├── permission.ts # v-perm / v-perms 权限指令
|
||||
│ ├── clickOutside.ts # 点击元素外部检测
|
||||
│ ├── dragModal.ts # 弹窗拖拽
|
||||
│ └── scrollBar.ts # 滚动条控制
|
||||
├── enums/ # 枚举常量
|
||||
│ ├── httpEnum.ts # HTTP 枚举
|
||||
│ └── pageEnum.ts # 页面枚举
|
||||
@@ -50,11 +54,10 @@ ui/src/
|
||||
│ ├── user.ts # 用户 Store(Token/用户信息/权限)
|
||||
│ ├── asyncRoute.ts # 动态路由 Store
|
||||
│ ├── tabsView.ts # 标签页 Store
|
||||
│ ├── projectSetting.ts # 项目配置 Store
|
||||
│ ├── designSetting.ts # 主题设计 Store
|
||||
│ ├── projectSetting.ts # 项目配置 Store(导航/顶栏主题等)
|
||||
│ ├── lockscreen.ts # 锁屏 Store
|
||||
│ └── ossConfig.ts # 云存储配置 Store
|
||||
├── styles/ # 全局样式(Tailwind / 主题变量)
|
||||
│ └── index.ts # Store 入口(setupStore 挂载)
|
||||
├── styles/ # 全局样式(WindiCSS / Less 主题变量)
|
||||
├── utils/ # 工具函数
|
||||
│ ├── http/axios/ # HTTP 请求封装(拦截器)
|
||||
│ └── Storage.ts # 本地存储封装
|
||||
@@ -65,9 +68,12 @@ ui/src/
|
||||
├── content/ # 内容管理页面
|
||||
├── data/ # 数据管理页面
|
||||
├── monitor/ # 监控管理页面
|
||||
├── logger/ # 日志管理页面
|
||||
├── oauth2/ # OAuth2 授权页面
|
||||
├── file/ # 文件管理页面
|
||||
├── setting/ # 设置页面(个人资料等)
|
||||
├── tool/ # 工具页面(示例/代码生成器)
|
||||
├── about/ # 关于页面
|
||||
├── iframe/ # 内嵌 iframe 页面
|
||||
├── redirect/ # 重定向页面
|
||||
└── exception/ # 异常页面(403/404/500)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
:row-selection="{ onChange: onSelectionChange }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -36,7 +36,7 @@ const actionColumn = {
|
||||
```
|
||||
|
||||
:::tip 分页字段约定
|
||||
本项目 `BasicTable` 通过 `:request` 传入分页请求函数(没有 `useTable` hook),内部绑定后端固定分页结构 `data.records` / `data.total`。`statusText` 等 `*Text` 字段由后端 `serializeMaps` 自动补全,直接作为列 `field` 使用即可。
|
||||
本项目 `BasicTable` 通过 `:request` 传入分页请求函数(没有 `useTable` hook),内部绑定后端固定分页结构 `data.records` / `data.total`。`statusText` 等 `*Text` 字段由后端 `serializeMaps` 自动补全,直接作为列 `dataIndex` 使用即可。
|
||||
:::
|
||||
|
||||
### 列定义常用属性
|
||||
@@ -44,10 +44,11 @@ const actionColumn = {
|
||||
| 属性 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `title` | 列标题 | `'创建时间'` |
|
||||
| `field` | 字段名(camelCase,与响应一致) | `'createTime'` |
|
||||
| `dataIndex` | 数据字段名(camelCase,与响应一致) | `'createTime'` |
|
||||
| `key` | 列标识(`#bodyCell` 插槽按 `column.key` 匹配) | `'action'` |
|
||||
| `width` | 列宽 | `180` |
|
||||
| `formatter` | 自定义格式化 | `(v) => v ?? '-'` |
|
||||
| `slots` | 自定义单元格插槽 | `'statusSlot'` |
|
||||
| `customRender` | 自定义渲染函数(参数 `{ record }`) | `({ record }) => h(Tag, ...)` |
|
||||
| `slots` | 自定义单元格插槽 | `{ customRender: 'statusSlot' }` |
|
||||
|
||||
## Page 页面容器
|
||||
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
ui/
|
||||
├── .env # 所有环境通用配置(端口、标题)
|
||||
├── .env.development # 开发环境(pnpm dev,默认 mode)
|
||||
├── .env.production # 生产构建(pnpm build)
|
||||
├── .env.test # 测试构建(pnpm build:test)
|
||||
├── .env.uat # UAT 构建(pnpm build:uat)
|
||||
└── .env.dev # dev 模式构建(pnpm build:dev)
|
||||
├── .env.production # 生产构建(pnpm build / build:prod)
|
||||
└── .env.test # 测试构建(pnpm build:test)
|
||||
```
|
||||
|
||||
**加载优先级:** `.env.{mode}` > `.env`(Vite 标准加载规则,可用 `.env.{mode}.local` 做本地覆盖)
|
||||
@@ -24,7 +22,7 @@ ui/
|
||||
|
||||
```bash
|
||||
# 开发服务器端口
|
||||
VITE_PORT = 8001
|
||||
VITE_PORT = 8002
|
||||
|
||||
# 公共基础路径(通常为 /)
|
||||
VITE_PUBLIC_PATH = /
|
||||
@@ -36,7 +34,7 @@ VITE_USE_MOCK = false
|
||||
VITE_DROP_CONSOLE = true
|
||||
|
||||
# 开发代理(JSON 数组格式:[["前缀","目标地址"]])
|
||||
VITE_PROXY=[["/api","http://admin.laravel13.elevue/api"]]
|
||||
VITE_PROXY=[["/api","http://admin.laravel13.antdvue/api"]]
|
||||
|
||||
# API 接口基础路径(留空,由代理处理)
|
||||
VITE_GLOB_API_URL =
|
||||
@@ -57,7 +55,7 @@ VITE_GLOB_API_URL_PREFIX = /api
|
||||
```
|
||||
|
||||
:::tip 代理目标地址
|
||||
`VITE_PROXY` 当前默认指向 Laragon 虚拟主机 `http://admin.laravel13.elevue/api`;使用 `php artisan serve` 时改为直连 `http://localhost:8000/api`(见 [2.2 Laragon 环境搭建](/zh/quickstart/laragon.html))。修改后需重启 `pnpm dev`。
|
||||
`VITE_PROXY` 当前默认指向 Laragon 虚拟主机 `http://admin.laravel13.antdvue/api`;使用 `php artisan serve` 时改为直连 `http://admin.laravel13.antdvue/api`(见 [2.2 Laragon 环境搭建](/zh/quickstart/laragon.html))。修改后需重启 `pnpm dev`。
|
||||
:::
|
||||
|
||||
## 生产环境配置
|
||||
@@ -99,7 +97,7 @@ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||
|
||||
| 环境 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| 开发 | `8001` | 前端开发服务器端口 |
|
||||
| 开发 | `8002` | 前端开发服务器端口 |
|
||||
| 生产 | 不生效 | 生产环境由 Nginx 提供服务 |
|
||||
|
||||
### VITE_PUBLIC_PATH — 公共基础路径
|
||||
@@ -121,10 +119,10 @@ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
|
||||
|
||||
```bash
|
||||
# 当前 .env.development 中的值(Laragon 虚拟主机)
|
||||
VITE_PROXY=[["/api","http://admin.laravel13.elevue/api"]]
|
||||
VITE_PROXY=[["/api","http://admin.laravel13.antdvue/api"]]
|
||||
|
||||
# 直连 artisan serve 时的写法
|
||||
VITE_PROXY=[["/api","http://localhost:8000/api"]]
|
||||
VITE_PROXY=[["/api","http://admin.laravel13.antdvue/api"]]
|
||||
```
|
||||
|
||||
**格式:** JSON 数组,每项为 `[前缀, 目标地址]`
|
||||
@@ -134,7 +132,7 @@ VITE_PROXY=[["/api","http://localhost:8000/api"]]
|
||||
**多代理示例:**
|
||||
|
||||
```bash
|
||||
VITE_PROXY = [["/api","http://localhost:8000/api"],["/uploads","http://localhost:8000/uploads"]]
|
||||
VITE_PROXY = [["/api","http://admin.laravel13.antdvue/api"],["/uploads","http://admin.laravel13.antdvue/uploads"]]
|
||||
```
|
||||
|
||||
### VITE_GLOB_API_URL_PREFIX — API 前缀
|
||||
@@ -246,7 +244,7 @@ export function createProxy(list: ProxyList = []) {
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000/api', // 目标地址(VITE_PROXY 驱动,含 /api 前缀)
|
||||
target: 'http://admin.laravel13.antdvue/api', // 目标地址(VITE_PROXY 驱动,含 /api 前缀)
|
||||
changeOrigin: true, // 修改请求头 Origin
|
||||
ws: true, // 支持 WebSocket
|
||||
rewrite: (path) => path.replace(new RegExp('^/api'), ''), // 去掉前缀,由目标地址补回
|
||||
@@ -262,7 +260,7 @@ server: {
|
||||
│
|
||||
▼ Vite proxy
|
||||
│
|
||||
▼ 转发到: http://localhost:8000/api/user/page
|
||||
▼ 转发到: http://admin.laravel13.antdvue/api/user/page
|
||||
```
|
||||
|
||||
## 构建命令
|
||||
@@ -294,7 +292,7 @@ ui/dist/
|
||||
│ ├── index-xxx.js # 主 JS(带 hash)
|
||||
│ ├── index-xxx.css # 主 CSS(带 hash)
|
||||
│ ├── vue-xxx.js # Vue 相关(分包)
|
||||
│ └── element-xxx.js # Element Plus(分包)
|
||||
│ └── antd-xxx.js # AntDesign Vue(分包)
|
||||
├── favicon.ico
|
||||
└── static/ # 静态资源
|
||||
```
|
||||
@@ -339,22 +337,19 @@ server {
|
||||
|
||||
## 多环境配置
|
||||
|
||||
### 测试 / UAT 环境(项目实际提供)
|
||||
### 测试环境(项目实际提供)
|
||||
|
||||
```bash
|
||||
# .env.test(pnpm build:test)
|
||||
VITE_GLOB_API_URL =
|
||||
VITE_GLOB_API_URL_PREFIX = /api
|
||||
VITE_DROP_CONSOLE = true
|
||||
|
||||
# .env.uat(pnpm build:uat)同结构,按需调整
|
||||
```
|
||||
|
||||
构建命令(见 `ui/package.json` scripts):
|
||||
|
||||
```bash
|
||||
pnpm build:test # 测试环境构建
|
||||
pnpm build:uat # UAT 环境构建
|
||||
pnpm build:prod # 生产环境构建
|
||||
```
|
||||
|
||||
@@ -362,10 +357,10 @@ pnpm build:prod # 生产环境构建
|
||||
|
||||
| 变量 | 开发 | 测试 | 生产 |
|
||||
|------|------|------|------|
|
||||
| `VITE_PORT` | 8001 | - | - |
|
||||
| `VITE_PORT` | 8002 | - | - |
|
||||
| `VITE_GLOB_API_URL` | 留空 | 留空 | 留空 |
|
||||
| `VITE_GLOB_API_URL_PREFIX` | /api | /api | /api |
|
||||
| `VITE_PROXY` | `[["/api","http://admin.laravel13.elevue/api"]]` | - | - |
|
||||
| `VITE_PROXY` | `[["/api","http://admin.laravel13.antdvue/api"]]` | - | - |
|
||||
| `VITE_GLOB_IMG_URL` | 留空 | 留空 | 留空(按需配置 CDN) |
|
||||
| `VITE_DROP_CONSOLE` | true | true | true |
|
||||
| `VITE_USE_MOCK` | false | false | false |
|
||||
@@ -376,7 +371,7 @@ pnpm build:prod # 生产环境构建
|
||||
|
||||
**原因:** 代理配置错误或后端未启动。
|
||||
|
||||
**解决:** 检查 `.env.development` 中 `VITE_PROXY` 的目标地址是否正确,`curl http://localhost:8000/api/captcha` 验证后端;改完配置重启 `pnpm dev`。
|
||||
**解决:** 检查 `.env.development` 中 `VITE_PROXY` 的目标地址是否正确,`curl http://admin.laravel13.antdvue/api/captcha` 验证后端;改完配置重启 `pnpm dev`。
|
||||
|
||||
### 问题 2:生产环境 API 请求跨域
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-button @click="handleImport">导入</el-button>
|
||||
<a-button @click="handleImport">导入</a-button>
|
||||
<ImportExcel @success="onImportSuccess" />
|
||||
</template>
|
||||
|
||||
@@ -24,32 +24,38 @@ const onImportSuccess = (data) => {
|
||||
|
||||
:::tip 前端解析 vs 后端导入
|
||||
- **前端解析(本组件)**:`xlsx` 库本地解析,适合导入前预览、无业务校验的场景;
|
||||
- **后端导入(推荐)**:`el-upload` 直传 `/api/{module}/import`,由 Service 逐行 `add()`(字典反查、唯一校验、事务隔离、错误行明细),见 [7.4 数据导入导出](/zh/tools/excel.html)。
|
||||
- **后端导入(推荐)**:`a-upload` 直传 `/api/{module}/import`,由 Service 逐行 `add()`(字典反查、唯一校验、事务隔离、错误行明细),见 [7.4 数据导入导出](/zh/tools/excel.html)。
|
||||
:::
|
||||
|
||||
### 后端导入对接
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
<a-upload
|
||||
:action="'/api/example/import'"
|
||||
:headers="{ Authorization: `Bearer ${token}` }"
|
||||
:on-success="handleImportSuccess"
|
||||
:show-upload-list="false"
|
||||
accept=".xls,.xlsx"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-button type="primary">导入</el-button>
|
||||
</el-upload>
|
||||
<a-button type="primary">导入</a-button>
|
||||
</a-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const handleImportSuccess = (response) => {
|
||||
if (response.code === 0) {
|
||||
const { count, errors } = response.data;
|
||||
if (errors.length === 0) {
|
||||
ElMessage.success(`成功导入 ${count} 条数据`);
|
||||
} else {
|
||||
ElMessage.warning(`成功 ${count} 条,失败 ${errors.length} 条`);
|
||||
// errors: [{ row: 3, name: '...', reason: '...' }]
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const handleChange = ({ file }) => {
|
||||
if (file.status === 'done') {
|
||||
const response = file.response;
|
||||
if (response.code === 0) {
|
||||
const { count, errors } = response.data;
|
||||
if (errors.length === 0) {
|
||||
message.success(`成功导入 ${count} 条数据`);
|
||||
} else {
|
||||
message.warning(`成功 ${count} 条,失败 ${errors.length} 条`);
|
||||
// errors: [{ row: 3, name: '...', reason: '...' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,13 +17,13 @@ import { BasicForm, useForm } from '@/components/Form';
|
||||
const [registerForm] = useForm({
|
||||
schemas: [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
rules: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
@@ -39,7 +39,7 @@ const [registerForm] = useForm({
|
||||
```
|
||||
|
||||
:::tip 前端校验与后端 FormRequest 的关系
|
||||
`schemas` 的 `required`/校验规则是**体验层**(即时提示);后端 `Http/Requests/*.php` 的 `rules()` 才是**最终防线**。两边规则应保持一致(如 max 长度、in 枚举值),以后端为准。
|
||||
`schemas` 的 `rules` 校验规则是**体验层**(即时提示);后端 `Http/Requests/*.php` 的 `rules()` 才是**最终防线**。两边规则应保持一致(如 max 长度、in 枚举值),以后端为准。
|
||||
:::
|
||||
|
||||
## Modal 弹窗组件
|
||||
|
||||
@@ -45,11 +45,11 @@ if (code === 401 || res.status === 401) {
|
||||
|
||||
if (code === 403 || res.status === 403) {
|
||||
// 权限不足 / 租户异常 / 演示模式拦截
|
||||
ElMessage.error(msg || '无访问权限');
|
||||
message.error(msg || '无访问权限');
|
||||
}
|
||||
|
||||
// 其他业务错误,显示提示
|
||||
ElMessage.error(msg || '操作失败');
|
||||
message.error(msg || '操作失败');
|
||||
```
|
||||
|
||||
:::tip 响应处理规则
|
||||
@@ -66,12 +66,17 @@ ElMessage.error(msg || '操作失败');
|
||||
// code === 401(ResultEnum.TIMEOUT)
|
||||
case ResultEnum.TIMEOUT:
|
||||
if (router.currentRoute.value?.name === PageEnum.BASE_LOGIN_NAME) return;
|
||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
Modal.warning({
|
||||
title: '提示',
|
||||
content: '登录身份已失效,请重新登录!',
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
okText: '确定',
|
||||
onOk: () => {
|
||||
storage.clear();
|
||||
window.location.href = PageEnum.BASE_LOGIN; // '/login'
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
break;
|
||||
```
|
||||
|
||||
|
||||
@@ -19,19 +19,22 @@
|
||||
```vue
|
||||
<!-- src/views/login/index.vue -->
|
||||
<template>
|
||||
<el-form :model="loginForm" @submit.prevent="handleLogin">
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.username" placeholder="用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="密码" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.code" placeholder="验证码" />
|
||||
<img :src="captchaImage" @click="refreshCaptcha" style="cursor: pointer" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="handleLogin">登录</el-button>
|
||||
</el-form>
|
||||
<a-form :model="loginForm">
|
||||
<a-form-item name="username">
|
||||
<a-input v-model:value="loginForm.username" placeholder="用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password v-model:value="loginForm.password" placeholder="密码" />
|
||||
</a-form-item>
|
||||
<a-form-item name="code">
|
||||
<a-input v-model:value="loginForm.code" placeholder="验证码">
|
||||
<template #suffix>
|
||||
<img :src="captchaImage" @click="refreshCaptcha" style="cursor: pointer" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-button type="primary" block @click="handleLogin">登录</a-button>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -70,6 +73,8 @@ onMounted(() => refreshCaptcha());
|
||||
## 完整登录示例(含失败处理)
|
||||
|
||||
```typescript
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
@@ -78,11 +83,11 @@ const handleLogin = async () => {
|
||||
|
||||
try {
|
||||
await userStore.login(loginForm.value);
|
||||
ElMessage.success('登录成功');
|
||||
message.success('登录成功');
|
||||
router.push('/');
|
||||
} catch (e: any) {
|
||||
// 登录失败(验证码错误/密码错误等),刷新验证码
|
||||
ElMessage.error(e.message || '登录失败');
|
||||
message.error(e.message || '登录失败');
|
||||
refreshCaptcha();
|
||||
loginForm.value.code = '';
|
||||
} finally {
|
||||
|
||||
@@ -97,77 +97,83 @@ export function exampleBatchDelete(data: any) {
|
||||
```typescript
|
||||
// ui/src/views/tool/example/columns.ts
|
||||
import { h } from 'vue';
|
||||
import { ElAvatar, ElTag } from 'element-plus';
|
||||
import { Tag, Avatar } from 'ant-design-vue';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
type: 'selection', // 多选框列
|
||||
},
|
||||
{
|
||||
label: 'ID',
|
||||
prop: 'id',
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
fixed: 'left',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
label: '名称',
|
||||
prop: 'name',
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'statusText',
|
||||
title: '状态',
|
||||
dataIndex: 'statusText',
|
||||
minWidth: 100,
|
||||
// 选项标签渲染:按值映射文字与颜色
|
||||
render(record) {
|
||||
customRender({ record }) {
|
||||
const statusMap = {
|
||||
2: { text: '停用', type: 'danger' },
|
||||
1: { text: '在用', type: 'success' },
|
||||
2: { text: '停用', color: 'error' },
|
||||
1: { text: '在用', color: 'success' },
|
||||
};
|
||||
const status = statusMap[record.row.status];
|
||||
return h(ElTag, { type: status?.type || 'info' }, {
|
||||
default: () => status?.text || record.row.statusText,
|
||||
const status = statusMap[record.status];
|
||||
return h(Tag, { color: status?.color || 'default' }, {
|
||||
default: () => status?.text || record.statusText,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '头像',
|
||||
prop: 'avatar',
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
minWidth: 80,
|
||||
// 图片渲染:以方形头像形式展示
|
||||
render(record) {
|
||||
return h(ElAvatar, {
|
||||
customRender({ record }) {
|
||||
return h(Avatar, {
|
||||
size: 48,
|
||||
src: record.row.avatar,
|
||||
src: record.avatar,
|
||||
shape: 'square',
|
||||
fit: 'fill',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUser',
|
||||
title: '创建人',
|
||||
dataIndex: 'createUser',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
:::tip 多选列
|
||||
AntDesign 表格的多选框列不在 `columns` 中定义,而是通过表格的 `:row-selection` 属性开启(见步骤四)。
|
||||
:::
|
||||
|
||||
### 列配置字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | string | 列类型:`selection`(多选框)/ `index`(序号) |
|
||||
| `label` | string | 表头标题 |
|
||||
| `prop` | string | 数据字段名(camelCase,与后端响应一致) |
|
||||
| `title` | string | 表头标题 |
|
||||
| `dataIndex` | string | 数据字段名(camelCase,与后端响应一致) |
|
||||
| `key` | string | 列标识(`#bodyCell` 插槽按 `column.key` 匹配) |
|
||||
| `fixed` | string | 固定列:`left` / `right` |
|
||||
| `width` | number | 固定宽度 |
|
||||
| `minWidth` | number | 最小宽度(自适应) |
|
||||
| `render` | function | 自定义渲染函数 |
|
||||
| `customRender` | function | 自定义渲染函数(参数 `{ record }`) |
|
||||
|
||||
:::tip 状态值对齐
|
||||
`statusMap` 的值域必须与数据库/字典一致(如 example:1=在用 2=停用)。推荐直接使用后端补全的 `statusText` 字段,仅颜色按值映射。
|
||||
@@ -181,21 +187,21 @@ import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'name',
|
||||
component: 'Input',
|
||||
label: '名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: 'status',
|
||||
component: 'Select',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '在用', value: 1 },
|
||||
{ label: '停用', value: 2 },
|
||||
@@ -215,7 +221,7 @@ export const schemas: FormSchema[] = [
|
||||
| `DatePicker` | 日期字段 | 日期选择器 |
|
||||
|
||||
:::tip 查询字段须与后端配置对应
|
||||
`field` 必须是后端 Service `pageLikeFields`(模糊)或 `pageEqFields`(精确)中声明的字段,否则查询条件不生效(见 [8.3 分页查询规范](/zh/api-spec/pagination.html))。下拉选项也可用 `getDictItemByCode(code)` 从字典动态加载。
|
||||
`name` 必须是后端 Service `pageLikeFields`(模糊)或 `pageEqFields`(精确)中声明的字段,否则查询条件不生效(见 [8.3 分页查询规范](/zh/api-spec/pagination.html))。下拉选项也可用 `getDictItemByCode(code)` 从字典动态加载。
|
||||
:::
|
||||
|
||||
## 步骤四:创建列表页
|
||||
@@ -228,42 +234,42 @@ export const schemas: FormSchema[] = [
|
||||
<SearchForm :schemas="querySchemas" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row :gutter="10" style="margin-bottom: 10px;">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-perm="['sys:example:add']" type="primary" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-perm="['sys:example:batchDelete']" type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<a-space style="margin-bottom: 10px;">
|
||||
<a-button v-perm="['sys:example:add']" type="primary" @click="handleAdd">新增</a-button>
|
||||
<a-button v-perm="['sys:example:batchDelete']" danger :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange" v-loading="loading">
|
||||
<el-table-column v-for="col in columns" v-bind="col" :key="col.prop || col.type">
|
||||
<template v-if="col.render" #default="scope">
|
||||
<component :is="() => col.render(scope)" />
|
||||
<a-spin :spinning="loading">
|
||||
<a-table
|
||||
:data-source="tableData"
|
||||
:columns="columns"
|
||||
:row-selection="{ selectedRowKeys: selectedIds, onChange: handleSelectionChange }"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button v-perm="['sys:example:update']" type="link" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button v-perm="['sys:example:detail']" type="link" @click="handleDetail(record)">详情</a-button>
|
||||
<a-button v-perm="['sys:example:delete']" type="link" danger @click="handleDelete(record.id)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perm="['sys:example:update']" type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-perm="['sys:example:detail']" type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
<el-button v-perm="['sys:example:delete']" type="danger" link @click="handleDelete(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pageNo"
|
||||
<a-pagination
|
||||
v-model:current="pageNo"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:show-size-changer="true"
|
||||
:page-size-options="[10, 20, 50, 100]"
|
||||
:show-total="(t) => `共 ${t} 条`"
|
||||
style="margin-top: 10px; text-align: right;"
|
||||
@change="fetchData"
|
||||
style="margin-top: 10px; justify-content: flex-end;"
|
||||
/>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
@@ -278,7 +284,7 @@ export const schemas: FormSchema[] = [
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
import { columns } from './columns';
|
||||
import { schemas as querySchemas } from './querySchemas';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import EditDialog from './edit.vue';
|
||||
import DetailDialog from './detail.vue';
|
||||
|
||||
@@ -336,24 +342,34 @@ const handleDetail = (row) => {
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id) => {
|
||||
await ElMessageBox.confirm('确认删除该记录?', '提示', { type: 'warning' });
|
||||
await exampleDelete(id);
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
const handleDelete = (id) => {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认删除该记录?',
|
||||
onOk: async () => {
|
||||
await exampleDelete(id);
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除(DELETE,请求体 { ids: [...] })
|
||||
const handleBatchDelete = async () => {
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录?`, '提示', { type: 'warning' });
|
||||
await exampleBatchDelete({ ids: selectedIds.value });
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
const handleBatchDelete = () => {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确认删除选中的 ${selectedIds.value.length} 条记录?`,
|
||||
onOk: async () => {
|
||||
await exampleBatchDelete({ ids: selectedIds.value });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 多选变化
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedIds.value = rows.map(row => row.id);
|
||||
// 多选变化(row-selection 的 onChange 返回选中行的 row-key 数组)
|
||||
const handleSelectionChange = (keys) => {
|
||||
selectedIds.value = keys;
|
||||
};
|
||||
|
||||
onMounted(() => fetchData());
|
||||
@@ -365,37 +381,39 @@ onMounted(() => fetchData());
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/edit.vue -->
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="formData.id ? '编辑' : '新增'" width="500px">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择类型">
|
||||
<el-option v-for="item in typeOptions" :key="item.value" :label="item.name" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">在用</el-radio>
|
||||
<el-radio :value="2">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<a-modal v-model:visible="visible" :title="formData.id ? '编辑' : '新增'" width="500px">
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" :label-col="{ style: { width: '80px' } }">
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择类型" style="width: 100%">
|
||||
<a-select-option v-for="item in typeOptions" :key="item.value" :value="item.value">
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">在用</a-radio>
|
||||
<a-radio :value="2">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" :min="0" :max="9999" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||
<a-button @click="visible = false">取消</a-button>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSubmit">确定</a-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
|
||||
import { getDictItemByCode } from '@/api/common/index';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
@@ -413,8 +431,8 @@ const formData = ref({
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
name: { required: true, message: '请输入名称', trigger: 'blur' },
|
||||
type: { required: true, message: '请选择类型', trigger: 'change' },
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
@@ -439,10 +457,10 @@ const handleSubmit = async () => {
|
||||
try {
|
||||
if (formData.value.id) {
|
||||
await exampleUpdate(formData.value);
|
||||
ElMessage.success('修改成功');
|
||||
message.success('修改成功');
|
||||
} else {
|
||||
await exampleAdd(formData.value);
|
||||
ElMessage.success('新增成功');
|
||||
message.success('新增成功');
|
||||
}
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
@@ -464,43 +482,43 @@ defineExpose({ open });
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/detail.vue -->
|
||||
<template>
|
||||
<el-dialog v-model="props.visible" title="案例详情" width="750" :close-on-click-modal="false">
|
||||
<el-descriptions column="2" border>
|
||||
<el-descriptions-item label="名称:">
|
||||
<a-modal v-model:visible="props.visible" title="案例详情" width="750px" :mask-closable="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="名称">
|
||||
{{ formData.name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型:">
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="类型">
|
||||
{{ formData.typeText || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态:">
|
||||
<el-tag :type="formData.status === 1 ? 'success' : 'danger'">
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="formData.status === 1 ? 'success' : 'error'">
|
||||
{{ formData.statusText || '-' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序:">
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="排序">
|
||||
{{ formData.sort ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="头像:">
|
||||
<el-image
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="头像">
|
||||
<a-image
|
||||
v-if="formData.avatar"
|
||||
:src="formData.avatar"
|
||||
:preview-src-list="[formData.avatar]"
|
||||
fit="cover"
|
||||
style="width: 80px; height: 80px"
|
||||
:width="80"
|
||||
:height="80"
|
||||
style="object-fit: cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人:">
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">
|
||||
{{ formData.createUser || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间:">
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formData.createTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:visible', false)">关闭</el-button>
|
||||
<a-button @click="emit('update:visible', false)">关闭</a-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 9.1 前端工程概述
|
||||
|
||||
:::tip 概述
|
||||
前端基于 Vue 3 + TypeScript + Vite + ElementPlus 构建,采用 Pinia 状态管理,支持动态菜单和权限控制。通过 Axios 封装统一处理请求/响应/错误,通过 `v-perm` 指令实现按钮级权限控制。前端为本仓库 **`ui/` 目录**内的 EleVue 管理后台,与本后端通过 API 对接。
|
||||
前端基于 Vue 3 + TypeScript + Vite + AntDesign 构建,采用 Pinia 状态管理,支持动态菜单和权限控制。通过 Axios 封装统一处理请求/响应/错误,通过 `v-perm` 指令实现按钮级权限控制。前端为本仓库 **`ui/` 目录**内的 AntdVue 管理后台,与本后端通过 API 对接。
|
||||
:::
|
||||
|
||||
## 技术栈
|
||||
@@ -10,7 +10,7 @@
|
||||
|------|------|------|------|
|
||||
| 框架 | Vue | 3.4+ | 核心 UI 框架 |
|
||||
| 语言 | TypeScript | 4.9+(实际 ^4.9.5) | 类型安全 |
|
||||
| UI 库 | ElementPlus | 2.7+ | 组件库 |
|
||||
| UI 库 | AntDesign Vue | 3.2+(实际 3.2.20) | 组件库 |
|
||||
| 构建 | Vite | 3.2+(实际 ^3.2.10) | 开发服务器与构建 |
|
||||
| 状态 | Pinia | 2.1+ | 全局状态管理 |
|
||||
| 路由 | Vue Router | 4.3+ | 路由管理 |
|
||||
@@ -36,7 +36,7 @@
|
||||
`v-perm` / `v-perms` 指令控制按钮级权限:
|
||||
|
||||
```vue
|
||||
<el-button v-perm="['sys:user:add']">新增</el-button>
|
||||
<a-button v-perm="['sys:user:add']">新增</a-button>
|
||||
```
|
||||
|
||||
### Token 管理
|
||||
@@ -96,7 +96,7 @@ ui/src/
|
||||
|
||||
```bash
|
||||
pnpm install # 安装依赖(或 npm install)
|
||||
pnpm dev # 启动开发服务器(默认 8001)
|
||||
pnpm dev # 启动开发服务器(默认 8002)
|
||||
pnpm build # 构建生产版本
|
||||
pnpm preview # 预览构建结果
|
||||
pnpm lint:eslint # ESLint 检查
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
```vue
|
||||
<template>
|
||||
<!-- 只有拥有 sys:user:add 权限的用户才能看到此按钮 -->
|
||||
<el-button v-perm="['sys:user:add']" type="primary">新增</el-button>
|
||||
<a-button v-perm="['sys:user:add']" type="primary">新增</a-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
```vue
|
||||
<template>
|
||||
<!-- 拥有 sys:user:add 或 sys:user:update 任一权限即可 -->
|
||||
<el-button v-perms="['sys:user:add', 'sys:user:update']">操作</el-button>
|
||||
<a-button v-perms="['sys:user:add', 'sys:user:update']">操作</a-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
|-------|------|------|
|
||||
| userStore | `store/modules/user.ts` | Token / 用户信息 / 权限列表 / 登录登出 |
|
||||
| asyncRouteStore | `store/modules/asyncRoute.ts` | 动态路由 / keep-alive 缓存 |
|
||||
| projectSettingStore | `store/modules/projectSetting.ts` | 项目配置 |
|
||||
| ossConfigStore | `store/modules/ossConfig.ts` | 云存储配置 |
|
||||
| designSettingStore | `store/modules/designSetting.ts` | 主题设计 |
|
||||
| projectSettingStore | `store/modules/projectSetting.ts` | 项目配置(导航/顶栏主题等) |
|
||||
| lockscreenStore | `store/modules/lockscreen.ts` | 锁屏 |
|
||||
| tabsViewStore | `store/modules/tabsView.ts` | 标签页 |
|
||||
|
||||
|
||||
@@ -1,115 +1,151 @@
|
||||
# 9.14 样式与主题定制
|
||||
|
||||
:::tip 概述
|
||||
前端使用 TailwindCSS + SCSS 实现样式管理,支持主题色切换和暗色模式。
|
||||
前端使用 WindiCSS + Less 实现样式管理,通过主题预处理器支持默认/暗色双主题切换。
|
||||
:::
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 用途 |
|
||||
|------|------|
|
||||
| TailwindCSS | 原子化 CSS 框架 |
|
||||
| SCSS | 全局样式变量和主题 |
|
||||
| ElementPlus 主题变量 | 组件样式覆盖 |
|
||||
| WindiCSS | 原子化 CSS 框架(Tailwind 语法兼容) |
|
||||
| Less | 全局样式变量和主题 |
|
||||
| AntDesign Vue 主题变量 | 组件样式覆盖(antd.less 变量) |
|
||||
|
||||
## 全局样式文件
|
||||
|
||||
```text
|
||||
ui/src/styles/
|
||||
├── index.scss # 全局样式入口(@import 所有子模块)
|
||||
├── var.scss # SCSS 变量定义(颜色、尺寸等)
|
||||
├── common.scss # 公共样式类
|
||||
├── tailwind.css # TailwindCSS 入口
|
||||
├── tailwindbase.css # TailwindCSS 基础重置
|
||||
├── index.less # 全局样式入口(@import 过渡动画与公共样式)
|
||||
├── color.less # 颜色变量定义(内容区背景、基础色)
|
||||
├── common.less # 公共样式类(页面容器、滚动条、字体)
|
||||
├── config.less # 配置样式(@import 引用 color.less)
|
||||
├── tailwind.css # Tailwind/Windi 兼容入口(可选引入)
|
||||
├── theme/
|
||||
│ ├── light.scss # 浅色主题(含 ElementPlus 组件覆盖)
|
||||
│ └── dark.scss # 深色主题
|
||||
└── transition/
|
||||
└── index.scss # 过渡动画
|
||||
│ ├── theme-default.less # 默认主题(引入 antd 默认主题变量)
|
||||
│ └── theme-dark.less # 暗色主题(引入 antd 暗色主题变量)
|
||||
└── transition/ # 过渡动画(index/base/fade/scale/scroll/slide/zoom)
|
||||
```
|
||||
|
||||
## SCSS 变量
|
||||
## 主题文件
|
||||
|
||||
```scss
|
||||
/* ui/src/styles/var.scss */
|
||||
$primaryColor: #165DFF;
|
||||
$primaryColorHover: #57a3f3;
|
||||
$header-height: 64px;
|
||||
$footer-height: 70px;
|
||||
主题文件直接引入 ant-design-vue 官方主题,通过 `@zougt/vite-plugin-theme-preprocessor` 编译为多套作用域变量:
|
||||
|
||||
```less
|
||||
/* ui/src/styles/theme/theme-default.less */
|
||||
@import "ant-design-vue/lib/style/themes/default.less";
|
||||
```
|
||||
|
||||
## 主题色切换
|
||||
```less
|
||||
/* ui/src/styles/theme/theme-dark.less */
|
||||
@import "ant-design-vue/lib/style/themes/dark.less";
|
||||
```
|
||||
|
||||
通过 `designSettingStore` 管理主题:
|
||||
构建时的主题作用域配置(`build/vite/plugin/theme.ts`):
|
||||
|
||||
```typescript
|
||||
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||
|
||||
const designStore = useDesignSettingStore();
|
||||
|
||||
// 切换暗色模式
|
||||
designStore.setDarkMode(true);
|
||||
|
||||
// 获取当前主题色
|
||||
const primaryColor = designStore.appTheme;
|
||||
themePreprocessorPlugin({
|
||||
less: {
|
||||
multipleScopeVars: [
|
||||
{ scopeName: 'theme-default', path: path.resolve('src/styles/theme/theme-default.less') },
|
||||
{ scopeName: 'theme-dark', path: path.resolve('src/styles/theme/theme-dark.less') },
|
||||
],
|
||||
extract: false, // 不抽取独立主题 CSS 文件
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## ElementPlus 主题覆盖
|
||||
:::tip 双主题切换
|
||||
页面挂载不同的作用域类名(`theme-default` / `theme-dark`)即可整体切换深浅色,所有 antd 组件变量随作用域自动切换。
|
||||
:::
|
||||
|
||||
ElementPlus 组件样式覆盖定义在主题文件中,通过 `[data-theme]` 选择器区分深浅色模式:
|
||||
## 颜色变量示例
|
||||
|
||||
```scss
|
||||
/* ui/src/styles/theme/light.scss */
|
||||
[data-theme='light'] {
|
||||
--el-border-color-base: #e4e7ed;
|
||||
```less
|
||||
/* ui/src/styles/color.less */
|
||||
html {
|
||||
//content
|
||||
--content-bg: #f0f2f5;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-header-text-color: rgb(31 34 37);
|
||||
}
|
||||
// ...更多组件覆盖
|
||||
@white: #fff;
|
||||
@content-bg: #f0f2f5;
|
||||
```
|
||||
|
||||
## 主题配置管理
|
||||
|
||||
布局主题(导航/顶栏配色)通过 `projectSetting` Store 管理:
|
||||
|
||||
```typescript
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
|
||||
const projectStore = useProjectSettingStore();
|
||||
|
||||
// 导航主题 / 顶栏主题
|
||||
projectStore.navTheme;
|
||||
projectStore.headerTheme;
|
||||
projectStore.setNavTheme('dark');
|
||||
```
|
||||
|
||||
## AntDesign 组件覆盖
|
||||
|
||||
如需覆盖 antd 组件样式,写在作用域主题文件或 `common.less` 中,使用 `.ant-*` 选择器:
|
||||
|
||||
```less
|
||||
/* ui/src/styles/theme/theme-default.less 或页面 scoped 样式 */
|
||||
.ant-table-thead > tr > th {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
```
|
||||
|
||||
## TailwindCSS 配置
|
||||
:::warning 覆盖要点
|
||||
- antd 样式类名为 `.ant-*`(如 `.ant-btn`、`.ant-table`),与 ElementPlus 的 `.el-*` 不同;
|
||||
- 优先调整主题变量(`@primary-color` 等),其次才是选择器覆盖;
|
||||
- 暗色主题的覆盖要放进 `theme-dark.less` 作用域,避免深浅色互相污染。
|
||||
:::
|
||||
|
||||
```javascript
|
||||
// ui/tailwind.config.js
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{vue,ts,tsx}'],
|
||||
## WindiCSS 配置
|
||||
|
||||
```typescript
|
||||
// ui/windi.config.ts
|
||||
import { defineConfig } from 'vite-plugin-windicss';
|
||||
|
||||
export default defineConfig({
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'var(--primary-color)',
|
||||
screens: {
|
||||
sm: '576px',
|
||||
md: '768px',
|
||||
lg: '992px',
|
||||
xl: '1200px',
|
||||
'2xl': '1600px',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## 自定义全局样式
|
||||
|
||||
```scss
|
||||
/* ui/src/styles/index.scss */
|
||||
```less
|
||||
/* ui/src/styles/common.less */
|
||||
|
||||
// 页面容器
|
||||
.page-container {
|
||||
padding: 16px;
|
||||
background: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
// 滚动条
|
||||
*::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 样式约定
|
||||
- 优先使用 TailwindCSS 原子类(布局/间距/颜色)
|
||||
- 组件级覆盖写在 `theme/*.scss`,避免散落在页面中
|
||||
- 与后端联调时注意:列表状态 Tag 颜色应与字典值语义一致(如 1=在用 success / 2=停用 danger)
|
||||
- 优先使用 WindiCSS 原子类(布局/间距/颜色)
|
||||
- 组件级覆盖写进 `theme/*.less` 作用域,避免散落在页面中
|
||||
- 与后端联调时注意:列表状态 Tag 颜色应与字典值语义一致(如 1=在用 success / 2=停用 error)
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user