简化履约匹配并支持mock订单测试

This commit is contained in:
yml
2026-05-27 12:55:44 +08:00
parent e8010728a4
commit dd6776c4e4
21 changed files with 1892 additions and 683 deletions
@@ -3,561 +3,198 @@
gap: var(--space-4);
}
.section-card {
border-radius: var(--radius-lg);
border: 1px solid var(--border-default);
background: var(--bg-surface);
}
.section-card :deep(.el-card__header) {
background: var(--bg-surface);
border-bottom: 1px solid var(--border-default);
padding: var(--space-4) var(--space-5);
}
.section-card :deep(.el-card__body) {
padding: var(--space-5);
}
.card-header {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.card-header--split {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.card-title {
display: block;
font-size: var(--text-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.card-desc {
display: block;
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.5;
}
.overview-card {
display: grid;
gap: var(--space-4);
}
.overview-file code {
color: var(--text-primary);
word-break: break-all;
}
.overview-stats {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
.overview-stat {
.fulfillment-simple-page {
min-width: 0;
padding: var(--space-3);
}
.fulfillment-overview {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(120px, auto);
gap: var(--space-3);
align-items: stretch;
}
.metric-card {
display: grid;
gap: var(--space-2);
min-width: 0;
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-surface);
}
.overview-stat :deep(.el-statistic__head) {
margin-bottom: 6px;
.metric-card span {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
font-size: var(--text-sm);
}
.overview-stat :deep(.el-statistic__content) {
.metric-card strong {
color: var(--text-primary);
font-size: var(--text-2xl);
font-weight: 700;
line-height: 1.1;
}
.overview-notes {
.overview-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.lookup-panel {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.lookup-toolbar {
display: grid;
grid-template-columns:
minmax(260px, 1.4fr)
minmax(132px, 0.36fr)
minmax(150px, 0.44fr)
minmax(210px, 0.5fr);
gap: var(--space-3) var(--space-4);
align-items: end;
}
.lookup-field {
min-width: 0;
}
.lookup-field--number :deep(.el-input-number) {
width: 100%;
}
.lookup-action {
width: 100%;
min-height: 40px;
}
.lookup-note {
margin-top: 0;
}
.lookup-note :deep(.el-alert__content) {
min-width: 0;
}
.lookup-note :deep(.el-alert__title) {
line-height: 1.6;
}
.rule-action-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--space-3);
justify-content: flex-end;
align-items: center;
}
.rule-enabled-control {
display: flex;
justify-content: flex-end;
.fulfillment-layout {
display: grid;
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
gap: var(--space-4);
align-items: start;
}
.binding-card {
margin-top: var(--space-3);
background: var(--bg-surface);
.source-panel,
.catalog-panel {
min-width: 0;
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
}
.binding-card :deep(.el-card__header) {
padding: var(--space-3) var(--space-4);
background: var(--bg-surface);
border-bottom: 1px solid var(--border-default);
}
.binding-card :deep(.el-card__body) {
.source-panel {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.catalog-panel {
display: grid;
gap: var(--space-4);
padding: var(--space-4);
}
.binding-card--invalid {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.08);
}
.binding-header {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: flex-start;
}
.binding-summary {
.section-head {
display: grid;
gap: var(--space-2);
}
.binding-title-row {
display: flex;
gap: var(--space-2);
align-items: center;
flex-wrap: wrap;
color: var(--text-primary);
}
.binding-subtle,
.cell-subtle {
color: var(--text-secondary);
}
.cell-subtle {
display: block;
font-size: var(--text-xs);
line-height: 1.6;
}
.cell-stack {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.binding-collapsed-preview {
.section-head h3 {
margin: 0;
color: var(--text-primary);
font-size: var(--text-lg);
font-weight: 700;
}
.section-head p {
margin: 0;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.mapping-editor {
.source-list {
display: grid;
grid-template-columns: minmax(0, 1fr) 74px minmax(0, 1fr);
gap: var(--space-3);
align-items: stretch;
gap: var(--space-2);
}
.mapping-panel {
min-width: 0;
background: var(--bg-surface);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
}
.mapping-panel :deep(.el-card__header) {
padding: var(--space-3) var(--space-4);
background: var(--bg-surface);
border-bottom: 1px solid var(--border-default);
}
.mapping-panel :deep(.el-card__body) {
padding: var(--space-4);
}
.mapping-panel-head {
.source-item {
display: flex;
width: 100%;
min-width: 0;
justify-content: space-between;
gap: var(--space-3);
align-items: baseline;
}
.mapping-panel-head strong {
color: var(--text-primary);
}
.mapping-panel-head span {
color: var(--text-secondary);
font-size: var(--text-xs);
}
.mapping-grid,
.mapping-extra {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-4);
}
.mapping-extra {
padding-top: var(--space-3);
border-top: 1px dashed var(--border-default);
}
.mapping-arrow {
display: grid;
place-items: center;
color: var(--color-primary);
font-size: var(--text-xs);
font-weight: 700;
}
.mapping-arrow span {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
min-width: 54px;
padding: 0 10px;
border-radius: var(--radius-full);
background: var(--bg-surface);
padding: var(--space-3);
border: 1px solid var(--border-default);
}
.rule-editor {
display: grid;
gap: var(--space-3);
}
.rule-editor-main {
display: grid;
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
gap: var(--space-3);
align-items: stretch;
}
.rule-panel {
display: grid;
align-content: start;
gap: var(--space-4);
min-width: 0;
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
border-radius: var(--radius-sm);
background: var(--bg-surface);
color: inherit;
text-align: left;
cursor: pointer;
}
.rule-panel--strategy {
.source-item:hover,
.source-item--active {
border-color: var(--color-primary);
background: var(--bg-subtle);
}
.rule-panel-head {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
.source-item span {
display: grid;
gap: 4px;
min-width: 0;
}
.rule-panel-head > span {
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.5;
text-align: right;
.source-item strong,
.source-item small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rule-panel-title {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
.source-item strong {
color: var(--text-primary);
}
.rule-panel-title strong {
font-size: var(--text-base);
font-weight: 700;
.source-item small {
color: var(--text-secondary);
}
.rule-step {
display: inline-grid;
place-items: center;
width: 30px;
height: 24px;
.catalog-toolbar {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: flex-start;
}
.match-checker {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto;
gap: var(--space-3);
align-items: center;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.sku-table {
width: 100%;
}
.sku-name-cell {
display: grid;
gap: 4px;
min-width: 0;
}
.sku-name-cell strong {
color: var(--text-primary);
}
.sku-name-cell small {
color: var(--text-secondary);
line-height: 1.5;
}
.match-name {
display: inline-block;
max-width: 100%;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--bg-subtle);
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
}
.rule-panel--strategy .rule-step {
background: var(--bg-surface);
}
.mapping-grid--core {
gap: var(--space-3) var(--space-4);
}
.strategy-grid {
display: grid;
grid-template-columns: minmax(150px, 0.24fr) minmax(150px, 0.24fr) minmax(0, 1fr);
gap: var(--space-4);
align-items: end;
}
.strategy-checks {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-4);
min-height: 32px;
align-items: center;
}
.advanced-fields {
border: 1px dashed var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-surface);
}
.advanced-fields summary {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
padding: var(--space-3) var(--space-4);
color: var(--text-primary);
cursor: pointer;
list-style: none;
white-space: normal;
word-break: break-all;
}
.advanced-fields summary::-webkit-details-marker {
display: none;
}
.advanced-fields summary span {
font-weight: 700;
}
.advanced-fields summary small {
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.5;
text-align: right;
}
.advanced-fields summary::after {
content: '展开';
flex: 0 0 auto;
min-width: 40px;
color: var(--color-primary);
font-size: var(--text-xs);
font-weight: 700;
text-align: right;
}
.advanced-fields[open] summary {
border-bottom: 1px dashed var(--border-default);
}
.advanced-fields[open] summary::after {
content: '收起';
}
.advanced-grid {
padding: var(--space-4);
}
.delivery-items-editor {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.delivery-items-head {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
color: var(--text-primary);
font-weight: 700;
}
.delivery-item-row {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(160px, 0.7fr) 132px 48px;
gap: var(--space-3);
align-items: center;
}
.delivery-item-sku,
.delivery-item-name,
.delivery-item-count {
width: 100%;
}
.field-wide {
grid-column: 1 / -1;
}
.field-help {
display: block;
margin-top: var(--space-2);
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.6;
}
.text-input {
width: 100%;
}
.textarea-input {
min-height: 72px;
}
.admin-panel :deep(.el-form-item) {
margin-bottom: 0;
}
.admin-panel :deep(.el-form-item__label) {
color: var(--text-secondary);
font-size: var(--text-sm);
font-weight: 600;
line-height: 1.5;
margin-bottom: var(--space-2);
}
.binding-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--space-3);
align-items: center;
padding-top: var(--space-3);
border-top: 1px solid var(--border-default);
}
.data-table {
margin-top: var(--space-4);
}
.data-table :deep(.el-table__header th) {
background: var(--bg-surface);
color: var(--text-secondary);
font-weight: 600;
font-size: var(--text-sm);
}
.data-table :deep(.el-table__body td) {
padding: var(--space-3) var(--space-2);
}
@media (max-width: 900px) {
.card-header--split,
.rule-action-row,
.binding-header,
.mapping-panel-head {
display: grid;
grid-template-columns: 1fr;
align-items: stretch;
}
.overview-stats,
.lookup-toolbar,
.rule-editor-main,
.strategy-grid,
.mapping-editor,
.delivery-item-row,
.mapping-grid,
.mapping-extra {
@media (max-width: 960px) {
.fulfillment-overview,
.fulfillment-layout,
.match-checker {
grid-template-columns: 1fr;
}
.rule-panel-head,
.advanced-fields summary {
display: grid;
grid-template-columns: 1fr;
align-items: start;
.overview-actions,
.catalog-toolbar {
justify-content: stretch;
}
.rule-panel-head > span,
.advanced-fields summary small {
text-align: left;
}
.mapping-arrow {
display: none;
}
.section-card :deep(.el-card__header),
.section-card :deep(.el-card__body) {
padding: var(--space-4);
}
.binding-card :deep(.el-card__header),
.binding-card :deep(.el-card__body) {
padding: var(--space-3);
.catalog-toolbar {
flex-direction: column;
}
}
@@ -1,110 +1,312 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import {
fetchAdminCloudtentaclesSkuList,
fetchAdminCloudtentaclesSourceConfig,
} from '@/services/admin'
import type {
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSkuItem,
AdminCloudtentaclesSourceItem,
AdminCloudtentaclesSessionsMap,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import AdminKuaishouCloudNinetyoneSection from './components/AdminKuaishouCloudNinetyoneSection.vue'
import AdminKuaishouCloudRuleSection from './components/AdminKuaishouCloudRuleSection.vue'
import { useKuaishouCloudConfig } from './composables/useKuaishouCloudConfig'
import { useKuaishouCloudNinetyone } from './composables/useKuaishouCloudNinetyone'
import { useKuaishouCloudSku } from './composables/useKuaishouCloudSku'
const config = useKuaishouCloudConfig()
const ninetyone = useKuaishouCloudNinetyone(config)
const cloudSku = useKuaishouCloudSku(config.cloudtentaclesSourceOptions)
function handlePrimaryCloudSkuSelected(item: (typeof config.items.value)[number], value: number | string | undefined) {
cloudSku.handleCloudSkuSelected(item, value)
config.syncDeliveryItemsFromPrimaryCloudSku(item)
type MatchState = {
input: string
}
function handleDeliveryItemCloudSkuSelected(
item: (typeof config.items.value)[number],
index: number,
value: number | string | undefined,
) {
const deliveryItem = item.deliveryItems[index]
if (!deliveryItem) {
const loading = ref(true)
const skuLoading = ref(false)
const errorMessage = ref('')
const skuErrorMessage = ref('')
const selectedSourceKey = ref('')
const sources = ref<AdminCloudtentaclesSourceItem[]>([])
const sessions = ref<AdminCloudtentaclesSessionsMap>({})
const skuItems = ref<AdminCloudtentaclesSkuItem[]>([])
const matchState = reactive<MatchState>({
input: '',
})
const sourceRows = computed(() =>
sources.value.map((source) => {
const session = resolveSourceSession(source.key)
return {
...source,
session,
ready: source.enabled !== false && Boolean(session?.hasToken),
}
}),
)
const readySources = computed(() => sourceRows.value.filter((source) => source.ready))
const selectedSource = computed(
() => sourceRows.value.find((source) => source.key === selectedSourceKey.value) || null,
)
const metrics = computed(() => ({
sourceCount: sources.value.length,
readySourceCount: readySources.value.length,
skuCount: skuItems.value.length,
inventoryTotal: skuItems.value.reduce((sum, item) => sum + Math.max(0, Number(item.inventory || 0)), 0),
}))
const matchedSku = computed(() => {
const normalizedInput = normalizeMatchName(matchState.input)
if (!normalizedInput) {
return null
}
return (
skuItems.value.find((item) => String(item.name || '').trim() === matchState.input.trim()) ||
skuItems.value.find((item) => normalizeMatchName(item.name) === normalizedInput) ||
null
)
})
watch(selectedSourceKey, (sourceKey) => {
if (sourceKey) {
void loadSkuList(sourceKey)
} else {
skuItems.value = []
}
})
onMounted(loadPage)
async function loadPage() {
if (!hasAdminRole('admin')) {
loading.value = false
return
}
cloudSku.handleDeliveryItemCloudSkuSelected(item, deliveryItem, value)
config.syncPrimaryCloudSkuFromDeliveryItems(item)
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminCloudtentaclesSourceConfig()
sources.value = Array.isArray(response.data.sources) ? response.data.sources : []
sessions.value = response.data.sessions || {}
selectedSourceKey.value = resolveDefaultSourceKey()
if (!selectedSourceKey.value) {
skuItems.value = []
}
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取 cloudtentacles 账号失败'
} finally {
loading.value = false
}
}
onMounted(config.loadConfigs)
async function loadSkuList(sourceKey = selectedSourceKey.value) {
if (!sourceKey) {
skuItems.value = []
return
}
skuLoading.value = true
skuErrorMessage.value = ''
try {
const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
skuItems.value = Array.isArray(response.data.items) ? response.data.items : []
} catch (error) {
skuItems.value = []
skuErrorMessage.value = error instanceof Error ? error.message : '读取 cloudtentacles 商品失败'
} finally {
skuLoading.value = false
}
}
function refreshAll() {
void loadPage()
}
function resolveDefaultSourceKey() {
const current = sourceRows.value.find((source) => source.key === selectedSourceKey.value)
if (current?.ready) {
return current.key
}
return readySources.value[0]?.key || sourceRows.value[0]?.key || ''
}
function resolveSourceSession(sourceKey: string): AdminCloudtentaclesSessionItem | null {
return sessions.value[sourceKey] || null
}
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: AdminCloudtentaclesSourceItem) {
return source.label || source.username || source.key
}
function formatCloudPrice(value: unknown) {
const price = Number(value)
return Number.isFinite(price) ? String(price) : '-'
}
function getSourceTagType(source: { ready: boolean; enabled?: boolean }) {
if (source.ready) return 'success'
if (source.enabled === false) return 'info'
return 'warning'
}
function getSourceStatusText(source: { ready: boolean; enabled?: boolean; session: AdminCloudtentaclesSessionItem | null }) {
if (source.ready) return '可用'
if (source.enabled === false) return '已停用'
if (!source.session?.hasToken) return '未登录'
return '待检查'
}
</script>
<template>
<section class="admin-panel">
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以维护新履约配置" />
<section class="admin-panel fulfillment-simple-page">
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以查看履约配置" />
<template v-else>
<el-alert
v-if="config.errorMessage.value"
:title="config.errorMessage.value"
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
/>
<el-skeleton v-if="config.loading.value" :rows="12" animated />
<el-skeleton v-if="loading" :rows="10" animated />
<template v-else>
<AdminKuaishouCloudNinetyoneSection
:ninetyone-lookup-loading="ninetyone.ninetyoneLookupLoading.value"
:ninetyone-lookup-error-message="ninetyone.ninetyoneLookupErrorMessage.value"
:ninetyone-lookup-results="ninetyone.ninetyoneLookupResults.value"
:ninetyone-lookup-metrics="ninetyone.ninetyoneLookupMetrics.value"
:ninetyone-lookup-form="ninetyone.ninetyoneLookupForm"
:is-imported="ninetyone.isNinetyoneProductImported"
@lookup="ninetyone.lookupNinetyoneProducts()"
@import-product="ninetyone.importNinetyoneProduct($event)"
/>
<section class="fulfillment-overview">
<div class="metric-card">
<span>履约账号</span>
<strong>{{ metrics.readySourceCount }} / {{ metrics.sourceCount }}</strong>
</div>
<div class="metric-card">
<span>可发商品</span>
<strong>{{ metrics.skuCount }}</strong>
</div>
<div class="metric-card">
<span>总库存</span>
<strong>{{ metrics.inventoryTotal }}</strong>
</div>
<div class="overview-actions">
<el-button :loading="loading || skuLoading" @click="refreshAll">刷新</el-button>
</div>
</section>
<AdminKuaishouCloudRuleSection
:enabled="config.enabled.value"
:saving="config.saving.value"
:cloudtentacles-source-options="config.cloudtentaclesSourceOptions.value"
:cloud-sku-catalog-loading="cloudSku.cloudSkuCatalogLoading.value"
:validation-state="config.validationState.value"
:items="config.items.value"
:filtered-items="config.filteredItems.value"
:rule-filter="config.ruleFilter.value"
:rule-filter-options="config.ruleFilterOptions.value"
:is-collapsed="config.isCollapsed"
:is-item-complete="config.isItemComplete"
:get-card-title="config.getCardTitle"
:get-card-summary="config.getCardSummary"
:get-rule-state="config.getRuleState"
:get-external-match-summary="config.getExternalMatchSummary"
:get-cloud-source-summary="config.getCloudSourceSummary"
:get-cloud-sku-summary="config.getCloudSkuSummary"
:get-consume-shop-summary="config.getConsumeShopSummary"
:get-delivery-plan-summary="config.getDeliveryPlanSummary"
:get-cloud-sku-options="cloudSku.getCloudSkuOptions"
:format-cloud-sku-option-label="cloudSku.formatCloudSkuOptionLabel"
:get-kuaishou-consume-shop-options="config.getKuaishouConsumeShopOptions"
:format-kuaishou-consume-shop-option="config.formatKuaishouConsumeShopOption"
:cloud-sku-catalog-error-message="cloudSku.cloudSkuCatalogErrorMessage.value"
@update:enabled="config.enabled.value = $event"
@update:rule-filter="config.ruleFilter.value = $event"
@expand-all="config.expandAllRules()"
@collapse-ready="config.collapseReadyRules()"
@refresh-cloud-sku="cloudSku.refreshCloudSkuCatalog()"
@add-item="config.addItem()"
@save-configs="config.saveConfigs()"
@toggle-collapsed="config.toggleCollapsed($event)"
@handle-cloud-sku-selected="handlePrimaryCloudSkuSelected"
@handle-delivery-item-cloud-sku-selected="handleDeliveryItemCloudSkuSelected"
@handle-cloud-sku-dropdown-visible="
(item, visible) => cloudSku.handleCloudSkuDropdownVisible(item, visible)
"
@handle-kuaishou-consume-shop-selected="config.handleKuaishouConsumeShopSelected($event)"
@handle-cloud-source-keys-changed="config.handleCloudSourceKeysChanged($event)"
@add-delivery-item="config.addDeliveryItem($event)"
@remove-delivery-item="(item, index) => config.removeDeliveryItem(item, index)"
@remove-item="config.removeItem($event)"
/>
<section class="fulfillment-layout">
<aside class="source-panel">
<div class="section-head">
<h3>cloudtentacles 账号</h3>
</div>
<el-empty v-if="sourceRows.length === 0" description="暂无账号,请先到平台店铺配置登录。" />
<div v-else class="source-list">
<button
v-for="source in sourceRows"
:key="source.key"
type="button"
class="source-item"
:class="{ 'source-item--active': source.key === selectedSourceKey }"
@click="selectedSourceKey = source.key"
>
<span>
<strong>{{ formatSourceLabel(source) }}</strong>
<small>{{ source.username || source.key }}</small>
</span>
<el-tag :type="getSourceTagType(source)" effect="light">
{{ getSourceStatusText(source) }}
</el-tag>
</button>
</div>
</aside>
<main class="catalog-panel">
<div class="catalog-toolbar">
<div class="section-head">
<h3>商品列表与匹配状态</h3>
<p v-if="selectedSource">
当前账号{{ formatSourceLabel(selectedSource) }}
</p>
</div>
<el-button :loading="skuLoading" :disabled="!selectedSourceKey" @click="loadSkuList()">
刷新商品
</el-button>
</div>
<el-alert
v-if="skuErrorMessage"
:title="skuErrorMessage"
type="error"
show-icon
:closable="false"
/>
<div class="match-checker">
<el-input
v-model="matchState.input"
clearable
placeholder="输入 91 卡券 productNo / 商品名字,查看会命中哪个 cloudtentacles 商品"
/>
<el-tag v-if="matchedSku" type="success" effect="light">
命中 #{{ matchedSku.id }} {{ matchedSku.name }}
</el-tag>
<el-tag v-else-if="matchState.input.trim()" type="warning" effect="light">
未命中
</el-tag>
</div>
<el-table
v-loading="skuLoading"
:data="skuItems"
border
stripe
class="sku-table"
empty-text="当前账号暂无可展示商品"
>
<el-table-column label="cloudtentacles 商品" min-width="260">
<template #default="{ row }">
<div class="sku-name-cell">
<strong>{{ row.name || '-' }}</strong>
<small>#{{ row.id }} · {{ row.description || row.name || '-' }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="91 自动匹配名" min-width="220">
<template #default="{ row }">
<code class="match-name">{{ row.name || '-' }}</code>
</template>
</el-table-column>
<el-table-column label="价格" width="120">
<template #default="{ row }">{{ formatCloudPrice(row.price) }}</template>
</el-table-column>
<el-table-column label="库存" prop="inventory" width="120" sortable />
<el-table-column label="发货限制" width="150">
<template #default="{ row }">
{{ row.buyLimitMin || 1 }} - {{ row.buyLimitMax || 1 }}
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag :type="Number(row.inventory || 0) > 0 ? 'success' : 'danger'" effect="light">
{{ Number(row.inventory || 0) > 0 ? '可履约' : '无库存' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</main>
</section>
</template>
</template>
</section>