9/28
1. 修改登录页面中:星期展示方法 2. 登录页面 3.
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-api-types
|
|
||||||
description: 前端 API 与类型定义专家。用于 plus-ui 前端项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
|
||||||
---
|
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中的 API 层和类型定义。
|
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
1. 先看当前模块已有 `src/api/<module>/<business>`。
|
|
||||||
2. API 路径、返回类型、命名风格与当前模块保持一致。
|
|
||||||
3. 能明确写出类型时,不要偷懒用 `any`。
|
|
||||||
4. 如果当前模块已有 `export default { ... }`,继续保持一致。
|
|
||||||
|
|
||||||
## 重点关注
|
|
||||||
|
|
||||||
- `Query`
|
|
||||||
- `VO`
|
|
||||||
- `Form`
|
|
||||||
- `InfoVO`
|
|
||||||
- `AxiosPromise<PageResult<T>>`
|
|
||||||
- 详情接口与列表接口返回结构
|
|
||||||
|
|
||||||
## 自检
|
|
||||||
|
|
||||||
- API 路径是否与后端一致
|
|
||||||
- 类型是否覆盖接口真实结构
|
|
||||||
- 是否不必要地把类型写宽了
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-crud-coding
|
|
||||||
description: 前端总入口。用于 plus-ui 前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
|
||||||
---
|
|
||||||
|
|
||||||
你是 plus-ui 前端项目的总入口 agent。
|
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要在 agent 文档中写本机绝对路径。
|
|
||||||
|
|
||||||
先判断任务类型,再按下面规则处理:
|
|
||||||
|
|
||||||
1. 如果是新增标准 CRUD 页面、补 `src/api`、`types.ts`、`index.vue`,优先使用 `frontend-crud-page.md`。
|
|
||||||
2. 如果是修改已有列表页、增强导入导出、树筛选、更多菜单、状态切换,优先使用 `frontend-page-enhancement.md`。
|
|
||||||
3. 如果只改接口层和类型定义,优先使用 `frontend-api-types.md`。
|
|
||||||
|
|
||||||
通用要求:
|
|
||||||
|
|
||||||
- 先读当前目录下最近似页面和 API,再动代码。
|
|
||||||
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是 plus-ui Gitee 仓库 `6.X-Vue` 分支的 `gen` 模板。
|
|
||||||
- 默认直接产出可落地代码,而不是只给抽象建议。
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-crud-page
|
|
||||||
description: 前端标准 CRUD 页面专家。用于 plus-ui 前端项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
|
||||||
---
|
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中的标准 CRUD 页面实现。
|
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端模板引用必须同时标记仓库、分支和路径,例如 `branch=6.X-Vue, path=gen/index.vue.ftl`;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
1. 先看当前模块最近似页面。
|
|
||||||
2. 再参考 plus-ui Gitee 仓库 `6.X-Vue` 分支中的 `gen` 模板。
|
|
||||||
3. 默认同时维护:
|
|
||||||
`src/api/<module>/<business>/index.ts`
|
|
||||||
`src/api/<module>/<business>/types.ts`
|
|
||||||
`src/views/<module>/<business>/index.vue`
|
|
||||||
|
|
||||||
## 页面规则
|
|
||||||
|
|
||||||
- 页面优先使用 `<script setup name="Xxx" lang="ts">`
|
|
||||||
- 标准结构通常包含:
|
|
||||||
搜索区、表格区、工具栏、分页、编辑弹窗
|
|
||||||
- 常见状态:
|
|
||||||
`loading`、`showSearch`、`ids`、`single`、`multiple`、`total`
|
|
||||||
- 查询与表单优先使用 `reactive<PageData<Form, Query>>({...})`
|
|
||||||
|
|
||||||
## API / types 规则
|
|
||||||
|
|
||||||
- 请求统一通过 `@/utils/request`
|
|
||||||
- 同目录维护 `index.ts` 与 `types.ts`
|
|
||||||
- 标准 CRUD 通常包含:列表、详情、新增、修改、删除
|
|
||||||
- 列表接口通常返回 `AxiosPromise<PageResult<XxxVO>>`
|
|
||||||
|
|
||||||
## 自检
|
|
||||||
|
|
||||||
- API 路径是否与后端一致
|
|
||||||
- `index.ts` 与 `types.ts` 是否同步补齐
|
|
||||||
- 页面是否只是模板裸输出,如果是要继续补强到当前项目风格
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-page-enhancement
|
|
||||||
description: 复杂前端页面增强专家。用于修改 plus-ui 前端项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
|
||||||
---
|
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中已有页面的增强,不是重写页面。
|
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
1. 优先阅读当前页面完整实现。
|
|
||||||
2. 增量修改,不重写整页。
|
|
||||||
3. 保留已有树筛选、导入导出、列显隐、更多菜单、状态切换、路由跳转、SCSS 页面壳。
|
|
||||||
4. 不要把复杂页面退化成 generator 式基础列表页。
|
|
||||||
|
|
||||||
## 常见任务
|
|
||||||
|
|
||||||
- 调整工具栏和更多菜单
|
|
||||||
- 增加筛选条件和日期范围
|
|
||||||
- 增加导入导出能力
|
|
||||||
- 增加状态切换、快捷操作、确认弹窗
|
|
||||||
- 补复杂页面的小型子功能
|
|
||||||
|
|
||||||
## 自检
|
|
||||||
|
|
||||||
- 是否破坏了原页面结构和样式
|
|
||||||
- 是否误删了已有权限控制或交互能力
|
|
||||||
- 是否应该拆成子组件而不是继续堆主页面
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-crud-coding
|
|
||||||
description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element Plus + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式壳。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与后端接口对应的 src/api、types 和 src/views 代码;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支,触发后应先读取适用 references,再阅读目标模块真实代码和项目内 gen 代码生成模板。
|
|
||||||
---
|
|
||||||
|
|
||||||
# 前端编码规范
|
|
||||||
|
|
||||||
先对齐当前前端项目里的真实实现,再参考项目内 `gen` 目录下的代码生成模板。不要只套通用 Vue 模板,也不要把 generator 模板原样复制进来而忽略当前项目已经演进出的 hooks、页面壳、类型入口和下载方式。
|
|
||||||
|
|
||||||
## 项目基线
|
|
||||||
|
|
||||||
- 基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
- 默认分支:`6.X-Vue`
|
|
||||||
- 远端引用必须同时标记仓库、分支和文件路径,例如 `https://gitee.com/JavaLionLi/plus-ui/blob/6.X-Vue/gen/index.vue.ftl` 或 `branch=6.X-Vue, path=gen/index.vue.ftl`。
|
|
||||||
- 不要在 skill、reference 或 agent 文档中写本机绝对路径;本地文件读取以运行时工作目录为准。
|
|
||||||
- 如果用户指定其他分支,先按用户分支读取同一仓库的对应文件,并在结果中说明使用的分支。
|
|
||||||
|
|
||||||
## 执行流程
|
|
||||||
|
|
||||||
1. 判断任务类型:新增标准 CRUD、树表、已有页面增强、复杂业务页、只补 API/types。
|
|
||||||
2. 按“文档读取规则”读取必要 reference,不一次性展开所有资料。
|
|
||||||
3. 阅读目标目录下最近似的真实代码:
|
|
||||||
- 标准单表优先看 `src/views/demo/demo/index.vue`、`src/api/demo/demo/*`。
|
|
||||||
- 树表优先看 `src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
|
|
||||||
- 系统复杂页优先看 `src/views/system/user/index.vue`、`system/role`、`system/post`、`system/config`。
|
|
||||||
- workflow 业务页优先看 `src/views/workflow/*` 同类页面。
|
|
||||||
4. 新增标准页面前,对照项目内模板确认基础骨架:
|
|
||||||
- API 模板:`gen/api.ts.ftl`
|
|
||||||
- types 模板:`gen/types.ts.ftl`
|
|
||||||
- 标准单表页模板:`gen/index.vue.ftl`
|
|
||||||
- 树表页模板:`gen/index-tree.vue.ftl`
|
|
||||||
5. 新增代码时通常同步维护 `src/api/<module>/<business>/index.ts`、`types.ts`、`src/views/<module>/<business>/index.vue`。
|
|
||||||
6. 增强已有页面时只做增量修改,保留原页面的树筛选、导入导出、列显隐、权限、字典、弹窗和路由跳转能力。
|
|
||||||
7. 修改完成后按影响范围运行验证:优先 `pnpm exec vue-tsc --noEmit`,改动页面或导入时再跑 `pnpm lint`,大范围变更再跑 `pnpm build`。
|
|
||||||
|
|
||||||
## 文档读取规则
|
|
||||||
|
|
||||||
- 前端 API、types、页面、hooks、样式和验证规则,先读 [references/frontend.md](references/frontend.md)。
|
|
||||||
- 不确定任务边界、需要标准用例或提问方式时,再读 [references/examples.md](references/examples.md)。
|
|
||||||
- reference 只约束实现方式和自检范围;发生冲突时,以当前模块真实代码和实际调用点为准。
|
|
||||||
|
|
||||||
## 优先级规则
|
|
||||||
|
|
||||||
发生冲突时按下面顺序决策:
|
|
||||||
|
|
||||||
1. 目标目录下最近似页面、API、types 的真实实现。
|
|
||||||
2. 当前项目公共 hooks、组件、工具和样式约定。
|
|
||||||
3. 项目内 `gen` 代码生成模板。
|
|
||||||
4. 通用 Vue 3 / Element Plus 习惯。
|
|
||||||
|
|
||||||
也就是说:
|
|
||||||
|
|
||||||
- 同模块已有页面怎么写,优先怎么写。
|
|
||||||
- 没有现成页面时,使用项目内 `gen` 模板作为骨架,再改成当前项目风格。
|
|
||||||
- 复杂模块不能为了“标准 CRUD”退化成裸模板页。
|
|
||||||
|
|
||||||
## 仓库通用规则
|
|
||||||
|
|
||||||
- 遵循 [`.editorconfig`](../../../.editorconfig):UTF-8、LF、2 空格缩进;Markdown 例外。
|
|
||||||
- 当前仓库没有 `.prettierrc`,格式脚本是 `pnpm run fmt` 调用 `oxfmt .`,lint 脚本是 `pnpm lint` 调用 `oxlint src`。
|
|
||||||
- 页面优先使用 `<script setup name="Xxx" lang="ts">`。
|
|
||||||
- API 返回类型优先从 `@/utils/api-types` 引入 `AxiosPromise`,分页结果从 `@/api/types` 引入 `PageResult`。
|
|
||||||
- 请求统一通过 `@/utils/request`,导出下载使用 `import { download as requestDownload } from '@/utils/request';`。
|
|
||||||
- 标准列表页优先复用 `useLoading`、`useSearchToggle`、`useSearchReset`、`useTableSelection`、`useFormDialog`、`useDateRangeQuery`。
|
|
||||||
- 页面壳优先使用 `p-2 app-container <module>-<business>-page`、`search-panel`、`toolbar-shell`、`data-table`、`right-toolbar`、`pagination`。
|
|
||||||
- 新页面不要无故引入另一套状态管理、请求封装、样式体系或权限写法。
|
|
||||||
|
|
||||||
## 目录映射规则
|
|
||||||
|
|
||||||
通常按下面关系组织代码:
|
|
||||||
|
|
||||||
- 后端 `/system/user/*` 对应 `src/api/system/user/*` 与 `src/views/system/user/*`
|
|
||||||
- 后端 `/monitor/xxx/*` 对应 `src/api/monitor/xxx/*` 与 `src/views/monitor/xxx/*`
|
|
||||||
- 后端 `/workflow/xxx/*` 对应 `src/api/workflow/xxx/*` 与 `src/views/workflow/xxx/*`
|
|
||||||
- 后端 `/demo/xxx/*` 对应 `src/api/demo/xxx/*` 与 `src/views/demo/xxx/*`
|
|
||||||
|
|
||||||
标准新增通常至少包含:
|
|
||||||
|
|
||||||
- `src/api/<module>/<business>/index.ts`
|
|
||||||
- `src/api/<module>/<business>/types.ts`
|
|
||||||
- `src/views/<module>/<business>/index.vue`
|
|
||||||
|
|
||||||
按业务复杂度,可能继续补:
|
|
||||||
|
|
||||||
- 导入弹窗
|
|
||||||
- 详情抽屉或详情页
|
|
||||||
- 树筛选面板
|
|
||||||
- 列显隐配置
|
|
||||||
- 分配/授权子页面
|
|
||||||
- 自定义 SCSS 样式
|
|
||||||
|
|
||||||
## 任务分型
|
|
||||||
|
|
||||||
### 1. 标准单表 CRUD
|
|
||||||
|
|
||||||
以 `gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl` 和 `src/views/demo/demo/index.vue` 为主要起点,补齐列表、搜索、分页、新增、编辑、删除、导出、权限、类型和验证。
|
|
||||||
|
|
||||||
### 2. 树表 CRUD
|
|
||||||
|
|
||||||
以 `src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue` 为主要起点。列表接口通常返回数组而不是 `PageResult`,页面使用 `handleTree`、`useTreeTableExpand`,`Query` 通常不继承 `PageQuery`。
|
|
||||||
|
|
||||||
### 3. 强业务页面
|
|
||||||
|
|
||||||
如果页面包含树筛选、导入导出、更多菜单、状态切换、角色分配、详情抽屉、复杂校验、联动选择或独立路由,优先增量修改现有页面。不要重写成简单 CRUD。
|
|
||||||
|
|
||||||
### 4. 工作流页面
|
|
||||||
|
|
||||||
workflow 目录优先参考 `src/views/workflow/*`。流程定义、流程实例、任务列表、请假申请等页面通常有业务按钮、弹窗和路由跳转,不要硬套 system 模块。
|
|
||||||
|
|
||||||
### 5. 只补 API 和 types
|
|
||||||
|
|
||||||
只维护 `src/api/<module>/<business>/index.ts` 与 `types.ts`,但仍要与后端路由、返回结构、当前模块导入方式和类型入口一致。
|
|
||||||
|
|
||||||
## 输出要求
|
|
||||||
|
|
||||||
使用本 skill 时,默认期望产出应满足:
|
|
||||||
|
|
||||||
- 类型完整,不把页面逻辑大量写成 `any`。
|
|
||||||
- API 路径、函数名、权限标识与后端接口保持一致。
|
|
||||||
- 标准页查询、重置、分页、弹窗、提交、删除、导出流程闭环完整。
|
|
||||||
- 复杂页面保留原有交互能力和业务约束。
|
|
||||||
- 代码体现当前项目 hooks、页面壳和下载方式,而不是 `gen` 模板裸输出。
|
|
||||||
- 交付前说明运行过的验证命令;如果无法验证,说明原因。
|
|
||||||
|
|
||||||
## 快速检查清单
|
|
||||||
|
|
||||||
- `AxiosPromise` 是否来自 `@/utils/api-types`。
|
|
||||||
- `PageResult` 是否来自 `@/api/types`。
|
|
||||||
- API `params` 和 `data` 是否与后端方法一致。
|
|
||||||
- 日期范围是否通过 `useDateRangeQuery` 或附近页面现有方式处理。
|
|
||||||
- 列表 loading 是否通过 `useLoading` 或原页面方式维护。
|
|
||||||
- 弹窗是否通过 `useFormDialog` 或原页面方式维护。
|
|
||||||
- 多选状态是否通过 `useTableSelection` 或原页面方式维护。
|
|
||||||
- 权限指令是否保持同文件一致,默认使用当前项目主流 `v-hasPermi`。
|
|
||||||
- 导出是否使用 `requestDownload('<module>/<business>/export', { ...queryParams.value }, '<name>_<time>.xlsx')`。
|
|
||||||
- 页面壳是否保留 `search-panel`、`table-panel`、`toolbar-shell`、`data-table`、`right-toolbar`、`pagination`。
|
|
||||||
|
|
||||||
## 推荐提问方式
|
|
||||||
|
|
||||||
推荐把请求描述到下面粒度:
|
|
||||||
|
|
||||||
- 目标模块和业务名
|
|
||||||
- 后端接口前缀
|
|
||||||
- 是新增页面、修改页面,还是只补 API/types
|
|
||||||
- 是否需要导入、导出、树筛选、树表、状态切换、字典、权限按钮
|
|
||||||
- 希望参考哪个现有页面
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
- 使用 `$frontend-crud-coding` 为 `/system/client` 补一套标准 CRUD 页面,参考 `gen` 模板、`demo/demo` 和 `system/client`。
|
|
||||||
- 使用 `$frontend-crud-coding` 修改 `workflow/category` 列表页,增加导出按钮和状态筛选,保持当前 workflow 风格。
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "前端编码"
|
|
||||||
short_description: "按 plus-ui 6.X-Vue 真实代码和 gen 模板编写前端 CRUD"
|
|
||||||
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference,再按 plus-ui Gitee 仓库 6.X-Vue 分支的真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
|
|
||||||
|
|
||||||
policy:
|
|
||||||
allow_implicit_invocation: true
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# 使用案例
|
|
||||||
|
|
||||||
## 案例 1:新增标准 CRUD 页面
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 为 system/client 补一套前端 CRUD 页面。
|
|
||||||
后端接口已经有 /system/client/list、/system/client/{id}、POST /system/client、PUT /system/client、DELETE /system/client/{ids}。
|
|
||||||
请参考 plus-ui Gitee 仓库 6.X-Vue 分支的项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- 先看 `src/api/system/client/*` 和 `src/views/system/client/index.vue` 是否已存在。
|
|
||||||
- 再看 `src/views/demo/demo/index.vue` 的标准 hooks 版 CRUD 骨架。
|
|
||||||
- 对照项目内 `gen/api.ts.ftl`、`gen/types.ts.ftl`、`gen/index.vue.ftl`。
|
|
||||||
- 生成或修改 `api/index.ts`、`types.ts`、`views/.../index.vue`。
|
|
||||||
- 使用 `AxiosPromise` from `@/utils/api-types`、`PageResult` from `@/api/types`、`useLoading`、`useFormDialog`、`useSearchReset`、`useTableSelection`。
|
|
||||||
|
|
||||||
## 案例 2:新增树表页面
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 为 demo/tree2 新增树表 CRUD,接口返回数组,字段包含 id、parentId、name、orderNum。
|
|
||||||
参考 src/views/demo/tree/index.vue 和 workflow/category。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- 优先判断这是树表,不生成分页 `PageResult` 页面。
|
|
||||||
- API 列表返回 `AxiosPromise<Tree2VO[]>`。
|
|
||||||
- `Query` 不继承 `PageQuery`。
|
|
||||||
- 页面使用 `handleTree`、`row-key`、`tree-props`、`useTreeTableExpand`、`el-tree-select`。
|
|
||||||
- 新增子节点时从当前行带入 `parentId`。
|
|
||||||
|
|
||||||
## 案例 3:修改已有复杂列表页
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 修改 system/user 页面:
|
|
||||||
1. 新增一个创建时间快捷筛选
|
|
||||||
2. 导出按钮保留在更多菜单中
|
|
||||||
3. 保持现有树筛选、导入、列显隐、详情抽屉和角色分配不变
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- 判断这是“已有复杂页面增强”,不是重新生成 CRUD。
|
|
||||||
- 优先阅读 `src/views/system/user/index.vue`。
|
|
||||||
- 保留 `TreePanel`、导入弹窗、`right-toolbar` 列显隐、`UserViewDrawer`、角色分配路由、权限控制。
|
|
||||||
- 只增量修改搜索和查询参数处理。
|
|
||||||
|
|
||||||
## 案例 4:修改 workflow 页面
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 为 workflow/category 增加状态筛选和导出按钮,保持 workflow 模块自己的树表风格。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- 优先看 `src/views/workflow/category/index.vue` 和 `src/api/workflow/category/*`。
|
|
||||||
- 判断是否需要后端新增导出接口;前端导出路径保持 `workflow/category/export`。
|
|
||||||
- 不迁移 system/user 的用户专属逻辑。
|
|
||||||
- 保留树表、`useTreeTableExpand`、`handleTree` 和分类弹窗逻辑。
|
|
||||||
|
|
||||||
## 案例 5:只补 API 和 types
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 为 monitor/cache 补全前端 API 和 types,页面先不改。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- 只维护 `src/api/monitor/cache/index.ts` 和 `src/api/monitor/cache/types.ts`。
|
|
||||||
- 仍然检查同目录 monitor API 的 `export function` / `export const` 风格。
|
|
||||||
- 返回类型使用 `AxiosPromise` from `@/utils/api-types`。
|
|
||||||
- 不创建页面,不改路由。
|
|
||||||
|
|
||||||
## 案例 6:接入后端新增状态切换接口
|
|
||||||
|
|
||||||
### 用户提问示例
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 给 system/client 页面接入 PUT /system/client/changeStatus,状态字段 status,参考 gen 模板。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 期望执行方式
|
|
||||||
|
|
||||||
- API 增加 `changeClientStatus(id, status)`。
|
|
||||||
- types 确认 `status` 类型是 string、number 还是 boolean。
|
|
||||||
- 表格列用 `el-switch`,active/inactive 值跟后端字段类型一致。
|
|
||||||
- 切换失败时回滚原状态。
|
|
||||||
- 权限使用 `system:client:edit` 或后端实际权限。
|
|
||||||
|
|
||||||
## 推荐的高质量任务描述
|
|
||||||
|
|
||||||
```text
|
|
||||||
使用 $frontend-crud-coding 在当前前端项目中新增 `/system/notice` 列表页增强:
|
|
||||||
1. 保留现有页面
|
|
||||||
2. 新增状态筛选和导出
|
|
||||||
3. API 路径沿用后端接口
|
|
||||||
4. 参考 system/config 的工具栏与导出交互
|
|
||||||
5. 参考 plus-ui Gitee 仓库 6.X-Vue 分支的 gen 模板补齐缺失 types
|
|
||||||
```
|
|
||||||
|
|
||||||
## 不推荐的任务描述
|
|
||||||
|
|
||||||
```text
|
|
||||||
帮我写个后台页面
|
|
||||||
```
|
|
||||||
|
|
||||||
更好的写法至少补充:
|
|
||||||
|
|
||||||
- 模块名
|
|
||||||
- 业务名
|
|
||||||
- 后端接口前缀
|
|
||||||
- 是新增还是修改
|
|
||||||
- 是否需要分页、导出、树表、字典、权限
|
|
||||||
- 想参考哪个现有页面
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# 前端约定
|
|
||||||
|
|
||||||
## 优先参考的代码来源
|
|
||||||
|
|
||||||
- 默认远端基线:`https://gitee.com/JavaLionLi/plus-ui`,分支 `6.X-Vue`。引用远端文件时必须同时写明 `branch=6.X-Vue` 和仓库内路径。
|
|
||||||
- 当前目标目录下最近似页面、API、types。
|
|
||||||
- 标准单表:`src/views/demo/demo/index.vue`、`src/api/demo/demo/index.ts`、`src/api/demo/demo/types.ts`。
|
|
||||||
- 树表:`src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
|
|
||||||
- 复杂系统页:`src/views/system/user/index.vue`、`src/views/system/role/index.vue`、`src/views/system/post/index.vue`、`src/views/system/config/index.vue`。
|
|
||||||
- workflow 页:`src/views/workflow/*`、`src/api/workflow/*`。
|
|
||||||
- 监控页:`src/views/monitor/*`、`src/api/monitor/*`。
|
|
||||||
- 公共 hooks:`src/hooks/async/useLoading.ts`、`src/hooks/dialog/*`、`src/hooks/form/*`、`src/hooks/table/*`、`src/hooks/tree/*`。
|
|
||||||
- 项目内 generator 模板:
|
|
||||||
`gen/api.ts.ftl`
|
|
||||||
`gen/types.ts.ftl`
|
|
||||||
`gen/index.vue.ftl`
|
|
||||||
`gen/index-tree.vue.ftl`
|
|
||||||
|
|
||||||
## 基础栈与格式
|
|
||||||
|
|
||||||
- 技术栈是 Vue 3 + TypeScript + Element Plus + Vite + Pinia。
|
|
||||||
- 包管理按仓库现状使用 pnpm。
|
|
||||||
- `.editorconfig` 要求 UTF-8、LF、2 空格缩进。
|
|
||||||
- 当前仓库没有 `.prettierrc`;格式化使用 `pnpm run fmt`,lint 使用 `pnpm lint`。
|
|
||||||
- 不要在一个页面里混入与仓库不一致的格式和写法。
|
|
||||||
- 不要把本机绝对路径写进 skill 输出或参考文档;跨环境参考统一使用 Gitee 仓库 URL、分支和仓库内相对路径。
|
|
||||||
|
|
||||||
## API 文件规则
|
|
||||||
|
|
||||||
- 标准 API 文件放在 `src/api/<module>/<business>/index.ts`,同目录维护 `types.ts`。
|
|
||||||
- import 顺序优先跟随附近文件,标准生成页常见形式:
|
|
||||||
`import type { XxxForm, XxxQuery, XxxVO } from '@/api/<module>/<business>/types';`
|
|
||||||
`import type { PageResult } from '@/api/types';`
|
|
||||||
`import type { AxiosPromise } from '@/utils/api-types';`
|
|
||||||
`import request from '@/utils/request';`
|
|
||||||
- 不要从 `axios` 引入 `AxiosPromise`。
|
|
||||||
- 列表分页接口通常返回 `AxiosPromise<PageResult<XxxVO>>`。
|
|
||||||
- 树表列表接口通常返回 `AxiosPromise<XxxVO[]>`。
|
|
||||||
- 详情接口返回 `AxiosPromise<XxxVO>`;复杂详情返回单独的 `InfoVO`。
|
|
||||||
- 标准函数命名:
|
|
||||||
`listXxx` -> `GET /<module>/<business>/list`
|
|
||||||
`getXxx` -> `GET /<module>/<business>/{id}`
|
|
||||||
`addXxx` -> `POST /<module>/<business>`
|
|
||||||
`updateXxx` -> `PUT /<module>/<business>`
|
|
||||||
`delXxx` -> `DELETE /<module>/<business>/{id or ids}`
|
|
||||||
`changeXxxStatus` -> `PUT /<module>/<business>/changeStatus`
|
|
||||||
- query string 用 `params`,请求体用 `data`。
|
|
||||||
- 加密、防重复提交等 headers 直接写在请求配置里,例如用户重置密码中的 `isEncrypt`、`repeatSubmit`。
|
|
||||||
- 当前仓库有些 API 使用 `export const`,有些使用 `export function`;新增标准 CRUD 优先跟随 `gen/api.ts.ftl` 和相邻模块。
|
|
||||||
- 只有相邻模块已有 `export default { ... }` 聚合时才新增默认导出。
|
|
||||||
|
|
||||||
## 类型文件规则
|
|
||||||
|
|
||||||
- 标准类型定义 `VO`、`Form`、`Query`,必要时补 `InfoVO`、`TreeVO`、`ResetPwdForm` 等扩展类型。
|
|
||||||
- `Form` 通常继承 `BaseEntity`。
|
|
||||||
- 非树表 `Query` 通常继承 `PageQuery`。
|
|
||||||
- 树表 `Query` 通常不继承 `PageQuery`。
|
|
||||||
- ID 字段通常使用 `string | number`,批量删除参数使用 `string | number | Array<string | number>`。
|
|
||||||
- Java 数值类型映射为 `number`,Boolean 映射为 `boolean`,日期/文本默认 `string`。
|
|
||||||
- 日期范围查询保留 `params?: any`,不要因为它看起来宽松就删掉。
|
|
||||||
- 列表对象、表单对象、查询对象职责分开;字段不一致时不要强行复用一个接口。
|
|
||||||
- 能明确写出类型时不要用 `any`;组件库、字典或历史接口确实无法收窄时再保留。
|
|
||||||
|
|
||||||
## Vue 页面结构规则
|
|
||||||
|
|
||||||
- 页面优先使用 `<script setup name="Xxx" lang="ts">`。
|
|
||||||
- 标准根节点使用 `class="p-2 app-container <module>-<business>-page"`;已有页面是特殊布局时保持原样。
|
|
||||||
- 标准列表页结构:
|
|
||||||
搜索卡片 `search-panel`
|
|
||||||
表格卡片 `table-panel`
|
|
||||||
工具栏 `toolbar-shell`
|
|
||||||
表格 `data-table`
|
|
||||||
`right-toolbar`
|
|
||||||
`pagination`
|
|
||||||
新增/编辑 `el-dialog`
|
|
||||||
- 搜索区通过 `useSearchToggle` 控制 `showSearch`,头部点击切换。
|
|
||||||
- 列表 loading 通过 `useLoading(true)` 和 `withLoading`。
|
|
||||||
- 选择状态通过 `useTableSelection<XxxVO>(item => item.id)` 返回 `ids`、`single`、`multiple`、`handleSelectionChange`。
|
|
||||||
- 表单弹窗优先使用 `useFormDialog({ form, formRef, initialFormData })`,返回 `dialog`、`resetForm`、`openDialog`、`showDialog`、`closeDialog`。
|
|
||||||
- 仅需要简单弹窗状态或一个页面多个弹窗时使用 `useDialogState`。
|
|
||||||
- 日期范围查询优先使用 `useDateRangeQuery()`;带后端参数名时使用 `useDateRangeQuery('CreateTime')` 等相邻页面模式。
|
|
||||||
- 查询和表单状态通常放在 `reactive<PageData<Form, Query>>({ form, queryParams, rules })`,再 `toRefs(data)`。
|
|
||||||
|
|
||||||
## 页面行为规则
|
|
||||||
|
|
||||||
- `getList` 负责设置 loading、调用列表接口、回填列表和 `total`。
|
|
||||||
- `handleQuery` 先把 `queryParams.value.pageNum = 1`,再调用 `getList()`;树表无分页时只调用 `getList()`。
|
|
||||||
- `resetQuery` 使用 `useSearchReset`,分页页传 `pageNumKey: 'pageNum'`,需要时传 `pageSizeKey` 和 `resetExtras`。
|
|
||||||
- `handleAdd` 使用 `openDialog('添加xxx')`;如果有树/联动选项,打开前后按现有页面加载选项。
|
|
||||||
- `handleUpdate` 先 `reset()`,再按行或 `ids.value[0]` 查详情,`Object.assign(form.value, res.data)`,最后 `showDialog('修改xxx')`。
|
|
||||||
- `submitForm` 表单校验通过后设置 `buttonLoading`,根据主键判断新增或修改,成功后 `modal.msgSuccess('操作成功')`、关闭弹窗、刷新列表。
|
|
||||||
- `handleDelete` 使用 `modal.confirm(...)` 二次确认,再调用删除接口,成功提示并刷新。
|
|
||||||
- `handleExport` 使用 `requestDownload('<module>/<business>/export', { ...queryParams.value }, '<business>_<timestamp>.xlsx')`。
|
|
||||||
- 状态切换失败时要把 switch 值回滚,参考 generator 模板和现有 `system/user`、`system/role`。
|
|
||||||
- 导入上传使用 `globalHeaders()`、`import.meta.env.VITE_APP_BASE_API`、`ElUpload`,优先参考 `system/user` 或流程定义页面。
|
|
||||||
|
|
||||||
## 字典、权限与公共工具
|
|
||||||
|
|
||||||
- 字典使用 `useDict`:
|
|
||||||
`const { sys_normal_disable } = toRefs<any>(useDict('sys_normal_disable'));`
|
|
||||||
- 新增代码默认使用当前项目主流 `v-hasPermi`。
|
|
||||||
- 如果正在修改的文件已经混用或使用 `v-has-permi`,保持同文件现状,不为统一写法而重排无关代码。
|
|
||||||
- `el-dropdown-item` 延迟加载导致权限指令不可靠时,使用 `v-if="checkPermi([...])"`,参考 `system/user`。
|
|
||||||
- 常用工具:
|
|
||||||
`modal` from `@/plugins/modal`
|
|
||||||
`download as requestDownload` from `@/utils/request`
|
|
||||||
`useDict` from `@/utils/dict`
|
|
||||||
`checkPermi` from `@/utils/permission`
|
|
||||||
`handleTree`、`parseStrEmpty` from `@/utils/ruoyi`
|
|
||||||
|
|
||||||
## 组件与样式规则
|
|
||||||
|
|
||||||
- 优先复用公共组件:`right-toolbar`、`pagination`、`DictTag` / `dict-tag`、`ImageUpload` / `image-upload`、`ImagePreview` / `image-preview`、`FileUpload` / `file-upload`、`Editor` / `editor`、`TreePanel`。
|
|
||||||
- 标准页面尽量使用已有页面壳类,不堆大量内联样式。
|
|
||||||
- 复杂页面需要 scoped SCSS 时优先复用:
|
|
||||||
`@use '@/assets/styles/components/page-shell' as pageShell;`
|
|
||||||
再按现有 mixin 使用。
|
|
||||||
- 不要为了单页需求修改全局组件样式。
|
|
||||||
- 类名保持模块语义:`system-client-page`、`workflow-category-page`、`demo-demo-page`。
|
|
||||||
|
|
||||||
## 树表规则
|
|
||||||
|
|
||||||
- 树表列表接口通常返回数组,页面通过 `handleTree<T>(res.data, 'id', 'parentId')` 组树。
|
|
||||||
- 使用 `row-key`、`:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"`。
|
|
||||||
- 展开/折叠使用 `useTreeTableExpand`。
|
|
||||||
- 表单中上级节点使用 `el-tree-select`。
|
|
||||||
- 新增子节点时从当前行回填 `parentId`。
|
|
||||||
- 删除确认文案优先使用业务名称,而不是批量 ID 文案。
|
|
||||||
|
|
||||||
## 与 gen 模板的关系
|
|
||||||
|
|
||||||
- `gen` 是 plus-ui 前端项目内的生成模板;默认远端位置是 `https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue/gen`,分支必须标记为 `6.X-Vue`。
|
|
||||||
- 本地仓库存在对应文件时优先读本地 `gen`,但不要把本地绝对路径写入 skill 或交付内容。
|
|
||||||
- 新增标准单表页面时读取 `gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
|
||||||
- 新增树表页面时读取 `gen/index-tree.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
|
||||||
- `gen` 模板是标准骨架,不是最终答案;落地时仍要对照目标模块真实页面和公共 hooks。
|
|
||||||
- 当前前端项目已经把生成页升级为 hooks 版:`useLoading`、`useFormDialog`、`useSearchReset`、`useTableSelection`、`useDateRangeQuery`。
|
|
||||||
- 新增标准 CRUD 时,先从 `gen` 确认字段、权限、导出、状态切换、排序、日期范围等,再落成当前项目的实际页面壳。
|
|
||||||
- 修改已有页面时,不要把现有强业务逻辑替换回 `gen` 的简化逻辑。
|
|
||||||
|
|
||||||
## 验证规则
|
|
||||||
|
|
||||||
- 只改文档或 skill:运行 skill 基础校验即可。
|
|
||||||
- 改前端 TS/Vue/API/types:优先运行 `pnpm exec vue-tsc --noEmit`。
|
|
||||||
- 改页面模板、import、权限或较多文件:再运行 `pnpm lint`。
|
|
||||||
- 改公共 hooks、组件、构建相关或大范围页面:再运行 `pnpm build`。
|
|
||||||
- 如果验证因为环境、依赖或权限失败,交付时说明失败命令和原因。
|
|
||||||
|
|
||||||
## 避免事项
|
|
||||||
|
|
||||||
- 不要从 `axios` 引入 `AxiosPromise`。
|
|
||||||
- 不要绕开 `request` 或 `requestDownload` 自造请求/下载封装。
|
|
||||||
- 不要跳过 `types.ts`,把类型全写在页面里。
|
|
||||||
- 不要删除日期范围 `params`、权限指令、导出、导入、树筛选、列显隐等现有能力。
|
|
||||||
- 不要为了“更整洁”重写复杂页面的大块业务逻辑。
|
|
||||||
- 不要在新增标准页里使用与仓库不一致的 UI 壳或状态管理方式。
|
|
||||||
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/dist/*
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
/node_modules/**
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
||||||
|
|
||||||
|
/public/*
|
||||||
20
.prettierrc
Normal file
20
.prettierrc
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 150,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "preserve",
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"arrowParens": "always",
|
||||||
|
"requirePragma": false,
|
||||||
|
"insertPragma": false,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { LocationQuery, RouteLocationNormalized, RouteMeta } from 'vue-router';
|
import type {
|
||||||
import { defineStore } from 'pinia';
|
LocationQuery,
|
||||||
import { ref } from 'vue';
|
RouteLocationNormalized,
|
||||||
import { useSettingsStore } from './settings';
|
RouteMeta,
|
||||||
|
} from "vue-router";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useSettingsStore } from "./settings";
|
||||||
|
|
||||||
const PERSIST_KEY = 'tags-view-visited';
|
const PERSIST_KEY = "tags-view-visited";
|
||||||
|
|
||||||
type PersistedTagView = {
|
type PersistedTagView = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -24,7 +28,7 @@ const isPersistEnabled = () => {
|
|||||||
|
|
||||||
const normalizeVisitedView = (view: RouteLocationNormalized): TagView => {
|
const normalizeVisitedView = (view: RouteLocationNormalized): TagView => {
|
||||||
return Object.assign({}, view, {
|
return Object.assign({}, view, {
|
||||||
title: view.meta?.title || 'no-name'
|
title: view.meta?.title || "no-name",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,14 +38,14 @@ const saveVisitedViews = (views: any[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: PersistedTagView[] = (views as TagView[])
|
const payload: PersistedTagView[] = (views as TagView[])
|
||||||
.filter(view => !view.meta?.affix)
|
.filter((view) => !view.meta?.affix)
|
||||||
.map(view => ({
|
.map((view) => ({
|
||||||
path: view.path,
|
path: view.path,
|
||||||
fullPath: view.fullPath,
|
fullPath: view.fullPath,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
title: (view as any).title || view.meta?.title || 'no-name',
|
title: (view as any).title || view.meta?.title || "no-name",
|
||||||
query: view.query,
|
query: view.query,
|
||||||
meta: view.meta
|
meta: view.meta,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
localStorage.setItem(PERSIST_KEY, JSON.stringify(payload));
|
localStorage.setItem(PERSIST_KEY, JSON.stringify(payload));
|
||||||
@@ -65,7 +69,7 @@ const clearVisitedViews = () => {
|
|||||||
localStorage.removeItem(PERSIST_KEY);
|
localStorage.removeItem(PERSIST_KEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTagsViewStore = defineStore('tagsView', () => {
|
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||||
const visitedViews = ref<TagView[]>([]);
|
const visitedViews = ref<TagView[]>([]);
|
||||||
const cachedViews = ref<string[]>([]);
|
const cachedViews = ref<string[]>([]);
|
||||||
const iframeViews = ref<TagView[]>([]);
|
const iframeViews = ref<TagView[]>([]);
|
||||||
@@ -86,26 +90,49 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addIframeView = (view: RouteLocationNormalized): void => {
|
const addIframeView = (view: RouteLocationNormalized): void => {
|
||||||
if (iframeViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
if (
|
||||||
iframeViews.value.push(normalizeVisitedView(view) as RouteLocationNormalized);
|
iframeViews.value.some(
|
||||||
|
(v: RouteLocationNormalized) => v.path === view.path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
iframeViews.value.push(
|
||||||
|
normalizeVisitedView(view) as RouteLocationNormalized,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const delIframeView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delIframeView = (
|
||||||
return new Promise(resolve => {
|
view: RouteLocationNormalized,
|
||||||
iframeViews.value = iframeViews.value.filter((item: RouteLocationNormalized) => item.path !== view.path);
|
): Promise<RouteLocationNormalized[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
iframeViews.value = iframeViews.value.filter(
|
||||||
|
(item: RouteLocationNormalized) => item.path !== view.path,
|
||||||
|
);
|
||||||
resolve(iframeViews.value.slice() as RouteLocationNormalized[]);
|
resolve(iframeViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addVisitedView = (view: RouteLocationNormalized): void => {
|
const addVisitedView = (view: RouteLocationNormalized): void => {
|
||||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
if (
|
||||||
|
visitedViews.value.some(
|
||||||
|
(v: RouteLocationNormalized) => v.path === view.path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
visitedViews.value.push(normalizeVisitedView(view));
|
visitedViews.value.push(normalizeVisitedView(view));
|
||||||
saveVisitedViews(visitedViews.value);
|
saveVisitedViews(visitedViews.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addAffixView = (view: RouteLocationNormalized): void => {
|
const addAffixView = (view: RouteLocationNormalized): void => {
|
||||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
if (
|
||||||
const insertIndex = visitedViews.value.findIndex(item => !item.meta?.affix);
|
visitedViews.value.some(
|
||||||
|
(v: RouteLocationNormalized) => v.path === view.path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const insertIndex = visitedViews.value.findIndex(
|
||||||
|
(item) => !item.meta?.affix,
|
||||||
|
);
|
||||||
const normalizedView = normalizeVisitedView(view);
|
const normalizedView = normalizeVisitedView(view);
|
||||||
if (insertIndex === -1) {
|
if (insertIndex === -1) {
|
||||||
visitedViews.value.push(normalizedView);
|
visitedViews.value.push(normalizedView);
|
||||||
@@ -115,13 +142,13 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadPersistedViews = (): void => {
|
const loadPersistedViews = (): void => {
|
||||||
loadVisitedViews().forEach(view => {
|
loadVisitedViews().forEach((view) => {
|
||||||
if (visitedViews.value.some(item => item.path === view.path)) {
|
if (visitedViews.value.some((item) => item.path === view.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitedViews.value.push({
|
visitedViews.value.push({
|
||||||
hash: '',
|
hash: "",
|
||||||
matched: [],
|
matched: [],
|
||||||
params: {},
|
params: {},
|
||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
@@ -130,29 +157,31 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
query: view.query || {},
|
query: view.query || {},
|
||||||
name: view.name || undefined,
|
name: view.name || undefined,
|
||||||
meta: view.meta || {},
|
meta: view.meta || {},
|
||||||
title: view.title || view.meta?.title || 'no-name'
|
title: view.title || view.meta?.title || "no-name",
|
||||||
} as TagView);
|
} as TagView);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delView = (
|
const delView = (
|
||||||
view: RouteLocationNormalized
|
view: RouteLocationNormalized,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
visitedViews: RouteLocationNormalized[];
|
visitedViews: RouteLocationNormalized[];
|
||||||
cachedViews: string[];
|
cachedViews: string[];
|
||||||
}> => {
|
}> => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
delVisitedView(view);
|
delVisitedView(view);
|
||||||
delCachedView(view);
|
delCachedView(view);
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delVisitedView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delVisitedView = (
|
||||||
return new Promise(resolve => {
|
view: RouteLocationNormalized,
|
||||||
|
): Promise<RouteLocationNormalized[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
for (const [i, v] of visitedViews.value.entries()) {
|
for (const [i, v] of visitedViews.value.entries()) {
|
||||||
if (v.path === view.path) {
|
if (v.path === view.path) {
|
||||||
visitedViews.value.splice(i, 1);
|
visitedViews.value.splice(i, 1);
|
||||||
@@ -165,11 +194,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
|
const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
|
||||||
let viewName = '';
|
let viewName = "";
|
||||||
if (view) {
|
if (view) {
|
||||||
viewName = view.name as string;
|
viewName = view.name as string;
|
||||||
}
|
}
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const index = cachedViews.value.indexOf(viewName);
|
const index = cachedViews.value.indexOf(viewName);
|
||||||
index > -1 && cachedViews.value.splice(index, 1);
|
index > -1 && cachedViews.value.splice(index, 1);
|
||||||
resolve([...cachedViews.value]);
|
resolve([...cachedViews.value]);
|
||||||
@@ -177,34 +206,40 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const delOthersViews = (
|
const delOthersViews = (
|
||||||
view: RouteLocationNormalized
|
view: RouteLocationNormalized,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
visitedViews: RouteLocationNormalized[];
|
visitedViews: RouteLocationNormalized[];
|
||||||
cachedViews: string[];
|
cachedViews: string[];
|
||||||
}> => {
|
}> => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
delOthersVisitedViews(view);
|
delOthersVisitedViews(view);
|
||||||
delOthersCachedViews(view);
|
delOthersCachedViews(view);
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delOthersVisitedViews = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delOthersVisitedViews = (
|
||||||
return new Promise(resolve => {
|
view: RouteLocationNormalized,
|
||||||
visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => {
|
): Promise<RouteLocationNormalized[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
visitedViews.value = visitedViews.value.filter(
|
||||||
|
(v: RouteLocationNormalized) => {
|
||||||
return v.meta?.affix || v.path === view.path;
|
return v.meta?.affix || v.path === view.path;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
saveVisitedViews(visitedViews.value);
|
saveVisitedViews(visitedViews.value);
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delOthersCachedViews = (view: RouteLocationNormalized): Promise<string[]> => {
|
const delOthersCachedViews = (
|
||||||
|
view: RouteLocationNormalized,
|
||||||
|
): Promise<string[]> => {
|
||||||
const viewName = view.name as string;
|
const viewName = view.name as string;
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const index = cachedViews.value.indexOf(viewName);
|
const index = cachedViews.value.indexOf(viewName);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||||
@@ -219,26 +254,28 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
visitedViews: RouteLocationNormalized[];
|
visitedViews: RouteLocationNormalized[];
|
||||||
cachedViews: string[];
|
cachedViews: string[];
|
||||||
}> => {
|
}> => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
delAllVisitedViews();
|
delAllVisitedViews();
|
||||||
delAllCachedViews();
|
delAllCachedViews();
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
|
const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
visitedViews.value = visitedViews.value.filter((tag: RouteLocationNormalized) => tag.meta?.affix);
|
visitedViews.value = visitedViews.value.filter(
|
||||||
|
(tag: RouteLocationNormalized) => tag.meta?.affix,
|
||||||
|
);
|
||||||
clearVisitedViews();
|
clearVisitedViews();
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delAllCachedViews = (): Promise<string[]> => {
|
const delAllCachedViews = (): Promise<string[]> => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
cachedViews.value = [];
|
cachedViews.value = [];
|
||||||
resolve([...cachedViews.value]);
|
resolve([...cachedViews.value]);
|
||||||
});
|
});
|
||||||
@@ -254,14 +291,19 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
saveVisitedViews(visitedViews.value);
|
saveVisitedViews(visitedViews.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const delRightTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delRightTags = (
|
||||||
return new Promise(resolve => {
|
view: RouteLocationNormalized,
|
||||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
): Promise<RouteLocationNormalized[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = visitedViews.value.findIndex(
|
||||||
|
(v: RouteLocationNormalized) => v.path === view.path,
|
||||||
|
);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
visitedViews.value = visitedViews.value.filter(
|
||||||
|
(item: RouteLocationNormalized, idx: number) => {
|
||||||
if (idx <= index || (item.meta && item.meta.affix)) {
|
if (idx <= index || (item.meta && item.meta.affix)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -270,20 +312,26 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
cachedViews.value.splice(i, 1);
|
cachedViews.value.splice(i, 1);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
saveVisitedViews(visitedViews.value);
|
saveVisitedViews(visitedViews.value);
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delLeftTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delLeftTags = (
|
||||||
return new Promise(resolve => {
|
view: RouteLocationNormalized,
|
||||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
): Promise<RouteLocationNormalized[]> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const index = visitedViews.value.findIndex(
|
||||||
|
(v: RouteLocationNormalized) => v.path === view.path,
|
||||||
|
);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
visitedViews.value = visitedViews.value.filter(
|
||||||
|
(item: RouteLocationNormalized, idx: number) => {
|
||||||
if (idx >= index || (item.meta && item.meta.affix)) {
|
if (idx >= index || (item.meta && item.meta.affix)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -292,7 +340,8 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
cachedViews.value.splice(i, 1);
|
cachedViews.value.splice(i, 1);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
saveVisitedViews(visitedViews.value);
|
saveVisitedViews(visitedViews.value);
|
||||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
@@ -332,6 +381,6 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
delLeftTags,
|
delLeftTags,
|
||||||
addIframeView,
|
addIframeView,
|
||||||
delIframeView,
|
delIframeView,
|
||||||
loadPersistedViews
|
loadPersistedViews,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
13
src/utils/time.ts
Normal file
13
src/utils/time.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 根据数字 1-7 生成对应星期
|
||||||
|
* @param num 1=周一,2=周二...7=周日
|
||||||
|
* @param options 选项对象
|
||||||
|
* @param options.prefix 选项,可自定义前缀,默认 '周'
|
||||||
|
* @returns 周一 / 周二 ... / 周日
|
||||||
|
*/
|
||||||
|
export function numToWeek(num: number,options = {
|
||||||
|
prefix: '周',
|
||||||
|
}): string {
|
||||||
|
const weekMap = ['一', '二', '三', '四', '五', '六', '日'];
|
||||||
|
return options.prefix + weekMap[num - 1] || '';
|
||||||
|
}
|
||||||
@@ -47,18 +47,12 @@
|
|||||||
<div
|
<div
|
||||||
class="metric-card"
|
class="metric-card"
|
||||||
:class="{
|
:class="{
|
||||||
active: item.isActivcty,
|
active: item.isAction,
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
<div>
|
<div>
|
||||||
周{{
|
<span>{{ numToWeek(item.formateDay + 1)}}</span>
|
||||||
new Intl.NumberFormat("zh-Hans-CN-u-nu-hanidec").format(
|
<span style="font-size: 12px">({{ item.month }}.{{ item.date }})</span>
|
||||||
item.formatlday + 1,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
<span style="font-size: 12px"
|
|
||||||
>({{ item.month }}.{{ item.date }})</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,6 +162,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import {getCodeImg} from "@/api/login";
|
import {getCodeImg} from "@/api/login";
|
||||||
import {LoginData} from "@/api/types";
|
import {LoginData} from "@/api/types";
|
||||||
import {useUserStore} from "@/store/modules/user";
|
import {useUserStore} from "@/store/modules/user";
|
||||||
|
import {numToWeek} from "@/utils/time";
|
||||||
|
|
||||||
const title = import.meta.env.VITE_APP_TITLE;
|
const title = import.meta.env.VITE_APP_TITLE;
|
||||||
const quickStats = ref([]);
|
const quickStats = ref([]);
|
||||||
@@ -292,17 +287,14 @@ const handleGetLoc = async () => {
|
|||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const arr = res.data.forecast.daily.pm10 ?? [];
|
const arr = res.data.forecast.daily.pm10 ?? [];
|
||||||
console.log(arr.slice(0, 7));
|
quickStats.value = handleMax(arr.slice(0, 7));
|
||||||
const list = handleMaxWaqi(arr.slice(0, 7));
|
|
||||||
quickStats.value = list;
|
|
||||||
console.log(list);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error((e as Error).message);
|
console.error((e as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleMaxWaqi(arr: { avg: number; day: string }[]) {
|
function handleMax(arr: { avg: number; day: string }[]) {
|
||||||
const maxVal = Math.max(...arr.map((item) => item.avg));
|
const maxVal = Math.max(...arr.map((item) => item.avg));
|
||||||
const nowDate = new Date();
|
const nowDate = new Date();
|
||||||
return arr.map((item) => {
|
return arr.map((item) => {
|
||||||
@@ -312,9 +304,9 @@ function handleMaxWaqi(arr: { avg: number; day: string }[]) {
|
|||||||
...item,
|
...item,
|
||||||
rate,
|
rate,
|
||||||
month: newDate.getMonth() + 1,
|
month: newDate.getMonth() + 1,
|
||||||
formatlday: newDate.getDay(),
|
formateDay: newDate.getDay(),
|
||||||
date: newDate.getDate(),
|
date: newDate.getDate(),
|
||||||
isActivcty: newDate.getDate() === nowDate.getDate(),
|
isAction: newDate.getDate() === nowDate.getDate(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -369,7 +361,7 @@ onMounted(() => {
|
|||||||
.title {
|
.title {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
border-radius: 0px 0px 0px 0px;
|
border-radius: 0 0 0 0;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgba(255, 255, 255, 1);
|
color: rgba(255, 255, 255, 1);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -377,7 +369,7 @@ onMounted(() => {
|
|||||||
.subTitle {
|
.subTitle {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
border-radius: 0px 0px 0px 0px;
|
border-radius: 0 0 0 0;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgba(255, 255, 255, 0.722);
|
color: rgba(255, 255, 255, 0.722);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -451,7 +443,7 @@ onMounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
.metric-card {
|
.metric-card {
|
||||||
border-radius: 8px 8px 0px 0px;
|
border-radius: 8px 8px 0 0;
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -475,7 +467,7 @@ onMounted(() => {
|
|||||||
.brand-footer {
|
.brand-footer {
|
||||||
margin-top: 157px;
|
margin-top: 157px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
border-radius: 0px 0px 0px 0px;
|
border-radius: 0 0 0 0;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgba(255, 255, 255, 0.502);
|
color: rgba(255, 255, 255, 0.502);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -540,7 +532,7 @@ onMounted(() => {
|
|||||||
.loginModel {
|
.loginModel {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
border-radius: 0px 0px 0px 0px;
|
border-radius: 0 0 0 0;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
color: rgba(37, 99, 235, 1);
|
color: rgba(37, 99, 235, 1);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -636,7 +628,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.login-form :deep(.el-input__wrapper) {
|
.login-form :deep(.el-input__wrapper) {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
background-color: var(--el-bg-color);
|
|
||||||
background-color: rgba(247, 248, 250, 1);
|
background-color: rgba(247, 248, 250, 1);
|
||||||
box-shadow: 0 0 0 1px var(--app-surface-border) inset;
|
box-shadow: 0 0 0 1px var(--app-surface-border) inset;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user