支持cloudtentacles覆盖规则
This commit is contained in:
@@ -2,6 +2,7 @@ import { apiDelete, apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
@@ -33,6 +34,35 @@ export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesOverrideRules() {
|
||||
return apiGet<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminCloudtentaclesOverrideRules(
|
||||
payload: {
|
||||
enabled: boolean
|
||||
rules: Array<{
|
||||
id?: string
|
||||
enabled?: boolean
|
||||
productName?: string
|
||||
sourceKey?: string
|
||||
deliveryItems?: Array<{
|
||||
cloudSkuId?: number
|
||||
cloudSkuName?: string
|
||||
quantity?: number
|
||||
}>
|
||||
notes?: string
|
||||
}>
|
||||
},
|
||||
) {
|
||||
return apiPost<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendAdminCloudtentaclesSmsCode(payload: {
|
||||
baseUrl?: string
|
||||
username?: string
|
||||
|
||||
@@ -54,6 +54,9 @@ export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
|
||||
@@ -41,6 +41,28 @@ export interface AdminCloudtentaclesSourceConfigResponse {
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideDeliveryItem {
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRule {
|
||||
id: string
|
||||
enabled: boolean
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
sourceKey: string
|
||||
deliveryItems: AdminCloudtentaclesOverrideDeliveryItem[]
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRuleConfig {
|
||||
filePath: string
|
||||
enabled: boolean
|
||||
rules: AdminCloudtentaclesOverrideRule[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSendSmsResult {
|
||||
baseUrl: string
|
||||
username: string
|
||||
|
||||
@@ -34,6 +34,9 @@ export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
|
||||
+55
-3
@@ -9,7 +9,7 @@
|
||||
|
||||
.fulfillment-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(120px, auto);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(120px, auto);
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -182,15 +182,67 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.override-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.override-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.override-rule-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.override-rule-item {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.override-rule-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(220px, 1fr) minmax(160px, 220px) auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delivery-item-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.delivery-item-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.1fr) minmax(180px, 1fr) 120px auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.fulfillment-overview,
|
||||
.fulfillment-layout,
|
||||
.match-checker {
|
||||
.match-checker,
|
||||
.override-rule-head,
|
||||
.delivery-item-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-actions,
|
||||
.catalog-toolbar {
|
||||
.catalog-toolbar,
|
||||
.override-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
|
||||
+220
-3
@@ -2,10 +2,14 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
fetchAdminCloudtentaclesOverrideRules,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
saveAdminCloudtentaclesOverrideRules,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
@@ -17,14 +21,23 @@ type MatchState = {
|
||||
input: string
|
||||
}
|
||||
|
||||
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
|
||||
normalizedProductName?: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const skuLoading = ref(false)
|
||||
const overrideLoading = ref(false)
|
||||
const overrideSaving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const skuErrorMessage = ref('')
|
||||
const overrideErrorMessage = ref('')
|
||||
const selectedSourceKey = ref('')
|
||||
const sources = ref<AdminCloudtentaclesSourceItem[]>([])
|
||||
const sessions = ref<AdminCloudtentaclesSessionsMap>({})
|
||||
const skuItems = ref<AdminCloudtentaclesSkuItem[]>([])
|
||||
const overrideEnabled = ref(true)
|
||||
const overrideRules = ref<EditableOverrideRule[]>([])
|
||||
const matchState = reactive<MatchState>({
|
||||
input: '',
|
||||
})
|
||||
@@ -65,6 +78,8 @@ const matchedSku = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const overrideRuleCount = computed(() => overrideRules.value.length)
|
||||
|
||||
watch(selectedSourceKey, (sourceKey) => {
|
||||
if (sourceKey) {
|
||||
void loadSkuList(sourceKey)
|
||||
@@ -85,9 +100,16 @@ async function loadPage() {
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesSourceConfig()
|
||||
sources.value = Array.isArray(response.data.sources) ? response.data.sources : []
|
||||
sessions.value = response.data.sessions || {}
|
||||
const [sourceResponse, overrideResponse] = await Promise.all([
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
fetchAdminCloudtentaclesOverrideRules(),
|
||||
])
|
||||
sources.value = Array.isArray(sourceResponse.data.sources) ? sourceResponse.data.sources : []
|
||||
sessions.value = sourceResponse.data.sessions || {}
|
||||
overrideEnabled.value = overrideResponse.data.enabled !== false
|
||||
overrideRules.value = Array.isArray(overrideResponse.data.rules)
|
||||
? overrideResponse.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: []
|
||||
selectedSourceKey.value = resolveDefaultSourceKey()
|
||||
|
||||
if (!selectedSourceKey.value) {
|
||||
@@ -100,6 +122,33 @@ async function loadPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOverrideRules() {
|
||||
overrideSaving.value = true
|
||||
overrideErrorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminCloudtentaclesOverrideRules({
|
||||
enabled: overrideEnabled.value,
|
||||
rules: overrideRules.value.map((rule) => ({
|
||||
id: rule.id,
|
||||
enabled: rule.enabled,
|
||||
productName: rule.productName,
|
||||
sourceKey: rule.sourceKey,
|
||||
deliveryItems: rule.deliveryItems,
|
||||
notes: rule.notes,
|
||||
})),
|
||||
})
|
||||
overrideEnabled.value = response.data.enabled !== false
|
||||
overrideRules.value = Array.isArray(response.data.rules)
|
||||
? response.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: []
|
||||
} catch (error) {
|
||||
overrideErrorMessage.value = error instanceof Error ? error.message : '保存 cloudtentacles 覆盖规则失败'
|
||||
} finally {
|
||||
overrideSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkuList(sourceKey = selectedSourceKey.value) {
|
||||
if (!sourceKey) {
|
||||
skuItems.value = []
|
||||
@@ -124,6 +173,45 @@ function refreshAll() {
|
||||
void loadPage()
|
||||
}
|
||||
|
||||
function addOverrideRule() {
|
||||
overrideRules.value.unshift({
|
||||
id: createRuleId(),
|
||||
enabled: true,
|
||||
productName: matchState.input.trim(),
|
||||
normalizedProductName: normalizeMatchName(matchState.input),
|
||||
sourceKey: selectedSourceKey.value,
|
||||
deliveryItems: [createDeliveryItem()],
|
||||
notes: '',
|
||||
})
|
||||
}
|
||||
|
||||
function removeOverrideRule(index: number) {
|
||||
overrideRules.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function addDeliveryItem(rule: EditableOverrideRule) {
|
||||
rule.deliveryItems.push(createDeliveryItem())
|
||||
}
|
||||
|
||||
function removeDeliveryItem(rule: EditableOverrideRule, index: number) {
|
||||
rule.deliveryItems.splice(index, 1)
|
||||
if (rule.deliveryItems.length === 0) {
|
||||
rule.deliveryItems.push(createDeliveryItem())
|
||||
}
|
||||
}
|
||||
|
||||
function handleRuleSourceChange(sourceKey: string) {
|
||||
if (sourceKey) {
|
||||
selectedSourceKey.value = sourceKey
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeliverySkuChange(item: AdminCloudtentaclesOverrideDeliveryItem, skuId: number) {
|
||||
const sku = skuItems.value.find((current) => Number(current.id || 0) === Number(skuId || 0))
|
||||
item.cloudSkuId = Number(skuId || 0) || 0
|
||||
item.cloudSkuName = String(sku?.name || item.cloudSkuName || '').trim()
|
||||
}
|
||||
|
||||
function resolveDefaultSourceKey() {
|
||||
const current = sourceRows.value.find((source) => source.key === selectedSourceKey.value)
|
||||
if (current?.ready) {
|
||||
@@ -156,6 +244,10 @@ function formatCloudPrice(value: unknown) {
|
||||
return Number.isFinite(price) ? String(price) : '-'
|
||||
}
|
||||
|
||||
function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
|
||||
return `#${sku.id} ${sku.name}`
|
||||
}
|
||||
|
||||
function getSourceTagType(source: { ready: boolean; enabled?: boolean }) {
|
||||
if (source.ready) return 'success'
|
||||
if (source.enabled === false) return 'info'
|
||||
@@ -168,6 +260,37 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
|
||||
if (!source.session?.hasToken) return '未登录'
|
||||
return '待检查'
|
||||
}
|
||||
|
||||
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(): AdminCloudtentaclesOverrideDeliveryItem {
|
||||
const firstSku = skuItems.value[0]
|
||||
return {
|
||||
cloudSkuId: Number(firstSku?.id || 0) || 0,
|
||||
cloudSkuName: String(firstSku?.name || '').trim(),
|
||||
quantity: 1,
|
||||
}
|
||||
}
|
||||
|
||||
function createRuleId() {
|
||||
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -199,6 +322,10 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
|
||||
<span>总库存</span>
|
||||
<strong>{{ metrics.inventoryTotal }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>覆盖规则</span>
|
||||
<strong>{{ overrideRuleCount }}</strong>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<el-button :loading="loading || skuLoading" @click="refreshAll">刷新</el-button>
|
||||
</div>
|
||||
@@ -305,6 +432,96 @@ function getSourceStatusText(source: { ready: boolean; enabled?: boolean; sessio
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<section class="override-panel">
|
||||
<div class="catalog-toolbar">
|
||||
<div class="section-head">
|
||||
<h3>商品覆盖规则</h3>
|
||||
<p>套装、多数量商品会优先按这里发货;没有规则时继续同名匹配。</p>
|
||||
</div>
|
||||
<div class="override-actions">
|
||||
<el-switch v-model="overrideEnabled" active-text="启用" inactive-text="停用" />
|
||||
<el-button @click="addOverrideRule">新增规则</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="overrideLoading || overrideSaving"
|
||||
@click="saveOverrideRules"
|
||||
>
|
||||
保存规则
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="overrideErrorMessage"
|
||||
:title="overrideErrorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<el-empty v-if="overrideRules.length === 0" description="暂无覆盖规则" />
|
||||
|
||||
<div v-else class="override-rule-list">
|
||||
<article
|
||||
v-for="(rule, ruleIndex) in overrideRules"
|
||||
:key="rule.id"
|
||||
class="override-rule-item"
|
||||
>
|
||||
<div class="override-rule-head">
|
||||
<el-switch v-model="rule.enabled" active-text="启用" inactive-text="停用" />
|
||||
<el-input v-model="rule.productName" placeholder="91 商品名 / productNo" clearable />
|
||||
<el-select
|
||||
v-model="rule.sourceKey"
|
||||
clearable
|
||||
placeholder="全部账号"
|
||||
@change="handleRuleSourceChange"
|
||||
>
|
||||
<el-option label="全部账号" value="" />
|
||||
<el-option
|
||||
v-for="source in sourceRows"
|
||||
:key="source.key"
|
||||
:label="formatSourceLabel(source)"
|
||||
:value="source.key"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="danger" plain @click="removeOverrideRule(ruleIndex)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="delivery-item-list">
|
||||
<div
|
||||
v-for="(item, itemIndex) in rule.deliveryItems"
|
||||
:key="`${rule.id}-${itemIndex}`"
|
||||
class="delivery-item-row"
|
||||
>
|
||||
<el-select
|
||||
v-model="item.cloudSkuId"
|
||||
filterable
|
||||
placeholder="选择 cloudtentacles 商品"
|
||||
@change="(value: number) => handleDeliverySkuChange(item, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="sku in skuItems"
|
||||
:key="sku.id"
|
||||
:label="formatSkuOption(sku)"
|
||||
:value="sku.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="item.cloudSkuName" placeholder="cloudtentacles 商品名" clearable />
|
||||
<el-input-number v-model="item.quantity" :min="1" :max="999" controls-position="right" />
|
||||
<el-button plain @click="removeDeliveryItem(rule, itemIndex)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button plain @click="addDeliveryItem(rule)">增加发货商品</el-button>
|
||||
</div>
|
||||
|
||||
<el-input v-model="rule.notes" placeholder="备注" clearable />
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user