外部测试内部绑定ok

This commit is contained in:
yml
2026-04-12 13:16:50 +08:00
parent a95247cd01
commit 0029147bc4
31 changed files with 2826 additions and 124 deletions
@@ -0,0 +1,945 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
fetchAdminFulfillmentBindingConfigs,
saveAdminFulfillmentBindingConfigs,
} from '@/services/admin'
import type { AdminFulfillmentBindingConfigItem, AdminObservedProductItem } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
type EditableBinding = {
id: string
provider: string
platform: string
shopId: string
skuCode: string
skuName: string
profileKey: string
enabled: boolean
priority: number
externalSkuCode: string
externalItemId: string
externalSkuName: string
resolvedSkuName: string
}
type SaveBindingPayload = {
provider: string
platform: string
shopId: string
skuCode: string
skuName: string
profileKey: string
enabled: boolean
priority: number
match: {
externalSkuCode: string
externalItemId: string
externalSkuName: string
config: {
resolvedSkuName: string
}
}
}
type ValidationField = 'skuCode' | 'match' | 'profileKey'
type ValidationState = {
bindingId: string
field: ValidationField
message: string
} | null
const PROFILE_OPTIONS = [
{ label: '腾讯领取兑换', value: 'tencent_claim_redeem' },
{ label: '人工发货', value: 'manual_review' },
]
const loading = ref(true)
const saving = ref(false)
const errorMessage = ref('')
const filePath = ref('')
const bindings = ref<EditableBinding[]>([])
const observedProducts = ref<AdminObservedProductItem[]>([])
const VALID_PROFILE_KEYS = new Set(PROFILE_OPTIONS.map((item) => item.value))
const validationState = ref<ValidationState>(null)
const collapsedBindingIds = ref<string[]>([])
function createEmptyBinding(): EditableBinding {
return {
id: crypto.randomUUID(),
provider: 'agiso',
platform: '',
shopId: '',
skuCode: '',
skuName: '',
profileKey: 'manual_review',
enabled: true,
priority: 100,
externalSkuCode: '',
externalItemId: '',
externalSkuName: '',
resolvedSkuName: '',
}
}
function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBinding {
return {
id: crypto.randomUUID(),
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shopId,
skuCode: item.skuCode,
skuName: item.skuName,
profileKey: item.profileKey || 'manual_review',
enabled: item.enabled !== false,
priority: item.priority || 100,
externalSkuCode: item.match.externalSkuCode,
externalItemId: item.match.externalItemId,
externalSkuName: item.match.externalSkuName,
resolvedSkuName: String(item.match.config?.resolvedSkuName || ''),
}
}
function hasMatchCondition(item: Pick<EditableBinding, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
return Boolean(item.externalSkuCode.trim() || item.externalItemId.trim() || item.externalSkuName.trim())
}
function isEditableBindingComplete(item: EditableBinding) {
return Boolean(item.skuCode.trim() && hasMatchCondition(item) && VALID_PROFILE_KEYS.has(item.profileKey.trim() || 'manual_review'))
}
function buildCollapsedIds(items: EditableBinding[]) {
return items.filter(isEditableBindingComplete).map((item) => item.id)
}
function isBindingCollapsed(bindingId: string) {
return collapsedBindingIds.value.includes(bindingId)
}
function setBindingCollapsed(bindingId: string, collapsed: boolean) {
const next = new Set(collapsedBindingIds.value)
if (collapsed) {
next.add(bindingId)
} else {
next.delete(bindingId)
}
collapsedBindingIds.value = Array.from(next)
}
function toggleBindingCollapsed(bindingId: string) {
setBindingCollapsed(bindingId, !isBindingCollapsed(bindingId))
}
function getBindingTitle(binding: EditableBinding, index: number) {
const internalName = binding.skuName.trim()
const externalName = binding.externalSkuName.trim()
const skuCode = binding.skuCode.trim()
return internalName || externalName || skuCode || `规则 ${index + 1}`
}
function getBindingStatusLabel(binding: EditableBinding) {
return isEditableBindingComplete(binding) ? '已完成' : '待完善'
}
function getBindingSummary(binding: EditableBinding) {
const parts = [
binding.provider.trim() || 'agiso',
binding.platform.trim() || '未选平台',
binding.shopId.trim() || '跨店铺',
binding.profileKey.trim() || 'manual_review',
]
return parts.join(' / ')
}
async function loadConfigs() {
if (!hasAdminRole('admin')) {
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminFulfillmentBindingConfigs()
filePath.value = response.data.filePath
const nextBindings = response.data.bindings.map(mapEditableBinding)
bindings.value = nextBindings
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
observedProducts.value = response.data.observedProducts
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败'
} finally {
loading.value = false
}
}
function addBinding() {
validationState.value = null
const next = createEmptyBinding()
setBindingCollapsed(next.id, false)
bindings.value.unshift(next)
}
function removeBinding(id: string) {
if (validationState.value?.bindingId === id) {
validationState.value = null
}
setBindingCollapsed(id, false)
bindings.value = bindings.value.filter((item) => item.id !== id)
}
function importObservedProduct(item: AdminObservedProductItem) {
validationState.value = null
const nextBinding = {
id: crypto.randomUUID(),
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shopId,
skuCode: '',
skuName: item.externalSkuName,
profileKey: 'manual_review',
enabled: true,
priority: 100,
externalSkuCode: item.externalSkuCode,
externalItemId: item.externalItemId,
externalSkuName: item.externalSkuName,
resolvedSkuName: item.externalSkuName,
}
setBindingCollapsed(nextBinding.id, false)
bindings.value.unshift(nextBinding)
}
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
return {
provider: item.provider.trim() || 'agiso',
platform: item.platform.trim(),
shopId: item.shopId.trim(),
skuCode: item.skuCode.trim(),
skuName: item.skuName.trim(),
profileKey: item.profileKey.trim() || 'manual_review',
enabled: item.enabled,
priority: Number(item.priority || 100),
match: {
externalSkuCode: item.externalSkuCode.trim(),
externalItemId: item.externalItemId.trim(),
externalSkuName: item.externalSkuName.trim(),
config: {
resolvedSkuName: item.resolvedSkuName.trim(),
},
},
}
}
function hasBindingContent(item: SaveBindingPayload) {
return Boolean(
item.platform
|| item.shopId
|| item.skuCode
|| item.skuName
|| item.match.externalSkuCode
|| item.match.externalItemId
|| item.match.externalSkuName
|| item.match.config.resolvedSkuName
|| item.provider !== 'agiso'
|| item.profileKey !== 'manual_review'
|| item.priority !== 100
|| item.enabled !== true,
)
}
function resolveBindingValidationState(item: SaveBindingPayload, index: number, bindingId: string): ValidationState {
const label = `${index + 1} 条规则`
if (!item.skuCode) {
return {
bindingId,
field: 'skuCode',
message: `${label} 还没填写“内部履约 SKU”`,
}
}
if (!item.match.externalSkuCode && !item.match.externalItemId && !item.match.externalSkuName) {
return {
bindingId,
field: 'match',
message: `${label} 至少填写一种外部匹配条件`,
}
}
if (!VALID_PROFILE_KEYS.has(item.profileKey)) {
return {
bindingId,
field: 'profileKey',
message: `${label} 的履约方式无效`,
}
}
return null
}
async function focusValidationTarget(state: ValidationState) {
if (!state?.bindingId) {
return
}
setBindingCollapsed(state.bindingId, false)
await nextTick()
const card = document.querySelector<HTMLElement>(`[data-binding-id="${state.bindingId}"]`)
if (!card) {
return
}
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
const selectors: Record<ValidationField, string> = {
skuCode: '[data-field="skuCode"]',
match: '[data-field="externalSkuCode"]',
profileKey: '[data-field="profileKey"]',
}
const target = card.querySelector<HTMLInputElement | HTMLSelectElement>(selectors[state.field])
target?.focus()
}
function clearValidationState() {
validationState.value = null
}
function isBindingInvalid(bindingId: string) {
return validationState.value?.bindingId === bindingId
}
function isFieldInvalid(bindingId: string, field: ValidationField | 'externalSkuCode' | 'externalItemId' | 'externalSkuName') {
if (validationState.value?.bindingId !== bindingId) {
return false
}
if (validationState.value.field === 'match') {
return ['externalSkuCode', 'externalItemId', 'externalSkuName'].includes(field)
}
return validationState.value.field === field
}
async function saveConfigs() {
const normalizedBindings = bindings.value.map(normalizeBindingForSave)
const nonEmptyBindings = normalizedBindings.filter(hasBindingContent)
const invalidState = normalizedBindings.reduce<ValidationState>((state, item, index) => {
if (state || !hasBindingContent(item)) {
return state
}
return resolveBindingValidationState(item, index, bindings.value[index]?.id || '')
}, null)
if (invalidState) {
validationState.value = invalidState
await focusValidationTarget(invalidState)
return
}
if (bindings.value.length > 0 && nonEmptyBindings.length === 0) {
validationState.value = {
bindingId: '',
field: 'skuCode',
message: '当前没有可保存的规则,请至少填写内部履约 SKU 和外部匹配条件',
}
return
}
const payload = nonEmptyBindings
saving.value = true
validationState.value = null
try {
const response = await saveAdminFulfillmentBindingConfigs({ bindings: payload })
filePath.value = response.data.filePath
bindings.value = response.data.bindings.map(mapEditableBinding)
ElMessage.success('履约配置已保存')
await loadConfigs()
} catch (error) {
validationState.value = {
bindingId: '',
field: 'skuCode',
message: error instanceof Error ? error.message : '保存履约配置失败',
}
} finally {
saving.value = false
}
}
onMounted(loadConfigs)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>履约配置</h1>
<p>把外部商品识别成内部履约 SKU再决定绑定库存还是转人工保存后立即同步到后端规则表</p>
</div>
<div class="header-actions">
<el-button round @click="loadConfigs">刷新</el-button>
<el-button round type="primary" @click="addBinding">新增规则</el-button>
</div>
</header>
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护履约配置</div>
<template v-else>
<section class="meta-card">
<div class="meta-line">
<span class="meta-label">配置文件</span>
<code>{{ filePath || '-' }}</code>
</div>
<div class="meta-line">
<span class="meta-label">说明</span>
<span>每条规则同时定义外部商品匹配条件内部履约 SKU / 履约方式未命中的商品会进入人工处理</span>
</div>
<div class="meta-line">
<span class="meta-label">字段怎么理解</span>
<div class="hint-grid">
<div class="hint-item">
<strong>外部 SKU 编码 / 外部商品 ID / 外部商品名</strong>
<span>这是平台或店铺回传的原始商品标识用来识别你卖的是哪一种商品</span>
</div>
<div class="hint-item">
<strong>内部履约 SKU</strong>
<span>这是你系统里的统一商品编码后续库存绑定自动履约人工处理都靠它继续往下走</span>
</div>
<div class="hint-item">
<strong>内部商品名</strong>
<span>主要给后台展示看建议填你们内部真正认识的商品名</span>
</div>
<div class="hint-item">
<strong>履约方式</strong>
<span>`腾讯领取兑换` 表示走自动库存链路`人工发货` 表示落到人工处理</span>
</div>
</div>
</div>
<div class="meta-line">
<span class="meta-label">内部履约 SKU 怎么填</span>
<div class="sku-guide">
<span>推荐用你们内部统一命名不要直接抄店铺里的外部 SKU</span>
<span>建议格式`游戏或业务_商品类型_规格`例如 `delta_havabi_500``dnf_cdk_blackdiamond``manual_peripheral_common`</span>
<span>原则是同一种履约商品无论来自哪个平台哪个店铺都尽量落到同一个内部履约 SKU</span>
</div>
</div>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">履约配置加载中</div>
<template v-else>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>已配置规则</h3>
<p>建议至少填写平台店铺外部 SKU 编码或外部商品名再指定内部履约 SKU 和履约档案</p>
</div>
<el-button :loading="saving" round type="primary" @click="saveConfigs">保存配置</el-button>
</div>
<p v-if="validationState?.message" class="validation-banner">{{ validationState.message }}</p>
<div v-if="bindings.length === 0" class="empty-inline">当前还没有履约规则先新增一条</div>
<div
v-for="(binding, index) in bindings"
:key="binding.id"
:data-binding-id="binding.id"
:class="['binding-card', { 'binding-card--invalid': isBindingInvalid(binding.id) }]"
>
<div class="binding-header">
<div class="binding-summary">
<div class="binding-title-row">
<strong>{{ getBindingTitle(binding, index) }}</strong>
<span :class="['binding-status-chip', { 'binding-status-chip--done': isEditableBindingComplete(binding) }]">
{{ getBindingStatusLabel(binding) }}
</span>
</div>
<span class="binding-subtle">{{ getBindingSummary(binding) }}</span>
</div>
<button class="collapse-button" type="button" @click="toggleBindingCollapsed(binding.id)">
{{ isBindingCollapsed(binding.id) ? '展开' : '折叠' }}
</button>
</div>
<div v-if="isBindingCollapsed(binding.id)" class="binding-collapsed-preview">
<span>外部商品{{ binding.externalSkuName || binding.externalSkuCode || binding.externalItemId || '-' }}</span>
<span>内部履约 SKU{{ binding.skuCode || '-' }}</span>
<span>履约方式{{ PROFILE_OPTIONS.find((item) => item.value === binding.profileKey)?.label || binding.profileKey }}</span>
</div>
<div v-else class="binding-grid">
<label class="field-block">
<span>渠道</span>
<input v-model="binding.provider" class="text-input" placeholder="agiso" @input="clearValidationState" />
</label>
<label class="field-block">
<span>平台</span>
<input v-model="binding.platform" class="text-input" placeholder="xianyu / taobao / pdd" @input="clearValidationState" />
</label>
<label class="field-block">
<span>店铺 ID</span>
<input v-model="binding.shopId" class="text-input" placeholder="留空表示跨店铺通用" @input="clearValidationState" />
</label>
<label class="field-block">
<span>优先级</span>
<input v-model.number="binding.priority" class="text-input" type="number" min="1" @input="clearValidationState" />
</label>
<label class="field-block">
<span>外部 SKU 编码</span>
<input
v-model="binding.externalSkuCode"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'externalSkuCode') }]"
data-field="externalSkuCode"
placeholder="例如 6046726460016"
@input="clearValidationState"
/>
</label>
<label class="field-block">
<span>外部商品 ID</span>
<input
v-model="binding.externalItemId"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'externalItemId') }]"
data-field="externalItemId"
placeholder="例如 1033324289962"
@input="clearValidationState"
/>
</label>
<label class="field-block field-wide">
<span>外部商品名</span>
<input
v-model="binding.externalSkuName"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'externalSkuName') }]"
data-field="externalSkuName"
placeholder="例如 奥利奥动作"
@input="clearValidationState"
/>
</label>
<label class="field-block">
<span>内部履约 SKU</span>
<input
v-model="binding.skuCode"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'skuCode') }]"
data-field="skuCode"
placeholder="例如 sjz_hdl_test01"
@input="clearValidationState"
/>
<small class="field-help">填你们系统内部统一 SKU相同履约商品尽量跨平台共用同一个内部 SKU</small>
</label>
<label class="field-block">
<span>内部商品名</span>
<input v-model="binding.skuName" class="text-input" placeholder="用于后台展示" @input="clearValidationState" />
</label>
<label class="field-block">
<span>履约方式</span>
<select
v-model="binding.profileKey"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'profileKey') }]"
data-field="profileKey"
@change="clearValidationState"
>
<option v-for="item in PROFILE_OPTIONS" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
</label>
<label class="field-block">
<span>匹配后展示名</span>
<input
v-model="binding.resolvedSkuName"
class="text-input"
placeholder="例如 三角洲行动-自动领取"
@input="clearValidationState"
/>
</label>
</div>
<p v-if="isBindingInvalid(binding.id)" class="binding-inline-error">{{ validationState?.message }}</p>
<div class="binding-actions">
<label class="checkbox-line">
<input v-model="binding.enabled" type="checkbox" />
<span>启用规则</span>
</label>
<el-button link type="danger" @click="removeBinding(binding.id)">删除</el-button>
</div>
</div>
</section>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>近期识别到的外部商品</h3>
<p>从已入库订单里提取的外部商品信息可一键导入规则后补充内部履约 SKU</p>
</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>来源</th>
<th>外部商品</th>
<th>订单商品数</th>
<th>最近出现时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in observedProducts" :key="`${item.provider}:${item.platform}:${item.shopId}:${item.externalSkuCode}:${item.externalItemId}:${item.externalSkuName}`">
<td>
<div class="cell-stack">
<strong>{{ item.platform || '-' }}</strong>
<span class="cell-subtle">{{ item.provider }} · {{ item.shopName || item.shopId || '全店铺' }}</span>
</div>
</td>
<td>
<div class="cell-stack">
<strong>{{ item.externalSkuName || item.externalSkuCode || item.externalItemId || '-' }}</strong>
<span class="cell-subtle">SKU: {{ item.externalSkuCode || '-' }}</span>
<span class="cell-subtle">ItemId: {{ item.externalItemId || '-' }}</span>
</div>
</td>
<td>{{ item.orderItemCount }}</td>
<td>{{ formatAdminDateTime(item.latestSeenAt) }}</td>
<td>{{ item.configured ? '已配置' : '未配置' }}</td>
<td>
<el-button
v-if="!item.configured"
link
type="primary"
@click="importObservedProduct(item)"
>
导入到规则
</el-button>
<span v-else class="cell-subtle">已存在</span>
</td>
</tr>
<tr v-if="observedProducts.length === 0">
<td colspan="6" class="empty-inline">最近还没有识别到可用的外部商品</td>
</tr>
</tbody>
</table>
</section>
</template>
</template>
</section>
</template>
<style scoped>
.admin-panel {
display: grid;
gap: 16px;
}
.panel-header,
.section-title-row {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
}
.panel-header h1,
.section-title-row h3 {
margin: 0;
color: #1d3555;
}
.panel-header p,
.section-title-row p {
margin: 8px 0 0;
color: #64748b;
}
.header-actions {
display: flex;
gap: 10px;
}
.meta-card,
.table-card,
.empty-block,
.error-copy {
padding: 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(86, 108, 138, 0.1);
}
.meta-card {
display: grid;
gap: 10px;
}
.hint-grid,
.sku-guide {
display: grid;
gap: 8px;
}
.hint-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hint-item {
display: grid;
gap: 4px;
padding: 10px 12px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.14);
}
.hint-item strong {
color: #1d3555;
font-size: 13px;
}
.hint-item span,
.sku-guide span {
color: #64748b;
font-size: 13px;
line-height: 1.6;
}
.meta-line {
display: grid;
gap: 6px;
}
.meta-label {
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.binding-card {
margin-top: 14px;
padding: 16px;
border-radius: 18px;
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.18);
}
.binding-card--invalid {
border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 0 0 4px rgba(254, 226, 226, 0.85);
}
.binding-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 12px;
}
.binding-summary {
display: grid;
gap: 4px;
}
.binding-title-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
color: #1d3555;
}
.binding-subtle {
color: #64748b;
font-size: 13px;
}
.binding-status-chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 2px 10px;
background: #fff1f2;
color: #be123c;
font-size: 12px;
font-weight: 700;
}
.binding-status-chip--done {
background: #ecfdf3;
color: #027a48;
}
.collapse-button {
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.96);
color: #1d3555;
border-radius: 999px;
padding: 8px 14px;
font: inherit;
cursor: pointer;
}
.binding-collapsed-preview {
display: grid;
gap: 6px;
color: #64748b;
font-size: 13px;
}
.binding-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.validation-banner {
margin: 14px 0 0;
padding: 12px 14px;
border-radius: 14px;
background: #fff4f2;
border: 1px solid rgba(244, 63, 94, 0.18);
color: #b42318;
font-weight: 600;
}
.field-block {
display: grid;
gap: 8px;
}
.field-block span {
color: #475467;
font-size: 12px;
font-weight: 700;
}
.field-help {
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.field-wide {
grid-column: 1 / -1;
}
.text-input {
width: 100%;
min-height: 42px;
padding: 0 14px;
border-radius: 14px;
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.96);
}
.text-input--invalid {
border-color: rgba(220, 38, 38, 0.45);
background: #fff8f8;
}
.text-input--invalid:focus {
outline: none;
border-color: rgba(220, 38, 38, 0.6);
box-shadow: 0 0 0 3px rgba(254, 226, 226, 0.95);
}
.binding-inline-error {
margin: 12px 0 0;
color: #b42318;
font-size: 13px;
font-weight: 600;
}
.binding-actions {
margin-top: 12px;
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.checkbox-line {
display: inline-flex;
gap: 8px;
align-items: center;
color: #475467;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
.data-table th,
.data-table td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
color: #334155;
vertical-align: top;
}
.data-table th {
color: #64748b;
font-size: 13px;
}
.cell-stack {
display: grid;
gap: 4px;
}
.cell-subtle {
color: #64748b;
font-size: 12px;
}
.empty-block,
.empty-inline {
color: #64748b;
}
.error-copy {
color: #b42318;
}
@media (max-width: 980px) {
.panel-header,
.section-title-row,
.binding-actions,
.binding-header {
flex-direction: column;
align-items: stretch;
}
.hint-grid,
.binding-grid {
grid-template-columns: 1fr;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -20,7 +20,8 @@ const navItems = computed(() => {
if (isAdmin.value) {
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
baseItems.push({ to: '/admin/platform-shops', label: '店铺配置' })
baseItems.push({ to: '/admin/platform-shops', label: 'Agiso店铺' })
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
}
@@ -118,7 +118,7 @@ onMounted(loadConfigs)
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>店铺配置</h1>
<h1>Agiso 店铺配置</h1>
<p>Agiso 多店铺配置改为文件维护保存后立即生效不需要再手改 `.env`</p>
</div>
<div class="header-actions">