优化更新
This commit is contained in:
@@ -10,8 +10,8 @@ API 接口速查表,按模块分组列出所有 RESTful 接口的请求方法
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|:----|:----|:----|:----|
|
||||
| GET | `/api/captcha` | 获取图形验证码 | 无需登录 |
|
||||
| POST | `/api/login` | 用户登录 | 无需登录 |
|
||||
| GET | `/api/v1/captcha` | 获取图形验证码 | 无需登录 |
|
||||
| POST | `/api/v1/login` | 用户登录 | 无需登录 |
|
||||
| GET | `/api/v1/logout` | 退出登录 | 需登录 |
|
||||
|
||||
## 用户管理
|
||||
|
||||
@@ -104,14 +104,14 @@ def get_tree(data, parent_id):
|
||||
|
||||
## 前端使用
|
||||
|
||||
分类选择通常使用 el-tree 或 el-cascader 组件:
|
||||
分类选择通常使用 a-tree-select 或 a-cascader 组件:
|
||||
|
||||
```vue
|
||||
<!-- 树形选择 -->
|
||||
<el-tree-select
|
||||
v-model="form.category_id"
|
||||
:data="categoryTree"
|
||||
:props="{ label: 'name', value: 'id' }"
|
||||
<a-tree-select
|
||||
v-model:value="form.category_id"
|
||||
:tree-data="categoryTree"
|
||||
:field-names="{ label: 'name', value: 'id' }"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -124,6 +124,6 @@ def get_tree(data, parent_id):
|
||||
2. 通用设计:适用于文章分类、产品分类等场景
|
||||
3. 分类编码:category_code 字段支持程序化标识
|
||||
4. 排序控制:sort 字段控制分类显示顺序
|
||||
5. 前端集成:el-tree-select 或 el-cascader 组件
|
||||
5. 前端集成:a-tree-select 或 a-cascader 组件
|
||||
6. 继承 BaseService:声明 page_like_fields/page_eq_fields 即可获得分页能力
|
||||
```
|
||||
|
||||
@@ -170,11 +170,11 @@ async def delete_role(role_id):
|
||||
角色管理页面通常包含菜单树组件,用于勾选分配权限:
|
||||
|
||||
```vue
|
||||
<el-tree
|
||||
<a-tree
|
||||
ref="menuTree"
|
||||
:data="menuTreeData"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:tree-data="menuTreeData"
|
||||
checkable
|
||||
:field-names="{ title: 'name', key: 'id' }"
|
||||
:default-checked-keys="checkedMenuIds"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
### 请求
|
||||
|
||||
```
|
||||
GET /api/captcha
|
||||
GET /api/v1/captcha
|
||||
```
|
||||
|
||||
### 响应
|
||||
@@ -89,7 +89,7 @@ async def captcha(request: Request):
|
||||
### 请求
|
||||
|
||||
```
|
||||
POST /api/login
|
||||
POST /api/v1/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
|
||||
@@ -140,7 +140,7 @@ PUT /api/v1/config/item/update
|
||||
| 用途 | 枚举值(下拉选项) | 系统参数(配置值) |
|
||||
| 值类型 | 简单值(字符串) | 可存储较长的配置内容 |
|
||||
| 典型场景 | 状态、性别、类型 | SMTP配置、站点信息 |
|
||||
| 前端用法 | el-select 选项 | 配置表单 |
|
||||
| 前端用法 | a-select 选项 | 配置表单 |
|
||||
|
||||
## 总结
|
||||
|
||||
|
||||
@@ -88,20 +88,41 @@ class TrainingService(BaseService[Training]):
|
||||
|
||||
效果:列表/详情返回数据中自动添加 `trainingMethod` 和 `trainingMethodName` 字段。
|
||||
|
||||
### 方式 2:手动获取字典值
|
||||
### 方式 2:通过 dict_util 获取(推荐后端使用)
|
||||
|
||||
```python
|
||||
from modules.dictionary.dict_item.repository import dict_item_repo
|
||||
from utils.dict_util import get_dict_map, get_dict_label, get_dict_items
|
||||
|
||||
# 获取字典项列表
|
||||
items = dict_item_repo.get_all(dict_code='training_method', status=1)
|
||||
# 获取 {value: label} 映射
|
||||
options = get_dict_map('training_method')
|
||||
# 结果:{'1': '线上', '2': '线下', '3': '混合'}
|
||||
|
||||
# 转为 {value: label} 字典
|
||||
options = {item.value: item.label for item in items}
|
||||
# 单值翻译
|
||||
label = get_dict_label('training_method', '1')
|
||||
# 结果:'线上'
|
||||
|
||||
# 获取字典项列表(含排序信息)
|
||||
items = get_dict_items('training_method')
|
||||
# 结果:[{'label': '线上', 'value': '1', 'sort': 1}, ...]
|
||||
```
|
||||
|
||||
### 方式 3:通过 Repository 直接查询
|
||||
|
||||
```python
|
||||
from modules.dictionary.dict.repository import dict_repo
|
||||
from modules.dictionary.dict_item.repository import dictitem_repo
|
||||
|
||||
# 先查字典类型获取 dict_id
|
||||
dict_obj = dict_repo.get_one(code='training_method')
|
||||
# 再查字典项
|
||||
items = dictitem_repo.filter_by(dict_id=dict_obj.id).all()
|
||||
|
||||
# 转为 {value: name} 字典
|
||||
options = {item.value: item.name for item in items}
|
||||
# 结果:{'1': '线上', '2': '线下', '3': '混合'}
|
||||
```
|
||||
|
||||
### 方式 3:通过参数模块获取(适合系统配置)
|
||||
### 方式 4:通过参数模块获取(适合系统配置)
|
||||
|
||||
```python
|
||||
from modules.param.service import param_service
|
||||
@@ -116,14 +137,15 @@ timeout = param_service.get_value_by_code('REQUEST_TIMEOUT', '30')
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-select v-model="form.trainingMethod" placeholder="请选择培训方式">
|
||||
<el-option
|
||||
<a-select v-model:value="form.trainingMethod" placeholder="请选择培训方式">
|
||||
<a-select-option
|
||||
v-for="item in dictOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -141,22 +163,22 @@ onMounted(async () => {
|
||||
|
||||
### 方式 2:表格列显示字典文本
|
||||
|
||||
在 `columns.ts` 中使用 `render` 函数:
|
||||
在 `columns.ts` 中使用 `customRender` 函数:
|
||||
|
||||
```typescript
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
label: '培训方式',
|
||||
prop: 'trainingMethod',
|
||||
render(record) {
|
||||
title: '培训方式',
|
||||
dataIndex: 'trainingMethod',
|
||||
customRender: ({ record }) => {
|
||||
const map = { 1: '线上', 2: '线下', 3: '混合' };
|
||||
const typeMap = { 1: 'success', 2: 'warning', 3: 'info' };
|
||||
return h(ElTag, { type: typeMap[record.row.trainingMethod] }, {
|
||||
default: () => map[record.row.trainingMethod] || '-'
|
||||
});
|
||||
const colorMap = { 1: 'success', 2: 'warning', 3: 'default' };
|
||||
return h(Tag, { color: colorMap[record.trainingMethod] }, () =>
|
||||
map[record.trainingMethod] || '-'
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -174,7 +196,7 @@ export const schemas: FormSchema[] = [
|
||||
label: '培训方式',
|
||||
componentProps: {
|
||||
placeholder: '请选择培训方式',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '线上', value: '1' },
|
||||
{ label: '线下', value: '2' },
|
||||
|
||||
@@ -94,18 +94,25 @@ class DictItem(base_model, base_db):
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 后端获取字典
|
||||
### 后端获取字典(推荐使用 dict_util)
|
||||
|
||||
```python
|
||||
# 通过字典编码获取字典项列表
|
||||
from modules.dictionary.dict_item import service as dict_item_service
|
||||
from utils.dict_util import get_dict_items, get_dict_map, get_dict_label
|
||||
|
||||
# 获取状态字典
|
||||
status_list = await dict_item_service.get_by_dict_code("sys_status")
|
||||
# 返回: [{"label": "正常", "value": "1"}, {"label": "停用", "value": "2"}]
|
||||
# 获取字典项列表(自动缓存,TTL 60秒兜底)
|
||||
items = get_dict_items("sys_status")
|
||||
# 返回: [{"label": "正常", "value": "1", "sort": 0}, {"label": "停用", "value": "2", "sort": 1}]
|
||||
|
||||
# 获取 {value: label} 映射
|
||||
status_map = get_dict_map("sys_status")
|
||||
# 返回: {"1": "正常", "2": "停用"}
|
||||
|
||||
# 单值翻译(value 自动转字符串匹配)
|
||||
label = get_dict_label("sys_status", 1, default="未知")
|
||||
# 返回: "正常"
|
||||
```
|
||||
|
||||
### dict_util 缓存工具
|
||||
### dict_util 缓存工具详解
|
||||
|
||||
项目提供 `dict_util` 工具(`src/utils/dict_util.py`),内置进程内 TTL 缓存。由于字典数据几乎不变,且读写均落在同步 service 路径(无法使用请求级异步 Redis),因此采用进程内缓存而非 Redis 缓存:
|
||||
|
||||
@@ -168,15 +175,16 @@ const dictStore = useDictStore()
|
||||
const statusOptions = dictStore.getDict('sys_status')
|
||||
// [{label: "正常", value: "1"}, {label: "停用", value: "2"}]
|
||||
|
||||
// 在 el-select 中使用
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
// 在 a-select 中使用
|
||||
<a-select v-model:value="form.status">
|
||||
<a-select-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
```
|
||||
|
||||
## 常用字典示例
|
||||
@@ -195,7 +203,7 @@ const statusOptions = dictStore.getDict('sys_status')
|
||||
```
|
||||
1. 两级结构:字典类型(分组)+ 字典项(具体值),清晰管理
|
||||
2. 进程内缓存:dict_util 内置 TTL 缓存(60秒兜底),变更时主动失效
|
||||
3. 前端集成:useDictStore 统一管理,el-select 直接绑定
|
||||
3. 前端集成:useDictStore 统一管理,a-select 直接绑定
|
||||
4. 值为字符串:字典项 value 统一为字符串,get_dict_label 自动转字符串匹配
|
||||
5. 避免硬编码:枚举值统一走字典,修改无需改代码
|
||||
6. 三种取值:get_dict_items(列表)、get_dict_map(映射)、get_dict_label(单值翻译)
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
|
||||
```javascript
|
||||
import { useLockFn } from '@/utils/lock'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
// 包装提交函数,防止重复点击
|
||||
const handleSubmit = useLockFn(async () => {
|
||||
await axios.post('/api/v1/level/add', formData)
|
||||
ElMessage.success('添加成功')
|
||||
message.success('添加成功')
|
||||
})
|
||||
```
|
||||
|
||||
@@ -37,25 +38,27 @@ function useLockFn(fn) {
|
||||
|
||||
### 按钮 loading
|
||||
|
||||
ElementPlus 的按钮组件支持 `loading` 属性,请求期间禁用按钮:
|
||||
AntDesign Vue 的按钮组件支持 `loading` 属性,请求期间禁用按钮:
|
||||
|
||||
```vue
|
||||
<el-button
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</a-button>
|
||||
|
||||
<script setup>
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await axios.post('/api/v1/level/add', formData)
|
||||
ElMessage.success('添加成功')
|
||||
message.success('添加成功')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -185,10 +185,10 @@ def permission_required(permission: str):
|
||||
|
||||
```html
|
||||
<!-- 只有拥有 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>
|
||||
|
||||
<!-- 多个权限,满足其一即可 -->
|
||||
<el-button v-perm="['sys:user:update', 'sys:user:delete']">操作</el-button>
|
||||
<a-button v-perm="['sys:user:update', 'sys:user:delete']">操作</a-button>
|
||||
```
|
||||
|
||||
### 路由守卫
|
||||
|
||||
@@ -136,7 +136,7 @@ class BaseForm(BaseModel):
|
||||
```
|
||||
|
||||
:::warning 前端空字符串
|
||||
前端某些组件(如 el-select)在未选择时可能传空字符串 `""`,BaseForm 的 `empty_str_to_none` 验证器会自动将其转为 `None`,避免整数字段解析报错。
|
||||
前端某些组件(如 a-select)在未选择时可能传空字符串 `""`,BaseForm 的 `empty_str_to_none` 验证器会自动将其转为 `None`,避免整数字段解析报错。
|
||||
:::
|
||||
|
||||
## 唯一性校验
|
||||
|
||||
@@ -208,17 +208,20 @@ transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
|
||||
// 业务失败(code === 1)
|
||||
if (code === ResultEnum.ERROR) {
|
||||
ElMessage.error(msg);
|
||||
message.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// 登录超时(code === 401)
|
||||
if (code === ResultEnum.TIMEOUT) {
|
||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { ... })
|
||||
.then(() => {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '登录身份已失效,请重新登录!',
|
||||
onOk: () => {
|
||||
storage.clear();
|
||||
window.location.href = '/login';
|
||||
});
|
||||
},
|
||||
});
|
||||
throw new Error(msg);
|
||||
}
|
||||
},
|
||||
@@ -291,7 +294,7 @@ const loadDataTable = async (res: any) => {
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
await positionAdd(formData);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
emit('success');
|
||||
};
|
||||
```
|
||||
@@ -304,20 +307,28 @@ const handleSubmit = async () => {
|
||||
|
||||
错误处理分两层:
|
||||
|
||||
1. **业务错误**(code !== 0):拦截器统一弹出 `ElMessage.error(msg)`,并 `throw new Error`
|
||||
1. **业务错误**(code !== 0):拦截器统一弹出 `message.error(msg)`,并 `throw new Error`
|
||||
2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理
|
||||
|
||||
```typescript
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
responseInterceptorsCatch: (error: any) => {
|
||||
const { response, code } = error || {};
|
||||
// 超时
|
||||
if (code === 'ECONNABORTED') {
|
||||
ElMessage.error('接口请求超时,请刷新页面重试!');
|
||||
message.error('接口请求超时,请刷新页面重试!');
|
||||
return;
|
||||
}
|
||||
// 网络异常
|
||||
if (error.toString().includes('Network Error')) {
|
||||
ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', { ... });
|
||||
Modal.confirm({
|
||||
title: '网络异常',
|
||||
content: '请检查您的网络连接是否正常',
|
||||
onOk: () => {
|
||||
// 处理确认操作
|
||||
},
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// 其他 HTTP 状态码
|
||||
|
||||
@@ -66,8 +66,8 @@ Vite 按 mode 加载环境变量,后面的文件覆盖前面的:
|
||||
|------|------|--------|------|
|
||||
| `VITE_PORT` | `number` | `8001` | 开发服务器端口 |
|
||||
| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) |
|
||||
| `VITE_GLOB_APP_TITLE` | `string` | `FastAPI+ElementPlus版` | 应用标题(注入 HTML) |
|
||||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `FastAPI+ElementPlus版` | 应用短名(用于生成运行时配置变量名) |
|
||||
| `VITE_GLOB_APP_TITLE` | `string` | `FastAPI+AntDesign版` | 应用标题(注入 HTML) |
|
||||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `FastAPI+AntDesign版` | 应用短名(用于生成运行时配置变量名) |
|
||||
| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) |
|
||||
| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 |
|
||||
| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) |
|
||||
@@ -175,7 +175,7 @@ import { defineStore } from 'pinia'; // 自动
|
||||
|
||||
```html
|
||||
<!-- 构建后的 index.html -->
|
||||
<title>FastAPI+ElementPlus版</title>
|
||||
<title>FastAPI+AntDesign版</title>
|
||||
<script src="/app.config.js?v=1.0.0-1725000000000"></script>
|
||||
```
|
||||
|
||||
@@ -185,14 +185,14 @@ import { defineStore } from 'pinia'; // 自动
|
||||
|
||||
```javascript
|
||||
// dist/app.config.js(构建后自动生成,不可手动修改)
|
||||
window.__PRODUCTION_FASTAPI_ELEMENTPLUS___CONF__ = {
|
||||
window.__PRODUCTION_FASTAPI_ANTDESIGN___CONF__ = {
|
||||
"VITE_GLOB_API_URL": "",
|
||||
"VITE_GLOB_API_URL_PREFIX": "/api",
|
||||
"VITE_GLOB_FILE_URL": "http://file.fastapi.elevue",
|
||||
"VITE_GLOB_APP_TITLE": "FastAPI+ElementPlus版",
|
||||
"VITE_GLOB_FILE_URL": "http://file.fastapi.antdvue",
|
||||
"VITE_GLOB_APP_TITLE": "FastAPI+AntDesign版",
|
||||
// ...
|
||||
};
|
||||
Object.freeze(window.__PRODUCTION_FASTAPI_ELEMENTPLUS___CONF__);
|
||||
Object.freeze(window.__PRODUCTION_FASTAPI_ANTDESIGN___CONF__);
|
||||
```
|
||||
|
||||
:::warning 为什么需要运行时配置?
|
||||
@@ -249,7 +249,7 @@ dist/
|
||||
├── app.config.js # 运行时配置(VITE_GLOB_* 变量)
|
||||
├── favicon.ico # 网站图标
|
||||
└── assets/
|
||||
├── index-[hash].js # 主包(Vue、ElementPlus、Pinia 等)
|
||||
├── index-[hash].js # 主包(Vue、AntDesign Vue、Pinia 等)
|
||||
├── index-[hash].css # 全局样式
|
||||
├── [name]-[hash].js # 路由懒加载 chunk(按视图分割)
|
||||
└── ... # 字体、图片等静态资源
|
||||
@@ -276,7 +276,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
||||
| chunk | 内容 |
|
||||
|-------|------|
|
||||
| `vendor-vue.js` | vue + vue-router + pinia |
|
||||
| `vendor-element.js` | element-plus |
|
||||
| `vendor-antd.js` | ant-design-vue |
|
||||
| `vendor-echarts.js` | echarts(如果使用) |
|
||||
| `vendor-*.js` | 其他大型依赖 |
|
||||
|
||||
@@ -286,7 +286,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
||||
|
||||
```typescript
|
||||
optimizeDeps: {
|
||||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@element-plus/icons-vue'],
|
||||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@ant-design/icons-vue'],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -300,13 +300,13 @@ optimizeDeps: {
|
||||
async function bootstrap() {
|
||||
const app = createApp(App);
|
||||
|
||||
// 1. 全局注册 ElementPlus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
// 1. 全局注册 AntDesign Vue 图标
|
||||
for (const [key, component] of Object.entries(AntdIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
// 2. 全局引入 ElementPlus(中文包)
|
||||
setupElement(app);
|
||||
// 2. 全局引入 AntDesign Vue(中文包)
|
||||
setupAntd(app);
|
||||
|
||||
// 3. 全局注册自定义组件(BasicTable、BasicForm 等)
|
||||
setupCustomComponents(app);
|
||||
@@ -332,7 +332,7 @@ async function bootstrap() {
|
||||
|
||||
| 注册项 | 文件 | 说明 |
|
||||
|--------|------|------|
|
||||
| ElementPlus | `element.ts` | 全局引入 + 中文包 |
|
||||
| AntDesign Vue | `antd.ts` | 全局引入 + 中文包 |
|
||||
| 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 |
|
||||
| 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` |
|
||||
|
||||
@@ -497,7 +497,7 @@ pnpm report
|
||||
**优化手段**:
|
||||
|
||||
1. 路由懒加载(已默认启用)
|
||||
2. 按需引入 ElementPlus 组件(当前为全量引入)
|
||||
2. 按需引入 AntDesign Vue 组件(当前为全量引入)
|
||||
3. 大型依赖(echarts、wangeditor)按需引入
|
||||
4. 开启 gzip 压缩(`VITE_BUILD_COMPRESS=gzip`)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 公共组件库
|
||||
|
||||
:::tip 说明
|
||||
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 ElementPlus 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
||||
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 AntDesign Vue 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
||||
:::
|
||||
|
||||
## 组件总览
|
||||
@@ -17,7 +17,7 @@
|
||||
| CropperImage | `components/Cropper/` | 图片裁剪(圆形/矩形、实时预览) |
|
||||
| QrCode | `components/Qrcode/` | 二维码生成(canvas/img、内嵌 logo) |
|
||||
| ChinaArea | `components/ChinaArea/` | 省市区级联(懒加载、二/三级联动) |
|
||||
| Icon | `components/icon/` | 图标(支持 ElementPlus 图标 + SVG 图标) |
|
||||
| Icon | `components/icon/` | 图标(支持 AntDesign 图标 + SVG 图标) |
|
||||
| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) |
|
||||
| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) |
|
||||
| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) |
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## BasicTable 表格
|
||||
|
||||
表格组件是使用频率最高的组件,基于 `el-table` + `el-pagination` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
||||
表格组件是使用频率最高的组件,基于 `a-table` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -129,18 +129,23 @@
|
||||
<template>
|
||||
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef">
|
||||
<template #tableTitle>
|
||||
<el-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
<a-space>
|
||||
<a-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const columns = [
|
||||
{ label: '名称', prop: 'name', width: 200 },
|
||||
{ label: '状态', prop: 'status', width: 100 },
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
{ title: '名称', dataIndex: 'name', width: 200 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
|
||||
];
|
||||
|
||||
const actionColumn = {
|
||||
@@ -287,8 +292,8 @@ const [registerForm] = useForm({
|
||||
{
|
||||
field: 'dateRange',
|
||||
label: '日期范围',
|
||||
component: 'DatePicker',
|
||||
componentProps: { type: 'daterange' },
|
||||
component: 'RangePicker',
|
||||
componentProps: { valueFormat: 'YYYY-MM-DD' },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -299,7 +304,7 @@ const [registerForm] = useForm({
|
||||
|
||||
## BasicModal 弹窗
|
||||
|
||||
弹窗组件基于 `el-dialog` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
||||
弹窗组件基于 `a-modal` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -356,7 +361,7 @@ function handleEdit(record) {
|
||||
|
||||
## BasicUpload 上传
|
||||
|
||||
文件/图片上传组件,基于 `el-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
|
||||
文件/图片上传组件,基于 `a-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -698,10 +703,10 @@ async function getDeptOptions() {
|
||||
<BasicTable :columns="columns" :request="loadData" />
|
||||
|
||||
<template #leftFooter>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</template>
|
||||
<template #rightFooter>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
<a-button type="primary" @click="handleSubmit">提交</a-button>
|
||||
</template>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
@@ -724,7 +729,7 @@ async function getDeptOptions() {
|
||||
```vue
|
||||
<template>
|
||||
<Authority :value="['sys:user:add']">
|
||||
<el-button type="primary">新增用户</el-button>
|
||||
<a-button type="primary">新增用户</a-button>
|
||||
</Authority>
|
||||
</template>
|
||||
```
|
||||
@@ -732,7 +737,7 @@ async function getDeptOptions() {
|
||||
:::tip v-perm 指令
|
||||
除了 `Authority` 组件,项目还提供 `v-perm` 指令,功能相同但更简洁:
|
||||
```vue
|
||||
<el-button v-perm="['sys:user:add']">新增用户</el-button>
|
||||
<a-button v-perm="['sys:user:add']">新增用户</a-button>
|
||||
```
|
||||
:::
|
||||
|
||||
@@ -740,7 +745,7 @@ async function getDeptOptions() {
|
||||
|
||||
## Icon 图标
|
||||
|
||||
统一图标组件,支持 ElementPlus 图标和 SVG 图标。
|
||||
统一图标组件,支持 AntDesign 图标和 SVG 图标。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -754,8 +759,8 @@ async function getDeptOptions() {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ElementPlus 图标 -->
|
||||
<Icon name="el-icon-Plus" size="18px" />
|
||||
<!-- AntDesign 图标 -->
|
||||
<Icon name="ant-design:plus-outlined" size="18px" />
|
||||
<!-- SVG 图标 -->
|
||||
<Icon name="svg-icon-user" />
|
||||
</template>
|
||||
@@ -836,4 +841,4 @@ setupCustomComponents(app); // 注册所有自定义组件
|
||||
|
||||
## 总结
|
||||
|
||||
公共组件库基于 ElementPlus 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
|
||||
公共组件库基于 AntDesign Vue 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# BasicForm 表单
|
||||
|
||||
:::tip 说明
|
||||
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 ElementPlus 的 `el-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
||||
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 AntDesign Vue 的 `a-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -24,9 +24,9 @@ src/components/Form/
|
||||
列表页的搜索区域使用 `useForm` 注册表单:
|
||||
|
||||
```vue
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<a-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
</a-card>
|
||||
```
|
||||
|
||||
### querySchemas 定义
|
||||
@@ -51,7 +51,7 @@ export const schemas: FormSchema[] = [
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '正常', value: '1' },
|
||||
{ label: '禁用', value: '2' },
|
||||
@@ -66,15 +66,19 @@ export const schemas: FormSchema[] = [
|
||||
```typescript
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { ColProps } from 'element-plus';
|
||||
|
||||
const formParams = reactive({ name: '', status: '' });
|
||||
|
||||
const [register, {}] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 } as ColProps,
|
||||
submitOnReset: true,
|
||||
rowProps: { gutter: [16, 0] },
|
||||
colProps: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
},
|
||||
labelCol: { span: 6, offset: 0 },
|
||||
schemas,
|
||||
});
|
||||
|
||||
@@ -93,73 +97,80 @@ function handleReset() {
|
||||
|
||||
## 业务表单(编辑弹窗)
|
||||
|
||||
编辑弹窗中直接使用 `el-form` + `el-form-item` 构建表单:
|
||||
编辑弹窗中直接使用 `a-form` + `a-form-item` 构建表单:
|
||||
|
||||
```vue
|
||||
<!-- src/views/system/position/edit.vue -->
|
||||
<template>
|
||||
<el-dialog v-model="props.visible" :title="props.positionId ? '编辑' : '新增'" width="500">
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
<el-form-item
|
||||
<a-modal v-model:visible="props.visible" :title="props.positionId ? '编辑' : '新增'" width="500px">
|
||||
<a-form
|
||||
class="ls-form"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ style: { width: '85px' } }"
|
||||
>
|
||||
<a-form-item
|
||||
label="岗位名称"
|
||||
prop="name"
|
||||
name="name"
|
||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||
>
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</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" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<a-input v-model:value="formData.name" placeholder="请输入岗位名称" allow-clear />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
```
|
||||
|
||||
## BasicForm 内部渲染机制
|
||||
|
||||
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 ElementPlus 组件:
|
||||
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 AntDesign Vue 组件:
|
||||
|
||||
```vue
|
||||
<!-- src/components/Form/src/BasicForm.vue 核心逻辑 -->
|
||||
<template v-for="schema in getSchema" :key="schema.field">
|
||||
<!-- Checkbox -->
|
||||
<template v-if="schema.component === 'Checkbox'">
|
||||
<el-checkbox-group v-model="formModel[schema.field]">
|
||||
<el-checkbox v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
<a-checkbox-group v-model:value="formModel[schema.field]">
|
||||
<a-checkbox v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</template>
|
||||
|
||||
<!-- RadioGroup -->
|
||||
<template v-else-if="schema.component === 'RadioGroup'">
|
||||
<el-radio-group v-model="formModel[schema.field]">
|
||||
<el-radio v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
<a-radio-group v-model:value="formModel[schema.field]">
|
||||
<a-radio v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<!-- Select -->
|
||||
<template v-else-if="schema.component === 'Select'">
|
||||
<el-select v-model="formModel[schema.field]">
|
||||
<el-option v-for="item in schema.componentProps?.options" :value="item.value" :label="item.label" />
|
||||
</el-select>
|
||||
<a-select v-model:value="formModel[schema.field]">
|
||||
<a-select-option v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<!-- Daterange -->
|
||||
<template v-else-if="schema.component === 'Daterange'">
|
||||
<el-date-picker v-model="formModel[schema.field]" type="daterange" value-format="YYYY-MM-DD" />
|
||||
<!-- RangePicker -->
|
||||
<template v-else-if="schema.component === 'RangePicker'">
|
||||
<a-range-picker v-model:value="formModel[schema.field]" valueFormat="YYYY-MM-DD" />
|
||||
</template>
|
||||
|
||||
<!-- 其他组件:动态渲染 el-{component} -->
|
||||
<component v-else :is="`el-${schema.component}`" v-model="formModel[schema.field]" />
|
||||
<!-- 其他组件:动态渲染 a-{component} -->
|
||||
<component v-else :is="`a-${schema.component}`" v-model:value="formModel[schema.field]" />
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -170,7 +181,7 @@ interface FormSchema {
|
||||
field: string; // 字段名
|
||||
label: string; // 标签文本
|
||||
component: string; // 组件类型:Input / Select / RadioGroup / Checkbox / DatePicker 等
|
||||
componentProps?: any; // 组件属性(placeholder、options、clearable 等)
|
||||
componentProps?: any; // 组件属性(placeholder、options、allowClear 等)
|
||||
required?: boolean; // 是否必填
|
||||
rules?: any[]; // 自定义校验规则
|
||||
defaultValue?: any; // 默认值
|
||||
@@ -237,4 +248,4 @@ onMounted(() => {
|
||||
|
||||
## 总结
|
||||
|
||||
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 ElementPlus 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `el-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
|
||||
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 AntDesign Vue 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `a-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
|
||||
|
||||
@@ -116,11 +116,12 @@ export function useLockFn(fn: (...args: any[]) => Promise<any>) {
|
||||
```typescript
|
||||
// src/views/system/position/edit.vue
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
@@ -130,7 +131,7 @@ const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||||
```
|
||||
|
||||
```vue
|
||||
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
|
||||
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
|
||||
```
|
||||
|
||||
## useAsync
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# 国际化
|
||||
|
||||
:::tip 说明
|
||||
当前项目仅支持中文界面,ElementPlus 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
||||
当前项目仅支持中文界面,AntDesign Vue 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
||||
:::
|
||||
|
||||
## 当前状态
|
||||
|
||||
项目当前为中文单语言版本:
|
||||
|
||||
- ElementPlus 全局配置中文语言包:`app.use(ElementPlus, { locale: zhCn })`
|
||||
- 所有界面文本直接硬编码在组件中:`<el-button>添加岗位</el-button>`
|
||||
- AntDesign Vue 全局配置中文语言包:`app.use(Antd, { locale: zhCn })`
|
||||
- 所有界面文本直接硬编码在组件中:`<a-button>添加岗位</a-button>`
|
||||
- 后端返回的 `msg` 字段为中文:`"操作成功"`、`"岗位名称不能重复"`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-button type="primary">添加岗位</el-button>
|
||||
<el-table-column label="岗位名称" prop="name" />
|
||||
<el-table-column label="创建时间" prop="createTime" />
|
||||
<a-button type="primary">添加岗位</a-button>
|
||||
<a-table-column title="岗位名称" data-index="name" />
|
||||
<a-table-column title="创建时间" data-index="createTime" />
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -65,8 +65,8 @@ export default i18n;
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-button type="primary">{{ $t('common.add') }}</el-button>
|
||||
<el-table-column :label="$t('position.name')" prop="name" />
|
||||
<a-button type="primary">{{ $t('common.add') }}</a-button>
|
||||
<a-table-column :title="$t('position.name')" data-index="name" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -75,13 +75,13 @@ import { useI18n } from 'vue-i18n';
|
||||
</script>
|
||||
```
|
||||
|
||||
### ElementPlus 国际化
|
||||
### AntDesign Vue 国际化
|
||||
|
||||
```typescript
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import enUS from 'ant-design-vue/es/locale/en_US';
|
||||
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
app.use(Antd, { locale: zhCN });
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
@@ -90,4 +90,4 @@ app.use(ElementPlus, { locale: zhCn });
|
||||
|
||||
## 总结
|
||||
|
||||
当前项目为中文单语言版本,ElementPlus 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 ElementPlus 内置语言包和后端消息 code 实现全链路多语言。
|
||||
当前项目为中文单语言版本,AntDesign Vue 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 AntDesign Vue 内置语言包和后端消息 code 实现全链路多语言。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Modal 弹窗
|
||||
|
||||
:::tip 说明
|
||||
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
|
||||
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `a-modal` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -9,7 +9,7 @@
|
||||
```
|
||||
src/components/Modal/
|
||||
├── src/
|
||||
│ ├── BasicModal.vue # 主组件(基于 el-dialog)
|
||||
│ ├── BasicModal.vue # 主组件(基于 a-modal)
|
||||
│ ├── props.ts # 属性定义
|
||||
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
|
||||
│ └── hooks/
|
||||
@@ -19,7 +19,7 @@ src/components/Modal/
|
||||
|
||||
## BasicModal 组件
|
||||
|
||||
`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力:
|
||||
`BasicModal` 基于 `a-modal` 封装,提供全屏切换、自定义头部、提交按钮等能力:
|
||||
|
||||
```vue
|
||||
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
|
||||
@@ -36,7 +36,7 @@ src/components/Modal/
|
||||
|
||||
## 实际用法(position 模块)
|
||||
|
||||
以岗位管理为例,实际使用原生 `el-dialog` + `defineAsyncComponent` 懒加载:
|
||||
以岗位管理为例,实际使用原生 `a-modal` + `defineAsyncComponent` 懒加载:
|
||||
|
||||
### 父组件(index.vue)
|
||||
|
||||
@@ -57,8 +57,8 @@ const handleAdd = async () => {
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
positionId.value = record.row.id;
|
||||
const handleEdit = async (id: number) => {
|
||||
positionId.value = id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
@@ -81,41 +81,47 @@ const handleEdit = async (record: Recordable) => {
|
||||
```vue
|
||||
<!-- src/views/system/position/edit.vue -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.visible"
|
||||
<a-modal
|
||||
v-model:visible="props.visible"
|
||||
:title="props.positionId ? '编辑' : '新增'"
|
||||
width="500"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="dialogClose"
|
||||
width="500px"
|
||||
@cancel="dialogClose"
|
||||
>
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
<el-form-item
|
||||
<a-form
|
||||
class="ls-form"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ style: { width: '85px' } }"
|
||||
>
|
||||
<a-form-item
|
||||
label="岗位名称"
|
||||
prop="name"
|
||||
name="name"
|
||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||
>
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</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" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<a-input v-model:value="formData.name" placeholder="请输入岗位名称" allow-clear />
|
||||
</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" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogClose">取消</el-button>
|
||||
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
|
||||
<a-button @click="dialogClose">取消</a-button>
|
||||
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
const formRef = shallowRef<FormInstance>();
|
||||
@@ -131,7 +137,7 @@ const props = defineProps({
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
@@ -161,7 +167,7 @@ onMounted(() => {
|
||||
## 弹窗数据流
|
||||
|
||||
```
|
||||
父组件点击编辑 → positionId = record.row.id → editVisible = true
|
||||
父组件点击编辑 → positionId = id → editVisible = true
|
||||
↓
|
||||
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
|
||||
↓
|
||||
@@ -172,25 +178,25 @@ emit('success') → 父组件 reloadTable() → 表格刷新
|
||||
|
||||
## 确认弹窗
|
||||
|
||||
删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`):
|
||||
删除等危险操作使用 `Modal.confirm` 方法(来自 `ant-design-vue`):
|
||||
|
||||
```typescript
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
|
||||
await confirm('确定要删除?');
|
||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
async function handleDelete(id?: number) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除?',
|
||||
onOk: async () => {
|
||||
id ? await positionDelete(id) : await positionBatchDelete(selectionData.value);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`confirm` 内部调用 `ElMessageBox.confirm`,返回 Promise,用户取消时抛出异常中断后续逻辑。
|
||||
`Modal.confirm` 返回 Promise,用户取消时不会执行 `onOk` 回调。
|
||||
|
||||
## v-model:visible 双向绑定
|
||||
|
||||
@@ -213,4 +219,4 @@ const dialogClose = () => { emit('update:visible', false); };
|
||||
|
||||
## 总结
|
||||
|
||||
编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
|
||||
编辑弹窗使用 `a-modal` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
|
||||
|
||||
@@ -90,15 +90,15 @@ export function usePermission() {
|
||||
### v-perm(满足任一权限)
|
||||
|
||||
```vue
|
||||
<el-button v-perm="['sys:position:add']">添加岗位</el-button>
|
||||
<el-button v-perm="['sys:position:batchDelete']">删除</el-button>
|
||||
<el-button v-perm="['sys:position:export']">导出</el-button>
|
||||
<a-button v-perm="['sys:position:add']">添加岗位</a-button>
|
||||
<a-button v-perm="['sys:position:batchDelete']">删除</a-button>
|
||||
<a-button v-perm="['sys:position:export']">导出</a-button>
|
||||
```
|
||||
|
||||
### v-perms(满足所有权限)
|
||||
|
||||
```vue
|
||||
<el-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</el-button>
|
||||
<a-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</a-button>
|
||||
```
|
||||
|
||||
### 与 v-if 结合
|
||||
@@ -113,7 +113,7 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
||||
```
|
||||
|
||||
```vue
|
||||
<el-button v-if="showAddBtn">添加岗位</el-button>
|
||||
<a-button v-if="showAddBtn">添加岗位</a-button>
|
||||
```
|
||||
|
||||
## 实际用法(position 模块)
|
||||
@@ -125,15 +125,17 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
||||
<template>
|
||||
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef" :actionColumn="actionColumn">
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon><el-icon><PlusOutlined /></el-icon></template>
|
||||
添加岗位
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']">
|
||||
<template #icon><el-icon><Delete /></el-icon></template>
|
||||
删除
|
||||
</el-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加岗位
|
||||
</a-button>
|
||||
<a-button type="primary" danger @click="handleDelete()" :disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
||||
@@ -202,7 +204,7 @@ def add(request: Request, data: PositionForm):
|
||||
|
||||
```vue
|
||||
<!-- 前端 -->
|
||||
<el-button v-perm="['sys:position:add']">添加岗位</el-button>
|
||||
<a-button v-perm="['sys:position:add']">添加岗位</a-button>
|
||||
```
|
||||
|
||||
## 超级管理员
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 前端环境与结构
|
||||
|
||||
:::tip 说明
|
||||
前端基于 Vue3 + ElementPlus 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
||||
前端基于 Vue3 + AntDesign Vue 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
||||
:::
|
||||
|
||||
## 技术栈
|
||||
@@ -9,7 +9,7 @@
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API |
|
||||
| ElementPlus | 2.x | Vue3 组件库 |
|
||||
| AntDesign Vue | 4.x | Vue3 组件库 |
|
||||
| Vite | 3.x | 下一代前端构建工具 |
|
||||
| Pinia | 2.x | 状态管理(Vuex 替代方案) |
|
||||
| Vue Router | 4.x | 路由管理 |
|
||||
@@ -140,24 +140,24 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
|
||||
|
||||
```typescript
|
||||
import './styles/tailwindbase.css';
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import './styles/index.scss';
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||
import * as AntdIconsVue from '@ant-design/icons-vue';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router, { setupRouter } from './router';
|
||||
import { setupStore } from '@/store';
|
||||
import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins';
|
||||
import { setupAntd, setupDirectives, setupCustomComponents } from '@/plugins';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册所有 ElementPlus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
// 注册所有 AntDesign Vue 图标
|
||||
for (const [key, component] of Object.entries(AntdIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
setupElement(app); // 全局引入 ElementPlus 组件
|
||||
setupAntd(app); // 全局引入 AntDesign Vue 组件
|
||||
setupCustomComponents(app); // 注册自定义组件
|
||||
setupDirectives(app); // 注册自定义指令(v-perm 等)
|
||||
setupStore(app); // 挂载 Pinia 状态管理
|
||||
@@ -170,7 +170,7 @@ async function bootstrap() {
|
||||
void bootstrap();
|
||||
```
|
||||
|
||||
初始化顺序:样式加载 -> ElementPlus -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
|
||||
初始化顺序:样式加载 -> AntDesign Vue -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
|
||||
|
||||
## 核心目录说明
|
||||
|
||||
@@ -180,7 +180,7 @@ API 接口定义目录,按模块组织文件。公共接口在 `api/common/`
|
||||
|
||||
### components/
|
||||
|
||||
公共组件目录,基于 ElementPlus 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
|
||||
公共组件目录,基于 AntDesign Vue 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
|
||||
|
||||
### directives/
|
||||
|
||||
@@ -212,4 +212,4 @@ Pinia 状态管理目录,核心模块:
|
||||
|
||||
## 总结
|
||||
|
||||
前端项目基于 Vue3 + ElementPlus + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
|
||||
前端项目基于 Vue3 + AntDesign Vue + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 样式与主题
|
||||
|
||||
:::tip 说明
|
||||
前端样式方案采用 Tailwind CSS + SCSS + ElementPlus 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
||||
前端样式方案采用 Tailwind CSS + SCSS + AntDesign Vue 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
||||
:::
|
||||
|
||||
## 样式架构
|
||||
@@ -11,7 +11,7 @@ src/styles/
|
||||
├── var.scss # SCSS 全局变量(通过 Vite additionalData 注入)
|
||||
├── index.scss # 入口文件,引入所有样式
|
||||
├── tailwindbase.css # Tailwind CSS 基础样式
|
||||
└── element-override.css # ElementPlus 主题覆盖(可选)
|
||||
└── ant-override.css # AntDesign Vue 主题覆盖(可选)
|
||||
```
|
||||
|
||||
## SCSS 全局变量
|
||||
@@ -56,36 +56,33 @@ import './styles/tailwindbase.css';
|
||||
<div class="w-full md:w-1/2 lg:w-1/3">响应式宽度</div>
|
||||
```
|
||||
|
||||
## ElementPlus 主题
|
||||
## AntDesign Vue 主题
|
||||
|
||||
ElementPlus 通过 CSS 变量支持主题定制:
|
||||
AntDesign Vue 通过 CSS 变量和 ConfigProvider 支持主题定制:
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'element-plus/theme-chalk/display.css';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'; // 深色模式 CSS 变量
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
```
|
||||
|
||||
### ElementPlus 全局引入
|
||||
### AntDesign Vue 全局引入
|
||||
|
||||
```typescript
|
||||
// src/plugins/setupElement.ts
|
||||
import ElementPlus from 'element-plus';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
// src/plugins/setupAntd.ts
|
||||
import Antd from 'ant-design-vue';
|
||||
|
||||
export function setupElement(app) {
|
||||
app.use(ElementPlus, { locale: zhCn, size: 'default' });
|
||||
export function setupAntd(app) {
|
||||
app.use(Antd);
|
||||
}
|
||||
```
|
||||
|
||||
## 深色模式
|
||||
|
||||
通过 ElementPlus 的 `dark/css-vars.css` 和 Tailwind 的 `dark` 模式实现深色主题:
|
||||
通过 AntDesign Vue 的 ConfigProvider 和 Tailwind 的 `dark` 模式实现深色主题:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-switch v-model="isDark" @change="toggleDark" />
|
||||
<a-switch v-model:checked="isDark" @change="toggleDark" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -100,9 +97,9 @@ const toggleDark = (val) => {
|
||||
|
||||
```css
|
||||
.dark {
|
||||
--el-bg-color: #1d1e1f;
|
||||
--el-text-color-primary: #e5eaf3;
|
||||
--el-border-color: #4c4d4f;
|
||||
--ant-color-bg-base: #1d1e1f;
|
||||
--ant-color-text: #e5eaf3;
|
||||
--ant-color-border: #4c4d4f;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -130,12 +127,12 @@ export function useDesignSetting() {
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<a-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<!-- 搜索区域 -->
|
||||
</el-card>
|
||||
<el-card :bordered="false" class="proCard">
|
||||
</a-card>
|
||||
<a-card :bordered="false" class="proCard">
|
||||
<!-- 表格区域 -->
|
||||
</el-card>
|
||||
</a-card>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
```
|
||||
@@ -150,4 +147,4 @@ export function useDesignSetting() {
|
||||
|
||||
## 总结
|
||||
|
||||
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ ElementPlus 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
||||
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ AntDesign Vue 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# BasicTable 表格
|
||||
|
||||
:::tip 说明
|
||||
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 ElementPlus 的 `el-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
||||
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 AntDesign Vue 的 `a-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -38,40 +38,45 @@ src/components/Table/
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">添加岗位</el-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加岗位
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
```
|
||||
|
||||
## 列定义
|
||||
|
||||
`columns` 定义表格列,支持 `render` 函数自定义渲染:
|
||||
`columns` 定义表格列,支持 `customRender` 函数自定义渲染:
|
||||
|
||||
```typescript
|
||||
// src/views/system/position/columns.ts
|
||||
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
export const columns = [
|
||||
{ type: 'selection' },
|
||||
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
|
||||
{ label: '岗位名称', prop: 'name', minWidth: 100 },
|
||||
{ title: 'ID', dataIndex: 'id', fixed: 'left', width: 50 },
|
||||
{ title: '岗位名称', dataIndex: 'name', minWidth: 100 },
|
||||
{
|
||||
label: '岗位状态',
|
||||
prop: 'status',
|
||||
title: '岗位状态',
|
||||
dataIndex: 'status',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
customRender: ({ record }) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => (record.row.status == 1 ? '正常' : '停用') },
|
||||
Tag,
|
||||
{ color: record.status == 1 ? 'success' : 'error' },
|
||||
() => (record.status == 1 ? '正常' : '停用'),
|
||||
);
|
||||
},
|
||||
},
|
||||
{ label: '排序', prop: 'sort', minWidth: 100 },
|
||||
{ label: '创建人', prop: 'createUser', minWidth: 100 },
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
{ title: '排序', dataIndex: 'sort', minWidth: 100 },
|
||||
{ title: '创建人', dataIndex: 'createUser', minWidth: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
|
||||
];
|
||||
```
|
||||
|
||||
@@ -79,17 +84,17 @@ export const columns = [
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `prop` | string | 字段名 |
|
||||
| `label` | string | 列标题 |
|
||||
| `dataIndex` | string | 字段名 |
|
||||
| `title` | string | 列标题 |
|
||||
| `width` | number | 固定宽度 |
|
||||
| `minWidth` | number | 最小宽度 |
|
||||
| `type` | string | 列类型:`selection` / `index` |
|
||||
| `fixed` | string | 固定列:`left` / `right` |
|
||||
| `render` | function | 自定义渲染函数,接收 `{ row, column, index }` |
|
||||
| `customRender` | function | 自定义渲染函数,接收 `{ record, text, index }` |
|
||||
| `isSlot` | boolean | 是否使用插槽渲染 |
|
||||
| `ellipsis` | boolean | 是否溢出省略 |
|
||||
|
||||
`render` 函数中 `record.row` 是行数据,`record.$index` 是行索引。
|
||||
`customRender` 函数中 `record` 是行数据,`text` 是当前单元格值,`index` 是行索引。
|
||||
|
||||
## 数据请求
|
||||
|
||||
@@ -154,16 +159,16 @@ function onSelectionChange(value) {
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
async function handleDelete(record) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
|
||||
await confirm('确定要删除?');
|
||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
async function handleDelete(id?: number) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除?',
|
||||
onOk: async () => {
|
||||
id ? await positionDelete(id) : await positionBatchDelete(selectionData.value);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -182,7 +187,7 @@ function reloadTable(noRefresh = '') {
|
||||
// 操作成功后刷新
|
||||
const handleAdd = async () => {
|
||||
await positionAdd(data);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
reloadTable();
|
||||
};
|
||||
```
|
||||
@@ -216,10 +221,15 @@ BasicTable 内置右侧工具栏,支持:
|
||||
const formParams = reactive({ name: '', status: '' });
|
||||
|
||||
const [register, {}] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 } as ColProps,
|
||||
submitOnReset: true,
|
||||
rowProps: { gutter: [16, 0] },
|
||||
colProps: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
},
|
||||
labelCol: { span: 6, offset: 0 },
|
||||
schemas, // querySchemas.ts 中定义
|
||||
});
|
||||
|
||||
@@ -258,9 +268,9 @@ const loadDataTable = async (res: any) => {
|
||||
| `setLoading(loading)` | 设置 loading |
|
||||
|
||||
:::tip 温馨提示
|
||||
BasicTable 内部自动处理分页参数(pageNo/pageSize),调用方只需提供请求函数。操作列的权限控制通过 `TableAction` 的 `auth` 属性实现,与后端 `@permission_required` 对应。`render` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM。
|
||||
BasicTable 内部自动处理分页参数(pageNo/pageSize),调用方只需提供请求函数。操作列的权限控制通过 `TableAction` 的 `auth` 属性实现,与后端 `@permission_required` 对应。`customRender` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列,`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。
|
||||
BasicTable 封装了列定义(含 `customRender` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列,`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
认证与通用工具函数,位于 `src/utils/auth.ts`:
|
||||
|
||||
```typescript
|
||||
import { Modal, message as antMessage } from 'ant-design-vue';
|
||||
|
||||
// 外部链接判断
|
||||
export function isExternal(path: string) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
@@ -22,17 +24,22 @@ export function getNormalPath(path: string) {
|
||||
}
|
||||
|
||||
// 确认弹窗封装
|
||||
export function confirm(msg: string, type: 'warning') {
|
||||
return ElMessageBox.confirm(msg, '温馨提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: type,
|
||||
export function confirm(msg: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Modal.confirm({
|
||||
title: '温馨提示',
|
||||
content: msg,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => reject(new Error('取消')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 消息提示封装
|
||||
export function message(msg: string, type = 'success') {
|
||||
ElMessage[type](msg);
|
||||
antMessage[type](msg);
|
||||
}
|
||||
|
||||
// 文件流下载
|
||||
@@ -233,9 +240,9 @@ export enum PageEnum {
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 ElementPlus 的简封装,在业务组件中直接调用即可。
|
||||
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 AntDesign Vue 的简封装,在业务组件中直接调用即可。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 ElementPlus 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
||||
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 AntDesign Vue 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
||||
|
||||
@@ -32,7 +32,7 @@ src/views/
|
||||
| 文件 | 职责 | 说明 |
|
||||
|------|------|------|
|
||||
| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 |
|
||||
| `edit.vue` | 编辑弹窗 | el-dialog + el-form,懒加载 |
|
||||
| `edit.vue` | 编辑弹窗 | a-modal + a-form,懒加载 |
|
||||
| `columns.ts` | 表格列定义 | 定义列 prop、label、render 函数等 |
|
||||
| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 |
|
||||
|
||||
@@ -44,10 +44,10 @@ src/views/
|
||||
<!-- src/views/system/position/index.vue -->
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<a-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
<el-card :bordered="false" class="proCard">
|
||||
</a-card>
|
||||
<a-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
@@ -57,30 +57,29 @@ src/views/
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</el-icon>
|
||||
</template>
|
||||
添加岗位
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']"
|
||||
>
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</template>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
添加岗位
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
</a-card>
|
||||
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
@@ -93,14 +92,14 @@ src/views/
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { ColProps } from 'element-plus';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
||||
import { columns } from './columns';
|
||||
import { PlusOutlined } from '@vicons/antd';
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
const positionId = ref(0);
|
||||
const editVisible = ref(false);
|
||||
@@ -167,10 +166,15 @@ src/views/
|
||||
* 注册
|
||||
*/
|
||||
const [register, {}] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 } as ColProps,
|
||||
submitOnReset: true,
|
||||
rowProps: { gutter: [16, 0] },
|
||||
colProps: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
},
|
||||
labelCol: { span: 6, offset: 0 },
|
||||
schemas,
|
||||
});
|
||||
|
||||
@@ -205,27 +209,28 @@ src/views/
|
||||
|
||||
/**
|
||||
* 执行编辑
|
||||
* @param record 参数
|
||||
* @param id 参数
|
||||
*/
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
positionId.value = record.row.id;
|
||||
const handleEdit = async (id: number) => {
|
||||
positionId.value = id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除
|
||||
* @param record 参数
|
||||
* @param id 参数
|
||||
*/
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
await confirm('确定要删除?');
|
||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
async function handleDelete(id?: number) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除?',
|
||||
onOk: async () => {
|
||||
id ? await positionDelete(id) : await positionBatchDelete(selectionData.value);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,44 +250,48 @@ src/views/
|
||||
```vue
|
||||
<!-- src/views/system/position/edit.vue -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.visible"
|
||||
<a-modal
|
||||
v-model:visible="props.visible"
|
||||
:title="props.positionId ? '编辑' : '新增'"
|
||||
width="500"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="dialogClose"
|
||||
width="500px"
|
||||
@cancel="dialogClose"
|
||||
>
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
<el-form-item
|
||||
<a-form
|
||||
class="ls-form"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ style: { width: '85px' } }"
|
||||
>
|
||||
<a-form-item
|
||||
label="岗位名称"
|
||||
prop="name"
|
||||
name="name"
|
||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||
>
|
||||
<el-input class="ls-input" v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位状态" prop="status">
|
||||
<el-radio-group v-model="formData.status" name="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" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<a-input v-model:value="formData.name" placeholder="请输入岗位名称" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="岗位状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status" name="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" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogClose">取消</el-button>
|
||||
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
|
||||
<a-button @click="dialogClose">取消</a-button>
|
||||
<a-button :loading="subLoading" type="primary" @click="submit"> 确定 </a-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||
import { onMounted, reactive, shallowRef } from 'vue';
|
||||
import { message } from '@/utils/auth';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
@@ -320,7 +329,7 @@ src/views/
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
@@ -356,8 +365,6 @@ src/views/
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</script>
|
||||
```
|
||||
|
||||
## 数据流
|
||||
@@ -375,52 +382,50 @@ src/views/
|
||||
```typescript
|
||||
// src/views/system/position/columns.ts
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { Tag } 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: 100,
|
||||
},
|
||||
{
|
||||
label: '岗位状态',
|
||||
prop: 'status',
|
||||
title: '岗位状态',
|
||||
dataIndex: 'status',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
customRender: ({ record }) => {
|
||||
return h(
|
||||
ElTag,
|
||||
Tag,
|
||||
{
|
||||
type: record.row.status == 1 ? 'success' : 'danger',
|
||||
},
|
||||
{
|
||||
default: () => (record.row.status == 1 ? '正常' : '停用'),
|
||||
color: record.status == 1 ? 'success' : 'error',
|
||||
},
|
||||
() => (record.status == 1 ? '正常' : '停用'),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
prop: 'sort',
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUser',
|
||||
title: '创建人',
|
||||
dataIndex: 'createUser',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
@@ -446,7 +451,7 @@ export const schemas: FormSchema[] = [
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
@@ -463,7 +468,7 @@ export const schemas: FormSchema[] = [
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。
|
||||
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `customRender` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
@@ -88,6 +88,8 @@ return R.failed("参数错误")
|
||||
|
||||
```typescript
|
||||
// src/utils/http/axios/index.ts
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
const { code, data, msg } = res.data;
|
||||
|
||||
@@ -98,19 +100,21 @@ transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
|
||||
// 业务失败(code === 1):弹出错误提示
|
||||
if (code === ResultEnum.ERROR) {
|
||||
ElMessage.error(msg);
|
||||
message.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// 登录超时(code === 401):弹出确认框,跳转登录页
|
||||
if (code === ResultEnum.TIMEOUT) {
|
||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false,
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
storage.clear();
|
||||
window.location.href = '/login';
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '登录身份已失效,请重新登录!',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
storage.clear();
|
||||
window.location.href = '/login';
|
||||
},
|
||||
});
|
||||
throw new Error(msg);
|
||||
}
|
||||
@@ -144,22 +148,24 @@ const result = await getPositionList(params);
|
||||
| code | 说明 | 前端行为 |
|
||||
|------|------|----------|
|
||||
| 0 | 成功 | 返回 `data` |
|
||||
| 1 | 业务失败 | `ElMessage.error(msg)` + `throw` |
|
||||
| 1 | 业务失败 | `message.error(msg)` + `throw` |
|
||||
| 401 | 未登录或 Token 过期 | 弹出确认框 → 跳转登录页 |
|
||||
|
||||
HTTP 状态码始终为 200,业务成功/失败通过 `code` 字段区分。
|
||||
|
||||
## 前端错误展示
|
||||
|
||||
业务错误通过 `ElMessage.error` 弹出提示,无需组件手动处理:
|
||||
业务错误通过 `message.error` 弹出提示,无需组件手动处理:
|
||||
|
||||
```typescript
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
// 组件中直接调用,错误由拦截器处理
|
||||
try {
|
||||
await positionAdd(formData);
|
||||
message('操作成功');
|
||||
message.success('操作成功');
|
||||
} catch (e) {
|
||||
// 拦截器已弹出 ElMessage.error,此处可选处理
|
||||
// 拦截器已弹出 message.error,此处可选处理
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -145,25 +145,25 @@ class ParamService(BaseService[Param]):
|
||||
```typescript
|
||||
// src/views/system/param/columns.ts
|
||||
{
|
||||
label: '参数类型',
|
||||
prop: 'type',
|
||||
title: '参数类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
// 方式一:直接使用后端返回的 typeText 字段
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.type == 0 ? 'primary' : 'warning' },
|
||||
{ default: () => record.row.typeText });
|
||||
customRender: ({ record }) => {
|
||||
return h(Tag,
|
||||
{ color: record.type == 0 ? 'processing' : 'warning' },
|
||||
() => record.typeText);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
// 方式二:直接使用后端返回的 statusText 字段
|
||||
render(record) {
|
||||
return h(ElTag,
|
||||
{ type: record.row.status == 1 ? 'success' : 'danger' },
|
||||
{ default: () => record.row.statusText });
|
||||
customRender: ({ record }) => {
|
||||
return h(Tag,
|
||||
{ color: record.status == 1 ? 'success' : 'error' },
|
||||
() => record.statusText);
|
||||
},
|
||||
},
|
||||
```
|
||||
@@ -205,7 +205,7 @@ export const schemas: FormSchema[] = [
|
||||
label: '参数类型',
|
||||
componentProps: {
|
||||
placeholder: '请选择参数类型',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '系统', value: 0 },
|
||||
{ label: '业务', value: 1 },
|
||||
@@ -218,7 +218,7 @@ export const schemas: FormSchema[] = [
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 2 },
|
||||
@@ -309,8 +309,8 @@ const options = [
|
||||
// 业务字段为数字
|
||||
formData.type = 0;
|
||||
|
||||
// el-option 绑定时需要转换
|
||||
<el-option :value="Number(item.value)" />
|
||||
// a-select-option 绑定时需要转换
|
||||
<a-select-option :value="Number(item.value)" />
|
||||
```
|
||||
|
||||
搜索表单中的 `options` 的 `value` 为字符串 `'0'`/`'1'`,后端 `page_eq_fields` 中的 `type` 字段期望数字。前端 API 传参时自动转换。
|
||||
@@ -353,7 +353,7 @@ biz_ —— 业务类字典
|
||||
```
|
||||
1. 后台「字典管理」中新增字典类型和字典项
|
||||
2. 后端 service 设置 serialize_maps(字段名 → 字典编码)
|
||||
3. 前端 columns.ts 使用 render 函数渲染状态标签(直接用 xxxText 字段)
|
||||
3. 前端 columns.ts 使用 customRender 函数渲染状态标签(直接用 xxxText 字段)
|
||||
4. 前端 querySchemas.ts 中配置搜索选项(硬编码或 API 获取)
|
||||
5. 表单提交时 value 为字符串,后端存储时注意类型处理
|
||||
```
|
||||
@@ -362,4 +362,4 @@ biz_ —— 业务类字典
|
||||
|
||||
## 总结
|
||||
|
||||
字典数据联调通过后端字典 API + `serialize_maps` 映射显示名 + 前端 `render` 函数渲染标签实现。`serialize_maps` 的 value 为数据字典编码,BaseService 自动查询字典并以 `xxxText` 格式输出。字典值统一为字符串,前端使用时注意类型转换。简单场景硬编码选项,动态场景通过 API 获取。
|
||||
字典数据联调通过后端字典 API + `serialize_maps` 映射显示名 + 前端 `customRender` 函数渲染标签实现。`serialize_maps` 的 value 为数据字典编码,BaseService 自动查询字典并以 `xxxText` 格式输出。字典值统一为字符串,前端使用时注意类型转换。简单场景硬编码选项,动态场景通过 API 获取。
|
||||
|
||||
@@ -51,13 +51,17 @@ def add(request: Request, data: PositionForm):
|
||||
|
||||
```vue
|
||||
<!-- src/views/system/position/index.vue -->
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
添加岗位
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']">
|
||||
删除
|
||||
</el-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加岗位
|
||||
</a-button>
|
||||
<a-button type="primary" danger @click="handleDelete()" :disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
```
|
||||
|
||||
### TableAction auth 属性
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|:----|:----|:----|
|
||||
| `UPLOAD_ALLOWED_EXTS` | `.jpg,.jpeg,.png,.gif,...,.zip` | 允许上传的文件扩展名(逗号分隔,带点号) |
|
||||
| `UPLOAD_MAX_SIZE_MB` | `10` | 上传文件大小上限(MB) |
|
||||
| `FASTAPI_FILE_URL` | `http://file.fastapi.elevue` | 文件访问域名 |
|
||||
| `FASTAPI_FILE_URL` | `http://file.fastapi.antdvue` | 文件访问域名 |
|
||||
| `FASTAPI_UPLOAD_DIR` | `uploads` | 上传文件存储目录 |
|
||||
|
||||
## 上传接口
|
||||
@@ -173,19 +173,20 @@ Service 层的 `BaseService._enrich_file_urls()` 会自动为文件字段补此
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
<a-upload
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:on-success="handleSuccess"
|
||||
@change="handleChange"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
<a-button type="primary">上传文件</a-button>
|
||||
</a-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const uploadUrl = '/api/v1/upload/uploadFile';
|
||||
const userStore = useUserStore();
|
||||
@@ -196,15 +197,18 @@ const headers = {
|
||||
function beforeUpload(file) {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('文件大小不能超过 10MB');
|
||||
message.error('文件大小不能超过 10MB');
|
||||
}
|
||||
return isLt10M;
|
||||
}
|
||||
|
||||
function handleSuccess(response) {
|
||||
if (response.code === 0) {
|
||||
// 保存 fileUrl 到业务字段
|
||||
form.value.cover = response.data.fileUrl;
|
||||
function handleChange(info) {
|
||||
if (info.file.status === 'done') {
|
||||
const response = info.file.response;
|
||||
if (response.code === 0) {
|
||||
// 保存 fileUrl 到业务字段
|
||||
form.value.cover = response.data.fileUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -214,12 +218,13 @@ function handleSuccess(response) {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-image
|
||||
<a-image
|
||||
v-if="form.cover"
|
||||
:src="getFileUrl(form.cover)"
|
||||
:preview-src-list="[getFileUrl(form.cover)]"
|
||||
:preview="{ src: getFileUrl(form.cover) }"
|
||||
:width="100"
|
||||
:height="100"
|
||||
fit="cover"
|
||||
style="width: 100px; height: 100px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user