Files
hfb_sys/frontend/src/features/admin/components/PickupShopDialog.vue
T
yml2213 f3c5235b81 feat(pickup): 店铺管理支持历史店铺可视化手动映射与一键批量清洗
- 数据库升级:新增 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构建)
2026-10-09 13:31:11 +08:00

882 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>