feat(admin): 后台新增「抖音码速达」配置面板

- 新增 Tab:/admin/platform-shops?tab=douyinMashida
- 面板字段:enabled / baseUrl / uid / secret / certLink / afterVerifyLink /
  afterVerifyMessage / messageType / timeoutMs / handledTopics(tags)
- 密钥脱敏:出参 secret 永远为空,输入框未编辑时对外 value 恒为 '',
  掩码不会污染后端「空串=保留原值」语义
- 顶部展示 ready 状态,未就绪时用 Alert 列出缺哪几项;
  始终展示平台侧要配的推送地址(可复制)
- baseUrl 命中本地地址时提示「当前指向本地网关桩,不会请求真实码速达」
- 把 AdminPlatformShopsPage 内联的 SecretFieldControl 抽成共享组件
  (函数体与原实现逐字节一致,8 处既有调用点参数不变)
This commit is contained in:
yml2213
2026-10-09 12:01:29 +08:00
parent 72606796b3
commit 080480f03b
9 changed files with 721 additions and 153 deletions
@@ -0,0 +1,158 @@
import { EditOutlined, UndoOutlined } from '@ant-design/icons'
import { Button, Input, Space, Tag, Typography } from 'antd'
import { useState } from 'react'
/**
* 脱敏密钥输入控件。
*
* 已配置时只显示掩码 + 「修改」按钮,点开后才出现密码输入框;
* 未编辑时对外 value 恒为 '',避免把掩码值当成新密钥回填提交。
*/
export function SecretFieldControl({
label,
field,
masked,
hasSecret,
value,
onChange,
placeholder,
}: {
label: string
field: string
masked?: string | undefined
hasSecret?: boolean | undefined
value: string
onChange: (value: string) => void
placeholder?: string | undefined
}) {
const [isEditing, setIsEditing] = useState(false)
const [draft, setDraft] = useState('')
const isStaged = Boolean(value && value.trim())
const configured = Boolean(hasSecret || (masked && masked.trim()))
function handleStartEdit() {
setDraft('')
setIsEditing(true)
}
function handleApply() {
const trimmed = draft.trim()
if (trimmed) {
onChange(trimmed)
setIsEditing(false)
setDraft('')
}
}
function handleCancel() {
setDraft('')
setIsEditing(false)
}
function handleRevert() {
onChange('')
setDraft('')
setIsEditing(false)
}
return (
<div style={{ minWidth: 0 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
minHeight: 22,
}}
>
<Typography.Text type="secondary">{label}</Typography.Text>
{isStaged ? (
<Button
type="link"
size="small"
icon={<UndoOutlined />}
style={{ padding: 0, height: 'auto', fontSize: 12 }}
onClick={handleRevert}
>
撤销修改
</Button>
) : null}
</div>
{isEditing ? (
<Space.Compact style={{ width: '100%' }}>
<Input.Password
autoFocus
autoComplete="new-password"
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
data-form-type="other"
name={`secure_field_${field}`}
id={`secure_field_${field}`}
placeholder={placeholder || `输入新 ${label}(留空不修改)`}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onPressEnter={handleApply}
/>
<Button type="primary" onClick={handleApply} disabled={!draft.trim()}>
应用
</Button>
<Button onClick={handleCancel}>取消</Button>
</Space.Compact>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div
style={{
flex: 1,
minWidth: 0,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 11px',
background: '#fafafa',
border: isStaged ? '1px dashed #faad14' : '1px solid #d9d9d9',
borderRadius: 6,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 12,
color: isStaged ? '#d46b08' : configured ? '#262626' : '#8c8c8c',
userSelect: 'none',
overflow: 'hidden',
}}
>
<span
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
marginRight: 8,
}}
>
{isStaged
? '已输入新密钥(待保存)'
: masked || (configured ? '••••••••••••••••' : '未配置')}
</span>
{isStaged ? (
<Tag color="warning" style={{ margin: 0, flexShrink: 0 }}>
待保存
</Tag>
) : configured ? (
<Tag color="success" style={{ margin: 0, flexShrink: 0 }}>
已配置
</Tag>
) : (
<Tag style={{ margin: 0, flexShrink: 0 }}>未配置</Tag>
)}
</div>
<Button icon={<EditOutlined />} onClick={handleStartEdit} style={{ flexShrink: 0 }}>
{configured ? '修改' : '设置'}
</Button>
</div>
)}
</div>
)
}