@wecode-team/we0-cms
v1.1.31
Published
A CMS component for React applications with shadcn/ui
Readme
@wecode-team/we0-cms
一个基于 React 的动态 CMS 前端组件库,支持 shadcn/ui 风格的现代化 UI,与 @wecode-team/cms-supabase-api 配合使用实现完整的内容管理系统。
🛡️ CSS样式隔离
本包已实现完整的CSS样式隔离,不会被外部项目的样式污染,也不会污染外部项目的样式。
隔离机制
- CSS变量命名空间:所有设计token使用
--we0-*前缀(如--we0-primary) - 容器作用域:所有样式仅在
.we0-cms-root容器内生效 - important选择器:确保CMS样式优先级高于外部样式
- 自动包装:组件已自动包裹在隔离容器中,无需额外配置
使用方式
只需正常引入和使用,样式隔离会自动生效:
```tsx import CmsLayoutShadcn from '@wecode-team/we0-cms'; import '@wecode-team/we0-cms/dist/index.css'; // 引入CSS
function App() { return ( {/* 你的应用代码 */}
{/* CMS组件 - 样式完全隔离 */}
<CmsLayoutShadcn
inputModels={{ models: yourModels }}
brandName="My CMS"
/>
</div>); } ```
无需任何额外配置,CMS的样式不会影响<YourApp />,外部样式也不会影响CMS。
📖 设计理念
核心思想
本包的核心理念是 "配置驱动的 CMS UI":
- 模型驱动 UI:根据 JSON Schema 模型配置自动生成表单和表格
- 零代码管理:无需编写代码,通过配置即可实现数据的增删改查
- 关系可视化:支持关联字段的下拉选择和数据展示
- 多租户支持:通过 Session ID 实现数据隔离
架构设计
┌─────────────────────────────────────────────────────────────┐
│ CmsLayoutShadcn │
│ (主布局组件) │
├─────────────────────────────────────────────────────────────┤
│ UI Components (shadcn/ui) │
│ ├── Sidebar 侧边栏导航 │
│ ├── DataTable 数据表格 │
│ ├── Dialog 弹窗表单 │
│ └── Form Controls 表单控件 │
├─────────────────────────────────────────────────────────────┤
│ Pages (页面组件) │
│ ├── LoginPage 登录页面 │
│ ├── DataListPage 数据列表页 │
│ └── DataManagePage 数据管理页(按模型) │
├─────────────────────────────────────────────────────────────┤
│ Services (服务层) │
│ ├── modelApi 模型管理 API │
│ ├── dataApi 数据操作 API │
│ └── authApi 认证 API │
├─────────────────────────────────────────────────────────────┤
│ Request Layer (请求层) │
│ └── request.ts Axios 封装,自动添加 Token │
└─────────────────────────────────────────────────────────────┘数据流
用户操作 → UI 组件 → Services API → HTTP 请求 → 后端 API → Supabase
↓
用户界面 ← UI 更新 ← State 更新 ← API 响应 ←🚀 安装
npm install @wecode-team/we0-cms
# 或
pnpm add @wecode-team/we0-cms
# 或
yarn add @wecode-team/we0-cms📋 依赖
Peer Dependencies(必须安装)
{
"react": "^18.2.0",
"react-dom": "^18.2.0"
}内置依赖
@radix-ui/*- 无障碍 UI 原语lucide-react- 图标库tailwind-merge- Tailwind 类名合并class-variance-authority- 变体样式管理dayjs- 日期处理
🔧 快速开始
第一步:配置 Tailwind CSS
确保你的项目已配置 Tailwind CSS:
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./node_modules/@wecode-team/we0-cms/dist/**/*.{js,jsx}'
],
theme: {
extend: {}
},
plugins: []
}第二步:引入样式
import '@wecode-team/we0-cms/dist/index.css'第三步:使用组件
import React from 'react'
import { CmsLayoutShadcn, setSessionId } from '@wecode-team/we0-cms'
import '@wecode-team/we0-cms/dist/index.css'
// 设置 Session ID(用于多租户隔离)
setSessionId('your-session-id')
// 定义模型配置
const modelData = {
models: [
{
id: 1,
name: '用户模型',
table_name: 'users',
json_schema: {
fields: [
{
name: 'name',
type: 'string',
comment: '用户姓名',
required: true,
maxLength: 100
},
{
name: 'email',
type: 'email',
unique: true,
comment: '用户邮箱',
required: true
},
{
name: 'age',
type: 'integer',
comment: '年龄',
required: false
}
]
},
created_at: '2023-07-18T19:00:28.098Z',
updated_at: '2023-07-18T19:00:28.099Z'
}
]
}
function App() {
const uploadHandler = async (file, context) => {
const body = new FormData()
body.append('file', file)
body.append('directory', context.field.upload?.directory || '')
const response = await fetch('/your-upload-api', {
method: 'POST',
body,
}).then((res) => res.json())
return { url: response.url }
}
return (
<div className="min-h-screen">
<CmsLayoutShadcn inputModels={modelData} uploadHandler={uploadHandler} />
</div>
)
}
export default App📚 组件 API
CmsLayoutShadcn
主布局组件,包含侧边栏、导航和内容区域。
interface CmsLayoutProps {
inputModels: {
models: CmsModel[]
}
/**
* 是否跳过登录验证(免登录模式)
* 当设置为 true 时,组件将跳过所有登录检查,直接进入后台配置页面
* 适用于由使用方统一处理鉴权的场景
* @default false
*/
skipAuth?: boolean
/**
* 处理 schema 中 asset 字段上传的宿主回调
*/
uploadHandler?: CmsUploadHandler
}Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| inputModels | { models: CmsModel[] } | - | 模型配置对象 |
| skipAuth | boolean | false | 是否跳过登录验证 |
| brandName | string | "WE0-CMS" | 侧边栏顶部显示的品牌名 |
| defaultLocale | "zh-CN" \| "en-US" | "zh-CN" | 默认语言 |
| uploadHandler | CmsUploadHandler | - | 宿主注入的资源上传回调,供 asset 字段使用 |
使用示例
// 标准模式(需要登录)
<CmsLayoutShadcn inputModels={modelData} />
// 免登录模式(适用于已有鉴权系统)
<CmsLayoutShadcn inputModels={modelData} skipAuth={true} />
// 设置默认语言为英文
<CmsLayoutShadcn inputModels={modelData} defaultLocale="en-US" />
// 启用资源上传字段
<CmsLayoutShadcn inputModels={modelData} uploadHandler={uploadHandler} />资源上传字段
如果 schema 里有需要在 CMS 后台上传资源并回填 URL 的字段,可以使用 type: "asset":
const siteConfigModel = {
id: 10,
name: '站点配置',
table_name: 'site_settings',
json_schema: {
fields: [
{
name: 'site_name',
type: 'string',
comment: '站点名称',
required: true,
},
{
name: 'logo',
type: 'asset',
comment: '网站 Logo',
upload: {
accept: 'image/*',
maxSize: 2 * 1024 * 1024,
directory: 'logos',
placeholder: '请上传网站 Logo',
buttonText: '上传 Logo',
},
},
],
},
}asset 字段的值始终是纯 URL 字符串。CMS 只负责选文件、调用 uploadHandler、回填 URL 和展示预览。
上传字段 Schema 写法
下面这个例子适合“网站 Logo / Banner / 封面图”这类字段:
const siteConfigModel = {
id: 10,
name: "站点配置",
table_name: "site_settings",
json_schema: {
fields: [
{
name: "site_name",
type: "string",
comment: "站点名称",
required: true,
},
{
name: "logo",
type: "asset",
comment: "网站 Logo",
required: false,
upload: {
accept: "image/*",
maxSize: 2 * 1024 * 1024,
directory: "logos",
placeholder: "请上传网站 Logo",
buttonText: "上传 Logo",
},
},
{
name: "favicon",
type: "asset",
comment: "站点图标",
required: false,
upload: {
accept: ".ico,image/png,image/svg+xml",
maxSize: 512 * 1024,
directory: "favicons",
placeholder: "请上传 favicon",
buttonText: "上传 favicon",
},
},
],
},
}upload 配置说明:
| 字段 | 类型 | 说明 |
|------|------|------|
| accept | string | 文件类型限制,等同于 <input type="file" accept="..."> |
| maxSize | number | 单文件大小限制,单位字节 |
| directory | string | 透传给宿主上传函数的目录标识,CMS 不直接处理存储 |
| placeholder | string | 字段为空时的占位文案 |
| buttonText | string | 上传按钮文案 |
宿主如何传入上传函数
uploadHandler 由使用 we0-cms 的宿主项目传入,CMS 在用户选中文件后会调用它:
type CmsUploadHandler = (
file: File,
context: {
field: SchemaField
model: CmsModel
sessionId?: string
}
) => Promise<{ url: string }>也就是说,宿主上传函数至少会拿到:
file: 用户刚刚选择的文件对象context.field: 当前 schema 字段配置,可以读取field.name、field.upload?.directory等信息context.model: 当前模型信息,可以读取table_name、namecontext.sessionId: 如果你调用过setSessionId(),这里会带上当前 sessionId
一个完整示例如下:
import React from "react"
import {
CmsLayoutShadcn,
CmsUploadHandler,
setSessionId,
} from "@wecode-team/we0-cms"
setSessionId("your-session-id")
const uploadHandler: CmsUploadHandler = async (file, context) => {
const formData = new FormData()
formData.append("file", file)
formData.append("fieldName", context.field.name)
formData.append("tableName", context.model.table_name)
formData.append("directory", context.field.upload?.directory || "")
formData.append("sessionId", context.sessionId || "")
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
}).then((res) => res.json())
if (!response?.url) {
throw new Error("Upload API did not return a url")
}
return {
url: response.url,
}
}
function App() {
return (
<CmsLayoutShadcn
inputModels={{ models: [siteConfigModel] }}
uploadHandler={uploadHandler}
/>
)
}如果你使用的是 Supabase Storage / S3 / OSS,也推荐保持同样的返回格式:
return { url: "https://cdn.example.com/logos/logo.png" }CMS 不关心你怎么上传,只要求最终返回可展示、可存库的 URL。
🌍 国际化(i18n)
CMS组件内置了完整的国际化支持,可在中英文之间自由切换。
支持的语言
- 简体中文(zh-CN)- 默认
- English(en-US)
切换语言
在侧边栏底部点击语言图标,选择所需语言即可切换。语言偏好会自动保存。
设置默认语言
<CmsLayoutShadcn
inputModels={modelData}
defaultLocale="en-US" // 使用英文
/>在自定义组件中使用i18n
import { useT, useLocale } from '@wecode-team/we0-cms';
function MyComponent() {
const t = useT(); // 翻译函数
const { locale, setLocale } = useLocale(); // 语言状态
return (
<div>
<p>{t('auth.welcome')}</p>
<button onClick={() => setLocale('en-US')}>Switch to English</button>
</div>
);
}i18n Hooks
| Hook | 返回值 | 说明 |
|------|--------|------|
| useT() | (key: string) => string | 翻译函数 |
| useLocale() | { locale, setLocale } | 语言状态和切换函数 |
| useTranslation() | { t, locale, setLocale, messages } | 完整i18n上下文 |
setSessionId
设置会话 ID,用于多租户数据隔离。
import { setSessionId } from '@wecode-team/we0-cms'
// 设置 Session ID
setSessionId('tenant-123')
// 后续 API 调用会自动添加前缀
// 例如:GET /data/users → GET /data/tenant-123_users🏗️ 模型配置
CmsModel 结构
interface CmsModel {
id: number
name: string // 模型显示名称
table_name: string // 数据表名
json_schema: {
fields: SchemaField[]
}
created_at: string
updated_at: string
}SchemaField 结构
interface SchemaField {
name: string // 字段名
type: string // 字段类型
comment?: string // 字段显示名称
required?: boolean // 是否必填
unique?: boolean // 是否唯一
maxLength?: number // 最大长度
defaultValue?: any // 默认值
relation?: RelationConfig // 关联配置
editable?: boolean // 是否允许在后台编辑(默认 true;false=不可编辑)
readOnly?: boolean // 兼容字段:readOnly=true 等价于 editable=false
}字段不可编辑(协议层)
当你希望某些字段(如 created_at、updated_at、owner_id、token)在后台 只展示不允许修改 时,可以在字段上标记:
editable: false(推荐)- 或
readOnly: true(兼容写法)
前端行为:
- 表单控件会被禁用
- create/update 提交时会自动跳过该字段(不会传到后端)
示例:
{
name: "created_at",
type: "datetime",
comment: "创建时间",
readOnly: true
},
{
name: "owner_id",
type: "string",
comment: "归属用户",
editable: false
}字段类型
| 类型 | 渲染组件 | 说明 |
|------|----------|------|
| string | <Input /> | 单行文本输入 |
| text | <Textarea /> | 多行文本输入 |
| integer | <Input type="number" /> | 整数输入 |
| float | <Input type="number" /> | 浮点数输入 |
| boolean | <Switch /> | 开关切换 |
| date | <Input type="date" /> | 日期选择 |
| datetime | <Input type="datetime-local" /> | 日期时间选择 |
| email | <Input type="email" /> | 邮箱输入 |
| relation | <Select /> | 关联下拉选择 |
关联字段配置
interface RelationConfig {
type: 'belongsTo' | 'hasMany' | 'belongsToMany'
target: string // 目标表名
foreignKey?: string // 外键字段名
displayField?: string // 下拉显示的字段
showInList?: boolean // 是否在列表中显示
}示例:文章关联作者
{
name: 'author',
type: 'relation',
comment: '作者',
required: true,
relation: {
type: 'belongsTo',
target: 'users', // 关联 users 表
foreignKey: 'author_id', // 外键字段
displayField: 'name' // 下拉显示用户姓名
}
}时间戳和枚举字段配置
CMS支持多种时间格式和枚举字段类型,以满足不同的业务需求。
时间戳字段(timestamp)
时间戳字段支持多种显示格式:
{
name: 'last_login',
type: 'timestamp',
comment: '最后登录时间',
format: 'datetime', // 可选: 'date' | 'datetime' | 'time' | 'timestamp' | 'iso'
required: false
}支持的format选项:
date: 仅日期(YYYY-MM-DD)datetime: 日期时间(YYYY-MM-DD HH:mm:ss)time: 仅时间(HH:mm:ss)timestamp: Unix时间戳(毫秒)iso: ISO 8601格式
枚举字段(enum)
枚举字段支持下拉选择,可配置选项标签:
{
name: 'status',
type: 'string',
comment: '状态',
required: true,
enum: ['active', 'inactive', 'pending'], // 枚举值
enumLabels: { // 可选:显示标签
'active': '激活',
'inactive': '未激活',
'pending': '待审核'
}
}日期和时间字段
// 日期字段
{
name: 'birthday',
type: 'date',
comment: '生日',
format: 'date', // YYYY-MM-DD
required: false
}
// 日期时间字段
{
name: 'created_at',
type: 'datetime',
comment: '创建时间',
format: 'datetime', // YYYY-MM-DD HH:mm:ss
required: true
}
// 时间字段
{
name: 'work_time',
type: 'time',
comment: '工作时间',
format: 'time', // HH:mm:ss
required: false
}完整示例
const userModel = {
name: "用户模型",
table_name: "users",
json_schema: {
fields: [
{
name: "name",
type: "string",
comment: "用户姓名",
required: true
},
{
name: "status",
type: "string",
comment: "状态",
required: true,
enum: ["active", "inactive", "pending"],
enumLabels: {
active: "激活",
inactive: "未激活",
pending: "待审核"
}
},
{
name: "birthday",
type: "date",
comment: "生日",
format: "date"
},
{
name: "last_login",
type: "timestamp",
comment: "最后登录时间",
format: "datetime"
}
]
}
}📡 API 配置
配置 API 基础 URL
在 request.ts 中配置:
// 创建自定义 request 实例
import axios from 'axios'
const request = axios.create({
baseURL: 'http://your-api-url/api/cms',
timeout: 10000
})
// 请求拦截器 - 自动添加 Token
request.interceptors.request.use((config) => {
const token = localStorage.getItem('cms_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})API 服务
import { modelApi, dataApi, authApi } from '@wecode-team/we0-cms'
// 模型 API
await modelApi.getModels()
await modelApi.createModel(data)
await modelApi.updateModel(data)
await modelApi.deleteModel(id)
// 数据 API
await dataApi.getTableData('users', { page: 1, limit: 10 })
await dataApi.createData('users', { name: 'John' })
await dataApi.updateData('users', { id: '1', name: 'Jane' })
await dataApi.deleteData('users', '1')
// 关联选项 API
await dataApi.getRelationOptions('users', { displayField: 'name' })
// 认证 API
await authApi.login({ username: 'admin', password: '123456' })
await authApi.verifyAuth()
await authApi.getCurrentUser()
await authApi.logout()🎨 自定义样式
覆盖 CSS 变量
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... 更多变量 */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... 暗色模式变量 */
}扩展组件
import { CmsLayoutShadcn } from '@wecode-team/we0-cms'
function CustomCms() {
return (
<div className="custom-wrapper">
<header className="custom-header">
<h1>我的 CMS</h1>
</header>
<CmsLayoutShadcn inputModels={modelData} skipAuth={true} />
</div>
)
}🔐 认证流程
1. 登录流程
用户输入账号密码 → 调用 authApi.login() → 保存 Token 到 localStorage
↓
跳转到数据管理页2. Token 验证
页面加载 → 检查 localStorage 中的 Token → 调用 authApi.verifyAuth()
↓
验证成功:显示内容
验证失败:跳转登录页3. 免登录模式
// 适用于已有鉴权系统的场景
<CmsLayoutShadcn inputModels={modelData} skipAuth={true} />📱 响应式设计
组件内置响应式支持:
- 桌面端:完整侧边栏 + 内容区域
- 平板端:可折叠侧边栏
- 移动端:底部抽屉式导航
🐛 常见问题
1. 样式不生效
确保引入了 CSS 文件:
import '@wecode-team/we0-cms/dist/index.css'2. Tailwind 类名冲突
确保 Tailwind 配置包含了组件库的路径:
content: [
'./node_modules/@wecode-team/we0-cms/dist/**/*.{js,jsx}'
]3. 关联字段不显示数据
检查模型配置中的 relation.displayField 是否正确设置为目标表中存在的字段。
4. API 请求失败
检查:
- Session ID 是否正确设置
- API 基础 URL 是否正确
- CORS 是否配置正确
📄 许可证
MIT
🔗 相关包
- @wecode-team/cms-supabase-api - 后端 API 包
