@ruovea/article
v1.1.3
Published
文章/内容管理模块(文章 / 分类 / 标签)
Readme
@ruovea/article
文章/内容管理模块(文章 / 分类 / 标签),以 pnpm 源码包 形式提供给各宿主项目复用。
- 不打 dist、不发 npm——exports 直指
./src/*,由宿主 Vite 编译 - 依赖注入解耦——request / auth / i18n / ModifyRecord 由宿主装配时注入;富文本编辑器已内置在包内
- 零侵入——包内不含
/@/等宿主别名引用,可脱离 运行
一、目录结构
src/
├─ index.ts # 入口:导出 ArticlePlugin + API + 类型 + 路由
├─ plugin.ts # Vue install(校验必填项 / provide / mergeLocaleMessage)
├─ context.ts # InjectionKey + useArticleContext()
├─ types.ts # ArticlePluginOptions
├─ routes.ts # 4 条静态路由(可选使用)
├─ api/
│ ├─ index.ts # 文章/分类/标签 API
│ ├─ http.ts # request 注入点
│ ├─ endpoints.ts # URL 前缀(可覆盖)
│ ├─ article.ts # 7 个文章 API
│ ├─ category.ts # 5 个分类 API
│ └─ tag.ts # 5 个标签 API
├─ components/
│ └─ editor/index.vue # 内置富文本编辑器(wangeditor,edit 页使用)
├─ i18n/locales/ # 6 语言(en / fr-fr / ja-jp / vi-vn / zh-cn / zh-tw)
└─ views/
├─ article/
│ ├─ index.vue # 文章列表页
│ └─ edit/index.vue # 文章新增/编辑/查看页
├─ category/index.vue # 分类管理页
└─ tag/index.vue # 标签管理页二、宿主接入
2.1 安装
宿主 package.json 用 file: 协议指向本地源码包:
{
"dependencies": {
"@ruovea/article": "file:../../module/article-module/packages/article"
}
}cd <宿主> && pnpm install宿主需自行安装 wangeditor 对等依赖(peerDependencies):
{
"dependencies": {
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12"
}
}2.2 装配插件
在宿主中创建插件文件,app.use(pinia) 之后调用:
// plugins/article.ts
import { defineAsyncComponent, type App } from 'vue';
import { ArticlePlugin } from '@ruovea/article';
import request from '/@/utils/request';
import { auth } from '/@/utils/authFunction';
import { i18n } from '/@/i18n/index';
export function setupArticle(app: App) {
app.use(ArticlePlugin, {
request: request as any,
auth,
i18n: i18n as any,
components: {
ModifyRecord: defineAsyncComponent(() => import('/@/components/table/modifyRecord.vue')),
},
});
}// main.ts
import { setupArticle } from '/@/plugins/article';
// ... app.use(pinia) 之后
setupArticle(app);富文本编辑器(Editor)已内置在
@ruovea/article/components/editor中,无需宿主再注入。包内编辑器使用包注入的request实例和endpoints().fileUploadImage端点上传图片,并自动处理图片 URL 折叠/展开(提交时仅保留相对路径/FileUpload/Preview?filePath=...,显示时自动拼接宿主 baseURL)。
三、ArticlePluginOptions 说明
| 字段 | 必填 | 说明 |
|---|---|---|
| request | ✅ | 宿主 axios 实例(已配 baseURL / token / 拦截器) |
| components.ModifyRecord | ✅ | 修改记录展示组件,接收 :data prop |
| auth | ➖ | (code: string) => boolean,按钮权限校验,缺省 () => true |
| i18n | ➖ | 宿主 vue-i18n 实例(用于 mergeLocaleMessage) |
| filePreviewApi | ➖ | 封面图片预览,缺省走 GET /FileUpload/Preview |
| fileUploadApi | ➖ | 封面上传,缺省走 POST /FileUpload/Image |
| endpoints | ➖ | 覆盖 API URL 前缀(见下方 Endpoints 章节) |
| registerI18n | ➖ | 是否自动 merge i18n,默认 true |
i18n 注入约定
包内所有视图使用 message.article.xxx / message.router.xxx 格式的 key。插件 merge 时同时注册到顶层和 message 层,兼容宿主两级结构。
包的 exports 已暴露 ./i18n 子路径(articleLocales),方便宿主自行组装。
四、Endpoints 说明
所有 API 路径均可通过 endpoints 覆盖,默认值:
interface ArticleEndpoints {
articlePages: '/Article/Pages'
articleData: '/Article/Data'
articleAdd: '/Article/AddData'
articleUpdate: '/Article/UpdateData'
articleDelete: '/Article/DeleteData'
articleUpload: '/FileUpload/UploadFile' // 文章上传(保留兼容,未在视图使用)
articleUploadImage:'/FileUpload/Image' // 编辑器插图
categoryPages: '/Category/Pages'
categoryList: '/Category/List'
categoryAdd: '/Category/AddData'
categoryUpdate: '/Category/UpdateData'
categoryDelete: '/Category/DeleteData'
tagPages: '/Tag/Pages'
tagAllList: '/Tag/AllList'
tagList: '/Tag/List'
tagAdd: '/Tag/AddData'
tagUpdate: '/Tag/UpdateData'
tagDelete: '/Tag/DeleteData'
filePreview: '/FileUpload/Preview' // 封面预览 + 图片显示
fileUploadImage: '/FileUpload/Image' // 封面上传
}文件相关端点对接 附件上传管理模块(
/FileUpload/*),与文章业务端点(/Article/*、/Category/*、/Tag/*)分开维护。
五、内置 Editor 组件
包内 src/components/editor/index.vue 是基于 wangeditor v5 的封装组件,文章 edit 页直接使用。
Props
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| getHtml | string | — | v-model:getHtml 双向绑定,获取 HTML 内容 |
| getText | string | — | v-model:getText 双向绑定,获取纯文本 |
| disable | boolean | false | 是否禁用(查看态用) |
| placeholder | string | '请输入内容...' | 占位提示 |
| mode | 'default' \| 'simple' | 'default' | wangeditor 工具栏模式 |
| height | string | '310px' | 编辑区高度 |
用法
<template>
<Editor v-model:getHtml="form.content" :disable="isView" height="500px" placeholder="请输入文章内容..." />
</template>
<script setup lang="ts">
import Editor from '@ruovea/article/components/editor/index.vue';
</script>图片上传行为
- 编辑器内插图:调用包注入的
request,POST /FileUpload/Image(endpoints().fileUploadImage) - 后端返回
RestfulResult<{ filePath, fileName, ... }>,组件取data.filePath拼接预览 URL - 存储格式:HTML 中
img src仅保留相对路径/FileUpload/Preview?filePath=...,不含宿主域名 - 显示格式:编辑器读取时自动用
window.__env__.VITE_API_URL前缀补全完整 URL insertImage校验放宽:http(s)://或/FileUpload/Preview?filePath=开头均放行
宿主无需关心路径折叠,提交到后端的 content 字段永远是相对路径。
六、权限编码清单
宿主 auth(code) 需识别以下编码:
| 编码 | 位置 | 说明 |
|---|---|---|
| article:pages | 文章列表 | 查询按钮 |
| article:addData | 文章列表 | 新增按钮 |
| article:updateData | 文章列表 | 编辑按钮 |
| article:deleteData | 文章列表 | 删除按钮 |
| article:data | 文章列表 | 查看按钮 |
| article:add | 分类/标签列表 | 新增按钮 |
| article:edit | 分类/标签列表 | 编辑按钮 |
| article:delete | 分类/标签列表 | 删除按钮 |
未注入 auth 时所有按钮可见(缺省 () => true)。
七、路由设计
本包提供 4 条 vue-router 路由,覆盖文章管理全流程。路由设计同时兼容「前端控制路由」与「后端下发菜单」两种宿主场景,非 框架也能直接接入。
7.1 路由一览
| path | name | keepAlive | isHide | icon | 说明 |
|---|---|---|---|---|---|
| /business/article | articleIndex | ✅ | ❌ | ele-Document | 文章列表 |
| /business/article/edit | articleEdit | ❌ | ✅ | ele-Edit | 文章新增/编辑/查看(隐藏路由) |
| /business/article/category | articleCategory | ✅ | ❌ | ele-Folder | 分类管理 |
| /business/article/tag | articleTag | ✅ | ❌ | ele-CollectionTag | 标签管理 |
7.2 路由 meta 字段
meta: {
title: 'message.router.articleIndex', // i18n 键,包内已自包含翻译
isHide: false, // 是否在菜单隐藏
isKeepAlive: true, // 是否缓存组件状态
icon: 'ele-Document', // 菜单图标(Element Plus 图标名)
}
meta.title使用message.router.xxx形式的 i18n 键,包内 locale 文件已自包含 6 种语言翻译(en / fr-fr / ja-jp / vi-vn / zh-cn / zh-tw)。即使宿主未启用 vue-i18n,这些 key 也可作为菜单标题占位,宿主可自行替换。
7.3 两种接入方式
方式 A:前端控制路由(推荐非 框架使用)
宿主在 vue-router 路由表中直接展开包内静态路由:
// router/routes.ts
import { articleRoutes } from '@ruovea/article';
const routes = [
{
path: '/',
component: () => import('@/layout/index.vue'),
children: [
// ...宿主自己的路由
...articleRoutes,
],
},
];完整 vue-router 示例(独立项目接入):
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { articleRoutes } from '@ruovea/article';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('./layout/index.vue'),
redirect: '/business/article',
children: [
...articleRoutes,
],
},
];
export const router = createRouter({
history: createWebHistory(),
routes,
});方式 B:后端控制路由(等后端下发菜单的框架)
后端菜单表返回的 Component 字段是字符串,Vite 的 import.meta.glob 默认只扫宿主 src/views,扫不到 node_modules 里的 .vue。本包提供动态组件映射表,把 Component 字符串直接绑定到包内 import 函数:
// router/backEnd.ts
import { articleDynamicComponents } from '@ruovea/article';
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
// 包内组件优先走映射表
if (articleDynamicComponents[component]) return articleDynamicComponents[component];
// 宿主自己的 views 走 glob 匹配
// ...
}后端菜单 Component 字符串与包内视图的对应关系:
| Component 字符串 | 对应视图 |
|---|---|
| @ruovea/article/article/index | 文章列表 |
| @ruovea/article/article/edit | 文章编辑 |
| @ruovea/article/article/category | 分类管理 |
| @ruovea/article/article/tag | 标签管理 |
7.4 自定义路由 path
如需修改路由路径(例如改 /article 而非 /business/article),不要直接改包内 src/routes.ts。推荐两种做法:
做法 1:宿主自行定义路由,不展开包内 articleRoutes
import { articleDynamicComponents } from '@ruovea/article';
const routes = [
{
path: '/article',
name: 'articleIndex',
component: articleDynamicComponents['@ruovea/article/article/index'],
meta: { title: '文章列表', isKeepAlive: true, icon: 'ele-Document' },
},
// ...其余 3 条同理
];做法 2:覆盖路由表(保留 i18n 与图标配置)
import { articleRoutes } from '@ruovea/article';
const customRoutes = articleRoutes.map(r => ({
...r,
path: r.path!.replace('/business/article', '/article'),
}));7.5 路由守卫建议
包内路由不含权限拦截。如需按钮级权限控制,见 六、权限编码清单;如需路由级权限控制,建议宿主在全局守卫中处理:
router.beforeEach((to) => {
if (to.name === 'articleIndex' && !hasPermission('article:pages')) {
return '/401';
}
});八、Exports 清单
package.json exports:
| 子路径 | 指向 | 用途 |
|---|---|---|
| @ruovea/article | src/index.ts | ArticlePlugin / API / 类型 / 路由 / 上下文 |
| @ruovea/article/api | src/api/index.ts | 直接导入 API 函数 |
| @ruovea/article/i18n | src/i18n/index.ts | articleLocales 多语言资源 |
| @ruovea/article/routes | src/routes.ts | articleRoutes + articleDynamicComponents |
| @ruovea/article/components/* | src/components/* | 内置组件(editor 等) |
| @ruovea/article/types | src/types.ts | ArticlePluginOptions 等类型 |
| @ruovea/article/views/* | src/views/* | 视图文件(一般不直接引用) |
九、Vite 预构建排除
⚠️ 宿主
vite.config.ts的optimizeDeps.exclude必须包含@ruovea/article,否则会出现"页面进入即报『查询失败』、但 Network 面板无任何请求"的诡异现象。
现象
- 列表页一打开就
ElMessage.error('查询失败'); - 浏览器 DevTools Network 面板没有发出任何 HTTP 请求;
- 控制台抛出
[@ruovea/article] request 未注入,请先 app.use(ArticlePlugin, { request })。
根因
本包是 pnpm 源码包,package.json 的 exports 把 . 与 ./api 都指向 ./src/*。若不排除预构建,Vite 会把主入口 @ruovea/article 预打包成 node_modules/.vite/deps/@ruovea_article.js,但 ./api 子路径仍走源码 src/api/index.ts → src/api/http.ts。两端各持一份模块级 _request:
main.ts中setupArticle(app)→ArticlePlugin.install→__setRequest(request)写入的是预构建产物里的_request;- 视图层
import { apiArticlePagePost } from '@ruovea/article/api'拿到的是源码 http.ts 的_request,永远是undefined; - 视图调
apiArticlePagePost(params)→http()→ throw → catch →ElMessage.error('查询失败'),全程不进 axios,所以无网络请求。
修复
宿主 vite.config.ts:
optimizeDeps: {
exclude: ['vue-demi', '@ruovea/article'],
},改完清除 Vite 预构建缓存后重启 dev:
rm -rf Web/node_modules/.vite/deps
pnpm dev验证
重启后进入"文章管理"菜单,DevTools Network 应能看到 GET /Article/Pages?... 请求,列表正常渲染。
十、License
MIT
