Files
clawpanel/src/components/toast.js
晴天 7eababad4a feat(ux): toast 智能行动按钮 + 拓展 ⓘ 到 gateway/agents + sidebar 加术语表入口
延续上一轮小白 UX 改造的尾声三连:

## 1. toast 智能行动按钮(U2 收尾)
- humanizeError 输出新增 action 字段:{ label, route?, handler?, kind }
- 自动按错误 kind 给默认按钮:
  · gatewayDown → [去启动 Gateway] → /services
  · cmdMissing / permission → [打开设置] → /settings
  · auth → [检查 API Key] → /models
  · network / timeout / rateLimit / generic → 不给(重试由用户控制)
- toast 结构化分支渲染 .toast-action-btn 按钮,点击后用 navigate(route) 或调 handler,并自动关闭 toast
- common.js 加 errorAction.* 三个按钮文案 i18n(11 语言)

## 2. ⓘ 拓展到 gateway / agents
- gateway.js: token label 后加 ⓘ(apikey 术语),renderConfig 末尾 attachTermTooltips
- agents.js: addAgent 弹窗 workspace 字段 label 加 ⓘ,setTimeout 扫 document.body 绑定
- term-tooltip.js 精简表新增 4 个术语:workspace / provider / baseurl + scope(已有)

## 3. sidebar 加术语表入口
- 在两个引擎(OpenClaw + Hermes)的最后一个 section 加「术语」条目
- sidebar.js i18n 新增 glossary 键(11 语言)
- 之前只能从 dashboard quick-actions 进入,现在 sidebar 永久可达

## 4. 顺手 bug 修复
- gateway.js 文件末尾历史残留的多余 `}` (line 348) syntax 错误,删除
- 与之前 hermes/cron.js 同类问题,本次改 ⓘ 时被 node --check 暴露

## 累计变动
- 10 个文件修改
- 7 个新 i18n 键(11 语言)
- Build OK
2026-05-14 03:47:25 +08:00

109 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Toast 通知组件
*
* 入参 message 支持两种:
* 1) string —— 原来的纯文本(向后兼容)
* 2) { message, hint?, raw?, action? } —— humanize-error.js 的友好错误对象:
* - message: 主行(用户视角)
* - hint: 副行小灰字(行动建议)
* - raw: 折叠在「技术详情」里的原始错误字符串
* - action: { label, route?, handler? } 智能行动按钮,点击跳转或调 handler
*/
import { t } from '../lib/i18n.js'
import { navigate } from '../router.js'
let _container = null
function ensureContainer() {
if (!_container) {
_container = document.createElement('div')
_container.className = 'toast-container'
document.body.appendChild(_container)
}
return _container
}
function isStructuredError(v) {
return v && typeof v === 'object' && typeof v.message === 'string'
}
export function toast(message, type = 'info', options = {}) {
// 结构化错误对象需要展示「主行 + hint + 技术详情折叠」duration 给长一些
const structured = isStructuredError(message)
const duration = options.duration || (structured && (message.hint || message.raw) ? 6000 : 3000)
const action = options.action // 可选的操作按钮DOM 元素)
const container = ensureContainer()
const el = document.createElement('div')
el.className = `toast ${type}${structured ? ' toast-structured' : ''}`
if (structured) {
const body = document.createElement('div')
body.className = 'toast-body'
const mainRow = document.createElement('div')
mainRow.className = 'toast-main'
mainRow.textContent = message.message
body.appendChild(mainRow)
if (message.hint) {
const hintRow = document.createElement('div')
hintRow.className = 'toast-hint'
hintRow.textContent = message.hint
body.appendChild(hintRow)
}
if (message.action && message.action.label) {
const actionBtn = document.createElement('button')
actionBtn.type = 'button'
actionBtn.className = 'btn btn-xs btn-primary toast-action-btn'
actionBtn.textContent = `${message.action.label}`
actionBtn.addEventListener('click', () => {
try {
if (typeof message.action.handler === 'function') message.action.handler()
else if (message.action.route) navigate(message.action.route)
} finally {
el.remove()
}
})
body.appendChild(actionBtn)
}
if (message.raw) {
const detail = document.createElement('details')
detail.className = 'toast-raw'
const summary = document.createElement('summary')
summary.textContent = t('common.errorRawLabel')
detail.appendChild(summary)
const pre = document.createElement('pre')
pre.textContent = message.raw
detail.appendChild(pre)
body.appendChild(detail)
}
el.appendChild(body)
} else {
const textSpan = document.createElement('span')
if (options.html) {
textSpan.innerHTML = message
} else {
textSpan.textContent = message
}
el.appendChild(textSpan)
}
// 如果有操作按钮,添加到 toast 中
if (action instanceof HTMLElement) {
el.appendChild(action)
}
container.appendChild(el)
setTimeout(() => {
el.style.opacity = '0'
el.style.transform = 'translateX(20px)'
el.style.transition = 'all 250ms ease'
setTimeout(() => el.remove(), 250)
}, duration)
}