InventoryTurnoverView.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <template>
  2. <div class="app-page">
  3. <section class="glass-card section-card">
  4. <el-form :model="filters" inline class="filter-form">
  5. <el-form-item label="SKU">
  6. <el-input v-model="filters.sku" placeholder="SKU编号/商品名称" clearable style="width:180px" @keyup.enter="loadData" />
  7. </el-form-item>
  8. <el-form-item label="仓库">
  9. <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
  10. <el-option v-for="w in warehouses" :key="w" :label="w" :value="w" />
  11. </el-select>
  12. </el-form-item>
  13. <el-form-item label="分析维度">
  14. <el-select v-model="filters.dimension" placeholder="全部" clearable style="width:130px">
  15. <el-option label="按SKU" value="sku" />
  16. <el-option label="按类目" value="category" />
  17. <el-option label="按仓库" value="warehouse" />
  18. </el-select>
  19. </el-form-item>
  20. <el-form-item label="状态">
  21. <el-select v-model="filters.status" placeholder="全部状态" clearable style="width:120px">
  22. <el-option label="正常" value="正常" />
  23. <el-option label="滞销" value="滞销" />
  24. <el-option label="严重滞销" value="严重滞销" />
  25. </el-select>
  26. </el-form-item>
  27. <el-form-item>
  28. <el-button type="primary" @click="loadData">查询</el-button>
  29. <el-button @click="resetFilters">重置</el-button>
  30. </el-form-item>
  31. </el-form>
  32. </section>
  33. <section class="glass-card section-card" style="padding:12px 24px">
  34. <div class="table-toolbar" style="margin-bottom:0">
  35. <div class="chip-list">
  36. <el-button type="primary" @click="setAlert">设置预警</el-button>
  37. <el-button @click="doExport">导出报表</el-button>
  38. <el-button type="success" @click="generateReplenishment">生成补货单</el-button>
  39. </div>
  40. <el-button @click="loadData">刷新</el-button>
  41. </div>
  42. </section>
  43. <section class="glass-card section-card" style="padding:16px">
  44. <div class="stat-grid" style="grid-template-columns:repeat(5, 1fr)">
  45. <article class="stat-card">
  46. <div class="stat-card__icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
  47. <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/></svg>
  48. </div>
  49. <div class="stat-card__content">
  50. <div class="stat-card__label">库存周转天数</div>
  51. <div class="stat-card__value" style="font-size:28px;color:#667eea">{{ kpiData.turnoverDays }}天</div>
  52. <div class="stat-card__trend up"><el-icon><ArrowUp /></el-icon>12%</div>
  53. </div>
  54. </article>
  55. <article class="stat-card">
  56. <div class="stat-card__icon" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%)">
  57. <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
  58. </div>
  59. <div class="stat-card__content">
  60. <div class="stat-card__label">动销率</div>
  61. <div class="stat-card__value" style="font-size:28px;color:#11998e">{{ kpiData.salesRate }}%</div>
  62. <div class="stat-card__trend up"><el-icon><ArrowUp /></el-icon>5%</div>
  63. </div>
  64. </article>
  65. <article class="stat-card">
  66. <div class="stat-card__icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
  67. <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
  68. </div>
  69. <div class="stat-card__content">
  70. <div class="stat-card__label">滞销SKU</div>
  71. <div class="stat-card__value" style="font-size:28px;color:#f5576c">{{ kpiData.slowMoving }}</div>
  72. <div class="stat-card__trend down"><el-icon><ArrowDown /></el-icon>3</div>
  73. </div>
  74. </article>
  75. <article class="stat-card">
  76. <div class="stat-card__icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
  77. <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
  78. </div>
  79. <div class="stat-card__content">
  80. <div class="stat-card__label">库存覆盖天数</div>
  81. <div class="stat-card__value" style="font-size:28px;color:#00f2fe">{{ kpiData.coverageDays }}天</div>
  82. <div class="stat-card__trend neutral">目标: 30天</div>
  83. </div>
  84. </article>
  85. <article class="stat-card">
  86. <div class="stat-card__icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)">
  87. <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
  88. </div>
  89. <div class="stat-card__content">
  90. <div class="stat-card__label">平均库龄</div>
  91. <div class="stat-card__value" style="font-size:28px;color:#fa709a">{{ kpiData.avgAge }}天</div>
  92. <div class="stat-card__trend down"><el-icon><ArrowDown /></el-icon>8%</div>
  93. </div>
  94. </article>
  95. </div>
  96. </section>
  97. <section class="glass-card section-card">
  98. <div class="section-header">
  99. <h3>库存周转分析</h3>
  100. <el-radio-group v-model="timeRange" size="small">
  101. <el-radio-button label="7d">近7天</el-radio-button>
  102. <el-radio-button label="30d">近30天</el-radio-button>
  103. <el-radio-button label="90d">近90天</el-radio-button>
  104. </el-radio-group>
  105. </div>
  106. <div class="chart-grid-2">
  107. <div class="chart-container">
  108. <div ref="turnoverTrendChart" style="height: 300px"></div>
  109. </div>
  110. <div class="chart-container">
  111. <div ref="warehouseComparisonChart" style="height: 300px"></div>
  112. </div>
  113. </div>
  114. </section>
  115. <section class="glass-card section-card">
  116. <div class="section-header">
  117. <h3>库存结构分析</h3>
  118. </div>
  119. <div class="chart-grid-3">
  120. <div class="chart-container">
  121. <div ref="categoryPieChart" style="height: 280px"></div>
  122. </div>
  123. <div class="chart-container">
  124. <div ref="stockDistributionChart" style="height: 280px"></div>
  125. </div>
  126. <div class="chart-container">
  127. <div ref="turnoverGaugeChart" style="height: 280px"></div>
  128. </div>
  129. </div>
  130. </section>
  131. <section class="glass-card section-card">
  132. <div class="section-header">
  133. <h3>滞销预警 ({{ alertItems.length }} 条)</h3>
  134. <div class="chip-list">
  135. <el-tag type="danger" effect="dark">严重滞销</el-tag>
  136. <el-tag type="warning" effect="dark">滞销</el-tag>
  137. <el-tag type="info">库存积压</el-tag>
  138. </div>
  139. </div>
  140. <el-table :data="alertItems" style="width:100%" v-loading="loading" :row-class-name="tableRowClass">
  141. <el-table-column prop="sku" label="SKU" width="150" />
  142. <el-table-column prop="productTitle" label="商品名称" min-width="200" show-overflow-tooltip />
  143. <el-table-column prop="warehouse" label="仓库" width="120" />
  144. <el-table-column prop="available" label="当前库存" width="90" align="center" />
  145. <el-table-column prop="sales30d" label="30天销量" width="90" align="center" />
  146. <el-table-column prop="stockDays" label="库存天数" width="100" align="center">
  147. <template #default="{ row }">
  148. <el-progress :percentage="Math.min(row.stockDays / 2, 100)" :status="stockProgressStatus(row.stockDays)" />
  149. </template>
  150. </el-table-column>
  151. <el-table-column prop="turnoverDays" label="周转天数" width="100" align="center">
  152. <template #default="{ row }">
  153. <span :class="turnoverClass(row.turnoverDays)">{{ row.turnoverDays }}天</span>
  154. </template>
  155. </el-table-column>
  156. <el-table-column prop="suggestion" label="建议" min-width="150">
  157. <template #default="{ row }">
  158. <el-tag :type="getInventorySuggestion(row.suggestion).type" size="small">
  159. {{ getInventorySuggestion(row.suggestion).label }}
  160. </el-tag>
  161. </template>
  162. </el-table-column>
  163. <el-table-column label="操作" width="150" fixed="right">
  164. <template #default="{ row }">
  165. <el-button link type="primary" @click="openDetail(row)">详情</el-button>
  166. <el-button link type="success" @click="createOrder(row)">补货</el-button>
  167. <el-button link type="danger" @click="handleLiquidation(row)">清仓</el-button>
  168. </template>
  169. </el-table-column>
  170. </el-table>
  171. </section>
  172. <section class="glass-card section-card">
  173. <div class="section-header">
  174. <h3>库存周转明细</h3>
  175. </div>
  176. <el-table :data="filteredItems" style="width:100%" v-loading="loading">
  177. <el-table-column prop="sku" label="SKU" width="160" />
  178. <el-table-column prop="productTitle" label="商品名称" min-width="200" show-overflow-tooltip />
  179. <el-table-column prop="category" label="类目" width="120" />
  180. <el-table-column prop="warehouse" label="仓库" width="120" />
  181. <el-table-column prop="available" label="可用库存" width="90" align="center" />
  182. <el-table-column prop="sales30d" label="30天销量" width="90" align="center" />
  183. <el-table-column prop="turnoverDays" label="周转天数" width="100" align="center">
  184. <template #default="{ row }">
  185. <span :class="turnoverClass(row.turnoverDays)">{{ row.turnoverDays }}天</span>
  186. </template>
  187. </el-table-column>
  188. <el-table-column prop="stockDays" label="库存覆盖天数" width="110" align="center">
  189. <template #default="{ row }">
  190. <span :class="stockDaysClass(row.stockDays)">{{ row.stockDays }}天</span>
  191. </template>
  192. </el-table-column>
  193. <el-table-column prop="avgAge" label="平均库龄" width="90" align="center">
  194. <template #default="{ row }">
  195. <el-tag size="small" :type="avgAgeTag(row.avgAge)">{{ row.avgAge }}天</el-tag>
  196. </template>
  197. </el-table-column>
  198. <el-table-column prop="status" label="状态" width="100">
  199. <template #default="{ row }">
  200. <el-tag :type="getInventoryTurnoverStatus(row.status).type" size="small">
  201. {{ getInventoryTurnoverStatus(row.status).label }}
  202. </el-tag>
  203. </template>
  204. </el-table-column>
  205. <el-table-column label="操作" width="140" fixed="right">
  206. <template #default="{ row }">
  207. <el-button link type="primary" @click="openDetail(row)">详情</el-button>
  208. <el-button link type="success" @click="createOrder(row)">补货</el-button>
  209. </template>
  210. </el-table-column>
  211. </el-table>
  212. </section>
  213. </div>
  214. </template>
  215. <script setup lang="ts">
  216. import { onMounted, ref, computed, watch } from 'vue';
  217. import { ElMessage } from 'element-plus';
  218. import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
  219. import * as echarts from 'echarts';
  220. import { getInventoryTurnoverStatus, getInventorySuggestion } from '@/utils/enumMappings';
  221. interface TurnoverItem {
  222. sku: string;
  223. productTitle: string;
  224. category: string;
  225. warehouse: string;
  226. available: number;
  227. sales30d: number;
  228. turnoverDays: number;
  229. stockDays: number;
  230. avgAge: number;
  231. status: string;
  232. suggestion?: string;
  233. }
  234. const items = ref<TurnoverItem[]>([
  235. { sku: 'SKU-LUGG-20-BLK', productTitle: 'TravelFlex Carry-On 20寸 / Black', category: '行李箱', warehouse: '深圳南山仓', available: 15, sales30d: 240, turnoverDays: 15, stockDays: 18, avgAge: 20, status: '正常', suggestion: '正常' },
  236. { sku: 'SKU-BAG-ML-BRW', productTitle: 'Classic Leather Tote / Brown', category: '皮革包', warehouse: '义乌商贸仓', available: 45, sales30d: 150, turnoverDays: 28, stockDays: 30, avgAge: 25, status: '正常', suggestion: '正常' },
  237. { sku: 'SKU-SPRT-YGA-BLU', productTitle: 'Yoga Mat Pro / Blue', category: '运动瑜伽', warehouse: '深圳南山仓', available: 20, sales30d: 360, turnoverDays: 8, stockDays: 5, avgAge: 12, status: '正常', suggestion: '加仓' },
  238. { sku: 'SKU-LUGG-28-NVY', productTitle: 'TravelFlex Large Check-In / Navy', category: '行李箱', warehouse: '洛杉矶海外仓', available: 8, sales30d: 90, turnoverDays: 45, stockDays: 9, avgAge: 60, status: '滞销', suggestion: '促销' },
  239. { sku: 'SKU-BAG-BPK-OLV', productTitle: 'Urban Backpack / Olive', category: '双肩包', warehouse: '义乌商贸仓', available: 55, sales30d: 180, turnoverDays: 32, stockDays: 30, avgAge: 40, status: '正常', suggestion: '正常' },
  240. { sku: 'SKU-TOWEL-SET-MIX', productTitle: 'AeroDry Towel Set / 混色', category: '毛巾浴袍', warehouse: '深圳南山仓', available: 30, sales30d: 300, turnoverDays: 12, stockDays: 10, avgAge: 18, status: '正常', suggestion: '加仓' },
  241. { sku: 'SKU-SPRT-BTL-GRN', productTitle: 'Sports Bottle 750ml / Green', category: '运动水壶', warehouse: '洛杉矶海外仓', available: 120, sales30d: 60, turnoverDays: 60, stockDays: 200, avgAge: 90, status: '严重滞销', suggestion: '清仓' },
  242. { sku: 'SKU-LUGG-24-RED', productTitle: 'TravelFlex Medium / Red', category: '行李箱', warehouse: '义乌商贸仓', available: 5, sales30d: 50, turnoverDays: 75, stockDays: 10, avgAge: 85, status: '严重滞销', suggestion: '清仓' },
  243. { sku: 'SKU-BAG-WLT-GLD', productTitle: 'Designer Wallet / Gold', category: '钱包卡包', warehouse: '深圳南山仓', available: 80, sales30d: 40, turnoverDays: 70, stockDays: 200, avgAge: 95, status: '严重滞销', suggestion: '清仓' },
  244. { sku: 'SKU-SPRT-MAT-GRY', productTitle: 'Fitness Mat / Grey', category: '运动瑜伽', warehouse: '洛杉矶海外仓', available: 15, sales30d: 100, turnoverDays: 35, stockDays: 15, avgAge: 30, status: '正常', suggestion: '正常' }
  245. ]);
  246. const loading = ref(false);
  247. const timeRange = ref('30d');
  248. const warehouses = ['深圳南山仓', '义乌商贸仓', '洛杉矶海外仓'];
  249. const filters = ref({ sku: '', warehouse: '', dimension: '', status: '' });
  250. const kpiData = ref({
  251. turnoverDays: 28,
  252. salesRate: 76,
  253. slowMoving: 15,
  254. coverageDays: 42,
  255. avgAge: 35
  256. });
  257. let turnoverTrendChart: echarts.ECharts | null = null;
  258. let warehouseComparisonChart: echarts.ECharts | null = null;
  259. let categoryPieChart: echarts.ECharts | null = null;
  260. let stockDistributionChart: echarts.ECharts | null = null;
  261. let turnoverGaugeChart: echarts.ECharts | null = null;
  262. const turnoverTrendRef = ref<HTMLDivElement | null>(null);
  263. const warehouseComparisonRef = ref<HTMLDivElement | null>(null);
  264. const categoryPieRef = ref<HTMLDivElement | null>(null);
  265. const stockDistributionRef = ref<HTMLDivElement | null>(null);
  266. const turnoverGaugeRef = ref<HTMLDivElement | null>(null);
  267. const filteredItems = computed(() => {
  268. return items.value.filter(item => {
  269. if (filters.value.sku && !item.sku.toLowerCase().includes(filters.value.sku.toLowerCase()) && !item.productTitle.toLowerCase().includes(filters.value.sku.toLowerCase())) return false;
  270. if (filters.value.warehouse && item.warehouse !== filters.value.warehouse) return false;
  271. if (filters.value.status && item.status !== filters.value.status) return false;
  272. return true;
  273. });
  274. });
  275. const alertItems = computed(() => {
  276. return items.value.filter(item => item.status === '滞销' || item.status === '严重滞销');
  277. });
  278. const initTurnoverTrendChart = () => {
  279. if (!turnoverTrendRef.value) return;
  280. turnoverTrendChart = echarts.init(turnoverTrendRef.value);
  281. const days = timeRange.value === '7d' ? 7 : timeRange.value === '30d' ? 30 : 90;
  282. const data = Array.from({ length: days }, (_, i) => ({
  283. day: `Day ${i + 1}`,
  284. turnover: Math.round(20 + Math.random() * 20),
  285. target: 30
  286. }));
  287. turnoverTrendChart.setOption({
  288. title: { text: '周转天数趋势', left: 'center', textStyle: { fontSize: 14, fontWeight: 500 } },
  289. tooltip: { trigger: 'axis' },
  290. legend: { data: ['实际周转', '目标'], bottom: 0 },
  291. grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
  292. xAxis: { type: 'category', data: data.map(d => d.day), boundaryGap: false },
  293. yAxis: { type: 'value', name: '天数' },
  294. series: [
  295. { name: '实际周转', type: 'line', smooth: true, data: data.map(d => d.turnover), areaStyle: { color: 'rgba(102, 126, 234, 0.2)' }, lineStyle: { color: '#667eea' }, itemStyle: { color: '#667eea' } },
  296. { name: '目标', type: 'line', data: data.map(d => d.target), lineStyle: { type: 'dashed', color: '#999' }, itemStyle: { color: '#999' } }
  297. ]
  298. });
  299. };
  300. const initWarehouseComparisonChart = () => {
  301. if (!warehouseComparisonRef.value) return;
  302. warehouseComparisonChart = echarts.init(warehouseComparisonRef.value);
  303. warehouseComparisonChart.setOption({
  304. title: { text: '仓库周转对比', left: 'center', textStyle: { fontSize: 14, fontWeight: 500 } },
  305. tooltip: { trigger: 'axis' },
  306. legend: { data: ['周转天数', '目标'], bottom: 0 },
  307. grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
  308. xAxis: { type: 'category', data: warehouses },
  309. yAxis: { type: 'value', name: '天数' },
  310. series: [
  311. { name: '周转天数', type: 'bar', data: [25, 32, 45], barWidth: '40%', itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#4facfe' }, { offset: 1, color: '#00f2fe' }]) } },
  312. { name: '目标', type: 'line', data: [30, 30, 30], lineStyle: { type: 'dashed' }, itemStyle: { color: '#f5576c' } }
  313. ]
  314. });
  315. };
  316. const initCategoryPieChart = () => {
  317. if (!categoryPieRef.value) return;
  318. categoryPieChart = echarts.init(categoryPieRef.value);
  319. categoryPieChart.setOption({
  320. title: { text: '类目库存占比', left: 'center', textStyle: { fontSize: 14, fontWeight: 500 } },
  321. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  322. legend: { orient: 'vertical', right: '5%', top: 'center' },
  323. series: [{
  324. type: 'pie',
  325. radius: ['40%', '70%'],
  326. center: ['40%', '50%'],
  327. data: [
  328. { value: 35, name: '行李箱' },
  329. { value: 25, name: '皮革包' },
  330. { value: 18, name: '运动瑜伽' },
  331. { value: 12, name: '双肩包' },
  332. { value: 10, name: '其他' }
  333. ],
  334. label: { show: false }
  335. }]
  336. });
  337. };
  338. const initStockDistributionChart = () => {
  339. if (!stockDistributionRef.value) return;
  340. stockDistributionChart = echarts.init(stockDistributionRef.value);
  341. stockDistributionChart.setOption({
  342. title: { text: '库存周转分布', left: 'center', textStyle: { fontSize: 14, fontWeight: 500 } },
  343. tooltip: { trigger: 'axis' },
  344. grid: { left: '3%', right: '4%', bottom: '10%', top: '15%', containLabel: true },
  345. xAxis: { type: 'category', data: ['<15天', '15-30天', '30-45天', '45-60天', '>60天'] },
  346. yAxis: { type: 'value', name: 'SKU数' },
  347. series: [{
  348. type: 'bar',
  349. data: [
  350. { value: 25, itemStyle: { color: '#11998e' } },
  351. { value: 40, itemStyle: { color: '#38ef7d' } },
  352. { value: 20, itemStyle: { color: '#fee140' } },
  353. { value: 10, itemStyle: { color: '#f5576c' } },
  354. { value: 5, itemStyle: { color: '#fa709a' } }
  355. ],
  356. barWidth: '50%'
  357. }]
  358. });
  359. };
  360. const initTurnoverGaugeChart = () => {
  361. if (!turnoverGaugeRef.value) return;
  362. turnoverGaugeChart = echarts.init(turnoverGaugeRef.value);
  363. turnoverGaugeChart.setOption({
  364. title: { text: '综合周转指数', left: 'center', textStyle: { fontSize: 14, fontWeight: 500 } },
  365. series: [{
  366. type: 'gauge',
  367. center: ['50%', '60%'],
  368. startAngle: 200,
  369. endAngle: -20,
  370. min: 0,
  371. max: 100,
  372. splitNumber: 10,
  373. itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{ offset: 0, color: '#11998e' }, { offset: 0.5, color: '#fee140' }, { offset: 1, color: '#f5576c' }]) },
  374. progress: { show: true, width: 20 },
  375. pointer: { show: false },
  376. axisLine: { lineStyle: { width: 20 } },
  377. axisTick: { show: false },
  378. splitLine: { show: false },
  379. axisLabel: { show: false },
  380. anchor: { show: false },
  381. title: { show: false },
  382. detail: { valueAnimation: true, fontSize: 28, offsetCenter: [0, '10%'], formatter: '{value}分', color: '#333' },
  383. data: [{ value: 72 }]
  384. }, {
  385. type: 'pie',
  386. radius: ['75%', '85%'],
  387. center: ['50%', '60%'],
  388. startAngle: 200,
  389. endAngle: -20,
  390. itemStyle: { color: '#f5f5f5' },
  391. label: { show: false },
  392. data: [{ value: 100 }]
  393. }]
  394. });
  395. };
  396. const initAllCharts = () => {
  397. initTurnoverTrendChart();
  398. initWarehouseComparisonChart();
  399. initCategoryPieChart();
  400. initStockDistributionChart();
  401. initTurnoverGaugeChart();
  402. };
  403. const handleResize = () => {
  404. turnoverTrendChart?.resize();
  405. warehouseComparisonChart?.resize();
  406. categoryPieChart?.resize();
  407. stockDistributionChart?.resize();
  408. turnoverGaugeChart?.resize();
  409. };
  410. const turnoverClass = (days: number) => {
  411. if (days > 60) return 'text-danger';
  412. if (days > 30) return 'text-warning';
  413. return '';
  414. };
  415. const stockDaysClass = (days: number) => {
  416. if (days < 10) return 'text-danger';
  417. if (days < 20) return 'text-warning';
  418. return '';
  419. };
  420. const avgAgeTag = (age: number) => {
  421. if (age > 60) return 'danger';
  422. if (age > 30) return 'warning';
  423. return 'info';
  424. };
  425. const stockProgressStatus = (days: number) => {
  426. if (days > 60) return 'exception';
  427. if (days > 30) return 'warning';
  428. return 'success';
  429. };
  430. const tableRowClass = ({ row }: { row: TurnoverItem }) => {
  431. if (row.status === '严重滞销') return 'danger-row';
  432. if (row.status === '滞销') return 'warning-row';
  433. return '';
  434. };
  435. const loadData = () => { loading.value = true; setTimeout(() => { loading.value = false; initAllCharts(); }, 300); };
  436. const resetFilters = () => { filters.value = { sku: '', warehouse: '', dimension: '', status: '' }; };
  437. const setAlert = () => { ElMessage.info('预警设置功能开发中'); };
  438. const doExport = () => { ElMessage.info('导出开始'); };
  439. const openDetail = (row: TurnoverItem) => { ElMessage.info(`查看 ${row.sku} 详情`); };
  440. const createOrder = (row: TurnoverItem) => { ElMessage.success(`已为 ${row.sku} 生成补货建议`); };
  441. const handleLiquidation = (row: TurnoverItem) => { ElMessage.warning(`已提交 ${row.sku} 清仓申请`); };
  442. const generateReplenishment = () => { ElMessage.success('已生成补货单,共 3 个SKU待处理'); };
  443. watch(timeRange, () => { initTurnoverTrendChart(); });
  444. onMounted(() => {
  445. loadData();
  446. window.addEventListener('resize', handleResize);
  447. });
  448. </script>
  449. <style scoped>
  450. .filter-form :deep(.el-form-item) { margin-bottom: 0; }
  451. .section-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
  452. .section-header h3 { margin: 0; font-size: 15px; font-weight: 600; color: #333; }
  453. .chart-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px; }
  454. .chart-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; padding: 16px; }
  455. .chart-container { background: #fafafa; border-radius: 8px; padding: 8px; }
  456. .text-danger { color: #f5576c; font-weight: 600; }
  457. .text-warning { color: #fee140; font-weight: 600; }
  458. .stat-card { display: flex; align-items: center; gap: 12px; }
  459. .stat-card__icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  460. .stat-card__icon svg { width: 24px; height: 24px; }
  461. .stat-card__content { flex: 1; min-width: 0; }
  462. .stat-card__label { font-size: 12px; color: #666; margin-bottom: 4px; }
  463. .stat-card__value { font-size: 24px; font-weight: 700; line-height: 1.2; }
  464. .stat-card__trend { font-size: 12px; margin-top: 4px; display: flex; align-items: center; gap: 2px; }
  465. .stat-card__trend.up { color: #11998e; }
  466. .stat-card__trend.down { color: #f5576c; }
  467. .stat-card__trend.neutral { color: #999; }
  468. .stat-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }
  469. :deep(.danger-row) { background-color: #fff5f5 !important; }
  470. :deep(.warning-row) { background-color: #fffbf0 !important; }
  471. </style>