简化履约匹配并支持mock订单测试
This commit is contained in:
+122
-485
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+285
-83
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user