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:
commit
f468d532b1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
111
.claude/skills/fa-crud-page-generator/SKILL.md
Normal file
111
.claude/skills/fa-crud-page-generator/SKILL.md
Normal 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 等)需根据业务自行替换。
|
||||||
614
.claude/skills/fa-crud-page-generator/references/templates.md
Normal file
614
.claude/skills/fa-crud-page-generator/references/templates.md
Normal 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()` |
|
||||||
|
| number(ID) | 自增 `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])` |
|
||||||
135
.claude/skills/fa-feedback/SKILL.md
Normal file
135
.claude/skills/fa-feedback/SKILL.md
Normal 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 参数是否生效都能顺利提交反馈。
|
||||||
|
|
||||||
|
### 第五步:继续协助
|
||||||
|
|
||||||
|
反馈流程完成后,继续协助用户解决当前的问题,不要因为反馈流程中断用户的工作。
|
||||||
88
.claude/skills/fa-form-builder/SKILL.md
Normal file
88
.claude/skills/fa-form-builder/SKILL.md
Normal 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"`。
|
||||||
286
.claude/skills/fa-form-builder/references/templates.md
Normal file
286
.claude/skills/fa-form-builder/references/templates.md
Normal 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 }]) -->
|
||||||
|
|
||||||
|
<!-- FaSwitch(componentField 会传字符串,需手动绑定 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`。
|
||||||
43
.claude/skills/fa-framework-settings/SKILL.md
Normal file
43
.claude/skills/fa-framework-settings/SKILL.md
Normal 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) - 快捷键、切换动画、进度条等
|
||||||
119
.claude/skills/fa-framework-settings/references/app-settings.md
Normal file
119
.claude/skills/fa-framework-settings/references/app-settings.md
Normal 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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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, // 更圆润的界面
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -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',
|
||||||
|
}
|
||||||
|
```
|
||||||
78
.claude/skills/fa-page-optimizer/SKILL.md
Normal file
78
.claude/skills/fa-page-optimizer/SKILL.md
Normal 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 了解完整 API(props、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` 核对。
|
||||||
@ -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` | 替代自定义多级联动下拉选择 |
|
||||||
@ -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 |
|
||||||
108
.claude/skills/fa-route-generator/SKILL.md
Normal file
108
.claude/skills/fa-route-generator/SKILL.md
Normal 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`
|
||||||
459
.claude/skills/fa-route-generator/references/examples.md
Normal file
459
.claude/skills/fa-route-generator/references/examples.md
Normal 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,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
115
.claude/skills/fa-route-generator/references/route-meta.md
Normal file
115
.claude/skills/fa-route-generator/references/route-meta.md
Normal 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 官网
|
||||||
|
```
|
||||||
102
.claude/skills/fa-slot-creator/SKILL.md
Normal file
102
.claude/skills/fa-slot-creator/SKILL.md
Normal 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/` 目录是否存在
|
||||||
258
.claude/skills/fa-slot-creator/references/slot-positions.md
Normal file
258
.claude/skills/fa-slot-creator/references/slot-positions.md
Normal 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
|
||||||
|
```
|
||||||
77
.claude/skills/fa-store-generator/SKILL.md
Normal file
77
.claude/skills/fa-store-generator/SKILL.md
Normal 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)`
|
||||||
178
.claude/skills/fa-store-generator/references/store-patterns.md
Normal file
178
.claude/skills/fa-store-generator/references/store-patterns.md
Normal 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'], // 只持久化指定字段
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 持久化默认使用 localStorage,key 为 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()`
|
||||||
126
.claude/skills/fa-theme-customizer/SKILL.md
Normal file
126
.claude/skills/fa-theme-customizer/SKILL.md
Normal 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 变量
|
||||||
292
.claude/skills/fa-theme-customizer/references/design-styles.md
Normal file
292
.claude/skills/fa-theme-customizer/references/design-styles.md
Normal 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 社区风格参考
|
||||||
|
|
||||||
|
tweakcn(https://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` 粉玫瑰
|
||||||
169
.claude/skills/fa-theme-customizer/references/theme-structure.md
Normal file
169
.claude/skills/fa-theme-customizer/references/theme-structure.md
Normal 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
18
.claude/skills/magic-script/.gitignore
vendored
Normal 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.
|
||||||
21
.claude/skills/magic-script/LICENSE
Normal file
21
.claude/skills/magic-script/LICENSE
Normal 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.
|
||||||
115
.claude/skills/magic-script/README.md
Normal file
115
.claude/skills/magic-script/README.md
Normal 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}` 慎用
|
||||||
154
.claude/skills/magic-script/SKILL.md
Normal file
154
.claude/skills/magic-script/SKILL.md
Normal 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();`
|
||||||
63
.claude/skills/magic-script/references/aggregation.md
Normal file
63
.claude/skills/magic-script/references/aggregation.md
Normal 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"
|
||||||
|
```
|
||||||
128
.claude/skills/magic-script/references/api-integration.md
Normal file
128
.claude/skills/magic-script/references/api-integration.md
Normal 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);
|
||||||
|
```
|
||||||
75
.claude/skills/magic-script/references/array-functions.md
Normal file
75
.claude/skills/magic-script/references/array-functions.md
Normal 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类型的数组
|
||||||
|
```
|
||||||
25
.claude/skills/magic-script/references/class-extensions.md
Normal file
25
.claude/skills/magic-script/references/class-extensions.md
Normal 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
|
||||||
|
```
|
||||||
304
.claude/skills/magic-script/references/collection-extensions.md
Normal file
304
.claude/skills/magic-script/references/collection-extensions.md
Normal 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"}]
|
||||||
|
```
|
||||||
11
.claude/skills/magic-script/references/date-extensions.md
Normal file
11
.claude/skills/magic-script/references/date-extensions.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Date扩展方法
|
||||||
|
|
||||||
|
## format
|
||||||
|
- 入参:`pattern`:`String` 格式
|
||||||
|
- 返回值:`String`
|
||||||
|
- 函数说明:将日期格式化
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var date = new Date();
|
||||||
|
return date.format('yyyy-MM-dd'); // 2020-01-01
|
||||||
|
```
|
||||||
36
.claude/skills/magic-script/references/date-functions.md
Normal file
36
.claude/skills/magic-script/references/date-functions.md
Normal 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;
|
||||||
|
```
|
||||||
22
.claude/skills/magic-script/references/db-cache.md
Normal file
22
.claude/skills/magic-script/references/db-cache.md
Normal 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');
|
||||||
|
```
|
||||||
63
.claude/skills/magic-script/references/db-query.md
Normal file
63
.claude/skills/magic-script/references/db-query.md
Normal 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');
|
||||||
|
```
|
||||||
23
.claude/skills/magic-script/references/db-transaction.md
Normal file
23
.claude/skills/magic-script/references/db-transaction.md
Normal 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(); // 回滚事务
|
||||||
|
}
|
||||||
|
```
|
||||||
49
.claude/skills/magic-script/references/db-update.md
Normal file
49
.claude/skills/magic-script/references/db-update.md
Normal 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()
|
||||||
|
])
|
||||||
|
```
|
||||||
18
.claude/skills/magic-script/references/env-module.md
Normal file
18
.claude/skills/magic-script/references/env-module.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 环境配置模块
|
||||||
|
|
||||||
|
## 引用模块
|
||||||
|
```javascript
|
||||||
|
import env;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
```javascript
|
||||||
|
import env;
|
||||||
|
return env.get('server.port')
|
||||||
|
```
|
||||||
|
|
||||||
|
## get
|
||||||
|
- 入参:`key`:`String` 配置项
|
||||||
|
- 入参:`defaultValue`:`String` 默认值,可省略
|
||||||
|
- 返回值:`String`
|
||||||
|
- 函数说明:获取`Spring`配置项
|
||||||
177
.claude/skills/magic-script/references/faq.md
Normal file
177
.claude/skills/magic-script/references/faq.md
Normal 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')})
|
||||||
|
""";
|
||||||
|
```
|
||||||
97
.claude/skills/magic-script/references/http-module.md
Normal file
97
.claude/skills/magic-script/references/http-module.md
Normal 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()
|
||||||
|
```
|
||||||
42
.claude/skills/magic-script/references/java-integration.md
Normal file
42
.claude/skills/magic-script/references/java-integration.md
Normal 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');
|
||||||
|
```
|
||||||
108
.claude/skills/magic-script/references/keywords.md
Normal file
108
.claude/skills/magic-script/references/keywords.md
Normal 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`
|
||||||
102
.claude/skills/magic-script/references/lambda-async.md
Normal file
102
.claude/skills/magic-script/references/lambda-async.md
Normal 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());
|
||||||
|
```
|
||||||
76
.claude/skills/magic-script/references/linq.md
Normal file
76
.claude/skills/magic-script/references/linq.md
Normal 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
|
||||||
|
```
|
||||||
15
.claude/skills/magic-script/references/log-module.md
Normal file
15
.claude/skills/magic-script/references/log-module.md
Normal 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');
|
||||||
|
```
|
||||||
45
.claude/skills/magic-script/references/magic-module.md
Normal file
45
.claude/skills/magic-script/references/magic-module.md
Normal 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
|
||||||
|
})
|
||||||
|
```
|
||||||
39
.claude/skills/magic-script/references/math-functions.md
Normal file
39
.claude/skills/magic-script/references/math-functions.md
Normal 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%"
|
||||||
|
```
|
||||||
51
.claude/skills/magic-script/references/number-extensions.md
Normal file
51
.claude/skills/magic-script/references/number-extensions.md
Normal 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%"
|
||||||
|
```
|
||||||
129
.claude/skills/magic-script/references/object-extensions.md
Normal file
129
.claude/skills/magic-script/references/object-extensions.md
Normal 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()` - 判断是否是集合
|
||||||
55
.claude/skills/magic-script/references/other-functions.md
Normal file
55
.claude/skills/magic-script/references/other-functions.md
Normal 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
|
||||||
|
```
|
||||||
32
.claude/skills/magic-script/references/page.md
Normal file
32
.claude/skills/magic-script/references/page.md
Normal 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
|
||||||
|
```
|
||||||
13
.claude/skills/magic-script/references/pattern-extensions.md
Normal file
13
.claude/skills/magic-script/references/pattern-extensions.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Pattern扩展方法
|
||||||
|
|
||||||
|
`java.util.regex.Pattern`的扩展方法
|
||||||
|
|
||||||
|
## test
|
||||||
|
- 入参:`source`:`String` 目标字符串
|
||||||
|
- 返回值:`boolean`
|
||||||
|
- 函数说明:校验文本是否符合正则
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var regx = /^\d+$/;
|
||||||
|
return regx.test('123456') // true
|
||||||
|
```
|
||||||
125
.claude/skills/magic-script/references/quick-crud.md
Normal file
125
.claude/skills/magic-script/references/quick-crud.md
Normal 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有值时,生成SQL:select * from sys_user where id = ?
|
||||||
|
// 当id无值时,生成SQL:select * 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)
|
||||||
|
```
|
||||||
47
.claude/skills/magic-script/references/quick-param.md
Normal file
47
.claude/skills/magic-script/references/quick-param.md
Normal 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`来获取
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
如果脚本自定义变量和参数变量冲突,自定义变量优先。
|
||||||
66
.claude/skills/magic-script/references/quick-start.md
Normal file
66
.claude/skills/magic-script/references/quick-start.md
Normal 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
|
||||||
|
```
|
||||||
64
.claude/skills/magic-script/references/request-module.md
Normal file
64
.claude/skills/magic-script/references/request-module.md
Normal 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();
|
||||||
|
```
|
||||||
141
.claude/skills/magic-script/references/response-module.md
Normal file
141
.claude/skills/magic-script/references/response-module.md
Normal 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输出)
|
||||||
163
.claude/skills/magic-script/references/script-syntax.md
Normal file
163
.claude/skills/magic-script/references/script-syntax.md
Normal 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}
|
||||||
|
```
|
||||||
104
.claude/skills/magic-script/references/single-table.md
Normal file
104
.claude/skills/magic-script/references/single-table.md
Normal 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()
|
||||||
|
```
|
||||||
125
.claude/skills/magic-script/references/sql-param.md
Normal file
125
.claude/skills/magic-script/references/sql-param.md
Normal 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有值时,生成SQL:select * from sys_user where id = ?
|
||||||
|
// 当id无值时,生成SQL:select * 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)
|
||||||
|
```
|
||||||
26
.claude/skills/magic-script/references/string-functions.md
Normal file
26
.claude/skills/magic-script/references/string-functions.md
Normal 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('')
|
||||||
|
```
|
||||||
22
.claude/skills/magic-script/references/validate.md
Normal file
22
.claude/skills/magic-script/references/validate.md
Normal 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
45
.gitignore
vendored
Normal 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
|
||||||
14
antdv-next-admin/.editorconfig
Normal file
14
antdv-next-admin/.editorconfig
Normal 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
5
antdv-next-admin/.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Application Title
|
||||||
|
VITE_APP_TITLE=Antdv Next Admin
|
||||||
|
|
||||||
|
# API Base URL
|
||||||
|
VITE_API_BASE_URL=/api
|
||||||
7
antdv-next-admin/.env.development
Normal file
7
antdv-next-admin/.env.development
Normal 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
|
||||||
7
antdv-next-admin/.env.production
Normal file
7
antdv-next-admin/.env.production
Normal 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=
|
||||||
35
antdv-next-admin/.github/workflows/build.yml
vendored
Normal file
35
antdv-next-admin/.github/workflows/build.yml
vendored
Normal 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
|
||||||
68
antdv-next-admin/.github/workflows/deploy.yml
vendored
Normal file
68
antdv-next-admin/.github/workflows/deploy.yml
vendored
Normal 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
31
antdv-next-admin/.gitignore
vendored
Normal 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
|
||||||
19
antdv-next-admin/.oxfmtrc.json
Normal file
19
antdv-next-admin/.oxfmtrc.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
antdv-next-admin/.oxlintrc.json
Normal file
29
antdv-next-admin/.oxlintrc.json
Normal 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
251
antdv-next-admin/AGENTS.md
Normal 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
343
antdv-next-admin/CLAUDE.md
Normal 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`
|
||||||
93
antdv-next-admin/DEPLOYMENT.md
Normal file
93
antdv-next-admin/DEPLOYMENT.md
Normal 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
21
antdv-next-admin/LICENSE
Normal 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
618
antdv-next-admin/README.md
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
# Antdv Next Admin
|
||||||
|
|
||||||
|
🎉 一个基于 Vue 3 + TypeScript + Ant Design Vue 的现代化、功能完整的后台管理系统脚手架。
|
||||||
|
|
||||||
|
[](https://vuejs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://vitejs.dev/)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
## 📸 预览
|
||||||
|
|
||||||
|
**在线体验:** [https://antdv-next-admin.yelog.org/dashboard](https://antdv-next-admin.yelog.org/dashboard)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 默认账号: 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
|
||||||
BIN
antdv-next-admin/docs/images/screenshot.png
Normal file
BIN
antdv-next-admin/docs/images/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 730 KiB |
23
antdv-next-admin/index.html
Normal file
23
antdv-next-admin/index.html
Normal 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>
|
||||||
219
antdv-next-admin/mock/data/config.data.ts
Normal file
219
antdv-next-admin/mock/data/config.data.ts
Normal 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
100
antdv-next-admin/mock/data/dashboard.data.ts
Normal file
100
antdv-next-admin/mock/data/dashboard.data.ts
Normal 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 })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
224
antdv-next-admin/mock/data/dept.data.ts
Normal file
224
antdv-next-admin/mock/data/dept.data.ts
Normal 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;
|
||||||
|
}
|
||||||
283
antdv-next-admin/mock/data/dict.data.ts
Normal file
283
antdv-next-admin/mock/data/dict.data.ts
Normal 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
79
antdv-next-admin/mock/data/file.data.ts
Normal file
79
antdv-next-admin/mock/data/file.data.ts
Normal 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));
|
||||||
128
antdv-next-admin/mock/data/log.data.ts
Normal file
128
antdv-next-admin/mock/data/log.data.ts
Normal 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));
|
||||||
607
antdv-next-admin/mock/data/permissions.data.ts
Normal file
607
antdv-next-admin/mock/data/permissions.data.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
40
antdv-next-admin/mock/data/roles.data.ts
Normal file
40
antdv-next-admin/mock/data/roles.data.ts
Normal 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
103
antdv-next-admin/mock/data/users.data.ts
Normal file
103
antdv-next-admin/mock/data/users.data.ts
Normal 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);
|
||||||
113
antdv-next-admin/mock/handlers/auth.mock.ts
Normal file
113
antdv-next-admin/mock/handlers/auth.mock.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
114
antdv-next-admin/mock/handlers/config.mock.ts
Normal file
114
antdv-next-admin/mock/handlers/config.mock.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
71
antdv-next-admin/mock/handlers/dashboard.mock.ts
Normal file
71
antdv-next-admin/mock/handlers/dashboard.mock.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
121
antdv-next-admin/mock/handlers/dept.mock.ts
Normal file
121
antdv-next-admin/mock/handlers/dept.mock.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
289
antdv-next-admin/mock/handlers/dict.mock.ts
Normal file
289
antdv-next-admin/mock/handlers/dict.mock.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
96
antdv-next-admin/mock/handlers/file.mock.ts
Normal file
96
antdv-next-admin/mock/handlers/file.mock.ts
Normal 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
Loading…
Reference in New Issue
Block a user