| | |
| | | <template> |
| | | <div class="dashboard"> |
| | | <!-- 顶鍿¨ªå两æ --> |
| | | <div class="dashboard-top"> |
| | | <!-- å·¦ï¼ä¼ä¸ä¿¡æ¯+ä¸å¤§æ°æ®å¡çï¼ä¸ä¸æåï¼ --> |
| | | <div class="top-left"> |
| | | <div class="company-info"> |
| | | <!-- é¡¶é¨é®åæ¡ --> |
| | | <div class="welcome-banner"> |
| | | <div class="welcome-title"> |
| | | <span class="welcome-user">{{ userStore.roleName || 'ç³»ç»ç®¡çå' }}</span> |
| | | <span> æ¨å¥½ï¼ç¥æ¨å¼å¿æ¯ä¸å¤©</span> |
| | | </div> |
| | | <div class="welcome-time">ç»å½äº: {{ userStore.currentLoginTime }}</div> |
| | | </div> |
| | | |
| | | <!-- ç¨æ·ä¿¡æ¯å¡ç --> |
| | | <div class="user-card"> |
| | | <img :src="userStore.avatar" class="avatar" alt="" /> |
| | | <div class="user-card-main"> |
| | | <div class="user-name">{{ userStore.name }}</div> |
| | | <div class="user-role">{{ userStore.roleName }}</div> |
| | | <div class="user-meta"> |
| | | <span>{{ userStore.phoneNumber || '123456789' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.deptName || 'ç»ç»æ¶æ' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.postName || 'å²ä½å' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="home-page"> |
| | | <div class="top-bar"> |
| | | <div class="user-box"> |
| | | <img :src="userStore.avatar" class="avatar" alt="" /> |
| | | <div> |
| | | <div class="hello">{{ userStore.roleName || "ç³»ç»ç®¡çå" }}ï¼ä½ 好</div> |
| | | <div class="sub">ç»å½æ¶é´ï¼{{ userStore.currentLoginTime }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="data-cards"> |
| | | <div class="data-card sales"> |
| | | <div class="data-title">é宿°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">æ¬æéå®é¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthSaleMoney }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">æªå¼ç¥¨éé¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthSaleHaveMoney }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | <div class="data-card purchase"> |
| | | <div class="data-title">éè´æ°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">æ¬æéè´é¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthPurchaseMoney }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">å¾
仿¬¾éé¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthPurchaseHaveMoney }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="data-card inventory"> |
| | | <div class="data-title">åºåæ°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">å½ååºåæ»é/ä»¶</div> |
| | | <div class="data-value">{{ businessInfo.inventoryNum }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">仿¥å
¥åº/ä»¶</div> |
| | | <div class="data-value">{{ businessInfo.todayInventoryNum }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- å³ï¼å¾
åäºé¡¹ --> |
| | | <div class="todo-panel"> |
| | | <div class="section-title">å¾
åäºé¡¹</div> |
| | | <ul class="todo-list" v-if="todoList.length > 0"> |
| | | <li v-for="item in todoList" :key="item.id"> |
| | | <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px"> |
| | | <div style="display: flex;justify-content: space-between;align-items: center;"> |
| | | <div class="todo-title">å¾
åç¼å·ï¼{{ item.approveId }}</div> |
| | | <div class="todo-division">é¨é¨ï¼{{ item.approveDeptName }}</div> |
| | | <div class="todo-time">{{ item.approveTime }}</div> |
| | | </div> |
| | | <div class="todo-division">å¾
åäºç±ï¼{{ item.approveReason }}</div> |
| | | </div> |
| | | </li> |
| | | </ul> |
| | | <div v-else style="text-align: center"> |
| | | ææ æ°æ® |
| | | </div> |
| | | <div class="top-actions"> |
| | | <span class="refresh-time">æ°æ®æ´æ°æ¶é´ï¼{{ lastUpdatedAt || "-" }}</span> |
| | | <el-button size="small" type="primary" plain @click="refreshDashboardData">å·æ°æ°æ®</el-button> |
| | | <el-button size="small" plain @click="configDialogVisible = true">é¦é¡µé
ç½®</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel process-panel"> |
| | | <div class="process-panel__header"> |
| | | <div class="section-title">å·¥åºæ°æ®ç产ç»è®¡æç»</div> |
| | | <div style="display: flex; gap: 10px; align-items: center;"> |
| | | <el-button type="primary" size="small" plain icon="Filter" @click="openProcessDialog">鿩工åº</el-button> |
| | | <el-button type="info" size="small" plain icon="Refresh" @click="resetProcessFilter">éç½®</el-button> |
| | | <el-radio-group v-model="processRange" size="small" @change="refreshProcessStats"> |
| | | <el-radio-button :value="1">æ¥</el-radio-button> |
| | | <el-radio-button :value="2">å¨</el-radio-button> |
| | | <el-radio-button :value="3">æ</el-radio-button> |
| | | |
| | | <div class="content-grid"> |
| | | <div class="left-col"> |
| | | <section class="section-card"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">å¿«æ·æä½</div> |
| | | <el-button size="small" type="primary" link @click="openShortcutDialog">æ·»å å¿«æ·å
¥å£</el-button> |
| | | </div> |
| | | <div class="quick-grid"> |
| | | <el-button |
| | | v-for="item in shortcuts" |
| | | :key="`${item.label}-${item.path}`" |
| | | :type="item.invalid ? 'danger' : 'default'" |
| | | @click="goTo(item.path)" |
| | | > |
| | | {{ item.label }} |
| | | </el-button> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card"> |
| | | <div class="section-title">éç¹å¾
å</div> |
| | | <div class="todo-row" v-for="todo in todos" :key="todo.title"> |
| | | <el-tag size="small" :type="todo.type">{{ todo.level }}</el-tag> |
| | | <span>{{ todo.title }}</span> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card"> |
| | | <div class="section-title">ç»è¥å
³æ³¨</div> |
| | | <div class="focus-row" v-for="item in businessFocus" :key="item.name"> |
| | | <span class="focus-name">{{ item.name }}</span> |
| | | <span class="focus-value">{{ item.value }}</span> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card flex-fill-card"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">仿¥å¾
å¤ç</div> |
| | | <el-radio-group v-model="pendingFilter" size="small"> |
| | | <el-radio-button label="all">å
¨é¨</el-radio-button> |
| | | <el-radio-button label="mine">æç</el-radio-button> |
| | | <el-radio-button label="high">é«ä¼å
</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="process-panel__body"> |
| | | <div class="process-panel__chart"> |
| | | <Echarts :chartStyle="{ width: '100%', height: '100%' }" :grid="processGrid" :series="processSeries" |
| | | :tooltip="processTooltip" :xAxis="processXAxis" :yAxis="processYAxis" style="height: 100%" |
| | | @click="handleChartClick" /> |
| | | <div class="task-row" v-for="task in filteredPendingTasks" :key="task.id"> |
| | | <div class="task-left"> |
| | | <el-tag size="small" :type="task.type">{{ task.level }}</el-tag> |
| | | <span class="task-title">{{ task.title }}</span> |
| | | </div> |
| | | <el-button link type="primary" @click="goTo(task.path)">å»å¤ç</el-button> |
| | | </div> |
| | | <el-empty v-if="filteredPendingTasks.length === 0" description="ææ å¾
å¤çäºé¡¹" :image-size="80" /> |
| | | </section> |
| | | </div> |
| | | |
| | | <div class="process-panel__aside"> |
| | | <div class="process-legend"> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-blue"></span><span>æå
¥é</span> |
| | | <div class="right-col"> |
| | | <section class="section-card" v-if="isSectionVisible('trendCards')"> |
| | | <div class="section-title">æè¿7天å
³é®ææ è¶å¿</div> |
| | | <div class="trend-cards"> |
| | | <div class="trend-card clickable" v-for="card in recentTrendCards" :key="card.key" @click="handleTrendCardClick(card)"> |
| | | <div class="trend-head"> |
| | | <span class="trend-label">{{ card.label }}</span> |
| | | <span class="trend-rate" :class="trendClass(card.change)"> |
| | | {{ card.change > 0 ? "+" : "" }}{{ card.change.toFixed(1) }}% |
| | | </span> |
| | | </div> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-yellow"></span><span>æ¥åºé</span> |
| | | </div> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-teal"></span><span>产åºé</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="process-card process-card--name">{{ processAside.processName }}</div> |
| | | |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æå
¥</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalInput) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æ¥åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">累计æ»äº§åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }} |
| | | <div class="trend-value">{{ card.latest }} {{ card.unit }}</div> |
| | | <div class="sparkline"> |
| | | <span |
| | | v-for="(v, idx) in card.values" |
| | | :key="`${card.key}-${idx}`" |
| | | class="sparkline-bar" |
| | | :style="{ height: `${calcBarHeight(v, card.values)}%` }" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card" v-if="isSectionVisible('planTrend')"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">计åä¸ç产è¶å¿</div> |
| | | <el-radio-group v-model="chartRangePlan" size="small" @change="loadPlanTrend"> |
| | | <el-radio-button :label="1">æ¥</el-radio-button> |
| | | <el-radio-button :label="2">å¨</el-radio-button> |
| | | <el-radio-button :label="3">æ</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :legend="planLegend" |
| | | :grid="grid" |
| | | :tooltip="lineTooltip" |
| | | :xAxis="planXAxis" |
| | | :yAxis="valueYAxis" |
| | | :series="planSeries" |
| | | style="height: 300px" |
| | | /> |
| | | </section> |
| | | |
| | | <div class="row-two" v-if="isSectionVisible('qualityChart') || isSectionVisible('costChart')"> |
| | | <section class="section-card" v-if="isSectionVisible('qualityChart')"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">è´¨æ£å¼å¸¸åå¸</div> |
| | | <el-radio-group v-model="chartRangeQuality" size="small" @change="loadQualityData"> |
| | | <el-radio-button :label="1">å¨</el-radio-button> |
| | | <el-radio-button :label="2">æ</el-radio-button> |
| | | <el-radio-button :label="3">å£åº¦</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :tooltip="barTooltip" |
| | | :xAxis="qualityXAxis" |
| | | :yAxis="valueYAxis" |
| | | :series="qualitySeries" |
| | | style="height: 260px" |
| | | /> |
| | | </section> |
| | | <section class="section-card" v-if="isSectionVisible('costChart')"> |
| | | <div class="section-title">è½è䏿æ¬ç»æ</div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :legend="costLegend" |
| | | :tooltip="pieTooltip" |
| | | :series="costSeries" |
| | | style="height: 260px" |
| | | /> |
| | | </section> |
| | | </div> |
| | | |
| | | <section class="section-card" v-if="isSectionVisible('warningCenter')"> |
| | | <div class="section-title">å¼å¸¸é¢è¦ä¸å¿</div> |
| | | <div class="warning-row" v-for="item in warningList" :key="item.id"> |
| | | <div class="warning-left"> |
| | | <el-tag size="small" :type="item.levelType">{{ item.levelText }}</el-tag> |
| | | <span class="warning-title">{{ item.title }}</span> |
| | | </div> |
| | | <el-button link type="danger" @click="goTo(item.path)">å¤ç</el-button> |
| | | </div> |
| | | <el-empty v-if="warningList.length === 0" description="ææ å¼å¸¸é¢è¦" :image-size="80" /> |
| | | </section> |
| | | |
| | | <section class="section-card mini-table-wrap" v-if="isSectionVisible('planTable')"> |
| | | <div class="section-title">çäº§è®¡åæ§è¡æç»</div> |
| | | <el-table :data="planTable" size="small" stripe> |
| | | <el-table-column prop="planNo" label="计ååå·" min-width="150" /> |
| | | <el-table-column prop="product" label="产å" min-width="120" /> |
| | | <el-table-column prop="qty" label="计åé" min-width="90" /> |
| | | <el-table-column prop="issued" label="å·²ä¸å" min-width="90" /> |
| | | <el-table-column prop="status" label="ç¶æ" min-width="100" /> |
| | | <el-table-column label="æä½" min-width="120"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" @click="goTo(routePathMap.plan)">æ¥ç</el-button> |
| | | <el-button |
| | | v-if="row.status !== '已宿'" |
| | | link |
| | | type="success" |
| | | @click="goTo(routePathMap.dispatch)" |
| | | > |
| | | ä¸å |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </section> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å·¥åºéæ©å¼¹çª --> |
| | | <el-dialog v-model="processDialogVisible" title="鿩工åº" width="500px" append-to-body> |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | | <div class="process-grid"> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border> |
| | | {{ item.name }} |
| | | </el-checkbox> |
| | | </div> |
| | | </el-checkbox-group> |
| | | <el-dialog v-model="shortcutDialogVisible" title="æ·»å å¿«æ·å
¥å£ï¼æå¤6个ï¼" width="680px"> |
| | | <div class="shortcut-form-row"> |
| | | <el-tree-select |
| | | v-model="selectedPagePath" |
| | | placeholder="è¯·éæ©é¡µé¢ï¼ç®å½ä¸å¯éï¼" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | node-key="value" |
| | | :data="menuTreeOptions" |
| | | :props="{ label: 'label', value: 'value', children: 'children', disabled: 'disabled' }" |
| | | style="grid-column: span 2" |
| | | /> |
| | | <el-button type="success" @click="addShortcutBySelect">éæ©æ·»å </el-button> |
| | | </div> |
| | | <el-table :data="shortcuts" size="small" border> |
| | | <el-table-column prop="label" label="åç§°" min-width="220" /> |
| | | <el-table-column label="ç¶æ" min-width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag size="small" :type="row.invalid ? 'danger' : 'success'">{{ row.invalid ? "æ æ" : "ææ" }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" min-width="90" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeShortcut($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="configDialogVisible" title="é¦é¡µæ¨¡åé
ç½®" width="520px"> |
| | | <el-checkbox-group v-model="enabledSectionKeys" class="config-check-group"> |
| | | <el-checkbox v-for="item in sectionConfigOptions" :key="item.key" :label="item.key"> |
| | | {{ item.label }} |
| | | </el-checkbox> |
| | | </el-checkbox-group> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="processDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button> |
| | | </span> |
| | | <el-button @click="configDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="saveSectionConfig">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ä¸é¨æ¨ªå两æ --> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div class="section-title">客æ·ååéé¢åæ</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-info"> |
| | | <img src="../assets/images/khtitle.png" alt="" style="width: 42px" /> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">æ»ååéé¢(å
)</div> |
| | | <div class="contract-meta"> |
| | | <div class="main-amount">{{ sum }}</div> |
| | | <div>å¨åæ¯: <span class="up">{{ yny }}% </span> æ¥ç¯æ¯: <span class="up">{{ chain }}% </span></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px"> |
| | | <div> |
| | | <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie" :series="materialPieSeries" |
| | | :tooltip="pieTooltip"></Echarts> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <div style="display: flex;align-items: center;justify-content: space-between;width: 100%"> |
| | | <div class="line" :style="{ color: item.itemStyle.color }">â{{ item.name }}</div> |
| | | <div style="width: 70px">{{ item.rate }}%</div> |
| | | <div>ï¿¥{{ item.value }}</div> |
| | | </div> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;"> |
| | | <div class="section-title">åºæ¶åºä»ç»è®¡</div> |
| | | <!-- <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">--> |
| | | <!-- <el-radio-button label="æå¨" :value="1" />--> |
| | | <!-- <el-radio-button label="ææ" :value="2" />--> |
| | | <!-- <el-radio-button label="æå£åº¦" :value="3" />--> |
| | | <!-- </el-radio-group>--> |
| | | </div> |
| | | <Echarts ref="chart" :color="barColors2" :chartStyle="chartStyle" :grid="grid" :series="barSeries" |
| | | :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis" style="height: 260px"></Echarts> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åºé¨æ¨ªå两æ --> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;"> |
| | | <div class="section-title" style="margin-bottom: 0;">è´¨éç»è®¡</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 ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="barSeries1" |
| | | :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1" style="height: 260px"></Echarts> |
| | | </div> |
| | | |
| | | <div class="main-panel"> |
| | | <div class="section-title">忬¾ä¸å¼ç¥¨åæ</div> |
| | | <Echarts ref="invoiceChart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries" |
| | | :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed, reactive } from 'vue' |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { ElMessage } from "element-plus"; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import * as echarts from 'echarts'; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | | import { |
| | | analysisCustomerContractAmounts, getAmountHalfYear, |
| | | getBusiness, |
| | | expenseCompositionAnalysis, |
| | | getProgressStatistics, |
| | | homeTodos, |
| | | orderCount, |
| | | processDataProductionStatistics, |
| | | statisticsReceivablePayable, |
| | | qualityInspectionStatistics |
| | | qualityInspectionStatistics, |
| | | nonComplianceWarning, |
| | | } from "@/api/viewIndex.js"; |
| | | import { list } from '@/api/productionManagement/productionProcess'; |
| | | |
| | | const router = useRouter(); |
| | | const userStore = useUserStore(); |
| | | const permissionStore = usePermissionStore(); |
| | | |
| | | const userStore = useUserStore() |
| | | const SHORTCUT_STORAGE_KEY = "home-shortcuts-v1"; |
| | | |
| | | const processOptions = ref([]) |
| | | const selectedProcessIds = ref([]) |
| | | const tempProcessIds = ref([]) |
| | | const processDialogVisible = ref(false) |
| | | const activeProcessIndex = ref(0) |
| | | const defaultShortcuts = [ |
| | | { label: "主ç产计å", path: "/productionManagement/productionPlan" }, |
| | | { label: "ç产订å", path: "/productionManagement/productionOrder" }, |
| | | { label: "ç产æ¥å·¥", path: "/productionManagement/productionReporting" }, |
| | | { label: "è¿ç¨æ£", path: "/qualityManagement/processInspection" }, |
| | | { label: "ç产è½è", path: "/energyManagement/productionEnergyConsumption" }, |
| | | { label: "çäº§ææ¬", path: "/costAccounting/productionCostAccounting" }, |
| | | { label: "æ åvså®é
", path: "/costAccounting/stdVsActCostAnalysis" }, |
| | | { label: "å³çåæ", path: "/reportAnalysis/dataDashboard" }, |
| | | ]; |
| | | |
| | | const businessInfo = ref({ |
| | | inventoryNum: 0, |
| | | monthPurchaseHaveMoney: 0, |
| | | monthPurchaseMoney: 0, |
| | | monthSaleHaveMoney: 0, |
| | | monthSaleMoney: 0, |
| | | todayInventoryNum: 0, |
| | | }) |
| | | const qualityStatisticsObject = ref({ |
| | | supplierNum: 0, |
| | | processNum: 0, |
| | | factoryNum: 0, |
| | | }) |
| | | const sum = ref(0) |
| | | const yny = ref(0) |
| | | const chain = ref(0) |
| | | const isRouteValid = (path) => { |
| | | if (!path || !path.startsWith("/")) return false; |
| | | const resolved = router.resolve(path); |
| | | return resolved.matched && resolved.matched.length > 0; |
| | | }; |
| | | |
| | | const pieLegend = reactive({ |
| | | show: false, |
| | | }) |
| | | const barSeries = ref([ |
| | | { |
| | | type: 'bar', |
| | | data: [], |
| | | label: { |
| | | show: true, |
| | | const withValidFlag = (list) => |
| | | list.map((item) => ({ |
| | | ...item, |
| | | invalid: !isRouteValid(item.path), |
| | | })); |
| | | |
| | | const pageOptions = router |
| | | .getRoutes() |
| | | .filter((route) => { |
| | | const hasTitle = Boolean(route.meta?.title); |
| | | const validPath = route.path && route.path.startsWith("/") && !route.path.includes(":"); |
| | | const isVisibleMenu = !route.meta?.hidden && route.path !== "/index"; |
| | | const notSpecial = |
| | | !route.path.includes("redirect") && |
| | | route.path !== "/login" && |
| | | route.path !== "/register" && |
| | | route.path !== "/401" && |
| | | !route.path.includes(":pathMatch"); |
| | | return hasTitle && validPath && isVisibleMenu && notSpecial; |
| | | }) |
| | | .map((route) => ({ |
| | | title: route.meta.title, |
| | | path: route.path, |
| | | })) |
| | | .sort((a, b) => a.path.localeCompare(b.path)); |
| | | |
| | | const normalizePath = (path) => String(path || "").replace(/\/+/g, "/"); |
| | | const joinPath = (parentPath, childPath) => { |
| | | if (!childPath) return normalizePath(parentPath || "/"); |
| | | if (String(childPath).startsWith("/")) return normalizePath(childPath); |
| | | return normalizePath(`${parentPath || ""}/${childPath}`); |
| | | }; |
| | | |
| | | const buildMenuTreeOptions = (routes = [], parentPath = "") => { |
| | | const result = []; |
| | | routes.forEach((route) => { |
| | | if (!route || route.hidden) return; |
| | | const fullPath = joinPath(parentPath, route.path); |
| | | const children = buildMenuTreeOptions(route.children || [], fullPath); |
| | | const title = route.meta?.title; |
| | | if (!title && children.length > 0) { |
| | | result.push(...children); |
| | | return; |
| | | } |
| | | }, |
| | | ]) |
| | | if (!title) return; |
| | | result.push({ |
| | | label: title, |
| | | value: fullPath, |
| | | disabled: children.length > 0, |
| | | children, |
| | | }); |
| | | }); |
| | | return result; |
| | | }; |
| | | |
| | | const barSeries1 = ref([ |
| | | { |
| | | name: 'åææä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | barGap: 0, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | { |
| | | name: 'è¿ç¨ä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | { |
| | | name: 'åºåä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | ]) |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%' // 设置å¾è¡¨å®¹å¨çé«åº¦ |
| | | } |
| | | const chartStylePie = { |
| | | width: '140%', |
| | | height: '140%' // 设置å¾è¡¨å®¹å¨çé«åº¦ |
| | | } |
| | | const grid = { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | } |
| | | const barLegend = { |
| | | show: true, |
| | | data: ['åææä¸åæ ¼æ°', 'è¿ç¨ä¸åæ ¼æ°', 'åºåä¸åæ ¼æ°'] |
| | | } |
| | | const barLegend1 = { |
| | | show: true, |
| | | data: ['é¢ä»è´¦æ¬¾', 'åºä»è´¦æ¬¾', '颿¶è´¦æ¬¾', 'åºæ¶è´¦æ¬¾'] |
| | | } |
| | | const lineLegend = { |
| | | show: true, |
| | | data: ['å¼ç¥¨', '忬¾'] |
| | | } |
| | | const tooltip = { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | } |
| | | const xAxis = [{ |
| | | type: 'value', |
| | | }] |
| | | const xAxis1 = ref([{ |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | data: [] |
| | | }]) |
| | | const yAxis = [{ |
| | | type: 'category', |
| | | data: ['åºä»è´¦æ¬¾', 'åºæ¶è´¦æ¬¾',] |
| | | }] |
| | | const yAxis1 = [{ |
| | | type: 'value' |
| | | }] |
| | | const pieTooltip = reactive({ |
| | | trigger: 'item', |
| | | formatter: function (params) { |
| | | // å¨æçææç¤ºä¿¡æ¯ï¼åºäºæ°æ®é¡¹ç name 屿§ |
| | | const description = params.name === 'æ¬æåæ¬¾éé¢' ? 'æ¬æåæ¬¾éé¢' : 'åºæ¶æ¬¾éé¢'; |
| | | return `${description} ${formatNumber(params.value)}å
${params.percent}%`; |
| | | }, |
| | | position: 'right' |
| | | }) |
| | | const materialPieSeries = ref([ |
| | | { |
| | | type: 'pie', |
| | | radius: ['66%', '90%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: false |
| | | }, |
| | | data: [] |
| | | } |
| | | ]) |
| | | const lineSeries = ref([ |
| | | { |
| | | type: 'line', |
| | | data: [], |
| | | label: { |
| | | show: true |
| | | }, |
| | | showSymbol: true, // æ¾ç¤ºåç¹ |
| | | }, |
| | | ]) |
| | | const tooltipLine = { |
| | | trigger: 'axis', |
| | | } |
| | | const yAxis2 = ref([ |
| | | { |
| | | type: 'value', |
| | | } |
| | | ]) |
| | | const xAxis2 = ref([ |
| | | { |
| | | type: 'category', |
| | | data: [], |
| | | axisLabel: { |
| | | interval: 0, |
| | | formatter: function (value) { |
| | | return value.replace(/~/g, '\n'); |
| | | }, |
| | | } |
| | | } |
| | | ]) |
| | | const menuTreeOptions = computed(() => buildMenuTreeOptions(permissionStore.sidebarRouters || [])); |
| | | const selectableMenuMap = computed(() => { |
| | | const map = new Map(); |
| | | const walk = (list = []) => { |
| | | list.forEach((item) => { |
| | | if (!item.disabled) map.set(item.value, item.label); |
| | | if (item.children?.length) walk(item.children); |
| | | }); |
| | | }; |
| | | walk(menuTreeOptions.value); |
| | | return map; |
| | | }); |
| | | |
| | | // å¾
åäºé¡¹ |
| | | const todoList = ref([]) |
| | | const radio1 = ref(1) |
| | | const qualityRange = ref(1) |
| | | const keywordMap = { |
| | | "主ç产计å": ["ç产计å", "productionPlan"], |
| | | "ç产订å": ["ç产订å", "productionOrder"], |
| | | "ç产æ¥å·¥": ["æ¥å·¥", "productionReporting"], |
| | | "è¿ç¨æ£": ["è¿ç¨æ£", "processInspection"], |
| | | "ç产è½è": ["ç产è½è", "productionEnergyConsumption"], |
| | | "çäº§ææ¬": ["çäº§ææ¬", "productionCostAccounting"], |
| | | "æ åvså®é
": ["æ å", "å®é
", "stdVsActCostAnalysis"], |
| | | "å³çåæ": ["å³ç", "çæ¿", "dataDashboard"], |
| | | }; |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const barChart = ref(null) |
| | | const lineChart = ref(null) |
| | | const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8'] |
| | | const findRouteByKeywords = (keywords = []) => { |
| | | const lowerKeywords = keywords.map((k) => String(k).toLowerCase()); |
| | | return pageOptions.find((item) => { |
| | | const title = String(item.title || "").toLowerCase(); |
| | | const path = String(item.path || "").toLowerCase(); |
| | | return lowerKeywords.some((k) => title.includes(k) || path.includes(k)); |
| | | }); |
| | | }; |
| | | |
| | | // éæºé¢è²çæå½æ° |
| | | const getRandomColor = () => { |
| | | return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'); |
| | | } |
| | | const getPathByKeywords = (keywords = []) => findRouteByKeywords(keywords)?.path || ""; |
| | | |
| | | onMounted(() => { |
| | | getBusinessData() |
| | | analysisCustomer() |
| | | todoInfoS() |
| | | statisticsReceivable() |
| | | qualityStatisticsInfo() |
| | | getAmountHalfYearNum() |
| | | getProcessList() |
| | | }) |
| | | // æ°æ®ç»è®¡ |
| | | const getBusinessData = () => { |
| | | getBusiness().then((res) => { |
| | | businessInfo.value = { ...res.data } |
| | | }) |
| | | } |
| | | // ååéé¢ |
| | | const analysisCustomer = () => { |
| | | analysisCustomerContractAmounts().then((res) => { |
| | | sum.value = res.data.sum |
| | | yny.value = res.data.yny |
| | | chain.value = res.data.chain |
| | | // 为æ¯ä¸ªæ°æ®é¡¹åé
éæºé¢è² |
| | | materialPieSeries.value[0].data = res.data.item.map(item => ({ |
| | | ...item, |
| | | itemStyle: { color: getRandomColor() } |
| | | })) |
| | | }) |
| | | } |
| | | // å¾
åäºé¡¹ |
| | | const todoInfoS = () => { |
| | | homeTodos().then((res) => { |
| | | todoList.value = res.data |
| | | }) |
| | | } |
| | | // è·åå·¥åºå表 |
| | | const getProcessList = () => { |
| | | list().then(res => { |
| | | processOptions.value = res.data |
| | | }) |
| | | } |
| | | |
| | | const openProcessDialog = () => { |
| | | tempProcessIds.value = [...selectedProcessIds.value] |
| | | processDialogVisible.value = true |
| | | } |
| | | |
| | | const handleProcessDialogConfirm = () => { |
| | | selectedProcessIds.value = [...tempProcessIds.value] |
| | | processDialogVisible.value = false |
| | | refreshProcessStats() |
| | | } |
| | | |
| | | const resetProcessFilter = () => { |
| | | selectedProcessIds.value = [] |
| | | tempProcessIds.value = [] |
| | | refreshProcessStats() |
| | | } |
| | | |
| | | const handleChartClick = (params) => { |
| | | if (params && params.dataIndex !== undefined) { |
| | | activeProcessIndex.value = params.dataIndex |
| | | } |
| | | } |
| | | // åºä»åºæ¶ç»è®¡ |
| | | const statisticsReceivable = () => { |
| | | statisticsReceivablePayable({ type: radio1.value }).then((res) => { |
| | | barSeries.value[0].data = [ |
| | | // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | // { value: res.data.advanceMoney, itemStyle: { color: barColors2[2] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } } |
| | | ] |
| | | }) |
| | | } |
| | | // è´¨æ£ç»è®¡ |
| | | const qualityStatisticsInfo = () => { |
| | | qualityInspectionStatistics({ type: qualityRange.value }).then((res) => { |
| | | xAxis1.value[0].data = [] |
| | | barSeries1.value[0].data = [] |
| | | barSeries1.value[1].data = [] |
| | | barSeries1.value[2].data = [] |
| | | res.data.item.forEach(item => { |
| | | xAxis1.value[0].data.push(item.date) |
| | | barSeries1.value[0].data.push(item.supplierNum) |
| | | barSeries1.value[1].data.push(item.processNum) |
| | | barSeries1.value[2].data.push(item.factoryNum) |
| | | const getRecommendedShortcuts = () => { |
| | | const list = defaultShortcuts |
| | | .map((item) => { |
| | | const matched = findRouteByKeywords(keywordMap[item.label] || [item.label]); |
| | | return matched ? { label: item.label, path: matched.path } : null; |
| | | }) |
| | | qualityStatisticsObject.value.supplierNum = res.data.supplierNum |
| | | qualityStatisticsObject.value.processNum = res.data.processNum |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum |
| | | }) |
| | | } |
| | | const getAmountHalfYearNum = async () => { |
| | | const res = await getAmountHalfYear() |
| | | console.log(res) |
| | | const monthName = [] |
| | | const receiptAmount = [] |
| | | const invoiceAmount = [] |
| | | res.data.forEach(item => { |
| | | monthName.push(item.month) |
| | | receiptAmount.push(item.receiptAmount) |
| | | invoiceAmount.push(item.invoiceAmount) |
| | | }) |
| | | // æ£ç¡®ååºå¼èµå¼ï¼å建æ°ç xAxis å series 对象 |
| | | xAxis2.value[0].data = monthName |
| | | xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~')); |
| | | lineSeries.value = [ |
| | | { |
| | | name: 'å¼ç¥¨', |
| | | type: 'line', |
| | | data: invoiceAmount, |
| | | stack: 'Total', |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { |
| | | offset: 0, |
| | | color: 'rgba(131, 207, 255, 1)' |
| | | }, |
| | | { |
| | | offset: 1, |
| | | color: 'rgba(186, 228, 255, 1)' |
| | | } |
| | | ]) |
| | | }, |
| | | itemStyle: { |
| | | color: '#2D99FF', |
| | | borderColor: '#2D99FF' |
| | | }, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | lineStyle: { |
| | | width: 0 |
| | | }, |
| | | showSymbol: true, |
| | | }, |
| | | { |
| | | name: '忬¾', |
| | | type: 'line', |
| | | data: receiptAmount, |
| | | stack: 'Total', |
| | | lineStyle: { |
| | | width: 0 |
| | | }, |
| | | itemStyle: { |
| | | color: '#83CFFF', |
| | | borderColor: '#83CFFF' |
| | | }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { |
| | | offset: 0, |
| | | color: 'rgba(54, 153, 255, 1)' |
| | | }, |
| | | { |
| | | offset: 1, |
| | | color: 'rgba(89, 169, 254, 1)' |
| | | } |
| | | ]) |
| | | }, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | } |
| | | ] |
| | | } |
| | | .filter(Boolean); |
| | | return list.length > 0 ? list : defaultShortcuts; |
| | | }; |
| | | |
| | | // å·¥åºæ°æ®ç产ç»è®¡æç»ï¼åæ°æ® + å¾è¡¨ï¼ |
| | | const processRange = ref(1) |
| | | const processChartData = ref([]) |
| | | const tryRepairSavedShortcut = (item) => { |
| | | const matched = findRouteByKeywords(keywordMap[item.label] || [item.label]); |
| | | if (matched) return { label: item.label, path: matched.path }; |
| | | return item; |
| | | }; |
| | | |
| | | const processXAxis = ref([ |
| | | const getSavedShortcuts = () => { |
| | | const recommended = getRecommendedShortcuts(); |
| | | try { |
| | | const saved = localStorage.getItem(SHORTCUT_STORAGE_KEY); |
| | | if (!saved) return recommended; |
| | | const parsed = JSON.parse(saved); |
| | | if (!Array.isArray(parsed) || parsed.length === 0) return recommended; |
| | | return parsed.map((item) => tryRepairSavedShortcut(item)); |
| | | } catch (error) { |
| | | return recommended; |
| | | } |
| | | }; |
| | | |
| | | const shortcuts = reactive(withValidFlag(getSavedShortcuts().slice(0, 6))); |
| | | const shortcutDialogVisible = ref(false); |
| | | const configDialogVisible = ref(false); |
| | | const selectedPagePath = ref(""); |
| | | const lastUpdatedAt = ref(""); |
| | | const pendingFilter = ref("all"); |
| | | const chartRangePlan = ref(3); |
| | | const chartRangeQuality = ref(2); |
| | | |
| | | const routePathMap = { |
| | | plan: getPathByKeywords(["ç产计å", "productionPlan"]), |
| | | order: getPathByKeywords(["ç产订å", "productionOrder"]), |
| | | processInspection: getPathByKeywords(["è¿ç¨æ£", "processInspection"]), |
| | | meter: getPathByKeywords(["æè¡¨", "meterCollection", "è½è"]), |
| | | dispatch: getPathByKeywords(["ç产è°åº¦", "productionDispatching"]), |
| | | }; |
| | | |
| | | const persistShortcuts = () => { |
| | | localStorage.setItem( |
| | | SHORTCUT_STORAGE_KEY, |
| | | JSON.stringify(shortcuts.slice(0, 6).map(({ label, path }) => ({ label, path }))) |
| | | ); |
| | | }; |
| | | |
| | | const todos = reactive([]); |
| | | |
| | | const businessFocus = reactive([ |
| | | { name: "çäº§è®¢åæ»æ°", value: "-" }, |
| | | { name: "å·²å®æè®¢åæ°", value: "-" }, |
| | | { name: "æªå®æè®¢åæ°", value: "-" }, |
| | | { name: "é¨åå®æè®¢åæ°", value: "-" }, |
| | | { name: "è´¨æ£æ»æ°", value: "-" }, |
| | | { name: "è¿ç¨æ£æ»æ°", value: "-" }, |
| | | ]); |
| | | |
| | | const pendingTasks = reactive([]); |
| | | const warningList = reactive([]); |
| | | const SECTION_CONFIG_KEY = "home-sections-v1"; |
| | | const sectionConfigOptions = [ |
| | | { key: "trendCards", label: "æè¿7天è¶å¿å¡" }, |
| | | { key: "planTrend", label: "计åä¸ç产è¶å¿å¾" }, |
| | | { key: "qualityChart", label: "è´¨æ£å¼å¸¸åå¸å¾" }, |
| | | { key: "costChart", label: "è½è䏿æ¬ç»æå¾" }, |
| | | { key: "warningCenter", label: "å¼å¸¸é¢è¦ä¸å¿" }, |
| | | { key: "planTable", label: "çäº§è®¡åæ§è¡æç»è¡¨" }, |
| | | ]; |
| | | const enabledSectionKeys = ref(sectionConfigOptions.map((i) => i.key)); |
| | | |
| | | const chartStyle = { width: "100%", height: "100%" }; |
| | | const grid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; |
| | | const lineTooltip = { trigger: "axis" }; |
| | | const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } }; |
| | | const pieTooltip = { trigger: "item" }; |
| | | |
| | | const valueYAxis = [{ type: "value" }]; |
| | | const planXAxis = [{ type: "category", data: [] }]; |
| | | const qualityXAxis = [{ type: "category", data: [] }]; |
| | | |
| | | const planLegend = { show: true, data: ["计åé", "ä¸åé", "宿é"] }; |
| | | const costLegend = { |
| | | show: true, |
| | | orient: "vertical", |
| | | right: 10, |
| | | top: "center", |
| | | data: ["è½èææ¬", "çäº§ææ¬", "è´¨éæå¤±ææ¬", "å
¶ä»ææ¬"], |
| | | }; |
| | | |
| | | const planSeries = reactive([ |
| | | { name: "计åé", type: "line", smooth: true, data: [] }, |
| | | { name: "ä¸åé", type: "line", smooth: true, data: [] }, |
| | | { name: "宿é", type: "line", smooth: true, data: [] }, |
| | | ]); |
| | | |
| | | const qualitySeries = reactive([ |
| | | { |
| | | nameTextStyle: { color: 'rgba(0,0,0,0.35)', fontSize: 12 }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.35)' }, |
| | | splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)', type: 'dashed' } }, |
| | | }, |
| | | ]) |
| | | |
| | | const processYAxis = ref([ |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.45)' }, |
| | | name: "å¼å¸¸æ°", |
| | | type: "bar", |
| | | barWidth: 26, |
| | | itemStyle: { color: "#e67e22", borderRadius: [6, 6, 0, 0] }, |
| | | data: [], |
| | | }, |
| | | ]) |
| | | ]); |
| | | |
| | | const processGrid = reactive({ left: 0, right: 100, top: 30, bottom: 20, containLabel: true }) |
| | | |
| | | const processTooltip = reactive({ |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: (params) => { |
| | | 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/>') |
| | | return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>` |
| | | const costSeries = reactive([ |
| | | { |
| | | type: "pie", |
| | | radius: ["45%", "68%"], |
| | | center: ["35%", "50%"], |
| | | label: { formatter: "{b}: {d}%" }, |
| | | data: [], |
| | | }, |
| | | }) |
| | | ]); |
| | | |
| | | const processSeries = computed(() => { |
| | | const input = processChartData.value.map((i) => i.input) |
| | | const scrap = processChartData.value.map((i) => i.scrap) |
| | | const output = processChartData.value.map((i) => i.output) |
| | | const planTable = reactive([]); |
| | | const recentTrendCards = reactive([ |
| | | { key: "planIssued", label: "计åä¸åé", unit: "å", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityRaw", label: "æ¥ææ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityProcess", label: "è¿ç¨æ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityFactory", label: "æåæ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | ]); |
| | | |
| | | return [ |
| | | { |
| | | name: 'æå
¥é', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#1E5BFF', borderRadius: [6, 0, 0, 6] }, |
| | | data: input, |
| | | }, |
| | | { |
| | | name: 'æ¥åºé', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#F7B500' }, |
| | | data: scrap, |
| | | }, |
| | | { |
| | | name: '产åºé', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#19C6C6', borderRadius: [0, 6, 6, 0] }, |
| | | data: output, |
| | | }, |
| | | ] |
| | | }) |
| | | const toNumber = (value) => { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : 0; |
| | | }; |
| | | |
| | | const processAside = computed(() => { |
| | | const list = processChartData.value |
| | | const item = list[activeProcessIndex.value] || {} |
| | | return { |
| | | processName: item.name || 'ææ æ°æ®', |
| | | totalInput: item.input || 0, |
| | | totalScrap: item.scrap || 0, |
| | | totalOutput: item.output || 0, |
| | | const pickFirstNumber = (obj, keys = []) => { |
| | | for (const key of keys) { |
| | | if (obj && obj[key] !== undefined && obj[key] !== null) return toNumber(obj[key]); |
| | | } |
| | | }) |
| | | return 0; |
| | | }; |
| | | |
| | | const formatAmount = (n) => { |
| | | const num = Number(n || 0) |
| | | return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| | | } |
| | | const updateArray = (target, list) => { |
| | | target.splice(0, target.length, ...list); |
| | | }; |
| | | |
| | | const refreshProcessStats = () => { |
| | | processDataProductionStatistics({ |
| | | type: processRange.value, |
| | | processIds: selectedProcessIds.value.length > 0 ? selectedProcessIds.value.join(',') : null |
| | | }).then(res => { |
| | | processChartData.value = res.data.map(item => ({ |
| | | name: item.processName, |
| | | input: item.totalInput, |
| | | scrap: item.totalScrap, |
| | | output: item.totalOutput |
| | | })) |
| | | processYAxis.value[0].data = processChartData.value.map((i) => i.name) |
| | | activeProcessIndex.value = 0 |
| | | }) |
| | | } |
| | | const toFixedOne = (num) => Number(num || 0).toFixed(1); |
| | | |
| | | const normalizeSeven = (list = []) => { |
| | | const nums = list.map((i) => toNumber(i)); |
| | | if (nums.length >= 7) return nums.slice(-7); |
| | | return [...Array(7 - nums.length).fill(0), ...nums]; |
| | | }; |
| | | |
| | | const calcTrend = (list = []) => { |
| | | if (!Array.isArray(list) || list.length === 0) return { latest: 0, change: 0 }; |
| | | const first = toNumber(list[0]); |
| | | const latest = toNumber(list[list.length - 1]); |
| | | if (first === 0) return { latest, change: latest > 0 ? 100 : 0 }; |
| | | return { latest, change: ((latest - first) / first) * 100 }; |
| | | }; |
| | | |
| | | const setTrendCard = (key, values) => { |
| | | const target = recentTrendCards.find((i) => i.key === key); |
| | | if (!target) return; |
| | | const series = normalizeSeven(values); |
| | | const { latest, change } = calcTrend(series); |
| | | target.values = series; |
| | | target.latest = latest; |
| | | target.change = Number(toFixedOne(change)); |
| | | }; |
| | | |
| | | const trendClass = (change) => (change > 0 ? "up" : change < 0 ? "down" : "flat"); |
| | | |
| | | const calcBarHeight = (value, list) => { |
| | | const max = Math.max(...list, 1); |
| | | return Math.max(18, Math.round((toNumber(value) / max) * 100)); |
| | | }; |
| | | |
| | | const filteredPendingTasks = computed(() => { |
| | | if (pendingFilter.value === "high") return pendingTasks.filter((i) => i.level === "é«"); |
| | | if (pendingFilter.value === "mine") { |
| | | const currentUserName = String(userStore?.name || "").toLowerCase(); |
| | | const currentUserId = String(userStore?.userId || ""); |
| | | return pendingTasks.filter((i) => { |
| | | const ownerName = String(i.ownerName || "").toLowerCase(); |
| | | const ownerId = String(i.ownerId || ""); |
| | | return (currentUserName && ownerName && ownerName.includes(currentUserName)) || (currentUserId && ownerId === currentUserId); |
| | | }); |
| | | } |
| | | return pendingTasks; |
| | | }); |
| | | |
| | | const isSectionVisible = (key) => enabledSectionKeys.value.includes(key); |
| | | |
| | | const goTo = (path) => { |
| | | if (!isRouteValid(path)) { |
| | | ElMessage.warning("å½åèåæªé
ç½®è¯¥é¡µé¢ææ è®¿é®æé"); |
| | | return; |
| | | } |
| | | router.push(path); |
| | | }; |
| | | |
| | | const handleTrendCardClick = (card) => { |
| | | const mapping = { |
| | | planIssued: routePathMap.plan || routePathMap.order, |
| | | qualityRaw: routePathMap.processInspection, |
| | | qualityProcess: routePathMap.processInspection, |
| | | qualityFactory: routePathMap.processInspection, |
| | | }; |
| | | const target = mapping[card.key]; |
| | | if (!target) { |
| | | ElMessage.warning("æªé
ç½®å¯è·³è½¬é¡µé¢"); |
| | | return; |
| | | } |
| | | const query = |
| | | card.key === "planIssued" |
| | | ? { dateType: String(chartRangePlan.value), source: "homeTrend" } |
| | | : { dateType: String(chartRangeQuality.value), source: "homeTrend" }; |
| | | router.push({ path: target, query }); |
| | | }; |
| | | |
| | | const openShortcutDialog = () => { |
| | | shortcutDialogVisible.value = true; |
| | | }; |
| | | |
| | | const addShortcutBySelect = () => { |
| | | if (shortcuts.length >= 6) { |
| | | ElMessage.warning("å¿«æ·å
¥å£æå¤åªè½æ·»å 6个"); |
| | | return; |
| | | } |
| | | if (!selectedPagePath.value) { |
| | | ElMessage.warning("请å
鿩页é¢"); |
| | | return; |
| | | } |
| | | if (shortcuts.some((item) => item.path === selectedPagePath.value)) { |
| | | ElMessage.warning("该快æ·å
¥å£å·²åå¨"); |
| | | return; |
| | | } |
| | | const label = selectableMenuMap.value.get(selectedPagePath.value); |
| | | if (!label) { |
| | | ElMessage.warning("è¯·éæ©å¯æ·»å ç页é¢ï¼ç®å½èç¹ä¸å¯é"); |
| | | return; |
| | | } |
| | | shortcuts.push({ |
| | | label, |
| | | path: selectedPagePath.value, |
| | | invalid: !isRouteValid(selectedPagePath.value), |
| | | }); |
| | | persistShortcuts(); |
| | | selectedPagePath.value = ""; |
| | | }; |
| | | |
| | | const removeShortcut = (index) => { |
| | | shortcuts.splice(index, 1); |
| | | persistShortcuts(); |
| | | ElMessage.success("å·²å é¤å¿«æ·å
¥å£"); |
| | | }; |
| | | |
| | | const loadHomeTodos = async () => { |
| | | try { |
| | | const res = await homeTodos(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 4).map((item, idx) => { |
| | | const text = item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const levelType = idx === 0 ? "danger" : idx <= 2 ? "warning" : "success"; |
| | | const level = idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½"; |
| | | return { level, title: text, type: levelType }; |
| | | }); |
| | | updateArray(todos, mapped); |
| | | const pendingMapped = list.slice(0, 4).map((item, idx) => { |
| | | const title = item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const path = inferTodoPath(item); |
| | | return { |
| | | id: item?.id || `${idx}-${title}`, |
| | | title, |
| | | level: idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½", |
| | | type: idx === 0 ? "danger" : idx <= 2 ? "warning" : "success", |
| | | path, |
| | | ownerId: item?.approveUserId || item?.userId || "", |
| | | ownerName: item?.approveUserName || item?.userName || "", |
| | | }; |
| | | }); |
| | | updateArray(pendingTasks, pendingMapped); |
| | | } catch (error) { |
| | | console.error("homeTodosæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadOrderAndProgress = async () => { |
| | | try { |
| | | const [orderRes, progressRes] = await Promise.allSettled([orderCount(), getProgressStatistics()]); |
| | | |
| | | if (orderRes.status === "fulfilled") { |
| | | const items = Array.isArray(orderRes.value?.data) ? orderRes.value.data : []; |
| | | const byName = Object.fromEntries( |
| | | items.map((i) => [String(i?.name || "").replace(/\s/g, ""), i?.value]) |
| | | ); |
| | | businessFocus[0].value = `${pickFirstNumber(byName, ["çäº§è®¢åæ°", "çäº§è®¢åæ»æ°", "æ»è®¢åæ°"]) || 0} å`; |
| | | businessFocus[1].value = `${pickFirstNumber(byName, ["å·²å®æè®¢åæ°"]) || 0} å`; |
| | | businessFocus[2].value = `${pickFirstNumber(byName, ["å¾
çäº§è®¢åæ°", "æªå®æè®¢åæ°"]) || 0} å`; |
| | | businessFocus[3].value = `${pickFirstNumber(byName, ["é¨åå®æè®¢åæ°"]) || 0} å`; |
| | | } |
| | | |
| | | if (progressRes.status === "fulfilled") { |
| | | const p = progressRes.value?.data || {}; |
| | | const detail = Array.isArray(p.completedOrderDetails) ? p.completedOrderDetails : []; |
| | | const rows = detail.slice(0, 6).map((item, index) => { |
| | | const qty = pickFirstNumber(item, ["quantity", "planQuantity"]); |
| | | const done = pickFirstNumber(item, ["completeQuantity", "completedQuantity"]); |
| | | return { |
| | | planNo: item.npsNo || item.productionPlanNo || `NO-${index + 1}`, |
| | | product: item.productCategory || item.productName || "-", |
| | | qty, |
| | | issued: done, |
| | | status: qty > 0 && done >= qty ? "已宿" : done > 0 ? "æ§è¡ä¸" : "å¾
ä¸å", |
| | | }; |
| | | }); |
| | | updateArray(planTable, rows); |
| | | setTrendCard( |
| | | "planIssued", |
| | | detail.slice(-7).map((i) => pickFirstNumber(i, ["completeQuantity", "completedQuantity", "issueNum"])) |
| | | ); |
| | | |
| | | } |
| | | } catch (error) { |
| | | console.error("orderCount/getProgressStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const inferTodoPath = (todo) => { |
| | | const text = `${todo?.approveTypeName || ""}${todo?.approveReason || ""}`.toLowerCase(); |
| | | if (text.includes("计å")) return routePathMap.plan || routePathMap.order; |
| | | if (text.includes("订å")) return routePathMap.order || routePathMap.plan; |
| | | if (text.includes("è¿ç¨æ£") || text.includes("è´¨æ£")) return routePathMap.processInspection || routePathMap.plan; |
| | | if (text.includes("è½è") || text.includes("æè¡¨")) return routePathMap.meter || routePathMap.plan; |
| | | return routePathMap.plan || routePathMap.order || ""; |
| | | }; |
| | | |
| | | const loadPlanTrend = async () => { |
| | | try { |
| | | const res = await processDataProductionStatistics({ type: chartRangePlan.value }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | planXAxis[0].data = list.map((i, index) => i.processName || `å·¥åº${index + 1}`); |
| | | planSeries[0].data = list.map((i) => pickFirstNumber(i, ["totalInput", "input", "planNum"])); |
| | | planSeries[1].data = list.map((i) => pickFirstNumber(i, ["totalOutput", "output", "issueNum"])); |
| | | planSeries[2].data = list.map((i) => pickFirstNumber(i, ["totalScrap", "scrap", "completeNum"])); |
| | | } catch (error) { |
| | | console.error("processDataProductionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadQualityData = async () => { |
| | | try { |
| | | const res = await qualityInspectionStatistics({ type: chartRangeQuality.value }); |
| | | const data = res?.data || {}; |
| | | const items = Array.isArray(data.item) ? data.item : []; |
| | | if (items.length > 0) { |
| | | qualityXAxis[0].data = items.map((i) => i.date || i.name || "-"); |
| | | qualitySeries[0].data = items.map((i) => |
| | | pickFirstNumber(i, ["supplierNum", "processNum", "factoryNum", "totalNum"]) |
| | | ); |
| | | setTrendCard("qualityRaw", items.map((i) => pickFirstNumber(i, ["supplierNum"]))); |
| | | setTrendCard("qualityProcess", items.map((i) => pickFirstNumber(i, ["processNum"]))); |
| | | setTrendCard("qualityFactory", items.map((i) => pickFirstNumber(i, ["factoryNum"]))); |
| | | } else { |
| | | qualityXAxis[0].data = ["æ¥ææ£", "è¿ç¨æ£", "æåæ£"]; |
| | | qualitySeries[0].data = [ |
| | | pickFirstNumber(data, ["supplierNum"]), |
| | | pickFirstNumber(data, ["processNum"]), |
| | | pickFirstNumber(data, ["factoryNum"]), |
| | | ]; |
| | | setTrendCard("qualityRaw", [pickFirstNumber(data, ["supplierNum"])]); |
| | | setTrendCard("qualityProcess", [pickFirstNumber(data, ["processNum"])]); |
| | | setTrendCard("qualityFactory", [pickFirstNumber(data, ["factoryNum"])]); |
| | | } |
| | | businessFocus[4].value = `${pickFirstNumber(data, ["supplierNum", "totalNum"])} æ¡`; |
| | | businessFocus[5].value = `${pickFirstNumber(data, ["processNum"])} æ¡`; |
| | | } catch (error) { |
| | | console.error("qualityInspectionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadWarningCenter = async () => { |
| | | try { |
| | | const res = await nonComplianceWarning(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 6).map((item, idx) => { |
| | | const levelNum = toNumber(item.level ?? item.warningLevel ?? 2); |
| | | const levelType = levelNum >= 3 ? "danger" : levelNum === 2 ? "warning" : "info"; |
| | | const levelText = levelNum >= 3 ? "é«" : levelNum === 2 ? "ä¸" : "ä½"; |
| | | const title = item.name || item.title || item.paramName || `å¼å¸¸é¢è¦ ${idx + 1}`; |
| | | const text = `${title}${item.processName || ""}${item.orderNo || ""}`.toLowerCase(); |
| | | const path = text.includes("è´¨æ£") |
| | | ? routePathMap.processInspection |
| | | : text.includes("订å") |
| | | ? routePathMap.order |
| | | : routePathMap.processInspection || routePathMap.order || routePathMap.plan; |
| | | return { id: item.id || `${idx}-${title}`, levelType, levelText, title, path }; |
| | | }); |
| | | updateArray(warningList, mapped); |
| | | } catch (error) { |
| | | console.error("nonComplianceWarningæ¥å£è·å失败:", error); |
| | | updateArray(warningList, []); |
| | | } |
| | | }; |
| | | |
| | | const initSectionConfig = () => { |
| | | try { |
| | | const raw = localStorage.getItem(SECTION_CONFIG_KEY); |
| | | if (!raw) return; |
| | | const parsed = JSON.parse(raw); |
| | | if (Array.isArray(parsed) && parsed.length > 0) { |
| | | enabledSectionKeys.value = parsed.filter((k) => sectionConfigOptions.some((i) => i.key === k)); |
| | | } |
| | | } catch (error) { |
| | | console.error("读åé¦é¡µé
置失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const saveSectionConfig = () => { |
| | | if (enabledSectionKeys.value.length === 0) { |
| | | ElMessage.warning("è³å°ä¿çä¸ä¸ªæ¨¡å"); |
| | | return; |
| | | } |
| | | localStorage.setItem(SECTION_CONFIG_KEY, JSON.stringify(enabledSectionKeys.value)); |
| | | configDialogVisible.value = false; |
| | | ElMessage.success("é¦é¡µé
置已ä¿å"); |
| | | }; |
| | | |
| | | const loadCostComposition = async () => { |
| | | try { |
| | | const res = await expenseCompositionAnalysis({ type: 1 }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.map((i) => ({ |
| | | name: i.name || "æªå½å", |
| | | value: pickFirstNumber(i, ["value", "amount", "cost"]), |
| | | })); |
| | | costSeries[0].data = mapped; |
| | | } catch (error) { |
| | | console.error("expenseCompositionAnalysisæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const refreshDashboardData = () => { |
| | | loadHomeTodos(); |
| | | loadOrderAndProgress(); |
| | | loadPlanTrend(); |
| | | loadQualityData(); |
| | | loadCostComposition(); |
| | | loadWarningCenter(); |
| | | lastUpdatedAt.value = new Date().toLocaleString(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getBusinessData() |
| | | analysisCustomer() |
| | | todoInfoS() |
| | | statisticsReceivable() |
| | | qualityStatisticsInfo() |
| | | getAmountHalfYearNum() |
| | | refreshProcessStats() |
| | | }) |
| | | initSectionConfig(); |
| | | refreshDashboardData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .dashboard { |
| | | background: #f5f7fa; |
| | | .home-page { |
| | | min-height: 100vh; |
| | | background: #f5f7fb; |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .dashboard-top { |
| | | .top-bar { |
| | | display: flex; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | align-items: flex-start; |
| | | justify-content: space-evenly; |
| | | } |
| | | |
| | | .company-info { |
| | | padding: 0; |
| | | overflow: hidden; |
| | | border-radius: 12px; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 16px; |
| | | background: #fff; |
| | | height: 100%; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .welcome-banner { |
| | | padding: 10px 10px; |
| | | background: linear-gradient(135deg, rgba(229, 240, 255, 0.9), rgba(214, 232, 255, 0.7), rgba(207, 236, 255, 0.9)); |
| | | } |
| | | |
| | | .welcome-title { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #222; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | .welcome-user { |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .welcome-time { |
| | | margin-top: 10px; |
| | | font-size: 16px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | |
| | | .user-card { |
| | | .top-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 18px 22px; |
| | | } |
| | | |
| | | .user-card-main { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #111; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .user-role { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 20px; |
| | | padding: 5px 10px; |
| | | background: rgba(245, 246, 248, 1); |
| | | color: #333; |
| | | width: fit-content; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .user-meta { |
| | | .refresh-time { |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | color: #7b8794; |
| | | } |
| | | |
| | | .user-meta .sep { |
| | | margin: 0 10px; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | .user-box { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .avatar { |
| | | width: 90px; |
| | | height: 90px; |
| | | width: 54px; |
| | | height: 54px; |
| | | border-radius: 50%; |
| | | object-fit: cover; |
| | | flex: 0 0 auto; |
| | | } |
| | | |
| | | .data-cards { |
| | | width: 50%; |
| | | display: flex; |
| | | gap: 16px; |
| | | justify-content: flex-start; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .data-title { |
| | | font-weight: 700; |
| | | font-size: 26px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-num { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .data-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 14px 10px 10px 10px; |
| | | min-width: 160px; |
| | | box-shadow: 0 2px 8px #eee; |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 32%; |
| | | height: 140px; |
| | | } |
| | | |
| | | .data-card.sales { |
| | | background-image: url("../assets/images/xioashoushuju.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.purchase { |
| | | background-image: url("../assets/images/caigou.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.inventory { |
| | | background-image: url("../assets/images/kucun.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-desc { |
| | | font-weight: 500; |
| | | font-size: 13px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-value { |
| | | .hello { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | margin: 10px 0; |
| | | color: #FFFFFF; |
| | | font-weight: 700; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | .top-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | height: 180px; |
| | | width: 20%; |
| | | } |
| | | |
| | | .todo-panel { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | height: 180px; |
| | | width: 30%; |
| | | } |
| | | |
| | | .todo-list { |
| | | height: 100px; |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .todo-list li { |
| | | border-radius: 8px; |
| | | margin-bottom: 12px; |
| | | padding: 8px 20px; |
| | | height: 74px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background: rgba(225, 227, 250, 0.62); |
| | | } |
| | | |
| | | .todo-title { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | position: relative; |
| | | } |
| | | |
| | | .todo-title::before { |
| | | content: ''; |
| | | /* å¿
éï¼è¡¨ç¤ºè¿éæä¸ä¸ªå
容 */ |
| | | position: absolute; |
| | | left: -10px; |
| | | /* å®ä½å°å·¦ä¾§ */ |
| | | top: 50%; |
| | | /* åç´å±
ä¸ */ |
| | | transform: translateY(-50%); |
| | | /* å¾®è°åç´å±
ä¸ */ |
| | | width: 6px; |
| | | /* åçç´å¾ */ |
| | | height: 6px; |
| | | /* åçç´å¾ */ |
| | | background: #498CEB; |
| | | border-radius: 50%; |
| | | /* 让å
¶åæåå½¢ */ |
| | | } |
| | | |
| | | .todo-division { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-time { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-meta { |
| | | color: #888; |
| | | .sub { |
| | | margin-top: 4px; |
| | | color: #6b7785; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .dashboard-row { |
| | | display: flex; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | .content-grid { |
| | | display: grid; |
| | | grid-template-columns: 320px 1fr; |
| | | gap: 16px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .main-panel { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | .left-col, |
| | | .right-col { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | margin-bottom: 16px; |
| | | box-shadow: 0 2px 10px rgba(20, 35, 90, 0.06); |
| | | } |
| | | |
| | | .flex-fill-card { |
| | | flex: 1; |
| | | } |
| | | |
| | | .section-title { |
| | | position: relative; |
| | | font-size: 18px; |
| | | color: #333; |
| | | padding-left: 10px; |
| | | margin-bottom: 10px; |
| | | margin-bottom: 14px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #243447; |
| | | } |
| | | |
| | | .section-title::before { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | content: ''; |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .contract-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; |
| | | height: 90px; |
| | | background: rgba(245, 245, 245, 0.59); |
| | | width: 100%; |
| | | border-radius: 10px; |
| | | padding: 10px 30px; |
| | | } |
| | | |
| | | .contract-summary { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 30px; |
| | | } |
| | | |
| | | .contract-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .contract-name { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #050505; |
| | | } |
| | | |
| | | .contract-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | gap: 80px; |
| | | } |
| | | |
| | | .main-amount { |
| | | font-size: 24px; |
| | | color: rgba(51, 50, 50, 0.85); |
| | | } |
| | | |
| | | .up { |
| | | color: #e57373; |
| | | } |
| | | |
| | | .contract-list { |
| | | margin-top: 16px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | list-style: none; |
| | | padding: 0; |
| | | height: 190px; |
| | | overflow-y: auto; |
| | | width: 460px; |
| | | } |
| | | |
| | | .line { |
| | | position: relative; |
| | | width: 230px; |
| | | } |
| | | |
| | | .line::after { |
| | | content: ''; |
| | | position: absolute; |
| | | right: 2px; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 1px; |
| | | background-color: #C9C5C5; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .contract-list li { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .quality-cards { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .quality-card { |
| | | border-radius: 8px; |
| | | padding: 15px 10px 10px 50px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.67); |
| | | width: 236px; |
| | | height: 49px; |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .quality-card.one { |
| | | background-image: url("../assets/images/yuancailiao.png"); |
| | | } |
| | | |
| | | .quality-card.two { |
| | | background-image: url("../assets/images/guocheng.png"); |
| | | } |
| | | |
| | | .quality-card.three { |
| | | background-image: url("../assets/images/chuchang.png"); |
| | | |
| | | } |
| | | |
| | | .quality-card span { |
| | | color: #4fc3f7; |
| | | font-weight: bold; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | height: 220px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .process-panel { |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .process-panel__header { |
| | | .section-title-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .process-panel__body { |
| | | display: flex; |
| | | gap: 24px; |
| | | align-items: stretch; |
| | | margin-top: 10px; |
| | | .section-title::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | width: 4px; |
| | | height: 16px; |
| | | border-radius: 2px; |
| | | background: #409eff; |
| | | } |
| | | |
| | | .process-panel__chart { |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding: 6px 0; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 260px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .process-legend { |
| | | display: flex; |
| | | flex-direction: column; |
| | | .quick-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | align-items: flex-start; |
| | | padding: 8px 6px; |
| | | } |
| | | |
| | | .process-legend__item { |
| | | .quick-grid :deep(.el-button) { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | .shortcut-form-row { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1.5fr auto; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .todo-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | |
| | | .dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 2px; |
| | | display: inline-block; |
| | | } |
| | | |
| | | .dot-blue { |
| | | background: #1E5BFF; |
| | | } |
| | | |
| | | .dot-yellow { |
| | | background: #F7B500; |
| | | } |
| | | |
| | | .dot-teal { |
| | | background: #19C6C6; |
| | | } |
| | | |
| | | .process-card { |
| | | background: rgba(245, 247, 250, 0.9); |
| | | border-radius: 10px; |
| | | padding: 16px 16px; |
| | | } |
| | | |
| | | .process-card--name { |
| | | background: rgba(235, 242, 255, 1); |
| | | color: #1E5BFF; |
| | | font-weight: 800; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .process-card__label { |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | font-size: 13px; |
| | | color: #3b4a5b; |
| | | } |
| | | |
| | | .process-card__value { |
| | | font-size: 24px; |
| | | font-weight: 800; |
| | | color: rgba(0, 0, 0, 0.8); |
| | | .focus-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .process-card__value .unit { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: rgba(0, 0, 0, 0.45); |
| | | margin-left: 6px; |
| | | .focus-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .process-panel__body { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 100%; |
| | | flex-direction: row; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .process-card { |
| | | flex: 1; |
| | | min-width: 220px; |
| | | } |
| | | .focus-name { |
| | | font-size: 13px; |
| | | color: #516174; |
| | | } |
| | | |
| | | .process-selection-wrapper { |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | padding: 10px; |
| | | .focus-value { |
| | | font-weight: 700; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | .process-grid { |
| | | .task-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .task-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .task-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .task-title { |
| | | font-size: 13px; |
| | | color: #3d4d5f; |
| | | } |
| | | |
| | | .row-two { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .trend-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | :deep(.el-checkbox.is-bordered) { |
| | | margin-left: 0 !important; |
| | | width: 100%; |
| | | .trend-card { |
| | | border: 1px solid #e8edf5; |
| | | border-radius: 10px; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .trend-card.clickable { |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | } |
| | | |
| | | .trend-card.clickable:hover { |
| | | border-color: #8eb8ff; |
| | | background: #f6f9ff; |
| | | } |
| | | |
| | | .trend-head { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .trend-label { |
| | | font-size: 13px; |
| | | color: #5f6b7a; |
| | | } |
| | | |
| | | .trend-rate { |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .trend-rate.up { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .trend-rate.down { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .trend-rate.flat { |
| | | color: #909399; |
| | | } |
| | | |
| | | .trend-value { |
| | | margin-top: 6px; |
| | | font-size: 20px; |
| | | color: #1f2d3d; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .sparkline { |
| | | margin-top: 10px; |
| | | height: 48px; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .sparkline-bar { |
| | | flex: 1; |
| | | min-height: 6px; |
| | | border-radius: 3px 3px 0 0; |
| | | background: linear-gradient(180deg, #82b1ff 0%, #409eff 100%); |
| | | } |
| | | |
| | | .warning-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .warning-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .warning-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .warning-title { |
| | | font-size: 13px; |
| | | color: #3d4d5f; |
| | | } |
| | | |
| | | .config-check-group { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px 16px; |
| | | } |
| | | |
| | | .mini-table-wrap :deep(.el-table th) { |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | | .content-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .row-two { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .trend-cards { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | </style> |