chore(frontend): 落地 ADS 治理体系并收录前端底座基线代码

This commit is contained in:
zhoulei
2026-08-19 14:39:02 +08:00
parent 775e075495
commit 1f84456421
177 changed files with 18574 additions and 0 deletions
+17
View File
@@ -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 项)、hooksuseVxeTableHandle 等通用组合式函数)、layout(菜单布局)、router(静态路由)、storesettings/permission/user 等 Pinia 状态)、styles、types、utilsstandard 5 项 + request 请求管线)、views、directivesv-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. 项目启动 ChecklistDay 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 messagecommit 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:init2026-08-19Retrofit)填入:本项目为前端底座模板,治理已入位。 -->
- 当前状态:前端底座(`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()`)用于组装 commonParammenuName/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 状态(settingsbackendServices/loginTypepermission:菜单生成与过滤;usertoken/业务域)
- 不负责:页面级状态(业务自建 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 已解包 dataArrayBuffer 返回整个 response
// url 首段 = 服务别名(I3),自动携带 token/commonParamI2
// 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. 持久化与边界
- 无后端式持久化;前端持久化仅 tokenlocalStorage,由 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 / Vue2 空格缩进;单行 ≤ 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 统一弹窗提示;ArrayBufferExcel)不解包返回整个 responseHTTP 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~I3ARCHITECTURE §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 页头
AbQueryFormfieldConfigs: input/daterange/select/radio + slot 自定义)
vxe-gridgridDefaultProps + proxyConfig.ajax.queryclientPage 按数据量选)
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 / UIUXPRD、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 自身已够说明)
- 实验性探索且最终 revertcommit 即可)
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [],
"deny": []
},
"hooks": {}
}
+120
View File
@@ -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中,增加本次升级内容
+286
View File
@@ -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'
}
+56
View File
@@ -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']
}
}
+21
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+53
View File
@@ -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/',//子系统动静分离版本页面地址配置,用于兼容历史模块使用
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+24
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

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

+297
View File
@@ -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>&nbsp;</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>&nbsp;</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&&params.tableDataCache) params.tableDataCache=[] //重置表格数据缓存
if(params&&params.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>

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