chore(frontend): 落地 ADS 治理体系并收录前端底座基线代码
@@ -0,0 +1,17 @@
|
|||||||
|
#需要过滤文件
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
#编辑器目录和文件
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.local
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
AGENTS.md is the cross-vendor contributor guide (Claude, Codex, and others read it). Keep it in English by convention. It overlaps CLAUDE.md on purpose — this is the canonical version for non-Claude agents.
|
||||||
|
|
||||||
|
## Authority & Required Reading
|
||||||
|
Rules in `.project.agents/CLAUDE.md` apply to every agent working here. Direct user instructions still take precedence.
|
||||||
|
|
||||||
|
The canonical contributor guide is `.project.agents/AGENTS.md`. Do not create or maintain a root-level `AGENTS.md` unless the user explicitly asks for one.
|
||||||
|
|
||||||
|
Before any code-changing task, read `.project.agents/SELF_CONSTRAINTS.md` and `.project.agents/VIBECODING_GUIDE.md`, then run `git status --short`. For product work, also read the relevant sections of `PRD.md`, `ARCHITECTURE.md`, `ROADMAP.md`, and `CONVENTIONS.md` under `.project.agents/docs/context/`. Shared front-backend constraints live in the workspace-level `docs/` (see `../../../docs/coding-standards.md`) — read the referenced sections instead of duplicating them here.
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
`abacus-static-framework` is an enterprise Vue 3 + TypeScript microservice front-end base (menu framework) built with Vue 3.5, TypeScript 5, Vite 6, Element Plus, vxe-table, Pinia, axios. Business code lives in `src/subsystem/**` (reference template: the `sms` examples, copy-and-rewrite); `src/framework/**` is READ-ONLY shared framework. No automated tests.
|
||||||
|
|
||||||
|
Target architecture is documented in `ARCHITECTURE.md`: framework (shared, read-only) / subsystem (business writable) / dynamic menu-permission routing.
|
||||||
|
|
||||||
|
## Source-of-Truth Rules
|
||||||
|
`PRD.md` defines behavior and scope.
|
||||||
|
`UIUX.md` defines visual and interaction rules.
|
||||||
|
`ARCHITECTURE.md` is the authority for modules, dependencies, contracts, and directory layout. Derived documents follow upstream changes, not the other way around.
|
||||||
|
|
||||||
|
Feature changes must update `PRD.md`. Module, dependency, entity, or invariant changes must update `ARCHITECTURE.md` in the same commit. Do not let code and architecture drift for more than 24 hours.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build (output goes straight to the target microservice's resources/html/vue/<app>/)
|
||||||
|
npm run build:prod
|
||||||
|
|
||||||
|
# Run
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Test
|
||||||
|
No automated tests (manual smoke only)
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment: Node 18.20.7, npm mirror `https://registry.npmmirror.com`. Before local integration, adjust `vite.config.ts` (`build.outDir` → target service's `resources/html/vue/<app>/`; `server.proxy` → target backend) and `public/data/data.js` (`currentSubsystem`). After a framework upgrade, run `npm install`; never overwrite `public`, `vite.config.ts`, or `src/subsystem`.
|
||||||
|
|
||||||
|
## Architecture & Coding Style
|
||||||
|
Use TypeScript, 2-space indentation. Kebab-case files, PascalCase component names (must equal the route `name`), `getXxx`/`saveXxx` API function names. Do not add modules that are not registered in `ARCHITECTURE.md`. Preserve the dependency DAG; no reverse dependencies and no cross-module private access. Avoid empty names such as `Manager`, `Helper`, `Util`, `Common`, `Misc`, or `Tools`.
|
||||||
|
|
||||||
|
UI/presentation code should consume state and send intents only — business logic goes into hooks/services; keep data-model types thin. Data access must go through the `request` wrapper (`src/framework/utils/request.ts`) — never raw `axios`/`fetch`; the wrapper handles service-alias mapping, token refresh, commonParam header, and response unwrapping (see shared spec §6).
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
No automated test framework. Required validation from `ARCHITECTURE.md`: manual smoke per page flow (list / query / create / edit / export). Multi-datasource routing, Kafka consumption, XXL-JOB scheduling, Nacos config pull, and real database migrations require real-environment validation; mock/simulator-only evidence is not enough for those claims.
|
||||||
|
|
||||||
|
## Git, Logs, and PRs
|
||||||
|
Do not pile new work onto unrelated dirty changes without calling it out. Prefer Conventional Commits such as `feat: ...`, `fix: ...`, `docs: ...`. This repository is part of a single git repo rooted at the workspace (`D:\workBuddySpace\member`); commit scope should stay within this subproject unless the change is cross-project (docs/).
|
||||||
|
|
||||||
|
Each independently verifiable feature, milestone, non-trivial fix, or refactor needs an execution log in `.project.agents/log/YYYY-MM-DD-<slug>.md` unless it is only a minor documentation edit. Logs must include what changed, relevant commits (or "uncommitted" with reason), verification performed, and next steps.
|
||||||
|
|
||||||
|
Pull requests should include scope, linked issues, screenshots for UI changes, environment/device coverage, and any documentation updates.
|
||||||
|
|
||||||
|
## Security & Configuration
|
||||||
|
Do not commit secrets, credentials, build output, or personal IDE files. Agent configuration and generated agent notes belong under `.project.agents/`, not the repository root, `.claude/`, or a global home directory.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file guides Claude Code (and other agents) when working in this repository.
|
||||||
|
**保持本文件最新**:它是项目级入口——项目是什么、怎么构建、门控一切变更的规则。
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`abacus-static-framework` 是企业级 **Vue 3 + TypeScript 微服务前端底座**(菜单框架),基于 Vue 3.5 / TypeScript 5 / Vite 6 / Element Plus / vxe-table / Pinia / axios。业务代码位于 `src/subsystem/**`(参考模板:`subsystem` 下的 sms 示例,供复制改写);`src/framework/**` 为**只读共享框架**,业务不得修改。
|
||||||
|
|
||||||
|
- Entry point: `src/main.ts`
|
||||||
|
- **只读/可写分区**:`src/framework/**` 只读;业务开发只写 `src/subsystem/**`(api / router / views / extend 及自建 components / hooks / store / styles / types / utils)。
|
||||||
|
- **接口契约**(统一响应体、服务别名映射、token 自动刷新、commonParam)由 `src/framework/utils/request.ts` 强制实现,业务无感;共享约束详见 `../../../docs/coding-standards.md`(§6 接口契约、§4 前端规范)。
|
||||||
|
- Known environment constraints: Node 18.20.7 + npm 淘宝镜像;`vite.config.ts` 的 `build.outDir` 与 `server.proxy` 需按目标微服务修改后才能联调/发布;`public/data/data.js` 的 `currentSubsystem`/`currentSubsystemName` 声明当前子系统。
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build(产物直出目标微服务 resources/html/vue/<应用名>/)
|
||||||
|
npm run build:prod
|
||||||
|
|
||||||
|
# Run(本地开发,Vite 反代后端)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Test
|
||||||
|
无自动化测试(人工冒烟验证)
|
||||||
|
```
|
||||||
|
|
||||||
|
环境注意:首次使用需 `npm install --registry=https://registry.npmmirror.com`(或已配置镜像则 `npm install`)。框架升级后需重新 `npm install`;升级**不得覆盖** `public`、`vite.config.ts`、`src/subsystem`。
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
实现遵循 `.project.agents/docs/context/ARCHITECTURE.md` 的模块边界;模块清单(职责一句话):
|
||||||
|
|
||||||
|
- **framework/**(只读共享框架):api(框架级后端接口)、components/standard(标准组件:AbForm/AbQueryForm/vxe 表格等 10 项)、hooks(useVxeTableHandle 等通用组合式函数)、layout(菜单布局)、router(静态路由)、store(settings/permission/user 等 Pinia 状态)、styles、types、utils(standard 5 项 + request 请求管线)、views、directives(v-debounce/v-throttle)。
|
||||||
|
- **subsystem/**(业务可写区):api/<服务别名>(按 Controller 分 ts)、router/dynamicRouter.ts(菜单路由唯一注册点)、views/<一级>/<二级>(页面)、extend(框架扩展钩子)、以及业务自建 components/hooks/store/styles/types/utils。
|
||||||
|
- 动态权限:`permissionStore.filterAsyncRoutes` 按 `url === path` 匹配后端菜单与 dynamicRouter 后 `addRoute`。
|
||||||
|
|
||||||
|
## Project-level rules
|
||||||
|
|
||||||
|
- **动手前必读两文件**——它们编码了本项目所有门控决策的经验:
|
||||||
|
- `.project.agents/VIBECODING_GUIDE.md` — 实践指南(为什么 & 怎么做)
|
||||||
|
- `.project.agents/SELF_CONSTRAINTS.md` — 硬约束(什么禁止、什么必须、何时停下)
|
||||||
|
|
||||||
|
任何非平凡任务,编辑前先跑 `SELF_CONSTRAINTS.md §A`(开工前自检)。
|
||||||
|
|
||||||
|
- **共享约束单一来源**:前后端共享的编码规范/接口契约/命名纪律,**只存在于工作区 `docs/`(`../../../docs/coding-standards.md`、`../../../docs/architecture.md`、`../../../docs/agent-guide.md`)**;本仓库 `.project.agents/docs/context/` 只写本仓库特有内容与指针,**禁止复制 docs/ 正文**。改共享内容先改 docs/ 再同步指针。
|
||||||
|
|
||||||
|
- **Agent 配置**一律写在 `.project.agents/` 下(或子目录),禁止写到仓库根、`.claude/` 或全局目录。
|
||||||
|
|
||||||
|
- **项目文档**位于 `.project.agents/docs/context/`(单层目录):`PRD.md`(行为权威)、`ARCHITECTURE.md`(模块/依赖/契约)、`ROADMAP.md`(里程碑)、`CONVENTIONS.md`(命名/风格/提交)、`UIUX.md`(视觉与交互)。
|
||||||
|
|
||||||
|
- **源真相层级**(冲突时上游优先):
|
||||||
|
```
|
||||||
|
PRD.md > ARCHITECTURE.md > UIUX.md > CONVENTIONS.md > 派生文档
|
||||||
|
仓库级 CONVENTIONS 再下接共享 docs/coding-standards.md(仅作全仓公共约定引用)
|
||||||
|
```
|
||||||
|
上游变更须在同 commit 内回写受影响的派生文档;代码与架构漂移不得超过 24 小时。
|
||||||
|
|
||||||
|
- **路线图执行规则**:按 `.project.agents/docs/context/ROADMAP.md` 顺序推进,勾选与完成同 commit;milestone 级/多文件任务先写 plan/spec 再动手;每个 milestone 后按 `.project.agents/log/` 模板写执行日志。
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Agent 自我约束清单(本项目硬规则)
|
||||||
|
|
||||||
|
> 这是 Agent 在本项目工作时必须遵守的硬约束。每次开工前自查本清单,违反任何一条都应**先停下**,与用户对齐后再继续。
|
||||||
|
> 完整背景与原理请见 [`VIBECODING_GUIDE.md`](./VIBECODING_GUIDE.md)。本清单只列"不许做什么 / 必须做什么"。
|
||||||
|
>
|
||||||
|
> 文档约定(本项目):上游文档位于 `.project.agents/docs/context`,依次为
|
||||||
|
> `PRD.md`(行为与范围)、`ARCHITECTURE.md`(模块/依赖/契约/目录)、`CONVENTIONS.md`(命名/风格/提交)。
|
||||||
|
> 派生文档不得反向修改上游。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. 开工前的强制自检(每个新会话开始时执行)
|
||||||
|
|
||||||
|
在进行任何编辑动作之前,必须确认以下事项;任一项不满足则先暂停业务任务,处理掉再继续。
|
||||||
|
|
||||||
|
- [ ] **A1. 仓库是否在版本控制下?** 若否:立即停下,先初始化仓库 + 配置 ignore + 初始 commit。
|
||||||
|
- [ ] **A2. 工作区是否干净?** 有未提交的旧改动?先与用户确认是否要先提交/暂存,**不允许在脏工作区上叠加新改动**。
|
||||||
|
- [ ] **A3. 是否已有 `ARCHITECTURE.md`?**
|
||||||
|
- 若否,且本次任务**涉及写业务代码**:先停下,按 `VIBECODING_GUIDE` 的流程产出架构文档,再写代码。
|
||||||
|
- 若否,且本次任务**只是文档/讨论**:可继续,但应主动提醒用户补架构文档。
|
||||||
|
- [ ] **A4. 任务的归属模块清楚吗?** 我能否在 `ARCHITECTURE.md` 的模块清单里指出"这个改动属于哪个模块"?指不出来则停下补架构。
|
||||||
|
- [ ] **A5. 上游文档是否已读?** `PRD.md` / `ARCHITECTURE.md` / `CONVENTIONS.md` 中与本任务相关的章节是否读过?没读过先读,不要凭印象动手。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. 写代码时的硬约束
|
||||||
|
|
||||||
|
### B1. 模块边界
|
||||||
|
- ❌ 不允许新增**未在 `ARCHITECTURE.md` 中登记**的模块;要新增必须先更新架构文档。
|
||||||
|
- ❌ 不允许引入**反向依赖**(低层模块依赖高层模块)。需要时改为接口/回调倒置。
|
||||||
|
- ❌ 不允许跨模块**直接访问私有实现**;只能走该模块对外暴露的接口。
|
||||||
|
- ❌ 不允许出现 `Manager` / `Helper` / `Util` / `Common` / `Misc` / `Tools` 这类语义为空的新模块名;用动词+名词描述真实职责。
|
||||||
|
|
||||||
|
### B2. 单一职责
|
||||||
|
- ❌ 不允许在 UI/展示层写业务判断或副作用(仅消费状态、发送意图)。
|
||||||
|
- ❌ 不允许在数据模型类里堆业务方法;模型保持瘦,逻辑放 Service / UseCase。
|
||||||
|
- ❌ 不允许"顺手"修改不在任务范围内的模块;越界的改动走单独提交并明确告知用户。
|
||||||
|
|
||||||
|
### B3. 文档同步(与代码改动**同一个 commit** 内)
|
||||||
|
- 加/删/改功能 → 同步更新 `PRD.md`
|
||||||
|
- 加/删/改模块或依赖 → 同步更新 `ARCHITECTURE.md`(模块清单 + 依赖图)
|
||||||
|
- 派生文档发现与上游不一致 → 回去改上游,不能让派生文档自走
|
||||||
|
|
||||||
|
### B4. 注释与命名
|
||||||
|
- 默认不写注释;只在 WHY 不明显时写一行。
|
||||||
|
- 不写解释 WHAT 的注释(命名应自解释)。
|
||||||
|
- 不写"为某 issue/任务而加"这类会随时间失效的注释。
|
||||||
|
- 严格遵守 `CONVENTIONS.md` 中的命名规范(不存在则先补)。
|
||||||
|
|
||||||
|
### B5. 错误处理
|
||||||
|
- 只在系统边界(用户输入、外部 API、文件 IO)做防御;内部模块互相信任。
|
||||||
|
- 不为不可能发生的情况加 fallback。
|
||||||
|
- 不引入向后兼容 shim,除非用户明确要求。
|
||||||
|
|
||||||
|
### B6. 实施日志(execution log)
|
||||||
|
- **每完成一个可独立验收的部分(milestone 或子任务),必须在 `.project.agents/log` 写日志**。格式与时机见 `VIBECODING_GUIDE`。
|
||||||
|
- 日志内必须包含本段的版本控制提交列表;若本段未提交,明确写出"未提交"及原因。
|
||||||
|
- 完成路线图任一 milestone → **必须**写日志,且与该 milestone 的提交同一会话完成。
|
||||||
|
- 单次会话即将结束(用户表示收工 / 上下文将被压缩)→ **必须**写一份兜底日志记录当前状态与下一步。
|
||||||
|
- ❌ 不允许只提交不写日志(除非属于豁免情况:纯文档微调 / 被回滚的实验性探索)。
|
||||||
|
- ❌ 不允许写日志但跳过更新 `ARCHITECTURE.md` —— 若本段触发架构变更,先改架构、再提交、再写日志。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. 改动范围控制
|
||||||
|
|
||||||
|
- ❌ 一次任务一个目的。不要"既加功能又重构又改样式"。
|
||||||
|
- ❌ 不允许在 bug 修复任务中做无关清理。
|
||||||
|
- ❌ 不允许引入"为未来准备"的抽象(YAGNI);三处相似优于一处过度抽象。
|
||||||
|
- ❌ 不允许半完成的实现(要么这次完成,要么不要开始)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. 出问题时的排查纪律
|
||||||
|
|
||||||
|
bug 处理必须按下列顺序,**严禁跳步**:
|
||||||
|
|
||||||
|
1. 复现现象(明确触发路径)
|
||||||
|
2. 在 `ARCHITECTURE.md` 上**圈出嫌疑模块**
|
||||||
|
3. 检查跨模块**接口契约**
|
||||||
|
4. 缩小到单模块后再读代码
|
||||||
|
5. 修完后:若是架构问题,必须更新 `ARCHITECTURE.md` 或 `CONVENTIONS.md`
|
||||||
|
|
||||||
|
**禁止行为**:bug 一来就全仓 grep、试改一行看效果、连续 try-fail 循环。出现这种倾向立刻停下,回到第 2 步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E. 与用户的协作约束
|
||||||
|
|
||||||
|
- ❌ 不替用户做架构决策。涉及模块拆分、依赖方向、命名规范这类**长期影响**的决定,必须明确询问。
|
||||||
|
- ❌ 不在用户没要求的时候主动创建文档(除非本清单或 `CLAUDE.md` 已要求)。
|
||||||
|
- ❌ 不在没看代码的情况下凭推测回答 "X 是怎么实现的"。
|
||||||
|
- ✅ 每次涉及架构/文档/规范变更,先告知影响面,再动手。
|
||||||
|
- ✅ 用户的明确指示永远高于本清单和任何 skill。冲突时遵循用户。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F. 触发"全员停车"的红线
|
||||||
|
|
||||||
|
出现以下任一情况,**立即停止当前任务,与用户对齐后再决定下一步**:
|
||||||
|
|
||||||
|
1. 发现自己在脏工作区上累积了大量未提交改动
|
||||||
|
2. 发现自己正在新增一个"无处归属"的模块/文件
|
||||||
|
3. 发现代码与 `ARCHITECTURE.md` 已经矛盾,且无法用小补丁同步
|
||||||
|
4. 同一个 bug 连续 3 次尝试修复未果
|
||||||
|
5. 用户的请求与本清单 / `CLAUDE.md` / 上游文档存在冲突 —— 必须先澄清,不能默默选边
|
||||||
|
6. 即将做出"难以回滚"的动作(删除文件 / 重命名模块 / 调整目录结构 / 强制 push)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G. 文件路径合规
|
||||||
|
|
||||||
|
- Agent 的配置文件**只**写入 `.project.agents` 子树,禁止写入仓库根目录或其他非约定位置。
|
||||||
|
- 项目文档**只**写入 `.project.agents/docs/context`(不允许再分子目录,除非用户明确要求)。
|
||||||
|
- 本清单(`SELF_CONSTRAINTS.md`)与指南(`VIBECODING_GUIDE.md`)位于 `.project.agents` 根,与 `CLAUDE.md` 同级。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## H. 自查触发器(什么时候重读本清单)
|
||||||
|
|
||||||
|
至少在以下时机重读本清单:
|
||||||
|
|
||||||
|
- 新会话开始,且任务涉及代码改动
|
||||||
|
- 用户要求"加新功能"/"修 bug"/"重构"
|
||||||
|
- 自己感到"差不多可以直接动手了" —— 这种感觉本身就是触发器
|
||||||
|
- 即将创建超过 1 个新文件
|
||||||
|
- 即将修改 3 个以上文件
|
||||||
|
- 即将修改任何 `.project.agents/docs/context` 下的文档
|
||||||
|
- **完成一个路线图 milestone 或子任务前**(确认是否需要按 §B6 写日志)
|
||||||
|
- **会话即将结束前**(确认兜底日志已写)
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# 项目开发实践指南(VIBECODING GUIDE)
|
||||||
|
|
||||||
|
> 本文档是基于"踩过的坑"沉淀下来的项目工作方式约定,适用于本仓库及后续所有同性质的项目。
|
||||||
|
> 它的姊妹文件是 [`SELF_CONSTRAINTS.md`](./SELF_CONSTRAINTS.md) —— 那是 Agent 每次开工前必须自查的硬约束清单。
|
||||||
|
> 本文回答 "为什么这么做";`SELF_CONSTRAINTS.md` 回答 "具体不许做什么"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 三条核心教训(本指南的来由,优先级严格递减)
|
||||||
|
|
||||||
|
1. **版本控制必须从第 0 天开始。**
|
||||||
|
不是 "等做出点东西再说",是写第一行代码之前先初始化仓库、写好 ignore 规则、提交一个空骨架。
|
||||||
|
理由:高频改动 + 频繁推翻是 AI 协作的常态,没有版本控制兜底就等于在悬崖边裸奔。
|
||||||
|
|
||||||
|
2. **文档必须在写代码之前定下来。**
|
||||||
|
需求、架构、路线图、开发规范、目录结构、命名规范、模块边界 —— 这些不是"做完之后补的",是"做之前定的"。
|
||||||
|
理由:一旦上下文被代码细节占满,后续每个新会话都只能 freestyle,项目会朝不可逆的方向漂移。
|
||||||
|
|
||||||
|
3. **架构 > UI > 用户体验。**
|
||||||
|
最致命的不是界面丑、不是体验差,而是模块互相纠缠到无法定位问题。
|
||||||
|
一个出问题就能立刻指认 "是 X 模块的责任" 的项目,比一个好看但耦合一团的项目,存活率高一个数量级。
|
||||||
|
理由:耦合一旦发生,AI 协作模式会迅速放大它 —— 因为每次新会话都看不全全貌,只会在原有耦合上继续添乱。
|
||||||
|
|
||||||
|
> 这三条的优先级是严格递减的:先有版本控制,再有文档,再谈架构;架构稳了之后才轮到 UI 和体验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目启动 Checklist(Day 0)
|
||||||
|
|
||||||
|
下列每一项都必须在写第一行业务代码之前完成。顺序即优先级。
|
||||||
|
|
||||||
|
### 1.1 仓库与版本控制
|
||||||
|
- [ ] 初始化版本控制并立即建立 ignore 规则(语言 / IDE / 系统三类至少齐全)
|
||||||
|
- [ ] 提交一个 **空骨架 commit**(README + ignore 规则 + 空目录结构),作为"零点参照系"
|
||||||
|
- [ ] 约定分支模型(最简:主干 + 短命的功能分支;不要等到分叉时才想)
|
||||||
|
- [ ] 约定 commit message 风格(推荐 Conventional Commits,至少要求动词开头 + 一句话原因)
|
||||||
|
|
||||||
|
### 1.2 文档骨架(必须先于代码存在)
|
||||||
|
在 `.project.agents/docs/context` 下建立以下文件,**哪怕只有大纲也比没有强**:
|
||||||
|
|
||||||
|
| 文件 | 角色 | 上下游关系 |
|
||||||
|
|---|---|---|
|
||||||
|
| `PRD.md` | 产品需求(要做什么) | 上游 |
|
||||||
|
| `ARCHITECTURE.md` | 架构(怎么拆) | 上游 |
|
||||||
|
| `ROADMAP.md` | 路线图(什么时候做哪一块) | 由需求 + 架构推导 |
|
||||||
|
| `CONVENTIONS.md` | 开发规范(命名、目录、提交、风格) | 上游 |
|
||||||
|
| `UIUX.md` | 视觉与交互细节(仅有 UI 的项目需要) | 上游(与需求并列) |
|
||||||
|
| 派生文档(实现 spec / 资产清单等) | 屏幕级实现 / 资产清单 | **派生**,由上游产生 |
|
||||||
|
|
||||||
|
**判断标准**:如果一个新会话的 Agent 只读需求 + 架构 + 开发规范就能正确开始一个任务 —— 文档就算合格。
|
||||||
|
|
||||||
|
### 1.3 Agent 协作配置
|
||||||
|
- [ ] 在 `CLAUDE.md` 写明项目级规则(已有,保持更新)
|
||||||
|
- [ ] 在本文件 + `SELF_CONSTRAINTS.md` 中固化经验
|
||||||
|
- [ ] 在 `.project.agents/settings.json` 中配置必要的 hook / 权限(不要散落到根目录或全局)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构原则
|
||||||
|
|
||||||
|
### 2.1 三条硬规则
|
||||||
|
|
||||||
|
1. **单一职责到模块级**
|
||||||
|
每个模块只回答一个问题。
|
||||||
|
反例:"一个模块既管数据持久化,又管网络请求,又管 UI 状态" —— 这种模块必须拆。
|
||||||
|
|
||||||
|
2. **依赖方向单向**
|
||||||
|
高层依赖低层,UI 依赖业务,业务依赖数据;**反向依赖一律禁止**,遇到就用接口/事件/回调倒置。
|
||||||
|
依赖关系必须能画成一张**有向无环图(DAG)**,且这张图要在 `ARCHITECTURE.md` 里画出来。
|
||||||
|
|
||||||
|
3. **跨模块只走"接口契约"**
|
||||||
|
模块 A 想用模块 B 的能力,只能通过 B 暴露的接口/协议,不能 reach into B 的内部实现。
|
||||||
|
接口契约必须在 `ARCHITECTURE.md` 中显式声明(哪怕只是函数签名清单)。
|
||||||
|
|
||||||
|
### 2.2 判断架构是否健康的三个问句
|
||||||
|
|
||||||
|
随时可以用这三个问句自检:
|
||||||
|
|
||||||
|
1. **指认问题**:现在出 bug 了,我能否在 30 秒内指出"这是哪个模块的责任"?
|
||||||
|
2. **替换实现**:如果要把模块 X 整体换掉(换数据库、换框架),改动是否能控制在 X 内部 + 一个接口适配层?
|
||||||
|
3. **新增功能归属**:用户要加一个新功能,我能否立刻说出它"属于哪个模块、或需要新增哪个模块"?
|
||||||
|
|
||||||
|
任何一个回答不出来,架构就有问题,**先停下修架构,再写代码**。
|
||||||
|
|
||||||
|
### 2.3 反耦合纪律
|
||||||
|
|
||||||
|
- **不在新功能开发时顺手重构无关模块**(重构走单独 commit + 单独会话)
|
||||||
|
- **不在 UI 层做业务判断**(UI 只接收状态、发送意图)
|
||||||
|
- **不让数据模型自己跑业务逻辑**(模型类保持瘦,业务放 Service / UseCase)
|
||||||
|
- **不要"为了少写一个文件"把两个模块塞进同一个文件**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 项目架构文档生成流程
|
||||||
|
|
||||||
|
这是从零到 `ARCHITECTURE.md` 的标准流程。每一步都有产出物,下一步必须基于上一步的产出物。
|
||||||
|
|
||||||
|
### Step 1 — 需求收敛(产出 `PRD.md`)
|
||||||
|
- 列功能清单,按 **核心 / 增强 / 可选** 三档分类
|
||||||
|
- 每个核心功能写一句 "用户故事"("作为 X,我想 Y,以便 Z")
|
||||||
|
- **冻结 MVP 范围**:圈出 MVP 包含哪些核心功能,其余明确标 "Out of MVP"
|
||||||
|
|
||||||
|
### Step 2 — 领域建模(产出 `ARCHITECTURE.md` §1 领域模型)
|
||||||
|
- 列出所有**名词实体**(用户、订单、会话…)
|
||||||
|
- 标注每个实体的字段、关系、生命周期
|
||||||
|
- 标记哪些是**持久化实体**,哪些是**瞬态状态**
|
||||||
|
|
||||||
|
### Step 3 — 模块划分(产出 `ARCHITECTURE.md` §2 模块清单)
|
||||||
|
对照领域模型与需求功能清单,拆分模块。每个模块写一张卡片:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 模块名:<Name>
|
||||||
|
- 职责:一句话
|
||||||
|
- 不负责:明确写出"不做什么"(防止职责蔓延)
|
||||||
|
- 输入:从哪些模块/外部来
|
||||||
|
- 输出:暴露给谁
|
||||||
|
- 关键类型/接口:列签名(不写实现)
|
||||||
|
- 持有状态:有没有自己的状态?
|
||||||
|
```
|
||||||
|
|
||||||
|
**经验**:如果一个模块的"不负责"写不出来,说明它的职责还没想清楚,回到 Step 2。
|
||||||
|
|
||||||
|
### Step 4 — 依赖图(产出 `ARCHITECTURE.md` §3 依赖图)
|
||||||
|
- 画出模块间依赖箭头(推荐 Mermaid,纯文本可 diff)
|
||||||
|
- **手动验证 DAG**:从任一节点 DFS 不应回到自己
|
||||||
|
- 标出"接口边界":哪些依赖是接口注入,哪些是直接调用
|
||||||
|
|
||||||
|
### Step 5 — 路线图(产出 `ROADMAP.md`)
|
||||||
|
按依赖图的拓扑顺序排开发节奏:
|
||||||
|
- 先做被依赖最多的底层模块
|
||||||
|
- 每个 milestone 必须能独立通过编译/构建并跑出某种"可见行为"
|
||||||
|
- 写明每个 milestone 完成的"验收标准"(不是"做完了",是"做完了之后能演示什么")
|
||||||
|
|
||||||
|
### Step 6 — 开发规范(产出 `CONVENTIONS.md`)
|
||||||
|
- 命名(类型 / 文件 / 目录 / 资产)
|
||||||
|
- 代码风格(缩进 / 注释策略 / 错误处理边界)
|
||||||
|
- 提交规范
|
||||||
|
- 测试策略(哪些必须有测试,哪些不要求)
|
||||||
|
|
||||||
|
### Step 7 — 派生文档(产出实现 spec / 资产清单等)
|
||||||
|
**只有上面 6 步都齐了,才能开始写派生文档**。
|
||||||
|
派生文档必须显式引用上游章节("对应 `ARCHITECTURE.md` §2.3 的 XxxService 模块"),方便后续 diff 校验。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 文档变更协议
|
||||||
|
|
||||||
|
文档不是写完就锁死的,但变更必须**沿着上下游传播**:
|
||||||
|
|
||||||
|
| 变更点 | 必须同步更新 |
|
||||||
|
|---|---|
|
||||||
|
| 需求加/删/改功能 | 架构模块清单、路线图、派生文档 |
|
||||||
|
| 架构调整模块边界 | 依赖图、开发规范(如有命名调整)、派生文档 |
|
||||||
|
| UI/UX 改视觉 token | 派生文档(实现 spec / 资产清单),不影响架构 |
|
||||||
|
| 加新模块 | 架构 §2 + §3、路线图、开发规范(如新目录) |
|
||||||
|
|
||||||
|
**权威链**:`PRD.md > ARCHITECTURE.md > UIUX.md > CONVENTIONS.md > 派生文档`。
|
||||||
|
派生文档永远不能**反向**修改上游。如果在派生文档中发现矛盾,回去改上游,再让派生文档重新对齐。
|
||||||
|
**代码与架构漂移 > 24 小时即违规。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 实施日志(execution log)
|
||||||
|
|
||||||
|
写代码 / 执行路线图期间,**每完成一个可独立验收的"部分",必须在 `.project.agents/log` 写一份日志**。日志是给"下一个会话的我"看的 —— 让接手的会话能在不读所有代码的情况下知道:刚才完成了什么、哪些提交、下一步是什么。
|
||||||
|
|
||||||
|
### 5.1 何时写日志
|
||||||
|
- 完成一个路线图 milestone
|
||||||
|
- 完成一个 milestone 内独立可验收的子任务
|
||||||
|
- 完成一次非平凡的重构 / bug 修复
|
||||||
|
- 当前会话即将结束时(兜底,把上下文沉淀下来)
|
||||||
|
|
||||||
|
**判断标准**:如果这段工作下一个会话需要知道,就写。
|
||||||
|
|
||||||
|
### 5.2 日志文件命名
|
||||||
|
`.project.agents/log/YYYY-MM-DD-<slug>.md`
|
||||||
|
- `<slug>` = 简短动词 + 范围,kebab-case
|
||||||
|
- 同日多份日志按写作顺序累积(不覆盖、不合并),文件名加序号后缀:`-1`、`-2`…
|
||||||
|
|
||||||
|
### 5.3 日志内容模板
|
||||||
|
```markdown
|
||||||
|
# <一句话标题:完成了什么>
|
||||||
|
|
||||||
|
- **日期**:YYYY-MM-DD
|
||||||
|
- **关联**:路线图 M<N> / 子任务名 / 关联文档
|
||||||
|
- **会话上下文**:(可选)本会话从哪开始接手的
|
||||||
|
|
||||||
|
## 做了什么
|
||||||
|
- bullet 1
|
||||||
|
- bullet 2
|
||||||
|
|
||||||
|
## 提交
|
||||||
|
- `<short hash>` <commit subject>
|
||||||
|
|
||||||
|
(若本段未提交:明确写"未提交,工作树状态:…"并说明原因)
|
||||||
|
|
||||||
|
## 状态变更
|
||||||
|
- 架构文档:是否动过?动了哪一节?
|
||||||
|
- 需求/UI 文档:是否触发回归?
|
||||||
|
- 测试:跑了什么,结果
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
- 接下来要做的第一件事(具体到任务名)
|
||||||
|
- 已知阻塞 / 待澄清项
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 与提交的关系
|
||||||
|
日志**不替代** commit message,commit message 仍按 `CONVENTIONS.md` 写;日志是提交的**索引与解释**。
|
||||||
|
|
||||||
|
### 5.5 不写日志的情况
|
||||||
|
- 还没完成一个"可独立验收"的部分 —— 不要"写日志凑数"
|
||||||
|
- 纯文档调整 —— commit 自身已足够说明
|
||||||
|
- 实验性探索(最终被回滚的内容)—— 用 commit message 记录即可
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 出问题时的排查流程
|
||||||
|
|
||||||
|
bug 来了不要直接钻进代码。按下列顺序:
|
||||||
|
|
||||||
|
1. **复现并定位现象**:能在哪个入口 / 哪个调用稳定复现?
|
||||||
|
2. **回到架构图**:这个现象涉及哪些模块?沿着依赖图圈出可能责任方。
|
||||||
|
3. **审查接口契约**:跨模块调用是否符合 `ARCHITECTURE.md` 中声明的接口?输入输出是否符合契约?
|
||||||
|
4. **缩小到单模块**:把嫌疑缩小到一个模块后,再读该模块代码。
|
||||||
|
5. **修复并回写经验**:如果发现是架构层面的漏洞,修完代码后**必须更新 `ARCHITECTURE.md` 或 `CONVENTIONS.md`**。
|
||||||
|
|
||||||
|
**反模式**:bug 一来就 grep 关键字、改一行试一下、再改一行试一下 —— 这是死亡螺旋,必须用流程压下去。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 反模式清单("看到这些立刻警觉")
|
||||||
|
|
||||||
|
- 出现语义为空的模块名(`Manager` / `Helper` / `Util` / `Common` / `Misc` / `Tools`)且行数 > 100 行
|
||||||
|
- 一个文件 import 超过 10 个本地模块
|
||||||
|
- 改一个 UI 改动需要动 3 个以上其他模块
|
||||||
|
- 同一份业务逻辑在两个地方各写了一遍
|
||||||
|
- PR 描述写"顺便修了 X"
|
||||||
|
- 新增功能时发现"无处归属",于是塞进入口文件或主组件
|
||||||
|
- 文档与代码不一致超过 24 小时
|
||||||
|
- 出现没有在 `ARCHITECTURE.md` 中登记的新模块/新依赖方向
|
||||||
|
|
||||||
|
任何一条出现,**先停下来回到本指南或架构文档,不要继续往前推**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 本项目当前阶段
|
||||||
|
|
||||||
|
> 本节随项目演进,不是规则的一部分。由 Agent 在每个 milestone 后更新。
|
||||||
|
|
||||||
|
<!-- 由 /ads:init(2026-08-19,Retrofit)填入:本项目为前端底座模板,治理已入位。 -->
|
||||||
|
- 当前状态:前端底座(`abacus-static-framework`)已完成治理落地(Retrofit),`src/framework/**` 只读、业务只写 `src/subsystem/**`;前后端共享约束单源化于工作区 `docs/`(`../../../docs/coding-standards.md`)。
|
||||||
|
- 下一步合规动作:新业务子系统按 `docs/coding-standards.md §7`(AI 生成工作流)开发;任何新增模块、目录或依赖方向先回写本仓库 `ARCHITECTURE.md`,再写代码。
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# abacus-static-framework — 架构文档(ARCHITECTURE)
|
||||||
|
|
||||||
|
> 模块、依赖、契约、目录布局的**唯一权威**(本仓库视角)。新增/重命名/移动模块或调整依赖方向,必须先改本文,再改代码(同一个 commit)。
|
||||||
|
> 上游:`PRD.md`(行为)。前后端共享约束见工作区 `docs/`(`../../../../docs/coding-standards.md`、`../../../../docs/architecture.md`),本文不复制其正文。
|
||||||
|
|
||||||
|
## 0. 阅读指引
|
||||||
|
|
||||||
|
本文描述前端底座的模块划分与依赖方向;§2 模块清单与 §6 目录一一对应,§3.2 是跨模块接口契约(写代码必须遵守)。新会话先读 §2 + §6 定位任务归属,再动手。
|
||||||
|
|
||||||
|
## 1. 领域模型(Domain Model)
|
||||||
|
|
||||||
|
### 1.1 实体
|
||||||
|
|
||||||
|
前端无持久化实体;领域概念即运行时状态:
|
||||||
|
|
||||||
|
| 实体 | 字段 | 关系 | 持久化? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 用户会话(userStore) | access_token/refresh_token/expires_dt/user_type | 全局单例 | 否(localStorage) |
|
||||||
|
| 菜单/路由(permissionStore) | routes(过滤后)、menuTree | 依赖后端菜单接口 | 否(内存) |
|
||||||
|
| 应用设置(settingsStore) | backendServices、loginType、loginAddress | 来自 window.frameBaseConfig | 否(运行时注入) |
|
||||||
|
|
||||||
|
### 1.2 派生概念(非持久化)
|
||||||
|
|
||||||
|
- `backendServices` 别名→真实服务名映射(`window.frameBaseConfig` 注入)。
|
||||||
|
- 当前路由上下文(`currentRouter()`)用于组装 commonParam(menuName/moduleId/moduleName)。
|
||||||
|
|
||||||
|
### 1.3 不变量(任何模块都必须维护)
|
||||||
|
|
||||||
|
- I1. `src/framework/**` 只读:业务代码不得 import 后修改框架文件;框架变更只能由框架负责人进行。
|
||||||
|
- I2. 业务请求一律经 `framework/utils/request.ts`,禁止裸 `axios`/`fetch`。
|
||||||
|
- I3. 服务别名首段必须是 `settingsStore.backendServices` 中已注册的键。
|
||||||
|
|
||||||
|
## 2. 模块清单
|
||||||
|
|
||||||
|
按依赖层级从低到高排列。
|
||||||
|
|
||||||
|
### Layer 0 — framework/utils(基础设施)
|
||||||
|
```
|
||||||
|
## 模块名:framework/utils
|
||||||
|
- 职责:请求管线(request.ts)、标准工具(standard/*:vxe、dataHandleUtil、globalHandle、security、baiduMap)
|
||||||
|
- 不负责:业务逻辑、页面状态
|
||||||
|
- 输入:各业务模块的请求配置
|
||||||
|
- 输出:统一响应体解包后的数据 / Blob;标准工具函数
|
||||||
|
- 关键类型/接口:request(config) -> Promise<data>;gridDefaultProps(opts);useQueryParamsHandle(...)
|
||||||
|
- 持有状态:token 刷新队列(模块内闭包)、settingsStore 引用
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 0 — framework/components/standard(视觉基座)
|
||||||
|
```
|
||||||
|
## 模块名:framework/components/standard
|
||||||
|
- 职责:标准 UI 组件(AbForm/AbQueryForm/AbSection/ClickCopy/FilePreviewDialog/ImportExcel/LogShow/RealTimeSearchSelect/SensitiveText/Text/BottomFloat)
|
||||||
|
- 不负责:业务特有布局(业务自建 components)
|
||||||
|
- 输入:props/插槽
|
||||||
|
- 输出:统一风格组件
|
||||||
|
- 持有状态:无(受控组件)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 1 — framework/store(有状态服务)
|
||||||
|
```
|
||||||
|
## 模块名:framework/store
|
||||||
|
- 职责:全局 Pinia 状态(settings:backendServices/loginType;permission:菜单生成与过滤;user:token/业务域)
|
||||||
|
- 不负责:页面级状态(业务自建 store)
|
||||||
|
- 输入:window.frameBaseConfig、后端菜单接口
|
||||||
|
- 输出:状态与 actions
|
||||||
|
- 持有状态:是
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 2 — framework/router + hooks + directives(路由与通用逻辑)
|
||||||
|
```
|
||||||
|
## 模块名:framework/router + hooks + directives
|
||||||
|
- 职责:静态路由与动态路由装配支持;useVxeTableHandle 等通用组合式函数;v-debounce/v-throttle 指令
|
||||||
|
- 不负责:业务菜单路由(subsystem/router/dynamicRouter.ts)
|
||||||
|
- 输入:permissionStore 过滤后的路由
|
||||||
|
- 输出:可用路由表、表格查询联动能力
|
||||||
|
- 持有状态:路由表(由 permissionStore 维护)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 3 — subsystem(业务可写区)
|
||||||
|
```
|
||||||
|
## 模块名:subsystem
|
||||||
|
- 职责:业务子系统(api 封装、dynamicRouter 菜单路由、views 页面、extend 扩展、自建 components/hooks/store/styles/types/utils)
|
||||||
|
- 不负责:框架能力(一律复用 framework)
|
||||||
|
- 输入:后端接口、用户操作
|
||||||
|
- 输出:业务页面与接口调用
|
||||||
|
- 持有状态:业务自身状态(自建 store)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 依赖图(DAG)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subsystem --> framework_utils[framework/utils]
|
||||||
|
subsystem --> framework_components[framework/components/standard]
|
||||||
|
subsystem --> framework_hooks[framework/hooks]
|
||||||
|
subsystem --> framework_router[framework/router]
|
||||||
|
framework_router --> framework_store[framework/store]
|
||||||
|
framework_hooks --> framework_utils
|
||||||
|
framework_hooks --> framework_components
|
||||||
|
framework_store --> framework_utils
|
||||||
|
framework_utils --> framework_store
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 依赖方向规则
|
||||||
|
|
||||||
|
- 高层依赖低层;业务(subsystem)依赖框架(framework),框架各层按上图单向;**反向依赖一律禁止**(如 framework 不得 import subsystem)。
|
||||||
|
- 例外:`framework/utils/request.ts` 依赖 `framework/store`(settingsStore)获取别名映射——两者同属 framework 层,允许。
|
||||||
|
|
||||||
|
### 3.2 跨模块接口契约(写代码时必须遵守的签名)
|
||||||
|
|
||||||
|
```
|
||||||
|
// module: framework/utils/request
|
||||||
|
// request(config: {url, method, params|data, headers?, custom?}) -> Promise<data> // status===200 已解包 data;ArrayBuffer 返回整个 response
|
||||||
|
// url 首段 = 服务别名(I3),自动携带 token/commonParam(I2)
|
||||||
|
|
||||||
|
// module: framework/utils/standard/vxe
|
||||||
|
// gridDefaultProps(opts: {params, rowConfig, pagerConfig, proxyConfig, columns}) -> VxeGridProps
|
||||||
|
|
||||||
|
// module: framework/hooks/useVxeTableHandle
|
||||||
|
// useQueryParamsHandle(params, page, sorts, filters) -> query params // 分页契约 {data,total},见共享规范 §6.4
|
||||||
|
// useTableQueryReload(...) / useInVxeGridSearch(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 持久化与边界
|
||||||
|
|
||||||
|
- 无后端式持久化;前端持久化仅 token(localStorage,由 userStore 管理)。
|
||||||
|
- 外部边界:所有 HTTP 请求必须经 request.ts;开发期经 vite proxy;运行时经 Nginx 反代;`window.frameBaseConfig` 由宿主页面注入(含 backendServices)。
|
||||||
|
|
||||||
|
## 5. 测试边界
|
||||||
|
|
||||||
|
- 无自动化单测(PRD 已声明);每页面人工冒烟(列表/查询/新增/编辑/导出)。
|
||||||
|
- 真机/真环境必验:多数据源路由、Kafka 消费、XXL-JOB 调度、Nacos 配置拉取、真实数据库迁移(属后端,但前端联调需一并验证)。
|
||||||
|
|
||||||
|
## 6. 目录结构(与 §2 模块清单一一对应)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── framework/ # Layer 0~2(只读共享)
|
||||||
|
│ ├── api/ components/ hooks/ layout/ router/ store/ styles/ types/ utils/ views/ directives/
|
||||||
|
│ └── utils/request.ts # 请求管线(契约见 §3.2)
|
||||||
|
├── subsystem/ # Layer 3(业务可写区)
|
||||||
|
│ ├── api/<服务别名>/ # 按 Controller 分 ts(如 sms/{index,applog,abnormal,config}.ts)
|
||||||
|
│ ├── router/dynamicRouter.ts # 菜单路由唯一注册点
|
||||||
|
│ ├── views/<一级菜单>/<二级菜单>/index.vue
|
||||||
|
│ ├── extend/ # 框架扩展钩子
|
||||||
|
│ └── (自建) components/ hooks/ store/ styles/ types/ utils/
|
||||||
|
├── permission.ts # 全局路由守卫(生成/过滤动态路由)
|
||||||
|
└── main.ts # 入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 自检(架构健康度三问)
|
||||||
|
|
||||||
|
1. 出 bug 了,能否 30 秒内指出是 framework 还是 subsystem、哪个子目录的责任?
|
||||||
|
2. 替换 UI 库/表格库,改动能否控制在 framework/components 与 utils/standard 内 + 适配层?
|
||||||
|
3. 加新业务页面,能否立刻说出落在 `subsystem/views/<一级>/<二级>/` 哪个位置?
|
||||||
|
|
||||||
|
## 8. 待办与已知技术负债
|
||||||
|
|
||||||
|
- 前端 README 引用的 `docs/` 文档中心已补齐(`../../../../docs/`)。
|
||||||
|
- 旧框架兼容入口 `externalOldERPIndex`(data.js)仅在需要兼容历史模块时配置。
|
||||||
|
|
||||||
|
## 9. 文档变更协议
|
||||||
|
|
||||||
|
- 加/删模块或改依赖方向 → 改本文 §2 + §3,同一 commit。
|
||||||
|
- 改对外签名(request/gridDefaultProps 等)→ 先改 §3.2,再改代码。
|
||||||
|
- 与 `PRD.md` 冲突 → 以 `PRD.md` 为准,回改本文;与共享 `docs/coding-standards.md` 冲突 → 以 docs/ 为准并回改本文。
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# abacus-static-framework — 开发规范(CONVENTIONS)
|
||||||
|
|
||||||
|
> 命名、目录、提交、风格、测试的权威(本仓库视角)。前后端共享约束见工作区 `docs/coding-standards.md`(`../../../../docs/coding-standards.md`),本文只写前端特有内容 + 指针,**禁止复制 docs/ 正文**。
|
||||||
|
|
||||||
|
## 0. 阅读指引
|
||||||
|
|
||||||
|
- 适用范围:`src/subsystem/**` 业务开发(`src/framework/**` 只读)。
|
||||||
|
- 分工:ARCHITECTURE 管"拆成什么模块";本文管"怎么命名/写";共享编码规范、接口契约、AI 生成工作流在 `docs/coding-standards.md`。
|
||||||
|
|
||||||
|
## 1. 命名
|
||||||
|
|
||||||
|
### 1.1 标识符
|
||||||
|
|
||||||
|
- 组件名:PascalCase,且**必须与路由 `name` 一致**(`<script lang="ts">export default { name: 'Xxx' }</script>`)。
|
||||||
|
- 文件/目录:kebab-case(如 `operationLog/index.vue`);views 目录按一级/二级菜单名组织。
|
||||||
|
- API 函数:`getXxx` / `saveXxx` / `deleteXxx` / `exportXxx`。
|
||||||
|
- Pinia store:`use<Xxx>Store`。
|
||||||
|
|
||||||
|
### 1.2 模块命名(语义禁区)
|
||||||
|
|
||||||
|
- 禁止 `Manager` / `Helper` / `Util` / `Common` / `Misc` / `Tools` 这类语义为空的名字;用"动词+名词"(如 `useTableQueryReload`、`gridDefaultProps`)。
|
||||||
|
|
||||||
|
### 1.3 文件 / 1.4 目录 / 1.5 资产命名
|
||||||
|
|
||||||
|
- 文件按主类型命名:页面 `index.vue`(views 目录内)、API 按 Controller 分 `api/<服务别名>/<模块>.ts`、组合式函数 `use<Xxx>.ts`。
|
||||||
|
- 目录:`api/<服务别名>/`、`views/<一级菜单>/<二级菜单>/`、`router/`、`extend/`;业务自建 `components/ hooks/ store/ styles/ types/ utils/`。
|
||||||
|
- 资产:图标/图片放 `src/assets/` 或 `public/`(按是否需编译/保持原名选择)。
|
||||||
|
|
||||||
|
### 1.6 字符串与本地化
|
||||||
|
|
||||||
|
- 用户可见文案用中文;菜单标题在路由 `meta.title` 与后端菜单保持一致。
|
||||||
|
|
||||||
|
## 2. 代码风格
|
||||||
|
|
||||||
|
### 2.1 排版
|
||||||
|
|
||||||
|
- TypeScript / Vue:2 空格缩进;单行 ≤ 120 字符;`<script setup>` + 选项式 `name`。
|
||||||
|
- 已配置 `unplugin-auto-import`(vue/vue-router/@vueuse/core + Element Plus resolver):**禁止**手动 import 这些自动导入项。
|
||||||
|
|
||||||
|
### 2.2 分层纪律
|
||||||
|
|
||||||
|
- 页面组件只消费状态/发请求(经 `api/` 封装),不直接拼 URL、不裸 axios(见共享规范 §4.3/§6)。
|
||||||
|
- 业务通用逻辑抽 `hooks/`;跨页面共享状态抽 `store/`;数据模型类型保持薄。
|
||||||
|
|
||||||
|
### 2.3 并发 / 2.4 错误处理
|
||||||
|
|
||||||
|
- 并发:请求层已处理 token 刷新并发队列;页面不自行管理并发刷新。
|
||||||
|
- 错误:由 request.ts 统一弹窗(9xx 业务 / 5xx 系统 / 401/429/503);页面只需处理成功分支与局部提示。
|
||||||
|
|
||||||
|
### 2.5 注释
|
||||||
|
|
||||||
|
- 默认不写注释;只在 WHY 不明显时写一行;不写解释 WHAT 的注释。
|
||||||
|
|
||||||
|
## 3. 提交规范
|
||||||
|
|
||||||
|
### 3.1 Commit message 风格
|
||||||
|
|
||||||
|
Conventional Commits:`type(scope): subject`。允许 type:`feat` / `fix` / `docs` / `refactor` / `test` / `chore`。例:`feat(sms): 新增操作日志查询页面`。
|
||||||
|
|
||||||
|
### 3.2 提交粒度 / 3.3 工作区纪律 / 3.4 分支模型
|
||||||
|
|
||||||
|
- 一次提交一个目的;不在脏工作区叠加不相关改动。
|
||||||
|
- 分支模型:主干 + 短命功能分支(按需,单人开发可直推主干)。
|
||||||
|
- 本仓库属于工作区单一 git 仓库(`D:\workBuddySpace\member`);纯前端改动 commit 限定本目录,跨项目改动(docs/)单独 commit。
|
||||||
|
|
||||||
|
### 3.5 实施日志
|
||||||
|
|
||||||
|
- 规则见 `VIBECODING_GUIDE §5`:每个可独立验收的部分写 `.project.agents/log/YYYY-MM-DD-<slug>.md`。
|
||||||
|
|
||||||
|
## 4. 测试策略
|
||||||
|
|
||||||
|
### 4.1 框架 / 4.2 必须有测试的模块
|
||||||
|
|
||||||
|
- 无自动化测试框架(PRD 决策);人工冒烟覆盖:列表查询、表单增改、导出、权限路由。
|
||||||
|
|
||||||
|
### 4.3 必须真机/真环境验证
|
||||||
|
|
||||||
|
- 涉及后端联动的场景:多数据源路由、Kafka 消费、XXL-JOB 调度、Nacos 配置拉取、真实数据库迁移——mock/模拟器不算数。
|
||||||
|
|
||||||
|
## 5. 与上游文档的同步矩阵
|
||||||
|
|
||||||
|
| 变更点 | 必须同步更新 |
|
||||||
|
|---|---|
|
||||||
|
| 新增/改动页面功能 | `PRD.md`(功能定义);共享规范如有涉及先改 `docs/coding-standards.md` |
|
||||||
|
| 新增模块/目录/依赖方向 | 本仓库 `ARCHITECTURE.md` §2+§3 |
|
||||||
|
| 改 request/gridDefaultProps 等对外签名 | `ARCHITECTURE.md` §3.2 + `docs/coding-standards.md` §6 |
|
||||||
|
| UI/视觉 token | 本仓库 `UIUX.md` |
|
||||||
|
|
||||||
|
## 6. 不在本文范围(明确划清)
|
||||||
|
|
||||||
|
- 模块拆分归 `ARCHITECTURE.md`;行为/范围归 `PRD.md`;前后端共享约束(接口契约/异常码/分页/APP_NAME)归 `docs/coding-standards.md`;视觉细节归 `UIUX.md`。
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# abacus-static-framework — 产品需求文档(PRD)
|
||||||
|
|
||||||
|
> 行为与范围的**唯一权威**。功能的加/删/改必须先改本文,再动代码与下游文档。
|
||||||
|
> 本文回答"做什么 / 给谁 / 为什么",不回答"怎么实现"(那是 `ARCHITECTURE.md`)。
|
||||||
|
|
||||||
|
## 1. 产品愿景
|
||||||
|
|
||||||
|
为 abacus 微服务体系提供**可复用的企业级 Vue 3 前端底座**:业务团队(人 + AI)只需在 `src/subsystem/**` 编写业务页面与接口封装,即可获得统一的菜单框架、权限路由、请求管线(统一响应体/服务别名/token 刷新)与标准 UI 组件,实现跨系统一致的前端体验与低成本复制开发。
|
||||||
|
|
||||||
|
## 2. 目标用户与典型场景
|
||||||
|
|
||||||
|
- 用户画像:公司内部业务系统开发团队(前端开发、全栈、AI 辅助开发人员)。
|
||||||
|
- 场景 1:作为业务开发,我想在底座上复制 `subsystem` 示例新建一个业务子系统,以便快速产出与既有系统风格一致的页面。
|
||||||
|
- 场景 2:作为 AI 助手,我想依据 `docs/coding-standards.md` 把用户给出的界面直接生成前后端代码,以便无需逐条追问技术细节。
|
||||||
|
- 场景 3:作为框架负责人,我想保证所有子系统的 UI 组件与交互模式统一,以便降低维护成本与学习成本。
|
||||||
|
|
||||||
|
## 3. 范围(Scope)
|
||||||
|
|
||||||
|
### 3.1 MVP 必含(In Scope)
|
||||||
|
- 菜单框架与动态路由/权限(后端菜单 → `filterAsyncRoutes` 匹配 → `addRoute`)。
|
||||||
|
- 请求管线:统一响应体解包、URL 服务别名映射、token 自动刷新(并发队列)、commonParam 审计头、Excel ArrayBuffer 分支。
|
||||||
|
- 标准组件与工具(`framework/components/standard` 10 项、`framework/utils/standard` 5 项)。
|
||||||
|
- `src/subsystem` 业务开发模板(sms 示例:api / dynamicRouter / views / extend)。
|
||||||
|
- 治理体系:`.project.agents/`(CLAUDE/AGENTS/SELF_CONSTRAINTS/VIBECODING_GUIDE + context 文档)。
|
||||||
|
|
||||||
|
### 3.2 明确不做(Out of Scope)
|
||||||
|
- 不包含具体业务功能(业务只存在于 `src/subsystem/**` 的复制改写)。
|
||||||
|
- 不提供单元测试框架与 CI 流水线(人工冒烟验证)。
|
||||||
|
- 不维护后端逻辑(后端属 `abacus.springboot.example` 仓库)。
|
||||||
|
|
||||||
|
## 4. 核心功能定义
|
||||||
|
|
||||||
|
### 4.1 菜单框架与权限路由(【核心】)
|
||||||
|
- **用户故事**:作为业务用户,我想登录后只看到有权限的菜单并直达页面,以便安全高效地工作。
|
||||||
|
- **行为**:`permissionStore.generateRoutes` 拉取后端菜单,按 `url === path` 与 `subsystem/router/dynamicRouter.ts` 匹配,过滤后 `router.addRoute`。
|
||||||
|
- **边界 / 异常**:菜单数据未注册 → 页面不可见;`public/data/data.js` 的 `currentSubsystem` 决定当前子系统。
|
||||||
|
|
||||||
|
### 4.2 请求管线(【核心】)
|
||||||
|
- **用户故事**:作为业务开发者,我想只写 URL 与参数就能完成带鉴权、带审计的接口调用,以便不重复处理公共逻辑。
|
||||||
|
- **行为**:`framework/utils/request.ts` 强制:服务别名映射(`settingsStore.backendServices`)、`status===200` 自动解包 `data`、token 剩余 <10 分钟自动刷新并重放并发请求、自动携带 `commonParam`。
|
||||||
|
- **边界 / 异常**:业务异常码首位 9、系统异常码首位 5 统一弹窗提示;ArrayBuffer(Excel)不解包返回整个 response;HTTP 401 跳登录。
|
||||||
|
|
||||||
|
### 4.3 标准组件与工具(【核心】)
|
||||||
|
- **用户故事**:作为业务开发,我想优先使用框架标准件,以便页面风格统一且少写重复代码。
|
||||||
|
- **行为**:UI 优先级 standard 组件 > vxe-table(仅表格)> Element Plus > 自研(泛用性高需提交框架负责人);工具同理。
|
||||||
|
- **边界 / 异常**:自研组件不得长期只存在于单个业务内。
|
||||||
|
|
||||||
|
## 5. 信息架构与导航
|
||||||
|
|
||||||
|
顶层为菜单框架(一级菜单 → 二级菜单 → `views/<一级>/<二级>/index.vue`);`dynamicRouter.ts` 是业务路由唯一注册点;子系统编码由 `public/data/data.js` 声明;开发期经 `vite.config.ts server.proxy` 反代后端。
|
||||||
|
|
||||||
|
## 6. 数据模型(概念级)
|
||||||
|
|
||||||
|
前端无持久化业务实体;运行时状态:用户登录信息(token/user)、菜单与路由(permissionStore)、服务别名映射(settingsStore.backendServices)、业务域(getCurrentBusinessDomain)。
|
||||||
|
|
||||||
|
## 7. 非功能性需求
|
||||||
|
|
||||||
|
- 构建产物直接输出到后端微服务 `resources/html/vue/<应用名>/`,随 `*-bin.zip` 一起发布(动静分离)。
|
||||||
|
- 请求超时 50s;上传限制由后端 multipart 控制(50MB)。
|
||||||
|
- 框架升级不覆盖 `public`、`vite.config.ts`、`src/subsystem`。
|
||||||
|
|
||||||
|
## 8. 国际化与文案语气
|
||||||
|
|
||||||
|
中文为主;用户可见文案使用统一中文,保持简洁、操作导向;禁止硬编码无意义字符串。
|
||||||
|
|
||||||
|
## 9. 隐私与合规
|
||||||
|
|
||||||
|
前端不收集个人数据;仅透传后端鉴权与业务域信息(commonParam);无上报逻辑。
|
||||||
|
|
||||||
|
## 10. 风险与缓解
|
||||||
|
|
||||||
|
- **APP_NAME 四处漏配静默失败**:按共享规范 §3 四联检(`bootstrap.yml`/扫描包/URL 首段/outDir)。
|
||||||
|
- **框架升级覆盖业务**:升级前备份 `public`、`vite.config.ts`、`src/subsystem`。
|
||||||
|
- **自研组件碎片化**:泛用组件必须走框架评审入库。
|
||||||
|
|
||||||
|
## 11. 发布里程碑(建议,待路线图细化)
|
||||||
|
|
||||||
|
- M0:治理与规范落地(本次已完成)。
|
||||||
|
- M1+:按 `ROADMAP.md` 以第一个业务子系统复制改写验证模板可复制性。
|
||||||
|
|
||||||
|
## 12. 假设与待澄清项(复核时请逐条回应)
|
||||||
|
|
||||||
|
- Q1. 是否所有新业务子系统都采用"复制 sms 示例 + 改写"的方式起步?(默认:是)
|
||||||
|
- Q2. 是否引入前端单元测试(如 Vitest)?(默认:否,人工冒烟)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# abacus-static-framework — 实施路线图(ROADMAP)
|
||||||
|
|
||||||
|
> 由 `PRD.md` + `ARCHITECTURE.md` 推导。按依赖图拓扑顺序排开发节奏。每完成一个复选框,在**同一个 commit**里打勾;每完成一个 milestone,按 `.project.agents/log/` 模板写日志。
|
||||||
|
|
||||||
|
## 0. 排序原则
|
||||||
|
|
||||||
|
- 先底层(framework 只读,不轻易动)后业务(subsystem)。
|
||||||
|
- 本仓库为**底座模板**:里程碑以"验证模板可复制性 + 治理可持续性"为主线,而非业务功能交付。
|
||||||
|
|
||||||
|
## 1. 里程碑总览
|
||||||
|
|
||||||
|
| Milestone | 目标 | 验收(能演示什么) |
|
||||||
|
|---|---|---|
|
||||||
|
| M0 | 治理落地(本次):git 单一仓库、docs/ 文档中心、.project.agents 全套、共享规范 | 克隆仓库 → `npm install` → `npm run dev` 可起;治理文档无残留 token |
|
||||||
|
| M1 | 模板可复制性验证:按 `docs/coding-standards.md §7` 用第一个业务子系统(界面→代码)跑通全流程 | 新业务子系统页面可访问、接口联调通 |
|
||||||
|
| M2 | 沉淀与回写:业务中发现的共性需求回写 framework/standard 或 docs/ | 标准件/规范有增量,框架评审入库 |
|
||||||
|
|
||||||
|
## 2. 里程碑详细
|
||||||
|
|
||||||
|
### M0 — 治理与规范落地(已完成)
|
||||||
|
- [x] 版本控制 + ignore + 骨架提交(工作区单一仓库 `D:\workBuddySpace\member`)
|
||||||
|
- [x] docs/ 文档中心(README / architecture / coding-standards / agent-guide)
|
||||||
|
- [x] 本仓库 .project.agents 治理文件(CLAUDE / AGENTS / SELF_CONSTRAINTS / VIBECODING_GUIDE / settings / context 五文档)
|
||||||
|
- [x] 完成门禁(治理文档无残留模板 token / FILL 标记)
|
||||||
|
- **验收**:克隆仓库 → `npm install` → `npm run dev` 可起;治理文档无残留 token。
|
||||||
|
|
||||||
|
### M1 — 模板可复制性验证(首个业务子系统)
|
||||||
|
- **依赖**:M0
|
||||||
|
- [ ] 选定/新建业务子系统(复制 `subsystem` sms 示例改写)
|
||||||
|
- [ ] 按 `docs/coding-standards.md §7`:界面 → views + api → dynamicRouter → 后端接口 → SQL → APP_NAME 四联检 → 联调冒烟
|
||||||
|
- [ ] 后端菜单数据注册(`sql/data/`),前端菜单可见
|
||||||
|
- **验收**:新子系统在 dev 环境跑通列表/查询/新增/编辑/导出全链路。
|
||||||
|
|
||||||
|
### M2 — 沉淀与回写
|
||||||
|
- **依赖**:M1
|
||||||
|
- [ ] 收集业务开发中的共性需求(组件/工具/规范盲点)
|
||||||
|
- [ ] 泛用组件走框架评审纳入 `framework/components/standard`(或 standard utils)
|
||||||
|
- [ ] 规范盲点回写 `docs/coding-standards.md`(先改 docs/ 再同步各仓库指针)
|
||||||
|
- **验收**:标准件与规范有可见增量,且新业务直接复用。
|
||||||
|
|
||||||
|
## 3. 并行轨道(与代码 milestone 解耦)
|
||||||
|
|
||||||
|
- 无(CI/资产生成等暂不纳入;如团队需要可补充构建门禁脚本到 `.project.agents/scripts/`)。
|
||||||
|
|
||||||
|
## 4. 完成定义(DoD)
|
||||||
|
|
||||||
|
- 构建通过(`npm run build:prod` 无错);涉及后端联动场景需后端启动联调。
|
||||||
|
- 触及的架构变更已回写 `ARCHITECTURE.md`;共享约束变更先改 `docs/coding-standards.md`。
|
||||||
|
- 已写执行日志(`.project.agents/log/YYYY-MM-DD-<slug>.md`)。
|
||||||
|
|
||||||
|
## 5. 跨 milestone 不变量
|
||||||
|
|
||||||
|
- I1~I3(ARCHITECTURE §1.3):framework 只读、请求走 request.ts、别名已注册。
|
||||||
|
- 任何新模块/目录先登记 `ARCHITECTURE.md` 再写代码。
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# abacus-static-framework — UI/UX 设计指南
|
||||||
|
|
||||||
|
> 视觉与交互细节的权威(与 `PRD.md` 并列上游)。视觉/交互改动先改本文,再让派生实现对齐。
|
||||||
|
> 本文面向业务子系统开发:**页面模式与标准组件是唯一推荐路径**(共享规范 `docs/coding-standards.md §4.4~4.7` 为强制约束,此处给设计视角的说明)。
|
||||||
|
|
||||||
|
## 1. 设计原则(优先级递减)
|
||||||
|
|
||||||
|
1. **复用优先**:先标准组件(`framework/components/standard`),再 vxe-table(仅表格),再 Element Plus,最后才自研。
|
||||||
|
2. **模式一致**:查询+列表页用同一套骨架(AbQueryForm + vxe-grid),让所有子系统页面观感统一。
|
||||||
|
3. **配置驱动**:表单/查询表单用 fieldConfigs 配置式,少写模板代码。
|
||||||
|
4. **表格是主角**:业务数据以表格呈现,列字段名与后端响应字段对齐。
|
||||||
|
|
||||||
|
## 2. 视觉身份
|
||||||
|
|
||||||
|
- 无独立品牌视觉;跟随公司菜单框架既有主题;不引入自定义品牌色(见 §3 语义色)。
|
||||||
|
|
||||||
|
## 3. 色板与字体
|
||||||
|
|
||||||
|
- 色板:使用 Element Plus 主题变量与 `framework/styles/variables.scss` 中定义的语义变量(禁止在业务内散落硬编码 hex)。
|
||||||
|
- 字体:沿用框架全局样式;字号层级用 Element Plus 默认(14px 正文 / 标题层级)。
|
||||||
|
|
||||||
|
## 4. 间距与栅格
|
||||||
|
|
||||||
|
- 页面骨架固定:`<Title>` 页头 → `.form-container`(查询区,AbQueryForm)→ `.table-container`(vxe-grid);间距沿用框架示例(sms 示例页面)的既有值。
|
||||||
|
- 弹窗内表单用 AbForm 的布局配置(默认 label 左对齐)。
|
||||||
|
|
||||||
|
## 5. 动效与触觉
|
||||||
|
|
||||||
|
- 无自定义动效需求;使用 Element Plus 默认过渡;表格行编辑为双击进入(`editConfig trigger:'dblclick', mode:'row'`)。
|
||||||
|
|
||||||
|
## 6. 状态系统
|
||||||
|
|
||||||
|
- 空态:vxe-grid 空数据默认展示;查询无结果提示由表格空态承担。
|
||||||
|
- 加载:`loading` 状态由 vxe-grid/按钮统一管理。
|
||||||
|
- 错误:接口错误由 request.ts 统一弹窗(业务 9xx / 系统 5xx),页面不重复提示。
|
||||||
|
- 成功:新增/保存成功用 `ElMessage.success`(框架既有约定)。
|
||||||
|
|
||||||
|
## 7. 屏幕规范
|
||||||
|
|
||||||
|
### 7.1 查询+列表页(标准模式)
|
||||||
|
```
|
||||||
|
Title 页头
|
||||||
|
AbQueryForm(fieldConfigs: input/daterange/select/radio + slot 自定义)
|
||||||
|
vxe-grid(gridDefaultProps + proxyConfig.ajax.query;clientPage 按数据量选)
|
||||||
|
el-dialog(新增/编辑,AbForm) / el-drawer(详情/日志)
|
||||||
|
```
|
||||||
|
### 7.2 可编辑表格页
|
||||||
|
- `editConfig: { trigger: 'dblclick', mode: 'row' }` + `editRender: { name: 'input' }` + `@edit-closed` 保存(参考 `views/config/business/index.vue`)。
|
||||||
|
### 7.3 容器页(List/Detail 切换)
|
||||||
|
- 容器 `index.vue` 用 `:is` 动态切换 `list.vue` / `detail.vue`(参考 `views/document/interfacedoc/`)。
|
||||||
|
|
||||||
|
## 8. 可访问性
|
||||||
|
|
||||||
|
- 表单字段 label 必填;操作按钮带语义化文字;表格 `seq` 序号列保留。
|
||||||
|
|
||||||
|
## 9. 文案语气
|
||||||
|
|
||||||
|
- 按钮:动词短语(新增/编辑/删除/导出/查询/重置)。
|
||||||
|
- 错误提示:简短、可直接行动的句子(由后端 message 提供,前端原样展示)。
|
||||||
|
|
||||||
|
## 10. 性能预算
|
||||||
|
|
||||||
|
- 列表默认服务端分页(`clientPage:false`),避免一次拉全量;大数据量禁客户端分页。
|
||||||
|
- 查询表单默认收起多余条件(按需扩展)。
|
||||||
|
|
||||||
|
## 11. 视觉资产清单
|
||||||
|
|
||||||
|
- 一般业务无需产出视觉资产;如确需图标,放入 `src/assets/` 由构建处理(或 `public/` 保持原名),命名 kebab-case。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 治理体系落地(ADS Retrofit)
|
||||||
|
|
||||||
|
- **日期**:2026-08-19
|
||||||
|
- **关联**:ROADMAP M0
|
||||||
|
- **会话上下文**:工作区两个参考模板(前端 abacus-static-framework、后端 abacus.springboot.example)此前无 git、无 docs/ 文档中心、无 .project.agents 治理。
|
||||||
|
|
||||||
|
## 做了什么
|
||||||
|
|
||||||
|
- 工作区根 `D:\workBuddySpace\member` 建单一 git 仓库 + `.gitignore`(node_modules/target/dist/.workbuddy 等)。
|
||||||
|
- 创建 `docs/` 文档中心(单源化):`README.md`(索引+映射表)、`architecture.md`(动静分离架构)、`coding-standards.md`(★前后端开发约束规范,约 4.5k 字)、`agent-guide.md`(AI 生成代码工作流与提示词协议)。
|
||||||
|
- 本仓库落地 `.project.agents/`:CLAUDE.md / AGENTS.md / SELF_CONSTRAINTS.md / VIBECODING_GUIDE.md / settings.json / log/README.md + `docs/context/{PRD,ARCHITECTURE,ROADMAP,CONVENTIONS,UIUX}.md`,全部填充无残留 token。
|
||||||
|
- 共享内容以指针引用 `../../../../docs/`(如 CONVENTIONS 引用 `coding-standards.md`),未复制正文。
|
||||||
|
- 分析过程使用 CodeGraph MCP 索引后端(82 文件)与前端 subsystem(7 文件),符号级上下文工具受限,降级为关键文件定向读取(request.ts / vite.config / yml / pom / assembly / JcBillController 等)。
|
||||||
|
|
||||||
|
## 提交
|
||||||
|
|
||||||
|
- 见工作区根 git 提交(docs/ 文档中心、前端治理、后端治理三个原子提交)。
|
||||||
|
|
||||||
|
## 状态变更
|
||||||
|
|
||||||
|
- ARCHITECTURE:本仓库 ARCHITECTURE.md 已按实际结构(framework 只读 + subsystem 业务)编写;不变量 I1~I3。
|
||||||
|
- PRD / UIUX:PRD、UIUX 均已填充;UIUX 面向业务子系统页面模式。
|
||||||
|
- 测试:无代码改动,未触发。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- 按 ROADMAP M1 用第一个业务子系统(界面→代码)验证模板可复制性。
|
||||||
|
- 待澄清项见 PRD §12(新业务复制起步方式 / 是否引入前端单测)。
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 实施日志(execution log)
|
||||||
|
|
||||||
|
本目录沉淀"做了什么 + 哪些提交 + 下一步",供下一个会话接手用。
|
||||||
|
规则与时机见 `../VIBECODING_GUIDE.md §5`;硬约束见 `../SELF_CONSTRAINTS.md §B6`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件命名
|
||||||
|
|
||||||
|
`YYYY-MM-DD-<slug>.md`
|
||||||
|
|
||||||
|
- `<slug>` = 简短动词 + 范围,kebab-case
|
||||||
|
- 同日多份按写作顺序加 `-1`、`-2` 后缀
|
||||||
|
|
||||||
|
## 模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <一句话标题:完成了什么>
|
||||||
|
|
||||||
|
- **日期**:YYYY-MM-DD
|
||||||
|
- **关联**:ROADMAP M<N> / 子任务名 / 关联文档
|
||||||
|
- **会话上下文**:(可选)本会话从哪开始接手的
|
||||||
|
|
||||||
|
## 做了什么
|
||||||
|
- bullet 1
|
||||||
|
- bullet 2
|
||||||
|
|
||||||
|
## 提交
|
||||||
|
- `<short hash>` <commit subject>
|
||||||
|
|
||||||
|
(若本段未提交:明确写"未提交,工作树状态:…"并说明原因)
|
||||||
|
|
||||||
|
## 状态变更
|
||||||
|
- ARCHITECTURE:是否动过?动了哪一节?
|
||||||
|
- PRD / UIUX:是否触发回归?
|
||||||
|
- 测试:跑了什么,结果
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
- 接下来要做的第一件事(具体到任务名)
|
||||||
|
- 已知阻塞 / 待澄清项
|
||||||
|
```
|
||||||
|
|
||||||
|
## 何时写
|
||||||
|
|
||||||
|
- 完成一个 ROADMAP milestone
|
||||||
|
- 完成一个独立可验收的子任务
|
||||||
|
- 完成非平凡的重构 / bug 修复
|
||||||
|
- 会话即将结束(兜底)
|
||||||
|
|
||||||
|
## 何时不写
|
||||||
|
|
||||||
|
- 还没完成一个"可独立验收"的部分(别凑数)
|
||||||
|
- 纯文档微调(commit 自身已够说明)
|
||||||
|
- 实验性探索且最终 revert(commit 即可)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [],
|
||||||
|
"deny": []
|
||||||
|
},
|
||||||
|
"hooks": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
> 📚 **团队文档中心**:前后端统一架构、编码标准、接口契约与 agent 协作边界见仓库同级 [`../docs/README.md`](../docs/README.md)(`architecture.md` / `coding-standards.md` / `agent-guide.md`)。业务开发请写在 `src/subsystem/**`,`src/framework/**` 为只读共享框架。
|
||||||
|
|
||||||
|
## 环境
|
||||||
|
|
||||||
|
### 安装node.js
|
||||||
|
安装18.20.7版本
|
||||||
|
|
||||||
|
### 安装npm
|
||||||
|
- 新版的nodejs已经集成了npm,输入 "npm -v" 来测试是否成功安装
|
||||||
|
- 设置npm镜像源为淘宝镜像:npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
### 安装开发工具及插件
|
||||||
|
安装Visual Studio Code开发工具
|
||||||
|
安装Vue Language Features (Volar)插件 : 为.vue单文件组件提供代码高亮以及语法支持
|
||||||
|
安装TypeScript Vue Plugin (Volar)插件 : ts语法支持
|
||||||
|
安装JavaScript (ES6) code snippets插件 : es6的快捷语法
|
||||||
|
|
||||||
|
|
||||||
|
#### 设置npm源为淘宝镜像
|
||||||
|
npm install --registry=https://registry.npmmirror.com
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
### 启动本地服务
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
### 打包测试环境
|
||||||
|
npm run build:stage
|
||||||
|
|
||||||
|
### 打包生产环境
|
||||||
|
npm run build:prod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 框架文件、开发规范说明
|
||||||
|
|
||||||
|
## public
|
||||||
|
|
||||||
|
公共文件夹,不会被编译,以下情况适用
|
||||||
|
1、不会被源码引用(例如 robots.txt)
|
||||||
|
2、必须保持原有文件名(没有经过 hash)
|
||||||
|
3、或者你压根不想引入该资源,只是想得到其 URL。
|
||||||
|
|
||||||
|
## src/assets
|
||||||
|
公共静态文件夹,目录中文件会被编译
|
||||||
|
|
||||||
|
## src/framework
|
||||||
|
|
||||||
|
**api** 菜单框架访问后端服务接口
|
||||||
|
**components** 框架提供公共组件
|
||||||
|
**hooks** 组合式函数,集成了多个基础组件实现的通用函数
|
||||||
|
**layout** 菜单框架布局组件
|
||||||
|
**router** 静态路由
|
||||||
|
**store** 菜单使用的状态机(菜单配置、用户登录信息等等)
|
||||||
|
**styles** 公共css
|
||||||
|
**types** 公共对象(分页参数对象、弹窗属性对象)
|
||||||
|
**utils** 无状态ts函数、常量配置,例如时间处理工具
|
||||||
|
**views** 二级菜单内容显示组件
|
||||||
|
|
||||||
|
## src/subsystem
|
||||||
|
1.子系统开发目录
|
||||||
|
|
||||||
|
### 子系统第一次开发必做事项
|
||||||
|
|
||||||
|
**vite.config.ts**
|
||||||
|
1、将“**build.outDir**”修改为编译后的输出路径,直接输出到微服务发布包“resources/html/微服务名称”中,与后端服务一起,按原有流程发布版本
|
||||||
|
2、将“**server.proxy**”修改为微服务访问路径,在VSCode开发环境下,将代替nginx的作用
|
||||||
|
|
||||||
|
**public/data/data.js**
|
||||||
|
|
||||||
|
1、修改publicData.currentSubsystem数据,用于框架获取子系统菜单模块
|
||||||
|
2、修改publicData.currentSubsystemName数据,用于框架获取子系统中文名称
|
||||||
|
3、如存在需要兼容老框架的的前端页面,修改externalOldERPIndex数据,否则改为''
|
||||||
|
|
||||||
|
### 子系统后期更新框架注意事项
|
||||||
|
|
||||||
|
不需要覆盖的文件夹、文件,以下有改动,会特别说明,并单独修改
|
||||||
|
|
||||||
|
1、public
|
||||||
|
|
||||||
|
2、vite.config.ts
|
||||||
|
|
||||||
|
3、src\subsystem
|
||||||
|
|
||||||
|
|
||||||
|
覆盖完文件后,执行npm install
|
||||||
|
|
||||||
|
### 开发规范
|
||||||
|
|
||||||
|
**api** 后端服务接口,按照微服务名称划分子文件夹(/api/pms),按照Controller划分ts文件(/api/pms/product.ts)
|
||||||
|
**router/dynamicRouter.ts** 菜单路由配置
|
||||||
|
**views** 菜单内容组件,按照一、二级菜单划分文件夹(/views/一级菜单名称/二级菜单名称)
|
||||||
|
**components** 子系统内部通用组件,如相同功能,在多个菜单内容组件中多次出现,可抽象为通用组件
|
||||||
|
**hooks** 子系统内业务相关组合式函数,集成了多个基础组件实现的通用函数
|
||||||
|
**store** 子系统内业务相关使用的状态机(参考框架使用场景)
|
||||||
|
**styles** 子系统内业务相关公共css
|
||||||
|
**types** 子系统内业务相关公共对象(参考框架使用场景)
|
||||||
|
**utils** 子系统内业务相关处理ts函数、业务常量配置
|
||||||
|
|
||||||
|
|
||||||
|
# UI组件使用规范
|
||||||
|
|
||||||
|
UI组件优先使用/src/framework/components/standard/中提供的UI组件,里面的组件使用都可以在框架提供的示例模块中找到使用代码。
|
||||||
|
|
||||||
|
如不满足或者没有,则表格组件使用vxe-table,除了表格组件,其他均使用elemen-plus提供的组件
|
||||||
|
|
||||||
|
如vxe-table、elemen-plus都不满足,则自己通过css、html实现组件化,并且泛用性高,请提交到框架负责人处,发布到公司的UI组件库
|
||||||
|
|
||||||
|
# JS/TS工具类推荐
|
||||||
|
|
||||||
|
相同功能优先使用/src/framework/utils/standard/中提供的ts,里面的组件使用都可以在框架提供的示例模块中找到使用代码。
|
||||||
|
|
||||||
|
已集成的其他ts工具:xe-utils
|
||||||
|
|
||||||
|
# 发布前注意事项
|
||||||
|
1、如果是独立模块或者系统,在/data/doc/升级日志.md中,增加本次升级内容
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const ElLoading: typeof import('element-plus/es')['ElLoading']
|
||||||
|
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||||
|
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||||
|
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
||||||
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||||
|
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||||
|
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||||
|
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||||
|
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||||
|
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||||
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||||
|
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||||
|
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactify: typeof import('@vueuse/core')['reactify']
|
||||||
|
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||||
|
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||||
|
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||||
|
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||||
|
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||||
|
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||||
|
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
|
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||||
|
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||||
|
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||||
|
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||||
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
|
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||||
|
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||||
|
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
|
const until: typeof import('@vueuse/core')['until']
|
||||||
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
|
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||||
|
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||||
|
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||||
|
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||||
|
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||||
|
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||||
|
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||||
|
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||||
|
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||||
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||||
|
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||||
|
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||||
|
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||||
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||||
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||||
|
const useCached: typeof import('@vueuse/core')['useCached']
|
||||||
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||||
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||||
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||||
|
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||||
|
const useDark: typeof import('@vueuse/core')['useDark']
|
||||||
|
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||||
|
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||||
|
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||||
|
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||||
|
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||||
|
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||||
|
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||||
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
|
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||||
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
|
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||||
|
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||||
|
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||||
|
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||||
|
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||||
|
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||||
|
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||||
|
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||||
|
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||||
|
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||||
|
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||||
|
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||||
|
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||||
|
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||||
|
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||||
|
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||||
|
const useFps: typeof import('@vueuse/core')['useFps']
|
||||||
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
|
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||||
|
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||||
|
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||||
|
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||||
|
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||||
|
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||||
|
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||||
|
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||||
|
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||||
|
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||||
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
|
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||||
|
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||||
|
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||||
|
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||||
|
const useNow: typeof import('@vueuse/core')['useNow']
|
||||||
|
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||||
|
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||||
|
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||||
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
|
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||||
|
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||||
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
|
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||||
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
|
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||||
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
|
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||||
|
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||||
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
|
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||||
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
|
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||||
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
|
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||||
|
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||||
|
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||||
|
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||||
|
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||||
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
|
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||||
|
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||||
|
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||||
|
const useToString: typeof import('@vueuse/core')['useToString']
|
||||||
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
|
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||||
|
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||||
|
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||||
|
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||||
|
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||||
|
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||||
|
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||||
|
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||||
|
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||||
|
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||||
|
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||||
|
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||||
|
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||||
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
|
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||||
|
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||||
|
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||||
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
|
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||||
|
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||||
|
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||||
|
const whenever: typeof import('@vueuse/core')['whenever']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElText: typeof import('element-plus/es')['ElText']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>首页</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/data/data.js"></script>
|
||||||
|
<!-- <script src="/frameconfig/index.js"></script> -->
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let t=new Date().getTime()
|
||||||
|
// document.write('<script src="/data/data.js?t=' + t + '"><\/script>');
|
||||||
|
document.write('<script src="/frameconfig/index.js?t=' + t + '"><\/script>');
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "abacus-static-framework",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build:prod": "vite build --mode production &&vue-tsc --noEmit",
|
||||||
|
"build:stage": "vite build --mode staging &&vue-tsc --noEmit",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@vue-office/docx": "^1.6.0",
|
||||||
|
"@vue-office/excel": "^1.7.2",
|
||||||
|
"@vue-office/pdf": "^1.6.5",
|
||||||
|
"@vueuse/core": "^10.2.1",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"axios": "^1.8.1",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"element-china-area-data": "^6.1.0",
|
||||||
|
"element-plus": "^2.9.5",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
|
"js-base64": "^3.7.7",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"path-to-regexp": "^6.2.1",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"sm-crypto": "^0.3.13",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"vxe-table": "^4.11.18",
|
||||||
|
"vxe-table-plugin-export-xlsx": "^4.0.7",
|
||||||
|
"xe-utils": "^3.7.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"sass": "^1.63.4",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"unplugin-auto-import": "^0.16.4",
|
||||||
|
"unplugin-vue-components": "^0.25.1",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
|
"vue-tsc": "^1.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 全局变量
|
||||||
|
// 用于定义通用模拟数据
|
||||||
|
let publicData={
|
||||||
|
currentSubsystemName:'系统管理',//当前子系统中文名称,与后端菜单子系统名称保持一致
|
||||||
|
currentSubsystem:'sms',//当前子系统名称,与后端菜单子系统编码保持一致
|
||||||
|
externalOldERPIndex:'/sms/sms/',//子系统动静分离版本页面地址配置,用于兼容历史模块使用
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<el-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { ElConfigProvider } from 'element-plus'
|
||||||
|
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
ElConfigProvider,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
locale: zhCn,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 782 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 735 B |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 803 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 437 B |
|
After Width: | Height: | Size: 537 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,297 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||||
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||||
|
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||||
|
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||||
|
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||||
|
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||||
|
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||||
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||||
|
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||||
|
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||||
|
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||||
|
const reactify: typeof import('@vueuse/core')['reactify']
|
||||||
|
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||||
|
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||||
|
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||||
|
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||||
|
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||||
|
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||||
|
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
|
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||||
|
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||||
|
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||||
|
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||||
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
|
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||||
|
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||||
|
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
|
const until: typeof import('@vueuse/core')['until']
|
||||||
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
|
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||||
|
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||||
|
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||||
|
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||||
|
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||||
|
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||||
|
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||||
|
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||||
|
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||||
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||||
|
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||||
|
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||||
|
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||||
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||||
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||||
|
const useCached: typeof import('@vueuse/core')['useCached']
|
||||||
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||||
|
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||||
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||||
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||||
|
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||||
|
const useDark: typeof import('@vueuse/core')['useDark']
|
||||||
|
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||||
|
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||||
|
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||||
|
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||||
|
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||||
|
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||||
|
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||||
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
|
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||||
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
|
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||||
|
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||||
|
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||||
|
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||||
|
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||||
|
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||||
|
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||||
|
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||||
|
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||||
|
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||||
|
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||||
|
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||||
|
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||||
|
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||||
|
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||||
|
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||||
|
const useFps: typeof import('@vueuse/core')['useFps']
|
||||||
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
|
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||||
|
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||||
|
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||||
|
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||||
|
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||||
|
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||||
|
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||||
|
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||||
|
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||||
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
|
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||||
|
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||||
|
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||||
|
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||||
|
const useNow: typeof import('@vueuse/core')['useNow']
|
||||||
|
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||||
|
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||||
|
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||||
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
|
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||||
|
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||||
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
|
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||||
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
|
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||||
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
|
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||||
|
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||||
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
|
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||||
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
|
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||||
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
|
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||||
|
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||||
|
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||||
|
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||||
|
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||||
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
|
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||||
|
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||||
|
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||||
|
const useToString: typeof import('@vueuse/core')['useToString']
|
||||||
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
|
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||||
|
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||||
|
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||||
|
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||||
|
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||||
|
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||||
|
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||||
|
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||||
|
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||||
|
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||||
|
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||||
|
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||||
|
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||||
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
|
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||||
|
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||||
|
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||||
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
|
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||||
|
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||||
|
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||||
|
const whenever: typeof import('@vueuse/core')['whenever']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import request from '@/framework/utils/request';
|
||||||
|
import { AxiosPromise } from 'axios';
|
||||||
|
import { UserInfo } from './types';
|
||||||
|
|
||||||
|
function replaceAcasService(url){
|
||||||
|
if(publicData.replaceAcasService){
|
||||||
|
return url.replace('/acas/','/'+publicData.replaceAcasService+'/')
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取路由列表
|
||||||
|
*/
|
||||||
|
export function listRoutes(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url: replaceAcasService('/acas/user/allowAccessMenuAndModules'),
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**获取key */
|
||||||
|
export function getKey(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url:replaceAcasService('/acas/pk'),
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**刷新*/
|
||||||
|
export function baseRefresh(url:string,data: any = {}) {
|
||||||
|
return request({
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
url:replaceAcasService(url),
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**退出登录 */
|
||||||
|
export function baseLogout(url:string,data: any = {}) {
|
||||||
|
return request({
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
url:replaceAcasService(url),
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**登录后修改密码 */
|
||||||
|
export function baseLoginUpdatePwd(url:string,data: any = {}) {
|
||||||
|
return request({
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
url:replaceAcasService(url),
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**获取key */
|
||||||
|
export function getEmployee(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url:replaceAcasService('/acas/employee/getEmployee'),
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**获取员工管理业务域*/
|
||||||
|
export function getBusinessDomain(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url: replaceAcasService('/acas/user/loginUserdomain'),
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取有权限的子系统*/
|
||||||
|
export function getAllowAccessSubsystem(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url: replaceAcasService('/acas/user/allowAccessSubsystem'),
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取员工信息 */
|
||||||
|
export function getUsermessage(params: any = {}) {
|
||||||
|
if (import.meta.env.MODE === 'development'){
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({"user":{"tenantId":"0000","tenantName":"奥博克","userId":"0","userName":"系统管理员","deptId":"01","deptName":"开发部","avatarUrl":""}});
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
return request({
|
||||||
|
url:'/acas/user/message',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 登录用户信息
|
||||||
|
*/
|
||||||
|
export interface UserInfo {
|
||||||
|
dept: any;
|
||||||
|
employee: any;
|
||||||
|
roles: any;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import request from '@/framework/utils/request';
|
||||||
|
|
||||||
|
/**获取异步事件列表 */
|
||||||
|
export function queryAsyncEvents(params: any = {}) {
|
||||||
|
return request({
|
||||||
|
url:'/sms/asyncEvent/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<el-breadcrumb class="h-[50px] flex items-center">
|
||||||
|
<transition-group name="breadcrumb">
|
||||||
|
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
item.redirect === 'noredirect' || index === breadcrumbs.length - 1
|
||||||
|
"
|
||||||
|
class="text-[var(--el-disabled-text-color)]"
|
||||||
|
>{{ item.meta.title }}</span
|
||||||
|
>
|
||||||
|
<a v-else @click.prevent="handleLink(item)">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</transition-group>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeMount, ref, watch } from "vue";
|
||||||
|
import { useRoute, RouteLocationMatched } from "vue-router";
|
||||||
|
import { compile } from "path-to-regexp";
|
||||||
|
import router from "@/framework/router";
|
||||||
|
// import { translateRouteTitleI18n } from "@/framework/utils/i18n";
|
||||||
|
|
||||||
|
const currentRoute = useRoute();
|
||||||
|
const pathCompile = (path: string) => {
|
||||||
|
const { params } = currentRoute;
|
||||||
|
const toPath = compile(path);
|
||||||
|
return toPath(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbs = ref([] as Array<RouteLocationMatched>);
|
||||||
|
|
||||||
|
function getBreadcrumb() {
|
||||||
|
console.log(currentRoute.matched,8888);
|
||||||
|
|
||||||
|
let matched = currentRoute.matched.filter(
|
||||||
|
(item) => item.meta && item.meta.title
|
||||||
|
);
|
||||||
|
// const first = matched[0];
|
||||||
|
// if (!isDashboard(first)) {
|
||||||
|
// matched = [
|
||||||
|
// { path: "/home", meta: { title: "首页" } } as any,
|
||||||
|
// ].concat(matched);
|
||||||
|
// }
|
||||||
|
breadcrumbs.value = matched.filter((item) => {
|
||||||
|
return item.meta && item.meta.title && item.meta.breadcrumb !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDashboard(route: RouteLocationMatched) {
|
||||||
|
const name = route && route.name;
|
||||||
|
if (!name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
name.toString().trim().toLocaleLowerCase() ===
|
||||||
|
"首页"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLink(item: any) {
|
||||||
|
const { redirect, path } = item;
|
||||||
|
if (redirect) {
|
||||||
|
router.push(redirect).catch((err) => {
|
||||||
|
console.warn(err);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(pathCompile(path)).catch((err) => {
|
||||||
|
console.warn(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentRoute.path,
|
||||||
|
(path) => {
|
||||||
|
if (path.startsWith("/redirect/")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getBreadcrumb();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
getBreadcrumb();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-breadcrumb.el-breadcrumb {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 element-plus 的样式
|
||||||
|
.el-breadcrumb__inner,
|
||||||
|
.el-breadcrumb__inner a {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-container">
|
||||||
|
<img src="@/assets/img/403.png" class="not-img" alt="403" />
|
||||||
|
<div class="not-detail">
|
||||||
|
<h2>403</h2>
|
||||||
|
<h4>抱歉,您无权访问该页面~🙅♂️🙅♀️</h4>
|
||||||
|
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="403">
|
||||||
|
// import { HOME_URL } from "@/config";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "./index.scss";
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-container">
|
||||||
|
<img src="@/assets/img/404.png" class="not-img" alt="404" />
|
||||||
|
<div class="not-detail">
|
||||||
|
<h2>404</h2>
|
||||||
|
<h4>抱歉,您访问的页面不存在~</h4>
|
||||||
|
<h4>有可能是您没有访问权限~</h4>
|
||||||
|
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="404">
|
||||||
|
// import { HOME_URL } from "@/config";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "./index.scss";
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-container">
|
||||||
|
<img src="@/assets/img/500.png" class="not-img" alt="500" />
|
||||||
|
<div class="not-detail">
|
||||||
|
<h2>500</h2>
|
||||||
|
<h4>抱歉,您的网络不见了~🤦♂️🤦♀️</h4>
|
||||||
|
<el-button type="primary" @click="router.push('/')"> 返回首页 </el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="500">
|
||||||
|
// import { HOME_URL } from "@/config";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "./index.scss";
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.not-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
.not-img {
|
||||||
|
margin-right: 120px;
|
||||||
|
}
|
||||||
|
.not-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
h2,
|
||||||
|
h4 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 60px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 30px 0 20px;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.el-button {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <div class="hambuger-wrapper" :class="{active: isActive}" @click="toggleClick">
|
||||||
|
<el-icon size="20">
|
||||||
|
<Expand />
|
||||||
|
</el-icon>
|
||||||
|
</div> -->
|
||||||
|
<div class="hambuger-wrapper" @click="toggleClick">
|
||||||
|
<img :src="foldPng" style="width: 28px;height: 28px;" :style="{transform: isActive ? '' :'rotate(180deg)'}"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import foldPng from '@/assets/img/icon/fold.png'
|
||||||
|
defineProps({
|
||||||
|
isActive: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["toggleClick"]);
|
||||||
|
|
||||||
|
function toggleClick() {
|
||||||
|
emit("toggleClick");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hambuger-wrapper{
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover{
|
||||||
|
background: rgba(0,0,0,.025);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.active{
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<img class="menu-img" :src="imgSrc"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { getAssetsImages } from '@/framework/utils/index'
|
||||||
|
const props = defineProps({
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
default: "/menu",
|
||||||
|
},
|
||||||
|
imgName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const imgSrc = computed(() => getAssetsImages(props.imgName, props.path));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-img{
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="批量添加" :destroy-on-close="true" width="580px" draggable top="1vh">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="模板下载:">
|
||||||
|
<el-button type="primary" @click="downloadFile">
|
||||||
|
<el-icon color="#fff">
|
||||||
|
<Download />
|
||||||
|
</el-icon> 模板下载
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文件上传:">
|
||||||
|
<el-upload class="upload" drag :action="uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请按模板上传文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import type {UploadRawFile } from 'element-plus'
|
||||||
|
|
||||||
|
let props = defineProps({
|
||||||
|
templateName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
uploadUrl: String
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
// 下载模板
|
||||||
|
const downloadFile = () => {
|
||||||
|
if (props.templateName === '') {
|
||||||
|
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = document.createElement("a");
|
||||||
|
dom.style.display = 'none'
|
||||||
|
dom.target = '_blank'
|
||||||
|
dom.href = `./templete/${props.templateName}`
|
||||||
|
document.body.appendChild(dom);
|
||||||
|
dom.click();
|
||||||
|
document.body.removeChild(dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件上传之前判断
|
||||||
|
* @param file 上传的文件
|
||||||
|
* */
|
||||||
|
const beforeExcelUpload = (file: UploadRawFile) => {
|
||||||
|
const suffix = file.name.split('.').pop()
|
||||||
|
const isExcel = ['xls', 'xlsx'].includes(suffix);
|
||||||
|
if (!isExcel)
|
||||||
|
ElMessage({type: 'warning',message: '上传文件只能是 xls / xlsx 格式' })
|
||||||
|
|
||||||
|
// const fileSize = file.size
|
||||||
|
// if (!fileSize)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// ElNotification({
|
||||||
|
// title: "温馨提示",
|
||||||
|
// message: `上传文件大小不能超过 ${parameter.value.fileSize}MB!`,
|
||||||
|
// type: "warning"
|
||||||
|
// });
|
||||||
|
// }, 0);
|
||||||
|
return isExcel;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const excelUploadSuccess = (response: any) => {
|
||||||
|
emit('success', response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传错误提示
|
||||||
|
const excelUploadError = () => {
|
||||||
|
ElMessage({
|
||||||
|
title: "温馨提示",
|
||||||
|
message: `批量添加失败,请您重新上传!`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
dialogVisible
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.upload {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="" :class="'pagination ' + { hidden: hidden }">
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:background="background"
|
||||||
|
:layout="layout"
|
||||||
|
:page-sizes="pageSizes"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { computed, PropType } from "vue";
|
||||||
|
import { scrollTo } from "@/framework/utils/scroll-to";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
total: {
|
||||||
|
required: true,
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
pageSizes: {
|
||||||
|
type: Array as PropType<number[]>,
|
||||||
|
default() {
|
||||||
|
return [10, 20, 30, 50];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: String,
|
||||||
|
default: "total, sizes, prev, pager, next, jumper",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
hidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["pagination", "update:page", "update:limit"]);
|
||||||
|
|
||||||
|
const currentPage = useVModel(props, "page", emit);
|
||||||
|
|
||||||
|
const pageSize = useVModel(props, "limit", emit);
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
emit("pagination", { page: currentPage, limit: val });
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
currentPage.value = val;
|
||||||
|
emit("pagination", { page: val, limit: props.limit });
|
||||||
|
if (props.autoScroll) {
|
||||||
|
scrollTo(0, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pagination {
|
||||||
|
padding: 20px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<el-select v-model="value" :placeholder="placeholder" style="width: 100%;" clearable :filterable="filterable">
|
||||||
|
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px;"
|
||||||
|
:value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed, onMounted} from 'vue'
|
||||||
|
/**
|
||||||
|
* SearchSelect
|
||||||
|
* @author nielang
|
||||||
|
* @description 用于实时检索的下拉框
|
||||||
|
* @property {String} placeholder input的placeholder
|
||||||
|
* @property {Function} request 接口请求函数
|
||||||
|
* @property {String} requestParam 接口请求参数名称
|
||||||
|
* @property {Boolean} showOptionValue 选项中是否显示value属性
|
||||||
|
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
|
||||||
|
* @property {String} optionLabel 接口用于显示的属性
|
||||||
|
* @property {Boolean} filterable 是否可搜索
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue:{
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
request:{
|
||||||
|
type: Function,
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
requestParam: {
|
||||||
|
type: Object,
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
showOptionValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
optionValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "id",
|
||||||
|
},
|
||||||
|
optionLabel: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "name",
|
||||||
|
},
|
||||||
|
filterable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ListItem {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
const options = ref<ListItem[]>([])
|
||||||
|
const initOptions = () => {
|
||||||
|
if (props.request) {
|
||||||
|
props.request(props.requestParam).then((data:any) => {
|
||||||
|
options.value = data.map((item: any) => {
|
||||||
|
return {
|
||||||
|
value: item[props.optionValue],
|
||||||
|
label: (props.showOptionValue ? `【${item[props.optionValue]}】` : '') + item[props.optionLabel]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initOptions();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable
|
||||||
|
:remote-method="remoteMethod" :loading="loading" clearable >
|
||||||
|
<el-option v-for="item in options" :key="item.value" :label="item.label" style="max-width:480px"
|
||||||
|
:value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
/**
|
||||||
|
* SearchSelect
|
||||||
|
* @author nielang
|
||||||
|
* @description 用于实时检索的下拉框
|
||||||
|
* @property {String} placeholder input的placeholder
|
||||||
|
* @property {Function} request 接口请求函数
|
||||||
|
* @property {String} requestParam 接口请求参数名称
|
||||||
|
* @property {Boolean} showOptionValue 选项中是否显示value属性
|
||||||
|
* @property {String} optionValue 接口用于显示的属性(该属性将返回父组件用于检索)
|
||||||
|
* @property {String} optionLabel 接口用于显示的属性
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue:{
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "请输入商品编码/名称",
|
||||||
|
},
|
||||||
|
request:{
|
||||||
|
type: Function,
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
requestParam: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
showOptionValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
optionValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "id",
|
||||||
|
},
|
||||||
|
optionLabel: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "name",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
interface ListItem {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const options = ref<ListItem[]>([])
|
||||||
|
const remoteMethod = (query: string) => {
|
||||||
|
if (query) {
|
||||||
|
loading.value = true
|
||||||
|
if (props.request) {
|
||||||
|
const params:any = {}
|
||||||
|
params[props.requestParam] = query
|
||||||
|
props.request(params).then((data:any) => {
|
||||||
|
loading.value = false
|
||||||
|
options.value = data.map((item: any) => {
|
||||||
|
return {
|
||||||
|
value: item[props.optionValue],
|
||||||
|
label: (props.showOptionValue ? `【${item[props.optionValue]}】` : '') + item[props.optionLabel]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!--主题-->
|
||||||
|
<template>
|
||||||
|
<div class="form-container ab-m-t-16 title">
|
||||||
|
<el-page-header :icon="null" >
|
||||||
|
<template #title>
|
||||||
|
<div style="display:flex;align-items: center;">
|
||||||
|
<slot name="titleBack"></slot>
|
||||||
|
<span style="font-size: 25px;font-weight: 500;"><slot name="titleName"></slot></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<span style="font-size: 15px;"><slot name="titleContent"></slot></span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<slot name="titleOperate"></slot>
|
||||||
|
</template>
|
||||||
|
</el-page-header>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.title{
|
||||||
|
padding: 10px 0px 10px 10px;;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<!-- 内容分隔组件 -->
|
||||||
|
<template>
|
||||||
|
<div class="title-container" >
|
||||||
|
<div class="header" :style="{ backgroundColor: bgColor, fontSize, color, borderBottom: arrow?'1px solid #CDD0D6':'' }" @click="handleShow" >
|
||||||
|
<div class="left">
|
||||||
|
<span class="line" :style="{display: showLine? 'inline' : 'none', borderColor: lineColor}"></span> <span class="name">{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="right" :style="{display: arrow? 'inline' : 'none'}">
|
||||||
|
<el-icon :style="{transform: show ? 'rotate(90deg)' : '', transition: 'transform 0.2s linear'}"><ArrowRightBold /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content" v-show="show" :style="{height: show ? 'auto' : '0px'}">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true, }, //标签名称
|
||||||
|
bgColor: { type: String, default: '#fff' },//背景颜色
|
||||||
|
color: { type: String, default: '#222' },//文字颜色
|
||||||
|
fontSize: { type: String, default: '20px' },//字体大小
|
||||||
|
showLine: { type: Boolean, default: true },//是否显示图标
|
||||||
|
lineColor: { type: String, default: '' },//图标颜色
|
||||||
|
arrow: { type: Boolean, default: false },//展开、收起功能
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = ref(true)
|
||||||
|
const handleShow = () => {
|
||||||
|
if (!props.arrow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
show.value = !show.value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
// border-bottom: 1px solid #CDD0D6;
|
||||||
|
|
||||||
|
.left{
|
||||||
|
.line {
|
||||||
|
width: 2px;
|
||||||
|
border: 2px solid var(--el-color-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right{
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content{
|
||||||
|
height: 0px;
|
||||||
|
transition: height 2s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- 点击复制 -->
|
||||||
|
<template >
|
||||||
|
<el-tooltip v-if="content" content="复制" placement="top" effect="dark">
|
||||||
|
<el-icon class="ab-cursor-pointer" @click="useCopy(content)"><CopyDocument /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
{{ content }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCopy } from '@/framework/hooks/useCopy'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: { type: String, required: true,}, //内容
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!--文档预览组件-->
|
||||||
|
<template>
|
||||||
|
<el-dialog title="预览" :model-value="dialogVisible" :before-close="handleClose" :modal="false" :show-close="true" :close-on-click-modal="false" append-to-body>
|
||||||
|
<template #title>
|
||||||
|
<span class="dialog-title">
|
||||||
|
<el-button type="primary" @click="downloadFile()">下载文件</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="['doc', 'docx'].includes(fileType)">
|
||||||
|
<vue-office-docx :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="fileType == 'xlsx'">
|
||||||
|
<vue-office-excel :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="fileType == 'pdf'">
|
||||||
|
<vue-office-pdf :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="['png', 'jpg', 'jpeg','gif'].includes(fileType)">
|
||||||
|
<el-image :src="fileUrl" style="height: 70vh;overflow: auto" @rendered="rendered" @error="errorHandler" :options="{width: '100%',overflow: 'auto'}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else="">
|
||||||
|
<el-input v-model="textarea1" style="height: 70vh;overflow: auto" autosize type="textarea" :options="{width: '100%',overflow: 'auto'}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 引入VueOffice组件
|
||||||
|
import VueOfficeDocx from '@vue-office/docx'
|
||||||
|
import VueOfficeExcel from '@vue-office/excel'
|
||||||
|
import VueOfficePdf from '@vue-office/pdf'
|
||||||
|
import XEUtils from 'xe-utils'
|
||||||
|
|
||||||
|
// 引入相关样式
|
||||||
|
import '@vue-office/docx/lib/index.css'
|
||||||
|
import '@vue-office/excel/lib/index.css'
|
||||||
|
|
||||||
|
const textarea1=ref('')
|
||||||
|
const emit = defineEmits(["closePreviewDialog"]);
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
fileName: { type: String, default: '' },// 文件url
|
||||||
|
fileType: { type: String, default: '' },// 文件url
|
||||||
|
fileUrl: { type: String, default: '' },// 文件url
|
||||||
|
dialogVisible: { type: Boolean, default: false },// 控制显示
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadFile = () => {
|
||||||
|
if(XEUtils.startsWith(props.fileUrl, './')||XEUtils.startsWith(props.fileUrl, '/')){
|
||||||
|
window.location.href=props.fileUrl
|
||||||
|
}else{
|
||||||
|
window.location.href=props.fileUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rendered = () => {
|
||||||
|
console.log('渲染完成')
|
||||||
|
}
|
||||||
|
const errorHandler = (e:any) => {
|
||||||
|
console.error('渲染失败',e)
|
||||||
|
ElMessage({message: '文档渲染失败',type: 'error',})
|
||||||
|
}
|
||||||
|
const handleClose = (done) => {
|
||||||
|
emit('closePreviewDialog', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<!-- 动态表单组件 -->
|
||||||
|
<!-- 新功能禁止使用该组件 -->
|
||||||
|
<template>
|
||||||
|
<div class="form-container">
|
||||||
|
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
|
||||||
|
<el-row :gutter="props.gutter">
|
||||||
|
<template v-for="(value, key, index) in props.formModel">
|
||||||
|
<el-col class="ab-p-b-5" v-if="'section'===fieldConfigMap.get(key)?.type" :span="24" v-show="isShow(fieldConfigMap.get(key))">
|
||||||
|
<AbSection :title="fieldConfigMap.get(key).name"></AbSection>
|
||||||
|
</el-col>
|
||||||
|
<el-col v-else :span="fieldConfigMap.get(key)?.span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))">
|
||||||
|
<template v-if="fieldConfigMap.get(key)?.slot">
|
||||||
|
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
|
||||||
|
<slot :name="fieldConfigMap.get(key)?.slot"></slot>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
|
||||||
|
</template>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
</template>
|
||||||
|
<el-col :span="24" class="ab-text-right ab-m-b-18">
|
||||||
|
<slot v-if="$slots['button']" name="button"></slot>
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary" @click="executeSave(formRef)">保存</el-button>
|
||||||
|
<el-button type="default" @click="formReset()">重置</el-button>
|
||||||
|
</template>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
|
||||||
|
import AbSection from "@/framework/components/standard/AbSection/index.vue";
|
||||||
|
const formRef = ref<FormInstance>() //表单引用
|
||||||
|
const dFormRules = reactive<FormRules>({}) //验证规则
|
||||||
|
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
|
||||||
|
//组件传参
|
||||||
|
let props = defineProps({
|
||||||
|
formModel: { type: Object },//表单绑定数据
|
||||||
|
fieldConfigs: { type: Array },//表单明细配置
|
||||||
|
gutter: { type: Number, default: 0 },//栅格间隔
|
||||||
|
span: { type: Number, default: 6 },//栅格占据的列数
|
||||||
|
saveEvent: { type: Function },//保存事件
|
||||||
|
resetEvent: { type: Function },//重置事件
|
||||||
|
})
|
||||||
|
|
||||||
|
// 外部使用响应式函数,不需要监听
|
||||||
|
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
|
||||||
|
// console.log(newValue);
|
||||||
|
// },{ deep: true }
|
||||||
|
// )
|
||||||
|
|
||||||
|
/**是否显示 */
|
||||||
|
const isShow = (fieldConfig) => {
|
||||||
|
if(fieldConfig&&undefined!==fieldConfig.show&&!fieldConfig.show)
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
/**数据处理 */
|
||||||
|
const dataHandle = (fieldConfigs) => {
|
||||||
|
for (let fc of fieldConfigs) {
|
||||||
|
//转换map,方便获取数据
|
||||||
|
fieldConfigMap.value.set(fc.key, fc)
|
||||||
|
//设置表单验证规则
|
||||||
|
if (undefined !== fc.rules) {
|
||||||
|
dFormRules[fc.key] = fc.rules
|
||||||
|
}
|
||||||
|
//绑定表单默认数据
|
||||||
|
if(!props.formModel[fc.key])
|
||||||
|
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataHandle(props.fieldConfigs)
|
||||||
|
|
||||||
|
/**执行查询 */
|
||||||
|
const executeSave = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.saveEvent) props.saveEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**重置查询条件 */
|
||||||
|
const formReset = () => {
|
||||||
|
if (props.resetEvent) {
|
||||||
|
props.resetEvent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let fc of props.fieldConfigs) {
|
||||||
|
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<!-- 动态表单明细组件 -->
|
||||||
|
<!-- 新功能禁止使用该组件 -->
|
||||||
|
<template>
|
||||||
|
<el-form-item :label-width="props.fieldSet?.labelWidth" :label="props.fieldSet?.name" :prop="props.fieldSet?.key" :required="props.fieldSet?.required">
|
||||||
|
<!-- 文本 -->
|
||||||
|
<el-text v-if="props.fieldSet?.type==='text'" style="width: 100%;" v-model="formModel[props.fieldSet?.key]" >
|
||||||
|
<Text :data="formModel[props.fieldSet?.key]"></Text>
|
||||||
|
</el-text>
|
||||||
|
<!-- 文本框 -->
|
||||||
|
<el-input v-else-if="props.fieldSet?.type==='input'" v-model="formModel[props.fieldSet?.key]"
|
||||||
|
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请输入'+props.fieldSet?.name"
|
||||||
|
:disabled="props.fieldSet?.disabled" />
|
||||||
|
<!-- 下拉框 -->
|
||||||
|
<el-select v-else-if="props.fieldSet?.type==='select'" v-model="formModel[props.fieldSet?.key]"
|
||||||
|
:placeholder="props.fieldSet?.placeholder?props.fieldSet?.placeholder:'请选择'+props.fieldSet?.name"
|
||||||
|
:disabled="props.fieldSet?.disabled" clearable filterable style="width: 100%;">
|
||||||
|
<el-option v-for="item in props.fieldSet?.listData" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||||
|
</el-select>
|
||||||
|
<!-- 单选 -->
|
||||||
|
<el-radio-group v-else-if="props.fieldSet?.type==='radio'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
|
||||||
|
<el-radio v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<!-- 多选 -->
|
||||||
|
<el-checkbox-group v-else-if="props.fieldSet?.type==='checkbox'" v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled">
|
||||||
|
<el-checkbox v-for="item in props.fieldSet?.listData" :label="item.value" :disabled="item.disabled">{{item.label}}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<!-- 时间选择器 -->
|
||||||
|
<el-date-picker style="width: 100%;" v-else-if="dataPickerType.find((o: any) => o.type === props.fieldSet?.type) " v-model="formModel[props.fieldSet?.key]" :disabled="props.fieldSet?.disabled"
|
||||||
|
:type="props.fieldSet?.type" unlink-panels range-separator="-"
|
||||||
|
:start-placeholder="props.fieldSet?.placeholder?'开始'+props.fieldSet?.placeholder:'开始'+props.fieldSet?.name"
|
||||||
|
:end-placeholder="props.fieldSet?.placeholder?'结束'+props.fieldSet?.placeholder:'结束'+props.fieldSet?.name"
|
||||||
|
:value-format="dataPickerType.find((o: any) => o.type === props.fieldSet?.type).valueFormat" />
|
||||||
|
<!-- 不指定类型、无类型不输出 -->
|
||||||
|
<template v-else></template>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Text from "@/framework/components/standard/Text/index.vue";
|
||||||
|
/**时间选择器配置 */
|
||||||
|
let dataPickerType=[
|
||||||
|
{type:'year',valueFormat:'YYYY'},
|
||||||
|
{type:'month',valueFormat:'YYYY-MM'},
|
||||||
|
{type:'date' ,valueFormat:'YYYY-MM-DD'},
|
||||||
|
{type:'dates' ,valueFormat:'YYYY-MM-DD'},
|
||||||
|
{type:'datetime' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
|
||||||
|
{type:'week' },
|
||||||
|
{type:'datetimerange' ,valueFormat:'YYYY-MM-DD HH:mm:ss'},
|
||||||
|
{type:'daterange' ,valueFormat:'YYYY-MM-DD'},
|
||||||
|
{type:'monthrange',valueFormat:'YYYY-MM'},
|
||||||
|
]
|
||||||
|
let props = defineProps({
|
||||||
|
formModel: {type:Object},//表单函数
|
||||||
|
editable:{type:Boolean,default:true},//可编辑
|
||||||
|
//当前字段设置
|
||||||
|
fieldSet: {
|
||||||
|
type:Object,
|
||||||
|
default:{
|
||||||
|
key:String,//字段Key
|
||||||
|
name:String, //显示名称
|
||||||
|
placeholder:String, //默认提示
|
||||||
|
required:Boolean,//是否必填
|
||||||
|
labelWidth:{type:String,default:'82px'},//标签宽度
|
||||||
|
show: { type: Boolean, default: true },//是否显示
|
||||||
|
disabled:Boolean,//是否禁用
|
||||||
|
type:String,//输入类型,当前支持input、select、radio、checkbox
|
||||||
|
//列表数据,当type为select、radio、checkbox时使用
|
||||||
|
listData:{
|
||||||
|
type:Array,
|
||||||
|
default:[
|
||||||
|
{
|
||||||
|
value:String,//值
|
||||||
|
text:String,//文本显示、
|
||||||
|
disabled:Boolean,//是否禁用
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<!-- 动态查询表单组件 -->
|
||||||
|
<!-- 新功能禁止使用该组件 -->
|
||||||
|
<template>
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="more-filter" @click="criteriaShow = !criteriaShow" v-if="criteriaShowNumber < fieldConfigMap.size">
|
||||||
|
{{ criteriaShow ? '收起' : '展开' }}更多筛选
|
||||||
|
<el-icon v-if="criteriaShow"><ArrowDown /></el-icon>
|
||||||
|
<el-icon v-else><ArrowUp /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" label-width="82px" :model="props.formModel" :rules="dFormRules">
|
||||||
|
<el-row :gutter="props.gutter">
|
||||||
|
<template v-for="(value, key, index) in props.formModel">
|
||||||
|
<el-col :span="fieldConfigMap.get(key).span ? fieldConfigMap.get(key).span : props.span" v-show="isShow(fieldConfigMap.get(key))&&(!(props.criteriaShowNumber-1<index)?true:criteriaShow)">
|
||||||
|
<!-- 自定义插槽-->
|
||||||
|
<template v-if="fieldConfigMap.get(key).slot">
|
||||||
|
<el-form-item :label-width="fieldConfigMap.get(key)?.labelWidth" :label="fieldConfigMap.get(key)?.name" :prop="fieldConfigMap.get(key)?.key" :required="fieldConfigMap.get(key)?.required">
|
||||||
|
<slot :name="fieldConfigMap.get(key).slot"></slot>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- 固定类型-->
|
||||||
|
<template v-else>
|
||||||
|
<AbFormItem :formModel="props.formModel" :fieldSet="fieldConfigMap.get(key)"></AbFormItem>
|
||||||
|
</template>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<el-col :span="3" class="ab-text-right ab-m-b-18" :offset="criteriaShow ? props.queryCriteriaOffset[1] : props.queryCriteriaOffset[0]">
|
||||||
|
<el-button type="primary" v-throttle:click.delay[1000]="()=>executeQuery(formRef)" >查询</el-button>
|
||||||
|
<el-button type="default" @click="formReset()">重置</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AbFormItem from "@/framework/components/standard/FormDynamic/abFormItem.vue";
|
||||||
|
const criteriaShow = ref(false) //查询条件隐藏功能
|
||||||
|
const formRef = ref<FormInstance>() //表单引用
|
||||||
|
const dFormRules = reactive<FormRules>({}) //验证规则
|
||||||
|
const fieldConfigMap = ref(new Map()) //表单明细配置,Map<表单函数key,表单函数key对应配置>
|
||||||
|
//组件传参
|
||||||
|
let props = defineProps({
|
||||||
|
formModel: { type: Object },//表单绑定数据
|
||||||
|
fieldConfigs: { type: Array },//表单明细配置
|
||||||
|
gutter: { type: Number, default: 0 },//栅格间隔
|
||||||
|
span: { type: Number, default: 6 },//栅格占据的列数
|
||||||
|
queryEvent: { type: Function },//查询事件
|
||||||
|
resetEvent: { type: Function },//重置事件
|
||||||
|
queryCriteriaOffset: { type: Array, default: [0,6] },//查询条件偏移量,[收起偏移量,展开偏移量]
|
||||||
|
criteriaShowNumber: { type: Number, default: 6 },//查询条件默认显示数量
|
||||||
|
})
|
||||||
|
|
||||||
|
// 外部使用响应式函数,不需要监听
|
||||||
|
// watch(() => props.fieldConfigs,(newValue, oldValue) => {
|
||||||
|
// console.log(newValue);
|
||||||
|
// },{ deep: true }
|
||||||
|
// )
|
||||||
|
|
||||||
|
/**是否显示 */
|
||||||
|
const isShow = (fieldConfig) => {
|
||||||
|
if(undefined!==fieldConfig.show&&!fieldConfig.show)
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
/**数据处理 */
|
||||||
|
const dataHandle = (fieldConfigs) => {
|
||||||
|
for (let fc of fieldConfigs) {
|
||||||
|
//转换map,方便获取数据
|
||||||
|
fieldConfigMap.value.set(fc.key, fc)
|
||||||
|
//设置表单验证规则
|
||||||
|
if (undefined !== fc.rules) {
|
||||||
|
dFormRules[fc.key] = fc.rules
|
||||||
|
}
|
||||||
|
//绑定表单默认数据
|
||||||
|
if(!props.formModel[fc.key])
|
||||||
|
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataHandle(props.fieldConfigs)
|
||||||
|
|
||||||
|
/**执行查询 */
|
||||||
|
const executeQuery = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl || !await formEl.validate((valid, fields) => { return valid })) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.queryEvent) props.queryEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**重置查询条件 */
|
||||||
|
const formReset = () => {
|
||||||
|
if (props.resetEvent) {
|
||||||
|
props.resetEvent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let fc of props.fieldConfigs) {
|
||||||
|
props.formModel[fc.key] = (undefined !== fc.value ? fc.value : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<!-- Excel上传组件 -->
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="文件上传" :destroy-on-close="true" width="580px" draggable top="1vh">
|
||||||
|
<el-row justify="end">
|
||||||
|
<el-col :span="24" style="text-align: center;">
|
||||||
|
<el-text size="large">注意事项:上传文件必须符合模板格式要求,点此<el-link type="primary" @click="downloadFile"><el-text type="primary" size="large">下载模板</el-text></el-link >! </el-text>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-upload ref="fileUpload" :headers="ht" :data="props.data" drag :auto-upload="false" :show-file-list="true" :action="props.uploadUrl" :on-success="excelUploadSuccess" :on-error="excelUploadError" :before-upload="beforeExcelUpload">
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<el-row v-show="''!==uploadResult" style="margin-top:10px" justify="end">
|
||||||
|
<el-col :span="24" style="text-align: right;">
|
||||||
|
<el-alert type="success" :closable="false">
|
||||||
|
<template #default>
|
||||||
|
<el-text >
|
||||||
|
上传结果:本次一共导入数据[<el-text type="primary">{{ uploadResult.totalNum }}</el-text>]条,
|
||||||
|
成功[<el-text type="success" >{{ uploadResult.successNum }}</el-text>]条,
|
||||||
|
<el-text v-if="uploadResult.updateNum>0" >修改[<el-text type="success" >{{ uploadResult.updateNum }}</el-text>]条,</el-text>
|
||||||
|
失败[<el-text type="warning" >{{ uploadResult.failureNum }}</el-text>]条,
|
||||||
|
<el-link type="primary" v-if="uploadResult.filePath!==''" :href="uploadResult.filePath" target="_blank">下载错误信息</el-link>
|
||||||
|
</el-text>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="margin-top:10px" justify="end">
|
||||||
|
<el-col :span="12" style="text-align: right;">
|
||||||
|
<el-button type="primary" @click="submitFile">上传</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {UploadRawFile } from 'element-plus'
|
||||||
|
import { getAllHeader } from "@/framework/store/modules/user";
|
||||||
|
|
||||||
|
|
||||||
|
const fileUpload=ref()
|
||||||
|
|
||||||
|
const ht=getAllHeader()
|
||||||
|
|
||||||
|
let props = defineProps({
|
||||||
|
//上传时附带的额外参数
|
||||||
|
data: {},
|
||||||
|
//模板名称(模板需要放在public/templete)
|
||||||
|
templateName: { type: String, default: '', },
|
||||||
|
//文件上传地址
|
||||||
|
uploadUrl: String,
|
||||||
|
//是否自定义结果处理
|
||||||
|
customResultHandle:{ type: Boolean, default: false },
|
||||||
|
//上传文件最大值,单位(MB)
|
||||||
|
uploadMaxSize: { type: Number, default: 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
// 下载模板
|
||||||
|
const downloadFile = () => {
|
||||||
|
if (props.templateName === '') {
|
||||||
|
return ElMessage({ type: 'error', message: '未配置模板下载路径' })
|
||||||
|
}
|
||||||
|
let dom = document.createElement("a");
|
||||||
|
dom.style.display = 'none'
|
||||||
|
dom.target = '_blank'
|
||||||
|
dom.href = `./templete/${props.templateName}`
|
||||||
|
document.body.appendChild(dom);
|
||||||
|
dom.click();
|
||||||
|
document.body.removeChild(dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFile = () => {
|
||||||
|
fileUpload.value.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文件上传之前判断
|
||||||
|
* @param file 上传的文件
|
||||||
|
* */
|
||||||
|
const beforeExcelUpload = (file: UploadRawFile) => {
|
||||||
|
uploadResult.value=''
|
||||||
|
if(!['xls', 'xlsx'].includes(file.name.split('.').pop())){
|
||||||
|
ElMessage.warning('上传文件只能是 xls / xlsx 格式' )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(props.uploadMaxSize>0&&file.size>(props.uploadMaxSize*1024*1024)){
|
||||||
|
ElMessage.warning('上传文件大小不能超过'+props.uploadMaxSize+'MB!')
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const uploadResult = ref('')
|
||||||
|
|
||||||
|
const emit = defineEmits(['updateCheck','resultHandleEvent'])
|
||||||
|
const excelUploadSuccess = (response: any) => {
|
||||||
|
const { message,status,data } = response;
|
||||||
|
if(200===response.status){
|
||||||
|
if(props.customResultHandle){
|
||||||
|
dialogVisible.value=false
|
||||||
|
emit('resultHandleEvent', data)
|
||||||
|
}else{
|
||||||
|
if(data.handleSuccess){
|
||||||
|
uploadResult.value=data
|
||||||
|
}else{
|
||||||
|
ElMessage.error("Excel文件处理失败:"+data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (status === 401) {
|
||||||
|
ElMessage.error('未通过身份验证');
|
||||||
|
}
|
||||||
|
const temp = String(status)[0]
|
||||||
|
if (temp === '9') {
|
||||||
|
ElMessage.error(message || '系统出错');
|
||||||
|
console.error('系统异常', response.data);
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
ElMessage.error('系统异常');
|
||||||
|
console.error('系统异常', response.data);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传错误提示
|
||||||
|
const excelUploadError = (response: any) => {
|
||||||
|
console.error(response);
|
||||||
|
if (response.status === 401) {
|
||||||
|
ElMessage.error('未通过身份验证');
|
||||||
|
}else{
|
||||||
|
ElMessage.error('文件上传服务异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
dialogVisible
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
uploadResult.value=''
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- 底部浮动-->
|
||||||
|
<template >
|
||||||
|
<div ref="bfRootRef" :key="isFloat? 'bottom-float':'bottom-fix'" :class="[isFloat ? 'bottom-float':'bottom-fix']" >
|
||||||
|
<slot v-if="$slots['content']" name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const bfRootRef= ref()
|
||||||
|
//组件传参
|
||||||
|
let props = defineProps({
|
||||||
|
height: { type: String,default:'50px' },//浮动层高度
|
||||||
|
backgroundColor: { type: String,default:'#fff' },//浮动层背景色
|
||||||
|
getWidthEvent: { type: Function },//获取浮动层宽度
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFloat=ref(false)
|
||||||
|
const width=ref('')
|
||||||
|
const initScrollHeight=ref(0)
|
||||||
|
const initClientHeight=ref(0)
|
||||||
|
const handleScroll = () => {
|
||||||
|
let clientHeight=document.documentElement.clientHeight || window.innerHeight
|
||||||
|
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
|
||||||
|
|
||||||
|
let temp=(scrollTop + clientHeight+bfRootRef.value.offsetHeight)
|
||||||
|
isFloat.value=(temp >= initScrollHeight.value)?false:true
|
||||||
|
}
|
||||||
|
const windowChangesListener = () => {
|
||||||
|
width.value=props.getWidthEvent()+'px'
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
width.value=props.getWidthEvent()+'px'
|
||||||
|
initScrollHeight.value=document.documentElement.scrollHeight || document.body.scrollHeight
|
||||||
|
initClientHeight.value=document.documentElement.clientHeight || window.innerHeight
|
||||||
|
handleScroll()
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
window.addEventListener('resize', windowChangesListener)
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bottom-float{
|
||||||
|
position: fixed;
|
||||||
|
height: v-bind('props.height');
|
||||||
|
width: v-bind('width');
|
||||||
|
bottom: 0vh;
|
||||||
|
background-color: v-bind('props.backgroundColor');
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-fix{
|
||||||
|
height: v-bind('props.height');
|
||||||
|
width: v-bind('width');
|
||||||
|
background-color: v-bind('props.backgroundColor');
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!--日志显示组件-->
|
||||||
|
<template>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12"><el-text tag="b" size="large">变更前内容</el-text></el-col>
|
||||||
|
<el-col :span="12"><el-text tag="b" size="large">变更后内容</el-text></el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row style="height: 90%;margin-top: 10px;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="logContent">
|
||||||
|
<!-- 优先插槽 -->
|
||||||
|
<slot v-if="$slots['beforeContent']" name="beforeContent"></slot>
|
||||||
|
<!-- 优先处理JSON格式 -->
|
||||||
|
<pre v-else-if="isJsonString(props.beforeContent)">{{ JSON.parse(props.beforeContent)}}</pre>
|
||||||
|
<!-- 默认输出 -->
|
||||||
|
<el-text v-else="props.beforeContent">{{ props.beforeContent }}</el-text>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="logContent">
|
||||||
|
<!-- 优先插槽 -->
|
||||||
|
<slot v-if="$slots['content']" name="content"></slot>
|
||||||
|
<!-- 优先处理JSON格式 -->
|
||||||
|
<pre v-else-if="isJsonString(props.content)">{{ JSON.parse(props.content)}}</pre>
|
||||||
|
<!-- 默认输出 -->
|
||||||
|
<el-text v-else="props.content">{{ props.content }}</el-text>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { isJsonString } from '@/framework/utils/standard/dataHandleUtil.ts'
|
||||||
|
|
||||||
|
let props = defineProps({
|
||||||
|
//变更前内容
|
||||||
|
beforeContent: { type: String, default: '', },
|
||||||
|
//变更后内容
|
||||||
|
content: { type: String, default: '', },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logContent{
|
||||||
|
border:1px solid;
|
||||||
|
border-color: #707175;
|
||||||
|
border-radius:5px;
|
||||||
|
height: 100%;
|
||||||
|
width: 90%;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
|
||||||
|
<!-- 实时搜索下拉框组件 -->
|
||||||
|
<template>
|
||||||
|
<el-select v-model="value" remote :placeholder="placeholder" style="width: 100%;" filterable :remote-method="remoteMethod" :loading="loading" clearable
|
||||||
|
:multiple="multiple" :collapse-tags="multiple" :collapse-tags-tooltip="multiple">
|
||||||
|
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
import { handleSelectData } from '@/framework/utils/standard/dataHandleUtil.ts'
|
||||||
|
/**
|
||||||
|
* 实时检索组件
|
||||||
|
* SearchSelect
|
||||||
|
* @author nielang
|
||||||
|
* @description 用于实时检索的下拉框
|
||||||
|
* @property {String} modelValue 使用v-model
|
||||||
|
* @property {String} placeholder input的placeholder
|
||||||
|
* @property {Boolean} multiple 是否支持多选
|
||||||
|
* @property {Function} request 接口请求函数,默认请求参数{'keyword':'关键字'}
|
||||||
|
* @property {Function} customRequestHandle 自定义请求参数处理
|
||||||
|
* @property {Function} customResponseHandle 自定义响应结果处理
|
||||||
|
* @property {Boolean} showOptionValue 选项中是否显示value属性,使用customResponseHandle后,该参数失效
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue:{
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "请输入关键字",
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
request:{
|
||||||
|
type: Function,
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
customRequestHandle: {
|
||||||
|
type: Function,
|
||||||
|
require: false
|
||||||
|
},
|
||||||
|
customResponseHandle: {
|
||||||
|
type: Function,
|
||||||
|
require: false
|
||||||
|
},
|
||||||
|
showOptionValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
interface ListItem {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = ref<ListItem[]>([])
|
||||||
|
const remoteMethod = (keyword: string) => {
|
||||||
|
if (keyword) {
|
||||||
|
loading.value = true
|
||||||
|
if (props.request) {
|
||||||
|
let params:any = {keyword:keyword}
|
||||||
|
//自定义请求参数处理
|
||||||
|
if(props.customRequestHandle)
|
||||||
|
params=props.customRequestHandle(params)
|
||||||
|
props.request(params).then((data: any) => {
|
||||||
|
//自定义响应结果处理
|
||||||
|
if(props.customRequestHandle){
|
||||||
|
options.value=props.customResponseHandle(data)
|
||||||
|
}else{
|
||||||
|
options.value=handleSelectData(data,props.showOptionValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
} else {
|
||||||
|
options.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!--脱敏文本展示组件-->
|
||||||
|
<template>
|
||||||
|
<span :class="props.underline?'underline':''">
|
||||||
|
<slot v-if="$slots.default"></slot>
|
||||||
|
<template v-else-if="props.data" >
|
||||||
|
<el-tooltip :content="props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data" placement="bottom" effect="light">
|
||||||
|
{{ desensitize(props.aesDecrypt?decrypt(props.data,props.decryptKey):props.data,props.type,props.custEvent) }}
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-else> </template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { decrypt,desensitize,DesensitizationType } from '@/framework/utils/standard/security.ts'
|
||||||
|
|
||||||
|
let props = defineProps({
|
||||||
|
data: { type: String, default: '', },//展示数据
|
||||||
|
aesDecrypt:{type:Boolean,default: true},//是否aes解密
|
||||||
|
decryptKey:{type:String,default: ''},//自定义密钥
|
||||||
|
underline:{type:Boolean,default: false},//是否需要下划线
|
||||||
|
type:DesensitizationType,//脱敏类型
|
||||||
|
custEvent:{type: Function},//自定义脱敏方法
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.underline {
|
||||||
|
border-bottom: 1px solid #CDD0D6;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!--文本展示组件-->
|
||||||
|
<template>
|
||||||
|
<span style="border-bottom: 1px solid #CDD0D6;width: 100%;display: block;">
|
||||||
|
<slot v-if="$slots.default"></slot>
|
||||||
|
<template v-else-if="props.data" >{{ props.data }}</template>
|
||||||
|
<template v-else> </template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
let props = defineProps({
|
||||||
|
data: { type: String, default: '', },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 防抖:在连续的操作中,无论进行了多长时间,只有某一次的操作后在指定的时间内没有再操作,这一次才被判定有效。
|
||||||
|
* 使用:
|
||||||
|
* <el-button v-debounce:click.delay[2000]="testDebounce">按钮防抖</el-button>
|
||||||
|
* <el-button v-debounce:click.delay[2000]="()=>testDebounce('debounce')">按钮防抖传参</el-button>
|
||||||
|
*/
|
||||||
|
import { ObjectDirective } from 'vue'
|
||||||
|
interface DebounceEl extends HTMLElement {
|
||||||
|
debounceEvent: () => void
|
||||||
|
}
|
||||||
|
// 防抖指令封装
|
||||||
|
const debounceDirective: ObjectDirective = {
|
||||||
|
beforeMount(el: DebounceEl, binding: any) {
|
||||||
|
let timer: number
|
||||||
|
let delay: number = 1000
|
||||||
|
const keys = Object.keys(binding.modifiers)
|
||||||
|
if (keys) {
|
||||||
|
const key: string | undefined = keys.find((key) => key.includes('delay'))?.toString()
|
||||||
|
const value = (key ?? '').match(/(?<=\[).*?(?=\])/)
|
||||||
|
if (value) delay = parseInt(value[0])
|
||||||
|
}
|
||||||
|
el.debounceEvent = () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
binding.value()
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
el.addEventListener(binding.arg, el.debounceEvent)
|
||||||
|
},
|
||||||
|
beforeUnmount(el: DebounceEl, binding: any) {
|
||||||
|
el.removeEventListener(binding.arg, el.debounceEvent)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default debounceDirective
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { App, Directive } from "vue";
|
||||||
|
import debounce from "./debounce";
|
||||||
|
import throttle from "./throttle";
|
||||||
|
|
||||||
|
const directivesList: { [key: string]: Directive } = {
|
||||||
|
debounce,
|
||||||
|
throttle,
|
||||||
|
};
|
||||||
|
|
||||||
|
const directives = {
|
||||||
|
install: function (app: App<Element>) {
|
||||||
|
Object.keys(directivesList).forEach(key => {
|
||||||
|
app.directive(key, directivesList[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default directives;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
*节流:规定时间内,只触发一次。比如我们设定500ms,在这个时间内,无论点击按钮多少次,它都只会触发一次。
|
||||||
|
* 使用:
|
||||||
|
* <el-button v-throttle:click.delay[2000]="testThrottle">按钮节流</el-button>
|
||||||
|
* el-button v-throttle:click.delay[2000]="()=>testThrottle('throttle')">按钮节流传参</el-button>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import { ObjectDirective } from 'vue'
|
||||||
|
|
||||||
|
interface ThrottleEl extends HTMLElement {
|
||||||
|
throttleEvent: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节流指令封装
|
||||||
|
const throttleDirective: ObjectDirective = {
|
||||||
|
beforeMount(el: ThrottleEl, binding: any) {
|
||||||
|
let throttled = false
|
||||||
|
let delay: number = 1000
|
||||||
|
const keys = Object.keys(binding.modifiers)
|
||||||
|
if (keys) {
|
||||||
|
const key: string | undefined = keys.find((key) => key.includes('delay'))?.toString()
|
||||||
|
const value = (key ?? '').match(/(?<=\[).*?(?=\])/)
|
||||||
|
if (value) delay = parseInt(value[0])
|
||||||
|
}
|
||||||
|
el.throttleEvent = () => {
|
||||||
|
if (!throttled) {
|
||||||
|
binding.value()
|
||||||
|
throttled = true
|
||||||
|
setTimeout(() => {
|
||||||
|
throttled = false
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el.addEventListener(binding.arg, el.throttleEvent)
|
||||||
|
},
|
||||||
|
beforeUnmount(el: ThrottleEl, binding: any) {
|
||||||
|
el.removeEventListener(binding.arg, el.throttleEvent)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default throttleDirective
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
const { copy } = useClipboard({ legacy: true });
|
||||||
|
|
||||||
|
export function useCopy(data: string){
|
||||||
|
copy(data)
|
||||||
|
ElMessage({type: 'success',message: '复制成功' })
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 跨页全选
|
||||||
|
* @param multipleTableRef 多选的表格
|
||||||
|
* @param showData 显示的数据
|
||||||
|
* @param idKey 选中的id名称
|
||||||
|
* @param tableFilterData 过滤掉不能选择数据后的全部数据,如果没有不能选中的数据,则为全部数据
|
||||||
|
* @returns checkAll: 跨页全选状态
|
||||||
|
* checkedIds 选中的所有行的id
|
||||||
|
* initTableRowCheckStatus 初始化选中行
|
||||||
|
* handlMultipageSelect 跨页全选状态操作
|
||||||
|
* handleRowSelect 操作行选中
|
||||||
|
* handleAllSelect 操作当前页选中
|
||||||
|
*/
|
||||||
|
export function useCrossPageSelect(multipleTableRef: any, showData: any, idKey: string, tableFilterData: any) {
|
||||||
|
|
||||||
|
const checkAll = ref(false) // 跨页全选转态
|
||||||
|
const checkedIds = ref([]) // 选中的ids
|
||||||
|
|
||||||
|
// 初始化行选中状态
|
||||||
|
const initTableRowCheckStatus = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
showData.value.forEach((item: any) => {
|
||||||
|
if (checkedIds.value.includes(item[idKey])) {
|
||||||
|
multipleTableRef.value!.toggleRowSelection(item, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
multipleTableRef.value!.toggleRowSelection(item, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => showData.value, () => {
|
||||||
|
initTableRowCheckStatus()
|
||||||
|
})
|
||||||
|
watch([checkedIds, tableFilterData], ([ids, tableData]) => {
|
||||||
|
ids.length === tableData.length ? (checkAll.value = true) : (checkAll.value = false)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 跨页全选
|
||||||
|
const handlMultipageSelect = (status: boolean) => {
|
||||||
|
if (status) {
|
||||||
|
checkedIds.value = tableFilterData.value.map((item: any) => item[idKey])
|
||||||
|
// checkedIds.value = tableData.value.filter(val => val.channel == "渠道销售").map(item => item.icapplynum)
|
||||||
|
} else {
|
||||||
|
checkedIds.value = []
|
||||||
|
}
|
||||||
|
initTableRowCheckStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换选中行
|
||||||
|
const handleRowSelect = (selection: any, row: any) => {
|
||||||
|
const id = row[idKey]
|
||||||
|
const item = selection.find((item: any) => item[idKey] === id)
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
if (!checkedIds.value.includes(id)) {
|
||||||
|
checkedIds.value.push(id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = checkedIds.value.indexOf(id)
|
||||||
|
if (index !== -1) {
|
||||||
|
checkedIds.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
checkAll.value = false
|
||||||
|
}
|
||||||
|
// 单页全选
|
||||||
|
const handleAllSelect = (selection: any) => {
|
||||||
|
if (selection.length > 0) {
|
||||||
|
selection.forEach((item: any) => {
|
||||||
|
if (!checkedIds.value.includes(item[idKey])) {
|
||||||
|
checkedIds.value.push(item[idKey])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showData.value.forEach((item: any) => {
|
||||||
|
const index = checkedIds.value.indexOf(item[idKey])
|
||||||
|
if (index != -1) {
|
||||||
|
checkedIds.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
checkAll.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return { checkAll, checkedIds, initTableRowCheckStatus, handlMultipageSelect, handleRowSelect, handleAllSelect }
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
import XEUtils from 'xe-utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表内检索,用于假分页时进行表内搜索
|
||||||
|
* @param tableData 全部数据
|
||||||
|
* @param keys 需要检索的key
|
||||||
|
* @returns 检索后的数据,检索关键词,总数,分页对象
|
||||||
|
*/
|
||||||
|
export function useInTableSearchAndPaging(tableData: any, keys: Array<string> = []){
|
||||||
|
|
||||||
|
const tableKeyword = ref('')
|
||||||
|
const debounceTableKeyword = refDebounced(tableKeyword, 500)
|
||||||
|
const total = ref(0);
|
||||||
|
const paging = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
const showData = computed(()=>{
|
||||||
|
const filterData = tableData.value.filter((item: any) => {
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const values = []
|
||||||
|
for (const key in item) {
|
||||||
|
if (keys.includes(key)) {
|
||||||
|
values.push(item[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values.some(val => String(val).includes(debounceTableKeyword.value))
|
||||||
|
}
|
||||||
|
return Object.values(item).some(val => String(val).includes(debounceTableKeyword.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = filterData.slice((paging.value.page - 1) * paging.value.pageSize, (paging.value.page - 1) * paging.value.pageSize + paging.value.pageSize)
|
||||||
|
total.value = filterData.length
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
return {tableKeyword, showData, total, paging}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VxeGrid表内检索,用于真分页时进行表内搜索
|
||||||
|
* @param filterName 关键字对象
|
||||||
|
* @param grid 表格对象
|
||||||
|
* @param tableData 全部数据对象
|
||||||
|
* @param searchProps 需要检索的key
|
||||||
|
* @param options 选项,{children: 'children'}是否作为子节点展示
|
||||||
|
* @param allTreeExpand 是否展开树形结构子节点
|
||||||
|
*/
|
||||||
|
export function useInVxeTableSearch(filterName:any, grid:any, tableData: any, searchProps: Array<string> = [],options:any,allTreeExpand:boolean=false){
|
||||||
|
const filterVal = XEUtils.toValueString(filterName.value).trim().toLowerCase()
|
||||||
|
const $table = grid.value
|
||||||
|
if(!$table)
|
||||||
|
return false
|
||||||
|
if (filterVal) {
|
||||||
|
if(searchProps.length==0){
|
||||||
|
searchProps=Object.keys(tableData.value[0])
|
||||||
|
}
|
||||||
|
const rest = XEUtils.searchTree(tableData.value, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||||
|
grid.value.loadData(rest)
|
||||||
|
if(allTreeExpand){
|
||||||
|
// 搜索之后默认展开所有子节点
|
||||||
|
nextTick(() => {
|
||||||
|
$table.setAllTreeExpand(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
$table.loadData(tableData.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VxeGrid表内检索,用于真分页时进行表内搜索
|
||||||
|
* @param filterName 关键字对象
|
||||||
|
* @param grid 表格对象
|
||||||
|
* @param tableData 全部数据对象
|
||||||
|
* @param searchProps 需要检索的key
|
||||||
|
* @param options 选项,{children: 'children'}是否作为子节点展示
|
||||||
|
* @param allTreeExpand 是否展开树形结构子节点
|
||||||
|
*/
|
||||||
|
export function useInVxeTableSearchAndPageing(filterName:any, grid:any, tableData: any, searchProps: Array<string> = [],options:any,allTreeExpand:boolean=false,
|
||||||
|
page:number,size:number){
|
||||||
|
const filterVal = XEUtils.toValueString(filterName.value).trim().toLowerCase()
|
||||||
|
const $table = grid.value
|
||||||
|
if(!$table)
|
||||||
|
return false
|
||||||
|
if (filterVal) {
|
||||||
|
if(searchProps.length==0){
|
||||||
|
searchProps=Object.keys(tableData.value[0])
|
||||||
|
}
|
||||||
|
const rest = XEUtils.searchTree(tableData.value, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||||
|
grid.value.loadData(rest.slice((page - 1) * size, page * size))//重新加载表格数据
|
||||||
|
if(allTreeExpand){
|
||||||
|
// 搜索之后默认展开所有子节点
|
||||||
|
nextTick(() => {
|
||||||
|
$table.setAllTreeExpand(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
grid.value.loadData(tableData.value.slice((page - 1) * size, page * size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTableDataPageing(tableData: any,page:number,size:number){
|
||||||
|
return tableData.slice((page - 1) * size, page * size)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { h } from 'vue'
|
||||||
|
|
||||||
|
export function useRenderHeader(node: any) {
|
||||||
|
const { column } = node
|
||||||
|
let realWidth = 0;
|
||||||
|
let span = document.createElement('span');
|
||||||
|
span.innerText = column.label;
|
||||||
|
document.body.appendChild(span);
|
||||||
|
realWidth = span.getBoundingClientRect().width;
|
||||||
|
column.minWidth = realWidth+16; // 可能还有边距/边框等值,需要根据实际情况加上
|
||||||
|
document.body.removeChild(span);
|
||||||
|
|
||||||
|
return h('span', column.label)
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import XEUtils from 'xe-utils'
|
||||||
|
|
||||||
|
|
||||||
|
/**表格相关数据查询重置 */
|
||||||
|
export async function useTableQueryReload(tableRef:any){
|
||||||
|
const params = tableRef.params
|
||||||
|
if(params&¶ms.tableDataCache) params.tableDataCache=[] //重置表格数据缓存
|
||||||
|
if(params&¶ms.filterNameRef) params.filterNameRef='' //重置表内搜索内容
|
||||||
|
if(!XEUtils.isUndefined(tableRef.proxyConfig)){
|
||||||
|
tableRef.commitProxy('reload') //重置表格相关数据
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vxe代理请求参数处理
|
||||||
|
* @param tableData 表所有数据
|
||||||
|
* @param page 第几页
|
||||||
|
* @param size 每页多少条
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useQueryParamsHandle(queryFrom:{},page: any, sorts:any, filters:any){
|
||||||
|
const queryParams: any = Object.assign({}, queryFrom)
|
||||||
|
// 处理分页条件
|
||||||
|
if (page) {
|
||||||
|
queryParams.page = page.currentPage
|
||||||
|
queryParams.size = page.pageSize
|
||||||
|
}
|
||||||
|
// 处理排序条件
|
||||||
|
const firstSort = sorts[0]
|
||||||
|
if (firstSort) {
|
||||||
|
queryParams.sort = firstSort.field
|
||||||
|
queryParams.order = firstSort.order
|
||||||
|
}
|
||||||
|
// 处理筛选条件
|
||||||
|
filters.forEach(({ field, values }) => {
|
||||||
|
queryParams[field] = values.join(',')
|
||||||
|
})
|
||||||
|
return queryParams
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据过滤
|
||||||
|
* @param $table 表格引用
|
||||||
|
* @param tableData 需要过滤的数据
|
||||||
|
* @param filterEvent 过滤成功事件
|
||||||
|
* @param resetEvent 重置过滤数据事件
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function dataFilter($table:any,tableData:[],filterEvent:undefined,resetEvent:undefined){
|
||||||
|
let tableParams=$table.params
|
||||||
|
const filterVal = XEUtils.toValueString(tableParams.filterNameRef).trim().toLowerCase()
|
||||||
|
if (filterVal) {
|
||||||
|
let searchProps=tableParams.searchProps
|
||||||
|
/**如需要检索的key不存在,则从表格对象中获取全部列字段名 */
|
||||||
|
if(XEUtils.isUndefined(searchProps)||searchProps.length==0){
|
||||||
|
searchProps=$table.getTableColumn().visibleColumn.map(function(vcolumn) {
|
||||||
|
return vcolumn.field;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let options={children: (XEUtils.isUndefined(tableParams.children)||!tableParams.children)?'no':''}
|
||||||
|
const filterData = XEUtils.searchTree(tableData, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||||
|
if(!XEUtils.isUndefined(filterEvent)&&'function'===typeof filterEvent){
|
||||||
|
filterEvent(filterData)
|
||||||
|
}
|
||||||
|
return filterData
|
||||||
|
}else {
|
||||||
|
if(!XEUtils.isUndefined(resetEvent)&&'function'===typeof resetEvent){
|
||||||
|
resetEvent(tableData)
|
||||||
|
}
|
||||||
|
return tableData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VxeTable表内检索
|
||||||
|
* @param tableRef 表格对象
|
||||||
|
*/
|
||||||
|
export function useInVxeTableSearch($table:any){
|
||||||
|
let tableParams=$table.params
|
||||||
|
const filterVal = XEUtils.toValueString(tableParams.filterNameRef).trim().toLowerCase()
|
||||||
|
if (filterVal) {
|
||||||
|
let searchProps=tableParams.searchProps
|
||||||
|
/**如需要检索的key不存在,则从表格对象中获取全部列字段名 */
|
||||||
|
if(XEUtils.isUndefined(searchProps)||searchProps.length==0){
|
||||||
|
searchProps=$table.getTableColumn().visibleColumn.map(function(vcolumn) {
|
||||||
|
return vcolumn.field;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let options={children: (XEUtils.isUndefined(tableParams.children)||!tableParams.children)?'no':''}
|
||||||
|
const filterData = XEUtils.searchTree(tableParams.tableDataCache, item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1), options)
|
||||||
|
return filterData
|
||||||
|
}else {
|
||||||
|
return tableParams.tableDataCache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VxeGrid表内检索
|
||||||
|
* 服务端分页(params.clientPage不配置或者配置为false),不会修改当前分页数据
|
||||||
|
* @param tableRef 表格对象
|
||||||
|
*/
|
||||||
|
export function useInVxeGridSearch(tableRef:any){
|
||||||
|
const $table = tableRef
|
||||||
|
|
||||||
|
if(!$table) {
|
||||||
|
console.error('表格对象tableRef不存在');
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(!$table.params) {
|
||||||
|
console.error('自定义参数配置params不存在');
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let tableParams=$table.params
|
||||||
|
/**客户端分页并且使用了代理配置,直接重新加载表格,通过useQueryAndClientPaging方法执行筛选操作 */
|
||||||
|
if(tableParams.clientPage&&!XEUtils.isUndefined($table.proxyConfig)){
|
||||||
|
$table.commitProxy('reload')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tableData=[] //默认获取表格data中信息
|
||||||
|
if(!XEUtils.isUndefined($table.data)){
|
||||||
|
$table.data.forEach(function(o) {
|
||||||
|
tableData.push(o)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!XEUtils.isUndefined($table.getProxyInfo())&&null!=$table.getProxyInfo())
|
||||||
|
tableData=$table.getProxyInfo().data //从表格代理信息中获取当前全量数据
|
||||||
|
/**如tableDataRef不存在,则从表格对象中初始化全量数据-----已从表格代理信息中获取当前全量数据,该逻辑暂不使用*/
|
||||||
|
// let tableData=tableParams.tableDataCache //待处理数据
|
||||||
|
// if(null==tableData||tableData.length==0){
|
||||||
|
// let visibleData=$table.getTableData().visibleData
|
||||||
|
// tableData=new Array()
|
||||||
|
// visibleData.forEach(function(v,k) {
|
||||||
|
// tableData.push(v)
|
||||||
|
// v.children.forEach(function(cv,ck) {
|
||||||
|
// tableData.push(cv)
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// tableParams.tableDataCache=tableData
|
||||||
|
// }
|
||||||
|
dataFilter($table,tableData,
|
||||||
|
function(filterData){
|
||||||
|
$table.loadData(filterData)
|
||||||
|
if(tableParams.allTreeExpand){
|
||||||
|
// 搜索之后默认展开所有子节点
|
||||||
|
nextTick(() => {
|
||||||
|
$table.setAllTreeExpand(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},function(resetData){
|
||||||
|
$table.loadData(resetData)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询数据,并由客户端分页
|
||||||
|
* @param tableRef 表格对象
|
||||||
|
* @param page 分页信息
|
||||||
|
* @param sorts 排序信息
|
||||||
|
* @param requestMethod 执行查询的方法
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function useQueryAndClientPaging(tableRef:any,page: any, sorts:any, requestMethod:any){
|
||||||
|
const $table = tableRef.value
|
||||||
|
if(!$table) {
|
||||||
|
console.error('表格对象tableRef不存在');
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(!$table.params) {
|
||||||
|
console.error('自定义参数配置params不存在');
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if(!$table.params.clientPage){
|
||||||
|
console.error('自定义参数配置params.clientPage应该设置为true');
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let cdata=$table.params.tableDataCache
|
||||||
|
/**缓存中不存在数据,则发起请求,并初始化缓存 */
|
||||||
|
if(cdata.length==0){
|
||||||
|
let dataPromise= await requestMethod()
|
||||||
|
$table.params.tableDataCache=dataPromise
|
||||||
|
cdata=dataPromise
|
||||||
|
}
|
||||||
|
/**排序*/
|
||||||
|
cdata=XEUtils.orderBy(cdata, sorts.map((sort:any) => [sort.field, sort.order]))
|
||||||
|
/**如表内搜索存在关键字,则首先过滤数据 */
|
||||||
|
cdata=dataFilter($table,cdata)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
total:cdata.length,
|
||||||
|
data:cdata.slice((page.currentPage - 1) * page.pageSize, page.currentPage * page.pageSize)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTagsViewStore } from "@/framework/store/modules/tagsView";
|
||||||
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="app-container" >
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<keep-alive :include="tagsViewStore.cachedViews">
|
||||||
|
<component :is="Component" :key="route.fullPath"/>
|
||||||
|
</keep-alive>
|
||||||
|
|
||||||
|
</router-view>
|
||||||
|
|
||||||
|
<el-backtop target=".app-container" :right="30" :bottom="5" :visibility-height="50" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-container {
|
||||||
|
// height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 16px;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-tags+.app-container {
|
||||||
|
// min-height: 100vh;
|
||||||
|
padding-top: 54px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 菜单列表-->
|
||||||
|
<el-menu :default-active="activeMenu" :router="true" mode="horizontal"
|
||||||
|
:background-color="settingsStore.currentLayoutColor.navrbarBackgroundColor"
|
||||||
|
:text-color="settingsStore.currentLayoutColor.menuFontTextColor"
|
||||||
|
:active-text-color="settingsStore.currentLayoutColor.activeMenuFontTextColor" >
|
||||||
|
<el-sub-menu v-for="item in routers" :key="item.path" :index="item.path" >
|
||||||
|
<template #title>{{ item.meta.title }}</template>
|
||||||
|
<el-menu-item v-for="twoitem in item.children" :key="twoitem.path" :index="item.path+'/'+twoitem.path">{{ twoitem.meta.title }}</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||||
|
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const routesStore = useRoutesStore();
|
||||||
|
|
||||||
|
|
||||||
|
const routers = computed(() => {
|
||||||
|
return routesStore.routes.filter((item: any) => {
|
||||||
|
return item.meta && item.meta.title && item.children && item.children.length > 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let activeMenu = ref('')
|
||||||
|
let router = useRouter()
|
||||||
|
watch(() => router,
|
||||||
|
(newValue) => {
|
||||||
|
activeMenu.value = newValue.currentRoute.value.fullPath
|
||||||
|
},
|
||||||
|
{ immediate: true,deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-menu {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
&>.el-menu-item {
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header-left">
|
||||||
|
<!-- 系统LOGO -->
|
||||||
|
<img :src="logoUrl" alt="logo" class="ab-m-r-50" v-if="settingsStore.showLogo"/>
|
||||||
|
<!-- 子系统选择功能-->
|
||||||
|
<SubSystemSwitch></SubSystemSwitch>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import logoPng from '@/assets/img/logo.png'
|
||||||
|
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||||
|
import SubSystemSwitch from "@/framework/layoutpublic/SubSystemSwitch/index.vue"
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let logoUrl = logoPng
|
||||||
|
if(''!=settingsStore.logoAddress){
|
||||||
|
logoUrl=settingsStore.logoAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.header-left{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.system-menu{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 20px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.current-menu{
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-menu{
|
||||||
|
width: 480px;
|
||||||
|
height: auto;
|
||||||
|
color: rgba(255,255,255,0.45);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
top: 45px;
|
||||||
|
.item{
|
||||||
|
width: 115px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 10px;
|
||||||
|
&:hover{
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active{
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header-right">
|
||||||
|
|
||||||
|
<!-- 搜索组件 -->
|
||||||
|
<HeaderSearch />
|
||||||
|
<!-- 信息通知组件 -->
|
||||||
|
<MessageNotify />
|
||||||
|
<!-- 登录人信息 -->
|
||||||
|
<NewUserDropDown></NewUserDropDown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderSearch from './Search.vue'
|
||||||
|
import MessageNotify from '../MessageNotify/index.vue'
|
||||||
|
import headPng from '@/assets/img/head.jpg'
|
||||||
|
import { useUserStore } from "@/framework/store/modules/user";
|
||||||
|
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||||
|
import NewUserDropDown from "@/framework/layoutpublic/NewUserDropDown/index.vue"
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
let headAddress = headPng
|
||||||
|
|
||||||
|
if(''!=settingsStore.headAddress){
|
||||||
|
headAddress=settingsStore.headAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () =>{
|
||||||
|
userStore.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-right {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
.name {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}</style>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{ 'show': show }" class="header-search">
|
||||||
|
<el-tooltip content="菜单搜索" placement="bottom" effect="light">
|
||||||
|
<el-icon @click.stop="click">
|
||||||
|
<Search />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-select ref="headerSearchSelect" v-model="search" :remote-method="querySearch" filterable default-first-option
|
||||||
|
remote placeholder="请输入菜单名称" class="header-search-select" @change="change">
|
||||||
|
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.item.title.join(' > ')" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import path from 'path-browserify';
|
||||||
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||||
|
const routesStore = useRoutesStore();
|
||||||
|
let router = useRouter()
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const options = ref([])
|
||||||
|
const searchPool = ref([])
|
||||||
|
const show = ref(false)
|
||||||
|
const fuse = ref({})
|
||||||
|
|
||||||
|
const routes = computed(() => {
|
||||||
|
return routesStore.routes.filter((item: any) => {
|
||||||
|
return item.meta && !item.meta.hidden && item.meta.title && item.children && item.children.length > 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(searchPool, (list) => {
|
||||||
|
initFuse(list)
|
||||||
|
})
|
||||||
|
watch(show, (value) => {
|
||||||
|
if (value) {
|
||||||
|
document.body.addEventListener('click', close)
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener('click', close)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const generateRoutes = (routes: any, basePath = '/', prefixTitle = []) => {
|
||||||
|
let res: any = []
|
||||||
|
|
||||||
|
for (const router of routes) {
|
||||||
|
const data:any = {
|
||||||
|
path: path.resolve(basePath, router.path),
|
||||||
|
title: [...prefixTitle]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (router.meta && router.meta.title) {
|
||||||
|
data.title = [...data.title, router.meta.title]
|
||||||
|
res.push(data)
|
||||||
|
}
|
||||||
|
if (router.children) {
|
||||||
|
const tempRoutes: any = generateRoutes(router.children, data.path, data.title)
|
||||||
|
if (tempRoutes.length >= 1) {
|
||||||
|
res = [...res, ...tempRoutes]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPool.value = generateRoutes(routes.value)
|
||||||
|
|
||||||
|
const initFuse = (list: any) => {
|
||||||
|
fuse.value = new Fuse(list, {
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.4,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
// maxPatternLength: 32,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
keys: [{
|
||||||
|
name: 'title',
|
||||||
|
weight: 0.7
|
||||||
|
}, {
|
||||||
|
name: 'path',
|
||||||
|
weight: 0.3
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerSearchSelect:any = ref(null)
|
||||||
|
|
||||||
|
const click = () => {
|
||||||
|
show.value = !show.value
|
||||||
|
if (show.value) {
|
||||||
|
headerSearchSelect.value && headerSearchSelect.value.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
|
headerSearchSelect.value && headerSearchSelect.value.blur()
|
||||||
|
options.value = []
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
const change = (val: any) => {
|
||||||
|
router .push(val.item.path)
|
||||||
|
search.value = ''
|
||||||
|
options.value = []
|
||||||
|
nextTick(() => {
|
||||||
|
show.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const querySearch = (query) => {
|
||||||
|
if (query !== '') {
|
||||||
|
options.value = fuse.value.search(query)
|
||||||
|
} else {
|
||||||
|
options.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header-search {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-right: 32px;
|
||||||
|
margin-top: -2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search-select {
|
||||||
|
font-size: 18px;
|
||||||
|
transition: width 0.2s;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
.header-search-select {
|
||||||
|
width: 210px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header-container">
|
||||||
|
<HeaderLeft />
|
||||||
|
<div class="center">
|
||||||
|
<HeaderCenter />
|
||||||
|
</div>
|
||||||
|
<HeaderRight />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderLeft from "./Left.vue"
|
||||||
|
import HeaderRight from "./Right.vue"
|
||||||
|
import HeaderCenter from "./Center.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.header-container{
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.left{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.center{
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-notify" v-if="settingsStore.showMessageNotify">
|
||||||
|
<el-badge :value="2" style="font-size: 1.3rem;">
|
||||||
|
<el-icon class="icon" @mouseenter="showNotify = true" @mouseleave="onLeave">
|
||||||
|
<Bell />
|
||||||
|
</el-icon>
|
||||||
|
</el-badge>
|
||||||
|
|
||||||
|
<div class="msg-list" v-show="showNotify" @mouseenter="onEnter" @mouseleave="showNotify = false">
|
||||||
|
<div class="ab-m-b-10">
|
||||||
|
<div class="msg-item" v-for="item in list" :key="item.id">
|
||||||
|
<div class="left">
|
||||||
|
<el-icon style="font-size: 1.3rem;">
|
||||||
|
<Bell />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div>{{ item.title }}</div>
|
||||||
|
<div class="date">{{ item.date }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="left">清空通知</div>
|
||||||
|
<div class="right">查看更多</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSettingsStore } from "@/framework/store/modules/settings";
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const showNotify = ref(false)
|
||||||
|
let timer: number = 0
|
||||||
|
const onEnter = () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
const onLeave = () => {
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
showNotify.value = false
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref([{
|
||||||
|
title: '你有十五条售后问题要处理',
|
||||||
|
date: '2023-06-30 10:21'
|
||||||
|
},{
|
||||||
|
title: '你有十五条售后问题要处理',
|
||||||
|
date: '2023-06-30 10:21'
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-notify {
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 32px;
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
.msg-list {
|
||||||
|
width: 350px;
|
||||||
|
height: auto;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
color: #333;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
top: 70px;
|
||||||
|
right: 50px;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
.msg-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
&:hover{
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
.right{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303335;
|
||||||
|
.date{
|
||||||
|
margin-top: 3px;
|
||||||
|
color: #b8bbbd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer{
|
||||||
|
display: flex;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
.left{
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
&:hover{
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.right{
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
&:hover{
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 左侧二级菜单 -->
|
||||||
|
<div class="sidebar-container" :class="{'sidebar-container-close': !appStore.sidebar.opened}">
|
||||||
|
<div class="top">
|
||||||
|
<span class="menu-name" v-show="appStore.sidebar.opened">{{ activeMenuName }}</span>
|
||||||
|
<Hamburger :is-active="appStore.sidebar.opened" @toggle-click="toggleSideBar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-menu :default-active="activePath" :router="true" background-color="#F6F8FB" text-color="#303335" :collapse="!appStore.sidebar.opened" :collapse-transition="false">
|
||||||
|
<el-menu-item v-for="item in subRoutes" :key="resolvePath(item.path)" :index="resolvePath(item.path)" >
|
||||||
|
<template v-if="!appStore.sidebar.opened">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</template>
|
||||||
|
<template #title >{{ item.meta.title }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Hamburger from "@/framework/components/Hamburger/index.vue";
|
||||||
|
import path from 'path-browserify';
|
||||||
|
import { isExternal } from '@/framework/utils/index';
|
||||||
|
import { ref, watch} from 'vue'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useRoutesStore } from "@/framework/store/modules/routes";
|
||||||
|
const routesStore = useRoutesStore();
|
||||||
|
|
||||||
|
import { useAppStore } from "@/framework/store/modules/app";
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左侧菜单栏显示/隐藏
|
||||||
|
*/
|
||||||
|
function toggleSideBar() {
|
||||||
|
appStore.toggleSidebar(!appStore.sidebar.opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
let activePath= ref('')
|
||||||
|
let activeMenuName = ref('')
|
||||||
|
let activeMenuPath = ref('')
|
||||||
|
let subRoutes:any = ref([])
|
||||||
|
|
||||||
|
|
||||||
|
let router = useRouter()
|
||||||
|
watch(
|
||||||
|
() => router,
|
||||||
|
(newValue) => {
|
||||||
|
activePath.value = newValue.currentRoute.value.fullPath
|
||||||
|
const path = newValue.currentRoute.value.matched[0].path
|
||||||
|
const activeRoute = routesStore.routes.find((item: any) => item.path === path)
|
||||||
|
if (activeRoute) {
|
||||||
|
activeMenuName.value = activeRoute.meta.title
|
||||||
|
activeMenuPath.value = activeRoute.path
|
||||||
|
subRoutes.value = activeRoute.children
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true,deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析路径
|
||||||
|
*
|
||||||
|
* @param routePath 路由路径
|
||||||
|
*/
|
||||||
|
function resolvePath(routePath: string) {
|
||||||
|
if (isExternal(routePath)) {
|
||||||
|
return routePath;
|
||||||
|
}
|
||||||
|
// 完整路径 = 父级路径(/level/level_3) + 路由路径
|
||||||
|
const fullPath = path.resolve(activeMenuPath.value, routePath); // 相对路径 → 绝对路径
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-container {
|
||||||
|
width: $sideBarWidth;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x:hidden;
|
||||||
|
background-color: #F6F8FB;
|
||||||
|
|
||||||
|
.top{
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
.menu-name{
|
||||||
|
padding-left: 16px;
|
||||||
|
color: #303335;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 3px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
:deep(.el-tooltip__trigger) {
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sidebar-container-close{
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--vertical{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||||
|
|
||||||
|
const tagAndTagSpacing = ref(4);
|
||||||
|
const { proxy } = getCurrentInstance() as any;
|
||||||
|
|
||||||
|
const emits = defineEmits(["scroll"]);
|
||||||
|
const emitScroll = () => {
|
||||||
|
emits("scroll");
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
|
||||||
|
const scrollWrapper = computed(
|
||||||
|
() => proxy?.$refs.scrollContainer.$refs.wrapRef
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollWrapper.value.addEventListener("scroll", emitScroll, true);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
scrollWrapper.value.removeEventListener("scroll", emitScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleScroll(e: WheelEvent) {
|
||||||
|
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||||
|
scrollWrapper.value.scrollLeft =
|
||||||
|
scrollWrapper.value.scrollLeft + eventDelta / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToTarget(currentTag: TagView) {
|
||||||
|
const $container = proxy.$refs.scrollContainer.$el;
|
||||||
|
const $containerWidth = $container.offsetWidth;
|
||||||
|
const $scrollWrapper = scrollWrapper.value;
|
||||||
|
|
||||||
|
let firstTag = null;
|
||||||
|
let lastTag = null;
|
||||||
|
|
||||||
|
// find first tag and last tag
|
||||||
|
if (tagsViewStore.visitedViews.length > 0) {
|
||||||
|
firstTag = tagsViewStore.visitedViews[0];
|
||||||
|
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = 0;
|
||||||
|
} else if (lastTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||||
|
} else {
|
||||||
|
const tagListDom = document.getElementsByClassName("tags-item");
|
||||||
|
const currentIndex = tagsViewStore.visitedViews.findIndex(
|
||||||
|
(item) => item === currentTag
|
||||||
|
);
|
||||||
|
let prevTag = null;
|
||||||
|
let nextTag = null;
|
||||||
|
for (const k in tagListDom) {
|
||||||
|
if (k !== "length" && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||||
|
if (
|
||||||
|
(tagListDom[k] as any).dataset.path ===
|
||||||
|
tagsViewStore.visitedViews[currentIndex - 1].path
|
||||||
|
) {
|
||||||
|
prevTag = tagListDom[k];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(tagListDom[k] as any).dataset.path ===
|
||||||
|
tagsViewStore.visitedViews[currentIndex + 1].path
|
||||||
|
) {
|
||||||
|
nextTag = tagListDom[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the tag's offsetLeft after of nextTag
|
||||||
|
const afterNextTagOffsetLeft =
|
||||||
|
(nextTag as any).offsetLeft +
|
||||||
|
(nextTag as any).offsetWidth +
|
||||||
|
tagAndTagSpacing.value;
|
||||||
|
|
||||||
|
// the tag's offsetLeft before of prevTag
|
||||||
|
const beforePrevTagOffsetLeft =
|
||||||
|
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
|
||||||
|
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||||
|
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
||||||
|
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||||
|
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
moveToTarget,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-scrollbar
|
||||||
|
ref="scrollContainer"
|
||||||
|
class="scroll-container"
|
||||||
|
:vertical="false"
|
||||||
|
@wheel.prevent="handleScroll"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.scroll-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.el-scrollbar__bar {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-scrollbar__wrap {
|
||||||
|
height: 49px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<div class="tags-container">
|
||||||
|
<scroll-pane ref="scrollPaneRef" @scroll="handleScroll">
|
||||||
|
<router-link v-for="tag in visitedViews" :key="tag.path" :class="'tags-item ' + (isActive(tag) ? 'active' : '')"
|
||||||
|
:data-path="tag.path" :to="{ path: tag.path, query: tag.query }"
|
||||||
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent="openTagMenu(tag, $event)">
|
||||||
|
{{ tag.meta?.title }}
|
||||||
|
<span v-if="!isAffix(tag)" class="tags-item-close" @click.prevent.stop="closeSelectedTag(tag)">×</span>
|
||||||
|
</router-link>
|
||||||
|
</scroll-pane>
|
||||||
|
|
||||||
|
<!-- tag标签操作菜单 -->
|
||||||
|
<ul v-show="tagMenuVisible" class="tag-menu" :style="{ left: left + 'px', top: top + 'px' }">
|
||||||
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
|
刷新
|
||||||
|
</li>
|
||||||
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
|
关闭
|
||||||
|
</li>
|
||||||
|
<li @click="closeOtherTags">
|
||||||
|
关闭其它标签页
|
||||||
|
</li>
|
||||||
|
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||||
|
关闭左侧标签页
|
||||||
|
</li>
|
||||||
|
<li v-if="!isLastView()" @click="closeRightTags">
|
||||||
|
关闭右侧标签页
|
||||||
|
</li>
|
||||||
|
<li @click="closeAllTags()">
|
||||||
|
关闭所有标签页
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
nextTick,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
} from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
|
import path from "path-browserify";
|
||||||
|
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import { usePermissionStore } from "@/framework/store/modules/permission";
|
||||||
|
import { useTagsViewStore, TagView } from "@/framework/store/modules/tagsView";
|
||||||
|
import ScrollPane from "./ScrollPane.vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
|
||||||
|
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||||
|
|
||||||
|
const selectedTag = ref({});
|
||||||
|
const scrollPaneRef = ref();
|
||||||
|
const left = ref(0);
|
||||||
|
const top = ref(0);
|
||||||
|
const affixTags = ref<TagView[]>([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
route,
|
||||||
|
() => {
|
||||||
|
addTags();
|
||||||
|
moveToCurrentTag();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//初始化立即执行
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagMenuVisible = ref(false); // 标签操作菜单显示状态
|
||||||
|
watch(tagMenuVisible, (value) => {
|
||||||
|
if (value) {
|
||||||
|
document.body.addEventListener("click", closeTagMenu);
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener("click", closeTagMenu);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function filterAffixTags(routes: any[], basePath = "/") {
|
||||||
|
let tags: TagView[] = [];
|
||||||
|
|
||||||
|
routes.forEach((route) => {
|
||||||
|
|
||||||
|
if (route.meta && route.meta.affix) {
|
||||||
|
const tagPath = path.resolve(basePath, route.path);
|
||||||
|
tags.push({
|
||||||
|
fullPath: tagPath,
|
||||||
|
path: tagPath,
|
||||||
|
name: route.name,
|
||||||
|
meta: { ...route.meta },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children) {
|
||||||
|
const childTags = filterAffixTags(route.children, route.path);
|
||||||
|
if (childTags.length >= 1) {
|
||||||
|
tags = tags.concat(childTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTags() {
|
||||||
|
const tags: TagView[] = filterAffixTags(permissionStore.routes);
|
||||||
|
affixTags.value = tags;
|
||||||
|
for (const tag of tags) {
|
||||||
|
// Must have tag name
|
||||||
|
if (tag.name) {
|
||||||
|
tagsViewStore.addVisitedView(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTags() {
|
||||||
|
if (route.name) {
|
||||||
|
tagsViewStore.addView(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToCurrentTag() {
|
||||||
|
nextTick(() => {
|
||||||
|
for (const r of tagsViewStore.visitedViews) {
|
||||||
|
if (r.path === route.path) {
|
||||||
|
scrollPaneRef.value.moveToTarget(r);
|
||||||
|
// when query is different then update
|
||||||
|
if (r.fullPath !== route.fullPath) {
|
||||||
|
tagsViewStore.updateVisitedView(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(tag: TagView) {
|
||||||
|
return tag.path === route.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAffix(tag: TagView) {
|
||||||
|
return tag.meta && tag.meta.affix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFirstView() {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
(selectedTag.value as TagView).fullPath ===
|
||||||
|
tagsViewStore.visitedViews[1].fullPath ||
|
||||||
|
(selectedTag.value as TagView).fullPath === "/index"
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLastView() {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
(selectedTag.value as TagView).fullPath ===
|
||||||
|
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSelectedTag(view: TagView) {
|
||||||
|
tagsViewStore.delCachedView(view);
|
||||||
|
const { fullPath } = view;
|
||||||
|
nextTick(() => {
|
||||||
|
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
||||||
|
console.warn(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLastView(visitedViews: TagView[]) {
|
||||||
|
const latestView = visitedViews.slice(-1)[0];
|
||||||
|
if (latestView && latestView.fullPath) {
|
||||||
|
router.push(latestView.fullPath);
|
||||||
|
} else {
|
||||||
|
router.replace({ path: "/redirect/" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSelectedTag(view: TagView) {
|
||||||
|
tagsViewStore.delView(view).then((res: any) => {
|
||||||
|
if (isActive(view)) {
|
||||||
|
toLastView(res.visitedViews);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLeftTags() {
|
||||||
|
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
|
||||||
|
if (
|
||||||
|
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||||
|
) {
|
||||||
|
toLastView(res.visitedViews);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function closeRightTags() {
|
||||||
|
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
|
||||||
|
if (
|
||||||
|
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
|
||||||
|
) {
|
||||||
|
toLastView(res.visitedViews);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOtherTags() {
|
||||||
|
router.push(selectedTag.value);
|
||||||
|
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||||
|
moveToCurrentTag();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllTags() {
|
||||||
|
tagsViewStore.delAllViews().then((res: any) => {
|
||||||
|
toLastView(res.visitedViews);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import { useAppStore } from "@/framework/store/modules/app";
|
||||||
|
const appStore = useAppStore()
|
||||||
|
function openTagMenu(tag: TagView, e: MouseEvent) {
|
||||||
|
left.value = e.clientX;
|
||||||
|
top.value = e.clientY - 40;
|
||||||
|
tagMenuVisible.value = true;
|
||||||
|
selectedTag.value = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTagMenu() {
|
||||||
|
tagMenuVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
closeTagMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initTags();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tags-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
box-shadow: 0 1px 1px var(--el-box-shadow-light);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tags-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 4px 0 0 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 5px;
|
||||||
|
content: "";
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
border-radius: 100%;
|
||||||
|
// font-size: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
transform: scale(2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgb(0 0 0 / 16%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||