mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 00:32:43 +08:00
Unify IM channel settings design
This commit is contained in:
parent
828a50398a
commit
a422084eb5
9 changed files with 796 additions and 607 deletions
|
|
@ -36,42 +36,40 @@ const Button = React.forwardRef(function Button(
|
|||
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||
return h('div', { className: 'dxw-heading' },
|
||||
h('div', { className: 'dxw-tools' },
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'dxw-badge' },
|
||||
h('span', { className: 'dxw-dot', 'data-tone': totals.connected > 0 ? 'success' : 'warning' }),
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
: null,
|
||||
h('div', { className: 'dxw-badge', title: 'bot_token 仅保存在 Harness Host 凭据服务中' },
|
||||
h('span', null, '凭据仅保存在本机')),
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
}, adding ? '正在绑定' : '扫码绑定微信'),
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'dxw-badge dim-onlineBadge' },
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingView() {
|
||||
return h('div', { className: 'dxw-card dxw-loading', 'aria-busy': 'true' },
|
||||
h('div', { className: 'dxw-spinner' }),
|
||||
return h('div', { className: 'dxw-card dxw-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||
h('div', { className: 'dxw-spinner dim-spinner' }),
|
||||
h('span', null, '正在读取微信连接状态…'));
|
||||
}
|
||||
|
||||
function EmptyView({ onStart, busy }) {
|
||||
return h('div', { className: 'dxw-card' },
|
||||
h('div', { className: 'dxw-cardBody dxw-empty' },
|
||||
h('div', null,
|
||||
h('div', { className: 'dxw-stateLabel' },
|
||||
h('span', { className: 'dxw-dot' }), h('span', null, '尚未绑定微信')),
|
||||
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||
h('div', { className: 'dxw-cardBody dxw-empty dim-surfaceBody dim-emptyView' },
|
||||
h('div', { className: 'dim-emptyCopy' },
|
||||
h('div', { className: 'dxw-stateLabel dim-stateLabel' },
|
||||
h('span', { className: 'dxw-dot dim-stateDot' }), h('span', null, '尚未绑定微信')),
|
||||
h('h3', null, '扫一次码,就能在微信里使用 Harness'),
|
||||
h('p', null, '二维码由腾讯微信 iLink 服务签发。用手机微信扫描并确认后,账号凭据会直接写入 Harness Host,浏览器不会收到 bot_token。'),
|
||||
h('div', { className: 'dxw-actions' },
|
||||
h('div', { className: 'dxw-actions dim-viewActions' },
|
||||
h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
|
||||
busy ? '正在生成二维码…' : '生成微信二维码')),
|
||||
),
|
||||
h('div', { className: 'dxw-logo', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 64 })),
|
||||
h('div', { className: 'dxw-logo dim-emptyBrand', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 64 })),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -85,35 +83,35 @@ function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
|
|||
const progress = Math.round(Math.min(1, remaining / duration) * 100);
|
||||
React.useEffect(() => setImageFailed(false), [source]);
|
||||
|
||||
return h('div', { className: 'dxw-card' },
|
||||
h('div', { className: 'dxw-cardBody dxw-qrLayout' },
|
||||
h('div', { className: 'dxw-qrColumn' },
|
||||
h('div', { className: 'dxw-qrFrame' },
|
||||
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||
h('div', { className: 'dxw-cardBody dxw-qrLayout dim-surfaceBody dim-qrLayout' },
|
||||
h('div', { className: 'dxw-qrColumn dim-qrColumn' },
|
||||
h('div', { className: 'dxw-qrFrame dim-qrFrame' },
|
||||
source && !imageFailed
|
||||
? h('img', {
|
||||
src: source,
|
||||
alt: '用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码',
|
||||
onError: () => setImageFailed(true),
|
||||
})
|
||||
: h('div', { className: 'dxw-qrFallback' }, '二维码图片未就绪,请使用备用链接。'),
|
||||
expired ? h('div', { className: 'dxw-expired' }, '二维码已过期\n请重新生成') : null,
|
||||
: h('div', { className: 'dxw-qrFallback dim-qrFallback' }, '二维码图片未就绪,请使用备用链接。'),
|
||||
expired ? h('div', { className: 'dxw-expired dim-qrExpired' }, '二维码已过期\n请重新生成') : null,
|
||||
),
|
||||
h('div', { className: 'dxw-countdown' },
|
||||
h('div', null, h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
||||
h('div', { className: 'dxw-progress', 'aria-hidden': 'true' },
|
||||
h('div', { className: 'dxw-countdown dim-countdown' },
|
||||
h('div', { className: 'dim-countdownTop' }, h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
||||
h('div', { className: 'dxw-progress dim-progress', 'aria-hidden': 'true' },
|
||||
h('span', { style: { '--dxw-progress': `${progress}%` } })),
|
||||
)),
|
||||
h('div', { className: 'dxw-qrCopy' },
|
||||
h('div', { className: 'dxw-stateLabel' },
|
||||
h('span', { className: 'dxw-dot', 'data-tone': provision.status === 'scanned' ? 'success' : 'warning' }),
|
||||
h('div', { className: 'dxw-qrCopy dim-qrCopy' },
|
||||
h('div', { className: 'dxw-stateLabel dim-stateLabel' },
|
||||
h('span', { className: 'dxw-dot dim-stateDot', 'data-tone': provision.status === 'scanned' ? 'success' : 'warning' }),
|
||||
h('span', null, provision.status === 'scanned' ? '已扫码,请在手机上确认' : '等待微信扫码')),
|
||||
h('h3', null, expired ? '二维码已失效' : '使用手机微信扫描二维码'),
|
||||
h('p', null, '请在手机上核对并确认授权。部分账号会额外显示一个配对数字,页面会在需要时提示输入。'),
|
||||
h('ol', { className: 'dxw-steps' },
|
||||
h('ol', { className: 'dxw-steps dim-steps' },
|
||||
h('li', null, '打开手机微信并扫描左侧二维码'),
|
||||
h('li', null, '在微信中确认连接该机器人'),
|
||||
h('li', null, '保持本页打开,等待消息长轮询变为在线')),
|
||||
h('div', { className: 'dxw-actions' },
|
||||
h('div', { className: 'dxw-actions dim-viewActions' },
|
||||
expired
|
||||
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
|
||||
: null,
|
||||
|
|
@ -130,9 +128,9 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
|
|||
const [code, setCode] = React.useState('');
|
||||
const valid = /^\d{4,8}$/.test(code);
|
||||
React.useEffect(() => setCode(''), [provision.attemptId]);
|
||||
return h('div', { className: 'dxw-card' },
|
||||
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||
h('form', {
|
||||
className: 'dxw-verify',
|
||||
className: 'dxw-verify dim-specialView',
|
||||
onSubmit: (event) => {
|
||||
event.preventDefault();
|
||||
if (valid && !busy) onSubmit(code);
|
||||
|
|
@ -163,22 +161,22 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
|
|||
}
|
||||
|
||||
function ProgressPanel({ scanned, onCancel, busy }) {
|
||||
return h('div', { className: 'dxw-card dxw-loading', 'aria-busy': 'true' },
|
||||
h('div', { className: 'dxw-spinner' }),
|
||||
return h('div', { className: 'dxw-card dxw-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||
h('div', { className: 'dxw-spinner dim-spinner' }),
|
||||
h('h3', null, scanned ? '微信已确认,正在启动消息连接' : '正在准备微信二维码'),
|
||||
h('p', null, scanned ? '正在保存凭据并验证 Harness 与微信长轮询。' : '正在联系腾讯微信 iLink 服务。'),
|
||||
onCancel ? h('div', { className: 'dxw-actions', style: { justifyContent: 'center', marginTop: 14 } },
|
||||
onCancel ? h('div', { className: 'dxw-actions dim-viewActions', style: { justifyContent: 'center', marginTop: 14 } },
|
||||
h(Button, { onClick: onCancel, disabled: busy }, '取消')) : null);
|
||||
}
|
||||
|
||||
function ProvisionError({ provision, busy, onRetry, onClose }) {
|
||||
const error = provision.error ?? { code: 'WEIXIN_PROVISION_FAILED', message: '微信绑定没有完成' };
|
||||
return h('div', { className: 'dxw-card' },
|
||||
h('div', { className: 'dxw-error', role: 'alert' },
|
||||
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||
h('div', { className: 'dxw-error dim-inlineError', role: 'alert' },
|
||||
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '微信没有绑定完成'),
|
||||
h('p', null, error.message),
|
||||
h('span', { className: 'dxw-errorCode' }, error.code),
|
||||
h('div', { className: 'dxw-actions' },
|
||||
h('div', { className: 'dxw-actions dim-viewActions' },
|
||||
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
||||
h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
|
||||
}
|
||||
|
|
@ -197,30 +195,30 @@ function checkedTime(timestamp) {
|
|||
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||
return h('article', { className: 'dxw-card', tabIndex: -1, 'data-bot-id': account.botId },
|
||||
h('div', { className: 'dxw-cardBody' },
|
||||
h('div', { className: 'dxw-accountTop' },
|
||||
h('div', { className: 'dxw-accountIdentity' },
|
||||
h('div', { className: 'dxw-avatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
|
||||
h('div', null, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
|
||||
h('div', { className: 'dxw-health' },
|
||||
h('span', { className: 'dxw-dot', 'data-tone': tone }),
|
||||
return h('article', { className: 'dxw-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
|
||||
h('div', { className: 'dxw-cardBody dim-botCardBody' },
|
||||
h('div', { className: 'dxw-accountTop dim-botCardTop' },
|
||||
h('div', { className: 'dxw-accountIdentity dim-botIdentity' },
|
||||
h('div', { className: 'dxw-avatar dim-botAvatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
|
||||
h('div', { className: 'dim-botName' }, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
|
||||
h('div', { className: 'dxw-health dim-botHealth' },
|
||||
h('span', { className: 'dxw-dot dim-healthDot', 'data-tone': tone }),
|
||||
h('span', null, account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪'))),
|
||||
h('dl', { className: 'dxw-metrics' },
|
||||
h('div', { className: 'dxw-metric' }, h('dt', null, '消息通道'),
|
||||
h('dl', { className: 'dxw-metrics dim-botMetrics' },
|
||||
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '消息通道'),
|
||||
h('dd', null, account.connected ? 'iLink 长轮询' : '离线')),
|
||||
h('div', { className: 'dxw-metric' }, h('dt', null, '最近检查'),
|
||||
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
||||
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||
h('div', { className: 'dxw-accountFooter' },
|
||||
h('div', { className: 'dxw-summary' }, account.error?.message ?? account.health.summary),
|
||||
h('div', { className: 'dxw-actions' },
|
||||
h(Button, { onClick: onReconnect, disabled: Boolean(busy) },
|
||||
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'dxw-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||
h('div', { className: 'dxw-actions dim-cardActions' },
|
||||
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
||||
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||
h(Button, { kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
||||
removing ? h('div', { className: 'dxw-confirm', role: 'alertdialog' },
|
||||
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
||||
removing ? h('div', { className: 'dxw-confirm dim-confirm', role: 'alertdialog' },
|
||||
h('strong', null, '从此 Harness 移除这个微信账号?'),
|
||||
h('p', null, '这会停止消息连接,并删除本机保存的 bot_token、账号配置和会话映射。其他微信账号不受影响。'),
|
||||
h('div', { className: 'dxw-actions' },
|
||||
h('div', { className: 'dxw-actions dim-viewActions' },
|
||||
h(Button, { onClick: onCancelRemove, disabled: busy === 'delete' }, '保留账号'),
|
||||
h(Button, { kind: 'danger', onClick: onConfirmRemove, disabled: busy === 'delete' },
|
||||
busy === 'delete' ? '正在移除…' : '确认移除')))
|
||||
|
|
@ -228,9 +226,9 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|||
}
|
||||
|
||||
function AccountList(props) {
|
||||
return h('section', null,
|
||||
h('div', { className: 'dxw-listHeading' }, h('h3', null, '已接入的微信账号'), h('span', null, `${props.bots.length} 个`)),
|
||||
h('ul', { className: 'dxw-list' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||
return h('section', { className: 'dim-listSection' },
|
||||
h('div', { className: 'dxw-listHeading dim-listHeading' }, h('h3', null, '已接入的微信账号'), h('span', null, `${props.bots.length} 个`)),
|
||||
h('ul', { className: 'dxw-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||
h(AccountCard, {
|
||||
account,
|
||||
busy: props.busyByBot[account.botId],
|
||||
|
|
@ -523,7 +521,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|||
});
|
||||
}
|
||||
|
||||
return h('section', { className: 'dxw-page', 'aria-label': '微信设置' },
|
||||
return h('section', { className: 'dxw-page dim-channelPage', 'aria-label': '微信设置' },
|
||||
h(Heading, {
|
||||
totals: model.totals,
|
||||
adding: Boolean(provision),
|
||||
|
|
@ -533,13 +531,13 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|||
}),
|
||||
h('div', { className: 'dxw-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
||||
model.error && model.phase === 'ready'
|
||||
? h('div', { className: 'dxw-statusNotice' }, `状态刷新失败:${model.error.message}`)
|
||||
? h('div', { className: 'dxw-statusNotice dim-statusNotice' }, `状态刷新失败:${model.error.message}`)
|
||||
: null,
|
||||
model.phase === 'loading'
|
||||
? h(LoadingView)
|
||||
: model.phase === 'error'
|
||||
? h('div', { className: 'dxw-card' },
|
||||
h('div', { className: 'dxw-error' },
|
||||
? h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||
h('div', { className: 'dxw-error dim-inlineError' },
|
||||
h('h3', null, '无法读取微信状态'),
|
||||
h('p', null, model.error?.message ?? '请稍后重试'),
|
||||
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const CSS = String.raw`
|
|||
.dxw-heading h2 { font-size: 20px; line-height: 28px; font-weight: 680; }
|
||||
.dxw-heading p { color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; margin-top: 5px; white-space: nowrap; }
|
||||
.dxw-tools, .dxw-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||
.dxw-tools { justify-content: flex-end; }
|
||||
.dxw-tools { width: 100%; justify-content: space-between; flex-wrap: nowrap; }
|
||||
.dxw-badge { display: inline-flex; align-items: center; gap: 7px; min-height: 30px; padding: 0 11px; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font-size: 12px; white-space: nowrap; }
|
||||
.dxw-dot { width: 8px; height: 8px; border-radius: 50%; background: #aeb3bb; flex: none; }
|
||||
.dxw-dot[data-tone="success"] { background: var(--dxw-success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dxw-success) 14%, transparent); }
|
||||
|
|
@ -83,6 +83,8 @@ const CSS = String.raw`
|
|||
.dxw-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
|
||||
.dxw-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dxw-accountFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); }
|
||||
.dxw-accountFooter .dxw-actions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
|
||||
.dxw-accountFooter .dxw-button { flex: none; white-space: nowrap; }
|
||||
.dxw-summary { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
|
||||
.dxw-confirm { padding: 18px 24px; border-top: 1px solid color-mix(in srgb, var(--dxw-error) 25%, transparent); background: color-mix(in srgb, var(--dxw-error) 5%, transparent); }
|
||||
.dxw-confirm strong { display: block; font-size: 14px; margin-bottom: 6px; }
|
||||
|
|
@ -93,8 +95,7 @@ const CSS = String.raw`
|
|||
.dxw-visuallyHidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
@keyframes dxw-spin { to { transform: rotate(360deg); } }
|
||||
@media (max-width: 720px) {
|
||||
.dxw-heading, .dxw-accountTop, .dxw-accountFooter { flex-direction: column; align-items: stretch; }
|
||||
.dxw-tools { justify-content: flex-start; }
|
||||
.dxw-heading, .dxw-accountTop { flex-direction: column; align-items: stretch; }
|
||||
.dxw-empty { grid-template-columns: minmax(0, 1fr); }
|
||||
.dxw-logo { display: none; }
|
||||
.dxw-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue