插件功能开发文档
插件功能开发文档
本文档详细介绍插件模块的开发规范和代码写法分析,帮助开发者快速理解并开发新功能。
📁 目录结构
src/views/plugin/
└── saicms/ # 插件名称目录
├── api/ # API 接口层
│ └── news/ # 业务模块
│ ├── article.ts # 文章管理 API
│ ├── category.ts # 分类管理 API
│ └── banner.ts # 轮播图 API
└── news/ # 视图页面层
├── article/ # 文章管理
│ ├── index.vue # 主页面
│ └── modules/ # 子模块
│ ├── edit-dialog.vue # 编辑弹窗
│ └── table-search.vue # 搜索表单
├── category/ # 分类管理
└── banner/ # 轮播图管理🔌 API 接口层
文件位置
src/views/plugin/{插件名}/api/{模块名}/{功能名}.ts标准 API 模板
import request from "@/utils/http";
/**
* 文章管理 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: "/app/saicms/admin/news/SaArticle/index",
params,
});
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: "/app/saicms/admin/news/SaArticle/read?id=" + id,
});
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: "/app/saicms/admin/news/SaArticle/save",
data: params,
});
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: "/app/saicms/admin/news/SaArticle/update",
data: params,
});
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: "/app/saicms/admin/news/SaArticle/destroy",
data: params,
});
},
};API 规范说明
| 方法 | HTTP 方法 | 返回类型 | 用途 |
|---|---|---|---|
list | GET | Api.Common.ApiPage | 分页列表(带 total、items) |
read | GET | Api.Common.ApiData | 单条数据详情 |
save | POST | any | 新增数据 |
update | PUT | any | 更新数据 |
delete | DELETE | any | 删除数据(支持批量) |
注意:
- 树形数据(如分类)的
list返回类型使用Api.Common.ApiData[]- URL 路径格式:
/app/{插件名}/admin/{模块名}/{控制器名}/{方法名}
📄 主页面 (index.vue)
文件位置
src/views/plugin/{插件名}/{模块名}/{功能名}/index.vue完整代码分析
<template>
<div class="art-full-height">
<!-- 搜索面板 -->
<TableSearch
v-model="searchForm"
@search="handleSearch"
@reset="resetSearchParams"
/>
<ElCard class="art-table-card" shadow="never">
<!-- 表格头部 -->
<ArtTableHeader
v-model:columns="columnChecks"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<ElSpace wrap>
<!-- 新增按钮 -->
<ElButton
v-permission="'saicms:news:article:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<!-- 批量删除按钮 -->
<ElButton
v-permission="'saicms:news:article:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
rowKey="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<!-- 操作列 -->
<template #operation="{ row }">
<div class="flex gap-2">
<SaButton
v-permission="'saicms:news:article:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'saicms:news:article:destroy'"
type="error"
@click="deleteRow(row, api.delete, refreshData)"
/>
</div>
</template>
</ArtTable>
</ElCard>
<!-- 编辑弹窗 -->
<EditDialog
v-model="dialogVisible"
:dialog-type="dialogType"
:data="dialogData"
@success="refreshData"
/>
</div>
</template>Script 核心逻辑解析
<script setup lang="ts">
import { useTable } from "@/hooks/core/useTable";
import { useSaiAdmin } from "@/composables/useSaiAdmin";
import api from "../../api/news/article";
import TableSearch from "./modules/table-search.vue";
import EditDialog from "./modules/edit-dialog.vue";
// ========== 1. 搜索表单 ==========
const searchForm = ref({
title: undefined, // 搜索字段,对应后端查询参数
});
// 搜索处理函数
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params); // 合并搜索参数
getData(); // 触发数据请求
};
// ========== 2. 表格配置 (useTable) ==========
const {
columns, // 表格列配置
columnChecks, // 列显示/隐藏状态
data, // 表格数据
loading, // 加载状态
getData, // 获取数据方法
searchParams, // 搜索参数对象
pagination, // 分页配置
resetSearchParams, // 重置搜索参数
handleSortChange, // 排序变化处理
handleSizeChange, // 每页条数变化
handleCurrentChange, // 当前页变化
refreshData, // 刷新数据
} = useTable({
core: {
apiFn: api.list, // API 请求函数
columnsFactory: () => [
// 列配置工厂函数
{ type: "selection" }, // 多选列
{ prop: "title", label: "文章标题" }, // 普通文本列
{ prop: "author", label: "文章作者" },
{ prop: "image", label: "文章图片", saiType: "image" }, // 图片列
{ prop: "views", label: "浏览次数" },
{ prop: "sort", label: "排序" },
{
prop: "status",
label: "状态",
saiType: "dict",
saiDict: "data_status",
}, // 字典列
{
prop: "operation",
label: "操作",
width: 100,
fixed: "right",
useSlot: true,
}, // 操作列
],
},
});
// ========== 3. 弹窗与操作 (useSaiAdmin) ==========
const {
dialogType, // 弹窗类型 ('add' | 'edit')
dialogVisible, // 弹窗显示状态
dialogData, // 传递给弹窗的数据
showDialog, // 显示弹窗方法
deleteRow, // 删除单行方法
deleteSelectedRows, // 批量删除方法
handleSelectionChange, // 选择变化处理
selectedRows, // 已选中的行
} = useSaiAdmin();
</script>关键写法说明
1. useTable Hook
useTable 是表格数据管理的核心 Hook,自动处理:
- 数据获取与缓存
- 分页控制
- 搜索功能
- 列配置管理
- 排序处理
columnsFactory 列配置:
| 属性 | 类型 | 说明 |
|---|---|---|
type | 'selection' | 'index' | 'expand' | 特殊列类型 |
prop | string | 数据字段名 |
label | string | 列标题 |
width | number | 列宽度 |
fixed | 'left' | 'right' | 固定列 |
saiType | 'image' | 'dict' | 'switch' | 特殊渲染类型 |
saiDict | string | 字典编码(saiType='dict' 时使用) |
useSlot | boolean | 是否使用插槽自定义渲染 |
2. useSaiAdmin Composable
useSaiAdmin 提供统一的 CRUD 操作状态管理:
// 显示新增弹窗
showDialog("add");
// 显示编辑弹窗,传入行数据
showDialog("edit", row);
// 删除单行,传入 API 函数和回调
deleteRow(row, api.delete, refreshData);
// 批量删除
deleteSelectedRows(api.delete, refreshData);3. 权限控制
使用 v-permission 指令控制按钮显示:
<ElButton v-permission="'saicms:news:article:save'">新增</ElButton>
<ElButton v-permission="'saicms:news:article:update'">编辑</ElButton>
<ElButton v-permission="'saicms:news:article:destroy'">删除</ElButton>权限编码格式:{插件名}:{模块名}:{功能名}:{操作}
🔍 搜索表单组件 (table-search.vue)
完整代码
<template>
<sa-search-bar
ref="searchBarRef"
v-model="formData"
label-width="100px"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
@expand="handleExpand"
>
<el-col v-bind="setSpan(6)">
<el-form-item label="文章标题" prop="title">
<el-input
v-model="formData.title"
placeholder="请输入文章标题"
clearable
/>
</el-form-item>
</el-col>
</sa-search-bar>
</template>
<script setup lang="ts">
interface Props {
modelValue: Record<string, any>;
}
interface Emits {
(e: "update:modelValue", value: Record<string, any>): void;
(e: "search", params: Record<string, any>): void;
(e: "reset"): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// 展开/收起状态
const isExpanded = ref<boolean>(false);
// 表单数据双向绑定
const searchBarRef = ref();
const formData = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
// 重置
function handleReset() {
searchBarRef.value?.ref.resetFields();
emit("reset");
}
// 搜索
async function handleSearch() {
emit("search", formData.value);
}
// 展开/收起
function handleExpand(expanded: boolean) {
isExpanded.value = expanded;
}
// 响应式栅格配置
const setSpan = (span: number) => {
return {
span: span,
xs: 24, // 手机:满宽
sm: span >= 12 ? span : 12, // 平板:半宽
md: span >= 8 ? span : 8, // 中屏:三分之一
lg: span,
xl: span,
};
};
</script>写法说明
- v-model 双向绑定:通过
computed+emit实现与父组件的数据同步 - 响应式栅格:
setSpan()函数根据屏幕尺寸自适应列宽 - sa-search-bar 组件:封装的搜索栏组件,内置搜索/重置按钮
✏️ 编辑弹窗组件 (edit-dialog.vue)
核心结构
<template>
<el-dialog
v-model="visible"
:title="dialogType === 'add' ? '新增文章管理' : '编辑文章管理'"
width="1024px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<!-- 表单项 -->
<el-form-item label="文章标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入文章标题" />
</el-form-item>
<!-- 图片上传 -->
<el-form-item label="文章图片" prop="image">
<sa-image-upload
v-model="formData.image"
:limit="1"
:multiple="false"
/>
</el-form-item>
<!-- 富文本编辑器 -->
<el-form-item label="文章内容" prop="content">
<sa-editor v-model="formData.content" height="400px" />
</el-form-item>
<!-- 字典单选 -->
<el-form-item label="状态" prop="status">
<sa-radio v-model="formData.status" dict="data_status" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>Script 核心逻辑
<script setup lang="ts">
import api from "../../../api/news/article";
import { ElMessage } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
// ========== Props & Emits 定义 ==========
interface Props {
modelValue: boolean; // 弹窗显示状态
dialogType: string; // 弹窗类型 'add' | 'edit'
data?: Record<string, any>; // 编辑时传入的数据
}
interface Emits {
(e: "update:modelValue", value: boolean): void;
(e: "success"): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dialogType: "add",
data: undefined,
});
const emit = defineEmits<Emits>();
const formRef = ref<FormInstance>();
// ========== 弹窗显示状态双向绑定 ==========
const visible = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
// ========== 表单验证规则 ==========
const rules = reactive<FormRules>({
title: [{ required: true, message: "文章标题必需填写", trigger: "blur" }],
content: [{ required: true, message: "文章内容必需填写", trigger: "blur" }],
});
// ========== 初始数据(用于重置) ==========
const initialFormData = {
id: null,
title: "",
author: "",
image: "",
content: "",
views: null,
sort: 100,
status: 1,
};
// ========== 表单数据 ==========
const formData = reactive({ ...initialFormData });
// ========== 监听弹窗打开 ==========
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
initPage();
}
},
);
// ========== 初始化页面 ==========
const initPage = async () => {
// 1. 先重置为初始值
Object.assign(formData, initialFormData);
// 2. 如果是编辑模式,填充数据
if (props.data) {
await nextTick();
initForm();
}
};
// ========== 填充表单数据 ==========
const initForm = () => {
if (props.data) {
for (const key in formData) {
if (props.data[key] != null && props.data[key] != undefined) {
(formData as any)[key] = props.data[key];
}
}
}
};
// ========== 关闭弹窗 ==========
const handleClose = () => {
visible.value = false;
formRef.value?.resetFields();
};
// ========== 提交表单 ==========
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
if (props.dialogType === "add") {
await api.save(formData);
ElMessage.success("新增成功");
} else {
await api.update(formData);
ElMessage.success("修改成功");
}
emit("success"); // 通知父组件刷新数据
handleClose();
} catch (error) {
console.log("表单验证失败:", error);
}
};
</script>关键模式说明
1. 弹窗 v-model 模式
// 使用 computed 实现 v-model 双向绑定
const visible = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});2. 表单数据初始化模式
// 定义初始数据常量
const initialFormData = { ... }
// 使用 reactive 创建响应式表单
const formData = reactive({ ...initialFormData })
// 打开弹窗时重置
Object.assign(formData, initialFormData)3. 编辑数据填充模式
const initForm = () => {
if (props.data) {
for (const key in formData) {
if (props.data[key] != null && props.data[key] != undefined) {
(formData as any)[key] = props.data[key];
}
}
}
};🌳 树形数据特殊处理
对于分类等树形结构数据,需要额外处理:
1. API 调用
// 获取树形结构数据
const data = await api.list({ tree: true });2. 编辑弹窗中的树形选择器
const optionData = reactive({
treeData: <any[]>[],
});
const initPage = async () => {
// 获取树形数据
const data = await api.list({ tree: true });
optionData.treeData = [
{
id: 0,
value: 0,
label: "无上级分类",
children: data,
},
];
};3. 表格展开/收起功能
<ElButton @click="toggleExpand" v-ripple>
<template #icon>
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
</template>
展开
</ElButton>
<script setup>
const isExpanded = ref(false)
const tableRef = ref()
const toggleExpand = (): void => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value?.elTableRef && data.value) {
const processRows = (rows: any[]) => {
rows.forEach((row) => {
if (row.children?.length) {
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
processRows(row.children)
}
})
}
processRows(data.value)
}
})
}
</script>📋 快速开发清单
新增功能模块步骤
创建 API 文件
- 路径:
api/{模块名}/{功能名}.ts - 包含:
list,read,save,update,delete五个方法
- 路径:
创建主页面
- 路径:
{模块名}/{功能名}/index.vue - 引入
useTable和useSaiAdmin - 配置
columnsFactory列定义
- 路径:
创建搜索组件
- 路径:
{模块名}/{功能名}/modules/table-search.vue - 使用
sa-search-bar组件
- 路径:
创建编辑弹窗
- 路径:
{模块名}/{功能名}/modules/edit-dialog.vue - 定义
initialFormData和rules - 实现
initPage和handleSubmit
- 路径:
配置路由和菜单(后台管理系统配置)
🎯 常用 SAI 组件速查
| 组件 | 用途 | 示例 |
|---|---|---|
sa-radio | 字典单选 | <sa-radio v-model="form.status" dict="data_status" /> |
sa-select | 字典下拉 | <sa-select v-model="form.type" dict="article_type" /> |
sa-switch | 状态开关 | <sa-switch v-model="form.status" /> |
sa-image-upload | 图片上传 | <sa-image-upload v-model="form.image" :limit="1" /> |
sa-editor | 富文本 | <sa-editor v-model="form.content" height="400px" /> |
SaButton | 操作按钮 | <SaButton type="secondary" @click="..." /> |
⚡ 最佳实践
- API 路径命名:遵循 RESTful 风格
- 权限编码:使用统一的命名规范
- 表单验证:必填字段使用
required规则 - 初始数据:定义
initialFormData便于重置 - 组件复用:使用 SAI 组件库减少重复代码
- 类型安全:使用 TypeScript 接口定义
