Commit 088d3c61 authored by Data Governance Dev's avatar Data Governance Dev

chore: web3 项目起步(前端骨架 + 日报 skill + 后续计划)

.gitignore 新增 web3/backend/configs/llm.yaml 和 web3/data/ 排除项。
CLAUDE.md 改为「现在在 web3 开发」,web 和 web2 都列为参考。
新增 .claude/skills/write-daily-report skill(按 git log + work-logs 生成当日日报,3-7 句纯描述性中文)。
新增 web3/ 子项目:Vue 3.5 + Vite 6 + Element Plus 2.9 + vue-router 4,端口 5175。
含 DataQualityView / TaskConfigView / TaskModal(三 Tab)/ RuleModal / FieldConfigDrawer。
当前用静态 mock(src/data/mockData.js),未接后端;npm run dev 在 5175 验证通过。
新增 web3/plan.md:后端 FastAPI + SQLite、进程内 JobManager 异步(不用 Redis/Celery)、
LLM 从 web1 core/llm.py 直接拷贝自包含;列了 10 个开发阶段。
parent d0f90abe
---
name: write-daily-report
description: 根据 git log + 本地 work-logs 自动生成当日日报,输出若干句纯描述性语句(每句独立一行),不包含评价、结论或下一步建议。适用于"写日报/总结今天/总结今天的工作"等请求。
metadata:
argument-hint: "[日期 YYYY-MM-DD,默认今天]"
---
# write-daily-report —— 生成当日日报
## 何时使用
- 用户说「写日报」「总结今天」「总结今天的工作」「daily report」
- 用户想回顾某一天(可选日期参数)的产出
- 一天结束时快速留个工作小结
## 数据来源
| 来源 | 路径 | 用途 |
|---|---|---|
| Git log | 项目根目录 `git log` | 当日 commit 列表(commit message 通常已含改动摘要) |
| 工作日志 | `work-logs/YYYY-MM-DD.md` | 当日的详细叙述(需求/改动/踩坑),内容远比 commit message 丰富 |
| Git status | `git status --short` | 列出未跟踪/未提交文件(如临时脚本、报告产物),不入库但算当日产出 |
## 工作流程
### Step 1:确定日期
- 默认今天(`/currentDate` 上下文)
- 用户传参时使用参数日期(`YYYY-MM-DD`)
### Step 2:读取数据源
按优先级读取:
1. `work-logs/<日期>.md` —— **主要叙事来源**
2. `git log --since="<日期> 00:00" --until="<日期+1> 00:00" --pretty=format:"%h %s"` —— **commit 列表**(验证/补充工作日志)
3. `git status --short` —— **未入库产物**(可选,是否纳入由内容判断)
如果工作日志不存在或当日无 commit,礼貌告知用户"没找到 X 日的数据"。
### Step 3:提炼主题线
从工作日志的「主题 N」标题和 commit message 中归纳主线,例如:
- 主线(如:「web2 项目从零搭建」)
- 主要模块(如:「数据库连接层、身份证治理工具」)
- 重要 bug 修复
- 次要产出(UI 调整、skill 新增等)
### Step 4:撰写输出
输出要求:
- **句子数**:3 ~ 7 句
- **语句简洁**:每句尽量短,能合并的信息合并,避免长定语堆叠
- **省略技术细节**:端口号、文件路径、commit hash、版本号、阈值参数、接口路径等不写入正文 —— 这些看 commit 和日志即可,日报只保留"做了什么"
- **少用英文**:以中文叙述为主,仅在指代项目代号、技术专有名词等无法替换的内容时保留英文(如 `web2` / `FastAPI`)
- **语言风格**:纯描述性("完成/实现/修复/调整"等动词),不含评价词("高效/顺利/不错")
- **不写**:不写"下一步"、不写"问题"清单、不写情绪、不写耗时统计
- **格式**:**每句独立一行**,不写 markdown 列表、不加 bullet
- **不写日期标题**:除非用户明确要"日报"格式,否则直接输出句子
示例输出:
```
在 web2 下从零搭建前端与 FastAPI 后端骨架。
完成数据库连接弹窗与界面布局调整。
实现"身份证合规验证"菜单,含国标规则卡片与字段查询表单。
落地四步扫描流水线,配合多线程与折叠展示。
修复了若干方言兼容与日志相关 bug。
```
## 不要做的事
- ❌ 不要把 commit message 原文罗列 —— commit message 里含 scope 前缀(如 `feat(ui):`),需要提炼为自然语言
- ❌ 不要超过 7 句 —— 超过就压缩次要产出
- ❌ 不要加 emoji / 标题 / 分隔线
- ❌ 不要主动写"踩坑"列表 —— 踩坑如果已修复且影响小,并入"修复了 X bug"即可
- ❌ 不要写端口号、文件路径、commit hash、阈值参数等技术细节 —— 这些看 commit 和工作日志即可
- ❌ 不要 commit / 写文件 —— 日报是只读总结,不入库
\ No newline at end of file
......@@ -15,8 +15,13 @@ web/outputs/
web/configs/llm.yaml
web/configs/db_defaults.yaml
web2/configs/db_defaults.yaml
web3/backend/configs/llm.yaml
workflow/config.yaml
# ── web3 运行时数据 ──
web3/data/
web3/backend/logs/
# ── Python 字节码缓存 ──
**/__pycache__/
*.pyc
......
# 说明
- 这是一个数据治理工具,现在主要开发web端
- `./web/`是之前开发的工具,里面的数据链接等内容可以参考
- 现在要在`./web2`里面新开发
- `./web/` 和 `./web2/` 是之前开发的工具,里面的数据链接等内容可以参考
- 现在要在`./web3`里面新开发
# 总体要求
- 永远不要使用数据操作语句,本工具只做查询!
......
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据治理工具 - 数据质量</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
\ No newline at end of file
{
"name": "data-governance-web3",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "data-governance-web3",
"version": "0.1.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.9.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.8"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
"integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.8.0",
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
"integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^22.20 || ^24.12 || >=25"
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
"integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
"integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
"integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
"integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
"integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
"integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
"integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
"integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
"integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
"integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
"integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
"integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
"cpu": [
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
"integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
"cpu": [
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
"integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
"integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
"integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
"integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
"integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
"integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
"integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
"integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
"integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
"integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
"integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
"integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
"integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.25",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",
"integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz",
"integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.8",
"@vue/shared": "3.5.41",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz",
"integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz",
"integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.8",
"@vue/compiler-core": "3.5.41",
"@vue/compiler-dom": "3.5.41",
"@vue/compiler-ssr": "3.5.41",
"@vue/shared": "3.5.41",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.19",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz",
"integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz",
"integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz",
"integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz",
"integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.41",
"@vue/runtime-core": "3.5.41",
"@vue/shared": "3.5.41",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz",
"integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.41",
"@vue/runtime-dom": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/shared": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz",
"integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
"integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.3.0",
"@vueuse/shared": "14.3.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz",
"integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
"integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.23",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
"license": "MIT"
},
"node_modules/element-plus": {
"version": "2.14.4",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.4.tgz",
"integrity": "sha512-vMKR9tFcLeNrJgFXA3zhUn6YuRKUQW9d0btakBR8U1Iq8MfzkjMOGcgs5de2VgiLldHt69brmuBHpxc3bK4ZgQ==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.2.0",
"@element-plus/icons-vue": "^2.3.2",
"@floating-ui/dom": "^1.8.0",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
"@types/lodash": "^4.17.24",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "14.3.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.20",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"lodash-unified": "^1.0.3",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0",
"vue-component-type-helpers": "^3.3.9"
},
"peerDependencies": {
"vue": "^3.3.7"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
"integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
"@rollup/rollup-android-arm-eabi": "4.62.4",
"@rollup/rollup-android-arm64": "4.62.4",
"@rollup/rollup-darwin-arm64": "4.62.4",
"@rollup/rollup-darwin-x64": "4.62.4",
"@rollup/rollup-freebsd-arm64": "4.62.4",
"@rollup/rollup-freebsd-x64": "4.62.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
"@rollup/rollup-linux-arm-musleabihf": "4.62.4",
"@rollup/rollup-linux-arm64-gnu": "4.62.4",
"@rollup/rollup-linux-arm64-musl": "4.62.4",
"@rollup/rollup-linux-loong64-gnu": "4.62.4",
"@rollup/rollup-linux-loong64-musl": "4.62.4",
"@rollup/rollup-linux-ppc64-gnu": "4.62.4",
"@rollup/rollup-linux-ppc64-musl": "4.62.4",
"@rollup/rollup-linux-riscv64-gnu": "4.62.4",
"@rollup/rollup-linux-riscv64-musl": "4.62.4",
"@rollup/rollup-linux-s390x-gnu": "4.62.4",
"@rollup/rollup-linux-x64-gnu": "4.62.4",
"@rollup/rollup-linux-x64-musl": "4.62.4",
"@rollup/rollup-openbsd-x64": "4.62.4",
"@rollup/rollup-openharmony-arm64": "4.62.4",
"@rollup/rollup-win32-arm64-msvc": "4.62.4",
"@rollup/rollup-win32-ia32-msvc": "4.62.4",
"@rollup/rollup-win32-x64-gnu": "4.62.4",
"@rollup/rollup-win32-x64-msvc": "4.62.4",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz",
"integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.41",
"@vue/compiler-sfc": "3.5.41",
"@vue/runtime-dom": "3.5.41",
"@vue/server-renderer": "3.5.41",
"@vue/shared": "3.5.41"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-component-type-helpers": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.10.tgz",
"integrity": "sha512-t7IQivQ3oD4D01b7s7a9AWzHAcr3DGBIa/1jZREsFQJcFgSL92gqUkqNiHTYgzTt7QDuJa0I9wCeDwEtZICoBQ==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
}
}
}
{
"name": "data-governance-web3",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.9.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.5"
}
}
\ No newline at end of file
# web3 后续开发计划
> 第三版 = 前端 Vue(已搭好)+ 后端 Python(FastAPI)+ SQLite(本地)+ 进程内异步任务(参考 web2)+ LLM(从 web1 拷贝,不直接引用)
> 数据落地在 `web3/data/` 下,与代码同级目录。
> **不** 引入 Redis / Celery —— 单进程内网工具,没必要。
---
## 1. 目录结构
```
web3/
├── plan.md # 本文档
├── start.bat / start.sh # 一键启动(前端 + 后端两个窗口)
├── package.json / vite.config.js
├── index.html
│
├── src/ # 前端 Vue(同 web2 风格)
│ ├── views/ # DataQualityView / TaskConfigView(待改:改 API)
│ ├── components/ # FieldConfigDrawer / TaskModal / RuleModal
│ ├── layouts/ # MainLayout
│ ├── router/ # index.js
│ ├── data/mockData.js # 待删/弱化为空
│ ├── api/ # 【新】fetch 封装,按资源划分(不引 axios,省一个依赖)
│ ├── styles/global.css
│ └── main.js / App.vue
│
├── backend/ # 【新】后端
│ ├── start.py # 启动 FastAPI(uvicorn,端口 8767,与 web2 8766 错开)
│ ├── app.py # FastAPI 实例 + 路由挂载 + SPA fallback
│ ├── _logging.py # 复用 web2 同款日志配置(拷贝)
│ ├── config.py # 路径/DB 常量
│ ├── core/ # 自包含的核心模块
│ │ ├── llm.py # ★ 从 web/core/llm.py 拷贝,自包含
│ │ ├── job_manager.py # ★ 从 web/core/job_manager.py 拷贝并精简(去掉 governance 依赖)
│ │ └── ai_regex.py # AI 生成正则服务(包装 core/llm.py)
│ ├── db/
│ │ ├── database.py # SQLAlchemy engine / SessionLocal
│ │ ├── schema.sql # 全部表结构(首次建表用)
│ │ └── seed.py # 把当前 mockData 灌进 sqlite(首次启动)
│ ├── models/ # ORM 模型(一个表一个文件)
│ │ ├── task_group.py
│ │ ├── task.py
│ │ ├── field.py
│ │ ├── rule.py
│ │ ├── validation_run.py # 一次检测任务
│ │ └── violation.py # 不合规明细
│ ├── routers/ # 按资源分文件
│ │ ├── task_groups.py
│ │ ├── tasks.py
│ │ ├── fields.py
│ │ ├── rules.py
│ │ ├── data_sources.py # 列出 data/sources/**.sqlite 的表 / 字段 schema
│ │ ├── validations.py # 提交检测、查询进度、查结果(用 core/job_manager)
│ │ └── ai.py # AI 生成正则 + 正则测试
│ ├── services/ # 业务逻辑(路由只调这里)
│ │ ├── validator.py # 同步校验引擎(worker 调这个)
│ │ ├── data_source.py # 打开本地 .db 读 schema / 数据
│ │ └── importer.py # CSV/XLSX → 写回源 SQLite
│ └── configs/ # 后端配置
│ ├── llm.yaml # ★ 从 web/configs/llm.yaml 拷贝(删掉真实 key,只留占位)
│ └── llm.example.yaml # ★ 从 web/configs/llm.example.yaml 拷贝(多 provider 用法示例)
│
└── data/ # 【新】运行时数据(不入 git,.gitignore 加一条)
├── web3.db # 主库(任务/字段/规则/不合规记录 等业务表)
├── sources/ # 用户数据源 SQLite 文件
│ └── customer_db/
│ └── customer.sqlite # 示例:里面塞 t_user_info 表 + 几条不合规数据
└── logs/ # 复用 backend/_logging.py 写到这里
```
---
## 2. 技术选型
| 项 | 选择 | 说明 |
|---|---|---|
| 后端框架 | FastAPI | 与 web2 一致,async + 自动 docs |
| ORM | SQLAlchemy 2.x + sqlite | 简单场景不上 Alembic,自己写 schema.sql + 首次启动执行 |
| **异步任务** | **进程内 JobManager** | 参考 web2/backend/routes/idcard_scan.py:内存 dict + `ThreadPoolExecutor` + 轮询 |
| LLM 客户端 | `core/llm.py`(从 web1 拷贝,自包含) | Anthropic + OpenAI SDK,多 provider 切换 |
| 前端 HTTP | `fetch` 封装(不引 axios) | 项目里只有几个接口,省一个依赖 |
| 文件解析 | `pandas` + `openpyxl` | CSV/XLSX 导入 |
环境变量(在 `web3/backend/.env`,不提交;或临时 export):
```
WEB3_PORT=8767
WEB3_DB_PATH=../data/web3.db
WEB3_SOURCES_DIR=../data/sources
LLM_CONFIG_FILE=../configs/llm.yaml # 默认就是这个
LLM_PROVIDER= # 留空走 yaml
LLM_API_KEY= # 留空走 yaml;专用 env 也支持(MINIMAX_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)
LLM_MODEL=
LLM_BASE_URL=
```
---
## 3. SQLite Schema(核心表)
> 全部写在 [backend/db/schema.sql](backend/db/schema.sql),首次启动时若 `data/web3.db` 不存在就建表并 seed。
```sql
-- 任务分组
CREATE TABLE task_group (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL
);
-- 任务
CREATE TABLE task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
group_id INTEGER NOT NULL REFERENCES task_group(id),
data_source_path TEXT, # 例如 ../data/sources/customer_db/customer.sqlite
source_table TEXT, # 例如 t_user_info
status TEXT DEFAULT '启用', # 启用 / 停用
description TEXT,
db_type TEXT DEFAULT 'MySQL', # 仅 UI 展示;真实读本地 sqlite
conn_json TEXT, # 冗余存 UI 上的连接配置
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 字段(每个字段属于一个 task)
CREATE TABLE field (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES task(id) ON DELETE CASCADE,
field_key TEXT NOT NULL, # 英文字段名
field_cn TEXT, # 中文名
data_type TEXT, # BIGINT/VARCHAR(18) 等
show_default INTEGER DEFAULT 1, # 0/1:结果表默认展示
ord INTEGER DEFAULT 0,
UNIQUE(task_id, field_key)
);
-- 规则
CREATE TABLE rule (
id INTEGER PRIMARY KEY AUTOINCREMENT,
field_id INTEGER NOT NULL REFERENCES field(id) ON DELETE CASCADE,
desc TEXT NOT NULL, # 规则说明(自然语言)
regex TEXT, # 正则表达式(可空)
ord INTEGER DEFAULT 0
);
-- 检测运行记录(历史持久化,运行时进度走内存)
CREATE TABLE validation_run (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL REFERENCES task(id),
status TEXT DEFAULT 'pending', # pending / running / success / failed
total_rows INTEGER DEFAULT 0,
bad_rows INTEGER DEFAULT 0,
started_at TIMESTAMP,
finished_at TIMESTAMP,
error_msg TEXT
);
-- 不合规明细
CREATE TABLE violation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES validation_run(id) ON DELETE CASCADE,
row_pk TEXT, # 源表的业务主键值
payload_json TEXT, # 该行的字段快照
issues_json TEXT, # 不合规说明列表
bad_field_id INTEGER REFERENCES field(id)
);
CREATE INDEX idx_violation_run ON violation(run_id);
```
---
## 4. REST API(按资源)
前缀 `/api`,返回 JSON。
### 任务分组
- `GET /api/task-groups` — 列表
- `POST /api/task-groups` — 新建
### 任务
- `GET /api/tasks?group=&keyword=` — 列表(带筛选)
- `POST /api/tasks` — 新建(body 含基础信息 + 数据源路径 + 表名)
- `GET /api/tasks/{id}` — 详情(含 fields + rules)
- `PUT /api/tasks/{id}` — 更新
- `DELETE /api/tasks/{id}` — 删除
- `POST /api/tasks/{id}/copy` — 复制(名字加 `_副本`)
### 字段
- `GET /api/tasks/{id}/fields` — 任务的字段列表(含规则数)
- `PUT /api/tasks/{id}/fields` — 整体保存(中文名、默认展示、顺序)
- `POST /api/tasks/{id}/fields/{fid}/rules` — 新增一条规则
- `PUT /api/tasks/{id}/fields/{fid}/rules/{rid}` — 更新规则
- `DELETE /api/tasks/{id}/fields/{fid}/rules/{rid}` — 删除规则
### 数据源(本地 SQLite 文件)
- `GET /api/data-sources/files` — 列出 `data/sources/**.sqlite` 文件
- `GET /api/data-sources/tables?path=xxx` — 列该文件下的表
- `GET /api/data-sources/schema?path=xxx&table=yyy` — 读表的字段定义(用于「选数据表后自动加载字段」)
- `POST /api/data-sources/test` — 测试连接(打开文件、读 sqlite_version)
### 检测(参考 web2 `idcard_scan` 模式)
- `POST /api/tasks/{id}/run` — 提交一次检测 → 立即返回 `run_id`,后台线程跑
- `GET /api/runs/{rid}/progress` — 轮询进度:`{status, done, total, current_field, elapsed_ms, error?}`
- `GET /api/runs/{rid}/partial` — 轮询用 partial 结果(status / violations 累计 / overview)
- `GET /api/runs/{rid}/result` — 拿最终结果
- `PATCH /api/violations/{vid}` — 修单条(写回源表对应行)
- `DELETE /api/violations/{vid}` — 删单条
- `POST /api/runs/{rid}/import` — 上传 CSV/XLSX,写回源 SQLite
### AI
- `POST /api/ai/regex` — body: `{desc}` → `{regex}`(先规则库兜底,再走 LLM)
- `POST /api/regex/test` — body: `{regex, value}` → `{ok, error?}`
---
## 5. 检测执行流程(进程内 JobManager)
> 整套参考 [web2/backend/routes/idcard_scan.py](../web2/backend/routes/idcard_scan.py):
> - 全局 dict `runs: dict[run_id, RunTracker]` 存运行时进度
> - `submit()` 起一个 daemon 线程跑主流程
> - 线程里用 `ThreadPoolExecutor(max_workers)` 并发扫字段
> - 前端 1s 轮询 `/runs/{rid}/progress` 或 `/partial`
> - 完成后保留 10 分钟,再 `_evict_expired_runs()`
```
1. POST /api/tasks/{id}/run
├─ 建 validation_run (status=pending)
├─ runs[rid] = RunTracker(rid, total_fields)
└─ threading.Thread(target=_run_validation_in_thread, ...).start()
↓ 立刻返回 {run_id}
2. 前端开始轮询
GET /api/runs/{rid}/progress → status / done / total / current_field / elapsed_ms
GET /api/runs/{rid}/partial → status / violations_by_field[] / overview_partial
3. _run_validation_in_thread(rid, task):
├─ 打开 data/sources/<...>.sqlite → 读 task.source_table
├─ ThreadPoolExecutor 并发扫每个 field:
│ ├─ SELECT <col_key> FROM <table> WHERE <col_key> IS NOT NULL
│ ├─ 对每条值逐条 rule.re.search()
│ ├─ 命中的写 violation 表 + 更新 progress._done / _current_field
│ └─ 跑完一个 field → progress.push_field_done(...)
├─ 全部跑完 → progress.set_result(...) / status=success
└─ 出错 → progress.set_error(...) / status=failed + error_msg
```
### 5.1 RunTracker(精简版 JobManager)
参考 web2 `ProgressTracker`,但简化:
```python
class RunTracker:
def __init__(self, run_id, total):
self.run_id = run_id
self.total = total
self._done = 0
self._status = "running"
self._current_field = None
self._result: dict | None = None
self._error: str | None = None
self._lock = threading.Lock()
self._partial_violations: list = []
def finish_one(self): ...
def set_current_field(self, f): ...
def push_field_done(self, payload): ... # 累计 violations 给 partial
def set_result(self, result): ...
def set_error(self, msg): ...
def snapshot(self) -> dict: ...
def partial_snapshot(self) -> dict: ...
# 全局
_RUNS: dict[str, RunTracker] = {}
_RUNS_LOCK = threading.Lock()
_RUN_TTL_SECONDS = 10 * 60
```
不需要 asyncio.Queue / SSE —— 跟 web2 一样用轮询就够,避免引 aiohttp 依赖。
---
## 6. AI 生成正则
`backend/core/ai_regex.py`(**新写**,包装 core/llm.py):
```python
from .llm import get_llm_client, LLMUnavailable
# 1) 规则库(与前端 mock 同款,无 key 时也可用)
_RULE_LIB = [...] # 关键字 → 正则模板
# 2) 调用 LLM 的 prompt(只输出正则字符串)
_PROMPT = """你是正则表达式专家。规则描述:「{desc}」
请输出对应 Python 正则表达式字符串(re 语法),不要任何额外解释,不要 Markdown 代码块。
要求:
- 严格匹配(如「必须是 18 位身份证号」→ 长度恰好 18)
- 中国大陆场景(手机 11 位、邮编 6 位、日期 yyyy-MM-dd 等)
只输出一行正则字符串"""
def gen_regex(desc: str) -> tuple[str, str]:
"""返回 (regex, note);失败 note 解释原因。
调用顺序:规则库 → LLM → 兜底空串
"""
# 1. 规则库
r = _match_rule_lib(desc)
if r:
return r.regex, r.note
# 2. LLM
client = get_llm_client()
if not client.available:
return "", "LLM 未配置 API Key;规则库也无模板"
try:
regex = client.complete(_PROMPT.format(desc=desc)).strip()
# 兜底编译一次
import re; re.compile(regex)
return regex, "已用 LLM 生成"
except LLMUnavailable as e:
return "", f"LLM 调用失败:{e}"
except re.error as e:
return "", f"LLM 返回的正则编译失败:{e}"
```
### 6.1 `core/llm.py` 来源与改造
**直接拷贝** [web/core/llm.py](../web/core/llm.py) 到 `web3/backend/core/llm.py`:
- 把 `default_config_path()` 从 `web/configs/llm.yaml` 改成 `web3/backend/configs/llm.yaml`
- 其他不动,**保持自包含**(已不依赖 `web.*`,只依赖 `anthropic` / `openai` SDK)
- 删除 [web/configs/llm.yaml](../web/configs/llm.yaml) 里那个真实 `api_key`,复制时换成占位 `sk-请填入你的_MiniMax_API_Key`
- 同时复制 `llm.example.yaml`(多 provider 模板,无 key 信息)
---
## 7. 前端改造点
| 文件 | 改动 |
|---|---|
| [src/data/mockData.js](src/data/mockData.js) | 弱化为空(保留空导出),改由 [src/api/](src/api/) 提供数据 |
| [src/api/](src/api/) (新) | `client.js`(fetch 封装 + 错误处理)+ `tasks.js / fields.js / rules.js / runs.js / dataSources.js / ai.js` |
| [vite.config.js](vite.config.js) | 加 `/api` 代理到 `http://localhost:8767` |
| [src/views/DataQualityView.vue](src/views/DataQualityView.vue) | 任务下拉、不合规表、勾选删除都改 API;点查询 → `POST /api/tasks/{id}/run` → 轮询 `/partial` |
| [src/views/TaskConfigView.vue](src/views/TaskConfigView.vue) | 列表、新增/编辑/复制/删除改 API |
| [src/components/TaskModal.vue](src/components/TaskModal.vue) | Tab2 「数据连接」从「填表」改成「选择本地 .db 文件 + 选表」;Tab3 字段表自动从源表 schema 加载 |
| [src/components/RuleModal.vue](src/components/RuleModal.vue) | AI 按钮调 `/api/ai/regex`(仍带本地规则库兜底) |
| [src/components/FieldConfigDrawer.vue](src/components/FieldConfigDrawer.vue) | 字段列表走 API |
| 顶栏「检测人」 / 「时间」 | 时间改成 `new Date()` 实时格式化 |
---
## 8. 启动脚本
`web3/start.bat` 升级为开两个窗口(前端 + 后端):
```bat
start "web3-backend" cmd /k "cd web3\backend && python start.py"
start "web3-frontend" cmd /k "cd web3 && npm run dev"
```
无 Redis、无 worker。F5 重启后端 = 内存里的运行进度会丢(这是预期行为,web2 同款);历史 run / violation 数据落在 `data/web3.db` 持久化。
---
## 9. 开发阶段(建议顺序)
| 阶段 | 内容 | 验收 |
|---|---|---|
| **P1 骨架** | 建目录、requirements、FastAPI hello、vite proxy 打通 | `curl http://localhost:8767/api/health` 返回 ok |
| **P2 DB + 迁移** | schema.sql、seed.py、SQLAlchemy 模型 | 启动后 `data/web3.db` 自动生成,业务表已有 mock 数据 |
| **P3 任务 CRUD** | tasks / task-groups 路由 + 前端替换 | 任务列表/新增/编辑/删除全走 API,刷新不丢 |
| **P4 字段 + 规则** | fields / rules 路由 + 弹窗改造 | 「规则设置」可增删改;AI 调 `/api/ai/regex` |
| **P5 数据源** | data-sources 路由 + TaskModal Tab2 | 能列 `data/sources/**/*.sqlite` 的表,选完自动加载字段 |
| **P6 检测引擎(同步版)** | `services/validator.py` 同步实现 + 单表扫 | `POST /api/tasks/{id}/run` 同步返回 violations(先不异步) |
| **P7 进程内异步化** | 加 `core/job_manager.py`(精简版 JobManager)+ RunTracker + 多线程扫 | UI 看得到进度;前端 1s 轮询 |
| **P8 数据修复** | 单条编辑/删除 + CSV/XLSX 批量导入回写 | 上传文件后源表被改,重新跑检测结果变化 |
| **P9 LLM** | `core/llm.py` 拷贝完成 + `core/ai_regex.py` + `/api/ai/regex` | 无 API key 时不报错,回退规则库 |
| **P10 收尾** | seed 一个示例 `customer.sqlite`(塞几条不合规数据);写 README;`.gitignore` 加 data/ 和 backend/configs/llm.yaml | 打开 web3 → 任务配置 → 跑一次检测 → 看到真实不合规行 |
---
## 10. 与 web1 / web2 的复用清单
| 复用 | 方式 | 文件 |
|---|---|---|
| web1 LLM 客户端 | **直接拷贝** + 改一处默认配置路径 | web1 `core/llm.py` → web3 `backend/core/llm.py` |
| web1 LLM 配置文件 | **直接拷贝**(清掉真实 key) | web1 `configs/llm.yaml` → web3 `backend/configs/llm.yaml` |
| web1 LLM 示例配置 | **直接拷贝** | web1 `configs/llm.example.yaml` → web3 `backend/configs/llm.example.yaml` |
| web1 JobManager | **参考模式,不拷贝**(web1 绑死 orchestrator/governance 工作流,不可拆) | web2 idcard_scan 的 ProgressTracker 更贴合 |
| web2 异步扫描模式 | **直接参考**(用其 ProgressTracker 结构) | web2 `backend/routes/idcard_scan.py` |
| web2 日志配置 | **直接拷贝** | web2 `backend/_logging.py` → web3 `backend/_logging.py` |
| web2 SPA fallback | **直接拷贝** | web2 `backend/app.py` 的 404 handler |
> **不引 `web.*`**:web3 是独立子项目,不往 `sys.path` 塞项目根,不依赖 `web.core.*`。
> 这样以后 web1 / web2 改动不会牵连 web3。
---
## 11. 待确认 / 后续再说
- [ ] `data/sources/` 是手动放还是 UI 提供「上传 .db」入口?**先手动放**
- [ ] 检测引擎要不要支持「跨字段一致性」规则(身份证位 ↔ 性别/出生日期)?**P6 先做单字段正则,后续在 `rule` 表加 `kind` 字段扩展**
- [ ] 不合规明细的「修改」是直接写回源 SQLite 还是先到 staging 表?**先写回源表,简单**
- [ ] 是否需要「检测历史」页(多次 run 对比)?**先不做**
- [ ] 前端轮询要不要升级到 SSE?**跟 web2 一致先用轮询,简单稳定**
---
## 12. 踩坑记录(持续更新)
> 这里记录开发过程中实际遇到的坑和解决办法,每次提交追加一条。
| 日期 | 现象 | 原因 | 解决 |
|---|---|---|---|
| | | | |
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据质量</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: "宋体", SimSun, serif; font-weight: normal; }
html, body { height: 100%; }
body { background: #f0f2f5; color: #333; font-size: 14px; line-height: 1.6; overflow: hidden; }
.layout { display: flex; height: 100vh; }
/* 左侧菜单 */
.sidebar { width: 200px; background: #304156; color: #bfcbd9; display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-logo { height: 50px; display: flex; align-items: center; padding: 0 16px; color: #fff; font-size: 15px; border-bottom: 1px solid #1d2b3a; background: #2b3a4d; }
.sidebar-menu { flex: 1; overflow-y: auto; padding: 8px 0; }
.menu-group-title { padding: 10px 16px 6px; font-size: 12px; color: #6b7c93; letter-spacing: 1px; }
.menu-item { padding: 11px 16px 11px 24px; font-size: 14px; cursor: pointer; border-left: 3px solid transparent; transition: all 0.2s; color: #bfcbd9; }
.menu-item:hover { background: #263445; color: #fff; }
.menu-item.active { background: #1f2d3d; color: #409eff; border-left-color: #409eff; }
.sidebar-footer { padding: 12px 16px; border-top: 1px solid #1d2b3a; font-size: 12px; color: #6b7c93; }
/* 右侧主区 */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.main-header { height: 50px; background: #fff; border-bottom: 1px solid #e8e8e8; display: flex; align-items: center; padding: 0 20px; justify-content: space-between; }
.breadcrumb { font-size: 14px; color: #606266; }
.breadcrumb .sep { color: #c0c4cc; margin: 0 6px; }
.breadcrumb .current { color: #1f2937; }
.header-right { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #606266; }
.main-body { flex: 1; overflow: auto; padding: 16px 20px; }
.view { display: none; }
.view.active { display: block; }
/* 查询区 */
.query-card { background: #fff; border-radius: 4px; border: 1px solid #ebeef5; padding: 14px 16px; margin-bottom: 12px; }
.query-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.query-item { display: flex; align-items: center; gap: 6px; }
.query-label { font-size: 14px; color: #606266; white-space: nowrap; }
.query-label .required { color: #f56c6c; margin-right: 2px; }
.select-input { min-width: 280px; height: 32px; padding: 0 28px 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; color: #333; background: #fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path fill='%23909399' d='M6 8L1 3h10z'/></svg>") no-repeat right 10px center; appearance: none; cursor: pointer; outline: none; font-family: inherit; }
.select-input:focus { border-color: #409eff; }
.text-input { height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; color: #333; outline: none; min-width: 160px; font-family: inherit; }
.text-input:focus { border-color: #409eff; }
.btn { height: 32px; padding: 0 16px; border: 1px solid #dcdfe6; background: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; color: #333; display: inline-flex; align-items: center; gap: 4px; transition: all 0.2s; font-family: inherit; }
.btn:hover { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; }
.btn-primary { background: #409eff; color: #fff; border-color: #409eff; }
.btn-primary:hover { background: #66b1ff; border-color: #66b1ff; color: #fff; }
.btn-success { background: #67c23a; color: #fff; border-color: #67c23a; }
.btn-success:hover { background: #85ce61; border-color: #85ce61; color: #fff; }
.btn-danger { background: #f56c6c; color: #fff; border-color: #f56c6c; }
.btn-danger:hover { background: #f78989; border-color: #f78989; color: #fff; }
.btn-text { border: none; background: transparent; color: #409eff; padding: 0 4px; height: auto; font-size: 13px; cursor: pointer; font-family: inherit; }
.btn-text:hover { color: #66b1ff; background: transparent; }
.btn-text.danger:hover { color: #f56c6c; }
.query-actions { margin-left: auto; display: flex; gap: 8px; }
.task-info-bar { background: #ecf5ff; border: 1px solid #d9ecff; border-radius: 4px; padding: 8px 12px; margin-bottom: 12px; font-size: 13px; color: #409eff; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.task-info-bar .meta { color: #606266; margin-left: 12px; font-size: 12px; }
/* 结果区 */
.result-card { background: #fff; border-radius: 4px; border: 1px solid #ebeef5; overflow: hidden; }
.result-header { padding: 10px 16px; border-bottom: 1px solid #ebeef5; display: flex; align-items: center; justify-content: space-between; background: #fafafa; }
.result-title { font-size: 14px; color: #1f2937; }
.result-title .count { color: #f56c6c; margin-left: 8px; font-size: 13px; }
.result-actions { display: flex; gap: 8px; align-items: center; }
.table-scroll { overflow-x: auto; }
table.data-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 900px; }
table.data-table thead th { background: #fafafa; padding: 10px 12px; text-align: left; color: #606266; border-bottom: 1px solid #ebeef5; white-space: nowrap; position: relative; }
table.data-table thead th .cn { color: #1f2937; font-size: 13px; }
table.data-table thead th .en { color: #909399; font-size: 11px; margin-left: 4px; }
table.data-table thead th .rule-mark { color: #e6a23c; margin-left: 4px; font-size: 12px; cursor: help; }
table.data-table thead th.col-reason { background: #fde2e2; color: #c0392b; border-bottom-color: #f9c6c6; }
table.data-table thead th.col-reason .cn { color: #c0392b; }
table.data-table thead th.col-reason .flag { display: inline-block; width: 16px; height: 16px; line-height: 16px; text-align: center; background: #f56c6c; color: #fff; border-radius: 50%; font-size: 11px; margin-right: 4px; vertical-align: middle; }
table.data-table td.cell-reason { background: #fef7f7; color: #c0392b; border-left: 3px solid #f56c6c; border-bottom: 1px solid #f0f2f5; min-width: 200px; max-width: 260px; }
table.data-table td.cell-reason .issue-line { display: block; line-height: 1.7; padding-left: 4px; }
table.data-table tbody td { padding: 8px 12px; border-bottom: 1px solid #f0f2f5; color: #333; vertical-align: middle; }
table.data-table tbody tr:hover { background: #f5f7fa; }
table.data-table tbody tr:last-child td { border-bottom: none; }
.cell-error { background: #fef0f0; color: #c0392b; position: relative; cursor: help; }
.cell-error .err-tip { position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: #303030; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 10; display: none; box-shadow: 0 2px 8px rgba(0,0,0,0.2); line-height: 1.4; }
.cell-error .err-tip::after { content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: #303030; }
.cell-error:hover .err-tip { display: block; }
.cell-error .err-flag { display: inline-block; width: 14px; height: 14px; line-height: 14px; text-align: center; background: #f56c6c; color: #fff; border-radius: 50%; font-size: 10px; margin-left: 4px; vertical-align: middle; }
.null-val { color: #c0c4cc; font-style: italic; }
/* 分页 */
.pagination { padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #ebeef5; background: #fafafa; flex-wrap: wrap; gap: 8px; }
.pagination-info { font-size: 13px; color: #606266; }
.pagination-info .highlight { color: #f56c6c; }
.pagination-controls { display: flex; gap: 4px; align-items: center; }
.page-btn { min-width: 30px; height: 30px; border: 1px solid #dcdfe6; background: #fff; border-radius: 3px; cursor: pointer; font-size: 13px; color: #606266; padding: 0 8px; font-family: inherit; }
.page-btn.active { background: #409eff; color: #fff; border-color: #409eff; }
.page-btn:hover:not(.active) { color: #409eff; border-color: #c6e2ff; }
/* 字段配置抽屉 */
.drawer-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 200; display: none; }
.drawer-mask.show { display: block; }
.drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 340px; background: #fff; z-index: 201; transform: translateX(100%); transition: transform 0.3s; display: flex; flex-direction: column; }
.drawer.show { transform: translateX(0); }
.drawer-header { padding: 14px 18px; border-bottom: 1px solid #ebeef5; display: flex; justify-content: space-between; align-items: center; }
.drawer-title { font-size: 15px; color: #1f2937; }
.drawer-close { cursor: pointer; color: #909399; font-size: 20px; line-height: 1; }
.drawer-close:hover { color: #f56c6c; }
.drawer-body { flex: 1; overflow-y: auto; padding: 14px 18px; }
.drawer-hint { font-size: 12px; color: #909399; margin-bottom: 10px; line-height: 1.5; }
.field-check-list { display: flex; flex-direction: column; }
.field-check-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 4px; cursor: pointer; }
.field-check-item:hover { background: #f5f7fa; }
.field-check-item .cn { color: #1f2937; }
.field-check-item .en { color: #909399; font-size: 12px; margin-left: 4px; }
.drawer-footer { padding: 10px 18px; border-top: 1px solid #ebeef5; display: flex; gap: 8px; }
.drawer-footer .btn { flex: 1; }
.checkbox { width: 16px; height: 16px; cursor: pointer; accent-color: #409eff; }
/* ============ 任务配置视图 ============ */
.config-toolbar { background: #fff; border-radius: 4px; border: 1px solid #ebeef5; padding: 12px 16px; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; }
.config-toolbar .left { display: flex; align-items: center; gap: 12px; }
.config-toolbar .right { display: flex; gap: 8px; }
table.config-list { width: 100%; border-collapse: collapse; font-size: 13px; }
table.config-list thead th { background: #fafafa; padding: 10px 12px; text-align: left; color: #606266; border-bottom: 1px solid #ebeef5; white-space: nowrap; font-weight: normal; }
table.config-list tbody td { padding: 9px 12px; border-bottom: 1px solid #f0f2f5; color: #333; }
table.config-list tbody tr:hover { background: #f5f7fa; }
table.config-list tbody tr:last-child td { border-bottom: none; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 3px; font-size: 12px; line-height: 18px; border: 1px solid; }
.tag-blue { color: #409eff; background: #ecf5ff; border-color: #d9ecff; }
.tag-green { color: #67c23a; background: #f0f9eb; border-color: #e1f3d8; }
.tag-gray { color: #909399; background: #f4f4f5; border-color: #e9e9eb; }
.tag-orange { color: #e6a23c; background: #fdf6ec; border-color: #faecd8; }
/* 配置弹窗 */
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 300; display: none; justify-content: center; align-items: center; }
.modal-mask.show { display: flex; }
.modal { background: #fff; border-radius: 6px; width: 880px; max-width: 94vw; max-height: 90vh; display: flex; flex-direction: column; }
.modal-header { padding: 14px 20px; border-bottom: 1px solid #ebeef5; display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 15px; color: #1f2937; }
.modal-close { cursor: pointer; color: #909399; font-size: 20px; line-height: 1; }
.modal-close:hover { color: #f56c6c; }
.modal-body { flex: 1; overflow-y: auto; }
.modal-footer { padding: 12px 20px; border-top: 1px solid #ebeef5; display: flex; justify-content: flex-end; gap: 8px; }
/* 弹窗 Tab */
.modal-tabs { display: flex; border-bottom: 1px solid #ebeef5; padding: 0 20px; background: #fafafa; }
.modal-tab { padding: 12px 22px; cursor: pointer; font-size: 14px; color: #606266; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all 0.2s; }
.modal-tab:hover { color: #409eff; }
.modal-tab.active { color: #409eff; border-bottom-color: #409eff; }
.tab-panel { display: none; padding: 18px 20px; }
.tab-panel.active { display: block; }
/* 表单 */
.form-section { margin-bottom: 22px; }
.form-section-title { font-size: 14px; color: #1f2937; padding-left: 8px; border-left: 3px solid #409eff; margin-bottom: 12px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
.form-row { display: flex; align-items: center; gap: 8px; }
.form-row.full { grid-column: 1 / -1; }
.form-label { font-size: 13px; color: #606266; width: 90px; flex-shrink: 0; text-align: right; }
.form-label .required { color: #f56c6c; margin-right: 2px; }
.form-control { flex: 1; }
.form-control .select-input, .form-control .text-input { width: 100%; min-width: 0; height: 32px; }
.form-control textarea { width: 100%; min-height: 60px; padding: 6px 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 13px; font-family: inherit; resize: vertical; outline: none; }
.form-control textarea:focus { border-color: #409eff; }
.form-hint { font-size: 12px; color: #909399; margin-top: 4px; }
.conn-tabs { display: flex; border-bottom: 1px solid #ebeef5; margin-bottom: 12px; flex-wrap: wrap; }
.conn-tab { padding: 8px 16px; cursor: pointer; font-size: 13px; color: #606266; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.conn-tab.active { color: #409eff; border-bottom-color: #409eff; }
.conn-tab:hover { color: #409eff; }
/* 校验规则 Tab - 字段列表 */
.field-table { width: 100%; border-collapse: collapse; font-size: 13px; border: 1px solid #ebeef5; }
.field-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #606266; border-bottom: 1px solid #ebeef5; font-weight: normal; white-space: nowrap; }
.field-table td { padding: 8px 12px; border-bottom: 1px solid #f0f2f5; vertical-align: middle; }
.field-table tr:hover { background: #f5f7fa; }
.field-table tr:last-child td { border-bottom: none; }
.field-table .text-input { height: 30px; width: 100%; min-width: 0; font-family: inherit; }
.field-name-cell { color: #1f2937; }
.field-name-cell .en { color: #909399; font-size: 11px; margin-left: 4px; }
.rule-set-btn { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border: 1px solid #dcdfe6; background: #fff; border-radius: 4px; cursor: pointer; font-size: 12px; color: #606266; font-family: inherit; white-space: nowrap; }
.rule-set-btn:hover { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; }
.rule-set-btn .num { background: #409eff; color: #fff; border-radius: 9px; padding: 0 6px; line-height: 16px; font-size: 11px; }
.rule-set-btn.empty .num { background: #c0c4cc; }
.table-wrap { overflow-x: auto; }
/* 规则设置弹窗 */
.rule-modal { z-index: 400; }
.rule-item { border: 1px solid #ebeef5; border-radius: 6px; padding: 14px 16px; margin-bottom: 12px; background: #fafbfc; }
.rule-item-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px dashed #ebeef5; }
.rule-item-head .rnum { color: #409eff; font-size: 13px; }
.rule-item-row { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
.rule-item-row:last-child { margin-bottom: 0; }
.rule-item-label { font-size: 13px; color: #606266; width: 80px; flex-shrink: 0; text-align: right; padding-top: 6px; }
.rule-item-ctrl { flex: 1; }
.rule-item-ctrl textarea { width: 100%; min-height: 44px; padding: 6px 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 13px; font-family: inherit; resize: vertical; outline: none; }
.rule-item-ctrl textarea:focus { border-color: #409eff; }
.rule-item-ctrl .text-input { width: 100%; min-width: 0; }
.regex-line { display: flex; gap: 8px; align-items: center; }
.regex-line .text-input { flex: 1; font-family: Consolas, monospace; font-size: 12px; color: #6c5ce7; }
.test-line { display: flex; gap: 8px; align-items: center; }
.test-line .text-input { flex: 1; }
.test-result { font-size: 12px; white-space: nowrap; }
.test-result.pass { color: #67c23a; }
.test-result.fail { color: #f56c6c; }
.ai-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; border: none; }
.ai-btn:hover { background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); color: #fff; border-color: transparent; }
.ai-btn:disabled { opacity: 0.6; cursor: wait; }
.add-rule-btn { width: 100%; padding: 10px; border: 1px dashed #dcdfe6; background: #fafafa; border-radius: 4px; cursor: pointer; font-size: 13px; color: #909399; font-family: inherit; }
.add-rule-btn:hover { border-color: #409eff; color: #409eff; background: #ecf5ff; }
.rule-empty-tip { text-align: center; padding: 30px 20px; color: #909399; font-size: 13px; }
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="sidebar-logo">数据质量平台</div>
<div class="sidebar-menu">
<div class="menu-group-title">校验任务</div>
<div class="menu-item active" data-view="data" data-name="身份证">身份证</div>
<div class="menu-item" data-view="data" data-name="手机号">手机号</div>
<div class="menu-item" data-view="data" data-name="邮箱">邮箱</div>
<div class="menu-item" data-view="data" data-name="地址信息">地址信息</div>
<div class="menu-item" data-view="data" data-name="银行卡号">银行卡号</div>
<div class="menu-item" data-view="data" data-name="日期格式">日期格式</div>
<div class="menu-group-title">管理</div>
<div class="menu-item" data-view="config" data-name="任务配置">任务配置</div>
</div>
<div class="sidebar-footer">v1.0.0</div>
</div>
<div class="main">
<div class="main-header">
<div class="breadcrumb"><span>数据质量</span><span class="sep">/</span><span class="current" id="bcCurrent">身份证</span></div>
<div class="header-right"><span>检测人:张三</span><span>|</span><span>2026-08-19 09:30</span></div>
</div>
<div class="main-body">
<!-- 数据视图 -->
<div class="view active" id="viewData">
<div class="query-card">
<div class="query-row">
<div class="query-item">
<span class="query-label"><span class="required">*</span>任务名称:</span>
<select class="select-input"><option>身份证_用户信息表</option><option>身份证_客户主表</option><option>身份证_员工档案表</option></select>
<button class="btn btn-primary">查询</button>
</div>
</div>
</div>
<div class="result-card">
<div class="result-header">
<div class="result-title">不合规数据明细<span class="count">共 12 条</span></div>
<div class="result-actions"><button class="btn" onclick="openFieldDrawer()">字段配置</button><button class="btn">导出</button><button class="btn btn-success" onclick="importData()">导入</button><button class="btn btn-danger" onclick="batchDelete()">批量删除</button></div>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" class="checkbox" id="selectAll" onclick="toggleSelectAll(this)"></th>
<th><span class="cn">用户ID</span><span class="en">user_id</span></th>
<th><span class="cn">姓名</span><span class="en">user_name</span></th>
<th><span class="cn">身份证号</span><span class="en">id_card</span><span class="rule-mark" title="校验规则:18位/地区码/生日/校验位">⚠ 3规则</span></th>
<th><span class="cn">性别</span><span class="en">gender</span><span class="rule-mark" title="校验规则:与身份证位一致">⚠ 1规则</span></th>
<th><span class="cn">出生日期</span><span class="en">birth_date</span><span class="rule-mark" title="校验规则:与身份证位一致">⚠ 1规则</span></th>
<th><span class="cn">手机号</span><span class="en">mobile</span><span class="rule-mark" title="校验规则:11位手机号">⚠ 1规则</span></th>
<th><span class="cn">地址</span><span class="en">address</span></th>
<th class="col-reason" style="min-width:220px;"><span class="cn">备注</span></th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center;"><input type="checkbox" class="checkbox row-check"></td>
<td>10023</td><td>张三</td>
<td class="cell-error">1101011990010<span class="err-flag">!</span><span class="err-tip">长度不足18位;缺少校验位</span></td>
<td>男</td><td>1990-01-01</td><td>13800130000</td><td>北京市朝阳区</td>
<td class="cell-reason"><span class="issue-line">• 身份证号:长度不足18位</span><span class="issue-line">• 身份证号:缺少校验位</span></td>
<td><button class="btn-text" onclick="editRow(this)">修改</button><button class="btn-text danger" onclick="deleteRow(this)">删除</button></td>
</tr>
<tr>
<td style="text-align:center;"><input type="checkbox" class="checkbox row-check"></td>
<td>10045</td><td>李四</td>
<td class="cell-error">110101199001011234<span class="err-flag">!</span><span class="err-tip">校验位错误(应为3,实际4)</span></td>
<td class="cell-error">男<span class="err-flag">!</span><span class="err-tip">与身份证第17位不一致(身份证为女)</span></td>
<td class="cell-error">1990-01-01<span class="err-flag">!</span><span class="err-tip">与身份证位(7-14)不一致</span></td>
<td>13800130001</td><td>北京市海淀区</td>
<td class="cell-reason"><span class="issue-line">• 身份证号:校验位错误(应为3,实际4)</span><span class="issue-line">• 性别:与身份证第17位不一致(身份证为女)</span><span class="issue-line">• 出生日期:与身份证位(7-14)不一致</span></td>
<td><button class="btn-text" onclick="editRow(this)">修改</button><button class="btn-text danger" onclick="deleteRow(this)">删除</button></td>
</tr>
<tr>
<td style="text-align:center;"><input type="checkbox" class="checkbox row-check"></td>
<td>10078</td><td>王五</td>
<td class="cell-error"><span class="null-val">(NULL)</span><span class="err-flag">!</span><span class="err-tip">必填字段为空</span></td>
<td>男</td><td>1985-05-20</td>
<td class="cell-error">1380013<span class="err-flag">!</span><span class="err-tip">手机号长度不足11位</span></td>
<td>上海市浦东新区</td>
<td class="cell-reason"><span class="issue-line">• 身份证号:必填字段为空</span><span class="issue-line">• 手机号:长度不足11位</span></td>
<td><button class="btn-text" onclick="editRow(this)">修改</button><button class="btn-text danger" onclick="deleteRow(this)">删除</button></td>
</tr>
<tr>
<td style="text-align:center;"><input type="checkbox" class="checkbox row-check"></td>
<td>10092</td><td>赵六</td><td>11010119900202123X</td><td>男</td><td>1990-02-02</td><td>13800130092</td>
<td class="cell-error"><span class="null-val">(NULL)</span><span class="err-flag">!</span><span class="err-tip">地址为空,但身份证地区码非空</span></td>
<td class="cell-reason"><span class="issue-line">• 地址:为空,但身份证地区码非空</span></td>
<td><button class="btn-text" onclick="editRow(this)">修改</button><button class="btn-text danger" onclick="deleteRow(this)">删除</button></td>
</tr>
<tr>
<td style="text-align:center;"><input type="checkbox" class="checkbox row-check"></td>
<td>10105</td><td>钱七</td>
<td class="cell-error">11010119900303<span class="err-flag">!</span><span class="err-tip">长度仅15位,应为18位</span></td>
<td>女</td><td>1990-03-03</td><td>13800130105</td><td>北京市西城区</td>
<td class="cell-reason"><span class="issue-line">• 身份证号:长度仅15位,应为18位(旧版15位)</span></td>
<td><button class="btn-text" onclick="editRow(this)">修改</button><button class="btn-text danger" onclick="deleteRow(this)">删除</button></td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pagination-info">显示第 1-5 条 / 共 <span class="highlight">12</span> 条不合规数据</div>
<div class="pagination-controls"><button class="page-btn">‹</button><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn">›</button></div>
</div>
</div>
</div>
<!-- 任务配置视图 -->
<div class="view" id="viewConfig">
<div class="config-toolbar">
<div class="left">
<span class="query-label">任务分组:</span>
<select class="select-input" style="min-width:160px;"><option>全部分组</option><option>身份证</option><option>手机号</option><option>邮箱</option></select>
<span class="query-label">关键字:</span>
<input type="text" class="text-input" placeholder="任务名称/数据表" style="min-width:200px;">
<button class="btn btn-primary">查询</button>
<button class="btn">重置</button>
</div>
<div class="right"><button class="btn btn-success" onclick="openTaskModal('add')">+ 新增任务</button></div>
</div>
<div class="result-card">
<div class="result-header"><div class="result-title">任务列表<span class="count" style="color:#909399;">共 6 个</span></div></div>
<div class="table-scroll">
<table class="config-list">
<thead><tr><th>任务名称</th><th>分组</th><th>数据库</th><th>数据表</th><th>字段数</th><th>规则数</th><th>状态</th><th>最近检测</th><th>操作</th></tr></thead>
<tbody>
<tr><td>身份证_用户信息表</td><td><span class="tag tag-blue">身份证</span></td><td>customer_db</td><td>t_user_info</td><td>10</td><td>6</td><td><span class="tag tag-green">启用</span></td><td>2026-08-19 09:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
<tr><td>身份证_客户主表</td><td><span class="tag tag-blue">身份证</span></td><td>customer_db</td><td>t_customer</td><td>8</td><td>4</td><td><span class="tag tag-green">启用</span></td><td>2026-08-19 09:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
<tr><td>身份证_员工档案表</td><td><span class="tag tag-blue">身份证</span></td><td>hr_db</td><td>t_employee</td><td>12</td><td>5</td><td><span class="tag tag-gray">停用</span></td><td>2026-08-18 18:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
<tr><td>手机号_用户信息表</td><td><span class="tag tag-orange">手机号</span></td><td>customer_db</td><td>t_user_info</td><td>10</td><td>2</td><td><span class="tag tag-green">启用</span></td><td>2026-08-19 09:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
<tr><td>邮箱_订阅用户</td><td><span class="tag" style="color:#6c5ce7;background:#f0ecff;border-color:#e0d8ff;">邮箱</span></td><td>marketing_db</td><td>t_subscriber</td><td>6</td><td>1</td><td><span class="tag tag-green">启用</span></td><td>2026-08-19 09:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
<tr><td>日期格式_订单</td><td><span class="tag tag-green">日期格式</span></td><td>order_db</td><td>t_order_detail</td><td>15</td><td>3</td><td><span class="tag tag-green">启用</span></td><td>2026-08-19 09:00</td><td><button class="btn-text" onclick="openTaskModal('edit')">编辑</button><button class="btn-text">复制</button><button class="btn-text danger">删除</button></td></tr>
</tbody>
</table>
</div>
<div class="pagination"><div class="pagination-info">共 6 个任务</div><div class="pagination-controls"><button class="page-btn">‹</button><button class="page-btn active">1</button><button class="page-btn">›</button></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- 字段配置抽屉 -->
<div class="drawer-mask" id="fieldDrawerMask" onclick="closeFieldDrawer()"></div>
<div class="drawer" id="fieldDrawer">
<div class="drawer-header"><span class="drawer-title">展示字段配置</span><span class="drawer-close" onclick="closeFieldDrawer()">×</span></div>
<div class="drawer-body">
<div class="drawer-hint">勾选要在结果表中展示的字段。</div>
<div class="field-check-list">
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">用户ID</span><span class="en">user_id</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">姓名</span><span class="en">user_name</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">身份证号</span><span class="en">id_card</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">性别</span><span class="en">gender</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">出生日期</span><span class="en">birth_date</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">手机号</span><span class="en">mobile</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox" checked><span class="cn">地址</span><span class="en">address</span></label>
<label class="field-check-item"><input type="checkbox" class="checkbox"><span class="cn">邮箱</span><span class="en">email</span></label>
</div>
</div>
<div class="drawer-footer"><button class="btn" onclick="closeFieldDrawer()">取消</button><button class="btn btn-primary" onclick="closeFieldDrawer()">保存</button></div>
</div>
<!-- 任务配置弹窗 -->
<div class="modal-mask" id="taskModal">
<div class="modal">
<div class="modal-header"><span class="modal-title" id="taskModalTitle">新增任务</span><span class="modal-close" onclick="closeTaskModal()">×</span></div>
<div class="modal-body">
<div class="modal-tabs">
<div class="modal-tab active" onclick="switchModalTab(this,'tab-basic')">基础信息</div>
<div class="modal-tab" onclick="switchModalTab(this,'tab-conn')">数据连接</div>
<div class="modal-tab" onclick="switchModalTab(this,'tab-rule')">校验规则</div>
</div>
<!-- Tab1:基础信息 -->
<div class="tab-panel active" id="tab-basic">
<div class="form-grid">
<div class="form-row">
<span class="form-label"><span class="required">*</span>任务名称</span>
<div class="form-control"><input type="text" class="text-input" value="身份证_用户信息表" placeholder="如:身份证_用户信息表"><div class="form-hint">用户在查询下拉框看到的名称,建议格式:校验类型_表名</div></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>任务分组</span>
<div class="form-control"><select class="select-input"><option>身份证</option><option>手机号</option><option>邮箱</option><option>地址信息</option><option>银行卡号</option><option>日期格式</option><option>+ 新建分组</option></select></div>
</div>
<div class="form-row full">
<span class="form-label">任务说明</span>
<div class="form-control"><textarea>对用户信息表中的身份证号、性别、出生日期进行格式与一致性校验</textarea></div>
</div>
</div>
</div>
<!-- Tab2:数据连接 -->
<div class="tab-panel" id="tab-conn">
<div class="form-section">
<div class="form-section-title">连接方式</div>
<div class="conn-tabs">
<div class="conn-tab active">MySQL</div><div class="conn-tab">Oracle</div><div class="conn-tab">SQL Server</div><div class="conn-tab">PostgreSQL</div><div class="conn-tab">达梦 DM</div><div class="conn-tab">JDBC 自定义</div>
</div>
<div class="form-grid">
<div class="form-row"><span class="form-label"><span class="required">*</span>连接名称</span><div class="form-control"><input type="text" class="text-input" value="customer_db_prod"></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>主机地址</span><div class="form-control"><input type="text" class="text-input" value="192.168.101.126"></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>端口</span><div class="form-control"><input type="text" class="text-input" value="3306"></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>数据库名</span><div class="form-control"><input type="text" class="text-input" value="customer_db"></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>用户名</span><div class="form-control"><input type="text" class="text-input" value="data_check"></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>密码</span><div class="form-control"><input type="password" class="text-input" value="******"></div></div>
<div class="form-row full"><span class="form-label">连接参数</span><div class="form-control"><input type="text" class="text-input" value="useUnicode=true&characterEncoding=utf8&useSSL=false"><div class="form-hint">JDBC 额外参数,可选</div></div></div>
<div class="form-row full"><span class="form-label"></span><div class="form-control" style="display:flex;gap:8px;align-items:center;"><button class="btn">测试连接</button><span class="form-hint" style="color:#67c23a;">✓ 连接成功(耗时 86ms)</span></div></div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">数据表</div>
<div class="form-grid">
<div class="form-row"><span class="form-label"><span class="required">*</span>Schema</span><div class="form-control"><select class="select-input"><option>public</option><option>customer</option><option>sys</option></select></div></div>
<div class="form-row"><span class="form-label"><span class="required">*</span>选择数据表</span><div class="form-control"><select class="select-input"><option>t_user_info(用户信息表)</option><option>t_customer(客户主表)</option><option>t_employee(员工档案表)</option></select><div class="form-hint">选择数据表后自动加载该表全部字段</div></div></div>
</div>
</div>
</div>
<!-- Tab3:校验规则(字段列表 + 每行末尾"规则设置"按钮) -->
<div class="tab-panel" id="tab-rule">
<div class="form-hint" style="margin-bottom:12px;color:#606266;font-size:13px;">
字段列表为所选数据表的全部字段;点击对应字段右侧"规则设置"可查看/配置该字段的校验规则;勾选"默认展示"则该字段在校验结果中默认展示。
</div>
<div class="table-wrap">
<table class="field-table">
<thead>
<tr>
<th>字段名</th>
<th style="min-width:120px;">中文名称</th>
<th>数据类型</th>
<th style="width:120px;">规则设置</th>
<th style="width:80px;">默认展示</th>
</tr>
</thead>
<tbody>
<tr>
<td class="field-name-cell">用户ID<span class="en">user_id</span></td>
<td><input type="text" class="text-input" value="用户ID"></td>
<td>BIGINT</td>
<td><button class="rule-set-btn empty" onclick="openRuleModal(this,'user_id','用户ID')">规则设置<span class="num">0</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">姓名<span class="en">user_name</span></td>
<td><input type="text" class="text-input" value="姓名"></td>
<td>VARCHAR(50)</td>
<td><button class="rule-set-btn empty" onclick="openRuleModal(this,'user_name','姓名')">规则设置<span class="num">0</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">身份证号<span class="en">id_card</span></td>
<td><input type="text" class="text-input" value="身份证号"></td>
<td>VARCHAR(18)</td>
<td><button class="rule-set-btn" onclick="openRuleModal(this,'id_card','身份证号')">规则设置<span class="num">3</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">性别<span class="en">gender</span></td>
<td><input type="text" class="text-input" value="性别"></td>
<td>CHAR(1)</td>
<td><button class="rule-set-btn" onclick="openRuleModal(this,'gender','性别')">规则设置<span class="num">1</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">出生日期<span class="en">birth_date</span></td>
<td><input type="text" class="text-input" value="出生日期"></td>
<td>DATE</td>
<td><button class="rule-set-btn" onclick="openRuleModal(this,'birth_date','出生日期')">规则设置<span class="num">1</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox"></td>
</tr>
<tr>
<td class="field-name-cell">手机号<span class="en">mobile</span></td>
<td><input type="text" class="text-input" value="手机号"></td>
<td>VARCHAR(11)</td>
<td><button class="rule-set-btn" onclick="openRuleModal(this,'mobile','手机号')">规则设置<span class="num">1</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">地址<span class="en">address</span></td>
<td><input type="text" class="text-input" value="地址"></td>
<td>VARCHAR(200)</td>
<td><button class="rule-set-btn empty" onclick="openRuleModal(this,'address','地址')">规则设置<span class="num">0</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox" checked></td>
</tr>
<tr>
<td class="field-name-cell">邮箱<span class="en">email</span></td>
<td><input type="text" class="text-input" value="邮箱"></td>
<td>VARCHAR(100)</td>
<td><button class="rule-set-btn empty" onclick="openRuleModal(this,'email','邮箱')">规则设置<span class="num">0</span></button></td>
<td style="text-align:center;"><input type="checkbox" class="checkbox"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeTaskModal()">取消</button><button class="btn btn-primary" onclick="closeTaskModal()">保存</button></div>
</div>
</div>
<!-- 规则设置弹窗 -->
<div class="modal-mask rule-modal" id="ruleModal">
<div class="modal" style="width:680px;">
<div class="modal-header"><span class="modal-title" id="ruleModalTitle">规则设置 - id_card(身份证号)</span><span class="modal-close" onclick="closeRuleModal()">×</span></div>
<div class="modal-body" style="padding:18px 20px;">
<div id="ruleItemList">
<!-- 规则1 -->
<div class="rule-item">
<div class="rule-item-head"><span class="rnum">规则1</span><button class="btn-text danger" onclick="this.closest('.rule-item').remove();checkRuleEmpty()">删除</button></div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl"><textarea class="rule-desc" placeholder="用自然语言描述规则,如:必须是18位中国大陆身份证号,最后一位可为X">必须是18位中国大陆身份证号,前6位地区码,中8位出生日期,最后一位校验位可为数字或X</textarea></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl"><button class="btn ai-btn" onclick="genRegex(this)">✨ AI 生成正则</button></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line"><input type="text" class="text-input regex-input" value="^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$"><button class="btn" onclick="copyRegex(this)">复制</button></div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line"><input type="text" class="text-input test-input" placeholder="输入测试值" value="110101199001011234"><button class="btn" onclick="testRegex(this)">测试</button><span class="test-result"></span></div>
</div>
</div>
</div>
<!-- 规则2 -->
<div class="rule-item">
<div class="rule-item-head"><span class="rnum">规则2</span><button class="btn-text danger" onclick="this.closest('.rule-item').remove();checkRuleEmpty()">删除</button></div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl"><textarea class="rule-desc" placeholder="用自然语言描述规则">身份证不能为空</textarea></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl"><button class="btn ai-btn" onclick="genRegex(this)">✨ AI 生成正则</button></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line"><input type="text" class="text-input regex-input" value="^.+$"><button class="btn" onclick="copyRegex(this)">复制</button></div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line"><input type="text" class="text-input test-input" placeholder="输入测试值"><button class="btn" onclick="testRegex(this)">测试</button><span class="test-result"></span></div>
</div>
</div>
</div>
<!-- 规则3 -->
<div class="rule-item">
<div class="rule-item-head"><span class="rnum">规则3</span><button class="btn-text danger" onclick="this.closest('.rule-item').remove();checkRuleEmpty()">删除</button></div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl"><textarea class="rule-desc" placeholder="用自然语言描述规则">必须是18位,不能是15位旧版</textarea></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl"><button class="btn ai-btn" onclick="genRegex(this)">✨ AI 生成正则</button></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line"><input type="text" class="text-input regex-input" placeholder="点击AI生成或手动填写"><button class="btn" onclick="copyRegex(this)">复制</button></div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line"><input type="text" class="text-input test-input" placeholder="输入测试值"><button class="btn" onclick="testRegex(this)">测试</button><span class="test-result"></span></div>
</div>
</div>
</div>
</div>
<div class="rule-empty-tip" id="ruleEmptyTip" style="display:none;">还没有规则,点击下方按钮添加</div>
<button class="add-rule-btn" onclick="addRuleItem()">+ 添加规则</button>
</div>
<div class="modal-footer"><button class="btn" onclick="closeRuleModal()">关闭</button></div>
</div>
</div>
<!-- 规则模板(隐藏,用于新增) -->
<template id="ruleItemTpl">
<div class="rule-item">
<div class="rule-item-head"><span class="rnum">规则N</span><button class="btn-text danger" onclick="this.closest('.rule-item').remove();checkRuleEmpty()">删除</button></div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl"><textarea class="rule-desc" placeholder="用自然语言描述规则,如:必须是18位中国大陆身份证号,最后一位可为X"></textarea></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl"><button class="btn ai-btn" onclick="genRegex(this)">✨ AI 生成正则</button></div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line"><input type="text" class="text-input regex-input" placeholder="点击AI生成或手动填写"><button class="btn" onclick="copyRegex(this)">复制</button></div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line"><input type="text" class="text-input test-input" placeholder="输入测试值"><button class="btn" onclick="testRegex(this)">测试</button><span class="test-result"></span></div>
</div>
</div>
</div>
</template>
<!-- 导入文件选择(隐藏) -->
<input type="file" id="importFile" accept=".xlsx,.xls,.csv" style="display:none;" onchange="onImportFile(this)">
<script>
// 菜单切换
document.querySelectorAll('.menu-item').forEach(function(item){
item.addEventListener('click', function(){
document.querySelectorAll('.menu-item').forEach(function(m){ m.classList.remove('active'); });
this.classList.add('active');
var view = this.getAttribute('data-view');
var name = this.getAttribute('data-name');
document.getElementById('bcCurrent').textContent = name;
document.getElementById('viewData').classList.toggle('active', view === 'data');
document.getElementById('viewConfig').classList.toggle('active', view === 'config');
});
});
// 字段配置抽屉
function openFieldDrawer(){ document.getElementById('fieldDrawer').classList.add('show'); document.getElementById('fieldDrawerMask').classList.add('show'); }
function closeFieldDrawer(){ document.getElementById('fieldDrawer').classList.remove('show'); document.getElementById('fieldDrawerMask').classList.remove('show'); }
// 列表行操作
function editRow(btn){
var tr = btn.closest('tr');
var cells = tr.querySelectorAll('td');
var userId = cells[2] ? cells[2].textContent.trim() : '';
alert('修改:打开「' + userId + '」该条数据的编辑表单');
}
function deleteRow(btn){
if(!confirm('确定删除该条数据?')) return;
btn.closest('tr').remove();
}
function toggleSelectAll(el){
document.querySelectorAll('.row-check').forEach(function(c){ c.checked = el.checked; });
}
function batchDelete(){
var checks = document.querySelectorAll('.row-check:checked');
if(checks.length === 0){ alert('请先勾选要删除的数据'); return; }
if(!confirm('确定删除选中的 ' + checks.length + ' 条数据?')) return;
checks.forEach(function(c){ c.closest('tr').remove(); });
var all = document.getElementById('selectAll');
if(all) all.checked = false;
}
// 导入:选择修改后的文件,导入回数据表
function importData(){ document.getElementById('importFile').click(); }
function onImportFile(input){
if(input.files && input.files[0]){
var name = input.files[0].name;
if(!confirm('已选择文件:' + name + '\n将解析后写回数据表,是否继续?')){ input.value = ''; return; }
alert('导入完成:已根据文件更新数据表(示例)');
input.value = '';
}
}
// 任务配置弹窗
function openTaskModal(mode){
document.getElementById('taskModalTitle').textContent = (mode === 'add') ? '新增任务' : '编辑任务';
var tabs = document.querySelectorAll('.modal-tab');
var panels = document.querySelectorAll('.tab-panel');
tabs.forEach(function(t,i){ t.classList.toggle('active', i===0); });
panels.forEach(function(p,i){ p.classList.toggle('active', i===0); });
document.getElementById('taskModal').classList.add('show');
}
function closeTaskModal(){ document.getElementById('taskModal').classList.remove('show'); }
// 弹窗 Tab 切换
function switchModalTab(el, panelId){
document.querySelectorAll('.modal-tab').forEach(function(t){ t.classList.remove('active'); });
el.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(function(p){ p.classList.remove('active'); });
document.getElementById(panelId).classList.add('active');
}
// 连接方式 Tab 切换
document.querySelectorAll('.conn-tab').forEach(function(tab){
tab.addEventListener('click', function(){
document.querySelectorAll('.conn-tab').forEach(function(t){ t.classList.remove('active'); });
this.classList.add('active');
});
});
// 规则设置弹窗
function openRuleModal(btn, fieldKey, fieldName){
document.getElementById('ruleModalTitle').textContent = '规则设置 - ' + fieldKey + '(' + fieldName + ')';
// 保存触发按钮,便于关闭时回写规则数
document.getElementById('ruleModal').dataset.trigger = '';
document.querySelectorAll('.rule-set-btn').forEach(function(b){ if(b === btn) document.getElementById('ruleModal').dataset.trigger = ''; });
document.getElementById('ruleModal').classList.add('show');
renumberRules();
checkRuleEmpty();
}
function closeRuleModal(){ document.getElementById('ruleModal').classList.remove('show'); }
// 添加规则
function addRuleItem(){
var tpl = document.getElementById('ruleItemTpl');
var list = document.getElementById('ruleItemList');
var clone = tpl.content.cloneNode(true);
list.appendChild(clone);
renumberRules();
checkRuleEmpty();
list.lastElementChild.scrollIntoView({ behavior:'smooth', block:'center' });
}
// 重新编号
function renumberRules(){
var items = document.querySelectorAll('#ruleItemList .rule-item');
items.forEach(function(it, i){
var n = it.querySelector('.rnum');
if(n) n.textContent = '规则' + (i+1);
});
}
// 检查空规则状态
function checkRuleEmpty(){
var count = document.querySelectorAll('#ruleItemList .rule-item').length;
document.getElementById('ruleEmptyTip').style.display = (count === 0) ? 'block' : 'none';
document.getElementById('ruleItemList').style.display = (count === 0) ? 'none' : 'block';
}
// AI 生成正则:取同行规则说明,转成正则填入
function genRegex(btn){
var item = btn.closest('.rule-item');
var desc = item.querySelector('.rule-desc').value.trim();
var regexInput = item.querySelector('.regex-input');
if(!desc){ item.querySelector('.rule-desc').focus(); return; }
btn.disabled = true; btn.textContent = '✨ AI 分析中…';
setTimeout(function(){
btn.disabled = false; btn.textContent = '✨ AI 生成正则';
var r = aiGenerateRegex(desc);
if(r.regex){
regexInput.value = r.regex;
regexInput.style.borderColor = '#67c23a';
setTimeout(function(){ regexInput.style.borderColor = ''; }, 1500);
} else {
regexInput.placeholder = r.note;
}
}, 600);
}
// AI 正则生成规则库(模拟)
function aiGenerateRegex(desc){
if(desc.indexOf('身份证') >= 0 || desc.indexOf('id_card') >= 0){
return { regex: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$', note: '已识别为18位身份证号' };
}
if(desc.indexOf('手机') >= 0 || desc.indexOf('电话') >= 0){
return { regex: '^1[3-9]\\d{9}$', note: '已识别为11位手机号' };
}
if(desc.indexOf('邮箱') >= 0 || desc.indexOf('email') >= 0 || desc.indexOf('邮件') >= 0){
return { regex: '^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$', note: '已识别为邮箱格式' };
}
if(desc.indexOf('银行卡') >= 0 || desc.indexOf('卡号') >= 0){
return { regex: '^\\d{16,19}$', note: '已识别为银行卡号' };
}
if(desc.indexOf('日期') >= 0 || desc.indexOf('yyyy') >= 0){
return { regex: '^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', note: '已识别为日期格式' };
}
if(desc.indexOf('邮编') >= 0){ return { regex: '^[1-9]\\d{5}$', note: '已识别为6位邮编' }; }
if(desc.indexOf('男或女') >= 0 || desc.indexOf('男/女') >= 0 || (desc.indexOf('男') >= 0 && desc.indexOf('女') >= 0)){ return { regex: '^(男|女)$', note: '已识别为性别枚举' }; }
if(desc.indexOf('不能为空') >= 0 || desc.indexOf('非空') >= 0 || desc.indexOf('不能空') >= 0){ return { regex: '^.+$', note: '已识别为非空校验' }; }
if(desc.indexOf('正整数') >= 0){ return { regex: '^[1-9]\\d*$', note: '已识别为正整数' }; }
var m = desc.match(/(\d+)\s*[-到至]\s*(\d+)\s*个/);
if(m){ return { regex: '^.{' + m[1] + ',' + m[2] + '}$', note: '已识别为长度' + m[1] + '-' + m[2] + '位' }; }
var n = desc.match(/(\d+)\s*位/);
if(n){ return { regex: '^.{' + n[1] + '}$', note: '已识别为固定' + n[1] + '位' }; }
if(desc.indexOf('纯数字') >= 0 || desc.indexOf('数字') >= 0){ return { regex: '^\\d+$', note: '已识别为纯数字' }; }
return { regex: '', note: '未能识别,请补充描述或手动填写正则' };
}
// 复制正则
function copyRegex(btn){
var input = btn.parentElement.querySelector('.regex-input');
if(!input.value) return;
input.select();
try { document.execCommand('copy'); } catch(e) {}
var old = btn.textContent; btn.textContent = '已复制';
setTimeout(function(){ btn.textContent = old; }, 1200);
}
// 测试正则
function testRegex(btn){
var item = btn.closest('.rule-item');
var regexStr = item.querySelector('.regex-input').value;
var testVal = item.querySelector('.test-input').value;
var result = item.querySelector('.test-result');
if(!regexStr){ result.textContent = '无正则'; result.className = 'test-result fail'; return; }
if(!testVal){ result.textContent = '请输入'; result.className = 'test-result fail'; return; }
var ok = false;
try { ok = new RegExp(regexStr).test(testVal); }
catch(e){ result.textContent = '语法错误'; result.className = 'test-result fail'; return; }
result.textContent = ok ? '✓ 通过' : '✗ 不匹配';
result.className = 'test-result ' + (ok ? 'pass' : 'fail');
}
</script>
</body>
</html>
<template>
<router-view />
</template>
<script setup>
// 根组件只负责挂载路由,由各 layout 决定具体结构
</script>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
</style>
\ No newline at end of file
<template>
<Teleport to="body">
<div class="drawer-mask" :class="{ show: open }" @click="$emit('update:open', false)"></div>
<div class="drawer" :class="{ show: open }">
<div class="drawer-header">
<span class="drawer-title">展示字段配置</span>
<button class="drawer-close" @click="$emit('update:open', false)">×</button>
</div>
<div class="drawer-body">
<div class="drawer-hint">勾选要在结果表中展示的字段。</div>
<div class="field-check-list">
<label v-for="f in businessFields" :key="f.key" class="field-check-item">
<input type="checkbox" class="checkbox" v-model="localChecked[f.key]" />
<span class="cn">{{ f.cn }}</span>
<span class="en">{{ f.en }}</span>
</label>
</div>
</div>
<div class="drawer-footer">
<button class="btn" @click="$emit('update:open', false)">取消</button>
<button class="btn btn-primary" @click="onSave">保存</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { computed, reactive, watch } from 'vue'
const props = defineProps({
open: { type: Boolean, default: false },
fields: { type: Array, default: () => [] },
visibleFields: { type: Array, default: () => [] },
})
const emit = defineEmits(['update:open', 'save'])
// 业务字段(排除内部 __reason)
const businessFields = computed(() => props.fields.filter((f) => f.key !== '__reason'))
const localChecked = reactive({})
watch(
() => [businessFields.value, props.visibleFields],
() => {
businessFields.value.forEach((f) => {
localChecked[f.key] = props.visibleFields.includes(f.key)
})
},
{ immediate: true, deep: true }
)
function onSave() {
const list = businessFields.value.filter((f) => localChecked[f.key]).map((f) => f.key)
emit('save', list)
}
</script>
\ No newline at end of file
<template>
<Teleport to="body">
<div class="modal-mask rule-modal" :class="{ show: open }">
<div class="modal modal-sm" @click.stop>
<div class="modal-header">
<span class="modal-title">{{ title }}</span>
<button class="modal-close" @click="$emit('update:open', false)">×</button>
</div>
<div class="modal-body" style="padding: 18px 20px;">
<div :style="{ display: ruleList.length === 0 ? 'none' : 'block' }">
<div v-for="(rule, idx) in ruleList" :key="rule.id" class="rule-item">
<div class="rule-item-head">
<span class="rnum">规则{{ idx + 1 }}</span>
<button class="btn-text danger" @click="removeRule(idx)">删除</button>
</div>
<div class="rule-item-row">
<span class="rule-item-label">规则说明</span>
<div class="rule-item-ctrl">
<textarea v-model="rule.desc" placeholder="用自然语言描述规则,如:必须是18位中国大陆身份证号,最后一位可为X"></textarea>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label"></span>
<div class="rule-item-ctrl">
<button class="btn ai-btn" :disabled="aiLoading[idx]" @click="genRegex(idx)">
{{ aiLoading[idx] ? '✨ AI 分析中…' : '✨ AI 生成正则' }}
</button>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">正则表达式</span>
<div class="rule-item-ctrl">
<div class="regex-line">
<input type="text" class="text-input" v-model="rule.regex" placeholder="点击AI生成或手动填写" />
<button class="btn" @click="copyRegex(rule)">复制</button>
</div>
</div>
</div>
<div class="rule-item-row">
<span class="rule-item-label">测试</span>
<div class="rule-item-ctrl">
<div class="test-line">
<input type="text" class="text-input" v-model="rule.testVal" placeholder="输入测试值" />
<button class="btn" @click="testRegex(idx)">测试</button>
<span class="test-result" :class="rule.testResult?.cls" v-if="rule.testResult">{{ rule.testResult.text }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="ruleList.length === 0" class="rule-empty-tip">还没有规则,点击下方按钮添加</div>
<button class="add-rule-btn" @click="addRule">+ 添加规则</button>
</div>
<div class="modal-footer">
<button class="btn" @click="$emit('update:open', false)">关闭</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
open: { type: Boolean, default: false },
field: { type: Object, default: null },
})
defineEmits(['update:open'])
const title = computed(() => {
if (!props.field) return '规则设置'
return `规则设置 - ${props.field.en}(${props.field.cnName || props.field.cn})`
})
const ruleList = ref([])
const aiLoading = ref({})
watch(() => [props.open, props.field], ([open, f]) => {
if (open && f) {
// 深拷贝字段上的规则,避免直接改父组件
ruleList.value = (f.rules || []).map((r) => ({ ...r, testVal: '', testResult: null }))
aiLoading.value = {}
}
}, { immediate: true })
function addRule() {
ruleList.value.push({
id: Date.now(),
desc: '',
regex: '',
testVal: '',
testResult: null,
})
}
function removeRule(idx) {
ruleList.value.splice(idx, 1)
}
// AI 模拟:根据规则说明返回正则
function aiGenerateRegex(desc) {
if (/身份证|id_card/i.test(desc)) return { regex: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$', note: '已识别为18位身份证号' }
if (/手机|电话/.test(desc)) return { regex: '^1[3-9]\\d{9}$', note: '已识别为11位手机号' }
if (/邮箱|email|邮件/i.test(desc)) return { regex: '^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$', note: '已识别为邮箱格式' }
if (/银行卡|卡号/.test(desc)) return { regex: '^\\d{16,19}$', note: '已识别为银行卡号' }
if (/日期|yyyy/i.test(desc)) return { regex: '^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', note: '已识别为日期格式' }
if (/邮编/.test(desc)) return { regex: '^[1-9]\\d{5}$', note: '已识别为6位邮编' }
if (/不能为空|非空|不能空/.test(desc)) return { regex: '^.+$', note: '已识别为非空校验' }
if (/正整数/.test(desc)) return { regex: '^[1-9]\\d*$', note: '已识别为正整数' }
if (/纯数字|数字/.test(desc)) return { regex: '^\\d+$', note: '已识别为纯数字' }
const m = desc.match(/(\d+)\s*[-到至]\s*(\d+)\s*个/)
if (m) return { regex: `^.{${m[1]},${m[2]}}$`, note: `已识别为长度${m[1]}-${m[2]}位` }
const n = desc.match(/(\d+)\s*位/)
if (n) return { regex: `^.{${n[1]}}$`, note: `已识别为固定${n[1]}位` }
return { regex: '', note: '未能识别,请补充描述或手动填写正则' }
}
function genRegex(idx) {
const rule = ruleList.value[idx]
if (!rule.desc.trim()) {
ElMessage.warning('请先填写规则说明')
return
}
aiLoading.value[idx] = true
setTimeout(() => {
const r = aiGenerateRegex(rule.desc)
aiLoading.value[idx] = false
if (r.regex) {
rule.regex = r.regex
ElMessage.success(r.note)
} else {
ElMessage.warning(r.note)
}
}, 500)
}
function copyRegex(rule) {
if (!rule.regex) return
navigator.clipboard?.writeText(rule.regex).catch(() => {})
ElMessage.success('已复制')
}
function testRegex(idx) {
const rule = ruleList.value[idx]
if (!rule.regex) { rule.testResult = { text: '无正则', cls: 'fail' }; return }
if (!rule.testVal) { rule.testResult = { text: '请输入', cls: 'fail' }; return }
try {
const ok = new RegExp(rule.regex).test(rule.testVal)
rule.testResult = { text: ok ? '✓ 通过' : '✗ 不匹配', cls: ok ? 'pass' : 'fail' }
} catch (e) {
rule.testResult = { text: '语法错误', cls: 'fail' }
}
}
</script>
\ No newline at end of file
<template>
<Teleport to="body">
<div class="modal-mask" :class="{ show: open }">
<div class="modal" @click.stop>
<div class="modal-header">
<span class="modal-title">{{ modalTitle }}</span>
<button class="modal-close" @click="$emit('update:open', false)">×</button>
</div>
<div class="modal-body">
<div class="modal-tabs">
<div
v-for="t in tabs"
:key="t.key"
class="modal-tab"
:class="{ active: activeTab === t.key }"
@click="activeTab = t.key"
>{{ t.label }}</div>
</div>
<!-- Tab1 基础信息 -->
<div class="tab-panel" :class="{ active: activeTab === 'basic' }">
<div class="form-grid">
<div class="form-row">
<span class="form-label"><span class="required">*</span>任务名称</span>
<div class="form-control">
<input type="text" class="text-input" v-model="form.name" placeholder="如:身份证_用户信息表" />
<div class="form-hint">用户在查询下拉框看到的名称,建议格式:校验类型_表名</div>
</div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>任务分组</span>
<div class="form-control">
<select class="select-input" v-model="form.group">
<option v-for="g in groupOptions" :key="g" :value="g">{{ g }}</option>
<option value="__new">+ 新建分组</option>
</select>
</div>
</div>
<div class="form-row full">
<span class="form-label">任务说明</span>
<div class="form-control">
<textarea v-model="form.desc" placeholder="对哪些字段做哪些校验的描述"></textarea>
</div>
</div>
</div>
</div>
<!-- Tab2 数据连接 -->
<div class="tab-panel" :class="{ active: activeTab === 'conn' }">
<div class="form-section">
<div class="form-section-title">连接方式</div>
<div class="conn-tabs">
<div
v-for="c in dbTypes"
:key="c"
class="conn-tab"
:class="{ active: form.dbType === c }"
@click="form.dbType = c"
>{{ c }}</div>
</div>
<div class="form-grid">
<div class="form-row">
<span class="form-label"><span class="required">*</span>连接名称</span>
<div class="form-control"><input type="text" class="text-input" v-model="form.connName" /></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>主机地址</span>
<div class="form-control"><input type="text" class="text-input" v-model="form.host" /></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>端口</span>
<div class="form-control"><input type="text" class="text-input" v-model="form.port" /></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>数据库名</span>
<div class="form-control"><input type="text" class="text-input" v-model="form.db" /></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>用户名</span>
<div class="form-control"><input type="text" class="text-input" v-model="form.user" /></div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>密码</span>
<div class="form-control"><input type="password" class="text-input" v-model="form.password" /></div>
</div>
<div class="form-row full">
<span class="form-label">连接参数</span>
<div class="form-control">
<input type="text" class="text-input" v-model="form.jdbcParams" />
<div class="form-hint">JDBC 额外参数,可选</div>
</div>
</div>
<div class="form-row full">
<span class="form-label"></span>
<div class="form-control" style="display: flex; gap: 8px; align-items: center;">
<button class="btn" @click="testConn">测试连接</button>
<span v-if="connResult" class="form-hint" style="color: #67c23a;">✓ {{ connResult }}</span>
</div>
</div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">数据表</div>
<div class="form-grid">
<div class="form-row">
<span class="form-label"><span class="required">*</span>Schema</span>
<div class="form-control">
<select class="select-input" v-model="form.schema">
<option v-for="s in schemaOptions" :key="s" :value="s">{{ s }}</option>
</select>
</div>
</div>
<div class="form-row">
<span class="form-label"><span class="required">*</span>选择数据表</span>
<div class="form-control">
<select class="select-input" v-model="form.table">
<option v-for="t in tableOptions" :key="t" :value="t">{{ t }}</option>
</select>
<div class="form-hint">选择数据表后自动加载该表全部字段</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab3 校验规则(字段列表) -->
<div class="tab-panel" :class="{ active: activeTab === 'rule' }">
<div class="form-hint" style="margin-bottom: 12px; color: #606266; font-size: 13px;">
字段列表为所选数据表的全部字段;点击对应字段右侧"规则设置"可查看/配置该字段的校验规则;勾选"默认展示"则该字段在校验结果中默认展示。
</div>
<div class="table-wrap">
<table class="field-table">
<thead>
<tr>
<th>字段名</th>
<th style="min-width: 120px;">中文名称</th>
<th>数据类型</th>
<th style="width: 120px;">规则设置</th>
<th style="width: 80px;">默认展示</th>
</tr>
</thead>
<tbody>
<tr v-for="f in fieldList" :key="f.key">
<td class="field-name-cell">{{ f.cn }}<span class="en">{{ f.en }}</span></td>
<td><input type="text" class="text-input" v-model="f.cnName" /></td>
<td>{{ f.type }}</td>
<td>
<button
class="rule-set-btn"
:class="{ empty: !f.rules || f.rules.length === 0 }"
@click="openRule(f)"
>规则设置<span class="num">{{ f.rules?.length || 0 }}</span></button>
</td>
<td style="text-align: center;">
<input type="checkbox" class="checkbox" v-model="f.showDefault" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" @click="$emit('update:open', false)">取消</button>
<button class="btn btn-primary" @click="onSave">保存</button>
</div>
</div>
</div>
</Teleport>
<!-- 规则弹窗 -->
<RuleModal v-model:open="ruleModalOpen" :field="activeField" />
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fieldsByType, fieldRuleSamples } from '@/data/mockData'
import RuleModal from '@/components/RuleModal.vue'
const props = defineProps({
open: { type: Boolean, default: false },
mode: { type: String, default: 'add' },
task: { type: Object, default: null },
})
const emit = defineEmits(['update:open', 'saved'])
const modalTitle = computed(() => (props.mode === 'add' ? '新增任务' : '编辑任务'))
const tabs = [
{ key: 'basic', label: '基础信息' },
{ key: 'conn', label: '数据连接' },
{ key: 'rule', label: '校验规则' },
]
const activeTab = ref('basic')
const groupOptions = ['身份证', '手机号', '邮箱', '地址信息', '银行卡号', '日期格式']
const dbTypes = ['MySQL', 'Oracle', 'SQL Server', 'PostgreSQL', '达梦 DM', 'JDBC 自定义']
const schemaOptions = ['public', 'customer', 'sys']
const tableOptions = ['t_user_info(用户信息表)', 't_customer(客户主表)', 't_employee(员工档案表)']
const form = reactive({
name: '身份证_用户信息表',
group: '身份证',
desc: '对用户信息表中的身份证号、性别、出生日期进行格式与一致性校验',
dbType: 'MySQL',
connName: 'customer_db_prod',
host: '192.168.101.126',
port: '3306',
db: 'customer_db',
user: 'data_check',
password: '******',
jdbcParams: 'useUnicode=true&characterEncoding=utf8&useSSL=false',
schema: 'public',
table: 't_user_info(用户信息表)',
})
const connResult = ref('')
function testConn() {
connResult.value = '连接成功(耗时 86ms)'
}
// 字段列表:根据分组反推 type,选对应 mock 字段
const fieldList = ref([])
function rebuildFieldList() {
const groupToType = {
'身份证': 'id-card', '手机号': 'mobile', '邮箱': 'email',
'地址信息': 'address', '银行卡号': 'bank-card', '日期格式': 'date',
}
const t = groupToType[form.group] || 'id-card'
const meta = fieldsByType[t] || []
fieldList.value = meta.map((f) => {
const samples = fieldRuleSamples[f.key] || []
return {
...f,
cnName: f.cn,
type: f.key === 'user_id' ? 'BIGINT'
: f.key === 'birth_date' || f.key === 'order_date' ? 'DATE'
: f.key === 'gender' ? 'CHAR(1)'
: f.key === 'mobile' ? 'VARCHAR(11)'
: f.key === 'address' ? 'VARCHAR(200)'
: f.key === 'email' ? 'VARCHAR(100)'
: f.key === 'bank_card' ? 'VARCHAR(19)'
: f.key === 'id_card' ? 'VARCHAR(18)'
: 'VARCHAR(50)',
rules: samples.map((s, i) => ({ id: i + 1, desc: s.desc, regex: s.regex })),
showDefault: true,
}
})
}
watch(() => props.open, (v) => {
if (v) {
activeTab.value = 'basic'
connResult.value = ''
if (props.task) {
// 编辑场景下还原基础字段
form.name = props.task.name
form.group = props.task.group
form.db = props.task.db
} else {
form.name = '身份证_用户信息表'
form.group = '身份证'
}
rebuildFieldList()
}
})
watch(() => form.group, () => rebuildFieldList())
// 规则弹窗
const ruleModalOpen = ref(false)
const activeField = ref(null)
function openRule(f) {
activeField.value = f
ruleModalOpen.value = true
}
function onSave() {
if (!form.name) {
ElMessage.warning('请填写任务名称')
activeTab.value = 'basic'
return
}
const payload = {
name: form.name,
group: form.group,
db: form.db,
table: form.table.split('(')[0],
fields: fieldList.value.length,
rules: fieldList.value.reduce((s, f) => s + (f.rules?.length || 0), 0),
status: '启用',
checkedAt: '2026-08-19 09:00',
tagColor: {
'身份证': 'blue', '手机号': 'orange', '邮箱': 'purple',
'地址信息': 'green', '银行卡号': 'green', '日期格式': 'green',
}[form.group] || 'blue',
}
emit('saved', payload)
}
</script>
\ No newline at end of file
// 静态 mock 数据 —— 后续替换为接口请求
// 数据视图:根据菜单选中的数据类型(type)提供对应表格
// 字段元信息:cn 中文名 / en 字段英文名 / rules 该字段挂的规则数 / required 是否必填
export const fieldsByType = {
'id-card': [
{ key: 'user_id', cn: '用户ID', en: 'user_id', rules: 0, required: true },
{ key: 'user_name', cn: '姓名', en: 'user_name', rules: 0, required: true },
{ key: 'id_card', cn: '身份证号', en: 'id_card', rules: 3, required: true },
{ key: 'gender', cn: '性别', en: 'gender', rules: 1, required: false },
{ key: 'birth_date', cn: '出生日期', en: 'birth_date', rules: 1, required: false },
{ key: 'mobile', cn: '手机号', en: 'mobile', rules: 1, required: false },
{ key: 'address', cn: '地址', en: 'address', rules: 0, required: false },
{ key: 'email', cn: '邮箱', en: 'email', rules: 0, required: false },
],
mobile: [
{ key: 'user_id', cn: '用户ID', en: 'user_id', rules: 0, required: true },
{ key: 'mobile', cn: '手机号', en: 'mobile', rules: 1, required: true },
{ key: 'user_name',cn: '姓名', en: 'user_name',rules: 0, required: false },
],
email: [
{ key: 'user_id', cn: '用户ID', en: 'user_id', rules: 0, required: true },
{ key: 'email', cn: '邮箱', en: 'email', rules: 1, required: true },
],
address: [
{ key: 'user_id', cn: '用户ID', en: 'user_id', rules: 0, required: true },
{ key: 'address', cn: '地址', en: 'address', rules: 1, required: true },
],
'bank-card': [
{ key: 'user_id', cn: '用户ID', en: 'user_id', rules: 0, required: true },
{ key: 'bank_card', cn: '银行卡号', en: 'bank_card', rules: 2, required: true },
],
date: [
{ key: 'order_id', cn: '订单ID', en: 'order_id', rules: 0, required: true },
{ key: 'order_date', cn: '下单日期', en: 'order_date', rules: 2, required: true },
],
}
// 任务名称下拉
export const taskOptionsByType = {
'id-card': ['身份证_用户信息表', '身份证_客户主表', '身份证_员工档案表'],
mobile: ['手机号_用户信息表'],
email: ['邮箱_订阅用户'],
address: ['地址信息_收货表'],
'bank-card':['银行卡号_用户信息表'],
date: ['日期格式_订单'],
}
// 不合规数据行 —— cells 顺序对应 fieldsByType[type]
// errorCells: 哪些列有错误(用于打红框 + 错误提示),issues: 备注里的多条说明
export const violationRows = [
{
user_id: '10023', user_name: '张三',
id_card: '1101011990010', gender: '男', birth_date: '1990-01-01', mobile: '13800130000', address: '北京市朝阳区',
errorCells: ['id_card'],
issues: [
{ field: '身份证号', desc: '长度不足18位' },
{ field: '身份证号', desc: '缺少校验位' },
],
},
{
user_id: '10045', user_name: '李四',
id_card: '110101199001011234', gender: '男', birth_date: '1990-01-01', mobile: '13800130001', address: '北京市海淀区',
errorCells: ['id_card', 'gender', 'birth_date'],
issues: [
{ field: '身份证号', desc: '校验位错误(应为3,实际4)' },
{ field: '性别', desc: '与身份证第17位不一致(身份证为女)' },
{ field: '出生日期', desc: '与身份证位(7-14)不一致' },
],
},
{
user_id: '10078', user_name: '王五',
id_card: null, gender: '男', birth_date: '1985-05-20', mobile: '1380013', address: '上海市浦东新区',
errorCells: ['id_card', 'mobile'],
issues: [
{ field: '身份证号', desc: '必填字段为空' },
{ field: '手机号', desc: '长度不足11位' },
],
},
{
user_id: '10092', user_name: '赵六',
id_card: '11010119900202123X', gender: '男', birth_date: '1990-02-02', mobile: '13800130092', address: null,
errorCells: ['address'],
issues: [
{ field: '地址', desc: '为空,但身份证地区码非空' },
],
},
{
user_id: '10105', user_name: '钱七',
id_card: '11010119900303', gender: '女', birth_date: '1990-03-03', mobile: '13800130105', address: '北京市西城区',
errorCells: ['id_card'],
issues: [
{ field: '身份证号', desc: '长度仅15位,应为18位(旧版15位)' },
],
},
]
// 任务配置列表
export const taskList = [
{ name: '身份证_用户信息表', group: '身份证', db: 'customer_db', table: 't_user_info', fields: 10, rules: 6, status: '启用', checkedAt: '2026-08-19 09:00', tagColor: 'blue' },
{ name: '身份证_客户主表', group: '身份证', db: 'customer_db', table: 't_customer', fields: 8, rules: 4, status: '启用', checkedAt: '2026-08-19 09:00', tagColor: 'blue' },
{ name: '身份证_员工档案表', group: '身份证', db: 'hr_db', table: 't_employee', fields: 12, rules: 5, status: '停用', checkedAt: '2026-08-18 18:00', tagColor: 'blue' },
{ name: '手机号_用户信息表', group: '手机号', db: 'customer_db', table: 't_user_info', fields: 10, rules: 2, status: '启用', checkedAt: '2026-08-19 09:00', tagColor: 'orange' },
{ name: '邮箱_订阅用户', group: '邮箱', db: 'marketing_db', table: 't_subscriber', fields: 6, rules: 1, status: '启用', checkedAt: '2026-08-19 09:00', tagColor: 'purple' },
{ name: '日期格式_订单', group: '日期格式', db: 'order_db', table: 't_order_detail', fields: 15, rules: 3, status: '启用', checkedAt: '2026-08-19 09:00', tagColor: 'green' },
]
// 字段规则样例
export const fieldRuleSamples = {
id_card: [
{ desc: '必须是18位中国大陆身份证号,前6位地区码,中8位出生日期,最后一位校验位可为数字或X',
regex: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$' },
{ desc: '身份证不能为空', regex: '^.+$' },
{ desc: '必须是18位,不能是15位旧版', regex: '' },
],
mobile: [
{ desc: '必须是11位手机号', regex: '^1[3-9]\\d{9}$' },
],
email: [
{ desc: '必须是合法邮箱', regex: '^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$' },
],
address: [
{ desc: '地址不能为空', regex: '^.+$' },
],
bank_card: [
{ desc: '银行卡号16-19位纯数字', regex: '^\\d{16,19}$' },
{ desc: '不能为空', regex: '^.+$' },
],
order_date: [
{ desc: '日期格式 yyyy-MM-dd', regex: '^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$' },
{ desc: '日期不能为空', regex: '^.+$' },
],
}
\ No newline at end of file
<template>
<el-container class="layout-root">
<!-- 左侧菜单 -->
<el-aside class="layout-aside" width="220px">
<div class="sidebar-logo">数据质量平台</div>
<el-menu
:default-active="activeMenu"
class="layout-menu"
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409eff"
@select="onMenuSelect"
>
<div class="menu-group-title">校验任务</div>
<el-menu-item
v-for="t in taskMenus"
:key="t.path"
:index="t.path"
>
<el-icon><component :is="t.icon" /></el-icon>
<template #title>{{ t.title }}</template>
</el-menu-item>
<div class="menu-group-title">管理</div>
<el-menu-item index="/config">
<el-icon><Setting /></el-icon>
<template #title>任务配置</template>
</el-menu-item>
</el-menu>
<div class="sidebar-footer">v1.0.0</div>
</el-aside>
<el-container class="layout-body">
<!-- 顶栏 -->
<el-header class="layout-header">
<div class="breadcrumb">
<span>数据质量</span>
<span class="breadcrumb-sep">/</span>
<span class="breadcrumb-current">{{ currentTitle }}</span>
</div>
<div class="header-right">
<span>检测人:张三</span>
<span class="header-divider">|</span>
<span>{{ todayStr }}</span>
</div>
</el-header>
<!-- 内容区 -->
<el-main class="layout-main">
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 校验任务子菜单
const taskMenus = [
{ path: '/quality/id-card', title: '身份证', icon: 'IdCard' },
{ path: '/quality/mobile', title: '手机号', icon: 'Cellphone' },
{ path: '/quality/email', title: '邮箱', icon: 'Message' },
{ path: '/quality/address', title: '地址信息', icon: 'Location' },
{ path: '/quality/bank-card', title: '银行卡号', icon: 'CreditCard' },
{ path: '/quality/date', title: '日期格式', icon: 'Calendar' },
]
const activeMenu = computed(() => route.path)
// 当前面包屑标题
const currentTitle = computed(() => {
if (route.path.startsWith('/quality')) {
const t = taskMenus.find((m) => m.path === route.path)
return t ? t.title : '数据质量'
}
if (route.path === '/config') return '任务配置'
return '数据质量'
})
const todayStr = computed(() => {
const d = new Date()
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 09:30`
})
function onMenuSelect(index) {
if (route.path !== index) router.push(index)
}
</script>
<style scoped>
.layout-root {
height: 100vh;
}
/* 侧边栏 */
.layout-aside {
background: #304156;
display: flex;
flex-direction: column;
}
.sidebar-logo {
height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
color: #fff;
font-size: 15px;
border-bottom: 1px solid #1d2b3a;
background: #2b3a4d;
}
.layout-menu {
border-right: none;
flex: 1;
overflow-y: auto;
}
.menu-group-title {
padding: 10px 16px 6px;
font-size: 12px;
color: #6b7c93;
letter-spacing: 1px;
}
.layout-menu :deep(.el-menu-item) {
font-size: 15px;
height: 44px;
line-height: 44px;
border-left: 3px solid transparent;
}
.layout-menu :deep(.el-menu-item.is-active) {
background: #1f2d3d !important;
border-left-color: #409eff;
}
.layout-menu :deep(.el-menu-item:hover) {
background: #263445 !important;
}
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid #1d2b3a;
font-size: 12px;
color: #6b7c93;
}
/* 顶栏 */
.layout-header {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border-bottom: 1px solid #e8e8e8;
padding: 0 20px;
height: 50px;
}
.breadcrumb {
font-size: 15px;
color: #606266;
}
.breadcrumb-sep {
color: #c0c4cc;
margin: 0 6px;
}
.breadcrumb-current {
color: #1f2937;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: #606266;
}
.header-divider {
color: #c0c4cc;
}
/* 主体 */
.layout-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.layout-main {
background: #f0f2f5;
padding: 16px 20px;
overflow: auto;
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import './styles/global.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册所有图标(全局可用)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus, { locale: zhCn })
app.use(router)
app.mount('#app')
\ No newline at end of file
import { createRouter, createWebHashHistory } from 'vue-router'
import MainLayout from '@/layouts/MainLayout.vue'
import DataQualityView from '@/views/DataQualityView.vue'
import TaskConfigView from '@/views/TaskConfigView.vue'
const routes = [
{
path: '/',
component: MainLayout,
redirect: '/quality/id-card',
children: [
{
// 校验任务菜单下的多种类型(身份证/手机号/邮箱/地址/银行卡/日期)
// 用 path param 区分当前选中的数据类型
path: 'quality/:type',
name: 'DataQuality',
component: DataQualityView,
},
{
path: 'config',
name: 'TaskConfig',
component: TaskConfigView,
meta: { title: '任务配置', icon: 'Setting' },
},
],
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
\ No newline at end of file
/* web3 共享样式 —— 表格、按钮、抽屉、弹窗、tag 等 */
/* ===== 通用按钮 ===== */
.btn {
height: 32px;
padding: 0 16px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #333;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.2s;
font-family: inherit;
}
.btn:hover { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; }
.btn-primary { background: #409eff; color: #fff; border-color: #409eff; }
.btn-primary:hover { background: #66b1ff; border-color: #66b1ff; color: #fff; }
.btn-success { background: #67c23a; color: #fff; border-color: #67c23a; }
.btn-success:hover { background: #85ce61; border-color: #85ce61; color: #fff; }
.btn-danger { background: #f56c6c; color: #fff; border-color: #f56c6c; }
.btn-danger:hover { background: #f78989; border-color: #f78989; color: #fff; }
.btn-text { border: none; background: transparent; color: #409eff; padding: 0 4px; height: auto; font-size: 13px; cursor: pointer; font-family: inherit; }
.btn-text:hover { color: #66b1ff; background: transparent; }
.btn-text.danger:hover { color: #f56c6c; }
/* ===== tag ===== */
.tag { display: inline-block; padding: 1px 8px; border-radius: 3px; font-size: 12px; line-height: 18px; border: 1px solid; }
.tag-blue { color: #409eff; background: #ecf5ff; border-color: #d9ecff; }
.tag-green { color: #67c23a; background: #f0f9eb; border-color: #e1f3d8; }
.tag-gray { color: #909399; background: #f4f4f5; border-color: #e9e9eb; }
.tag-orange { color: #e6a23c; background: #fdf6ec; border-color: #faecd8; }
.tag-purple { color: #6c5ce7; background: #f0ecff; border-color: #e0d8ff; }
/* ===== 卡片 ===== */
.q-card {
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
overflow: hidden;
margin-bottom: 12px;
}
.q-card__header {
padding: 10px 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
background: #fafafa;
}
.q-card__title { font-size: 14px; color: #1f2937; }
.q-card__title .count { color: #f56c6c; margin-left: 8px; font-size: 13px; }
.q-card__title .count.gray { color: #909399; }
.q-card__actions { display: flex; gap: 8px; align-items: center; }
/* ===== 查询区 ===== */
.query-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.query-item { display: flex; align-items: center; gap: 6px; }
.query-label { font-size: 14px; color: #606266; white-space: nowrap; }
.query-label .required { color: #f56c6c; margin-right: 2px; }
.select-input, .text-input {
height: 32px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #333;
outline: none;
font-family: inherit;
background: #fff;
}
.select-input { min-width: 280px; padding-right: 28px; }
.text-input { min-width: 160px; }
.select-input:focus, .text-input:focus { border-color: #409eff; }
.query-actions { margin-left: auto; display: flex; gap: 8px; }
/* ===== 任务信息条 ===== */
.task-info-bar {
background: #ecf5ff;
border: 1px solid #d9ecff;
border-radius: 4px;
padding: 8px 12px;
margin-bottom: 12px;
font-size: 13px;
color: #409eff;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.task-info-bar .meta { color: #606266; margin-left: 12px; font-size: 12px; }
/* ===== 数据表格(不公用 Element Plus,自定义风格贴近 mock) ===== */
.table-scroll { overflow-x: auto; }
table.dq-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
min-width: 900px;
}
table.dq-table thead th {
background: #fafafa;
padding: 10px 12px;
text-align: left;
color: #606266;
border-bottom: 1px solid #ebeef5;
white-space: nowrap;
}
table.dq-table thead th .cn { color: #1f2937; font-size: 13px; }
table.dq-table thead th .en { color: #909399; font-size: 11px; margin-left: 4px; }
table.dq-table thead th .rule-mark {
color: #e6a23c;
margin-left: 4px;
font-size: 12px;
cursor: help;
}
table.dq-table thead th.col-reason {
background: #fde2e2;
color: #c0392b;
border-bottom-color: #f9c6c6;
}
table.dq-table thead th.col-reason .cn { color: #c0392b; }
table.dq-table thead th.col-reason .flag {
display: inline-block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
background: #f56c6c;
color: #fff;
border-radius: 50%;
font-size: 11px;
margin-right: 4px;
vertical-align: middle;
}
table.dq-table td.cell-reason {
background: #fef7f7;
color: #c0392b;
border-left: 3px solid #f56c6c;
border-bottom: 1px solid #f0f2f5;
min-width: 200px;
max-width: 260px;
}
table.dq-table td.cell-reason .issue-line { display: block; line-height: 1.7; padding-left: 4px; }
table.dq-table tbody td {
padding: 8px 12px;
border-bottom: 1px solid #f0f2f5;
color: #333;
vertical-align: middle;
}
table.dq-table tbody tr:hover { background: #f5f7fa; }
table.dq-table tbody tr:last-child td { border-bottom: none; }
/* 单元格错误标记 */
.cell-error {
background: #fef0f0;
color: #c0392b;
position: relative;
cursor: help;
}
.cell-error .err-flag {
display: inline-block;
width: 14px;
height: 14px;
line-height: 14px;
text-align: center;
background: #f56c6c;
color: #fff;
border-radius: 50%;
font-size: 10px;
margin-left: 4px;
vertical-align: middle;
}
.cell-error .err-tip {
position: absolute;
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
background: #303030;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10;
display: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
line-height: 1.4;
}
.cell-error .err-tip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #303030;
}
.cell-error:hover .err-tip { display: block; }
.null-val { color: #c0c4cc; font-style: italic; }
/* ===== 分页 ===== */
.pagination {
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #ebeef5;
background: #fafafa;
flex-wrap: wrap;
gap: 8px;
}
.pagination-info { font-size: 13px; color: #606266; }
.pagination-info .highlight { color: #f56c6c; }
.pagination-controls { display: flex; gap: 4px; align-items: center; }
.page-btn {
min-width: 30px;
height: 30px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 3px;
cursor: pointer;
font-size: 13px;
color: #606266;
padding: 0 8px;
font-family: inherit;
}
.page-btn.active {
background: #409eff;
color: #fff;
border-color: #409eff;
}
.page-btn:hover:not(.active) { color: #409eff; border-color: #c6e2ff; }
/* ===== 抽屉 ===== */
.drawer-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 200;
display: none;
}
.drawer-mask.show { display: block; }
.drawer {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 340px;
background: #fff;
z-index: 201;
transform: translateX(100%);
transition: transform 0.3s;
display: flex;
flex-direction: column;
}
.drawer.show { transform: translateX(0); }
.drawer-header {
padding: 14px 18px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.drawer-title { font-size: 15px; color: #1f2937; }
.drawer-close { cursor: pointer; color: #909399; font-size: 20px; line-height: 1; background: none; border: none; }
.drawer-close:hover { color: #f56c6c; }
.drawer-body { flex: 1; overflow-y: auto; padding: 14px 18px; }
.drawer-hint { font-size: 12px; color: #909399; margin-bottom: 10px; line-height: 1.5; }
.field-check-list { display: flex; flex-direction: column; }
.field-check-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 8px;
border-radius: 4px;
cursor: pointer;
}
.field-check-item:hover { background: #f5f7fa; }
.field-check-item .cn { color: #1f2937; }
.field-check-item .en { color: #909399; font-size: 12px; margin-left: 4px; }
.drawer-footer {
padding: 10px 18px;
border-top: 1px solid #ebeef5;
display: flex;
gap: 8px;
}
.drawer-footer .btn { flex: 1; }
/* ===== 弹窗 ===== */
.modal-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 300;
display: none;
justify-content: center;
align-items: center;
}
.modal-mask.show { display: flex; }
.modal {
background: #fff;
border-radius: 6px;
width: 880px;
max-width: 94vw;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal.modal-sm { width: 680px; }
.modal-header {
padding: 14px 20px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title { font-size: 15px; color: #1f2937; }
.modal-close {
cursor: pointer;
color: #909399;
font-size: 20px;
line-height: 1;
background: none;
border: none;
}
.modal-close:hover { color: #f56c6c; }
.modal-body { flex: 1; overflow-y: auto; }
.modal-footer {
padding: 12px 20px;
border-top: 1px solid #ebeef5;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* ===== 弹窗 Tab ===== */
.modal-tabs {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 0 20px;
background: #fafafa;
}
.modal-tab {
padding: 12px 22px;
cursor: pointer;
font-size: 14px;
color: #606266;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.modal-tab:hover { color: #409eff; }
.modal-tab.active { color: #409eff; border-bottom-color: #409eff; }
.tab-panel { display: none; padding: 18px 20px; }
.tab-panel.active { display: block; }
/* ===== 表单 ===== */
.form-section { margin-bottom: 22px; }
.form-section-title {
font-size: 14px;
color: #1f2937;
padding-left: 8px;
border-left: 3px solid #409eff;
margin-bottom: 12px;
}
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 20px; }
.form-row { display: flex; align-items: center; gap: 8px; }
.form-row.full { grid-column: 1 / -1; }
.form-label {
font-size: 13px;
color: #606266;
width: 90px;
flex-shrink: 0;
text-align: right;
}
.form-label .required { color: #f56c6c; margin-right: 2px; }
.form-control { flex: 1; }
.form-control .select-input,
.form-control .text-input { width: 100%; min-width: 0; height: 32px; }
.form-control textarea {
width: 100%;
min-height: 60px;
padding: 6px 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
resize: vertical;
outline: none;
}
.form-control textarea:focus { border-color: #409eff; }
.form-hint { font-size: 12px; color: #909399; margin-top: 4px; }
.conn-tabs {
display: flex;
border-bottom: 1px solid #ebeef5;
margin-bottom: 12px;
flex-wrap: wrap;
}
.conn-tab {
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
color: #606266;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.conn-tab.active { color: #409eff; border-bottom-color: #409eff; }
.conn-tab:hover { color: #409eff; }
/* ===== 字段表(弹窗内) ===== */
.field-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 1px solid #ebeef5;
}
.field-table th {
background: #fafafa;
padding: 10px 12px;
text-align: left;
color: #606266;
border-bottom: 1px solid #ebeef5;
font-weight: normal;
white-space: nowrap;
}
.field-table td {
padding: 8px 12px;
border-bottom: 1px solid #f0f2f5;
vertical-align: middle;
}
.field-table tr:hover { background: #f5f7fa; }
.field-table tr:last-child td { border-bottom: none; }
.field-table .text-input { height: 30px; width: 100%; min-width: 0; font-family: inherit; }
.field-name-cell { color: #1f2937; }
.field-name-cell .en { color: #909399; font-size: 11px; margin-left: 4px; }
.rule-set-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
color: #606266;
font-family: inherit;
white-space: nowrap;
}
.rule-set-btn:hover { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; }
.rule-set-btn .num { background: #409eff; color: #fff; border-radius: 9px; padding: 0 6px; line-height: 16px; font-size: 11px; }
.rule-set-btn.empty .num { background: #c0c4cc; }
/* ===== 规则弹窗内的单条规则 ===== */
.rule-item {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 14px 16px;
margin-bottom: 12px;
background: #fafbfc;
}
.rule-item-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
}
.rule-item-head .rnum { color: #409eff; font-size: 13px; }
.rule-item-row {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.rule-item-row:last-child { margin-bottom: 0; }
.rule-item-label {
font-size: 13px;
color: #606266;
width: 80px;
flex-shrink: 0;
text-align: right;
padding-top: 6px;
}
.rule-item-ctrl { flex: 1; }
.rule-item-ctrl textarea {
width: 100%;
min-height: 44px;
padding: 6px 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
resize: vertical;
outline: none;
}
.rule-item-ctrl textarea:focus { border-color: #409eff; }
.rule-item-ctrl .text-input { width: 100%; min-width: 0; }
.regex-line { display: flex; gap: 8px; align-items: center; }
.regex-line .text-input {
flex: 1;
font-family: Consolas, monospace;
font-size: 12px;
color: #6c5ce7;
}
.test-line { display: flex; gap: 8px; align-items: center; }
.test-line .text-input { flex: 1; }
.test-result { font-size: 12px; white-space: nowrap; }
.test-result.pass { color: #67c23a; }
.test-result.fail { color: #f56c6c; }
.ai-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
}
.ai-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
color: #fff;
border-color: transparent;
}
.ai-btn:disabled { opacity: 0.6; cursor: wait; }
.add-rule-btn {
width: 100%;
padding: 10px;
border: 1px dashed #dcdfe6;
background: #fafafa;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
color: #909399;
font-family: inherit;
}
.add-rule-btn:hover { border-color: #409eff; color: #409eff; background: #ecf5ff; }
.rule-empty-tip { text-align: center; padding: 30px 20px; color: #909399; font-size: 13px; }
/* ===== 任务配置视图列表 ===== */
.config-toolbar {
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
padding: 12px 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.config-toolbar .left { display: flex; align-items: center; gap: 12px; }
.config-toolbar .right { display: flex; gap: 8px; }
table.config-list { width: 100%; border-collapse: collapse; font-size: 13px; }
table.config-list thead th {
background: #fafafa;
padding: 10px 12px;
text-align: left;
color: #606266;
border-bottom: 1px solid #ebeef5;
white-space: nowrap;
font-weight: normal;
}
table.config-list tbody td {
padding: 9px 12px;
border-bottom: 1px solid #f0f2f5;
color: #333;
}
table.config-list tbody tr:hover { background: #f5f7fa; }
table.config-list tbody tr:last-child td { border-bottom: none; }
\ No newline at end of file
<template>
<!-- 查询区 -->
<div class="q-card" style="padding: 14px 16px;">
<div class="query-row">
<div class="query-item">
<span class="query-label"><span class="required">*</span>任务名称:</span>
<select class="select-input" v-model="selectedTask">
<option v-for="t in taskOptions" :key="t" :value="t">{{ t }}</option>
</select>
<button class="btn btn-primary" @click="onQuery">查询</button>
</div>
</div>
</div>
<div class="task-info-bar">
<el-icon><InfoFilled /></el-icon>
<span>当前任务:<b>{{ selectedTask }}</b></span>
<span class="meta">最近检测:2026-08-19 09:00 · 共扫描 10,234 条 · 命中不合规 <b style="color:#f56c6c;">12</b> 条</span>
</div>
<!-- 结果区 -->
<div class="q-card">
<div class="q-card__header">
<div class="q-card__title">不合规数据明细<span class="count">共 12 条</span></div>
<div class="q-card__actions">
<button class="btn" @click="fieldDrawerOpen = true">字段配置</button>
<button class="btn">导出</button>
<button class="btn btn-success" @click="onImport">导入</button>
<button class="btn btn-danger" @click="batchDelete">批量删除</button>
</div>
</div>
<div class="table-scroll">
<table class="dq-table">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="checkbox" :checked="allChecked" @change="toggleAll" />
</th>
<th v-for="f in fields" :key="f.key" :class="{ 'col-reason': f.key === '__reason' }">
<template v-if="f.key === '__reason'">
<span class="flag">!</span><span class="cn">备注</span>
</template>
<template v-else>
<span class="cn">{{ f.cn }}</span>
<span class="en">{{ f.en }}</span>
<span v-if="f.rules > 0" class="rule-mark" :title="`校验规则:${ruleHint(f)}`">⚠ {{ f.rules }}规则</span>
</template>
</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in pagedRows" :key="i">
<td style="text-align: center;">
<input type="checkbox" class="checkbox" v-model="row.checked" />
</td>
<td v-for="f in fields" :key="f.key" :class="cellClass(row, f.key)">
<template v-if="f.key === '__reason'">
<span v-for="(it, idx) in row.issues" :key="idx" class="issue-line">• {{ it.field }}:{{ it.desc }}</span>
</template>
<template v-else-if="isErrorCell(row, f.key)">
<span :class="{ 'null-val': row[f.key] === null || row[f.key] === undefined }">
{{ row[f.key] === null || row[f.key] === undefined ? '(NULL)' : row[f.key] }}
</span>
<span class="err-flag">!</span>
<span class="err-tip">{{ errTip(row, f.key) }}</span>
</template>
<template v-else>
<span :class="{ 'null-val': row[f.key] === null || row[f.key] === undefined }">
{{ row[f.key] === null || row[f.key] === undefined ? '(NULL)' : row[f.key] }}
</span>
</template>
</td>
<td>
<button class="btn-text" @click="editRow(row)">修改</button>
<button class="btn-text danger" @click="deleteRow(row)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pagination-info">显示第 {{ rangeStart }}-{{ rangeEnd }} 条 / 共 <span class="highlight">12</span> 条不合规数据</div>
<div class="pagination-controls">
<button class="page-btn" :disabled="page === 1" @click="page--">‹</button>
<button
v-for="p in totalPages"
:key="p"
class="page-btn"
:class="{ active: page === p }"
@click="page = p"
>{{ p }}</button>
<button class="page-btn" :disabled="page === totalPages" @click="page++">›</button>
</div>
</div>
</div>
<!-- 字段配置抽屉 -->
<FieldConfigDrawer
v-model:open="fieldDrawerOpen"
:fields="fields"
:visible-fields="visibleFields"
@save="onSaveFields"
/>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fieldsByType, taskOptionsByType, violationRows, fieldRuleSamples } from '@/data/mockData'
import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue'
const route = useRoute()
// 当前数据类型(来自路由 param)
const type = computed(() => route.params.type || 'id-card')
// 表格字段:业务字段 + 末尾的「备注」列
const fields = computed(() => [...(fieldsByType[type.value] || []), { key: '__reason', cn: '备注' }])
const taskOptions = computed(() => taskOptionsByType[type.value] || [])
const selectedTask = ref(taskOptions.value[0])
watch(taskOptions, (opts) => { selectedTask.value = opts[0] })
// 字段展示开关(默认全开)
const visibleFields = ref(fields.value.filter((f) => f.key !== '__reason').map((f) => f.key))
watch(fields, (fs) => { visibleFields.value = fs.filter((f) => f.key !== '__reason').map((f) => f.key) })
const rows = ref(violationRows.map((r) => ({ ...r, checked: false })))
// 分页
const page = ref(1)
const pageSize = 5
const totalPages = computed(() => Math.max(1, Math.ceil(rows.value.length / pageSize)))
const pagedRows = computed(() => {
const start = (page.value - 1) * pageSize
return rows.value.slice(start, start + pageSize)
})
const rangeStart = computed(() => rows.value.length === 0 ? 0 : (page.value - 1) * pageSize + 1)
const rangeEnd = computed(() => Math.min(page.value * pageSize, rows.value.length))
// 全选
const allChecked = computed(() => pagedRows.value.length > 0 && pagedRows.value.every((r) => r.checked))
function toggleAll(e) {
const v = e.target.checked
pagedRows.value.forEach((r) => { r.checked = v })
}
// 错误单元格判定
function isErrorCell(row, key) {
return Array.isArray(row.errorCells) && row.errorCells.includes(key)
}
function errTip(row, key) {
if (!isErrorCell(row, key)) return ''
const f = fields.value.find((x) => x.key === key)
const fieldLabel = f ? f.cn : key
const issue = (row.issues || []).find((it) => it.field === fieldLabel)
return issue ? issue.desc : '该字段不合法'
}
function cellClass(row, key) {
return {
'cell-error': isErrorCell(row, key),
'cell-reason': key === '__reason',
}
}
function ruleHint(f) {
return fieldRuleSamples[f.key]?.map((r) => r.desc).join(';') || ''
}
// 操作
function onQuery() {
ElMessage.success(`已查询:${selectedTask.value}`)
}
function editRow(row) {
ElMessageBox.alert(`打开「${row.user_id}」该条数据的编辑表单`, '修改')
}
function deleteRow(row) {
ElMessageBox.confirm('确定删除该条数据?', '提示', { type: 'warning' })
.then(() => {
const i = rows.value.indexOf(row)
if (i >= 0) rows.value.splice(i, 1)
ElMessage.success('已删除')
}).catch(() => {})
}
function batchDelete() {
const checked = rows.value.filter((r) => r.checked)
if (checked.length === 0) {
ElMessage.warning('请先勾选要删除的数据')
return
}
ElMessageBox.confirm(`确定删除选中的 ${checked.length} 条数据?`, '提示', { type: 'warning' })
.then(() => {
rows.value = rows.value.filter((r) => !r.checked)
ElMessage.success('已删除')
}).catch(() => {})
}
function onImport() {
ElMessage.info('触发导入(mock:实际将请求后端上传接口)')
}
// 抽屉保存
function onSaveFields(list) {
visibleFields.value = list
fieldDrawerOpen.value = false
ElMessage.success('已保存展示字段')
}
const fieldDrawerOpen = ref(false)
</script>
\ No newline at end of file
<template>
<!-- 工具栏 -->
<div class="config-toolbar">
<div class="left">
<span class="query-label">任务分组:</span>
<select class="select-input" style="min-width: 160px;" v-model="groupFilter">
<option value="">全部分组</option>
<option v-for="g in groups" :key="g" :value="g">{{ g }}</option>
</select>
<span class="query-label">关键字:</span>
<input type="text" class="text-input" placeholder="任务名称/数据表" style="min-width: 200px;" v-model="keyword" />
<button class="btn btn-primary" @click="onQuery">查询</button>
<button class="btn" @click="onReset">重置</button>
</div>
<div class="right">
<button class="btn btn-success" @click="openAdd">+ 新增任务</button>
</div>
</div>
<!-- 任务列表 -->
<div class="q-card">
<div class="q-card__header">
<div class="q-card__title">任务列表<span class="count gray">共 {{ filteredList.length }} 个</span></div>
</div>
<div class="table-scroll">
<table class="config-list">
<thead>
<tr>
<th>任务名称</th><th>分组</th><th>数据库</th><th>数据表</th>
<th>字段数</th><th>规则数</th><th>状态</th><th>最近检测</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in filteredList" :key="i">
<td>{{ row.name }}</td>
<td><span class="tag" :class="`tag-${row.tagColor}`">{{ row.group }}</span></td>
<td>{{ row.db }}</td>
<td>{{ row.table }}</td>
<td>{{ row.fields }}</td>
<td>{{ row.rules }}</td>
<td>
<span class="tag" :class="row.status === '启用' ? 'tag-green' : 'tag-gray'">{{ row.status }}</span>
</td>
<td>{{ row.checkedAt }}</td>
<td>
<button class="btn-text" @click="openEdit(row)">编辑</button>
<button class="btn-text">复制</button>
<button class="btn-text danger" @click="onDelete(row)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pagination-info">共 {{ filteredList.length }} 个任务</div>
<div class="pagination-controls">
<button class="page-btn" disabled>‹</button>
<button class="page-btn active">1</button>
<button class="page-btn" disabled>›</button>
</div>
</div>
</div>
<!-- 任务编辑弹窗 -->
<TaskModal v-model:open="modalOpen" :mode="modalMode" :task="editingTask" @saved="onSaved" />
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { taskList } from '@/data/mockData'
import TaskModal from '@/components/TaskModal.vue'
const list = ref([...taskList])
const groupFilter = ref('')
const keyword = ref('')
const groups = computed(() => [...new Set(list.value.map((r) => r.group))])
const filteredList = computed(() => {
return list.value.filter((r) => {
if (groupFilter.value && r.group !== groupFilter.value) return false
if (keyword.value && !(r.name.includes(keyword.value) || r.table.includes(keyword.value))) return false
return true
})
})
function onQuery() { /* mock: 过滤已实时生效 */ }
function onReset() { groupFilter.value = ''; keyword.value = '' }
const modalOpen = ref(false)
const modalMode = ref('add')
const editingTask = ref(null)
function openAdd() {
modalMode.value = 'add'
editingTask.value = null
modalOpen.value = true
}
function openEdit(row) {
modalMode.value = 'edit'
editingTask.value = row
modalOpen.value = true
}
function onSaved(payload) {
if (modalMode.value === 'add') {
list.value.unshift(payload)
ElMessage.success('已新增任务')
} else {
const i = list.value.findIndex((r) => r.name === editingTask.value?.name)
if (i >= 0) list.value[i] = { ...list.value[i], ...payload }
ElMessage.success('已保存修改')
}
modalOpen.value = false
}
function onDelete(row) {
ElMessageBox.confirm(`确定删除任务「${row.name}」?`, '提示', { type: 'warning' })
.then(() => {
list.value = list.value.filter((r) => r.name !== row.name)
ElMessage.success('已删除')
}).catch(() => {})
}
</script>
\ No newline at end of file
@echo off
REM ============================================================
REM web3 - Windows launcher (frontend only)
REM
REM 启动后:
REM - 前端 Vite dev server http://localhost:5175
REM
REM 关闭此窗口或 Ctrl+C 即可停止 dev server。
REM ============================================================
REM Switch to project root (parent of web3/)
cd /d "%~dp0\.."
echo.
echo ============================================================
echo web3 - Data Governance Tool (frontend only)
echo ============================================================
echo.
REM Check Node + npm
where npm >nul 2>&1
if errorlevel 1 (
echo [ERROR] npm not found. Please install Node.js 18+ first.
pause
exit /b 1
)
REM Check if node_modules exists
if not exist "web3\node_modules" (
echo [INFO] Installing frontend deps...
cd web3 && npm install && cd ..
if errorlevel 1 (
echo [ERROR] npm install failed.
pause
exit /b 1
)
)
echo Starting frontend in new window...
start "web3-frontend" cmd /k "cd /d %CD%\web3 && npm run dev"
echo.
echo ============================================================
echo Frontend starting at http://localhost:5175
echo.
echo Close this window or press any key to dismiss.
echo ============================================================
echo.
pause
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5175,
open: false,
},
})
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment