优化更新
This commit is contained in:
@@ -68,8 +68,8 @@ Vite 按 mode 加载环境变量,后面的文件覆盖前面的:
|
||||
|------|------|--------|------|
|
||||
| `VITE_PORT` | `number` | `8001` | 开发服务器端口 |
|
||||
| `VITE_PUBLIC_PATH` | `string` | `/` | 部署基础路径(子目录部署时修改) |
|
||||
| `VITE_GLOB_APP_TITLE` | `string` | `Django+ElementPlus版` | 应用标题(注入 HTML) |
|
||||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Django+ElementPlus版` | 应用短名(用于生成运行时配置变量名) |
|
||||
| `VITE_GLOB_APP_TITLE` | `string` | `Django+NaiveUI版` | 应用标题(注入 HTML) |
|
||||
| `VITE_GLOB_APP_SHORT_NAME` | `string` | `Django+NaiveUI版` | 应用短名(用于生成运行时配置变量名) |
|
||||
| `VITE_GLOB_API_URL` | `string` | 空 | API 基础路径(留空使用相对路径) |
|
||||
| `VITE_GLOB_API_URL_PREFIX` | `string` | `/api` | API 前缀 |
|
||||
| `VITE_GLOB_FILE_URL` | `string` | - | 文件访问域名(拼接上传文件 URL) |
|
||||
@@ -177,7 +177,7 @@ import { defineStore } from 'pinia'; // 自动
|
||||
|
||||
```html
|
||||
<!-- 构建后的 index.html -->
|
||||
<title>Django+ElementPlus版</title>
|
||||
<title>Django+NaiveUI版</title>
|
||||
<script src="/app.config.js?v=1.0.0-1725000000000"></script>
|
||||
```
|
||||
|
||||
@@ -191,8 +191,8 @@ import { defineStore } from 'pinia'; // 自动
|
||||
window.__{SHORT_NAME}__PRODUCTION__CONF__ = {
|
||||
"VITE_GLOB_API_URL": "",
|
||||
"VITE_GLOB_API_URL_PREFIX": "/api",
|
||||
"VITE_GLOB_FILE_URL": "http://file.django.elevue",
|
||||
"VITE_GLOB_APP_TITLE": "Django+ElementPlus版",
|
||||
"VITE_GLOB_FILE_URL": "http://file.django.naivevue",
|
||||
"VITE_GLOB_APP_TITLE": "Django+NaiveUI版",
|
||||
// ...
|
||||
};
|
||||
Object.freeze(window.__{SHORT_NAME}__PRODUCTION__CONF__);
|
||||
@@ -256,7 +256,7 @@ dist/
|
||||
├── app.config.js # 运行时配置(VITE_GLOB_* 变量)
|
||||
├── favicon.ico # 网站图标
|
||||
└── assets/
|
||||
├── index-[hash].js # 主包(Vue、ElementPlus、Pinia 等)
|
||||
├── index-[hash].js # 主包(Vue、NaiveUI、Pinia 等)
|
||||
├── index-[hash].css # 全局样式
|
||||
├── [name]-[hash].js # 路由懒加载 chunk(按视图分割)
|
||||
└── ... # 字体、图片等静态资源
|
||||
@@ -283,7 +283,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
||||
| chunk | 内容 |
|
||||
|-------|------|
|
||||
| `vendor-vue.js` | vue + vue-router + pinia |
|
||||
| `vendor-element.js` | element-plus |
|
||||
| `vendor-naive.js` | naive-ui |
|
||||
| `vendor-echarts.js` | echarts(如果使用) |
|
||||
| `vendor-*.js` | 其他大型依赖 |
|
||||
|
||||
@@ -293,7 +293,7 @@ Vite 自动将 `node_modules` 中的库分割为独立 chunk:
|
||||
|
||||
```typescript
|
||||
optimizeDeps: {
|
||||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd', '@element-plus/icons-vue'],
|
||||
include: ['dayjs', '@vicons/ionicons5', '@vicons/antd'],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -307,13 +307,8 @@ optimizeDeps: {
|
||||
async function bootstrap() {
|
||||
const app = createApp(App);
|
||||
|
||||
// 1. 全局注册 ElementPlus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
// 2. 全局引入 ElementPlus(中文包)
|
||||
setupElement(app);
|
||||
// 1. 全局引入 NaiveUI(中文包)
|
||||
setupNaive(app);
|
||||
|
||||
// 3. 全局注册自定义组件(BasicTable、BasicForm 等)
|
||||
setupCustomComponents(app);
|
||||
@@ -339,7 +334,7 @@ async function bootstrap() {
|
||||
|
||||
| 注册项 | 文件 | 说明 |
|
||||
|--------|------|------|
|
||||
| ElementPlus | `element.ts` | 全局引入 + 中文包 |
|
||||
| NaiveUI | `naive.ts` | 全局引入 + 中文包 |
|
||||
| 自定义组件 | `customComponents.ts` | BasicTable、BasicForm、PageWrapper 等 |
|
||||
| 自定义指令 | `directives.ts` | `v-perm`、`v-perms`、`v-scrollBar` |
|
||||
|
||||
@@ -504,7 +499,7 @@ pnpm report
|
||||
**优化手段**:
|
||||
|
||||
1. 路由懒加载(已默认启用)
|
||||
2. 按需引入 ElementPlus 组件(当前为全量引入)
|
||||
2. 按需引入 NaiveUI 组件(当前为全量引入)
|
||||
3. 大型依赖(echarts、wangeditor)按需引入
|
||||
4. 开启 gzip 压缩(`VITE_BUILD_COMPRESS=gzip`)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 ElementPlus 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
||||
项目提供丰富的公共组件,位于 `ui/src/components/` 目录,基于 NaiveUI 二次封装,覆盖表格、表单、弹窗、上传、富文本编辑等常见场景。组件通过 `src/plugins/` 中的 `setupCustomComponents` 全局注册,任何页面直接使用,无需单独 import。
|
||||
:::
|
||||
|
||||
## 组件总览
|
||||
@@ -19,7 +19,7 @@
|
||||
| CropperImage | `components/Cropper/` | 图片裁剪(圆形/矩形、实时预览) |
|
||||
| QrCode | `components/Qrcode/` | 二维码生成(canvas/img、内嵌 logo) |
|
||||
| ChinaArea | `components/ChinaArea/` | 省市区级联(懒加载、二/三级联动) |
|
||||
| Icon | `components/icon/` | 图标(支持 ElementPlus 图标 + SVG 图标) |
|
||||
| Icon | `components/icon/` | 图标(支持 @vicons/antd 图标 + SVG 图标) |
|
||||
| BasicSelect | `components/Select/` | 增强下拉(远程请求、本地缓存) |
|
||||
| CountTo | `components/CountTo/` | 数字动画(前缀/后缀、千分位、缓动曲线) |
|
||||
| Password | `components/Password/` | 密码强度(强度指示器、重复确认、复杂度校验) |
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
## BasicTable 表格
|
||||
|
||||
表格组件是使用频率最高的组件,基于 `el-table` + `el-pagination` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
||||
表格组件是使用频率最高的组件,基于 NaiveUI 的 `n-data-table` 封装,详见 [BasicTable 表格](/zh/volume4/frontend/table) 章节。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -79,8 +79,8 @@
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `label` | `string` | 列标题 |
|
||||
| `prop` | `string` | 字段名 |
|
||||
| `title` | `string` | 列标题 |
|
||||
| `key` | `string` | 字段名 |
|
||||
| `width` | `number` | 列宽 |
|
||||
| `align` | `string` | 对齐方式 |
|
||||
| `type` | `string` | 类型:`index` / `selection` |
|
||||
@@ -96,7 +96,7 @@
|
||||
|------|------|------|
|
||||
| `fetch-success` | `(data, result)` | 数据请求成功 |
|
||||
| `fetch-error` | `(error)` | 数据请求失败 |
|
||||
| `selection-change` | `(rows)` | 多选变化 |
|
||||
| `update:checked-row-keys` | `(keys)` | 多选变化 |
|
||||
| `columns-change` | `(columns)` | 列设置变化 |
|
||||
| `checked-row-change` | `(rowKeys)` | 选中行变化 |
|
||||
|
||||
@@ -129,39 +129,59 @@
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<BasicTable :columns="columns" :request="loadDataTable" ref="tableRef">
|
||||
<BasicTable :columns="columns" :request="loadDataTable" ref="basicTableRef" :actionColumn="actionColumn">
|
||||
<template #tableTitle>
|
||||
<el-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
<n-space>
|
||||
<n-button v-perm="['sys:level:add']" type="primary" @click="handleAdd">
|
||||
<template #icon><n-icon><PlusOutlined /></n-icon></template>
|
||||
新增
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, reactive, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { PlusOutlined, FormOutlined, DeleteOutlined } from '@vicons/antd';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { renderIcon } from '@/utils';
|
||||
|
||||
const basicTableRef = ref();
|
||||
|
||||
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,
|
||||
render(record) {
|
||||
return h(NTag, { bordered: false, type: record.status == 1 ? 'success' : 'error' },
|
||||
{ default: () => record.status == 1 ? '正常' : '停用' });
|
||||
},
|
||||
},
|
||||
{ title: '创建时间', key: 'createTime', width: 180 },
|
||||
];
|
||||
|
||||
const actionColumn = {
|
||||
const actionColumn = reactive({
|
||||
width: 200,
|
||||
actions: [
|
||||
{ label: '编辑', auth: ['sys:level:update'], onClick: handleEdit },
|
||||
{
|
||||
label: '删除',
|
||||
auth: ['sys:level:delete'],
|
||||
isConfirm: true,
|
||||
popConfirm: { title: '确认删除?' },
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
};
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{ label: '编辑', icon: renderIcon(FormOutlined), type: 'info', onClick: handleEdit.bind(null, record), auth: ['sys:level:update'] },
|
||||
{ label: '删除', icon: renderIcon(DeleteOutlined), type: 'error', onClick: handleDelete.bind(null, record), auth: ['sys:level:delete'] },
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function loadDataTable({ page, pageSize }) {
|
||||
const res = await getListApi({ pageNo: page, pageSize });
|
||||
return { items: res.data, total: res.count };
|
||||
async function loadDataTable(params) {
|
||||
const result = await getListApi(params);
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -230,13 +250,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` | 栅格配置(如 `{ cols: '1 s:1 m:2 l:3' }`) |
|
||||
| `labelMessage` | `string` | 标签提示信息 |
|
||||
| `suffix` | `string` | 后缀文字 |
|
||||
|
||||
@@ -268,17 +288,19 @@ async function loadDataTable({ page, pageSize }) {
|
||||
|
||||
<script setup lang="ts">
|
||||
const [registerForm] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 80,
|
||||
schemas: [
|
||||
{
|
||||
field: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
component: 'NInput',
|
||||
componentProps: { placeholder: '请输入名称' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
component: 'NSelect',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '启用', value: 1 },
|
||||
@@ -286,12 +308,6 @@ const [registerForm] = useForm({
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dateRange',
|
||||
label: '日期范围',
|
||||
component: 'DatePicker',
|
||||
componentProps: { type: 'daterange' },
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
@@ -301,7 +317,7 @@ const [registerForm] = useForm({
|
||||
|
||||
## BasicModal 弹窗
|
||||
|
||||
弹窗组件基于 `el-dialog` 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
||||
弹窗组件基于 NaiveUI 封装,支持拖拽、自定义按钮,详见 [Modal 弹窗](/zh/volume4/frontend/modal) 章节。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -335,21 +351,29 @@ const [registerForm] = useForm({
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<BasicModal title="编辑用户" :width="600" @register="registerModal" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
<basicModal @register="modalRegister" ref="modalRef" class="basicModal basicFormModal" @on-ok="handleSubmit">
|
||||
<template #default>
|
||||
<n-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="请输入用户名" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</basicModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const [registerModal, { openModal, closeModal, setModalProps }] = useModal();
|
||||
import { useModal } from '@/components/Modal';
|
||||
|
||||
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
|
||||
title: '编辑用户',
|
||||
subBtuText: '确定',
|
||||
width: 600,
|
||||
});
|
||||
|
||||
function handleEdit(record) {
|
||||
setModalProps({ title: '编辑用户' });
|
||||
openModal();
|
||||
// 回填表单数据
|
||||
nextTick(() => {
|
||||
setFieldsValue(record);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -358,7 +382,7 @@ function handleEdit(record) {
|
||||
|
||||
## BasicUpload 上传
|
||||
|
||||
文件/图片上传组件,基于 `el-upload` 封装,支持卡片/列表模式、数量限制、图片预览。
|
||||
文件/图片上传组件,基于 NaiveUI 封装,支持卡片/列表模式、数量限制、图片预览。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -700,10 +724,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>
|
||||
@@ -726,7 +750,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>
|
||||
```
|
||||
@@ -734,7 +758,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>
|
||||
```
|
||||
:::
|
||||
|
||||
@@ -742,7 +766,7 @@ async function getDeptOptions() {
|
||||
|
||||
## Icon 图标
|
||||
|
||||
统一图标组件,支持 ElementPlus 图标和 SVG 图标。
|
||||
统一图标组件,支持 @vicons/antd 图标和 SVG 图标。
|
||||
|
||||
### Props
|
||||
|
||||
@@ -756,8 +780,8 @@ async function getDeptOptions() {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ElementPlus 图标 -->
|
||||
<Icon name="el-icon-Plus" size="18px" />
|
||||
<!-- @vicons/antd 图标 -->
|
||||
<n-icon :component="PlusOutlined" size="18" />
|
||||
<!-- SVG 图标 -->
|
||||
<Icon name="svg-icon-user" />
|
||||
</template>
|
||||
@@ -838,4 +862,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。优先使用公共组件,避免重复开发。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 ElementPlus 的 `el-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
||||
`BasicForm` 位于 `src/components/Form/src/BasicForm.vue`,基于 NaiveUI 的 `n-form` 二次封装,通过 schemas 配置化定义表单字段,支持查询表单和业务表单两种模式。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -26,9 +26,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 定义
|
||||
@@ -41,7 +41,7 @@ import { FormSchema } from '@/components/Form/index';
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
component: 'NInput',
|
||||
label: '岗位名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入岗位名称',
|
||||
@@ -49,14 +49,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: '2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -68,26 +68,25 @@ 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({
|
||||
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 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();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -95,73 +94,173 @@ function handleReset() {
|
||||
|
||||
## 业务表单(编辑弹窗)
|
||||
|
||||
编辑弹窗中直接使用 `el-form` + `el-form-item` 构建表单:
|
||||
编辑弹窗使用 `basicModal` + `useModal` + `n-form` 构建表单:
|
||||
|
||||
```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' }"
|
||||
<basicModal
|
||||
@register="modalRegister"
|
||||
ref="modalRef"
|
||||
class="basicModal basicFormModal"
|
||||
@on-ok="handleSubmit"
|
||||
@on-close="handleClose"
|
||||
>
|
||||
<template #default>
|
||||
<n-form
|
||||
class="ls-form"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-placement="left"
|
||||
label-width="85px"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
```
|
||||
|
||||
### useModal 注册
|
||||
|
||||
```typescript
|
||||
import { useModal } from '@/components/Modal';
|
||||
|
||||
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||
title: props.positionId ? '编辑岗位' : '添加岗位',
|
||||
subBtuText: '确定',
|
||||
width: 600,
|
||||
});
|
||||
```
|
||||
|
||||
### 完整 script 示例
|
||||
|
||||
```typescript
|
||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { useModal } from '@/components/Modal';
|
||||
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
const formRef = ref();
|
||||
const message = useMessage();
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||
title: props.positionId ? '编辑岗位' : '添加岗位',
|
||||
subBtuText: '确定',
|
||||
width: 600,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
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 handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const setFormData = async () => {
|
||||
const data = await getPositionDetail(props.positionId);
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
formData[key] = data[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.positionId) setFormData();
|
||||
});
|
||||
|
||||
defineExpose({ openModal });
|
||||
```
|
||||
|
||||
## 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">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<!-- NCheckbox -->
|
||||
<template v-if="schema.component === 'NCheckbox'">
|
||||
<n-checkbox-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-checkbox
|
||||
v-for="item in schema.componentProps?.options"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
/>
|
||||
</n-space>
|
||||
</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">
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<!-- NRadioGroup -->
|
||||
<template v-else-if="schema.component === 'NRadioGroup'">
|
||||
<n-radio-group v-model:value="formModel[schema.field]">
|
||||
<n-space>
|
||||
<n-radio v-for="item in schema.componentProps?.options" :value="item.value">
|
||||
{{ item.label }}
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</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>
|
||||
<!-- NDatePicker(带 valueFormat) -->
|
||||
<template v-else-if="schema.component === 'NDatePicker' && schema.componentProps?.valueFormat">
|
||||
<n-date-picker
|
||||
v-model:formatted-value="formModel[schema.field]"
|
||||
v-bind="schema.componentProps"
|
||||
clearable
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 其他组件:动态渲染 el-{component} -->
|
||||
<component v-else :is="`el-${schema.component}`" v-model="formModel[schema.field]" />
|
||||
<!-- 动态渲染表单组件 -->
|
||||
<component
|
||||
v-else
|
||||
v-bind="getComponentProps(schema)"
|
||||
:is="schema.component"
|
||||
:type="schema.type"
|
||||
v-model:value="formModel[schema.field]"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -170,11 +269,10 @@ function handleReset() {
|
||||
```typescript
|
||||
interface FormSchema {
|
||||
field: string; // 字段名
|
||||
label: string; // 标签文本
|
||||
component: string; // 组件类型:Input / Select / RadioGroup / Checkbox / DatePicker 等
|
||||
label?: string; // 标签文本
|
||||
component?: ComponentType; // 组件类型:NInput / NSelect / NRadioGroup / NCheckbox / NDatePicker 等
|
||||
componentProps?: any; // 组件属性(placeholder、options、clearable 等)
|
||||
required?: boolean; // 是否必填
|
||||
rules?: any[]; // 自定义校验规则
|
||||
rules?: object | object[]; // 自定义校验规则
|
||||
defaultValue?: any; // 默认值
|
||||
hidden?: boolean | (({ schema, values, model }) => boolean); // 是否隐藏
|
||||
slot?: string; // 自定义插槽名
|
||||
@@ -182,6 +280,9 @@ interface FormSchema {
|
||||
labelMessage?: string; // 标签提示信息
|
||||
isFull?: boolean; // 是否全宽
|
||||
type?: string; // 组件子类型
|
||||
giProps?: GridItemProps; // 栅格子项属性
|
||||
showFeedback?: boolean; // 是否显示反馈
|
||||
labelWidth?: number | string; // 标签宽度
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,8 +291,14 @@ interface FormSchema {
|
||||
```typescript
|
||||
// 手动校验
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
// 校验通过,提交数据
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
// 校验通过,提交数据
|
||||
})
|
||||
.catch(() => {
|
||||
// 校验失败
|
||||
});
|
||||
};
|
||||
|
||||
// 清除校验
|
||||
@@ -203,15 +310,27 @@ Schema 中的 `required: true` 会自动生成必填校验规则,也可以通
|
||||
```typescript
|
||||
{
|
||||
field: 'name',
|
||||
label: '岗位名称',
|
||||
component: 'Input',
|
||||
label: '案例名称',
|
||||
component: 'NInput',
|
||||
rules: [
|
||||
{ required: true, message: '请输入岗位名称', trigger: 'blur' },
|
||||
{ required: true, message: '请输入案例名称', trigger: 'blur' },
|
||||
{ min: 1, max: 150, message: '长度在 1 到 150 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
在模板中使用 `n-form-item` 的 `rule` 属性也可以直接定义校验规则:
|
||||
|
||||
```vue
|
||||
<n-form-item
|
||||
label="案例名称"
|
||||
path="name"
|
||||
:rule="{ required: true, message: '请输入案例名称', trigger: 'blur' }"
|
||||
>
|
||||
<n-input v-model:value="formData.name" placeholder="请输入案例名称" clearable />
|
||||
</n-form-item>
|
||||
```
|
||||
|
||||
## 表单赋值
|
||||
|
||||
编辑时需要将行数据赋值给表单:
|
||||
@@ -233,10 +352,55 @@ onMounted(() => {
|
||||
});
|
||||
```
|
||||
|
||||
## n-select 用法
|
||||
|
||||
NaiveUI 的 `n-select` 使用 `:options` 传递选项数据,无需嵌套 `el-option`:
|
||||
|
||||
```vue
|
||||
<n-select
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择案例类型"
|
||||
clearable
|
||||
filterable
|
||||
:options="optionsData.typeList"
|
||||
/>
|
||||
```
|
||||
|
||||
选项数据格式:
|
||||
|
||||
```typescript
|
||||
const optionsData = reactive({
|
||||
typeList: [
|
||||
{ label: '类型1', value: 1 },
|
||||
{ label: '类型2', value: 2 },
|
||||
{ label: '类型3', value: 3 },
|
||||
{ label: '类型4', value: 4 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 查询表单完整示例(带响应式栅格)
|
||||
|
||||
```typescript
|
||||
const [register, {}] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 80,
|
||||
schemas,
|
||||
});
|
||||
```
|
||||
|
||||
`gridProps` 使用 NaiveUI 的响应式栅格断点:
|
||||
- `1` — 默认每行 1 列
|
||||
- `s:1` — 小屏 1 列
|
||||
- `m:2` — 中屏 2 列
|
||||
- `l:3` — 大屏 3 列
|
||||
- `xl:4` — 超大屏 4 列
|
||||
- `2xl:4` — 超超大屏 4 列
|
||||
|
||||
:::tip 温馨提示
|
||||
查询表单的字段名需要与后端 `page_like_fields` 和 `page_eq_fields` 对应,确保查询参数能正确传递到后端。`useForm` 返回的 `register` 函数传给 `@register` 事件完成表单绑定,`formActionType` 提供 `setFieldsValue`、`resetFields`、`validate` 等方法。
|
||||
查询表单的字段名需要与后端 `page_like_fields` 和 `page_eq_fields` 对应,确保查询参数能正确传递到后端。`useForm` 返回的 `register` 函数传给 `@register` 事件完成表单绑定,`formActionType` 提供 `setFieldsValue`、`resetFields`、`validate` 等方法。业务弹窗使用 `basicModal` + `useModal` 管理弹窗状态,通过 `@on-ok` 事件处理提交,通过 `setSubLoading(false)` 关闭提交按钮的 loading 状态。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 ElementPlus 组件。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单直接使用 `el-form` + `reactive` 管理数据。支持多种组件类型、表单校验、数据绑定、条件隐藏等能力。
|
||||
BasicForm 通过 schemas 配置化定义表单字段,内部根据 `component` 动态渲染对应的 NaiveUI 组件(`NInput`、`NSelect`、`NRadioGroup`、`NCheckbox`、`NDatePicker` 等)。查询表单使用 `useForm` 注册并绑定 `@submit`/`@reset` 事件,业务表单使用 `basicModal` + `useModal` + `n-form` + `reactive` 管理数据。NaiveUI 的表单组件统一使用 `v-model:value` 进行双向绑定,`n-form-item` 使用 `path` 代替 `prop` 指定校验字段,使用 `rule` 代替 `rules` 指定校验规则。支持多种组件类型、表单校验、数据绑定、条件隐藏、响应式栅格等能力。
|
||||
|
||||
@@ -132,7 +132,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
|
||||
|
||||
@@ -3,38 +3,42 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
当前项目仅支持中文界面,ElementPlus 使用中文语言包(`zh-cn`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
||||
当前项目仅支持中文界面,NaiveUI 使用中文语言包(`zhCN`),所有界面文本直接硬编码在组件中。如需扩展多语言支持,可基于 vue-i18n 实现。
|
||||
:::
|
||||
|
||||
## 当前状态
|
||||
|
||||
项目当前为中文单语言版本:
|
||||
|
||||
- ElementPlus 全局配置中文语言包:`app.use(ElementPlus, { locale: zhCn })`
|
||||
- 所有界面文本直接硬编码在组件中:`<el-button>添加岗位</el-button>`
|
||||
- NaiveUI 通过 ConfigProvider 配置中文语言包:`<n-config-provider :locale="zhCN" :date-locale="dateZhCN">`
|
||||
- 所有界面文本直接硬编码在组件中:`<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>
|
||||
<!-- 表格列通过 columns.ts 配置,使用 title/key -->
|
||||
</template>
|
||||
```
|
||||
|
||||
## ElementPlus 中文配置
|
||||
## NaiveUI 中文配置
|
||||
|
||||
项目在入口文件中全局配置 ElementPlus 中文语言包:
|
||||
项目通过 NaiveUI 的 ConfigProvider 配置中文语言包:
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
import ElementPlus from 'element-plus';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
// App.vue 或入口组件
|
||||
import { zhCN, dateZhCN } from 'naive-ui';
|
||||
```
|
||||
|
||||
配置后,ElementPlus 组件的默认文本(如分页、日期选择器、表格空数据等)自动显示中文。
|
||||
```vue
|
||||
<template>
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<App />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
```
|
||||
|
||||
配置后,NaiveUI 组件的默认文本(如分页、日期选择器、表格空数据等)自动显示中文。
|
||||
|
||||
## 扩展方案
|
||||
|
||||
@@ -126,8 +130,8 @@ 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>
|
||||
<!-- 表格列通过 columns.ts 配置,使用 title/key -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -136,23 +140,26 @@ const { t } = useI18n();
|
||||
</script>
|
||||
```
|
||||
|
||||
### ElementPlus 国际化联动
|
||||
### NaiveUI 国际化联动
|
||||
|
||||
```typescript
|
||||
// 动态切换 ElementPlus 语言
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
// 动态切换 NaiveUI 语言
|
||||
import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui';
|
||||
|
||||
const locale = computed(() => {
|
||||
return i18n.global.locale.value === 'zh-CN' ? zhCn : en;
|
||||
return i18n.global.locale.value === 'zh-CN' ? zhCN : enUS;
|
||||
});
|
||||
|
||||
const dateLocale = computed(() => {
|
||||
return i18n.global.locale.value === 'zh-CN' ? dateZhCN : dateEnUS;
|
||||
});
|
||||
```
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<n-config-provider :locale="locale" :date-locale="dateLocale">
|
||||
<App />
|
||||
</el-config-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -163,7 +170,7 @@ const locale = computed(() => {
|
||||
function changeLanguage(lang: string) {
|
||||
i18n.global.locale.value = lang;
|
||||
localStorage.setItem('lang', lang);
|
||||
// 同步切换 ElementPlus 语言
|
||||
// 同步切换 NaiveUI 语言
|
||||
}
|
||||
```
|
||||
|
||||
@@ -206,7 +213,7 @@ const errorMap = {
|
||||
```
|
||||
|
||||
:::warning 注意事项
|
||||
1. ElementPlus 内置组件的文本(如"暂无数据"、"请选择")通过 `locale` 属性自动切换
|
||||
1. NaiveUI 内置组件的文本(如"暂无数据"、"请选择")通过 `locale` 属性自动切换
|
||||
2. 后端返回的 `msg` 字段需要额外处理
|
||||
3. 日期格式、数字格式也需要考虑国际化
|
||||
:::
|
||||
@@ -227,4 +234,4 @@ const errorMap = {
|
||||
|
||||
## 总结
|
||||
|
||||
当前项目为中文单语言版本,ElementPlus 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 ElementPlus 内置语言包和后端消息 code 实现全链路多语言。改造需考虑前端文本、ElementPlus 组件、后端消息三个维度的国际化。
|
||||
当前项目为中文单语言版本,NaiveUI 使用中文语言包。如需扩展多语言支持,可基于 vue-i18n 实现前端国际化,配合 NaiveUI 内置语言包和后端消息 code 实现全链路多语言。改造需考虑前端文本、NaiveUI 组件、后端消息三个维度的国际化。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
|
||||
项目使用 `basicModal` 组件(`src/components/Modal/src/BasicModal.vue`)配合 `useModal` hook 实现弹窗。编辑弹窗通过 `v-if` 懒加载,减少首屏渲染负担。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -11,7 +11,7 @@
|
||||
```
|
||||
src/components/Modal/
|
||||
├── src/
|
||||
│ ├── BasicModal.vue # 主组件(基于 el-dialog)
|
||||
│ ├── BasicModal.vue # 主组件(基于 NaiveUI n-modal)
|
||||
│ ├── props.ts # 属性定义
|
||||
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
|
||||
│ └── hooks/
|
||||
@@ -19,108 +19,149 @@ src/components/Modal/
|
||||
└── index.ts # 导出 useModal 等
|
||||
```
|
||||
|
||||
## BasicModal 组件
|
||||
## basicModal 组件
|
||||
|
||||
`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力:
|
||||
`basicModal` 基于 NaiveUI `n-modal` 封装,通过 `useModal` hook 管理弹窗状态,提供全屏切换、自定义头部、自动 OK/Cancel 按钮等能力:
|
||||
|
||||
```vue
|
||||
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
<basicModal @register="modalRegister" ref="modalRef" class="basicModal basicFormModal" @on-ok="handleSubmit" @on-close="handleClose">
|
||||
<template #default>
|
||||
<n-form ref="formRef" :model="formData" label-placement="left" label-width="85px">
|
||||
...
|
||||
</n-form>
|
||||
</template>
|
||||
</basicModal>
|
||||
```
|
||||
|
||||
```typescript
|
||||
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||
title: '编辑',
|
||||
subBtuText: '确定',
|
||||
width: 600,
|
||||
});
|
||||
```
|
||||
|
||||
核心功能:
|
||||
|
||||
- 自定义头部(标题 + 全屏/关闭按钮)
|
||||
- 提交按钮自动 loading(`setSubLoading`)
|
||||
- `setProps` 动态修改弹窗属性
|
||||
- `openModal` / `closeModal` 控制显隐
|
||||
- `useModal` hook 返回 `modalRegister`(绑定到组件 `@register`)和操作方法
|
||||
- `openModal` 打开弹窗、`setSubLoading` 控制提交按钮 loading
|
||||
- 自动渲染 OK/Cancel 底部按钮,无需手动编写 footer 模板
|
||||
- `@on-ok` 监听确定按钮点击,`@on-close` 监听关闭事件
|
||||
|
||||
## 实际用法(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 { h, nextTick, reactive, ref } from 'vue';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import editDialog from './edit.vue';
|
||||
|
||||
const positionId = ref(0);
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const createModalRef = ref();
|
||||
const editVisible = ref(false);
|
||||
const positionId = ref(0);
|
||||
|
||||
// 新增
|
||||
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();
|
||||
positionId.value = record.id;
|
||||
editVisible.value = true;
|
||||
await nextTick();
|
||||
createModalRef.value.openModal();
|
||||
};
|
||||
|
||||
// 删除
|
||||
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>
|
||||
|
||||
<template>
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
ref="createModalRef"
|
||||
:positionId="positionId"
|
||||
v-if="editVisible"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。
|
||||
父组件通过 `ref` 获取子组件实例,调用 `createModalRef.value.openModal()` 打开弹窗。子组件通过 `defineExpose({ openModal })` 暴露该方法。
|
||||
|
||||
### 编辑弹窗(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' }"
|
||||
<template #default>
|
||||
<n-form
|
||||
class="ls-form"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-placement="left"
|
||||
label-width="85px"
|
||||
>
|
||||
<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>
|
||||
<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="code">
|
||||
<n-radio-group v-model:value="formData.status" name="status">
|
||||
<n-radio :value="1">正常</n-radio>
|
||||
<n-radio :value="2">停用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="排序" path="sort">
|
||||
<n-input-number v-model:value="formData.sort" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</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 });
|
||||
|
||||
@@ -129,20 +170,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((error) => {
|
||||
setSubLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const dialogClose = () => { emit('update:visible', false); };
|
||||
|
||||
// useLockFn 防重复提交,isLock 绑定按钮 loading
|
||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||||
const handleClose = () => { emit('update:visible', false); };
|
||||
|
||||
// 编辑时加载数据
|
||||
const setFormData = async () => {
|
||||
@@ -157,27 +209,34 @@ const setFormData = async () => {
|
||||
onMounted(() => {
|
||||
if (props.positionId) setFormData();
|
||||
});
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
```
|
||||
|
||||
## 弹窗数据流
|
||||
|
||||
```
|
||||
父组件点击编辑 → positionId = record.row.id → editVisible = true
|
||||
父组件点击编辑 → positionId = record.id → editVisible = true → nextTick → createModalRef.openModal()
|
||||
↓
|
||||
basicModal 显示 → useModal 配置标题和按钮
|
||||
↓
|
||||
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
|
||||
↓
|
||||
用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)
|
||||
用户点击确定 → @on-ok → handleSubmit → formRef.validate() → positionUpdate(formData) → setSubLoading(false)
|
||||
↓
|
||||
emit('success') → 父组件 reloadTable() → 表格刷新
|
||||
```
|
||||
|
||||
## 确认弹窗
|
||||
|
||||
删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`):
|
||||
删除等危险操作使用 NaiveUI 的 `useDialog` 组合式函数:
|
||||
|
||||
```typescript
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
@@ -185,34 +244,26 @@ async function handleDelete(record: Recordable) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
|
||||
await confirm('确定要删除?');
|
||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
record ? await positionDelete(record.id) : await positionBatchDelete(ids);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`confirm` 内部调用 `ElMessageBox.confirm`,返回 Promise,用户取消时抛出异常中断后续逻辑。
|
||||
|
||||
## v-model:visible 双向绑定
|
||||
|
||||
弹窗使用 `v-model:visible` 实现父子组件双向绑定:
|
||||
|
||||
```vue
|
||||
<!-- 父组件 -->
|
||||
<editDialog v-model:visible="editVisible" />
|
||||
|
||||
<!-- 子组件 -->
|
||||
<script setup>
|
||||
const emit = defineEmits(['update:visible']);
|
||||
const dialogClose = () => { emit('update:visible', false); };
|
||||
</script>
|
||||
```
|
||||
`dialog.warning` 返回一个对话框实例,用户点击"确定"后执行 `onPositiveClick` 回调。与旧版 `confirm` 不同,不需要 `await` 和 `try/catch`,逻辑全部写在回调中。
|
||||
|
||||
:::tip 温馨提示
|
||||
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。
|
||||
弹窗组件通过 `v-if` 控制懒加载,首屏不加载弹窗代码。`useModal` 的 `setSubLoading` 控制确定按钮 loading 状态,防止重复提交。`message.success` 和 `dialog.warning` 来自 NaiveUI 的 `useMessage()` 和 `useDialog()` 组合式函数,需要在 `setup` 中调用获取实例。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
|
||||
编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态,通过 `defineExpose({ openModal })` 暴露给父组件调用。`useModal` 返回 `modalRegister` 绑定到 `@register`,以及 `openModal`、`setSubLoading` 等操作方法。表单使用 NaiveUI 的 `n-form` 系列组件,绑定方式为 `v-model:value`,校验字段用 `path` 属性。删除确认使用 `useDialog` 的 `dialog.warning` 回调模式。操作成功后通过 `emit('success')` 通知父组件刷新表格。
|
||||
|
||||
@@ -92,15 +92,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 结合
|
||||
@@ -115,7 +115,7 @@ const showAddBtn = computed(() => hasPermission(['sys:position:add']));
|
||||
```
|
||||
|
||||
```vue
|
||||
<el-button v-if="showAddBtn">添加岗位</el-button>
|
||||
<n-button v-if="showAddBtn">添加岗位</n-button>
|
||||
```
|
||||
|
||||
## 实际用法(position 模块)
|
||||
@@ -127,15 +127,15 @@ 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>
|
||||
<n-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon><n-icon><PlusOutlined /></n-icon></template>
|
||||
添加岗位
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
||||
</n-button>
|
||||
<n-button type="error" @click="handleDelete()" :disabled="!rowKeys.length"
|
||||
v-perm="['sys:position:batchDelete']">
|
||||
<template #icon><el-icon><Delete /></el-icon></template>
|
||||
<template #icon><n-icon><DeleteOutlined /></n-icon></template>
|
||||
删除
|
||||
</el-button>
|
||||
</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
||||
@@ -206,7 +206,7 @@ class ExampleAddView(PermissionRequired, View):
|
||||
|
||||
```vue
|
||||
<!-- 前端 -->
|
||||
<el-button v-perm="['sys:example:add']">添加案例</el-button>
|
||||
<n-button v-perm="['sys:example:add']">添加案例</n-button>
|
||||
```
|
||||
|
||||
## 超级管理员
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
前端基于 Vue3 + ElementPlus 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
||||
前端基于 Vue3 + NaiveUI 构建,使用 pnpm 作为包管理工具,Vite 作为构建工具,TypeScript 作为开发语言。
|
||||
:::
|
||||
|
||||
## 技术栈
|
||||
@@ -11,7 +11,7 @@
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Vue | 3.x | 渐进式 JavaScript 框架,组合式 API |
|
||||
| ElementPlus | 2.x | Vue3 组件库 |
|
||||
| NaiveUI | 2.x | Vue3 组件库 |
|
||||
| Vite | 3.x | 下一代前端构建工具 |
|
||||
| Pinia | 2.x | 状态管理(Vuex 替代方案) |
|
||||
| Vue Router | 4.x | 路由管理 |
|
||||
@@ -147,12 +147,12 @@ 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 { setupNaive, setupDirectives, setupCustomComponents } from '@/plugins';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App);
|
||||
|
||||
setupElement(app); // 全局引入 ElementPlus 组件
|
||||
setupNaive(app); // 全局引入 NaiveUI 组件
|
||||
setupCustomComponents(app); // 注册自定义组件
|
||||
setupDirectives(app); // 注册自定义指令(v-perm 等)
|
||||
setupStore(app); // 挂载 Pinia 状态管理
|
||||
@@ -165,8 +165,8 @@ async function bootstrap() {
|
||||
void bootstrap();
|
||||
```
|
||||
|
||||
初始化顺序:样式加载 -> ElementPlus -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
|
||||
初始化顺序:样式加载 -> NaiveUI -> 自定义组件 -> 自定义指令 -> Pinia Store -> Router -> 挂载。
|
||||
|
||||
## 总结
|
||||
|
||||
前端项目基于 Vue3 + ElementPlus + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
|
||||
前端项目基于 Vue3 + NaiveUI + Vite + TypeScript + Pinia 构建,使用 pnpm 管理依赖,Tailwind CSS + SCSS 提供样式能力。通过 `main.ts` 的 `bootstrap()` 函数按顺序初始化所有基础设施,目录结构按功能分层组织,便于维护和扩展。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
前端样式方案采用 Tailwind CSS + SCSS + ElementPlus 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
||||
前端样式方案采用 Tailwind CSS + SCSS + NaiveUI 主题覆盖,支持深色模式切换。全局 SCSS 变量通过 Vite 的 `additionalData` 注入。
|
||||
:::
|
||||
|
||||
## 样式架构
|
||||
@@ -13,7 +13,7 @@ src/styles/
|
||||
├── var.scss # SCSS 全局变量(通过 Vite additionalData 注入)
|
||||
├── index.scss # 入口文件,引入所有样式
|
||||
├── tailwindbase.css # Tailwind CSS 基础样式
|
||||
└── element-override.css # ElementPlus 主题覆盖(可选)
|
||||
└── naive-override.css # NaiveUI 主题覆盖(可选)
|
||||
```
|
||||
|
||||
## SCSS 全局变量
|
||||
@@ -58,36 +58,33 @@ import './styles/tailwindbase.css';
|
||||
<div class="w-full md:w-1/2 lg:w-1/3">响应式宽度</div>
|
||||
```
|
||||
|
||||
## ElementPlus 主题
|
||||
## NaiveUI 主题
|
||||
|
||||
ElementPlus 通过 CSS 变量支持主题定制:
|
||||
NaiveUI 通过 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 变量
|
||||
// App.vue 或入口组件
|
||||
import { zhCN, dateZhCN } from 'naive-ui';
|
||||
```
|
||||
|
||||
### ElementPlus 全局引入
|
||||
### NaiveUI 全局引入
|
||||
|
||||
```typescript
|
||||
// src/plugins/setupElement.ts
|
||||
import ElementPlus from 'element-plus';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
// src/plugins/naive.ts
|
||||
import { setupNaiveUI } from '@/plugins/naive';
|
||||
|
||||
export function setupElement(app) {
|
||||
app.use(ElementPlus, { locale: zhCn, size: 'default' });
|
||||
export function setupNaive(app) {
|
||||
setupNaiveUI(app);
|
||||
}
|
||||
```
|
||||
|
||||
## 深色模式
|
||||
|
||||
通过 ElementPlus 的 `dark/css-vars.css` 和 Tailwind 的 `dark` 模式实现深色主题:
|
||||
通过 NaiveUI 的暗色主题和 Tailwind 的 `dark` 模式实现深色主题:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-switch v-model="isDark" @change="toggleDark" />
|
||||
<n-switch v-model:value="isDark" @update:value="toggleDark" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -102,9 +99,9 @@ const toggleDark = (val) => {
|
||||
|
||||
```css
|
||||
.dark {
|
||||
--el-bg-color: #1d1e1f;
|
||||
--el-text-color-primary: #e5eaf3;
|
||||
--el-border-color: #4c4d4f;
|
||||
--n-color: #1d1e1f;
|
||||
--n-text-color: #e5eaf3;
|
||||
--n-border-color: #4c4d4f;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -131,14 +128,14 @@ export function useDesignSetting() {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<div>
|
||||
<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>
|
||||
</PageWrapper>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -152,4 +149,4 @@ export function useDesignSetting() {
|
||||
|
||||
## 总结
|
||||
|
||||
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ ElementPlus 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
||||
样式方案采用 Tailwind CSS 提供原子化样式 + SCSS 全局变量(Vite `additionalData` 注入)+ NaiveUI 主题覆盖,支持深色模式切换。`designSetting` store 管理主题配置,`PageWrapper` 组件提供统一页面容器。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 ElementPlus 的 `el-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
||||
`BasicTable` 位于 `src/components/Table/src/Table.vue`,基于 NaiveUI 的 `n-data-table` 二次封装,提供列定义、数据请求、分页、操作列、多选、全屏、密度调整、列设置等能力。
|
||||
:::
|
||||
|
||||
## 组件架构
|
||||
@@ -40,7 +40,7 @@ src/components/Table/
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<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']">添加岗位</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
```
|
||||
@@ -53,27 +53,27 @@ 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', 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,
|
||||
{ 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 },
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
@@ -235,9 +235,9 @@ export enum PageEnum {
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 ElementPlus 的简封装,在业务组件中直接调用即可。
|
||||
`Storage` 工具类自动将键名转为大写(如 `access-token` → `ACCESS-TOKEN`),存储时包含过期时间,读取时自动检查是否过期。`auth.ts` 中的 `confirm` 和 `message` 函数是对 NaiveUI 的简封装,在业务组件中直接调用即可。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 ElementPlus 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
||||
工具函数库提供了 Storage(本地存储,支持过期)、auth(认证管理、树结构操作、文件下载)、dateUtil(日期格式化)、httpEnum/pageEnum(枚举定义)等通用能力。`confirm` 和 `message` 是 NaiveUI 的简封装,`arrayToTree`/`treeToArray` 处理菜单数据转换。
|
||||
|
||||
@@ -26,9 +26,9 @@ src/views/
|
||||
| 文件 | 职责 | 说明 |
|
||||
|------|------|------|
|
||||
| `index.vue` | 列表页 | 搜索表单 + 工具栏 + BasicTable + 编辑弹窗引用 |
|
||||
| `edit.vue` | 编辑弹窗 | el-dialog + el-form,懒加载 |
|
||||
| `detail.vue` | 详情弹窗 | el-dialog 展示详情,懒加载 |
|
||||
| `columns.ts` | 表格列定义 | 定义列 prop、label、render 函数等 |
|
||||
| `edit.vue` | 编辑弹窗 | n-dialog + n-form,懒加载 |
|
||||
| `detail.vue` | 详情弹窗 | n-dialog 展示详情,懒加载 |
|
||||
| `columns.ts` | 表格列定义 | 定义列 key、title、render 函数等 |
|
||||
| `querySchemas.ts` | 搜索表单定义 | FormSchema 数组,定义搜索字段 |
|
||||
|
||||
## index.vue 列表页
|
||||
@@ -37,122 +37,284 @@ src/views/
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<div>
|
||||
<!-- 搜索表单卡片 -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
</n-card>
|
||||
|
||||
<!-- 数据表格卡片 -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<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"
|
||||
:auto-scroll-x="true"
|
||||
>
|
||||
<!-- 表格顶部工具栏 -->
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
|
||||
添加案例
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length"
|
||||
v-perm="['sys:example:batchDelete']">
|
||||
批量删除
|
||||
</el-button>
|
||||
<n-space>
|
||||
<!-- 新增按钮 -->
|
||||
<n-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<PlusOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
添加案例
|
||||
</n-button>
|
||||
|
||||
<!-- 批量删除按钮 -->
|
||||
<n-button
|
||||
type="error"
|
||||
@click="handleDelete()"
|
||||
:disabled="!rowKeys.length"
|
||||
v-perm="['sys:example:batchDelete']"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<DeleteOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
批量删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
</n-card>
|
||||
|
||||
<!-- 编辑对话框组件 -->
|
||||
<editDialog v-if="editVisible" :id="currentId" v-model:visible="editVisible" @success="reloadTable('noRefresh')" />
|
||||
<editDialog
|
||||
ref="editModalRef"
|
||||
v-if="editVisible"
|
||||
:id="currentId"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
<!-- 查看详情对话框组件 -->
|
||||
<detailDialog v-if="detailVisible" :id="currentId" v-model:visible="detailVisible" @success="reloadTable('noRefresh')" />
|
||||
</PageWrapper>
|
||||
<detailDialog
|
||||
ref="detailModalRef"
|
||||
v-if="detailVisible"
|
||||
:id="currentId"
|
||||
v-model:visible="detailVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { h, nextTick, reactive, ref } from 'vue';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, EyeOutlined } from '@vicons/antd';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
import { renderIcon } from '@/utils';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
import { schemas } from './querySchemas';
|
||||
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
|
||||
// 动态导入编辑对话框组件
|
||||
import editDialog from './edit.vue';
|
||||
// 动态导入详情对话框组件
|
||||
import detailDialog from './detail.vue';
|
||||
|
||||
const currentId = ref(0);
|
||||
const editVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const selectionData = ref([]);
|
||||
const tableRef = ref();
|
||||
// 消息提示和对话框
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
|
||||
const formParams = reactive({ name: '', type: '', status: '' });
|
||||
// 表格引用
|
||||
const basicTableRef = ref();
|
||||
const editModalRef = ref();
|
||||
const detailModalRef = ref();
|
||||
|
||||
// 响应式状态定义
|
||||
const currentId = ref(0); // 当前操作的ID
|
||||
const editVisible = ref(false); // 编辑对话框显示状态
|
||||
const detailVisible = ref(false); // 详情对话框显示状态
|
||||
const rowKeys = ref([]); // 表格选中行Key
|
||||
|
||||
/**
|
||||
* 定义查询参数
|
||||
* 用于存储搜索表单的值
|
||||
*/
|
||||
const formParams = reactive({
|
||||
name: '',
|
||||
type: '',
|
||||
status: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* 定义操作栏配置
|
||||
* 包含编辑、详情和删除按钮
|
||||
*/
|
||||
const actionColumn = reactive({
|
||||
width: 300,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
width: 280,
|
||||
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', onClick: handleEdit.bind(null, record), auth: ['sys:example:update'] },
|
||||
{ label: '详情', icon: 'View', type: '', onClick: handleDetail.bind(null, record), auth: ['sys:example:detail'] },
|
||||
{ label: '删除', icon: 'Delete', type: 'danger', onClick: handleDelete.bind(null, record), auth: ['sys:example:delete'] },
|
||||
{
|
||||
label: '编辑',
|
||||
icon: renderIcon(EditOutlined),
|
||||
type: 'info',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['sys:example:update'],
|
||||
},
|
||||
{
|
||||
label: '详情',
|
||||
icon: renderIcon(EyeOutlined),
|
||||
type: 'default',
|
||||
onClick: handleDetail.bind(null, record),
|
||||
auth: ['sys:example:detail'],
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: renderIcon(DeleteOutlined),
|
||||
type: 'error',
|
||||
onClick: handleDelete.bind(null, record),
|
||||
auth: ['sys:example:delete'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载数据列表
|
||||
* @param params 分页参数
|
||||
* @returns 表格数据
|
||||
*/
|
||||
const loadDataTable = async (params: any) => {
|
||||
// 清空选中行
|
||||
rowKeys.value = [];
|
||||
const result = await getExamplePage({ ...formParams, ...params });
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新表格数据
|
||||
* @param noRefresh 是否重置页码
|
||||
*/
|
||||
function reloadTable(noRefresh = '') {
|
||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册搜索表单
|
||||
* 配置表单布局和字段
|
||||
*/
|
||||
const [register, {}] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 },
|
||||
submitOnReset: true,
|
||||
schemas,
|
||||
});
|
||||
|
||||
function handleSubmit(values) {
|
||||
handleReset();
|
||||
for (const key in values) { formParams[key] = values[key]; }
|
||||
/**
|
||||
* 提交搜索表单
|
||||
* @param values 表单值
|
||||
*/
|
||||
function handleSubmit(values: Recordable) {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
for (const key in formParams) { formParams[key] = ''; }
|
||||
/**
|
||||
* 重置搜索表单
|
||||
*/
|
||||
function handleReset(values: Recordable) {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
const handleAdd = async () => { currentId.value = 0; await nextTick(); editVisible.value = true; };
|
||||
const handleEdit = async (record) => { currentId.value = record.row.id; await nextTick(); editVisible.value = true; };
|
||||
const handleDetail = async (record) => { currentId.value = record.row.id; await nextTick(); detailVisible.value = true; };
|
||||
/**
|
||||
* 表格选择项变化时的回调
|
||||
* @param keys 选中的key数组
|
||||
*/
|
||||
function onCheckedRow(keys: (string | number)[]) {
|
||||
rowKeys.value = keys;
|
||||
}
|
||||
|
||||
async function handleDelete(record) {
|
||||
/**
|
||||
* 处理新增按钮点击
|
||||
*/
|
||||
const handleAdd = async () => {
|
||||
currentId.value = 0;
|
||||
editVisible.value = true;
|
||||
await nextTick();
|
||||
editModalRef.value?.openModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理编辑按钮点击
|
||||
* @param record 当前行数据
|
||||
*/
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
currentId.value = record.id;
|
||||
editVisible.value = true;
|
||||
await nextTick();
|
||||
editModalRef.value?.openModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理详情按钮点击
|
||||
* @param record 当前行数据
|
||||
*/
|
||||
const handleDetail = async (record: Recordable) => {
|
||||
currentId.value = record.id;
|
||||
detailVisible.value = true;
|
||||
await nextTick();
|
||||
detailModalRef.value?.openModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理删除操作
|
||||
* @param record 当前行数据(可选,不传时表示批量删除)
|
||||
*/
|
||||
async function handleDelete(record?: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
if (!ids.length) { message('请选择要删除的数据', 'warning'); return; }
|
||||
// 批量删除:获取选中行的ID
|
||||
ids = rowKeys.value;
|
||||
if (!ids.length) {
|
||||
message.warning('请选择要删除的数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await confirm('确定要删除选中的案例吗?');
|
||||
record ? await exampleDelete(record.row.id) : await exampleBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
function onSelectionChange(value) { selectionData.value = value; }
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的案例吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
record ? await exampleDelete(record.id) : await exampleBatchDelete(ids);
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -160,37 +322,119 @@ src/views/
|
||||
|
||||
```typescript
|
||||
import { h } from 'vue';
|
||||
import { ElAvatar, ElTag } from 'element-plus';
|
||||
import { NTag, NAvatar, NImage } from 'naive-ui';
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
* 定义表格显示的列
|
||||
*/
|
||||
export const columns = [
|
||||
{ type: 'selection' },
|
||||
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
|
||||
{ label: '案例名称', prop: 'name', minWidth: 120 },
|
||||
{
|
||||
label: '案例图片', prop: 'avatar', minWidth: 120,
|
||||
render(record) {
|
||||
return h(ElAvatar, { size: 48, src: record.row.avatar, shape: 'square', fit: 'fill' });
|
||||
type: 'selection', // 多选框列
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
fixed: 'left',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '案例名称',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
// 普通文本渲染(无需额外配置)
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '案例类型', prop: 'type', minWidth: 100,
|
||||
title: '案例图片',
|
||||
key: 'avatar',
|
||||
width: 150,
|
||||
// 图片渲染
|
||||
render(record) {
|
||||
const statusMap = { 1: { text: '类型1', type: 'success' }, 2: { text: '类型2', type: 'danger' } };
|
||||
const status = statusMap[record.row.type];
|
||||
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.type });
|
||||
return record.avatar
|
||||
? h(NAvatar, {
|
||||
src: record.avatar,
|
||||
size: 48,
|
||||
shape: 'square',
|
||||
fit: 'cover',
|
||||
style: {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
})
|
||||
: h('span', '-');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '案例状态', prop: 'status', minWidth: 100,
|
||||
title: '案例类型',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
// 选项标签渲染
|
||||
render(record) {
|
||||
const statusMap = { 1: { text: '正常', type: 'success' }, 2: { text: '禁用', type: 'danger' } };
|
||||
const status = statusMap[record.row.status];
|
||||
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.status });
|
||||
const statusMap = {
|
||||
1: { text: '类型1', type: 'success' },
|
||||
2: { text: '类型2', type: 'error' },
|
||||
3: { text: '类型3', type: 'warning' },
|
||||
4: { text: '类型4', type: 'info' },
|
||||
};
|
||||
const status = statusMap[record.type];
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
bordered: false,
|
||||
type: status?.type || 'info',
|
||||
},
|
||||
{
|
||||
default: () => status?.text || record.type,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{ label: '排序', prop: 'sort', minWidth: 100 },
|
||||
{ label: '创建人', prop: 'createUser', minWidth: 100 },
|
||||
{ label: '创建时间', prop: 'createTime', width: 180 },
|
||||
{
|
||||
title: '案例状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
// 选项标签渲染
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
1: { text: '正常', type: 'success' },
|
||||
2: { text: '禁用', type: 'error' },
|
||||
};
|
||||
const status = statusMap[record.status];
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
bordered: false,
|
||||
type: status?.type || 'info',
|
||||
},
|
||||
{
|
||||
default: () => status?.text || record.status,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
key: 'sort',
|
||||
width: 100,
|
||||
// 普通文本渲染(无需额外配置)
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
key: 'createUser',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
@@ -199,28 +443,66 @@ export const columns = [
|
||||
```typescript
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
/**
|
||||
* 查询表单配置
|
||||
* 定义搜索区域的表单项
|
||||
*/
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name', component: 'Input', label: '案例名称',
|
||||
componentProps: { placeholder: '请输入案例名称', clearable: true },
|
||||
field: 'name',
|
||||
component: 'NInput',
|
||||
label: '案例名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入案例名称',
|
||||
clearable: true,
|
||||
filterable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type', component: 'Select', label: '案例类型',
|
||||
field: 'type',
|
||||
component: 'NSelect',
|
||||
label: '案例类型',
|
||||
componentProps: {
|
||||
placeholder: '请输入案例类型', clearable: true,
|
||||
placeholder: '请输入案例类型',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
options: [
|
||||
{ label: '类型1', value: 1 },
|
||||
{ label: '类型2', value: 2 },
|
||||
{
|
||||
label: '类型1',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '类型2',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '类型3',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '类型4',
|
||||
value: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status', component: 'Select', label: '案例状态',
|
||||
field: 'status',
|
||||
component: 'NSelect',
|
||||
label: '案例状态',
|
||||
componentProps: {
|
||||
placeholder: '请输入案例状态', clearable: true,
|
||||
placeholder: '请输入案例状态',
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
options: [
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '禁用', value: 2 },
|
||||
{
|
||||
label: '正常',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -234,7 +516,7 @@ export const schemas: FormSchema[] = [
|
||||
用户搜索 → handleSubmit → 更新 formParams → reloadTable → 重新请求
|
||||
用户新增 → handleAdd → currentId=0 → editVisible=true → edit.vue 加载 → 表单填写 → 提交
|
||||
用户编辑 → handleEdit → currentId=id → editVisible=true → edit.vue 加载详情 → 修改 → 提交
|
||||
用户删除 → confirm → API → reloadTable
|
||||
用户删除 → dialog.warning → 确认 → API → reloadTable
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
@@ -243,4 +525,4 @@ export const schemas: FormSchema[] = [
|
||||
|
||||
## 总结
|
||||
|
||||
页面视图遵循 `index.vue` + `edit.vue` + `detail.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `defineAsyncComponent` 懒加载 + `useLockFn` 防重复提交。
|
||||
页面视图遵循 `index.vue` + `edit.vue` + `detail.vue` + `columns.ts` + `querySchemas.ts` 标准模式。列表页使用 `BasicForm` 搜索 + `BasicTable` 展示数据 + `TableAction` 操作按钮,编辑弹窗使用 `import` 直接导入 + `useLockFn` 防重复提交。消息提示使用 NaiveUI 的 `useMessage()`,确认对话框使用 `useDialog()`。
|
||||
|
||||
Reference in New Issue
Block a user