| | |
| | | <section v-if="dashboardCards.length > 0" class="top-row"> |
| | | <div class="stats-grid"> |
| | | <article |
| | | v-for="card in dashboardCards" |
| | | :key="card.key" |
| | | class="stat-card" |
| | | :class="card.key" |
| | | v-for="card in dashboardCards" |
| | | :key="card.key" |
| | | class="stat-card" |
| | | :class="card.key" |
| | | > |
| | | <div class="stat-header"> |
| | | <div class="stat-title-wrap"> |
| | |
| | | <div class="process-body"> |
| | | <div class="process-chart" :class="{ empty: !hasProcessData }"> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="{ width: '100%', height: '100%' }" |
| | | :grid="processGrid" |
| | | :series="processSeries" |
| | | :tooltip="processTooltip" |
| | | :xAxis="processXAxis" |
| | | :yAxis="processYAxis" |
| | | :style="{ height: hasProcessData ? '340px' : '280px' }" |
| | | @click="handleChartClick" |
| | | :options="chartBaseOptions" |
| | | :chartStyle="{ width: '100%', height: '100%' }" |
| | | :grid="processGrid" |
| | | :series="processSeries" |
| | | :tooltip="processTooltip" |
| | | :xAxis="processXAxis" |
| | | :yAxis="processYAxis" |
| | | :style="{ height: hasProcessData ? '340px' : '280px' }" |
| | | @click="handleChartClick" |
| | | /> |
| | | <div v-if="!hasProcessData" class="chart-empty"> |
| | | <el-icon><DataAnalysis /></el-icon> |
| | |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | | <div class="panel-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">总合同金额(元)</div> |
| | | <div class="contract-main digital-number">{{ formatNumber(sum) }}</div> |
| | | <div class="contract-compare"> |
| | | 同比 |
| | | <span class="rise">{{ trendText(yny) }}</span> |
| | | 环比 |
| | | <span class="rise">{{ trendText(chain) }}</span> |
| | | </div> |
| | | <div class="panel-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">总合同金额(元)</div> |
| | | <div class="contract-main digital-number">{{ formatNumber(sum) }}</div> |
| | | <div class="contract-compare"> |
| | | 同比 |
| | | <span class="rise">{{ trendText(yny) }}</span> |
| | | 环比 |
| | | <span class="rise">{{ trendText(chain) }}</span> |
| | | </div> |
| | | <div class="contract-chart-wrap"> |
| | | <Echarts |
| | | </div> |
| | | <div class="contract-chart-wrap"> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :legend="pieLegend" |
| | | :chartStyle="chartStylePie" |
| | | :series="materialPieSeries" |
| | | :tooltip="pieTooltip" |
| | | /> |
| | | </div> |
| | | /> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <span class="legend-dot" :style="{ backgroundColor: item.itemStyle?.color }"></span> |
| | | <span class="contract-item-name">{{ item.name }}</span> |
| | | <span class="contract-item-rate">{{ item.rate }}%</span> |
| | | <span class="contract-item-value digital-number">¥{{ formatNumber(item.value) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <span class="legend-dot" :style="{ backgroundColor: item.itemStyle?.color }"></span> |
| | | <span class="contract-item-name">{{ item.name }}</span> |
| | | <span class="contract-item-rate">{{ item.rate }}%</span> |
| | | <span class="contract-item-value digital-number">¥{{ formatNumber(item.value) }}</span> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <el-radio-button :value="1">周</el-radio-button> |
| | | <el-radio-button :value="2">月</el-radio-button> |
| | | <el-radio-button :value="3">季度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}件</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}件</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}件</span></div> |
| | | </div> |
| | | <Echarts |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <el-radio-button :value="1">周</el-radio-button> |
| | | <el-radio-button :value="2">月</el-radio-button> |
| | | <el-radio-button :value="3">季度</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}件</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}件</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}件</span></div> |
| | | </div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 270px" |
| | | /> |
| | | </div> |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="hasRightPanels" class="right-column"> |
| | |
| | | <div class="realtime-grid"> |
| | | <div class="realtime-item" v-for="item in realtimeBoard" :key="item.key"> |
| | | <el-progress |
| | | type="circle" |
| | | :percentage="item.percent" |
| | | :stroke-width="10" |
| | | :width="94" |
| | | :color="item.color" |
| | | type="circle" |
| | | :percentage="item.percent" |
| | | :stroke-width="10" |
| | | :width="94" |
| | | :color="item.color" |
| | | > |
| | | <template #default> |
| | | <div class="realtime-value digital-number">{{ item.display }}</div> |
| | |
| | | </div> |
| | | <div class="quick-grid"> |
| | | <button |
| | | v-for="item in quickEntries" |
| | | :key="item.label" |
| | | class="quick-item" |
| | | type="button" |
| | | @click="goToQuick(item.path)" |
| | | v-for="item in quickEntries" |
| | | :key="item.label" |
| | | class="quick-item" |
| | | type="button" |
| | | @click="goToQuick(item.path)" |
| | | > |
| | | <span class="quick-icon"> |
| | | <el-icon> |
| | |
| | | <div v-if="visiblePanels.receipt" class="cockpit-panel receipt-panel"> |
| | | <div class="panel-title">回款与开票分析</div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries" |
| | | :tooltip="tooltipLine" |
| | | :xAxis="xAxis2" |
| | | :yAxis="yAxis2" |
| | | style="height: 300px" |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries" |
| | | :tooltip="tooltipLine" |
| | | :xAxis="xAxis2" |
| | | :yAxis="yAxis2" |
| | | style="height: 300px" |
| | | /> |
| | | </div> |
| | | |
| | |
| | | }); |
| | | |
| | | const welcomeAvatar = computed(() => |
| | | welcomeAvatarLoadFailed.value || !userStore.avatar ? defaultWelcomeAvatar : userStore.avatar |
| | | welcomeAvatarLoadFailed.value || !userStore.avatar ? defaultWelcomeAvatar : userStore.avatar |
| | | ); |
| | | |
| | | const handleWelcomeAvatarError = () => { |
| | |
| | | }; |
| | | |
| | | watch( |
| | | () => userStore.avatar, |
| | | () => { |
| | | welcomeAvatarLoadFailed.value = false; |
| | | } |
| | | () => userStore.avatar, |
| | | () => { |
| | | welcomeAvatarLoadFailed.value = false; |
| | | } |
| | | ); |
| | | |
| | | const axisTextColor = "#5f6f86"; |
| | |
| | | const name = params?.[0]?.name ?? ""; |
| | | const list = Array.isArray(params) ? params : []; |
| | | const lines = list |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`; |
| | | return `${colorBox}${p.seriesName}<b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`; |
| | | }) |
| | | .join("<br/>"); |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`; |
| | | return `${colorBox}${p.seriesName}<b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`; |
| | | }) |
| | | .join("<br/>"); |
| | | return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>`; |
| | | }, |
| | | }); |
| | |
| | | }); |
| | | |
| | | const processTotals = computed(() => |
| | | processChartData.value.reduce( |
| | | (acc, cur) => { |
| | | acc.input += Number(cur.input || 0); |
| | | acc.scrap += Number(cur.scrap || 0); |
| | | acc.output += Number(cur.output || 0); |
| | | return acc; |
| | | }, |
| | | { input: 0, scrap: 0, output: 0 } |
| | | ) |
| | | processChartData.value.reduce( |
| | | (acc, cur) => { |
| | | acc.input += Number(cur.input || 0); |
| | | acc.scrap += Number(cur.scrap || 0); |
| | | acc.output += Number(cur.output || 0); |
| | | return acc; |
| | | }, |
| | | { input: 0, scrap: 0, output: 0 } |
| | | ) |
| | | ); |
| | | |
| | | const hasProcessData = computed(() => { |
| | |
| | | subLabel: "待付款金额", |
| | | subValue: formatNumber(businessInfo.value.monthPurchaseHaveMoney), |
| | | trend: `占比 ${ratioText( |
| | | businessInfo.value.monthPurchaseHaveMoney, |
| | | businessInfo.value.monthPurchaseMoney |
| | | businessInfo.value.monthPurchaseHaveMoney, |
| | | businessInfo.value.monthPurchaseMoney |
| | | )}`, |
| | | icon: ShoppingCartFull, |
| | | visible: visibleModules.value.procurement, |
| | |
| | | }); |
| | | |
| | | const todayPlanList = computed(() => |
| | | productionOrders.value |
| | | .slice() |
| | | .sort((a, b) => dayjs(a.deliveryDate).valueOf() - dayjs(b.deliveryDate).valueOf()) |
| | | .slice(0, 5) |
| | | productionOrders.value |
| | | .slice() |
| | | .sort((a, b) => dayjs(a.deliveryDate).valueOf() - dayjs(b.deliveryDate).valueOf()) |
| | | .slice(0, 5) |
| | | ); |
| | | |
| | | const avgCompletionRate = computed(() => { |
| | |
| | | |
| | | const normalizeMenuTitle = (title) => String(title || "").replace(/\s+/g, "").trim(); |
| | | const normalizeRoutePath = (path) => |
| | | String(path || "") |
| | | .trim() |
| | | .replace(/\/+/g, "/") |
| | | .replace(/\/$/, "") |
| | | .toLowerCase(); |
| | | String(path || "") |
| | | .trim() |
| | | .replace(/\/+/g, "/") |
| | | .replace(/\/$/, "") |
| | | .toLowerCase(); |
| | | |
| | | const resolveRoutePath = (route, parentPath = "") => { |
| | | const currentPath = String(route?.path || "").trim(); |
| | |
| | | |
| | | const accessibleMenuRoutes = computed(() => { |
| | | const routePool = |
| | | permissionStore.defaultRoutes?.length > 0 |
| | | ? permissionStore.defaultRoutes |
| | | : permissionStore.sidebarRouters?.length > 0 |
| | | ? permissionStore.sidebarRouters |
| | | : permissionStore.routes; |
| | | permissionStore.defaultRoutes?.length > 0 |
| | | ? permissionStore.defaultRoutes |
| | | : permissionStore.sidebarRouters?.length > 0 |
| | | ? permissionStore.sidebarRouters |
| | | : permissionStore.routes; |
| | | return collectAccessibleRoutes(routePool || []); |
| | | }); |
| | | |
| | |
| | | }; |
| | | |
| | | const hasModuleAccess = (config) => |
| | | accessibleMenuRoutes.value.some((route) => { |
| | | const matchedTitle = (config.titles || []).some((title) => route.title === normalizeMenuTitle(title)); |
| | | const matchedPath = (config.pathPrefixes || []).some( |
| | | (prefix) => route.path === prefix || route.path.startsWith(`${prefix}/`) |
| | | ); |
| | | return matchedTitle || matchedPath; |
| | | }); |
| | | accessibleMenuRoutes.value.some((route) => { |
| | | const matchedTitle = (config.titles || []).some((title) => route.title === normalizeMenuTitle(title)); |
| | | const matchedPath = (config.pathPrefixes || []).some( |
| | | (prefix) => route.path === prefix || route.path.startsWith(`${prefix}/`) |
| | | ); |
| | | return matchedTitle || matchedPath; |
| | | }); |
| | | |
| | | const visibleModules = computed(() => ({ |
| | | sales: hasModuleAccess(moduleAccessConfig.sales), |
| | |
| | | })); |
| | | |
| | | const hasLeftPanels = computed( |
| | | () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract || visiblePanels.value.quality |
| | | () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract || visiblePanels.value.quality |
| | | ); |
| | | const hasRightPanels = computed( |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan || visiblePanels.value.receipt |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan || visiblePanels.value.receipt |
| | | ); |
| | | const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value); |
| | | |
| | | const quickEntries = computed(() => |
| | | quickEntryConfigs |
| | | .map((item) => { |
| | | const targetRoute = accessibleMenuRoutes.value.find((route) => |
| | | item.titles.some((title) => route.title === normalizeMenuTitle(title)) |
| | | ); |
| | | const resolvedPath = targetRoute?.path || ""; |
| | | return resolvedPath |
| | | ? { |
| | | label: item.label, |
| | | icon: item.icon, |
| | | path: resolvedPath, |
| | | } |
| | | : null; |
| | | }) |
| | | .filter(Boolean) |
| | | quickEntryConfigs |
| | | .map((item) => { |
| | | const targetRoute = accessibleMenuRoutes.value.find((route) => |
| | | item.titles.some((title) => route.title === normalizeMenuTitle(title)) |
| | | ); |
| | | const resolvedPath = targetRoute?.path || ""; |
| | | return resolvedPath |
| | | ? { |
| | | label: item.label, |
| | | icon: item.icon, |
| | | path: resolvedPath, |
| | | } |
| | | : null; |
| | | }) |
| | | .filter(Boolean) |
| | | ); |
| | | |
| | | const updateNowTime = () => { |
| | |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | overflow-x: clip; |
| | | margin-top: 10px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .digital-number { |
| | |
| | | min-height: 92px; |
| | | padding: 18px 22px; |
| | | background: |
| | | radial-gradient(circle at 8% 20%, rgba(59, 130, 246, 0.12), transparent 32%), |
| | | linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.88)); |
| | | radial-gradient(circle at 8% 20%, rgba(59, 130, 246, 0.12), transparent 32%), |
| | | linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.88)); |
| | | } |
| | | |
| | | .welcome-user { |
| | |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | background: |
| | | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 340 40' preserveAspectRatio='none'%3E%3Cpath d='M0 31C20 16 44 36 66 24C87 12 107 31 129 18C148 8 169 28 193 16C214 5 237 25 259 14C280 3 306 19 340 8' fill='none' stroke='%236ea4ee' stroke-width='1.5'/%3E%3C/svg%3E") |
| | | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 340 40' preserveAspectRatio='none'%3E%3Cpath d='M0 31C20 16 44 36 66 24C87 12 107 31 129 18C148 8 169 28 193 16C214 5 237 25 259 14C280 3 306 19 340 8' fill='none' stroke='%236ea4ee' stroke-width='1.5'/%3E%3C/svg%3E") |
| | | center / 100% 100% no-repeat; |
| | | } |
| | | |
| | |
| | | border: 1px solid rgba(148, 163, 184, 0.24); |
| | | border-radius: 14px; |
| | | background: |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.9)), |
| | | repeating-linear-gradient( |
| | | to right, |
| | | rgba(148, 163, 184, 0.07) 0, |
| | | rgba(148, 163, 184, 0.07) 1px, |
| | | transparent 1px, |
| | | transparent 48px |
| | | ), |
| | | repeating-linear-gradient( |
| | | to bottom, |
| | | rgba(148, 163, 184, 0.06) 0, |
| | | rgba(148, 163, 184, 0.06) 1px, |
| | | transparent 1px, |
| | | transparent 34px |
| | | ); |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.9)), |
| | | repeating-linear-gradient( |
| | | to right, |
| | | rgba(148, 163, 184, 0.07) 0, |
| | | rgba(148, 163, 184, 0.07) 1px, |
| | | transparent 1px, |
| | | transparent 48px |
| | | ), |
| | | repeating-linear-gradient( |
| | | to bottom, |
| | | rgba(148, 163, 184, 0.06) 0, |
| | | rgba(148, 163, 184, 0.06) 1px, |
| | | transparent 1px, |
| | | transparent 34px |
| | | ); |
| | | overflow: hidden; |
| | | padding: 10px; |
| | | } |