优化更新

This commit is contained in:
2026-09-13 23:01:19 +08:00
parent 0c67b3e8b5
commit 23bd737b82
88 changed files with 2214 additions and 2503 deletions
@@ -208,17 +208,23 @@ transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
// 业务失败(code === 1)
if (code === ResultEnum.ERROR) {
ElMessage.error(msg);
// 通过 window.__message 获取消息实例(NaiveUI 的 useMessage 需在 setup 中调用)
window.__message?.error(msg);
throw new Error(msg);
}
// 登录超时(code === 401)
if (code === ResultEnum.TIMEOUT) {
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', { ... })
.then(() => {
// 通过 window.__dialog 获取对话框实例
window.__dialog?.warning({
title: '提示',
content: '登录身份已失效,请重新登录!',
positiveText: '确定',
onPositiveClick: () => {
storage.clear();
window.location.href = '/login';
});
},
});
throw new Error(msg);
}
},
@@ -291,7 +297,7 @@ const loadDataTable = async (res: any) => {
const handleSubmit = async () => {
await formRef.value?.validate();
await positionAdd(formData);
message('操作成功');
message.success('操作成功');
emit('success');
};
```
@@ -304,7 +310,7 @@ const handleSubmit = async () => {
错误处理分两层:
1. **业务错误**(code !== 0):拦截器统一弹出 `ElMessage.error(msg)`,并 `throw new Error`
1. **业务错误**(code !== 0):拦截器统一弹出错误提示,并 `throw new Error`
2. **HTTP 错误**(网络异常、超时等):`responseInterceptorsCatch` 处理
```typescript
@@ -312,12 +318,16 @@ responseInterceptorsCatch: (error: any) => {
const { response, code } = error || {};
// 超时
if (code === 'ECONNABORTED') {
ElMessage.error('接口请求超时,请刷新页面重试!');
window.__message?.error('接口请求超时,请刷新页面重试!');
return;
}
// 网络异常
if (error.toString().includes('Network Error')) {
ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', { ... });
window.__dialog?.warning({
title: '网络异常',
content: '请检查您的网络连接是否正常',
positiveText: '确定',
});
return Promise.reject(error);
}
// 其他 HTTP 状态码
@@ -66,8 +66,8 @@ Vite 按 mode 加载环境变量,后面的文件覆盖前面的:
|------|------|--------|------|
| `VITE_PORT` | `number` | `8001` | 开发服务器端口 |
| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) |
| `VITE_GLOB_APP_TITLE` | `string` | `Flask+ElementPlus版` | 应用标题(注入 HTML) |
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Flask+ElementPlus版` | 应用短名(用于生成运行时配置变量名) |
| `VITE_GLOB_APP_TITLE` | `string` | `Flask+NaiveUI版` | 应用标题(注入 HTML) |
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Flask+NaiveUI版` | 应用短名(用于生成运行时配置变量名) |
| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) |
| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 |
| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) |
@@ -141,10 +141,10 @@ dist/
```javascript
window['<%- appName %>'] = {
VITE_GLOB_APP_TITLE: 'Flask+ElementPlus版',
VITE_GLOB_APP_TITLE: 'Flask+NaiveUI版',
VITE_GLOB_API_URL: '',
VITE_GLOB_API_URL_PREFIX: '/api',
VITE_GLOB_FILE_URL: 'http://file.flask.elevue',
VITE_GLOB_FILE_URL: 'http://file.flask.naivevue',
};
```
@@ -1,7 +1,7 @@
# 公共组件库
:::tip 说明
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 ElementPlus 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 NaiveUI 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `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/` | 图标(支持 SVG 图标 + @vicons 图标库) |
| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) |
| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) |
| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) |
@@ -38,7 +38,7 @@
## BasicTable 表格
表格组件是使用频率最高的组件,基于 `el-table` + `el-pagination` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
表格组件是使用频率最高的组件,基于 NaiveUI 的 `n-data-table` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
### Props
@@ -77,12 +77,12 @@
| 属性 | 类型 | 说明 |
|------|------|------|
| `label` | `string` | 列标题 |
| `prop` | `string` | 字段名 |
| `title` | `string` | 列标题 |
| `key` | `string` | 字段名 |
| `width` | `number` | 列宽 |
| `align` | `string` | 对齐方式 |
| `type` | `string` | 类型:`index` / `selection` |
| `render` | `Function` | 自定义渲染函数 `(column, row, index) => VNode` |
| `type` | `string` | 类型:`selection` / `index` |
| `render` | `Function` | 自定义渲染函数 `(record) => VNode` |
| `isSlot` | `boolean` | 是否使用插槽 |
| `ellipsis` | `boolean` | 超出省略(tooltip) |
| `auth` | `string[]` | 权限编码,控制列是否显示 |
@@ -94,7 +94,7 @@
|------|------|------|
| `fetch-success` | `(data, result)` | 数据请求成功 |
| `fetch-error` | `(error)` | 数据请求失败 |
| `selection-change` | `(rows)` | 多选变化 |
| `update:checked-row-keys` | `(keys)` | 多选变化 |
| `columns-change` | `(columns)` | 列设置变化 |
| `checked-row-change` | `(rowKeys)` | 选中行变化 |
@@ -104,7 +104,7 @@
|------|------|
| `tableTitle` | 表格标题左侧区域(常放添加按钮) |
| `toolbar` | 工具栏右侧区域 |
| `{prop}` | 列名对应插槽,自定义列渲染 |
| `{key}` | 列名对应插槽,自定义列渲染 |
### Expose 方法
@@ -129,18 +129,21 @@
<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>
<n-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
<template #icon><n-icon><PlusOutlined /></n-icon></template>
新增
</n-button>
</template>
</BasicTable>
</template>
<script setup lang="ts">
import { PlusOutlined } from '@vicons/antd';
const columns = [
{ label: '名称', prop: 'name', width: 200 },
{ label: '状态', prop: 'status', width: 100 },
{ label: '创建时间', prop: 'createTime', width: 180 },
{ title: '名称', key: 'name', width: 200 },
{ title: '状态', key: 'status', width: 100 },
{ title: '创建时间', key: 'createTime', width: 180 },
];
const actionColumn = {
@@ -228,13 +231,13 @@ async function loadDataTable({ page, pageSize }) {
|------|------|------|
| `field` | `string` | 字段名(必填) |
| `label` | `string` | 标签文字 |
| `component` | `ComponentType` | 渲染组件:`Input` / `Select` / `DatePicker` 等 |
| `component` | `ComponentType` | 渲染组件:`NInput` / `NSelect` / `NDatePicker` 等 |
| `componentProps` | `object` | 组件属性 |
| `defaultValue` | `any` | 默认值 |
| `rules` | `object \| object[]` | 字段校验规则 |
| `slot` | `string` | 自定义插槽名 |
| `hidden` | `boolean \| Function` | 是否隐藏 |
| `colProps` | `ColProps` | 栅格配置 |
| `gridProps` | `GridProps` | 栅格配置 |
| `labelMessage` | `string` | 标签提示信息 |
| `suffix` | `string` | 后缀文字 |
@@ -270,13 +273,13 @@ const [registerForm] = useForm({
{
field: 'name',
label: '名称',
component: 'Input',
component: 'NInput',
componentProps: { placeholder: '请输入名称' },
},
{
field: 'status',
label: '状态',
component: 'Select',
component: 'NSelect',
componentProps: {
options: [
{ label: '启用', value: 1 },
@@ -287,7 +290,7 @@ const [registerForm] = useForm({
{
field: 'dateRange',
label: '日期范围',
component: 'DatePicker',
component: 'NDatePicker',
componentProps: { type: 'daterange' },
},
],
@@ -299,7 +302,7 @@ const [registerForm] = useForm({
## BasicModal 弹窗
弹窗组件基于 `el-dialog` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
弹窗组件基于 NaiveUI 的 `n-modal` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
### Props
@@ -318,7 +321,8 @@ const [registerForm] = useForm({
| 事件 | 说明 |
|------|------|
| `ok` | 点击确认 |
| `on-ok` | 点击确认 |
| `on-close` | 点击关闭 |
| `register` | 弹窗注册回调 |
### Expose 方法
@@ -333,9 +337,9 @@ const [registerForm] = useForm({
```vue
<template>
<BasicModal title="编辑用户" :width="600" @register="registerModal" @ok="handleSubmit">
<basicModal title="编辑用户" :width="600" @register="registerModal" @on-ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</basicModal>
</template>
<script setup lang="ts">
@@ -356,7 +360,7 @@ function handleEdit(record) {
## BasicUpload 上传
文件/图片上传组件,基于 `el-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
文件/图片上传组件,基于 NaiveUI 的 `n-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
### Props
@@ -698,10 +702,10 @@ async function getDeptOptions() {
<BasicTable :columns="columns" :request="loadData" />
<template #leftFooter>
<el-button @click="handleCancel">取消</el-button>
<n-button @click="handleCancel">取消</n-button>
</template>
<template #rightFooter>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<n-button type="primary" @click="handleSubmit">提交</n-button>
</template>
</PageWrapper>
</template>
@@ -724,7 +728,7 @@ async function getDeptOptions() {
```vue
<template>
<Authority :value="['sys:user:add']">
<el-button type="primary">新增用户</el-button>
<n-button type="primary">新增用户</n-button>
</Authority>
</template>
```
@@ -732,7 +736,7 @@ async function getDeptOptions() {
:::tip v-perm 指令
除了 `Authority` 组件,项目还提供 `v-perm` 指令,功能相同但更简洁:
```vue
<el-button v-perm="['sys:user:add']">新增用户</el-button>
<n-button v-perm="['sys:user:add']">新增用户</n-button>
```
:::
@@ -740,7 +744,7 @@ async function getDeptOptions() {
## Icon 图标
统一图标组件,支持 ElementPlus 图标和 SVG 图标。
统一图标组件,支持 SVG 图标和 @vicons 图标库。
### Props
@@ -754,11 +758,15 @@ async function getDeptOptions() {
```vue
<template>
<!-- ElementPlus 图标 -->
<Icon name="el-icon-Plus" size="18px" />
<!-- SVG 图标 -->
<Icon name="svg-icon-user" />
<!-- @vicons 图标 -->
<n-icon size="18"><PlusOutlined /></n-icon>
</template>
<script setup>
import { PlusOutlined } from '@vicons/antd';
</script>
```
---
@@ -836,4 +844,4 @@ setupCustomComponents(app); // 注册所有自定义组件
## 总结
公共组件库基于 ElementPlus 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
公共组件库基于 NaiveUI 二次封装,核心三件套 BasicTable(表格)、BasicForm(表单)、BasicModal(弹窗)覆盖列表页标准场景。辅以 Upload、Editor、Cropper 等业务组件,以及 Authority、v-perm 权限体系。通过全局注册,任何页面直接使用,无需单独 import。优先使用公共组件,避免重复开发。
@@ -1,7 +1,7 @@
# BasicForm 表单
:::tip 说明
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 ElementPlus 的 `el-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 NaiveUI 的 `n-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
:::
## 组件架构
@@ -24,9 +24,9 @@ src/components/Form/
列表页的搜索区域使用 `useForm` 注册表单:
```vue
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<n-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</el-card>
</n-card>
```
### querySchemas 定义
@@ -39,7 +39,7 @@ import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
component: 'NInput',
label: '岗位名称',
componentProps: {
placeholder: '请输入岗位名称',
@@ -47,7 +47,7 @@ export const schemas: FormSchema[] = [
},
{
field: 'status',
component: 'Select',
component: 'NSelect',
label: '状态',
componentProps: {
placeholder: '请选择状态',
@@ -66,14 +66,12 @@ 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,
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
submitOnReset: true,
schemas,
});
@@ -93,73 +91,73 @@ function handleReset() {
## 业务表单(编辑弹窗)
编辑弹窗中直接使用 `el-form` + `el-form-item` 构建表单:
编辑弹窗中直接使用 `n-form` + `n-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
label="岗位名称"
prop="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>
<basicModal @register="modalRegister" ref="modalRef" @on-ok="handleSubmit" @on-close="handleClose">
<template #default>
<n-form class="ls-form" ref="formRef" :model="formData" label-placement="left" label-width="85px">
<n-form-item
label="岗位名称"
path="name"
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
>
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
</n-form-item>
<n-form-item label="岗位状态" path="status">
<n-radio-group v-model:value="formData.status" name="status">
<n-radio :value="1">正常</n-radio>
<n-radio :value="2">停用</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formData.sort" />
</n-form-item>
</n-form>
</template>
</basicModal>
</template>
```
## BasicForm 内部渲染机制
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 ElementPlus 组件:
`BasicForm.vue` 根据 `schema.component` 动态渲染对应的 NaiveUI 组件:
```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">
<template v-if="schema.component === 'NCheckbox'">
<n-checkbox-group v-model:value="formModel[schema.field]">
<n-checkbox v-for="item in schema.componentProps?.options" :value="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</n-checkbox>
</n-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">
<template v-else-if="schema.component === 'NRadioGroup'">
<n-radio-group v-model:value="formModel[schema.field]">
<n-radio v-for="item in schema.componentProps?.options" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</n-radio>
</n-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>
<template v-else-if="schema.component === 'NSelect'">
<n-select v-model:value="formModel[schema.field]" :options="schema.componentProps?.options" />
</template>
<!-- Daterange -->
<template v-else-if="schema.component === 'Daterange'">
<el-date-picker v-model="formModel[schema.field]" type="daterange" value-format="YYYY-MM-DD" />
<template v-else-if="schema.component === 'NDatePicker'">
<n-date-picker v-model:value="formModel[schema.field]" type="daterange" value-format="yyyy-MM-dd" />
</template>
<!-- 其他组件:动态渲染 el-{component} -->
<component v-else :is="`el-${schema.component}`" v-model="formModel[schema.field]" />
<!-- 其他组件:动态渲染 n-{component} -->
<component v-else :is="schema.component" v-model:value="formModel[schema.field]" v-bind="schema.componentProps" />
</template>
```
@@ -169,7 +167,7 @@ function handleReset() {
interface FormSchema {
field: string; // 字段名
label: string; // 标签文本
component: string; // 组件类型:Input / Select / RadioGroup / Checkbox / DatePicker 等
component: string; // 组件类型:NInput / NSelect / NRadioGroup / NCheckbox / NDatePicker 等
componentProps?: any; // 组件属性(placeholder、options、clearable 等)
required?: boolean; // 是否必填
rules?: any[]; // 自定义校验规则
@@ -202,7 +200,7 @@ Schema 中的 `required: true` 会自动生成必填校验规则,也可以通
{
field: 'name',
label: '岗位名称',
component: 'Input',
component: 'NInput',
rules: [
{ required: true, message: '请输入岗位名称', trigger: 'blur' },
{ min: 1, max: 150, message: '长度在 1 到 150 个字符', trigger: 'blur' },
@@ -237,4 +235,4 @@ onMounted(() => {
## 总结
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 ElementPlus 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `el-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 NaiveUI 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `n-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
@@ -120,7 +120,7 @@ import { useLockFn } from '@/utils/useLockFn';
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 +130,7 @@ const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
```
```vue
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
<n-button :loading="subLoading" type="primary" @click="submit">确定</n-button>
```
## useAsync
@@ -1,25 +1,39 @@
# 国际化
:::tip 说明
当前项目仅支持中文界面,ElementPlus 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
当前项目仅支持中文界面,NaiveUI 通过 `n-config-provider` 的 `locale` 属性配置中文语言包,所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
:::
## 当前状态
项目当前为中文单语言版本:
- ElementPlus 全局配置中文语言包:`app.use(ElementPlus, { locale: zhCn })`
- 所有界面文本直接硬编码在组件中:`<el-button>添加岗位</el-button>`
- NaiveUI 通过 `n-config-provider` 全局配置中文语言包:`:locale="zhCN"`
- 所有界面文本直接硬编码在组件中:`<n-button>添加岗位</n-button>`
- 后端返回的 `msg` 字段为中文:`"操作成功"`、`"岗位名称不能重复"`
```vue
<template>
<el-button type="primary">添加岗位</el-button>
<el-table-column label="岗位名称" prop="name" />
<el-table-column label="创建时间" prop="createTime" />
<n-button type="primary">添加岗位</n-button>
</template>
```
## NaiveUI 语言配置
NaiveUI 通过 `n-config-provider` 组件配置语言:
```vue
<template>
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
<router-view />
</n-config-provider>
</template>
<script setup>
import { zhCN, dateZhCN } from 'naive-ui';
</script>
```
## 扩展方案
如需支持多语言,推荐使用 vue-i18n:
@@ -65,8 +79,7 @@ export default i18n;
```vue
<template>
<el-button type="primary">{{ $t('common.add') }}</el-button>
<el-table-column :label="$t('position.name')" prop="name" />
<n-button type="primary">{{ $t('common.add') }}</n-button>
</template>
<script setup>
@@ -75,19 +88,27 @@ import { useI18n } from 'vue-i18n';
</script>
```
### ElementPlus 国际化
### NaiveUI 国际化
```typescript
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
```vue
<template>
<n-config-provider :locale="currentLocale" :date-locale="currentDateLocale">
<router-view />
</n-config-provider>
</template>
app.use(ElementPlus, { locale: zhCn });
<script setup>
import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui';
const currentLocale = computed(() => lang === 'zh-CN' ? zhCN : enUS);
const currentDateLocale = computed(() => lang === 'zh-CN' ? dateZhCN : dateEnUS);
</script>
```
:::tip 温馨提示
国际化改造工作量较大,建议在项目初期规划。如仅需中文,当前硬编码方式即可满足需求。改造时需要同时处理前端文本和后端返回的 `msg` 字段。
国际化改造工作量较大,建议在项目初期规划。如仅需中文,当前硬编码方式即可满足需求。改造时需要同时处理前端文本和后端返回的 `msg` 字段。NaiveUI 内置中文语言包,无需额外安装。
:::
## 总结
当前项目为中文单语言版本,ElementPlus 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 ElementPlus 内置语言包和后端消息 code 实现全链路多语言。
当前项目为中文单语言版本,NaiveUI 通过 `n-config-provider` 的 `:locale="zhCN"` 配置中文。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 NaiveUI 内置语言包和后端消息 code 实现全链路多语言。
@@ -1,7 +1,7 @@
# Modal 弹窗
:::tip 说明
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `basicModal` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
:::
## 组件架构
@@ -9,7 +9,7 @@
```
src/components/Modal/
├── src/
│ ├── BasicModal.vue # 主组件(基于 el-dialog)
│ ├── BasicModal.vue # 主组件(基于 n-modal)
│ ├── props.ts # 属性定义
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
│ └── hooks/
@@ -19,12 +19,12 @@ src/components/Modal/
## BasicModal 组件
`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力:
`BasicModal` 基于 NaiveUI 的 `n-modal` 封装,提供全屏切换、自定义头部、提交按钮等能力:
```vue
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
<basicModal title="编辑" :width="600" @register="registerModal" @on-ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</basicModal>
```
核心功能:
@@ -36,36 +36,40 @@ src/components/Modal/
## 实际用法(position 模块)
以岗位管理为例,实际使用原生 `el-dialog` + `defineAsyncComponent` 懒加载:
以岗位管理为例,使用 `basicModal` + `useModal` hook:
### 父组件(index.vue)
```vue
<!-- src/views/system/position/index.vue -->
<script setup>
import { defineAsyncComponent } from 'vue';
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
import { nextTick, ref } from 'vue';
import editDialog from './edit.vue';
const positionId = ref(0);
const editVisible = ref(false);
const createModalRef = ref();
// 新增
const handleAdd = async () => {
positionId.value = 0;
await nextTick();
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
// 编辑
const handleEdit = async (record: Recordable) => {
positionId.value = record.row.id;
await nextTick();
async function handleEdit(record) {
positionId.value = record.id;
editVisible.value = true;
};
await nextTick();
createModalRef.value.openModal();
}
</script>
<template>
<editDialog
ref="createModalRef"
v-if="editVisible"
:positionId="positionId"
v-model:visible="editVisible"
@@ -74,51 +78,50 @@ const handleEdit = async (record: Recordable) => {
</template>
```
`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。
### 编辑弹窗(edit.vue)
```vue
<!-- src/views/system/position/edit.vue -->
<template>
<el-dialog
v-model="props.visible"
:title="props.positionId ? '编辑' : '新增'"
width="500"
:close-on-click-modal="false"
:before-close="dialogClose"
<basicModal
@register="modalRegister"
ref="modalRef"
class="basicModal basicFormModal"
@on-ok="handleSubmit"
@on-close="handleClose"
>
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
<el-form-item
label="岗位名称"
prop="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>
<template #footer>
<el-button @click="dialogClose">取消</el-button>
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
<template #default>
<n-form class="ls-form" ref="formRef" :model="formData" label-placement="left" label-width="85px">
<n-form-item
label="岗位名称"
path="name"
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
>
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
</n-form-item>
<n-form-item label="岗位状态" path="status">
<n-radio-group v-model:value="formData.status" name="status">
<n-radio :value="1">正常</n-radio>
<n-radio :value="2">停用</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formData.sort" />
</n-form-item>
</n-form>
</template>
</el-dialog>
</basicModal>
</template>
<script lang="ts" setup>
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { useLockFn } from '@/utils/useLockFn';
import { onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useModal } from '@/components/Modal';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
const formRef = ref();
const message = useMessage();
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
@@ -127,22 +130,31 @@ const props = defineProps({
positionId: { type: Number, required: true, default: 0 },
});
// 提交处理
const [modalRegister, { openModal, setSubLoading }] = useModal({
title: props.positionId ? '编辑岗位' : '添加岗位',
subBtuText: '确定',
width: 600,
});
const handleSubmit = async () => {
await formRef.value?.validate();
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message('操作成功');
emit('update:visible', false);
emit('success');
formRef.value
.validate()
.then(async () => {
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message.success('操作成功');
setSubLoading(false);
emit('update:visible', false);
emit('success');
})
.catch(() => {
setSubLoading(false);
});
};
// 关闭弹窗
const dialogClose = () => { emit('update:visible', false); };
const handleClose = () => {
emit('update:visible', false);
};
// useLockFn 防重复提交,isLock 绑定按钮 loading
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
// 编辑时加载数据
const setFormData = async () => {
const data = await getPositionDetail(props.positionId);
for (const key in formData) {
@@ -155,42 +167,54 @@ const setFormData = async () => {
onMounted(() => {
if (props.positionId) setFormData();
});
defineExpose({ openModal });
</script>
```
## 弹窗数据流
```
父组件点击编辑 → positionId = record.row.id → editVisible = true
父组件点击编辑 → positionId = record.id → editVisible = true
↓
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
↓
用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)
用户修改 → submit → formRef.validate() → positionUpdate(formData)
↓
emit('success') → 父组件 reloadTable() → 表格刷新
```
## 确认弹窗
删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`):
删除等危险操作使用 NaiveUI 的 `useDialog`:
```typescript
import { message, confirm } from '@/utils/auth';
import { useMessage, useDialog } from 'naive-ui';
async function handleDelete(record: Recordable) {
const message = useMessage();
const dialog = useDialog();
async function handleDelete(record) {
let ids = [];
if (!record) {
ids = selectionData.value.map(({ id }) => id);
ids = rowKeys.value;
}
await confirm('确定要删除?');
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
message('删除成功');
reloadTable();
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record?.id ? await positionDelete(record.id) : await positionBatchDelete(ids);
message.success('删除成功');
reloadTable();
},
});
}
```
`confirm` 内部调用 `ElMessageBox.confirm`,返回 Promise,用户取消时抛出异常中断后续逻辑。
`dialog.warning()` 是 NaiveUI 的命令式对话框,返回 Promise,用户取消时不执行 `onPositiveClick` 回调。
## v-model:visible 双向绑定
@@ -203,14 +227,14 @@ async function handleDelete(record: Recordable) {
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:visible']);
const dialogClose = () => { emit('update:visible', false); };
const handleClose = () => { emit('update:visible', false); };
</script>
```
:::tip 温馨提示
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useModal` hook 提供 `openModal`、`closeModal`、`setSubLoading` 等方法。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。消息提示使用 `useMessage()`,确认弹窗使用 `useDialog()`。
:::
## 总结
编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
编辑弹窗使用 `basicModal`(基于 `n-modal`)+ `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。消息提示使用 `useMessage()`,确认对话框使用 `useDialog()`。
@@ -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>
<n-button v-perm="['sys:position:add']">添加岗位</n-button>
<n-button v-perm="['sys:position:batchDelete']">删除</n-button>
<n-button v-perm="['sys:position:export']">导出</n-button>
```
### v-perms(满足所有权限)
```vue
<el-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</el-button>
<n-button v-perms="['sys:user:add', 'sys:user:import']">高级操作</n-button>
```
### 与 v-if 结合
@@ -113,7 +113,7 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
```
```vue
<el-button v-if="showAddBtn">添加岗位</el-button>
<n-button v-if="showAddBtn">添加岗位</n-button>
```
## 实际用法(position 模块)
@@ -123,20 +123,26 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
```vue
<!-- src/views/system/position/index.vue -->
<template>
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef" :actionColumn="actionColumn">
<BasicTable :columns="columns" :request="loadDataTable" ref="basicTableRef" :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>
<n-space>
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
<template #icon><n-icon><PlusOutlined /></n-icon></template>
新建
</n-button>
<n-button type="error" @click="handleDelete" :disabled="!rowKeys.length"
v-perm="['sys:position:batchDelete']">
<template #icon><n-icon><DeleteOutlined /></n-icon></template>
删除
</n-button>
</n-space>
</template>
</BasicTable>
</template>
<script setup>
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
</script>
```
操作列中的权限控制通过 `auth` 属性实现:
@@ -144,23 +150,25 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
```typescript
const actionColumn = reactive({
width: 200,
label: '操作',
title: '操作',
align: 'center',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction, {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
icon: 'Edit',
type: 'warning',
icon: renderIcon(FormOutlined),
type: 'info',
onClick: handleEdit.bind(null, record),
auth: ['sys:position:update'], // 权限节点
},
{
label: '删除',
icon: 'Delete',
type: 'danger',
icon: renderIcon(DeleteOutlined),
type: 'error',
onClick: handleDelete.bind(null, record),
auth: ['sys:position:delete'], // 权限节点
},
@@ -202,7 +210,7 @@ def add(request: Request, data: PositionForm):
```vue
<!-- 前端 -->
<el-button v-perm="['sys:position:add']">添加岗位</el-button>
<n-button v-perm="['sys:position:add']">添加岗位</n-button>
```
## 超级管理员
@@ -1,7 +1,7 @@
# 前端环境与结构
:::tip 说明
前端基于 Vue3 + ElementPlus 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
前端基于 Vue3 + NaiveUI 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
:::
## 技术栈
@@ -9,11 +9,11 @@
| 技术 | 版本 | 说明 |
|------|------|------|
| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API |
| ElementPlus | 2.x | Vue3 组件库 |
| NaiveUI | 2.x | Vue3 组件库 |
| Vite | 3.x | 下一代前端构建工具 |
| Pinia | 2.x | 状态管理(Vuex 替代方案) |
| Vue Router | 4.x | 路由管理 |
| Axios | 1.x | HTTP 请求库 |
| Axios | 0.27.x | HTTP 请求库 |
| TypeScript | 5.x | 类型安全的 JavaScript 超集 |
| Tailwind CSS | 3.x | 原子化 CSS 框架 |
| SCSS | - | CSS 预处理器 |
@@ -82,34 +82,23 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
const { VITE_PUBLIC_PATH, VITE_PORT, VITE_GLOB_PROD_MOCK } = viteEnv;
let VITE_PROXY: any = viteEnv.VITE_PROXY;
// VITE_PROXY 容错处理
if (!VITE_PROXY) {
VITE_PROXY = '[[]]';
}
if (!VITE_PROXY) { VITE_PROXY = '[[]]'; }
try {
if (typeof VITE_PROXY === 'string') {
VITE_PROXY = JSON.parse(VITE_PROXY);
}
} catch (e) {
VITE_PROXY = [[]];
}
if (typeof VITE_PROXY === 'string') { VITE_PROXY = JSON.parse(VITE_PROXY); }
} catch (e) { VITE_PROXY = [[]]; }
const isBuild = command === 'build';
return {
base: VITE_PUBLIC_PATH,
resolve: {
alias: [
{ find: '@', replacement: pathResolve('src') + '/' },
],
alias: [{ find: '@', replacement: pathResolve('src') + '/' }],
dedupe: ['vue'],
},
plugins: createVitePlugins(viteEnv, isBuild, prodMock),
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "src/styles/var.scss";`,
},
scss: { additionalData: `@import "src/styles/var.scss";` },
},
},
server: {
@@ -140,24 +129,16 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
```typescript
import './styles/tailwindbase.css';
import 'element-plus/dist/index.css';
import './styles/index.scss';
import * as ElementPlusIconsVue from '@element-plus/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 { setupDirectives, setupCustomComponents } from '@/plugins';
async function bootstrap() {
const app = createApp(App);
// 注册所有 ElementPlus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
setupElement(app); // 全局引入 ElementPlus 组件
setupCustomComponents(app); // 注册自定义组件
setupDirectives(app); // 注册自定义指令(v-perm 等)
setupStore(app); // 挂载 Pinia 状态管理
@@ -170,7 +151,7 @@ async function bootstrap() {
void bootstrap();
```
初始化顺序:样式加载 -> ElementPlus -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
初始化顺序:样式加载 -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
## 核心目录说明
@@ -180,7 +161,7 @@ API 接口定义目录,按模块组织文件。公共接口在 `api/common/`
### components/
公共组件目录,基于 ElementPlus 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
公共组件目录,基于 NaiveUI 二次封装,包含 `Table`(BasicTable 表格)、`Form`(BasicForm 表单)、`Modal`(BasicModal 弹窗)、`Upload`、`Editor`、`Cropper`、`Select` 等。
### directives/
@@ -212,4 +193,4 @@ Pinia 状态管理目录,核心模块:
## 总结
前端项目基于 Vue3 + ElementPlus + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
前端项目基于 Vue3 + NaiveUI + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
@@ -1,7 +1,7 @@
# 样式与主题
:::tip 说明
前端样式方案采用 Tailwind CSS + SCSS + ElementPlus 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
前端样式方案采用 Tailwind CSS + SCSS + NaiveUI 主题系统,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
:::
## 样式架构
@@ -10,8 +10,7 @@
src/styles/
├── var.scss # SCSS 全局变量(通过 Vite additionalData 注入)
├── index.scss # 入口文件,引入所有样式
├── tailwindbase.css # Tailwind CSS 基础样式
└── element-override.css # ElementPlus 主题覆盖(可选)
└── tailwindbase.css # Tailwind CSS 基础样式
```
## SCSS 全局变量
@@ -56,39 +55,42 @@ import './styles/tailwindbase.css';
<div class="w-full md:w-1/2 lg:w-1/3">响应式宽度</div>
```
## ElementPlus 主题
## NaiveUI 主题
ElementPlus 通过 CSS 变量支持主题定制:
NaiveUI 通过 CSS 变量和主题覆盖机制支持主题定制:
```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 变量
// App.vue 或 Application 组件中
import { darkTheme, zhCN, dateZhCN } from 'naive-ui';
// 通过 n-config-provider 全局配置
<n-config-provider :theme="isDark ? darkTheme : null" :locale="zhCN" :date-locale="dateZhCN">
<router-view />
</n-config-provider>
```
### ElementPlus 全局引入
### NaiveUI 全局配置
```typescript
// src/plugins/setupElement.ts
import ElementPlus from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
export function setupElement(app) {
app.use(ElementPlus, { locale: zhCn, size: 'default' });
}
// NaiveUI 组件按需使用,无需全局引入
// 通过 n-config-provider 提供全局主题和语言配置
```
## 深色模式
通过 ElementPlus 的 `dark/css-vars.css` 和 Tailwind 的 `dark` 模式实现深色主题:
通过 NaiveUI 的 `darkTheme` 和 Tailwind 的 `dark` 模式实现深色主题:
```vue
<template>
<el-switch v-model="isDark" @change="toggleDark" />
<n-switch v-model:value="isDark" @change="toggleDark">
<template #checked>暗</template>
<template #unchecked>亮</template>
</n-switch>
</template>
<script setup>
import { darkTheme } from 'naive-ui';
const isDark = ref(false);
const toggleDark = (val) => {
document.documentElement.classList.toggle('dark', val);
@@ -96,15 +98,7 @@ const toggleDark = (val) => {
</script>
```
深色模式样式:
```css
.dark {
--el-bg-color: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-border-color: #4c4d4f;
}
```
深色模式样式通过 NaiveUI 的 CSS 变量自动适配,无需手动覆盖。
## 设计配置 Store
@@ -130,12 +124,12 @@ export function useDesignSetting() {
```vue
<template>
<PageWrapper>
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<n-card :bordered="false" class="pt-3 mb-3 proCard">
<!-- 搜索区域 -->
</el-card>
<el-card :bordered="false" class="proCard">
</n-card>
<n-card :bordered="false" class="proCard">
<!-- 表格区域 -->
</el-card>
</n-card>
</PageWrapper>
</template>
```
@@ -145,9 +139,9 @@ export function useDesignSetting() {
`proCard` 是项目定义的卡片样式类,用于统一卡片组件的间距和边框。
:::tip 温馨提示
优先使用 Tailwind 原子类实现样式,避免编写自定义 CSS。需要全局覆盖时使用 CSS 变量。SCSS 变量通过 Vite `additionalData` 自动注入,无需每个文件手动引入。
优先使用 Tailwind 原子类实现样式,避免编写自定义 CSS。需要全局覆盖时使用 CSS 变量。SCSS 变量通过 Vite `additionalData` 自动注入,无需每个文件手动引入。NaiveUI 主题通过 `n-config-provider` 的 `theme` 属性控制,深色模式使用 `darkTheme`。
:::
## 总结
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ ElementPlus 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ NaiveUI 主题系统,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。NaiveUI 通过 `n-config-provider` 实现全局主题和语言配置。
@@ -1,7 +1,7 @@
# BasicTable 表格
:::tip 说明
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 ElementPlus 的 `el-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 NaiveUI 的 `n-data-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
:::
## 组件架构
@@ -35,10 +35,13 @@ src/components/Table/
:row-key="(row) => row.id"
ref="tableRef"
:actionColumn="actionColumn"
@selection-change="onSelectionChange"
@update:checked-row-keys="onCheckedRow"
>
<template #tableTitle>
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">添加岗位</el-button>
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
<template #icon><n-icon><PlusOutlined /></n-icon></template>
添加岗位
</n-button>
</template>
</BasicTable>
```
@@ -51,27 +54,31 @@ src/components/Table/
// src/views/system/position/columns.ts
import { h } from 'vue';
import { ElTag } from 'element-plus';
import { NTag } from 'naive-ui';
export const columns = [
{ type: 'selection' },
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
{ label: '岗位名称', prop: 'name', minWidth: 100 },
{ type: 'selection', width: 50, fixed: 'left' },
{ title: 'ID', key: 'id', fixed: 'left', width: 50 },
{ title: '岗位名称', key: 'name', width: 100 },
{
label: '岗位状态',
prop: 'status',
minWidth: 100,
title: '岗位状态',
key: 'status',
width: 100,
render(record) {
return h(
ElTag,
{ type: record.row.status == 1 ? 'success' : 'danger' },
{ default: () => (record.row.status == 1 ? '正常' : '停用') },
NTag,
{
style: { marginRight: '6px' },
bordered: false,
type: record.status == 1 ? 'success' : 'error',
},
{ default: () => (record.status == 1 ? '正常' : '停用') },
);
},
},
{ label: '排序', prop: 'sort', minWidth: 100 },
{ label: '创建人', prop: 'createUser', minWidth: 100 },
{ label: '创建时间', prop: 'createTime', width: 180 },
{ title: '排序', key: 'sort', width: 100 },
{ title: '创建人', key: 'createUser', width: 100 },
{ title: '创建时间', key: 'createTime', width: 180 },
];
```
@@ -79,8 +86,8 @@ export const columns = [
| 属性 | 类型 | 说明 |
|------|------|------|
| `prop` | string | 字段名 |
| `label` | string | 列标题 |
| `key` | string | 字段名 |
| `title` | string | 列标题 |
| `width` | number | 固定宽度 |
| `minWidth` | number | 最小宽度 |
| `type` | string | 列类型:`selection` / `index` |
@@ -89,7 +96,7 @@ export const columns = [
| `isSlot` | boolean | 是否使用插槽渲染 |
| `ellipsis` | boolean | 是否溢出省略 |
`render` 函数中 `record.row` 是行数据,`record.$index` 是行索引。
`render` 函数中 `record` 直接是行数据对象,通过 `record.key` 访问字段值。
## 数据请求
@@ -110,27 +117,30 @@ const loadDataTable = async (res: any) => {
```typescript
import { TableAction } from '@/components/Table';
import { renderIcon } from '@/utils';
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
const actionColumn = reactive({
width: 200,
label: '操作',
prop: 'action',
title: '操作',
align: 'center',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction, {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
icon: 'Edit',
type: 'warning',
icon: renderIcon(FormOutlined),
type: 'info',
onClick: handleEdit.bind(null, record),
auth: ['sys:position:update'], // 权限节点
},
{
label: '删除',
icon: 'Delete',
type: 'danger',
icon: renderIcon(DeleteOutlined),
type: 'error',
onClick: handleDelete.bind(null, record),
auth: ['sys:position:delete'], // 权限节点
},
@@ -144,26 +154,33 @@ const actionColumn = reactive({
## 多选
通过 `selection-change` 事件获取选中行:
通过 `update:checked-row-keys` 事件获取选中行:
```typescript
const selectionData = ref([]);
const rowKeys = ref([]);
function onSelectionChange(value) {
selectionData.value = value;
function onCheckedRow(keys) {
rowKeys.value = keys;
}
// 批量删除
async function handleDelete(record) {
let ids = [];
if (!record) {
ids = selectionData.value.map(({ id }) => id);
ids = rowKeys.value;
}
await confirm('确定要删除?');
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
message('删除成功');
reloadTable();
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record?.id ? await positionDelete(record.id) : await positionBatchDelete(ids);
message.success('删除成功');
reloadTable();
},
});
}
```
@@ -172,17 +189,17 @@ async function handleDelete(record) {
通过 ref 调用 `reload` 或 `reloadTable` 方法:
```typescript
const tableRef = ref();
const basicTableRef = ref();
// 刷新到第一页
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
// 操作成功后刷新
const handleAdd = async () => {
await positionAdd(data);
message('操作成功');
message.success('操作成功');
reloadTable();
};
```
@@ -217,22 +234,23 @@ const formParams = reactive({ name: '', status: '' });
const [register, {}] = useForm({
labelWidth: 80,
layout: 'horizontal',
colProps: { span: 6 } as ColProps,
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
submitOnReset: true,
schemas, // querySchemas.ts 中定义
});
// 表单提交:更新查询参数,刷新表格
function handleSubmit(values: Recordable) {
handleReset();
for (const key in formParams) { formParams[key] = ''; }
for (const key in values) { formParams[key] = values[key]; }
reloadTable();
}
// 表单重置:清空查询参数,刷新表格
function handleReset() {
function handleReset(values: Recordable) {
for (const key in formParams) { formParams[key] = ''; }
for (const key in values) { formParams[key] = values[key]; }
reloadTable();
}
// 数据请求:合并表单参数和分页参数
@@ -258,9 +276,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` 对应。`render` 函数使用 Vue 的 `h()` 函数创建虚拟 DOM,直接接收行数据对象,通过 `record.key` 访问字段值。
:::
## 总结
BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列,`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。
BasicTable 封装了列定义(含 `render` 函数自定义渲染)、数据请求(自动分页和 loading)、操作列(`TableAction` + 权限控制)、多选、全屏、密度、列设置等表格常用能力。通过 `columns` 配置化定义列(使用 `title`/`key` 而非 `label`/`prop`),`request` 函数自动管理数据加载,`actionColumn` 配置操作按钮。
@@ -21,20 +21,6 @@ export function getNormalPath(path: string) {
return newPath[newPath.length - 1] === '/' ? newPath.slice(0, -1) : newPath;
}
// 确认弹窗封装
export function confirm(msg: string, type: 'warning') {
return ElMessageBox.confirm(msg, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: type,
});
}
// 消息提示封装
export function message(msg: string, type = 'success') {
ElMessage[type](msg);
}
// 文件流下载
export function streamFileDownload(file: any, fileName = '文件名称.zip') {
const blob = new Blob([file], { type: 'application/octet-stream;charset=UTF-8' });
@@ -49,6 +35,38 @@ export function streamFileDownload(file: any, fileName = '文件名称.zip') {
}
```
### 消息提示与确认弹窗
项目使用 NaiveUI 的 `useMessage` 和 `useDialog` 提供消息提示和确认弹窗:
```typescript
import { useMessage, useDialog } from 'naive-ui';
// 在 setup 中初始化
const message = useMessage();
const dialog = useDialog();
// 消息提示
message.success('操作成功');
message.error('操作失败');
message.warning('请注意');
// 确认弹窗
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
// 执行删除操作
},
});
```
:::warning 注意
`useMessage` 和 `useDialog` 必须在 Vue 组件的 `setup` 中调用。如果需要在非组件环境(如 Axios 拦截器)中使用,可通过 `window.__message` 和 `window.__dialog` 全局引用访问。
:::
### 树结构操作
`auth.ts` 中还包含树结构转换工具:
@@ -181,14 +199,31 @@ export function formatToDate(date: Date, formatStr = DATE_FORMAT): string {
}
```
`dateUtil` 也被 `vite.config.ts` 使用,记录构建时间戳:
## renderIcon 工具
图标渲染工具,用于在 TableAction 等组件中渲染 NaiveUI 图标:
```typescript
// vite.config.ts
import { formatToDateTime } from './src/utils/dateUtil';
const __APP_INFO__ = {
pkg: { dependencies, devDependencies, name, version },
lastBuildTime: formatToDateTime(new Date()),
// src/utils/index.ts
import { h } from 'vue';
import { NIcon } from 'naive-ui';
export function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) });
}
```
使用示例:
```typescript
import { renderIcon } from '@/utils';
import { FormOutlined, DeleteOutlined } from '@vicons/antd';
const actionColumn = {
actions: [
{ label: '编辑', icon: renderIcon(FormOutlined), type: 'info' },
{ label: '删除', icon: renderIcon(DeleteOutlined), type: 'error' },
],
};
```
@@ -233,9 +268,9 @@ export enum PageEnum {
```
:::tip 温馨提示
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 ElementPlus 的简封装,在业务组件中直接调用即可。
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。项目使用 NaiveUI 的 `useMessage` 和 `useDialog` 提供消息提示和确认弹窗,`renderIcon` 工具函数用于在操作列等场景渲染 @vicons 图标。
:::
## 总结
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 ElementPlus 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、renderIcon(图标渲染)、httpEnum/pageEnum(枚举定义)等通用能力。消息提示使用 NaiveUI 的 `useMessage`,确认弹窗使用 `useDialog`,图标使用 `@vicons/antd` 库。
@@ -32,8 +32,8 @@ src/views/
| 文件 | 职责 | 说明 |
|------|------|------|
| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 |
| `edit.vue` | 编辑弹窗 | el-dialog + el-form,懒加载 |
| `columns.ts` | 表格列定义 | 定义列 prop、label、render 函数等 |
| `edit.vue` | 编辑弹窗 | basicModal + n-form,懒加载 |
| `columns.ts` | 表格列定义 | 定义列 key、title、render 函数等 |
| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 |
## index.vue 列表页
@@ -43,101 +43,96 @@ src/views/
```vue
<!-- src/views/system/position/index.vue -->
<template>
<PageWrapper>
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</el-card>
<el-card :bordered="false" class="proCard">
<div>
<n-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</n-card>
<n-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
ref="basicTableRef"
:actionColumn="actionColumn"
@selection-change="onSelectionChange"
@update:checked-row-keys="onCheckedRow"
:autoScrollX="true"
>
<template #tableTitle>
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
<template #icon>
<el-icon class="el-input__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>
<n-space>
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
<template #icon><n-icon><PlusOutlined /></n-icon></template>
新建
</n-button>
<n-button
type="error"
@click="handleDelete"
:disabled="!rowKeys.length"
v-perm="['sys:position:batchDelete']"
>
<template #icon><n-icon><DeleteOutlined /></n-icon></template>
删除
</n-button>
</n-space>
</template>
</BasicTable>
</el-card>
</n-card>
<editDialog
v-if="editVisible"
ref="createModalRef"
:positionId="positionId"
v-if="editVisible"
v-model:visible="editVisible"
@success="reloadTable('noRefresh')"
/>
</PageWrapper>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
import { ColProps } from 'element-plus';
import { schemas } from './querySchemas';
import { useForm } from '@/components/Form/index';
import { h, nextTick, reactive, ref } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import { TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { message, confirm } from '@/utils/auth';
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
const positionId = ref(0);
import { PlusOutlined, DeleteOutlined, FormOutlined } from '@vicons/antd';
import editDialog from './edit.vue';
import { schemas } from './querySchemas';
import { renderIcon } from '@/utils';
const message = useMessage();
const dialog = useDialog();
const basicTableRef = ref();
const createModalRef = ref();
const editVisible = ref(false);
const selectionData = ref([]);
const tableRef = ref();
const positionId = ref(0);
const rowKeys = ref([]);
/**
* 定义查询参数
*/
const formParams = reactive({
name: '',
status: '',
});
const formParams = reactive({ name: '', status: '' });
/**
* 定义操作栏
*/
const actionColumn = reactive({
width: 200,
label: '操作',
prop: 'action',
title: '操作',
align: 'center',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction, {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
icon: 'Edit',
type: 'warning',
icon: renderIcon(FormOutlined),
type: 'info',
onClick: handleEdit.bind(null, record),
auth: ['sys:position:update'],
},
{
label: '删除',
icon: 'Delete',
type: 'danger',
icon: renderIcon(DeleteOutlined),
type: 'error',
onClick: handleDelete.bind(null, record),
auth: ['sys:position:delete'],
},
@@ -146,98 +141,66 @@ src/views/
},
});
/**
* 加载数据列表
* @param res 参数
*/
const loadDataTable = async (res: any) => {
const loadDataTable = async (res) => {
rowKeys.value = [];
const result = await getPositionList({ ...formParams, ...res });
return result;
};
/**
* 刷新数据列表
* @param noRefresh 参数
*/
function onCheckedRow(keys) {
rowKeys.value = keys;
}
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
function handleSubmit(values: Recordable) {
for (const key in formParams) { formParams[key] = ''; }
for (const key in values) { formParams[key] = values[key]; }
reloadTable();
}
function handleReset(values: Recordable) {
for (const key in formParams) { formParams[key] = ''; }
for (const key in values) { formParams[key] = values[key]; }
reloadTable();
}
/**
* 注册
*/
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
layout: 'horizontal',
colProps: { span: 6 } as ColProps,
submitOnReset: true,
schemas,
});
/**
* 执行提交表单
*/
function handleSubmit(values: Recordable) {
handleReset();
for (const key in values) {
formParams[key] = values[key];
}
reloadTable();
}
/**
* 执行重置
*/
function handleReset() {
for (const key in formParams) {
formParams[key] = '';
}
}
/**
* 执行添加
*/
const handleAdd = async () => {
positionId.value = 0;
await nextTick();
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
/**
* 执行编辑
* @param record 参数
*/
const handleEdit = async (record: Recordable) => {
positionId.value = record.row.id;
await nextTick();
async function handleEdit(record) {
positionId.value = record.id;
editVisible.value = true;
};
/**
* 执行删除
* @param record 参数
*/
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();
await nextTick();
createModalRef.value.openModal();
}
/**
* 选项发生变化
* @param value 参数
*/
function onSelectionChange(value) {
selectionData.value = value;
async function handleDelete(record) {
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record?.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
message.success('删除成功');
reloadTable();
},
});
}
</script>
<style lang="scss" scoped></style>
```
## edit.vue 编辑弹窗
@@ -245,118 +208,92 @@ src/views/
```vue
<!-- src/views/system/position/edit.vue -->
<template>
<el-dialog
v-model="props.visible"
:title="props.positionId ? '编辑' : '新增'"
width="500"
:close-on-click-modal="false"
:before-close="dialogClose"
<basicModal
@register="modalRegister"
ref="modalRef"
class="basicModal basicFormModal"
@on-ok="handleSubmit"
@on-close="handleClose"
>
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
<el-form-item
label="岗位名称"
prop="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>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogClose">取消</el-button>
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
</span>
<template #default>
<n-form class="ls-form" ref="formRef" :model="formData" label-placement="left" label-width="85px">
<n-form-item
label="岗位名称"
path="name"
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
>
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
</n-form-item>
<n-form-item label="岗位状态" path="status">
<n-radio-group v-model:value="formData.status" name="status">
<n-radio :value="1">正常</n-radio>
<n-radio :value="2">停用</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formData.sort" />
</n-form-item>
</n-form>
</template>
</el-dialog>
</basicModal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus';
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { onMounted, reactive, shallowRef } from 'vue';
import { message } from '@/utils/auth';
import { useLockFn } from '@/utils/useLockFn';
import { onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useModal } from '@/components/Modal';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
const formRef = ref();
const message = useMessage();
/**
* 定义表单参数
*/
const formData = reactive({
id: '',
name: '',
status: 1,
sort: 0,
});
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
/**
* 定义接收的参数
*/
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
positionId: {
type: Number,
required: true,
default: 0,
},
visible: { type: Boolean, required: true, default: false },
positionId: { type: Number, required: true, default: 0 },
});
const [modalRegister, { openModal, setSubLoading }] = useModal({
title: props.positionId ? '编辑岗位' : '添加岗位',
subBtuText: '确定',
width: 600,
});
/**
* 执行提交表单
*/
const handleSubmit = async () => {
await formRef.value?.validate();
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message('操作成功');
emit('update:visible', false);
emit('success');
formRef.value
.validate()
.then(async () => {
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message.success('操作成功');
setSubLoading(false);
emit('update:visible', false);
emit('success');
})
.catch(() => {
setSubLoading(false);
});
};
/**
* 关闭窗体
*/
const dialogClose = () => {
const handleClose = () => {
emit('update:visible', false);
};
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
/**
* 设置表单数据
*/
const setFormData = async () => {
const data = await getPositionDetail(props.positionId);
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key];
}
}
};
/**
* 钩子函数
*/
onMounted(() => {
if (props.positionId) {
setFormData();
}
if (props.positionId) setFormData();
});
</script>
defineExpose({ openModal });
</script>
```
@@ -367,7 +304,7 @@ src/views/
用户搜索 → handleSubmit → 更新 formParams → reloadTable → 重新请求
用户新增 → handleAdd → positionId=0 → editVisible=true → edit.vue 加载 → 表单填写 → 提交
用户编辑 → handleEdit → positionId=id → editVisible=true → edit.vue 加载详情 → 修改 → 提交
用户删除 → confirm → API → reloadTable
用户删除 → dialog.warning → API → reloadTable
```
## columns.ts
@@ -375,52 +312,54 @@ src/views/
```typescript
// src/views/system/position/columns.ts
import { h } from 'vue';
import { ElTag } from 'element-plus';
import { NTag } from 'naive-ui';
export const columns = [
{
type: 'selection',
width: 50,
fixed: 'left',
},
{
label: 'ID',
prop: 'id',
title: 'ID',
key: 'id',
fixed: 'left',
width: 50,
},
{
label: '岗位名称',
prop: 'name',
minWidth: 100,
title: '岗位名称',
key: 'name',
width: 100,
},
{
label: '岗位状态',
prop: 'status',
minWidth: 100,
title: '岗位状态',
key: 'status',
width: 100,
render(record) {
return h(
ElTag,
NTag,
{
type: record.row.status == 1 ? 'success' : 'danger',
},
{
default: () => (record.row.status == 1 ? '正常' : '停用'),
style: { marginRight: '6px' },
bordered: false,
type: record.status == 1 ? 'success' : 'error',
},
{ default: () => (record.status == 1 ? '正常' : '停用') },
);
},
},
{
label: '排序',
prop: 'sort',
minWidth: 100,
title: '排序',
key: 'sort',
width: 100,
},
{
label: '创建人',
prop: 'createUser',
minWidth: 100,
title: '创建人',
key: 'createUser',
width: 100,
},
{
label: '创建时间',
prop: 'createTime',
title: '创建时间',
key: 'createTime',
width: 180,
},
];
@@ -434,7 +373,7 @@ import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
component: 'NInput',
label: '岗位名称',
componentProps: {
placeholder: '请输入岗位名称',
@@ -442,20 +381,14 @@ export const schemas: FormSchema[] = [
},
{
field: 'status',
component: 'Select',
component: 'NSelect',
label: '状态',
componentProps: {
placeholder: '请选择状态',
clearable: true,
options: [
{
label: '正常',
value: '1',
},
{
label: '禁用',
value: '2',
},
{ label: '正常', value: '1' },
{ label: '禁用', value: '2' },
],
},
},
@@ -463,9 +396,9 @@ export const schemas: FormSchema[] = [
```
:::tip 温馨提示
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`querySchemas` 中的 `options` 硬编码或通过字典 API 动态获取。
`columns.ts` 和 `querySchemas.ts` 独立为配置文件,便于维护和复用。`columns` 中的 `render` 函数使用 Vue 的 `h()` 创建虚拟 DOM,可实现任意复杂的自定义渲染。`render` 函数直接接收行数据对象,通过 `record.key` 访问字段值(如 `record.status`)。`querySchemas` 中的 `component` 使用 NaiveUI 组件名(如 `NInput`、`NSelect`),`options` 硬编码或通过字典 API 动态获取。
:::
## 总结
页面视图遵循 `index.vue` + `edit.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `defineAsyncComponent` 懒加载 + `useLockFn` 防重复提交。数据流清晰:搜索更新参数 → 刷新表格,操作成功 → emit 事件 → 刷新表格。
页面视图遵循 `index.vue` + `edit.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `defineAsyncComponent` 懒加载。数据流清晰:搜索更新参数 → 刷新表格,操作成功 → emit 事件 → 刷新表格。消息提示使用 `useMessage()`,确认对话框使用 `useDialog()`,图标来自 `@vicons/antd`。