feat: 初始化tansci资产管理项目

包含以下模块:
- antdv-next-admin: Vue 3 + TypeScript + Ant Design Vue 管理后台
  - 设备/许可证/配件/耗材 CRUD 管理页面
  - 基础数据管理 (分类/位置/制造商/型号/供应商)
  - 业务管理 (故障报修/盘点/资产分配/资产申请/交易记录)
  - 下拉选项改造 (ID输入框 → 搜索下拉选择)
  - 资产状态字典化 (接入sys_dict系统)
  - 界面文案优化 (设备→资产, 在库/在用/维修中/已报废)
  - 修复 console 警告 (popupClassName, 重复组件注册)
- our-itam: Java Spring Boot + magic-api 后端服务
- fantastic-admin: 前端底层框架 (pnpm monorepo)
- ciyo-itasset: CIYO 资产模块
- magic-script-skill: Claude Code skill 定义
- .claude: 对话历史记录

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
xuewuerduo 2026-05-17 21:41:22 +08:00
commit f468d532b1
3353 changed files with 522094 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,111 @@
---
name: fa-crud-page-generator
description: "为 Fantastic-admin 框架生成完整的 CRUD 业务模块,包含列表页、详情/编辑页、表单组件、API 模块和 Mock 数据。当用户说:'帮我做一个商品管理页面'、'搭一个用户列表,能搜索、能分页、能新增编辑删除'、'生成一个订单模块'、'我需要一个带增删改查的页面'、'快速搭一个管理页面骨架',即使用户只是说'新建一个 XX 管理模块'也应触发此技能。"
---
# CRUD 页面生成器
在 Fantastic-admin 框架中生成完整的 CRUD 业务模块,一次性生成所有相关文件,让开发者专注于业务逻辑而非重复代码。
**生成的文件清单:**
- `apps/<app>/src/views/{path}/{name}/list.vue` — 列表页(搜索栏 + 表格 + 分页)
- `apps/<app>/src/views/{path}/{name}/detail.vue` — 详情/编辑页(仅 router 模式需要)
- `apps/<app>/src/views/{path}/{name}/components/DetailForm/index.vue` — 表单组件
- `apps/<app>/src/api/modules/{fileName}.ts` — API 模块
- `apps/<app>/src/api/modules/{fileName}.fake.ts` — Mock 数据(可选)
---
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中生成 CRUD 模块,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根。
---
## 工作流程
### Step 1收集基本信息
向用户询问以下内容(可合并为一次提问):
1. **模块名**(英文,用于文件名和路由,如 `user`、`product`、`order`
2. **模块中文名**(用于页面标题和提示文字,如 `用户`、`商品`、`订单`
3. **存放路径**(在 `apps/<app>/src/views/` 下的子目录,如 `system`、`mall/product`,留空则直接放在 `src/views/` 下)
### Step 2收集字段信息
询问用户该模块有哪些字段,每个字段需要:
- **字段名**(英文,如 `name`、`status`、`createdAt`
- **中文标签**(如 `姓名`、`状态`、`创建时间`
- **字段类型**`string` | `number` | `boolean` | `date`
- **是否在列表中显示**
- **是否在搜索栏中显示**
- **是否在表单中显示**(新增/编辑)
- **是否必填**(表单验证)
如果用户没有提供字段信息,使用默认字段 `title`标题string必填作为示例占位并在生成后提示用户替换。
### Step 3收集功能选项
以下三个选项会直接影响生成的文件结构,需要在同一轮提问中一起确认,避免后续重复生成:
4. **详情展示模式**
- `router` — 跳转到独立的详情页(默认,适合复杂表单)
- `modal` — 弹窗(适合简单表单)
- `drawer` — 抽屉(适合中等复杂度表单)
5. **是否生成 Mock 数据**(是/否,开发阶段推荐开启)
6. **是否同时生成路由配置**(是/否,仅 `app.routeBaseOn``frontend` 时有效;选是时需额外询问归属的主导航分组名称)
### Step 4确认并生成
汇总信息,展示将要生成的文件列表,确认后写入文件。
---
## 命名规范
给定模块名 `name` 和路径 `path`(相对于 `apps/<app>/src/views/`
| 用途 | 规则 | 示例path=system, name=user |
|------|------|-------------------------------|
| 视图目录 | `apps/<app>/src/views/{path}/{snakeCase(name)}/` | `apps/<app>/src/views/system/user/` |
| API 文件名 | path 非空时:`{camelCase(path)}_{camelCase(name)}`path 为空时:`{camelCase(name)}` | `systemUser` |
| 组件名 | `PascalCase({path}-{name}-list/detail)` | `SystemUserList` |
| API URL 前缀 | `/{path}/{snakeCase(name)}/` | `/system/user/` |
**snakeCase 规则**:多个单词用下划线连接,如 `productCategory``product_category`
---
## 代码模板
详细的代码模板见 [references/templates.md](references/templates.md),其中也包含 faker.js 字段类型映射表。
生成代码时,根据用户提供的字段信息替换模板中的占位符:
- 列表页的 `ElTableColumn` 根据"列表显示"字段生成
- 搜索栏的 `ElFormItem` 根据"搜索显示"字段生成
- 表单组件的 `ElFormItem` 根据"表单显示"字段生成
- API 模块的 TypeScript 类型根据字段定义生成
- Mock 数据根据字段类型使用合适的 faker.js 方法生成(见 templates.md 中的映射表)
---
## 路由生成(可选)
如果用户选择同时生成路由,先读取 `apps/<app>/src/settings.ts` 确认 `app.routeBaseOn === 'frontend'`,然后使用 `fa-route-generator` 技能生成对应路由配置:
- **router 模式**:生成列表路由 + 详情路由(详情路由带 `menu: false``activeMenu` 指向列表)
- **modal/drawer 模式**:只生成列表路由
---
## 生成后提示用户
文件生成完成后简要告知:如果未生成路由,可用 `fa-route-generator` 技能添加;生成了 `.fake.ts` 则 API 请求会自动走 mock表单字段类型Select、DatePicker 等)需根据业务自行替换。

View File

@ -0,0 +1,614 @@
# CRUD 代码模板
以下模板基于 `src/views/pages_example/manager` 真实模块提炼,使用以下占位符:
- `{cname}` — 模块中文名,如 `用户`
- `{componentNameList}` — 列表页组件名PascalCase`SystemUserList`
- `{componentNameDetail}` — 详情页组件名PascalCase`SystemUserDetail`
- `{fileName}` — API 文件名camelCase`systemUser`
- `{apiPrefix}` — API URL 前缀(无前导斜杠),如 `system/user`
- `{routeListName}` — 列表路由 name`systemUserList`
- `{routeDetailName}` — 详情路由 name`systemUserDetail`(仅 router 模式)
---
## list.vue 模板
```vue
<script setup lang="ts">
import api{FileName} from '@/api/modules/{fileName}'
import eventBus from '@/utils/eventBus'
import DetailForm from './components/DetailForm/index.vue'
defineOptions({
name: '{componentNameList}',
})
const router = useRouter()
const { pagination, getParams, onSizeChange, onCurrentChange, onSortChange } = usePagination()
// 表格是否自适应高度
const tableAutoHeight = ref(false)
/**
* 详情展示模式
* router 路由跳转
* modal 模态框
* drawer 抽屉
*/
const formMode = ref<'router' | 'modal' | 'drawer'>('{formMode}')
// 详情
const formModeProps = ref({
id: '',
})
// 搜索
const searchDefault = {
{searchDefaults}
}
const search = ref({ ...searchDefault })
function searchReset() {
Object.assign(search.value, searchDefault)
}
// 批量操作
const batch = ref({
enable: true,
selectionDataList: [],
})
// 列表
const loading = ref(false)
const dataList = ref([])
onMounted(() => {
getDataList()
if (formMode.value === 'router') {
eventBus.on('get-data-list', () => {
getDataList()
})
}
})
onBeforeUnmount(() => {
if (formMode.value === 'router') {
eventBus.off('get-data-list')
}
})
function getDataList() {
loading.value = true
const params = {
...getParams(),
{searchParams}
}
api{FileName}.list(params).then((res: any) => {
loading.value = false
dataList.value = res.data.list
pagination.value.total = res.data.total
})
}
// 每页数量切换
function sizeChange(size: number) {
onSizeChange(size).then(() => getDataList())
}
// 当前页码切换(翻页)
function currentChange(page = 1) {
onCurrentChange(page).then(() => getDataList())
}
// 字段排序
function sortChange({ prop, order }: { prop: string, order: string }) {
onSortChange(prop, order).then(() => getDataList())
}
const formRef = ref<InstanceType<typeof DetailForm>>()
const { open: openModal, update: updateModal } = useFaModal().create({
destroyOnClose: true,
closeOnClickOverlay: false,
closeOnPressEscape: false,
beforeClose: (action, done) => {
if (action === 'confirm') {
// 调用 DetailForm 组件内部 submit 方法
formRef.value?.submit().then(() => {
getDataList()
done()
})
}
else {
done()
}
},
content: () => h(DetailForm, {
ref: formRef,
id: formModeProps.value.id,
}),
})
const { open: openDrawer, update: updateDrawer } = useFaDrawer().create({
destroyOnClose: true,
closeOnClickOverlay: false,
closeOnPressEscape: false,
beforeClose: (action, done) => {
if (action === 'confirm') {
// 调用 DetailForm 组件内部 submit 方法
formRef.value?.submit().then(() => {
getDataList()
done()
})
}
else {
done()
}
},
content: () => h(DetailForm, {
ref: formRef,
id: formModeProps.value.id,
}),
})
function onCreate() {
if (formMode.value === 'router') {
router.push({ name: '{routeDetailName}' })
}
else {
formModeProps.value.id = ''
if (formMode.value === 'modal') {
updateModal({ title: '新增{cname}' })
openModal()
}
else {
updateDrawer({ title: '新增{cname}' })
openDrawer()
}
}
}
function onEdit(row: any) {
if (formMode.value === 'router') {
router.push({ name: '{routeDetailName}', params: { id: row.id } })
}
else {
formModeProps.value.id = row.id
if (formMode.value === 'modal') {
updateModal({ title: '编辑{cname}' })
openModal()
}
else {
updateDrawer({ title: '编辑{cname}' })
openDrawer()
}
}
}
function onDel(row: any) {
useFaModal().confirm({
title: '确认信息',
content: `确认删除「${row.{firstField}}」吗?`,
onConfirm: () => {
api{FileName}.delete(row.id).then(() => {
getDataList()
faToast.success('删除成功')
})
},
})
}
</script>
<template>
<div :class="{ 'absolute flex flex-col size-full': tableAutoHeight }">
<FaPageHeader title="{cname}管理" class="mb-0" />
<FaPageMain :class="{ 'flex-1 overflow-auto': tableAutoHeight }" :main-class="{ 'flex-1 flex flex-col overflow-auto': tableAutoHeight }">
<FaSearchBar :show-toggle="false">
<template #default="{ fold, toggle }">
<div class="gap-x-8 gap-y-2 grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))]">
{fields.search}
<div class="flex gap-2 col-end--1 justify-end">
<FaButton variant="outline" @click="searchReset(); currentChange()">
重置
</FaButton>
<FaButton type="primary" @click="currentChange()">
<FaIcon name="i-ri:search-line" />
筛选
</FaButton>
<FaButton variant="ghost" @click="toggle">
{{ fold ? '展开' : '收起' }}
<FaIcon :name="fold ? 'i-ep:caret-bottom' : 'i-ep:caret-top'" />
</FaButton>
</div>
</div>
</template>
</FaSearchBar>
<div class="mx--4 my-4 border-t border-t-dashed" />
<div class="flex-center-between gap-2">
<FaButton v-if="batch.enable" variant="outline" :disabled="!batch.selectionDataList.length">
批量操作
</FaButton>
<FaButton @click="onCreate">
<FaIcon name="i-ri:add-line" />
新增
</FaButton>
</div>
<ElTable v-loading="loading" class="my-4" :data="dataList" stripe highlight-current-row border height="100%" @sort-change="sortChange" @selection-change="batch.selectionDataList = $event">
<ElTableColumn v-if="batch.enable" type="selection" align="center" fixed />
{fields.list}
<ElTableColumn label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<div class="flex-center gap-2">
<FaButton variant="outline" size="icon-sm" @click="onEdit(scope.row)">
<FaIcon name="i-ri:edit-line" />
</FaButton>
<FaDropdown
:items="[
[
{ label: '删除', variant: 'destructive', handle: () => onDel(scope.row) },
],
]"
>
<FaButton variant="outline" size="icon-sm">
<FaIcon name="i-ri:more-line" />
</FaButton>
</FaDropdown>
</div>
</template>
</ElTableColumn>
</ElTable>
<FaPagination :page="pagination.page" :size="pagination.size" :total="pagination.total" @page-change="currentChange" @size-change="sizeChange" />
</FaPageMain>
</div>
</template>
```
> 注意:`list.vue` 不需要 `<style scoped>` 块,所有样式均通过 UnoCSS 工具类实现。
---
## detail.vue 模板(仅 router 模式)
```vue
<script setup lang="ts">
import eventBus from '@/utils/eventBus'
import DetailForm from './components/DetailForm/index.vue'
defineOptions({
name: '{componentNameDetail}',
})
const route = useRoute()
const router = useRouter()
const formRef = useTemplateRef('formRef')
function onSubmit() {
formRef.value?.submit().then(() => {
eventBus.emit('get-data-list')
onCancel()
})
}
function onCancel() {
router.back({ name: '{routeListName}' })
}
</script>
<template>
<div>
<FaFixedBar position="top" class="p-0">
<FaPageHeader :title="route.params.id ? '编辑{cname}' : '新增{cname}'" class="mb-0 border-b-none">
<FaButton variant="outline" size="sm" class="rounded-full" @click="onCancel">
<FaIcon name="i-ep:arrow-left" />
返回
</FaButton>
</FaPageHeader>
</FaFixedBar>
<FaPageMain>
<ElRow>
<ElCol :md="24" :lg="16">
<DetailForm :id="route.params.id as string" ref="formRef" />
</ElCol>
</ElRow>
</FaPageMain>
<FaFixedBar position="bottom" class="flex-center gap-4">
<FaButton @click="onSubmit">
提交
</FaButton>
<FaButton variant="outline" @click="onCancel">
取消
</FaButton>
</FaFixedBar>
</div>
</template>
```
---
## components/DetailForm/index.vue 模板
```vue
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import api{FileName} from '@/api/modules/{fileName}'
export interface Props {
id?: number | string
}
const props = withDefaults(
defineProps<Props>(),
{
id: '',
},
)
const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = ref({
id: props.id,
{fields.formInit}
})
const formRules = ref<FormRules>({
{fields.rules}
})
onMounted(() => {
if (form.value.id !== '') {
getInfo()
}
})
function getInfo() {
loading.value = true
api{FileName}.detail(form.value.id).then((res: any) => {
loading.value = false
{fields.formAssign}
}).catch(() => {
loading.value = false
})
}
defineExpose({
submit() {
return new Promise<void>((resolve) => {
formRef.value?.validate((valid) => {
if (valid) {
if (form.value.id === '') {
api{FileName}.create(form.value).then(() => {
faToast.success('新增成功')
resolve()
})
}
else {
api{FileName}.edit(form.value).then(() => {
faToast.success('编辑成功')
resolve()
})
}
}
})
})
},
})
</script>
<template>
<div v-loading="loading">
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px" label-suffix="">
{fields.form}
</ElForm>
</div>
</template>
```
---
## api.ts 模板
```typescript
import api from '../index'
export default {
list: (data: {
{searchTypes}
from: number
limit: number
}) => api.get('{apiPrefix}/list', {
params: data,
fake: true,
}),
detail: (id: number | string) => api.get('{apiPrefix}/detail', {
params: { id },
fake: true,
}),
create: (data: any) => api.post('{apiPrefix}/create', data, {
fake: true,
}),
edit: (data: any) => api.post('{apiPrefix}/edit', data, {
fake: true,
}),
delete: (id: number | string) => api.post('{apiPrefix}/delete', { id }, {
fake: true,
}),
}
```
> 不需要 Mock 数据时,去掉所有 `fake: true` 选项即可。
---
## fake.ts 模板
```typescript
import { faker } from '@faker-js/faker/locale/zh_CN'
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
const {moduleName}List: any[] = []
for (let i = 0; i < 50; i++) {
{moduleName}List.push({
id: i + 1,
{fields.mock}
})
}
export default defineFakeRoute([
{
url: '/fake/{apiPrefix}/list',
method: 'get',
response: ({ query }) => {
const { {searchQueryFields}, from, limit } = query
let list = {moduleName}List
{searchFilters}
const pageList = list.filter((_item, index) => {
return index >= ~~from && index < (~~from + ~~limit)
})
return {
error: '',
status: 1,
data: {
list: pageList,
total: list.length,
},
}
},
},
{
url: '/fake/{apiPrefix}/detail',
method: 'get',
response: ({ query }) => {
const info = {moduleName}List.filter(item => item.id === ~~query.id)
return {
error: '',
status: 1,
data: info[0],
}
},
},
{
url: '/fake/{apiPrefix}/create',
method: 'post',
response: () => {
return { error: '', status: 1, data: { isSuccess: true } }
},
},
{
url: '/fake/{apiPrefix}/edit',
method: 'post',
response: () => {
return { error: '', status: 1, data: { isSuccess: true } }
},
},
{
url: '/fake/{apiPrefix}/delete',
method: 'post',
response: () => {
return { error: '', status: 1, data: { isSuccess: true } }
},
},
])
```
---
## 字段生成规则
### 搜索栏字段
每个搜索字段用 `FaLabel` 包裹,内部控件根据字段类型自行选择合适的 Fa* 或 El* 组件。次要搜索条件加 `v-show="!fold"` 实现折叠。
```vue
<FaLabel label="{label}" class="col-span-1">
<!-- 内部放对应的输入控件string 用 FaInput枚举用 FaSelect日期用 ElDatePicker 等 -->
</FaLabel>
```
### 列表列
普通列直接用 `prop` 渲染,需要自定义展示时使用 `#default` slot
```vue
<!-- 普通列 -->
<ElTableColumn prop="{field}" label="{label}" />
<!-- 需要自定义渲染时 -->
<ElTableColumn prop="{field}" label="{label}" width="100" align="center">
<template #default="scope">
<!-- 根据字段含义选择合适的展示方式,如 ElTag、FaSwitch 等 -->
</template>
</ElTableColumn>
```
### 表单字段
每个表单字段用 `ElFormItem` 包裹,内部控件根据字段类型自行选择合适的 Fa* 或 El* 组件,注意加 `class="w-full"`
```vue
<ElFormItem label="{label}" prop="{field}">
<!-- 内部放对应的输入控件 -->
</ElFormItem>
```
### 表单验证规则
```typescript
// 必填文本
{field}: [
{ required: true, message: '请输入{label}', trigger: 'blur' },
],
// 必填选择
{field}: [
{ required: true, message: '请选择{label}', trigger: 'change' },
],
```
### Mock 数据字段映射
| 字段类型 | faker 方法 |
|---------|-----------|
| 姓名 | `faker.person.fullName()` |
| 账号/用户名 | `faker.person.firstName()` |
| 手机号 | `faker.phone.number({ style: 'international' })` |
| 邮箱 | `faker.internet.email()` |
| 标题/名称 | `faker.lorem.words(3)` |
| 描述 | `faker.lorem.sentence()` |
| 数字(枚举) | `faker.number.int(2)` |
| 布尔(状态) | `faker.datatype.boolean()` |
| 金额 | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` |
| 日期 | `faker.date.recent().toISOString()` |
### Mock 搜索过滤模式
**string 模糊匹配:**
```typescript
list = list.filter((item) => {
return {field} ? item.{field}.includes({field}) : true
})
```
**枚举/数字精确匹配注意query 参数均为字符串,`0` 是 falsy不能直接用 `{field} ?` 判断):**
```typescript
list = list.filter((item) => {
return {field} !== undefined && {field} !== '' ? item.{field} === ~~{field} : true
})
```
---
## faker.js 字段类型映射
| 字段类型 | 推荐 faker 方法 |
|---------|----------------|
| string名称类 | `faker.person.fullName()``faker.commerce.productName()` |
| string标题类 | `faker.lorem.words(3)` |
| string描述类 | `faker.lorem.sentence()` |
| numberID | 自增 `i + 1` |
| number金额 | `faker.number.float({ min: 10, max: 9999, fractionDigits: 2 })` |
| number数量 | `faker.number.int({ min: 1, max: 100 })` |
| boolean | `faker.datatype.boolean()` |
| date | `faker.date.recent().toISOString()` |
| status枚举 | `faker.helpers.arrayElement([0, 1, 2])` |

View File

@ -0,0 +1,135 @@
---
name: fa-feedback
description: 当用户在使用 fa-* 系列技能(如 fa-framework-settings、fa-slot-creator、fa-form-builder、fa-route-generator、fa-store-generator、fa-page-optimizer、fa-theme-customizer 等)时,在同一个目标上经历了 3 次及以上的修改仍未达到预期效果,必须触发此技能。触发信号包括:用户反复要求调整同一处配置或代码、连续说"不对"/"再改改"/"还是不行"、对同一个功能点多次提出修正意见。即使用户没有明确表示"上报"或"反馈",只要检测到反复沟通修改的模式,就应主动触发。
---
# Fantastic-admin 问题反馈
当用户在使用 fa-* 系列技能时遇到反复修改仍无法达到预期的情况,这通常意味着框架本身可能存在改进空间(比如 skill 指令不够精确、框架 API 不够直观、文档缺失等)。此时应主动询问用户是否愿意将问题反馈给框架作者。
## 触发条件
在当前对话中,如果满足以下任一条件,则触发此技能:
1. **同一目标的修改次数 >= 3 次**:用户针对同一个功能点或配置项,已经要求修改 3 次及以上
2. **用户表达持续不满**:用户连续使用"不对"、"还是不行"、"再试试"、"跟我说的不一样"等表述
3. **循环修改模式**:修改 A -> 改回 -> 再改 A出现来回反复的情况
## 执行流程
### 第一步:分析问题
回顾当前对话历史,提炼以下信息:
1. **使用的技能**:用户在使用哪个 fa-* 技能
2. **用户的原始需求**:用户最初想要实现什么
3. **反复修改的焦点**:哪个具体的配置项/代码/功能点在被反复调整
4. **未达预期的原因**:为什么始终无法满足用户的需求(是 skill 指令有误?框架 API 限制?还是理解偏差?)
### 第二步:询问用户
用以下方式询问用户(注意语气要友好自然,不要让用户感到被指责):
```
我注意到在 [具体功能] 上我们已经来回调整了好几次,这很可能说明框架的 [skill/文档/API] 在这方面有改进空间。
你是否愿意将这个问题反馈给 Fantastic-admin 的作者?这有助于改进框架,让以后的使用体验更好。
如果你同意,我会帮你整理一份精简的问题描述,然后打开 GitHub Discussions 页面,内容会自动填好,你只需要检查一下就可以提交。
```
- 如果用户**同意**,继续第三步
- 如果用户**拒绝**,尊重用户的决定,继续协助解决当前问题,不再提及反馈
### 第三步:整理反馈内容
生成精简的反馈报告,格式如下:
**标题**(简洁明了,一句话概括问题):
```
[技能名称] 在 [场景] 下无法正确 [操作]
```
**正文**(使用 Markdown 格式):
```markdown
## 问题描述
[一句话说明用户想做什么,以及遇到了什么问题]
## 使用的技能
[fa-xxx-xxx]
## 复现步骤
1. [用户的原始请求]
2. [第一次修改及结果]
3. [后续修改及结果]
## 期望行为
[用户期望的结果是什么]
## 实际行为
[实际发生了什么,为什么不符合预期]
## 可能的原因
[基于分析,推测问题可能出在哪里,比如 skill 指令、框架 API、默认配置等]
```
内容整理原则:
- **精简**:只保留关键信息,去掉对话中的冗余内容
- **客观**:描述事实,不添加情绪化表达
- **可操作**:让框架作者看到后能理解问题并采取行动
### 第四步:生成链接并打开
将整理好的标题和正文通过 URL 参数编码,拼接到 GitHub Discussions 链接中:
```
https://github.com/orgs/fantastic-admin/discussions/new?category=通用&title={编码后的标题}&body={编码后的正文}
```
使用以下方式生成并打开链接:
```bash
# 生成 URL 编码的链接并打开
python3 -c "
import urllib.parse
import subprocess
title = '''在此填入标题'''
body = '''在此填入正文'''
params = urllib.parse.urlencode({
'category': '通用',
'title': title,
'body': body
}, quote_via=urllib.parse.quote)
url = f'https://github.com/orgs/fantastic-admin/discussions/new?{params}'
print(f'链接已生成:{url}')
subprocess.run(['open', url])
"
```
打开链接后,告诉用户:
```
已在浏览器中打开 GitHub Discussions 页面。请检查预填的内容是否准确,确认无误后点击提交即可。
如果页面中的标题和内容没有自动填充GitHub Discussions 可能不支持 URL 参数预填),你可以手动复制以下内容:
**标题**[标题内容]
**内容**
[正文内容]
```
始终同时展示原文内容作为备选方案,确保用户无论 URL 参数是否生效都能顺利提交反馈。
### 第五步:继续协助
反馈流程完成后,继续协助用户解决当前的问题,不要因为反馈流程中断用户的工作。

View File

@ -0,0 +1,88 @@
---
name: fa-form-builder
description: "为 Fantastic-admin 框架生成独立的表单页面,使用 vee-validate + zod 验证,全部使用框架内建 Fa* 组件。当用户说:'帮我做一个用户信息填写页'、'我只需要一个提交表单,不需要列表'、'做个设置页面,有几个输入框和保存按钮'、'生成一个注册/编辑/配置表单页'、'只要表单页,不需要增删改查',即使用户只是说'做个表单页面'也应触发此技能。"
---
# 表单页面生成器
在 Fantastic-admin 框架中生成独立的 Router 表单页面,使用 vee-validate + zod 完成表单验证,全部使用框架内建 Fa* 组件,不引入任何 Element Plus 组件。
**生成的文件:**
- `apps/<app>/src/views/{path}/{name}/index.vue` — 表单页面(含验证、提交骨架、固定操作栏)
---
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中生成表单页面,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根。
---
## 工作流程
### Step 1收集基本信息
向用户询问(可合并为一次提问):
1. **模块名**(英文,用于文件路径,如 `user`、`profile`、`setting`
2. **模块中文名**(用于页面标题,如 `用户信息`、`个人资料`
3. **存放路径**(在 `apps/<app>/src/views/` 下的子目录,如 `system`、`account`,留空则直接放在 `src/views/` 下)
### Step 2收集字段信息
询问用户该表单有哪些字段,每个字段需要:
- **字段名**(英文,如 `name`、`avatar`、`status`
- **中文标签**(如 `姓名`、`头像`、`状态`
- **字段类型**(见 references/templates.md 中的字段类型映射表)
- **是否必填**
如果用户没有提供字段信息,使用默认字段 `title`标题string必填作为示例占位并在生成后提示用户替换。
### Step 3判断布局
字段较多时单列会导致页面过长,双列更紧凑。根据字段数量和复杂度判断是否询问用户布局偏好:
- **单列**`max-w-600px``space-y-6`(字段少、字段较长时适合)
- **双列**`max-w-1200px``grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2`(字段多、字段较短时适合)
### Step 4确认并生成
汇总信息,展示将要生成的文件,确认后写入。
生成完成后提示:如需配置路由,请使用 `fa-route-generator` 技能;如需 API 模块,请手动创建。
---
## 命名规范
给定模块名 `name` 和路径 `path`(相对于 `apps/<app>/src/views/`
| 用途 | 规则 | 示例path=system, name=user |
|------|------|-------------------------------|
| 视图目录 | `apps/<app>/src/views/{path}/{name}/` | `apps/<app>/src/views/system/user/` |
| 组件名 | `PascalCase({path}-{name}-form)` | `SystemUserForm` |
---
## 代码模板
详细的代码模板和字段类型映射表见 [references/templates.md](references/templates.md)。
生成代码时替换模板中的占位符:
- `{cname}` → 模块中文名
- `{componentName}` → PascalCase 组件名
- `{zodSchema}` → zod 字段定义(每个必填字段对应一行 zod 规则)
- `{initialValues}` → 字段初始值string 默认 `''`boolean 默认 `false`number 默认 `0`array 默认 `[]`
- `{formItems}` → 各字段对应的 FormField 代码片段
- `{maxWidth}` → 单列 `max-w-600px` / 双列 `max-w-1200px`
- `{gridClass}` → 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `space-y-6`
生成的代码是骨架API 调用处用 `// TODO:` 注释标记动态数据源select options、upload action 等)用占位注释标记,用户根据实际接口替换。操作栏按钮使用 `FaButton`:取消用 `variant="outline"`,提交用默认 variant 并传 `:loading="isSubmitting"`

View File

@ -0,0 +1,286 @@
# 表单页面代码模板
使用 vee-validate + zod 验证,全部使用 Fa* 内建组件,不引入任何 Element Plus 组件。
占位符说明:
- `{cname}` — 模块中文名
- `{componentName}` — 组件名PascalCase
- `{zodSchema}` — zod 字段定义
- `{initialValues}` — 表单初始值
- `{formItems}` — FormField 列表
- `{imports}` — 需要手动 import 的组件
- `{maxWidth}` — 单列 `max-w-600px` / 双列 `max-w-1200px`
- `{gridClass}` — 双列时 `grid grid-cols-1 gap-x-8 gap-y-6 items-start md:grid-cols-2` / 单列时 `space-y-6`
---
## index.vue 模板
```vue
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/ui/shadcn/ui/form'
{imports}
defineOptions({
name: '{componentName}',
})
const router = useRouter()
const formSchema = toTypedSchema(z.object({
{zodSchema}
}))
const { handleSubmit, isSubmitting } = useForm({
validationSchema: formSchema,
initialValues: {
{initialValues}
},
})
const loading = ref(false)
const onSubmit = handleSubmit(async (values) => {
// TODO: 调用 API如 apiXxx.create(values) 或 apiXxx.edit(values)
})
function handleCancel() {
router.back()
}
</script>
<template>
<div>
<FaPageHeader title="{cname}" />
<FaPageMain>
<div v-loading="loading" class="mx-auto {maxWidth}">
<form class="{gridClass}" @submit="onSubmit">
{formItems}
</form>
</div>
</FaPageMain>
<FaFixedBar position="bottom" class="flex gap-2 justify-center">
<FaButton type="button" variant="outline" @click="handleCancel">
取消
</FaButton>
<FaButton type="submit" :loading="isSubmitting" @click="onSubmit">
提交
</FaButton>
</FaFixedBar>
</div>
</template>
```
---
## 各字段类型的 FormField 片段
```vue
<!-- FaInput文本 -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaInput v-bind="componentField" placeholder="请输入{label}" class="w-full" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaInput密码+ FaPasswordStrength -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<div class="w-full space-y-2">
<FaInput v-bind="componentField" type="password" placeholder="请输入{label}" class="w-full" />
<FaPasswordStrength :model-value="componentField.modelValue" />
</div>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaTextarea -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaTextarea v-bind="componentField" placeholder="请输入{label}" class="w-full" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaSelect -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaSelect v-bind="componentField" :options="{field}Options" placeholder="请选择{label}" class="w-full" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- script 中同时生成 options 占位const {field}Options = ref([{ label: '选项1', value: 1 }]) -->
<!-- FaSwitchcomponentField 会传字符串,需手动绑定 boolean -->
<FormField v-slot="{ value, handleChange }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaSwitch :model-value="value" @update:model-value="handleChange" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaCheckbox多选手动维护数组 -->
<FormField v-slot="{ value, handleChange }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<div class="flex flex-wrap gap-4">
<FaCheckbox
v-for="opt in {field}Options"
:key="opt.value"
:model-value="value?.includes(opt.value)"
@update:model-value="(checked) => handleChange(checked ? [...(value || []), opt.value] : (value || []).filter(v => v !== opt.value))"
>
{{ opt.label }}
</FaCheckbox>
</div>
<FormMessage />
</FormItem>
</FormField>
<!-- script 中同时生成 options 占位const {field}Options = [{ label: '选项1', value: '1' }] -->
<!-- 日期(原生 input -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<input v-bind="componentField" type="date" class="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaImageUpload -->
<FormField v-slot="{ value, handleChange }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaImageUpload :model-value="value" action="/upload/image" @update:model-value="handleChange" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaFileUpload -->
<FormField v-slot="{ value, handleChange }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaFileUpload :model-value="value" action="/upload/file" @update:model-value="handleChange" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaIconPicker -->
<FormField v-slot="{ componentField }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaIconPicker v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- FaNumberField -->
<FormField v-slot="{ value, handleChange }" name="{field}">
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<FaNumberField :model-value="value" class="w-full" @update:model-value="handleChange" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<!-- 双列布局中需要占满整行的字段,加 md:col-span-2 -->
<!-- <FormField ... class="md:col-span-2"><FormItem class="md:col-span-2"> -->
```
---
## zod 验证规则片段
```typescript
// 必填文本
{field}: z.string().min(1, '请输入{label}'),
// 必填文本 + 最大长度
{field}: z.string().min(1, '请输入{label}').max(50, '最多50个字符'),
// 必填数字(最小值)
{field}: z.number({ message: '请输入{label}' }).min(0.01, '最小值为0.01'),
// 必填选择string
{field}: z.string().min(1, '请选择{label}'),
// 必填选择number
{field}: z.number({ message: '请选择{label}' }),
// 布尔(开关,非必填)
{field}: z.boolean(),
// 数组(多选,非必填)
{field}: z.array(z.string()),
// 图片上传string[],必填至少一张)
{field}: z.array(z.string()).min(1, '请上传{label}'),
// 非必填文本
{field}: z.string().optional(),
```
---
## 需要手动 import 的组件
以下组件不在自动导入范围内,使用时需在 script 顶部添加 import
```typescript
import FaImageUpload from '@/ui/components/FaImageUpload/index.vue'
import FaFileUpload from '@/ui/components/FaFileUpload/index.vue'
import FaIconPicker from '@/ui/components/FaIconPicker/index.vue'
import FaNumberField from '@/ui/components/FaNumberField/index.vue'
```
---
## 字段类型映射表
根据用户描述的关键词选择对应组件:
| 用户描述关键词 | 生成组件 | 备注 |
|---|---|---|
| 文本、名称、标题、账号、邮箱、手机 | `FaInput` | 默认文本输入 |
| 密码 | `FaInput type="password"` | 自动添加 FaPasswordStrength |
| 多行、描述、备注、内容、简介 | `FaTextarea` | |
| 下拉、选择、类型、分类、状态(枚举值) | `FaSelect` | 生成 options 数组占位 |
| 开关、启用、禁用、是否、boolean | `FaSwitch` | |
| 复选、多选 | `FaCheckbox`(多个) | 每个选项一个 FaCheckbox手动维护数组 |
| 日期 | 原生 `<input type="date">` | 暂无 Fa 内建日期选择器 |
| 日期时间 | 原生 `<input type="datetime-local">` | |
| 图片、头像、封面、缩略图 | `FaImageUpload` | |
| 文件、附件 | `FaFileUpload` | |
| 图标 | `FaIconPicker` | |
| 数字、金额、数量、年龄 | `FaNumberField` | |
字段类型不明确时,默认使用 `FaInput`

View File

@ -0,0 +1,43 @@
---
name: fa-framework-settings
description: 管理和配置 Fantastic-admin 框架设置。当用户提到以下任何需求时必须使用此技能:开启/关闭水印、锁屏、错误日志、更新检查、哀悼模式、移动端访问;切换暗色/亮色/跟随系统主题;修改菜单模式(侧边栏/顶部/精简/面板配置标签栏风格fashion/card/square启用/禁用工具栏功能(收藏夹、面包屑、搜索、通知、国际化、全屏、刷新);设置版权信息;配置认证/权限/登录过期;调整页面切换动画;配置居中布局;修改路由模式;以及任何涉及 src/settings/index.ts 的修改。
---
# 框架设置
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中修改设置,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根,例如 `apps/<app>/src/settings.ts`
## 核心文件
- `apps/<app>/src/settings.ts` - 当前配置文件(修改此文件)
- `packages/settings/types.ts` - TypeScript 类型定义(只读参考)
- `packages/settings/src/default.ts` - 默认完整配置(禁止修改,仅供参考)
## 工作流程
1. 读取 `apps/<app>/src/settings.ts` 了解当前配置
2. 查阅 `packages/settings/types.ts` 中的类型定义了解可用选项
3. 查阅 `packages/settings/src/default.ts` 了解默认值
4. 仅修改 `apps/<app>/src/settings.ts`
5. 修改后检查:与默认值相同的配置项直接移除——`settings.ts` 只需保留真正自定义的内容,框架会自动继承默认配置,这样维护时一眼就能看出哪些是项目定制的
## 配置领域
详细配置选项请参考:
- **应用设置**: [references/app-settings.md](references/app-settings.md) - 认证、路由、功能开关、布局、主页、版权等
- **主题设置**: [references/theme-settings.md](references/theme-settings.md) - 颜色方案、主题同步、圆角、色弱模式等
- **导航菜单设置**: [references/menu-settings.md](references/menu-settings.md) - 导航菜单模式、风格、展开/收起行为、快捷键等
- **顶栏设置**: [references/topbar-settings.md](references/topbar-settings.md) - 标签栏、工具栏、显示模式等
- **标签栏设置**: [references/tabbar-settings.md](references/tabbar-settings.md) - 风格、图标、双击动作、记忆功能等
- **工具栏设置**: [references/toolbar-settings.md](references/toolbar-settings.md) - 收藏夹、面包屑、搜索、通知、国际化等
- **页面设置**: [references/page-settings.md](references/page-settings.md) - 快捷键、切换动画、进度条等

View File

@ -0,0 +1,119 @@
# 应用设置 (app)
## 目录
- [认证配置 (auth)](#认证配置-auth)
- [路由配置](#路由配置)
- [功能开关](#功能开关)
- [主页配置 (home)](#主页配置-home)
- [版权配置 (copyright)](#版权配置-copyright)
## 认证配置 (auth)
### permission
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 是否开启权限功能,控制是否启用权限验证功能
**示例:**
```typescript
app: {
auth: {
permission: true,
}
}
```
## 路由配置
### routeMode
- **类型**: `'hash' | 'html5'`
- **默认值**: `'hash'`
- **说明**: 设置应用的路由模式
- `'hash'` - Hash 模式
- `'html5'` - HTML5 模式
### routeBaseOn
- **类型**: `'frontend' | 'backend'
- **默认值**: `'frontend'`
- **说明**: 指定路由数据的来源方式
- `'frontend'` - 前端
- `'backend'` - 后端
## 功能开关
### dynamicTitle
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用动态页面标题功能
### rip
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 哀悼模式,开启后网站将会整体变灰
### mobile
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 移动端访问,关闭后网站将禁用移动端访问
## 主页配置 (home)
### enable
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 是否开启主页功能
### title
- **类型**: `string`
- **默认值**: `'主页'`
- **说明**: 主页的标题
### fullPath
- **类型**: `string`
- **默认值**: `'/'`
- **说明**: 主页的完整路由路径
**示例:**
```typescript
app: {
home: {
enable: true,
title: 'app.route.home',
fullPath: '/',
}
}
```
## 版权配置 (copyright)
### enable
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 是否开启版权信息显示,同时在路由 meta 对象里可以单独设置某个路由是否显示底部版权信息
### dates
- **类型**: `string`
- **默认值**: `''`
- **说明**: 网站运行日期
### company
- **类型**: `string`
- **默认值**: `''`
- **说明**: 版权信息中显示的公司名称
### website
- **类型**: `string`
- **默认值**: `''`
- **说明**: 版权信息中显示的网站地址
**示例:**
```typescript
app: {
copyright: {
enable: true,
dates: '2020-present',
company: 'Fantastic-admin',
website: 'https://fantastic-admin.hurui.me',
}
}
```

View File

@ -0,0 +1,87 @@
# 导航菜单设置 (menu)
## 目录
- [导航菜单模式 (mode)](#导航菜单模式-mode)
- [主导航点击模式 (mainMenuClickMode)](#主导航点击模式-mainmenuclickmode)
- [次导航展开行为](#次导航展开行为)
- [快捷键 (hotkeys)](#快捷键-hotkeys)
## 导航菜单模式 (mode)
- **类型**: `'side' | 'head' | 'single'`
- **默认值**: `'side'`
- **说明**: 设置导航菜单的显示模式
- `'side'` - 侧边栏模式(有主导航菜单)
- `'head'` - 顶部模式
- `'single'` - 侧边栏模式(无主导航菜单)
## 主导航菜单点击模式 (mainMenuClickMode)
- **类型**: `'switch' | 'jump' | 'smart'`
- **默认值**: `'switch'`
- **说明**: 设置主导航菜单项的点击行为
- `'switch'` - 切换
- `'jump'` - 跳转
- `'smart'` - 智能选择,判断次导航是否只有且只有一个可访问的菜单进行切换或跳转操作
## 次导航菜单展开行为
### subMenuUniqueExpand
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 次导航菜单是否只保持一个子项的展开
### subMenuCollapse
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 次导航菜单是否收起
### subMenuCollapseButton
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 是否开启次导航菜单的展开/收起按钮
## 快捷键 (hotkeys)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用导航菜单相关的快捷键功能
## 完整示例
```typescript
menu: {
mode: 'side',
mainMenuClickMode: 'switch',
subMenuUniqueExpand: true,
subMenuCollapse: false,
subMenuCollapseButton: true,
hotkeys: true,
}
```
## 常见配置
**顶部导航菜单模式:**
```typescript
menu: {
mode: 'head',
}
```
**智能主导航菜单点击:**
```typescript
menu: {
mainMenuClickMode: 'smart',
}
```
**启用次导航菜单收起按钮:**
```typescript
menu: {
subMenuCollapseButton: true,
}
```

View File

@ -0,0 +1,63 @@
# 页面设置 (page)
## 目录
- [页面切换动画 (transitionMode)](#页面切换动画-transitionmode)
- [载入进度条 (progress)](#载入进度条-progress)
- [快捷键 (hotkeys)](#快捷键-hotkeys)
## 页面切换动画 (transitionMode)
- **类型**: `'' | 'fade' | 'slide-left' | 'slide-right' | 'slide-top' | 'slide-bottom'`
- **默认值**: `''`
- **说明**: 设置页面切换时的动画效果
- `''` - 无动画
- `'fade'` - 淡入淡出
- `'slide-left'` - 向左滑动
- `'slide-right'` - 向右滑动
- `'slide-top'` - 向上滑动
- `'slide-bottom'` - 向下滑动
## 载入进度条 (progress)
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 控制是否显示页面载入进度条
## 快捷键 (hotkeys)
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 控制是否启用页面相关的快捷键功能
## 完整示例
```typescript
page: {
transitionMode: 'fade',
progress: true,
hotkeys: true,
}
```
## 常见配置
**启用页面切换动画:**
```typescript
page: {
transitionMode: 'slide-right',
}
```
**禁用进度条:**
```typescript
page: {
progress: false,
}
```
**禁用快捷键:**
```typescript
page: {
hotkeys: false,
}
```

View File

@ -0,0 +1,36 @@
# 标签栏设置 (tabbar)
## 目录
- [显示图标 (icon)](#显示图标-icon)
- [双击执行动作 (dblclickAction)](#双击执行动作-dblclickaction)
- [快捷键 (hotkeys)](#快捷键-hotkeys)
## 显示图标 (icon)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制标签是否显示图标
## 快捷键 (hotkeys)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用标签栏相关的快捷键功能
## 完整示例
```typescript
tabbar: {
icon: true,
hotkeys: true,
}
```
## 常见配置
**显示图标:**
```typescript
tabbar: {
icon: true,
}
```

View File

@ -0,0 +1,65 @@
# 主题设置 (theme)
## 目录
- [颜色方案 (colorScheme)](#颜色方案-colorscheme)
- [圆角系数 (radius)](#圆角系数-radius)
- [色弱模式 (colorAmblyopia)](#色弱模式-coloramblyopia)
## 颜色方案 (colorScheme)
- **类型**: `'light' | 'dark' | ''`
- **默认值**: `'light'`
- **说明**: 设置应用的颜色方案
- `'light'` - 明亮模式
- `'dark'` - 暗黑模式
- `''` - 跟随系统
## 圆角系数 (radius)
- **类型**: `number`
- **默认值**: `0.5`
- **说明**: 设置界面元素的圆角大小,取值范围 0 到 1
- `0` - 无圆角(方形)
- `0.5` - 中等圆角
- `1` - 最大圆角
## 色弱模式 (colorAmblyopia)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 启用色弱友好的颜色方案
## 完整示例
```typescript
theme: {
light: 'default',
dark: 'default',
colorScheme: 'light',
radius: 0.5,
colorAmblyopia: false,
}
```
## 常见配置
**启用暗色模式:**
```typescript
theme: {
colorScheme: 'dark',
}
```
**跟随系统颜色方案:**
```typescript
theme: {
colorScheme: '',
}
```
**调整圆角:**
```typescript
theme: {
radius: 0.8, // 更圆润的界面
}
```

View File

@ -0,0 +1,82 @@
# 工具栏设置 (toolbar)
## 目录
- [面包屑导航 (breadcrumb)](#面包屑导航-breadcrumb)
- [导航搜索 (menuSearch)](#导航搜索-menusearch)
- [全屏功能 (fullscreen)](#全屏功能-fullscreen)
- [页面刷新 (pageReload)](#页面刷新-pagereload)
- [颜色主题切换 (colorScheme)](#颜色主题切换-colorscheme)
## 面包屑导航 (breadcrumb)
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 控制是否显示面包屑导航
## 导航搜索 (menuSearch)
### enable
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 控制是否启用菜单搜索功能
### hotkeys
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 控制是否启用导航搜索的快捷键
**示例:**
```typescript
toolbar: {
menuSearch: {
enable: true,
hotkeys: true,
}
}
```
## 全屏功能 (fullscreen)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用全屏切换功能
## 页面刷新 (pageReload)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用页面刷新功能
## 颜色主题切换 (colorScheme)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否启用颜色主题切换功能
## 完整示例
```typescript
toolbar: {
breadcrumb: true,
menuSearch: {
enable: true,
hotkeys: true,
},
fullscreen: true,
pageReload: true,
colorScheme: true,
}
```
## 常见配置
**启用所有工具栏功能:**
```typescript
toolbar: {
breadcrumb: true,
menuSearch: { enable: true },
fullscreen: true,
pageReload: true,
colorScheme: true,
}
```

View File

@ -0,0 +1,61 @@
# 顶栏设置 (topbar)
## 目录
- [标签栏 (tabbar)](#标签栏-tabbar)
- [工具栏 (toolbar)](#工具栏-toolbar)
- [顶栏模式 (mode)](#顶栏模式-mode)
## 标签栏 (tabbar)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否在顶栏显示标签栏
## 工具栏 (toolbar)
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制是否在顶栏显示工具栏
## 顶栏模式 (mode)
- **类型**: `'static' | 'fixed' | 'sticky'`
- **默认值**: `'static'`
- **说明**: 设置顶栏的显示模式
- `'static'` - 静止,跟随页面滚动
- `'fixed'` - 固定,不跟随页面滚动,始终固定在顶部
- `'sticky'` - 粘性,页面往下滚动时隐藏,往上滚动时显示
## 完整示例
```typescript
topbar: {
tabbar: true,
toolbar: true,
mode: 'fixed',
}
```
## 常见配置
**启用标签栏和工具栏:**
```typescript
topbar: {
tabbar: true,
toolbar: true,
}
```
**固定顶栏:**
```typescript
topbar: {
mode: 'fixed',
}
```
**粘性顶栏(智能显示/隐藏):**
```typescript
topbar: {
mode: 'sticky',
}
```

View File

@ -0,0 +1,78 @@
---
name: fa-page-optimizer
description: "优化 Vue 页面,用 Fantastic-admin 框架内建组件packages/components/)替换自定义实现和原生 HTML。当用户说'帮我优化这个页面'、'把这些原生 HTML 换成框架组件'、'用 FaCard/FaModal/FaButton 重构一下'、'这个页面太乱了'、'统一一下 UI 风格'、'用内建组件替换自定义弹窗/分页/加载',即使用户只是说'看看这个页面能不能改进'也应触发此技能。"
---
# 页面优化器
利用 Fantastic-admin 的 70+ 内建组件优化 Vue 页面,用框架组件替换自定义实现,减少重复代码,保持 UI 一致性。
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要优化哪个应用中的页面,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有页面路径均在 `apps/<app>/src/views/` 下。
## 优化工作流
### 步骤 1: 分析页面,识别替换机会
读取目标页面,找出三类可优化点:
- **原生 HTML 元素**`<button>`、`<input>`、`<select>`、自定义卡片 div 等
- **重复造轮子的自定义组件**:自定义弹窗、分页、加载状态、提示消息等
- **可简化的内联逻辑**:手动标签页切换、手动状态管理等
### 步骤 2: 查找合适的框架组件
先查组件目录确认有哪些可用组件:
```
Read: references/components-catalog.md
```
确定组件后,读取对应 README 了解完整 APIprops、slots、events
```
Read: packages/components/src/<component-name>/README.md
```
README 是最权威的用法来源,不确定时优先看文档而不是猜。
### 步骤 3: 参考优化模式(可选)
如果不确定如何替换,查看前后对比示例:
```
Read: references/optimization-patterns.md
```
### 步骤 4: 实施替换
替换时保留所有原始功能,不要改变业务逻辑。框架组件通常提供组合式函数用于编程式控制,优先使用:
- `useFaModal()` — 编程式打开/关闭弹窗、confirm 确认框
- `useFaToast()` — 显示成功/错误提示
- `useFaLoading()` — 控制加载遮罩
组件可以组合使用,例如:
```vue
<FaCard title="用户列表">
<FaSearchBar v-model="keyword" />
<FaEmpty v-if="users.length === 0" />
<template #footer>
<FaPagination :page="page" :size="size" :total="total" />
</template>
</FaCard>
```
### 步骤 5: 验证
Fa 组件通过 `unplugin-auto-import` 自动导入,无需手动写 import 语句。替换后确认 props 和 v-model 绑定符合组件 API。如有问题回到 `packages/components/src/<component-name>/README.md` 核对。

View File

@ -0,0 +1,286 @@
# Fantastic Admin 内建组件目录
本文档列出所有可用的框架内建组件及其使用场景。组件位于 `packages/components/src/`
> **提示**: 每个组件目录下都有 `README.md` 使用文档,包含完整的 API 说明 (Props/Slots/Events/Methods) 和示例代码。
> 查阅方式:`Read: packages/components/src/<component-name>/README.md`
## 基础组件
### FaButton
**用途**: 按钮组件,支持多种样式和状态
**变体**: default, destructive, outline, secondary, ghost, link
**特性**: loading 状态、disabled 状态
### FaIcon
**用途**: 图标组件,基于 [iconify](https://icon-sets.iconify.design/)
**示例**: <FaIcon name="i-mdi:home" />
### FaKbd / FaKbdGroup
**用途**: 键盘按键显示组件
### FaLabel
**用途**: 表单标签组件
## 布局组件
### FaCard
**用途**: 卡片容器,支持标题、描述、内容、底部插槽
**特性**: title, description, footer slot
### FaDivider
**用途**: 分割线组件
### FaPageHeader
**用途**: 页面头部组件
### FaPageMain
**用途**: 页面主体容器
### FaScrollArea
**用途**: 滚动区域容器
### FaScrollingText
**用途**: 滚动文字特效
## 表单组件
### FaInput
**用途**: 输入框组件,支持 v-model
**特性**: placeholder, disabled 等标准输入属性
### FaTextarea
**用途**: 多行文本输入
### FaCheckbox
**用途**: 复选框组件
### FaSwitch
**用途**: 开关切换组件
### FaSelect
**用途**: 下拉选择组件
### FaSlider
**用途**: 滑块组件
### FaNumberField
**用途**: 数字输入组件
### FaInputOTP
**用途**: OTP 验证码输入组件
### FaPasswordStrength
**用途**: 密码强度显示组件
### FaFileUpload
**用途**: 文件上传组件
### FaImageUpload
**用途**: 图片上传组件
### FaIconPicker
**用途**: 图标选择器
### FaSearchBar
**用途**: 搜索栏组件
### FaCascader
**用途**: 级联选择组件,支持多级树形数据选择
## 交互组件
### FaModal
**用途**: 模态对话框,支持拖拽、最大化等功能
**特性**: maximizable, closable, draggable, alignCenter, loading, before-close
**API**: useFaModal() 用于编程式调用
### FaDrawer
**用途**: 抽屉组件
### FaDropdown
**用途**: 下拉菜单
### FaContextMenu
**用途**: 右键上下文菜单
### FaPopover
**用途**: 弹出框组件
### FaTooltip
**用途**: 工具提示
### FaHoverCard
**用途**: 悬停卡片
### FaCollapsible
**用途**: 可折叠容器
## 反馈组件
### FaToast
**用途**: 轻提示组件
### FaLoading
**用途**: 加载状态组件
### FaProgress
**用途**: 进度条组件
### FaEmpty
**用途**: 空状态组件
## 数据展示组件
### FaTabs
**用途**: 标签页组件
### FaTimeline
**用途**: 时间轴组件
### FaTree
**用途**: 树形组件
### FaPagination
**用途**: 分页组件
**特性**: page, size, total
### FaAvatar
**用途**: 头像组件
### FaCarousel
**用途**: 轮播图组件
### FaImagePreview
**用途**: 图片预览组件
## 数据可视化组件
### FaSparkline
**用途**: 迷你图表组件
### FaTrend
**用途**: 趋势指示器
### FaCountTo
**用途**: 数字动画组件
### FaAnimatedCountTo
**用途**: 动画数字组件
### FaAnimatedCountToGroup
**用途**: 动画数字组组件
## 特效组件
### FaBorderBeam
**用途**: 边框光束特效
### FaGlowyCard / FaGlowyCardWrapper
**用途**: 发光卡片特效
### FaSpotlightCard
**用途**: 聚光灯卡片特效
### FaParticlesBg
**用途**: 粒子背景特效
### FaPatternBg
**用途**: 图案背景
### FaBlurReveal
**用途**: 模糊揭示特效
### FaAnimatedBeam
**用途**: 动画光束特效
### FaFlipCard
**用途**: 翻转卡片
### FaFlipWords
**用途**: 翻转文字特效
### FaSparklesText
**用途**: 闪光文字特效
### FaTextHighlight
**用途**: 文字高亮特效
### FaMarquee
**用途**: 跑马灯组件
### FaSmoothSwipe
**用途**: 平滑滑动组件
## 高级组件
### FaButtonGroup
**用途**: 按钮组组件
### FaGradientButton
**用途**: 渐变按钮
### FaInteractiveButton
**用途**: 交互式按钮
### FaDigitalCard
**用途**: 数字卡片
### FaMultiStepLoader
**用途**: 多步骤加载器
### FaLinkPreview
**用途**: 链接预览组件
### FaTimeAgo
**用途**: 相对时间显示
### FaCode
**用途**: 代码展示组件
### FaCodePreview
**用途**: 代码预览组件
### FaQrcode
**用途**: 二维码生成组件
### FaStorageBox
**用途**: 存储盒组件
### FaScratchOff
**用途**: 刮刮卡组件
### FaBackToTop
**用途**: 返回顶部按钮
### FaFixedBar
**用途**: 固定栏组件
## 组件选择指南
### 替换原生 HTML 元素
| 原生元素 | 推荐组件 | 说明 |
|---------|---------|------|
| `<button>` | `FaButton` | 统一的按钮样式和状态管理 |
| `<input>` | `FaInput` | 统一的输入框样式 |
| `<textarea>` | `FaTextarea` | 统一的多行输入样式 |
| `<select>` | `FaSelect` | 更好的下拉选择体验 |
| `<div class="card">` | `FaCard` | 标准化的卡片布局 |
| `<hr>` | `FaDivider` | 统一的分割线样式 |
| `<img>` (头像) | `FaAvatar` | 头像专用组件 |
### 替换常见功能实现
| 功能 | 推荐组件 | 说明 |
|------|---------|------|
| 模态对话框 | `FaModal` | 替代自定义 modal 实现 |
| 分页逻辑 | `FaPagination` | 替代自定义分页组件 |
| 加载状态 | `FaLoading` | 替代自定义 loading 动画 |
| 空状态提示 | `FaEmpty` | 替代自定义空状态页面 |
| 消息提示 | `FaToast` | 替代 alert 或自定义提示 |
| 标签页切换 | `FaTabs` | 替代自定义 tab 实现 |
| 树形数据 | `FaTree` | 替代自定义树形组件 |
| 图片预览 | `FaImagePreview` | 替代自定义图片查看器 |
| 搜索框 | `FaSearchBar` | 替代普通 input + 搜索逻辑 |
| 级联选择 | `FaCascader` | 替代自定义多级联动下拉选择 |

View File

@ -0,0 +1,604 @@
# 页面优化模式
本文档展示如何用框架内建组件替换常见的自定义代码和原生 HTML 实现。
## 基础元素替换
### 按钮优化
**优化前**:
```vue
<button class="btn btn-primary" @click="handleClick">
提交
</button>
<button class="custom-button" :disabled="loading">
<span v-if="loading">加载中...</span>
<span v-else>提交</span>
</button>
```
**优化后**:
```vue
<FaButton @click="handleClick">
提交
</FaButton>
<FaButton :loading="loading">
提交
</FaButton>
```
### 输入框优化
**优化前**:
```vue
<div class="form-group">
<input
v-model="username"
type="text"
class="form-control"
placeholder="请输入用户名"
>
</div>
```
**优化后**:
```vue
<FaInput v-model="username" placeholder="请输入用户名" />
```
### 卡片容器优化
**优化前**:
```vue
<div class="card">
<div class="card-header">
<h3>卡片标题</h3>
<p class="text-muted">卡片描述</p>
</div>
<div class="card-body">
卡片内容
</div>
<div class="card-footer">
底部内容
</div>
</div>
```
**优化后**:
```vue
<FaCard title="卡片标题" description="卡片描述">
卡片内容
<template #footer>
底部内容
</template>
</FaCard>
```
## 交互组件替换
### 模态对话框优化
**优化前**:
```vue
<script setup>
const visible = ref(false)
const title = ref('')
const content = ref('')
function handleClose() {
visible.value = false
}
function handleConfirm() {
// 确认逻辑
visible.value = false
}
</script>
<template>
<div v-if="visible" class="modal-overlay" @click="handleClose">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="close-btn" @click="handleClose">
×
</button>
</div>
<div class="modal-body">
{{ content }}
</div>
<div class="modal-footer">
<button @click="handleClose">
取消
</button>
<button @click="handleConfirm">
确定
</button>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
/* 更多样式... */
</style>
```
**优化后**:
```vue
<script setup>
const modal = ref(false)
</script>
<template>
<FaButton @click="modal = true">
打开对话框
</FaButton>
<FaModal v-model="modal" title="标题">
内容
</FaModal>
</template>
```
### 分页组件优化
**优化前**:
```vue
<script setup>
const page = ref(1)
const size = ref(10)
const total = ref(100)
const totalPages = computed(() => Math.ceil(total.value / size.value))
</script>
<template>
<div class="pagination">
<button :disabled="page === 1" @click="page--">
上一页
</button>
<span>{{ page }} / {{ totalPages }}</span>
<button :disabled="page === totalPages" @click="page++">
下一页
</button>
</div>
</template>
```
**优化后**:
```vue
<FaPagination :page="page" :size="size" :total="total" />
<script setup>
const page = ref(1)
const size = ref(10)
const total = ref(100)
</script>
```
## 反馈组件替换
### 加载状态优化
**优化前**:
```vue
<div v-if="loading" class="loading-overlay">
<div class="spinner">
</div>
<p>
加载中...
</p>
</div>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
/* 自定义动画 */
}
</style>
```
**优化后**:
```vue
<script setup>
import { useFaLoading } from '@/ui/components/FaLoading'
async function fetchData() {
const { close } = useFaLoading()
try {
await api.getData()
}
finally {
close()
}
}
</script>
```
### 空状态优化
**优化前**:
```vue
<div v-if="list.length === 0" class="empty-state">
<img src="/empty.png" alt="空状态">
<p>暂无数据</p>
</div>
```
**优化后**:
```vue
<FaEmpty v-if="list.length === 0" description="暂无数据" />
```
### 消息提示优化
**优化前**:
```vue
<script setup>
function showMessage(message: string) {
alert(message)
}
// 或自定义实现
const messages = ref<string[]>([])
function showMessage(message: string) {
messages.value.push(message)
setTimeout(() => {
messages.value.shift()
}, 3000)
}
</script>
```
**优化后**:
```vue
<script setup>
import { useFaToast } from '@/ui/components/FaToast'
const toast = useFaToast()
function showMessage(message: string) {
toast.success(message)
}
</script>
```
## 表单组件替换
### 复选框优化
**优化前**:
```vue
<label class="checkbox-wrapper">
<input v-model="checked" type="checkbox">
<span>同意协议</span>
</label>
```
**优化后**:
```vue
<FaCheckbox v-model="checked">
同意协议
</FaCheckbox>
```
### 开关优化
**优化前**:
```vue
<div class="switch-wrapper">
<input
v-model="enabled"
type="checkbox"
class="switch-input"
>
<span class="switch-slider"></span>
</div>
<style scoped>
.switch-wrapper {
/* 自定义开关样式 */
}
</style>
```
**优化后**:
```vue
<FaSwitch v-model="enabled" />
```
### 下拉选择优化
**优化前**:
```vue
<select v-model="selected" class="form-select">
<option value="">请选择</option>
<option v-for="item in options" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
```
**优化后**:
```vue
<FaSelect v-model="selected" :options="options" placeholder="请选择" />
```
### 级联选择优化
**优化前**:
```vue
<script setup>
// 自定义多级联动下拉:需要手动管理每一层的选中状态
const province = ref('')
const city = ref('')
const district = ref('')
const cities = computed(() => getCities(province.value))
const districts = computed(() => getDistricts(city.value))
</script>
<template>
<select v-model="province">
<option v-for="p in provinces" :key="p.value" :value="p.value">{{ p.label }}</option>
</select>
<select v-model="city" :disabled="!province">
<option v-for="c in cities" :key="c.value" :value="c.value">{{ c.label }}</option>
</select>
<select v-model="district" :disabled="!city">
<option v-for="d in districts" :key="d.value" :value="d.value">{{ d.label }}</option>
</select>
</template>
```
**优化后**:
```vue
<script setup>
const selected = ref<string | undefined>()
const options = [
{
label: '浙江省', value: 'zj',
children: [
{
label: '杭州市', value: 'hz',
children: [
{ label: '西湖区', value: 'xh' },
{ label: '余杭区', value: 'yh' },
],
},
],
},
]
</script>
<template>
<FaCascader v-model="selected" :options="options" placeholder="请选择地区" clearable />
</template>
```
## 数据展示优化
### 标签页优化
**优化前**:
```vue
<script setup>
const activeTab = ref('tab1')
const tabs = [
{ key: 'tab1', label: '标签1' },
{ key: 'tab2', label: '标签2' },
]
</script>
<template>
<div class="tabs">
<div class="tab-headers">
<div
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<div class="tab-content">
<div v-if="activeTab === 'tab1'">
内容1
</div>
<div v-if="activeTab === 'tab2'">
内容2
</div>
</div>
</div>
</template>
```
**优化后**:
```vue
<FaTabs v-model="activeTab" :tabs="tabs">
<template #tab1>内容1</template>
<template #tab2>内容2</template>
</FaTabs>
<script setup>
const activeTab = ref('tab1')
const tabs = [
{ key: 'tab1', label: '标签1' },
{ key: 'tab2', label: '标签2' },
]
</script>
```
### 头像显示优化
**优化前**:
```vue
<div class="avatar">
<img v-if="user.avatar" :src="user.avatar" alt="头像">
<span v-else class="avatar-fallback">{{ user.name[0] }}</span>
</div>
<style scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
</style>
```
**优化后**:
```vue
<FaAvatar :src="user.avatar" :alt="user.name" />
```
## 特效组件应用
### 卡片特效增强
**优化前**:
```vue
<div class="card">
<!-- 普通卡片 -->
</div>
```
**优化后**:
```vue
<!-- 添加发光效果 -->
<FaGlowyCard>
<!-- 卡片内容 -->
</FaGlowyCard>
<!-- 添加聚光灯效果 -->
<FaSpotlightCard>
<!-- 卡片内容 -->
</FaSpotlightCard>
<!-- 添加边框光束 -->
<FaCard>
<FaBorderBeam />
<!-- 卡片内容 -->
</FaCard>
```
### 文字特效增强
**优化前**:
```vue
<h1 class="title">
欢迎使用
</h1>
```
**优化后**:
```vue
<!-- 添加闪光效果 -->
<FaSparklesText text="欢迎使用" />
<!-- 添加高亮效果 -->
<FaTextHighlight>
欢迎使用
</FaTextHighlight>
<!-- 添加翻转效果 -->
<FaFlipWords :words="['欢迎', 'Welcome', 'Bienvenue']" />
```
## 布局优化
### 页面结构优化
**优化前**:
```vue
<template>
<div class="page">
<div class="page-header">
<h1>页面标题</h1>
<div class="actions">
<button>操作</button>
</div>
</div>
<div class="page-content">
<!-- 内容 -->
</div>
</div>
</template>
<style scoped>
.page {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
</style>
```
**优化后**:
```vue
<template>
<FaPageHeader title="页面标题">
<template #actions>
<FaButton>操作</FaButton>
</template>
</FaPageHeader>
<FaPageMain>
<!-- 内容 -->
</FaPageMain>
</template>
```
## 优化原则
1. **优先使用框架组件**: 检查 `src/ui/components/` 是否有对应的内建组件
2. **参考组件源码**: 查看组件实现了解使用方式和 API
3. **保持功能一致**: 确保替换后的组件功能与原代码一致
4. **简化代码**: 移除不必要的自定义样式和逻辑
5. **统一风格**: 使用框架组件可以保持整个应用的视觉一致性
6. **提升可维护性**: 框架组件有统一的 API 和文档,更易维护
## 常见场景映射
| 场景 | 原实现 | 框架组件 |
| ---------- | ---------------------- | ----------------------------- |
| 用户列表 | 自定义表格 + 分页 | FaCard + FaPagination |
| 表单提交 | 原生 form + input | FaInput + FaButton + FaModal |
| 数据加载 | 自定义 loading | FaLoading |
| 错误提示 | alert / 自定义 toast | FaToast |
| 确认对话框 | confirm / 自定义 modal | FaModal (useFaModal) |
| 图片上传 | 自定义上传组件 | FaImageUpload |
| 搜索功能 | input + 搜索逻辑 | FaSearchBar |
| 级联选择 | 多级联动 select | FaCascader |
| 数据统计 | 自定义数字展示 | FaCountTo / FaAnimatedCountTo |
| 趋势展示 | 自定义图表 | FaSparkline + FaTrend |
| 时间显示 | 自定义格式化 | FaTimeAgo |

View File

@ -0,0 +1,108 @@
---
name: fa-route-generator
description: 管理 Vue Router 路由配置,用于 Fantastic-admin 框架。当用户提到以下任何需求时必须使用此技能:新建页面需要加路由、详情页不想显示在菜单里、新增页和编辑页老是开两个标签、从列表跳详情返回后状态丢失、给菜单项加 NEW 标签或红点、路由需要权限控制、配置页面保活、修改路由 meta、添加面包屑配置。即使用户只是说"加个路由"或"新建了一个页面",也应触发此技能。
---
# 路由生成器
框架通过路由自动生成导航菜单,路由配置需要遵循特定约定。
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中操作路由,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根,例如 `apps/<app>/src/router/`
## 前置检查
读取 `apps/<app>/src/settings.ts`,检查 `app.routeBaseOn` 的值:
- `'frontend'`(默认):可以继续
- `'backend'`:路由由后端驱动,手动创建的路由文件会被忽略,告知用户此模式不支持前端路由文件生成
## 工作流程
### 场景 A: 创建新路由
#### 1. 收集路由信息
向用户询问或确认:路由路径、页面标题、图标(可选)、是否多级路由、所属主导航分组。
#### 2. 创建路由文件
`apps/<app>/src/router/modules/` 下创建 `<模块名>.ts`
```typescript
import type { RouteRecordRaw } from 'vue-router'
function Layout() {
return import('@/layouts/index.vue')
}
const routes: RouteRecordRaw = {
path: '/example',
component: Layout,
name: 'example',
meta: {
title: '示例',
icon: 'i-ep:menu',
},
children: [
{
path: '',
name: 'exampleIndex',
component: () => import('@/views/example/index.vue'),
meta: {
title: '示例页面',
},
},
],
}
export default routes
```
#### 3. 更新 routes.ts
`apps/<app>/src/router/routes.ts` 中添加 import 并注册到对应主导航分组的 `children` 数组。
### 场景 B: 修改现有路由
定位路由文件(在 `apps/<app>/src/router/modules/` 下搜索),读取后按需修改 meta 属性。常见修改:
**权限控制**
```typescript
meta: { auth: 'user:view' } // 或数组 ['user:view', 'user:edit']
```
**页面保活**(从详情返回列表时保留列表状态):
```typescript
// 列表页
meta: { keepAlive: ['productDetail'] }
// 详情页
meta: { menu: false, activeMenu: '/product', noKeepAlive: 'productList' }
```
**隐藏菜单项**
```typescript
meta: { menu: false, activeMenu: '/parent/path' }
```
## 框架约定
- 一级路由 `path` 必须以 `/` 开头,`component` 必须是 `Layout`
- 子路由 `path` 不要以 `/` 开头
- 多级路由的中间层级无需设置 `component`
- 所有路由的 `name` 必须全局唯一
## Meta 属性配置
详细属性说明见 [references/route-meta.md](references/route-meta.md),更多示例见 [references/examples.md](references/examples.md)。
常用属性:`title`(必需)、`icon`、`menu`、`auth`、`keepAlive`、`activeMenu`、`breadcrumb`

View File

@ -0,0 +1,459 @@
# 路由配置示例
本文档提供了各种常见场景的路由配置示例,包括创建新路由和修改现有路由。
## 目录
- [基础示例](#基础示例)
- [多级路由](#多级路由)
- [特殊场景](#特殊场景)
- [路由配置调整](#路由配置调整)
- [完整示例](#完整示例)
## 基础示例
### 单页面路由
最简单的单页面路由配置:
```typescript
import type { RouteRecordRaw } from 'vue-router'
function Layout() {
return import('@/layouts/index.vue')
}
const routes: RouteRecordRaw = {
path: '/dashboard',
component: Layout,
name: 'dashboard',
meta: {
title: '仪表盘',
icon: 'i-ep:data-line',
},
children: [
{
path: '',
name: 'dashboardIndex',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '数据概览',
},
},
],
}
export default routes
```
### 列表-详情路由
常见的列表和详情页面配置:
```typescript
const routes: RouteRecordRaw = {
path: '/article',
component: Layout,
name: 'article',
meta: {
title: '文章管理',
icon: 'i-ep:document',
},
children: [
{
path: '',
name: 'articleList',
component: () => import('@/views/article/list.vue'),
meta: {
title: '文章列表',
keepAlive: 'articleDetail', // 从详情返回时保持列表状态
},
},
{
path: 'detail/:id?',
name: 'articleDetail',
component: () => import('@/views/article/detail.vue'),
meta: {
title: '文章详情',
menu: false, // 不在导航中显示
activeMenu: '/article', // 高亮文章管理菜单
keepAlive: true,
noKeepAlive: 'articleList', // 从列表进入时不保活
},
},
],
}
```
## 多级路由
### 二级菜单
```typescript
const routes: RouteRecordRaw = {
path: '/system',
component: Layout,
name: 'system',
meta: {
title: '系统管理',
icon: 'i-ep:setting',
},
children: [
{
path: 'user',
name: 'systemUser',
component: () => import('@/views/system/user/index.vue'),
meta: {
title: '用户管理',
icon: 'i-ep:user',
},
},
{
path: 'role',
name: 'systemRole',
component: () => import('@/views/system/role/index.vue'),
meta: {
title: '角色管理',
icon: 'i-ep:avatar',
},
},
{
path: 'permission',
name: 'systemPermission',
component: () => import('@/views/system/permission/index.vue'),
meta: {
title: '权限管理',
icon: 'i-ep:lock',
},
},
],
}
```
### 三级菜单
```typescript
const routes: RouteRecordRaw = {
path: '/content',
component: Layout,
name: 'content',
meta: {
title: '内容管理',
icon: 'i-ep:folder',
},
children: [
{
path: 'article',
name: 'contentArticle',
meta: {
title: '文章管理',
icon: 'i-ep:document',
},
// 注意: 多级路由的中间层级不需要设置 component
children: [
{
path: 'list',
name: 'contentArticleList',
component: () => import('@/views/content/article/list.vue'),
meta: {
title: '文章列表',
},
},
{
path: 'category',
name: 'contentArticleCategory',
component: () => import('@/views/content/article/category.vue'),
meta: {
title: '文章分类',
},
},
],
},
{
path: 'media',
name: 'contentMedia',
meta: {
title: '媒体管理',
icon: 'i-ep:picture',
},
children: [
{
path: 'image',
name: 'contentMediaImage',
component: () => import('@/views/content/media/image.vue'),
meta: {
title: '图片管理',
},
},
{
path: 'video',
name: 'contentMediaVideo',
component: () => import('@/views/content/media/video.vue'),
meta: {
title: '视频管理',
},
},
],
},
],
}
```
## 特殊场景
### 带权限的路由
```typescript
const routes: RouteRecordRaw = {
path: '/admin',
component: Layout,
name: 'admin',
meta: {
title: '管理员',
icon: 'i-ep:user-filled',
auth: 'admin', // 需要 admin 权限
},
children: [
{
path: 'users',
name: 'adminUsers',
component: () => import('@/views/admin/users.vue'),
meta: {
title: '用户列表',
auth: ['admin:view', 'admin:edit'], // 需要其中一个权限
},
},
],
}
```
### 外部链接
```typescript
const routes: RouteRecordRaw = {
path: '/external',
component: Layout,
name: 'external',
meta: {
title: '外部链接',
icon: 'i-ep:link',
},
children: [
{
path: 'github',
name: 'externalGithub',
component: () => import('@/views/external/link.vue'),
meta: {
title: 'GitHub',
link: 'https://github.com',
},
},
],
}
```
### 默认展开的路由
```typescript
const routes: RouteRecordRaw = {
path: '/menu',
component: Layout,
name: 'menu',
meta: {
title: '菜单示例',
icon: 'i-ep:menu',
expand: true, // 默认展开
},
children: [
{
path: 'always',
name: 'menuAlways',
meta: {
title: '默认展开',
expand: true,
},
children: [
{
path: 'item1',
name: 'menuAlwaysItem1',
component: () => import('@/views/menu/item1.vue'),
meta: {
title: '菜单项 1',
},
},
],
},
],
}
```
## 路由配置调整
### 权限配置调整
**场景 1: 添加单个权限**
```typescript
// 修改前
meta: {
title: '用户管理',
}
// 修改后
meta: {
title: '用户管理',
auth: 'user:view', // 需要 user:view 权限
}
```
**场景 2: 添加多个权限(或关系)**
```typescript
meta: {
title: '用户管理',
auth: ['user:view', 'user:edit'], // 满足其中一个即可
}
```
### 页面保活配置
**场景 1: 列表页保活配置**
列表页需要在从详情页返回时保持状态。
```typescript
{
path: '',
name: 'orderList',
component: () => import('@/views/order/list.vue'),
meta: {
title: '订单列表',
keepAlive: ['orderDetail', 'orderEdit'], // 从这些页面返回时保活
},
}
```
**场景 2: 详情页保活配置**
详情页需要保活,但从列表页进入时不保活(确保显示最新数据)。
```typescript
{
path: 'detail/:id',
name: 'orderDetail',
component: () => import('@/views/order/detail.vue'),
meta: {
title: '订单详情',
menu: false,
activeMenu: '/order',
keepAlive: true, // 始终保活
noKeepAlive: 'orderList', // 从列表进入时不保活
},
}
```
**场景 3: 取消保活**
某些页面不需要保活,每次进入都重新加载。
```typescript
// 删除或注释掉 keepAlive 相关配置
meta: {
title: '实时数据',
// keepAlive: true, // 删除此行
}
```
### 导航显示调整
**场景 1: 隐藏导航项**
某些页面不需要在导航菜单中显示。
```typescript
meta: {
title: '个人设置',
menu: false, // 不在导航中显示
activeMenu: '/user', // 但高亮用户菜单
}
```
### 面包屑配置
**场景 1: 隐藏面包屑**
某些页面不需要显示面包屑。
```typescript
meta: {
title: '登录',
breadcrumb: false, // 不显示面包屑
}
```
**场景 2: 自定义面包屑高亮**
```typescript
meta: {
title: '编辑文章',
activeMenu: '/article/list', // 面包屑高亮到文章列表
}
```
### 外部链接配置
**场景 1: 在新窗口打开外部链接**
```typescript
{
path: 'github',
name: 'externalGithub',
component: () => import('@/views/external/link.vue'),
meta: {
title: 'GitHub',
link: 'https://github.com', // 在新窗口打开
},
}
```
## 在 routes.ts 中的使用
```typescript
import ArticleRoute from './modules/article'
import ContentRoute from './modules/content'
// 1. 导入路由模块
import DashboardRoute from './modules/dashboard'
import SystemRoute from './modules/system'
// 2. 添加到 asyncRoutes
const asyncRoutes: Route.recordMainRaw[] = [
{
meta: {
title: '工作台',
icon: 'i-ep:monitor',
},
children: [
DashboardRoute,
],
},
{
meta: {
title: '内容',
icon: 'i-ep:document',
},
children: [
ArticleRoute,
ContentRoute,
],
},
{
meta: {
title: '系统',
icon: 'i-ep:setting',
},
children: [
SystemRoute,
],
},
]
```

View File

@ -0,0 +1,115 @@
# RouteMetaRaw 类型属性说明
本文档详细说明了 Fantastic-admin 框架中路由 meta 对象的所有可用属性。
## 目录
- [权限相关](#权限相关)
- [导航显示](#导航显示)
- [标签页](#标签页)
- [页面行为](#页面行为)
- [布局](#布局)
- [其他](#其他)
## 权限相关
### auth
- **类型**: `string | string[]`
- **默认值**: `undefined`
- **说明**: 路由访问权限,配置为数组时,只需满足一个即可进入
- **示例**:
```typescript
auth: 'news:view' // 需要具备 news:view 权限
auth: ['news:view', 'news:edit'] // 需要具备其中一个权限
```
## 导航显示
### title
- **类型**: `string | (() => string)`
- **默认值**: `undefined`
- **说明**: 标题会在导航、标签页、面包屑等需要的展示位置显示
- **示例**:
```typescript
title: '新闻管理'
title: () => '动态标题'
```
### icon
- **类型**: `string`
- **默认值**: `undefined`
- **说明**: 图标
- **示例**:
```typescript
icon: 'i-ep:lock' // 默认显示 i-ep:lock 图标
```
### menu
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 是否在导航中显示,当子导航里没有可展示的导航时,会直接显示父导航
### activeMenu
- **类型**: `string`
- **默认值**: `undefined`
- **说明**: 高亮导航,需要设置完整路由地址
- **示例**:
```typescript
activeMenu: '/news/list'
```
### expand
- **类型**: `boolean`
- **默认值**: `undefined`
- **说明**: 是否默认展开
- **示例**:
```typescript
expand: true // 默认展开
```
### breadcrumb
- **类型**: `boolean`
- **默认值**: `true`
- **说明**: 是否在面包屑中显示
## 页面行为
### keepAlive
- **类型**: `boolean | string | string[]`
- **默认值**: `undefined`
- **说明**: 保活,根据规则保活当前路由页面
- **示例**:
```typescript
keepAlive: true // 始终保活
keepAlive: 'news' // 访问路由name为news的页面时保活
keepAlive: ['news', 'user'] // 访问路由name为news或user的页面时保活
```
### noKeepAlive
- **类型**: `string | string[]`
- **默认值**: `undefined`
- **说明**: 不保活,根据规则不保活当前路由页面
- **示例**:
```typescript
noKeepAlive: 'news' // 访问路由name为news的页面时不保活
noKeepAlive: ['news', 'user'] // 访问路由name为news或user的页面时不保活
```
### link
- **类型**: `string`
- **默认值**: `undefined`
- **说明**: 外部链接,会在浏览器新窗口访问该链接
- **示例**:
```typescript
link: 'https://fantastic-admin.hurui.me' // 在浏览器新窗口打开 Fantastic-admin 官网
```

View File

@ -0,0 +1,102 @@
---
name: fa-slot-creator
description: 在 Fantastic-admin 框架中创建插槽,向布局的任意位置注入自定义内容。当用户提到以下任何需求时必须使用此技能:在头部/侧边栏/工具栏/标签栏加东西、logo 旁边加内容、侧边栏底部加用户信息、加个悬浮按钮、在导航菜单右侧加自定义组件、想在框架布局某个区域插入内容、在页面顶部或底部插入全局横幅/公告/版权栏。即使用户只是说"在顶部加个 XX"或"侧边栏加点东西",也应触发此技能。支持 19 个插槽位置。
---
# Fantastic-admin 插槽创建器
框架通过约定自动发现插槽:目录名必须与插槽名完全匹配(区分大小写),文件名固定为 `index.vue`。不符合约定的文件不会被加载,这是框架的自动发现机制决定的。
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中创建插槽,并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根,例如 `apps/<app>/src/slots/`
## 创建插槽(手动步骤)
1. 创建目录:`apps/<app>/src/slots/{插槽名称}/`
2. 在该目录下创建 `index.vue` 文件,内容参考下方模板
### 普通插槽模板
```vue
<script setup lang="ts">
// 在此添加插槽逻辑
</script>
<template>
<div>
<!-- 在此添加插槽内容 -->
</div>
</template>
<style scoped>
/* 在此添加插槽样式 */
</style>
```
### FreePosition 插槽模板
```vue
<script setup lang="ts">
// 在此添加插槽逻辑
</script>
<template>
<div class="free-position-slot">
<!-- 在此添加插槽内容 -->
<!-- 注意:此插槽需要绝对定位 -->
</div>
</template>
<style scoped>
.free-position-slot {
position: absolute;
/* 在此设置定位坐标,例如: */
/* bottom: 20px; */
/* right: 20px; */
/* z-index: 1000; */
}
</style>
```
## 可用插槽位置
| 区域 | 插槽名称 |
|------|---------|
| 布局 | `LayoutTop`、`LayoutBottom` |
| 头部 | `HeaderStart`、`HeaderAfterLogo`、`HeaderAfterMenu`、`HeaderEnd` |
| 主侧边栏 | `MainSidebarTop`、`MainSidebarAfterLogo`、`MainSidebarAfterMenu`、`MainSidebarBottom` |
| 子侧边栏 | `SubSidebarTop`、`SubSidebarAfterLogo`、`SubSidebarAfterMenu`、`SubSidebarBottom` |
| 标签栏 | `TabbarStart`、`TabbarEnd` |
| 工具栏 | `ToolbarStart`、`ToolbarEnd` |
| 自由定位 | `FreePosition` |
## 选择指南
- 全局顶部横幅/公告(覆盖整个布局最上方) → `LayoutTop`
- 全局底部栏/版权声明(覆盖整个布局最下方) → `LayoutBottom`
- 全局导航元素 → `HeaderStart` / `HeaderEnd`
- 侧边栏用户信息/品牌内容 → `MainSidebarTop` / `MainSidebarAfterLogo`
- 工具栏自定义操作 → `ToolbarStart` / `ToolbarEnd`
- 悬浮元素(需要绝对定位) → `FreePosition`
> `LayoutTop` / `LayoutBottom` 位于整个布局的最外层,内容会横跨全宽,适合全局公告横幅、版权栏等需要独占一行的场景。
## FreePosition 特殊说明
`FreePosition` 插槽需要在样式中手动设置定位,否则内容不可见(模板见上方"FreePosition 插槽模板")。
## 故障排除
插槽未显示?检查:
- 目录名是否与插槽名完全匹配(区分大小写)
- 文件名是否为 `index.vue`
- `apps/<app>/src/slots/` 目录是否存在

View File

@ -0,0 +1,258 @@
# Fantastic-admin 插槽位置参考
本文档详细介绍 Fantastic-admin 框架中所有可用的插槽位置。
## 插槽分类
### 布局插槽2 个位置)
位于应用布局的最外层,横跨全宽。
#### LayoutTop
- **位置**:位于整个应用的最顶部,在头部之上
- **适用场景**
- 全局公告横幅
- 系统维护通知
- Cookie 同意栏
- 试用到期提醒
- **布局方式**:全宽块级容器
- **说明**:内容会将整个布局向下撑开,适合需要立即引起注意的横幅
#### LayoutBottom
- **位置**:位于整个应用的最底部,在页脚之下
- **适用场景**
- 全局版权声明
- 法律免责声明
- 持久状态栏
- **布局方式**:全宽块级容器
---
### 头部插槽4 个位置)
位于应用顶部的头部导航栏中。
#### HeaderStart
- **位置**头部最左侧logo 之前
- **适用场景**
- 菜单折叠按钮
- 面包屑导航
- 自定义品牌元素
- **布局方式**:水平弹性布局
#### HeaderAfterLogo
- **位置**:头部 logo 紧后方
- **适用场景**
- 应用标题或副标题
- 版本徽标
- 环境标识(开发/预发/生产)
- **布局方式**:水平弹性布局
#### HeaderAfterMenu
- **位置**:头部主菜单之后
- **版本要求**v5.3.0+
- **适用场景**
- 搜索框
- 快捷操作
- 通知提示
- **布局方式**:水平弹性布局
#### HeaderEnd
- **位置**:头部最右侧
- **适用场景**
- 用户头像下拉菜单
- 设置按钮
- 退出登录按钮
- 主题切换器
- **布局方式**:水平弹性布局
---
### 主侧边栏插槽4 个位置)
位于主导航侧边栏中。
#### MainSidebarTop
- **位置**主侧边栏顶部logo 之前
- **适用场景**
- 折叠/展开按钮
- 自定义头部内容
- 工作区选择器
- **布局方式**:垂直弹性布局
#### MainSidebarAfterLogo
- **位置**:主侧边栏 logo 紧后方
- **适用场景**
- 用户信息卡片
- 快速统计数据
- 工作区名称
- **布局方式**:垂直弹性布局
#### MainSidebarAfterMenu
- **位置**:主侧边栏导航菜单之后
- **版本要求**v5.3.0+
- **适用场景**
- 附加导航项
- 快捷方式
- 固定项目
- **布局方式**:垂直弹性布局
#### MainSidebarBottom
- **位置**:主侧边栏底部
- **适用场景**
- 帮助/支持链接
- 版本信息
- 底部内容
- 折叠按钮
- **布局方式**:垂直弹性布局
---
### 子侧边栏插槽4 个位置)
位于次级导航侧边栏中(使用多级导航时显示)。
#### SubSidebarTop
- **位置**:子侧边栏顶部
- **适用场景**
- 区块标题
- 返回按钮
- 面包屑
- **布局方式**:垂直弹性布局
#### SubSidebarAfterLogo
- **位置**:子侧边栏 logo 之后
- **适用场景**
- 区块描述
- 上下文信息
- **布局方式**:垂直弹性布局
#### SubSidebarAfterMenu
- **位置**:子侧边栏导航菜单之后
- **版本要求**v5.3.0+
- **适用场景**
- 附加子导航
- 相关链接
- **布局方式**:垂直弹性布局
#### SubSidebarBottom
- **位置**:子侧边栏底部
- **适用场景**
- 区块专属操作
- 底部内容
- **布局方式**:垂直弹性布局
---
### 顶部栏插槽4 个位置)
位于标签栏和工具栏区域。
#### TabbarStart
- **位置**:标签栏左侧
- **适用场景**
- 标签导航控件
- 刷新按钮
- 自定义标签操作
- **布局方式**:水平弹性布局
#### TabbarEnd
- **位置**:标签栏右侧
- **适用场景**
- 关闭所有标签按钮
- 标签管理操作
- **布局方式**:水平弹性布局
#### ToolbarStart
- **位置**:工具栏左侧
- **适用场景**
- 页面专属操作
- 面包屑
- 页面标题
- **布局方式**:水平弹性布局
#### ToolbarEnd
- **位置**:工具栏右侧
- **适用场景**
- 操作按钮
- 筛选器
- 导出/导入按钮
- **布局方式**:水平弹性布局
---
### 自由定位插槽
#### FreePosition
- **位置**:灵活定位,需手动设置坐标
- **特殊要求**
- **必须**在样式中使用 `position: absolute;`
- **必须**手动设置定位坐标top/right/bottom/left
- **必须**设置合适的 z-index
- **适用场景**
- 悬浮操作按钮FAB
- 客服聊天组件
- 自定义遮罩层
- 通知 Toast
- 帮助按钮
- **样式示例**
```css
.free-position-slot {
position: absolute;
bottom: 20px;
right: 20px;
z-index: 1000;
}
```
---
## 插槽选择指南
### 何时使用布局插槽:
- 需要显示在所有内容之上的全局横幅(公告、维护通知)
- 需要显示在所有内容之下的全局底栏(版权声明、法律免责)
### 何时使用头部插槽:
- 全局导航元素
- 用户账号控件
- 全局操作按钮
- 品牌元素
### 何时使用侧边栏插槽:
- 导航增强内容
- 用户信息展示
- 工作区上下文
- 帮助与支持链接
### 何时使用顶部栏插槽:
- 页面专属操作
- 标签管理
- 上下文控件
- 面包屑导航
### 何时使用 FreePosition
- 不适合放入标准布局的悬浮元素
- 需要覆盖在内容之上的元素
- 客服聊天组件或帮助按钮
- 需要自定义定位的组件
---
## 文件结构
所有插槽必须遵循以下目录结构:
```
/src/slots/{插槽名称}/index.vue
```
**注意**:文件名必须为 `index.vue`
示例:
```
/src/slots/LayoutTop/index.vue
/src/slots/LayoutBottom/index.vue
/src/slots/HeaderStart/index.vue
/src/slots/MainSidebarBottom/index.vue
/src/slots/FreePosition/index.vue
```

View File

@ -0,0 +1,77 @@
---
name: fa-store-generator
description: 为 Fantastic-admin 框架生成 Pinia Store 模块。当用户提到以下任何需求时必须使用此技能:多个页面需要共享数据、全局状态管理、数据需要持久化(刷新后还在)、登录状态/用户信息需要缓存、购物车/通知/权限等全局数据、需要在组件外访问状态。即使用户只是说"这个数据要全局共享"或"刷新后数据不能丢",也应触发此技能。触发关键词:创建 store、全局状态、状态管理、pinia、持久化、共享数据。
---
# Store Generator
## 第一步:确认工作区(必须阻塞等待用户回复)
本项目是 monorepo 架构,`apps/` 目录下存放各应用。**在执行任何文件读写操作之前**,必须先确认目标应用:
1. 执行 `ls apps/` 列出所有可用应用
2. **立即向用户提问**,明确询问要在哪个应用中创建 Store并**停止等待回复**
3. 收到用户明确回复后,才能继续后续步骤
> **严格规则**:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根,例如 `apps/<app>/src/store/modules/`
## 项目 Store 概览
- 位置:`apps/<app>/src/store/modules/` (业务 store
- 全部使用 **Composition API** 风格(`defineStore` + setup 函数)
- 通过 `unplugin-auto-import` 自动导入,组件中无需手动 import
- 持久化:`pinia-plugin-persistedstate` v4配置 `persist: { pick: [...] }`
详细模板和示例见 [references/store-patterns.md](references/store-patterns.md)
---
## 交互式工作流
先收集信息再生成代码,因为 store 的字段结构、持久化需求、异步 action 直接决定代码骨架——跳过这步会生成空壳,用户还要大量修改。
### Step 1收集基本信息
向用户提问(可合并为一次):
1. **Store 用途**:管理什么数据?(例如:用户信息、购物车、通知列表)
2. **存放位置**
- `apps/<app>/src/store/modules/` — 业务 store推荐
- `apps/<app>/src/store/modules/app/` — 框架级 store仅框架内部使用
3. **State 字段**:需要哪些状态字段?请列出字段名、类型和初始值
### Step 2收集功能需求
根据 Step 1 的回答,继续询问:
4. **持久化**:是否需要持久化到 localStorage如果是哪些字段需要持久化
5. **异步 Action**:是否有需要调用 API 的操作?如果有,请描述接口用途
6. **Computed**是否需要派生状态computed例如从列表中过滤、统计数量等
### Step 3确认并生成
汇总用户的回答,展示将要生成的内容摘要,确认后再写入文件。
---
## 命名规范
| 类型 | Store ID | 函数名 | 文件名 |
|------|----------|--------|--------|
| 业务 store | `camelCase` | `use<Name>Store` | `<name>.ts` |
| 框架 store | `app<Name>` | `useApp<Name>Store` | `<name>.ts` |
示例:
- 购物车 → ID: `cart`,函数: `useCartStore`,文件: `apps/<app>/src/store/modules/cart.ts`
- 通知 → ID: `notification`,函数: `useNotificationStore`,文件: `apps/<app>/src/store/modules/notification.ts`
---
## 生成后的操作
Store 文件创建完成后,告知用户:
- 无需手动 import`unplugin-auto-import` 已自动处理
- 在任意组件/composable 中直接使用:`const xxxStore = useXxxStore()`
- 如需在 store 外部(如路由守卫)使用,需传入 pinia 实例:`useXxxStore(pinia)`

View File

@ -0,0 +1,178 @@
# Store 模板与示例
## 基础模板
```typescript
import { defineStore } from 'pinia'
export const use<Name>Store = defineStore('<id>', () => {
// State
const <field> = ref<<Type>>(<initialValue>)
// Computed
const <computed> = computed(() => ...)
// Actions
function <action>() {
...
}
return {
<field>,
<computed>,
<action>,
}
})
```
---
## 模板变体
### 1. 纯状态 Store无持久化、无异步
```typescript
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const visible = ref(false)
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
)
function addItem(item: CartItem) {
const existing = items.value.find(i => i.id === item.id)
if (existing) {
existing.quantity++
}
else {
items.value.push({ ...item, quantity: 1 })
}
}
function removeItem(id: string) {
items.value = items.value.filter(i => i.id !== id)
}
function clear() {
items.value = []
}
return { items, visible, total, addItem, removeItem, clear }
})
```
### 2. 带持久化的 Store
```typescript
import { defineStore } from 'pinia'
export const useUserPreferenceStore = defineStore(
'userPreference',
() => {
const theme = ref<'light' | 'dark'>('light')
const language = ref('zh-cn')
const pageSize = ref(20)
function setTheme(val: 'light' | 'dark') {
theme.value = val
}
return { theme, language, pageSize, setTheme }
},
{
persist: {
pick: ['theme', 'language', 'pageSize'], // 只持久化指定字段
},
},
)
```
> 持久化默认使用 localStoragekey 为 store ID。
> 使用 `pick` 只持久化部分字段,避免持久化临时状态。
### 3. 带异步 Action 的 Store
```typescript
import { defineStore } from 'pinia'
export const useNotificationStore = defineStore('notification', () => {
const list = ref<Notification[]>([])
const loading = ref(false)
const unreadCount = computed(() => list.value.filter(n => !n.read).length)
async function fetchList() {
loading.value = true
try {
const res = await api.notification.list()
list.value = res.data
}
finally {
loading.value = false
}
}
async function markRead(id: string) {
await api.notification.markRead(id)
const item = list.value.find(n => n.id === id)
if (item) {
item.read = true
}
}
return { list, loading, unreadCount, fetchList, markRead }
})
```
### 4. 带 TypeScript 接口定义的 Store
```typescript
import { defineStore } from 'pinia'
interface DictionaryItem {
label: string
value: string | number
}
interface DictionaryState {
[key: string]: DictionaryItem[]
}
export const useDictionaryStore = defineStore('dictionary', () => {
const data = ref<DictionaryState>({})
function getItems(type: string): DictionaryItem[] {
return data.value[type] ?? []
}
async function fetchByType(type: string) {
if (data.value[type]) return // 已缓存,跳过
const res = await api.dictionary.getByType(type)
data.value[type] = res.data
}
return { data, getItems, fetchByType }
})
```
---
## 项目现有 Store 参考
| Store | 文件 | 用途 |
|-------|------|------|
| `useAppAccountStore` | `modules/app/account.ts` | 登录/登出、token、多账号 |
| `useAppSettingsStore` | `modules/app/settings.ts` | 主题、语言、布局配置 |
| `useAppMenuStore` | `modules/app/menu.ts` | 菜单生成与导航状态 |
| `useAppTabbarStore` | `modules/app/tabbar.ts` | 标签栏管理 |
---
## 注意事项
- Store 文件放在 `src/store/modules/` 后,`unplugin-auto-import` 会自动扫描并全局注入,**无需手动 import**
- Store ID 必须全局唯一camelCase
- 避免在 store 中直接引用 DOM 或组件实例
- 跨 store 调用:直接在 setup 函数内调用其他 store`const authStore = useAppAccountStore()`

View File

@ -0,0 +1,126 @@
---
name: fa-theme-customizer
description: 为 Fantastic-admin 框架创建和定制主题配色方案始终同时生成明色light和暗色dark两套主题。当用户提到以下任何需求时必须使用此技能换主题颜色、换配色、做一个 XX 风格的主题、主题太单调了、品牌色是 XXX 帮我生成主题、把 tweakcn 配色转成框架主题、想要暗色/冷色/暖色调、根据设计稿颜色生成主题。即使用户只是描述一种感觉("清新"、"沉稳"、"科技感")或提供一个颜色值("#2563EB"),也应触发此技能。支持"吉卜力"、"赛博朋克"、"莫兰迪"、"北欧极简"等专业设计风格关键词。
---
# 主题定制器
为 Fantastic-admin 框架生成符合规范的主题配色。
## 主题文件位置
- 主题定义:`packages/themes/index.ts`(在此替换新主题,跨所有应用共享)
- 类型推断:自动从 `packages/themes/index.ts` 的 key 推断,无需手动更新类型
## 工作流程
### 第一步:确认设计风格
如果用户没有明确指定颜色,先提供风格参考。读取 `references/design-styles.md` 获取完整的风格目录和配色建议。
### 第二步:生成 OKLCH 色值
框架使用 **OKLCH 色彩空间**,格式为 `L C H`(不含 `oklch()` 包裹,直接写三个数值)。
转换规则:
- 明色背景:`1 0 0`(纯白)或接近白色的暖/冷色调
- 暗色背景:`0.141 0.005 285.823`(默认深灰)或更深的色调
- 主色primary明色通常比暗色亮度L值高 0.05~0.1
读取 `references/theme-structure.md` 了解完整的 CSS 变量说明和取值规范。
### 第三步:输出主题代码
`packages/themes/index.ts` 中替换新主题,格式严格遵循现有主题结构:
```typescript
export const lightTheme = {
// shadcn 标准 token必填
'--background': 'L C H',
'--foreground': 'L C H',
'--card': 'L C H',
'--card-foreground': 'L C H',
'--popover': 'L C H',
'--popover-foreground': 'L C H',
'--primary': 'L C H',
'--primary-foreground': 'L C H',
'--secondary': 'L C H',
'--secondary-foreground': 'L C H',
'--muted': 'L C H',
'--muted-foreground': 'L C H',
'--accent': 'L C H',
'--accent-foreground': 'L C H',
'--destructive': '0.577 0.245 27.325', // 通常保持不变
'--border': 'L C H',
'--input': 'L C H',
'--ring': 'L C H', // 通常与 primary 相同
// 框架专属 token必填
'--g-main-area-bg': 'oklch(L C H)', // 明色略深于背景,形成层次感
'--g-header-bg': 'oklch(var(--background))',
'--g-header-color': 'oklch(var(--foreground))',
'--g-header-menu-color': 'oklch(var(--accent-foreground))',
'--g-header-menu-hover-bg': 'oklch(var(--accent))',
'--g-header-menu-hover-color': 'oklch(var(--accent-foreground))',
'--g-header-menu-active-bg': 'oklch(var(--primary))',
'--g-header-menu-active-color': 'oklch(var(--primary-foreground))',
'--g-main-sidebar-bg': 'oklch(var(--background))',
'--g-main-sidebar-menu-color': 'oklch(var(--accent-foreground))',
'--g-main-sidebar-menu-hover-bg': 'oklch(var(--accent))',
'--g-main-sidebar-menu-hover-color': 'oklch(var(--accent-foreground))',
'--g-main-sidebar-menu-active-bg': 'oklch(var(--primary))',
'--g-main-sidebar-menu-active-color': 'oklch(var(--primary-foreground))',
'--g-sub-sidebar-bg': 'oklch(var(--background))',
'--g-sub-sidebar-menu-color': 'oklch(var(--accent-foreground))',
'--g-sub-sidebar-menu-hover-bg': 'oklch(var(--accent))',
'--g-sub-sidebar-menu-hover-color': 'oklch(var(--accent-foreground))',
'--g-sub-sidebar-menu-active-bg': 'oklch(var(--primary))',
'--g-sub-sidebar-menu-active-color': 'oklch(var(--primary-foreground))',
'--g-tabbar-bg': 'oklch(var(--background))',
'--g-tabbar-tab-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-hover-bg': 'oklch(var(--accent) / 50%)',
'--g-tabbar-tab-hover-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-active-bg': 'oklch(var(--accent))',
'--g-tabbar-tab-active-color': 'oklch(var(--foreground))',
'--g-toolbar-bg': 'oklch(var(--background))',
} as const
export const darkTheme = {
// shadcn 标准 token同明色结构但色值更深
'--background': 'L C H',
// ... 其余 shadcn token
// 框架专属 token暗色与明色有 5 处关键差异,见 references/theme-structure.md
'--g-main-area-bg': 'oklch(var(--background))', // 暗色与背景相同,避免过多层次
'--g-header-bg': 'oklch(var(--background))',
'--g-header-color': 'oklch(var(--foreground))',
'--g-header-menu-color': 'oklch(var(--muted-foreground))', // 差异:用 muted-foreground
'--g-header-menu-hover-bg': 'oklch(var(--muted))', // 差异:用 muted
'--g-header-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-header-menu-active-bg': 'oklch(var(--accent))', // 差异:用 accent 而非 primary
'--g-header-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-main-sidebar-bg': 'oklch(var(--background))',
'--g-main-sidebar-menu-color': 'oklch(var(--muted-foreground))',
'--g-main-sidebar-menu-hover-bg': 'oklch(var(--muted))',
'--g-main-sidebar-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-main-sidebar-menu-active-bg': 'oklch(var(--accent))',
'--g-main-sidebar-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-sub-sidebar-bg': 'oklch(var(--background))',
'--g-sub-sidebar-menu-color': 'oklch(var(--muted-foreground))',
'--g-sub-sidebar-menu-hover-bg': 'oklch(var(--muted))',
'--g-sub-sidebar-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-sub-sidebar-menu-active-bg': 'oklch(var(--accent))',
'--g-sub-sidebar-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-tabbar-bg': 'oklch(var(--background))',
'--g-tabbar-tab-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-hover-bg': 'oklch(var(--accent) / 50%)',
'--g-tabbar-tab-hover-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-active-bg': 'oklch(var(--accent))',
'--g-tabbar-tab-active-color': 'oklch(var(--foreground))',
'--g-toolbar-bg': 'oklch(var(--background))',
} as const
```
## 参考资源
- **设计风格目录**`references/design-styles.md` — 包含 20+ 种专业设计风格的配色方案和灵感来源
- **CSS 变量说明**`references/theme-structure.md` — 每个变量的作用、取值规范和明暗差异(含完整暗色模板)
- **社区配色**https://tweakcn.com/community — 可直接参考社区主题的 shadcn CSS 变量

View File

@ -0,0 +1,292 @@
# 设计风格配色参考目录
收录 20+ 种专业设计风格每种风格提供核心配色建议OKLCH 格式)。
## 目录
1. [日系动画风格](#日系动画风格)
2. [科技/未来风格](#科技未来风格)
3. [自然/有机风格](#自然有机风格)
4. [复古/怀旧风格](#复古怀旧风格)
5. [极简主义风格](#极简主义风格)
6. [艺术设计流派](#艺术设计流派)
7. [品牌/商业风格](#品牌商业风格)
8. [tweakcn 社区风格参考](#tweakcn-社区风格参考)
9. [OKLCH 快速参考](#oklch-快速参考)
---
## 日系动画风格
### 吉卜力Ghibli
宫崎骏作品的自然温暖色调,大地色系与天空蓝的结合。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.97 0.008 85` | `0.18 0.012 85` | 米白/深棕 |
| primary | `0.62 0.12 145` | `0.55 0.10 145` | 森林绿 |
| secondary | `0.88 0.06 85` | `0.28 0.06 85` | 暖米色 |
| accent | `0.75 0.10 85` | `0.35 0.08 85` | 大地棕 |
| muted | `0.92 0.04 85` | `0.25 0.04 85` | 浅米色 |
灵感:《龙猫》草绿、《千与千寻》暖橙、《哈尔的移动城堡》天蓝
### 新海诚Makoto Shinkai
高饱和度的天空蓝与城市光晕,强烈的光影对比。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.97 0.005 240` | `0.12 0.015 240` | 冷白/深夜蓝 |
| primary | `0.58 0.22 240` | `0.52 0.20 240` | 天空蓝 |
| accent | `0.75 0.15 55` | `0.65 0.18 55` | 黄昏橙 |
| muted | `0.92 0.03 240` | `0.22 0.03 240` | 淡蓝灰 |
灵感:《你的名字》黄昏渐变、《天气之子》雨后天空
---
## 科技/未来风格
### 赛博朋克Cyberpunk
霓虹粉紫与深黑的强烈对比,高饱和度荧光色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.95 0.005 300` | `0.08 0.01 300` | 浅紫白/极深黑 |
| primary | `0.65 0.28 330` | `0.72 0.30 330` | 霓虹粉 |
| secondary | `0.60 0.25 280` | `0.55 0.22 280` | 电子紫 |
| accent | `0.75 0.25 195` | `0.70 0.28 195` | 青色荧光 |
| border | `0.85 0.08 300` | `0.25 0.08 300` | 紫色边框 |
灵感:《银翼杀手 2049》、《赛博朋克 2077》
### 终端/黑客Terminal
经典绿色终端风格,深黑背景配荧光绿。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.96 0.01 145` | `0.08 0.01 145` | 浅绿白/终端黑 |
| primary | `0.65 0.22 145` | `0.72 0.25 145` | 荧光绿 |
| foreground | `0.15 0.01 145` | `0.90 0.15 145` | 深绿/亮绿 |
| muted | `0.88 0.04 145` | `0.18 0.04 145` | 暗绿 |
### 深空Deep Space
NASA 风格的深蓝黑与星云紫,科技感十足。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.96 0.008 250` | `0.10 0.015 250` | 冷白/深空蓝 |
| primary | `0.55 0.20 260` | `0.60 0.22 260` | 星云蓝 |
| accent | `0.65 0.18 300` | `0.60 0.20 300` | 星云紫 |
| secondary | `0.88 0.05 250` | `0.20 0.05 250` | 深蓝灰 |
---
## 自然/有机风格
### 莫兰迪Morandi
低饱和度的灰调色彩,优雅克制,源自意大利画家乔治·莫兰迪。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.95 0.008 60` | `0.20 0.008 60` | 灰米白/深灰棕 |
| primary | `0.62 0.06 180` | `0.55 0.05 180` | 灰蓝绿 |
| secondary | `0.85 0.04 60` | `0.30 0.04 60` | 灰米色 |
| accent | `0.72 0.05 30` | `0.40 0.04 30` | 灰玫瑰 |
| muted | `0.90 0.02 60` | `0.25 0.02 60` | 浅灰 |
特点所有颜色饱和度C值控制在 0.02~0.08 之间
### 北欧极简Scandinavian
白桦木色调,干净的白色与自然木色,受北欧设计影响。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.98 0.005 80` | `0.16 0.008 80` | 纯白/深木色 |
| primary | `0.45 0.08 80` | `0.55 0.06 80` | 深木棕 |
| secondary | `0.92 0.03 80` | `0.26 0.03 80` | 浅木色 |
| accent | `0.70 0.12 145` | `0.55 0.10 145` | 苔藓绿 |
### 日式侘寂Wabi-Sabi
不完美之美,枯山水的灰褐色调,禅意十足。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.94 0.01 70` | `0.18 0.01 70` | 宣纸白/墨色 |
| primary | `0.40 0.05 70` | `0.50 0.04 70` | 枯叶棕 |
| accent | `0.65 0.04 145` | `0.45 0.03 145` | 苔绿 |
| border | `0.82 0.03 70` | `0.28 0.03 70` | 淡墨色 |
---
## 复古/怀旧风格
### 蒸汽朋克Steampunk
维多利亚时代的铜色与皮革棕,工业齿轮美学。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.92 0.02 70` | `0.16 0.02 70` | 羊皮纸/深棕 |
| primary | `0.62 0.12 55` | `0.58 0.14 55` | 铜金色 |
| secondary | `0.80 0.06 70` | `0.28 0.06 70` | 皮革棕 |
| accent | `0.55 0.08 200` | `0.50 0.10 200` | 铜绿 |
### 洛可可Rococo
18世纪法国宫廷风格粉彩色调精致华丽。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.97 0.01 330` | `0.18 0.01 330` | 玫瑰白/深玫瑰 |
| primary | `0.72 0.12 330` | `0.62 0.10 330` | 粉玫瑰 |
| secondary | `0.90 0.05 280` | `0.28 0.05 280` | 淡薰衣草 |
| accent | `0.80 0.08 55` | `0.45 0.08 55` | 香槟金 |
### 复古美式Retro Americana
50-70年代美国风格暖橙与奶油色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.96 0.02 80` | `0.18 0.02 80` | 奶油色/深棕 |
| primary | `0.68 0.18 45` | `0.62 0.16 45` | 复古橙 |
| secondary | `0.88 0.06 80` | `0.28 0.06 80` | 奶油黄 |
| accent | `0.55 0.12 25` | `0.50 0.10 25` | 砖红 |
---
## 极简主义风格
### 包豪斯Bauhaus
功能主义美学,原色(红黄蓝)与黑白灰的几何构成。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `1 0 0` | `0.14 0 0` | 纯白/纯黑 |
| primary | `0.55 0.25 260` | `0.60 0.22 260` | 包豪斯蓝 |
| secondary | `0.90 0 0` | `0.25 0 0` | 浅灰/深灰 |
| accent | `0.65 0.25 25` | `0.60 0.22 25` | 包豪斯红 |
### 瑞士国际主义Swiss International
网格系统Helvetica 精神,黑白红三色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `1 0 0` | `0.12 0 0` | 纯白/近黑 |
| primary | `0.50 0.25 25` | `0.55 0.22 25` | 瑞士红 |
| foreground | `0.10 0 0` | `0.95 0 0` | 近黑/近白 |
| muted | `0.92 0 0` | `0.22 0 0` | 浅灰/深灰 |
---
## 艺术设计流派
### 孟菲斯Memphis
80年代意大利设计运动高饱和度几何图案大胆撞色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.97 0.01 300` | `0.15 0.01 300` | 浅紫白/深紫 |
| primary | `0.70 0.25 330` | `0.65 0.22 330` | 孟菲斯粉 |
| secondary | `0.85 0.08 300` | `0.30 0.08 300` | 薰衣草 |
| accent | `0.75 0.22 85` | `0.70 0.20 85` | 柠檬黄 |
### 新艺术运动Art Nouveau
有机曲线,自然植物纹样,金色与绿色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.96 0.01 80` | `0.16 0.01 80` | 象牙白/深橄榄 |
| primary | `0.55 0.12 145` | `0.50 0.10 145` | 橄榄绿 |
| accent | `0.72 0.15 75` | `0.65 0.12 75` | 金色 |
| secondary | `0.88 0.04 80` | `0.26 0.04 80` | 米色 |
### 波普艺术Pop Art
安迪·沃霍尔风格,高对比度,鲜艳原色。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `1 0 0` | `0.10 0 0` | 纯白/纯黑 |
| primary | `0.72 0.28 25` | `0.68 0.25 25` | 波普红 |
| secondary | `0.88 0.20 85` | `0.35 0.15 85` | 波普黄 |
| accent | `0.60 0.25 240` | `0.55 0.22 240` | 波普蓝 |
---
## 品牌/商业风格
### 科技蓝Tech Blue
企业级科技感,参考 IBM、Microsoft 风格。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.98 0.003 240` | `0.13 0.005 240` | 冷白/深蓝黑 |
| primary | `0.55 0.20 250` | `0.60 0.18 250` | 科技蓝 |
| secondary | `0.92 0.02 240` | `0.22 0.02 240` | 浅蓝灰 |
| accent | `0.65 0.15 200` | `0.58 0.12 200` | 青蓝 |
### 金融绿Finance Green
稳健专业,参考彭博、路透风格。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.97 0.005 145` | `0.12 0.008 145` | 冷白/深绿黑 |
| primary | `0.52 0.18 145` | `0.58 0.16 145` | 金融绿 |
| secondary | `0.90 0.03 145` | `0.22 0.03 145` | 浅绿灰 |
| accent | `0.65 0.12 85` | `0.60 0.10 85` | 金色点缀 |
### 奢华黑金Luxury
高端品牌风格,深黑配金色,参考 Chanel、Rolex。
| 变量 | 明色 L C H | 暗色 L C H | 说明 |
|------|-----------|-----------|------|
| background | `0.96 0.005 75` | `0.10 0.005 75` | 象牙白/近黑 |
| primary | `0.68 0.12 75` | `0.72 0.14 75` | 金色 |
| foreground | `0.12 0.005 75` | `0.92 0.005 75` | 近黑/近白 |
| border | `0.80 0.06 75` | `0.25 0.06 75` | 金色边框 |
---
## tweakcn 社区风格参考
tweakcnhttps://tweakcn.com/community是 shadcn/ui 的主题社区,提供大量社区贡献的配色方案。
### 如何使用社区主题
1. 访问 https://tweakcn.com/community 浏览社区主题
2. 找到喜欢的主题,点击查看其 CSS 变量
3. 社区主题使用标准 shadcn CSS 变量格式,可直接转换
### 转换说明
社区主题的 CSS 变量格式通常为:
```css
:root {
--background: oklch(1 0 0);
--primary: oklch(0.623 0.214 259.815);
}
```
转换到框架格式时,去掉 `oklch()` 包裹,只保留三个数值,再补充框架专属的 `--g-*` 变量。
### 知名社区主题风格
- **Catppuccin** — 柔和粉彩,有 Latte/Frappé/Macchiato/Mocha 四个变体
- **Dracula** — 经典暗色,紫色系
- **Nord** — 北极极光,冷蓝色系
- **Solarized** — 护眼配色,暖黄底色
- **Gruvbox** — 复古暖色,棕黄系
- **Tokyo Night** — 深蓝紫夜晚城市风格
- **One Dark** — Atom 编辑器经典暗色主题
- **Everforest** — 森林绿色系,护眼舒适
---
## OKLCH 快速参考
### 亮度L
- `1.0` 纯白 / `0.97~0.99` 明色背景 / `0.50~0.70` 主色常用范围 / `0.10~0.18` 暗色背景 / `0.0` 纯黑
### 饱和度C
- `0` 无彩色 / `0.01~0.05` 极低(莫兰迪)/ `0.15~0.22` 中等(大多数主题)/ `0.25+` 高饱和(赛博朋克)
### 色相H
- `0~30` 红 / `30~60` 橙棕 / `60~90` 黄 / `90~150` 绿 / `150~210` 青 / `210~270` 蓝 / `270~330` 紫 / `330~360` 粉玫瑰

View File

@ -0,0 +1,169 @@
# 主题 CSS 变量结构说明
## 变量分类
主题由两类变量组成:
1. **shadcn 标准 token** — 通用设计系统变量,控制组件颜色
2. **框架专属 token`--g-*`** — 控制布局区域颜色
---
## shadcn 标准 Token
格式:`'L C H'`OKLCH 三个数值,不含 `oklch()` 包裹)
| 变量 | 作用 |
|------|------|
| `--background` | 页面主背景色 |
| `--foreground` | 主文字颜色 |
| `--card` | 卡片背景色(通常同 background |
| `--card-foreground` | 卡片文字颜色 |
| `--popover` | 弹出层背景色 |
| `--popover-foreground` | 弹出层文字颜色 |
| `--primary` | 主色调(按钮、高亮、激活状态) |
| `--primary-foreground` | 主色上的文字颜色 |
| `--secondary` | 次要色(次要按钮、标签背景) |
| `--secondary-foreground` | 次要色上的文字颜色 |
| `--muted` | 静音色(禁用状态、次要背景) |
| `--muted-foreground` | 静音色上的文字颜色 |
| `--accent` | 强调色hover 状态背景) |
| `--accent-foreground` | 强调色上的文字颜色 |
| `--destructive` | 危险/错误色(删除、警告) |
| `--border` | 边框颜色 |
| `--input` | 输入框边框颜色 |
| `--ring` | 焦点环颜色(通常同 primary |
---
## 框架专属 Token`--g-*`
格式:`'oklch(L C H)'` 或 `'oklch(var(--xxx))'`(含 `oklch()` 包裹)
| 变量 | 作用 |
|------|------|
| `--g-main-area-bg` | 主内容区域背景 |
| `--g-header-bg` | 顶部背景 |
| `--g-header-color` | 顶部文字颜色 |
| `--g-header-menu-color` | 顶部导航菜单项文字颜色 |
| `--g-header-menu-hover-bg` | 顶部导航菜单项 hover 背景 |
| `--g-header-menu-hover-color` | 顶部导航菜单项 hover 文字颜色 |
| `--g-header-menu-active-bg` | 顶部导航菜单项激活背景 |
| `--g-header-menu-active-color` | 顶部导航菜单项激活文字颜色 |
| `--g-main-sidebar-bg` | 主侧边栏背景 |
| `--g-main-sidebar-menu-color` | 主侧边栏导航菜单文字颜色 |
| `--g-main-sidebar-menu-hover-bg` | 主侧边栏导航菜单 hover 背景 |
| `--g-main-sidebar-menu-hover-color` | 主侧边栏导航菜单 hover 文字颜色 |
| `--g-main-sidebar-menu-active-bg` | 主侧边栏导航菜单激活背景 |
| `--g-main-sidebar-menu-active-color` | 主侧边栏导航菜单激活文字颜色 |
| `--g-sub-sidebar-bg` | 次侧边栏背景 |
| `--g-sub-sidebar-menu-color` | 次侧边栏导航菜单文字颜色 |
| `--g-sub-sidebar-menu-hover-bg` | 次侧边栏导航菜单 hover 背景 |
| `--g-sub-sidebar-menu-hover-color` | 次侧边栏导航菜单 hover 文字颜色 |
| `--g-sub-sidebar-menu-active-bg` | 次侧边栏导航菜单激活背景 |
| `--g-sub-sidebar-menu-active-color` | 次侧边栏导航菜单激活文字颜色 |
| `--g-tabbar-bg` | 标签栏背景 |
| `--g-tabbar-tab-color` | 标签项文字颜色 |
| `--g-tabbar-tab-hover-bg` | 标签项 hover 背景 |
| `--g-tabbar-tab-hover-color` | 标签项 hover 文字颜色 |
| `--g-tabbar-tab-active-bg` | 标签项激活背景 |
| `--g-tabbar-tab-active-color` | 标签项激活文字颜色 |
| `--g-toolbar-bg` | 工具栏背景 |
---
## 明色与暗色的关键差异
### 差异一:菜单激活状态
明色使用 `primary`,暗色使用 `accent`
```typescript
// 明色
'--g-header-menu-active-bg': 'oklch(var(--primary))',
'--g-header-menu-active-color': 'oklch(var(--primary-foreground))',
// 暗色
'--g-header-menu-active-bg': 'oklch(var(--accent))',
'--g-header-menu-active-color': 'oklch(var(--accent-foreground))',
```
同样适用于 `--g-main-sidebar-menu-active-*``--g-sub-sidebar-menu-active-*`
### 差异二:主内容区背景
```typescript
// 明色:略深于背景,形成层次感
'--g-main-area-bg': 'oklch(0.9612 0 0)',
// 暗色:与背景相同,避免过多层次
'--g-main-area-bg': 'oklch(var(--background))',
```
### 差异三:标签栏 hover 背景
```typescript
// 明色
'--g-tabbar-tab-hover-bg': 'oklch(var(--border))',
// 暗色
'--g-tabbar-tab-hover-bg': 'oklch(var(--accent) / 50%)',
```
### 差异四:标签栏激活背景
```typescript
// 明色
'--g-tabbar-tab-active-bg': 'oklch(var(--background))',
// 暗色
'--g-tabbar-tab-active-bg': 'oklch(var(--secondary))',
```
### 差异五:菜单颜色引用
```typescript
// 明色:使用 accent-foreground
'--g-header-menu-color': 'oklch(var(--accent-foreground))',
'--g-header-menu-hover-color': 'oklch(var(--accent-foreground))',
// 暗色:使用 muted-foreground
'--g-header-menu-color': 'oklch(var(--muted-foreground))',
'--g-header-menu-hover-color': 'oklch(var(--muted-foreground))',
```
---
## 完整暗色 `--g-*` 模板
```typescript
dark: {
// shadcn token ...
'--g-main-area-bg': 'oklch(var(--background))',
'--g-header-bg': 'oklch(var(--background))',
'--g-header-color': 'oklch(var(--foreground))',
'--g-header-menu-color': 'oklch(var(--muted-foreground))',
'--g-header-menu-hover-bg': 'oklch(var(--muted))',
'--g-header-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-header-menu-active-bg': 'oklch(var(--accent))',
'--g-header-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-main-sidebar-bg': 'oklch(var(--background))',
'--g-main-sidebar-menu-color': 'oklch(var(--muted-foreground))',
'--g-main-sidebar-menu-hover-bg': 'oklch(var(--muted))',
'--g-main-sidebar-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-main-sidebar-menu-active-bg': 'oklch(var(--accent))',
'--g-main-sidebar-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-sub-sidebar-bg': 'oklch(var(--background))',
'--g-sub-sidebar-menu-color': 'oklch(var(--muted-foreground))',
'--g-sub-sidebar-menu-hover-bg': 'oklch(var(--muted))',
'--g-sub-sidebar-menu-hover-color': 'oklch(var(--muted-foreground))',
'--g-sub-sidebar-menu-active-bg': 'oklch(var(--accent))',
'--g-sub-sidebar-menu-active-color': 'oklch(var(--accent-foreground))',
'--g-tabbar-bg': 'oklch(var(--background))',
'--g-tabbar-tab-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-hover-bg': 'oklch(var(--accent) / 50%)',
'--g-tabbar-tab-hover-color': 'oklch(var(--accent-foreground) / 50%)',
'--g-tabbar-tab-active-bg': 'oklch(var(--accent))',
'--g-tabbar-tab-active-color': 'oklch(var(--foreground))',
'--g-toolbar-bg': 'oklch(var(--background))',
},
```

18
.claude/skills/magic-script/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# Build and Release Folders
bin-debug/
bin-release/
[Oo]bj/
[Bb]in/
# Other files and folders
.settings/
# Executables
*.swf
*.air
*.ipa
*.apk
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
# should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Assassin-Q
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,115 @@
# Magic-Script 脚本开发助手
Magic-API 是一个基于 Java 的接口快速开发框架,支持通过 magic-script 脚本语言编写接口。magic-script 是一种基于 Mozilla Rhino 的脚本语言,语法类似 JavaScript专为接口开发优化。
> **说明**:本 Skill 同时兼容以下称呼方式:
> - magic-api / magicapi框架名称
> - magic-script / MagicScript / magicscript脚本语言名称
> 无论用户使用哪种称呼,都指代同一套技术栈。
## 任务目标
- 本 Skill 用于:为 magic-api/magicapi 框架提供 magic-script/MagicScript 脚本编写的完整指导
- 能力包含magic-script 语法参考、内置模块db、http 等)使用方法、接口开发示例和脚本故障排查
- 触发条件:用户需要在 magic-api/magicapi 中编写接口脚本、调试 magic-script/MagicScript 代码或解决脚本执行错误时使用
## 功能特性
- **语法参考**关键字、运算符、数据类型、Lambda 表达式、异步调用、类型转换
- **函数扩展**:字符串、数字、集合、日期、聚合函数、数学函数
- **数据库操作**:查询、增删改、单表操作、分页、事务
- **内置模块**HTTP、Lambda/LINQ、日志、环境配置、请求响应处理
- **接口示例**CRUD 操作、带事务的业务处理、HTTP 集成
- **故障排查**参数获取、SQL 注入、${} 与 #{} 区别、多数据源配置
## 使用方式
### 在 Coze 中使用
1. 解压 `magic-script.zip` 文件
2. 将 `magic-script` 文件夹放入 skills 目录
3. 在技能管理中选择该技能使用
### 在 Claude Code 中使用
```bash
# 解压并复制到技能目录
unzip magic-script.zip
cp -r magic-script ~/.claude/skills/
```
### 在 OpenCode 中使用
```bash
# 解压并复制到技能目录
unzip magic-script.zip
cp -r magic-script ~/.config/opencode/skills/
```
### 目录结构
```
magic-script/
├── SKILL.md
├── README.md
└── references/
├── keywords.md # 关键字、运算符、数据类型
├── script-syntax.md # 脚本语法详解
├── lambda-async.md # Lambda表达式与异步调用
├── aggregation.md # 聚合函数
├── string-functions.md # 字符串函数
├── date-functions.md # 日期函数
├── array-functions.md # 数组创建函数
├── math-functions.md # 数学函数
├── other-functions.md # 其它函数
├── object-extensions.md # Object扩展方法
├── number-extensions.md # Number扩展方法
├── collection-extensions.md # 列表与Map扩展
├── date-extensions.md # 日期扩展
├── class-extensions.md # Class扩展方法
├── pattern-extensions.md # Pattern扩展方法
├── db-query.md # 数据库查询
├── db-update.md # 数据库增删改
├── db-transaction.md # 事务操作
├── db-cache.md # 缓存操作
├── single-table.md # 单表操作db.table链式API
├── sql-param.md # SQL参数#{}、${}、动态SQL
├── page.md # 分页查询
├── http-module.md # HTTP模块
├── request-module.md # Request模块
├── response-module.md # Response模块
├── log-module.md # 日志模块
├── env-module.md # 环境配置模块
├── magic-module.md # Magic模块
├── java-integration.md # 脚本调用Java
├── api-integration.md # Java调用接口
├── quick-start.md # 快速入门
├── quick-param.md # 请求参数获取
├── quick-crud.md # CRUD操作示例
├── linq.md # Lambda/LINQ操作示例
├── faq.md # 常见问题
└── validate.md # 参数校验
```
## 技术栈
- **框架**magic-api又称 magicapi
- **脚本语言**magic-script又称 MagicScript、magicscript
## 适用场景
- 需要在 magic-api 框架中编写接口脚本
- 调试 magic-script 脚本代码
- 解决脚本执行错误
## 注意事项
1. 本技能专注于编写 magic-script 脚本代码,不包含 magic-api 框架配置相关内容
2. magic-script 不是标准 JavaScript有语法限制如不支持 forEach 用 each 替代)
3. SQL 参数建议使用 `#{param}` 防注入,`${param}` 慎用

View File

@ -0,0 +1,154 @@
---
name: magic-script
description: magic-api 框架接口开发助手,提供 magic-script 脚本编写指导、DB/HTTP 模块使用及语法纠错;当用户需要在 magic-api 中编写接口脚本、调试脚本或排查脚本错误时使用
---
# Magic-Script 脚本开发助手
Magic-API 是一个基于 Java 的接口快速开发框架,支持通过 magic-script 脚本语言编写接口。magic-script 是一种基于 Mozilla Rhino 的脚本语言,语法类似 JavaScript专为接口开发优化。
> **说明**:本 Skill 同时兼容以下称呼方式:
> - magic-api / magicapi框架名称
> - magic-script / MagicScript / magicscript脚本语言名称
> 无论用户使用哪种称呼,都指代同一套技术栈。
## 任务目标
- 本 Skill 用于:为 magic-api/magicapi 框架提供 magic-script/MagicScript 脚本编写的完整指导
- 能力包含magic-script 语法参考、内置模块db、http 等)使用方法、接口开发示例和脚本故障排查
- 触发条件:用户需要在 magic-api/magicapi 中编写接口脚本、调试 magic-script/MagicScript 代码或解决脚本执行错误时使用
## 操作步骤
- 标准流程:确认场景 → 阅读 SKILL.md → 查阅 references/ 文档 → 编写脚本
- 可选分支:遇到脚本错误 → 查阅 faq 相关文档
## 使用示例
**示例1快速查询接口**
```javascript
// GET /api/user/list 分页查询
return db.table('sys_user')
.column('id', 'userId')
.column('name', 'userName')
.where()
.eq('status', 1)
.page(page, size);
```
**示例2带参数的增删改查**
```javascript
// POST /api/user/save 新增用户
assert not_blank(body.name) : 400, '用户名不能为空';
var userId = db.table('sys_user')
.column('name', body.name)
.column('create_time', now())
.insert();
return {code: 200, data: {id: userId}};
```
**示例3调用外部接口**
```javascript
import http;
var result = http.connect('https://api.example.com/data')
.param({key: 'value'})
.header('Authorization', 'Bearer xxx')
.post()
.getBody();
return result;
```
## 资源索引
### 语法类
| 文件 | 说明 |
|------|------|
| `references/keywords.md` | 关键字、运算符、数据类型 |
| `references/script-syntax.md` | 脚本语法详解 |
| `references/lambda-async.md` | Lambda表达式与异步调用 |
### 函数扩展
| 文件 | 说明 |
|------|------|
| `references/aggregation.md` | 聚合函数count/sum/max/min/avg/group_concat |
| `references/string-functions.md` | 字符串函数uuid/is_blank/not_blank |
| `references/date-functions.md` | 日期函数date_format/now/current_timestamp_millis/current_timestamp |
| `references/number-extensions.md` | Number扩展与数学函数 |
| `references/collection-extensions.md` | 列表与Map扩展 |
| `references/date-extensions.md` | 日期扩展 |
| `references/array-functions.md` | 数组创建函数 |
| `references/math-functions.md` | 数学函数round/floor/ceil/percent |
| `references/other-functions.md` | 其它函数print/println/ifnull/is_null/not_null |
| `references/object-extensions.md` | Object扩展方法 |
| `references/class-extensions.md` | Class扩展方法 |
| `references/pattern-extensions.md` | Pattern扩展方法 |
### 数据库
| 文件 | 说明 |
|------|------|
| `references/db-query.md` | 数据库查询select/selectOne/selectInt/selectValue |
| `references/db-update.md` | 数据库增删改insert/update/batchUpdate/call |
| `references/db-transaction.md` | 事务操作 |
| `references/db-cache.md` | 缓存操作cache/deleteCache |
| `references/single-table.md` | 单表操作db.table链式API |
| `references/sql-param.md` | SQL参数#{}、${}、动态SQL、Mybatis语法 |
| `references/page.md` | 分页查询 |
### 模块
| 文件 | 说明 |
|------|------|
| `references/http-module.md` | HTTP模块调用外部接口 |
| `references/request-module.md` | Request模块获取请求信息 |
| `references/response-module.md` | Response模块设置响应 |
| `references/log-module.md` | 日志模块 |
| `references/env-module.md` | 环境配置模块 |
| `references/magic-module.md` | Magic模块调用其他接口 |
### 集成
| 文件 | 说明 |
|------|------|
| `references/java-integration.md` | 脚本调用Java |
| `references/api-integration.md` | Java调用接口 |
### 示例
| 文件 | 说明 |
|------|------|
| `references/quick-start.md` | 快速入门(工程创建、三分钟写接口) |
| `references/quick-param.md` | 请求参数获取 |
| `references/quick-crud.md` | CRUD操作示例 |
| `references/linq.md` | Lambda/LINQ操作示例 |
### FAQ
| 文件 | 说明 |
|------|------|
| `references/faq.md` | 常见问题 |
| `references/validate.md` | 参数校验 |
## 注意事项
1. **语法限制**magic-script 不支持 ES6 默认参数语法 `excludeValue = null`,需用 `if (excludeValue === undefined)` 判断;不支持 `new Set()`,需用数组替代
2. **数组方法**:不支持传统 `for` 循环,需用 `for (value in list)``for (index, value in list)`**注意** Collection 扩展提供了 `reduce` 方法用于归约操作
3. **方法命名**:遍历集合用 `each` 而不是 `forEach`;获取长度用 `size()` 而不是 `length`判断Map键用 `containsKey()` 而不是 `hasOwnProperty()`
4. **SQL参数**`#{}` 防注入占位符用于普通参数,`${}` 字符串拼接用于动态表名/列名/排序字段(存在注入风险)
5. **类型转换**`::type` 语法用于类型转换,如 `'123'::int`,转换失败可指定默认值 `'abc'::int(0)`
6. **获取请求参数**
- URL参数直接使用变量名 `name`
- 表单参数:直接使用变量名 `name`
- Header参数`header.xxx`
- Body参数`body.xxx`
- Path参数`path.xxx` 或直接使用变量名
- Cookie参数`cookie.xxx`
- Session参数`session.xxx`
7. **循环拼接参数**`in (#{ids})` 语法会自动对集合参数展开
8. **多数据源**:使用 `db.slave.select(...)` 格式切换数据源
9. **SQL缓存**`db.cache("cacheName", ttl).select(...)` 使用缓存
10. **事务操作**
- 自动事务:`db.transaction(()=>{...})`
- 手动事务:`var tx = db.transaction(); tx.commit(); tx.rollback();`

View File

@ -0,0 +1,63 @@
# 聚合函数
## count
- 入参:`target`:`Object`
- 返回值:`int`
- 函数说明:计算集合大小
```javascript
var list = [1,2,3,4,5]
return count(list); // 5
```
## sum
- 入参:`target`:`Object`
- 返回值:`Number`
- 函数说明:对集合进行求和
```javascript
var list = [1,2,3,4,5]
return sum(list); // 15
```
## max
- 入参:`target`:`Object`
- 返回值:`Object`
- 函数说明:求集合最大值
```javascript
var list = [1,2,3,4,5]
return max(list); // 5
```
## min
- 入参:`target`:`Object`
- 返回值:`Object`
- 函数说明:求集合最小值
```javascript
var list = [1,2,3,4,5]
return min(list); // 1
```
## avg
- 入参:`target`:`Object`
- 返回值:`Object`
- 函数说明:求集合平均值
```javascript
var list = [1,2,3,4,5]
return avg(list); // 3
```
## group_concat
- 入参:`target`:`Object`
- 入参:`separator`:`String` 分隔符,可省略
- 返回值:`Object`
- 函数说明:将集合拼接起来
```javascript
var list = [1,2,3,4,5]
return group_concat(list); // "1,2,3,4,5"
// return group_concat(list,'|'); // "1|2|3|4|5"
```

View File

@ -0,0 +1,128 @@
# Java调用接口
## 调用接口
```java
@Autowired
MagicAPIService service;
Map<String, Object> params = new HashMap<>();
// 注入变量信息
params.put("id", 123);
// 内部调用接口不包含code以及message信息同时也不走拦截器。
Object value = service.execute("GET", "/hello", params);
// 内部调用接口包含code以及message信息同时也不走拦截器。
// Object value = service.call("GET", "/hello", params);
```
## 调用函数
```java
@Autowired
MagicAPIService service;
Map<String, Object> params = new HashMap<>();
// 注入变量信息
params.put("a", 1);
params.put("b", 1);
// 调用函数
Object value = service.invoke("/test/add", params);
```
## 保存资源
```java
@Autowired
MagicResourceService service;
// 保存分组信息
service.saveGroup(group);
// 保存接口ApiInfo、函数FunctionInfo、数据源DataSourceInfo
service.saveFile(apiInfo);
```
## 删除资源
```java
@Autowired
MagicResourceService service;
// 删除分组或文件
service.delete(id);
```
## 资源列表
```java
@Autowired
MagicResourceService service;
// 获取分组下的所有文件
service.listFiles(groupId);
// 获取接口api、函数function、数据源datasource列表
service.files(type);
// 获取接口api、函数function、数据源datasource树结构
service.tree(type);
// 获取全部资源的树结构
service.tree();
```
## 其它API
除了以上列举的`API`以外 `MagicAPIService`还有:
```java
/**
* 上传
*/
boolean upload(InputStream inputStream, String mode) throws IOException;
/**
* 下载
*/
void download(String groupId, List<SelectedResource> resources, OutputStream os) throws IOException;
/**
* 推送
*/
JsonBean<?> push(String target, String secretKey, String mode, List<SelectedResource> resources);
/**
* 处理刷新通知
*/
boolean processNotify(MagicNotify magicNotify);
```
`MagicResourceService` 还有以下方法:
```java
/**
* 刷新缓存
*/
void refresh();
/**
* 移动
* @param src 源ID
* @param groupId 目标分组
*/
boolean move(String src, String groupId);
/**
* 复制分组
* @param src 源ID
* @param target 目标分组
*/
String copyGroup(String src, String target);
/**
* 获取文件详情
*/
<T extends MagicEntity> T file(String id);
/**
* 获取分组详情
*/
Group getGroup(String id);
/**
* 获取完整分组路径
*/
String getGroupPath(String groupId);
/**
* 获取完整分组名称
*/
String getGroupName(String groupId);
```

View File

@ -0,0 +1,75 @@
# 数组函数
## new_int_array
- 入参:`size``int` 数组长度
- 函数说明,创建`int`类型的数组
```javascript
return new_int_array(1); // [0]
```
## new_long_array
- 入参:`size``int` 数组长度
- 函数说明,创建`long`类型的数组
```javascript
return new_long_array(1); // [0]
```
## new_double_array
- 入参:`size``int` 数组长度
- 函数说明,创建`double`类型的数组
```javascript
return new_double_array(1); // [0.0]
```
## new_float_array
- 入参:`size``int` 数组长度
- 函数说明,创建`float`类型的数组
```javascript
return new_float_array(1); // [0.0]
```
## new_short_array
- 入参:`size``int` 数组长度
- 函数说明,创建`short`类型的数组
```javascript
return new_short_array(1); // [0]
```
## new_byte_array
- 入参:`size``int` 数组长度
- 函数说明,创建`byte`类型的数组
```javascript
return new_byte_array(1); // [0]
```
## new_boolean_array
- 入参:`size``int` 数组长度
- 函数说明,创建`boolean`类型的数组
```javascript
return new_boolean_array(1); // [false]
```
## new_char_array
- 入参:`size``int` 数组长度
- 函数说明,创建`char`类型的数组
```javascript
return new_char_array(1); // ['\0']
```
## new_array
- 入参:`Class`:类型
- 入参:`size``int` 数组长度
- 函数说明,创建`Object`类型的数组
```javascript
return new_array(1); // [null] // Object 类型的数组
return new_array(String.class, 1); // [null] String类型的数组
```

View File

@ -0,0 +1,25 @@
# Class扩展方法
## newInstance
- 入参:`values`:`Object` 可变参数,构造函数的参数
- 返回值:`Object`
- 函数说明:将`Class`实例化
```javascript
import 'java.text.SimpleDateFormat' as SimpleDateFormat;
return SimpleDateFormat.newInstance('yyyy-MM-dd HH:mm:ss');
//其实可以简写成 new SimpleDateFormat('yyyy-MM-dd HH:mm:ss'); //这是一个语法糖
```
## 获取类名称
支持以下方法
- `getName` - 获取完整类名
- `getSimpleName` - 获取简单类名
- `getCanonicalName` - 获取规范类名
```javascript
import 'java.text.SimpleDateFormat' as SimpleDateFormat;
println(SimpleDateFormat.getName()); // java.text.SimpleDateFormat
println(SimpleDateFormat.getSimpleName()); // SimpleDateFormat
println(SimpleDateFormat.getCanonicalName()); // java.text.SimpleDateFormat
```

View File

@ -0,0 +1,304 @@
# 数组&集合扩展方法
为`Collection`,`Iterator`,`Enumeration`,`Object[]` 添加的扩展方法
## map
- 入参:`function`:`Function` 接收一个`Lambda`表达式
- 返回值:`Object`
- 函数说明:将集合进行循环转换
```javascript
var list = [1,2,3,4,5];
return list.map(e=>e+1); //返回[2,3,4,5,6]
```
## filter
- 入参:`function`:`Function` 接收一个`Lambda`表达式
- 返回值:`Object`
- 函数说明:将集合进行过滤
```javascript
var list = [1,2,3,4,5];
return list.filter(e=>e>3); //返回[4,5]
return list.filter((item,index)=>index>1); //返回[3,4,5]
```
## each
- 入参:`function`:`Function` 接收一个`Lambda`表达式
- 返回值:`Object`
- 函数说明:循环处理
```javascript
var list = [{name : '小明'},{name : '小花'}];
return list.each(item=>item.put('age',18)); //循环添加age属性
```
## sort
- 入参:`function`:`Function` 接收一个`Lambda`表达式
- 返回值:`Object`
- 函数说明:对集合进行排序
```javascript
var list = [1,5,2,3,6];
return list.sort((a,b)=>a-b);
```
## first
- 返回值:`Object`
- 函数说明:返回集合的第一项,集合为空时返回`null`
```javascript
var list = [1,2,3,4,5]
return list.first(); // 1
```
## last
- 返回值:`Object`
- 函数说明:返回集合的最后一项,集合为空时返回`null`
```javascript
var list = [1,2,3,4,5]
return list.last(); // 5
```
## reserve
- 返回值:`Object`
- 函数说明:对集合进行反转操作
```javascript
var list = [1,5,2,3,6];
return list.reserve();
```
## join(拼接)
- 入参:`separator` : `String` 分隔符
- 返回值:`String`
- 函数说明:对集合进行拼接操作
```javascript
var list = [1,5,2,3,6];
return list.join('-'); // 1-5-2-3-6
```
## shuffle
- 返回值:`Object`
- 函数说明:对集合进行打乱处理
```javascript
var list = [1,5,2,3,6];
return list.shuffle();
```
## max
- 返回值:`Object`
- 函数说明取出集合最大值如果找不到返回null
```javascript
var list = [1,6,8,9,18,12];
return list.max(); // 18
```
## min
- 返回值:`Object`
- 函数说明取出集合最小值如果找不到返回null
```javascript
var list = [6,1,8,9,18,12];
return list.min(); // 1
```
## sum
- 返回值:`Object`
- 函数说明累加求和计算不出返回0.0
```javascript
var list = [1,2,3,4];
return list.sum(); // 10
```
## avg
- 返回值:`Object`
- 函数说明计算平均值计算不出返回null
```javascript
var list = [1,2,3,4];
return list.avg(); // 2.5
```
## group
- 入参:`condition` : `Function` 分组条件
- 入参:`mapping` : `Function` 结果映射(省略时不做映射返回List)
- 返回值:`Map<Object, List<Object>>`或`Map<Object, Object>`
- 函数说明:分组
```javascript
var result = [
{ xxx : 1, yyy : 2, value : 11},
{ xxx : 1, yyy : 2, value : 22},
{ xxx : 2, yyy : 2, value : 33}
];
return result.group(item=>item.xxx + '_' + item.yyy)
// 结果:{"1_2": [{...}, {...}], "2_2": [{...}]}
```
## join(关联)
- 入参:`target` : `Object` 关联的集合
- 入参:`condition` : `Function` 关联条件
- 入参:`mapping` : `Function` 结果映射
- 返回值:`List<Object>`
- 函数说明:将两个集合关联起来
```javascript
var year2019 = [
{ "pt":2019, "item_code":"code_1", "sum_price":2234 },
{ "pt":2019, "item_code":"code_2", "sum_price":234 }
];
var year2018 = [
{ "pt":2018, "item_code":"code_1", "sum_price":1234.0 }
];
return year2019.join(year2018, (left, right) => left.item_code == right.item_code, (left, right) => {
'年份' : left.pt,
'编号' : left.item_code,
'今年' : left.sum_price,
'去年' : right == null ? 'unknow' : right.sum_price
});
```
## asBean(转为Java对象)
- 入参:`target` : `Class<?>` 目标类型
- 返回值:`List<?>`
- 函数说明:将`List<Object>` 转为目标`List`
```javascript
import 'org.ssssssss.script.functions.User' as User;
var userList = [{
age : 18,
weight : 121,
money : 123456789L,
name : '法外狂徒'
}]
return userList.asBean(User.class);
```
## every
- 入参:`condition` : `Function` 判断条件
- 返回值:`boolean`
- 函数说明:判断集合是否都符合条件
```javascript
var vals = [1, 2, 3, 4, 5, 6, 7];
return vals.every(e => e > 0); // true
```
## some
- 入参:`condition` : `Function` 判断条件
- 返回值:`boolean`
- 函数说明:判断集合是否有符合条件的
```javascript
var vals = [1, 2, 3, 4, 5, 6, 7];
return vals.some(e => e == 0); // false
```
## reduce
- 入参:`function` : `Function` 计算函数
- 返回值:`Object`
- 函数说明:循环集合通过给定的计算函数返回一个新值
```javascript
var vals = [1, 2, 3];
return vals.reduce((sum, val) => sum + val); // 6
```
## find
- 入参:`function``Function` 查找函数
- 返回值:`Object`
- 函数说明:循环集合查找符合条件的对象
```javascript
var list = [{name: 'A'}, {name:'B'}]
return list.find(it => it.name == 'A'); // {name: 'A'}
```
## findIndex
- 入参:`function``Function` 查找函数
- 返回值:`Object`
- 函数说明:循环集合查找符合条件的对象位置
```javascript
var list = [{name: 'A'}, {name: 'B'}]
return list.findIndex(it => it.name == 'A'); // 0
```
## concat
- 入参:`Object`,要连接的集合对象,可写多个
- 返回值:`Object`
- 函数说明:拼接一个或多个集合,返回新的集合
```javascript
var list = [1];
return [1].concat([2]); // [1,2] list不变
return [1].concat([2],[3, 4]); // [1, 2, 3, 4] list不变
```
## toMap
- 入参:`mappingKey``Function`key映射方法
- 入参:`mappingValue``Function`value映射方法可省略默认为本身
```javascript
var list = [
{id : 1, name: 'A'},
{id : 2, name: 'B'},
{id : 3, name: 'C'},
]
return list.toMap(k => k.id, v => v.name) // {1: 'A', 2: 'B', 3: 'C'}
```
## skip
- 入参:`value` : `int` 跳过的数量
- 返回值:`Object`
- 函数说明:跳过指定个数截取集合
```javascript
var vals = [1, 2, 3, 4];
return vals.skip(2); // [3, 4]
```
## limit
- 入参:`value` : `int` 限制的数量
- 返回值:`Object`
- 函数说明:取指定个数的集合
```javascript
var vals = [1, 2, 3, 4];
return vals.limit(3); // [1, 2, 3]
```
## findNotNull
- 返回值:`Object`
- 函数说明:找到第一个不为`null`的值
```javascript
var vals = [null, null, 3, null];
return vals.findNotNull(); // 3
```
## distinct
- 返回值:`Object`
- 函数说明:去掉重复元素
```javascript
var arr = [1, 2, 2, 3];
return arr.distinct(); // [1, 2, 3]
```
## distinct(func)
- 入参: 映射函数, 形如`e -> e.id`
- 返回值:`Object`
- 函数说明:根据函数返回值去重,去掉重复元素
```javascript
var arr = [{id: 1, name: "xiaodong"}, {id:1, name: "magic-api"}];
return arr.distinct(e => e.id); // [{id: 1, name: "xiaodong"}]
```

View File

@ -0,0 +1,11 @@
# Date扩展方法
## format
- 入参:`pattern`:`String` 格式
- 返回值:`String`
- 函数说明:将日期格式化
```javascript
var date = new Date();
return date.format('yyyy-MM-dd'); // 2020-01-01
```

View File

@ -0,0 +1,36 @@
# 日期函数
## date_format
- 入参:`target`:`Date` 日期
- 入参:`pattern`:`String` 格式
- 返回值:`String`
- 函数说明:日期格式化
```javascript
return date_format(new Date()); // 2020-01-01 20:30:30
// return date_format(new Date(),'yyyy-MM-dd'); // 2020-01-01
```
## now
- 返回值:`Date`
- 函数说明:返回当前日期
```javascript
return now(); // 等同于 new Date();
```
## current_timestamp_millis
- 返回值:`long`
- 函数说明:取当前时间戳(毫秒)
```javascript
return current_timestamp_millis(); // 等同于 System.currentTimeMillis();
```
## current_timestamp
- 返回值:`long`
- 函数说明:取当前时间戳(秒)
```javascript
return current_timestamp(); // 等同于 current_timestamp_millis() / 1000;
```

View File

@ -0,0 +1,22 @@
# 缓存操作
## cache
- 入参:`cacheName`:`String`
- 入参:`ttl`:`long` 缓存有效期,单位毫秒,可省略,默认为配置的值
- 返回值:`db` //返回当前实例,即可以链式调用
- 函数说明:使用缓存
```javascript
// 使用缓存名为user的查询
return db.cache('user').select('select * from sys_user');
```
## deleteCache
- 入参:`cacheName`:`String`
- 返回值:`db` //返回当前实例,即可以链式调用
- 函数说明:删除名为`cacheName`的缓存
```javascript
// 删除名为user的缓存
db.deleteCache('user');
```

View File

@ -0,0 +1,63 @@
# 数据库查询
db模块是默认引入的模块无需import。
## select
- 入参:`sql`:`String`
- 返回值:`List<Map<String,Object>>`
- 函数说明:查询`List`结果
```javascript
return db.select('select * from sys_user');
```
## selectInt
- 入参:`sql`:`String`
- 返回值:`Integer`
- 函数说明:查询`int`结果
```javascript
// 需要保证结果返回一行一列
return db.selectInt('select count(*) from sys_user');
```
## selectOne
- 入参:`sql`:`String`
- 返回值:`Map<String,Object>`
- 函数说明:查询单个对象
```javascript
return db.selectOne('select * from sys_user limit 1');
```
## selectValue
- 入参:`sql`:`String`
- 返回值:`Object`
- 函数说明:查询单个值
```javascript
//需要保证结果返回一行一列
return db.selectValue('select user_name from sys_user limit 1');
```
## page
- 入参:`sql`:`String`
- 入参:`limit` : `long` 可省略
- 入参:`offset` : `long` 可省略
- 返回值:`Object` 默认返回为Object如果自定义了分页结果则返回自定义结果
- 函数说明:分页查询
```javascript
return db.page('select * from sys_user');
```
## 列名转换
- normal 列名保持原样
- camel 列名使用驼峰命名
- pascal 列名使用帕斯卡命名
- upper 列名保持全大写
- lower 列名保持全小写
```javascript
return db.camel().select('select * from sys_user');
```

View File

@ -0,0 +1,23 @@
# 事务操作
## 自动事务
```javascript
var val = db.transaction(()=>{
var v1 = db.update('...');
var v2 = db.update('....');
return v2;
});
return val;
```
## 手动事务
```javascript
var tx = db.transaction(); //开启事务
try{
var value = db.update('...');
tx.commit(); // 提交事务
return value;
}catch(e){
tx.rollback(); // 回滚事务
}
```

View File

@ -0,0 +1,49 @@
# 数据库增删改
## update
- 入参:`sql`:`String`
- 返回值:`Integer`
- 函数说明:执行增删改操作
```javascript
return db.update('delete from sys_user');
```
## insert
- 入参:`sql``String`
- 入参:`id``String`,主键列,可空,如无特殊情况不需要传入
- 返回值: `Object`
```javascript
return db.insert("insert into sys_user(username,password) values('admin','admin)");
```
## call
- 入参:`sql`: `String`
- 返回值:`Map<String,Object>`
- 函数说明:调用存储过程
```javascript
// 入参格式: #{参数名}
// 出参格式: @{参数名, java.sql.Types的类型字符串}
// 出入参格式:@{参数名(值、变量、表达式), java.sql.Types的类型字符串}
var cs1 = body.cs1;
var cs2 = body.cs2;
return db.call("""
call test(#{cs1}, @{height(cs2), INTEGER}, @{v_area, VARCHAR})
""")
// 返回:{height: 10, v_area: "16.85"}
```
## batchUpdate
- 入参:`sql``String`
- 入参:`batchArgs``List<Object[]>`数据,占位符和数组下标对应
- 返回值: `int`
```javascript
return db.batchUpdate("""
update sys_dict set is_del = ? where is_del = ?
""", [
["1", "0"].toArray()
])
```

View File

@ -0,0 +1,18 @@
# 环境配置模块
## 引用模块
```javascript
import env;
```
## 使用
```javascript
import env;
return env.get('server.port')
```
## get
- 入参:`key`:`String` 配置项
- 入参:`defaultValue`:`String` 默认值,可省略
- 返回值:`String`
- 函数说明:获取`Spring`配置项

View File

@ -0,0 +1,177 @@
# 常见问题
## 如何配置JSON日期的格式
使用`Jackson`的配置如下(`Spring Boot`默认使用`Jackson`)
```yaml
spring:
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
```
## 出现找不到db模块的错误
目前已知两种情况:
- 未配置数据源
- 未引用`spring-boot-starter-jdbc`
## 如何获取RequestBody中的参数
脚本中使用`body.xxx`获取`RequestBody`中的参数
SQL中使用`#{body.xxx}`或`${body.xxx}`获取`RequestBody`中的参数
## 如何获取Header中的参数
脚本中使用`header.xxx`获取`Header`中的参数
SQL中使用`#{header.xxx}`或`${header.xxx}`获取`Header`中的参数
## 如何获取Cookie中的参数
脚本使用`cookie.xxx`获取`Cookie`中的参数
SQL中使用`#{cookie.xxx}`或`${cookie.xxx}`获取`Cookie`中的参数
## 如何获取Session中的参数
脚本中使用`session.xxx`获取`Session`中的参数
SQL中使用`#{session.xxx}`获取`Session`中的参数
## 如何获取PathVariable中的参数
脚本中使用`PathVariableName`或`path.xxxx`获取`PathVariable`中的参数
SQL中使用`#{PathVariableName}`或`#{path.xxx}`获取`PathVariable`中的参数
## 如何获取上传的文件
利用Request模块
```javascript
import request;
request.getFile('name');
```
## 如何获取提交的数组参数
利用Request模块
```javascript
import request;
return request.getValues('name');
```
## 如何给接口添加权限
一般情况采用`拦截器`实现。在`接口选项`中配置`permisson`或`role`或自定义选项,随后在拦截器中实现:
```java
@Component
@Order(1)
public class PermissionInterceptor implements RequestInterceptor {
@Override
public Object preHandle(ApiInfo info, MagicScriptContext context, MagicHttpServletRequest request, MagicHttpServletResponse response) {
String permissionCode = info.getOptionValue(Options.PERMISSION);
// 执行自己的代码逻辑判断是否有权限
if(无权限){
return new JsonBean<>(403,"无权访问");
}
return null;
}
}
```
## 如何给UI添加权限
请参考[自定义UI鉴权](https://www.ssssssss.org/magic-api/pages/security/operation/)
## ${}和#{}的区别
主要区别在于`${}`用于拼接SQL(会产生SQL注入问题)`#{}`会替换成占位符不会产生SQL注入问题这里的区别与`Mybatis`一致
## 如何循环拼接参数
两种办法:
- `in (#{ids})`的语法会自动对集合参数展开
```javascript
var ids = [1,2,3,4,5,6];
return db.select('select * from sys_user where id in(#{ids})');
//会自动变成select * from sys_user where id in(?,?,?,?,?,?)
```
- 循环拼接SQL
```javascript
var list = [1,2,3,4,5];
var sql = "select * from sys_user where ";
for(index,item in list){
sql = sql + 'id = #{list['+index+']}';
if(index + 1 < list.size()){
sql = sql + ' or ';
}
}
return db.select(sql);
```
## 多数据源如何配置
编写java代码如下
```java
@Bean
public MagicDynamicDataSource magicDynamicDataSource(){
MagicDynamicDataSource dynamicDataSource = new MagicDynamicDataSource();
dynamicDataSource.setDefault(ds1);
dynamicDataSource.add("slave",ds2);
return dynamicDataSource;
}
```
脚本中使用:
```javascript
db.select('select * from sys_user'); //使用默认数据源
db.slave.select('select * from sys_user'); //使用slave数据源
```
## SQL执行报错java.sql.SQLFeatureNotSupportedException: null
原因druid版本过低升级至最新版后即可。
## 如何自定义返回结果
- 通过配置文件进行配置,具体参考[spring-boot配置](https://www.ssssssss.org/magic-api/pages/config/spring-boot/)
- 通过`自定义JSON结果`,具体定义方法查看[自定义JSON结果](https://www.ssssssss.org/magic-api/pages/base/response/)
- 通过`自定义拦截器`拦截返回自己想要的格式,具体定义方法查看[自定义拦截器](https://www.ssssssss.org/magic-api/pages/senior/interceptor/)
- 通过`spring`的拦截器返回想要的格式,如`ResponseBodyAdvice``HandlerMethodReturnValueHandler`这种方式目前会影响到UI,故不推荐使用)
## 页面加载缓慢
由于`monaco-editor`编辑器比较大,建议开启压缩静态资源
```yaml
server.compression.enabled=true #启用压缩
server.compression.min-response-size=256 #大于256kb时压缩
```
## 脚本内容被转义
出现这种情况,请检查自身项目是否有`XSS`一类的过滤器,需要把`UI`界面对应的后台接口排除掉即可。
## 执行测试无响应
目前已知有两种情况:
- 使用了Spring Boot 2.3.5版本升级至2.3.6解决
- 使用了`nginx`代理,加一条配置`proxy_buffering off;`解决
## 访问UI404
- 请检查访问路径是否正确
- 请检查`magic-editor`包是否被引入
- 如果是拉源码运行,则需要编译一下前端
- 如果以上确定没问题,请检查应用中是否有关于`mvc`的配置,如果有请检查是否是`extends WebMvcConfigurationSupport`的形式,是的话,改成`implements WebMvcConfigurer`的形式
- 如以上问题均不存在,请提[ISSUE](https://gitee.com/ssssssss-team/magic-api/issues) 或加群700818216反馈
## 无法DEBUG或无法查看日志
由于`DEBUG`和日志是依赖于`WebSocket`实现的,所以需要`WebSocket`支持。
- 请检查`Web`容器是否支持`WebSocket`,如果不支持需要引入对应依赖或更换支持`WebSocket`的`Web`容器
- 请检查是否使用了`nginx`之类的代理,如果使用了,需要对配置其支持`WebSocket`,样例如下:
```nginx
location /magic/web/console {
proxy_pass http://localhost:9999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 900s;
}
```
## 保存图片(Blob)数据到数据库
假设将图片的二进制数据传输到body.img中, sql可以这么写
```javascript
var sql = """
insert into img_table(img)
values(#{img::sql('blob')})
""";
```

View File

@ -0,0 +1,97 @@
# HTTP模块
## 模块说明
`http`模块是基于`RestTemplate`封装而来,目前只做了少量的封装。对于一些通用的配置可以使用自定义`RestTemplate`来实现
```java
@Bean
public HttpModule magicHttpModule() {
RestTemplate template = new RestTemplate();
// 对RestTemplate进行配置.
return new HttpModule(template);
}
```
## 引用模块
```javascript
import http;
```
## connect
- 入参:`url`:`string`
- 返回值:`HttpModule`
- 函数说明创建新的http请求对象
```javascript
import http;
http.connect("http://localhost:9999/sql/select")
```
## 设置URL参数、表单参数、Header
```javascript
import http;
http.param('url_param1','url_param_value1') // 设置URL参数
.param({ // 批量设置URL参数
url_param_2 : 2,
url_param_3 : 3,
})
.data('form_param1','form_param_value1') // 设置表单参数
.data({ // 批量设置表单参数
form_param_2 : 2,
form_param_3 : 3,
})
.header('header_param1','header_param_value1') // 设置header参数
.header({ // 批量设置header参数
header_param_2 : 2,
header_param_3 : 3,
})
```
## body
- 入参:`body`:`Object`
- 函数说明设置请求Body
```javascript
import http;
http.connect('..').body({
id: 1,
name: 'magic-api'
});
```
## entity
- 入参: `entity`: `HttpEntity`
- 函数说明:自定义`HttpEntity`
```javascript
import http;
http.connect('..').entity(entity)
```
## contentType
- 入参: `contentType`: `String`或`MediaType`
- 函数说明:定义请求内容类型
```javascript
import http;
http.connect('..').contentType('application/json')
```
## 请求方法
- `post()` - POST请求
- `get()` - GET请求
- `delete()` - DELETE请求
- `put()` - PUT请求
- `head()` - HEAD请求
- `patch()` - PATCH请求
- `options()` - OPTIONS请求
- `trace()` - TRACE请求
## execute
- 返回值:`ResponseEntity`
- 函数说明:执行对应的请求
```javascript
import http;
return http.connect('http://localhost:9999/sql/select').post().getBody()
```

View File

@ -0,0 +1,42 @@
# 脚本调用Java
## 注入Spring Bean
```javascript
// 第一种方式
import xx.xxx.xxx.xxx.UserService; // 使用类名
return UserService.selectUserList();
// 第二种方式
import "userUservice" as userService; // 使用Bean名
return userService.selectUserList();
```
## 调用静态方法
```javascript
import xxx.xxx.xx.xx.xx.StringUtils;
return StringUtils.isBlank("");
```
## 调用普通方法
```javascript
// 对于java.util、java.lang 包下的类,可以直接使用。
return new ArrayList();
// 对于其他类需要import
import "java.text.SimpleDateFormat";
return new SimpleDateFormat("yyyy-MM-dd").format(new Date());
```
## 调用magic-api的接口
```javascript
// 可以在脚本中直接调用非http方式
import "@get:/api/sys/user/list" as userList; // 导入定义的GET请求的 /api/sys/user/list 接口。
// 脚本中变量是共享给调用者的。所以无需指定参数传入。只需要在本脚本中定义该变量即可。
return userList();
```
## 调用magic-api的函数
```javascript
import "@/common/encode/md5" as md5; // 导入页面上定义的函数信息
return md5('123456');
```

View File

@ -0,0 +1,108 @@
# 关键字、运算符、数据类型
## 关键字
| 关键字 | 含义 |
|--------|------|
| var | 定义变量 |
| if | 条件语句的引导词 |
| else | 用在条件语句中,表明当条件不成立时的分支 |
| for | for循环语句 |
| in | 与for配合使用 |
| while | while循环语句 |
| continue | 执行下一次循环 |
| break | 跳出循环 |
| return | 终止当前过程的执行并正常退出到上一个执行过程中 |
| exit | 终止当前脚本,并退出返回,如`exit 200,'执行成功',[1,2,3];` |
| assert | 断言 |
| instanceof | 判断一个对象是否为一个类的实例 |
| try | 用于捕获可能发生异常的代码块 |
| catch | 与try关键字配合使用当发生异常时执行 |
| finally | 与try关键字配合使用finally块无论发生异常都会执行 |
| import | 导入Java类或导入已定义好的模块 |
| as | 与 import 关键字配合使用,用作将导入的 Java类或模块 命名为一个本地变量名 |
| new | 创建对象 |
| true | 基础类型之一,表示 Boolean 的:真值 |
| false | 基础类型之一,表示 Boolean 的:假值 |
| null | 基础类型之一,表示 NULL 值 |
| async | 异步调用 |
## 运算符
### 数学运算
| 运算符 | 说明 |
|--------|------|
| + | 加法 |
| - | 减法 |
| * | 乘法 |
| / | 除法 |
| % | 取模 |
| ++ | 自增 |
| -- | 自减 |
| += | 加等于 |
| -= | 减等于 |
| *= | 乘等于 |
| /= | 除等于 |
| %= | 取模等于 |
### 比较运算符
| 运算符 | 说明 |
|--------|------|
| < | 小于 |
| <= | 小于等于 |
| > | 大于 |
| >= | 大于等于 |
| == | 等于 |
| != | 不等于 |
| === | 等于 |
| !== | 不等于 |
### 逻辑运算符
| 运算符 | 说明 |
|--------|------|
| && | 并且 |
| \|\| | 或者 |
| ! | 取反 |
### 位运算符
| 运算符 | 说明 |
|--------|------|
| & | 与 |
| \| | 或 |
| ^ | 异或 |
| ~ | 取反 |
| << | 左移 |
| >> | 右移 |
| >>> | 无符号右移 |
## 数据类型
| 类型 | 写法 |
|------|------|
| byte | `123b`、`123B` |
| short | `123s`、`123S` |
| int | `123` |
| long | `123l`、`123L` |
| float | `123f`、`123F` |
| double | `123d`、`123D` |
| BigDecimal | `123m`、`123M` |
| boolean | `true`、`false` |
| string | `'hello'``"hello"` |
| string | `"""多行文本块,主要用于编写SQL"""` |
| Pattern | `/\d+/g`,`/pattern/gimuy` 用于定义正则 |
| lambda | `()=>expr`、`(param1,param2....)=>{...}` |
| list | `[1,2,3,4,5]` |
| map | `{key : value,key1 : value}``{[key] : "value"}` |
## 三元运算符
三元运算符是`if`语句的简写形式其工作方式类似于Java中例如`true ? "yes" : "no"`
增强的`if`和三元运算符,不再强制值必须是布尔类型,可以写`if(xxx)`的形式当`xxx`为以下情况时为`false`、其它情况为`true`
- `null`
- 空集合
- 空Map
- 空数组
- 数值==0
- 空字符串
- `false`

View File

@ -0,0 +1,102 @@
# Lambda表达式与异步调用
## Lambda表达式
### 映射(map)
```javascript
var list = [
{sex : 0,name : '小明',age : 19},
{sex : 1,name : '小花',age : 18}
];
var getAge = (age) => age > 18 ? '成人' : '未成年'
return list.map(item => {
age : getAge(item.age),
sex : item.sex == 0 ? '男' : '女',
name : item.name
});
// 结果:[{sex: "男", name: "小明", age: "成人"}, {sex: "女", name: "小花", age: "未成年"}]
```
### 过滤(filter)
```javascript
var list = [
{sex : 0,name : '小明'},
{sex : 1,name : '小花'}
]
return list.filter(item => item.sex == 0);
// 结果:[{sex: 0, name: "小明"}]
```
### 过滤+映射(filter + map)
```javascript
var list = [
{sex : 0,name : '小明'},
{sex : 1,name : '小花'}
]
return list.filter(item => item.sex == 0).map(item => {
sex : item.sex == 0 ? '男' : '女',
name : item.name
});
// 结果:[{sex: "男", name: "小明"}]
```
### 分组(group)
默认聚合为List
```javascript
var result = [
{ xxx : 1, yyy : 2, value : 11},
{ xxx : 1, yyy : 2, value : 22},
{ xxx : 2, yyy : 2, value : 33}
];
return result.group(item => item.xxx + '_' + item.yyy)
// 结果:{"1_2": [{...}, {...}], "2_2": [{...}]}
```
自定义聚合对象
```javascript
return result.group(item => item.xxx + '_' + item.yyy,list => {
count : list.size(),
sum : list.map(v=>v.value).sum(),
avg : list.map(v=>v.value).avg()
})
// 结果:{"1_2": {"avg": 16.5, "count": 2, "sum": 33}, "2_2": {"avg": 33, "count": 1, "sum": 33}}
```
### 关联(join)
```javascript
var year2019 = [
{ "pt":2019, "item_code":"code_1", "sum_price":2234 },
{ "pt":2019, "item_code":"code_2", "sum_price":234 }
];
var year2018 = [
{ "pt":2018, "item_code":"code_1", "sum_price":1234.0 }
];
return year2019.join(year2018, (left, right) => left.item_code == right.item_code, (left, right) => {
'年份' : left.pt,
'编号' : left.item_code,
'今年' : left.sum_price,
'去年' : right == null ? 'unknow' : right.sum_price
});
```
## 异步调用
### 普通方法
```javascript
// 使用async关键字会启动一个线程去执行返回Future
var user1 = async db.select("select * from sys_user where id = 1");
var user2 = async db.select("select * from sys_user where id = 2");
// 调用get方法表示阻塞等待获取结果
return [user1.get(),user2.get()];
```
### lambda
```javascript
var list = [];
for(index in range(1,10)){
// 当异步中使用外部变量时,为了确保线程安全的变量,可以将其放在形参中
list.add(async (index)=>db.select("select * from sys_user where id = #{index}"));
}
return list.map(item=>item.get());
```

View File

@ -0,0 +1,76 @@
# Linq
## 基本语法
```sql
select
tableAlias.*|[tableAlias.]field[ columnAlias]
[,tableAlias.field2[ columnAlias2][,…]]
from expr[,…] tableAlias
[[left ]join expr tableAlias2 on condition]
[where condition]
[group by tableAlias.field[,...]]
[having condition]
[order by tableAlias.field[asc|desc][,tableAlias.field[asc|desc]]]
[limit expr [offset expr]]
```
## 执行步骤
- 先从`from`子句创建虚拟表VT1
- 处理`join`创建虚拟表VT2筛选符合条件`condition`的行加入到虚拟表VT2中
- 处理`where` 将符合`condition`的行加入虚拟表VT1中
- 处理`group by` 对虚拟表VT1、VT2进行分组操作将符合`having condition`的值加入虚拟表VT3中
- 处理`select` 从VT3中选择指定的列加入虚拟表VT4中
- 处理`order by` 对虚拟表VT4进行排序
- 处理`limit`
## select子句
```sql
select t.name,sum(t.score) score,t.*
```
> select 中带有聚合函数的应该有group by语句否则不会进行聚合处理
## from子句
```sql
-- 以下三种方式均可(别名是必须的)
from [{name: 'Gitee'},[name:'Github']] t
from results t
from {name:'Gitee'} t
```
> from 跟着的必须是`List`或者`Map`
## join子句
```sql
-- 以下三种方式均可(别名是必须的)
[left] join [{name: 'Gitee'},[name:'Github']] t1 on t1.name = t.name
[left] join results t1 on 1 = 1
[left] join {name:'Gitee'} t1 on t1.name = 'Gitee' and 1=1
```
## where子句
```sql
-- or 等价于|| and 等价于 && 可以混合使用。
where t.name = 'Gitee' or t.name = 'Github' and 1=1 && 2=2
```
## group by子句
```sql
group by t.name, t1.xxx
```
## having 子句
```sql
having count(t.name) > 1
```
## order by子句
```sql
-- asc可以不写默认是asc
order by t.name desc,t.xxx
```
## limit 子句
```sql
limit 1 -- 固定取第一项返回值会是对象而非List
limit pageSize offset (page - 1) * pageSize
limit pageSize
```

View File

@ -0,0 +1,15 @@
# 日志模块
## 引用模块
```javascript
import log;
```
## 使用
```javascript
import log; //org.slf4j.Logger
// 使用方法与SLF4J完全一致
log.info('Hello');
log.info('Hello {}','MagicAPI');
log.debug('test');
```

View File

@ -0,0 +1,45 @@
# Magic模块
## 引用模块
```javascript
import magic;
```
## call
- 入参:`method`:`String` 定义的请求方法
- 入参:`path`:`String` 定义的路径
- 入参:`parameters`:`Map` 变量信息
- 返回值:`Object`
- 函数说明执行MagicAPI中的接口,返回值带code和message信息
```javascript
return magic.call('get','execute/sql',{
message : 'Hello,Magic API!' //传入参数
})
```
## execute
- 入参:`method`:`String` 定义的请求方法
- 入参:`path`:`String` 定义的请求路径
- 入参:`parameters`:`Map` 变量信息
- 返回值:`Object`
- 函数说明执行MagicAPI中的接口,返回原始内容不包含code以及message信息
```javascript
return magic.execute('get','execute/sql',{
message : 'Hello,Magic API!' //传入参数
})
```
## invoke
- 入参:`path`:`String` 函数路径
- 入参:`parameters`:`Map` 变量信息
- 返回值:`Object`
- 函数说明执行MagicAPI中的函数
```javascript
return magic.invoke('/test/add',{
a: 1,
b: 2
})
```

View File

@ -0,0 +1,39 @@
# 数学函数
## round
- 入参:`number`:`Number` 目标值
- 入参:`len`:`int` 要保留的小数位数 可省略默认0
- 返回值:`Number`
- 函数说明四舍五入保留N位小数
```javascript
return round(123.456d,2); //123.46
```
## floor
- 入参:`number`:`Number` 目标值
- 返回值:`Number`
- 函数说明:向下取整
```javascript
return floor(123.456d); // 123;
```
## ceil
- 入参:`number`:`Number` 目标值
- 返回值:`Number`
- 函数说明:向上取整
```javascript
return ceil(123.456d); // 124;
```
## percent
- 入参:`number`:`Number` 目标值
- 入参:`len`:`int` 要保留的小数 可省略默认0
- 返回值:`String`
- 函数说明:将数值转为百分比
```javascript
return percent(0.1289999999,2); // "12.90%"
```

View File

@ -0,0 +1,51 @@
# Number扩展方法
`java.lang.Number`的扩展方法,用于数值类型的扩展
## round
- 入参:`number`:`int` 要保留的小数
- 返回值:`Number`
- 函数说明四舍五入保留N位小数
```javascript
var value = 123.456d;
return value.round(2); //123.46
```
## toFixed
- 入参:`number`:`int` 要保留的小数
- 返回值:`String`
- 函数说明四舍五入保留N位小数(和JS一样强制限制位数)
```javascript
var value = 123.456d;
return value.toFixed(10); // "123.4560000000"
```
## floor
- 返回值:`Number`
- 函数说明:向下取整
```javascript
var value = 123.456d;
return value.floor(); // 123;
```
## ceil
- 返回值:`Number`
- 函数说明:向上取整
```javascript
var value = 123.456d;
return value.ceil(); // 124;
```
## asPercent
- 入参:`number`:`int` 要保留的小数
- 返回值:`String`
- 函数说明:将数值转为百分比
```javascript
var value = 0.1289999999;
return value.asPercent(2); // "12.90%"
```

View File

@ -0,0 +1,129 @@
# Object扩展方法
## asInt
- 入参:`defaultValue`:`int` 选填,当转换失败时返回默认值,默认为`0`
- 返回值:`int`
- 函数说明转对象为int类型
```javascript
var obj = '123';
return obj.asInt();
//return obj.asInt(1); //转换失败时返回1
```
## asDouble
- 入参:`defaultValue`:`double` 选填,当转换失败时返回默认值,默认为`0.0`
- 返回值:`double`
- 函数说明:转对象为`double`类型
```javascript
var obj = '123';
return obj.asDouble();
//return obj.asDouble(1.0d); //转换失败时返回1.0d
```
## asDecimal
- 入参:`defaultValue`:`BigDecimal` 选填,当转换失败时返回默认值,默认为`null`
- 返回值:`BigDecimal`
- 函数说明:转对象为`BigDecimal`类型
```javascript
var obj = '123.456';
return obj.asDecimal();
//return obj.asDecimal(1.5m); //转换失败时返回1.5m
```
## asFloat
- 入参:`defaultValue`:`float` 选填,当转换失败时返回默认值,默认为`0.0f`
- 返回值:`float`
- 函数说明:转对象为`float`类型
```javascript
var obj = '123';
return obj.asFloat();
//return obj.asFloat(1.0f); //转换失败时返回1.0f
```
## asLong
- 入参:`defaultValue`:`long` 选填,当转换失败时返回默认值,默认为`0L`
- 返回值:`long`
- 函数说明:转对象为`long`类型
```javascript
var obj = '123';
return obj.asLong();
//return obj.asLong(1L); //转换失败时返回1L
```
## asByte
- 入参:`defaultValue`:`byte` 选填,当转换失败时返回默认值,默认为`0b`
- 返回值:`byte`
- 函数说明:转对象为`byte`类型
```javascript
var obj = '123';
return obj.asByte();
//return obj.asByte(1b); //转换失败时返回1b
```
## asShort
- 入参:`defaultValue`:`short` 选填,当转换失败时返回默认值,默认为`0s`
- 返回值:`short`
- 函数说明:转对象为`short`类型
```javascript
var obj = '123';
return obj.asShort();
//return obj.asShort(1s); //转换失败时返回1s
```
## asDate
- 入参:`formats`:`String` 可变参数,日期格式
- 返回值:`Date`
- 函数说明:转对象为`Date`类型
```javascript
var obj = '2020-01-01 08:00:00';
return obj.asDate('yyyy-MM-dd HH:mm:ss','yyyy-MM-dd HH:mm');
```
## asString
- 入参:`defaultValue`:`String` 选填,当转换失败时返回默认值,默认为`null`
- 返回值:`String`
- 函数说明:转对象为`String`类型
```javascript
var obj = 123;
return obj.asString();
//return obj.asString("empty"); //转换失败时,返回"empty"
```
## is
- 入参:`type`:`String/Class` 判断是否该类型
- 返回值:`boolean`
- 函数说明:判断是否是指定类型
```javascript
import 'java.util.Date' as Date;
var str = 'hello,MagicAPI';
return str.is('string'); // true
return str.is('java.lang.String'); // true
return str.is('java.lang.Integer'); // false
return str.is(Date); // false
```
## 类型判断方法
- `isString()` - 判断是否是String类型
- `isInt()` - 判断是否是int类型
- `isLong()` - 判断是否是long类型
- `isDouble()` - 判断是否是double类型
- `isFloat()` - 判断是否是float类型
- `isByte()` - 判断是否是byte类型
- `isBoolean()` - 判断是否是boolean类型
- `isShort()` - 判断是否是short类型
- `isDecimal()` - 判断是否是decimal类型
- `isDate()` - 判断是否是Date类型
- `isArray()` - 判断是否是数组
- `isList()` - 判断是否是List
- `isMap()` - 判断是否是Map
- `isCollection()` - 判断是否是集合

View File

@ -0,0 +1,55 @@
# 其它函数
## print
- 入参:`target``Object` 要打印的对象
- 函数说明:打印
```javascript
print('abc'); // 等同于 System.out.print("abc");
```
## println
- 入参:`target``Object` 要打印的对象
- 函数说明:打印并换行
```javascript
println('abc'); // 等同于 System.out.println("abc");
```
## printf
- 入参:`format``String` 要打印的对象
- 入参:`target` `Object` 参数值,可以写多个
- 函数说明:按照格式打印并换行
```javascript
printf('%s:%s', 'a','b'); // 等同于 System.out.printf("%s:%S", "a", "b");
```
## not_null
- 入参:`target` : `Object` 判断的模板
- 返回值:`boolean`
- 函数说明:判断值不是`null`
```javascript
return not_null(target); // 等同于 target != null
```
## is_null
- 入参:`target` : `Object` 判断的模板
- 返回值:`boolean`
- 函数说明:判断值是`null`
```javascript
return is_null(target); // 等同于 target == null
```
## ifnull
- 入参:`target`:`Object` 判断的目标
- 入参:`trueValue`:`Object` 为空时的值
- 返回值:`Object`
- 函数说明:对空值进行判断,返回特定值
```javascript
return ifnull(null,1) // 1
// return ifnull(0,1) // 0
```

View File

@ -0,0 +1,32 @@
# 分页查询
## 自动分页
`db.page`可从形如`xxx?page=1&size=10`的url中获取分页参数。
```javascript
// 自动从请求参数中获取页码(默认为page)、页大小(默认为size)
return db.page("""
select * from sys_user
""")
```
## 手动分页
可手动传入分页参数。
```javascript
return db.page("""
select * from sys_user
""", 10, 20) // 跳过前20条查10条(limit, offset)
```
## 自定义分页参数
可根据需要在自己的项目中,调整以下分页参数。
```yaml
magic-api:
page:
size: size # 页大小的请求参数名称 缺省时为size
page: page # 页码的请求参数名称 缺省时为page
default-page: 1 # 自定义默认首页 缺省时为1
default-size: 10 # 自定义为默认页大小 缺省时为10
```

View File

@ -0,0 +1,13 @@
# Pattern扩展方法
`java.util.regex.Pattern`的扩展方法
## test
- 入参:`source`:`String` 目标字符串
- 返回值:`boolean`
- 函数说明:校验文本是否符合正则
```javascript
var regx = /^\d+$/;
return regx.test('123456') // true
```

View File

@ -0,0 +1,125 @@
# 增删改查
## SQL参数
### #{} 注入参数
作用和`mybatis`一致,都是将`#{}`区域替换为占位符`?`
```javascript
var id = 123;
return db.select("""
select * from sys_user where id = #{id}
""");
// 运行时生成的SQL为select * from sys_user where id = ?
```
### ${} 拼接参数
作用和`mybatis`一致,都是将`${}`区域替换为对应的字符串
```javascript
var id = 123;
return db.select("""
select * from sys_user where id = ${id}
""");
// 运行时生成的SQL为select * from sys_user where id = 123
```
## 动态SQL参数
通过`?{condition,expression}`来实现动态拼接`SQL`
```javascript
return db.select("select * from sys_user ?{id,where id = #{id}}");
// 当id有值时,生成SQLselect * from sys_user where id = ?
// 当id无值时,生成SQLselect * from sys_user
```
## 切换数据源
```javascript
// 从数据源key定义为slave的库中查询
return db.slave.select("""
select * from sys_user
""")
```
## SQL缓存
```javascript
// 将查询结果缓存到名为user_cache的缓存中有效期1小时
return db.cache("user_cache", 3600 * 1000).select("""
select * from sys_user
""")
```
## 使用事务
### 自动事务
```javascript
var val = db.transaction(()=>{
var v1 = db.update('...');
var v2 = db.update('....');
return v2;
});
return val;
```
### 手动事务
```javascript
var tx = db.transaction(); //开启事务
try{
var value = db.update('...');
tx.commit(); // 提交事务
return value;
}catch(e){
tx.rollback(); // 回滚事务
}
```
## Mybatis语法支持
### if
```javascript
var sql = """
select * from test_data
where 1 = 1
<if test="id != null">
and id = #{id}
</if>
"""
return db.select(sql)
```
### where
```javascript
var sql = """
select * from test_data
<where>
<if test="id != null">
and id = #{id}
</if>
</where>
"""
return db.select(sql)
```
### set、trim
```javascript
var sql = """
update test_data
<set>
<if test="name != null">
name = #{name}
</if>
</set>
where `id` = #{id}
"""
return db.update(sql)
```
### foreach
```javascript
var sql = """
select * from test_data
where id in
<foreach item='item' index='index' collection='body.ids'
open="(" separator="," close=")">
#{item}
</foreach>
"""
return db.select(sql)
```

View File

@ -0,0 +1,47 @@
# 请求参数获取
## RequestParam
```
GET http://localhost:9999/xxx/xxx?name=abc&age=49
```
这样的`URL`参数`magic-api` 会自动将`name`和`age`映射为同名变量。
## 表单参数
```
POST http://localhost:9999/xxx/xxx
name=abc&age=49
```
这样的表单参数`magic-api` 也会自动将`name`和`age`映射为同名变量。
## Request Header参数获取
`magic-api` 会对所有`RequestHeader`统一封装为一个名为`header`的变量
如要获取 `token` 可以通过`header.token` 来获取
## Request Body参数获取
对于`RequestBody` `magic-api`会将整个请求体映射为`body`变量,如:
```json
{
"name": "magic-api",
"version": "9.9.9"
}
```
如要获取`name`属性 则可通过 `body.name` 来获取
如果提交的body为数组或者List, `body`为数组。
## Path参数获取
主要是针对`URL`定义为`http://localhost:9999/user/{id}` 的类似接口
如要获取path路径上的id可通过`path.id` 或 `id`来获取。
对于请求时使用了`http://localhost:9999/user/1?id=2`的请求, `id`变量的值将是`RequestParam`中的值,此时可以通过`path.id` 来避免冲突。
## Cookie参数获取
`magic-api` 会对所有`Cookie`统一封装为一个名为`cookie`的对象。
如要获取 `JSESSIONID` 可以通过`cookie.JSESSIONID` 来获取。
## Session参数获取
`magic-api` 会将`HttpSession`封装为一个名为`session`的变量
要获取`session`中的值,可以通过`session.xxx`来获取
## 注意事项
如果脚本自定义变量和参数变量冲突,自定义变量优先。

View File

@ -0,0 +1,66 @@
# 快速入门
## 初始化工程
创建一个空的`Spring Boot`工程, 以`mysql`作为默认数据库进行演示。
## 添加依赖
引入`Spring Boot Starter`父工程:
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>spring-boot-latest-version</version>
<relativePath/>
</parent>
```
引入`magic-api-spring-boot-starter`依赖:
```xml
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-spring-boot-starter</artifactId>
<version>magic-api-lastest-version</version>
</dependency>
```
## 配置
`application.yml`
```yaml
server:
port: 9999
magic-api:
web: /magic/web
resource:
location: D:/data/magic-api
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/magic-api-test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: test
```
## 访问api管理界面
启动项目之后,访问`http://localhost:9999/magic/web` 即可看到Web页面
## 三分钟写出查询接口
**1. 创建分组**
点击创建分组按钮后,输入分组信息,点击创建。
**2. 新建接口**
右键分组,点击新建接口。在编辑器输入内容后,填写接口名称和及其路径。
```javascript
var sql = """
select * from test_data
"""
return db.select(sql)
```
`ctrl+s`保存后,即可访问接口。
**3.访问接口**
```
> curl http://localhost:9999/test/test
```

View File

@ -0,0 +1,64 @@
# Request模块
## 引用模块
```javascript
import request;
```
## getFile
- 入参:`name`:`string`
- 返回值:`MultipartFile`
- 函数说明:获取上传的文件
```javascript
import request;
request.getFile('image');
```
## getFiles
- 入参:`name`:`String`
- 返回值:`List<MultipartFile>`
- 函数说明:获取上传的文件集合
```javascript
import request;
request.getFiles('image');
```
## getValues
- 入参:`name`:`String`
- 返回值:`List<String>`
- 函数说明:获取提交的数组参数
```javascript
import request;
return request.getValues('name');
```
## getHeaders
- 入参:`name`:`String`
- 返回值:`List<String>`
- 函数说明获取请求的header数组
```javascript
import request;
return request.getHeaders('xxx');
```
## get
- 返回值:`MagicHttpServletRequest`
- 函数说明:获取`Request`对象
```javascript
import request;
request.get();
```
## getClientIP
- 返回值:`String`
- 函数说明:获取客户端`IP`
```javascript
import request;
return request.getClientIP();
```

View File

@ -0,0 +1,141 @@
# Response模块
## 引用模块
```javascript
import response;
```
## page
- 入参:`total`:`long`
- 入参:`values`:`list`
- 返回值:`Object`
- 函数说明:构建分页结果
```javascript
import response;
//返回: 共计10条第一页的5条数据
return response.page(10,[1,2,3,4,5]);
```
## json
- 入参:`value`:`Object`
- 返回值:`ResponseEntity`
- 函数说明构建Json结果
```javascript
import response;
//直接返回该json不会被包装处理
return response.json({
success : true,
message : '执行成功'
});
```
## text
- 入参:`value`:`String` 文本内容
- 返回值:`ResponseEntity`
- 函数说明:输出文本
```javascript
import response;
//直接返回该text不会被包装处理
return response.text('ok');
```
## redirect
- 入参:`url`:`String` 目标网址
- 返回值:`ResponseEntity`
- 函数说明:重定向
```javascript
import response;
//重定向到该地址内部利用HttpServletResponse的sendRedirect方法
return response.redirect('/xxx/xx');
```
## download
- 入参:`value`:`Object`
- 入参:`filename`:`文件名`
- 返回值:`ResponseEntity`
- 函数说明:下载文件
```javascript
import response;
return response.download('文件内容','test.txt');
```
## image
- 入参:`value`:`Object`
- 入参:`mine`:`String`
- 返回值:`ResponseEntity`
- 函数说明:主要用于输出图片
```javascript
import response;
// 输出图片
return response.image(bytes,'image/png');
```
## addHeader
- 入参:`key`:`string`
- 入参:`value`:`String`
- 返回值:无返回值
- 函数说明添加Response Header
```javascript
import response;
response.addHeader('AccessToken','123');
```
## setHeader
- 入参:`key`:`string`
- 入参:`value`:`String`
- 返回值:无返回值
- 函数说明设置Response Header
```javascript
import response;
response.setHeader('AccessToken','123');
```
## addCookie
- 入参:`key`:`string`
- 入参:`value`:`String`
- 入参:`options`:`Map` cookie参数可选
- 返回值:无返回值
- 函数说明添加Cookie
```javascript
import response;
response.addCookie('cookieKey','cookieValue');
response.addCookie('cookieKey','cookieValue',{
path : '/',
httpOnly : true,
domain : 'ssssssss.org',
maxAge : 3600
});
```
## addCookies
- 入参:`cookies`:`Map` cookie Map必填
- 入参:`options`:`Map` cookie参数可选
- 返回值:无返回值
- 函数说明批量添加Cookie
```javascript
import response;
response.addCookies({
cookieKey1 : 'cookieValue1',
cookieKey2 : 'cookieValue2',
});
```
## getOutputStream
- 返回值:`OutputStream`
- 函数说明:获取`ServletOutputStream`
注意:在调用`getOutputStream`后 返回值应为`response.end()` 告诉框架无需处理返回值。
## end
- 返回值:无返回值
- 函数说明取消返回默认的json结构通过其他方式的输出结果调用outputstream输出

View File

@ -0,0 +1,163 @@
# 脚本语法详解
## for循环
### 循环集合
```javascript
var list = [1,2,3];
for(index,item in list){ //如果不需要index也可以写成for(item in list)
println(index + ":" + item);
}
// 结果0:1, 1:2, 2:3
```
### 循环指定次数
```javascript
var sum = 0;
for(value in range(0,100)){ //包括0包括100
sum = sum + value; //不支持+= -= *= /= ++ -- 这种运算
}
return sum; // 5050
```
## while循环
```javascript
var count = 100;
var sum = 0;
while(count){
sum = sum + count;
count = count - 1;
}
return sum; // 5050
```
## 循环map
```javascript
var map = {
key1 : 123,
key2 : 456
};
for(key,value in map){ //如果不需要key也可以写成for(value in map)
println(key + ":" + value);
}
// 结果key1:123, key2:456
```
## Import导入
### 导入Java类
```javascript
import 'java.lang.System' as System;
import 'javax.sql.DataSource' as ds;
import 'org.apache.commons.lang3.StringUtils' as string;
import 'java.text.*' // 此写法跟Java一致
System.out.println('调用System打印');
System.out.println(ds);
System.out.println(string.isBlank(''));
System.out.println(new SimpleDateFormat('yyyy-MM-dd').format(new Date()));
```
### 导入已定义的模块
```javascript
import log; //导入log模块并定义一个与模块名相同的变量名
//import log as logger; //导入log模块并赋值给变量 logger
log.info('Hello {}','Magic API!')
```
## new创建对象
```javascript
import 'java.util.Date' as Date;//创建之前先导包,不支持.*的操作
return new Date();
```
## 异步调用
### 异步调用方法
```javascript
var val = async db.select('.....'); // 异步调用返回Future类型
return val.get(); //调用Future的get方法
```
### 异步调用lambda
```javascript
var list = [];
for(index in range(1,10)){
list.add(async (index)=>db.selectInt('select #{index}'));
}
return list.map(item=>item.get()); // 循环获取结果
```
## exit
语法格式为 `exit expr[,expr][,expr]....`
在`magic-api`中只取前三个值,分别对应`code`、`message`、`data`
如:`exit 400,'参数填写有误'`
## assert
语法格式为 `assert expr : expr[,expr][,expr]....`
如:`assert a == 1 : 400, 'a的值应为1'` 相当于
```javascript
if(a != 1){
exit 400, 'a的值应为1'
}
```
## 类型转换
通过`::`进行类型转换,如`xxx::int`、`xxx::double`等,当前支持转换类型有`int`、`double`、`long`、`byte`、`short`、`float`、`date`
```javascript
var a = "1";
return {
v1: a::int,
v2: a::int(0), //转换失败时值为0
v3: "2020-01-01"::date('yyyy-MM-dd') //转为Date
}
```
`::sql`支持将数据转换为对应的sql类型比如
```javascript
img::sql('blob')
```
可传入的参数请参考`java.sql.Types`中定义的常量,不区分大小写。
## 嵌入其它脚本语言
```javascript
var name = "hello";
var test = ```javascript
name + ' ~ world'
```;
return test();
```
## 可选链操作符
可选链操作符(`?.`)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。
```javascript
var a = null;
var b = a?.name; // b = null;
var c = a?.getName(); // c = null;
```
## 扩展运算符
扩展运算符,又叫展开语法(Spread syntax)是用于将list或map在语法层面展开
### lambda 调用
```javascript
var sum = (a,b,c) => a + b + c;
System.out.println(sum(...[1,2,3])) // 结果6
```
### list 展开
```javascript
var arr = [3,4,5];
System.out.println([1,2,...arr,6,7]) // 结果:[1, 2, 3, 4, 5, 6, 7]
```
### map 展开
```javascript
var map = {key2:2}
System.out.println({key1:1,...map,key3:3}) // 结果:{key1=1, key2=2, key3=3}
```

View File

@ -0,0 +1,104 @@
# 单表操作
操作入口:`db.table('table_name')`
## logic
- 作用:设置本查询是带有逻辑删除的,在执行`delete`方法时,会转换为`update`语句,在执行`select`相关方法时,会拼接`logic_field <> logic_value`
## withBlank
- 作用:设置后续插入或修改时,不过滤空值。
## column
- 入参:`column`: `String` 列名
- 作用:设置要查询列,`select`语句中有效
## column
- 入参:`column`: `String` 列名
- 入参:`value` : `Object`
- 作用:设置要操作的列的值,非`select`语句中有效
## primary
- 入参:`primary`: `String` 主键
- 入参:`defaultValue`: `Object` 插入时使用的默认值,可省略
- 作用:设置主键列,在`update`中语句有效,或`save`方法判断标准
## insert
- 入参: `data` : `Map` `insert`的列和值,可省略(通过`column`设置)
```javascript
// insert into sys_user(user_name,role) values('李富贵','admin')
return db.table('sys_user').insert({ user_name : '李富贵', role : 'admin'})
```
## batchInsert
- 入参: `collection` : `Collection` `insert`的列和值的集合
- 入参: `batchSize` `int` batchSize
```javascript
return db.table('sys_user').batchInsert([
{ user_name : '李富贵', role : 'admin'},
{ user_name : '王二狗', role : 'admin'},
{ user_name : '管理员', role : 'super-admin'},
])
```
## update
- 入参: `data` : `Map` `insert`的列和值,可省略(通过`column`设置)
- 入参:`isUpdateBlank`: `boolean` 是否更新空值字段(可省略,默认为`false`
```javascript
// update sys_user set user_name = '王二狗' where id = 1
return db.table('sys_user').primary('id').update({ id: 1, user_name : '王二狗'})
```
## save
- 入参: `data` : `Map` `insert`或`update`的列和值,可省略(通过`column`设置)
- 入参:`beforeQuery` `boolean` 是否根据id查询有没有数据可省略(默认`false`)
```javascript
// insert into sys_user(id,user_name) values('xxx','王二狗');
return db.table('sys_user').primary('id', uuid()).save({user_name: '王二狗'});
// insert into sys_user(user_name) values('王二狗');
return db.table('sys_user').primary('id').save({user_name: '王二狗'});
// update sys_user set user_name = '王二狗' where id = 1
return db.table('sys_user').primary('id').save({id: 1,user_name: '王二狗'});
```
## select
查询list与db.select 作用相同)
```javascript
// select * from sys_user
return db.table('sys_user').select()
```
## page
分页查询与db.page 作用相同)
```javascript
// select * from sys_user
return db.table('sys_user').page()
```
## where
设置查询条件
- eq --> `==`
- ne --> `<>`
- lt --> `<`
- gt --> `>`
- lte --> `<=`
- gte --> `>=`
- in --> `in`
- notIn --> `not in`
- like --> `like`
- notLike --> `not like`
```javascript
// select * from sys_user where user_name like '%李富贵%' and role = 'admin'
return db.table('sys_user')
.where()
.like('user_name','%李富贵%')
.eq('role','admin')
.select()
```

View File

@ -0,0 +1,125 @@
# SQL参数
## #{} 注入参数
作用和`mybatis`一致,都是将`#{}`区域替换为占位符`?`
```javascript
var id = 123;
return db.select("""
select * from sys_user where id = #{id}
""");
// 运行时生成的SQL为select * from sys_user where id = ?
```
此方法可以避免`sql`注入。
## ${} 拼接参数
作用和`mybatis`一致,都是将`${}`区域替换为对应的字符串
```javascript
var id = 123;
return db.select("""
select * from sys_user where id = ${id}
""");
// 运行时生成的SQL为select * from sys_user where id = 123
```
## 动态SQL参数
通过`?{condition,expression}`来实现动态拼接`SQL`
```javascript
return db.select("select * from sys_user ?{id,where id = #{id}}");
// 当id有值时,生成SQLselect * from sys_user where id = ?
// 当id无值时,生成SQLselect * from sys_user
return db.select("select * from sys_user ?{id!=null&&id.length() > 3,where id = #{id}}");
```
## 循环拼接参数
两种办法:
### in语法自动展开
```javascript
var ids = [1,2,3,4,5,6];
//会自动变成select * from sys_user where id in(?,?,?,?,?,?)
return db.select('select * from sys_user where id in(#{ids})');
```
### 循环拼接SQL
```javascript
var list = [1,2,3,4,5];
var sql = "select * from sys_user where ";
for(index,item in list){
sql = sql + 'id = #{list['+index+']}';
if(index + 1 < list.size()){
sql = sql + ' or ';
}
}
return db.select(sql);
```
## Mybatis语法支持
### 支持的关键字
- `<if>`
- `<elseif>`
- `<else>`
- `<where>`
- `<foreach>`
- `<trim>`
- `<set>`
### if
```javascript
var sql = """
select * from test_data
where 1 = 1
<if test="id != null">
and id = #{id}
</if>
"""
return db.select(sql)
```
### where
```javascript
var sql = """
select * from test_data
<where>
<if test="id != null">
and id = #{id}
</if>
</where>
"""
return db.select(sql)
```
### set、trim
```javascript
var sql = """
update test_data
<set>
<if test="name != null">
name = #{name}
</if>
<if test="content != null">
content = #{content}
</if>
</set>
where `id` = #{id}
"""
return db.update(sql)
```
### foreach
```javascript
var sql = """
select * from test_data
where id in
<foreach item='item' index='index' collection='body.ids'
open="(" separator="," close=")">
#{item}
</foreach>
"""
return db.select(sql)
```

View File

@ -0,0 +1,26 @@
# 字符串函数
## uuid
- 返回值: `String` `32`位无`-`的`UUID`
```javascript
return uuid(); // 等同于 UUID.randomUUID().toString().replace("-", "");
```
## is_blank
- 入参:`target`:`String` 判断的目标
- 返回值:`boolean`
- 函数说明:判断字符串是否为空
```javascript
return is_blank(''); // true 等同于 StringUtils.isBlank 一致
```
## not_blank
- 入参:`target`:`String` 判断的目标
- 返回值:`boolean`
- 函数说明:判断字符串是否不为空
```javascript
return not_blank(''); // false 等同于 !is_blank('')
```

View File

@ -0,0 +1,22 @@
# 参数校验
## 自动验证
在magic-api的Web界面中可配置
- 必填验证
- 表达式验证
- 正则验证
## 手动验证
对于表达式和正则无法实现的可以通过脚本来实现。
```javascript
var count = db.selectInt("""
select count(*) from sys_user where phone = #{phone}
""")
// count 值应该为0如果不为0则验证不予通过。
assert count == 0 : 400, '手机号已存在';
// 上述写法可以转换为
if(count != 0){
exit 400, '手机号已存在'
}
```

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Dependencies
**/node_modules/
# Build output
**/dist/
**/dist-ssr/
**/target/
# IDE
.idea/
.vscode/
*.sw?
*.suo
*.ntvs*
*.njsproj
*.sln
# Environment
.env.local
.env.*.local
# OS
.DS_Store
Thumbs.db
# Claude Code internal
.claude/memory/
.claude/plans/
.claude/worktrees/
.claude/settings.json
.claude/settings.local.json
.claude/projects/
# Session artifacts
*_session_summary.txt
session-export.jsonl
app.log
2026-05-15-234803-this-session-is-being-continued-from-a-previous-c.txt

View File

@ -0,0 +1,14 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

5
antdv-next-admin/.env Normal file
View File

@ -0,0 +1,5 @@
# Application Title
VITE_APP_TITLE=Antdv Next Admin
# API Base URL
VITE_API_BASE_URL=/api

View File

@ -0,0 +1,7 @@
# Development Environment
# Disable Mock Data
VITE_USE_MOCK=false
# API Base URL (proxy to our-itam backend)
VITE_API_BASE_URL=/api

View File

@ -0,0 +1,7 @@
# Production Environment
# Enable Mock Data for demo (GitHub Pages is static, so we must NOT call /api)
VITE_USE_MOCK=true
# API Base URL (unused in mock mode; keep empty to avoid accidental /api calls)
VITE_API_BASE_URL=

View File

@ -0,0 +1,35 @@
name: Build
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install rolldown native bindings
run: pnpm add @rolldown/binding-linux-x64-gnu -D
- name: Build
run: pnpm run build

View File

@ -0,0 +1,68 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- main # 当推送到 main 分支时触发
# 允许手动触发
workflow_dispatch:
# 设置 GITHUB_TOKEN 的权限
permissions:
contents: read
pages: write
id-token: write
# 只允许一个并发部署
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install rolldown native bindings
run: pnpm add @rolldown/binding-linux-x64-gnu -D
- name: Build
run: pnpm run build
env:
NODE_ENV: production
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist"
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

31
antdv-next-admin/.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment files
.env.local
.env.*.local
.claude
.agents

View File

@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"insertFinalNewline": true,
"sortImports": {
"groups": [
"type-import",
["value-builtin", "value-external"],
"value-internal",
["value-parent", "value-sibling", "value-index"],
"unknown"
]
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["vue", "typescript", "import"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn"
},
"rules": {
"no-unused-vars": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-unnecessary-type-assertion": "warn",
"import/no-duplicates": "error"
},
"overrides": [
{
"files": ["**/*.vue"],
"rules": {
"vue/no-unused-vars": "warn"
}
},
{
"files": ["mock/**/*"],
"rules": {
"no-console": "off"
}
}
]
}

251
antdv-next-admin/AGENTS.md Normal file
View File

@ -0,0 +1,251 @@
# Antdv Next Admin - Agent Guidelines
A Vue 3 + TypeScript + Ant Design Vue admin scaffold with RBAC, theming, i18n, and mock APIs.
## Project Structure
```
src/
├── api/ # API layer - organized by domain (auth.ts, user.ts)
├── assets/styles/ # Global styles (variables.css, animations.css, global.css)
├── components/ # Reusable components (Layout/, Permission/, etc.)
├── composables/ # Composition functions (usePermission.ts, useWatermark.ts)
├── directives/ # Custom Vue directives (permission.ts)
├── locales/ # i18n translations (zh-CN.ts, en-US.ts)
├── router/ # Vue Router (routes.ts, guards.ts, utils.ts)
├── stores/ # Pinia stores - one per domain (auth.ts, theme.ts, layout.ts)
├── types/ # TypeScript interfaces/types (auth.ts, api.ts, router.ts)
├── utils/ # Pure utility functions (request.ts, storage.ts, helpers.ts)
└── views/ # Page components (dashboard/, system/, examples/)
mock/
├── data/ # Mock datasets (users.data.ts, roles.data.ts)
└── handlers/ # Mock API handlers (auth.mock.ts, user.mock.ts)
tests/
├── e2e/ # End-to-end tests (*.spec.ts) - templates for future Playwright setup
└── unit/ # Unit tests (*.spec.ts) - templates for future Vitest setup
```
## Build, Test, and Development Commands
### Essential Commands
```bash
npm install # Install all dependencies
npm run dev # Start dev server at http://localhost:3000 (with mock APIs)
npm run build # Type check + production build → dist/
npm run preview # Preview production build locally
npm run type-check # Run vue-tsc --noEmit (NO auto-fix)
```
### Pre-commit Requirements
**BEFORE any commit or PR:**
1. Run `npm run type-check` - must exit 0 with no errors
2. Run `npm run build` - must complete successfully
3. For RBAC/auth changes: manually verify login with `admin/123456` and `user/123456`
### Testing Notes
- **No test runner configured yet** - Playwright/Vitest dependencies are NOT installed
- Test files in `tests/` are **templates** for future setup
- To add tests later: install test framework first, update package.json scripts, then write tests
## Code Style Guidelines
### Formatting (EditorConfig)
- **Indentation**: 2 spaces (NO tabs)
- **Line endings**: LF (Unix-style)
- **Encoding**: UTF-8
- **Final newline**: required
- **Trailing whitespace**: trimmed (except in .md files)
### TypeScript
- **Strict mode enabled** (`tsconfig.json`): all strict checks ON
- **Path aliases**: use `@/` for `src/` (e.g., `import { useAuthStore } from '@/stores/auth'`)
- **Type annotations**: explicit return types for public functions/composables
- **Type definitions**: place shared types in `src/types/`, domain-specific types near usage
- **No type suppression**: NEVER use `as any`, `@ts-ignore`, or `@ts-expect-error`
### Vue Component Style
**Component naming:**
- **Files**: PascalCase for reusable components (`NotificationPanel.vue`, `ThemeToggle.vue`)
- **Views**: route-based folders with `index.vue` (`src/views/dashboard/index.vue`)
**Component structure (Composition API only):**
```vue
<template>
<!-- Template using script setup's reactive state -->
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
import type { User } from '@/types/auth'
// Props
interface Props {
userId: string
mode?: 'edit' | 'view'
}
const props = withDefaults(defineProps<Props>(), {
mode: 'view'
})
// Emits
const emit = defineEmits<{
save: [user: User]
cancel: []
}>()
// State
const authStore = useAuthStore()
const loading = ref(false)
const user = computed(() => authStore.user)
// Methods (prefer explicit function declarations)
function handleSave() {
// Implementation
}
</script>
<style scoped>
/* Component-specific styles */
</style>
```
### Import Ordering
Group imports in this order (blank line between groups):
```ts
// 1. Vue core
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 2. Third-party libraries
import { message } from 'antdv-next'
import dayjs from 'dayjs'
// 3. Project imports (@/ alias)
import { useAuthStore } from '@/stores/auth'
import { login, getUserInfo } from '@/api/auth'
import type { User, LoginParams } from '@/types/auth'
```
### Naming Conventions
| Type | Convention | Example |
|------|-----------|---------|
| Components | PascalCase | `NotificationPanel.vue`, `TabBar.vue` |
| Composables | `useXxx.ts` | `usePermission.ts`, `useFullscreen.ts` |
| Stores | Domain-based | `auth.ts`, `permission.ts`, `theme.ts` |
| Types/Interfaces | PascalCase | `User`, `LoginParams`, `ApiResponse<T>` |
| Functions | camelCase | `getUserInfo()`, `checkPermission()` |
| Constants | SCREAMING_SNAKE_CASE | `TOKEN_KEY`, `API_BASE_URL` |
### Error Handling
- **Try/catch**: wrap all async operations with meaningful error messages
- **Axios interceptors**: global error handling in `src/utils/request.ts`
- **User feedback**: use `message.error()` or `notification.error()` from antdv-next
```ts
try {
const response = await getUserInfo()
// Success path
} catch (error) {
console.error('Failed to fetch user info:', error)
message.error('获取用户信息失败')
}
```
### State Management (Pinia)
- **Setup stores only** (NOT options API)
- **One store per domain** - no god-objects
- **Store structure pattern:**
```ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// State (ref)
const token = ref<string | null>(null)
// Getters (computed)
const isLoggedIn = computed(() => !!token.value)
// Actions (functions)
const setToken = (newToken: string | null) => {
token.value = newToken
}
return { token, isLoggedIn, setToken }
})
```
### Permission System Usage
**Directive (in templates):**
```vue
<!-- Single permission (OR logic by default) -->
<a-button v-permission="'user.create'">Create</a-button>
<!-- Multiple permissions (OR logic) -->
<a-button v-permission="['user.edit', 'user.delete']">Actions</a-button>
<!-- ALL permissions required (AND logic) -->
<a-button v-permission.all="['user.edit', 'user.approve']">Approve</a-button>
```
**Composable (in script):**
```ts
const { can, canAll, hasRole } = usePermission()
if (can('user.create')) {
// User has permission
}
if (canAll(['user.edit', 'user.approve'])) {
// User has ALL permissions
}
```
## Configuration & Environment
### Environment Variables
- **Development** (`.env.development`): `VITE_USE_MOCK=true`, `VITE_API_BASE_URL=/api`
- **Production** (`.env.production`): `VITE_USE_MOCK=false`, set real API URL
- **Never commit secrets** - use `.env.local` for sensitive values (gitignored)
### Mock API System
- **Auto-enabled in dev** via `vite-plugin-mock-dev-server`
- **Handlers**: `mock/handlers/*.mock.ts` define endpoints
- **Data**: `mock/data/*.data.ts` contain sample datasets
- **Prefix**: all mock APIs use `/api` prefix (e.g., `/api/auth/login`)
## Common Pitfalls to Avoid
1. **No linter configured** - manually match nearby code style
2. **Don't suppress TypeScript errors** - fix the root cause instead
3. **Test files are templates** - don't try to run them without installing test frameworks
4. **Mock users**: `admin/123456` has full permissions, `user/123456` has limited permissions
5. **Dynamic routes**: permissions control route visibility via `src/router/guards.ts`
6. **KeepAlive caching**: managed by `tabs` store - check cached component names
## Commit Guidelines
**Use Conventional Commits:**
```
type(scope): summary
Examples:
feat(auth): add biometric login support
fix(permission): correct role-based route filtering
refactor(layout): extract sidebar menu logic to composable
docs(readme): update installation instructions
```
**Commit types:** `feat`, `fix`, `refactor`, `docs`, `style`, `test`, `chore`, `perf`
## Pull Request Checklist
- [ ] `npm run type-check` passes
- [ ] `npm run build` succeeds
- [ ] Manually tested login flow (if auth-related)
- [ ] Manually verified permissions (if RBAC-related)
- [ ] Screenshots/GIFs included (for UI changes)
- [ ] Commit messages follow Conventional Commits
- [ ] Changes are scoped (no unrelated refactors mixed in)

343
antdv-next-admin/CLAUDE.md Normal file
View File

@ -0,0 +1,343 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**antdv-next-admin** is a modern Vue 3 + TypeScript admin scaffold built on:
- **antdv-next** (Ant Design Vue) - UI component library
- **Pinia** - State management
- **Vue Router** - Routing with dynamic route generation
- **vue-i18n** - Internationalization (Chinese/English)
- **Vite** - Build tool
- Full RBAC permission system with dynamic routes
- Mock data system for development
## Environment Requirements
- Node.js >= 16
- npm >= 8
## Common Commands
```bash
# Development
npm run dev # Start dev server on http://localhost:3000
# Building
npm run build # Production build
npm run build:check # Type check before build
npm run preview # Preview production build
# Type Checking
npm run type-check # TypeScript type checking
```
**Note**: This project currently has no test or lint scripts configured.
## Architecture
### State Management (Pinia Stores)
All stores use the **setup syntax** pattern. Located in `src/stores/`:
- **auth** - Authentication, user info, roles, permissions. Includes demo mode for development.
- **permission** - Dynamic route generation based on user roles/permissions
- **theme** - Theme mode (light/dark/auto), primary color (6 presets), CSS variable management
- **layout** - Layout mode (vertical/horizontal), sidebar state, mobile detection
- **tabs** - Multi-tab system with KeepAlive caching, affix tabs, right-click menu
- **settings** - User preferences (animations, gray mode, menu theme, etc.)
- **notification** - Notification panel state
**Key Pattern**: Store initialization happens in router guards. Auth store includes both demo mode (mock) and production mode (real API) login flows.
### Routing System
Routes are organized in three categories (`src/router/routes.ts`):
1. **staticRoutes** - No auth required (login, error pages)
2. **basicRoutes** - Require auth (dashboard, profile)
3. **asyncRoutes** - Require specific permissions (system management, etc.)
**Dynamic Route Generation**:
- Routes are generated in `src/router/guards.ts` during navigation
- Permission store (`generateRoutes`) filters async routes based on user roles/permissions
- Routes are added dynamically with `router.addRoute()` after successful login
- First navigation to dynamic route may redirect to 404, then recover by regenerating routes
**Route Meta Fields**:
```typescript
{
title: string // i18n key for page title
icon?: string // Icon component name (e.g., 'DashboardOutlined')
requiresAuth?: boolean // Default true
requiredPermissions?: string[] // Permission codes required
requiredRoles?: string[] // Role codes required
hidden?: boolean // Hide from menu
affix?: boolean // Pin tab (can't be closed)
order?: number // Menu sort order
}
```
### Permission System
Three ways to check permissions:
1. **Directive** (`src/directives/permission.ts`):
```vue
<a-button v-permission="'user.create'">Create</a-button>
<a-button v-permission="['user.edit', 'user.delete']">Actions</a-button>
<a-button v-permission.all="['user.edit', 'user.approve']">Approve</a-button>
```
2. **Composable** (`src/composables/usePermission.ts`):
```typescript
const { can, canAll } = usePermission()
if (can('user.create')) { /* ... */ }
if (canAll(['user.edit', 'user.approve'])) { /* ... */ }
```
3. **Component** (`src/components/Permission/PermissionButton.vue`):
```vue
<PermissionButton permission="user.create">
<a-button>Create User</a-button>
</PermissionButton>
```
### API & Request Handling
**Base Service**: `src/utils/request.ts` - Axios instance with interceptors
- Auto-adds Bearer token from auth store
- Handles 401 (logout + redirect to login), 403 (forbidden), 404, 500
- Response interceptor checks `res.code` field (expects 200)
- All API calls use the `request` helper with typed responses
**Mock System** (`mock/` directory):
- Enabled via `VITE_USE_MOCK=true` in `.env.development`
- Two-layer structure: `data/` (mock data sources) + `handlers/` (request handlers)
- Available mock APIs: auth, users, roles, permissions, dashboard
- Supports pagination, search, CRUD operations
### Pro Components
**ProTable** (`src/components/Pro/ProTable/`):
- Configuration-based table with search form, toolbar, pagination
- Column types defined via `ProTableColumn` interface (see `src/types/pro.ts`)
- Supports value types: text, date, dateTime, tag, badge, money, percent, avatar, etc.
- Search types: input, select, dateRange, datePicker, etc.
- Built-in features: column resizing, fixed headers, sorting, actions column
- **Important**: Uses CSS variables for scrollbar width alignment (see scrollbar.ts utility)
- Two rendering modes:
- `scroll-mode`: Table handles its own scrolling
- `fill-mode`: Parent container scrolls, table fills height with `fixedHeader`
**ProForm** (`src/components/Pro/ProForm/`):
- Configuration-based form with validation
- Form item types: input, password, textarea, number, select, radio, checkbox, switch, datePicker, etc.
- Grid layout support with `colSpan` and responsive `cols`
- Dynamic options via `request` function
- Custom rendering via `render` prop
**Type Definitions**: Always reference `src/types/pro.ts` for column/form configurations.
### Icons
Two icon systems are available:
1. **Ant Design Icons** (`@antdv-next/icons`):
```vue
import { UserOutlined } from '@antdv-next/icons'
```
2. **Iconify** (`@iconify/vue`):
- Component: `src/components/Icon/index.vue`
- Picker: `src/components/IconPicker/index.vue`
- Supports online/offline modes with local icon sets (ion, mdi, ri)
- Use `<Icon icon="mdi:home" />` syntax
### Theme System
Themes use **CSS variables** defined in `src/assets/styles/variables.css`:
- 6 preset primary colors: blue (default), green, purple, red, orange, cyan
- Dark/light/auto modes
- CSS variables follow pattern: `--ant-primary-color`, `--bg-color`, `--text-color`, etc.
- Theme store dynamically updates CSS variables on document root
- Sidebar supports independent dark/light theme (via `--sidebar-bg-color` variables)
### Internationalization
**System**: vue-i18n with locale files in `src/locales/`
- `zh-CN.ts` - Chinese (default)
- `en-US.ts` - English
- Access via `$t('key')` in templates or `t('key')` from `useI18n()`
- Helper: `src/utils/i18n.ts` - `resolveLocaleText()` for dynamic text resolution
### Charts & Visualization
**ECharts Integration**: The project includes `echarts` and `vue-echarts` for data visualization in the dashboard. Use the `<v-chart>` component from `vue-echarts` for rendering charts.
### Keyboard Shortcuts
- `Ctrl/Cmd + K` - Open global menu search
## Development Guidelines
### File Naming & Structure
- Components: **PascalCase** (e.g., `AdminLayout.vue`)
- Files: **kebab-case** (e.g., `use-permission.ts`)
- Path alias: Use `@/` for `src/` (configured in vite.config.ts and tsconfig.json)
### TypeScript
- **Strict mode enabled** - All code must be type-safe
- Types organized in `src/types/`: api.ts, auth.ts, router.ts, layout.ts, pro.ts
- Use `type` for object shapes, `interface` for extensible contracts
- Route type: `AppRouteRecordRaw` (extends Vue Router's `RouteRecordRaw`)
- API responses: `ApiResponse<T>` pattern
### Adding New Pages
1. Create view in `src/views/[module]/`
2. Add route to appropriate category in `src/router/routes.ts`
3. Add i18n keys to `src/locales/zh-CN.ts` and `en-US.ts`
4. If requires permissions, set `meta.requiredPermissions` or `meta.requiredRoles`
5. Router guards will handle dynamic route injection
### Adding New Stores
Follow the **setup syntax** pattern:
```typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useMyStore = defineStore('my-store', () => {
// State
const data = ref<MyType | null>(null)
// Getters
const processedData = computed(() => /* ... */)
// Actions
const fetchData = async () => { /* ... */ }
return { data, processedData, fetchData }
})
```
Export from `src/stores/index.ts` for centralized access.
### Working with ProTable
Always define columns using the `ProTableColumn` type:
```typescript
import type { ProTableColumn } from '@/types/pro'
const columns: ProTableColumn[] = [
{
title: 'Name',
dataIndex: 'name',
search: true, // Enable search
searchType: 'input', // Search field type
valueType: 'text' // Display type
},
{
title: 'Status',
dataIndex: 'status',
valueType: 'tag',
valueEnum: {
active: { text: 'Active', status: 'success' },
inactive: { text: 'Inactive', status: 'default' }
}
}
]
```
**Known Issue**: ProTable had scrollbar alignment bugs that were fixed by:
- Using CSS variables for dynamic scrollbar width (`--actual-scrollbar-width`)
- Scrollbar detection utility in `src/utils/scrollbar.ts`
- When modifying table layout, verify scrollbar placeholder alignment
### Authentication Flow
**Demo Mode** (development):
- Credentials: `admin/123456` or `user/123456`
- No real backend, uses mock data from auth store
- Token stored in localStorage
**Production Mode**:
- Set `VITE_USE_MOCK=false` and `VITE_API_BASE_URL` in `.env.production`
- Uses real API calls via `src/api/auth.ts`
### Mock Data Development
To add new mock endpoints:
1. Create data source in `mock/data/[entity].data.ts`
2. Create handler in `mock/handlers/[entity].mock.ts`
3. Mock server auto-reloads, accessible at `/api/*` prefix
## Common Patterns
### Conditional Rendering by Permission
```typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
if (authStore.hasAnyPermission(['user.edit', 'user.delete'])) {
// Show actions
}
```
### Accessing Current Route in Components
```typescript
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.meta.title)
```
### Multi-Tab Operations
```typescript
import { useTabsStore } from '@/stores/tabs'
const tabsStore = useTabsStore()
tabsStore.closeTab(path) // Close specific tab
tabsStore.closeOtherTabs(path) // Close all except this
tabsStore.closeAllTabs() // Close all closeable tabs
tabsStore.refreshTab(path) // Refresh tab content
```
### Theme Changes
```typescript
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
themeStore.setThemeMode('dark') // 'light' | 'dark' | 'auto'
themeStore.setPrimaryColor('#1890ff') // Any valid color
```
## Important Notes
- **Never commit** environment-specific values to `.env` files
- **Router guards** handle most auth/permission logic - avoid duplicating checks
- **CSS variables** are the preferred method for theming - avoid hardcoded colors
- **ProTable fixedHeader mode** requires parent container with fixed height
- **Dynamic routes** are regenerated on each login - changes to `asyncRoutes` require re-login
- **Tabs state** persists in localStorage via settings store
- **Mock mode** is determined by `VITE_USE_MOCK` env variable, checked at runtime
## Default Accounts
Development mode credentials:
- Admin: `admin / 123456`
- User: `user / 123456`

View File

@ -0,0 +1,93 @@
# GitHub Pages 部署指南
本项目已配置为可自动部署到 GitHub Pages。
## 📦 部署地址
- **生产环境**: https://yelog.github.io/antdv-next-admin/
## 🚀 自动部署
项目使用 GitHub Actions 实现自动化部署:
1. 当代码推送到 `main` 分支时,会自动触发部署流程
2. GitHub Actions 会自动构建项目并部署到 GitHub Pages
3. 部署完成后,可以通过上述地址访问
## ⚙️ 配置说明
### 1. Vite 配置
```typescript
// vite.config.ts
base: process.env.NODE_ENV === 'production' ? '/antdv-next-admin/' : '/'
```
### 2. GitHub Actions
- 工作流文件: `.github/workflows/deploy.yml`
- 触发条件: 推送到 main 分支或手动触发
- 构建命令: `npm run build`
### 3. SPA 路由支持
- `public/404.html`: 处理 404 重定向
- `index.html`: 接收重定向并恢复路由
- `public/.nojekyll`: 禁用 Jekyll 处理
## 📝 手动部署步骤
如果需要手动部署:
```bash
# 1. 构建项目
npm run build
# 2. 进入构建目录
cd dist
# 3. 初始化 git 仓库
git init
git add -A
git commit -m 'deploy'
# 4. 推送到 gh-pages 分支
git push -f git@github.com:yelog/antdv-next-admin.git main:gh-pages
# 5. 返回项目根目录
cd -
```
## 🔧 GitHub 仓库设置
确保在 GitHub 仓库设置中:
1. 进入仓库 Settings → Pages
2. Source 选择 "GitHub Actions"
3. 等待首次部署完成
## 📊 查看部署状态
- 在 GitHub 仓库的 "Actions" 标签页查看部署进度
- 绿色勾号表示部署成功
- 红色叉号表示部署失败,点击查看日志
## 🐛 常见问题
### 1. 404 错误
- 确保 `base` 配置正确
- 检查 GitHub Pages 设置是否正确
### 2. 路由不工作
- 确保 `404.html``index.html` 中的重定向脚本存在
- 检查浏览器控制台是否有错误
### 3. 样式/资源 404
- 确保 `base` 路径配置正确
- 检查构建后的资源路径是否正确
## 🔐 权限说明
GitHub Actions 需要以下权限:
- `contents: read` - 读取代码
- `pages: write` - 写入 Pages
- `id-token: write` - 身份验证
这些权限已在 workflow 文件中配置。

21
antdv-next-admin/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 yelog
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

618
antdv-next-admin/README.md Normal file
View File

@ -0,0 +1,618 @@
# Antdv Next Admin
🎉 一个基于 Vue 3 + TypeScript + Ant Design Vue 的现代化、功能完整的后台管理系统脚手架。
[![Vue](https://img.shields.io/badge/Vue-3.4-brightgreen.svg)](https://vuejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
[![Vite](https://img.shields.io/badge/Vite-8.0-purple.svg)](https://vitejs.dev/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
## 📸 预览
**在线体验:** [https://antdv-next-admin.yelog.org/dashboard](https://antdv-next-admin.yelog.org/dashboard)
![系统截图](docs/images/screenshot.png)
> 默认账号: admin / 123456 或 user / 123456
## ✨ 特性
### 核心功能
- ✅ **最新技术栈**: Vue 3 + Vite + TypeScript + Pinia
- ✅ **UI 组件**: Ant Design Vue (antdv-next)
- ✅ **布局系统**: 响应式布局,支持垂直/水平两种模式
- ✅ **多标签页**: 基于 KeepAlive 的多标签页系统,支持固定、刷新、右键菜单
- ✅ **主题系统**: 支持亮色/暗色/跟随系统三种模式
- ✅ **国际化**: 完整的中英文切换,支持运行时动态切换
- ✅ **权限系统**: RBAC 权限控制,支持动态路由、按钮级权限、指令权限
- ✅ **Mock 数据**: 开发环境完整的 Mock 数据支持
### 高级功能
- 🎨 **偏好设置**:
- 6 种预设主题色 (拂晓蓝、极光绿、酱紫、薄暮红、日暮橙、明青)
- 左侧菜单栏样式切换 (深色/浅色)
- 布局方式切换 (垂直/水平)
- 5种页面切换动画 (淡入、滑动、缩放等)
- 灰色模式/色弱模式
- 🎯 **精致设计**:
- 流畅的动画效果
- 细腻的交互反馈
- 响应式设计
- 一致的设计语言
### Pro 组件
- 📊 **ProTable**: 高级表格组件
- 自动生成查询表单
- 列配置化(显示/隐藏、排序、固定)
- 内置分页、刷新、密度切换
- 支持多种值类型渲染(日期、标签、进度条等)
- 📝 **ProForm**: 高级表单组件
- 配置化表单生成
- 自动布局和验证
- 支持多种表单类型
- 内置提交/重置逻辑
- 🎭 **ProModal**: 高级弹窗组件
- 支持拖拽、全屏
- 自动表单集成
- 统一的确认/取消逻辑
### 业务组件
- 📝 **富文本编辑器**: 基于 TipTap支持图片、链接、格式化
- 🔐 **验证码组件**:
- 滑块验证码
- 拼图验证码
- 点选验证码
- 旋转验证码
- 🎨 **图标选择器**: 支持 Iconify 图标库搜索选择
- 💧 **水印组件**: 支持文字/图片水印,可配置透明度、角度
## 🚀 快速开始
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm run dev
```
访问 `http://localhost:3000`
### 默认账号
```bash
管理员账号:
用户名: admin
密码: 123456
普通用户账号:
用户名: user
密码: 123456
```
### 构建生产版本
```bash
npm run build
```
### 预览生产构建
```bash
npm run preview
```
## 📁 项目结构
```
antdv-next-admin/
├── public/ # 静态资源
│ └── logo.svg # 应用Logo
├── src/
│ ├── api/ # API接口
│ │ ├── auth.ts # 认证接口
│ │ ├── user.ts # 用户管理
│ │ ├── role.ts # 角色管理
│ │ ├── permission.ts # 权限管理
│ │ ├── dept.ts # 部门管理
│ │ ├── dict.ts # 字典管理
│ │ ├── config.ts # 系统配置
│ │ ├── file.ts # 文件管理
│ │ └── log.ts # 日志管理
│ ├── assets/ # 资源文件
│ │ └── styles/ # 全局样式
│ │ ├── global.css # 全局样式
│ │ ├── variables.css # CSS变量
│ │ └── animations.css # 动画定义
│ ├── components/ # 组件
│ │ ├── Layout/ # 布局组件
│ │ │ ├── AdminLayout.vue
│ │ │ ├── Sidebar.vue
│ │ │ ├── Header.vue
│ │ │ ├── TabBar.vue
│ │ │ ├── MenuItem.vue
│ │ │ ├── Breadcrumb.vue
│ │ │ ├── ThemeToggle.vue
│ │ │ ├── LanguageSwitch.vue
│ │ │ ├── FullscreenToggle.vue
│ │ │ ├── NotificationPanel.vue
│ │ │ ├── AvatarDropdown.vue
│ │ │ ├── GlobalSearch.vue
│ │ │ └── SettingsDrawer.vue
│ │ ├── Pro/ # Pro 高级组件
│ │ │ ├── ProTable/ # 高级表格
│ │ │ ├── ProForm/ # 高级表单
│ │ │ └── ProModal/ # 高级弹窗
│ │ ├── Permission/ # 权限组件
│ │ │ └── PermissionButton.vue
│ │ ├── Editor/ # 富文本编辑器
│ │ ├── Captcha/ # 验证码组件
│ │ │ ├── SliderCaptcha.vue # 滑块验证码
│ │ │ ├── PuzzleCaptcha.vue # 拼图验证码
│ │ │ ├── PointCaptcha.vue # 点选验证码
│ │ │ └── RotateCaptcha.vue # 旋转验证码
│ │ ├── IconPicker/ # 图标选择器
│ │ └── Icon/ # 图标组件
│ ├── composables/ # 组合式函数
│ │ ├── usePermission.ts # 权限判断
│ │ ├── useWatermark.ts # 水印功能
│ │ └── useFullscreen.ts # 全屏切换
│ ├── directives/ # 自定义指令
│ │ ├── index.ts
│ │ └── permission.ts # 权限指令
│ ├── locales/ # 国际化
│ │ ├── index.ts
│ │ ├── zh-CN.ts # 中文
│ │ └── en-US.ts # 英文
│ ├── router/ # 路由
│ │ ├── index.ts
│ │ ├── routes.ts # 路由配置
│ │ ├── guards.ts # 路由守卫
│ │ └── utils.ts # 路由工具
│ ├── stores/ # Pinia状态管理
│ │ ├── index.ts
│ │ ├── auth.ts # 认证状态
│ │ ├── layout.ts # 布局状态
│ │ ├── theme.ts # 主题状态
│ │ ├── tabs.ts # 标签页状态
│ │ ├── permission.ts # 权限状态
│ │ ├── notification.ts # 通知状态
│ │ ├── settings.ts # 偏好设置
│ │ ├── dict.ts # 字典数据
│ │ ├── watermark.ts # 水印状态
│ │ └── demoStateCache.ts # 演示缓存
│ ├── types/ # TypeScript类型
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ ├── router.ts
│ │ ├── layout.ts
│ │ └── pro.ts
│ ├── utils/ # 工具函数
│ │ ├── request.ts # Axios封装
│ │ ├── storage.ts # Storage封装
│ │ ├── auth.ts # 认证工具
│ │ ├── helpers.ts # 辅助函数
│ │ ├── i18n.ts # 国际化工具
│ │ └── icon.ts # 图标工具
│ ├── views/ # 页面
│ │ ├── login/ # 登录页
│ │ ├── dashboard/ # 数据看板
│ │ ├── profile/ # 个人中心
│ │ ├── notification/ # 通知中心
│ │ ├── system/ # 系统管理
│ │ │ ├── user/ # 用户管理
│ │ │ ├── role/ # 角色管理
│ │ │ ├── permission/ # 权限管理
│ │ │ ├── dept/ # 部门管理
│ │ │ ├── dict/ # 字典管理
│ │ │ ├── config/ # 系统配置
│ │ │ ├── file/ # 文件管理
│ │ │ └── log/ # 日志管理
│ │ ├── examples/ # 示例页面
│ │ │ ├── table/ # 表格示例
│ │ │ ├── form/ # 表单示例
│ │ │ ├── editor/ # 编辑器示例
│ │ │ ├── icon/ # 图标示例
│ │ │ ├── modal/ # 弹窗示例
│ │ │ ├── captcha/ # 验证码示例
│ │ │ ├── watermark/ # 水印示例
│ │ │ ├── external/ # 外部链接示例
│ │ │ └── scaffold/ # 脚手架示例
│ │ │ ├── rbac/ # RBAC权限示例
│ │ │ ├── state-cache/ # 状态缓存示例
│ │ │ ├── upload-system/ # 文件上传示例
│ │ │ ├── pro-table-advanced/# 高级表格示例
│ │ │ ├── observability/ # 可观测性示例
│ │ │ ├── testing/ # 测试示例
│ │ │ ├── complex-form/ # 复杂表单示例
│ │ │ ├── request-auth/ # 请求鉴权示例
│ │ │ └── master-detail/ # 主从表示例
│ │ └── error/ # 错误页面
│ │ ├── 404.vue
│ │ ├── 403.vue
│ │ └── 500.vue
│ ├── App.vue
│ └── main.ts
├── mock/ # Mock数据
│ ├── data/ # Mock数据源
│ │ ├── users.data.ts
│ │ ├── roles.data.ts
│ │ ├── permissions.data.ts
│ │ └── dashboard.data.ts
│ └── handlers/ # Mock处理器
│ ├── auth.mock.ts
│ ├── user.mock.ts
│ ├── role.mock.ts
│ ├── permission.mock.ts
│ └── dashboard.mock.ts
├── docs/ # 文档资源
│ └── images/ # 图片资源
├── .env # 环境变量
├── .env.development # 开发环境
├── .env.production # 生产环境
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
```
## 🎯 项目状态
### ✅ 已完成功能 (13/15 - 87%)
**核心架构:**
- ✅ 项目初始化和配置
- ✅ 完整的设计系统6种主题色、动画、工具类
- ✅ TypeScript类型定义体系
- ✅ 7个Pinia状态管理Store
- ✅ 路由系统(静态+动态路由、路由守卫)
- ✅ 权限系统(指令、组合函数、组件)
- ✅ 国际化(中英文完整翻译)
- ✅ Mock数据系统完整的CRUD API
- ✅ 工具函数库30+实用函数)
- ✅ 组合式函数usePermission、useWatermark、useFullscreen等
**布局组件:**
- ✅ AdminLayout主布局支持垂直/水平模式)
- ✅ Sidebar侧边栏支持深色/浅色主题)
- ✅ Header顶部导航栏
- ✅ TabBar多标签页系统右键菜单
- ✅ MenuItem递归菜单组件
- ✅ Breadcrumb面包屑导航
- ✅ ThemeToggle主题切换
- ✅ LanguageSwitch语言切换
- ✅ FullscreenToggle全屏切换
- ✅ NotificationPanel通知面板
- ✅ AvatarDropdown用户下拉菜单
- ✅ GlobalSearch全局搜索
- ✅ SettingsDrawer偏好设置抽屉
**页面视图:**
- ✅ 登录页(完整的登录表单和验证)
- ✅ Dashboard统计卡片、图表、活动列表
- ✅ 个人中心(用户信息、账号设置)
- ✅ 通知中心(系统通知、消息管理)
- ✅ 系统管理模块
- 用户管理CRUD、搜索、分页
- 角色管理(权限分配、菜单权限)
- 权限管理(权限树、增删改查)
- 部门管理(树形结构、组织架构)
- 字典管理(数据字典、键值对)
- 系统配置(参数配置、系统设置)
- 文件管理(上传、下载、预览)
- 日志管理(操作日志、登录日志)
- ✅ 示例页面(丰富的示例代码)
- 表格示例ProTable 高级表格)
- 表单示例ProForm 高级表单)
- 富文本编辑器TipTap 集成)
- 图标示例Iconify 图标库)
- 弹窗示例ProModal 高级弹窗)
- 验证码示例4种验证码类型
- 水印示例(文字/图片水印)
- 外部链接iframe/新窗口打开)
- ✅ 脚手架示例(最佳实践模板)
- RBAC 权限示例
- 状态缓存示例
- 文件上传示例
- 高级表格示例
- 可观测性示例
- 测试示例
- 复杂表单示例
- 请求鉴权示例
- 主从表示例
- ✅ 错误页面404、403、500
## 🎨 技术栈
### 核心框架
- **Vue 3.4+** - 渐进式 JavaScript 框架
- **TypeScript 5+** - JavaScript 的超集
- **Vite 8+** - 下一代前端构建工具 (基于 Rolldown + Oxc)
### UI & 样式
- **Ant Design Vue** - 企业级 UI 组件库
- **CSS Variables** - 现代化的主题系统
- **SCSS** - CSS 预处理器
### 状态管理 & 路由
- **Pinia 2+** - Vue 官方状态管理
- **Vue Router 4+** - Vue 官方路由
### 工具库
- **vue-i18n** - 国际化
- **Axios** - HTTP 客户端
- **dayjs** - 日期处理
- **lodash-es** - 工具函数库
- **@faker-js/faker** - 假数据生成
### 开发工具
- **vite-plugin-mock-dev-server** - Mock 服务
- **oxlint** - 极速代码检查 (50-100x 快于 ESLint)
- **oxfmt** - 极速代码格式化 (30x 快于 Prettier)
## 🔧 开发指南
### 环境要求
- Node.js >= 16
- npm >= 8
### 环境变量
**开发环境 (.env.development):**
```bash
VITE_USE_MOCK=true
VITE_API_BASE_URL=/api
```
**生产环境 (.env.production):**
```bash
VITE_USE_MOCK=false
VITE_API_BASE_URL=https://your-api-domain.com/api
```
### 代码规范
- 使用 TypeScript 进行类型安全的开发
- 遵循 Vue 3 Composition API 最佳实践
- 组件命名使用 PascalCase
- 文件命名使用 kebab-case
- 使用 CSS Variables 进行主题定制
### 权限使用
**指令方式:**
```vue
<a-button v-permission="'user.create'">创建用户</a-button>
<a-button v-permission="['user.edit', 'user.delete']">操作</a-button>
<a-button v-permission.all="['user.edit', 'user.approve']">审批</a-button>
```
**组合函数方式:**
```ts
const { can, canAll } = usePermission();
if (can("user.create")) {
// 有创建权限
}
if (canAll(["user.edit", "user.approve"])) {
// 同时有编辑和审批权限
}
```
**组件方式:**
```vue
<PermissionButton permission="user.create">
<a-button>创建用户</a-button>
</PermissionButton>
```
## 📝 Mock 数据
项目集成了完整的 Mock 数据系统,开发环境下自动启用。
### 可用的 Mock API
- **认证接口**
- POST `/api/auth/login` - 登录
- POST `/api/auth/logout` - 登出
- GET `/api/auth/info` - 获取用户信息
- **用户管理**
- GET `/api/users` - 用户列表(支持分页、搜索)
- GET `/api/users/:id` - 获取用户详情
- POST `/api/users` - 创建用户
- PUT `/api/users/:id` - 更新用户
- DELETE `/api/users/:id` - 删除用户
- **角色管理**
- GET `/api/roles` - 角色列表
- GET `/api/roles/:id` - 获取角色详情
- POST `/api/roles` - 创建角色
- PUT `/api/roles/:id` - 更新角色
- DELETE `/api/roles/:id` - 删除角色
- **权限管理**
- GET `/api/permissions` - 权限列表
- GET `/api/permissions/tree` - 权限树
- **部门管理**
- GET `/api/depts` - 部门列表
- GET `/api/depts/tree` - 部门树形结构
- **字典管理**
- GET `/api/dict/types` - 字典类型列表
- GET `/api/dict/data/:type` - 字典数据
- **系统配置**
- GET `/api/config` - 配置列表
- PUT `/api/config/:key` - 更新配置
- **文件管理**
- POST `/api/files/upload` - 上传文件
- GET `/api/files` - 文件列表
- **日志管理**
- GET `/api/logs` - 日志列表
- **Dashboard**
- GET `/api/dashboard/stats` - 统计数据
- GET `/api/dashboard/chart-data` - 图表数据
## 🎯 特色功能
### 1. 多主题支持
6种预设主题色 + 深色/浅色模式 + 跟随系统共18种主题组合。
### 2. 灵活布局
- 垂直布局(侧边栏在左)
- 水平布局(菜单在顶部)
- 响应式适配移动端
### 3. 多标签页系统
- 支持标签页缓存KeepAlive
- 支持固定标签affix/pinned
- 右键菜单(刷新、固定、关闭、关闭其他、关闭左侧、关闭右侧、关闭所有)
- 标签页持久化存储
### 4. 全局搜索
快捷键 `Ctrl/Cmd + K` 唤起全局菜单搜索。
### 5. 国际化
完整的中英文翻译,支持运行时切换,所有组件均已国际化。
### 6. Pro 组件使用
#### ProTable 示例
```vue
<template>
<ProTable :columns="columns" :request="loadData" :search="searchConfig">
<template #toolbar>
<a-button type="primary">新建</a-button>
</template>
</ProTable>
</template>
<script setup lang="ts">
import { ProTable } from "@/components/Pro";
const columns = [
{ title: "姓名", dataIndex: "name", valueType: "text" },
{ title: "年龄", dataIndex: "age", valueType: "number" },
{ title: "状态", dataIndex: "status", valueType: "tag" },
{ title: "创建时间", dataIndex: "createdAt", valueType: "date" },
];
const loadData = async (params: any) => {
// 返回 { data: [], total: 0 }
};
</script>
```
#### ProForm 示例
```vue
<template>
<ProForm
:schema="formSchema"
:initial-values="initialValues"
@submit="handleSubmit"
/>
</template>
<script setup lang="ts">
import { ProForm } from "@/components/Pro";
const formSchema = [
{ label: "用户名", name: "username", type: "input", required: true },
{ label: "邮箱", name: "email", type: "input", rules: [{ type: "email" }] },
{ label: "角色", name: "role", type: "select", options: roleOptions },
{ label: "状态", name: "status", type: "switch" },
];
const handleSubmit = (values: any) => {
console.log("提交表单:", values);
};
</script>
```
### 7. 验证码组件
```vue
<template>
<!-- 滑块验证码 -->
<SliderCaptcha @success="handleSuccess" />
<!-- 拼图验证码 -->
<PuzzleCaptcha @success="handleSuccess" />
<!-- 点选验证码 -->
<PointCaptcha @success="handleSuccess" />
<!-- 旋转验证码 -->
<RotateCaptcha @success="handleSuccess" />
</template>
```
### 8. 富文本编辑器
基于 TipTap 的富文本编辑器,支持:
- 文本格式化(粗体、斜体、下划线)
- 标题、列表、引用
- 图片上传和插入
- 链接插入
- 代码块
- 历史记录(撤销/重做)
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
## 📄 许可
MIT License
## 🙏 致谢
- [Vue 3](https://vuejs.org/)
- [Vite](https://vitejs.dev/)
- [Ant Design Vue](https://antdv.com/)
- [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin)
- [Ant Design Pro Vue](https://pro.antdv.com/)
---
Made with ❤️ by Claude Code

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Antdv Next Admin</title>
<!-- GitHub Pages SPA 路由重定向处理 -->
<script>
(function() {
var redirect = sessionStorage.redirect;
delete sessionStorage.redirect;
if (redirect && redirect !== location.href) {
history.replaceState(null, null, redirect);
}
})();
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,219 @@
import type { SysConfig } from '@/types/config';
export const sysConfigs: SysConfig[] = [
// basic
{
id: '1',
name: 'Site Name',
key: 'site.name',
value: 'Antdv Next Admin',
valueType: 'string',
group: 'basic',
description: 'System display name',
builtIn: true,
sort: 1,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '2',
name: 'Site Description',
key: 'site.description',
value: 'Vue3 + Ant Design Vue Admin System',
valueType: 'string',
group: 'basic',
description: 'Site SEO description',
builtIn: true,
sort: 2,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '3',
name: 'Copyright',
key: 'site.copyright',
value: '© 2024 Antdv Next Admin',
valueType: 'string',
group: 'basic',
description: 'Footer copyright text',
builtIn: false,
sort: 3,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '4',
name: 'System Version',
key: 'site.version',
value: '1.0.0',
valueType: 'string',
group: 'basic',
description: 'Current system version',
builtIn: true,
sort: 4,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// security
{
id: '10',
name: 'Max Login Attempts',
key: 'security.maxLoginAttempts',
value: '5',
valueType: 'number',
group: 'security',
description: 'Lock account after N failed login attempts',
builtIn: true,
sort: 1,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '11',
name: 'Lock Duration (min)',
key: 'security.lockDuration',
value: '30',
valueType: 'number',
group: 'security',
description: 'Account lock duration in minutes',
builtIn: true,
sort: 2,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '12',
name: 'Token Expiry (hours)',
key: 'security.tokenExpiry',
value: '24',
valueType: 'number',
group: 'security',
description: 'Login token expiration time',
builtIn: true,
sort: 3,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '13',
name: 'Enable Captcha',
key: 'security.captchaEnabled',
value: 'true',
valueType: 'boolean',
group: 'security',
description: 'Require captcha for login',
builtIn: false,
sort: 4,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '14',
name: 'Min Password Length',
key: 'security.minPasswordLength',
value: '6',
valueType: 'number',
group: 'security',
description: 'Minimum password character count',
builtIn: true,
sort: 5,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// upload
{
id: '20',
name: 'Max Upload Size (MB)',
key: 'upload.maxSize',
value: '10',
valueType: 'number',
group: 'upload',
description: 'Maximum single file upload size',
builtIn: true,
sort: 1,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '21',
name: 'Allowed Upload Types',
key: 'upload.allowedTypes',
value: 'jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,zip',
valueType: 'string',
group: 'upload',
description: 'Allowed file extensions, comma separated',
builtIn: true,
sort: 2,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '22',
name: 'Image Compression Quality',
key: 'upload.imageQuality',
value: '80',
valueType: 'number',
group: 'upload',
description: 'Auto compression quality for uploaded images (0-100)',
builtIn: false,
sort: 3,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// notification
{
id: '30',
name: 'Enable Email Notification',
key: 'notify.emailEnabled',
value: 'true',
valueType: 'boolean',
group: 'notification',
description: 'Enable email notification feature',
builtIn: false,
sort: 1,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '31',
name: 'SMTP Server',
key: 'notify.smtpHost',
value: 'smtp.example.com',
valueType: 'string',
group: 'notification',
description: 'SMTP server address for sending emails',
builtIn: false,
sort: 2,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '32',
name: 'SMTP Port',
key: 'notify.smtpPort',
value: '465',
valueType: 'number',
group: 'notification',
description: 'SMTP server port',
builtIn: false,
sort: 3,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '33',
name: 'Sender Email',
key: 'notify.senderEmail',
value: 'noreply@example.com',
valueType: 'string',
group: 'notification',
description: 'System notification sender email',
builtIn: false,
sort: 4,
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
];

View File

@ -0,0 +1,100 @@
import { faker } from '@faker-js/faker';
// Statistics data
export const mockStats = {
totalUsers: 12458,
totalOrders: 8946,
totalRevenue: 456789.56,
conversionRate: 3.24,
};
// Sales trend data (last 12 months)
export const mockSalesTrend = Array.from({ length: 12 }, (_, index) => {
const date = new Date();
date.setMonth(date.getMonth() - (11 - index));
return {
month: date.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit' }),
sales: faker.number.int({ min: 20000, max: 80000 }),
orders: faker.number.int({ min: 500, max: 2000 }),
};
});
// User distribution by city
export const mockUserDistribution = [
{ city: 'Beijing', value: 2341 },
{ city: 'Shanghai', value: 2156 },
{ city: 'Guangzhou', value: 1876 },
{ city: 'Shenzhen', value: 1654 },
{ city: 'Hangzhou', value: 1432 },
{ city: 'Chengdu', value: 1289 },
{ city: 'Other', value: 2710 },
];
// Recent activities
export const mockActivities = Array.from({ length: 10 }, (_, index) => ({
id: faker.string.uuid(),
user: faker.person.fullName(),
avatar: faker.image.avatar(),
action: faker.helpers.arrayElement([
'Created a new user',
'Updated role permissions',
'Deleted expired data',
'Exported reports',
'Updated system settings',
'Uploaded a new file',
]),
timestamp: faker.date.recent({ days: 7 }).toISOString(),
type: faker.helpers.arrayElement(['success', 'info', 'warning', 'error']),
}));
// Chart data for different visualizations
export const mockChartData = {
// Line chart - Sales trend
lineChart: {
xAxis: mockSalesTrend.map((item) => item.month),
series: [
{
name: 'Sales',
data: mockSalesTrend.map((item) => item.sales),
},
{
name: 'Orders',
data: mockSalesTrend.map((item) => item.orders),
},
],
},
// Bar chart - Monthly comparison
barChart: {
xAxis: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
series: [
{
name: 'Current Year',
data: [820, 932, 901, 934, 1290, 1330],
},
{
name: 'Last Year',
data: [720, 832, 801, 834, 1190, 1230],
},
],
},
// Pie chart - User distribution
pieChart: {
data: mockUserDistribution.map((item) => ({
name: item.city,
value: item.value,
})),
},
// Area chart - Traffic trend
areaChart: {
xAxis: Array.from({ length: 24 }, (_, i) => `${i}:00`),
series: [
{
name: 'Visits',
data: Array.from({ length: 24 }, () => faker.number.int({ min: 100, max: 1000 })),
},
],
},
};

View File

@ -0,0 +1,224 @@
import type { Department } from '@/types/dept';
/**
*
*/
export const departments: Department[] = [
{
id: '1',
name: '总公司',
parentId: null,
leader: '张总',
phone: '13800000001',
email: 'ceo@example.com',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// 一级部门
{
id: '10',
name: '技术研发部',
parentId: '1',
leader: '李工',
phone: '13800000010',
email: 'tech@example.com',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '11',
name: '产品设计部',
parentId: '1',
leader: '王设计',
phone: '13800000011',
email: 'design@example.com',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '12',
name: '市场营销部',
parentId: '1',
leader: '赵市场',
phone: '13800000012',
email: 'market@example.com',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '13',
name: '人力资源部',
parentId: '1',
leader: '孙HR',
phone: '13800000013',
email: 'hr@example.com',
sort: 4,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '14',
name: '财务部',
parentId: '1',
leader: '周财务',
phone: '13800000014',
email: 'finance@example.com',
sort: 5,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// 二级部门 - 技术研发部下属
{
id: '101',
name: '前端开发组',
parentId: '10',
leader: '陈前端',
phone: '13800000101',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '102',
name: '后端开发组',
parentId: '10',
leader: '刘后端',
phone: '13800000102',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '103',
name: '测试组',
parentId: '10',
leader: '吴测试',
phone: '13800000103',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '104',
name: '运维组',
parentId: '10',
leader: '郑运维',
phone: '13800000104',
sort: 4,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// 二级部门 - 产品设计部下属
{
id: '111',
name: 'UI设计组',
parentId: '11',
leader: '钱UI',
phone: '13800000111',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '112',
name: '产品策划组',
parentId: '11',
leader: '冯策划',
phone: '13800000112',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// 二级部门 - 市场营销部下属
{
id: '121',
name: '品牌推广组',
parentId: '12',
leader: '韩品牌',
phone: '13800000121',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '122',
name: '渠道销售组',
parentId: '12',
leader: '杨销售',
phone: '13800000122',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
// 二级部门 - 人力资源部下属
{
id: '131',
name: '招聘组',
parentId: '13',
leader: '朱招聘',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-01-01 00:00:00',
},
{
id: '132',
name: '培训组',
parentId: '13',
leader: '秦培训',
sort: 2,
status: 'disabled',
remark: '暂停运营',
createTime: '2024-01-01 00:00:00',
updateTime: '2024-06-01 00:00:00',
},
];
/**
*
*/
export function buildDeptTree(list: Department[]): Department[] {
const map = new Map<string, Department>();
const roots: Department[] = [];
list.forEach((item) => {
map.set(item.id, { ...item, children: [] });
});
map.forEach((item) => {
if (item.parentId && map.has(item.parentId)) {
map.get(item.parentId)!.children!.push(item);
} else if (!item.parentId) {
roots.push(item);
}
});
// 递归排序
const sortTree = (nodes: Department[]) => {
nodes.sort((a, b) => a.sort - b.sort);
nodes.forEach((n) => {
if (n.children?.length) sortTree(n.children);
else delete n.children;
});
};
sortTree(roots);
return roots;
}

View File

@ -0,0 +1,283 @@
import type { DictType, DictData } from '@/types/dict';
/**
*
*/
export const dictTypes: DictType[] = [
{
id: '1',
name: '用户状态',
code: 'user_status',
description: '用户账号状态',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '2',
name: '性别',
code: 'gender',
description: '用户性别',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '3',
name: '订单状态',
code: 'order_status',
description: '订单状态',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '4',
name: '支付方式',
code: 'payment_method',
description: '支付方式',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '5',
name: '通知类型',
code: 'notification_type',
description: '系统通知类型',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '6',
name: '资产状态',
code: 'asset_status',
description: '资产状态(在库/在用/维修中/已报废)',
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
];
/**
*
*/
export const dictData: DictData[] = [
// 用户状态
{
id: '1',
typeCode: 'user_status',
label: '正常',
value: 'active',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '2',
typeCode: 'user_status',
label: '禁用',
value: 'inactive',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '3',
typeCode: 'user_status',
label: '锁定',
value: 'locked',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
// 性别
{
id: '4',
typeCode: 'gender',
label: '男',
value: 'male',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '5',
typeCode: 'gender',
label: '女',
value: 'female',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '6',
typeCode: 'gender',
label: '未知',
value: 'unknown',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
// 订单状态
{
id: '7',
typeCode: 'order_status',
label: '待支付',
value: 'pending',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '8',
typeCode: 'order_status',
label: '已支付',
value: 'paid',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '9',
typeCode: 'order_status',
label: '配送中',
value: 'shipping',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '10',
typeCode: 'order_status',
label: '已完成',
value: 'completed',
sort: 4,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '11',
typeCode: 'order_status',
label: '已取消',
value: 'cancelled',
sort: 5,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
// 支付方式
{
id: '12',
typeCode: 'payment_method',
label: '支付宝',
value: 'alipay',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '13',
typeCode: 'payment_method',
label: '微信支付',
value: 'wechat',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '14',
typeCode: 'payment_method',
label: '银行卡',
value: 'bank_card',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
// 通知类型
{
id: '15',
typeCode: 'notification_type',
label: '系统通知',
value: 'system',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '16',
typeCode: 'notification_type',
label: '订单通知',
value: 'order',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '17',
typeCode: 'notification_type',
label: '活动通知',
value: 'activity',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
// 资产状态
{
id: '18',
typeCode: 'asset_status',
label: '在库',
value: '1',
sort: 1,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '19',
typeCode: 'asset_status',
label: '在用',
value: '2',
sort: 2,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '20',
typeCode: 'asset_status',
label: '维修中',
value: '3',
sort: 3,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
{
id: '21',
typeCode: 'asset_status',
label: '已报废',
value: '4',
sort: 4,
status: 'enabled',
createTime: '2024-01-01 10:00:00',
updateTime: '2024-01-01 10:00:00',
},
];

View File

@ -0,0 +1,79 @@
import type { SysFile } from '@/types/file';
const exts = ['jpg', 'png', 'pdf', 'docx', 'xlsx', 'zip', 'mp4', 'txt', 'pptx', 'svg'];
const mimeMap: Record<string, string> = {
jpg: 'image/jpeg',
png: 'image/png',
pdf: 'application/pdf',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
zip: 'application/zip',
mp4: 'video/mp4',
txt: 'text/plain',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
svg: 'image/svg+xml',
};
const storages: SysFile['storage'][] = ['local', 'oss', 'cos'];
const uploaders = ['admin', 'zhangsan', 'lisi', 'wangwu'];
const fileNames: Record<string, string[]> = {
jpg: ['产品封面图', '用户头像', '活动海报', '团队合照', '办公环境'],
png: ['系统Logo', '二维码', '图标素材', '截图', '水印模板'],
pdf: ['用户手册', '合同模板', '年度报告', '技术文档', '发票'],
docx: ['需求文档', '会议纪要', '工作总结', '项目方案', '操作指南'],
xlsx: ['员工花名册', '财务报表', '数据统计', '考勤记录', '库存清单'],
zip: ['项目源码', '资源包', '备份文件', '部署包', '日志归档'],
mp4: ['产品演示', '培训视频', '操作教程', '宣传片', '会议录像'],
txt: ['配置说明', '更新日志', '临时笔记', '导入模板', '错误日志'],
pptx: ['季度汇报', '产品介绍', '培训课件', '方案演示', '年终总结'],
svg: ['图标文件', '流程图', '架构图', '组织结构图', '数据图表'],
};
function randomDate(start: string, end: string) {
const s = new Date(start).getTime();
const e = new Date(end).getTime();
const d = new Date(s + Math.random() * (e - s));
return d.toISOString().replace('T', ' ').slice(0, 19);
}
function randomSize(ext: string): number {
const ranges: Record<string, [number, number]> = {
jpg: [50_000, 5_000_000],
png: [20_000, 3_000_000],
pdf: [100_000, 20_000_000],
docx: [30_000, 10_000_000],
xlsx: [20_000, 15_000_000],
zip: [500_000, 100_000_000],
mp4: [5_000_000, 500_000_000],
txt: [100, 500_000],
pptx: [200_000, 50_000_000],
svg: [1_000, 200_000],
};
const [min, max] = ranges[ext] || [1000, 1_000_000];
return Math.floor(min + Math.random() * (max - min));
}
export const sysFiles: SysFile[] = [];
let id = 1;
for (const ext of exts) {
const names = fileNames[ext];
for (const name of names) {
const storage = storages[Math.floor(Math.random() * storages.length)];
const originalName = `${name}.${ext}`;
sysFiles.push({
id: String(id++),
name: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`,
originalName,
path: `/uploads/${ext}/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`,
size: randomSize(ext),
mimeType: mimeMap[ext] || 'application/octet-stream',
ext,
storage,
uploader: uploaders[Math.floor(Math.random() * uploaders.length)],
remark: '',
createTime: randomDate('2024-01-01', '2024-12-31'),
});
}
}
sysFiles.sort((a, b) => b.createTime.localeCompare(a.createTime));

View File

@ -0,0 +1,128 @@
import type { OperationLog, LoginLog } from '@/types/log';
const modules = [
'userManagement',
'roleManagement',
'menuManagement',
'dictionary',
'systemLogin',
'profile',
'dashboard',
];
const actions: OperationLog['action'][] = [
'login',
'logout',
'create',
'update',
'delete',
'export',
'other',
];
const browsers = ['Chrome 120', 'Firefox 121', 'Safari 17', 'Edge 120'];
const osList = ['Windows 11', 'macOS 14', 'Ubuntu 22.04', 'iOS 17'];
const ips = [
'192.168.1.100',
'192.168.1.101',
'10.0.0.50',
'172.16.0.88',
'192.168.2.200',
'10.10.1.33',
];
const usernames = ['admin', 'user', 'zhangsan', 'lisi', 'wangwu'];
const actionDescMap: Record<string, string[]> = {
login: ['System login'],
logout: ['System logout'],
create: ['Create user', 'Create role', 'Create menu', 'Create dict type', 'Create dict data'],
update: [
'Update user info',
'Update role permissions',
'Update menu config',
'Update dict data',
'Update profile',
'Reset user password',
],
delete: ['Delete user', 'Delete role', 'Delete menu', 'Delete dict data'],
export: ['Export user list', 'Export role list', 'Export operation log'],
other: ['View dashboard', 'Refresh cache'],
};
const actionUrlMap: Record<string, string[]> = {
login: ['/api/auth/login'],
logout: ['/api/auth/logout'],
create: ['/api/user', '/api/role', '/api/permission', '/api/dict/type', '/api/dict/data'],
update: ['/api/user/1', '/api/role/1', '/api/permission/1', '/api/dict/data/1', '/api/profile'],
delete: ['/api/user/1', '/api/role/1', '/api/permission/1', '/api/dict/data/1'],
export: ['/api/user/export', '/api/role/export', '/api/log/export'],
other: ['/api/dashboard/stats', '/api/cache/refresh'],
};
function randomItem<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
function generateTime(daysAgo: number): string {
const d = new Date();
d.setDate(d.getDate() - daysAgo);
d.setHours(Math.floor(Math.random() * 14) + 8);
d.setMinutes(Math.floor(Math.random() * 60));
d.setSeconds(Math.floor(Math.random() * 60));
return d.toISOString().replace('T', ' ').slice(0, 19);
}
export const operationLogs: OperationLog[] = [];
for (let i = 1; i <= 80; i++) {
const action = randomItem(actions);
const descs = actionDescMap[action];
const urls = actionUrlMap[action];
const isFail = Math.random() < 0.08;
operationLogs.push({
id: String(i),
username: randomItem(usernames),
module: randomItem(modules),
action,
description: randomItem(descs),
method:
action === 'create'
? 'POST'
: action === 'update'
? 'PUT'
: action === 'delete'
? 'DELETE'
: 'GET',
url: randomItem(urls),
ip: randomItem(ips),
browser: randomItem(browsers),
os: randomItem(osList),
status: isFail ? 'fail' : 'success',
errorMsg: isFail ? 'Insufficient permissions' : undefined,
duration: Math.floor(Math.random() * 500) + 10,
createTime: generateTime(Math.floor(i / 6)),
});
}
operationLogs.sort((a, b) => b.createTime.localeCompare(a.createTime));
export const loginLogs: LoginLog[] = [];
const loginMessages = [
'Login successful',
'Login successful',
'Login successful',
'Wrong password',
'Account locked',
'Captcha error',
];
for (let i = 1; i <= 50; i++) {
const msg = randomItem(loginMessages);
const isSuccess = msg === 'Login successful';
loginLogs.push({
id: String(i),
username: randomItem(usernames),
ip: randomItem(ips),
browser: randomItem(browsers),
os: randomItem(osList),
status: isSuccess ? 'success' : 'fail',
message: msg,
createTime: generateTime(Math.floor(i / 4)),
});
}
loginLogs.sort((a, b) => b.createTime.localeCompare(a.createTime));

View File

@ -0,0 +1,607 @@
import type { Permission } from '@/types/auth';
export const mockPermissions: Permission[] = [
// Dashboard Menu
{
id: '1',
name: 'Dashboard',
code: 'dashboard.view',
description: 'Dashboard menu',
resource: '/dashboard',
action: 'view',
type: 'menu',
path: '/dashboard',
component: 'dashboard/index',
icon: 'DashboardOutlined',
sort: 1,
status: 'active',
visible: true,
},
// Organization & Permissions Menu
{
id: '60',
name: 'Organization & Permissions',
code: 'organization.menu',
description: 'Organization and permissions root menu',
resource: '/organization',
action: '*',
type: 'menu',
path: '/organization',
component: 'Layout',
icon: 'TeamOutlined',
sort: 2,
status: 'active',
visible: true,
children: [
{
id: '50',
name: 'Department Management',
code: 'system.dept.view',
description: 'Department management menu',
resource: '/organization/dept',
action: 'view',
type: 'menu',
parentId: '60',
path: '/organization/dept',
component: 'system/dept/index',
icon: 'ApartmentOutlined',
sort: 1,
status: 'active',
visible: true,
children: [
{
id: '51',
name: 'Create Department',
code: 'system.dept.create',
description: 'Can create department',
resource: 'system.dept',
action: 'create',
type: 'button',
parentId: '50',
status: 'active',
visible: true,
},
{
id: '52',
name: 'Edit Department',
code: 'system.dept.edit',
description: 'Can edit department',
resource: 'system.dept',
action: 'edit',
type: 'button',
parentId: '50',
status: 'active',
visible: true,
},
{
id: '53',
name: 'Delete Department',
code: 'system.dept.delete',
description: 'Can delete department',
resource: 'system.dept',
action: 'delete',
type: 'button',
parentId: '50',
status: 'active',
visible: true,
},
],
},
{
id: '11',
name: 'User Management',
code: 'system.user.view',
description: 'User management menu',
resource: '/organization/user',
action: 'view',
type: 'menu',
parentId: '60',
path: '/organization/user',
component: 'system/user/index',
icon: 'UserOutlined',
sort: 2,
status: 'active',
visible: true,
children: [
{
id: '12',
name: 'Create User',
code: 'system.user.create',
description: 'Can create users',
resource: 'system.user',
action: 'create',
type: 'button',
parentId: '11',
status: 'active',
visible: true,
},
{
id: '13',
name: 'Edit User',
code: 'system.user.edit',
description: 'Can edit users',
resource: 'system.user',
action: 'edit',
type: 'button',
parentId: '11',
status: 'active',
visible: true,
},
{
id: '14',
name: 'Delete User',
code: 'system.user.delete',
description: 'Can delete users',
resource: 'system.user',
action: 'delete',
type: 'button',
parentId: '11',
status: 'active',
visible: true,
},
],
},
{
id: '20',
name: 'Role Management',
code: 'system.role.view',
description: 'Role management menu',
resource: '/organization/role',
action: 'view',
type: 'menu',
parentId: '60',
path: '/organization/role',
component: 'system/role/index',
icon: 'TeamOutlined',
sort: 3,
status: 'active',
visible: true,
children: [
{
id: '21',
name: 'Create Role',
code: 'system.role.create',
description: 'Can create roles',
resource: 'system.role',
action: 'create',
type: 'button',
parentId: '20',
status: 'active',
visible: true,
},
{
id: '22',
name: 'Edit Role',
code: 'system.role.edit',
description: 'Can edit roles',
resource: 'system.role',
action: 'edit',
type: 'button',
parentId: '20',
status: 'active',
visible: true,
},
{
id: '23',
name: 'Delete Role',
code: 'system.role.delete',
description: 'Can delete roles',
resource: 'system.role',
action: 'delete',
type: 'button',
parentId: '20',
status: 'active',
visible: true,
},
],
},
{
id: '30',
name: 'Menu Management',
code: 'system.permission.view',
description: 'Menu management menu',
resource: '/organization/permission',
action: 'view',
type: 'menu',
parentId: '60',
path: '/organization/permission',
component: 'system/permission/index',
icon: 'SafetyOutlined',
sort: 4,
status: 'active',
visible: true,
children: [
{
id: '31',
name: 'Create Menu',
code: 'system.permission.create',
description: 'Can create menu',
resource: 'system.permission',
action: 'create',
type: 'button',
parentId: '30',
status: 'active',
visible: true,
},
{
id: '32',
name: 'Edit Menu',
code: 'system.permission.edit',
description: 'Can edit menu',
resource: 'system.permission',
action: 'edit',
type: 'button',
parentId: '30',
status: 'active',
visible: true,
},
{
id: '33',
name: 'Delete Menu',
code: 'system.permission.delete',
description: 'Can delete menu',
resource: 'system.permission',
action: 'delete',
type: 'button',
parentId: '30',
status: 'active',
visible: true,
},
],
},
],
},
// System Management Menu
{
id: '10',
name: 'System Management',
code: 'system.menu',
description: 'System management root menu',
resource: '/system',
action: '*',
type: 'menu',
path: '/system',
component: 'Layout',
icon: 'SettingOutlined',
sort: 3,
status: 'active',
visible: true,
children: [
{
id: '54',
name: 'System Config',
code: 'system.config.view',
description: 'System config menu',
resource: '/system/config',
action: 'view',
type: 'menu',
parentId: '10',
path: '/system/config',
component: 'system/config/index',
icon: 'ControlOutlined',
sort: 1,
status: 'active',
visible: true,
children: [
{
id: '55',
name: 'Create Config',
code: 'system.config.create',
description: 'Can create config',
resource: 'system.config',
action: 'create',
type: 'button',
parentId: '54',
status: 'active',
visible: true,
},
{
id: '56',
name: 'Edit Config',
code: 'system.config.edit',
description: 'Can edit config',
resource: 'system.config',
action: 'edit',
type: 'button',
parentId: '54',
status: 'active',
visible: true,
},
{
id: '57',
name: 'Delete Config',
code: 'system.config.delete',
description: 'Can delete config',
resource: 'system.config',
action: 'delete',
type: 'button',
parentId: '54',
status: 'active',
visible: true,
},
],
},
{
id: '34',
name: 'Dictionary Management',
code: 'system.dict.view',
description: 'Dictionary management menu',
resource: '/system/dict',
action: 'view',
type: 'menu',
parentId: '10',
path: '/system/dict',
component: 'system/dict/index',
icon: 'BookOutlined',
sort: 2,
status: 'active',
visible: true,
children: [
{
id: '35',
name: 'Create Dictionary',
code: 'system.dict.create',
description: 'Can create dictionary',
resource: 'system.dict',
action: 'create',
type: 'button',
parentId: '34',
status: 'active',
visible: true,
},
{
id: '36',
name: 'Edit Dictionary',
code: 'system.dict.edit',
description: 'Can edit dictionary',
resource: 'system.dict',
action: 'edit',
type: 'button',
parentId: '34',
status: 'active',
visible: true,
},
{
id: '37',
name: 'Delete Dictionary',
code: 'system.dict.delete',
description: 'Can delete dictionary',
resource: 'system.dict',
action: 'delete',
type: 'button',
parentId: '34',
status: 'active',
visible: true,
},
],
},
{
id: '58',
name: 'File Management',
code: 'system.file.view',
description: 'File management menu',
resource: '/system/file',
action: 'view',
type: 'menu',
parentId: '10',
path: '/system/file',
component: 'system/file/index',
icon: 'FolderOutlined',
sort: 3,
status: 'active',
visible: true,
children: [
{
id: '59',
name: 'Delete File',
code: 'system.file.delete',
description: 'Can delete file',
resource: 'system.file',
action: 'delete',
type: 'button',
parentId: '58',
status: 'active',
visible: true,
},
],
},
{
id: '38',
name: 'System Log',
code: 'system.log.view',
description: 'System log menu',
resource: '/system/log',
action: 'view',
type: 'menu',
parentId: '10',
path: '/system/log',
component: 'system/log/index',
icon: 'FileTextOutlined',
sort: 4,
status: 'active',
visible: true,
children: [
{
id: '39',
name: 'Clear Log',
code: 'system.log.clear',
description: 'Can clear logs',
resource: 'system.log',
action: 'delete',
type: 'button',
parentId: '38',
status: 'active',
visible: true,
},
],
},
],
},
// Examples Menu
{
id: '40',
name: 'Examples',
code: 'examples.menu',
description: 'Examples root menu',
resource: '/examples',
action: '*',
type: 'menu',
path: '/examples',
component: 'Layout',
icon: 'AppstoreOutlined',
sort: 4,
status: 'active',
visible: true,
children: [
{
id: '41',
name: 'Table Example',
code: 'examples.table.view',
description: 'Table example menu',
resource: '/examples/table',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/table',
component: 'examples/table/index',
icon: 'TableOutlined',
sort: 1,
status: 'active',
visible: true,
},
{
id: '42',
name: 'Icon Example',
code: 'examples.icon.view',
description: 'Icon example menu',
resource: '/examples/icon',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/icon',
component: 'examples/icon/index',
icon: 'SmileOutlined',
sort: 2,
status: 'active',
visible: true,
},
{
id: '43',
name: 'Form Example',
code: 'examples.form.view',
description: 'Form example menu',
resource: '/examples/form',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/form',
component: 'examples/form/index',
icon: 'FormOutlined',
sort: 3,
status: 'active',
visible: true,
},
{
id: '44',
name: 'Modal Example',
code: 'examples.modal.view',
description: 'Modal example menu',
resource: '/examples/modal',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/modal',
component: 'examples/modal/index',
icon: 'ExpandOutlined',
sort: 4,
status: 'active',
visible: true,
},
{
id: '45',
name: 'Watermark Example',
code: 'examples.watermark.view',
description: 'Watermark example menu',
resource: '/examples/watermark',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/watermark',
component: 'examples/watermark/index',
icon: 'HighlightOutlined',
sort: 5,
status: 'active',
visible: true,
},
{
id: '46',
name: 'Exception Page',
code: 'examples.exception.menu',
description: 'Exception root menu',
resource: '/examples/exception',
action: 'view',
type: 'menu',
parentId: '40',
path: '/examples/exception',
component: 'RouteView',
icon: 'WarningOutlined',
sort: 6,
status: 'active',
visible: true,
children: [
{
id: '47',
name: '403',
code: 'examples.exception.403.view',
description: '403 exception page menu',
resource: '/examples/exception/403',
action: 'view',
type: 'menu',
parentId: '46',
path: '/examples/exception/403',
component: 'examples/exception/403',
icon: 'StopOutlined',
sort: 1,
status: 'active',
visible: true,
},
{
id: '48',
name: '404',
code: 'examples.exception.404.view',
description: '404 exception page menu',
resource: '/examples/exception/404',
action: 'view',
type: 'menu',
parentId: '46',
path: '/examples/exception/404',
component: 'examples/exception/404',
icon: 'FileUnknownOutlined',
sort: 2,
status: 'active',
visible: true,
},
{
id: '49',
name: '500',
code: 'examples.exception.500.view',
description: '500 exception page menu',
resource: '/examples/exception/500',
action: 'view',
type: 'menu',
parentId: '46',
path: '/examples/exception/500',
component: 'examples/exception/500',
icon: 'BugOutlined',
sort: 3,
status: 'active',
visible: true,
},
],
},
],
},
];

View File

@ -0,0 +1,40 @@
import type { Role } from '@/types/auth';
export const mockRoles: Role[] = [
{
id: '1',
name: 'Administrator',
code: 'admin',
description: 'System administrator with full access',
permissions: [],
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
{
id: '2',
name: 'Manager',
code: 'manager',
description: 'Department manager with management permissions',
permissions: [],
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
{
id: '3',
name: 'User',
code: 'user',
description: 'Regular user with basic permissions',
permissions: [],
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
{
id: '4',
name: 'Guest',
code: 'guest',
description: 'Guest user with read-only access',
permissions: [],
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
];

View File

@ -0,0 +1,103 @@
import type { User } from "@/types/auth";
import { faker } from "@faker-js/faker";
// Generate mock users
export const mockUsers: User[] = Array.from({ length: 50 }, () => ({
id: faker.string.uuid(),
username: faker.internet.username(),
email: faker.internet.email(),
realName: faker.person.fullName(),
avatar: faker.image.avatar(),
phone: `1${faker.string.numeric(10)}`,
gender: faker.helpers.arrayElement(["male", "female"] as const),
birthDate: faker.date
.birthdate({ min: 18, max: 65, mode: "age" })
.toISOString()
.split("T")[0],
bio: faker.person.bio(),
status: faker.helpers.arrayElement(["active", "inactive"] as const),
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
roles: [],
permissions: [],
}));
// Admin user
export const adminUser: User = {
id: "1",
username: "admin",
email: "admin@example.com",
realName: "Administrator",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=admin",
phone: "13800138000",
gender: "male",
birthDate: "1990-01-01",
bio: "System Administrator",
status: "active",
createdAt: "2023-01-01T00:00:00.000Z",
updatedAt: new Date().toISOString(),
roles: [
{
id: "1",
name: "Administrator",
code: "admin",
description: "System Administrator",
permissions: [],
createdAt: "2023-01-01T00:00:00.000Z",
updatedAt: "2023-01-01T00:00:00.000Z",
},
],
permissions: [
{
id: "1",
name: "All Permissions",
code: "*",
description: "Has all permissions",
resource: "*",
action: "*",
type: "api",
},
],
};
// Regular user
export const regularUser: User = {
id: "2",
username: "user",
email: "user@example.com",
realName: "Regular User",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=user",
phone: "13800138001",
gender: "female",
birthDate: "1995-05-15",
bio: "Regular User",
status: "active",
createdAt: "2023-01-01T00:00:00.000Z",
updatedAt: new Date().toISOString(),
roles: [
{
id: "2",
name: "User",
code: "user",
description: "Regular User",
permissions: [],
createdAt: "2023-01-01T00:00:00.000Z",
updatedAt: "2023-01-01T00:00:00.000Z",
},
],
permissions: [
{
id: "2",
name: "View Dashboard",
code: "dashboard.view",
description: "Can view dashboard",
resource: "dashboard",
action: "view",
type: "menu",
},
],
};
// Add admin and regular users to the beginning of the array
mockUsers.unshift(adminUser, regularUser);

View File

@ -0,0 +1,113 @@
import { defineMock } from 'vite-plugin-mock-dev-server';
import { adminUser, regularUser } from '../data/users.data';
export default defineMock([
// Login
{
url: '/api/auth/login',
method: 'POST',
body: (req) => {
const { username, password } = req.body;
// Validate credentials
let user = null;
if (username === 'admin' && password === '123456') {
user = adminUser;
} else if (username === 'user' && password === '123456') {
user = regularUser;
}
if (user) {
return {
code: 200,
message: 'Login successful',
data: {
token: `mock-token-${user.id}-${Date.now()}`,
refreshToken: `mock-refresh-token-${user.id}-${Date.now()}`,
expiresIn: 7200,
},
success: true,
};
} else {
return {
code: 401,
message: 'Invalid username or password',
data: null,
success: false,
};
}
},
},
// Logout
{
url: '/api/auth/logout',
method: 'POST',
body: {
code: 200,
message: 'Logout successful',
data: null,
success: true,
},
},
// Get user info
{
url: '/api/auth/info',
method: 'GET',
body: (req) => {
// Get token from header
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return {
code: 401,
message: 'Unauthorized',
data: null,
success: false,
};
}
// Extract user ID from token
const userId = token.split('-')[2];
const user = userId === '1' ? adminUser : regularUser;
return {
code: 200,
message: 'Success',
data: user,
success: true,
};
},
},
// Refresh token
{
url: '/api/auth/refresh',
method: 'POST',
body: (req) => {
const { refreshToken } = req.body;
if (refreshToken) {
return {
code: 200,
message: 'Token refreshed',
data: {
token: `new-mock-token-${Date.now()}`,
refreshToken: `new-mock-refresh-token-${Date.now()}`,
expiresIn: 7200,
},
success: true,
};
} else {
return {
code: 401,
message: 'Invalid refresh token',
data: null,
success: false,
};
}
},
},
]);

View File

@ -0,0 +1,114 @@
import type { SysConfig } from "@/types/config";
import { defineMock } from "vite-plugin-mock-dev-server";
import { sysConfigs } from "../data/config.data";
export default defineMock([
{
url: "/api/config/list",
method: "GET",
body: (req) => {
const { name, key, group, page = 1, pageSize = 20 } = req.query;
let filtered = [...sysConfigs];
if (name)
filtered = filtered.filter((item) =>
item.name.includes(name as string),
);
if (key)
filtered = filtered.filter((item) => item.key.includes(key as string));
if (group) filtered = filtered.filter((item) => item.group === group);
filtered.sort((a, b) => a.sort - b.sort);
const start = (Number(page) - 1) * Number(pageSize);
const list = filtered.slice(start, start + Number(pageSize));
return {
code: 200,
message: "success",
success: true,
data: {
list,
total: filtered.length,
current: Number(page),
pageSize: Number(pageSize),
},
};
},
},
{
url: "/api/config/key/:key",
method: "GET",
body: (req) => {
const item = sysConfigs.find((c) => c.key === req.params.key);
return item
? { code: 200, message: "success", success: true, data: item }
: { code: 404, message: "Config not found", success: false };
},
},
{
url: "/api/config",
method: "POST",
body: (req) => {
const exists = sysConfigs.find((c) => c.key === req.body.key);
if (exists)
return {
code: 400,
message: "Config key already exists",
success: false,
};
const newConfig: SysConfig = {
id: String(Date.now()),
...req.body,
builtIn: false,
createTime: new Date().toISOString().replace("T", " ").slice(0, 19),
updateTime: new Date().toISOString().replace("T", " ").slice(0, 19),
};
sysConfigs.push(newConfig);
return { code: 200, message: "success", success: true, data: newConfig };
},
},
{
url: "/api/config/:id",
method: "PUT",
body: (req) => {
const index = sysConfigs.findIndex((item) => item.id === req.params.id);
if (index !== -1) {
sysConfigs[index] = {
...sysConfigs[index],
...req.body,
updateTime: new Date().toISOString().replace("T", " ").slice(0, 19),
};
return {
code: 200,
message: "success",
success: true,
data: sysConfigs[index],
};
}
return { code: 404, message: "Config not found", success: false };
},
},
{
url: "/api/config/:id",
method: "DELETE",
body: (req) => {
const index = sysConfigs.findIndex((item) => item.id === req.params.id);
if (index === -1)
return { code: 404, message: "Config not found", success: false };
if (sysConfigs[index].builtIn)
return {
code: 400,
message: "Built-in config cannot be deleted",
success: false,
};
sysConfigs.splice(index, 1);
return { code: 200, message: "success", success: true };
},
},
]);

View File

@ -0,0 +1,71 @@
import { defineMock } from 'vite-plugin-mock-dev-server';
import {
mockStats,
mockSalesTrend,
mockUserDistribution,
mockActivities,
mockChartData,
} from '../data/dashboard.data';
export default defineMock([
// Get statistics
{
url: '/api/dashboard/stats',
method: 'GET',
body: {
code: 200,
message: 'Success',
data: mockStats,
success: true,
},
},
// Get sales trend
{
url: '/api/dashboard/sales-trend',
method: 'GET',
body: {
code: 200,
message: 'Success',
data: mockSalesTrend,
success: true,
},
},
// Get user distribution
{
url: '/api/dashboard/user-distribution',
method: 'GET',
body: {
code: 200,
message: 'Success',
data: mockUserDistribution,
success: true,
},
},
// Get recent activities
{
url: '/api/dashboard/activities',
method: 'GET',
body: {
code: 200,
message: 'Success',
data: mockActivities,
success: true,
},
},
// Get chart data
{
url: '/api/dashboard/chart-data',
method: 'GET',
body: {
code: 200,
message: 'Success',
data: mockChartData,
success: true,
},
},
]);

View File

@ -0,0 +1,121 @@
import type { Department } from "@/types/dept";
import { defineMock } from "vite-plugin-mock-dev-server";
import { departments, buildDeptTree } from "../data/dept.data";
export default defineMock([
// 获取部门树
{
url: "/api/dept/tree",
method: "GET",
body: (req) => {
const { name, status } = req.query;
let filtered = [...departments];
if (name) {
filtered = filtered.filter((item) =>
item.name.includes(name as string),
);
}
if (status) {
filtered = filtered.filter((item) => item.status === status);
}
return {
code: 200,
message: "success",
success: true,
data: buildDeptTree(filtered),
};
},
},
// 获取部门列表(扁平)
{
url: "/api/dept/list",
method: "GET",
body: (req) => {
const { name, status } = req.query;
let filtered = [...departments];
if (name) {
filtered = filtered.filter((item) =>
item.name.includes(name as string),
);
}
if (status) {
filtered = filtered.filter((item) => item.status === status);
}
filtered.sort((a, b) => a.sort - b.sort);
return {
code: 200,
message: "success",
success: true,
data: filtered,
};
},
},
// 创建部门
{
url: "/api/dept",
method: "POST",
body: (req) => {
const newDept: Department = {
id: String(Date.now()),
...req.body,
createTime: new Date().toISOString().replace("T", " ").slice(0, 19),
updateTime: new Date().toISOString().replace("T", " ").slice(0, 19),
};
departments.push(newDept);
return { code: 200, message: "创建成功", success: true, data: newDept };
},
},
// 更新部门
{
url: "/api/dept/:id",
method: "PUT",
body: (req) => {
const { id } = req.params;
const index = departments.findIndex((item) => item.id === id);
if (index !== -1) {
departments[index] = {
...departments[index],
...req.body,
updateTime: new Date().toISOString().replace("T", " ").slice(0, 19),
};
return {
code: 200,
message: "更新成功",
success: true,
data: departments[index],
};
}
return { code: 404, message: "部门不存在", success: false };
},
},
// 删除部门
{
url: "/api/dept/:id",
method: "DELETE",
body: (req) => {
const { id } = req.params;
// 检查是否有子部门
const hasChildren = departments.some((item) => item.parentId === id);
if (hasChildren) {
return { code: 400, message: "存在子部门,无法删除", success: false };
}
const index = departments.findIndex((item) => item.id === id);
if (index !== -1) {
departments.splice(index, 1);
return { code: 200, message: "删除成功", success: true };
}
return { code: 404, message: "部门不存在", success: false };
},
},
]);

View File

@ -0,0 +1,289 @@
import type { DictType, DictData } from "@/types/dict";
import { defineMock } from "vite-plugin-mock-dev-server";
import { dictTypes, dictData } from "../data/dict.data";
export default defineMock([
// 获取所有字典类型
{
url: "/api/dict/types",
method: "GET",
body: () => {
return {
code: 200,
message: "success",
success: true,
data: dictTypes,
};
},
},
// 获取字典类型列表(分页)
{
url: "/api/dict/type/list",
method: "GET",
body: (req) => {
const { name, code, status, page = 1, pageSize = 10 } = req.query;
let filtered = [...dictTypes];
if (name) {
filtered = filtered.filter((item) =>
item.name.includes(name as string),
);
}
if (code) {
filtered = filtered.filter((item) =>
item.code.includes(code as string),
);
}
if (status) {
filtered = filtered.filter((item) => item.status === status);
}
const start = (Number(page) - 1) * Number(pageSize);
const end = start + Number(pageSize);
const list = filtered.slice(start, end);
return {
code: 200,
message: "success",
success: true,
data: {
list,
total: filtered.length,
current: Number(page),
pageSize: Number(pageSize),
},
};
},
},
// 创建字典类型
{
url: "/api/dict/type",
method: "POST",
body: (req) => {
const newType: DictType = {
id: String(Date.now()),
...req.body,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
};
dictTypes.push(newType);
return {
code: 200,
message: "创建成功",
success: true,
data: newType,
};
},
},
// 更新字典类型
{
url: "/api/dict/type/:id",
method: "PUT",
body: (req) => {
const { id } = req.params;
const index = dictTypes.findIndex((item) => item.id === id);
if (index !== -1) {
dictTypes[index] = {
...dictTypes[index],
...req.body,
updateTime: new Date().toISOString(),
};
return {
code: 200,
message: "更新成功",
success: true,
data: dictTypes[index],
};
}
return {
code: 404,
message: "字典类型不存在",
success: false,
};
},
},
// 删除字典类型
{
url: "/api/dict/type/:id",
method: "DELETE",
body: (req) => {
const { id } = req.params;
const index = dictTypes.findIndex((item) => item.id === id);
if (index !== -1) {
dictTypes.splice(index, 1);
return {
code: 200,
message: "删除成功",
success: true,
};
}
return {
code: 404,
message: "字典类型不存在",
success: false,
};
},
},
// 获取所有字典数据
{
url: "/api/dict/data/all",
method: "GET",
body: () => {
return {
code: 200,
message: "success",
success: true,
data: dictData,
};
},
},
// 获取字典数据列表(分页) - 必须在 :typeCode 之前,避免被参数路由匹配
{
url: "/api/dict/data/list",
method: "GET",
body: (req) => {
const {
typeCode,
label,
value,
status,
page = 1,
pageSize = 10,
} = req.query;
let filtered = [...dictData];
if (typeCode) {
filtered = filtered.filter((item) => item.typeCode === typeCode);
}
if (label) {
filtered = filtered.filter((item) =>
item.label.includes(label as string),
);
}
if (value) {
filtered = filtered.filter((item) =>
item.value.includes(value as string),
);
}
if (status) {
filtered = filtered.filter((item) => item.status === status);
}
const start = (Number(page) - 1) * Number(pageSize);
const end = start + Number(pageSize);
const list = filtered.slice(start, end);
return {
code: 200,
message: "success",
success: true,
data: {
list,
total: filtered.length,
current: Number(page),
pageSize: Number(pageSize),
},
};
},
},
// 根据类型获取字典数据
{
url: "/api/dict/data/:typeCode",
method: "GET",
body: (req) => {
const { typeCode } = req.params;
const filtered = dictData.filter(
(item) => item.typeCode === typeCode && item.status === "enabled",
);
return {
code: 200,
message: "success",
success: true,
data: filtered,
};
},
},
// 创建字典数据
{
url: "/api/dict/data",
method: "POST",
body: (req) => {
const newData: DictData = {
id: String(Date.now()),
...req.body,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
};
dictData.push(newData);
return {
code: 200,
message: "创建成功",
success: true,
data: newData,
};
},
},
// 更新字典数据
{
url: "/api/dict/data/:id",
method: "PUT",
body: (req) => {
const { id } = req.params;
const index = dictData.findIndex((item) => item.id === id);
if (index !== -1) {
dictData[index] = {
...dictData[index],
...req.body,
updateTime: new Date().toISOString(),
};
return {
code: 200,
message: "更新成功",
success: true,
data: dictData[index],
};
}
return {
code: 404,
message: "字典数据不存在",
success: false,
};
},
},
// 删除字典数据
{
url: "/api/dict/data/:id",
method: "DELETE",
body: (req) => {
const { id } = req.params;
const index = dictData.findIndex((item) => item.id === id);
if (index !== -1) {
dictData.splice(index, 1);
return {
code: 200,
message: "删除成功",
success: true,
};
}
return {
code: 404,
message: "字典数据不存在",
success: false,
};
},
},
]);

View File

@ -0,0 +1,96 @@
import { defineMock } from "vite-plugin-mock-dev-server";
import { sysFiles } from "../data/file.data";
export default defineMock([
{
url: "/api/file/list",
method: "GET",
body: (req) => {
const { name, ext, storage, page = 1, pageSize = 20 } = req.query;
let filtered = [...sysFiles];
if (name)
filtered = filtered.filter((item) =>
item.originalName.includes(name as string),
);
if (ext) filtered = filtered.filter((item) => item.ext === ext);
if (storage)
filtered = filtered.filter((item) => item.storage === storage);
const start = (Number(page) - 1) * Number(pageSize);
const list = filtered.slice(start, start + Number(pageSize));
return {
code: 200,
message: "success",
success: true,
data: {
list,
total: filtered.length,
current: Number(page),
pageSize: Number(pageSize),
},
};
},
},
{
url: "/api/file/:id",
method: "GET",
body: (req) => {
const file = sysFiles.find((item) => item.id === req.params.id);
if (!file) return { code: 404, message: "文件不存在", success: false };
return { code: 200, message: "success", success: true, data: file };
},
},
{
url: "/api/file/upload",
method: "POST",
body: (req) => {
const { originalName, size, mimeType, storage = "local" } = req.body;
if (!originalName) {
return { code: 400, message: "文件名不能为空", success: false };
}
const ext = originalName.includes(".")
? originalName.split(".").pop()
: "";
const newFile = {
id: `file-${Date.now()}`,
originalName,
storedName: `${Date.now()}-${originalName}`,
size: size || 0,
ext: ext || "",
mimeType: mimeType || "application/octet-stream",
storage,
url: `/uploads/${Date.now()}-${originalName}`,
uploader: "admin",
uploadTime: new Date().toISOString().replace("T", " ").slice(0, 19),
};
sysFiles.unshift(newFile);
return {
code: 200,
message: "上传成功",
success: true,
data: newFile,
};
},
},
{
url: "/api/file/:id",
method: "DELETE",
body: (req) => {
const index = sysFiles.findIndex((item) => item.id === req.params.id);
if (index === -1)
return { code: 404, message: "文件不存在", success: false };
sysFiles.splice(index, 1);
return { code: 200, message: "删除成功", success: true };
},
},
]);

Some files were not shown because too many files have changed in this diff Show More