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>
|
||||