feat(ui): 页内弹框替代原生 alert/confirm/prompt——分 dialog/message 两族,复用改密码卡片样式(bump 0.58.42)
新增 web/static/js/dialog.js 两族原语(对标 Element dialog/message): - dialogConfirm/dialogPrompt:遮罩+卡片 modal,Promise 化以一比一替换同步的 confirm/prompt;Esc/点遮罩=取消,Enter=确认,删除类走 danger 红键。 - message:顶部居中轻 toast,success/error/info 三态,自动消失、点即消。 dev.html 加 #app-dialog 容器 + #app-messages toast + 对应 CSS。全量替换主应用 8 文件调用点(chat/files/crons/procs/wechat/skills/media/auth,共 30 处)。 admin.js 独立 admin-only 页(自带 <style>、无 CSS 变量)刻意保留原生 alert 避免缺样式。纯前端加法,不动后端/接口。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6efda0a157
commit
20c7512370
|
|
@ -5,6 +5,10 @@
|
||||||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## 0.58.42 — 2026-07-19
|
||||||
|
|
||||||
|
- 提示和确认弹框换新样式:重命名、删除、清空、解绑等操作的确认框,以及各类操作提示,不再用浏览器那种顶部灰框,改为和「修改密码」一致的页内弹框;成功/失败提示改为页面顶部轻提示,几秒后自动消失。
|
||||||
|
|
||||||
## 0.58.41 — 2026-07-18
|
## 0.58.41 — 2026-07-18
|
||||||
|
|
||||||
- 可以给任务改名了:在任务列表行或聊天区顶部的「⋯」菜单里选「重命名」即可修改任务显示名,改完列表和标题栏立即同步。改名只动显示名,不影响工作目录和已生成的文件。
|
- 可以给任务改名了:在任务列表行或聊天区顶部的「⋯」菜单里选「重命名」即可修改任务显示名,改完列表和标题栏立即同步。改名只动显示名,不影响工作目录和已生成的文件。
|
||||||
|
|
|
||||||
|
|
@ -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-07-18(PROGRESS 精简:2026-07 段压回全文一行式,字符数减半,bump 0.58.40)
|
最后更新:2026-07-19(页内弹框替代原生 alert/confirm/prompt,新增 dialog.js,bump 0.58.42)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-07
|
### 2026-07
|
||||||
|
|
||||||
|
- **07-19 / 0.58.42**:页内弹框替代原生 alert/confirm/prompt——用户反馈原生浏览器弹框(顶部丑框)应改成页内样式,且区分两类(对标 Element `dialog`/`message`)。新增 `web/static/js/dialog.js`:`dialogConfirm`/`dialogPrompt`(遮罩+卡片 modal,复用改密码那套头/体/脚布局,Promise 化以一比一替换同步的 `confirm`/`prompt`,Esc/点遮罩=取消、Enter=确认、danger 变体红键)+ `message`(顶部居中轻量 toast,success/error/info 三态,自动消失、点即消)。dev.html 加 `#app-dialog` 容器 + `#app-messages` toast + 对应 CSS。全量替换主应用 8 文件调用点(chat/files/crons/procs/wechat/skills/media/auth,共 30 处);`admin.js`(独立 admin-only 页、独立 `<style>`、无 CSS 变量)刻意保留原生 alert 避免缺样式。纯前端加法,不动后端/接口。
|
||||||
- **07-18 / 0.58.41**:任务改名 UI——后端 `PATCH /v1/tasks/{id}` 早支持 `name`(validate_task_name 校验)但前端无入口;`taskMenuItems` 加「重命名」项(列表行 ⋯ 与顶栏 ⋯ 共用此菜单,一处插入两处生效),`renameTask()` 走 prompt→PATCH→更 `state.tasksById`/`taskMeta.name` + `renderChatMeta()` + `loadTaskList()` 全同步。刻意只改 DB 显示名,不动 working_dir/磁盘宪法文件(定位锚是 task_id/short_id,name 设计为自由可变标签)。仅改 `web/static/js/chat.js`。
|
- **07-18 / 0.58.41**:任务改名 UI——后端 `PATCH /v1/tasks/{id}` 早支持 `name`(validate_task_name 校验)但前端无入口;`taskMenuItems` 加「重命名」项(列表行 ⋯ 与顶栏 ⋯ 共用此菜单,一处插入两处生效),`renameTask()` 走 prompt→PATCH→更 `state.tasksById`/`taskMeta.name` + `renderChatMeta()` + `loadTaskList()` 全同步。刻意只改 DB 显示名,不动 working_dir/磁盘宪法文件(定位锚是 task_id/short_id,name 设计为自由可变标签)。仅改 `web/static/js/chat.js`。
|
||||||
- **07-18 / 0.58.39**:JWT 滑动续签——活跃用户临期(剩余寿命 < TTL 1/4,env `ZCBOT_JWT_REFRESH_THRESHOLD_SECONDS` 可调、0 关)由 `_authenticate` 中间件就地重签发新 token 走响应头 `X-Refreshed-Token`/`X-Token-Expires-At`,前端 `api()` 无感吸收换 `state.token`,不再因 7d TTL 掉线。additive 惠及 platform 层 + SPA(不认退回原重登行为);CORS 加 `expose_headers`。
|
- **07-18 / 0.58.39**:JWT 滑动续签——活跃用户临期(剩余寿命 < TTL 1/4,env `ZCBOT_JWT_REFRESH_THRESHOLD_SECONDS` 可调、0 关)由 `_authenticate` 中间件就地重签发新 token 走响应头 `X-Refreshed-Token`/`X-Token-Expires-At`,前端 `api()` 无感吸收换 `state.token`,不再因 7d TTL 掉线。additive 惠及 platform 层 + SPA(不认退回原重登行为);CORS 加 `expose_headers`。
|
||||||
- **07-17 / 0.58.38**:修定时简报产物文件链接点开 404——推送端 `_build_push_message` 把产物写成完整 `/v1/files/download?path=` URL,撞前端相对路径拦截器致二次包装 404。A 拦截器识别 `^/?v1/files/` 抽真 rel 救 DB 历史存量 + B 推送端源头收敛为 user_root 相对 rel(企微 send_file 独立通道不碰此链接,不受影响)。
|
- **07-17 / 0.58.38**:修定时简报产物文件链接点开 404——推送端 `_build_push_message` 把产物写成完整 `/v1/files/download?path=` URL,撞前端相对路径拦截器致二次包装 404。A 拦截器识别 `^/?v1/files/` 抽真 rel 救 DB 历史存量 + B 推送端源头收敛为 user_root 相对 rel(企微 send_file 独立通道不碰此链接,不受影响)。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.58.41"
|
__version__ = "0.58.42"
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,54 @@
|
||||||
display: flex; gap: 8px; justify-content: flex-end;
|
display: flex; gap: 8px; justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───── 通用页内弹框 #app-dialog(confirm / prompt,复用改密码头/体/脚布局)───── */
|
||||||
|
#app-dialog { z-index: 130; }
|
||||||
|
#app-dialog .card { width: 400px; max-width: calc(100vw - 32px); display: flex; flex-direction: column; }
|
||||||
|
#app-dialog h3 {
|
||||||
|
margin: 0; padding: 14px 18px; font-size: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
#app-dialog .body { padding: 16px 18px; }
|
||||||
|
#app-dialog .body .msg { font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
#app-dialog label {
|
||||||
|
display: block; margin-bottom: 6px; font-size: 12px; color: var(--muted);
|
||||||
|
}
|
||||||
|
#app-dialog input {
|
||||||
|
width: 100%; padding: 8px 10px; border-radius: var(--r-md);
|
||||||
|
border: 1px solid var(--border); background: #fafafa;
|
||||||
|
}
|
||||||
|
#app-dialog input:focus { outline: none; border-color: var(--accent); background: #fff; }
|
||||||
|
#app-dialog .actions {
|
||||||
|
padding: 12px 18px; border-top: 1px solid var(--border);
|
||||||
|
display: flex; gap: 8px; justify-content: flex-end;
|
||||||
|
}
|
||||||
|
#app-dialog button.primary.danger { background: var(--c-red); border-color: var(--c-red); }
|
||||||
|
|
||||||
|
/* ───── 单向提示 toast #app-messages(顶部居中,自动消失,对标 Element message)───── */
|
||||||
|
#app-messages {
|
||||||
|
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||||
|
z-index: 200; display: flex; flex-direction: column; align-items: center; gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.app-msg {
|
||||||
|
pointer-events: auto; cursor: pointer;
|
||||||
|
max-width: min(90vw, 480px);
|
||||||
|
padding: 10px 16px; border-radius: var(--r-md);
|
||||||
|
background: var(--panel); border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 6px 20px rgba(0,0,0,.14);
|
||||||
|
font-size: 13px; line-height: 1.5; color: var(--text);
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
opacity: 0; transform: translateY(-12px);
|
||||||
|
transition: opacity .25s ease, transform .25s ease;
|
||||||
|
}
|
||||||
|
.app-msg.in { opacity: 1; transform: translateY(0); }
|
||||||
|
.app-msg::before { font-weight: 700; }
|
||||||
|
.app-msg.success { border-color: var(--c-green-bd); background: var(--c-green-bg); color: #1e7e45; }
|
||||||
|
.app-msg.success::before { content: "\2713"; color: var(--c-green); }
|
||||||
|
.app-msg.error { border-color: var(--c-red-bd); background: var(--c-red-bg); color: var(--c-red); }
|
||||||
|
.app-msg.error::before { content: "\2715"; color: var(--c-red); }
|
||||||
|
.app-msg.info::before { content: "\2139"; color: var(--c-blue); }
|
||||||
|
|
||||||
/* ───── 左侧 rail 底部「我的资源」入口(技能,后续可加记忆)───── */
|
/* ───── 左侧 rail 底部「我的资源」入口(技能,后续可加记忆)───── */
|
||||||
#rail-resources {
|
#rail-resources {
|
||||||
flex-shrink: 0; border-top: 1px solid var(--border);
|
flex-shrink: 0; border-top: 1px solid var(--border);
|
||||||
|
|
@ -1454,6 +1502,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ───── 通用页内弹框(confirm / prompt,内容由 dialog.js 动态填)───── -->
|
||||||
|
<div id="app-dialog" class="modal"></div>
|
||||||
|
|
||||||
<!-- ───── 技能查看 modal ───── -->
|
<!-- ───── 技能查看 modal ───── -->
|
||||||
<div id="skills-modal" class="modal">
|
<div id="skills-modal" class="modal">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { $ } from "./dom.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { enterApp } from "./main.js";
|
import { enterApp } from "./main.js";
|
||||||
import { embedPostToParent, embedShowWaiting, embedWecomRelogin } from "./embed.js";
|
import { embedPostToParent, embedShowWaiting, embedWecomRelogin } from "./embed.js";
|
||||||
|
import { message } from "./dialog.js";
|
||||||
|
|
||||||
// ───── login ─────
|
// ───── login ─────
|
||||||
let loginTab = "pw"; // "pw" | "key";持久化 last-used tab 在 LS,刷新后默认那个
|
let loginTab = "pw"; // "pw" | "key";持久化 last-used tab 在 LS,刷新后默认那个
|
||||||
|
|
@ -222,7 +223,7 @@ async function doChangePassword() {
|
||||||
await api("POST", "/v1/auth/change_password", { old_password: oldPw, new_password: newPw });
|
await api("POST", "/v1/auth/change_password", { old_password: oldPw, new_password: newPw });
|
||||||
closeChpwModal();
|
closeChpwModal();
|
||||||
// 不登出:同一会话 JWT 仍有效,下次登录用新密码即可
|
// 不登出:同一会话 JWT 仍有效,下次登录用新密码即可
|
||||||
alert("密码已修改,下次登录请用新密码");
|
message("密码已修改,下次登录请用新密码", "success");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { closeChpwModal(); logout(); return; }
|
if (e.status === 401) { closeChpwModal(); logout(); return; }
|
||||||
$("cp-err").textContent = e.message;
|
$("cp-err").textContent = e.message;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { api } from "./api.js";
|
||||||
import { escapeHtml, fmtTime, fmtTokens, fmtTimeAgo, taskUsageTooltip, formatTaskUsage, formatContextStats, formatUsageStats, ctxRingHtml } from "./format.js";
|
import { escapeHtml, fmtTime, fmtTokens, fmtTimeAgo, taskUsageTooltip, formatTaskUsage, formatContextStats, formatUsageStats, ctxRingHtml } from "./format.js";
|
||||||
import { renderMd, highlightIn } from "./markdown.js";
|
import { renderMd, highlightIn } from "./markdown.js";
|
||||||
import { mqPhone, setMobileView } from "./layout.js";
|
import { mqPhone, setMobileView } from "./layout.js";
|
||||||
|
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
|
||||||
|
|
||||||
// 微信 logo(simple-icons WeChat path),用于渠道任务徽章;fill 走 currentColor(徽章里为白)
|
// 微信 logo(simple-icons WeChat path),用于渠道任务徽章;fill 走 currentColor(徽章里为白)
|
||||||
const WECHAT_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.5-6.446 1.45-1.595 3.711-2.55 6.286-2.55.165 0 .33.01.495.027C18.486 4.916 13.929 2.188 8.691 2.188zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.121 2.361-.343a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088a8.067 8.067 0 0 0-.346-.034zm-2.71 3.711c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>`;
|
const WECHAT_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.5-6.446 1.45-1.595 3.711-2.55 6.286-2.55.165 0 .33.01.495.027C18.486 4.916 13.929 2.188 8.691 2.188zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.121 2.361-.343a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088a8.067 8.067 0 0 0-.346-.034zm-2.71 3.711c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>`;
|
||||||
|
|
@ -1504,7 +1505,7 @@ async function deletePastedFile(rel, wrap) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (btn) btn.disabled = false;
|
if (btn) btn.disabled = false;
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("删除失败:" + e.message);
|
message("删除失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2513,7 +2514,11 @@ $("btn-task-menu").onclick = (e) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function clearMessages(tid, name, nMsg) {
|
async function clearMessages(tid, name, nMsg) {
|
||||||
if (!confirm(`确认清空「${name}」的对话(${nMsg} 条消息)?\n\n将删除全部对话历史并重置 token 计数;工作目录下的文件保留。`)) return;
|
if (!await dialogConfirm({
|
||||||
|
title: "清空对话",
|
||||||
|
message: `确认清空「${name}」的对话(${nMsg} 条消息)?\n\n将删除全部对话历史并重置 token 计数;工作目录下的文件保留。`,
|
||||||
|
okText: "清空", danger: true,
|
||||||
|
})) return;
|
||||||
try {
|
try {
|
||||||
const updated = await api("POST", "/v1/tasks/" + tid + "/clear");
|
const updated = await api("POST", "/v1/tasks/" + tid + "/clear");
|
||||||
if (state.taskId === tid) {
|
if (state.taskId === tid) {
|
||||||
|
|
@ -2530,14 +2535,16 @@ async function clearMessages(tid, name, nMsg) {
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("清空失败:" + e.message);
|
message("清空失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameTask(tid, curName) {
|
async function renameTask(tid, curName) {
|
||||||
// 只改 tasks.name(显示名),不动 working_dir/磁盘产物 —— 定位主锚是 task_id/short_id。
|
// 只改 tasks.name(显示名),不动 working_dir/磁盘产物 —— 定位主锚是 task_id/short_id。
|
||||||
// 后端 PATCH 带 validate_task_name 校验;成功后前端各处(列表/标题栏)重渲染即同步。
|
// 后端 PATCH 带 validate_task_name 校验;成功后前端各处(列表/标题栏)重渲染即同步。
|
||||||
const next = prompt("将任务重命名为:", curName || "");
|
const next = await dialogPrompt({
|
||||||
|
title: "重命名任务", label: "任务名称", value: curName || "", placeholder: "输入新的任务名",
|
||||||
|
});
|
||||||
if (next == null) return;
|
if (next == null) return;
|
||||||
const trimmed = next.trim();
|
const trimmed = next.trim();
|
||||||
if (!trimmed || trimmed === curName) return;
|
if (!trimmed || trimmed === curName) return;
|
||||||
|
|
@ -2551,25 +2558,32 @@ async function renameTask(tid, curName) {
|
||||||
loadTaskList(); // 左栏列表重拉,行内名字同步
|
loadTaskList(); // 左栏列表重拉,行内名字同步
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("重命名失败:" + e.message);
|
message("重命名失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setTaskStatus(tid, status, name) {
|
async function setTaskStatus(tid, status, name) {
|
||||||
const labels = { completed: "已完成", abandoned: "已废弃" };
|
const labels = { completed: "已完成", abandoned: "已废弃" };
|
||||||
if (!confirm(`确认将「${name || tid.slice(0,8)}」置为「${labels[status] || status}」?`)) return;
|
if (!await dialogConfirm({
|
||||||
|
title: "更改任务状态",
|
||||||
|
message: `确认将「${name || tid.slice(0,8)}」置为「${labels[status] || status}」?`,
|
||||||
|
})) return;
|
||||||
try {
|
try {
|
||||||
await api("PATCH", "/v1/tasks/" + tid, { status });
|
await api("PATCH", "/v1/tasks/" + tid, { status });
|
||||||
if (state.taskId === tid) await selectTask(tid);
|
if (state.taskId === tid) await selectTask(tid);
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("操作失败:" + e.message);
|
message("操作失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTask(tid, name, nMsg) {
|
async function deleteTask(tid, name, nMsg) {
|
||||||
if (!confirm(`确认硬删除任务「${name}」(${nMsg} 条消息)?\n\n将清掉对话历史和 DB 行,但工作目录下的文件保留。`)) return;
|
if (!await dialogConfirm({
|
||||||
|
title: "删除任务",
|
||||||
|
message: `确认硬删除任务「${name}」(${nMsg} 条消息)?\n\n将清掉对话历史和 DB 行,但工作目录下的文件保留。`,
|
||||||
|
okText: "删除", danger: true,
|
||||||
|
})) return;
|
||||||
try {
|
try {
|
||||||
await api("DELETE", "/v1/tasks/" + tid);
|
await api("DELETE", "/v1/tasks/" + tid);
|
||||||
if (state.taskId === tid) {
|
if (state.taskId === tid) {
|
||||||
|
|
@ -2589,7 +2603,7 @@ async function deleteTask(tid, name, nMsg) {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("删除失败:" + e.message);
|
message("删除失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2597,7 +2611,7 @@ function exportTask(tid) {
|
||||||
fetch("/v1/tasks/" + tid + "/export", {
|
fetch("/v1/tasks/" + tid + "/export", {
|
||||||
headers: { "Authorization": "Bearer " + state.token },
|
headers: { "Authorization": "Bearer " + state.token },
|
||||||
}).then(async (r) => {
|
}).then(async (r) => {
|
||||||
if (!r.ok) { alert("导出失败:" + r.status); return; }
|
if (!r.ok) { message("导出失败:" + r.status, "error"); return; }
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { $ } from "./dom.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { escapeHtml } from "./format.js";
|
import { escapeHtml } from "./format.js";
|
||||||
import { selectTask } from "./chat.js";
|
import { selectTask } from "./chat.js";
|
||||||
|
import { dialogConfirm, message } from "./dialog.js";
|
||||||
|
|
||||||
const PLACEHOLDER = '<div class="sk-empty">← 选一个定时任务查看详情</div>';
|
const PLACEHOLDER = '<div class="sk-empty">← 选一个定时任务查看详情</div>';
|
||||||
let _jobs = [];
|
let _jobs = [];
|
||||||
|
|
@ -230,7 +231,7 @@ $("cr-detail").addEventListener("click", async (e) => {
|
||||||
await renderList();
|
await renderList();
|
||||||
selectJob(id);
|
selectJob(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("操作失败: " + err.message);
|
message("操作失败: " + err.message, "error");
|
||||||
toggle.disabled = false;
|
toggle.disabled = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -238,14 +239,18 @@ $("cr-detail").addEventListener("click", async (e) => {
|
||||||
if (del) {
|
if (del) {
|
||||||
const id = del.getAttribute("data-del");
|
const id = del.getAttribute("data-del");
|
||||||
const j = _jobs.find((x) => x.job_id === id);
|
const j = _jobs.find((x) => x.job_id === id);
|
||||||
if (!confirm(`删除定时任务「${j ? j.name : id}」?不可撤销(停用可保留改用「停用」)。`)) return;
|
if (!await dialogConfirm({
|
||||||
|
title: "删除定时任务",
|
||||||
|
message: `删除定时任务「${j ? j.name : id}」?不可撤销(停用可保留改用「停用」)。`,
|
||||||
|
okText: "删除", danger: true,
|
||||||
|
})) return;
|
||||||
del.disabled = true;
|
del.disabled = true;
|
||||||
try {
|
try {
|
||||||
await api("DELETE", "/v1/schedules/" + encodeURIComponent(id));
|
await api("DELETE", "/v1/schedules/" + encodeURIComponent(id));
|
||||||
$("cr-detail").innerHTML = PLACEHOLDER;
|
$("cr-detail").innerHTML = PLACEHOLDER;
|
||||||
await renderList();
|
await renderList();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("删除失败: " + err.message);
|
message("删除失败: " + err.message, "error");
|
||||||
del.disabled = false;
|
del.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
// 页内弹框原语:替代原生 alert/confirm/prompt。分两类(对标 Element 的 dialog / message):
|
||||||
|
// - dialogConfirm / dialogPrompt:需要交互(确认 / 输入)→ 遮罩 + 卡片 modal,复用 .modal/.card。
|
||||||
|
// - message:单向提示(成功 / 报错 / 信息)→ 顶部居中轻量 toast,自动消失。
|
||||||
|
// 复用改密码 modal 那套头/体/脚布局;单例容器 #app-dialog 每次调用重建内容,同一时刻只开一个。
|
||||||
|
import { $ } from "./dom.js";
|
||||||
|
import { escapeHtml } from "./format.js";
|
||||||
|
|
||||||
|
// ───── 交互式 modal(confirm / prompt)─────
|
||||||
|
|
||||||
|
let _resolve = null; // 当前打开弹框的 resolve
|
||||||
|
let _onKey = null; // 当前绑定的 keydown 处理器(关闭时解绑)
|
||||||
|
|
||||||
|
function closeDialog(result) {
|
||||||
|
const wrap = $("app-dialog");
|
||||||
|
wrap.classList.remove("show");
|
||||||
|
wrap.innerHTML = "";
|
||||||
|
if (_onKey) { document.removeEventListener("keydown", _onKey, true); _onKey = null; }
|
||||||
|
const r = _resolve; _resolve = null;
|
||||||
|
if (r) r(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内部通用打开:kind = "confirm" | "prompt"
|
||||||
|
function openDialog(kind, opts) {
|
||||||
|
const {
|
||||||
|
title = "",
|
||||||
|
message = "",
|
||||||
|
label = "",
|
||||||
|
value = "",
|
||||||
|
placeholder = "",
|
||||||
|
okText = "确认",
|
||||||
|
cancelText = "取消",
|
||||||
|
danger = false,
|
||||||
|
} = opts || {};
|
||||||
|
|
||||||
|
// 若已有弹框未关(极少见:非阻塞下重复触发),先把旧的按取消收掉
|
||||||
|
if (_resolve) closeDialog(kind === "prompt" ? null : false);
|
||||||
|
|
||||||
|
const wrap = $("app-dialog");
|
||||||
|
const isPrompt = kind === "prompt";
|
||||||
|
const bodyHtml = isPrompt
|
||||||
|
? `${label ? `<label for="app-dialog-input">${escapeHtml(label)}</label>` : ""}
|
||||||
|
<input id="app-dialog-input" type="text" autocomplete="off" />`
|
||||||
|
: `<div class="msg">${escapeHtml(message)}</div>`;
|
||||||
|
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<div class="card" role="dialog" aria-modal="true">
|
||||||
|
${title ? `<h3>${escapeHtml(title)}</h3>` : ""}
|
||||||
|
<div class="body">${bodyHtml}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="app-dialog-cancel">${escapeHtml(cancelText)}</button>
|
||||||
|
<button id="app-dialog-ok" class="primary${danger ? " danger" : ""}">${escapeHtml(okText)}</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
wrap.classList.add("show");
|
||||||
|
|
||||||
|
const inp = isPrompt ? $("app-dialog-input") : null;
|
||||||
|
if (inp) { inp.value = value; inp.placeholder = placeholder; }
|
||||||
|
|
||||||
|
const ok = () => closeDialog(isPrompt ? (inp.value) : true);
|
||||||
|
const cancel = () => closeDialog(isPrompt ? null : false);
|
||||||
|
|
||||||
|
$("app-dialog-ok").onclick = ok;
|
||||||
|
$("app-dialog-cancel").onclick = cancel;
|
||||||
|
wrap.onclick = (e) => { if (e.target === wrap) cancel(); }; // 点遮罩 = 取消
|
||||||
|
|
||||||
|
_onKey = (e) => {
|
||||||
|
if (e.key === "Escape") { e.stopPropagation(); cancel(); }
|
||||||
|
else if (e.key === "Enter" && (!isPrompt || e.target === inp)) { e.preventDefault(); ok(); }
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", _onKey, true);
|
||||||
|
|
||||||
|
// 聚焦:输入框选中已有文字(方便重命名整段改),否则聚焦确认键
|
||||||
|
if (inp) { inp.focus(); inp.select(); }
|
||||||
|
else $("app-dialog-ok").focus();
|
||||||
|
|
||||||
|
return new Promise((res) => { _resolve = res; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认框 → Promise<boolean>
|
||||||
|
export function dialogConfirm(opts) {
|
||||||
|
return openDialog("confirm", typeof opts === "string" ? { message: opts } : opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框 → Promise<string|null>(取消返回 null;确认返回原始输入,调用方自行 trim)
|
||||||
|
export function dialogPrompt(opts) {
|
||||||
|
return openDialog("prompt", opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供 main.js Esc 栈查询 / 关闭(与其它 modal 统一)
|
||||||
|
export function isDialogOpen() { return !!_resolve; }
|
||||||
|
export function closeTopDialog() { if (_resolve) closeDialog(false); }
|
||||||
|
|
||||||
|
// ───── 单向提示 toast(message)─────
|
||||||
|
// type: "info" | "success" | "error";顶部居中,自动消失。
|
||||||
|
export function message(text, type = "info", duration = 3200) {
|
||||||
|
let wrap = $("app-messages");
|
||||||
|
if (!wrap) {
|
||||||
|
wrap = document.createElement("div");
|
||||||
|
wrap.id = "app-messages";
|
||||||
|
document.body.appendChild(wrap);
|
||||||
|
}
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "app-msg " + type;
|
||||||
|
el.textContent = text;
|
||||||
|
wrap.appendChild(el);
|
||||||
|
// 触发进入动画
|
||||||
|
requestAnimationFrame(() => el.classList.add("in"));
|
||||||
|
const kill = () => {
|
||||||
|
el.classList.remove("in");
|
||||||
|
el.addEventListener("transitionend", () => el.remove(), { once: true });
|
||||||
|
setTimeout(() => el.remove(), 400); // 兜底(transition 未触发时)
|
||||||
|
};
|
||||||
|
setTimeout(kill, duration);
|
||||||
|
el.onclick = kill; // 点一下立刻消
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import { logout } from "./auth.js";
|
||||||
import { downloadFile } from "./media.js";
|
import { downloadFile } from "./media.js";
|
||||||
import { selectTask, loadTaskList } from "./chat.js";
|
import { selectTask, loadTaskList } from "./chat.js";
|
||||||
import { loadFolderSuggestions } from "./newtask.js";
|
import { loadFolderSuggestions } from "./newtask.js";
|
||||||
|
import { dialogConfirm, dialogPrompt, message } from "./dialog.js";
|
||||||
|
|
||||||
// ───── files(user-rooted,不绑 task) ─────
|
// ───── files(user-rooted,不绑 task) ─────
|
||||||
$("btn-refresh-files").onclick = () => loadFiles();
|
$("btn-refresh-files").onclick = () => loadFiles();
|
||||||
|
|
@ -128,7 +129,7 @@ async function doSrcTransfer(mode) {
|
||||||
await loadFolderSuggestions();
|
await loadFolderSuggestions();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert(verb + "失败:" + e.message);
|
message(verb + "失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,7 +286,7 @@ function fileMenuItems(e) {
|
||||||
async function deleteFile(rel, name, isDir) {
|
async function deleteFile(rel, name, isDir) {
|
||||||
let recursive = false;
|
let recursive = false;
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
if (!confirm(`确认删除文件 "${name}"?`)) return;
|
if (!await dialogConfirm({ title: "删除文件", message: `确认删除文件 "${name}"?`, okText: "删除", danger: true })) return;
|
||||||
} else {
|
} else {
|
||||||
// 探一下目录内容:空目录走普通 rmdir;非空才递归,二次确认显示条目数
|
// 探一下目录内容:空目录走普通 rmdir;非空才递归,二次确认显示条目数
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -294,19 +295,22 @@ async function deleteFile(rel, name, isDir) {
|
||||||
entries = data.entries || [];
|
entries = data.entries || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("读目录失败:" + e.message);
|
message("读目录失败:" + e.message, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
if (!confirm(`确认删除空目录 "${name}"?`)) return;
|
if (!await dialogConfirm({ title: "删除目录", message: `确认删除空目录 "${name}"?`, okText: "删除", danger: true })) return;
|
||||||
} else {
|
} else {
|
||||||
const hasSub = entries.some((x) => x.is_dir);
|
const hasSub = entries.some((x) => x.is_dir);
|
||||||
const tip = hasSub ? "(含子目录)" : "";
|
const tip = hasSub ? "(含子目录)" : "";
|
||||||
if (!confirm(
|
if (!await dialogConfirm({
|
||||||
`目录 "${name}" 含 ${entries.length} 项${tip},` +
|
title: "删除目录",
|
||||||
`将递归删除全部内容,不可恢复。\n` +
|
message:
|
||||||
`(若为顶层目录且仍被 task 引用,需先删 task)\n确认?`
|
`目录 "${name}" 含 ${entries.length} 项${tip},` +
|
||||||
)) return;
|
`将递归删除全部内容,不可恢复。\n` +
|
||||||
|
`(若为顶层目录且仍被 task 引用,需先删 task)\n确认?`,
|
||||||
|
okText: "递归删除", danger: true,
|
||||||
|
})) return;
|
||||||
recursive = true;
|
recursive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,13 +321,15 @@ async function deleteFile(rel, name, isDir) {
|
||||||
await loadFolderSuggestions();
|
await loadFolderSuggestions();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("删除失败:" + e.message);
|
message("删除失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameFile(rel, name, isDir) {
|
async function renameFile(rel, name, isDir) {
|
||||||
const what = isDir ? "目录" : "文件";
|
const what = isDir ? "目录" : "文件";
|
||||||
const newName = prompt(`将${what} "${name}" 重命名为:`, name);
|
const newName = await dialogPrompt({
|
||||||
|
title: `重命名${what}`, label: `${what}名称`, value: name, placeholder: `输入新的${what}名`,
|
||||||
|
});
|
||||||
if (newName == null) return;
|
if (newName == null) return;
|
||||||
const trimmed = newName.trim();
|
const trimmed = newName.trim();
|
||||||
if (!trimmed || trimmed === name) return;
|
if (!trimmed || trimmed === name) return;
|
||||||
|
|
@ -344,7 +350,7 @@ async function renameFile(rel, name, isDir) {
|
||||||
await loadFiles();
|
await loadFiles();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
alert("重命名失败:" + e.message);
|
message("重命名失败:" + e.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +430,7 @@ export async function uploadFiles(files, opts = {}) {
|
||||||
return data.saved || [];
|
return data.saved || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return null; }
|
if (e.status === 401) { logout(); return null; }
|
||||||
alert("上传失败:" + e.message);
|
message("上传失败:" + e.message, "error");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { state } from "./state.js";
|
||||||
import { escapeHtml } from "./format.js";
|
import { escapeHtml } from "./format.js";
|
||||||
import { _categorize } from "./preview.js";
|
import { _categorize } from "./preview.js";
|
||||||
import { logout } from "./auth.js";
|
import { logout } from "./auth.js";
|
||||||
|
import { message } from "./dialog.js";
|
||||||
|
|
||||||
// ───── artifact 抽取(对话内 chip → 复用文件预览 modal) ─────
|
// ───── artifact 抽取(对话内 chip → 复用文件预览 modal) ─────
|
||||||
// task.working_dir 在 DB 是 `workspace/users/<uuid>/<name>` 形态(to_db_path),
|
// task.working_dir 在 DB 是 `workspace/users/<uuid>/<name>` 形态(to_db_path),
|
||||||
|
|
@ -239,7 +240,7 @@ export function downloadFile(rel) {
|
||||||
fetch("/v1/files/download?path=" + encodeURIComponent(rel), {
|
fetch("/v1/files/download?path=" + encodeURIComponent(rel), {
|
||||||
headers: { "Authorization": "Bearer " + state.token },
|
headers: { "Authorization": "Bearer " + state.token },
|
||||||
}).then(async (r) => {
|
}).then(async (r) => {
|
||||||
if (!r.ok) { alert("下载失败:" + r.status); return; }
|
if (!r.ok) { message("下载失败:" + r.status, "error"); return; }
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { state } from "./state.js";
|
||||||
import { $ } from "./dom.js";
|
import { $ } from "./dom.js";
|
||||||
import { escapeHtml } from "./format.js";
|
import { escapeHtml } from "./format.js";
|
||||||
import { selectTask, syncBgprocLock } from "./chat.js";
|
import { selectTask, syncBgprocLock } from "./chat.js";
|
||||||
|
import { dialogConfirm } from "./dialog.js";
|
||||||
|
|
||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
let _pollTimer = null;
|
let _pollTimer = null;
|
||||||
|
|
@ -52,7 +53,7 @@ export async function killTaskProcs(taskId) {
|
||||||
(p) => p.task_id === taskId && p.status === "running"
|
(p) => p.task_id === taskId && p.status === "running"
|
||||||
);
|
);
|
||||||
if (!running.length) return;
|
if (!running.length) return;
|
||||||
if (!confirm("确定终止后台进程?已产生的输出会保留,进程本身无法恢复。")) return;
|
if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
|
||||||
for (const p of running) {
|
for (const p of running) {
|
||||||
try {
|
try {
|
||||||
await api("POST", `/v1/tasks/${p.task_id}/procs/${p.proc_id}/kill`);
|
await api("POST", `/v1/tasks/${p.task_id}/procs/${p.proc_id}/kill`);
|
||||||
|
|
@ -105,7 +106,7 @@ export function decorateBgprocCard(det, txt) {
|
||||||
async function killProc(procId, btn) {
|
async function killProc(procId, btn) {
|
||||||
const p = _byId.get(procId);
|
const p = _byId.get(procId);
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
if (!confirm("确定终止这个后台进程?已产生的输出会保留,进程本身无法恢复。")) return;
|
if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止这个后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await api("POST", `/v1/tasks/${p.task_id}/procs/${procId}/kill`);
|
await api("POST", `/v1/tasks/${p.task_id}/procs/${procId}/kill`);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { state } from "./state.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { escapeHtml } from "./format.js";
|
import { escapeHtml } from "./format.js";
|
||||||
import { renderMd, highlightIn } from "./markdown.js";
|
import { renderMd, highlightIn } from "./markdown.js";
|
||||||
|
import { dialogConfirm, message } from "./dialog.js";
|
||||||
|
|
||||||
const PLACEHOLDER = '<div class="sk-empty">← 从左侧选一个 skill 查看完整说明</div>';
|
const PLACEHOLDER = '<div class="sk-empty">← 从左侧选一个 skill 查看完整说明</div>';
|
||||||
|
|
||||||
|
|
@ -110,7 +111,11 @@ $("sk-detail").addEventListener("click", async (e) => {
|
||||||
const del = e.target.closest("[data-del]");
|
const del = e.target.closest("[data-del]");
|
||||||
if (!del) return;
|
if (!del) return;
|
||||||
const name = del.getAttribute("data-del");
|
const name = del.getAttribute("data-del");
|
||||||
if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return;
|
if (!await dialogConfirm({
|
||||||
|
title: "删除 skill",
|
||||||
|
message: `删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`,
|
||||||
|
okText: "删除", danger: true,
|
||||||
|
})) return;
|
||||||
del.disabled = true;
|
del.disabled = true;
|
||||||
try {
|
try {
|
||||||
await api("DELETE", "/v1/skills/" + encodeURIComponent(name));
|
await api("DELETE", "/v1/skills/" + encodeURIComponent(name));
|
||||||
|
|
@ -118,7 +123,7 @@ $("sk-detail").addEventListener("click", async (e) => {
|
||||||
$("sk-detail").innerHTML = PLACEHOLDER;
|
$("sk-detail").innerHTML = PLACEHOLDER;
|
||||||
renderList();
|
renderList();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("删除失败: " + err.message);
|
message("删除失败: " + err.message, "error");
|
||||||
del.disabled = false;
|
del.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// GET /v1/wechat/bind、DELETE /v1/wechat/bind、POST /v1/wechat/test。
|
// GET /v1/wechat/bind、DELETE /v1/wechat/bind、POST /v1/wechat/test。
|
||||||
import { $ } from "./dom.js";
|
import { $ } from "./dom.js";
|
||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
|
import { dialogConfirm } from "./dialog.js";
|
||||||
|
|
||||||
let _polling = false;
|
let _polling = false;
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
@ -141,7 +142,7 @@ $("wechat-modal").addEventListener("click", (e) => {
|
||||||
$("wx-bind").onclick = bindFlow;
|
$("wx-bind").onclick = bindFlow;
|
||||||
$("wx-unbind").onclick = async () => {
|
$("wx-unbind").onclick = async () => {
|
||||||
_polling = false;
|
_polling = false;
|
||||||
if (!confirm("确定解绑微信?")) return;
|
if (!await dialogConfirm({ title: "解绑微信", message: "确定解绑微信?", okText: "解绑", danger: true })) return;
|
||||||
try {
|
try {
|
||||||
await api("DELETE", "/v1/wechat/bind");
|
await api("DELETE", "/v1/wechat/bind");
|
||||||
$("wx-qrbox").hidden = true;
|
$("wx-qrbox").hidden = true;
|
||||||
|
|
@ -172,7 +173,7 @@ $("wc-bind-id").onclick = async () => {
|
||||||
finally { $("wc-bind-id").disabled = false; }
|
finally { $("wc-bind-id").disabled = false; }
|
||||||
};
|
};
|
||||||
$("wc-unbind").onclick = async () => {
|
$("wc-unbind").onclick = async () => {
|
||||||
if (!confirm("确定解绑企业微信?")) return;
|
if (!await dialogConfirm({ title: "解绑企业微信", message: "确定解绑企业微信?", okText: "解绑", danger: true })) return;
|
||||||
try { await api("DELETE", "/v1/wecom/bind"); await refreshWecom(); }
|
try { await api("DELETE", "/v1/wecom/bind"); await refreshWecom(); }
|
||||||
catch (e) { setWc("err", "解绑失败: " + e.message); }
|
catch (e) { setWc("err", "解绑失败: " + e.message); }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue