fix(WMS): 修复首页、3D、websocket
| | |
| | | <!-- 顶部:本月出入库趋势 (全宽) --> |
| | | <div class="chart-row full-width"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本月出入库趋势</div> |
| | | <div class="card-title">每月出入库趋势</div> |
| | | <div id="chart-monthly-trend" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第二行:今日/本周出入库对比 --> |
| | | <div class="chart-row"> |
| | | <!-- 第二行:每日出入库趋势 (全宽) --> |
| | | <div class="chart-row full-width"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">今日出入库对比</div> |
| | | <div id="chart-today" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本周出入库对比</div> |
| | | <div id="chart-week" class="chart-content"></div> |
| | | <div class="card-title">每日出入库趋势</div> |
| | | <div id="chart-daily" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第三行:本月对比/库存总量 --> |
| | | <!-- 第四行:仓库分布 --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本月出入库对比</div> |
| | | <div id="chart-month" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">当前库存总量</div> |
| | | <div class="stock-total"> |
| | | <div class="total-number">{{ overviewData.TotalStock || 0 }}</div> |
| | | <div class="total-label">托盘</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第四行:库龄分布/仓库分布 --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">库存库龄分布</div> |
| | | <div id="chart-stock-age" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">各仓库库存分布</div> |
| | | <div id="chart-warehouse" class="chart-content"></div> |
| | |
| | | data() { |
| | | return { |
| | | charts: {}, |
| | | overviewData: { |
| | | TodayInbound: 0, |
| | | TodayOutbound: 0, |
| | | MonthInbound: 0, |
| | | MonthOutbound: 0, |
| | | TotalStock: 0 |
| | | }, |
| | | weeklyData: [], |
| | | dailyData: [], |
| | | monthlyData: [], |
| | | stockAgeData: [], |
| | | warehouseData: [] |
| | | }; |
| | | }, |
| | |
| | | |
| | | initCharts() { |
| | | this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend")); |
| | | this.charts.today = echarts.init(document.getElementById("chart-today")); |
| | | this.charts.week = echarts.init(document.getElementById("chart-week")); |
| | | this.charts.month = echarts.init(document.getElementById("chart-month")); |
| | | this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age")); |
| | | this.charts.daily = echarts.init(document.getElementById("chart-daily")); |
| | | this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse")); |
| | | }, |
| | | |
| | | async loadData() { |
| | | await this.loadOverview(); |
| | | await this.loadWeeklyStats(); |
| | | await this.loadMonthlyStats(); |
| | | await this.loadStockAgeDistribution(); |
| | | await this.loadDailyStats(); |
| | | await this.loadStockByWarehouse(); |
| | | }, |
| | | |
| | | async loadOverview() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/Overview"); |
| | | console.log("总览数据", res.Data); |
| | | if (res.Status && res.Data) { |
| | | this.overviewData = res.Data; |
| | | this.updateTodayChart(); |
| | | this.updateWeekChart(); |
| | | this.updateMonthChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载总览数据失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadWeeklyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 }); |
| | | if (res.Status && res.Data) { |
| | | this.weeklyData = res.Data; |
| | | this.updateWeekChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载每周统计失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadMonthlyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 }); |
| | | if (res.Status && res.Data) { |
| | | this.monthlyData = res.Data; |
| | | if (res.status && res.data) { |
| | | console.log("每月统计数据:", res.data); |
| | | this.monthlyData = res.data; |
| | | this.updateMonthlyTrendChart(); |
| | | } |
| | | } catch (e) { |
| | |
| | | } |
| | | }, |
| | | |
| | | async loadStockAgeDistribution() { |
| | | async loadDailyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockAgeDistribution"); |
| | | if (res.Status && res.Data) { |
| | | this.stockAgeData = res.Data; |
| | | this.updateStockAgeChart(); |
| | | const res = await this.http.get("/api/Dashboard/DailyStats", { days: 30 }); |
| | | if (res.status && res.data) { |
| | | console.log("每日统计数据:", res.data); |
| | | this.dailyData = res.data; |
| | | this.updateDailyChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载库龄分布失败", e); |
| | | console.error("加载每日统计失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadStockByWarehouse() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockByWarehouse"); |
| | | if (res.Status && res.Data) { |
| | | this.warehouseData = res.Data; |
| | | if (res.status && res.data) { |
| | | console.log("仓库分布数据:", res.data); |
| | | this.warehouseData = res.data.data || res.data; |
| | | this.updateWarehouseChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载仓库分布失败", e); |
| | | } |
| | | }, |
| | | |
| | | updateTodayChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["今日"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.today.setOption(option, true); |
| | | }, |
| | | |
| | | updateWeekChart() { |
| | | const thisWeek = this.getThisWeekData(this.weeklyData); |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["本周"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.week.setOption(option, true); |
| | | }, |
| | | |
| | | getThisWeekData(weeklyData) { |
| | | if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 }; |
| | | const thisWeekKey = this.getCurrentWeekKey(); |
| | | const thisWeek = weeklyData.find(w => w.Week === thisWeekKey); |
| | | return thisWeek || { Inbound: 0, Outbound: 0 }; |
| | | }, |
| | | |
| | | getCurrentWeekKey() { |
| | | const now = new Date(); |
| | | const diff = (7 + (now.getDay() - 1)) % 7; |
| | | const monday = new Date(now); |
| | | monday.setDate(now.getDate() - diff); |
| | | const year = monday.getFullYear(); |
| | | const jan1 = new Date(year, 0, 1); |
| | | const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7); |
| | | return `${year}-W${String(weekNum).padStart(2, "0")}`; |
| | | }, |
| | | |
| | | updateMonthChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["本月"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.month.setOption(option, true); |
| | | }, |
| | | |
| | | updateMonthlyTrendChart() { |
| | |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.monthlyData.map(m => m.Month), |
| | | data: this.monthlyData.map(m => m.month), |
| | | axisLabel: { color: "#fff", rotate: 45 } |
| | | }, |
| | | yAxis: [ |
| | |
| | | } |
| | | ], |
| | | series: [ |
| | | { name: "入库", type: "bar", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } } |
| | | { name: "入库", type: "bar", data: this.monthlyData.map(m => m.inbound), itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "line", data: this.monthlyData.map(m => m.outbound), itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.monthlyTrend.setOption(option, true); |
| | | }, |
| | | |
| | | updateStockAgeChart() { |
| | | updateDailyChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.stockAgeData.map(s => s.Range), |
| | | axisLabel: { color: "#fff" } |
| | | data: this.dailyData.map(d => d.date), |
| | | axisLabel: { |
| | | color: "#fff", |
| | | interval: 0, |
| | | rotate: 45, |
| | | fontSize: 12, |
| | | margin: 10 |
| | | }, |
| | | axisTick: { |
| | | alignWithLabel: true |
| | | } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '15%', |
| | | top: '10%', |
| | | containLabel: true |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "bar", |
| | | data: this.stockAgeData.map(s => s.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | } |
| | | { name: "入库", type: "bar", data: this.dailyData.map(d => d.inbound), itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: this.dailyData.map(d => d.outbound), itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.stockAge.setOption(option, true); |
| | | this.charts.daily.setOption(option, true); |
| | | }, |
| | | |
| | | updateWarehouseChart() { |
| | | const warehouseNames = this.warehouseData.map(w => w.warehouse); |
| | | const totalStocks = this.warehouseData.map(w => w.total); |
| | | const hasStocks = this.warehouseData.map(w => w.hasStock); |
| | | const noStocks = this.warehouseData.map(w => w.noStock); |
| | | const hasStockPercentages = this.warehouseData.map(w => w.hasStockPercentage); |
| | | const noStockPercentages = this.warehouseData.map(w => w.noStockPercentage); |
| | | |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | }, |
| | | formatter: function(params) { |
| | | let tip = params[0].name + '<br/>'; |
| | | params.forEach(param => { |
| | | const dataIndex = param.dataIndex; |
| | | const warehouse = window.homeComponent.warehouseData[dataIndex]; |
| | | |
| | | if (param.seriesName === '已用容量') { |
| | | tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.hasStockPercentage})<br/>`; |
| | | tip += `有库存: ${warehouse.hasStock}<br/>`; |
| | | tip += `无库存: ${warehouse.noStock}<br/>`; |
| | | tip += `总容量: ${warehouse.total}`; |
| | | } else if (param.seriesName === '剩余容量') { |
| | | tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.noStockPercentage})<br/>`; |
| | | tip += `有库存: ${warehouse.hasStock}<br/>`; |
| | | tip += `无库存: ${warehouse.noStock}<br/>`; |
| | | tip += `总容量: ${warehouse.total}`; |
| | | } |
| | | }); |
| | | return tip; |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['已用容量', '剩余容量'], |
| | | textStyle: { color: '#fff' } |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.warehouseData.map(w => w.Warehouse), |
| | | axisLabel: { color: "#fff", rotate: 30 } |
| | | type: 'category', |
| | | data: warehouseNames, |
| | | axisLabel: { color: '#fff', rotate: 30 } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | type: 'value', |
| | | axisLabel: { color: '#fff' } |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "bar", |
| | | data: this.warehouseData.map(w => w.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | name: '已用容量', |
| | | type: 'bar', |
| | | data: hasStocks.map((value, index) => ({ |
| | | value: value, |
| | | label: { |
| | | show: true, |
| | | position: 'top', |
| | | formatter: '{c} {a|' + hasStockPercentages[index] + '}', |
| | | rich: { |
| | | a: { |
| | | lineHeight: 20, |
| | | borderColor: '#91cc75', |
| | | color: '#91cc75' |
| | | } |
| | | } |
| | | } |
| | | })), |
| | | itemStyle: { color: '#91cc75' } |
| | | }, |
| | | { |
| | | name: '剩余容量', |
| | | type: 'bar', |
| | | data: noStocks.map((value, index) => ({ |
| | | value: value, |
| | | label: { |
| | | show: true, |
| | | position: 'top', |
| | | formatter: '{c} {a|' + noStockPercentages[index] + '}', |
| | | rich: { |
| | | a: { |
| | | lineHeight: 20, |
| | | borderColor: '#fac858', |
| | | color: '#fac858' |
| | | } |
| | | } |
| | | } |
| | | })), |
| | | itemStyle: { color: '#fac858' } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | window.homeComponent = this; |
| | | |
| | | this.charts.warehouse.setOption(option, true); |
| | | } |
| | | } |
| | |
| | | <style scoped> |
| | | .dashboard-container { |
| | | padding: 20px; |
| | | background-color: #0e1a2b; |
| | | color: #e0e0e0; |
| | | min-height: calc(100vh - 60px); |
| | | background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); |
| | | background-attachment: fixed; |
| | | } |
| | | |
| | | .chart-row { |
| | |
| | | |
| | | .chart-card { |
| | | flex: 1; |
| | | background: rgba(255, 255, 255, 0.05); |
| | | border: 1px solid rgba(25, 186, 139, 0.17); |
| | | border-radius: 4px; |
| | | background: rgba(10, 16, 35, 0.6); |
| | | backdrop-filter: blur(10px); |
| | | border: 1px solid rgba(64, 224, 208, 0.3); |
| | | border-radius: 12px; |
| | | padding: 15px; |
| | | position: relative; |
| | | box-shadow: |
| | | 0 0 15px rgba(0, 255, 255, 0.1), |
| | | inset 0 0 10px rgba(64, 224, 208, 0.1); |
| | | transition: all 0.3s ease; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .chart-card:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: |
| | | 0 0 25px rgba(0, 255, 255, 0.3), |
| | | inset 0 0 15px rgba(64, 224, 208, 0.2); |
| | | border: 1px solid rgba(64, 224, 208, 0.6); |
| | | } |
| | | |
| | | .chart-card::before { |
| | |
| | | left: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-left: 2px solid #02a6b5; |
| | | border-top: 2px solid #00ffff; |
| | | border-left: 2px solid #00ffff; |
| | | box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | |
| | | .chart-card::after { |
| | |
| | | right: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-right: 2px solid #02a6b5; |
| | | border-top: 2px solid #00ffff; |
| | | border-right: 2px solid #00ffff; |
| | | box-shadow: 2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | |
| | | .chart-card::before, |
| | | .chart-card::after { |
| | | animation: neon-flicker 2s infinite alternate; |
| | | } |
| | | |
| | | @keyframes neon-flicker { |
| | | 0%, 100% { |
| | | opacity: 1; |
| | | box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | 50% { |
| | | opacity: 0.8; |
| | | box-shadow: -2px -2px 5px #00ffff, 0 0 5px rgba(0, 255, 255, 0.5); |
| | | } |
| | | } |
| | | |
| | | .card-title { |
| | | color: #fff; |
| | | color: #00ffff; |
| | | font-size: 16px; |
| | | text-align: center; |
| | | margin-bottom: 10px; |
| | | text-shadow: 0 0 10px rgba(0, 255, 255, 0.7); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 280px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .stock-total { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 280px; |
| | | } |
| | | |
| | | .total-number { |
| | | font-size: 64px; |
| | | font-weight: bold; |
| | | color: #67caca; |
| | | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; |
| | | } |
| | | |
| | | .total-label { |
| | | font-size: 18px; |
| | | color: #fcf0d8; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | /* 全宽图表 */ |
| | |
| | | .full-width .chart-content { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 添加网格线效果 */ |
| | | .dashboard-container::before { |
| | | content: ""; |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-image: |
| | | linear-gradient(rgba(64, 224, 208, 0.05) 1px, transparent 1px), |
| | | linear-gradient(90deg, rgba(64, 224, 208, 0.05) 1px, transparent 1px); |
| | | background-size: 30px 30px; |
| | | pointer-events: none; |
| | | z-index: -1; |
| | | } |
| | | </style> |
| | |
| | | <div class="vol-aside" :style="{ width: menuWidth + 'px' }"> |
| | | <div class="header" :style="{ width: menuWidth - 1 + 'px' }"> |
| | | <img v-show="!isCollapse" v-bind:src="logo" /> |
| | | <i |
| | | @click="toggleLeft" |
| | | class="collapse-menu" |
| | | :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" |
| | | /> |
| | | <i @click="toggleLeft" class="collapse-menu" :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" /> |
| | | </div> |
| | | <div class="vol-menu"> |
| | | <el-scrollbar style="height: 100%"> |
| | | <VolMenu |
| | | :currentMenuId="currentMenuId" |
| | | :on-select="onSelect" |
| | | :enable="true" |
| | | :open-select="false" |
| | | :isCollapse="isCollapse" |
| | | :list="menuOptions" |
| | | ></VolMenu> |
| | | <VolMenu :currentMenuId="currentMenuId" :on-select="onSelect" :enable="true" :open-select="false" |
| | | :isCollapse="isCollapse" :list="menuOptions"></VolMenu> |
| | | </el-scrollbar> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="project-name">WMS</div> |
| | | <div class="header-text"> |
| | | <div class="h-link"> |
| | | <a |
| | | href="javascript:void(0)" |
| | | @click="to(item)" |
| | | v-for="(item, index) in links.filter((c) => { |
| | | <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => { |
| | | return !c.icon; |
| | | })" |
| | | :key="index" |
| | | > |
| | | })" :key="index"> |
| | | <span v-if="!item.icon"> {{ item.text }}</span> |
| | | <i v-else :class="item.icon"></i> |
| | | </a> |
| | |
| | | </div> |
| | | <div class="header-info"> |
| | | <div class="h-link"> |
| | | <a |
| | | href="javascript:void(0)" |
| | | @click="to(item)" |
| | | v-for="(item, index) in links.filter((c) => { |
| | | <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => { |
| | | return c.icon; |
| | | })" |
| | | :key="index" |
| | | > |
| | | })" :key="index"> |
| | | <span v-if="!item.icon"> {{ item.text }}</span> |
| | | <i v-else :class="item.icon"></i> |
| | | </a> |
| | |
| | | <!--消息管理--> |
| | | |
| | | <div class="h-link" @click="messageModel = true"> |
| | | <a |
| | | ><i class="el-icon-message-solid" |
| | | ><el-badge |
| | | :value="messageList.length" |
| | | :type="messageList.length > 0 ? 'danger' : 'success'" |
| | | class="item" |
| | | style="width: 10px" |
| | | ></el-badge></i |
| | | ></a> |
| | | <a><i class="el-icon-message-solid"><el-badge :value="messageList.length" |
| | | :type="messageList.length > 0 ? 'danger' : 'success'" class="item" |
| | | style="width: 10px"></el-badge></i></a> |
| | | </div> |
| | | <div> |
| | | <img class="user-header" :src="userImg" :onerror="errorImg" /> |
| | |
| | | <span id="index-date"></span> |
| | | </div> |
| | | <div class="settings"> |
| | | <i |
| | | style="font-size: 20px" |
| | | class="el-icon-s-tools" |
| | | @click="drawer_model = true" |
| | | /> |
| | | <i style="font-size: 20px" class="el-icon-s-tools" @click="drawer_model = true" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="vol-path"> |
| | | <el-tabs |
| | | @tab-click="selectNav" |
| | | @tab-remove="removeNav" |
| | | @contextmenu.prevent="bindRightClickMenu(false)" |
| | | type="border-card" |
| | | class="header-navigation" |
| | | v-model="selectId" |
| | | :strtch="false" |
| | | > |
| | | <el-tab-pane |
| | | v-for="(item, navIndex) in navigation" |
| | | type="card" |
| | | :name="navIndex + ''" |
| | | :closable="navIndex > 0" |
| | | :key="navIndex" |
| | | :label="item.name" |
| | | > |
| | | <el-tabs @tab-click="selectNav" @tab-remove="removeNav" @contextmenu.prevent="bindRightClickMenu(false)" |
| | | type="border-card" class="header-navigation" v-model="selectId" :strtch="false"> |
| | | <el-tab-pane v-for="(item, navIndex) in navigation" type="card" :name="navIndex + ''" :closable="navIndex > 0" |
| | | :key="navIndex" :label="item.name"> |
| | | <span style="display: none">{{ navIndex }}</span> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <!-- 右键菜单 --> |
| | | <div v-show="contextMenuVisible"> |
| | | <ul |
| | | :style="{ left: menuLeft + 'px', top: menuTop + 'px' }" |
| | | class="contextMenu" |
| | | > |
| | | <ul :style="{ left: menuLeft + 'px', top: menuTop + 'px' }" class="contextMenu"> |
| | | <li v-show="visibleItem.all"> |
| | | <el-button link @click="closeTabs()"> |
| | | <i class="el-icon-close"></i> |
| | | {{ |
| | | navigation.length == 2 ? "关闭菜单" : "关闭所有" |
| | | }}</el-button |
| | | > |
| | | }}</el-button> |
| | | </li> |
| | | <li v-show="visibleItem.left"> |
| | | <el-button link @click="closeTabs('left')" |
| | | ><i class="el-icon-back"></i>关闭左边</el-button |
| | | > |
| | | <el-button link @click="closeTabs('left')"><i class="el-icon-back"></i>关闭左边</el-button> |
| | | </li> |
| | | <li v-show="visibleItem.right"> |
| | | <el-button link @click="closeTabs('right')"> |
| | | <i class="el-icon-right"></i>关闭右边</el-button |
| | | > |
| | | <i class="el-icon-right"></i>关闭右边</el-button> |
| | | </li> |
| | | <li v-show="visibleItem.other"> |
| | | <el-button link @click="closeTabs('other')" |
| | | ><i class="el-icon-right"></i>关闭其他 |
| | | <el-button link @click="closeTabs('other')"><i class="el-icon-right"></i>关闭其他 |
| | | </el-button> |
| | | </li> |
| | | </ul> |
| | |
| | | <loading v-show="$store.getters.isLoading()"></loading> |
| | | <router-view v-slot="{ Component }"> |
| | | <keep-alive> |
| | | <component |
| | | :is="Component" |
| | | :key="$route.name" |
| | | v-if=" |
| | | <component :is="Component" :key="$route.name" v-if=" |
| | | !$route.meta || |
| | | ($route.meta && !$route.meta.hasOwnProperty('keepAlive')) |
| | | " |
| | | /> |
| | | " /> |
| | | </keep-alive> |
| | | <component |
| | | :is="Component" |
| | | :key="$route.name" |
| | | v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')" |
| | | /> |
| | | <component :is="Component" :key="$route.name" |
| | | v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')" /> |
| | | </router-view> |
| | | </el-scrollbar> |
| | | </div> |
| | | </div> |
| | | <el-drawer |
| | | title="选择主题" |
| | | v-model="drawer_model" |
| | | direction="rtl" |
| | | destroy-on-close |
| | | > |
| | | <el-drawer title="选择主题" v-model="drawer_model" direction="rtl" destroy-on-close> |
| | | <div class="theme-selector"> |
| | | <div |
| | | @click="changeTheme(item.name)" |
| | | class="item" |
| | | v-for="(item, index) in theme_color" |
| | | :key="index" |
| | | :style="{ background: item.color }" |
| | | > |
| | | <div |
| | | v-show="item.leftColor" |
| | | :style="{ background: item.leftColor }" |
| | | style="height: 100%; width: 20px" |
| | | class="t-left" |
| | | ></div> |
| | | <div @click="changeTheme(item.name)" class="item" v-for="(item, index) in theme_color" :key="index" |
| | | :style="{ background: item.color }"> |
| | | <div v-show="item.leftColor" :style="{ background: item.leftColor }" style="height: 100%; width: 20px" |
| | | class="t-left"></div> |
| | | <div class="t-right"></div> |
| | | </div> |
| | | </div> |
| | | </el-drawer> |
| | | |
| | | <el-drawer |
| | | title="消息列表" |
| | | v-model="messageModel" |
| | | direction="rtl" |
| | | destroy-on-close |
| | | size="40%" |
| | | > |
| | | <el-drawer title="消息列表" v-model="messageModel" direction="rtl" destroy-on-close size="40%"> |
| | | <Message :list="messageList"></Message> |
| | | </el-drawer> |
| | | </div> |
| | |
| | | } |
| | | |
| | | createSocket("ws://127.0.0.1:9296/" + _userInfo.userName); |
| | | |
| | | // createSocket("ws://127.0.0.1:9296"); |
| | | Object.assign(_config.$tabs, { open: open, close: close }); |
| | | |
| | | http.get("api/Sys_Menu/getTreeMenu", {}, true).then((data) => { |
| | |
| | | font-size: 14px; |
| | | color: #333; |
| | | box-shadow: 2px 2px 3px 0 rgb(182 182 182 / 20%); |
| | | |
| | | i, |
| | | button { |
| | | font-size: 14px !important; |
| | |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .el-tabs.el-tabs--top.el-tabs--border-card.header-navigation |
| | | > .el-tabs__header |
| | | .el-tabs__item:last-child, |
| | | .el-tabs--top.el-tabs--border-card.header-navigation |
| | | > .el-tabs__header |
| | | .el-tabs__item:nth-child(2) { |
| | | .el-tabs.el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:last-child, |
| | | .el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:nth-child(2) { |
| | | padding: 0; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | function hasCargo(location) { |
| | | // 如果LocationStatus=100,即使没有库存信息也显示货位 |
| | | if (location.locationStatus === 100) { |
| | | return true |
| | | } |
| | | return Number(location.stockQuantity || 0) > 0 || ((location.details && location.details.length > 0) || false) |
| | | } |
| | | |
| | |
| | | { |
| | | public static class WebSocketSetup |
| | | { |
| | | //public static void AddWebSocketSetup(this IServiceCollection services) |
| | | //{ |
| | | // if (services == null) throw new ArgumentNullException(nameof(services)); |
| | | |
| | | // int port = AppSettings.Get("WebSocketPort").ObjToInt(); |
| | | // if (port == 0) |
| | | // { |
| | | // port = 9296; |
| | | // } |
| | | // services.AddSingleton(x => |
| | | // { |
| | | // WebSocketServer socketServer = new WebSocketServer(); |
| | | // socketServer.ServerStart(port); |
| | | // return socketServer; |
| | | // }); |
| | | //} |
| | | public static void AddWebSocketSetup(this IServiceCollection services) |
| | | { |
| | | if (services == null) throw new ArgumentNullException(nameof(services)); |
| | |
| | | port = 9296; |
| | | } |
| | | |
| | | services.AddSingleton(x => |
| | | { |
| | | // 直接创建并启动 WebSocket 服务器
|
| | | WebSocketServer socketServer = new WebSocketServer(); |
| | | socketServer.ServerStart(port); |
| | | return socketServer; |
| | | }); |
| | | services.AddSingleton(socketServer);
|
| | |
|
| | | } |
| | | } |
| | | } |
| | |
| | | _unitOfWorkManage.RollbackTran(); |
| | | return response.Error("库存信息创建失败"); |
| | | }
|
| | | // 使用库存服务添加库存主记录和明细
|
| | | //var stockResult = await _stockInfoService.AddStockWithDetailsUsingTransactionAsync(stock); |
| | | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | using Microsoft.AspNetCore.Authorization; |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using SqlSugar; |
| | | using WIDESEA_Common.LocationEnum;
|
| | | using WIDESEA_Core; |
| | | using WIDESEA_Model.Models; |
| | | |
| | |
| | | /// <summary> |
| | | /// 每日统计 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 注意:数据在 SQL 层过滤后,在应用层按日期分组。 |
| | | /// SqlSugar 的 GroupBy 不支持对 .Date 这样的计算列直接生成 SQL GROUP BY, |
| | | /// 因此采用此方式以确保跨数据库兼容性。 |
| | | /// </remarks> |
| | | [HttpGet("DailyStats"), AllowAnonymous] |
| | | public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30) |
| | | { |
| | |
| | | if (days > 365) days = 365; |
| | | |
| | | var startDate = DateTime.Today.AddDays(-days + 1); |
| | | var endDate = DateTime.Today; // 包含今天
|
| | | |
| | | var query = await _db.Queryable<Dt_Task_Hty>() |
| | | .Where(t => t.InsertTime >= startDate) |
| | | .Where(t => t.InsertTime >= startDate && t.InsertTime <= endDate)
|
| | | .Select(t => new { t.InsertTime, t.TaskType }) |
| | | .ToListAsync(); |
| | | |
| | | var result = query |
| | | // 生成日期范围
|
| | | var allDates = new List<DateTime>();
|
| | | for (var date = startDate; date <= endDate; date = date.AddDays(1))
|
| | | {
|
| | | allDates.Add(date);
|
| | | }
|
| | |
|
| | | // 按日期分组统计
|
| | | var groupedData = query
|
| | | .GroupBy(t => t.InsertTime.Date) |
| | | .Select(g => new |
| | | { |
| | | Date = g.Key.ToString("yyyy-MM-dd"), |
| | | Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600), |
| | | Date = g.Key,
|
| | | Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
|
| | | Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200) |
| | | })
|
| | | .ToDictionary(x => x.Date, x => x);
|
| | |
|
| | | // 补全缺失日期
|
| | | var result = allDates.Select(date =>
|
| | | {
|
| | | if (groupedData.TryGetValue(date, out var data))
|
| | | {
|
| | | return new
|
| | | {
|
| | | Date = date.ToString("MM-dd"),
|
| | | Inbound = data.Inbound,
|
| | | Outbound = data.Outbound
|
| | | };
|
| | | }
|
| | | else
|
| | | {
|
| | | return new
|
| | | {
|
| | | Date = date.ToString("MM-dd"),
|
| | | Inbound = 0,
|
| | | Outbound = 0
|
| | | };
|
| | | }
|
| | | }) |
| | | .OrderBy(x => x.Date) |
| | | .ToList(); |
| | |
| | | return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 每周统计 |
| | | /// </summary> |
| | |
| | | .Select(g => new |
| | | { |
| | | Week = g.Key, |
| | | Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600), |
| | | Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
|
| | | Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200) |
| | | }) |
| | | .OrderBy(x => x.Week) |
| | |
| | | /// 每月统计 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 注意:数据在 SQL 层过滤后,在应用层按年月分组。 |
| | | /// SqlSugar 的 GroupBy 不支持匿名对象 (Year, Month) 直接映射到 SQL GROUP BY, |
| | | /// 因此采用此方式以确保跨数据库兼容性。 |
| | | /// 按年月统计入站和出站任务数量
|
| | | /// </remarks> |
| | | [HttpGet("MonthlyStats"), AllowAnonymous] |
| | | public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12) |
| | |
| | | var startDate = DateTime.Today.AddMonths(-months + 1); |
| | | startDate = new DateTime(startDate.Year, startDate.Month, 1); |
| | | |
| | | var query = await _db.Queryable<Dt_Task_Hty>() |
| | | var monthlyStats = await _db.Queryable<Dt_Task_Hty>()
|
| | | .Where(t => t.InsertTime >= startDate) |
| | | .Select(t => new { t.InsertTime, t.TaskType }) |
| | | .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
|
| | | .Select(t => new
|
| | | {
|
| | | Year = t.InsertTime.Year,
|
| | | Month = t.InsertTime.Month,
|
| | | Inbound = SqlFunc.AggregateSum(
|
| | | SqlFunc.IIF(t.TaskType >= 200 && t.TaskType < 300, 1, 0)
|
| | | ),
|
| | | Outbound = SqlFunc.AggregateSum(
|
| | | SqlFunc.IIF(t.TaskType >= 100 && t.TaskType < 200, 1, 0)
|
| | | )
|
| | | })
|
| | | .OrderBy(t => t.Year)
|
| | | .OrderBy(t => t.Month)
|
| | | .ToListAsync(); |
| | | |
| | | var result = query |
| | | .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month }) |
| | | .Select(g => new |
| | | // 生成所有需要统计的月份列表
|
| | | var allMonths = new List<DateTime>();
|
| | | var currentMonth = startDate;
|
| | | var endMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
|
| | |
|
| | | while (currentMonth <= endMonth)
|
| | | { |
| | | Month = $"{g.Key.Year}-{g.Key.Month:D2}", |
| | | Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600), |
| | | Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200) |
| | | }) |
| | | .OrderBy(x => x.Month) |
| | | .ToList(); |
| | | allMonths.Add(currentMonth);
|
| | | currentMonth = currentMonth.AddMonths(1);
|
| | | }
|
| | |
|
| | | // 将查询结果转换为字典,方便查找
|
| | | var statsDict = monthlyStats.ToDictionary(
|
| | | s => $"{s.Year}-{s.Month:D2}",
|
| | | s => new { s.Inbound, s.Outbound }
|
| | | );
|
| | |
|
| | | // 构建完整的结果列表,包含所有月份
|
| | | var result = new List<object>();
|
| | | foreach (var month in allMonths)
|
| | | {
|
| | | var monthKey = $"{month.Year}-{month.Month:D2}";
|
| | |
|
| | | if (statsDict.TryGetValue(monthKey, out var stat))
|
| | | {
|
| | | result.Add(new
|
| | | {
|
| | | Month = monthKey,
|
| | | Inbound = stat.Inbound,
|
| | | Outbound = stat.Outbound
|
| | | });
|
| | | }
|
| | | else
|
| | | {
|
| | | result.Add(new
|
| | | {
|
| | | Month = monthKey,
|
| | | Inbound = 0,
|
| | | Outbound = 0
|
| | | });
|
| | | }
|
| | | }
|
| | | |
| | | return WebResponseContent.Instance.OK(null, result); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | // 记录异常日志(实际项目中建议使用日志框架)
|
| | | // _logger.LogError(ex, "每月统计获取失败");
|
| | |
|
| | | return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}"); |
| | | } |
| | | } |
| | |
| | | { |
| | | try |
| | | { |
| | | // 查询仓库名称 |
| | | // 查询所有仓库信息
|
| | | var warehouses = await _db.Queryable<Dt_Warehouse>() |
| | | .Select(w => new { w.WarehouseId, w.WarehouseName }) |
| | | .ToListAsync(); |
| | | var warehouseDict = warehouses.ToDictionary(w => w.WarehouseId, w => w.WarehouseName); |
| | | |
| | | // 使用 SQL GROUP BY 在数据库层面聚合,仅返回聚合结果 |
| | | var stockGroups = await _db.Queryable<Dt_StockInfo>() |
| | | .GroupBy(s => s.WarehouseId) |
| | | .Select(s => new { s.WarehouseId, Count = SqlFunc.AggregateCount(s.Id) }) |
| | | // 查询所有货位信息,按仓库分组统计总数
|
| | | var locationGroups = await _db.Queryable<Dt_LocationInfo>()
|
| | | .GroupBy(l => l.WarehouseId)
|
| | | .Select(l => new
|
| | | {
|
| | | WarehouseId = l.WarehouseId,
|
| | | TotalLocations = SqlFunc.AggregateCount(l.Id)
|
| | | })
|
| | | .ToListAsync(); |
| | | |
| | | var result = stockGroups |
| | | .Select(g => new |
| | | // 查询状态不为Free的货位信息(有货货位),按仓库分组统计
|
| | | var occupiedLocationGroups = await _db.Queryable<Dt_LocationInfo>()
|
| | | .Where(l => l.LocationStatus != (int)LocationStatusEnum.Free)
|
| | | .GroupBy(l => l.WarehouseId)
|
| | | .Select(l => new
|
| | | { |
| | | Warehouse = warehouseDict.TryGetValue(g.WarehouseId, out var name) ? name : $"仓库{g.WarehouseId}", |
| | | Count = g.Count |
| | | WarehouseId = l.WarehouseId,
|
| | | OccupiedLocations = SqlFunc.AggregateCount(l.Id)
|
| | | }) |
| | | .ToList(); |
| | | .ToListAsync();
|
| | |
|
| | | // 将仓库信息与货位统计信息合并
|
| | | var result = warehouses.Select(w =>
|
| | | {
|
| | | var totalLocations = locationGroups.FirstOrDefault(lg => lg.WarehouseId == w.WarehouseId)?.TotalLocations ?? 0;
|
| | | var occupiedLocations = occupiedLocationGroups.FirstOrDefault(og => og.WarehouseId == w.WarehouseId)?.OccupiedLocations ?? 0;
|
| | | var emptyLocations = totalLocations - occupiedLocations;
|
| | |
|
| | | var occupiedPercentage = totalLocations > 0 ? Math.Round((double)occupiedLocations / totalLocations * 100, 2) : 0.0;
|
| | | var emptyPercentage = totalLocations > 0 ? Math.Round((double)emptyLocations / totalLocations * 100, 2) : 0.0;
|
| | |
|
| | | return new
|
| | | {
|
| | | Warehouse = w.WarehouseName,
|
| | | Total = totalLocations,
|
| | | HasStock = occupiedLocations,
|
| | | NoStock = emptyLocations,
|
| | | HasStockPercentage = $"{occupiedPercentage}%",
|
| | | NoStockPercentage = $"{emptyPercentage}%"
|
| | | };
|
| | | }).ToList();
|
| | | |
| | | return WebResponseContent.Instance.OK(null, result); |
| | | } |