mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +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
651
lib/client.js
651
lib/client.js
File diff suppressed because it is too large
Load diff
|
|
@ -53,44 +53,37 @@ function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||||
h('h2', null, '钉钉机器人'),
|
h('h2', null, '钉钉机器人'),
|
||||||
h('p', null, '通过扫码把钉钉机器人接入 DeepSeek Harness')),
|
h('p', null, '通过扫码把钉钉机器人接入 DeepSeek Harness')),
|
||||||
h('div', { className: 'ddt-tools' },
|
h('div', { className: 'ddt-tools' },
|
||||||
totals.configured > 0
|
|
||||||
? h('div', { className: 'ddt-badge' },
|
|
||||||
h('span', {
|
|
||||||
className: 'ddt-dot',
|
|
||||||
'data-tone': totals.connected > 0 ? 'success' : 'warning',
|
|
||||||
}),
|
|
||||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
|
||||||
: null,
|
|
||||||
h('div', {
|
|
||||||
className: 'ddt-badge',
|
|
||||||
title: '应用密钥只写入 Harness Host 凭据服务,不会发送到浏览器',
|
|
||||||
}, '凭据仅保存在本机'),
|
|
||||||
h(Button, {
|
h(Button, {
|
||||||
kind: 'primary',
|
kind: 'primary',
|
||||||
|
className: 'dim-scanButton',
|
||||||
onClick: onAdd,
|
onClick: onAdd,
|
||||||
disabled: adding || busy,
|
disabled: adding || busy,
|
||||||
ref: addButtonRef,
|
ref: addButtonRef,
|
||||||
}, adding ? '正在接入' : '扫码接入钉钉')));
|
}, adding ? '正在接入' : '扫码接入钉钉'),
|
||||||
|
totals.configured > 0
|
||||||
|
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
||||||
|
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||||
|
: null));
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingView() {
|
function LoadingView() {
|
||||||
return h('div', { className: 'ddt-card ddt-loading', 'aria-busy': 'true' },
|
return h('div', { className: 'ddt-card ddt-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||||
h('div', { className: 'ddt-spinner' }),
|
h('div', { className: 'ddt-spinner dim-spinner' }),
|
||||||
h('span', null, '正在读取钉钉连接状态…'));
|
h('span', null, '正在读取钉钉连接状态…'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyView({ busy, onStart }) {
|
function EmptyView({ busy, onStart }) {
|
||||||
return h('div', { className: 'ddt-card' },
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
||||||
h('div', { className: 'ddt-cardBody ddt-empty' },
|
h('div', { className: 'ddt-cardBody ddt-empty dim-surfaceBody dim-emptyView' },
|
||||||
h('div', null,
|
h('div', { className: 'dim-emptyCopy' },
|
||||||
h('div', { className: 'ddt-stateLabel' },
|
h('div', { className: 'ddt-stateLabel dim-stateLabel' },
|
||||||
h('span', { className: 'ddt-dot' }), h('span', null, '尚未接入钉钉机器人')),
|
h('span', { className: 'ddt-dot dim-stateDot' }), h('span', null, '尚未接入钉钉机器人')),
|
||||||
h('h3', null, '扫一次码,自动创建并连接机器人'),
|
h('h3', null, '扫一次码,自动创建并连接机器人'),
|
||||||
h('p', null, '授权由钉钉官方页面完成。扫码账号必须已加入一个企业/组织并有权创建机器人;创建成功后,应用凭据会直接写入 Harness Host。'),
|
h('p', null, '授权由钉钉官方页面完成。扫码账号必须已加入一个企业/组织并有权创建机器人;创建成功后,应用凭据会直接写入 Harness Host。'),
|
||||||
h('div', { className: 'ddt-actions' },
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
||||||
h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
|
h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
|
||||||
busy ? '正在生成二维码…' : '生成钉钉二维码'))),
|
busy ? '正在生成二维码…' : '生成钉钉二维码'))),
|
||||||
h('div', { className: 'ddt-brandMark', 'aria-hidden': 'true' },
|
h('div', { className: 'ddt-brandMark dim-emptyBrand', 'aria-hidden': 'true' },
|
||||||
h(DingtalkIcon, { size: 68 }))));
|
h(DingtalkIcon, { size: 68 }))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,34 +97,34 @@ function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
|
||||||
|
|
||||||
React.useEffect(() => setImageFailed(false), [source]);
|
React.useEffect(() => setImageFailed(false), [source]);
|
||||||
|
|
||||||
return h('div', { className: 'ddt-card' },
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
||||||
h('div', { className: 'ddt-cardBody ddt-qrLayout' },
|
h('div', { className: 'ddt-cardBody ddt-qrLayout dim-surfaceBody dim-qrLayout' },
|
||||||
h('div', { className: 'ddt-qrColumn' },
|
h('div', { className: 'ddt-qrColumn dim-qrColumn' },
|
||||||
h('div', { className: 'ddt-qrFrame' },
|
h('div', { className: 'ddt-qrFrame dim-qrFrame' },
|
||||||
source && !imageFailed
|
source && !imageFailed
|
||||||
? h('img', {
|
? h('img', {
|
||||||
src: source,
|
src: source,
|
||||||
alt: '用于把钉钉机器人接入 DeepSeek Harness 的一次性二维码',
|
alt: '用于把钉钉机器人接入 DeepSeek Harness 的一次性二维码',
|
||||||
onError: () => setImageFailed(true),
|
onError: () => setImageFailed(true),
|
||||||
})
|
})
|
||||||
: h('div', { className: 'ddt-qrFallback' }, '二维码图片未就绪,请重新生成。'),
|
: h('div', { className: 'ddt-qrFallback dim-qrFallback' }, '二维码图片未就绪,请重新生成。'),
|
||||||
expired ? h('div', { className: 'ddt-expired' }, '二维码已过期\n请重新生成') : null),
|
expired ? h('div', { className: 'ddt-expired dim-qrExpired' }, '二维码已过期\n请重新生成') : null),
|
||||||
h('div', { className: 'ddt-countdown' },
|
h('div', { className: 'ddt-countdown dim-countdown' },
|
||||||
h('div', { className: 'ddt-countdownTop' },
|
h('div', { className: 'ddt-countdownTop dim-countdownTop' },
|
||||||
h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
||||||
h('div', { className: 'ddt-progress', 'aria-hidden': 'true' },
|
h('div', { className: 'ddt-progress dim-progress', 'aria-hidden': 'true' },
|
||||||
h('span', { style: { '--ddt-progress': `${progress}%` } })))),
|
h('span', { style: { '--ddt-progress': `${progress}%` } })))),
|
||||||
h('div', { className: 'ddt-qrCopy' },
|
h('div', { className: 'ddt-qrCopy dim-qrCopy' },
|
||||||
h('div', { className: 'ddt-stateLabel' },
|
h('div', { className: 'ddt-stateLabel dim-stateLabel' },
|
||||||
h('span', { className: 'ddt-dot', 'data-tone': expired ? 'error' : 'warning' }),
|
h('span', { className: 'ddt-dot dim-stateDot', 'data-tone': expired ? 'error' : 'warning' }),
|
||||||
h('span', null, expired ? '二维码已失效' : '等待钉钉扫码授权')),
|
h('span', null, expired ? '二维码已失效' : '等待钉钉扫码授权')),
|
||||||
h('h3', null, expired ? '重新生成二维码后继续' : '使用钉钉 App 完成机器人授权'),
|
h('h3', null, expired ? '重新生成二维码后继续' : '使用钉钉 App 完成机器人授权'),
|
||||||
h('p', null, '扫码账号必须已加入企业/组织。如果钉钉提示尚未加入组织,请在提示页创建组织,或换用已加入组织的账号。'),
|
h('p', null, '扫码账号必须已加入企业/组织。如果钉钉提示尚未加入组织,请在提示页创建组织,或换用已加入组织的账号。'),
|
||||||
h('ol', { className: 'ddt-steps' },
|
h('ol', { className: 'ddt-steps dim-steps' },
|
||||||
h('li', null, '使用已加入企业/组织的钉钉账号扫描左侧二维码'),
|
h('li', null, '使用已加入企业/组织的钉钉账号扫描左侧二维码'),
|
||||||
h('li', null, '在授权页点击“一键创建新机器人”'),
|
h('li', null, '在授权页点击“一键创建新机器人”'),
|
||||||
h('li', null, '保持本页打开,等待机器人自动连接')),
|
h('li', null, '保持本页打开,等待机器人自动连接')),
|
||||||
h('div', { className: 'ddt-actions' },
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
||||||
expired
|
expired
|
||||||
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
|
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -142,15 +135,15 @@ function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
|
||||||
function ProgressPanel({ status, busy, onCancel }) {
|
function ProgressPanel({ status, busy, onCancel }) {
|
||||||
const connecting = status === 'connecting';
|
const connecting = status === 'connecting';
|
||||||
const creating = status === 'creating';
|
const creating = status === 'creating';
|
||||||
return h('div', { className: 'ddt-card ddt-loading', 'aria-busy': 'true' },
|
return h('div', { className: 'ddt-card ddt-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||||
h('div', { className: 'ddt-spinner' }),
|
h('div', { className: 'ddt-spinner dim-spinner' }),
|
||||||
h('h3', null, connecting
|
h('h3', null, connecting
|
||||||
? '机器人已创建,正在建立消息连接'
|
? '机器人已创建,正在建立消息连接'
|
||||||
: creating ? '授权已确认,正在创建钉钉机器人' : '正在确认钉钉授权'),
|
: creating ? '授权已确认,正在创建钉钉机器人' : '正在确认钉钉授权'),
|
||||||
h('p', null, connecting
|
h('p', null, connecting
|
||||||
? '正在检查钉钉 Stream 长连接,成功后会自动显示为在线。'
|
? '正在检查钉钉 Stream 长连接,成功后会自动显示为在线。'
|
||||||
: '请勿关闭本页,钉钉完成授权后将自动继续。'),
|
: '请勿关闭本页,钉钉完成授权后将自动继续。'),
|
||||||
h('div', { className: 'ddt-actions', style: { justifyContent: 'center', marginTop: 14 } },
|
h('div', { className: 'ddt-actions dim-viewActions', style: { justifyContent: 'center', marginTop: 14 } },
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, '取消接入')));
|
h(Button, { onClick: onCancel, disabled: busy }, '取消接入')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,12 +152,12 @@ function ProvisionError({ provision, busy, onRetry, onClose }) {
|
||||||
code: 'DINGTALK_PROVISION_FAILED',
|
code: 'DINGTALK_PROVISION_FAILED',
|
||||||
message: '钉钉机器人没有接入完成',
|
message: '钉钉机器人没有接入完成',
|
||||||
};
|
};
|
||||||
return h('div', { className: 'ddt-card' },
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
||||||
h('div', { className: 'ddt-inlineError', role: 'alert' },
|
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
|
||||||
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '钉钉机器人没有接入完成'),
|
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '钉钉机器人没有接入完成'),
|
||||||
h('p', null, error.message),
|
h('p', null, error.message),
|
||||||
h('span', { className: 'ddt-errorCode' }, error.code),
|
h('span', { className: 'ddt-errorCode' }, error.code),
|
||||||
h('div', { className: 'ddt-actions' },
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
||||||
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
||||||
h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
|
h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +177,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
||||||
const cancelRef = React.useRef(null);
|
const cancelRef = React.useRef(null);
|
||||||
React.useEffect(() => cancelRef.current?.focus(), []);
|
React.useEffect(() => cancelRef.current?.focus(), []);
|
||||||
return h('div', {
|
return h('div', {
|
||||||
className: 'ddt-confirm',
|
className: 'ddt-confirm dim-confirm',
|
||||||
role: 'alertdialog',
|
role: 'alertdialog',
|
||||||
'aria-label': `移除${account.bot.name}`,
|
'aria-label': `移除${account.bot.name}`,
|
||||||
onKeyDown: (event) => {
|
onKeyDown: (event) => {
|
||||||
|
|
@ -193,7 +186,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
||||||
},
|
},
|
||||||
h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
|
h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
|
||||||
h('p', null, '这会停止消息连接,并删除本机保存的应用凭据、机器人配置及会话映射。钉钉开放平台中的机器人不会被自动删除。'),
|
h('p', null, '这会停止消息连接,并删除本机保存的应用凭据、机器人配置及会话映射。钉钉开放平台中的机器人不会被自动删除。'),
|
||||||
h('div', { className: 'ddt-actions' },
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
||||||
h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, '保留机器人'),
|
h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, '保留机器人'),
|
||||||
h(Button, { kind: 'danger', onClick: onConfirm, disabled: busy },
|
h(Button, { kind: 'danger', onClick: onConfirm, disabled: busy },
|
||||||
busy ? '正在移除…' : '确认移除接入')));
|
busy ? '正在移除…' : '确认移除接入')));
|
||||||
|
|
@ -211,27 +204,27 @@ export function AccountCard({
|
||||||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||||
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
||||||
return h('article', { className: 'ddt-card', tabIndex: -1, 'data-bot-id': account.botId },
|
return h('article', { className: 'ddt-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
|
||||||
h('div', { className: 'ddt-cardBody' },
|
h('div', { className: 'ddt-cardBody dim-botCardBody' },
|
||||||
h('div', { className: 'ddt-accountTop' },
|
h('div', { className: 'ddt-accountTop dim-botCardTop' },
|
||||||
h('div', { className: 'ddt-accountIdentity' },
|
h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
|
||||||
h('div', { className: 'ddt-avatar', 'aria-hidden': 'true' }, h(DingtalkIcon, { size: 29 })),
|
h('div', { className: 'ddt-avatar dim-botAvatar', 'aria-hidden': 'true' }, h(DingtalkIcon, { size: 29 })),
|
||||||
h('div', null,
|
h('div', { className: 'dim-botName' },
|
||||||
h('h3', { title: account.bot.name }, account.bot.name),
|
h('h3', { title: account.bot.name }, account.bot.name),
|
||||||
h('p', { title: account.bot.clientIdMasked }, account.bot.clientIdMasked))),
|
h('p', { title: account.bot.clientIdMasked }, account.bot.clientIdMasked))),
|
||||||
h('div', { className: 'ddt-health' },
|
h('div', { className: 'ddt-health dim-botHealth' },
|
||||||
h('span', { className: 'ddt-dot', 'data-tone': tone }), h('span', null, stateLabel))),
|
h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }), h('span', null, stateLabel))),
|
||||||
h('dl', { className: 'ddt-metrics' },
|
h('dl', { className: 'ddt-metrics dim-botMetrics' },
|
||||||
h('div', { className: 'ddt-metric' }, h('dt', null, '消息通道'),
|
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'),
|
||||||
h('dd', null, account.connected ? 'Stream 长连接' : '离线')),
|
h('dd', null, account.connected ? 'Stream 长连接' : '离线')),
|
||||||
h('div', { className: 'ddt-metric' }, h('dt', null, '最近检查'),
|
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
||||||
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||||
h('div', { className: 'ddt-accountFooter' },
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||||
h('div', { className: 'ddt-summary' }, account.error?.message ?? account.health.summary),
|
h('div', { className: 'ddt-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||||
h('div', { className: 'ddt-actions' },
|
h('div', { className: 'ddt-actions dim-cardActions' },
|
||||||
h(Button, { onClick: onReconnect, disabled: Boolean(busy) },
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
||||||
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||||
h(Button, { kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
|
||||||
'移除接入')))),
|
'移除接入')))),
|
||||||
removing ? h(RemoveConfirmation, {
|
removing ? h(RemoveConfirmation, {
|
||||||
account,
|
account,
|
||||||
|
|
@ -242,10 +235,10 @@ export function AccountCard({
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountList(props) {
|
function AccountList(props) {
|
||||||
return h('section', null,
|
return h('section', { className: 'dim-listSection' },
|
||||||
h('div', { className: 'ddt-listHeading' },
|
h('div', { className: 'ddt-listHeading dim-listHeading' },
|
||||||
h('h3', null, '已接入的钉钉机器人'), h('span', null, `${props.bots.length} 个`)),
|
h('h3', null, '已接入的钉钉机器人'), h('span', null, `${props.bots.length} 个`)),
|
||||||
h('ul', { className: 'ddt-list' }, props.bots.map((account) => h('li', { key: account.botId },
|
h('ul', { className: 'ddt-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||||
h(AccountCard, {
|
h(AccountCard, {
|
||||||
account,
|
account,
|
||||||
busy: props.busyByBot[account.botId],
|
busy: props.busyByBot[account.botId],
|
||||||
|
|
@ -605,7 +598,7 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return h('section', { className: 'ddt-page', 'aria-label': '钉钉设置' },
|
return h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
|
||||||
h(Heading, {
|
h(Heading, {
|
||||||
totals: model.totals,
|
totals: model.totals,
|
||||||
adding: Boolean(provision),
|
adding: Boolean(provision),
|
||||||
|
|
@ -615,13 +608,13 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
||||||
}),
|
}),
|
||||||
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
||||||
model.error && model.phase === 'ready'
|
model.error && model.phase === 'ready'
|
||||||
? h('div', { className: 'ddt-statusNotice', role: 'alert' }, `状态刷新失败:${model.error.message}`)
|
? h('div', { className: 'ddt-statusNotice dim-statusNotice', role: 'alert' }, `状态刷新失败:${model.error.message}`)
|
||||||
: null,
|
: null,
|
||||||
model.phase === 'loading'
|
model.phase === 'loading'
|
||||||
? h(LoadingView)
|
? h(LoadingView)
|
||||||
: model.phase === 'error'
|
: model.phase === 'error'
|
||||||
? h('div', { className: 'ddt-card' },
|
? h('div', { className: 'ddt-card dim-surfaceCard' },
|
||||||
h('div', { className: 'ddt-inlineError', role: 'alert' },
|
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
|
||||||
h('h3', null, '无法读取钉钉机器人状态'),
|
h('h3', null, '无法读取钉钉机器人状态'),
|
||||||
h('p', null, model.error?.message ?? '请稍后重试'),
|
h('p', null, model.error?.message ?? '请稍后重试'),
|
||||||
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ const CSS = String.raw`
|
||||||
.ddt-heading h2 { font-size: 20px; line-height: 28px; font-weight: 680; }
|
.ddt-heading h2 { font-size: 20px; line-height: 28px; font-weight: 680; }
|
||||||
.ddt-heading p { margin-top: 5px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; }
|
.ddt-heading p { margin-top: 5px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; white-space: nowrap; }
|
||||||
.ddt-tools, .ddt-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
|
.ddt-tools, .ddt-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||||
.ddt-tools { justify-content: flex-end; }
|
.ddt-tools { width: 100%; justify-content: space-between; flex-wrap: nowrap; }
|
||||||
.ddt-badge { min-height: 30px; display: inline-flex; align-items: center; gap: 7px; 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; }
|
.ddt-badge { min-height: 30px; display: inline-flex; align-items: center; gap: 7px; 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; }
|
||||||
.ddt-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #aeb3bb; }
|
.ddt-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: #aeb3bb; }
|
||||||
.ddt-dot[data-tone="success"] { background: var(--ddt-success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ddt-success) 14%, transparent); }
|
.ddt-dot[data-tone="success"] { background: var(--ddt-success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ddt-success) 14%, transparent); }
|
||||||
|
|
@ -92,6 +92,8 @@ const CSS = String.raw`
|
||||||
.ddt-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
|
.ddt-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
|
||||||
.ddt-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
.ddt-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.ddt-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); }
|
.ddt-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); }
|
||||||
|
.ddt-accountFooter .ddt-actions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
|
||||||
|
.ddt-accountFooter .ddt-button { flex: none; white-space: nowrap; }
|
||||||
.ddt-summary { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
|
.ddt-summary { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
|
||||||
.ddt-confirm { padding: 18px 24px; border-top: 1px solid color-mix(in srgb, var(--ddt-error) 25%, transparent); background: color-mix(in srgb, var(--ddt-error) 5%, transparent); }
|
.ddt-confirm { padding: 18px 24px; border-top: 1px solid color-mix(in srgb, var(--ddt-error) 25%, transparent); background: color-mix(in srgb, var(--ddt-error) 5%, transparent); }
|
||||||
.ddt-confirm strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
.ddt-confirm strong { display: block; margin-bottom: 6px; font-size: 14px; }
|
||||||
|
|
@ -113,9 +115,8 @@ const CSS = String.raw`
|
||||||
.ddt-metric { padding: 10px; }
|
.ddt-metric { padding: 10px; }
|
||||||
}
|
}
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.ddt-heading, .ddt-accountTop, .ddt-accountFooter { flex-direction: column; align-items: stretch; }
|
.ddt-heading, .ddt-accountTop { flex-direction: column; align-items: stretch; }
|
||||||
.ddt-heading p { white-space: normal; }
|
.ddt-heading p { white-space: normal; }
|
||||||
.ddt-tools { justify-content: flex-start; }
|
|
||||||
.ddt-empty { grid-template-columns: minmax(0, 1fr); }
|
.ddt-empty { grid-template-columns: minmax(0, 1fr); }
|
||||||
.ddt-brandMark { display: none; }
|
.ddt-brandMark { display: none; }
|
||||||
.ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
.ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||||
|
|
|
||||||
|
|
@ -129,58 +129,51 @@ function Heading({ totals, onAdd, adding, busy, addButtonRef }) {
|
||||||
const hasBots = totals.configured > 0;
|
const hasBots = totals.configured > 0;
|
||||||
return h("div", { className: "bxf-heading" },
|
return h("div", { className: "bxf-heading" },
|
||||||
h("div", { className: "bxf-headingTools" },
|
h("div", { className: "bxf-headingTools" },
|
||||||
hasBots
|
|
||||||
? h("div", {
|
|
||||||
className: "bxf-totalBadge",
|
|
||||||
"aria-label": `已接入 ${totals.configured} 个机器人,其中 ${totals.connected} 个在线`,
|
|
||||||
}, h("strong", null, totals.connected), h("span", null, `/ ${totals.configured} 在线`))
|
|
||||||
: null,
|
|
||||||
h("div", {
|
|
||||||
className: "bxf-localBadge",
|
|
||||||
title: "每个应用的凭据均由 Host 独立保存,不会发送到浏览器",
|
|
||||||
}, h(ShieldIcon, { size: 14 }), h("span", null, "凭据仅保存在本机")),
|
|
||||||
h(Button, {
|
h(Button, {
|
||||||
kind: "primary",
|
kind: "primary",
|
||||||
size: "small",
|
size: "small",
|
||||||
className: "bxf-bindButton",
|
className: "bxf-bindButton dim-scanButton",
|
||||||
onClick: onAdd,
|
onClick: onAdd,
|
||||||
disabled: adding || busy,
|
disabled: adding || busy,
|
||||||
ref: addButtonRef,
|
ref: addButtonRef,
|
||||||
"aria-busy": busy ? "true" : undefined,
|
"aria-busy": busy ? "true" : undefined,
|
||||||
}, adding ? "正在绑定" : "扫码绑定机器人"),
|
}, adding ? "正在绑定" : "扫码绑定机器人"),
|
||||||
|
hasBots
|
||||||
|
? h("div", {
|
||||||
|
className: "bxf-totalBadge dim-onlineBadge",
|
||||||
|
"aria-label": `已接入 ${totals.configured} 个机器人,其中 ${totals.connected} 个在线`,
|
||||||
|
}, h("span", null, `${totals.connected} / ${totals.configured} 在线`))
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingView() {
|
function LoadingView() {
|
||||||
return h("div", {
|
return h("div", {
|
||||||
className: "bxf-card", "aria-busy": "true", "aria-label": "正在读取飞书机器人列表",
|
className: "bxf-card dim-surfaceCard dim-loadingView",
|
||||||
}, h("div", { className: "bxf-skeleton" },
|
"aria-busy": "true",
|
||||||
h("div", { className: "bxf-skeletonLine" }),
|
"aria-label": "正在读取飞书机器人列表",
|
||||||
h("div", { className: "bxf-skeletonLine" }),
|
},
|
||||||
h("div", { className: "bxf-skeletonLine" }),
|
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
||||||
h("div", { className: "bxf-skeletonLine" }),
|
h("span", null, "正在读取飞书连接状态…"),
|
||||||
h("div", { className: "bxf-skeletonBox" }),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyView({ onStart, busy }) {
|
function EmptyView({ onStart, busy }) {
|
||||||
return h("div", { className: "bxf-card" },
|
return h("div", { className: "bxf-card dim-surfaceCard" },
|
||||||
h("div", { className: "bxf-cardBody bxf-intro" },
|
h("div", { className: "bxf-cardBody bxf-intro dim-surfaceBody dim-emptyView" },
|
||||||
h("div", { className: "bxf-introCopy" },
|
h("div", { className: "bxf-introCopy dim-emptyCopy" },
|
||||||
h("div", { className: "bxf-stateLabel" },
|
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
||||||
h("span", { className: "bxf-dot" }), h("span", null, "尚未接入机器人")),
|
h("span", { className: "bxf-dot dim-stateDot" }), h("span", null, "尚未接入机器人")),
|
||||||
h("h3", null, "扫码,创建第一个飞书入口"),
|
h("h3", null, "扫码,创建第一个飞书入口"),
|
||||||
h("p", null, "无需手动填写 App ID。以后还可以继续添加机器人,分别服务不同团队或飞书租户。"),
|
h("p", null, "无需手动填写 App ID。以后还可以继续添加机器人,分别服务不同团队或飞书租户。"),
|
||||||
h("div", { className: "bxf-actions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
h(Button, {
|
h(Button, {
|
||||||
kind: "primary", icon: h(SparkIcon), onClick: onStart,
|
kind: "primary", onClick: onStart,
|
||||||
disabled: busy, "aria-busy": busy ? "true" : undefined,
|
disabled: busy, "aria-busy": busy ? "true" : undefined,
|
||||||
}, busy ? "正在创建…" : "一键创建飞书机器人")),
|
}, busy ? "正在生成二维码…" : "生成飞书二维码")),
|
||||||
h("div", { className: "bxf-note" }, h(ShieldIcon, { size: 16 }),
|
|
||||||
h("span", null, "每个 App Secret 都只写入 Host 凭据存储,浏览器不会收到 Secret。")),
|
|
||||||
),
|
),
|
||||||
h("div", { className: "bxf-markStage", "aria-hidden": "true" }, h(BrandMark)),
|
h("div", { className: "bxf-markStage dim-emptyBrand", "aria-hidden": "true" }, h(BrandMark)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -212,57 +205,57 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
|
|
||||||
React.useEffect(() => setImageFailed(false), [qrSource]);
|
React.useEffect(() => setImageFailed(false), [qrSource]);
|
||||||
|
|
||||||
return h("div", { className: "bxf-card bxf-provisionCard" },
|
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
||||||
h("div", { className: "bxf-cardBody bxf-qrLayout" },
|
h("div", { className: "bxf-cardBody bxf-qrLayout dim-surfaceBody dim-qrLayout" },
|
||||||
h("div", { className: "bxf-qrColumn" },
|
h("div", { className: "bxf-qrColumn dim-qrColumn" },
|
||||||
h("div", { className: "bxf-qrFrame" },
|
h("div", { className: "bxf-qrFrame dim-qrFrame" },
|
||||||
qrSource && !imageFailed
|
qrSource && !imageFailed
|
||||||
? h("img", {
|
? h("img", {
|
||||||
src: qrSource,
|
src: qrSource,
|
||||||
alt: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
alt: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
||||||
onError: () => setImageFailed(true),
|
onError: () => setImageFailed(true),
|
||||||
})
|
})
|
||||||
: h("div", { className: "bxf-qrFallback" },
|
: h("div", { className: "bxf-qrFallback dim-qrFallback" },
|
||||||
h("div", null, h(QrIcon), h("span", null, "二维码未就绪,请打开授权链接"))),
|
h("div", null, h(QrIcon), h("span", null, "二维码未就绪,请打开授权链接"))),
|
||||||
expired
|
expired
|
||||||
? h("div", { className: "bxf-expiredOverlay", role: "status" },
|
? h("div", { className: "bxf-expiredOverlay dim-qrExpired", role: "status" },
|
||||||
h("div", null, "二维码已失效", h("br"), "请刷新后重新扫码"))
|
h("div", null, "二维码已失效", h("br"), "请刷新后重新扫码"))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
h("div", {
|
h("div", {
|
||||||
className: "bxf-countdown",
|
className: "bxf-countdown dim-countdown",
|
||||||
"aria-label": expired ? "二维码已失效" : `二维码剩余 ${formatRemaining(remaining)}`,
|
"aria-label": expired ? "二维码已失效" : `二维码剩余 ${formatRemaining(remaining)}`,
|
||||||
},
|
},
|
||||||
h("div", { className: "bxf-countdownTop", "aria-hidden": "true" },
|
h("div", { className: "bxf-countdownTop dim-countdownTop", "aria-hidden": "true" },
|
||||||
h("span", null, expired ? "等待刷新" : "二维码有效时间"),
|
h("span", null, expired ? "等待刷新" : "二维码有效时间"),
|
||||||
h("strong", null, formatRemaining(remaining))),
|
h("strong", null, formatRemaining(remaining))),
|
||||||
h("div", { className: "bxf-progress", "aria-hidden": "true" },
|
h("div", { className: "bxf-progress dim-progress", "aria-hidden": "true" },
|
||||||
h("span", { style: { "--bxf-progress": `${Math.round(progress * 100)}%` } })),
|
h("span", { style: { "--bxf-progress": `${Math.round(progress * 100)}%` } })),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h("div", { className: "bxf-qrCopy" },
|
h("div", { className: "bxf-qrCopy dim-qrCopy" },
|
||||||
h("div", { className: "bxf-stateLabel" },
|
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
||||||
h("span", { className: "bxf-dot", "data-tone": "warning" }),
|
h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
||||||
h("span", null, "正在添加新机器人")),
|
h("span", null, "正在添加新机器人")),
|
||||||
h("h3", null, expired ? "刷新二维码后继续" : "使用飞书扫码创建机器人"),
|
h("h3", null, expired ? "刷新二维码后继续" : "使用飞书扫码创建机器人"),
|
||||||
h("p", null, "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
h("p", null, "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
||||||
h("ol", { className: "bxf-steps" },
|
h("ol", { className: "bxf-steps dim-steps" },
|
||||||
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
||||||
h("li", null, "核对应用名称与权限范围,并确认创建"),
|
h("li", null, "核对应用名称与权限范围,并确认创建"),
|
||||||
h("li", null, "保持本页打开,等待新机器人的长连接就绪")),
|
h("li", null, "保持本页打开,等待新机器人的长连接就绪")),
|
||||||
h("div", { className: "bxf-actions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
expired
|
expired
|
||||||
? h(Button, {
|
? h(Button, {
|
||||||
kind: "primary", icon: h(RefreshIcon), onClick: onRefresh, disabled: busy,
|
kind: "primary", onClick: onRefresh, disabled: busy,
|
||||||
}, busy ? "刷新中…" : "刷新二维码")
|
}, busy ? "刷新中…" : "刷新二维码")
|
||||||
: href
|
: href
|
||||||
? h("a", {
|
? h("a", {
|
||||||
className: "bxf-button bxf-link", "data-kind": "secondary",
|
className: "bxf-button bxf-link", "data-kind": "secondary",
|
||||||
href, target: "_blank", rel: "noopener noreferrer",
|
href, target: "_blank", rel: "noopener noreferrer",
|
||||||
}, h(ExternalIcon), h("span", null, "在飞书中打开"))
|
}, h("span", null, "在飞书中打开"))
|
||||||
: null,
|
: null,
|
||||||
!expired
|
!expired
|
||||||
? h(Button, { icon: h(RefreshIcon), onClick: onRefresh, disabled: busy }, "换一个二维码")
|
? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
|
||||||
: null,
|
: null,
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, "取消添加")),
|
h(Button, { onClick: onCancel, disabled: busy }, "取消添加")),
|
||||||
),
|
),
|
||||||
|
|
@ -272,35 +265,31 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
|
|
||||||
function ProvisionProgress({ phase, onCancel, busy }) {
|
function ProvisionProgress({ phase, onCancel, busy }) {
|
||||||
const connecting = phase === "connecting";
|
const connecting = phase === "connecting";
|
||||||
return h("div", { className: "bxf-card bxf-provisionCard", "aria-busy": "true" },
|
return h("div", {
|
||||||
h("div", { className: "bxf-connecting bxf-connectingCompact" },
|
className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
|
||||||
h("div", { className: "bxf-connectingCopy" },
|
"aria-busy": "true",
|
||||||
h("div", { className: "bxf-orbit" },
|
},
|
||||||
h("div", { className: "bxf-orbitCore" },
|
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
||||||
connecting ? h(RobotIcon, { size: 24 }) : h(SparkIcon, { size: 24 }))),
|
h("h3", null, connecting ? "已确认,正在连接新机器人" : "正在准备授权二维码"),
|
||||||
h("h3", null, connecting ? "已确认,正在连接新机器人" : "正在准备授权二维码"),
|
h("p", null, connecting
|
||||||
h("p", null, connecting
|
? "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
||||||
? "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
||||||
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
connecting
|
||||||
connecting
|
? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
||||||
? h("div", { className: "bxf-actions", style: { justifyContent: "center" } },
|
h(Button, { onClick: onCancel, disabled: busy }, "取消添加"))
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, "取消添加"))
|
: null,
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProvisionError({ error, onRetry, onCancel, busy }) {
|
function ProvisionError({ error, onRetry, onCancel, busy }) {
|
||||||
return h("div", { className: "bxf-card bxf-provisionCard" },
|
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
||||||
h("div", { className: "bxf-inlineError", role: "alert" },
|
h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
|
||||||
h("div", { className: "bxf-errorIcon" }, h(AlertIcon)),
|
|
||||||
h("div", null,
|
h("div", null,
|
||||||
h("h3", null, "新机器人没有添加完成"),
|
h("h3", null, "新机器人没有添加完成"),
|
||||||
h("p", null, error.message),
|
h("p", null, error.message),
|
||||||
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
||||||
h("div", { className: "bxf-actions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
h(Button, { kind: "primary", icon: h(RefreshIcon), onClick: onRetry, disabled: busy },
|
h(Button, { kind: "primary", onClick: onRetry, disabled: busy },
|
||||||
busy ? "重试中…" : "重新生成二维码"),
|
busy ? "重试中…" : "重新生成二维码"),
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, "关闭")),
|
h(Button, { onClick: onCancel, disabled: busy }, "关闭")),
|
||||||
),
|
),
|
||||||
|
|
@ -335,7 +324,7 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
||||||
React.useEffect(() => cancelRef.current?.focus(), []);
|
React.useEffect(() => cancelRef.current?.focus(), []);
|
||||||
|
|
||||||
return h("div", {
|
return h("div", {
|
||||||
className: "bxf-confirm",
|
className: "bxf-confirm dim-confirm",
|
||||||
role: "alertdialog",
|
role: "alertdialog",
|
||||||
"aria-labelledby": titleId,
|
"aria-labelledby": titleId,
|
||||||
"aria-describedby": descriptionId,
|
"aria-describedby": descriptionId,
|
||||||
|
|
@ -349,7 +338,7 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
||||||
h("h4", { id: titleId }, `从 DeepSeek Harness 移除“${bot.bot.name}”?`),
|
h("h4", { id: titleId }, `从 DeepSeek Harness 移除“${bot.bot.name}”?`),
|
||||||
h("p", { id: descriptionId },
|
h("p", { id: descriptionId },
|
||||||
"此操作会停止这个机器人的连接,并删除保存在本机的接入配置和凭据。飞书开放平台中的应用不会被自动删除,其他机器人也不受影响。"),
|
"此操作会停止这个机器人的连接,并删除保存在本机的接入配置和凭据。飞书开放平台中的应用不会被自动删除,其他机器人也不受影响。"),
|
||||||
h("div", { className: "bxf-actions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, "保留机器人"),
|
h(Button, { ref: cancelRef, onClick: onCancel, disabled: busy }, "保留机器人"),
|
||||||
h(Button, { kind: "danger", onClick: onConfirm, disabled: busy },
|
h(Button, { kind: "danger", onClick: onConfirm, disabled: busy },
|
||||||
busy ? "正在移除…" : "确认移除接入")),
|
busy ? "正在移除…" : "确认移除接入")),
|
||||||
|
|
@ -382,43 +371,42 @@ export function BotCard({
|
||||||
?? health.summary;
|
?? health.summary;
|
||||||
const titleId = `bxf-bot-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
const titleId = `bxf-bot-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
||||||
return h("article", {
|
return h("article", {
|
||||||
className: "bxf-card bxf-botCard",
|
className: "bxf-card bxf-botCard dim-botCard",
|
||||||
"aria-labelledby": titleId,
|
"aria-labelledby": titleId,
|
||||||
"data-bot-id": connection.botId,
|
"data-bot-id": connection.botId,
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
ref: cardRef,
|
ref: cardRef,
|
||||||
},
|
},
|
||||||
h("div", { className: "bxf-cardBody" },
|
h("div", { className: "bxf-cardBody dim-botCardBody" },
|
||||||
h("div", { className: "bxf-connectedTop" },
|
h("div", { className: "bxf-connectedTop dim-botCardTop" },
|
||||||
h("div", { className: "bxf-botIdentity" },
|
h("div", { className: "bxf-botIdentity dim-botIdentity" },
|
||||||
h("div", { className: "bxf-avatar", "aria-hidden": "true" },
|
h("div", { className: "bxf-avatar dim-botAvatar", "aria-hidden": "true" },
|
||||||
h(FeishuLogoGlyph, { size: 34 })),
|
h(FeishuLogoGlyph, { size: 34 })),
|
||||||
h("div", { className: "bxf-botName" },
|
h("div", { className: "bxf-botName dim-botName" },
|
||||||
h("h3", { id: titleId, title: bot.name }, bot.name),
|
h("h3", { id: titleId, title: bot.name }, bot.name),
|
||||||
h("p", { title: bot.appIdMasked }, bot.appIdMasked ?? "应用标识已安全保存")),
|
h("p", { title: bot.appIdMasked }, bot.appIdMasked ?? "应用标识已安全保存")),
|
||||||
),
|
),
|
||||||
h("div", { className: "bxf-healthPill", "data-health": stateForDisplay },
|
h("div", { className: "bxf-healthPill dim-botHealth", "data-health": stateForDisplay },
|
||||||
h("span", { className: "bxf-dot", "data-tone": tone }),
|
h("span", { className: "bxf-dot dim-healthDot", "data-tone": tone }),
|
||||||
h("span", null, HEALTH_LABELS[stateForDisplay] ?? "状态未知")),
|
h("span", null, HEALTH_LABELS[stateForDisplay] ?? "状态未知")),
|
||||||
),
|
),
|
||||||
h("dl", { className: "bxf-statusGrid" },
|
h("dl", { className: "bxf-statusGrid dim-botMetrics" },
|
||||||
h("div", { className: "bxf-metric" }, h("dt", null, "消息通道"),
|
h("div", { className: "bxf-metric dim-botMetric" }, h("dt", null, "消息通道"),
|
||||||
h("dd", null, connected ? "长连接" : stateForDisplay === "connecting" ? "连接中" : "已断开")),
|
h("dd", null, connected ? "长连接" : stateForDisplay === "connecting" ? "连接中" : "已断开")),
|
||||||
h("div", { className: "bxf-metric" }, h("dt", null, "最近检查"),
|
h("div", { className: "bxf-metric dim-botMetric" }, h("dt", null, "最近检查"),
|
||||||
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
|
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
|
||||||
),
|
),
|
||||||
h("div", { className: "bxf-divider" }),
|
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
||||||
h("div", { className: "bxf-connectedFooter" },
|
h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
||||||
h("div", { className: "bxf-healthSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
summary),
|
||||||
h("strong", null, "连接状态:"), h("span", null, summary)),
|
h("div", { className: "bxf-actions bxf-botActions dim-cardActions" },
|
||||||
h("div", { className: "bxf-actions bxf-botActions" },
|
|
||||||
h(Button, {
|
h(Button, {
|
||||||
size: "small", icon: h(RefreshIcon), onClick: onReconnect,
|
className: "dim-cardAction", onClick: onReconnect,
|
||||||
disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : undefined,
|
disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : undefined,
|
||||||
"aria-label": `${connected ? "检查连接" : "重试连接"}${bot.name}`,
|
"aria-label": `${connected ? "检查连接" : "重试连接"}${bot.name}`,
|
||||||
}, busy === "reconnect" ? (connected ? "检查中…" : "正在连接…") : connected ? "检查连接" : "重试连接"),
|
}, busy === "reconnect" ? (connected ? "检查中…" : "正在连接…") : connected ? "检查连接" : "重试连接"),
|
||||||
h(Button, {
|
h(Button, {
|
||||||
size: "small", kind: "danger", onClick: onRequestRemove,
|
className: "dim-cardAction", kind: "danger", onClick: onRequestRemove,
|
||||||
disabled: Boolean(busy), ref: removeButtonRef,
|
disabled: Boolean(busy), ref: removeButtonRef,
|
||||||
"aria-label": `从 DeepSeek Harness 移除${bot.name}`,
|
"aria-label": `从 DeepSeek Harness 移除${bot.name}`,
|
||||||
}, "移除接入")),
|
}, "移除接入")),
|
||||||
|
|
@ -436,11 +424,11 @@ export function BotCard({
|
||||||
}
|
}
|
||||||
|
|
||||||
function BotList(props) {
|
function BotList(props) {
|
||||||
return h("section", { className: "bxf-listSection", "aria-labelledby": "bxf-bot-list-title" },
|
return h("section", { className: "bxf-listSection dim-listSection", "aria-labelledby": "bxf-bot-list-title" },
|
||||||
h("div", { className: "bxf-listHeading" },
|
h("div", { className: "bxf-listHeading dim-listHeading" },
|
||||||
h("h3", { id: "bxf-bot-list-title" }, "已接入的机器人"),
|
h("h3", { id: "bxf-bot-list-title" }, "已接入的机器人"),
|
||||||
h("span", null, `${props.bots.length} 个`)),
|
h("span", null, `${props.bots.length} 个`)),
|
||||||
h("ul", { className: "bxf-botList", role: "list" },
|
h("ul", { className: "bxf-botList dim-botList", role: "list" },
|
||||||
props.bots.map((bot) => h("li", { key: bot.botId },
|
props.bots.map((bot) => h("li", { key: bot.botId },
|
||||||
h(BotCard, {
|
h(BotCard, {
|
||||||
connection: bot,
|
connection: bot,
|
||||||
|
|
@ -459,15 +447,14 @@ function BotList(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PageError({ error, onRetry, busy }) {
|
function PageError({ error, onRetry, busy }) {
|
||||||
return h("div", { className: "bxf-card" },
|
return h("div", { className: "bxf-card dim-surfaceCard" },
|
||||||
h("div", { className: "bxf-error", role: "alert" },
|
h("div", { className: "bxf-error dim-inlineError", role: "alert" },
|
||||||
h("div", { className: "bxf-errorIcon" }, h(AlertIcon)),
|
|
||||||
h("div", null,
|
h("div", null,
|
||||||
h("h3", null, "无法读取飞书机器人"),
|
h("h3", null, "无法读取飞书机器人"),
|
||||||
h("p", null, error.message),
|
h("p", null, error.message),
|
||||||
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
||||||
h("div", { className: "bxf-actions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
h(Button, { kind: "primary", icon: h(RefreshIcon), onClick: onRetry, disabled: busy },
|
h(Button, { kind: "primary", onClick: onRetry, disabled: busy },
|
||||||
busy ? "重试中…" : "重新读取"))),
|
busy ? "重试中…" : "重新读取"))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -874,7 +861,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
else removeButtonRefs.current.delete(botId);
|
else removeButtonRefs.current.delete(botId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return h("section", { className: "bxf-page", "aria-label": "飞书机器人设置" },
|
return h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
|
||||||
h(Heading, {
|
h(Heading, {
|
||||||
totals: model.totals,
|
totals: model.totals,
|
||||||
onAdd: () => void startProvisioning(),
|
onAdd: () => void startProvisioning(),
|
||||||
|
|
@ -886,7 +873,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
className: "bxf-visuallyHidden", role: "status", "aria-live": "polite", "aria-atomic": "true",
|
className: "bxf-visuallyHidden", role: "status", "aria-live": "polite", "aria-atomic": "true",
|
||||||
}, announcement),
|
}, announcement),
|
||||||
model.statusError
|
model.statusError
|
||||||
? h("div", { className: "bxf-statusNotice", role: "status" },
|
? h("div", { className: "bxf-statusNotice dim-statusNotice", role: "status" },
|
||||||
h(AlertIcon, { size: 16 }),
|
h(AlertIcon, { size: 16 }),
|
||||||
h("span", null, `状态自动刷新失败:${model.statusError.message}`),
|
h("span", null, `状态自动刷新失败:${model.statusError.message}`),
|
||||||
h(Button, { size: "small", onClick: () => void loadStatus({ silent: true }), disabled: pageBusy }, "立即重试"))
|
h(Button, { size: "small", onClick: () => void loadStatus({ silent: true }), disabled: pageBusy }, "立即重试"))
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const CSS = String.raw`
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -81,22 +81,6 @@ const CSS = String.raw`
|
||||||
|
|
||||||
.bxf-totalBadge strong { color: var(--bxf-success); font-size: 13px; }
|
.bxf-totalBadge strong { color: var(--bxf-success); font-size: 13px; }
|
||||||
|
|
||||||
.bxf-localBadge {
|
|
||||||
flex: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
min-height: 28px;
|
|
||||||
border: 1px solid var(--dsw-alias-border-l2, #dee0e3);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
color: var(--dsw-alias-label-secondary, #646a73);
|
|
||||||
background: var(--dsw-alias-bg-layer-1, #fff);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 16px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bxf-card {
|
.bxf-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -424,12 +408,11 @@ const CSS = String.raw`
|
||||||
.bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
|
.bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
|
||||||
.bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
|
.bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
|
||||||
|
|
||||||
.bxf-divider { height: 1px; background: var(--dsw-alias-border-l2, #dee0e3); margin: 22px 0 0; }
|
.bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); }
|
||||||
.bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 17px; }
|
|
||||||
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
||||||
.bxf-healthSummary strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 600; }
|
|
||||||
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
|
.bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
|
||||||
.bxf-botActions { margin-top: 0; justify-content: flex-end; }
|
.bxf-botActions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
|
||||||
|
.bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
|
||||||
|
|
||||||
.bxf-confirm {
|
.bxf-confirm {
|
||||||
border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
|
border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
|
||||||
|
|
@ -478,8 +461,7 @@ const CSS = String.raw`
|
||||||
|
|
||||||
@container (max-width: 620px) {
|
@container (max-width: 620px) {
|
||||||
.bxf-headingTools { gap: 6px; }
|
.bxf-headingTools { gap: 6px; }
|
||||||
.bxf-headingTools .bxf-totalBadge, .bxf-headingTools .bxf-localBadge { padding-inline: 8px; }
|
.bxf-headingTools .bxf-totalBadge { padding-inline: 8px; }
|
||||||
.bxf-headingTools .bxf-localBadge { gap: 4px; }
|
|
||||||
.bxf-headingTools .bxf-bindButton { padding-inline: 10px; }
|
.bxf-headingTools .bxf-bindButton { padding-inline: 10px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,9 +471,7 @@ const CSS = String.raw`
|
||||||
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||||
.bxf-qrCopy { width: 100%; }
|
.bxf-qrCopy { width: 100%; }
|
||||||
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-connectedTop, .bxf-connectedFooter { align-items: flex-start; flex-direction: column; }
|
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
|
||||||
.bxf-botActions { width: 100%; justify-content: flex-start; }
|
|
||||||
.bxf-botActions .bxf-button { min-height: 44px; }
|
|
||||||
.bxf-inlineError { grid-template-columns: minmax(0, 1fr); padding: 20px; }
|
.bxf-inlineError { grid-template-columns: minmax(0, 1fr); padding: 20px; }
|
||||||
.bxf-statusNotice { align-items: flex-start; flex-wrap: wrap; }
|
.bxf-statusNotice { align-items: flex-start; flex-wrap: wrap; }
|
||||||
.bxf-cardBody { padding: 20px; }
|
.bxf-cardBody { padding: 20px; }
|
||||||
|
|
|
||||||
|
|
@ -36,42 +36,40 @@ const Button = React.forwardRef(function Button(
|
||||||
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||||
return h('div', { className: 'dxw-heading' },
|
return h('div', { className: 'dxw-heading' },
|
||||||
h('div', { className: 'dxw-tools' },
|
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, {
|
h(Button, {
|
||||||
kind: 'primary',
|
kind: 'primary',
|
||||||
|
className: 'dim-scanButton',
|
||||||
onClick: onAdd,
|
onClick: onAdd,
|
||||||
disabled: adding || busy,
|
disabled: adding || busy,
|
||||||
ref: addButtonRef,
|
ref: addButtonRef,
|
||||||
}, adding ? '正在绑定' : '扫码绑定微信'),
|
}, adding ? '正在绑定' : '扫码绑定微信'),
|
||||||
|
totals.configured > 0
|
||||||
|
? h('div', { className: 'dxw-badge dim-onlineBadge' },
|
||||||
|
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingView() {
|
function LoadingView() {
|
||||||
return h('div', { className: 'dxw-card dxw-loading', 'aria-busy': 'true' },
|
return h('div', { className: 'dxw-card dxw-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||||
h('div', { className: 'dxw-spinner' }),
|
h('div', { className: 'dxw-spinner dim-spinner' }),
|
||||||
h('span', null, '正在读取微信连接状态…'));
|
h('span', null, '正在读取微信连接状态…'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyView({ onStart, busy }) {
|
function EmptyView({ onStart, busy }) {
|
||||||
return h('div', { className: 'dxw-card' },
|
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||||
h('div', { className: 'dxw-cardBody dxw-empty' },
|
h('div', { className: 'dxw-cardBody dxw-empty dim-surfaceBody dim-emptyView' },
|
||||||
h('div', null,
|
h('div', { className: 'dim-emptyCopy' },
|
||||||
h('div', { className: 'dxw-stateLabel' },
|
h('div', { className: 'dxw-stateLabel dim-stateLabel' },
|
||||||
h('span', { className: 'dxw-dot' }), h('span', null, '尚未绑定微信')),
|
h('span', { className: 'dxw-dot dim-stateDot' }), h('span', null, '尚未绑定微信')),
|
||||||
h('h3', null, '扫一次码,就能在微信里使用 Harness'),
|
h('h3', null, '扫一次码,就能在微信里使用 Harness'),
|
||||||
h('p', null, '二维码由腾讯微信 iLink 服务签发。用手机微信扫描并确认后,账号凭据会直接写入 Harness Host,浏览器不会收到 bot_token。'),
|
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 },
|
h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
|
||||||
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);
|
const progress = Math.round(Math.min(1, remaining / duration) * 100);
|
||||||
React.useEffect(() => setImageFailed(false), [source]);
|
React.useEffect(() => setImageFailed(false), [source]);
|
||||||
|
|
||||||
return h('div', { className: 'dxw-card' },
|
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||||
h('div', { className: 'dxw-cardBody dxw-qrLayout' },
|
h('div', { className: 'dxw-cardBody dxw-qrLayout dim-surfaceBody dim-qrLayout' },
|
||||||
h('div', { className: 'dxw-qrColumn' },
|
h('div', { className: 'dxw-qrColumn dim-qrColumn' },
|
||||||
h('div', { className: 'dxw-qrFrame' },
|
h('div', { className: 'dxw-qrFrame dim-qrFrame' },
|
||||||
source && !imageFailed
|
source && !imageFailed
|
||||||
? h('img', {
|
? h('img', {
|
||||||
src: source,
|
src: source,
|
||||||
alt: '用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码',
|
alt: '用于把微信机器人绑定到 DeepSeek Harness 的一次性二维码',
|
||||||
onError: () => setImageFailed(true),
|
onError: () => setImageFailed(true),
|
||||||
})
|
})
|
||||||
: h('div', { className: 'dxw-qrFallback' }, '二维码图片未就绪,请使用备用链接。'),
|
: h('div', { className: 'dxw-qrFallback dim-qrFallback' }, '二维码图片未就绪,请使用备用链接。'),
|
||||||
expired ? h('div', { className: 'dxw-expired' }, '二维码已过期\n请重新生成') : null,
|
expired ? h('div', { className: 'dxw-expired dim-qrExpired' }, '二维码已过期\n请重新生成') : null,
|
||||||
),
|
),
|
||||||
h('div', { className: 'dxw-countdown' },
|
h('div', { className: 'dxw-countdown dim-countdown' },
|
||||||
h('div', null, h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
h('div', { className: 'dim-countdownTop' }, h('span', null, '二维码有效时间'), h('strong', null, formatRemaining(remaining))),
|
||||||
h('div', { className: 'dxw-progress', 'aria-hidden': 'true' },
|
h('div', { className: 'dxw-progress dim-progress', 'aria-hidden': 'true' },
|
||||||
h('span', { style: { '--dxw-progress': `${progress}%` } })),
|
h('span', { style: { '--dxw-progress': `${progress}%` } })),
|
||||||
)),
|
)),
|
||||||
h('div', { className: 'dxw-qrCopy' },
|
h('div', { className: 'dxw-qrCopy dim-qrCopy' },
|
||||||
h('div', { className: 'dxw-stateLabel' },
|
h('div', { className: 'dxw-stateLabel dim-stateLabel' },
|
||||||
h('span', { className: 'dxw-dot', 'data-tone': provision.status === 'scanned' ? 'success' : 'warning' }),
|
h('span', { className: 'dxw-dot dim-stateDot', 'data-tone': provision.status === 'scanned' ? 'success' : 'warning' }),
|
||||||
h('span', null, provision.status === 'scanned' ? '已扫码,请在手机上确认' : '等待微信扫码')),
|
h('span', null, provision.status === 'scanned' ? '已扫码,请在手机上确认' : '等待微信扫码')),
|
||||||
h('h3', null, expired ? '二维码已失效' : '使用手机微信扫描二维码'),
|
h('h3', null, expired ? '二维码已失效' : '使用手机微信扫描二维码'),
|
||||||
h('p', null, '请在手机上核对并确认授权。部分账号会额外显示一个配对数字,页面会在需要时提示输入。'),
|
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('li', null, '在微信中确认连接该机器人'),
|
||||||
h('li', null, '保持本页打开,等待消息长轮询变为在线')),
|
h('li', null, '保持本页打开,等待消息长轮询变为在线')),
|
||||||
h('div', { className: 'dxw-actions' },
|
h('div', { className: 'dxw-actions dim-viewActions' },
|
||||||
expired
|
expired
|
||||||
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
|
? h(Button, { kind: 'primary', onClick: onRefresh, disabled: busy }, '重新生成二维码')
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -130,9 +128,9 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
|
||||||
const [code, setCode] = React.useState('');
|
const [code, setCode] = React.useState('');
|
||||||
const valid = /^\d{4,8}$/.test(code);
|
const valid = /^\d{4,8}$/.test(code);
|
||||||
React.useEffect(() => setCode(''), [provision.attemptId]);
|
React.useEffect(() => setCode(''), [provision.attemptId]);
|
||||||
return h('div', { className: 'dxw-card' },
|
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||||
h('form', {
|
h('form', {
|
||||||
className: 'dxw-verify',
|
className: 'dxw-verify dim-specialView',
|
||||||
onSubmit: (event) => {
|
onSubmit: (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (valid && !busy) onSubmit(code);
|
if (valid && !busy) onSubmit(code);
|
||||||
|
|
@ -163,22 +161,22 @@ function VerificationPanel({ provision, busy, onSubmit, onCancel }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProgressPanel({ scanned, onCancel, busy }) {
|
function ProgressPanel({ scanned, onCancel, busy }) {
|
||||||
return h('div', { className: 'dxw-card dxw-loading', 'aria-busy': 'true' },
|
return h('div', { className: 'dxw-card dxw-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
||||||
h('div', { className: 'dxw-spinner' }),
|
h('div', { className: 'dxw-spinner dim-spinner' }),
|
||||||
h('h3', null, scanned ? '微信已确认,正在启动消息连接' : '正在准备微信二维码'),
|
h('h3', null, scanned ? '微信已确认,正在启动消息连接' : '正在准备微信二维码'),
|
||||||
h('p', null, scanned ? '正在保存凭据并验证 Harness 与微信长轮询。' : '正在联系腾讯微信 iLink 服务。'),
|
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);
|
h(Button, { onClick: onCancel, disabled: busy }, '取消')) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProvisionError({ provision, busy, onRetry, onClose }) {
|
function ProvisionError({ provision, busy, onRetry, onClose }) {
|
||||||
const error = provision.error ?? { code: 'WEIXIN_PROVISION_FAILED', message: '微信绑定没有完成' };
|
const error = provision.error ?? { code: 'WEIXIN_PROVISION_FAILED', message: '微信绑定没有完成' };
|
||||||
return h('div', { className: 'dxw-card' },
|
return h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||||
h('div', { className: 'dxw-error', role: 'alert' },
|
h('div', { className: 'dxw-error dim-inlineError', role: 'alert' },
|
||||||
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '微信没有绑定完成'),
|
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '微信没有绑定完成'),
|
||||||
h('p', null, error.message),
|
h('p', null, error.message),
|
||||||
h('span', { className: 'dxw-errorCode' }, error.code),
|
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, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
||||||
h(Button, { onClick: onClose, 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 }) {
|
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||||
return h('article', { className: 'dxw-card', tabIndex: -1, 'data-bot-id': account.botId },
|
return h('article', { className: 'dxw-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
|
||||||
h('div', { className: 'dxw-cardBody' },
|
h('div', { className: 'dxw-cardBody dim-botCardBody' },
|
||||||
h('div', { className: 'dxw-accountTop' },
|
h('div', { className: 'dxw-accountTop dim-botCardTop' },
|
||||||
h('div', { className: 'dxw-accountIdentity' },
|
h('div', { className: 'dxw-accountIdentity dim-botIdentity' },
|
||||||
h('div', { className: 'dxw-avatar', 'aria-hidden': 'true' }, h(WeixinLogoGlyph, { size: 27 })),
|
h('div', { className: 'dxw-avatar dim-botAvatar', '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: 'dim-botName' }, h('h3', null, account.bot.name), h('p', null, account.bot.accountIdMasked))),
|
||||||
h('div', { className: 'dxw-health' },
|
h('div', { className: 'dxw-health dim-botHealth' },
|
||||||
h('span', { className: 'dxw-dot', 'data-tone': tone }),
|
h('span', { className: 'dxw-dot dim-healthDot', 'data-tone': tone }),
|
||||||
h('span', null, account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪'))),
|
h('span', null, account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪'))),
|
||||||
h('dl', { className: 'dxw-metrics' },
|
h('dl', { className: 'dxw-metrics dim-botMetrics' },
|
||||||
h('div', { className: 'dxw-metric' }, h('dt', null, '消息通道'),
|
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '消息通道'),
|
||||||
h('dd', null, account.connected ? 'iLink 长轮询' : '离线')),
|
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('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||||
h('div', { className: 'dxw-accountFooter' },
|
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
||||||
h('div', { className: 'dxw-summary' }, account.error?.message ?? account.health.summary),
|
h('div', { className: 'dxw-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||||
h('div', { className: 'dxw-actions' },
|
h('div', { className: 'dxw-actions dim-cardActions' },
|
||||||
h(Button, { onClick: onReconnect, disabled: Boolean(busy) },
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
||||||
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||||
h(Button, { kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
||||||
removing ? h('div', { className: 'dxw-confirm', role: 'alertdialog' },
|
removing ? h('div', { className: 'dxw-confirm dim-confirm', role: 'alertdialog' },
|
||||||
h('strong', null, '从此 Harness 移除这个微信账号?'),
|
h('strong', null, '从此 Harness 移除这个微信账号?'),
|
||||||
h('p', null, '这会停止消息连接,并删除本机保存的 bot_token、账号配置和会话映射。其他微信账号不受影响。'),
|
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, { onClick: onCancelRemove, disabled: busy === 'delete' }, '保留账号'),
|
||||||
h(Button, { kind: 'danger', onClick: onConfirmRemove, disabled: busy === 'delete' },
|
h(Button, { kind: 'danger', onClick: onConfirmRemove, disabled: busy === 'delete' },
|
||||||
busy === 'delete' ? '正在移除…' : '确认移除')))
|
busy === 'delete' ? '正在移除…' : '确认移除')))
|
||||||
|
|
@ -228,9 +226,9 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountList(props) {
|
function AccountList(props) {
|
||||||
return h('section', null,
|
return h('section', { className: 'dim-listSection' },
|
||||||
h('div', { className: 'dxw-listHeading' }, h('h3', null, '已接入的微信账号'), h('span', null, `${props.bots.length} 个`)),
|
h('div', { className: 'dxw-listHeading dim-listHeading' }, h('h3', null, '已接入的微信账号'), h('span', null, `${props.bots.length} 个`)),
|
||||||
h('ul', { className: 'dxw-list' }, props.bots.map((account) => h('li', { key: account.botId },
|
h('ul', { className: 'dxw-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||||
h(AccountCard, {
|
h(AccountCard, {
|
||||||
account,
|
account,
|
||||||
busy: props.busyByBot[account.botId],
|
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, {
|
h(Heading, {
|
||||||
totals: model.totals,
|
totals: model.totals,
|
||||||
adding: Boolean(provision),
|
adding: Boolean(provision),
|
||||||
|
|
@ -533,13 +531,13 @@ export function WeixinSettingsTab({ rpcCall }) {
|
||||||
}),
|
}),
|
||||||
h('div', { className: 'dxw-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
h('div', { className: 'dxw-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
||||||
model.error && model.phase === 'ready'
|
model.error && model.phase === 'ready'
|
||||||
? h('div', { className: 'dxw-statusNotice' }, `状态刷新失败:${model.error.message}`)
|
? h('div', { className: 'dxw-statusNotice dim-statusNotice' }, `状态刷新失败:${model.error.message}`)
|
||||||
: null,
|
: null,
|
||||||
model.phase === 'loading'
|
model.phase === 'loading'
|
||||||
? h(LoadingView)
|
? h(LoadingView)
|
||||||
: model.phase === 'error'
|
: model.phase === 'error'
|
||||||
? h('div', { className: 'dxw-card' },
|
? h('div', { className: 'dxw-card dim-surfaceCard' },
|
||||||
h('div', { className: 'dxw-error' },
|
h('div', { className: 'dxw-error dim-inlineError' },
|
||||||
h('h3', null, '无法读取微信状态'),
|
h('h3', null, '无法读取微信状态'),
|
||||||
h('p', null, model.error?.message ?? '请稍后重试'),
|
h('p', null, model.error?.message ?? '请稍后重试'),
|
||||||
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
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 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-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, .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-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 { 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); }
|
.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 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-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 { 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-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 { 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; }
|
.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; }
|
.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); } }
|
@keyframes dxw-spin { to { transform: rotate(360deg); } }
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.dxw-heading, .dxw-accountTop, .dxw-accountFooter { flex-direction: column; align-items: stretch; }
|
.dxw-heading, .dxw-accountTop { flex-direction: column; align-items: stretch; }
|
||||||
.dxw-tools { justify-content: flex-start; }
|
|
||||||
.dxw-empty { grid-template-columns: minmax(0, 1fr); }
|
.dxw-empty { grid-template-columns: minmax(0, 1fr); }
|
||||||
.dxw-logo { display: none; }
|
.dxw-logo { display: none; }
|
||||||
.dxw-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
.dxw-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,100 @@ const CSS = String.raw`
|
||||||
.dim-panel { min-width: 0; container-type: inline-size; }
|
.dim-panel { min-width: 0; container-type: inline-size; }
|
||||||
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page { width: 100%; max-width: none; padding: 0 0 24px; }
|
||||||
.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
|
.dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
|
||||||
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: flex-end; }
|
.dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: space-between; }
|
||||||
|
.dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid #1677ff; border-radius: 8px; color: #fff; background: #1677ff; box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; white-space: nowrap; }
|
||||||
|
.dim-panel .bxf-headingTools .dim-scanButton:hover:not(:disabled), .dim-panel .dxw-tools .dim-scanButton:hover:not(:disabled), .dim-panel .ddt-tools .dim-scanButton:hover:not(:disabled) { border-color: #0958d9; background: #0958d9; }
|
||||||
|
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { flex: none; min-height: 30px; display: inline-flex; align-items: center; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-fill-secondary, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 18px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
|
||||||
|
.dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
|
||||||
|
.dim-panel .dim-surfaceCard::before { display: none; }
|
||||||
|
.dim-panel .dim-surfaceBody { padding: 24px; }
|
||||||
|
.dim-panel .dim-listSection { display: flex; flex-direction: column; gap: 0; }
|
||||||
|
.dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 2px 0 9px; padding: 0; }
|
||||||
|
.dim-panel .dim-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
|
||||||
|
.dim-panel .dim-listHeading span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; line-height: normal; }
|
||||||
|
.dim-panel .dim-botList { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
||||||
|
.dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
|
||||||
|
.dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
|
||||||
|
.dim-panel .dim-loadingView p { margin: 0; line-height: 1.6; }
|
||||||
|
.dim-panel .dim-spinner { width: 24px; height: 24px; margin: 0 auto 13px; border: 3px solid var(--dsw-alias-line-border, #e6e8eb); border-top-color: #1677ff; border-radius: 50%; animation: dim-spin .8s linear infinite; }
|
||||||
|
@keyframes dim-spin { to { transform: rotate(360deg); } }
|
||||||
|
.dim-panel .dim-emptyView { min-height: 230px; display: grid; grid-template-columns: minmax(0, 1fr) 180px; align-items: center; gap: 30px; }
|
||||||
|
.dim-panel .dim-emptyCopy { min-width: 0; }
|
||||||
|
.dim-panel .dim-emptyCopy h3 { margin: 8px 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 18px; line-height: 1.35; font-weight: 650; }
|
||||||
|
.dim-panel .dim-emptyCopy > p { max-width: 560px; margin: 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.65; }
|
||||||
|
.dim-panel .dim-emptyBrand { width: 110px; height: 110px; display: grid; place-items: center; justify-self: center; border-radius: 28px; box-shadow: 0 18px 45px rgb(22 119 255 / 18%); }
|
||||||
|
.dim-panel .dim-stateLabel { display: inline-flex; align-items: center; gap: 8px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; font-weight: 600; }
|
||||||
|
.dim-panel .dim-stateDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #8f959e); box-shadow: none; }
|
||||||
|
.dim-panel .dim-stateDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); }
|
||||||
|
.dim-panel .dim-stateDot[data-tone="warning"] { background: var(--dsw-alias-state-warning-primary, #d97706); }
|
||||||
|
.dim-panel .dim-stateDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
|
||||||
|
.dim-panel .dim-viewActions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
|
||||||
|
.dim-panel .dim-viewActions .bxf-button, .dim-panel .dim-viewActions .dxw-button, .dim-panel .dim-viewActions .ddt-button { min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); box-shadow: none; font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-viewActions .bxf-button[data-kind="primary"], .dim-panel .dim-viewActions .dxw-button[data-kind="primary"], .dim-panel .dim-viewActions .ddt-button[data-kind="primary"] { border-color: #1677ff; color: #fff; background: #1677ff; box-shadow: none; }
|
||||||
|
.dim-panel .dim-viewActions .bxf-button[data-kind="danger"], .dim-panel .dim-viewActions .dxw-button[data-kind="danger"], .dim-panel .dim-viewActions .ddt-button[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||||
|
.dim-panel .dim-qrLayout { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 34px; align-items: start; }
|
||||||
|
.dim-panel .dim-qrColumn { width: 100%; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||||
|
.dim-panel .dim-qrFrame { position: relative; width: min(270px, 100%); aspect-ratio: 1; display: grid; place-items: center; overflow: hidden; padding: 10px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 16px; background: #fff; }
|
||||||
|
.dim-panel .dim-qrFrame::before { content: ""; position: absolute; inset: 7px; z-index: 0; border: 1px solid color-mix(in srgb, #1677ff 16%, var(--dsw-alias-line-border, #dfe1e5)); border-radius: 12px; pointer-events: none; }
|
||||||
|
.dim-panel .dim-qrFrame::after { display: none; }
|
||||||
|
.dim-panel .dim-qrFrame img { position: relative; z-index: 1; width: 100%; height: 100%; display: block; object-fit: contain; }
|
||||||
|
.dim-panel .dim-qrFallback { position: relative; z-index: 1; display: grid; place-items: center; gap: 8px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 1.5; text-align: center; }
|
||||||
|
.dim-panel .dim-qrExpired { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; padding: 20px; color: var(--dsw-alias-label-primary, #1f2329); background: rgb(255 255 255 / 92%); font-size: 15px; line-height: 1.6; font-weight: 650; text-align: center; white-space: pre-line; backdrop-filter: blur(3px); }
|
||||||
|
.dim-panel .dim-countdown { width: min(270px, 100%); margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
|
||||||
|
.dim-panel .dim-countdownTop { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
|
||||||
|
.dim-panel .dim-countdownTop strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 650; }
|
||||||
|
.dim-panel .dim-progress { height: 4px; overflow: hidden; margin: 0; border-radius: 99px; background: var(--dsw-alias-fill-secondary, #eef0f3); }
|
||||||
|
.dim-panel .dim-progress span { display: block; width: var(--bxf-progress, var(--dxw-progress, var(--ddt-progress, 0%))); height: 100%; border-radius: inherit; background: #1677ff; transition: width .25s linear; }
|
||||||
|
.dim-panel .dim-qrCopy { min-width: 0; overflow-wrap: anywhere; }
|
||||||
|
.dim-panel .dim-qrCopy h3 { margin: 9px 0 8px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 18px; line-height: 1.35; font-weight: 650; }
|
||||||
|
.dim-panel .dim-qrCopy > p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.65; }
|
||||||
|
.dim-panel .dim-steps { margin: 18px 0 16px; padding: 0; list-style: none; counter-reset: dim-step; }
|
||||||
|
.dim-panel .dim-steps li { position: relative; min-height: 28px; display: flex; align-items: center; padding: 5px 0 5px 36px; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.5; counter-increment: dim-step; }
|
||||||
|
.dim-panel .dim-steps li::before { content: counter(dim-step); position: absolute; left: 0; top: 4px; width: 25px; height: 25px; display: grid; place-items: center; border-radius: 8px; color: #1677ff; background: color-mix(in srgb, #1677ff 10%, white); font-size: 12px; font-weight: 650; }
|
||||||
|
.dim-panel .dim-specialView { padding: 32px; text-align: center; }
|
||||||
|
.dim-panel .dim-statusNotice { display: flex; align-items: flex-start; gap: 10px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 22%, white); border-radius: 10px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 6%, white); font-size: 13px; line-height: 1.5; }
|
||||||
|
.dim-panel .dim-inlineError { display: flex; align-items: flex-start; flex-direction: column; gap: 10px; padding: 22px; color: var(--dsw-alias-state-error-primary, #d54941); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 6%, white); }
|
||||||
|
.dim-panel .dim-inlineError > div { min-width: 0; }
|
||||||
|
.dim-panel .dim-inlineError h3 { margin: 0; color: inherit; font-size: 17px; line-height: 1.35; font-weight: 650; }
|
||||||
|
.dim-panel .dim-inlineError p { margin: 7px 0 0; color: inherit; line-height: 1.6; }
|
||||||
|
.dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
|
||||||
|
.dim-panel .dim-confirm strong, .dim-panel .dim-confirm h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 1.4; font-weight: 650; }
|
||||||
|
.dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
|
||||||
|
.dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-line-divider, #eef0f3); }
|
||||||
|
.dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
|
||||||
|
.dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
|
||||||
|
.dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
|
||||||
|
.dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||||
|
.dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
|
||||||
|
.dim-panel .dim-botCard::before { display: none; }
|
||||||
|
.dim-panel .dim-botCardBody { position: relative; padding: 24px; }
|
||||||
|
.dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||||
|
.dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
|
||||||
|
.dim-panel .dim-botAvatar { flex: none; width: 42px; height: 42px; display: grid; place-items: center; overflow: hidden; border-radius: 12px; box-shadow: none; }
|
||||||
|
.dim-panel .dim-botAvatar svg { width: 29px; height: 29px; }
|
||||||
|
.dim-panel .dim-botName { min-width: 0; }
|
||||||
|
.dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-botCard .dim-healthDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: #aeb3bb; box-shadow: none; }
|
||||||
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
|
||||||
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warning-primary, #d97706); }
|
||||||
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
|
||||||
|
.dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 20px 0; }
|
||||||
|
.dim-panel .dim-botMetric { min-width: 0; padding: 12px; border: 0; border-radius: 9px; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
|
||||||
|
.dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
|
||||||
|
.dim-panel .dim-botMetric dd { overflow: hidden; margin: 5px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
|
||||||
.dim-panel .ddt-headingCopy { display: none; }
|
.dim-panel .ddt-headingCopy { display: none; }
|
||||||
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
||||||
@container (max-width: 680px) {
|
@container (max-width: 680px) {
|
||||||
|
.dim-panel .dim-emptyView { min-height: 0; grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.dim-panel .dim-emptyBrand { display: none; }
|
||||||
|
.dim-panel .dim-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 24px; }
|
||||||
|
.dim-panel .dim-qrColumn { width: 100%; min-width: 0; }
|
||||||
|
.dim-panel .dim-qrCopy { width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
||||||
.dim-panel .ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 24px; }
|
.dim-panel .ddt-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 24px; }
|
||||||
.dim-panel .ddt-qrColumn { width: 100%; min-width: 0; }
|
.dim-panel .ddt-qrColumn { width: 100%; min-width: 0; }
|
||||||
.dim-panel .ddt-qrCopy { width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
.dim-panel .ddt-qrCopy { width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
||||||
|
|
@ -51,6 +141,9 @@ const CSS = String.raw`
|
||||||
.dim-rail { max-height: none; overflow: visible; padding-right: 1px; }
|
.dim-rail { max-height: none; overflow: visible; padding-right: 1px; }
|
||||||
.dim-channel { min-height: 48px; }
|
.dim-channel { min-height: 48px; }
|
||||||
}
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.dim-panel .dim-botCardTop { flex-direction: column; align-items: stretch; }
|
||||||
|
}
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.dim-title { flex-direction: column; gap: 10px; }
|
.dim-title { flex-direction: column; gap: 10px; }
|
||||||
.dim-title p { white-space: normal; }
|
.dim-title p { white-space: normal; }
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,40 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
|
|
||||||
import { IMSettingsTab } from '../plugin-src/client/index.js';
|
import { IMSettingsTab } from '../plugin-src/client/index.js';
|
||||||
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
|
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
|
||||||
import { AccountCard as DingtalkAccountCard } from '../plugin-src/client/channels/dingtalk/index.js';
|
import {
|
||||||
|
AccountCard as DingtalkAccountCard,
|
||||||
|
DingtalkSettingsTab,
|
||||||
|
} from '../plugin-src/client/channels/dingtalk/index.js';
|
||||||
import {
|
import {
|
||||||
BotCard as FeishuBotCard,
|
BotCard as FeishuBotCard,
|
||||||
FeishuSettingsTab,
|
FeishuSettingsTab,
|
||||||
} from '../plugin-src/client/channels/feishu/index.js';
|
} from '../plugin-src/client/channels/feishu/index.js';
|
||||||
import { AccountCard as WeixinAccountCard } from '../plugin-src/client/channels/weixin/index.js';
|
import {
|
||||||
|
AccountCard as WeixinAccountCard,
|
||||||
|
WeixinSettingsTab,
|
||||||
|
} from '../plugin-src/client/channels/weixin/index.js';
|
||||||
|
|
||||||
const STYLES_URL = new URL('../plugin-src/client/styles.js', import.meta.url);
|
const STYLES_URL = new URL('../plugin-src/client/styles.js', import.meta.url);
|
||||||
const FEISHU_STYLES_URL = new URL(
|
const FEISHU_STYLES_URL = new URL(
|
||||||
'../plugin-src/client/channels/feishu/styles.js',
|
'../plugin-src/client/channels/feishu/styles.js',
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
);
|
);
|
||||||
|
const WEIXIN_STYLES_URL = new URL(
|
||||||
|
'../plugin-src/client/channels/weixin/styles.js',
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
const DINGTALK_STYLES_URL = new URL(
|
||||||
|
'../plugin-src/client/channels/dingtalk/styles.js',
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
const FEISHU_SOURCE_URL = new URL(
|
||||||
|
'../plugin-src/client/channels/feishu/index.js',
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
const WEIXIN_SOURCE_URL = new URL(
|
||||||
|
'../plugin-src/client/channels/weixin/index.js',
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
const CLIENT_BUNDLE_URL = new URL('../lib/client.js', import.meta.url);
|
const CLIENT_BUNDLE_URL = new URL('../lib/client.js', import.meta.url);
|
||||||
const DINGTALK_CLIENT_SOURCE_URL = new URL(
|
const DINGTALK_CLIENT_SOURCE_URL = new URL(
|
||||||
'../plugin-src/client/channels/dingtalk/index.js',
|
'../plugin-src/client/channels/dingtalk/index.js',
|
||||||
|
|
@ -86,26 +108,122 @@ test('Feishu bot cards place the application identifier under the bot name', asy
|
||||||
|
|
||||||
assert.match(markup, /<h3[^>]*>今天是牢梁<\/h3><p[^>]*>cli_aaf4••••1234<\/p>/);
|
assert.match(markup, /<h3[^>]*>今天是牢梁<\/h3><p[^>]*>cli_aaf4••••1234<\/p>/);
|
||||||
assert.match(markup, /data-im-channel-logo="feishu"/);
|
assert.match(markup, /data-im-channel-logo="feishu"/);
|
||||||
|
assert.match(markup, /class="bxf-card bxf-botCard dim-botCard"/);
|
||||||
|
assert.match(markup, /class="bxf-healthPill dim-botHealth"/);
|
||||||
|
assert.match(markup, /<button[^>]*aria-label="检查连接今天是牢梁"[^>]*><span>检查连接<\/span><\/button>/);
|
||||||
|
assert.match(markup, /class="bxf-connectedFooter dim-cardFooter"/);
|
||||||
|
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
||||||
|
assert.doesNotMatch(markup, /连接状态:|bxf-divider/);
|
||||||
assert.doesNotMatch(markup, /custom-bot-avatar/);
|
assert.doesNotMatch(markup, /custom-bot-avatar/);
|
||||||
assert.equal((markup.match(/class="bxf-metric"/g) ?? []).length, 2);
|
assert.equal((markup.match(/class="bxf-metric dim-botMetric"/g) ?? []).length, 2);
|
||||||
assert.match(markup, />消息通道<[^]*>最近检查</);
|
assert.match(markup, />消息通道<[^]*>最近检查</);
|
||||||
assert.doesNotMatch(markup, />应用标识<|>飞书机器人</);
|
assert.doesNotMatch(markup, />应用标识<|>飞书机器人</);
|
||||||
assert.match(styles, /\.bxf-statusGrid \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
|
assert.match(styles, /\.bxf-statusGrid \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Feishu keeps its three heading controls on one row without a plus icon', async () => {
|
test('Feishu keeps its heading controls on one row without a plus icon', async () => {
|
||||||
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
|
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
|
||||||
const markup = renderToStaticMarkup(React.createElement(FeishuSettingsTab, {
|
const markup = renderToStaticMarkup(React.createElement(FeishuSettingsTab, {
|
||||||
rpcCall: async () => ({ ok: true, value: {} }),
|
rpcCall: async () => ({ ok: true, value: {} }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
assert.match(markup, /class="bxf-button bxf-bindButton"[^>]*><span>扫码绑定机器人<\/span><\/button>/);
|
assert.match(markup, /class="bxf-button bxf-bindButton dim-scanButton"[^>]*><span>扫码绑定机器人<\/span><\/button>/);
|
||||||
assert.doesNotMatch(markup, />添加机器人</);
|
assert.doesNotMatch(markup, />添加机器人</);
|
||||||
assert.match(styles, /\.bxf-headingTools \{[^}]*flex-wrap: nowrap;/);
|
assert.match(styles, /\.bxf-headingTools \{[^}]*justify-content: space-between;[^}]*flex-wrap: nowrap;/);
|
||||||
assert.match(styles, /@container \(max-width: 620px\)[^]*\.bxf-headingTools \{ gap: 6px; \}/);
|
assert.match(styles, /@container \(max-width: 620px\)[^]*\.bxf-headingTools \{ gap: 6px; \}/);
|
||||||
assert.doesNotMatch(styles, /\.bxf-headingTools \.bxf-button \{ margin-left: auto; \}/);
|
assert.doesNotMatch(styles, /\.bxf-headingTools \.bxf-button \{ margin-left: auto; \}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('scan actions align left while online totals align right in every channel', async () => {
|
||||||
|
const [imStyles, feishuStyles, weixinStyles, dingtalkStyles, feishuSource, weixinSource, dingtalkSource] = await Promise.all([
|
||||||
|
readFile(STYLES_URL, 'utf8'),
|
||||||
|
readFile(FEISHU_STYLES_URL, 'utf8'),
|
||||||
|
readFile(WEIXIN_STYLES_URL, 'utf8'),
|
||||||
|
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
||||||
|
readFile(FEISHU_SOURCE_URL, 'utf8'),
|
||||||
|
readFile(WEIXIN_SOURCE_URL, 'utf8'),
|
||||||
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools, \.dim-panel \.dxw-tools, \.dim-panel \.ddt-tools \{[^}]*justify-content: space-between;/);
|
||||||
|
assert.match(feishuStyles, /\.bxf-headingTools \{[^}]*justify-content: space-between;/);
|
||||||
|
assert.match(weixinStyles, /\.dxw-tools \{[^}]*justify-content: space-between;/);
|
||||||
|
assert.match(dingtalkStyles, /\.ddt-tools \{[^}]*justify-content: space-between;/);
|
||||||
|
|
||||||
|
const headingSource = (source) => source.slice(
|
||||||
|
source.indexOf('function Heading'),
|
||||||
|
source.indexOf('function LoadingView'),
|
||||||
|
);
|
||||||
|
const feishuHeading = headingSource(feishuSource);
|
||||||
|
const weixinHeading = headingSource(weixinSource);
|
||||||
|
const dingtalkHeading = headingSource(dingtalkSource);
|
||||||
|
assert.ok(feishuHeading.indexOf('扫码绑定机器人') < feishuHeading.indexOf('bxf-totalBadge'));
|
||||||
|
assert.ok(weixinHeading.indexOf('扫码绑定微信') < weixinHeading.indexOf('dxw-badge'));
|
||||||
|
assert.ok(dingtalkHeading.indexOf('扫码接入钉钉') < dingtalkHeading.indexOf('ddt-badge'));
|
||||||
|
|
||||||
|
for (const heading of [feishuHeading, weixinHeading, dingtalkHeading]) {
|
||||||
|
assert.match(heading, /dim-scanButton/);
|
||||||
|
assert.match(heading, /dim-onlineBadge/);
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(weixinHeading, /dxw-dot/);
|
||||||
|
assert.doesNotMatch(dingtalkHeading, /ddt-dot/);
|
||||||
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-scanButton,[^}]*border: 1px solid #1677ff;[^}]*border-radius: 8px;[^}]*background: #1677ff;[^}]*box-shadow: none;/);
|
||||||
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-onlineBadge,[^}]*border-radius: 999px;[^}]*background: var\(--dsw-alias-fill-secondary, #f2f3f5\);[^}]*font-size: 12px;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('channel headings omit the redundant local credential badge', () => {
|
||||||
|
const components = [FeishuSettingsTab, WeixinSettingsTab, DingtalkSettingsTab];
|
||||||
|
|
||||||
|
for (const Component of components) {
|
||||||
|
const markup = renderToStaticMarkup(React.createElement(Component, {
|
||||||
|
rpcCall: async () => ({ ok: true, value: {} }),
|
||||||
|
}));
|
||||||
|
assert.doesNotMatch(markup, /凭据仅保存在本机/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all channel settings states use the DingTalk page treatment', async () => {
|
||||||
|
const [styles, feishuSource, weixinSource, dingtalkSource] = await Promise.all([
|
||||||
|
readFile(STYLES_URL, 'utf8'),
|
||||||
|
readFile(FEISHU_SOURCE_URL, 'utf8'),
|
||||||
|
readFile(WEIXIN_SOURCE_URL, 'utf8'),
|
||||||
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const Component of [FeishuSettingsTab, WeixinSettingsTab, DingtalkSettingsTab]) {
|
||||||
|
const markup = renderToStaticMarkup(React.createElement(Component, {
|
||||||
|
rpcCall: async () => ({ ok: true, value: {} }),
|
||||||
|
}));
|
||||||
|
assert.match(markup, /dim-channelPage/);
|
||||||
|
assert.match(markup, /dim-surfaceCard dim-loadingView/);
|
||||||
|
assert.match(markup, /dim-spinner/);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of [feishuSource, weixinSource, dingtalkSource]) {
|
||||||
|
for (const className of [
|
||||||
|
'dim-channelPage',
|
||||||
|
'dim-surfaceCard',
|
||||||
|
'dim-loadingView',
|
||||||
|
'dim-emptyView',
|
||||||
|
'dim-qrLayout',
|
||||||
|
'dim-inlineError',
|
||||||
|
'dim-listHeading',
|
||||||
|
'dim-confirm',
|
||||||
|
]) {
|
||||||
|
assert.match(source, new RegExp(className));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-channelPage \{[^}]*flex-direction: column;[^}]*gap: 18px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-surfaceCard \{[^}]*border-radius: 14px;[^}]*box-shadow: 0 1px 2px/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-loadingView \{[^}]*padding: 38px;[^}]*text-align: center;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-emptyView \{[^}]*grid-template-columns: minmax\(0, 1fr\) 180px;[^}]*gap: 30px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-qrLayout \{[^}]*grid-template-columns: 300px minmax\(0, 1fr\);[^}]*gap: 34px;[^}]*align-items: start;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-viewActions \.bxf-button,[^}]*min-height: 34px;[^}]*border-radius: 8px;[^}]*font-size: 13px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-inlineError \{[^}]*padding: 22px;[^}]*background:/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-confirm \{[^}]*padding: 18px 24px;[^}]*border-top: 1px solid/);
|
||||||
|
});
|
||||||
|
|
||||||
test('bot cards reuse the same channel brand logos as the channel rail', () => {
|
test('bot cards reuse the same channel brand logos as the channel rail', () => {
|
||||||
const railMarkup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
|
const railMarkup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
|
||||||
feishuRpcCall: async () => ({ ok: true, value: {} }),
|
feishuRpcCall: async () => ({ ok: true, value: {} }),
|
||||||
|
|
@ -129,8 +247,12 @@ test('bot cards reuse the same channel brand logos as the channel rail', () => {
|
||||||
|
|
||||||
assert.match(railMarkup, /data-im-channel-logo="weixin"/);
|
assert.match(railMarkup, /data-im-channel-logo="weixin"/);
|
||||||
assert.match(railMarkup, /data-im-channel-logo="feishu"/);
|
assert.match(railMarkup, /data-im-channel-logo="feishu"/);
|
||||||
assert.match(accountMarkup, /class="dxw-avatar"[^]*data-im-channel-logo="weixin"/);
|
assert.match(accountMarkup, /class="dxw-card dim-botCard"/);
|
||||||
assert.equal((accountMarkup.match(/class="dxw-metric"/g) ?? []).length, 2);
|
assert.match(accountMarkup, /class="dxw-avatar dim-botAvatar"[^]*data-im-channel-logo="weixin"/);
|
||||||
|
assert.match(accountMarkup, /class="dxw-health dim-botHealth"/);
|
||||||
|
assert.match(accountMarkup, /class="dxw-accountFooter dim-cardFooter"/);
|
||||||
|
assert.equal((accountMarkup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
||||||
|
assert.equal((accountMarkup.match(/class="dxw-metric dim-botMetric"/g) ?? []).length, 2);
|
||||||
assert.doesNotMatch(accountMarkup, /收到 \/ 回复/);
|
assert.doesNotMatch(accountMarkup, /收到 \/ 回复/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -150,11 +272,48 @@ test('DingTalk bot cards omit the redundant received and replied metric', () =>
|
||||||
onCancelRemove() {},
|
onCancelRemove() {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
assert.equal((markup.match(/class="ddt-metric"/g) ?? []).length, 2);
|
assert.match(markup, /class="ddt-card dim-botCard"/);
|
||||||
|
assert.match(markup, /class="ddt-health dim-botHealth"/);
|
||||||
|
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
|
||||||
|
assert.match(markup, /class="ddt-accountFooter dim-cardFooter"/);
|
||||||
|
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
||||||
assert.match(markup, />消息通道<[^]*>最近检查</);
|
assert.match(markup, />消息通道<[^]*>最近检查</);
|
||||||
assert.doesNotMatch(markup, /收到 \/ 回复/);
|
assert.doesNotMatch(markup, /收到 \/ 回复/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('all channel card action buttons stay on one row', async () => {
|
||||||
|
const [imStyles, feishuStyles, weixinStyles, dingtalkStyles] = await Promise.all([
|
||||||
|
readFile(STYLES_URL, 'utf8'),
|
||||||
|
readFile(FEISHU_STYLES_URL, 'utf8'),
|
||||||
|
readFile(WEIXIN_STYLES_URL, 'utf8'),
|
||||||
|
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(feishuStyles, /\.bxf-botActions \{[^}]*flex-wrap: nowrap;/);
|
||||||
|
assert.match(weixinStyles, /\.dxw-accountFooter \.dxw-actions \{[^}]*flex-wrap: nowrap;/);
|
||||||
|
assert.match(dingtalkStyles, /\.ddt-accountFooter \.ddt-actions \{[^}]*flex-wrap: nowrap;/);
|
||||||
|
assert.match(imStyles, /\.dim-panel \.dim-cardFooter \{[^}]*gap: 15px;[^}]*padding-top: 16px;[^}]*border-top: 1px solid/);
|
||||||
|
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction \{[^}]*min-height: 34px;[^}]*border-radius: 8px;[^}]*font-size: 13px;/);
|
||||||
|
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction\[data-kind="danger"\] \{[^}]*#d54941/);
|
||||||
|
assert.doesNotMatch(feishuStyles, /\.bxf-connectedFooter \{[^}]*flex-direction: column/);
|
||||||
|
assert.doesNotMatch(weixinStyles, /\.dxw-accountFooter \{[^}]*flex-direction: column/);
|
||||||
|
assert.doesNotMatch(dingtalkStyles, /\.ddt-accountFooter \{[^}]*flex-direction: column/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all channel bot cards use the DingTalk card treatment', async () => {
|
||||||
|
const styles = await readFile(STYLES_URL, 'utf8');
|
||||||
|
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botCard \{[^}]*border-radius: 14px;[^}]*background: var\(--dsw-alias-bg-body, #fff\);[^}]*box-shadow: 0 1px 2px/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botCardBody \{[^}]*padding: 24px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botCardTop \{[^}]*align-items: flex-start;[^}]*gap: 16px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botAvatar \{[^}]*width: 42px;[^}]*height: 42px;[^}]*border-radius: 12px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botName h3 \{[^}]*font-size: 15px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botCard \.dim-botHealth \{[^}]*background: transparent;[^}]*font-size: 12px;[^}]*font-weight: 400;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botMetrics \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);[^}]*gap: 10px;[^}]*margin: 20px 0;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botMetric \{[^}]*padding: 12px;[^}]*border: 0;[^}]*border-radius: 9px;/);
|
||||||
|
assert.match(styles, /\.dim-panel \.dim-botMetric dd \{[^}]*margin: 5px 0 0;[^}]*font-size: 13px;[^}]*font-weight: 400;/);
|
||||||
|
});
|
||||||
|
|
||||||
test('the bundled DingTalk channel has no local sender approval workflow', async () => {
|
test('the bundled DingTalk channel has no local sender approval workflow', async () => {
|
||||||
const [source, bundle] = await Promise.all([
|
const [source, bundle] = await Promise.all([
|
||||||
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue