refactor: move logout/change-password UI from floating widget to settings

The injected floating buttons overlapped the mobile composer send button.
Replace the tapIndex-injected widget with a proper client plugin
(panel-auth-ui, ui/ package): registers an account & security section in
the panel settings via the settings.section slot, with change-password
form and logout action. Host plugin keeps only the /panel-auth/change-
password and /panel-auth/logout endpoints.

- ui/: ModuleLoader-format client bundle (dsh.client declaration,
  exports ./client and ./package.json), host no-op stub, bare-package
  symlink required in the profile node_modules (documented in README).
- Host: renderAuthWidget/injectWidget/tapIndex injection removed.
- Tests: widget tests removed; change-password flows unchanged.
This commit is contained in:
dsh
2026-08-16 03:28:31 -04:00
parent f9883150e0
commit 96b5fb6ed0
6 changed files with 208 additions and 151 deletions
+154
View File
@@ -0,0 +1,154 @@
// panel-auth-ui client bundle (ModuleLoader format, served by the client
// module system). Registers an "账号与安全" section in the panel settings
// with change-password and logout actions. Depends on the panel-auth host
// plugin's /panel-auth/change-password and /panel-auth/logout endpoints.
window.__ModuleLoader__.load({
id: "panel-auth-ui",
factory: (require) => {
const React = require("react")
const inputStyle = {
width: "100%",
boxSizing: "border-box",
padding: "9px 11px",
fontSize: 14,
borderRadius: 10,
border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.14))",
background: "var(--dsw-alias-bg-module-platform, rgba(255,255,255,0.05))",
color: "var(--dsw-alias-label-primary, #e8eaf2)",
outline: "none",
}
const cardStyle = {
border: "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.12))",
borderRadius: 16,
padding: "18px 16px",
}
const titleStyle = {
margin: "0 0 6px",
fontSize: 15,
fontWeight: 600,
color: "var(--dsw-alias-label-primary, #e8eaf2)",
}
const hintStyle = {
margin: "0 0 14px",
fontSize: 12.5,
color: "var(--dsw-alias-label-secondary, #9aa1b5)",
}
const buttonStyle = (tone) => ({
padding: "9px 16px",
fontSize: 14,
fontWeight: 600,
borderRadius: 10,
cursor: "pointer",
border: tone === "danger" ? "1px solid rgba(248,113,113,0.5)" : "1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.16))",
background: tone === "danger" ? "rgba(248,113,113,0.12)" : "var(--dsw-alias-bg-module-platform, rgba(255,255,255,0.06))",
color: tone === "danger" ? "#f87171" : "var(--dsw-alias-label-primary, #e8eaf2)",
})
const messageStyle = (ok) => ({
margin: "0 0 12px",
padding: "8px 11px",
fontSize: 12.5,
borderRadius: 8,
border: ok ? "1px solid rgba(74,222,128,0.4)" : "1px solid rgba(248,113,113,0.4)",
background: ok ? "rgba(74,222,128,0.12)" : "rgba(248,113,113,0.12)",
color: ok ? "#4ade80" : "#f87171",
})
function Field(props) {
return React.createElement(
"label",
{ style: { display: "block", marginBottom: 12 } },
React.createElement("div", { style: { fontSize: 12, marginBottom: 6, color: "var(--dsw-alias-label-secondary, #9aa1b5)" } }, props.label),
React.createElement("input", {
type: "password",
autoComplete: props.autoComplete || "off",
value: props.value,
onChange: (event) => props.onChange(event.target.value),
style: inputStyle,
}),
)
}
function PanelAuthSection() {
const [oldPw, setOldPw] = React.useState("")
const [newPw, setNewPw] = React.useState("")
const [confirm, setConfirm] = React.useState("")
const [busy, setBusy] = React.useState(false)
const [message, setMessage] = React.useState(null)
const submit = () => {
if (busy) return
if (!oldPw || !newPw) {
setMessage({ ok: false, text: "请填写当前密码和新密码" })
return
}
if (newPw.length < 8) {
setMessage({ ok: false, text: "新密码至少 8 位" })
return
}
if (newPw !== confirm) {
setMessage({ ok: false, text: "两次输入的新密码不一致" })
return
}
setBusy(true)
setMessage(null)
fetch("/panel-auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ oldPassword: oldPw, newPassword: newPw }),
})
.then((response) => response.json().then((data) => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (ok) {
setOldPw("")
setNewPw("")
setConfirm("")
setMessage({ ok: true, text: (data && data.message) || "密码已更新" })
} else {
setMessage({ ok: false, text: (data && data.message) || "修改失败" })
}
})
.catch(() => setMessage({ ok: false, text: "网络错误,请稍后再试" }))
.finally(() => setBusy(false))
}
const logout = () => {
window.location.href = "/panel-auth/logout"
}
return React.createElement(
"div",
{ style: { display: "flex", flexDirection: "column", gap: 18 } },
React.createElement(
"section",
{ style: cardStyle },
React.createElement("h3", { style: titleStyle }, "修改密码"),
React.createElement("p", { style: hintStyle }, "需要输入当前密码验证身份,修改后无需重新登录。"),
React.createElement(Field, { label: "当前密码", autoComplete: "current-password", value: oldPw, onChange: setOldPw }),
React.createElement(Field, { label: "新密码(至少 8 位)", autoComplete: "new-password", value: newPw, onChange: setNewPw }),
React.createElement(Field, { label: "确认新密码", autoComplete: "new-password", value: confirm, onChange: setConfirm }),
message === null ? null : React.createElement("div", { style: messageStyle(message.ok) }, message.text),
React.createElement("button", { onClick: submit, disabled: busy, style: { ...buttonStyle("primary"), opacity: busy ? 0.6 : 1 } }, busy ? "提交中…" : "确认修改"),
),
React.createElement(
"section",
{ style: cardStyle },
React.createElement("h3", { style: titleStyle }, "退出登录"),
React.createElement("p", { style: hintStyle }, "清除本浏览器的登录 Cookie 并返回登录页。"),
React.createElement("button", { onClick: logout, style: buttonStyle("danger") }, "退出登录"),
),
)
}
return {
apply(ctx) {
const slots = ctx.get("slots")
if (slots === undefined) return
slots.inject("settings.section", () => slots.register(
{ name: "settings.section", id: "panel-auth", order: 30, label: () => "账号与安全" },
(props) => React.createElement(PanelAuthSection, { close: props.close }),
))
},
}
},
})
+7
View File
@@ -0,0 +1,7 @@
// panel-auth-ui — host-side no-op stub.
// The loader mounts every row host-side; the real work lives in client.js,
// served by the client module system under the dsh.client declaration.
export default {
name: 'panel-auth-ui',
apply() {},
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "panel-auth-ui",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "index.js",
"exports": {
".": "./index.js",
"./client": "./client.js",
"./package.json": "./package.json"
},
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings"
],
"platform": "web",
"immediately": false
}
}
}