迁移平台配置页面
This commit is contained in:
@@ -10,6 +10,10 @@ const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')
|
|||||||
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
||||||
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
||||||
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
||||||
|
const AdminPlatformFulfillmentPage = lazy(
|
||||||
|
() => import('@/pages/admin/platform/AdminPlatformFulfillmentPage'),
|
||||||
|
)
|
||||||
|
const AdminPlatformShopsPage = lazy(() => import('@/pages/admin/platform/AdminPlatformShopsPage'))
|
||||||
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
|
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
|
||||||
const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage'))
|
const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage'))
|
||||||
const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage'))
|
const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage'))
|
||||||
@@ -59,8 +63,8 @@ export default function App() {
|
|||||||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||||||
<Route element={<RequireRole roles={['admin']} />}>
|
<Route element={<RequireRole roles={['admin']} />}>
|
||||||
<Route path="users" element={<AdminUsersPage />} />
|
<Route path="users" element={<AdminUsersPage />} />
|
||||||
<Route path="platform-shops" element={<NotFoundPage title="平台配置迁移中" />} />
|
<Route path="platform-shops" element={<AdminPlatformShopsPage />} />
|
||||||
<Route path="platform-fulfillment" element={<NotFoundPage title="履约配置迁移中" />} />
|
<Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} />
|
||||||
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -0,0 +1,873 @@
|
|||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
|
import type { TableColumnsType } from 'antd'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
|
import {
|
||||||
|
fetchAdminCloudtentaclesOverrideRules,
|
||||||
|
fetchAdminCloudtentaclesSkuList,
|
||||||
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
|
matchAdminKuaishouFeifeiProduct,
|
||||||
|
saveAdminCloudtentaclesOverrideRules,
|
||||||
|
syncAdminKuaishouFeifeiProducts,
|
||||||
|
} from '@/services/admin'
|
||||||
|
import type {
|
||||||
|
AdminCloudtentaclesOverrideDeliveryItem,
|
||||||
|
AdminCloudtentaclesOverrideRule,
|
||||||
|
AdminCloudtentaclesSessionItem,
|
||||||
|
AdminCloudtentaclesSessionsMap,
|
||||||
|
AdminCloudtentaclesSkuItem,
|
||||||
|
AdminCloudtentaclesSourceItem,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
AdminKuaishouFeifeiProductRule,
|
||||||
|
} from '@/types/admin'
|
||||||
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
|
|
||||||
|
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
|
||||||
|
normalizedProductName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceRow = AdminCloudtentaclesSourceItem & {
|
||||||
|
session: AdminCloudtentaclesSessionItem | null
|
||||||
|
ready: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPlatformFulfillmentPage() {
|
||||||
|
return (
|
||||||
|
<section className="page-stack">
|
||||||
|
<PageHeader
|
||||||
|
title="履约配置"
|
||||||
|
description="维护 91 卡券商品到 kuaishou-feifei 与 cloudtentacles 的履约映射规则。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!hasAdminRole('admin') ? (
|
||||||
|
<Card>
|
||||||
|
<Empty description="仅管理员可以查看履约配置" />
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
className="platform-tabs"
|
||||||
|
destroyOnHidden={false}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'kuaishou-feifei',
|
||||||
|
label: 'kuaishou-feifei',
|
||||||
|
children: <KuaishouFeifeiFulfillmentPanel />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kuaishou-cloud',
|
||||||
|
label: 'kuaishou-cloud',
|
||||||
|
children: <KuaishouCloudFulfillmentPanel />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KuaishouFeifeiFulfillmentPanel() {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [syncing, setSyncing] = useState(false)
|
||||||
|
const [matching, setMatching] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
const [filePath, setFilePath] = useState('')
|
||||||
|
const [rules, setRules] = useState<AdminKuaishouFeifeiProductRule[]>([])
|
||||||
|
const [matchInput, setMatchInput] = useState('')
|
||||||
|
const [matchResult, setMatchResult] = useState<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||||
|
const [lastSyncText, setLastSyncText] = useState('')
|
||||||
|
|
||||||
|
const enabledRuleCount = rules.filter((rule) => rule.enabled !== false).length
|
||||||
|
const mappingCount = rules.filter(
|
||||||
|
(rule) => rule.productName.trim() && rule.productCode.trim(),
|
||||||
|
).length
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadConfig()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
setLoading(true)
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-feifei 履约映射失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncProducts() {
|
||||||
|
setSyncing(true)
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await syncAdminKuaishouFeifeiProducts({ status: 'on_sale' })
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
const text = `商品 ${response.data.sync.productCount} 个,映射 ${response.data.sync.ruleCount} 条`
|
||||||
|
setLastSyncText(text)
|
||||||
|
showSuccess(`kuaishou-feifei 商品映射已同步,${text}`)
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败'
|
||||||
|
setErrorMessage(message)
|
||||||
|
showError(message)
|
||||||
|
} finally {
|
||||||
|
setSyncing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewMatch() {
|
||||||
|
const productName = matchInput.trim()
|
||||||
|
if (!productName) {
|
||||||
|
showError('请输入 91 商品名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setMatching(true)
|
||||||
|
setMatchResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||||
|
setMatchResult(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败')
|
||||||
|
} finally {
|
||||||
|
setMatching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||||
|
setFilePath(data.filePath || '')
|
||||||
|
setRules(
|
||||||
|
Array.isArray(data.source.productRules)
|
||||||
|
? data.source.productRules.map((rule) => normalizeFeifeiRule(rule))
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumnsType<AdminKuaishouFeifeiProductRule> = [
|
||||||
|
{
|
||||||
|
title: 'feifei 商品名',
|
||||||
|
dataIndex: 'productName',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text strong ellipsis>
|
||||||
|
{row.productName || row.skuName || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" ellipsis>
|
||||||
|
{row.normalizedProductName || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: 'product_code', dataIndex: 'productCode', minWidth: 150 },
|
||||||
|
{ title: '展示名称', dataIndex: 'skuName', minWidth: 220 },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={row.enabled !== false ? 'green' : 'default'}>
|
||||||
|
{row.enabled !== false ? '启用' : '停用'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '来源', dataIndex: 'notes', minWidth: 180 },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="platform-panel-stack">
|
||||||
|
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
||||||
|
|
||||||
|
<div className="metric-grid four">
|
||||||
|
<MetricCard label="名字映射" value={String(mappingCount)} detail={`启用 ${enabledRuleCount} 条`} />
|
||||||
|
<MetricCard label="匹配方式" value="同名" detail="使用 91 商品名规范化匹配" />
|
||||||
|
<MetricCard
|
||||||
|
label="配置文件"
|
||||||
|
value={filePath ? 'GUI' : '默认'}
|
||||||
|
detail={filePath || 'data/kuaishou-feifei-config.json'}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<Space wrap>
|
||||||
|
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadConfig}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" icon={<ReloadOutlined />} loading={syncing} onClick={syncProducts}>
|
||||||
|
同步商品映射
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="商品名字映射"
|
||||||
|
extra={<Typography.Text type="secondary">{lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。'}</Typography.Text>}
|
||||||
|
>
|
||||||
|
<Space.Compact className="full-width">
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
value={matchInput}
|
||||||
|
placeholder="91 商品名,例如 套装-Alan Walker"
|
||||||
|
onChange={(event) => setMatchInput(event.target.value)}
|
||||||
|
onPressEnter={previewMatch}
|
||||||
|
/>
|
||||||
|
<Button icon={<SearchOutlined />} loading={matching} onClick={previewMatch}>
|
||||||
|
预览命中
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
|
||||||
|
{matchResult ? (
|
||||||
|
<Alert
|
||||||
|
className="platform-section-gap"
|
||||||
|
type={matchResult.matched ? 'success' : 'warning'}
|
||||||
|
showIcon
|
||||||
|
message={matchResult.matched ? '已命中' : '未命中'}
|
||||||
|
description={
|
||||||
|
matchResult.match
|
||||||
|
? `${matchResult.match.productName} -> ${matchResult.match.productCode}`
|
||||||
|
: matchResult.normalizedProductName || matchResult.productName
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Table<AdminKuaishouFeifeiProductRule>
|
||||||
|
rowKey={(row) => row.id || `${row.productName}-${row.productCode}`}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={rules}
|
||||||
|
pagination={{ pageSize: 20, showSizeChanger: true }}
|
||||||
|
scroll={{ x: 920, y: 520 }}
|
||||||
|
locale={{ emptyText: '暂无 kuaishou-feifei 商品映射' }}
|
||||||
|
className="platform-section-gap"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KuaishouCloudFulfillmentPanel() {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [skuLoading, setSkuLoading] = useState(false)
|
||||||
|
const [overrideSaving, setOverrideSaving] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
const [skuErrorMessage, setSkuErrorMessage] = useState('')
|
||||||
|
const [overrideErrorMessage, setOverrideErrorMessage] = useState('')
|
||||||
|
const [selectedSourceKey, setSelectedSourceKey] = useState('')
|
||||||
|
const [sources, setSources] = useState<AdminCloudtentaclesSourceItem[]>([])
|
||||||
|
const [sessions, setSessions] = useState<AdminCloudtentaclesSessionsMap>({})
|
||||||
|
const [skuItems, setSkuItems] = useState<AdminCloudtentaclesSkuItem[]>([])
|
||||||
|
const [overrideEnabled, setOverrideEnabled] = useState(true)
|
||||||
|
const [overrideRules, setOverrideRules] = useState<EditableOverrideRule[]>([])
|
||||||
|
const [matchInput, setMatchInput] = useState('')
|
||||||
|
|
||||||
|
const sourceRows = useMemo(
|
||||||
|
() =>
|
||||||
|
sources.map((source) => {
|
||||||
|
const session = sessions[source.key] || null
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
session,
|
||||||
|
ready: source.enabled !== false && Boolean(session?.hasToken),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[sessions, sources],
|
||||||
|
)
|
||||||
|
const readySources = sourceRows.filter((source) => source.ready)
|
||||||
|
const selectedSource = sourceRows.find((source) => source.key === selectedSourceKey) || null
|
||||||
|
const matchedSku = useMemo(() => {
|
||||||
|
const normalizedInput = normalizeMatchName(matchInput)
|
||||||
|
if (!normalizedInput) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
skuItems.find((item) => String(item.name || '').trim() === matchInput.trim()) ||
|
||||||
|
skuItems.find((item) => normalizeMatchName(item.name) === normalizedInput) ||
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}, [matchInput, skuItems])
|
||||||
|
const metrics = {
|
||||||
|
sourceCount: sources.length,
|
||||||
|
readySourceCount: readySources.length,
|
||||||
|
skuCount: skuItems.length,
|
||||||
|
inventoryTotal: skuItems.reduce((sum, item) => sum + Math.max(0, Number(item.inventory || 0)), 0),
|
||||||
|
overrideRuleCount: overrideRules.length,
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadPage()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSourceKey) {
|
||||||
|
void loadSkuList(selectedSourceKey)
|
||||||
|
} else {
|
||||||
|
setSkuItems([])
|
||||||
|
}
|
||||||
|
}, [selectedSourceKey])
|
||||||
|
|
||||||
|
async function loadPage() {
|
||||||
|
setLoading(true)
|
||||||
|
setErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [sourceResponse, overrideResponse] = await Promise.all([
|
||||||
|
fetchAdminCloudtentaclesSourceConfig(),
|
||||||
|
fetchAdminCloudtentaclesOverrideRules(),
|
||||||
|
])
|
||||||
|
const nextSources = Array.isArray(sourceResponse.data.sources) ? sourceResponse.data.sources : []
|
||||||
|
const nextSessions = sourceResponse.data.sessions || {}
|
||||||
|
const nextRows = nextSources.map((source) => {
|
||||||
|
const session = nextSessions[source.key] || null
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
session,
|
||||||
|
ready: source.enabled !== false && Boolean(session?.hasToken),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setSources(nextSources)
|
||||||
|
setSessions(nextSessions)
|
||||||
|
setOverrideEnabled(overrideResponse.data.enabled !== false)
|
||||||
|
setOverrideRules(
|
||||||
|
Array.isArray(overrideResponse.data.rules)
|
||||||
|
? overrideResponse.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
setSelectedSourceKey(resolveDefaultSourceKey(nextRows, selectedSourceKey))
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 履约配置失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSkuList(sourceKey = selectedSourceKey) {
|
||||||
|
if (!sourceKey) {
|
||||||
|
setSkuItems([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSkuLoading(true)
|
||||||
|
setSkuErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
|
||||||
|
setSkuItems(Array.isArray(response.data.items) ? response.data.items : [])
|
||||||
|
} catch (error) {
|
||||||
|
setSkuItems([])
|
||||||
|
setSkuErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 商品失败')
|
||||||
|
} finally {
|
||||||
|
setSkuLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveOverrideRules() {
|
||||||
|
setOverrideSaving(true)
|
||||||
|
setOverrideErrorMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await saveAdminCloudtentaclesOverrideRules({
|
||||||
|
enabled: overrideEnabled,
|
||||||
|
rules: overrideRules.map((rule) => ({
|
||||||
|
id: rule.id,
|
||||||
|
enabled: rule.enabled,
|
||||||
|
productName: rule.productName,
|
||||||
|
sourceKey: rule.sourceKey,
|
||||||
|
deliveryItems: rule.deliveryItems,
|
||||||
|
notes: rule.notes,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
setOverrideEnabled(response.data.enabled !== false)
|
||||||
|
setOverrideRules(
|
||||||
|
Array.isArray(response.data.rules)
|
||||||
|
? response.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
showSuccess(`商品覆盖规则已保存,共 ${response.data.rules.length} 条`)
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '保存 cloudtentacles 覆盖规则失败'
|
||||||
|
setOverrideErrorMessage(message)
|
||||||
|
showError(message)
|
||||||
|
} finally {
|
||||||
|
setOverrideSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOverrideRule() {
|
||||||
|
setOverrideRules((current) => [
|
||||||
|
{
|
||||||
|
id: createRuleId(),
|
||||||
|
enabled: true,
|
||||||
|
productName: matchInput.trim(),
|
||||||
|
normalizedProductName: normalizeMatchName(matchInput),
|
||||||
|
sourceKey: selectedSourceKey,
|
||||||
|
deliveryItems: [createDeliveryItem(skuItems)],
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
...current,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRule(index: number, patch: Partial<EditableOverrideRule>) {
|
||||||
|
setOverrideRules((current) =>
|
||||||
|
current.map((rule, ruleIndex) => (ruleIndex === index ? { ...rule, ...patch } : rule)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDeliveryItem(ruleIndex: number, itemIndex: number, patch: Partial<AdminCloudtentaclesOverrideDeliveryItem>) {
|
||||||
|
setOverrideRules((current) =>
|
||||||
|
current.map((rule, currentRuleIndex) => {
|
||||||
|
if (currentRuleIndex !== ruleIndex) {
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
deliveryItems: rule.deliveryItems.map((item, currentItemIndex) =>
|
||||||
|
currentItemIndex === itemIndex ? { ...item, ...patch } : item,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOverrideRule(index: number) {
|
||||||
|
setOverrideRules((current) => current.filter((_, ruleIndex) => ruleIndex !== index))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDeliveryItem(ruleIndex: number) {
|
||||||
|
setOverrideRules((current) =>
|
||||||
|
current.map((rule, currentRuleIndex) =>
|
||||||
|
currentRuleIndex === ruleIndex
|
||||||
|
? { ...rule, deliveryItems: [...rule.deliveryItems, createDeliveryItem(skuItems)] }
|
||||||
|
: rule,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDeliveryItem(ruleIndex: number, itemIndex: number) {
|
||||||
|
setOverrideRules((current) =>
|
||||||
|
current.map((rule, currentRuleIndex) => {
|
||||||
|
if (currentRuleIndex !== ruleIndex) {
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItems = rule.deliveryItems.filter((_, currentItemIndex) => currentItemIndex !== itemIndex)
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
deliveryItems: nextItems.length ? nextItems : [createDeliveryItem(skuItems)],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeliverySkuChange(ruleIndex: number, itemIndex: number, skuId: number) {
|
||||||
|
const sku = skuItems.find((item) => Number(item.id || 0) === Number(skuId || 0))
|
||||||
|
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||||
|
cloudSkuId: Number(skuId || 0) || 0,
|
||||||
|
cloudSkuName: String(sku?.name || '').trim(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="platform-panel-stack">
|
||||||
|
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
||||||
|
|
||||||
|
<div className="metric-grid five">
|
||||||
|
<MetricCard label="履约账号" value={`${metrics.readySourceCount} / ${metrics.sourceCount}`} />
|
||||||
|
<MetricCard label="可发商品" value={String(metrics.skuCount)} />
|
||||||
|
<MetricCard label="总库存" value={String(metrics.inventoryTotal)} />
|
||||||
|
<MetricCard label="覆盖规则" value={String(metrics.overrideRuleCount)} />
|
||||||
|
<Card>
|
||||||
|
<Button icon={<ReloadOutlined />} loading={loading || skuLoading} onClick={loadPage}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="platform-split-layout">
|
||||||
|
<Card title="cloudtentacles 账号" className="platform-side-card">
|
||||||
|
{sourceRows.length === 0 ? (
|
||||||
|
<Empty description="暂无账号,请先到平台配置登录。" />
|
||||||
|
) : (
|
||||||
|
<Space direction="vertical" className="full-width">
|
||||||
|
{sourceRows.map((source) => (
|
||||||
|
<button
|
||||||
|
key={source.key}
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
source.key === selectedSourceKey
|
||||||
|
? 'platform-select-card active'
|
||||||
|
: 'platform-select-card'
|
||||||
|
}
|
||||||
|
onClick={() => setSelectedSourceKey(source.key)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<strong>{formatSourceLabel(source)}</strong>
|
||||||
|
<small>{source.username || source.key}</small>
|
||||||
|
</span>
|
||||||
|
<Tag color={source.ready ? 'green' : source.enabled === false ? 'default' : 'orange'}>
|
||||||
|
{source.ready ? '可用' : source.enabled === false ? '已停用' : '未登录'}
|
||||||
|
</Tag>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<section className="platform-panel-stack">
|
||||||
|
<Card
|
||||||
|
title="匹配状态与覆盖规则"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
当前账号:{selectedSource ? formatSourceLabel(selectedSource) : '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={skuLoading}
|
||||||
|
disabled={!selectedSourceKey}
|
||||||
|
onClick={() => loadSkuList()}
|
||||||
|
>
|
||||||
|
刷新商品
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{skuErrorMessage ? (
|
||||||
|
<Alert type="error" showIcon message={skuErrorMessage} className="platform-section-gap" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Space className="full-width" direction="vertical">
|
||||||
|
<Space.Compact className="full-width">
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
value={matchInput}
|
||||||
|
placeholder="输入 91 卡券 productNo / 商品名字,查看命中的 cloudtentacles 商品"
|
||||||
|
onChange={(event) => setMatchInput(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={addOverrideRule}>
|
||||||
|
生成规则
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
{matchedSku ? (
|
||||||
|
<Tag color="green">命中 #{matchedSku.id} {matchedSku.name}</Tag>
|
||||||
|
) : matchInput.trim() ? (
|
||||||
|
<Tag color="orange">未命中</Tag>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="商品覆盖规则"
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Switch
|
||||||
|
checked={overrideEnabled}
|
||||||
|
checkedChildren="启用"
|
||||||
|
unCheckedChildren="停用"
|
||||||
|
onChange={setOverrideEnabled}
|
||||||
|
/>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={addOverrideRule}>
|
||||||
|
新增规则
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
loading={overrideSaving}
|
||||||
|
onClick={saveOverrideRules}
|
||||||
|
>
|
||||||
|
保存规则
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{overrideErrorMessage ? (
|
||||||
|
<Alert type="error" showIcon message={overrideErrorMessage} className="platform-section-gap" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{overrideRules.length === 0 ? (
|
||||||
|
<Empty description="暂无覆盖规则" />
|
||||||
|
) : (
|
||||||
|
<Space direction="vertical" className="full-width" size={12}>
|
||||||
|
{overrideRules.map((rule, ruleIndex) => (
|
||||||
|
<Card
|
||||||
|
key={rule.id}
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={rule.enabled !== false}
|
||||||
|
onChange={(enabled) => updateRule(ruleIndex, { enabled })}
|
||||||
|
/>
|
||||||
|
<span>{rule.productName || '未命名规则'}</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => removeOverrideRule(ruleIndex)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="platform-rule-head">
|
||||||
|
<Input
|
||||||
|
value={rule.productName}
|
||||||
|
placeholder="91 商品名 / productNo"
|
||||||
|
onChange={(event) => updateRule(ruleIndex, { productName: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
value={rule.sourceKey || undefined}
|
||||||
|
placeholder="全部账号"
|
||||||
|
options={[
|
||||||
|
{ label: '全部账号', value: '' },
|
||||||
|
...sourceRows.map((source) => ({
|
||||||
|
label: formatSourceLabel(source),
|
||||||
|
value: source.key,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
onChange={(sourceKey) => {
|
||||||
|
updateRule(ruleIndex, { sourceKey: String(sourceKey || '') })
|
||||||
|
if (sourceKey) {
|
||||||
|
setSelectedSourceKey(String(sourceKey))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" className="full-width platform-section-gap" size={8}>
|
||||||
|
{rule.deliveryItems.map((item, itemIndex) => (
|
||||||
|
<div key={`${rule.id}-${itemIndex}`} className="platform-delivery-row">
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
value={item.cloudSkuId || undefined}
|
||||||
|
placeholder="选择 cloudtentacles 商品"
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={skuItems.map((sku) => ({
|
||||||
|
label: formatSkuOption(sku),
|
||||||
|
value: sku.id,
|
||||||
|
}))}
|
||||||
|
onChange={(skuId) => handleDeliverySkuChange(ruleIndex, itemIndex, Number(skuId || 0))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={item.cloudSkuName}
|
||||||
|
placeholder="cloudtentacles 商品名"
|
||||||
|
onChange={(event) =>
|
||||||
|
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||||
|
cloudSkuName: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(quantity) =>
|
||||||
|
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||||
|
quantity: Math.max(1, Number(quantity || 1) || 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => removeDeliveryItem(ruleIndex, itemIndex)}>删除</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button onClick={() => addDeliveryItem(ruleIndex)}>增加发货商品</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
className="platform-section-gap"
|
||||||
|
value={rule.notes}
|
||||||
|
placeholder="备注"
|
||||||
|
onChange={(event) => updateRule(ruleIndex, { notes: event.target.value })}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="cloudtentacles 商品列表">
|
||||||
|
<Table<AdminCloudtentaclesSkuItem>
|
||||||
|
rowKey="id"
|
||||||
|
loading={skuLoading}
|
||||||
|
dataSource={skuItems}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'cloudtentacles 商品',
|
||||||
|
dataIndex: 'name',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text strong ellipsis>{row.name || '-'}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" ellipsis>
|
||||||
|
#{row.id} · {row.description || row.name || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '91 自动匹配名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
minWidth: 220,
|
||||||
|
render: (value) => <code>{String(value || '-')}</code>,
|
||||||
|
},
|
||||||
|
{ title: '价格', dataIndex: 'price', width: 120, render: formatCloudPrice },
|
||||||
|
{ title: '库存', dataIndex: 'inventory', width: 120, sorter: (a, b) => a.inventory - b.inventory },
|
||||||
|
{
|
||||||
|
title: '发货限制',
|
||||||
|
width: 150,
|
||||||
|
render: (_, row) => `${row.buyLimitMin || 1} - ${row.buyLimitMax || 1}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={Number(row.inventory || 0) > 0 ? 'green' : 'red'}>
|
||||||
|
{Number(row.inventory || 0) > 0 ? '可履约' : '无库存'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pagination={{ pageSize: 20, showSizeChanger: true }}
|
||||||
|
scroll={{ x: 1000, y: 420 }}
|
||||||
|
locale={{ emptyText: '当前账号暂无可展示商品' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
|
||||||
|
return (
|
||||||
|
<Card className="metric-card">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{value}</strong>
|
||||||
|
{detail ? <small>{detail}</small> : null}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFeifeiRule(
|
||||||
|
rule: Partial<AdminKuaishouFeifeiProductRule>,
|
||||||
|
): AdminKuaishouFeifeiProductRule {
|
||||||
|
const productName = String(rule.productName || '').trim()
|
||||||
|
const productCode = String(rule.productCode || '').trim()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(rule.id || productName || productCode).trim(),
|
||||||
|
enabled: rule.enabled !== false,
|
||||||
|
productName,
|
||||||
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||||
|
productCode,
|
||||||
|
skuName: String(rule.skuName || productName || productCode).trim(),
|
||||||
|
notes: String(rule.notes || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEditableRule(rule: AdminCloudtentaclesOverrideRule): EditableOverrideRule {
|
||||||
|
return {
|
||||||
|
id: String(rule.id || createRuleId()).trim(),
|
||||||
|
enabled: rule.enabled !== false,
|
||||||
|
productName: String(rule.productName || '').trim(),
|
||||||
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||||
|
sourceKey: String(rule.sourceKey || '').trim(),
|
||||||
|
deliveryItems:
|
||||||
|
Array.isArray(rule.deliveryItems) && rule.deliveryItems.length > 0
|
||||||
|
? rule.deliveryItems.map((item) => ({
|
||||||
|
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||||
|
cloudSkuName: String(item.cloudSkuName || '').trim(),
|
||||||
|
quantity: Math.max(1, Number(item.quantity || 1) || 1),
|
||||||
|
}))
|
||||||
|
: [createDeliveryItem([])],
|
||||||
|
notes: String(rule.notes || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeliveryItem(skuItems: AdminCloudtentaclesSkuItem[]): AdminCloudtentaclesOverrideDeliveryItem {
|
||||||
|
const firstSku = skuItems[0]
|
||||||
|
return {
|
||||||
|
cloudSkuId: Number(firstSku?.id || 0) || 0,
|
||||||
|
cloudSkuName: String(firstSku?.name || '').trim(),
|
||||||
|
quantity: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDefaultSourceKey(sourceRows: SourceRow[], selectedSourceKey: string) {
|
||||||
|
const current = sourceRows.find((source) => source.key === selectedSourceKey)
|
||||||
|
if (current?.ready) {
|
||||||
|
return current.key
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceRows.find((source) => source.ready)?.key || sourceRows[0]?.key || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMatchName(value: unknown) {
|
||||||
|
return String(value || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[【】\[\]()()]/g, ' ')
|
||||||
|
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSourceLabel(source: Pick<AdminCloudtentaclesSourceItem, 'key' | 'label' | 'username'>) {
|
||||||
|
return source.label || source.username || source.key
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCloudPrice(value: unknown) {
|
||||||
|
const price = Number(value)
|
||||||
|
return Number.isFinite(price) ? String(price) : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
|
||||||
|
return `#${sku.id} ${sku.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuleId() {
|
||||||
|
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -569,6 +569,166 @@ select {
|
|||||||
background: rgba(248, 250, 252, 0.72);
|
background: rgba(248, 250, 252, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platform-tabs {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-panel-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-active-summary .ant-card-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-active-summary h3 {
|
||||||
|
margin: 4px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-active-summary p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-kicker {
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid.four {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid.five {
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card .ant-card-body,
|
||||||
|
.metric-card {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card .ant-card-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card span,
|
||||||
|
.metric-card small {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card strong {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-section-gap {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-switch {
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-switch span {
|
||||||
|
color: #4b5563;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-inline-row,
|
||||||
|
.platform-shop-row,
|
||||||
|
.platform-rule-head,
|
||||||
|
.platform-delivery-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-inline-row {
|
||||||
|
grid-template-columns: auto minmax(120px, 0.8fr) minmax(180px, 1.2fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-shop-row {
|
||||||
|
grid-template-columns: auto auto minmax(120px, 0.9fr) minmax(140px, 1fr) minmax(220px, 1.5fr) auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-rule-head {
|
||||||
|
grid-template-columns: minmax(220px, 1fr) minmax(180px, 0.8fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-delivery-row {
|
||||||
|
grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) 110px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-split-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-side-card {
|
||||||
|
position: sticky;
|
||||||
|
top: 82px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select-card {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select-card.active,
|
||||||
|
.platform-source-card.active {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select-card span {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-select-card small {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.admin-sider {
|
.admin-sider {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
@@ -589,6 +749,28 @@ select {
|
|||||||
.page-header {
|
.page-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-grid.four,
|
||||||
|
.metric-grid.five,
|
||||||
|
.platform-form-grid,
|
||||||
|
.platform-split-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-side-card {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-inline-row,
|
||||||
|
.platform-shop-row,
|
||||||
|
.platform-rule-head,
|
||||||
|
.platform-delivery-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-active-summary .ant-card-body {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user