ui: dev.html 手机自适应 (两档 @media + tab 单列切换)
平板 641-1024px 强制 rail (不写 localStorage,回桌面用户偏好仍生效);
手机 ≤640px 单列 + body.mv-{left,mid,right} 切换 + header tab 按钮换行铺底;
selectTask 自动切到对话视图,100dvh 解决 iOS 工具栏挤压,
input/textarea ≥16px 防 focus 缩放,4 modal 改 min(92vw,…) / file-preview 全屏化。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ff58c488e
commit
7d3a93fc1f
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-05-22(豆包 Seedance 2.0 Fast 视频生成接入 + videogen skill)
|
最后更新:2026-05-22(dev SPA 手机自适应:tab 单列 + tablet rail 强制)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-05-22
|
### 2026-05-22
|
||||||
|
|
||||||
|
- **dev SPA 手机自适应:两档断点 + tab 单列**:`web/static/dev.html` 加 `@media`,**平板段(641-1024px)**纯 CSS 强制 rail(grid `40px 1fr 260px` + 左 pane 子项 `display:none` + 留 toggle 按钮),不写 localStorage —— 回桌面用户原偏好仍生效。**手机段(≤640px)**单列布局 grid `1fr` + `grid-template-areas:"head" "main"`,三 pane 都 `grid-area: main` 且默认 `display:none`,新加 `body.mv-{left,mid,right}` 控制当前可见 pane;header 加 `.mobile-tabs`(任务/对话/文件)桌面 `display:none`,手机段 `order:99 + flex-basis:100%` 换行铺底;`selectTask` 末加 `if (mqPhone.matches) setMobileView("mv-mid")` 选中任务自动切对话。`applyMobileMode()` 监听 `matchMedia("(max-width: 640px)")`,进手机时清 DOM 上的 `left-collapsed` class(localStorage 不动),回桌面再 `applyLeftCollapsed(读 localStorage)` 恢复。`100vh → 100dvh` 解决 iOS Safari 工具栏挤压;`textarea / input` 强制 ≥16px 防 focus 双指缩放;4 个 modal 卡片宽度从固定 px 改 `min(92vw, …)`,file-preview 改 `100vw × 100dvh` 全屏化。`#pane-toggle-left { display:none !important; }` 手机不允许折叠避免与 tab 切换语义冲突。否决:(a) 抽屉式(要写遮罩+手势 JS,自用工具不划算);(b) 只缩字号不改布局(三列在 360px 屏字段严重截断)。
|
||||||
- **豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill**:`config/media/doubao.yaml` 展开 video 段(`seedance_2_fast`:¥37/Mtok 文生 / ¥22/Mtok 图生,实测档位 480p 5s ¥1.86 / 720p 5s ¥4.00 — 由 token 公式 `(in+out)×W×H×fps/1024` 反推校验通过);`tools/seedance.py` 走 ark POST `/contents/generations/tasks` → 5s 间隔轮询 → succeeded 后 download mp4 + .meta.json 落 `<wd>/videos/<ts>-<rand>.mp4`,失败/cancel 不计费;`core/storage/usage.py::record_video_usage` 多态 units snapshot(resolution/duration/ratio/fps/tokens/单价);`build_agent` 加 `video_variant` + `cancel_check` 形参 — cancel_check 必须在 build 阶段传(SeedanceTool ctor 持有用于轮询期间响应停止按钮,改了原"build 后赋 agent.cancel_check"的延迟绑定,web 入口同步迁移);`web/app.py` 加 `_list_video_variants` / `_resolve_video_model` / `GET /v1/video_models` / `MessageRequest.video_model` / `OptimizePromptRequest.video_model`;`dev.html` 顶栏第三下拉 + `state.videoModels/videoModel` + 发消息一起 POST。前端 chip / inline `<video>` / `extractMediaBanner` / `_categorize` 在前期工作里已为 seedance 留好脚手架,几乎不动。`skills/videogen/SKILL.md` 六维诊断把 imagegen 的"光线"换成"运动+镜头"两维(运动必填,否则应该走 seedream 而非 seedance —— 差 18 倍价钱);BLOCKING 门槛比 imagegen 更严(¥4 vs ¥0.22)且要等 30-90s,贴 prompt+参数+预计花费+预计等待四件套等明确确认。`general_v1.md` 加 seedance 触发指引(平行 seedream)。phase 1 仅 t2v,**不支持 i2v**(skill 明示告诉用户)。fast 上限 720p,1080p+ 留给 pro variant(yaml 当前未配)。否决:(a) progress 事件流化(需要给 tool 加 sink 注入,phase 1 用 `run_status=running` 够了);(b) 远端 cgt-task DELETE(Volcengine 无明确 API,best-effort 不动);(c) i2v phase 1 拉进来(要图片转 URL + UI 选已有图,延后)。
|
- **豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill**:`config/media/doubao.yaml` 展开 video 段(`seedance_2_fast`:¥37/Mtok 文生 / ¥22/Mtok 图生,实测档位 480p 5s ¥1.86 / 720p 5s ¥4.00 — 由 token 公式 `(in+out)×W×H×fps/1024` 反推校验通过);`tools/seedance.py` 走 ark POST `/contents/generations/tasks` → 5s 间隔轮询 → succeeded 后 download mp4 + .meta.json 落 `<wd>/videos/<ts>-<rand>.mp4`,失败/cancel 不计费;`core/storage/usage.py::record_video_usage` 多态 units snapshot(resolution/duration/ratio/fps/tokens/单价);`build_agent` 加 `video_variant` + `cancel_check` 形参 — cancel_check 必须在 build 阶段传(SeedanceTool ctor 持有用于轮询期间响应停止按钮,改了原"build 后赋 agent.cancel_check"的延迟绑定,web 入口同步迁移);`web/app.py` 加 `_list_video_variants` / `_resolve_video_model` / `GET /v1/video_models` / `MessageRequest.video_model` / `OptimizePromptRequest.video_model`;`dev.html` 顶栏第三下拉 + `state.videoModels/videoModel` + 发消息一起 POST。前端 chip / inline `<video>` / `extractMediaBanner` / `_categorize` 在前期工作里已为 seedance 留好脚手架,几乎不动。`skills/videogen/SKILL.md` 六维诊断把 imagegen 的"光线"换成"运动+镜头"两维(运动必填,否则应该走 seedream 而非 seedance —— 差 18 倍价钱);BLOCKING 门槛比 imagegen 更严(¥4 vs ¥0.22)且要等 30-90s,贴 prompt+参数+预计花费+预计等待四件套等明确确认。`general_v1.md` 加 seedance 触发指引(平行 seedream)。phase 1 仅 t2v,**不支持 i2v**(skill 明示告诉用户)。fast 上限 720p,1080p+ 留给 pro variant(yaml 当前未配)。否决:(a) progress 事件流化(需要给 tool 加 sink 注入,phase 1 用 `run_status=running` 够了);(b) 远端 cgt-task DELETE(Volcengine 无明确 API,best-effort 不动);(c) i2v phase 1 拉进来(要图片转 URL + UI 选已有图,延后)。
|
||||||
|
|
||||||
### 2026-05-21
|
### 2026-05-21
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,88 @@
|
||||||
|
|
||||||
.small { font-size: 12px; }
|
.small { font-size: 12px; }
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
|
|
||||||
|
/* ───── mobile tab nav (header) — 桌面隐藏,手机断点内显示 ───── */
|
||||||
|
.mobile-tabs { display: none; gap: 4px; }
|
||||||
|
.mobile-tabs button {
|
||||||
|
padding: 5px 10px; font-size: 12px; line-height: 1.2;
|
||||||
|
background: transparent; border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-md); color: var(--muted); cursor: pointer;
|
||||||
|
transition: var(--t);
|
||||||
|
}
|
||||||
|
.mobile-tabs button.active {
|
||||||
|
background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── responsive: tablet (641-1024px) ─────
|
||||||
|
断点内强制 rail(纯 CSS,不写 localStorage;回桌面用户偏好仍生效) */
|
||||||
|
@media (min-width: 641px) and (max-width: 1024px) {
|
||||||
|
#app.ready { grid-template-columns: 40px 1fr 260px; }
|
||||||
|
#pane-left > * { display: none; }
|
||||||
|
#pane-left > .pane-head:first-child {
|
||||||
|
display: flex; justify-content: center; align-items: center;
|
||||||
|
padding: 6px 4px; border-bottom: none; background: transparent;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
#pane-left > .pane-head:first-child > * { display: none; }
|
||||||
|
#pane-left > .pane-head:first-child > #pane-toggle-left { display: inline-block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── responsive: phone (≤ 640px) — 单列 + tab 切换 ─────
|
||||||
|
JS 在进入手机视口时会清掉 body.left-collapsed,这里无需为它写覆盖
|
||||||
|
iOS 用 100dvh 避免地址栏/工具栏挤压视口高度 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
html, body { height: 100dvh; }
|
||||||
|
#app.ready {
|
||||||
|
height: 100dvh;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
grid-template-areas: "head" "main";
|
||||||
|
}
|
||||||
|
/* 三 pane 默认隐藏,body.mv-* 决定显示哪个 */
|
||||||
|
#pane-left, #pane-mid, #pane-right {
|
||||||
|
grid-area: main; border-right: none; display: none;
|
||||||
|
}
|
||||||
|
body.mv-left #pane-left { display: block; }
|
||||||
|
body.mv-mid #pane-mid { display: flex; }
|
||||||
|
body.mv-right #pane-right { display: block; }
|
||||||
|
/* 折叠按钮在手机不可见 */
|
||||||
|
#pane-toggle-left { display: none !important; }
|
||||||
|
|
||||||
|
/* header 紧凑化 */
|
||||||
|
header { padding: 6px 10px; gap: 6px; flex-wrap: wrap; }
|
||||||
|
header .who { display: none; }
|
||||||
|
header .title { font-size: 14px; }
|
||||||
|
/* tab 按钮:整行铺底,order:99 让它换行到 header 第二行 */
|
||||||
|
.mobile-tabs { display: flex; order: 99; flex-basis: 100%; }
|
||||||
|
.mobile-tabs button { flex: 1; }
|
||||||
|
#hd-new { padding: 4px 8px; font-size: 12px; }
|
||||||
|
#hd-logout { padding: 4px 8px; font-size: 12px; }
|
||||||
|
|
||||||
|
/* iOS 防 focus 自动缩放:input/textarea 字号 ≥ 16 */
|
||||||
|
textarea,
|
||||||
|
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* chat / 文件 微调 */
|
||||||
|
.msg { max-width: 96%; }
|
||||||
|
#chat-meta { padding: 6px 10px; gap: 8px; font-size: 11px; }
|
||||||
|
#chat-form { padding: 8px; }
|
||||||
|
.art-media img, .art-media video { max-width: 100%; }
|
||||||
|
.file-row { padding: 8px 12px; }
|
||||||
|
|
||||||
|
/* 4 个 modal 卡片自适应宽度 */
|
||||||
|
#login .card { width: min(92vw, 380px); padding: 24px 22px 22px; }
|
||||||
|
#admin-modal .card { width: min(92vw, 360px); }
|
||||||
|
#new-task-modal .card { width: min(92vw, 420px); }
|
||||||
|
#src-picker-modal .card { width: min(96vw, 560px); max-height: 88dvh; }
|
||||||
|
#file-preview-modal .card {
|
||||||
|
width: 100vw; height: 100dvh;
|
||||||
|
max-width: 100vw; max-height: 100dvh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -610,6 +692,12 @@
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button id="hd-new" class="primary">+ 新建任务</button>
|
<button id="hd-new" class="primary">+ 新建任务</button>
|
||||||
<button id="hd-logout">退出登录</button>
|
<button id="hd-logout">退出登录</button>
|
||||||
|
<!-- 手机 tab(桌面 display:none):任务 / 对话 / 文件 -->
|
||||||
|
<div class="mobile-tabs" role="tablist">
|
||||||
|
<button id="mv-tab-left" data-mv="mv-left" class="active">任务</button>
|
||||||
|
<button id="mv-tab-mid" data-mv="mv-mid">对话</button>
|
||||||
|
<button id="mv-tab-right" data-mv="mv-right">文件</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- left -->
|
<!-- left -->
|
||||||
|
|
@ -1108,6 +1196,36 @@ $("pane-toggle-left").onclick = () => {
|
||||||
};
|
};
|
||||||
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
|
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
|
||||||
|
|
||||||
|
// ───── 手机视图切换(单列 + tab) ─────
|
||||||
|
// body.mv-{left,mid,right} 控制当前显示的 pane;桌面下三 pane 都可见,本函数仅维护 class
|
||||||
|
// 进入手机视口时清掉 left-collapsed(只 DOM,不动 localStorage —— 回桌面用户偏好仍生效)
|
||||||
|
const mqPhone = window.matchMedia("(max-width: 640px)");
|
||||||
|
function setMobileView(view) {
|
||||||
|
// view ∈ "mv-left" | "mv-mid" | "mv-right"
|
||||||
|
document.body.classList.remove("mv-left", "mv-mid", "mv-right");
|
||||||
|
document.body.classList.add(view);
|
||||||
|
for (const b of document.querySelectorAll(".mobile-tabs button")) {
|
||||||
|
b.classList.toggle("active", b.dataset.mv === view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function applyMobileMode() {
|
||||||
|
if (mqPhone.matches) {
|
||||||
|
// 手机:清掉桌面 rail 状态,默认显示任务列表(若未设过)
|
||||||
|
document.body.classList.remove("left-collapsed");
|
||||||
|
if (!document.body.matches(".mv-left, .mv-mid, .mv-right")) {
|
||||||
|
setMobileView("mv-left");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 桌面/平板:恢复 localStorage 的 collapsed 偏好(平板靠 @media 强制 rail,不需依赖 class)
|
||||||
|
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mqPhone.addEventListener("change", applyMobileMode);
|
||||||
|
applyMobileMode();
|
||||||
|
for (const b of document.querySelectorAll(".mobile-tabs button")) {
|
||||||
|
b.onclick = () => setMobileView(b.dataset.mv);
|
||||||
|
}
|
||||||
|
|
||||||
// ───── enter app ─────
|
// ───── enter app ─────
|
||||||
function enterApp() {
|
function enterApp() {
|
||||||
$("login").style.display = "none";
|
$("login").style.display = "none";
|
||||||
|
|
@ -1321,6 +1439,8 @@ async function selectTask(tid) {
|
||||||
document.querySelectorAll(".task-row").forEach((el) => {
|
document.querySelectorAll(".task-row").forEach((el) => {
|
||||||
el.classList.toggle("active", el.dataset.tid === tid);
|
el.classList.toggle("active", el.dataset.tid === tid);
|
||||||
});
|
});
|
||||||
|
// 手机视图:选中任务自动切到对话面板(桌面 mqPhone 不命中 → no-op)
|
||||||
|
if (mqPhone.matches) setMobileView("mv-mid");
|
||||||
try {
|
try {
|
||||||
const meta = await api("GET", "/v1/tasks/" + tid);
|
const meta = await api("GET", "/v1/tasks/" + tid);
|
||||||
state.taskMeta = meta;
|
state.taskMeta = meta;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue