docs(prototype): confirm p0 web and app flows
This commit is contained in:
@@ -53,7 +53,8 @@
|
|||||||
|
|
||||||
当前已完成 Phase 0 和 Phase 1:Android 可运行、设备就绪、workflow、私有样本导入、
|
当前已完成 Phase 0 和 Phase 1:Android 可运行、设备就绪、workflow、私有样本导入、
|
||||||
动态词搜索、最多 5 个候选截图采集、结构化需求提取、候选评估和人工确认停止点均已
|
动态词搜索、最多 5 个候选截图采集、结构化需求提取、候选评估和人工确认停止点均已
|
||||||
验证。T-201 后端骨架也已完成,下一步是 T-202,生成并确认 P0 Web/App 低保真原型。
|
验证。T-201 后端骨架和 T-202 P0 Web/App 低保真原型均已完成,原型已通过自动验证
|
||||||
|
和人工确认。下一步按编号开始 T-203,实现任务创建 API 与管理 Web。
|
||||||
|
|
||||||
严格按以下顺序推进:
|
严格按以下顺序推进:
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
- 角色:采购管理员
|
- 角色:采购管理员
|
||||||
- 前置条件:种子管理账号已建立。
|
- 前置条件:种子管理账号已建立。
|
||||||
- 服务依赖:管理会话创建。
|
- 服务依赖:管理会话创建。
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[管理登录](design/admin-login.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
- 页面:`/tasks/new`
|
- 页面:`/tasks/new`
|
||||||
- 角色:采购管理员
|
- 角色:采购管理员
|
||||||
- 服务依赖:`POST /api/v1/assets`、`POST /api/v1/tasks`
|
- 服务依赖:`POST /api/v1/assets`、`POST /api/v1/tasks`
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[新建任务](design/admin-task-create.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -76,7 +76,8 @@
|
|||||||
- 页面:`/tasks`、`/tasks/{id}`
|
- 页面:`/tasks`、`/tasks/{id}`
|
||||||
- 角色:采购管理员
|
- 角色:采购管理员
|
||||||
- 服务依赖:任务列表、详情、取消和资产读取 API。
|
- 服务依赖:任务列表、详情、取消和资产读取 API。
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[任务列表](design/admin-task-list.html)、
|
||||||
|
[任务详情](design/admin-task-detail.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -104,7 +105,8 @@
|
|||||||
- 页面:Android“登录/设备绑定”
|
- 页面:Android“登录/设备绑定”
|
||||||
- 角色:采购执行员
|
- 角色:采购执行员
|
||||||
- 服务依赖:`POST /api/v1/auth/token`、设备心跳。
|
- 服务依赖:`POST /api/v1/auth/token`、设备心跳。
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[App 登录](design/android-login.html)、
|
||||||
|
[设备设置](design/android-settings.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -129,7 +131,9 @@
|
|||||||
- 页面:Android“任务”
|
- 页面:Android“任务”
|
||||||
- 角色:采购执行员
|
- 角色:采购执行员
|
||||||
- 服务依赖:设备预检、`POST /api/v1/tasks/claim-next`
|
- 服务依赖:设备预检、`POST /api/v1/tasks/claim-next`
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[任务主页](design/android-task-home.html)、
|
||||||
|
[任务预览](design/android-task-preview.html)、
|
||||||
|
[设备设置](design/android-settings.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -155,7 +159,8 @@
|
|||||||
- 页面:Android“任务执行”及拼多多前台
|
- 页面:Android“任务执行”及拼多多前台
|
||||||
- 角色:采购执行员
|
- 角色:采购执行员
|
||||||
- 服务依赖:start、heartbeat、AI、event API 和 Android 自动化。
|
- 服务依赖:start、heartbeat、AI、event API 和 Android 自动化。
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[任务预览](design/android-task-preview.html)、
|
||||||
|
[任务执行](design/android-task-running.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -185,7 +190,8 @@
|
|||||||
- 页面:Android“候选确认”
|
- 页面:Android“候选确认”
|
||||||
- 角色:采购执行员
|
- 角色:采购执行员
|
||||||
- 服务依赖:`POST /api/v1/tasks/{id}/complete`
|
- 服务依赖:`POST /api/v1/tasks/{id}/complete`
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[候选确认](design/android-candidate-confirm.html)、
|
||||||
|
[任务结果](design/android-task-result.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
@@ -212,7 +218,9 @@
|
|||||||
- 页面:Android“任务执行/结果”、Web 任务详情
|
- 页面:Android“任务执行/结果”、Web 任务详情
|
||||||
- 角色:采购执行员、采购管理员
|
- 角色:采购执行员、采购管理员
|
||||||
- 服务依赖:fail、event、asset API。
|
- 服务依赖:fail、event、asset API。
|
||||||
- 关联原型:待 `T-301` 创建。
|
- 关联原型:[管理任务详情](design/admin-task-detail.html)、
|
||||||
|
[App 任务执行](design/android-task-running.html)、
|
||||||
|
[App 任务结果](design/android-task-result.html)(T-202)。
|
||||||
|
|
||||||
**正常路径**
|
**正常路径**
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-25
|
- 日期:2026-07-26
|
||||||
- 阶段:T-201 已完成,准备开始 T-202 P0 低保真原型
|
- 阶段:T-202 P0 Web/App 低保真原型已确认完成,准备开始 T-203
|
||||||
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 均已纳入 Git 历史
|
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201、T-202
|
||||||
|
均已纳入 Git 历史
|
||||||
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码
|
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码
|
||||||
- Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留
|
- Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留
|
||||||
- 后端:Go 1.23.0 + Gin 1.11.0 + SQLite + Goose 3.26.0 骨架已建立;Go Blueprint
|
- 后端:Go 1.23.0 + Gin 1.11.0 + SQLite + Goose 3.26.0 骨架已建立;Go Blueprint
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
共 26 份报告、166 次测试,0 failure、0 error、0 skipped
|
共 26 份报告、166 次测试,0 failure、0 error、0 skipped
|
||||||
- 后端测试:`GOTOOLCHAIN=local go test -count=1 ./...` 共 25 个测试通过;
|
- 后端测试:`GOTOOLCHAIN=local go test -count=1 ./...` 共 25 个测试通过;
|
||||||
`go vet ./...`、API/migration Windows 构建和根 `init.ps1` 均通过
|
`go vet ./...`、API/migration Windows 构建和根 `init.ps1` 均通过
|
||||||
|
- 原型:4 个管理 Web 页面和 7 个 Android 页面均可离线独立打开;Playwright
|
||||||
|
以 1440×900、390×844、360×800 验证 36 个页面/视口组合,无页面横向溢出、
|
||||||
|
脚本错误或外部请求,Android 可见交互控件均不小于 44px
|
||||||
- Workflow:纯 Kotlin runner 已支持步骤 timeout、最多 3 次 retry、安全阻塞、
|
- Workflow:纯 Kotlin runner 已支持步骤 timeout、最多 3 次 retry、安全阻塞、
|
||||||
用户停止和单 runner 并发拒绝;T-102 已接入搜索加有界候选采集五步
|
用户停止和单 runner 并发拒绝;T-102 已接入搜索加有界候选采集五步
|
||||||
- TaskSource:严格 CLI 已生成并验证真实私有 ProbeTask;默认 APK 不含私有 fixture
|
- TaskSource:严格 CLI 已生成并验证真实私有 ProbeTask;默认 APK 不含私有 fixture
|
||||||
@@ -51,6 +55,8 @@
|
|||||||
| `docs/tasks/T-103.md` | DONE | 私有任务需求 schema、硬约束、隐私边界和 VLM 适配器 |
|
| `docs/tasks/T-103.md` | DONE | 私有任务需求 schema、硬约束、隐私边界和 VLM 适配器 |
|
||||||
| `docs/tasks/T-104.md` | DONE | 动态搜索、候选严格评估、本地建议和人工确认点 |
|
| `docs/tasks/T-104.md` | DONE | 动态搜索、候选严格评估、本地建议和人工确认点 |
|
||||||
| `docs/tasks/T-201.md` | DONE | Go-Gin、SQLite、Goose migration 和健康检查骨架 |
|
| `docs/tasks/T-201.md` | DONE | Go-Gin、SQLite、Goose migration 和健康检查骨架 |
|
||||||
|
| `docs/tasks/T-202.md` | DONE | 生成并确认 P0 Web/App 低保真原型 |
|
||||||
|
| `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 |
|
||||||
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
|
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
|
||||||
| `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 |
|
| `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 |
|
||||||
| `android-buyer/task-contract/` | 已有 | Android/CLI 共享 ProbeTask 与 TaskSource |
|
| `android-buyer/task-contract/` | 已有 | Android/CLI 共享 ProbeTask 与 TaskSource |
|
||||||
@@ -60,9 +66,10 @@
|
|||||||
|
|
||||||
## 任务摘要
|
## 任务摘要
|
||||||
|
|
||||||
- 已完成:T-001 至 T-004、T-101 至 T-104,以及 T-201。
|
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201、T-202。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:T-202 生成并确认 P0 Web/App 低保真原型。
|
- 下一个可领取任务:T-203 实现任务创建 API 与管理 Web;T-204 也已满足依赖,
|
||||||
|
但按编号顺序在 T-203 之后处理。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -11,22 +11,29 @@
|
|||||||
- 使用假数据,不放真实账号、商品图、订单、地址、token 或生产 URL。
|
- 使用假数据,不放真实账号、商品图、订单、地址、token 或生产 URL。
|
||||||
- 使用语义化 `button`、`form`、`table`、`dialog` 等元素。
|
- 使用语义化 `button`、`form`、`table`、`dialog` 等元素。
|
||||||
|
|
||||||
建议文件:
|
P0 文件:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
index.html
|
||||||
admin-login.html
|
admin-login.html
|
||||||
admin-task-list.html
|
admin-task-list.html
|
||||||
admin-task-create.html
|
admin-task-create.html
|
||||||
admin-task-detail.html
|
admin-task-detail.html
|
||||||
android-login.html
|
android-login.html
|
||||||
android-task-home.html
|
android-task-home.html
|
||||||
|
android-task-preview.html
|
||||||
android-task-running.html
|
android-task-running.html
|
||||||
android-candidate-confirm.html
|
android-candidate-confirm.html
|
||||||
|
android-task-result.html
|
||||||
|
android-settings.html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`index.html` 只用于离线逐页导航。其余 11 个文件分别覆盖
|
||||||
|
[`routes.md`](../routes.md) 中的 4 条管理 Web 路由和 7 个 Android 逻辑目的地。
|
||||||
|
|
||||||
## 工作流
|
## 工作流
|
||||||
|
|
||||||
1. 根据需求、US 和 routes 生成对应 P0 页面原型。
|
1. 在 T-202 根据需求、US 和 routes 生成对应 P0 页面原型。
|
||||||
2. 人工确认页面区块、控件集合、文案和主要动作。
|
2. 人工确认页面区块、控件集合、文案和主要动作。
|
||||||
3. 对照原型更新 IX 总表和 P0 详情。
|
3. 对照原型更新 IX 总表和 P0 详情。
|
||||||
4. 再落正式 UI 任务;实现时按真实框架重写,禁止复制原型代码。
|
4. 再落正式 UI 任务;实现时按真实框架重写,禁止复制原型代码。
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<title>管理端登录原型</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #18201d;
|
||||||
|
--muted: #5b6862;
|
||||||
|
--line: #cfd7d3;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--canvas: #f3f5f4;
|
||||||
|
--brand: #0b6b50;
|
||||||
|
--brand-dark: #07513d;
|
||||||
|
--danger: #a62f2f;
|
||||||
|
--focus: #1a73b8;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 280px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--canvas);
|
||||||
|
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
button, input { font: inherit; }
|
||||||
|
button, input { min-height: 44px; }
|
||||||
|
button:focus-visible, input:focus-visible {
|
||||||
|
outline: 3px solid rgba(26, 115, 184, .28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.prototype {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #24312c;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 32px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 58px 1fr;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 clamp(18px, 5vw, 48px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
.brand-mark {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--brand);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
width: min(100% - 32px, 420px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(40px, 10vh, 96px) 0 48px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
padding: clamp(22px, 6vw, 32px);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 8px 24px rgba(31, 47, 40, .06);
|
||||||
|
}
|
||||||
|
h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.25; }
|
||||||
|
.lead { margin: 0 0 24px; color: var(--muted); }
|
||||||
|
.field { margin-top: 16px; }
|
||||||
|
label { display: block; margin-bottom: 6px; font-weight: 650; }
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #9eaaa4;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.password-wrap { position: relative; }
|
||||||
|
.password-wrap input { padding-right: 72px; }
|
||||||
|
.reveal {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--brand-dark);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.error-summary {
|
||||||
|
display: none;
|
||||||
|
margin: 18px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-left: 4px solid var(--danger);
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #762222;
|
||||||
|
}
|
||||||
|
.field-error { min-height: 20px; margin: 4px 0 0; color: var(--danger); font-size: 13px; }
|
||||||
|
.submit {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 18px;
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--brand);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.submit:hover { background: var(--brand-dark); }
|
||||||
|
.submit:disabled { cursor: wait; opacity: .7; }
|
||||||
|
.demo-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.demo-tools button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #9eaaa4;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #34423c;
|
||||||
|
}
|
||||||
|
.hint { margin: 14px 0 0; color: var(--muted); font-size: 13px; }
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
|
||||||
|
header { padding-inline: 16px; }
|
||||||
|
main { width: min(100% - 24px, 420px); padding-top: 28px; }
|
||||||
|
.panel { padding: 20px 16px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="shell">
|
||||||
|
<header>
|
||||||
|
<div class="brand"><span class="brand-mark" aria-hidden="true">采</span>采购任务管理</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="panel" aria-labelledby="page-title">
|
||||||
|
<h1 id="page-title">管理端登录</h1>
|
||||||
|
<p class="lead">使用采购管理员账号继续</p>
|
||||||
|
<form id="login-form" novalidate>
|
||||||
|
<div id="summary" class="error-summary" role="alert" tabindex="-1"></div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="account">账号</label>
|
||||||
|
<input id="account" name="account" autocomplete="username" autofocus>
|
||||||
|
<p id="account-error" class="field-error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<div class="password-wrap">
|
||||||
|
<input id="password" name="password" type="password" autocomplete="current-password">
|
||||||
|
<button class="reveal" id="reveal" type="button" aria-controls="password" aria-pressed="false">显示</button>
|
||||||
|
</div>
|
||||||
|
<p id="password-error" class="field-error"></p>
|
||||||
|
</div>
|
||||||
|
<button class="submit" id="submit" type="submit">登录</button>
|
||||||
|
</form>
|
||||||
|
<div class="demo-tools" aria-label="原型状态">
|
||||||
|
<button type="button" data-state="credentials">账号错误</button>
|
||||||
|
<button type="button" data-state="network">网络错误</button>
|
||||||
|
<button type="button" data-state="expired">会话过期</button>
|
||||||
|
<button type="button" data-state="reset">重置</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">原型状态按钮用于确认错误文案和焦点,不代表生产功能。</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector('#login-form');
|
||||||
|
const account = document.querySelector('#account');
|
||||||
|
const password = document.querySelector('#password');
|
||||||
|
const submit = document.querySelector('#submit');
|
||||||
|
const summary = document.querySelector('#summary');
|
||||||
|
const accountError = document.querySelector('#account-error');
|
||||||
|
const passwordError = document.querySelector('#password-error');
|
||||||
|
|
||||||
|
function clearErrors() {
|
||||||
|
summary.style.display = 'none';
|
||||||
|
summary.textContent = '';
|
||||||
|
accountError.textContent = '';
|
||||||
|
passwordError.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSummary(message) {
|
||||||
|
summary.textContent = message;
|
||||||
|
summary.style.display = 'block';
|
||||||
|
summary.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
clearErrors();
|
||||||
|
if (!account.value.trim() || !password.value) {
|
||||||
|
if (!account.value.trim()) accountError.textContent = '请输入账号。';
|
||||||
|
if (!password.value) passwordError.textContent = '请输入密码。';
|
||||||
|
showSummary('请检查并补全必填项。');
|
||||||
|
(!account.value.trim() ? account : password).focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submit.disabled = true;
|
||||||
|
submit.textContent = '正在登录…';
|
||||||
|
window.setTimeout(() => {
|
||||||
|
submit.disabled = false;
|
||||||
|
submit.textContent = '登录';
|
||||||
|
showSummary('原型:登录成功后进入任务列表。');
|
||||||
|
}, 650);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('#reveal').addEventListener('click', (event) => {
|
||||||
|
const showing = password.type === 'text';
|
||||||
|
password.type = showing ? 'password' : 'text';
|
||||||
|
event.currentTarget.textContent = showing ? '显示' : '隐藏';
|
||||||
|
event.currentTarget.setAttribute('aria-pressed', String(!showing));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('.demo-tools').addEventListener('click', (event) => {
|
||||||
|
const state = event.target.dataset.state;
|
||||||
|
if (!state) return;
|
||||||
|
clearErrors();
|
||||||
|
if (state === 'credentials') showSummary('账号或密码不正确。');
|
||||||
|
if (state === 'network') {
|
||||||
|
password.value = '';
|
||||||
|
showSummary('网络连接失败,密码已清空。请检查网络后重试。');
|
||||||
|
}
|
||||||
|
if (state === 'expired') showSummary('登录已过期,请重新登录后继续访问原页面。');
|
||||||
|
if (state === 'reset') {
|
||||||
|
account.value = '';
|
||||||
|
password.value = '';
|
||||||
|
account.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<title>新建采购任务原型</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #18201d;
|
||||||
|
--muted: #5b6862;
|
||||||
|
--line: #cfd7d3;
|
||||||
|
--surface: #fff;
|
||||||
|
--canvas: #f3f5f4;
|
||||||
|
--brand: #0b6b50;
|
||||||
|
--brand-dark: #07513d;
|
||||||
|
--danger: #a62f2f;
|
||||||
|
--info: #185b8f;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 280px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--canvas);
|
||||||
|
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
button, input, textarea { font: inherit; }
|
||||||
|
button, input, textarea { min-height: 44px; }
|
||||||
|
button:focus-visible, input:focus-visible, textarea:focus-visible {
|
||||||
|
outline: 3px solid rgba(26, 115, 184, .28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.prototype {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #24312c;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.app { min-height: 100vh; padding-top: 32px; }
|
||||||
|
header {
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 0 clamp(16px, 4vw, 40px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.brand { font-size: 16px; font-weight: 750; white-space: nowrap; }
|
||||||
|
nav { display: flex; align-items: stretch; height: 100%; gap: 6px; }
|
||||||
|
nav a {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #3d4b45;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
nav a[aria-current="page"] { color: var(--brand-dark); border-color: var(--brand); font-weight: 700; }
|
||||||
|
.user { margin-left: auto; color: var(--muted); }
|
||||||
|
main { width: min(100% - 32px, 920px); margin: 0 auto; padding: 28px 0 56px; }
|
||||||
|
.title-row { display: flex; justify-content: space-between; gap: 16px; align-items: start; margin-bottom: 18px; }
|
||||||
|
h1 { margin: 0; font-size: 24px; line-height: 1.3; }
|
||||||
|
.subtitle { margin: 4px 0 0; color: var(--muted); }
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.section { padding: 22px 24px; border-bottom: 1px solid var(--line); }
|
||||||
|
.section:last-child { border-bottom: 0; }
|
||||||
|
h2 { margin: 0 0 16px; font-size: 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 20px; }
|
||||||
|
.span-2 { grid-column: 1 / -1; }
|
||||||
|
label { display: block; margin-bottom: 6px; font-weight: 650; }
|
||||||
|
.required::after { content: " *"; color: var(--danger); }
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid #9eaaa4;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
textarea { min-height: 104px; resize: vertical; }
|
||||||
|
.help, .error { min-height: 20px; margin: 4px 0 0; font-size: 13px; }
|
||||||
|
.help { color: var(--muted); }
|
||||||
|
.error { color: var(--danger); }
|
||||||
|
.upload {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 160px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.dropzone {
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px dashed #82928a;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f9fbfa;
|
||||||
|
}
|
||||||
|
.dropzone input { padding: 7px; border: 0; min-height: 40px; }
|
||||||
|
.preview {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eef2f0;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.preview.has-file { color: #fff; background: #52645c; }
|
||||||
|
.notice {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 11px 13px;
|
||||||
|
border-left: 4px solid var(--info);
|
||||||
|
background: #eef7fd;
|
||||||
|
color: #184e76;
|
||||||
|
}
|
||||||
|
.notice.error-state { border-color: var(--danger); background: #fff1f1; color: #762222; }
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fafbfa;
|
||||||
|
}
|
||||||
|
button, .link-button {
|
||||||
|
padding: 9px 14px;
|
||||||
|
border: 1px solid #91a099;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
color: #26352f;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.primary { border-color: var(--brand); background: var(--brand); color: #fff; font-weight: 700; }
|
||||||
|
.primary:hover { background: var(--brand-dark); }
|
||||||
|
button:disabled { opacity: .65; cursor: wait; }
|
||||||
|
.demo-tools { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
|
||||||
|
.demo-tools button { min-height: 38px; padding-block: 6px; }
|
||||||
|
dialog {
|
||||||
|
width: min(100% - 32px, 430px);
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
dialog::backdrop { background: rgba(13, 25, 20, .5); }
|
||||||
|
.dialog-body { padding: 22px; }
|
||||||
|
.dialog-body h2 { margin-bottom: 8px; }
|
||||||
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line); }
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
header { gap: 12px; }
|
||||||
|
nav a { padding-inline: 8px; }
|
||||||
|
.user { display: none; }
|
||||||
|
main { width: min(100% - 20px, 920px); padding-top: 20px; }
|
||||||
|
.grid, .upload { grid-template-columns: 1fr; }
|
||||||
|
.span-2 { grid-column: auto; }
|
||||||
|
.preview { max-width: 220px; }
|
||||||
|
.section { padding: 18px 14px; }
|
||||||
|
.actions { padding: 14px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
|
||||||
|
.brand { font-size: 14px; }
|
||||||
|
nav a:first-child { display: none; }
|
||||||
|
h1 { font-size: 21px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="app">
|
||||||
|
<header>
|
||||||
|
<div class="brand">采购任务管理</div>
|
||||||
|
<nav aria-label="主导航">
|
||||||
|
<a href="admin-task-list.html">任务列表</a>
|
||||||
|
<a href="admin-task-create.html" aria-current="page">新建任务</a>
|
||||||
|
</nav>
|
||||||
|
<div class="user">演示管理员</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="title-row">
|
||||||
|
<div>
|
||||||
|
<h1>新建采购任务</h1>
|
||||||
|
<p class="subtitle">填写商品要求和不可覆盖的采购约束</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="notice" class="notice" role="status" tabindex="-1"></div>
|
||||||
|
<form id="task-form" class="panel" novalidate>
|
||||||
|
<section class="section" aria-labelledby="basic-title">
|
||||||
|
<h2 id="basic-title">商品要求</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="span-2">
|
||||||
|
<label class="required" for="title">商品标题</label>
|
||||||
|
<input id="title" name="title" maxlength="120" placeholder="例如:演示用桌面收纳盒">
|
||||||
|
<p id="title-error" class="error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="span-2">
|
||||||
|
<label for="description">商品描述</label>
|
||||||
|
<textarea id="description" name="description" placeholder="填写规格、颜色、材质等判断条件"></textarea>
|
||||||
|
<p class="help">仅填写采购判断所需信息,不放订单、地址或个人信息。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="required" for="quantity">数量</label>
|
||||||
|
<input id="quantity" name="quantity" type="number" value="1" min="1" step="1" inputmode="numeric">
|
||||||
|
<p id="quantity-error" class="error"></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="budget">最高总预算(元)</label>
|
||||||
|
<input id="budget" name="budget" type="number" min="0.01" step="0.01" inputmode="decimal" placeholder="不填写则不设上限">
|
||||||
|
<p id="budget-error" class="error"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="section" aria-labelledby="image-title">
|
||||||
|
<h2 id="image-title">参考图片</h2>
|
||||||
|
<div class="upload">
|
||||||
|
<div class="dropzone">
|
||||||
|
<label class="required" for="image">选择一张商品参考图</label>
|
||||||
|
<input id="image" name="image" type="file" accept="image/png,image/jpeg,image/webp">
|
||||||
|
<p id="file-meta" class="help">支持 JPG、PNG、WebP;原型不读取或上传文件。</p>
|
||||||
|
<p id="image-error" class="error"></p>
|
||||||
|
</div>
|
||||||
|
<div id="preview" class="preview" role="img" aria-label="尚未选择参考图片">图片预览位置</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="link-button" href="admin-task-list.html">取消</a>
|
||||||
|
<button id="submit" class="primary" type="submit">创建任务</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="demo-tools" aria-label="原型状态">
|
||||||
|
<button type="button" data-state="upload">模拟图片错误</button>
|
||||||
|
<button type="button" data-state="create">模拟创建失败</button>
|
||||||
|
<button type="button" data-state="uncertain">模拟网络结果不确定</button>
|
||||||
|
<button type="button" data-state="reset">重置状态</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<dialog id="leave-dialog" aria-labelledby="leave-title">
|
||||||
|
<div class="dialog-body">
|
||||||
|
<h2 id="leave-title">放弃未提交内容?</h2>
|
||||||
|
<p>已填写的商品要求不会保存。</p>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button id="stay" type="button">继续填写</button>
|
||||||
|
<button id="discard" type="button">放弃并离开</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector('#task-form');
|
||||||
|
const notice = document.querySelector('#notice');
|
||||||
|
const submit = document.querySelector('#submit');
|
||||||
|
const image = document.querySelector('#image');
|
||||||
|
const preview = document.querySelector('#preview');
|
||||||
|
const leaveDialog = document.querySelector('#leave-dialog');
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
form.addEventListener('input', () => { dirty = true; });
|
||||||
|
image.addEventListener('change', () => {
|
||||||
|
document.querySelector('#image-error').textContent = '';
|
||||||
|
if (!image.files.length) return;
|
||||||
|
const file = image.files[0];
|
||||||
|
document.querySelector('#file-meta').textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`;
|
||||||
|
preview.textContent = '已选择参考图';
|
||||||
|
preview.classList.add('has-file');
|
||||||
|
preview.setAttribute('aria-label', `已选择参考图片:${file.name}`);
|
||||||
|
dirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearFeedback() {
|
||||||
|
['title', 'quantity', 'budget', 'image'].forEach((name) => {
|
||||||
|
document.querySelector(`#${name}-error`).textContent = '';
|
||||||
|
});
|
||||||
|
notice.style.display = 'none';
|
||||||
|
notice.classList.remove('error-state');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotice(message, error = false) {
|
||||||
|
notice.textContent = message;
|
||||||
|
notice.style.display = 'block';
|
||||||
|
notice.classList.toggle('error-state', error);
|
||||||
|
notice.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
clearFeedback();
|
||||||
|
let firstInvalid = null;
|
||||||
|
const title = document.querySelector('#title');
|
||||||
|
const quantity = document.querySelector('#quantity');
|
||||||
|
const budget = document.querySelector('#budget');
|
||||||
|
if (!title.value.trim()) {
|
||||||
|
document.querySelector('#title-error').textContent = '请输入商品标题。';
|
||||||
|
firstInvalid ||= title;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(Number(quantity.value)) || Number(quantity.value) < 1) {
|
||||||
|
document.querySelector('#quantity-error').textContent = '数量必须是大于 0 的整数。';
|
||||||
|
firstInvalid ||= quantity;
|
||||||
|
}
|
||||||
|
if (budget.value && Number(budget.value) <= 0) {
|
||||||
|
document.querySelector('#budget-error').textContent = '预算必须大于 0,或留空。';
|
||||||
|
firstInvalid ||= budget;
|
||||||
|
}
|
||||||
|
if (!image.files.length) {
|
||||||
|
document.querySelector('#image-error').textContent = '请选择一张参考图片。';
|
||||||
|
firstInvalid ||= image;
|
||||||
|
}
|
||||||
|
if (firstInvalid) {
|
||||||
|
showNotice('请检查表单中的错误后再创建。', true);
|
||||||
|
firstInvalid.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submit.disabled = true;
|
||||||
|
submit.textContent = '正在创建…';
|
||||||
|
window.setTimeout(() => {
|
||||||
|
submit.disabled = false;
|
||||||
|
submit.textContent = '创建任务';
|
||||||
|
dirty = false;
|
||||||
|
showNotice('原型:任务已创建,状态为“待领取(PENDING)”,随后进入任务详情。');
|
||||||
|
}, 700);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('a').forEach((link) => {
|
||||||
|
link.addEventListener('click', (event) => {
|
||||||
|
if (!dirty) return;
|
||||||
|
event.preventDefault();
|
||||||
|
leaveDialog.dataset.target = link.href;
|
||||||
|
leaveDialog.showModal();
|
||||||
|
document.querySelector('#stay').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelector('#stay').addEventListener('click', () => leaveDialog.close());
|
||||||
|
document.querySelector('#discard').addEventListener('click', () => {
|
||||||
|
dirty = false;
|
||||||
|
window.location.href = leaveDialog.dataset.target;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('.demo-tools').addEventListener('click', (event) => {
|
||||||
|
const state = event.target.dataset.state;
|
||||||
|
if (!state) return;
|
||||||
|
clearFeedback();
|
||||||
|
if (state === 'upload') {
|
||||||
|
document.querySelector('#image-error').textContent = '图片无法解码,请重新选择 JPG、PNG 或 WebP 文件。';
|
||||||
|
image.focus();
|
||||||
|
}
|
||||||
|
if (state === 'create') showNotice('图片已上传,但任务创建失败。再次提交时将复用已上传图片。', true);
|
||||||
|
if (state === 'uncertain') showNotice('网络中断,正在使用幂等标识查询创建结果,请勿重复提交。', true);
|
||||||
|
if (state === 'reset') showNotice('已恢复默认表单状态。');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<title>采购任务详情原型</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #18201d;
|
||||||
|
--muted: #5b6862;
|
||||||
|
--line: #cfd7d3;
|
||||||
|
--surface: #fff;
|
||||||
|
--canvas: #f3f5f4;
|
||||||
|
--brand: #0b6b50;
|
||||||
|
--brand-dark: #07513d;
|
||||||
|
--info: #185b8f;
|
||||||
|
--warn: #865f12;
|
||||||
|
--danger: #9b3030;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 280px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--canvas);
|
||||||
|
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
button { font: inherit; min-height: 42px; }
|
||||||
|
button:focus-visible, a:focus-visible {
|
||||||
|
outline: 3px solid rgba(26, 115, 184, .28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.prototype {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #24312c;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.app { min-height: 100vh; padding-top: 32px; }
|
||||||
|
header {
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 0 clamp(16px, 4vw, 40px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.brand { font-size: 16px; font-weight: 750; white-space: nowrap; }
|
||||||
|
nav { display: flex; height: 100%; gap: 6px; }
|
||||||
|
nav a {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #3d4b45;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
nav a[aria-current="page"] { color: var(--brand-dark); border-color: var(--brand); font-weight: 700; }
|
||||||
|
.user { margin-left: auto; color: var(--muted); }
|
||||||
|
main { width: min(100% - 32px, 1120px); margin: 0 auto; padding: 24px 0 56px; }
|
||||||
|
.back { display: inline-block; margin-bottom: 12px; color: var(--brand-dark); font-weight: 700; }
|
||||||
|
.title-row { display: flex; justify-content: space-between; align-items: start; gap: 16px; margin-bottom: 16px; }
|
||||||
|
h1 { margin: 0 0 3px; font-size: 24px; line-height: 1.3; }
|
||||||
|
.task-id { color: var(--muted); }
|
||||||
|
.title-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 13px;
|
||||||
|
border: 1px solid #91a099;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #26352f;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.danger-button { color: #842828; border-color: #c68a8a; }
|
||||||
|
.status {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #7db6a3;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #07513d;
|
||||||
|
background: #eaf7f1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
.status.pending { color: #6a4a0b; border-color: #d6b970; background: #fff8e5; }
|
||||||
|
.status.cancelled { color: #842828; border-color: #d99c9c; background: #fff1f1; }
|
||||||
|
.success-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 15px 16px;
|
||||||
|
border: 1px solid #76af9c;
|
||||||
|
border-left: 5px solid var(--brand);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eaf7f1;
|
||||||
|
}
|
||||||
|
.success-mark {
|
||||||
|
flex: 0 0 28px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--brand);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.success-banner h2 { margin: 0; font-size: 18px; color: #064c39; }
|
||||||
|
.success-banner p { margin: 2px 0 0; color: #315e4f; }
|
||||||
|
.layout { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(280px, .8fr); gap: 16px; }
|
||||||
|
.stack { display: grid; align-content: start; gap: 16px; }
|
||||||
|
.panel {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.panel h2 { margin: 0 0 14px; font-size: 16px; }
|
||||||
|
.section-note { margin: -8px 0 14px; color: var(--muted); font-size: 13px; }
|
||||||
|
dl { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 10px 14px; margin: 0; }
|
||||||
|
dt { color: var(--muted); }
|
||||||
|
dd { margin: 0; overflow-wrap: anywhere; }
|
||||||
|
.requirement {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.image-placeholder {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #405149;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, transparent 48%, #c8d3ce 49%, #c8d3ce 51%, transparent 52%),
|
||||||
|
#e8eeeb;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.description { margin: 0 0 14px; white-space: pre-line; }
|
||||||
|
.constraints { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.constraint { padding: 5px 8px; border: 1px solid #aab5b0; border-radius: 5px; background: #f8faf9; font-size: 12px; }
|
||||||
|
.derived { border-left: 4px solid var(--info); background: #f3f8fb; }
|
||||||
|
.derived-list { margin: 0; padding-left: 20px; }
|
||||||
|
.candidate-head { display: flex; justify-content: space-between; gap: 10px; align-items: start; }
|
||||||
|
.candidate-title { margin: 0; font-size: 15px; }
|
||||||
|
.price { color: #7d351b; font-size: 18px; font-weight: 750; }
|
||||||
|
.checks { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 12px 0; }
|
||||||
|
.check { padding: 8px; border: 1px solid var(--line); border-radius: 5px; }
|
||||||
|
.check strong { display: block; }
|
||||||
|
.pass { color: #07513d; }
|
||||||
|
.unknown { color: #785612; }
|
||||||
|
.evidence {
|
||||||
|
min-height: 150px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eef2f0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.evidence-error { color: #762222; background: #fff1f1; }
|
||||||
|
.evidence-error p { margin: 0 0 10px; }
|
||||||
|
.timeline { margin: 0; padding: 0 0 0 23px; }
|
||||||
|
.timeline li { position: relative; padding: 0 0 18px 9px; }
|
||||||
|
.timeline li:last-child { padding-bottom: 0; }
|
||||||
|
.timeline time { display: block; color: var(--muted); font-size: 12px; }
|
||||||
|
.notice {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
border-left: 4px solid var(--danger);
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #762222;
|
||||||
|
}
|
||||||
|
.page-state {
|
||||||
|
display: none;
|
||||||
|
min-height: 360px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 30px 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-state.visible { display: grid; }
|
||||||
|
.page-state div { max-width: 520px; }
|
||||||
|
.page-state h2 { margin: 0 0 7px; font-size: 20px; }
|
||||||
|
.page-state p { margin: 0 0 16px; color: var(--muted); }
|
||||||
|
.demo-tools { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
||||||
|
.demo-tools button { min-height: 36px; padding-block: 6px; font-size: 13px; }
|
||||||
|
.demo-tools button[aria-pressed="true"] { color: #fff; border-color: #394a43; background: #394a43; }
|
||||||
|
dialog {
|
||||||
|
width: min(100% - 32px, 450px);
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
dialog::backdrop { background: rgba(13, 25, 20, .52); }
|
||||||
|
.dialog-body { padding: 22px; }
|
||||||
|
.dialog-body h2 { margin: 0 0 8px; font-size: 19px; }
|
||||||
|
.dialog-body p { margin: 0; color: var(--muted); }
|
||||||
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--line); }
|
||||||
|
.confirm-cancel { color: #fff; border-color: var(--danger); background: var(--danger); font-weight: 700; }
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
header { gap: 10px; }
|
||||||
|
.brand { font-size: 14px; }
|
||||||
|
nav a { padding-inline: 8px; }
|
||||||
|
nav a:last-child { display: none; }
|
||||||
|
.user { display: none; }
|
||||||
|
main { width: min(100% - 20px, 1120px); padding-top: 18px; }
|
||||||
|
.title-row { flex-direction: column; }
|
||||||
|
.title-actions { justify-content: flex-start; }
|
||||||
|
.requirement { grid-template-columns: 1fr; }
|
||||||
|
.image-placeholder { max-width: 220px; }
|
||||||
|
.checks { grid-template-columns: 1fr; }
|
||||||
|
.panel { padding: 16px 14px; }
|
||||||
|
dl { grid-template-columns: 1fr; gap: 2px; }
|
||||||
|
dd { margin-bottom: 8px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="app">
|
||||||
|
<header>
|
||||||
|
<div class="brand">采购任务管理</div>
|
||||||
|
<nav aria-label="主导航">
|
||||||
|
<a href="admin-task-list.html" aria-current="page">任务列表</a>
|
||||||
|
<a href="admin-task-create.html">新建任务</a>
|
||||||
|
</nav>
|
||||||
|
<div class="user">演示管理员</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<a class="back" href="admin-task-list.html">返回任务列表</a>
|
||||||
|
<div id="notice" class="notice" role="status" tabindex="-1"></div>
|
||||||
|
<div id="content">
|
||||||
|
<div class="title-row">
|
||||||
|
<div>
|
||||||
|
<h1>演示桌面收纳盒</h1>
|
||||||
|
<span class="task-id">任务编号 RB-DEMO-1004</span>
|
||||||
|
</div>
|
||||||
|
<div class="title-actions">
|
||||||
|
<span id="status" class="status">验证完成 · SUCCEEDED</span>
|
||||||
|
<button id="cancel-task" class="danger-button" type="button" hidden>取消任务</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="success-banner" class="success-banner" aria-labelledby="success-title">
|
||||||
|
<div class="success-mark" aria-hidden="true">✓</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="success-title">验证完成,未提交订单</h2>
|
||||||
|
<p>采购人员已完成人工确认;系统只回传验证结果,order_submitted=false。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="layout">
|
||||||
|
<div class="stack">
|
||||||
|
<section class="panel" aria-labelledby="requirement-title">
|
||||||
|
<h2 id="requirement-title">原始采购要求</h2>
|
||||||
|
<p class="section-note">以下内容来自管理员输入,不会被 AI 结果覆盖。</p>
|
||||||
|
<div class="requirement">
|
||||||
|
<div class="image-placeholder" role="img" aria-label="演示商品参考图片占位">脱敏参考图<br>RB-DEMO-1004</div>
|
||||||
|
<div>
|
||||||
|
<p class="description">桌面用分格收纳盒,浅灰色,塑料材质。
|
||||||
|
需要带可拆分隔板,外观接近参考图。</p>
|
||||||
|
<div class="constraints">
|
||||||
|
<span class="constraint">数量:2</span>
|
||||||
|
<span class="constraint">最高总预算:60.00 元</span>
|
||||||
|
<span class="constraint">关键属性:浅灰 / 分隔板</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="panel derived" aria-labelledby="derived-title">
|
||||||
|
<h2 id="derived-title">AI 派生理解</h2>
|
||||||
|
<p class="section-note">辅助判断信息,必须经过结构与范围校验。</p>
|
||||||
|
<ul class="derived-list">
|
||||||
|
<li>检索词:桌面分格收纳盒、可拆隔板、浅灰</li>
|
||||||
|
<li>推断材质:PP 塑料(置信度:中)</li>
|
||||||
|
<li>不可忽略:数量 2、总价不超过 60.00 元</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="candidate-title">
|
||||||
|
<div class="candidate-head">
|
||||||
|
<div>
|
||||||
|
<h2 id="candidate-title" class="candidate-title">人工确认的候选</h2>
|
||||||
|
<span class="task-id">候选 2 / 最多检查 5 个</span>
|
||||||
|
</div>
|
||||||
|
<div class="price">合计 49.80 元</div>
|
||||||
|
</div>
|
||||||
|
<p>演示多格桌面塑料收纳盒(浅灰,含可拆隔板)</p>
|
||||||
|
<div class="checks">
|
||||||
|
<div class="check"><strong class="pass">符合</strong>数量与总预算</div>
|
||||||
|
<div class="check"><strong class="pass">符合</strong>颜色与分隔板</div>
|
||||||
|
<div class="check"><strong class="pass">符合</strong>外观接近参考图</div>
|
||||||
|
<div class="check"><strong class="unknown">人工确认</strong>材质详情以页面为准</div>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
<dt>人员结论</dt><dd>接受候选</dd>
|
||||||
|
<dt>提交订单</dt><dd>否(系统硬停止点)</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="evidence-title">
|
||||||
|
<h2 id="evidence-title">执行证据</h2>
|
||||||
|
<div id="evidence-ok" class="evidence" role="img" aria-label="脱敏后的候选页面证据截图占位">
|
||||||
|
脱敏证据截图占位<br>不包含账号、地址或会话信息
|
||||||
|
</div>
|
||||||
|
<div id="evidence-error" class="evidence evidence-error" hidden>
|
||||||
|
<div>
|
||||||
|
<p><strong>证据图片加载失败</strong><br>其他任务信息仍可查看。</p>
|
||||||
|
<button id="retry-evidence" type="button">重试加载证据</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<aside class="stack" aria-label="任务状态与时间线">
|
||||||
|
<section class="panel" aria-labelledby="summary-title">
|
||||||
|
<h2 id="summary-title">任务摘要</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>创建时间</dt><dd>2026-07-25 09:42</dd>
|
||||||
|
<dt>执行设备</dt><dd>演示设备 A</dd>
|
||||||
|
<dt>执行人员</dt><dd>演示采购员</dd>
|
||||||
|
<dt>最后更新</dt><dd>2026-07-25 10:24</dd>
|
||||||
|
<dt>结果同步</dt><dd>已同步</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="timeline-title">
|
||||||
|
<h2 id="timeline-title">执行时间线</h2>
|
||||||
|
<ol class="timeline">
|
||||||
|
<li><strong>验证结果已回传</strong><time datetime="2026-07-25T10:24:00+08:00">10:24</time></li>
|
||||||
|
<li><strong>采购人员接受候选</strong><time datetime="2026-07-25T10:22:00+08:00">10:22</time></li>
|
||||||
|
<li><strong>到达人工确认硬停止点</strong><time datetime="2026-07-25T10:18:00+08:00">10:18</time></li>
|
||||||
|
<li><strong>开始检查候选</strong><time datetime="2026-07-25T10:10:00+08:00">10:10</time></li>
|
||||||
|
<li><strong>演示设备 A 领取任务</strong><time datetime="2026-07-25T09:56:00+08:00">09:56</time></li>
|
||||||
|
<li><strong>任务已创建</strong><time datetime="2026-07-25T09:42:00+08:00">09:42</time></li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section id="page-state" class="page-state" aria-live="polite">
|
||||||
|
<div>
|
||||||
|
<h2 id="state-title"></h2>
|
||||||
|
<p id="state-copy"></p>
|
||||||
|
<button id="state-action" type="button"></button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="demo-tools" aria-label="原型页面状态">
|
||||||
|
<button type="button" data-state="success" aria-pressed="true">验证完成</button>
|
||||||
|
<button type="button" data-state="pending" aria-pressed="false">待领取可取消</button>
|
||||||
|
<button type="button" data-state="evidence" aria-pressed="false">证据失败</button>
|
||||||
|
<button type="button" data-state="loading" aria-pressed="false">加载中</button>
|
||||||
|
<button type="button" data-state="failed" aria-pressed="false">加载失败</button>
|
||||||
|
<button type="button" data-state="forbidden" aria-pressed="false">权限不足</button>
|
||||||
|
<button type="button" data-state="missing" aria-pressed="false">任务不存在</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<dialog id="cancel-dialog" aria-labelledby="cancel-title">
|
||||||
|
<div class="dialog-body">
|
||||||
|
<h2 id="cancel-title">确认取消任务?</h2>
|
||||||
|
<p>任务取消后不会自动恢复,采购执行员将不能再领取此任务。</p>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button id="keep-task" type="button">保留任务</button>
|
||||||
|
<button id="confirm-cancel" class="confirm-cancel" type="button">确认取消</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<script>
|
||||||
|
const content = document.querySelector('#content');
|
||||||
|
const pageState = document.querySelector('#page-state');
|
||||||
|
const successBanner = document.querySelector('#success-banner');
|
||||||
|
const statusBadge = document.querySelector('#status');
|
||||||
|
const cancelTask = document.querySelector('#cancel-task');
|
||||||
|
const evidenceOk = document.querySelector('#evidence-ok');
|
||||||
|
const evidenceError = document.querySelector('#evidence-error');
|
||||||
|
const notice = document.querySelector('#notice');
|
||||||
|
const cancelDialog = document.querySelector('#cancel-dialog');
|
||||||
|
const stateTitle = document.querySelector('#state-title');
|
||||||
|
const stateCopy = document.querySelector('#state-copy');
|
||||||
|
const stateAction = document.querySelector('#state-action');
|
||||||
|
const pageStates = {
|
||||||
|
loading: ['正在加载任务详情', '页面框架和返回入口保持可用,请稍候。', '取消加载'],
|
||||||
|
failed: ['任务详情加载失败', '没有可用的缓存内容,请检查网络后重试。', '重试'],
|
||||||
|
forbidden: ['没有查看权限', '当前账号无权查看此任务,页面不会泄露任务标题、图片或状态。', '返回任务列表'],
|
||||||
|
missing: ['任务不存在', '该编号不存在或已不可用。页面不会显示任何任务内容。', '返回任务列表']
|
||||||
|
};
|
||||||
|
|
||||||
|
function setPressed(state) {
|
||||||
|
document.querySelectorAll('.demo-tools button').forEach((button) => {
|
||||||
|
button.setAttribute('aria-pressed', String(button.dataset.state === state));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContentState(state) {
|
||||||
|
setPressed(state);
|
||||||
|
notice.style.display = 'none';
|
||||||
|
content.style.display = 'block';
|
||||||
|
pageState.classList.remove('visible');
|
||||||
|
evidenceOk.hidden = false;
|
||||||
|
evidenceError.hidden = true;
|
||||||
|
document.querySelector('#retry-evidence').disabled = false;
|
||||||
|
document.querySelector('#retry-evidence').textContent = '重试加载证据';
|
||||||
|
successBanner.hidden = state === 'pending';
|
||||||
|
cancelTask.hidden = state !== 'pending';
|
||||||
|
statusBadge.classList.remove('cancelled');
|
||||||
|
statusBadge.classList.toggle('pending', state === 'pending');
|
||||||
|
statusBadge.textContent = state === 'pending' ? '待领取 · PENDING' : '验证完成 · SUCCEEDED';
|
||||||
|
if (state === 'evidence') {
|
||||||
|
evidenceOk.hidden = true;
|
||||||
|
evidenceError.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageState(state) {
|
||||||
|
setPressed(state);
|
||||||
|
content.style.display = 'none';
|
||||||
|
pageState.classList.add('visible');
|
||||||
|
const values = pageStates[state];
|
||||||
|
stateTitle.textContent = values[0];
|
||||||
|
stateCopy.textContent = values[1];
|
||||||
|
stateAction.textContent = values[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('.demo-tools').addEventListener('click', (event) => {
|
||||||
|
const state = event.target.dataset.state;
|
||||||
|
if (!state) return;
|
||||||
|
if (['success', 'pending', 'evidence'].includes(state)) setContentState(state);
|
||||||
|
else setPageState(state);
|
||||||
|
});
|
||||||
|
stateAction.addEventListener('click', () => {
|
||||||
|
if (stateAction.textContent === '返回任务列表') window.location.href = 'admin-task-list.html';
|
||||||
|
else setContentState('success');
|
||||||
|
});
|
||||||
|
cancelTask.addEventListener('click', () => {
|
||||||
|
cancelDialog.showModal();
|
||||||
|
document.querySelector('#keep-task').focus();
|
||||||
|
});
|
||||||
|
document.querySelector('#keep-task').addEventListener('click', () => cancelDialog.close());
|
||||||
|
document.querySelector('#confirm-cancel').addEventListener('click', () => {
|
||||||
|
cancelDialog.close();
|
||||||
|
cancelTask.hidden = true;
|
||||||
|
statusBadge.textContent = '已取消 · CANCELLED';
|
||||||
|
statusBadge.classList.remove('pending');
|
||||||
|
statusBadge.classList.add('cancelled');
|
||||||
|
notice.textContent = '任务已取消,不会自动恢复。原型不发送任何请求。';
|
||||||
|
notice.style.display = 'block';
|
||||||
|
notice.focus();
|
||||||
|
});
|
||||||
|
document.querySelector('#retry-evidence').addEventListener('click', (event) => {
|
||||||
|
event.currentTarget.disabled = true;
|
||||||
|
event.currentTarget.textContent = '正在重试…';
|
||||||
|
window.setTimeout(() => {
|
||||||
|
evidenceOk.hidden = false;
|
||||||
|
evidenceError.hidden = true;
|
||||||
|
setPressed('success');
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<title>采购任务列表原型</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #18201d;
|
||||||
|
--muted: #5b6862;
|
||||||
|
--line: #cfd7d3;
|
||||||
|
--surface: #fff;
|
||||||
|
--canvas: #f3f5f4;
|
||||||
|
--brand: #0b6b50;
|
||||||
|
--brand-dark: #07513d;
|
||||||
|
--info: #185b8f;
|
||||||
|
--warn: #865f12;
|
||||||
|
--danger: #9b3030;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 280px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--canvas);
|
||||||
|
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
button, input, select { font: inherit; min-height: 42px; }
|
||||||
|
button:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible {
|
||||||
|
outline: 3px solid rgba(26, 115, 184, .28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.prototype {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: #24312c;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.app { min-height: 100vh; padding-top: 32px; }
|
||||||
|
header {
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 0 clamp(16px, 4vw, 40px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.brand { font-size: 16px; font-weight: 750; white-space: nowrap; }
|
||||||
|
nav { display: flex; height: 100%; gap: 6px; }
|
||||||
|
nav a {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #3d4b45;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
nav a[aria-current="page"] { color: var(--brand-dark); border-color: var(--brand); font-weight: 700; }
|
||||||
|
.user { margin-left: auto; color: var(--muted); }
|
||||||
|
main { width: min(100% - 32px, 1180px); margin: 0 auto; padding: 26px 0 56px; }
|
||||||
|
.title-row { display: flex; justify-content: space-between; gap: 16px; align-items: center; margin-bottom: 18px; }
|
||||||
|
h1 { margin: 0; font-size: 24px; line-height: 1.3; }
|
||||||
|
.subtitle { margin: 3px 0 0; color: var(--muted); }
|
||||||
|
.button, button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 13px;
|
||||||
|
border: 1px solid #91a099;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #26352f;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.primary { border-color: var(--brand); color: #fff; background: var(--brand); font-weight: 700; }
|
||||||
|
.primary:hover { background: var(--brand-dark); }
|
||||||
|
.filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 1fr) 180px 130px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #9eaaa4;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.table-wrap {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||||
|
th, td { padding: 13px 12px; border-bottom: 1px solid #e1e6e3; text-align: left; vertical-align: middle; overflow-wrap: anywhere; }
|
||||||
|
th { color: #45534d; background: #f8faf9; font-size: 13px; font-weight: 700; }
|
||||||
|
tbody tr:hover { background: #f8fbfa; }
|
||||||
|
tbody tr:last-child td { border-bottom: 0; }
|
||||||
|
.task-title { display: block; color: var(--ink); font-weight: 700; }
|
||||||
|
.task-id { color: var(--muted); font-size: 12px; }
|
||||||
|
.status {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 28px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px solid #9eaaa4;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f8faf9;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.status.pending { color: #6a4a0b; border-color: #d6b970; background: #fff8e5; }
|
||||||
|
.status.running { color: #174f79; border-color: #8eb7d4; background: #eef7fd; }
|
||||||
|
.status.success { color: #07513d; border-color: #7db6a3; background: #eaf7f1; }
|
||||||
|
.status.failed { color: #842828; border-color: #d99c9c; background: #fff1f1; }
|
||||||
|
.detail-link { color: var(--brand-dark); font-weight: 700; }
|
||||||
|
.state-panel {
|
||||||
|
display: none;
|
||||||
|
min-height: 240px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 32px 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.state-panel.visible { display: grid; }
|
||||||
|
.state-box { max-width: 520px; }
|
||||||
|
.state-box h2 { margin: 0 0 6px; font-size: 18px; }
|
||||||
|
.state-box p { margin: 0 0 16px; color: var(--muted); }
|
||||||
|
.stale {
|
||||||
|
display: none;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-left: 4px solid var(--warn);
|
||||||
|
color: #684a0f;
|
||||||
|
background: #fff8e5;
|
||||||
|
}
|
||||||
|
.demo-tools { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
||||||
|
.demo-tools button { min-height: 36px; padding-block: 6px; font-size: 13px; }
|
||||||
|
.demo-tools button[aria-pressed="true"] { color: #fff; border-color: #394a43; background: #394a43; }
|
||||||
|
.updated { margin: 12px 0 0; color: var(--muted); font-size: 13px; }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
main { width: min(100% - 20px, 1180px); padding-top: 20px; }
|
||||||
|
.filters { grid-template-columns: 1fr 1fr; }
|
||||||
|
.filters button { grid-column: 1 / -1; }
|
||||||
|
.table-wrap { border-radius: 0 0 8px 8px; }
|
||||||
|
table, tbody { display: block; }
|
||||||
|
thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
||||||
|
tr { display: grid; grid-template-columns: 1fr 1fr; padding: 12px; border-bottom: 1px solid var(--line); }
|
||||||
|
tbody tr:last-child { border-bottom: 0; }
|
||||||
|
td { display: block; padding: 6px; border: 0; }
|
||||||
|
td::before { content: attr(data-label); display: block; margin-bottom: 2px; color: var(--muted); font-size: 11px; font-weight: 700; }
|
||||||
|
td:first-child, td:last-child { grid-column: 1 / -1; }
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.prototype { padding-inline: 2px; font-size: 10px; white-space: nowrap; }
|
||||||
|
header { gap: 10px; }
|
||||||
|
.brand { font-size: 14px; }
|
||||||
|
nav a { padding-inline: 8px; }
|
||||||
|
nav a:last-child { display: none; }
|
||||||
|
.user { display: none; }
|
||||||
|
.title-row { align-items: stretch; flex-direction: column; }
|
||||||
|
.primary { width: 100%; }
|
||||||
|
.filters { grid-template-columns: 1fr; }
|
||||||
|
.filters button { grid-column: auto; }
|
||||||
|
tr { grid-template-columns: 1fr; }
|
||||||
|
td, td:first-child, td:last-child { grid-column: auto; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="app">
|
||||||
|
<header>
|
||||||
|
<div class="brand">采购任务管理</div>
|
||||||
|
<nav aria-label="主导航">
|
||||||
|
<a href="admin-task-list.html" aria-current="page">任务列表</a>
|
||||||
|
<a href="admin-task-create.html">新建任务</a>
|
||||||
|
</nav>
|
||||||
|
<div class="user">演示管理员</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="title-row">
|
||||||
|
<div>
|
||||||
|
<h1>采购任务</h1>
|
||||||
|
<p class="subtitle">按最近创建时间查看执行状态和证据</p>
|
||||||
|
</div>
|
||||||
|
<a class="button primary" href="admin-task-create.html">新建任务</a>
|
||||||
|
</div>
|
||||||
|
<p id="stale" class="stale" role="status">刷新失败,当前显示的是上次成功加载的内容,数据可能已过期。</p>
|
||||||
|
<form class="filters" id="filters">
|
||||||
|
<input type="search" aria-label="搜索任务编号或标题" placeholder="搜索编号或标题">
|
||||||
|
<select aria-label="筛选任务状态">
|
||||||
|
<option>全部状态</option>
|
||||||
|
<option>待领取</option>
|
||||||
|
<option>执行中</option>
|
||||||
|
<option>等待人工确认</option>
|
||||||
|
<option>验证完成</option>
|
||||||
|
<option>失败</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">应用筛选</button>
|
||||||
|
</form>
|
||||||
|
<section class="table-wrap" aria-label="任务列表内容">
|
||||||
|
<div id="normal">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:30%">任务</th>
|
||||||
|
<th style="width:16%">状态</th>
|
||||||
|
<th style="width:15%">执行设备</th>
|
||||||
|
<th style="width:22%">更新时间</th>
|
||||||
|
<th style="width:17%">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td data-label="任务"><span class="task-title">演示桌面收纳盒</span><span class="task-id">RB-DEMO-1004</span></td>
|
||||||
|
<td data-label="状态"><span class="status success">验证完成</span></td>
|
||||||
|
<td data-label="执行设备">演示设备 A</td>
|
||||||
|
<td data-label="更新时间"><time datetime="2026-07-25T10:24:00+08:00">今天 10:24</time></td>
|
||||||
|
<td data-label="操作"><a class="detail-link" href="admin-task-detail.html">查看详情</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="任务"><span class="task-title">演示纯棉毛巾</span><span class="task-id">RB-DEMO-1003</span></td>
|
||||||
|
<td data-label="状态"><span class="status running">执行中</span></td>
|
||||||
|
<td data-label="执行设备">演示设备 B</td>
|
||||||
|
<td data-label="更新时间"><time datetime="2026-07-25T10:12:00+08:00">今天 10:12</time></td>
|
||||||
|
<td data-label="操作"><a class="detail-link" href="admin-task-detail.html">查看详情</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="任务"><span class="task-title">演示文件收纳袋</span><span class="task-id">RB-DEMO-1002</span></td>
|
||||||
|
<td data-label="状态"><span class="status pending">待领取</span></td>
|
||||||
|
<td data-label="执行设备">尚未分配</td>
|
||||||
|
<td data-label="更新时间"><time datetime="2026-07-25T09:48:00+08:00">今天 09:48</time></td>
|
||||||
|
<td data-label="操作"><a class="detail-link" href="admin-task-detail.html">查看详情</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="任务"><span class="task-title">演示替换刷头</span><span class="task-id">RB-DEMO-1001</span></td>
|
||||||
|
<td data-label="状态"><span class="status failed">已停止 · 页面未知</span></td>
|
||||||
|
<td data-label="执行设备">演示设备 A</td>
|
||||||
|
<td data-label="更新时间"><time datetime="2026-07-25T09:31:00+08:00">今天 09:31</time></td>
|
||||||
|
<td data-label="操作"><a class="detail-link" href="admin-task-detail.html">查看详情</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="state-panel" class="state-panel" aria-live="polite">
|
||||||
|
<div class="state-box">
|
||||||
|
<h2 id="state-title"></h2>
|
||||||
|
<p id="state-copy"></p>
|
||||||
|
<button id="state-action" type="button"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<p class="updated">共 4 条演示任务 · 手动刷新,不自动轮询</p>
|
||||||
|
<div class="demo-tools" aria-label="原型页面状态">
|
||||||
|
<button type="button" data-state="normal" aria-pressed="true">有数据</button>
|
||||||
|
<button type="button" data-state="loading" aria-pressed="false">加载中</button>
|
||||||
|
<button type="button" data-state="empty" aria-pressed="false">空状态</button>
|
||||||
|
<button type="button" data-state="stale" aria-pressed="false">刷新失败</button>
|
||||||
|
<button type="button" data-state="failed" aria-pressed="false">加载失败</button>
|
||||||
|
<button type="button" data-state="forbidden" aria-pressed="false">权限不足</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const normal = document.querySelector('#normal');
|
||||||
|
const panel = document.querySelector('#state-panel');
|
||||||
|
const stale = document.querySelector('#stale');
|
||||||
|
const stateTitle = document.querySelector('#state-title');
|
||||||
|
const stateCopy = document.querySelector('#state-copy');
|
||||||
|
const stateAction = document.querySelector('#state-action');
|
||||||
|
const states = {
|
||||||
|
loading: ['正在加载任务', '筛选条件已保留,请稍候。', '取消加载'],
|
||||||
|
empty: ['还没有采购任务', '新建任务后,它会显示在这里。', '新建任务'],
|
||||||
|
failed: ['任务加载失败', '当前没有可展示的缓存内容,请检查网络后重试。', '重试'],
|
||||||
|
forbidden: ['没有查看权限', '当前账号无权访问任务列表,页面不会显示任何任务内容。', '返回登录']
|
||||||
|
};
|
||||||
|
|
||||||
|
function showState(state) {
|
||||||
|
document.querySelectorAll('.demo-tools button').forEach((button) => {
|
||||||
|
button.setAttribute('aria-pressed', String(button.dataset.state === state));
|
||||||
|
});
|
||||||
|
stale.style.display = state === 'stale' ? 'block' : 'none';
|
||||||
|
const showRows = state === 'normal' || state === 'stale';
|
||||||
|
normal.style.display = showRows ? 'block' : 'none';
|
||||||
|
panel.classList.toggle('visible', !showRows);
|
||||||
|
if (!showRows) {
|
||||||
|
const values = states[state];
|
||||||
|
stateTitle.textContent = values[0];
|
||||||
|
stateCopy.textContent = values[1];
|
||||||
|
stateAction.textContent = values[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('.demo-tools').addEventListener('click', (event) => {
|
||||||
|
if (event.target.dataset.state) showState(event.target.dataset.state);
|
||||||
|
});
|
||||||
|
stateAction.addEventListener('click', () => {
|
||||||
|
if (stateAction.textContent === '新建任务') window.location.href = 'admin-task-create.html';
|
||||||
|
else if (stateAction.textContent === '返回登录') window.location.href = 'admin-login.html';
|
||||||
|
else showState('normal');
|
||||||
|
});
|
||||||
|
document.querySelector('#filters').addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showState('loading');
|
||||||
|
window.setTimeout(() => showState('normal'), 500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 候选确认 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cbd3d9;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line);padding-bottom:18px}
|
||||||
|
header{min-height:58px;display:flex;align-items:center;padding:0 18px;background:#fff;border-bottom:1px solid var(--line)}h1,h2,h3,p{margin-top:0}h1{font-size:19px;margin-bottom:0}main{padding:16px}
|
||||||
|
.hard-stop{padding:14px;margin-bottom:14px;border:2px solid var(--danger);background:#fff0ee}.hard-stop strong,.hard-stop span{display:block}.hard-stop strong{font-size:17px;color:#7d2821}.hard-stop span{margin-top:4px}
|
||||||
|
.panel{padding:15px;margin-bottom:14px;background:#fff;border:1px solid var(--line);border-radius:6px}.panel h2{font-size:16px;margin-bottom:12px}.panel h3{font-size:14px;margin-bottom:8px}
|
||||||
|
label{font-size:13px;font-weight:700}select,button,textarea{font:inherit}select,textarea{width:100%;border:1px solid #89959e;border-radius:6px;background:#fff}select{min-height:44px;margin-top:6px;padding:8px}textarea{min-height:80px;padding:10px;resize:vertical}
|
||||||
|
.facts{display:grid;grid-template-columns:1fr auto;gap:8px 12px;margin:0}.facts dt{color:var(--muted)}.facts dd{margin:0;font-weight:750}.price{font-size:19px;color:var(--primary)}
|
||||||
|
.evidence{height:118px;display:grid;place-items:center;margin:12px 0;border:1px dashed #87939c;background:#e6eaec;color:#3c4851;font-weight:700;text-align:center}.checks{list-style:none;margin:0;padding:0}.checks li{padding:7px 0;border-top:1px solid #e4e8eb}.checks li:first-child{border-top:0}.ok{color:var(--primary);font-weight:800}.risk{color:var(--danger);font-weight:800}
|
||||||
|
.actions{display:grid;grid-template-columns:1fr 1fr;gap:10px}.actions button{min-height:50px;border-radius:6px;font-weight:800}.accept{border:0;background:var(--primary);color:#fff}.reject{border:1px solid var(--danger);background:#fff;color:var(--danger)}.none,.manual{border:1px solid #73808a;background:#fff;color:var(--ink)}button:disabled{opacity:.48}
|
||||||
|
.reason{margin-top:14px}.reason label{display:block;margin-bottom:6px}.completion{display:none;padding:16px;margin-bottom:14px;border-left:5px solid var(--primary);background:#e9f3ef}.completion.show{display:block}.completion strong{display:block;font-size:18px}.machine{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted)}
|
||||||
|
dialog{width:min(calc(100% - 32px),380px);padding:0;border:1px solid var(--line);border-radius:6px}dialog::backdrop{background:#18212a88}.dialog-body{padding:20px}.dialog-body h2{font-size:19px}.dialog-actions{display:flex;gap:10px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--line)}.dialog-actions button{min-height:46px;padding:0 15px;border-radius:6px;font-weight:750}.cancel{border:1px solid #76838c;background:#fff}.confirm{border:0;background:var(--primary);color:#fff}
|
||||||
|
button:focus-visible,select:focus,textarea:focus{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}@media(max-width:360px){.actions{grid-template-columns:1fr}}@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><h1>人工确认候选</h1></header>
|
||||||
|
<main>
|
||||||
|
<div class="hard-stop" role="alert"><strong>自动化已硬停止</strong><span>返回或关闭本页都不会恢复自动点击。请由采购人员作出结论。</span></div>
|
||||||
|
<div class="completion" id="completion" role="status"><strong>验证完成,未提交订单</strong><span id="completionText">人员结论已记录。</span><p class="machine">order_submitted=false</p></div>
|
||||||
|
<section class="panel">
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="valid">候选符合硬约束</option>
|
||||||
|
<option value="over">候选超出预算</option>
|
||||||
|
<option value="none">没有找到候选</option>
|
||||||
|
<option value="unknown">关键属性未知</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<section class="panel" id="requirement" aria-labelledby="requirementTitle">
|
||||||
|
<h2 id="requirementTitle">管理员原始要求</h2>
|
||||||
|
<dl class="facts"><dt>商品</dt><dd>透明抽屉式收纳盒</dd><dt>SKU</dt><dd>白色 · 中号</dd><dt>数量</dt><dd>2 件</dd><dt>最高预算</dt><dd>¥ 48.00 / 件</dd></dl>
|
||||||
|
</section>
|
||||||
|
<section class="panel" id="candidate" aria-labelledby="candidateTitle">
|
||||||
|
<h2 id="candidateTitle">候选 1 / 3</h2>
|
||||||
|
<div class="evidence" role="img" aria-label="脱敏的候选商品截图占位">脱敏候选截图<br>证据占位</div>
|
||||||
|
<dl class="facts"><dt>候选标题</dt><dd>透明可叠放抽屉收纳盒</dd><dt>单价</dt><dd class="price" id="price">¥ 39.90</dd></dl>
|
||||||
|
<h3>匹配与风险</h3>
|
||||||
|
<ul class="checks" id="checks"><li><span class="ok">符合</span> 颜色:白色</li><li><span class="ok">符合</span> 规格:中号</li><li><span class="ok">符合</span> 单价未超过预算</li></ul>
|
||||||
|
</section>
|
||||||
|
<section class="panel">
|
||||||
|
<h2>人员结论</h2>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="accept" id="accept" type="button" data-action="接受候选">接受候选</button>
|
||||||
|
<button class="reject" id="reject" type="button" data-action="拒绝候选">拒绝候选</button>
|
||||||
|
<button class="none" id="noneAction" type="button" data-action="确认无匹配">无匹配</button>
|
||||||
|
<button class="manual" type="button" data-action="转人工处理">转人工</button>
|
||||||
|
</div>
|
||||||
|
<div class="reason"><label for="reason">原因(拒绝、无匹配或转人工时填写)</label><textarea id="reason" placeholder="输入简短、非敏感的原因"></textarea></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<dialog id="confirmDialog" aria-labelledby="dialogTitle">
|
||||||
|
<div class="dialog-body"><h2 id="dialogTitle">确认人员结论?</h2><p id="dialogText"></p><p>提交只回传验证结论,不会提交订单或支付。</p></div>
|
||||||
|
<div class="dialog-actions"><button class="cancel" id="cancel" type="button">取消</button><button class="confirm" id="confirm" type="button">确认回传</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script>
|
||||||
|
const scenario=document.querySelector("#scenario"),candidate=document.querySelector("#candidate"),accept=document.querySelector("#accept"),reject=document.querySelector("#reject"),noneAction=document.querySelector("#noneAction"),price=document.querySelector("#price"),checks=document.querySelector("#checks"),dialog=document.querySelector("#confirmDialog"),cancel=document.querySelector("#cancel"),completion=document.querySelector("#completion");let pending="";
|
||||||
|
function render(){const v=scenario.value,missing=v==="none";candidate.hidden=missing;noneAction.disabled=!missing;accept.disabled=v!=="valid";reject.disabled=missing;if(v==="over"){price.textContent="¥ 56.00";checks.innerHTML='<li><span class="ok">符合</span> 颜色:白色</li><li><span class="ok">符合</span> 规格:中号</li><li><span class="risk">不符合</span> 单价超过最高预算,不能接受</li>'}else if(v==="unknown"){price.textContent="¥ 41.00";checks.innerHTML='<li><span class="ok">符合</span> 颜色:白色</li><li><span class="risk">未知</span> 规格无法确认,不能接受</li><li><span class="ok">符合</span> 单价未超过预算</li>'}else{price.textContent="¥ 39.90";checks.innerHTML='<li><span class="ok">符合</span> 颜色:白色</li><li><span class="ok">符合</span> 规格:中号</li><li><span class="ok">符合</span> 单价未超过预算</li>'}}
|
||||||
|
document.querySelectorAll("[data-action]").forEach(button=>button.addEventListener("click",()=>{pending=button.dataset.action;document.querySelector("#dialogText").textContent="选择:"+pending;dialog.showModal();setTimeout(()=>cancel.focus(),0)}));
|
||||||
|
scenario.addEventListener("change",render);cancel.addEventListener("click",()=>dialog.close());dialog.addEventListener("cancel",e=>{e.preventDefault();dialog.close()});document.querySelector("#confirm").addEventListener("click",()=>{dialog.close();completion.classList.add("show");document.querySelector("#completionText").textContent="“"+pending+"”已记录,结果正在同步。";completion.scrollIntoView({behavior:"smooth",block:"start"})});render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 登录与设备绑定 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5e6b76;--line:#cdd4da;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--primary-dark:#0f5540;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:20;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}
|
||||||
|
.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f8f9fa;border-inline:1px solid var(--line)}
|
||||||
|
header{height:58px;display:flex;align-items:center;padding:0 20px;background:var(--surface);border-bottom:1px solid var(--line)}h1{font-size:20px;margin:0}
|
||||||
|
main{padding:24px 20px 32px}.eyebrow{margin:0 0 6px;color:var(--primary);font-size:13px;font-weight:700}.intro{margin:0 0 24px;color:var(--muted)}
|
||||||
|
form{display:grid;gap:18px}.field{display:grid;gap:7px}label{font-weight:650}.hint{color:var(--muted);font-size:13px}.input-wrap{display:flex}
|
||||||
|
input,select,button{font:inherit}input,select{width:100%;min-height:48px;padding:10px 12px;border:1px solid #8f9aa3;border-radius:6px;background:#fff;color:var(--ink)}
|
||||||
|
input:focus,select:focus,button:focus-visible,a:focus-visible{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}
|
||||||
|
.input-wrap input{border-radius:6px 0 0 6px}.icon-button{width:54px;min-height:48px;border:1px solid #8f9aa3;border-left:0;border-radius:0 6px 6px 0;background:#fff;font-weight:700}
|
||||||
|
.device{padding:14px;border-left:4px solid var(--primary);background:#e9f3ef}.device strong,.device span{display:block}.device span{margin-top:3px;color:var(--muted);font-size:13px}
|
||||||
|
.error{display:none;padding:12px;border:1px solid #d9a09a;background:#fff1ef;color:#76271f}.error.show{display:block}
|
||||||
|
.primary{min-height:50px;border:0;border-radius:6px;background:var(--primary);color:#fff;font-weight:750}.primary:hover{background:var(--primary-dark)}button:disabled{opacity:.58;cursor:not-allowed}
|
||||||
|
.privacy{margin:18px 0 0;color:var(--muted);font-size:12px}.status{min-height:24px;margin:0;color:var(--primary);font-weight:650}
|
||||||
|
@media(max-width:420px){.phone{border:0}main{padding-inline:18px}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><h1>采购执行</h1></header>
|
||||||
|
<main>
|
||||||
|
<p class="eyebrow">人员与设备身份</p>
|
||||||
|
<p class="intro">登录后将绑定当前测试设备,并检查采购所需能力。</p>
|
||||||
|
<form id="loginForm" novalidate>
|
||||||
|
<div class="field">
|
||||||
|
<label for="account">采购账号</label>
|
||||||
|
<input id="account" autocomplete="username" value="buyer.demo" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<input id="password" type="password" autocomplete="current-password" required>
|
||||||
|
<button class="icon-button" id="togglePassword" type="button" aria-label="显示密码">显示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="device">设备身份</label>
|
||||||
|
<select id="device">
|
||||||
|
<option>测试设备 A · 本机</option>
|
||||||
|
<option>重新登记此设备</option>
|
||||||
|
</select>
|
||||||
|
<div class="device" aria-label="设备状态">
|
||||||
|
<strong>设备可登记</strong>
|
||||||
|
<span>仅上报版本和就绪能力,不上传设备中的敏感内容</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="scenario">原型提交结果</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="success">登录成功</option>
|
||||||
|
<option value="invalid">账号或密码不正确</option>
|
||||||
|
<option value="network">网络连接失败</option>
|
||||||
|
<option value="disabled">账号或设备已停用</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="error" id="error" role="alert">请输入账号和密码。</div>
|
||||||
|
<button class="primary" id="submit" type="submit">登录并绑定设备</button>
|
||||||
|
<p class="status" id="status" role="status" aria-live="polite"></p>
|
||||||
|
</form>
|
||||||
|
<p class="privacy">登录凭据由 Android 安全存储处理。原型不保存任何输入。</p>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form=document.querySelector("#loginForm"),password=document.querySelector("#password"),account=document.querySelector("#account"),error=document.querySelector("#error"),submit=document.querySelector("#submit"),status=document.querySelector("#status"),scenario=document.querySelector("#scenario");
|
||||||
|
document.querySelector("#togglePassword").addEventListener("click",e=>{const hidden=password.type==="password";password.type=hidden?"text":"password";e.currentTarget.textContent=hidden?"隐藏":"显示";e.currentTarget.setAttribute("aria-label",hidden?"隐藏密码":"显示密码")});
|
||||||
|
form.addEventListener("submit",e=>{e.preventDefault();error.classList.remove("show");if(!account.value.trim()||!password.value){error.textContent="请输入账号和密码。";error.classList.add("show");(!account.value.trim()?account:password).focus();return}submit.disabled=true;submit.textContent="正在登录";status.textContent="正在建立人员与设备身份…";setTimeout(()=>{if(scenario.value==="success"){status.textContent="身份已建立,即将进入任务主页。";submit.textContent="已登录";location.hash="success";return}password.value="";submit.disabled=false;submit.textContent="登录并绑定设备";status.textContent="";error.textContent=scenario.value==="invalid"?"账号或密码不正确。":scenario.value==="network"?"网络连接失败,账号已保留,请重试。":"账号或设备已停用,请联系管理员。";error.classList.add("show");password.focus()},650)});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 设备设置 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cbd3d9;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line)}
|
||||||
|
header{height:58px;display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}h1,h2,p{margin-top:0}h1{font-size:19px;margin-bottom:0}.back{min-width:44px;min-height:44px;display:grid;place-items:center;color:var(--ink);text-decoration:none;border-radius:6px;font-size:22px}
|
||||||
|
main{padding:16px}.panel{margin-bottom:14px;background:#fff;border:1px solid var(--line);border-radius:6px}.panel h2{font-size:15px;margin:0;padding:14px 15px;border-bottom:1px solid var(--line)}
|
||||||
|
.row{min-height:64px;display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;padding:10px 15px;border-top:1px solid #e4e8eb}.row:first-of-type{border-top:0}.row strong,.row span{display:block}.row span{font-size:13px;color:var(--muted);margin-top:3px}.state{font-size:13px!important;font-weight:800!important;color:var(--primary)!important}.state.warn{color:var(--warn)!important}.state.bad{color:var(--danger)!important}
|
||||||
|
button,a,select{font:inherit}.scenario{padding:14px 15px}.scenario label{display:block;font-weight:750;margin-bottom:7px}.scenario select{width:100%;min-height:46px;padding:0 10px;border:1px solid #75818a;border-radius:6px;background:#fff;color:var(--ink)}.action{min-width:88px;min-height:44px;border:1px solid #75818a;border-radius:6px;background:#fff;color:var(--ink);font-weight:750}.wide{width:100%;min-height:50px;border:1px solid #75818a;border-radius:6px;background:#fff;font-weight:800}.logout{color:var(--danger);border-color:var(--danger)}.logout:disabled{color:#7d878f;border-color:#aeb6bc;background:#e5e8ea}
|
||||||
|
.notice{padding:12px;margin-bottom:14px;border-left:4px solid var(--primary);background:#e9f3ef}.notice strong,.notice span{display:block}.notice span{color:var(--muted);margin-top:3px}.machine{font:12px ui-monospace,SFMono-Regular,Consolas,monospace!important}
|
||||||
|
dialog{width:min(calc(100% - 32px),380px);padding:0;border:1px solid var(--line);border-radius:6px}dialog::backdrop{background:#18212a88}.dialog-body{padding:20px}.dialog-body h2{font-size:19px}.dialog-actions{display:flex;gap:10px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--line)}.dialog-actions button{min-height:46px;padding:0 15px;border-radius:6px;font-weight:750}.cancel{border:1px solid #76838c;background:#fff}.confirm{border:0;background:var(--danger);color:#fff}
|
||||||
|
button:focus-visible,a:focus-visible{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}@media(max-width:360px){.row{grid-template-columns:1fr}.action{width:100%}}@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><a class="back" href="android-task-home.html" aria-label="返回任务主页">‹</a><h1>设备设置</h1></header>
|
||||||
|
<main>
|
||||||
|
<div class="notice" id="notice" role="status" aria-live="polite"><strong>设备可以执行采购任务</strong><span>以下状态仅为脱敏的测试环境示例。</span></div>
|
||||||
|
<section class="panel scenario">
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="ready">全部就绪</option>
|
||||||
|
<option value="missing">无障碍权限缺失</option>
|
||||||
|
<option value="connection">服务连接失败</option>
|
||||||
|
<option value="disabled">设备已停用</option>
|
||||||
|
<option value="active">存在活跃任务</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="permissionTitle">
|
||||||
|
<h2 id="permissionTitle">权限与应用</h2>
|
||||||
|
<div class="row"><div><strong>无障碍服务</strong><span>用于识别稳定语义和执行允许动作</span></div><button class="action" id="accessibility" type="button" data-open="无障碍设置">已开启</button></div>
|
||||||
|
<div class="row"><div><strong>通知权限</strong><span>提供返回任务和停止的稳定入口</span></div><button class="action" type="button" data-open="通知设置">已允许</button></div>
|
||||||
|
<div class="row"><div><strong>拼多多</strong><span class="machine">com.xunmeng.pinduoduo · 测试记录</span></div><span class="state">已安装</span></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="connectionTitle">
|
||||||
|
<h2 id="connectionTitle">连接状态</h2>
|
||||||
|
<div class="row"><div><strong>后端服务</strong><span>测试环境 · 不显示内部地址</span></div><span class="state" id="backendState">已连接</span></div>
|
||||||
|
<div class="row"><div><strong>模型服务</strong><span>能力检查通过 · 不显示密钥</span></div><span class="state" id="modelState">可用</span></div>
|
||||||
|
<div class="row"><div><strong>最近心跳</strong><span>刚刚,仅上报版本和就绪能力</span></div><button class="action" id="test" type="button">重新检查</button></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="aboutTitle">
|
||||||
|
<h2 id="aboutTitle">版本与设备</h2>
|
||||||
|
<div class="row"><div><strong>App 版本</strong><span class="machine">0.1.0-prototype</span></div><span class="state">测试版</span></div>
|
||||||
|
<div class="row"><div><strong>设备身份</strong><span>测试设备 A · 已绑定演示账号</span></div><span class="state" id="deviceState">有效</span></div>
|
||||||
|
</section>
|
||||||
|
<button class="wide logout" id="logout" type="button">退出登录</button>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<dialog id="logoutDialog" aria-labelledby="logoutTitle">
|
||||||
|
<div class="dialog-body"><h2 id="logoutTitle">退出采购账号?</h2><p>当前没有运行任务。退出后将清除本机安全会话并返回登录页。</p></div>
|
||||||
|
<div class="dialog-actions"><button class="cancel" id="cancel" type="button">取消</button><button class="confirm" id="confirm" type="button">确认退出</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script>
|
||||||
|
const notice=document.querySelector("#notice"),test=document.querySelector("#test"),dialog=document.querySelector("#logoutDialog"),cancel=document.querySelector("#cancel"),scenario=document.querySelector("#scenario"),accessibility=document.querySelector("#accessibility"),backendState=document.querySelector("#backendState"),modelState=document.querySelector("#modelState"),deviceState=document.querySelector("#deviceState"),logout=document.querySelector("#logout");
|
||||||
|
const states={
|
||||||
|
ready:{title:"设备可以执行采购任务",text:"以下状态仅为脱敏的测试环境示例。"},
|
||||||
|
missing:{title:"无障碍服务未开启",text:"打开对应系统设置并授权后,返回本页重新检查。"},
|
||||||
|
connection:{title:"服务连接失败",text:"后端或模型能力暂不可用,当前不能领取任务。"},
|
||||||
|
disabled:{title:"设备已停用",text:"不要反复重试,请联系管理员检查设备绑定。"},
|
||||||
|
active:{title:"存在活跃任务",text:"运行任务安全停止前不能退出登录。"}
|
||||||
|
};
|
||||||
|
function render(){
|
||||||
|
const value=scenario.value,state=states[value];
|
||||||
|
notice.innerHTML="<strong>"+state.title+"</strong><span>"+state.text+"</span>";
|
||||||
|
accessibility.textContent=value==="missing"?"打开设置":"已开启";
|
||||||
|
backendState.textContent=value==="connection"?"连接失败":"已连接";
|
||||||
|
backendState.className="state "+(value==="connection"?"bad":"");
|
||||||
|
modelState.textContent=value==="connection"?"不可用":"可用";
|
||||||
|
modelState.className="state "+(value==="connection"?"bad":"");
|
||||||
|
deviceState.textContent=value==="disabled"?"已停用":"有效";
|
||||||
|
deviceState.className="state "+(value==="disabled"?"bad":"");
|
||||||
|
logout.disabled=value==="active";
|
||||||
|
}
|
||||||
|
scenario.addEventListener("change",render);
|
||||||
|
document.querySelectorAll("[data-open]").forEach(button=>button.addEventListener("click",()=>{notice.innerHTML="<strong>已发出系统命令</strong><span>即将打开"+button.dataset.open+",返回后会重新检查。</span>";notice.scrollIntoView({behavior:"smooth"})}));
|
||||||
|
test.addEventListener("click",()=>{test.disabled=true;test.textContent="检查中";notice.innerHTML="<strong>正在检查服务连接</strong><span>不会显示地址、密钥或设备令牌。</span>";setTimeout(()=>{test.disabled=false;test.textContent="重新检查";notice.innerHTML="<strong>连接检查完成</strong><span>后端和模型服务均可用。</span>"},650)});
|
||||||
|
logout.addEventListener("click",()=>{dialog.showModal();setTimeout(()=>cancel.focus(),0)});cancel.addEventListener("click",()=>dialog.close());dialog.addEventListener("cancel",e=>{e.preventDefault();dialog.close()});document.querySelector("#confirm").addEventListener("click",()=>{location.href="android-login.html"});render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 任务主页 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cdd4da;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}
|
||||||
|
.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line);padding-bottom:96px}
|
||||||
|
header{height:58px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;background:#fff;border-bottom:1px solid var(--line)}h1,h2,p{margin-top:0}h1{font-size:20px;margin-bottom:0}h2{font-size:16px;margin-bottom:12px}
|
||||||
|
.settings{min-width:48px;min-height:44px;display:grid;place-items:center;color:var(--ink);text-decoration:none;border:1px solid var(--line);border-radius:6px;font-weight:700}
|
||||||
|
main{padding:18px}.operator{color:var(--muted);margin:0 0 18px}.panel{background:#fff;border:1px solid var(--line);border-radius:6px;padding:16px;margin-bottom:14px}
|
||||||
|
.readiness{list-style:none;margin:0;padding:0}.readiness li{min-height:44px;display:grid;grid-template-columns:28px 1fr auto;align-items:center;border-top:1px solid #e5e9ec}.readiness li:first-child{border-top:0}.mark{font-weight:800;color:var(--primary)}.mark.bad{color:var(--danger)}.state{font-size:13px;color:var(--muted)}
|
||||||
|
label{font-size:13px;font-weight:700}select,button,a{font:inherit}select{width:100%;min-height:44px;margin:6px 0 14px;padding:8px;border:1px solid #8f9aa3;border-radius:6px;background:#fff}
|
||||||
|
.message{min-height:74px;padding:12px;border-left:4px solid var(--primary);background:#e9f3ef}.message.warn{border-color:var(--warn);background:#fff7e7}.message.danger{border-color:var(--danger);background:#fff1ef}.message strong,.message span{display:block}.message span{margin-top:4px;color:var(--muted)}
|
||||||
|
.secondary{min-height:46px;width:100%;margin-top:10px;border:1px solid #78858e;border-radius:6px;background:#fff;color:var(--ink);font-weight:700}.secondary[hidden]{display:none}
|
||||||
|
.bottom{position:fixed;z-index:10;bottom:0;left:50%;transform:translateX(-50%);width:min(100%,420px);padding:12px 18px 16px;background:#fff;border-top:1px solid var(--line)}.primary{width:100%;min-height:52px;border:0;border-radius:6px;background:var(--primary);color:#fff;font-weight:800}.primary:disabled{opacity:.5}
|
||||||
|
button:focus-visible,a:focus-visible,select:focus{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}
|
||||||
|
.busy{display:none;margin:8px 0 0;text-align:center;color:var(--muted);font-size:13px}.busy.show{display:block}
|
||||||
|
@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><h1>任务</h1><a class="settings" href="android-settings.html" aria-label="打开设备设置">设置</a></header>
|
||||||
|
<main>
|
||||||
|
<p class="operator">采购员:演示账号 · 测试设备 A</p>
|
||||||
|
<section class="panel" aria-labelledby="readyTitle">
|
||||||
|
<h2 id="readyTitle">设备就绪检查</h2>
|
||||||
|
<ul class="readiness" id="readiness">
|
||||||
|
<li><span class="mark">✓</span><span>无障碍服务</span><span class="state">已开启</span></li>
|
||||||
|
<li><span class="mark">✓</span><span>拼多多</span><span class="state">已安装</span></li>
|
||||||
|
<li><span class="mark">✓</span><span>网络连接</span><span class="state">正常</span></li>
|
||||||
|
<li><span class="mark">✓</span><span>当前任务</span><span class="state">无活跃任务</span></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="workTitle">
|
||||||
|
<h2 id="workTitle">工作状态</h2>
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="ready">全部就绪</option>
|
||||||
|
<option value="missing">无障碍权限缺失</option>
|
||||||
|
<option value="active">已有活跃任务</option>
|
||||||
|
<option value="empty">暂时没有任务</option>
|
||||||
|
<option value="timeout">领取请求超时</option>
|
||||||
|
</select>
|
||||||
|
<div class="message" id="message" role="status" aria-live="polite"><strong>可以获取任务</strong><span>点击后只领取一条任务,不会自动开始采购。</span></div>
|
||||||
|
<button class="secondary" id="secondary" type="button" hidden>手动刷新</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div class="bottom">
|
||||||
|
<button class="primary" id="claim" type="button">获取任务</button>
|
||||||
|
<p class="busy" id="busy" role="status">正在检查并领取任务…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const scenario=document.querySelector("#scenario"),message=document.querySelector("#message"),claim=document.querySelector("#claim"),secondary=document.querySelector("#secondary"),busy=document.querySelector("#busy"),readiness=document.querySelector("#readiness");
|
||||||
|
const states={
|
||||||
|
ready:{title:"可以获取任务",text:"点击后只领取一条任务,不会自动开始采购。",button:"获取任务",disabled:false,kind:"",secondary:""},
|
||||||
|
missing:{title:"尚未就绪",text:"无障碍服务未开启。完成设置后再返回检查。",button:"获取任务",disabled:true,kind:"danger",secondary:"打开无障碍设置"},
|
||||||
|
active:{title:"任务 #DEMO-2407 等待继续",text:"有活跃任务时不能领取下一条。",button:"继续任务",disabled:false,kind:"warn",secondary:""},
|
||||||
|
empty:{title:"暂时没有可领取任务",text:"稍后可手动刷新,页面不会循环弹出错误。",button:"获取任务",disabled:true,kind:"",secondary:"手动刷新"},
|
||||||
|
timeout:{title:"领取状态需要确认",text:"请求超时,先查询本设备的活跃任务,避免重复领取。",button:"检查活跃任务",disabled:false,kind:"warn",secondary:""}
|
||||||
|
};
|
||||||
|
function render(){const s=states[scenario.value];message.className="message "+s.kind;message.innerHTML="<strong>"+s.title+"</strong><span>"+s.text+"</span>";claim.textContent=s.button;claim.disabled=s.disabled;secondary.hidden=!s.secondary;secondary.textContent=s.secondary;const first=readiness.children[0];first.querySelector(".mark").textContent=scenario.value==="missing"?"!":"✓";first.querySelector(".mark").className="mark "+(scenario.value==="missing"?"bad":"");first.querySelector(".state").textContent=scenario.value==="missing"?"未开启":"已开启"}
|
||||||
|
scenario.addEventListener("change",render);
|
||||||
|
claim.addEventListener("click",()=>{claim.disabled=true;busy.classList.add("show");setTimeout(()=>{busy.classList.remove("show");if(scenario.value==="ready")location.href="android-task-preview.html";else if(scenario.value==="active")location.href="android-task-running.html";else{message.innerHTML="<strong>未发现重复任务</strong><span>现在可以重新领取。</span>";claim.disabled=false;claim.textContent="重新获取任务"}},650)});
|
||||||
|
secondary.addEventListener("click",()=>{message.innerHTML="<strong>已发出打开命令</strong><span>"+secondary.textContent+"将由 Android 系统处理。</span>"});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 任务预览 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cbd3d9;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line);padding-bottom:94px}
|
||||||
|
header{height:58px;display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}h1{font-size:19px;margin:0}.back{min-width:44px;min-height:44px;display:grid;place-items:center;color:var(--ink);text-decoration:none;border-radius:6px;font-size:22px}
|
||||||
|
main{padding:16px}.task-id{color:var(--muted);margin:0 0 14px}.panel{padding:16px;margin-bottom:14px;background:#fff;border:1px solid var(--line);border-radius:6px}.panel h2{font-size:16px;margin:0 0 12px}
|
||||||
|
.reference{height:150px;display:grid;place-items:center;margin-bottom:14px;border:1px dashed #88949d;background:linear-gradient(135deg,#e4e8ea 25%,#f7f8f9 25%,#f7f8f9 50%,#e4e8ea 50%,#e4e8ea 75%,#f7f8f9 75%);background-size:18px 18px;color:#34414b;font-weight:700;text-align:center}
|
||||||
|
dl{display:grid;grid-template-columns:84px 1fr;gap:10px 12px;margin:0}dt{color:var(--muted)}dd{margin:0;font-weight:650}.constraint{font-size:18px;color:var(--primary)}
|
||||||
|
.derived{border-left:4px solid #b57a20;background:#fff8e9}.derived p{margin:0;color:#574619}.notice{padding:12px;border-left:4px solid var(--primary);background:#e9f3ef}.notice.warn{border-color:var(--warn);background:#fff4dc}.notice strong,.notice span{display:block}.notice span{color:var(--muted);margin-top:3px}
|
||||||
|
label{font-size:13px;font-weight:700}select,button,a{font:inherit}select{width:100%;min-height:44px;padding:8px;margin-top:6px;border:1px solid #89959e;border-radius:6px;background:#fff}
|
||||||
|
.bottom{position:fixed;z-index:10;bottom:0;left:50%;transform:translateX(-50%);width:min(100%,420px);padding:12px 18px 16px;background:#fff;border-top:1px solid var(--line)}.primary{width:100%;min-height:52px;border:0;border-radius:6px;background:var(--primary);color:#fff;font-weight:800}.primary:disabled{opacity:.5}
|
||||||
|
a:focus-visible,button:focus-visible,select:focus{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><a class="back" href="android-task-home.html" aria-label="返回任务主页">‹</a><h1>任务预览</h1></header>
|
||||||
|
<main>
|
||||||
|
<p class="task-id">任务 #DEMO-2407 · 状态 CLAIMED</p>
|
||||||
|
<section class="panel" aria-labelledby="originalTitle">
|
||||||
|
<h2 id="originalTitle">管理员原始要求</h2>
|
||||||
|
<div class="reference" role="img" aria-label="脱敏的收纳盒参考图占位">脱敏参考图<br>收纳盒样例</div>
|
||||||
|
<dl>
|
||||||
|
<dt>商品标题</dt><dd>透明抽屉式桌面收纳盒</dd>
|
||||||
|
<dt>SKU</dt><dd>白色 · 中号</dd>
|
||||||
|
<dt>数量</dt><dd class="constraint">2 件</dd>
|
||||||
|
<dt>最高预算</dt><dd class="constraint">¥ 48.00 / 件</dd>
|
||||||
|
<dt>描述</dt><dd>带把手,可叠放,优先选择相同外观。</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section class="panel derived" aria-labelledby="derivedTitle">
|
||||||
|
<h2 id="derivedTitle">AI 辅助理解 · 待验证</h2>
|
||||||
|
<p>可能关键词:透明、抽屉式、中号、可叠放。最终数量和预算始终以原始要求为准。</p>
|
||||||
|
</section>
|
||||||
|
<section class="panel">
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="ready">租约有效,可以开始</option>
|
||||||
|
<option value="expired">任务租约已释放</option>
|
||||||
|
</select>
|
||||||
|
<div class="notice" id="notice" role="status"><strong>开始前请再次检查</strong><span>开始后将打开拼多多并执行有界检索。</span></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div class="bottom"><button class="primary" id="start" type="button">开始采购</button></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const scenario=document.querySelector("#scenario"),notice=document.querySelector("#notice"),start=document.querySelector("#start");
|
||||||
|
scenario.addEventListener("change",()=>{const expired=scenario.value==="expired";notice.className="notice "+(expired?"warn":"");notice.innerHTML=expired?"<strong>任务已释放</strong><span>不能继续执行,请返回任务主页重新获取。</span>":"<strong>开始前请再次检查</strong><span>开始后将打开拼多多并执行有界检索。</span>";start.textContent=expired?"返回任务主页":"开始采购";start.dataset.expired=expired});
|
||||||
|
start.addEventListener("click",()=>{location.href=start.dataset.expired==="true"?"android-task-home.html":"android-task-running.html"});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 任务结果 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cbd3d9;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line)}
|
||||||
|
header{height:58px;display:flex;align-items:center;padding:0 18px;background:#fff;border-bottom:1px solid var(--line)}h1,h2,p{margin-top:0}h1{font-size:19px;margin-bottom:0}main{padding:16px}
|
||||||
|
.panel{padding:15px;margin-bottom:14px;background:#fff;border:1px solid var(--line);border-radius:6px}.panel h2{font-size:16px;margin-bottom:12px}
|
||||||
|
label{font-size:13px;font-weight:700}select,button,a{font:inherit}select{width:100%;min-height:44px;margin-top:6px;padding:8px;border:1px solid #89959e;border-radius:6px;background:#fff}
|
||||||
|
.result{padding:18px;margin-bottom:14px;border-left:5px solid var(--primary);background:#e9f3ef}.result.warn{border-color:var(--warn);background:#fff5df}.result.danger{border-color:var(--danger);background:#fff0ee}.result strong,.result span{display:block}.result strong{font-size:19px}.result span{margin-top:5px}.machine{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted)}
|
||||||
|
dl{display:grid;grid-template-columns:98px 1fr;gap:10px;margin:0}dt{color:var(--muted)}dd{margin:0;font-weight:650}.steps{list-style:none;margin:0;padding:0}.steps li{padding:9px 0;border-top:1px solid #e4e8eb}.steps li:first-child{border-top:0}.steps strong,.steps span{display:block}.steps span{color:var(--muted);font-size:13px}
|
||||||
|
.actions{display:grid;gap:10px}.actions button,.actions a{min-height:50px;display:grid;place-items:center;border-radius:6px;text-decoration:none;font-weight:800}.primary{border:0;background:var(--primary);color:#fff}.secondary{border:1px solid #74818a;background:#fff;color:var(--ink)}.disabled-note{margin:0;color:var(--danger);font-size:13px;font-weight:700}
|
||||||
|
button:focus-visible,a:focus-visible,select:focus{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><h1>任务结果</h1></header>
|
||||||
|
<main>
|
||||||
|
<section class="panel">
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="success">验证成功 · 已同步</option>
|
||||||
|
<option value="rejected">人员拒绝候选</option>
|
||||||
|
<option value="none">确认无匹配</option>
|
||||||
|
<option value="manual">转人工处理</option>
|
||||||
|
<option value="pending">结果待同步</option>
|
||||||
|
<option value="failed">安全错误 · 已停止</option>
|
||||||
|
<option value="canceled">任务已取消</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<div class="result" id="result" role="status"><strong>验证完成,未提交订单</strong><span>人员已接受候选,结论已同步到后台。</span><p class="machine">SUCCEEDED · order_submitted=false</p></div>
|
||||||
|
<section class="panel" aria-labelledby="summaryTitle">
|
||||||
|
<h2 id="summaryTitle">处理摘要</h2>
|
||||||
|
<dl id="summary"><dt>停止状态</dt><dd>自动化已停止</dd><dt>最终步骤</dt><dd>人工确认候选</dd><dt>结果同步</dt><dd>服务端已确认接收</dd><dt>本地证据</dt><dd>可按保留策略清理</dd></dl>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="eventsTitle">
|
||||||
|
<h2 id="eventsTitle">关键记录</h2>
|
||||||
|
<ol class="steps" id="events"><li><strong>候选检查完成</strong><span>最多检查 5 个,本次发现 3 个</span></li><li><strong>人员作出结论</strong><span>接受候选</span></li><li><strong>结果同步完成</strong><span>后台已确认接收</span></li></ol>
|
||||||
|
</section>
|
||||||
|
<section class="panel actions" id="actions"><a class="primary" href="android-task-home.html">返回任务主页</a></section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const scenario=document.querySelector("#scenario"),result=document.querySelector("#result"),summary=document.querySelector("#summary"),events=document.querySelector("#events"),actions=document.querySelector("#actions");
|
||||||
|
const states={
|
||||||
|
success:{kind:"",title:"验证完成,未提交订单",text:"人员已接受候选,结论已同步到后台。",code:"SUCCEEDED · order_submitted=false",summary:"<dt>停止状态</dt><dd>自动化已停止</dd><dt>最终步骤</dt><dd>人工确认候选</dd><dt>结果同步</dt><dd>服务端已确认接收</dd><dt>本地证据</dt><dd>可按保留策略清理</dd>",events:"<li><strong>候选检查完成</strong><span>最多检查 5 个,本次发现 3 个</span></li><li><strong>人员作出结论</strong><span>接受候选</span></li><li><strong>结果同步完成</strong><span>后台已确认接收</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a>'},
|
||||||
|
rejected:{kind:"warn",title:"候选已拒绝,未提交订单",text:"人员认为当前候选不符合采购要求,结论已同步。",code:"REJECTED · order_submitted=false",summary:"<dt>停止状态</dt><dd>自动化已停止</dd><dt>人员结论</dt><dd>拒绝候选</dd><dt>结果同步</dt><dd>服务端已确认接收</dd><dt>恢复规则</dt><dd>本任务不会自动继续</dd>",events:"<li><strong>候选检查完成</strong><span>候选证据已保留</span></li><li><strong>人员拒绝候选</strong><span>已记录非敏感原因</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a>'},
|
||||||
|
none:{kind:"warn",title:"确认无匹配,未提交订单",text:"本次有界检索没有可接受候选,结论已同步。",code:"NO_MATCH · order_submitted=false",summary:"<dt>停止状态</dt><dd>自动化已停止</dd><dt>人员结论</dt><dd>无匹配</dd><dt>结果同步</dt><dd>服务端已确认接收</dd><dt>恢复规则</dt><dd>需要管理员重新评估任务</dd>",events:"<li><strong>候选检查完成</strong><span>已达到最多 5 个的边界</span></li><li><strong>人员确认无匹配</strong><span>不会自动扩大检索</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a>'},
|
||||||
|
manual:{kind:"warn",title:"已转人工处理,未提交订单",text:"自动化保持停止,等待管理员安排后续人工处理。",code:"MANUAL_REVIEW · order_submitted=false",summary:"<dt>停止状态</dt><dd>自动化已停止</dd><dt>人员结论</dt><dd>需人工处理</dd><dt>结果同步</dt><dd>服务端已确认接收</dd><dt>恢复规则</dt><dd>不能从原步骤自动继续</dd>",events:"<li><strong>检测到不确定项</strong><span>关键属性无法可靠确认</span></li><li><strong>人员转人工处理</strong><span>自动化保持硬停止</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a>'},
|
||||||
|
pending:{kind:"warn",title:"结果待同步",text:"本地已受控保存结论和证据,尚未得到服务端确认。",code:"SYNC_PENDING · order_submitted=false",summary:"<dt>停止状态</dt><dd>自动化已停止</dd><dt>最终步骤</dt><dd>人工确认候选</dd><dt>结果同步</dt><dd>等待网络恢复</dd><dt>本地证据</dt><dd>受控保留,暂不清理</dd>",events:"<li><strong>人员作出结论</strong><span>结论已保存在本机</span></li><li><strong>上传超时</strong><span>最终状态尚未确认</span></li>",actions:'<button class="primary" id="sync" type="button">查询状态并重试同步</button><a class="secondary" href="android-task-home.html">稍后处理</a>'},
|
||||||
|
failed:{kind:"danger",title:"安全错误,任务已停止",text:"检测到验证码。不会自动重试或从原步骤继续。",code:"FAILED · SAFETY_CAPTCHA",summary:"<dt>停止状态</dt><dd>已安全停止</dd><dt>失败步骤</dt><dd>打开检索结果页</dd><dt>错误类别</dt><dd>SAFETY_CAPTCHA</dd><dt>建议操作</dt><dd>联系管理员检查后新建尝试</dd>",events:"<li><strong>检测到验证码</strong><span>自动化立即请求安全停止</span></li><li><strong>证据已保存</strong><span>等待服务端确认接收</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a><p class="disabled-note">安全错误不允许自动重试。</p>'},
|
||||||
|
canceled:{kind:"warn",title:"任务已取消",text:"执行已停止并完成清理,本任务不会自动恢复。",code:"CANCELED · order_submitted=false",summary:"<dt>停止状态</dt><dd>已安全停止</dd><dt>最终步骤</dt><dd>检查候选</dd><dt>结果同步</dt><dd>服务端已确认终态</dd><dt>恢复规则</dt><dd>不能原地继续</dd>",events:"<li><strong>采购人员请求停止</strong><span>等待当前动作到达安全边界</span></li><li><strong>任务已取消</strong><span>前台服务已清理</span></li>",actions:'<a class="primary" href="android-task-home.html">返回任务主页</a>'}
|
||||||
|
};
|
||||||
|
function render(){const s=states[scenario.value];result.className="result "+s.kind;result.innerHTML="<strong>"+s.title+"</strong><span>"+s.text+'</span><p class="machine">'+s.code+"</p>";summary.innerHTML=s.summary;events.innerHTML=s.events;actions.innerHTML=s.actions;const sync=document.querySelector("#sync");if(sync)sync.addEventListener("click",()=>{sync.disabled=true;sync.textContent="正在查询服务端状态…";setTimeout(()=>{scenario.value="success";render()},650)})}
|
||||||
|
scenario.addEventListener("change",render);if(location.hash==="#failed")scenario.value="failed";if(location.hash==="#canceled")scenario.value="canceled";render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>App 任务执行 - 低保真原型</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light;--ink:#17212b;--muted:#5d6973;--line:#cbd3d9;--surface:#fff;--bg:#eef1f3;--primary:#176b52;--warn:#8b5a08;--danger:#a13a32;--focus:#c57414}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;letter-spacing:0;padding-top:38px}
|
||||||
|
.prototype{position:fixed;z-index:30;top:0;left:0;right:0;height:38px;display:grid;place-items:center;background:#2d343a;color:#fff;font-size:12px;font-weight:700}.phone{width:min(100%,420px);min-height:calc(100vh - 38px);margin:auto;background:#f7f8f9;border-inline:1px solid var(--line);padding-bottom:104px}
|
||||||
|
header{min-height:64px;display:flex;align-items:center;justify-content:space-between;padding:8px 18px;background:#fff;border-bottom:1px solid var(--line)}h1,h2,p{margin-top:0}h1{font-size:19px;margin-bottom:2px}.sub{color:var(--muted);font-size:12px}.status-word{padding:6px 8px;border:1px solid #76a994;color:#0e5942;font-size:12px;font-weight:800}
|
||||||
|
main{padding:16px}.panel{padding:15px;margin-bottom:14px;background:#fff;border:1px solid var(--line);border-radius:6px}.panel h2{font-size:16px;margin-bottom:12px}
|
||||||
|
label{font-size:13px;font-weight:700}select,button,a{font:inherit}select{width:100%;min-height:44px;margin-top:6px;padding:8px;border:1px solid #89959e;border-radius:6px;background:#fff}
|
||||||
|
.current{border-left:4px solid var(--primary);background:#e9f3ef}.current.warn{border-color:var(--warn);background:#fff5df}.current.danger{border-color:var(--danger);background:#fff0ee}.current strong,.current span{display:block}.current strong{font-size:17px}.current span{margin-top:4px;color:var(--muted)}
|
||||||
|
ol{list-style:none;margin:0;padding:0}li{position:relative;min-height:52px;padding:5px 0 10px 38px;border-left:2px solid #c8d0d5;margin-left:12px}li:last-child{border-left-color:transparent}li::before{content:"";position:absolute;left:-8px;top:7px;width:14px;height:14px;border:2px solid #7b8790;border-radius:50%;background:#fff}li.done::before{background:var(--primary);border-color:var(--primary)}li.now::before{border:4px solid var(--primary)}li strong,li span{display:block}li span{font-size:13px;color:var(--muted)}
|
||||||
|
.notification{border-left:4px solid #596a78;background:#edf0f2}.notification p{margin:0;color:var(--muted)}.notification-actions{display:flex;gap:8px;margin-top:10px}.notification a{min-height:44px;display:flex;align-items:center;padding:0 12px;border:1px solid #7e8991;border-radius:6px;color:var(--ink);text-decoration:none;font-weight:700}
|
||||||
|
.bottom{position:fixed;z-index:10;bottom:0;left:50%;transform:translateX(-50%);width:min(100%,420px);display:flex;gap:10px;padding:12px 18px 16px;background:#fff;border-top:1px solid var(--line)}.bottom button,.bottom a{min-height:52px;display:grid;place-items:center;border-radius:6px;font-weight:800;text-decoration:none}.stop{flex:1;border:1px solid var(--danger);background:#fff;color:var(--danger)}.return{flex:1.2;border:0;background:var(--primary);color:#fff}
|
||||||
|
dialog{width:min(calc(100% - 32px),380px);padding:0;border:1px solid var(--line);border-radius:6px}dialog::backdrop{background:#18212a88}.dialog-body{padding:20px}.dialog-body h2{font-size:19px}.dialog-actions{display:flex;gap:10px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--line)}.dialog-actions button{min-height:46px;padding:0 16px;border-radius:6px;font-weight:750}.cancel{border:1px solid #7e8991;background:#fff}.confirm-stop{border:0;background:var(--danger);color:#fff}
|
||||||
|
button:focus-visible,a:focus-visible,select:focus{outline:3px solid color-mix(in srgb,var(--focus) 45%,transparent);outline-offset:2px}@media(max-width:420px){.phone{border:0}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<div class="phone">
|
||||||
|
<header><div><h1>正在执行任务</h1><span class="sub">#DEMO-2407</span></div><span class="status-word" id="statusWord">运行中</span></header>
|
||||||
|
<main>
|
||||||
|
<section class="panel">
|
||||||
|
<label for="scenario">原型状态</label>
|
||||||
|
<select id="scenario">
|
||||||
|
<option value="running">正在检查候选</option>
|
||||||
|
<option value="stopping">正在安全停止</option>
|
||||||
|
<option value="captcha">验证码拦截 · 安全错误</option>
|
||||||
|
<option value="manual">低置信度 · 等待人工</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<section class="panel current" id="current" role="status" aria-live="polite">
|
||||||
|
<strong>检查候选 3 / 最多 5 个</strong><span>正在核对标题、规格、数量与预算约束。</span>
|
||||||
|
</section>
|
||||||
|
<section class="panel" aria-labelledby="stepsTitle">
|
||||||
|
<h2 id="stepsTitle">执行步骤</h2>
|
||||||
|
<ol id="steps">
|
||||||
|
<li class="done"><strong>设备预检</strong><span>权限、应用和网络均可用</span></li>
|
||||||
|
<li class="done"><strong>理解采购要求</strong><span>原始数量和预算已锁定</span></li>
|
||||||
|
<li class="done"><strong>打开拼多多并搜索</strong><span>使用派生关键词检索</span></li>
|
||||||
|
<li class="now"><strong>检查候选</strong><span>有界检查,不自动下单</span></li>
|
||||||
|
<li><strong>等待人工确认</strong><span>到达后自动化硬停止</span></li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
<section class="panel notification">
|
||||||
|
<h2>前台服务通知</h2>
|
||||||
|
<p>切换到拼多多后,可通过持续通知稳定返回本页或请求停止。</p>
|
||||||
|
<div class="notification-actions"><a href="android-task-running.html">返回任务</a><a href="#stop" id="notificationStop">停止</a></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div class="bottom" id="bottom"><button class="stop" id="stop" type="button">安全停止</button><a class="return" href="android-candidate-confirm.html">模拟找到候选</a></div>
|
||||||
|
</div>
|
||||||
|
<dialog id="stopDialog" aria-labelledby="stopTitle">
|
||||||
|
<div class="dialog-body"><h2 id="stopTitle">安全停止任务?</h2><p>系统会等待当前动作到达安全边界,保存执行状态后停止。不会立即销毁状态。</p></div>
|
||||||
|
<div class="dialog-actions"><button class="cancel" id="cancelStop" type="button">取消</button><button class="confirm-stop" id="confirmStop" type="button">确认停止</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script>
|
||||||
|
const scenario=document.querySelector("#scenario"),current=document.querySelector("#current"),statusWord=document.querySelector("#statusWord"),bottom=document.querySelector("#bottom"),dialog=document.querySelector("#stopDialog"),cancel=document.querySelector("#cancelStop");
|
||||||
|
const states={
|
||||||
|
running:{status:"运行中",kind:"",html:"<strong>检查候选 3 / 最多 5 个</strong><span>正在核对标题、规格、数量与预算约束。</span>",actions:'<button class="stop" id="stop" type="button">安全停止</button><a class="return" href="android-candidate-confirm.html">模拟找到候选</a>'},
|
||||||
|
stopping:{status:"安全停止中",kind:"warn",html:"<strong>正在安全停止</strong><span>等待当前动作结束并保存状态,请勿强制关闭。</span>",actions:'<a class="return" href="android-task-result.html#canceled">查看停止结果</a>'},
|
||||||
|
captcha:{status:"已安全停止",kind:"danger",html:"<strong>检测到验证码,自动化已停止</strong><span>安全错误 SAFETY_CAPTCHA 不允许自动继续或自动重试。</span>",actions:'<a class="return" href="android-task-result.html#failed">查看失败原因</a>'},
|
||||||
|
manual:{status:"等待人工",kind:"warn",html:"<strong>AI 结果置信度不足</strong><span>自动化已停止,请转入人工确认,不会猜测继续。</span>",actions:'<a class="return" href="android-candidate-confirm.html#manual">进入人工确认</a>'}
|
||||||
|
};
|
||||||
|
function bindStop(){const stop=document.querySelector("#stop");if(stop)stop.addEventListener("click",openStop)}
|
||||||
|
function openStop(e){if(e)e.preventDefault();dialog.showModal();setTimeout(()=>cancel.focus(),0)}
|
||||||
|
scenario.addEventListener("change",()=>{const s=states[scenario.value];statusWord.textContent=s.status;current.className="panel current "+s.kind;current.innerHTML=s.html;bottom.innerHTML=s.actions;bindStop()});
|
||||||
|
document.querySelector("#notificationStop").addEventListener("click",openStop);cancel.addEventListener("click",()=>dialog.close());document.querySelector("#confirmStop").addEventListener("click",()=>{dialog.close();scenario.value="stopping";scenario.dispatchEvent(new Event("change"))});dialog.addEventListener("cancel",e=>{e.preventDefault();dialog.close()});bindStop();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>肉包 P0 原型索引</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; font-family: system-ui, "Microsoft YaHei", sans-serif; color: #17202a; background: #f4f6f8; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; }
|
||||||
|
.prototype { position: sticky; top: 0; z-index: 10; padding: 8px 16px; background: #332b00; color: #fff4b8; font-size: 13px; text-align: center; }
|
||||||
|
header, main { width: min(1080px, calc(100% - 32px)); margin-inline: auto; }
|
||||||
|
header { padding: 32px 0 20px; border-bottom: 1px solid #cfd6dc; }
|
||||||
|
h1 { margin: 0 0 8px; font-size: 28px; letter-spacing: 0; }
|
||||||
|
h2 { margin: 28px 0 12px; font-size: 19px; letter-spacing: 0; }
|
||||||
|
p { margin: 0; color: #4e5b66; line-height: 1.6; }
|
||||||
|
.notice { margin-top: 16px; padding: 12px 14px; border-left: 4px solid #b26a00; background: #fff9e8; color: #513a00; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||||
|
a { display: grid; gap: 6px; min-height: 92px; padding: 16px; border: 1px solid #cfd6dc; border-radius: 6px; background: #fff; color: #0b5cab; text-decoration: none; }
|
||||||
|
a:hover, a:focus-visible { border-color: #0b5cab; outline: 3px solid #b9d8f5; outline-offset: 1px; }
|
||||||
|
strong { color: #17202a; font-size: 16px; }
|
||||||
|
small { color: #5b6770; line-height: 1.45; }
|
||||||
|
footer { padding: 28px 0 40px; color: #5b6770; font-size: 13px; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
header, main { width: min(100% - 24px, 1080px); }
|
||||||
|
header { padding-top: 24px; }
|
||||||
|
h1 { font-size: 23px; }
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prototype">PROTOTYPE - 仅供确认布局和交互,不是生产实现</div>
|
||||||
|
<header>
|
||||||
|
<h1>肉包 P0 页面确认</h1>
|
||||||
|
<p>按工作流逐页检查页面区块、控件、文案和主要动作。所有内容均为脱敏假数据。</p>
|
||||||
|
<p class="notice"><strong>安全边界:</strong>原型只验证候选,不触发加购、结算、支付或下单。</p>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<h2>管理 Web</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<a href="admin-login.html"><strong>1. 管理登录</strong><small>/login · IX-001 · 会话、校验与通用错误</small></a>
|
||||||
|
<a href="admin-task-list.html"><strong>2. 任务列表</strong><small>/tasks · IX-003/008 · 筛选、加载、空与失败</small></a>
|
||||||
|
<a href="admin-task-create.html"><strong>3. 新建任务</strong><small>/tasks/new · IX-002 · 商品资料、图片与约束</small></a>
|
||||||
|
<a href="admin-task-detail.html"><strong>4. 任务详情</strong><small>/tasks/{id} · IX-003/008 · 时间线、候选与证据</small></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Android 采购 App</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<a href="android-login.html"><strong>1. 登录与设备绑定</strong><small>login · IX-004 · 人员和设备身份</small></a>
|
||||||
|
<a href="android-task-home.html"><strong>2. 任务主页</strong><small>tasks · IX-005 · 就绪检查和手动领取</small></a>
|
||||||
|
<a href="android-task-preview.html"><strong>3. 任务预览</strong><small>task/{id}/preview · IX-005/006 · 检查要求后开始</small></a>
|
||||||
|
<a href="android-task-running.html"><strong>4. 任务执行</strong><small>task/{id}/running · IX-006/008 · 步骤、安全停止与回到拼多多</small></a>
|
||||||
|
<a href="android-candidate-confirm.html"><strong>5. 候选确认</strong><small>task/{id}/confirm · IX-007 · 人工结论硬停止点</small></a>
|
||||||
|
<a href="android-task-result.html"><strong>6. 任务结果</strong><small>task/{id}/result · IX-007/008 · 终态与同步状态</small></a>
|
||||||
|
<a href="android-settings.html"><strong>7. 设备设置</strong><small>settings · IX-004/005 · 权限、版本和连接状态</small></a>
|
||||||
|
</div>
|
||||||
|
<footer>T-202 · 已于 2026-07-26 确认,不作为正式界面代码复用。</footer>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
# 路由与页面结构
|
# 路由与页面结构
|
||||||
|
|
||||||
|
T-202 的离线 P0 页面入口见[原型索引](design/index.html)。原型仅用于确认页面区块、
|
||||||
|
控件、文案和主要动作,不代表正式路由实现。
|
||||||
|
|
||||||
## 管理 Web
|
## 管理 Web
|
||||||
|
|
||||||
| 路由 | 页面 | 职责 | 用户故事 | 交互 |
|
| 路由 | 页面 | 职责 | 用户故事 | 交互 |
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
id: T-202
|
||||||
|
title: 生成并确认 P0 Web/App 低保真原型
|
||||||
|
phase: 2
|
||||||
|
deps:
|
||||||
|
- T-104
|
||||||
|
status: DONE
|
||||||
|
created: 2026-07-25
|
||||||
|
context_ref: ab21219a07630b705f3eb681d32b97d9ab49188b
|
||||||
|
work_branch: main
|
||||||
|
write_paths:
|
||||||
|
- docs/design/**
|
||||||
|
- docs/08-interaction-checklist.md
|
||||||
|
- docs/routes.md
|
||||||
|
- docs/current-state.md
|
||||||
|
- docs/00-ai-start-here.md
|
||||||
|
- docs/tasks/T-202.md
|
||||||
|
- progress.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题 / 背景
|
||||||
|
|
||||||
|
Android 已验证到人工确认点,Go-Gin 后端骨架也已建立,但管理 Web 与采购 App 的
|
||||||
|
P0 页面还没有经过人工确认。若直接实现正式 UI,页面区块、控件集合、状态反馈和
|
||||||
|
主要动作可能在开发期间反复变化,并增加 T-203、T-204 和 T-206 的返工。
|
||||||
|
|
||||||
|
## 关联需求与交互
|
||||||
|
|
||||||
|
- 功能:F-001 至 F-007。
|
||||||
|
- 用户故事:US-001 至 US-007。
|
||||||
|
- 交互:IX-001 至 IX-008。
|
||||||
|
- 页面:管理 Web 4 条 P0 路由;Android 7 个 P0 逻辑目的地。
|
||||||
|
- 架构/API:`docs/routes.md`、`docs/api.md`、`docs/04-architecture.md`。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
1. 在 `docs/design/` 生成零构建依赖、可独立打开的 HTML 原型,使用完全脱敏的假数据。
|
||||||
|
2. 管理 Web 覆盖登录、任务列表、新建任务和任务详情;Android 覆盖登录、任务主页、
|
||||||
|
任务预览、执行、候选确认、结果和设置。
|
||||||
|
3. 原型提供可点击的主要命令和状态切换,用于确认区块、控件、文案和安全边界,不连接
|
||||||
|
API、不读取本地私有样本,也不作为正式 UI 代码复用。
|
||||||
|
4. 使用 Playwright 在桌面和手机视口验证页面可打开、关键交互可触发、无水平溢出或
|
||||||
|
明显遮挡。
|
||||||
|
5. 用户实际查看并明确确认后,才把本任务改为 `DONE`、同步 IX 原型链接并提交 Git。
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
- [x] 4 个管理 Web P0 路由均有可独立打开的原型页面。
|
||||||
|
- [x] 7 个 Android P0 逻辑目的地均在手机尺寸原型中可到达。
|
||||||
|
- [x] IX-001 至 IX-008 的正常路径、关键异常和主要命令可被确认。
|
||||||
|
- [x] 所有页面固定显示“仅供确认”的原型标记,且不含真实订单、账号、图片或路径。
|
||||||
|
- [x] 任务执行页没有虚假百分比;安全错误不提供自动继续。
|
||||||
|
- [x] 候选确认页明确展示 `order_submitted=false` 对应的人类文案。
|
||||||
|
- [x] 桌面和移动视口无水平溢出、控件遮挡或不可达主要操作。
|
||||||
|
- [x] 用户已人工确认页面区块、控件集合、文案和主要动作。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
- 不实现正式 Web 框架、Compose/Fragment 页面或任何 API 对接。
|
||||||
|
- 不修改 Android 自动化、VLM、后端业务表、鉴权或任务接口。
|
||||||
|
- 不使用真实商品、订单、店铺、账号、设备标识、token 或生产 URL。
|
||||||
|
- 不在人工确认前标记完成或提交 Git。
|
||||||
|
|
||||||
|
## 执行记录
|
||||||
|
|
||||||
|
### 2026-07-25:任务开始
|
||||||
|
|
||||||
|
- 基于提交 `ab21219` 开始。
|
||||||
|
- 已核对 `docs/routes.md` 的 4 个管理 Web 路由、7 个 Android 目的地,以及
|
||||||
|
IX-001 至 IX-008 的正常、异常与可访问性要求。
|
||||||
|
- 当前先生成原型和自动化验证;人工确认是完成本任务的硬门禁。
|
||||||
|
|
||||||
|
### 2026-07-25:原型生成与自动验收
|
||||||
|
|
||||||
|
- 生成 `docs/design/index.html`、4 个管理 Web 页面和 7 个 Android 页面;每个实际
|
||||||
|
页面均可通过 `file://` 独立打开,CSS/JS 全内联且没有外部依赖。
|
||||||
|
- Web 覆盖登录、任务创建、列表/详情、加载/空/失败/权限、取消二次确认、证据失败
|
||||||
|
和“验证完成,未提交订单”;Android 覆盖登录绑定、手动领取、预览、执行、安全
|
||||||
|
停止、人工结论、终态、待同步和设备设置。
|
||||||
|
- 需求审计确认 11 个逻辑页面均已覆盖,并发现 `docs/02-requirements.md` 的 F-001
|
||||||
|
至 F-003 单任务验收引用存在 3 处历史编号错误;该文件不在本任务写入边界,本次
|
||||||
|
不越界修改。
|
||||||
|
- 静态审计:12 个 HTML 均只有 1 个固定原型标记、1 个内联 style、0 个外部 script、
|
||||||
|
0 个 HTTP URL,未命中本机绝对路径、生产地址、令牌、密钥或私有样本关键词。
|
||||||
|
- Playwright 依次以 1440×900、390×844、360×800 打开 12 个页面,共 36 个
|
||||||
|
页面/视口组合:文档横向溢出 0、console error 0、page error 0、外部请求 0;
|
||||||
|
Android 可见按钮、链接、输入、选择和文本域小于 44px 的数量为 0。
|
||||||
|
- Playwright 交互验证通过:Web 首错焦点、通用登录错误、数量默认 1、未提交离开
|
||||||
|
和取消二次确认默认焦点;App 缺权限/已有任务/租约释放、安全错误无自动继续、
|
||||||
|
停止确认默认焦点、超预算禁用接受、无候选动作约束、人员结论回传、结果待同步及
|
||||||
|
活跃任务禁止退出。
|
||||||
|
- 实际截图复核索引、管理详情、候选确认、安全错误和设备设置;修复任务详情成功态
|
||||||
|
被 CSS 错误显示取消命令与证据错误占位的问题,复核后两者均不可见。
|
||||||
|
|
||||||
|
### 2026-07-26:人工确认与完成
|
||||||
|
|
||||||
|
- 用户在对话中明确回复“确认 T-202 原型”,确认当前页面区块、控件集合、文案和
|
||||||
|
主要动作可作为后续正式 UI 的实现输入。
|
||||||
|
- 最后一项人工门禁已满足,任务状态改为 `DONE`;后续显著改变布局或控件集合时,
|
||||||
|
必须按 `docs/design/README.md` 重新生成整页并再次确认。
|
||||||
@@ -110,3 +110,12 @@
|
|||||||
Gin 1.11.0、SQLite 和 Goose 3.26.0,并建立健康检查、migration 与有界生命周期。
|
Gin 1.11.0、SQLite 和 Goose 3.26.0,并建立健康检查、migration 与有界生命周期。
|
||||||
- 影响:Phase 2 后端基础可复现;T-202 可先确认 P0 原型,T-203 至 T-205 再分别实现
|
- 影响:Phase 2 后端基础可复现;T-202 可先确认 P0 原型,T-203 至 T-205 再分别实现
|
||||||
任务业务、鉴权和原子领取,不需要背负生成器演示逻辑或危险默认。
|
任务业务、鉴权和原子领取,不需要背负生成器演示逻辑或危险默认。
|
||||||
|
|
||||||
|
## 2026-07-26 P0 Web/App 原型确认
|
||||||
|
|
||||||
|
- 类型:交互门禁完成
|
||||||
|
- 内容:完成 T-202;生成并确认 4 个管理 Web 页面和 7 个 Android 页面,IX-001
|
||||||
|
至 IX-008 已链接到实际原型。Playwright 验证 36 个页面/视口组合,并检查关键
|
||||||
|
表单、二次确认、安全停止、人工结论、结果同步和设备状态交互。
|
||||||
|
- 影响:页面区块、控件集合、文案和主要动作已获得人工确认;T-203、T-204 和
|
||||||
|
T-206 可按依赖使用该原型作为实现输入,但不得复制低保真原型代码到生产界面。
|
||||||
|
|||||||
Reference in New Issue
Block a user