2026-02-27 09:47:06 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="product-detail-page">
|
|
|
|
|
|
<!-- 查询条件区域:与主页一致 -->
|
|
|
|
|
|
<el-card class="query-card" shadow="never">
|
|
|
|
|
|
<div class="query-form">
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">时间区间</span>
|
|
|
|
|
|
<el-date-picker
|
|
|
|
|
|
v-model="dateRange"
|
|
|
|
|
|
type="daterange"
|
|
|
|
|
|
range-separator="至"
|
|
|
|
|
|
start-placeholder="开始日期"
|
|
|
|
|
|
end-placeholder="结束日期"
|
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
|
style="width: 260px"
|
|
|
|
|
|
:disabled-date="disabledDate"
|
|
|
|
|
|
@change="handleDateRangeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">快捷</span>
|
|
|
|
|
|
<el-button-group>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:type="activeTimeRange === 'week7' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="handleTimeRangeClick('week7')"
|
|
|
|
|
|
>
|
|
|
|
|
|
近一周
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:type="activeTimeRange === 'day15' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="handleTimeRangeClick('day15')"
|
|
|
|
|
|
>
|
|
|
|
|
|
近15天
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
:type="activeTimeRange === 'day30' ? 'primary' : 'default'"
|
|
|
|
|
|
@click="handleTimeRangeClick('day30')"
|
|
|
|
|
|
>
|
|
|
|
|
|
近30天
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-button-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-form-right">
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<el-button type="primary" @click="handleQuery" :loading="loading">查询</el-button>
|
|
|
|
|
|
<el-button @click="handleReset">重置</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="query-item query-more-toggle">
|
|
|
|
|
|
<span class="query-toggle-link" @click="queryMoreExpanded = !queryMoreExpanded">
|
|
|
|
|
|
{{ queryMoreExpanded ? '收起 ▴' : '更多条件 ▾' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="queryMoreExpanded">
|
|
|
|
|
|
<div class="query-form-more">
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">年份</span>
|
|
|
|
|
|
<el-button-group>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-for="y in yearOptions"
|
|
|
|
|
|
:key="y"
|
|
|
|
|
|
:type="activeTimeRange === `year${y}` ? 'primary' : 'default'"
|
|
|
|
|
|
@click="handleTimeRangeClick(`year${y}`)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ y }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-button-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">季节</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.season"
|
|
|
|
|
|
placeholder="请选择季节(可多选)"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
multiple
|
|
|
|
|
|
collapse-tags
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in seasonOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">正特价</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.zgj"
|
|
|
|
|
|
placeholder="请选择(可多选)"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
multiple
|
|
|
|
|
|
collapse-tags
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 160px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option label="正价" value="1" />
|
|
|
|
|
|
<el-option label="特价" value="0" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">大类</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.category"
|
|
|
|
|
|
placeholder="请选择大类(可多选)"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
multiple
|
|
|
|
|
|
collapse-tags
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in categoryOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">品牌</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="brandModel"
|
|
|
|
|
|
:placeholder="isBrandMultiSelect ? '请选择品牌(可多选)' : '请选择品牌'"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
:multiple="isBrandMultiSelect"
|
|
|
|
|
|
:collapse-tags="isBrandMultiSelect"
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in brandOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">线路</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.line"
|
|
|
|
|
|
placeholder="请选择线路(可多选)"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
multiple
|
|
|
|
|
|
collapse-tags
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in lineOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="query-item">
|
|
|
|
|
|
<span class="query-label">门店</span>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="queryParams.ckdm"
|
|
|
|
|
|
placeholder="请选择门店(可多选)"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
clearable
|
|
|
|
|
|
multiple
|
|
|
|
|
|
collapse-tags
|
|
|
|
|
|
collapse-tags-tooltip
|
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in storeOptions"
|
|
|
|
|
|
:key="opt.value"
|
|
|
|
|
|
:label="opt.label"
|
|
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 展示形式切换(放在最上面) -->
|
2026-03-03 15:36:57 +08:00
|
|
|
|
<!-- 内容卡片:展示形式切换 + 标签 + 卡片/列表内容(标签放在卡片内,不突兀) -->
|
|
|
|
|
|
<el-card class="content-card" shadow="never">
|
|
|
|
|
|
<!-- 展示形式切换:移入内容卡片内部,置于标签上方,仅保留切换按钮 -->
|
|
|
|
|
|
<div class="view-toolbar">
|
|
|
|
|
|
<div class="view-switch">
|
|
|
|
|
|
<el-radio-group v-model="displayMode" size="default">
|
|
|
|
|
|
<el-radio-button label="card">
|
|
|
|
|
|
<el-icon><Grid /></el-icon>
|
|
|
|
|
|
卡片
|
|
|
|
|
|
</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="table">
|
|
|
|
|
|
<el-icon><List /></el-icon>
|
|
|
|
|
|
列表
|
|
|
|
|
|
</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="displayMode === 'table'" class="table-search">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="tableSearchKeyword"
|
|
|
|
|
|
placeholder="搜索款号/名称"
|
|
|
|
|
|
style="width: 200px"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<el-icon><Search /></el-icon>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
2026-02-27 09:47:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 标签分类筛选:放在卡片内部 -->
|
|
|
|
|
|
<div v-if="labelOpts.length > 0" class="filter-bar">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="opt in labelOpts"
|
|
|
|
|
|
:key="opt.value"
|
2026-03-03 15:36:57 +08:00
|
|
|
|
:class="['pill', { active: labelFilter === opt.value }]"
|
2026-02-27 09:47:06 +08:00
|
|
|
|
@click="labelFilter = opt.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ opt.label }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 卡片形式:商品卡片列表 -->
|
|
|
|
|
|
<div v-show="displayMode === 'card'" v-loading="loading" class="product-cards-container">
|
|
|
|
|
|
<div v-if="productList.length === 0 && !loading" class="empty-state">
|
|
|
|
|
|
<el-empty description="暂无数据" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="product-cards-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(item, index) in visibleProductList"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="product-card-item"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="product-detail-card kb22-card">
|
|
|
|
|
|
<div class="kb22-header-bar">
|
|
|
|
|
|
<div>款号: <span class="kb22-code">{{ item.code }}</span></div>
|
|
|
|
|
|
<div class="kb22-color-badge">
|
|
|
|
|
|
<span class="kb22-color-dot" :style="{ background: getColorCode(item.color) }"></span>
|
|
|
|
|
|
{{ item.color }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-card-body">
|
|
|
|
|
|
<div class="kb22-media-row">
|
|
|
|
|
|
<div class="kb22-thumb-box">
|
|
|
|
|
|
<img v-if="item.imageUrl" :src="item.imageUrl" alt="" class="kb22-thumb-img" />
|
|
|
|
|
|
<div v-else class="kb22-no-img"><el-icon :size="32"><Picture /></el-icon></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-info-col">
|
|
|
|
|
|
<div class="kb22-prod-title">{{ item.name }}</div>
|
|
|
|
|
|
<div class="kb22-tags-container">
|
|
|
|
|
|
<span class="kb22-tag-pill kb22-tag-blue">{{ item.season }}</span>
|
|
|
|
|
|
<span class="kb22-tag-pill kb22-tag-purple">{{ item.discount }}</span>
|
|
|
|
|
|
<el-tag v-if="item.type" :type="item.type === '畅销款' ? 'success' : item.type === '滞销款' ? 'danger' : 'info'" size="small" style="margin-left: 4px;">
|
|
|
|
|
|
{{ item.type }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-price-section">
|
|
|
|
|
|
<span class="kb22-current-price">¥{{ item.sellingPrice ?? 0 }}</span>
|
|
|
|
|
|
<span class="kb22-cost-price">¥{{ item.purchasePrice ?? 0 }}</span>
|
|
|
|
|
|
<span class="kb22-margin-text">毛利 {{ formatGrossMarginPct(item.grossMargin, item.purchasePrice != null && item.sellingPrice != null ? (1 - item.purchasePrice / item.sellingPrice) * 100 : undefined) }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-stats-grid">
|
|
|
|
|
|
<div class="kb22-stat-card kb22-sales">
|
|
|
|
|
|
<div class="kb22-stat-label">总销量</div>
|
|
|
|
|
|
<div class="kb22-stat-value">{{ item.salesCount ?? 0 }}<span class="kb22-stat-sub">件</span></div>
|
|
|
|
|
|
<div class="kb22-sales-footer">
|
|
|
|
|
|
<div class="kb22-mini-progress">
|
|
|
|
|
|
<div class="kb22-mini-fill" :style="{ width: Math.min(100, item.selloutRate ?? 0) + '%' }"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-rate-text">售罄率 {{ item.selloutRate ?? 0 }}%</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-stat-card kb22-stock">
|
|
|
|
|
|
<div class="kb22-stat-label">当前库存</div>
|
|
|
|
|
|
<div class="kb22-stat-value kcsl-display">{{ item.kcslRaw || (item.inventoryCount != null ? item.inventoryCount + '件' : '0件') }}</div>
|
|
|
|
|
|
<div v-if="item.turnoverText" class="kb22-turnover-text" :class="uiTextClass(item.turnoverStatus)">周转: {{ item.turnoverText }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="item.sizes && item.sizes.length" class="kb22-stock-footer">
|
|
|
|
|
|
<div class="kb22-section-header">
|
|
|
|
|
|
<span>SKU明细 ({{ item.sizes.length }}码)</span>
|
|
|
|
|
|
<span v-if="hasOutOfStockSize(item.sizes)" class="kb22-alert-tip">⚠️ 有断货</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-size-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(size, i) in item.sizes"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
class="kb22-size-cell"
|
|
|
|
|
|
:class="{ 'kb22-alert': size.status === 'out' || size.stock === 0 }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="size.status === 'out' || size.stock === 0" class="kb22-alert-badge">补</div>
|
|
|
|
|
|
<span class="kb22-sz-tag">{{ size.label }}</span>
|
|
|
|
|
|
<div class="kb22-sz-data-row">
|
|
|
|
|
|
<span class="kb22-sz-stock">{{ size.stock != null ? size.stock : '—' }}</span>
|
|
|
|
|
|
<span class="kb22-sz-sales">销 {{ size.sales != null ? size.sales : '—' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="kb22-legend">
|
|
|
|
|
|
<div class="kb22-legend-item"><span class="kb22-dot" style="background:#0f172a"></span>库存</div>
|
|
|
|
|
|
<div class="kb22-legend-item"><span class="kb22-dot" style="background:#3b82f6"></span>销量</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 列表形式:商品明细表格(与主页商品明细列表一致,放在同一张内容卡片内) -->
|
|
|
|
|
|
<div v-show="displayMode === 'table'" class="detail-table-wrap">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="filteredTableList"
|
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
|
border
|
|
|
|
|
|
stripe
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
row-class-name="product-list-row-clickable"
|
|
|
|
|
|
@row-click="handleDetailRowClick"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="productInfo" label="商品信息" width="280">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="product-info">
|
|
|
|
|
|
<div class="product-image">
|
|
|
|
|
|
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
|
|
|
|
|
<el-icon v-else><Picture /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="product-details">
|
|
|
|
|
|
<div class="product-code">款号: {{ row.code || '-' }}</div>
|
|
|
|
|
|
<div class="product-code">条码: {{ row.barcode || '-' }}</div>
|
|
|
|
|
|
<div class="product-code">颜色: {{ row.color }}</div>
|
|
|
|
|
|
<div class="product-code">进价: ¥{{ formatNumber(row.purchasePrice || 0) }}</div>
|
|
|
|
|
|
<div class="product-code">售价: ¥{{ formatNumber(row.sellingPrice || 0) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="category" label="商品属性" width="200" show-overflow-tooltip>
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>{{ row.category }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="j7sl" label="近7天销售" align="right" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="font-bold">{{ row.j7slRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="kcsl" label="库存数量" align="right" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="font-bold kcsl-display">{{ row.kcslRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="zksql" label="平均折扣/售罄率" align="center" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>{{ row.zksqlRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="shdxd" label="上货店/动销店" align="center" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>{{ row.shdxdRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="wsdp" label="未上店铺" align="center" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>{{ row.wsdpRaw || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="sizeStatus" label="断码监控 (S/M/L/XL)" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="size-status">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="(size, i) in row.sizes"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
class="size-badge"
|
|
|
|
|
|
:class="sizeChipClass(size.status)"
|
|
|
|
|
|
:title="size.title"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ size.label }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div :class="uiTextClass(row.sizeStatusStatus)">{{ row.sizeStatusText }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="supplierName" label="供货商" width="140" show-overflow-tooltip>
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div>{{ row.supplierName || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="type" label="商品类型" width="120" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag v-if="row.type" :type="row.type === '畅销款' ? 'success' : row.type === '滞销款' ? 'danger' : 'info'" size="small">
|
|
|
|
|
|
{{ row.type }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" align="center" width="100" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-button :type="row.actionType" size="small" @click.stop="handleDetailAction(row)">
|
|
|
|
|
|
{{ row.actionText }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
|
|
|
|
|
import { useUserStore } from '@/store/modules/user'
|
|
|
|
|
|
import type { ProductDetailData, ProductSizeStatus, UiStatus } from './index.vue'
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'ProductDetailPage' })
|
|
|
|
|
|
|
|
|
|
|
|
const REPORT_ID = 6
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const productList = ref<ProductDetailData[]>([])
|
|
|
|
|
|
/** 标签分类筛选 */
|
|
|
|
|
|
const labelFilter = ref<string>('all')
|
|
|
|
|
|
/** 展示形式:卡片 | 列表 */
|
|
|
|
|
|
const displayMode = ref<'card' | 'table'>('card')
|
|
|
|
|
|
/** 列表模式下的搜索关键字 */
|
|
|
|
|
|
const tableSearchKeyword = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// 从路由参数获取spdm
|
|
|
|
|
|
const spdmFromRoute = computed(() => route.query.spdm as string || '')
|
|
|
|
|
|
|
|
|
|
|
|
const username = computed(() => {
|
|
|
|
|
|
return userStore.user?.username ?? ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 查询参数(与主页一致)
|
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
|
rq: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
|
rq2: dayjs().format('YYYY-MM-DD'),
|
|
|
|
|
|
ckdm: [] as string[],
|
|
|
|
|
|
pp: [] as string[],
|
|
|
|
|
|
season: [] as string[],
|
|
|
|
|
|
zgj: [] as string[],
|
|
|
|
|
|
category: [] as string[],
|
|
|
|
|
|
line: [] as string[],
|
|
|
|
|
|
spdm: '' // 商品代码
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化spdm
|
|
|
|
|
|
queryParams.spdm = spdmFromRoute.value
|
|
|
|
|
|
|
|
|
|
|
|
// 从路由参数初始化查询条件
|
|
|
|
|
|
function initQueryParamsFromRoute() {
|
|
|
|
|
|
const q = route.query
|
|
|
|
|
|
if (q.rq) queryParams.rq = String(q.rq)
|
|
|
|
|
|
if (q.rq2) queryParams.rq2 = String(q.rq2)
|
|
|
|
|
|
if (q.ckdm) queryParams.ckdm = String(q.ckdm).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.pp) queryParams.pp = String(q.pp).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.season) queryParams.season = String(q.season).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.zgj) queryParams.zgj = String(q.zgj).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.category) queryParams.category = String(q.category).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
|
|
|
|
|
if (q.spdm) queryParams.spdm = String(q.spdm)
|
|
|
|
|
|
dateRange.value = [queryParams.rq, queryParams.rq2]
|
2026-03-03 15:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据路由传入的时间区间反推当前快捷按钮
|
|
|
|
|
|
try {
|
|
|
|
|
|
const rq = queryParams.rq
|
|
|
|
|
|
const rq2 = queryParams.rq2
|
|
|
|
|
|
const today = dayjs().format('YYYY-MM-DD')
|
|
|
|
|
|
|
|
|
|
|
|
// 仅在 rq2 为今天时才尝试匹配 week7/day15/day30 或年份
|
|
|
|
|
|
if (rq && rq2 && rq2 === today) {
|
|
|
|
|
|
const diff = dayjs(today).diff(dayjs(rq), 'day')
|
|
|
|
|
|
if (diff === 6) {
|
|
|
|
|
|
activeTimeRange.value = 'week7'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (diff === 14) {
|
|
|
|
|
|
activeTimeRange.value = 'day15'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (diff === 29) {
|
|
|
|
|
|
activeTimeRange.value = 'day30'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试匹配全年快捷:从某年1月1日到该年末或今天
|
|
|
|
|
|
if (rq && rq2) {
|
|
|
|
|
|
const startYear = dayjs(rq).year()
|
|
|
|
|
|
const yearStart = dayjs(`${startYear}-01-01`).format('YYYY-MM-DD')
|
|
|
|
|
|
const yearEnd = startYear === dayjs().year() ? today : dayjs(`${startYear}-12-31`).format('YYYY-MM-DD')
|
|
|
|
|
|
if (rq === yearStart && rq2 === yearEnd) {
|
|
|
|
|
|
activeTimeRange.value = `year${startYear}`
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 其余情况,不高亮任何快捷按钮
|
|
|
|
|
|
activeTimeRange.value = ''
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
activeTimeRange.value = ''
|
|
|
|
|
|
}
|
2026-02-27 09:47:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下拉选项(门店通过 executeTable tableName=kehu 获取)
|
|
|
|
|
|
const storeOptions = ref<{ label: string; value: string }[]>([])
|
|
|
|
|
|
const brandOptions = ref<{ label: string; value: string }[]>([])
|
|
|
|
|
|
const seasonOptions = ref<{ label: string; value: string }[]>([])
|
|
|
|
|
|
const categoryOptions = ref<{ label: string; value: string }[]>([])
|
|
|
|
|
|
const lineOptions = ref<{ label: string; value: string }[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
// 年份快捷选项
|
|
|
|
|
|
const yearOptions = computed(() => {
|
|
|
|
|
|
const current = dayjs().year()
|
|
|
|
|
|
return Array.from({ length: 4 }, (_, i) => current - i)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 品牌多选权限
|
|
|
|
|
|
const isBrandMultiSelect = computed(() => {
|
|
|
|
|
|
const roles = userStore.getRoles ?? []
|
|
|
|
|
|
return Array.isArray(roles) && (roles.includes('经理') || roles.includes('manager'))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 品牌选择双向绑定
|
|
|
|
|
|
const brandModel = computed({
|
|
|
|
|
|
get: () => (isBrandMultiSelect.value ? queryParams.pp : queryParams.pp[0] ?? ''),
|
|
|
|
|
|
set: (val: string | string[]) => {
|
|
|
|
|
|
queryParams.pp = Array.isArray(val) ? val : val ? [val] : []
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 日期范围
|
|
|
|
|
|
const dateRange = ref<[string, string] | null>([
|
|
|
|
|
|
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
|
dayjs().format('YYYY-MM-DD')
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
// 更多查询条件折叠
|
|
|
|
|
|
const queryMoreExpanded = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 时间范围快捷按钮
|
|
|
|
|
|
const activeTimeRange = ref<string>('week7')
|
|
|
|
|
|
|
|
|
|
|
|
// 禁用超过当天的日期
|
|
|
|
|
|
const disabledDate = (time: Date) => time.getTime() > Date.now()
|
|
|
|
|
|
|
|
|
|
|
|
// 计算时间区间
|
|
|
|
|
|
const getTimeRange = (range: string) => {
|
|
|
|
|
|
const today = dayjs()
|
|
|
|
|
|
let start: dayjs.Dayjs, end: dayjs.Dayjs = today
|
|
|
|
|
|
if (range.startsWith('year')) {
|
|
|
|
|
|
const year = parseInt(range.replace('year', ''), 10)
|
|
|
|
|
|
start = dayjs(`${year}-01-01`)
|
|
|
|
|
|
end = year === today.year() ? today : dayjs(`${year}-12-31`)
|
|
|
|
|
|
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (range) {
|
|
|
|
|
|
case 'week7':
|
|
|
|
|
|
start = today.subtract(6, 'day')
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'day15':
|
|
|
|
|
|
start = today.subtract(14, 'day')
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'day30':
|
|
|
|
|
|
start = today.subtract(29, 'day')
|
|
|
|
|
|
break
|
|
|
|
|
|
default:
|
|
|
|
|
|
start = today.subtract(6, 'day')
|
|
|
|
|
|
}
|
|
|
|
|
|
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 日期范围变化
|
|
|
|
|
|
const handleDateRangeChange = (val: [string, string] | null) => {
|
|
|
|
|
|
if (val && val.length === 2) {
|
|
|
|
|
|
queryParams.rq = val[0]
|
|
|
|
|
|
queryParams.rq2 = val[1]
|
|
|
|
|
|
activeTimeRange.value = ''
|
|
|
|
|
|
} else {
|
|
|
|
|
|
queryParams.rq = ''
|
|
|
|
|
|
queryParams.rq2 = ''
|
|
|
|
|
|
dateRange.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 快捷时间按钮点击
|
|
|
|
|
|
const handleTimeRangeClick = (range: string) => {
|
|
|
|
|
|
activeTimeRange.value = range
|
|
|
|
|
|
const timeRange = getTimeRange(range)
|
|
|
|
|
|
queryParams.rq = timeRange.rq
|
|
|
|
|
|
queryParams.rq2 = timeRange.rq2
|
|
|
|
|
|
dateRange.value = [timeRange.rq, timeRange.rq2]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数组转查询字符串
|
|
|
|
|
|
const arrToQuery = (arr: string[]) => (Array.isArray(arr) && arr.length > 0 ? arr.join(',') : '')
|
|
|
|
|
|
|
|
|
|
|
|
// 解析接口返回数组
|
|
|
|
|
|
function resolveTableList(res: any): any[] {
|
|
|
|
|
|
if (res == null) return []
|
|
|
|
|
|
if (Array.isArray(res)) return res
|
|
|
|
|
|
const data = (res as any).data
|
|
|
|
|
|
if (Array.isArray(data)) return data
|
|
|
|
|
|
if (data && typeof data === 'object') {
|
|
|
|
|
|
const inner = data.list ?? data.data ?? data.result ?? data.rows
|
|
|
|
|
|
if (Array.isArray(inner)) return inner
|
|
|
|
|
|
}
|
|
|
|
|
|
const list = (res as any).list ?? (res as any).result ?? (res as any).rows
|
|
|
|
|
|
return Array.isArray(list) ? list : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 映射函数
|
|
|
|
|
|
function mapJijieToOptions(list: any): { label: string; value: string }[] {
|
|
|
|
|
|
if (!Array.isArray(list)) return []
|
|
|
|
|
|
return list.map((item: any) => ({
|
|
|
|
|
|
label: item?.JJMC ?? '',
|
|
|
|
|
|
value: item?.JJDM != null ? String(item.JJDM) : ''
|
|
|
|
|
|
})).filter((o) => o.label && o.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapPinpaiToOptions(list: any): { label: string; value: string }[] {
|
|
|
|
|
|
if (!Array.isArray(list)) return []
|
|
|
|
|
|
return list.map((item: any) => ({
|
|
|
|
|
|
label: item?.PPMC ?? '',
|
|
|
|
|
|
value: item?.PPDM != null ? String(item.PPDM) : ''
|
|
|
|
|
|
})).filter((o) => o.label && o.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapDaleiToOptions(list: any): { label: string; value: string }[] {
|
|
|
|
|
|
if (!Array.isArray(list)) return []
|
|
|
|
|
|
return list.map((item: any) => ({
|
|
|
|
|
|
label: item?.DLMC ?? '',
|
|
|
|
|
|
value: item?.DLDM != null ? String(item.DLDM) : ''
|
|
|
|
|
|
})).filter((o) => o.label && o.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** kehu 返回:{ khmc, khdm };兼容后端返回大类结构 { DLMC, DLDM } */
|
|
|
|
|
|
function mapKehuToOptions(list: any): { label: string; value: string }[] {
|
|
|
|
|
|
if (!Array.isArray(list)) return []
|
|
|
|
|
|
return list.map((item: any) => {
|
|
|
|
|
|
const label = item?.khmc ?? item?.CKMC ?? item?.KHMC ?? item?.DLMC ?? item?.label ?? item?.name ?? ''
|
|
|
|
|
|
const value = item?.khdm != null ? String(item.khdm) : item?.CKDM != null ? String(item.CKDM) : item?.KHDM != null ? String(item.KHDM) : item?.DLDM != null ? String(item.DLDM) : item?.value != null ? String(item.value) : item?.code != null ? String(item.code) : ''
|
|
|
|
|
|
return { label, value }
|
|
|
|
|
|
}).filter((o) => o.label && o.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 拉取下拉选项
|
|
|
|
|
|
async function fetchBrandOptions() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'pinpai' })
|
|
|
|
|
|
brandOptions.value = mapPinpaiToOptions(resolveTableList(res))
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
brandOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchSeasonOptions() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'jijie' })
|
|
|
|
|
|
seasonOptions.value = mapJijieToOptions(resolveTableList(res))
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
seasonOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchCategoryOptions() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'dalei' })
|
|
|
|
|
|
categoryOptions.value = mapDaleiToOptions(resolveTableList(res))
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
categoryOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchStoreOptions() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'kehu' })
|
|
|
|
|
|
storeOptions.value = mapKehuToOptions(resolveTableList(res))
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
storeOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 线路选项:暂不查后端,使用空默认值 */
|
|
|
|
|
|
function fetchLineOptions() {
|
|
|
|
|
|
lineOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询条件默认值:品牌有数据且含 1 则默认 1,门店默认全选
|
|
|
|
|
|
function applyQueryDefaults() {
|
|
|
|
|
|
const brands = brandOptions.value
|
|
|
|
|
|
if (queryParams.pp.length === 0 && Array.isArray(brands) && brands.length > 0 && brands.some((b) => b.value === '1')) {
|
|
|
|
|
|
queryParams.pp = ['1']
|
|
|
|
|
|
}
|
|
|
|
|
|
const stores = storeOptions.value
|
|
|
|
|
|
if (Array.isArray(stores) && stores.length > 0 && queryParams.ckdm.length === 0) {
|
|
|
|
|
|
queryParams.ckdm = stores.map((s) => s.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工具函数(从index.vue复制)
|
|
|
|
|
|
function parseSizeColors(cmStr: string): Array<{ label: string; color: string }> {
|
|
|
|
|
|
if (!cmStr || typeof cmStr !== 'string') return []
|
|
|
|
|
|
return cmStr
|
|
|
|
|
|
.split(';')
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
const [label, color] = item.split(',')
|
|
|
|
|
|
return { label: label?.trim() || '', color: color?.trim() || 'flat' }
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((item) => item.label)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseSizeDetails(cmjsStr: string): Array<{ label: string; stock: number; color: string }> {
|
|
|
|
|
|
if (!cmjsStr || typeof cmjsStr !== 'string') return []
|
|
|
|
|
|
return cmjsStr
|
|
|
|
|
|
.split(';')
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
const parts = item.split(',')
|
|
|
|
|
|
const label = parts[0]?.trim() || ''
|
|
|
|
|
|
const stock = Number(parts[1]?.trim()) || 0
|
|
|
|
|
|
let color = (parts[2]?.trim() || 'flat').toLowerCase()
|
|
|
|
|
|
if (color === 'yello') color = 'yellow'
|
|
|
|
|
|
return { label, stock, color }
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((item) => item.label)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseSalesData(lsStr: string): { amount: number; count: number } {
|
|
|
|
|
|
if (!lsStr || typeof lsStr !== 'string') {
|
|
|
|
|
|
return { amount: 0, count: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
const parts = lsStr.split('/')
|
|
|
|
|
|
let amount = 0
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 1) {
|
|
|
|
|
|
const countMatch = parts[0].match(/(\d+)/)
|
|
|
|
|
|
if (countMatch) {
|
|
|
|
|
|
count = Number(countMatch[1]) || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 2) {
|
|
|
|
|
|
const amountMatch = parts[1].match(/(\d+)/)
|
|
|
|
|
|
if (amountMatch) {
|
|
|
|
|
|
amount = Number(amountMatch[1]) || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { amount, count }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseDiscountAndSelloutRate(zksqlStr: string): { discount: string; selloutRate: number } {
|
|
|
|
|
|
if (!zksqlStr || typeof zksqlStr !== 'string') {
|
|
|
|
|
|
return { discount: '-', selloutRate: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
const parts = zksqlStr.split(',').map((s) => s.trim())
|
|
|
|
|
|
let discount = '-'
|
|
|
|
|
|
let selloutRate = 0
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 1) {
|
|
|
|
|
|
const discountVal = parts[0]
|
|
|
|
|
|
if (discountVal.includes('折')) {
|
|
|
|
|
|
discount = discountVal
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const num = Number(discountVal)
|
|
|
|
|
|
if (!isNaN(num) && num > 0 && num <= 1) {
|
|
|
|
|
|
discount = `${Math.round(num * 100) / 10}折`
|
|
|
|
|
|
} else if (!isNaN(num) && num > 1 && num <= 10) {
|
|
|
|
|
|
discount = `${num}折`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 2) {
|
|
|
|
|
|
const rateVal = parts[1].replace('%', '')
|
|
|
|
|
|
const rate = Number(rateVal)
|
|
|
|
|
|
if (!isNaN(rate)) {
|
|
|
|
|
|
selloutRate = rate > 100 ? rate / 100 : rate
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { discount, selloutRate }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapColorToSizeStatus(color: string, stock: number): 'out' | 'low' | 'warn' | 'ok' {
|
|
|
|
|
|
if (stock === 0) return 'out'
|
|
|
|
|
|
const colorLower = color.toLowerCase()
|
|
|
|
|
|
if (colorLower === 'red') {
|
|
|
|
|
|
return stock <= 5 ? 'out' : stock <= 20 ? 'low' : 'warn'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (colorLower === 'yellow') {
|
|
|
|
|
|
return 'warn'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'ok'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSizeTitle(label: string, status: string, stock: number): string {
|
|
|
|
|
|
if (status === 'out' || stock === 0) return `${label}码 缺货`
|
|
|
|
|
|
if (status === 'low') return `${label}码 紧张`
|
|
|
|
|
|
if (status === 'warn') return `${label}码 需关注`
|
|
|
|
|
|
return `${label}码 充足`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSelloutRateStatus(rate: number): UiStatus {
|
|
|
|
|
|
if (rate >= 80) return 'danger'
|
|
|
|
|
|
if (rate >= 60) return 'success'
|
|
|
|
|
|
if (rate >= 40) return 'info'
|
|
|
|
|
|
return 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getDiscountStatus(discount: string): UiStatus {
|
|
|
|
|
|
if (!discount || discount === '-') return 'info'
|
|
|
|
|
|
const match = discount.match(/(\d+(?:\.\d+)?)/)
|
|
|
|
|
|
if (!match) return 'info'
|
|
|
|
|
|
const val = Number(match[1])
|
|
|
|
|
|
if (val >= 9) return 'info'
|
|
|
|
|
|
if (val >= 7) return 'warning'
|
|
|
|
|
|
return 'danger'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 毛利显示为两位小数(支持接口返回比例 0.34 或百分比 34) */
|
|
|
|
|
|
function formatGrossMarginPct(val: number | null | undefined, fallback?: number): string {
|
|
|
|
|
|
const n = val != null ? Number(val) : (fallback != null ? fallback : 0)
|
|
|
|
|
|
if (typeof n !== 'number' || isNaN(n)) return '0.00'
|
|
|
|
|
|
const pct = n > 0 && n <= 1 ? n * 100 : n
|
|
|
|
|
|
return Number(pct).toFixed(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getColorCode(colorName: string): string {
|
|
|
|
|
|
const colorMap: Record<string, string> = {
|
|
|
|
|
|
'黑色': '#1a1a1a',
|
|
|
|
|
|
'白色': '#f5f5f5',
|
|
|
|
|
|
'红色': '#e53935',
|
|
|
|
|
|
'蓝色': '#1e88e5',
|
|
|
|
|
|
'绿色': '#43a047',
|
|
|
|
|
|
'黄色': '#fdd835',
|
|
|
|
|
|
'橙色': '#fb8c00',
|
|
|
|
|
|
'紫色': '#8e24aa',
|
|
|
|
|
|
'粉色': '#ec407a',
|
|
|
|
|
|
'灰色': '#757575',
|
|
|
|
|
|
'米白': '#f5f5dc',
|
|
|
|
|
|
'米色': '#f5f5dc',
|
|
|
|
|
|
'驼色': '#c19a6b',
|
|
|
|
|
|
'卡其色': '#c3b091',
|
|
|
|
|
|
'藏青': '#1a237e',
|
|
|
|
|
|
'深蓝': '#0d47a1',
|
|
|
|
|
|
'浅蓝': '#64b5f6',
|
|
|
|
|
|
'天蓝': '#03a9f4',
|
|
|
|
|
|
'深灰': '#424242',
|
|
|
|
|
|
'浅灰': '#bdbdbd',
|
|
|
|
|
|
'咖啡色': '#795548',
|
|
|
|
|
|
'棕色': '#8d6e63',
|
|
|
|
|
|
'酒红': '#880e4f',
|
|
|
|
|
|
'墨绿': '#1b5e20',
|
|
|
|
|
|
'军绿': '#558b2f',
|
|
|
|
|
|
'杏色': '#ffcc80',
|
|
|
|
|
|
'奶白': '#fffaf0',
|
|
|
|
|
|
'藕粉': '#ffcdd2',
|
|
|
|
|
|
'均色': '#9e9e9e',
|
|
|
|
|
|
'淡蓝': '#64b5f6',
|
|
|
|
|
|
'兰色': '#1e88e5',
|
|
|
|
|
|
'枣红': '#880e4f'
|
|
|
|
|
|
}
|
|
|
|
|
|
return colorMap[colorName] || '#9e9e9e'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapApiRowToProductDetail(row: any): ProductDetailData {
|
|
|
|
|
|
const salesData = parseSalesData(row.ls || '')
|
|
|
|
|
|
const sizeDetails = parseSizeDetails(row.cmjs || '')
|
|
|
|
|
|
const sizeColors = parseSizeColors(row.cm || '')
|
|
|
|
|
|
const sizes: ProductSizeStatus[] = sizeDetails.length > 0
|
|
|
|
|
|
? sizeDetails.map((item) => {
|
|
|
|
|
|
const status = mapColorToSizeStatus(item.color, item.stock)
|
|
|
|
|
|
return {
|
|
|
|
|
|
label: item.label,
|
|
|
|
|
|
status,
|
|
|
|
|
|
title: getSizeTitle(item.label, status, item.stock),
|
|
|
|
|
|
stock: item.stock,
|
|
|
|
|
|
sales: undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
: sizeColors.map((item) => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
label: item.label,
|
|
|
|
|
|
status: item.color === 'red' ? 'low' : item.color === 'yellow' ? 'warn' : 'ok',
|
|
|
|
|
|
title: `${item.label}码`,
|
|
|
|
|
|
stock: undefined,
|
|
|
|
|
|
sales: undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { discount, selloutRate } = parseDiscountAndSelloutRate(row.zksql || '')
|
|
|
|
|
|
let finalSelloutRate = selloutRate
|
|
|
|
|
|
if (finalSelloutRate === 0 && row.ls != null && row.kcsl != null) {
|
|
|
|
|
|
const sold = salesData.count || 0
|
|
|
|
|
|
const kcslStr = String(row.kcsl || '').split('\r\n')[0].trim()
|
|
|
|
|
|
const total = Number(kcslStr) || 0
|
|
|
|
|
|
if (total > 0) {
|
|
|
|
|
|
finalSelloutRate = Math.round((sold / (sold + total)) * 1000) / 10
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let finalDiscount = discount
|
|
|
|
|
|
if (finalDiscount === '-' && row.jj != null && row.sj != null) {
|
|
|
|
|
|
const purchasePrice = Number(row.jj) || 0
|
|
|
|
|
|
const sellingPrice = Number(row.sj) || 0
|
|
|
|
|
|
if (sellingPrice > 0) {
|
|
|
|
|
|
const discountRate = purchasePrice / sellingPrice
|
|
|
|
|
|
finalDiscount = `${Math.round(discountRate * 100) / 10}折`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const purchasePrice = Number(row.jj) || 0
|
|
|
|
|
|
const sellingPrice = Number(row.sj) || 0
|
|
|
|
|
|
const grossMargin = sellingPrice > 0 ? (sellingPrice - purchasePrice) / sellingPrice : 0
|
|
|
|
|
|
const kcslStr = String(row.kcsl || '').split('\r\n')[0].trim()
|
|
|
|
|
|
const inventoryCount = Number(kcslStr) || 0
|
|
|
|
|
|
|
|
|
|
|
|
const turnoverDays = 0
|
|
|
|
|
|
const turnoverText = ''
|
|
|
|
|
|
const turnoverStatus: UiStatus = 'info'
|
|
|
|
|
|
|
|
|
|
|
|
const outOfStockSizes = sizes.filter((s) => s.status === 'out' || s.stock === 0)
|
|
|
|
|
|
let sizeStatusText = ''
|
|
|
|
|
|
let sizeStatusStatus: UiStatus = 'info'
|
|
|
|
|
|
if (outOfStockSizes.length > 0) {
|
|
|
|
|
|
sizeStatusText = `缺${outOfStockSizes.map((s) => s.label).join('/')}码`
|
|
|
|
|
|
sizeStatusStatus = 'danger'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const lowStockSizes = sizes.filter((s) => s.status === 'low' || (s.stock != null && s.stock < 10))
|
|
|
|
|
|
if (lowStockSizes.length > 0) {
|
|
|
|
|
|
sizeStatusText = `${lowStockSizes.map((s) => s.label).join('/')}码需补货`
|
|
|
|
|
|
sizeStatusStatus = 'warning'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sizeStatusText = '库存齐色齐码'
|
|
|
|
|
|
sizeStatusStatus = 'info'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let lifecycle = '正常销售'
|
|
|
|
|
|
let lifecycleType: 'success' | 'danger' | 'info' | 'warning' = 'info'
|
|
|
|
|
|
let lifecycleIcon = 'ep:circle-check'
|
|
|
|
|
|
const salesCountForLifecycle = salesData.count || 0
|
|
|
|
|
|
if (finalSelloutRate >= 70 && salesCountForLifecycle > 100) {
|
|
|
|
|
|
lifecycle = '爆发成长期'
|
|
|
|
|
|
lifecycleType = 'success'
|
|
|
|
|
|
lifecycleIcon = 'fa-solid:fire-alt'
|
|
|
|
|
|
} else if (finalSelloutRate < 20) {
|
|
|
|
|
|
lifecycle = '衰退期'
|
|
|
|
|
|
lifecycleType = 'danger'
|
|
|
|
|
|
lifecycleIcon = 'ep:circle-close'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let actionText = '分析'
|
|
|
|
|
|
let actionType: 'primary' | 'warning' | 'default' | 'danger' = 'default'
|
|
|
|
|
|
if (outOfStockSizes.length > 0) {
|
|
|
|
|
|
actionText = '补货'
|
|
|
|
|
|
actionType = 'primary'
|
|
|
|
|
|
} else if (finalSelloutRate < 20) {
|
|
|
|
|
|
actionText = '清仓'
|
|
|
|
|
|
actionType = 'danger'
|
|
|
|
|
|
} else if (finalDiscount.includes('5') || finalDiscount.includes('6')) {
|
|
|
|
|
|
actionText = '调价'
|
|
|
|
|
|
actionType = 'warning'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const productCode = String(row.spdm || '') + String(row.spmc || '')
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: row.spsx || row.spdm || '-',
|
|
|
|
|
|
code: productCode || String(row.spdm || ''),
|
|
|
|
|
|
spdm: String(row.spdm || ''),
|
|
|
|
|
|
color: row.ysmc || '-',
|
|
|
|
|
|
season: '-',
|
|
|
|
|
|
category: row.spsx || '-',
|
|
|
|
|
|
daysOnMarket: 0,
|
|
|
|
|
|
salesAmount: salesData.amount,
|
|
|
|
|
|
salesCount: salesData.count,
|
|
|
|
|
|
inventoryCount: inventoryCount,
|
|
|
|
|
|
turnoverText,
|
|
|
|
|
|
turnoverStatus,
|
|
|
|
|
|
selloutRate: finalSelloutRate,
|
|
|
|
|
|
selloutRateStatus: getSelloutRateStatus(finalSelloutRate),
|
|
|
|
|
|
discount: finalDiscount,
|
|
|
|
|
|
discountStatus: getDiscountStatus(finalDiscount),
|
|
|
|
|
|
sizes,
|
|
|
|
|
|
sizeStatusText,
|
|
|
|
|
|
sizeStatusStatus,
|
|
|
|
|
|
lifecycle,
|
|
|
|
|
|
lifecycleType,
|
|
|
|
|
|
lifecycleIcon,
|
|
|
|
|
|
actionText,
|
|
|
|
|
|
actionType,
|
|
|
|
|
|
imageUrl: row.pic || undefined,
|
|
|
|
|
|
barcode: row.sptm || undefined,
|
|
|
|
|
|
purchasePrice: purchasePrice > 0 ? purchasePrice : undefined,
|
|
|
|
|
|
sellingPrice: sellingPrice > 0 ? sellingPrice : undefined,
|
|
|
|
|
|
grossMargin: grossMargin > 0 ? grossMargin : undefined,
|
|
|
|
|
|
supplierName: row.ghs || undefined,
|
|
|
|
|
|
type: row.type || undefined,
|
|
|
|
|
|
turnoverDays: turnoverDays > 0 ? turnoverDays : undefined,
|
|
|
|
|
|
lsRaw: row.ls || undefined,
|
|
|
|
|
|
j7slRaw: row.j7sl || undefined,
|
|
|
|
|
|
kcslRaw: row.kcsl || undefined,
|
|
|
|
|
|
zksqlRaw: row.zksql || undefined,
|
|
|
|
|
|
shdxdRaw: row.shdxd || undefined,
|
|
|
|
|
|
wsdpRaw: row.wsdp || undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function uiTextClass(status?: string): string {
|
|
|
|
|
|
const s = String(status ?? '').toLowerCase()
|
|
|
|
|
|
if (s === 'success') return 'text-success'
|
|
|
|
|
|
if (s === 'danger') return 'text-danger'
|
|
|
|
|
|
if (s === 'warning') return 'text-warning'
|
|
|
|
|
|
return 'text-info'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasOutOfStockSize(sizes: ProductSizeStatus[]): boolean {
|
|
|
|
|
|
return Array.isArray(sizes) && sizes.some((s) => s.status === 'out' || s.stock === 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 从 productList 中收集所有标签(type 和 lifecycle)去重后作为筛选项,首项为「全部」 */
|
|
|
|
|
|
const labelOpts = computed(() => {
|
|
|
|
|
|
const labels = new Set<string>()
|
|
|
|
|
|
productList.value.forEach((item) => {
|
|
|
|
|
|
if (item.type) labels.add(item.type)
|
|
|
|
|
|
if (item.lifecycle) labels.add(item.lifecycle)
|
|
|
|
|
|
})
|
|
|
|
|
|
const list = Array.from(labels).sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
|
|
|
|
|
return [{ label: '全部', value: 'all' }, ...list.map((l) => ({ label: l, value: l }))]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/** 行是否匹配当前标签筛选 */
|
|
|
|
|
|
function productMatchesLabelFilter(item: ProductDetailData, filter: string): boolean {
|
|
|
|
|
|
if (filter === 'all') return true
|
|
|
|
|
|
return item.type === filter || item.lifecycle === filter
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 按标签筛选后的商品列表 */
|
|
|
|
|
|
const visibleProductList = computed(() => {
|
|
|
|
|
|
const filter = labelFilter.value
|
|
|
|
|
|
if (filter === 'all') return productList.value
|
|
|
|
|
|
return productList.value.filter((item) => productMatchesLabelFilter(item, filter))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表模式下再按款号/名称搜索后的列表 */
|
|
|
|
|
|
const filteredTableList = computed(() => {
|
|
|
|
|
|
const list = visibleProductList.value
|
|
|
|
|
|
if (!tableSearchKeyword.value) return list
|
|
|
|
|
|
const kw = tableSearchKeyword.value.toLowerCase()
|
|
|
|
|
|
return list.filter((row) => (row.name || '').toLowerCase().includes(kw) || (row.code || '').toLowerCase().includes(kw))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function formatNumber(n: number): string {
|
|
|
|
|
|
return n >= 10000 ? (n / 10000).toFixed(1) + 'w' : n.toLocaleString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sizeChipClass(status?: string): string {
|
|
|
|
|
|
const s = String(status ?? '').toLowerCase()
|
|
|
|
|
|
if (s === 'out') return 'size-red'
|
|
|
|
|
|
if (s === 'low') return 'size-orange'
|
|
|
|
|
|
if (s === 'warn') return 'size-yellow'
|
|
|
|
|
|
return 'size-gray'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDetailProductCodeClick(row: ProductDetailData) {
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
path: '/reports/lijun/reportpage6/detail',
|
|
|
|
|
|
query: { spdm: row.spdm || row.code || '' }
|
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDetailAction(row: ProductDetailData) {
|
|
|
|
|
|
ElMessage.info(`执行操作: ${row.actionText}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDetailRowClick(_row: ProductDetailData) {
|
|
|
|
|
|
// 点击行不跳转,与当前详情页一致
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询
|
|
|
|
|
|
async function handleQuery() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
productList.value = []
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
applyQueryDefaults()
|
|
|
|
|
|
const baseParams = {
|
|
|
|
|
|
reportId: REPORT_ID,
|
|
|
|
|
|
rq_s: queryParams.rq,
|
|
|
|
|
|
rq_e: queryParams.rq2,
|
|
|
|
|
|
ckdm: arrToQuery(queryParams.ckdm),
|
|
|
|
|
|
pp: arrToQuery(queryParams.pp),
|
|
|
|
|
|
dalei: arrToQuery(queryParams.category),
|
|
|
|
|
|
jj: arrToQuery(queryParams.season),
|
|
|
|
|
|
p: '123',
|
|
|
|
|
|
username: username.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const productRes: any = await ReportApi.executeProcedureWithData({
|
|
|
|
|
|
...baseParams,
|
|
|
|
|
|
name: 'YDY_AI_GET_SPDP5'
|
|
|
|
|
|
} as any)
|
|
|
|
|
|
.then((productRes: any) => {
|
|
|
|
|
|
// 处理返回数据结构:可能是 { code, msg, data: [...] } 或直接是数组
|
|
|
|
|
|
let data: any[] | null = null
|
|
|
|
|
|
if (productRes != null) {
|
|
|
|
|
|
// 如果返回的是标准格式 { code, msg, data: [...] }
|
|
|
|
|
|
if (productRes.code != null && productRes.data != null) {
|
|
|
|
|
|
data = Array.isArray(productRes.data) ? productRes.data : null
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果直接是数组
|
|
|
|
|
|
else if (Array.isArray(productRes)) {
|
|
|
|
|
|
data = productRes
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果返回对象中有 data 字段
|
|
|
|
|
|
else if (productRes.data != null) {
|
|
|
|
|
|
data = Array.isArray(productRes.data) ? productRes.data : null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data && data.length > 0) {
|
|
|
|
|
|
productList.value = data.map((item: any) => mapApiRowToProductDetail(item))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
productList.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('查询商品明细列表失败:', error)
|
|
|
|
|
|
productList.value = []
|
|
|
|
|
|
throw error
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('查询商品明细列表失败:', error)
|
|
|
|
|
|
ElMessage.error('查询失败,请重试')
|
|
|
|
|
|
productList.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置
|
|
|
|
|
|
function handleReset() {
|
|
|
|
|
|
queryParams.rq = dayjs().subtract(6, 'day').format('YYYY-MM-DD')
|
|
|
|
|
|
queryParams.rq2 = dayjs().format('YYYY-MM-DD')
|
|
|
|
|
|
queryParams.ckdm = []
|
|
|
|
|
|
queryParams.pp = []
|
|
|
|
|
|
queryParams.season = []
|
|
|
|
|
|
queryParams.zgj = []
|
|
|
|
|
|
queryParams.category = []
|
|
|
|
|
|
queryParams.line = []
|
|
|
|
|
|
queryParams.spdm = '' // 清除spdm
|
|
|
|
|
|
dateRange.value = [
|
|
|
|
|
|
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
|
|
|
|
|
dayjs().format('YYYY-MM-DD')
|
|
|
|
|
|
]
|
|
|
|
|
|
activeTimeRange.value = 'week7'
|
|
|
|
|
|
productList.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
initQueryParamsFromRoute()
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
fetchBrandOptions(),
|
|
|
|
|
|
fetchSeasonOptions(),
|
|
|
|
|
|
fetchCategoryOptions(),
|
|
|
|
|
|
fetchLineOptions(),
|
|
|
|
|
|
fetchStoreOptions()
|
|
|
|
|
|
])
|
|
|
|
|
|
// 如果有spdm参数,自动查询
|
|
|
|
|
|
if (spdmFromRoute.value) {
|
|
|
|
|
|
await handleQuery()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.product-detail-page {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: var(--el-bg-color-page);
|
|
|
|
|
|
|
|
|
|
|
|
.query-card {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
|
|
|
|
|
|
|
.query-form {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24px 32px;
|
|
|
|
|
|
|
|
|
|
|
|
.query-form-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24px 32px;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.query-form-more {
|
|
|
|
|
|
flex: 0 0 100%;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24px 32px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.query-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.query-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.query-more-toggle {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.query-toggle-link {
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
color: var(--el-color-primary-light-3);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
padding: 10px 0 2px;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pill {
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
background: var(--el-fill-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
background: var(--el-color-primary);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
}
|
2026-03-03 15:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
&.badge-danger {
|
|
|
|
|
|
background: #fee2e2;
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
border-color: #fecaca;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.badge-warn {
|
|
|
|
|
|
background: #fef3c7;
|
|
|
|
|
|
color: #d97706;
|
|
|
|
|
|
border-color: #fde68a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.badge-success {
|
|
|
|
|
|
background: #dcfce7;
|
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
|
border-color: #bbf7d0;
|
|
|
|
|
|
}
|
2026-02-27 09:47:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.view-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.view-switch {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
.toolbar-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.table-search {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-card {
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-bar {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table-wrap {
|
|
|
|
|
|
:deep(.product-list-row-clickable) {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
|
|
|
|
|
|
.product-image {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
background-color: var(--el-fill-color);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
.product-img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-details {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
.product-code {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
.product-code-link {
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.font-bold {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kcsl-display {
|
|
|
|
|
|
white-space: pre-line;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.size-status {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
.size-badge {
|
|
|
|
|
|
min-width: 28px;
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
&.size-red {
|
|
|
|
|
|
background-color: var(--el-color-danger);
|
|
|
|
|
|
}
|
|
|
|
|
|
&.size-orange {
|
|
|
|
|
|
background-color: var(--el-color-warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
&.size-yellow {
|
|
|
|
|
|
background-color: var(--el-color-warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
&.size-gray {
|
|
|
|
|
|
background-color: var(--el-fill-color);
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-cards-container {
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-cards-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
.product-card-item {
|
|
|
|
|
|
.kb22-card {
|
|
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-header-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background: var(--el-fill-color-lighter);
|
|
|
|
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-code {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-color-badge {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-color-dot {
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-card-body {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-media-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-thumb-box {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 96px;
|
|
|
|
|
|
background: var(--el-fill-color);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-thumb-img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-no-img {
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-info-col {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-prod-title {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-tags-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-tag-pill {
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
|
|
|
|
&.kb22-tag-blue {
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.kb22-tag-purple {
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-price-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-current-price {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-cost-price {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-margin-text {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stats-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stat-card {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stat-label {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stat-value {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stat-sub {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.kcsl-display {
|
|
|
|
|
|
white-space: pre-line;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.kb22-sales {
|
|
|
|
|
|
.kb22-sales-footer {
|
|
|
|
|
|
.kb22-mini-progress {
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-mini-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--el-color-primary);
|
|
|
|
|
|
transition: width 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-rate-text {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.kb22-stock {
|
|
|
|
|
|
.kb22-turnover-text {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&.text-success {
|
|
|
|
|
|
color: var(--el-color-success);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.text-danger {
|
|
|
|
|
|
color: var(--el-color-danger);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.text-warning {
|
|
|
|
|
|
color: var(--el-color-warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.text-info {
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-stock-footer {
|
|
|
|
|
|
.kb22-section-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-alert-tip {
|
|
|
|
|
|
color: var(--el-color-danger);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-size-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-size-cell {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
&.kb22-alert {
|
|
|
|
|
|
border-color: var(--el-color-danger);
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-alert-badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -6px;
|
|
|
|
|
|
right: -6px;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
background: var(--el-color-danger);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-sz-tag {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-sz-data-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-sz-stock {
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-sz-sales {
|
|
|
|
|
|
color: #3b82f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-legend {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-legend-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
.kb22-dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|