|
|
|
@@ -71,6 +71,7 @@ import type {
|
|
|
|
|
AdminKuaishouEticketShopInfoResult,
|
|
|
|
|
AdminKuaishouEticketSourceConfig,
|
|
|
|
|
AdminKuaishouIndustryConfigResponse,
|
|
|
|
|
AdminKuaishouIndustryShopConfig,
|
|
|
|
|
AdminKuaishouIndustrySourceConfig,
|
|
|
|
|
AdminKuaishouFeifeiConfig,
|
|
|
|
|
AdminKuaishouFeifeiConfigResponse,
|
|
|
|
@@ -1070,12 +1071,45 @@ function KuaishouIndustryPanel({
|
|
|
|
|
config: AdminKuaishouIndustryConfigResponse
|
|
|
|
|
onChange: (config: AdminKuaishouIndustryConfigResponse) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [searchParams] = useSearchParams()
|
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
|
|
|
const [exchanging, setExchanging] = useState(false)
|
|
|
|
|
const [authorizationCode, setAuthorizationCode] = useState('')
|
|
|
|
|
const [actionLoading, setActionLoading] = useState('')
|
|
|
|
|
const [authorizationCodes, setAuthorizationCodes] = useState<Record<string, string>>({})
|
|
|
|
|
const source = config.source
|
|
|
|
|
const authorizationUrl = buildIndustryAuthorizationUrl(source)
|
|
|
|
|
const shops = source.shops || []
|
|
|
|
|
const enabledShopCount = shops.filter((shop) => shop.enabled !== false).length
|
|
|
|
|
const validTokenCount = shops.filter((shop) => shop.accessTokenStatus === 'valid').length
|
|
|
|
|
const problemTokenCount = shops.filter((shop) =>
|
|
|
|
|
['expired', 'expiring', 'missing'].includes(shop.accessTokenStatus),
|
|
|
|
|
).length
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const code = String(searchParams.get('code') || '').trim()
|
|
|
|
|
if (!code || shops.length === 0) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = String(searchParams.get('state') || '').trim()
|
|
|
|
|
const matchedIndex = state
|
|
|
|
|
? shops.findIndex((shop) =>
|
|
|
|
|
[shop.authState, shop.sellerId, shop.shopId].some((value) => value === state),
|
|
|
|
|
)
|
|
|
|
|
: -1
|
|
|
|
|
const enabledIndex = shops.findIndex((shop) => shop.enabled !== false)
|
|
|
|
|
const targetIndex = matchedIndex >= 0 ? matchedIndex : Math.max(enabledIndex, 0)
|
|
|
|
|
const actionKey = buildIndustryShopActionKey(targetIndex)
|
|
|
|
|
|
|
|
|
|
setAuthorizationCodes((current) => {
|
|
|
|
|
if (current[actionKey] || Object.values(current).some((value) => value.trim() === code)) {
|
|
|
|
|
return current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...current,
|
|
|
|
|
[actionKey]: code,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}, [searchParams, shops])
|
|
|
|
|
|
|
|
|
|
async function saveConfig() {
|
|
|
|
|
setSaving(true)
|
|
|
|
@@ -1090,44 +1124,63 @@ function KuaishouIndustryPanel({
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshToken() {
|
|
|
|
|
setRefreshing(true)
|
|
|
|
|
async function refreshToken(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
|
|
|
|
const sellerId = shop.sellerId.trim()
|
|
|
|
|
if (!sellerId) {
|
|
|
|
|
showError('请先填写 sellerId')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setActionLoading(`refresh-${index}`)
|
|
|
|
|
try {
|
|
|
|
|
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
|
|
|
|
onChange(saved.data)
|
|
|
|
|
const response = await refreshAdminKuaishouIndustryAccessToken()
|
|
|
|
|
const response = await refreshAdminKuaishouIndustryAccessToken({ sellerId })
|
|
|
|
|
onChange(response.data)
|
|
|
|
|
showSuccess('accessToken 已刷新')
|
|
|
|
|
showSuccess(`${resolveIndustryShopDisplayName(shop)} 的 accessToken 已刷新`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showError(error instanceof Error ? error.message : '刷新 accessToken 失败')
|
|
|
|
|
} finally {
|
|
|
|
|
setRefreshing(false)
|
|
|
|
|
setActionLoading('')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function exchangeAuthorizationCode() {
|
|
|
|
|
const code = authorizationCode.trim()
|
|
|
|
|
async function exchangeAuthorizationCode(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
|
|
|
|
const sellerId = shop.sellerId.trim()
|
|
|
|
|
if (!sellerId) {
|
|
|
|
|
showError('请先填写 sellerId')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actionKey = buildIndustryShopActionKey(index)
|
|
|
|
|
const code = String(authorizationCodes[actionKey] || '').trim()
|
|
|
|
|
if (!code) {
|
|
|
|
|
showError('请输入授权 code')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setExchanging(true)
|
|
|
|
|
setActionLoading(`exchange-${index}`)
|
|
|
|
|
try {
|
|
|
|
|
const saved = await saveAdminKuaishouIndustrySourceConfig(source)
|
|
|
|
|
onChange(saved.data)
|
|
|
|
|
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({ code })
|
|
|
|
|
const response = await exchangeAdminKuaishouIndustryAuthorizationCode({
|
|
|
|
|
code,
|
|
|
|
|
sellerId,
|
|
|
|
|
shopName: shop.shopName,
|
|
|
|
|
customShopName: shop.customShopName,
|
|
|
|
|
})
|
|
|
|
|
onChange(response.data)
|
|
|
|
|
setAuthorizationCode('')
|
|
|
|
|
showSuccess('授权 token 已保存')
|
|
|
|
|
setAuthorizationCodes((current) => ({ ...current, [actionKey]: '' }))
|
|
|
|
|
showSuccess(`${resolveIndustryShopDisplayName(shop)} 的授权 token 已保存`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showError(error instanceof Error ? error.message : '授权 code 换 token 失败')
|
|
|
|
|
} finally {
|
|
|
|
|
setExchanging(false)
|
|
|
|
|
setActionLoading('')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyAuthorizationUrl() {
|
|
|
|
|
async function copyAuthorizationUrl(shop: AdminKuaishouIndustryShopConfig) {
|
|
|
|
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, shop)
|
|
|
|
|
if (!authorizationUrl) {
|
|
|
|
|
showError('授权链接未生成')
|
|
|
|
|
return
|
|
|
|
@@ -1141,7 +1194,8 @@ function KuaishouIndustryPanel({
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAuthorizationUrl() {
|
|
|
|
|
function openAuthorizationUrl(shop: AdminKuaishouIndustryShopConfig) {
|
|
|
|
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, shop)
|
|
|
|
|
if (!authorizationUrl) {
|
|
|
|
|
showError('授权链接未生成')
|
|
|
|
|
return
|
|
|
|
@@ -1160,9 +1214,42 @@ function KuaishouIndustryPanel({
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateShop(index: number, patch: Partial<AdminKuaishouIndustryShopConfig>) {
|
|
|
|
|
updateSource({
|
|
|
|
|
shops: shops.map((shop, shopIndex) =>
|
|
|
|
|
shopIndex === index ? { ...shop, ...patch } : shop,
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateShopSellerId(index: number, sellerId: string) {
|
|
|
|
|
const shop = shops[index]
|
|
|
|
|
if (!shop) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateShop(index, {
|
|
|
|
|
sellerId,
|
|
|
|
|
shopId: !shop.shopId || shop.shopId === shop.sellerId ? sellerId : shop.shopId,
|
|
|
|
|
authState: !shop.authState || shop.authState === shop.sellerId ? sellerId : shop.authState,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addShop() {
|
|
|
|
|
updateSource({
|
|
|
|
|
shops: [...shops, createEmptyIndustryShopConfig()],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteShop(index: number) {
|
|
|
|
|
updateSource({
|
|
|
|
|
shops: shops.filter((_, shopIndex) => shopIndex !== index),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSecretInput(
|
|
|
|
|
label: string,
|
|
|
|
|
field: 'appSecret' | 'signSecret' | 'messageSecret' | 'accessToken' | 'refreshToken',
|
|
|
|
|
field: 'appSecret' | 'signSecret' | 'messageSecret',
|
|
|
|
|
masked: string,
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
@@ -1177,7 +1264,123 @@ function KuaishouIndustryPanel({
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accessTokenStatus = resolveIndustryAccessTokenStatus(source)
|
|
|
|
|
const shopColumns: TableColumnsType<AdminKuaishouIndustryShopConfig> = [
|
|
|
|
|
{
|
|
|
|
|
title: '启用',
|
|
|
|
|
width: 76,
|
|
|
|
|
fixed: 'left',
|
|
|
|
|
render: (_, row, index) => (
|
|
|
|
|
<Switch
|
|
|
|
|
checked={row.enabled !== false}
|
|
|
|
|
onChange={(enabled) => updateShop(index, { enabled })}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '店铺',
|
|
|
|
|
minWidth: 260,
|
|
|
|
|
render: (_, row, index) => (
|
|
|
|
|
<div className="cell-stack">
|
|
|
|
|
<Input
|
|
|
|
|
value={row.sellerId}
|
|
|
|
|
placeholder="sellerId"
|
|
|
|
|
onChange={(event) => updateShopSellerId(index, event.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={row.shopName}
|
|
|
|
|
placeholder="官方店铺名称"
|
|
|
|
|
onChange={(event) => updateShop(index, { shopName: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={row.customShopName}
|
|
|
|
|
placeholder="自定义店铺名"
|
|
|
|
|
onChange={(event) => updateShop(index, { customShopName: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Token 状态',
|
|
|
|
|
width: 150,
|
|
|
|
|
render: (_, row) => {
|
|
|
|
|
const status = resolveIndustryAccessTokenStatus(row)
|
|
|
|
|
return (
|
|
|
|
|
<div className="status-stack">
|
|
|
|
|
<Tag color={status.color}>{status.label}</Tag>
|
|
|
|
|
{row.accessTokenMasked ? <Typography.Text type="secondary">{row.accessTokenMasked}</Typography.Text> : null}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '过期时间',
|
|
|
|
|
minWidth: 220,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<div className="cell-stack">
|
|
|
|
|
<span>{row.accessTokenExpiresAt ? formatAdminDateTime(row.accessTokenExpiresAt) : '-'}</span>
|
|
|
|
|
<span className="muted">{formatIndustryTokenCountdown(row.accessTokenExpiresInSeconds)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '授权操作',
|
|
|
|
|
width: 420,
|
|
|
|
|
render: (_, row, index) => {
|
|
|
|
|
const actionKey = buildIndustryShopActionKey(index)
|
|
|
|
|
return (
|
|
|
|
|
<Space direction="vertical" className="full-width" size={8}>
|
|
|
|
|
<Space.Compact className="full-width">
|
|
|
|
|
<Input
|
|
|
|
|
value={authorizationCodes[actionKey] || ''}
|
|
|
|
|
placeholder="授权 code"
|
|
|
|
|
onChange={(event) =>
|
|
|
|
|
setAuthorizationCodes((current) => ({
|
|
|
|
|
...current,
|
|
|
|
|
[actionKey]: event.target.value,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
onPressEnter={() => exchangeAuthorizationCode(row, index)}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
loading={actionLoading === `exchange-${index}`}
|
|
|
|
|
disabled={saving || Boolean(actionLoading && actionLoading !== `exchange-${index}`)}
|
|
|
|
|
onClick={() => exchangeAuthorizationCode(row, index)}
|
|
|
|
|
>
|
|
|
|
|
换取
|
|
|
|
|
</Button>
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Button size="small" icon={<CopyOutlined />} onClick={() => copyAuthorizationUrl(row)}>
|
|
|
|
|
复制授权
|
|
|
|
|
</Button>
|
|
|
|
|
<Button size="small" icon={<LinkOutlined />} onClick={() => openAuthorizationUrl(row)}>
|
|
|
|
|
打开授权
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
loading={actionLoading === `refresh-${index}`}
|
|
|
|
|
disabled={saving || Boolean(actionLoading && actionLoading !== `refresh-${index}`)}
|
|
|
|
|
onClick={() => refreshToken(row, index)}
|
|
|
|
|
>
|
|
|
|
|
刷新
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
danger
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
disabled={saving || Boolean(actionLoading)}
|
|
|
|
|
onClick={() => deleteShop(index)}
|
|
|
|
|
>
|
|
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="platform-panel-stack">
|
|
|
|
@@ -1192,45 +1395,37 @@ function KuaishouIndustryPanel({
|
|
|
|
|
|
|
|
|
|
<div className="metric-grid four">
|
|
|
|
|
<MetricCard
|
|
|
|
|
label="回调状态"
|
|
|
|
|
label="接入状态"
|
|
|
|
|
value={source.enabled ? '已启用' : '停用'}
|
|
|
|
|
detail={source.enabled ? '配置启用' : '配置停用'}
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
label="accessToken"
|
|
|
|
|
value={accessTokenStatus.label}
|
|
|
|
|
detail={formatIndustryTokenCountdown(source.accessTokenExpiresInSeconds)}
|
|
|
|
|
label="店铺授权"
|
|
|
|
|
value={`${enabledShopCount}/${shops.length}`}
|
|
|
|
|
detail="启用店铺 / 全部店铺"
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
label="过期时间"
|
|
|
|
|
value={source.accessTokenExpiresAt ? formatAdminDateTime(source.accessTokenExpiresAt) : '-'}
|
|
|
|
|
detail={source.lastRefreshedAt ? `刷新:${formatAdminDateTime(source.lastRefreshedAt)}` : '尚未刷新'}
|
|
|
|
|
label="有效 token"
|
|
|
|
|
value={String(validTokenCount)}
|
|
|
|
|
detail={problemTokenCount > 0 ? `${problemTokenCount} 个需处理` : '全部正常'}
|
|
|
|
|
/>
|
|
|
|
|
<MetricCard
|
|
|
|
|
label="refreshToken"
|
|
|
|
|
value={source.hasRefreshToken ? '已配置' : '未配置'}
|
|
|
|
|
detail={source.refreshTokenExpiresAt ? `到期:${formatAdminDateTime(source.refreshTokenExpiresAt)}` : source.refreshTokenMasked || '-'}
|
|
|
|
|
label="授权范围"
|
|
|
|
|
value={source.scopes || '-'}
|
|
|
|
|
detail={source.redirectUri || '-'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
title="快手行业电子凭证"
|
|
|
|
|
title="应用参数"
|
|
|
|
|
extra={
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
|
|
|
|
|
<Button
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
loading={refreshing}
|
|
|
|
|
disabled={saving || exchanging}
|
|
|
|
|
onClick={refreshToken}
|
|
|
|
|
>
|
|
|
|
|
保存并刷新 token
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<SaveOutlined />}
|
|
|
|
|
loading={saving}
|
|
|
|
|
disabled={refreshing || exchanging}
|
|
|
|
|
disabled={Boolean(actionLoading)}
|
|
|
|
|
onClick={saveConfig}
|
|
|
|
|
>
|
|
|
|
|
保存配置
|
|
|
|
@@ -1238,12 +1433,6 @@ function KuaishouIndustryPanel({
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Space wrap size={[8, 8]} className="platform-section-gap">
|
|
|
|
|
<Tag color={accessTokenStatus.color}>{accessTokenStatus.label}</Tag>
|
|
|
|
|
{source.accessTokenMasked ? <Tag>{source.accessTokenMasked}</Tag> : null}
|
|
|
|
|
{source.refreshTokenMasked ? <Tag>{source.refreshTokenMasked}</Tag> : null}
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
<div className="platform-form-grid">
|
|
|
|
|
<FieldSwitch
|
|
|
|
|
label="启用配置"
|
|
|
|
@@ -1265,105 +1454,139 @@ function KuaishouIndustryPanel({
|
|
|
|
|
value={source.appKey}
|
|
|
|
|
onChange={(appKey) => updateSource({ appKey })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="回调地址"
|
|
|
|
|
value={source.redirectUri}
|
|
|
|
|
onChange={(redirectUri) => updateSource({ redirectUri })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="scope"
|
|
|
|
|
value={source.scopes}
|
|
|
|
|
onChange={(scopes) => updateSource({ scopes })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="全局 state 兜底"
|
|
|
|
|
value={source.authState}
|
|
|
|
|
onChange={(authState) => updateSource({ authState })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="版本"
|
|
|
|
|
value={source.version}
|
|
|
|
|
onChange={(version) => updateSource({ version })}
|
|
|
|
|
/>
|
|
|
|
|
{renderSecretInput('appSecret', 'appSecret', source.appSecretMasked)}
|
|
|
|
|
{renderSecretInput('signSecret', 'signSecret', source.signSecretMasked)}
|
|
|
|
|
{renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
size="small"
|
|
|
|
|
title="授权"
|
|
|
|
|
className="platform-section-gap"
|
|
|
|
|
extra={
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Button icon={<CopyOutlined />} disabled={!authorizationUrl} onClick={copyAuthorizationUrl}>
|
|
|
|
|
复制链接
|
|
|
|
|
</Button>
|
|
|
|
|
<Button icon={<LinkOutlined />} disabled={!authorizationUrl} onClick={openAuthorizationUrl}>
|
|
|
|
|
打开授权
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<div className="platform-form-grid">
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="回调地址"
|
|
|
|
|
value={source.redirectUri}
|
|
|
|
|
onChange={(redirectUri) => updateSource({ redirectUri })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="scope"
|
|
|
|
|
value={source.scopes}
|
|
|
|
|
onChange={(scopes) => updateSource({ scopes })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="state"
|
|
|
|
|
value={source.authState}
|
|
|
|
|
onChange={(authState) => updateSource({ authState })}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">授权链接</Typography.Text>
|
|
|
|
|
<Input readOnly value={authorizationUrl} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">openId</Typography.Text>
|
|
|
|
|
<Input
|
|
|
|
|
value={source.openId}
|
|
|
|
|
onChange={(event) => updateSource({ openId: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">已授权 scope</Typography.Text>
|
|
|
|
|
<Input
|
|
|
|
|
value={source.grantedScopes}
|
|
|
|
|
onChange={(event) => updateSource({ grantedScopes: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Space.Compact className="full-width platform-section-gap">
|
|
|
|
|
<Input
|
|
|
|
|
value={authorizationCode}
|
|
|
|
|
placeholder="code"
|
|
|
|
|
onChange={(event) => setAuthorizationCode(event.target.value)}
|
|
|
|
|
onPressEnter={exchangeAuthorizationCode}
|
|
|
|
|
/>
|
|
|
|
|
<Card
|
|
|
|
|
title="店铺授权"
|
|
|
|
|
extra={
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Button icon={<PlusOutlined />} onClick={addShop}>
|
|
|
|
|
新增店铺
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
loading={exchanging}
|
|
|
|
|
disabled={saving || refreshing}
|
|
|
|
|
onClick={exchangeAuthorizationCode}
|
|
|
|
|
icon={<SaveOutlined />}
|
|
|
|
|
loading={saving}
|
|
|
|
|
disabled={Boolean(actionLoading)}
|
|
|
|
|
onClick={saveConfig}
|
|
|
|
|
>
|
|
|
|
|
换取 token
|
|
|
|
|
保存店铺
|
|
|
|
|
</Button>
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card size="small" title="Token" className="platform-section-gap">
|
|
|
|
|
<div className="platform-form-grid">
|
|
|
|
|
{renderSecretInput('accessToken', 'accessToken', source.accessTokenMasked)}
|
|
|
|
|
{renderSecretInput('refreshToken', 'refreshToken', source.refreshTokenMasked)}
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="accessToken 过期时间"
|
|
|
|
|
value={source.accessTokenExpiresAt}
|
|
|
|
|
onChange={(accessTokenExpiresAt) => updateSource({ accessTokenExpiresAt })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="refreshToken 过期时间"
|
|
|
|
|
value={source.refreshTokenExpiresAt}
|
|
|
|
|
onChange={(refreshTokenExpiresAt) => updateSource({ refreshTokenExpiresAt })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="sellerId"
|
|
|
|
|
value={source.sellerId}
|
|
|
|
|
onChange={(sellerId) => updateSource({ sellerId })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="版本"
|
|
|
|
|
value={source.version}
|
|
|
|
|
onChange={(version) => updateSource({ version })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{shops.length === 0 ? (
|
|
|
|
|
<Empty description="暂无店铺授权" />
|
|
|
|
|
) : (
|
|
|
|
|
<Table<AdminKuaishouIndustryShopConfig>
|
|
|
|
|
rowKey={(row, index) => buildIndustryShopRowKey(row, index || 0)}
|
|
|
|
|
columns={shopColumns}
|
|
|
|
|
dataSource={shops}
|
|
|
|
|
pagination={false}
|
|
|
|
|
scroll={{ x: 1120 }}
|
|
|
|
|
expandable={{
|
|
|
|
|
expandedRowRender: (row, index) => {
|
|
|
|
|
const authorizationUrl = buildIndustryAuthorizationUrl(source, row)
|
|
|
|
|
return (
|
|
|
|
|
<div className="platform-form-grid">
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="shopId"
|
|
|
|
|
value={row.shopId}
|
|
|
|
|
onChange={(shopId) => updateShop(index, { shopId })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="官方店铺名称"
|
|
|
|
|
value={row.shopName}
|
|
|
|
|
onChange={(shopName) => updateShop(index, { shopName })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="自定义店铺名"
|
|
|
|
|
value={row.customShopName}
|
|
|
|
|
onChange={(customShopName) => updateShop(index, { customShopName })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="state"
|
|
|
|
|
value={row.authState}
|
|
|
|
|
onChange={(authState) => updateShop(index, { authState })}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">授权链接</Typography.Text>
|
|
|
|
|
<Input readOnly value={authorizationUrl} />
|
|
|
|
|
</div>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="openId"
|
|
|
|
|
value={row.openId}
|
|
|
|
|
onChange={(openId) => updateShop(index, { openId })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="已授权 scope"
|
|
|
|
|
value={row.grantedScopes}
|
|
|
|
|
onChange={(grantedScopes) => updateShop(index, { grantedScopes })}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">accessToken</Typography.Text>
|
|
|
|
|
<Input.Password
|
|
|
|
|
value={row.accessToken}
|
|
|
|
|
placeholder={row.accessTokenMasked || '留空保持原值'}
|
|
|
|
|
onChange={(event) => updateShop(index, { accessToken: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">refreshToken</Typography.Text>
|
|
|
|
|
<Input.Password
|
|
|
|
|
value={row.refreshToken}
|
|
|
|
|
placeholder={row.refreshTokenMasked || '留空保持原值'}
|
|
|
|
|
onChange={(event) => updateShop(index, { refreshToken: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="accessToken 过期时间"
|
|
|
|
|
value={row.accessTokenExpiresAt}
|
|
|
|
|
onChange={(accessTokenExpiresAt) => updateShop(index, { accessTokenExpiresAt })}
|
|
|
|
|
/>
|
|
|
|
|
<LabeledInput
|
|
|
|
|
label="refreshToken 过期时间"
|
|
|
|
|
value={row.refreshTokenExpiresAt}
|
|
|
|
|
onChange={(refreshTokenExpiresAt) => updateShop(index, { refreshTokenExpiresAt })}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text type="secondary">最近错误</Typography.Text>
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
|
|
|
|
value={row.lastRefreshError}
|
|
|
|
|
onChange={(event) => updateShop(index, { lastRefreshError: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
@@ -1815,7 +2038,9 @@ function CloudtentaclesPlatformPanel({
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveIndustryAccessTokenStatus(source: AdminKuaishouIndustrySourceConfig) {
|
|
|
|
|
function resolveIndustryAccessTokenStatus(
|
|
|
|
|
source: Pick<AdminKuaishouIndustrySourceConfig, 'accessTokenStatus'>,
|
|
|
|
|
) {
|
|
|
|
|
switch (source.accessTokenStatus) {
|
|
|
|
|
case 'valid':
|
|
|
|
|
return { label: '有效', color: 'green' }
|
|
|
|
@@ -1852,9 +2077,12 @@ function formatIndustryTokenCountdown(value: number | null) {
|
|
|
|
|
return `${minutes} 分钟后过期`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig) {
|
|
|
|
|
function buildIndustryAuthorizationUrl(
|
|
|
|
|
source: AdminKuaishouIndustrySourceConfig,
|
|
|
|
|
shop?: AdminKuaishouIndustryShopConfig,
|
|
|
|
|
) {
|
|
|
|
|
if (!source.authBaseUrl || !source.appKey || !source.redirectUri || !source.scopes) {
|
|
|
|
|
return source.authorizationUrl || ''
|
|
|
|
|
return shop?.authorizationUrl || source.authorizationUrl || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
@@ -1863,16 +2091,55 @@ function buildIndustryAuthorizationUrl(source: AdminKuaishouIndustrySourceConfig
|
|
|
|
|
url.searchParams.set('redirect_uri', source.redirectUri)
|
|
|
|
|
url.searchParams.set('scope', normalizeIndustryScopeText(source.scopes))
|
|
|
|
|
url.searchParams.set('response_type', 'code')
|
|
|
|
|
if (source.authState) {
|
|
|
|
|
url.searchParams.set('state', source.authState)
|
|
|
|
|
const state = String(shop?.authState || source.authState || shop?.sellerId || '').trim()
|
|
|
|
|
if (state) {
|
|
|
|
|
url.searchParams.set('state', state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url.toString()
|
|
|
|
|
} catch {
|
|
|
|
|
return source.authorizationUrl || ''
|
|
|
|
|
return shop?.authorizationUrl || source.authorizationUrl || ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createEmptyIndustryShopConfig(): AdminKuaishouIndustryShopConfig {
|
|
|
|
|
return {
|
|
|
|
|
enabled: true,
|
|
|
|
|
sellerId: '',
|
|
|
|
|
shopId: '',
|
|
|
|
|
shopName: '',
|
|
|
|
|
customShopName: '',
|
|
|
|
|
authState: '',
|
|
|
|
|
authorizationUrl: '',
|
|
|
|
|
accessToken: '',
|
|
|
|
|
accessTokenMasked: '',
|
|
|
|
|
hasAccessToken: false,
|
|
|
|
|
refreshToken: '',
|
|
|
|
|
refreshTokenMasked: '',
|
|
|
|
|
hasRefreshToken: false,
|
|
|
|
|
accessTokenExpiresAt: '',
|
|
|
|
|
refreshTokenExpiresAt: '',
|
|
|
|
|
accessTokenStatus: 'missing',
|
|
|
|
|
accessTokenExpiresInSeconds: null,
|
|
|
|
|
openId: '',
|
|
|
|
|
grantedScopes: '',
|
|
|
|
|
lastRefreshedAt: '',
|
|
|
|
|
lastRefreshError: '',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIndustryShopRowKey(shop: AdminKuaishouIndustryShopConfig, index: number) {
|
|
|
|
|
return shop.sellerId || shop.shopId || `shop-${index}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIndustryShopActionKey(index: number) {
|
|
|
|
|
return `shop-${index}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveIndustryShopDisplayName(shop: AdminKuaishouIndustryShopConfig) {
|
|
|
|
|
return shop.customShopName || shop.shopName || shop.sellerId || '未命名店铺'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeIndustryScopeText(value: string) {
|
|
|
|
|
return value
|
|
|
|
|
.split(/[,\s]+/)
|
|
|
|
|