- 数据库升级:新增 pickup_shop_mappings 映射别名表,并在 admin_pickups 增加 raw_shop_name 永久保留原始店铺录入值 - 后端映射能力:提供历史店铺聚合统计、单条/批量映射至目标标准店铺、映射规则撤销与提号单还原接口;新建提号时若录入已映射别名自动归一化 - 前端管理升级:PickupShopDialog 升级为双 Tab 结构,Tab 2「历史店铺映射与清洗」支持总览统计卡片、状态筛选、单条/批量映射清洗、一键设为标准店铺及撤销还原 - 验证方式:执行并通过全套代码门禁(scripts/check.sh:SQL约束、go vet/build/test、MySQL测试库覆盖率、vue大小、eslint、prettier、vue-tsc及vite构建)
882 lines
28 KiB
Vue
882 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Check, Connection, Delete, Edit, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||
import {
|
||
createPickupShop,
|
||
deletePickupShop,
|
||
deleteShopMapping,
|
||
fetchHistoricalShopStats,
|
||
fetchPickupShops,
|
||
mapHistoricalShops,
|
||
updatePickupShop,
|
||
type PickupShop,
|
||
type PickupShopCreateRequest,
|
||
type PickupShopHistoricalItem,
|
||
type PickupShopHistoricalStats,
|
||
type PickupShopUpdateRequest,
|
||
} from '@/features/admin/api/adminPickup'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', value: boolean): void
|
||
(e: 'updated'): void
|
||
}>()
|
||
|
||
const activeTab = ref<'standard' | 'history'>('standard')
|
||
|
||
// ==================== 标准店铺状态 ====================
|
||
const loading = ref(false)
|
||
const shopList = ref<PickupShop[]>([])
|
||
const filters = reactive({
|
||
keyword: '',
|
||
platform: '',
|
||
status: '',
|
||
})
|
||
|
||
// 新建/编辑店铺弹窗状态
|
||
const formDialogVisible = ref(false)
|
||
const formSaving = ref(false)
|
||
const editingId = ref<number | null>(null)
|
||
const shopForm = reactive({
|
||
name: '',
|
||
platform: '',
|
||
sort_order: 0,
|
||
status: 'active' as 'active' | 'disabled',
|
||
remark: '',
|
||
})
|
||
|
||
const platformOptions = ['淘宝', '拼多多', '闲鱼', '微信', 'QQ', '抖音', '快手']
|
||
|
||
// ==================== 历史店铺映射状态 ====================
|
||
const historyLoading = ref(false)
|
||
const historyStats = ref<PickupShopHistoricalStats | null>(null)
|
||
const historyFilters = reactive({
|
||
keyword: '',
|
||
status: '' as '' | 'standard' | 'mapped' | 'unmapped',
|
||
})
|
||
const selectedHistory = ref<PickupShopHistoricalItem[]>([])
|
||
|
||
// 映射弹窗状态
|
||
const mapDialogVisible = ref(false)
|
||
const mappingSubmitting = ref(false)
|
||
const mappingTargets = ref<string[]>([])
|
||
const mappingForm = reactive({
|
||
targetShopName: '',
|
||
createTargetMissing: true,
|
||
targetPlatform: '',
|
||
remark: '',
|
||
})
|
||
|
||
function handleClose() {
|
||
emit('update:modelValue', false)
|
||
}
|
||
|
||
// -------------------- 标准店铺方法 --------------------
|
||
async function loadShops() {
|
||
loading.value = true
|
||
try {
|
||
shopList.value = await fetchPickupShops({
|
||
keyword: filters.keyword.trim(),
|
||
platform: filters.platform,
|
||
status: filters.status,
|
||
})
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || '加载店铺列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
loadShops()
|
||
}
|
||
|
||
function resetFilters() {
|
||
filters.keyword = ''
|
||
filters.platform = ''
|
||
filters.status = ''
|
||
loadShops()
|
||
}
|
||
|
||
function openCreateForm() {
|
||
editingId.value = null
|
||
shopForm.name = ''
|
||
shopForm.platform = ''
|
||
shopForm.sort_order = 0
|
||
shopForm.status = 'active'
|
||
shopForm.remark = ''
|
||
formDialogVisible.value = true
|
||
}
|
||
|
||
function openEditForm(row: PickupShop) {
|
||
editingId.value = row.id
|
||
shopForm.name = row.name
|
||
shopForm.platform = row.platform
|
||
shopForm.sort_order = row.sort_order
|
||
shopForm.status = row.status
|
||
shopForm.remark = row.remark
|
||
formDialogVisible.value = true
|
||
}
|
||
|
||
async function handleSaveShop() {
|
||
if (!shopForm.name.trim()) {
|
||
ElMessage.warning('请输入店铺名称')
|
||
return
|
||
}
|
||
formSaving.value = true
|
||
try {
|
||
if (editingId.value) {
|
||
const payload: PickupShopUpdateRequest = {
|
||
name: shopForm.name.trim(),
|
||
platform: shopForm.platform.trim(),
|
||
sort_order: Number(shopForm.sort_order) || 0,
|
||
status: shopForm.status,
|
||
remark: shopForm.remark.trim(),
|
||
}
|
||
await updatePickupShop(editingId.value, payload)
|
||
ElMessage.success('店铺更新成功')
|
||
} else {
|
||
const payload: PickupShopCreateRequest = {
|
||
name: shopForm.name.trim(),
|
||
platform: shopForm.platform.trim(),
|
||
sort_order: Number(shopForm.sort_order) || 0,
|
||
status: shopForm.status,
|
||
remark: shopForm.remark.trim(),
|
||
}
|
||
await createPickupShop(payload)
|
||
ElMessage.success('店铺创建成功')
|
||
}
|
||
formDialogVisible.value = false
|
||
await loadShops()
|
||
if (historyStats.value) {
|
||
loadHistoryStats()
|
||
}
|
||
emit('updated')
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || '保存店铺失败')
|
||
} finally {
|
||
formSaving.value = false
|
||
}
|
||
}
|
||
|
||
async function handleToggleStatus(row: PickupShop) {
|
||
const nextStatus = row.status === 'active' ? 'disabled' : 'active'
|
||
const actionText = nextStatus === 'active' ? '启用' : '禁用'
|
||
try {
|
||
await updatePickupShop(row.id, {
|
||
name: row.name,
|
||
platform: row.platform,
|
||
sort_order: row.sort_order,
|
||
status: nextStatus,
|
||
remark: row.remark,
|
||
})
|
||
ElMessage.success(`已${actionText}店铺「${row.name}」`)
|
||
row.status = nextStatus
|
||
emit('updated')
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || `${actionText}失败`)
|
||
}
|
||
}
|
||
|
||
async function handleDeleteShop(row: PickupShop) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除店铺「${row.name}」吗?历史提号中的记录不会被删除。`,
|
||
'确认删除店铺',
|
||
{
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
loading.value = true
|
||
try {
|
||
await deletePickupShop(row.id)
|
||
ElMessage.success('店铺已删除')
|
||
await loadShops()
|
||
if (historyStats.value) {
|
||
loadHistoryStats()
|
||
}
|
||
emit('updated')
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || '删除店铺失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
} catch {
|
||
/* 用户放弃 */
|
||
}
|
||
}
|
||
|
||
// -------------------- 历史店铺映射方法 --------------------
|
||
async function loadHistoryStats() {
|
||
historyLoading.value = true
|
||
try {
|
||
historyStats.value = await fetchHistoricalShopStats()
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || '加载历史店铺统计失败')
|
||
} finally {
|
||
historyLoading.value = false
|
||
}
|
||
}
|
||
|
||
const filteredHistoryList = computed(() => {
|
||
if (!historyStats.value?.items) return []
|
||
const kw = historyFilters.keyword.trim().toLowerCase()
|
||
const status = historyFilters.status
|
||
return historyStats.value.items.filter(item => {
|
||
if (status && item.mapping_status !== status) {
|
||
return false
|
||
}
|
||
if (kw) {
|
||
const sourceMatch = (item.source_name || '(未填/空)').toLowerCase().includes(kw)
|
||
const targetMatch = (item.target_shop_name || '').toLowerCase().includes(kw)
|
||
const remarkMatch = (item.remark || '').toLowerCase().includes(kw)
|
||
if (!sourceMatch && !targetMatch && !remarkMatch) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
})
|
||
})
|
||
|
||
function handleHistorySelectionChange(selection: PickupShopHistoricalItem[]) {
|
||
selectedHistory.value = selection
|
||
}
|
||
|
||
function openSingleMapDialog(row: PickupShopHistoricalItem) {
|
||
mappingTargets.value = [row.source_name]
|
||
mappingForm.targetShopName = row.target_shop_name || ''
|
||
mappingForm.createTargetMissing = true
|
||
mappingForm.targetPlatform = ''
|
||
mappingForm.remark = ''
|
||
mapDialogVisible.value = true
|
||
}
|
||
|
||
function openBatchMapDialog() {
|
||
if (selectedHistory.value.length === 0) {
|
||
ElMessage.warning('请先勾选需要映射的历史店铺')
|
||
return
|
||
}
|
||
mappingTargets.value = selectedHistory.value.map(item => item.source_name)
|
||
mappingForm.targetShopName = ''
|
||
mappingForm.createTargetMissing = true
|
||
mappingForm.targetPlatform = ''
|
||
mappingForm.remark = ''
|
||
mapDialogVisible.value = true
|
||
}
|
||
|
||
async function handleConfirmMap() {
|
||
const target = mappingForm.targetShopName.trim()
|
||
if (!target) {
|
||
ElMessage.warning('请输入或选择目标标准店铺名称')
|
||
return
|
||
}
|
||
mappingSubmitting.value = true
|
||
try {
|
||
const res = await mapHistoricalShops({
|
||
from_shop_names: mappingTargets.value,
|
||
target_shop_name: target,
|
||
create_target_missing: mappingForm.createTargetMissing,
|
||
target_platform: mappingForm.targetPlatform.trim(),
|
||
remark: mappingForm.remark.trim(),
|
||
})
|
||
ElMessage.success(
|
||
`映射成功!已同步清洗 ${res.updated_pickups_count} 单历史提号为「${res.target_shop_name}」`
|
||
)
|
||
mapDialogVisible.value = false
|
||
await Promise.all([loadHistoryStats(), loadShops()])
|
||
emit('updated')
|
||
} catch (err: unknown) {
|
||
ElMessage.error(getErrorMessage(err) || '映射清洗失败')
|
||
} finally {
|
||
mappingSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleConvertToStandard(row: PickupShopHistoricalItem) {
|
||
const shopName = row.source_name.trim()
|
||
if (!shopName) {
|
||
ElMessage.warning('历史名称为空,无法直接收录为标准店铺,请使用映射功能')
|
||
return
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定将历史店铺「${shopName}」直接收录为标准店铺吗?收录后该名称将作为标准店铺供后续选择。`,
|
||
'收录为标准店铺',
|
||
{
|
||
confirmButtonText: '确定收录',
|
||
cancelButtonText: '取消',
|
||
type: 'info',
|
||
}
|
||
)
|
||
await createPickupShop({
|
||
name: shopName,
|
||
platform: '',
|
||
sort_order: 0,
|
||
status: 'active',
|
||
remark: '历史店铺一键收录',
|
||
})
|
||
ElMessage.success(`店铺「${shopName}」已成功收录至标准店铺库`)
|
||
await Promise.all([loadHistoryStats(), loadShops()])
|
||
emit('updated')
|
||
} catch {
|
||
/* 用户放弃 */
|
||
}
|
||
}
|
||
|
||
async function handleResetMapping(row: PickupShopHistoricalItem) {
|
||
const nameLabel = row.source_name || '(未填/空)'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要撤销「${nameLabel}」的映射规则吗?历史提号单中的店铺名将被还原为原始名称。`,
|
||
'撤销映射确认',
|
||
{
|
||
confirmButtonText: '确定撤销',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
await deleteShopMapping(row.source_name)
|
||
ElMessage.success(`已撤销映射并还原历史提号单`)
|
||
await Promise.all([loadHistoryStats(), loadShops()])
|
||
emit('updated')
|
||
} catch {
|
||
/* 用户放弃 */
|
||
}
|
||
}
|
||
|
||
function handleTabChange(tabName: string | number) {
|
||
if (tabName === 'history' && !historyStats.value) {
|
||
loadHistoryStats()
|
||
}
|
||
}
|
||
|
||
function getErrorMessage(e: unknown): string {
|
||
if (e && typeof e === 'object') {
|
||
const response = (e as { response?: { data?: { message?: string } } }).response
|
||
if (response?.data?.message) return response.data.message
|
||
const message = (e as { message?: string }).message
|
||
if (message) return message
|
||
}
|
||
return ''
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadShops()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog
|
||
:model-value="props.modelValue"
|
||
title="店铺管理与历史映射"
|
||
width="min(980px, calc(100vw - 32px))"
|
||
:before-close="handleClose"
|
||
append-to-body
|
||
>
|
||
<el-tabs v-model="activeTab" class="shop-tabs" @tab-change="handleTabChange">
|
||
<!-- ==================== Tab 1: 标准店铺库 ==================== -->
|
||
<el-tab-pane label="标准店铺库" name="standard">
|
||
<div class="shop-mgr-toolbar">
|
||
<el-form inline @submit.prevent>
|
||
<el-form-item label="店铺名称">
|
||
<el-input
|
||
v-model="filters.keyword"
|
||
placeholder="搜索名称或备注"
|
||
clearable
|
||
style="width: 170px"
|
||
@keyup.enter="handleSearch"
|
||
@clear="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="平台">
|
||
<el-select
|
||
v-model="filters.platform"
|
||
placeholder="全部平台"
|
||
clearable
|
||
style="width: 120px"
|
||
@change="handleSearch"
|
||
>
|
||
<el-option
|
||
v-for="item in platformOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select
|
||
v-model="filters.status"
|
||
placeholder="全部状态"
|
||
clearable
|
||
style="width: 110px"
|
||
@change="handleSearch"
|
||
>
|
||
<el-option label="启用" value="active" />
|
||
<el-option label="禁用" value="disabled" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||
<el-button @click="resetFilters">重置</el-button>
|
||
<el-button :icon="Refresh" @click="loadShops">刷新</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="toolbar-right">
|
||
<el-button type="success" :icon="Plus" @click="openCreateForm">新增店铺</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table v-loading="loading" :data="shopList" border stripe max-height="450">
|
||
<el-table-column prop="name" label="店铺名称" min-width="150">
|
||
<template #default="{ row }">
|
||
<strong style="color: var(--el-text-color-primary)">{{ row.name }}</strong>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="platform" label="平台/渠道" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.platform" size="small" effect="plain">{{ row.platform }}</el-tag>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sort_order" label="排序权重" width="100" align="center" />
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
|
||
{{ row.status === 'active' ? '启用中' : '已禁用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="updated_at" label="更新时间" width="160">
|
||
<template #default="{ row }">{{ formatDateTime(row.updated_at) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="shop-actions">
|
||
<el-button size="small" :icon="Edit" @click="openEditForm(row)">编辑</el-button>
|
||
<el-button
|
||
size="small"
|
||
:type="row.status === 'active' ? 'warning' : 'success'"
|
||
plain
|
||
@click="handleToggleStatus(row)"
|
||
>
|
||
{{ row.status === 'active' ? '禁用' : '启用' }}
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="danger"
|
||
plain
|
||
:icon="Delete"
|
||
@click="handleDeleteShop(row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
|
||
<!-- ==================== Tab 2: 历史店铺映射与清洗 ==================== -->
|
||
<el-tab-pane label="历史店铺映射与清洗" name="history">
|
||
<!-- 统计面板 -->
|
||
<div v-if="historyStats" class="history-stats-bar">
|
||
<div class="stat-card">
|
||
<span class="stat-label">历史店铺总数</span>
|
||
<span class="stat-value">{{ historyStats.total_shops }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">已入标准库</span>
|
||
<span class="stat-value text-success">{{ historyStats.standard_count }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">已映射清洗</span>
|
||
<span class="stat-value text-primary">{{ historyStats.mapped_count }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">待映射</span>
|
||
<span class="stat-value text-warning">{{ historyStats.unmapped_count }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">涉及历史单量</span>
|
||
<span class="stat-value">{{ historyStats.total_pickups }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工具栏 -->
|
||
<div class="shop-mgr-toolbar">
|
||
<el-form inline @submit.prevent>
|
||
<el-form-item label="店铺名称">
|
||
<el-input
|
||
v-model="historyFilters.keyword"
|
||
placeholder="搜索历史或目标店铺"
|
||
clearable
|
||
style="width: 180px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="映射状态">
|
||
<el-select
|
||
v-model="historyFilters.status"
|
||
placeholder="全部状态"
|
||
clearable
|
||
style="width: 130px"
|
||
>
|
||
<el-option label="待映射" value="unmapped" />
|
||
<el-option label="已映射" value="mapped" />
|
||
<el-option label="标准店铺" value="standard" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button :icon="Refresh" @click="loadHistoryStats">刷新统计</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="toolbar-right">
|
||
<el-button
|
||
type="primary"
|
||
:icon="Connection"
|
||
:disabled="selectedHistory.length === 0"
|
||
@click="openBatchMapDialog"
|
||
>
|
||
批量映射 ({{ selectedHistory.length }})
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="historyLoading"
|
||
:data="filteredHistoryList"
|
||
border
|
||
stripe
|
||
max-height="430"
|
||
@selection-change="handleHistorySelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="45" align="center" />
|
||
<el-table-column prop="source_name" label="历史店铺名称" min-width="160">
|
||
<template #default="{ row }">
|
||
<span v-if="row.source_name">
|
||
<strong>{{ row.source_name }}</strong>
|
||
</span>
|
||
<el-tag v-else type="info" size="small">(未填店铺/空)</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="count" label="历史提号单量" width="120" sortable align="center">
|
||
<template #default="{ row }">
|
||
<el-badge
|
||
:value="row.count"
|
||
:type="row.count > 100 ? 'danger' : row.count > 20 ? 'warning' : 'info'"
|
||
class="count-badge"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="映射状态" width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.mapping_status === 'standard'" type="success" size="small">
|
||
标准店铺
|
||
</el-tag>
|
||
<el-tag v-else-if="row.mapping_status === 'mapped'" type="primary" size="small">
|
||
已映射
|
||
</el-tag>
|
||
<el-tag v-else type="warning" size="small">待映射</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="映射目标店铺" min-width="150">
|
||
<template #default="{ row }">
|
||
<span v-if="row.mapping_status === 'mapped'" class="text-primary">
|
||
➜ <strong>{{ row.target_shop_name }}</strong>
|
||
</span>
|
||
<span v-else-if="row.mapping_status === 'standard'" class="text-success">
|
||
自身为标准店铺
|
||
</span>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="映射说明" min-width="130" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="shop-actions">
|
||
<template v-if="row.mapping_status === 'unmapped'">
|
||
<el-button
|
||
v-if="row.source_name"
|
||
size="small"
|
||
type="success"
|
||
plain
|
||
:icon="Check"
|
||
@click="handleConvertToStandard(row)"
|
||
>
|
||
设为标准
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
plain
|
||
:icon="Connection"
|
||
@click="openSingleMapDialog(row)"
|
||
>
|
||
映射
|
||
</el-button>
|
||
</template>
|
||
<template v-else-if="row.mapping_status === 'mapped'">
|
||
<el-button size="small" :icon="Edit" @click="openSingleMapDialog(row)">
|
||
重映射
|
||
</el-button>
|
||
<el-button size="small" type="danger" plain @click="handleResetMapping(row)">
|
||
撤销
|
||
</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<span class="text-muted" style="font-size: 12px">已收录</span>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 新建/编辑标准店铺弹窗 -->
|
||
<el-dialog
|
||
v-model="formDialogVisible"
|
||
:title="editingId ? '编辑标准店铺' : '新增标准店铺'"
|
||
width="460px"
|
||
append-to-body
|
||
>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="店铺名称" required>
|
||
<el-input
|
||
v-model="shopForm.name"
|
||
placeholder="例如:糖浆、王大锤游戏、王大锤数娱"
|
||
maxlength="100"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="所属平台">
|
||
<el-select
|
||
v-model="shopForm.platform"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
clearable
|
||
placeholder="选择或输入平台(如 淘宝/拼多多)"
|
||
style="width: 100%"
|
||
>
|
||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="排序权重">
|
||
<el-input-number
|
||
v-model="shopForm.sort_order"
|
||
:min="0"
|
||
:max="9999"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
<div class="form-hint">权重越大的店铺在下拉选择时排在越前面</div>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-radio-group v-model="shopForm.status">
|
||
<el-radio value="active">启用</el-radio>
|
||
<el-radio value="disabled">禁用</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="备注说明">
|
||
<el-input
|
||
v-model="shopForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="填写店铺说明或客服负责人(可选)"
|
||
maxlength="255"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="formDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="formSaving" @click="handleSaveShop">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 映射配置弹窗 -->
|
||
<el-dialog v-model="mapDialogVisible" title="映射至标准店铺" width="520px" append-to-body>
|
||
<el-form label-width="110px">
|
||
<el-form-item label="待映射历史店">
|
||
<div class="mapping-targets-tags">
|
||
<el-tag
|
||
v-for="target in mappingTargets"
|
||
:key="target"
|
||
size="small"
|
||
type="info"
|
||
effect="plain"
|
||
>
|
||
{{ target || '(未填/空)' }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="form-hint">共勾选 {{ mappingTargets.length }} 个待映射历史店铺</div>
|
||
</el-form-item>
|
||
<el-form-item label="目标标准店铺" required>
|
||
<el-select
|
||
v-model="mappingForm.targetShopName"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
placeholder="选择已有标准店铺或直接输入新店名"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="item in shopList.filter(s => s.status === 'active')"
|
||
:key="item.name"
|
||
:label="item.name + (item.platform ? ` (${item.platform})` : '')"
|
||
:value="item.name"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="自动创建标准">
|
||
<el-switch
|
||
v-model="mappingForm.createTargetMissing"
|
||
active-text="若目标店铺尚不存在,自动收录至标准店铺库"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="mappingForm.createTargetMissing" label="新店所属平台">
|
||
<el-select
|
||
v-model="mappingForm.targetPlatform"
|
||
filterable
|
||
allow-create
|
||
clearable
|
||
placeholder="新收录标准店铺的平台渠道(可选)"
|
||
style="width: 100%"
|
||
>
|
||
<el-option v-for="item in platformOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="映射备注">
|
||
<el-input
|
||
v-model="mappingForm.remark"
|
||
placeholder="例如:历史客服拼写变体合并"
|
||
maxlength="255"
|
||
/>
|
||
</el-form-item>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="执行映射后,对应历史提号单的店铺名将被批量清洗更新为目标标准店铺!"
|
||
style="margin-top: 8px"
|
||
/>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="mapDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="mappingSubmitting" @click="handleConfirmMap">
|
||
确认映射并清洗数据
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<template #footer>
|
||
<el-button @click="handleClose">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.shop-tabs {
|
||
margin-top: -12px;
|
||
}
|
||
|
||
.history-stats-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
background: var(--el-fill-color-light);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 6px;
|
||
padding: 10px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.shop-mgr-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.shop-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mapping-targets-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
max-height: 100px;
|
||
overflow-y: auto;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.text-muted {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.text-success {
|
||
color: var(--el-color-success);
|
||
}
|
||
|
||
.text-primary {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.text-warning {
|
||
color: var(--el-color-warning);
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
line-height: 1.4;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.count-badge :deep(.el-badge__content) {
|
||
position: static;
|
||
transform: none;
|
||
}
|
||
</style>
|