mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
Add manual IM bot credential access
This commit is contained in:
parent
d5d2c18ce0
commit
7123fce47a
42 changed files with 2458 additions and 889 deletions
|
|
@ -5,6 +5,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
|||
beginProvisioning: 'provision.begin',
|
||||
pollProvisioning: 'provision.poll',
|
||||
cancelProvisioning: 'provision.cancel',
|
||||
bindCredentials: 'bot.bind-credentials',
|
||||
reconnectBot: 'bot.reconnect',
|
||||
deleteBot: 'bot.delete',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import {
|
||||
DINGTALK_ENDPOINTS,
|
||||
DINGTALK_RPC_CHANNEL,
|
||||
|
|
@ -46,20 +47,30 @@ const Button = React.forwardRef(function Button(
|
|||
}, children);
|
||||
});
|
||||
|
||||
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||
function Heading({ totals, adding, busy, onAdd, onCredential, credentialOpen, addButtonRef }) {
|
||||
return h('div', { className: 'ddt-heading' },
|
||||
h('div', { className: 'ddt-headingCopy' },
|
||||
h('div', { className: 'ddt-eyebrow' }, 'Channel'),
|
||||
h('h2', null, '钉钉机器人'),
|
||||
h('p', null, '通过扫码把钉钉机器人接入 DeepSeek Harness')),
|
||||
h('div', { className: 'ddt-tools' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
}, adding ? '正在接入' : '扫码接入钉钉'),
|
||||
h('div', { className: 'dim-bindActions' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
'aria-label': '扫码接入钉钉机器人',
|
||||
}, h(QrActionIcon), adding ? '正在接入' : '扫码接入机器人'),
|
||||
h(Button, {
|
||||
kind: 'credential',
|
||||
className: 'dim-credentialButton',
|
||||
onClick: onCredential,
|
||||
disabled: adding || busy,
|
||||
'aria-pressed': credentialOpen,
|
||||
'aria-label': '使用 Client ID 和 Client Secret 绑定钉钉机器人',
|
||||
}, h(CredentialActionIcon), credentialOpen ? '收起凭据' : '手动接入')),
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
|
|
@ -204,6 +215,7 @@ export function AccountCard({
|
|||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
||||
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
||||
return h('article', { className: 'ddt-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
|
||||
h('div', { className: 'ddt-cardBody dim-botCardBody' },
|
||||
h('div', { className: 'ddt-accountTop dim-botCardTop' },
|
||||
|
|
@ -220,7 +232,7 @@ export function AccountCard({
|
|||
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
||||
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'ddt-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
||||
h('div', { className: 'ddt-actions dim-cardActions' },
|
||||
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
||||
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||
|
|
@ -237,7 +249,7 @@ export function AccountCard({
|
|||
function AccountList(props) {
|
||||
return h('section', { className: 'dim-listSection' },
|
||||
h('div', { className: 'ddt-listHeading dim-listHeading' },
|
||||
h('h3', null, '已接入的钉钉机器人'), h('span', null, `${props.bots.length} 个`)),
|
||||
h('h3', null, '已接入的钉钉机器人')),
|
||||
h('ul', { className: 'ddt-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||
h(AccountCard, {
|
||||
account,
|
||||
|
|
@ -260,6 +272,8 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|||
const [busy, setBusy] = React.useState(false);
|
||||
const [busyByBot, setBusyByBot] = React.useState({});
|
||||
const [removeTarget, setRemoveTarget] = React.useState(null);
|
||||
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
||||
const [credentialError, setCredentialError] = React.useState(null);
|
||||
const [notice, setNotice] = React.useState('');
|
||||
const [now, setNow] = React.useState(() => Date.now());
|
||||
const addButtonRef = React.useRef(null);
|
||||
|
|
@ -397,6 +411,8 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|||
|
||||
const startProvisioning = React.useCallback(async ({ replace = false } = {}) => {
|
||||
if (!mountedRef.current) return;
|
||||
setCredentialOpen(false);
|
||||
setCredentialError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
if (replace && provision?.attemptId) {
|
||||
|
|
@ -432,6 +448,32 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|||
}
|
||||
}, [announce, invoke, provision?.attemptId]);
|
||||
|
||||
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
||||
if (!mountedRef.current) return;
|
||||
setBusy(true);
|
||||
setCredentialError(null);
|
||||
try {
|
||||
const snapshot = normalizeSnapshot(await invoke(
|
||||
DINGTALK_ENDPOINTS.bindCredentials,
|
||||
{ clientId: identity, clientSecret: secret },
|
||||
));
|
||||
if (!mountedRef.current) return;
|
||||
setModel({
|
||||
phase: 'ready',
|
||||
bots: snapshot.bots,
|
||||
totals: snapshot.totals,
|
||||
revision: snapshot.revision,
|
||||
error: null,
|
||||
});
|
||||
setCredentialOpen(false);
|
||||
announce('钉钉机器人凭据已绑定。');
|
||||
} catch (error) {
|
||||
if (mountedRef.current) setCredentialError(presentError(error));
|
||||
} finally {
|
||||
if (mountedRef.current) setBusy(false);
|
||||
}
|
||||
}, [announce, invoke]);
|
||||
|
||||
const cancelProvisioning = React.useCallback(async () => {
|
||||
if (!mountedRef.current) return;
|
||||
setBusy(true);
|
||||
|
|
@ -598,12 +640,28 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|||
});
|
||||
}
|
||||
|
||||
const credentialView = credentialOpen
|
||||
? h(CredentialBindingPanel, {
|
||||
channel: '钉钉',
|
||||
identityLabel: 'Client ID',
|
||||
identityPlaceholder: '填写钉钉应用 Client ID',
|
||||
secretLabel: 'Client Secret',
|
||||
secretPlaceholder: '填写钉钉应用 Client Secret',
|
||||
busy,
|
||||
error: credentialError,
|
||||
onSubmit: bindCredentials,
|
||||
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
||||
})
|
||||
: null;
|
||||
|
||||
return h('section', { className: 'ddt-page dim-channelPage', 'aria-label': '钉钉设置' },
|
||||
h(Heading, {
|
||||
totals: model.totals,
|
||||
adding: Boolean(provision),
|
||||
busy,
|
||||
onAdd: () => void startProvisioning(),
|
||||
onCredential: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
|
||||
credentialOpen,
|
||||
addButtonRef,
|
||||
}),
|
||||
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
||||
|
|
@ -619,8 +677,9 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|||
h('p', null, model.error?.message ?? '请稍后重试'),
|
||||
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
||||
: h(React.Fragment, null,
|
||||
credentialView,
|
||||
provisionView,
|
||||
model.bots.length === 0 && !provision
|
||||
model.bots.length === 0 && !provision && !credentialOpen
|
||||
? h(EmptyView, { busy, onStart: () => void startProvisioning() })
|
||||
: null,
|
||||
model.bots.length > 0
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ const CSS = String.raw`
|
|||
.ddt-errorCode { font: 11px ui-monospace, SFMono-Regular, monospace; opacity: .8; }
|
||||
.ddt-listHeading { display: flex; align-items: center; justify-content: space-between; margin: 2px 0 9px; }
|
||||
.ddt-listHeading h3 { margin: 0; font-size: 14px; }
|
||||
.ddt-listHeading span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; }
|
||||
.ddt-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
||||
.ddt-accountTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.ddt-accountIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|||
beginProvisioning: "provision.begin",
|
||||
pollProvisioning: "provision.poll",
|
||||
cancelProvisioning: "provision.cancel",
|
||||
bindCredentials: "bot.bind-credentials",
|
||||
reconnectBot: "bot.reconnect",
|
||||
disconnectBot: "bot.disconnect",
|
||||
deleteBot: "bot.delete",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { FeishuLogoGlyph } from "../../channel-logos.js";
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from "../../credential-binding.js";
|
||||
import {
|
||||
FEISHU_ENDPOINTS,
|
||||
FEISHU_RPC_CHANNEL,
|
||||
|
|
@ -125,19 +126,32 @@ function BrandMark() {
|
|||
return h("div", { className: "bxf-brandMark" }, h(RobotIcon, { size: 34 }));
|
||||
}
|
||||
|
||||
function Heading({ totals, onAdd, adding, busy, addButtonRef }) {
|
||||
function Heading({ totals, onAdd, onCredential, credentialOpen, adding, busy, addButtonRef }) {
|
||||
const hasBots = totals.configured > 0;
|
||||
return h("div", { className: "bxf-heading" },
|
||||
h("div", { className: "bxf-headingTools" },
|
||||
h(Button, {
|
||||
kind: "primary",
|
||||
size: "small",
|
||||
className: "bxf-bindButton dim-scanButton",
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
"aria-busy": busy ? "true" : undefined,
|
||||
}, adding ? "正在绑定" : "扫码绑定机器人"),
|
||||
h("div", { className: "dim-bindActions" },
|
||||
h(Button, {
|
||||
kind: "primary",
|
||||
size: "small",
|
||||
className: "bxf-bindButton dim-scanButton",
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
"aria-busy": busy ? "true" : undefined,
|
||||
"aria-label": "扫码接入飞书机器人",
|
||||
icon: h(QrActionIcon),
|
||||
}, adding ? "正在接入" : "扫码接入机器人"),
|
||||
h(Button, {
|
||||
kind: "credential",
|
||||
size: "small",
|
||||
className: "dim-credentialButton",
|
||||
onClick: onCredential,
|
||||
disabled: adding || busy,
|
||||
"aria-pressed": credentialOpen,
|
||||
"aria-label": "使用 App ID 和 App Secret 绑定飞书机器人",
|
||||
icon: h(CredentialActionIcon),
|
||||
}, credentialOpen ? "收起凭据" : "手动接入")),
|
||||
hasBots
|
||||
? h("div", {
|
||||
className: "bxf-totalBadge dim-onlineBadge",
|
||||
|
|
@ -368,7 +382,7 @@ export function BotCard({
|
|||
: "error";
|
||||
const summary = actionError?.message
|
||||
?? connection.error?.message
|
||||
?? health.summary;
|
||||
?? (connected ? null : health.summary);
|
||||
const titleId = `bxf-bot-${connection.botId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
||||
return h("article", {
|
||||
className: "bxf-card bxf-botCard dim-botCard",
|
||||
|
|
@ -397,8 +411,8 @@ export function BotCard({
|
|||
h("dd", null, formatCheckedTime(health.lastCheckedAt))),
|
||||
),
|
||||
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
||||
h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
||||
summary),
|
||||
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
||||
summary) : null,
|
||||
h("div", { className: "bxf-actions bxf-botActions dim-cardActions" },
|
||||
h(Button, {
|
||||
className: "dim-cardAction", onClick: onReconnect,
|
||||
|
|
@ -426,8 +440,7 @@ export function BotCard({
|
|||
function BotList(props) {
|
||||
return h("section", { className: "bxf-listSection dim-listSection", "aria-labelledby": "bxf-bot-list-title" },
|
||||
h("div", { className: "bxf-listHeading dim-listHeading" },
|
||||
h("h3", { id: "bxf-bot-list-title" }, "已接入的机器人"),
|
||||
h("span", null, `${props.bots.length} 个`)),
|
||||
h("h3", { id: "bxf-bot-list-title" }, "已接入的机器人")),
|
||||
h("ul", { className: "bxf-botList dim-botList", role: "list" },
|
||||
props.bots.map((bot) => h("li", { key: bot.botId },
|
||||
h(BotCard, {
|
||||
|
|
@ -501,6 +514,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
});
|
||||
const [pageBusy, setPageBusy] = React.useState(false);
|
||||
const [provisionBusy, setProvisionBusy] = React.useState(false);
|
||||
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
||||
const [credentialBusy, setCredentialBusy] = React.useState(false);
|
||||
const [credentialError, setCredentialError] = React.useState(null);
|
||||
const [busyByBot, setBusyByBot] = React.useState({});
|
||||
const [errorsByBot, setErrorsByBot] = React.useState({});
|
||||
const [removeTargetId, setRemoveTargetId] = React.useState(null);
|
||||
|
|
@ -589,6 +605,8 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
}, [focusBotId, model.bots]);
|
||||
|
||||
const startProvisioning = React.useCallback(async ({ replace = false } = {}) => {
|
||||
setCredentialOpen(false);
|
||||
setCredentialError(null);
|
||||
setProvisionBusy(true);
|
||||
announce("");
|
||||
const previousAttemptId = model.provisioning?.attemptId;
|
||||
|
|
@ -627,6 +645,24 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
}
|
||||
}, [announce, invoke, model.provisioning?.attemptId]);
|
||||
|
||||
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
||||
setCredentialBusy(true);
|
||||
setCredentialError(null);
|
||||
try {
|
||||
const snapshot = normalizeBotsSnapshot(await invoke(
|
||||
FEISHU_ENDPOINTS.bindCredentials,
|
||||
{ appId: identity, appSecret: secret },
|
||||
));
|
||||
mergeSnapshot(snapshot);
|
||||
setCredentialOpen(false);
|
||||
announce("飞书机器人凭据已绑定。");
|
||||
} catch (error) {
|
||||
setCredentialError(presentError(error));
|
||||
} finally {
|
||||
setCredentialBusy(false);
|
||||
}
|
||||
}, [announce, invoke, mergeSnapshot]);
|
||||
|
||||
const cancelProvisioning = React.useCallback(async () => {
|
||||
const attemptId = model.provisioning?.attemptId;
|
||||
setProvisionBusy(true);
|
||||
|
|
@ -852,6 +888,20 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
});
|
||||
}
|
||||
|
||||
const credentialContent = credentialOpen
|
||||
? h(CredentialBindingPanel, {
|
||||
channel: "飞书",
|
||||
identityLabel: "App ID",
|
||||
identityPlaceholder: "填写飞书开放平台 App ID",
|
||||
secretLabel: "App Secret",
|
||||
secretPlaceholder: "填写飞书开放平台 App Secret",
|
||||
busy: credentialBusy,
|
||||
error: credentialError,
|
||||
onSubmit: bindCredentials,
|
||||
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
||||
})
|
||||
: null;
|
||||
|
||||
const setCardRef = React.useCallback((botId, node) => {
|
||||
if (node) cardRefs.current.set(botId, node);
|
||||
else cardRefs.current.delete(botId);
|
||||
|
|
@ -865,8 +915,10 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
h(Heading, {
|
||||
totals: model.totals,
|
||||
onAdd: () => void startProvisioning(),
|
||||
onCredential: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
|
||||
credentialOpen,
|
||||
adding: Boolean(provision),
|
||||
busy: provisionBusy,
|
||||
busy: provisionBusy || credentialBusy,
|
||||
addButtonRef,
|
||||
}),
|
||||
h("div", {
|
||||
|
|
@ -887,8 +939,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|||
busy: pageBusy,
|
||||
})
|
||||
: h(React.Fragment, null,
|
||||
credentialContent,
|
||||
provisionContent,
|
||||
model.bots.length === 0 && !provision
|
||||
model.bots.length === 0 && !provision && !credentialOpen
|
||||
? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
|
||||
: null,
|
||||
model.bots.length > 0
|
||||
|
|
|
|||
|
|
@ -386,7 +386,6 @@ const CSS = String.raw`
|
|||
.bxf-listSection { display: flex; flex-direction: column; gap: 10px; }
|
||||
.bxf-listHeading { min-height: 28px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 2px; }
|
||||
.bxf-listHeading h3 { font-size: 14px; line-height: 22px; font-weight: 650; margin: 0; }
|
||||
.bxf-listHeading span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; }
|
||||
.bxf-botList { display: flex; flex-direction: column; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
||||
.bxf-botList > li { min-width: 0; }
|
||||
.bxf-botCard:focus { outline: none; }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
|||
beginProvisioning: 'provision.begin',
|
||||
pollProvisioning: 'provision.poll',
|
||||
cancelProvisioning: 'provision.cancel',
|
||||
bindCredentials: 'bot.bind-credentials',
|
||||
reconnectBot: 'bot.reconnect',
|
||||
deleteBot: 'bot.delete',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { QqLogoGlyph } from '../../channel-logos.js';
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
import {
|
||||
QQ_ENDPOINTS,
|
||||
|
|
@ -38,16 +39,26 @@ function checkedTime(value) {
|
|||
}
|
||||
}
|
||||
|
||||
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||
function Heading({ totals, adding, busy, onAdd, onCredential, credentialOpen, addButtonRef }) {
|
||||
return h('div', { className: 'ddt-heading' },
|
||||
h('div', { className: 'ddt-tools' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
}, adding ? '正在绑定' : '扫码绑定QQ机器人'),
|
||||
h('div', { className: 'dim-bindActions' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
'aria-label': '扫码接入 QQ 机器人',
|
||||
}, h(QrActionIcon), adding ? '正在接入' : '扫码接入机器人'),
|
||||
h(Button, {
|
||||
kind: 'credential',
|
||||
className: 'dim-credentialButton',
|
||||
onClick: onCredential,
|
||||
disabled: adding || busy,
|
||||
'aria-pressed': credentialOpen,
|
||||
'aria-label': '使用 AppID 和 AppSecret 绑定 QQ 机器人',
|
||||
}, h(CredentialActionIcon), credentialOpen ? '收起凭据' : '手动接入')),
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
|
|
@ -138,6 +149,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|||
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
const tone = account.connected ? 'success' : account.state === 'error' ? 'error' : 'warning';
|
||||
const stateLabel = account.connected ? '运行正常' : account.state === 'connecting' ? '正在连接' : '连接未就绪';
|
||||
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
||||
return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
|
||||
h('div', { className: 'ddt-cardBody dim-botCardBody' },
|
||||
h('div', { className: 'ddt-accountTop dim-botCardTop' },
|
||||
|
|
@ -151,7 +163,7 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|||
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'), h('dd', null, account.connected ? 'WebSocket 长连接' : '离线')),
|
||||
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'ddt-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
||||
h('div', { className: 'ddt-actions dim-cardActions' },
|
||||
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
||||
|
|
@ -166,6 +178,8 @@ export function QqSettingsTab({ rpcCall }) {
|
|||
const [busy, setBusy] = React.useState(false);
|
||||
const [busyByBot, setBusyByBot] = React.useState({});
|
||||
const [removeTarget, setRemoveTarget] = React.useState(null);
|
||||
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
||||
const [credentialError, setCredentialError] = React.useState(null);
|
||||
const [now, setNow] = React.useState(Date.now());
|
||||
const mounted = React.useRef(true);
|
||||
const addButtonRef = React.useRef(null);
|
||||
|
|
@ -225,6 +239,8 @@ export function QqSettingsTab({ rpcCall }) {
|
|||
}, [provision?.attemptId, provision?.status]);
|
||||
|
||||
const startProvisioning = React.useCallback(async (replace = false) => {
|
||||
setCredentialOpen(false);
|
||||
setCredentialError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
if (replace && provision?.attemptId) await invoke(QQ_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId });
|
||||
|
|
@ -241,6 +257,24 @@ export function QqSettingsTab({ rpcCall }) {
|
|||
}
|
||||
}, [invoke, provision?.attemptId]);
|
||||
|
||||
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
||||
setBusy(true);
|
||||
setCredentialError(null);
|
||||
try {
|
||||
const snapshot = normalizeSnapshot(await invoke(
|
||||
QQ_ENDPOINTS.bindCredentials,
|
||||
{ appId: identity, appSecret: secret },
|
||||
));
|
||||
if (!mounted.current) return;
|
||||
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
||||
setCredentialOpen(false);
|
||||
} catch (error) {
|
||||
if (mounted.current) setCredentialError(presentError(error));
|
||||
} finally {
|
||||
if (mounted.current) setBusy(false);
|
||||
}
|
||||
}, [invoke]);
|
||||
|
||||
const closeProvision = React.useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
|
|
@ -308,7 +342,7 @@ export function QqSettingsTab({ rpcCall }) {
|
|||
const botList = model.bots.length > 0
|
||||
? h('section', { className: 'dim-listSection' },
|
||||
h('div', { className: 'ddt-listHeading dim-listHeading' },
|
||||
h('h3', null, '已绑定的 QQ 机器人'), h('span', null, `${model.bots.length} 个`)),
|
||||
h('h3', null, '已绑定的 QQ 机器人')),
|
||||
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
|
||||
h('li', { key: account.botId }, h(AccountCard, {
|
||||
account,
|
||||
|
|
@ -324,14 +358,38 @@ export function QqSettingsTab({ rpcCall }) {
|
|||
})))))
|
||||
: null;
|
||||
|
||||
const credentialView = credentialOpen
|
||||
? h(CredentialBindingPanel, {
|
||||
channel: 'QQ',
|
||||
identityLabel: 'AppID',
|
||||
identityPlaceholder: '填写 QQ 开放平台 AppID',
|
||||
secretLabel: 'AppSecret',
|
||||
secretPlaceholder: '填写 QQ 开放平台 AppSecret',
|
||||
busy,
|
||||
error: credentialError,
|
||||
onSubmit: bindCredentials,
|
||||
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
||||
})
|
||||
: null;
|
||||
|
||||
return h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
|
||||
h(Heading, { totals: model.totals, adding: Boolean(provision), busy, onAdd: () => void startProvisioning(), addButtonRef }),
|
||||
h(Heading, {
|
||||
totals: model.totals,
|
||||
adding: Boolean(provision),
|
||||
busy,
|
||||
onAdd: () => void startProvisioning(),
|
||||
onCredential: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
|
||||
credentialOpen,
|
||||
addButtonRef,
|
||||
}),
|
||||
model.phase === 'loading' ? h(LoadingView)
|
||||
: model.phase === 'error'
|
||||
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取 QQ 机器人状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
||||
: h(React.Fragment, null,
|
||||
credentialView,
|
||||
provisionView,
|
||||
model.bots.length === 0 && !provision ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
||||
model.bots.length === 0 && !provision && !credentialOpen
|
||||
? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
||||
botList));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
|
|||
beginProvisioning: 'provision.begin',
|
||||
pollProvisioning: 'provision.poll',
|
||||
cancelProvisioning: 'provision.cancel',
|
||||
bindCredentials: 'bot.bind-credentials',
|
||||
reconnectBot: 'bot.reconnect',
|
||||
deleteBot: 'bot.delete',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { WecomLogoGlyph } from '../../channel-logos.js';
|
||||
import { CredentialActionIcon, CredentialBindingPanel, QrActionIcon } from '../../credential-binding.js';
|
||||
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
||||
import {
|
||||
WECOM_ENDPOINTS,
|
||||
|
|
@ -38,16 +39,26 @@ function checkedTime(value) {
|
|||
}
|
||||
}
|
||||
|
||||
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
||||
function Heading({ totals, adding, busy, onAdd, onCredential, credentialOpen, addButtonRef }) {
|
||||
return h('div', { className: 'ddt-heading' },
|
||||
h('div', { className: 'ddt-tools' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
}, adding ? '正在绑定' : '扫码绑定企业微信机器人'),
|
||||
h('div', { className: 'dim-bindActions' },
|
||||
h(Button, {
|
||||
kind: 'primary',
|
||||
className: 'dim-scanButton',
|
||||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
'aria-label': '扫码接入企业微信机器人',
|
||||
}, h(QrActionIcon), adding ? '正在接入' : '扫码接入机器人'),
|
||||
h(Button, {
|
||||
kind: 'credential',
|
||||
className: 'dim-credentialButton',
|
||||
onClick: onCredential,
|
||||
disabled: adding || busy,
|
||||
'aria-pressed': credentialOpen,
|
||||
'aria-label': '使用 Bot ID 和 Secret 绑定企业微信机器人',
|
||||
}, h(CredentialActionIcon), credentialOpen ? '收起凭据' : '手动接入')),
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
|
|
@ -138,6 +149,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|||
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
const tone = account.connected ? 'success' : account.state === 'error' ? 'error' : 'warning';
|
||||
const stateLabel = account.connected ? '运行正常' : account.state === 'connecting' ? '正在连接' : '连接未就绪';
|
||||
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
||||
return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
|
||||
h('div', { className: 'ddt-cardBody dim-botCardBody' },
|
||||
h('div', { className: 'ddt-accountTop dim-botCardTop' },
|
||||
|
|
@ -151,7 +163,7 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|||
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '消息通道'), h('dd', null, account.connected ? 'WebSocket 长连接' : '离线')),
|
||||
h('div', { className: 'ddt-metric dim-botMetric' }, h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'ddt-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
||||
h('div', { className: 'ddt-actions dim-cardActions' },
|
||||
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
|
||||
|
|
@ -166,6 +178,8 @@ export function WecomSettingsTab({ rpcCall }) {
|
|||
const [busy, setBusy] = React.useState(false);
|
||||
const [busyByBot, setBusyByBot] = React.useState({});
|
||||
const [removeTarget, setRemoveTarget] = React.useState(null);
|
||||
const [credentialOpen, setCredentialOpen] = React.useState(false);
|
||||
const [credentialError, setCredentialError] = React.useState(null);
|
||||
const [now, setNow] = React.useState(Date.now());
|
||||
const mounted = React.useRef(true);
|
||||
const addButtonRef = React.useRef(null);
|
||||
|
|
@ -225,6 +239,8 @@ export function WecomSettingsTab({ rpcCall }) {
|
|||
}, [provision?.attemptId, provision?.status]);
|
||||
|
||||
const startProvisioning = React.useCallback(async (replace = false) => {
|
||||
setCredentialOpen(false);
|
||||
setCredentialError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
if (replace && provision?.attemptId) await invoke(WECOM_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId });
|
||||
|
|
@ -241,6 +257,24 @@ export function WecomSettingsTab({ rpcCall }) {
|
|||
}
|
||||
}, [invoke, provision?.attemptId]);
|
||||
|
||||
const bindCredentials = React.useCallback(async ({ identity, secret }) => {
|
||||
setBusy(true);
|
||||
setCredentialError(null);
|
||||
try {
|
||||
const snapshot = normalizeSnapshot(await invoke(
|
||||
WECOM_ENDPOINTS.bindCredentials,
|
||||
{ botId: identity, secret },
|
||||
));
|
||||
if (!mounted.current) return;
|
||||
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
||||
setCredentialOpen(false);
|
||||
} catch (error) {
|
||||
if (mounted.current) setCredentialError(presentError(error));
|
||||
} finally {
|
||||
if (mounted.current) setBusy(false);
|
||||
}
|
||||
}, [invoke]);
|
||||
|
||||
const closeProvision = React.useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
|
|
@ -308,7 +342,7 @@ export function WecomSettingsTab({ rpcCall }) {
|
|||
const botList = model.bots.length > 0
|
||||
? h('section', { className: 'dim-listSection' },
|
||||
h('div', { className: 'ddt-listHeading dim-listHeading' },
|
||||
h('h3', null, '已绑定的企业微信机器人'), h('span', null, `${model.bots.length} 个`)),
|
||||
h('h3', null, '已绑定的企业微信机器人')),
|
||||
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
|
||||
h('li', { key: account.botId }, h(AccountCard, {
|
||||
account,
|
||||
|
|
@ -324,14 +358,38 @@ export function WecomSettingsTab({ rpcCall }) {
|
|||
})))))
|
||||
: null;
|
||||
|
||||
const credentialView = credentialOpen
|
||||
? h(CredentialBindingPanel, {
|
||||
channel: '企业微信',
|
||||
identityLabel: 'Bot ID',
|
||||
identityPlaceholder: '填写企业微信智能机器人 Bot ID',
|
||||
secretLabel: 'Secret',
|
||||
secretPlaceholder: '填写企业微信智能机器人 Secret',
|
||||
busy,
|
||||
error: credentialError,
|
||||
onSubmit: bindCredentials,
|
||||
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
||||
})
|
||||
: null;
|
||||
|
||||
return h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
|
||||
h(Heading, { totals: model.totals, adding: Boolean(provision), busy, onAdd: () => void startProvisioning(), addButtonRef }),
|
||||
h(Heading, {
|
||||
totals: model.totals,
|
||||
adding: Boolean(provision),
|
||||
busy,
|
||||
onAdd: () => void startProvisioning(),
|
||||
onCredential: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
|
||||
credentialOpen,
|
||||
addButtonRef,
|
||||
}),
|
||||
model.phase === 'loading' ? h(LoadingView)
|
||||
: model.phase === 'error'
|
||||
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
||||
: h(React.Fragment, null,
|
||||
credentialView,
|
||||
provisionView,
|
||||
model.bots.length === 0 && !provision ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
||||
model.bots.length === 0 && !provision && !credentialOpen
|
||||
? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
||||
botList));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { WeixinLogoGlyph } from '../../channel-logos.js';
|
||||
import { QrActionIcon } from '../../credential-binding.js';
|
||||
import {
|
||||
WEIXIN_ENDPOINTS,
|
||||
WEIXIN_RPC_CHANNEL,
|
||||
|
|
@ -42,7 +43,8 @@ function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
|||
onClick: onAdd,
|
||||
disabled: adding || busy,
|
||||
ref: addButtonRef,
|
||||
}, adding ? '正在绑定' : '扫码绑定微信'),
|
||||
'aria-label': '扫码接入微信机器人',
|
||||
}, h(QrActionIcon), adding ? '正在接入' : '扫码接入机器人'),
|
||||
totals.configured > 0
|
||||
? h('div', { className: 'dxw-badge dim-onlineBadge' },
|
||||
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
||||
|
|
@ -195,6 +197,7 @@ function checkedTime(timestamp) {
|
|||
export function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
||||
return h('article', { className: 'dxw-card dim-botCard', tabIndex: -1, 'data-bot-id': account.botId },
|
||||
h('div', { className: 'dxw-cardBody dim-botCardBody' },
|
||||
h('div', { className: 'dxw-accountTop dim-botCardTop' },
|
||||
|
|
@ -210,7 +213,7 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|||
h('div', { className: 'dxw-metric dim-botMetric' }, h('dt', null, '最近检查'),
|
||||
h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
||||
h('div', { className: 'dxw-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'dxw-summary dim-cardSummary' }, account.error?.message ?? account.health.summary),
|
||||
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
|
||||
h('div', { className: 'dxw-actions dim-cardActions' },
|
||||
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
|
||||
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
||||
|
|
@ -227,7 +230,7 @@ export function AccountCard({ account, busy, removing, onReconnect, onRequestRem
|
|||
|
||||
function AccountList(props) {
|
||||
return h('section', { className: 'dim-listSection' },
|
||||
h('div', { className: 'dxw-listHeading dim-listHeading' }, h('h3', null, '已接入的微信账号'), h('span', null, `${props.bots.length} 个`)),
|
||||
h('div', { className: 'dxw-listHeading dim-listHeading' }, h('h3', null, '已接入的微信账号')),
|
||||
h('ul', { className: 'dxw-list dim-botList' }, props.bots.map((account) => h('li', { key: account.botId },
|
||||
h(AccountCard, {
|
||||
account,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ const CSS = String.raw`
|
|||
.dxw-errorCode { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 11px; opacity: .8; }
|
||||
.dxw-listHeading { display: flex; justify-content: space-between; align-items: center; margin: 2px 0 9px; }
|
||||
.dxw-listHeading h3 { margin: 0; font-size: 14px; }
|
||||
.dxw-listHeading span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; }
|
||||
.dxw-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
||||
.dxw-accountTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.dxw-accountIdentity { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
|
|
|
|||
113
plugin-src/client/credential-binding.js
Normal file
113
plugin-src/client/credential-binding.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import * as React from 'react';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
function ActionIcon({ children }) {
|
||||
return h('svg', {
|
||||
className: 'dim-actionIcon',
|
||||
width: 15,
|
||||
height: 15,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
'aria-hidden': 'true',
|
||||
focusable: 'false',
|
||||
}, children);
|
||||
}
|
||||
|
||||
export function QrActionIcon() {
|
||||
return h(ActionIcon, null,
|
||||
h('path', {
|
||||
d: 'M2.5 2.5h5v5h-5v-5Zm10 0h5v5h-5v-5Zm-10 10h5v5h-5v-5Z',
|
||||
stroke: 'currentColor', strokeWidth: '1.6', strokeLinejoin: 'round',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M11.5 11.5h2v2h-2v-2Zm4 0h2v3h-2v-3Zm-4 4h3v2h-3v-2Zm5 1h1v1h-1v-1Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
|
||||
export function CredentialActionIcon() {
|
||||
return h(ActionIcon, null,
|
||||
h('circle', {
|
||||
cx: '6.25', cy: '10', r: '3.5', stroke: 'currentColor', strokeWidth: '1.6',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M9.75 10h7.75m-2.5 0v2m-2.5-2v2',
|
||||
stroke: 'currentColor', strokeWidth: '1.6', strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
}));
|
||||
}
|
||||
|
||||
export function CredentialBindingPanel({
|
||||
channel,
|
||||
identityLabel,
|
||||
identityPlaceholder,
|
||||
secretLabel,
|
||||
secretPlaceholder,
|
||||
busy = false,
|
||||
error = null,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) {
|
||||
const [identity, setIdentity] = React.useState('');
|
||||
const [secret, setSecret] = React.useState('');
|
||||
const headingId = React.useId();
|
||||
|
||||
const submit = (event) => {
|
||||
event.preventDefault();
|
||||
const normalizedIdentity = identity.trim();
|
||||
const normalizedSecret = secret.trim();
|
||||
if (!normalizedIdentity || !normalizedSecret || busy) return;
|
||||
void onSubmit?.({ identity: normalizedIdentity, secret: normalizedSecret });
|
||||
};
|
||||
|
||||
return h('section', {
|
||||
className: 'ddt-card dim-surfaceCard dim-credentialPanel',
|
||||
'aria-labelledby': headingId,
|
||||
},
|
||||
h('h3', { id: headingId, className: 'dim-credentialTitle' }, `手动接入${channel}机器人`),
|
||||
h('form', { className: 'dim-credentialForm', onSubmit: submit },
|
||||
h('label', { className: 'dim-credentialField' },
|
||||
h('span', null, identityLabel),
|
||||
h('input', {
|
||||
value: identity,
|
||||
onChange: (event) => setIdentity(event.target.value),
|
||||
placeholder: identityPlaceholder,
|
||||
maxLength: 512,
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
disabled: busy,
|
||||
required: true,
|
||||
})),
|
||||
h('label', { className: 'dim-credentialField' },
|
||||
h('span', null, secretLabel),
|
||||
h('input', {
|
||||
type: 'password',
|
||||
value: secret,
|
||||
onChange: (event) => setSecret(event.target.value),
|
||||
placeholder: secretPlaceholder,
|
||||
maxLength: 1024,
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
autoComplete: 'new-password',
|
||||
disabled: busy,
|
||||
required: true,
|
||||
})),
|
||||
error ? h('p', { className: 'dim-credentialError', role: 'alert' }, error.message ?? String(error)) : null,
|
||||
h('div', { className: 'ddt-actions dim-viewActions dim-credentialActions' },
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
className: 'ddt-button',
|
||||
'data-kind': 'primary',
|
||||
disabled: busy || !identity.trim() || !secret.trim(),
|
||||
}, busy ? '正在绑定…' : '绑定并连接'),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'ddt-button',
|
||||
onClick: onCancel,
|
||||
disabled: busy,
|
||||
}, '取消'))));
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ const CSS = String.raw`
|
|||
.dim-logoWeixin { color: white; background: #07c160; }
|
||||
.dim-logoWeixin svg { width: 19px; height: 19px; }
|
||||
.dim-logoFeishu { background: white; border: 1px solid var(--dsw-alias-line-border, #e5e6eb); }
|
||||
.dim-logoFeishu svg { width: 22px; height: 22px; }
|
||||
.dim-logoFeishu svg { width: 28px; height: 28px; }
|
||||
.dim-logoDingtalk { color: white; background: #1677ff; }
|
||||
.dim-logoDingtalk svg { width: 24px; height: 24px; }
|
||||
.dim-logoQq { color: white; background: #1677ff; }
|
||||
|
|
@ -40,17 +40,31 @@ const CSS = String.raw`
|
|||
.dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-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-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 .dim-bindActions { min-width: 0; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
|
||||
.dim-panel .bxf-headingTools .dim-scanButton, .dim-panel .dxw-tools .dim-scanButton, .dim-panel .ddt-tools .dim-scanButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; 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 .dim-credentialButton { flex: none; min-height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 1px solid #86909c; border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 5%); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
||||
.dim-panel .dim-actionIcon { width: 15px; height: 15px; flex: 0 0 15px; }
|
||||
.dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-fill-tertiary, #f7f8fa); }
|
||||
.dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-fill-secondary, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
|
||||
.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-credentialPanel { display: grid; gap: 18px; padding: 20px; }
|
||||
.dim-panel .dim-credentialTitle { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: 1.35; font-weight: 650; }
|
||||
.dim-panel .dim-credentialForm { min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }
|
||||
.dim-panel .dim-credentialField { min-width: 0; display: grid; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; font-weight: 560; }
|
||||
.dim-panel .dim-credentialField input { width: 100%; min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-line-border, #dfe1e5); border-radius: 8px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-body, #fff); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; transition: border-color .16s ease, box-shadow .16s ease; }
|
||||
.dim-panel .dim-credentialField input:focus { border-color: #4e5969; box-shadow: 0 0 0 3px rgb(78 89 105 / 10%); }
|
||||
.dim-panel .dim-credentialField input::placeholder { color: var(--dsw-alias-label-tertiary, #8f959e); font-family: inherit; }
|
||||
.dim-panel .dim-credentialError, .dim-panel .dim-credentialActions { grid-column: 1 / -1; }
|
||||
.dim-panel .dim-credentialError { margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.5; }
|
||||
.dim-panel .dim-credentialActions { margin-top: 0; }
|
||||
.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; }
|
||||
|
|
@ -101,7 +115,7 @@ const CSS = String.raw`
|
|||
.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 { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
|
||||
.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); }
|
||||
|
|
@ -128,6 +142,8 @@ const CSS = String.raw`
|
|||
.dim-panel .ddt-headingCopy { display: none; }
|
||||
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
||||
@container (max-width: 680px) {
|
||||
.dim-panel .dim-credentialForm { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-panel .dim-credentialError, .dim-panel .dim-credentialActions { grid-column: auto; }
|
||||
.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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue