| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container strategy-control"> |
| | | <el-tabs v-model="activeTab" type="border-card" class="main-tabs" @tab-change="handleTabChange"> |
| | | <!-- ä»·æ ¼çç¥é
ç½® --> |
| | | <el-tab-pane label="ä»·æ ¼çç¥é
ç½®" name="priceStrategy"> |
| | | <el-card class="box-card"> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-select v-model="priceSearchForm.customerName" placeholder="è¯·éæ©å®¢æ·" clearable> |
| | | <el-option label="å
¨é¨å®¢æ·" value=""></el-option> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢"></el-option> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸"></el-option> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="priceSearchForm.productType" placeholder="è¯·éæ©æ°´æ³¥ç±»å" clearable> |
| | | <el-option label="å
¨é¨ç±»å" value=""></el-option> |
| | | <el-option label="æ®éç¡
é
¸çæ°´æ³¥" value="æ®éç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="ç¿æ¸£ç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="å¤åç¡
é
¸çæ°´æ³¥" value="å¤åç¡
é
¸çæ°´æ³¥"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="priceSearchForm.strategyType" placeholder="çç¥ç±»å" clearable> |
| | | <el-option label="å
¨é¨çç¥" value=""></el-option> |
| | | <el-option label="ä¸å±ä»·æ ¼" value="ä¸å±ä»·æ ¼"></el-option> |
| | | <el-option label="é¶æ¢¯æ¥ä»·" value="é¶æ¢¯æ¥ä»·"></el-option> |
| | | <el-option label="ä¿éææ£" value="ä¿éææ£"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-button type="primary" @click="searchPriceStrategy">æ¥è¯¢</el-button> |
| | | <el-button @click="resetPriceSearch">éç½®</el-button> |
| | | <el-button type="primary" @click="handleAddPriceStrategy">æ°å¢çç¥</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table :data="priceStrategyList" border stripe v-loading="priceLoading" height="calc(100vh - 26em)"> |
| | | <el-table-column prop="id" label="ID" width="60" align="center"/> |
| | | <el-table-column prop="strategyNo" label="çç¥ç¼å·" width="150"/> |
| | | <el-table-column prop="strategyType" label="çç¥ç±»å" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStrategyTypeColor(scope.row.strategyType)"> |
| | | {{ scope.row.strategyType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="customerName" label="客æ·åç§°" width="180"/> |
| | | <el-table-column prop="productName" label="产ååç§°" width="200"/> |
| | | <el-table-column prop="specification" label="è§æ ¼åå·" width="120"/> |
| | | <el-table-column prop="basePrice" label="åºç¡ä»·æ ¼" width="100"> |
| | | <template #default="scope"> |
| | | Â¥{{ scope.row.basePrice }}/å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="strategyPrice" label="çç¥ä»·æ ¼" width="120"> |
| | | <template #default="scope"> |
| | | <span style="color: #f56c6c; font-weight: bold;"> |
| | | {{ scope.row.strategyPrice }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="validPeriod" label="æææ" width="200"> |
| | | <template #default="scope"> |
| | | {{ scope.row.startDate }} è³ {{ scope.row.endDate }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="ç¶æ" width="80"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.status === 'çæä¸' ? 'success' : 'info'"> |
| | | {{ scope.row.status }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="200" fixed="right" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="handleViewPriceStrategy(scope.row)">æ¥ç</el-button> |
| | | <el-button link type="primary" @click="handleEditPriceStrategy(scope.row)">ç¼è¾</el-button> |
| | | <el-button link type="danger" @click="handleDeletePriceStrategy(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | :total="pricePagination.total" |
| | | :page="pricePagination.currentPage" |
| | | :limit="pricePagination.pageSize" |
| | | @pagination="handlePricePageChange" |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | |
| | | <!-- ååæ§è¡çæ§ --> |
| | | <el-tab-pane label="ååæ§è¡çæ§" name="contractMonitor"> |
| | | <el-card class="box-card"> |
| | | <!-- ç»è®¡æ¦è§ --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #ecf5ff;"> |
| | | <el-icon :size="30" color="#409eff"><Document /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ contractStats.totalContracts }}</div> |
| | | <div class="stat-label">ååæ»æ°</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f0f9ff;"> |
| | | <el-icon :size="30" color="#67c23a"><Van /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ contractStats.deliveryRate }}%</div> |
| | | <div class="stat-label">交ä»å®æç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #fef0f0;"> |
| | | <el-icon :size="30" color="#e6a23c"><Tickets /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ contractStats.invoiceRate }}%</div> |
| | | <div class="stat-label">å票å¼å
·ç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f4f4f5;"> |
| | | <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ contractStats.paymentRate }}%</div> |
| | | <div class="stat-label">忬¾å®æç</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æç´¢åºå --> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-input v-model="contractSearchForm.contractNo" placeholder="请è¾å
¥ååç¼å·" clearable> |
| | | <template #prefix> |
| | | <el-icon><Search /></el-icon> |
| | | </template> |
| | | </el-input> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="contractSearchForm.customerName" placeholder="è¯·éæ©å®¢æ·" clearable> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢"></el-option> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸"></el-option> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="contractSearchForm.executionStatus" placeholder="æ§è¡ç¶æ" clearable> |
| | | <el-option label="å¾
æ§è¡" value="å¾
æ§è¡"></el-option> |
| | | <el-option label="æ§è¡ä¸" value="æ§è¡ä¸"></el-option> |
| | | <el-option label="已宿" value="已宿"></el-option> |
| | | <el-option label="å¼å¸¸" value="å¼å¸¸"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-button type="primary" @click="searchContract">æ¥è¯¢</el-button> |
| | | <el-button @click="resetContractSearch">éç½®</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- ååå表 --> |
| | | <el-table :data="contractList" border stripe v-loading="contractLoading" height="calc(100vh - 36em)"> |
| | | <el-table-column type="expand"> |
| | | <template #default="scope"> |
| | | <div class="contract-detail-expand"> |
| | | <el-steps :active="getContractStep(scope.row)" align-center> |
| | | <el-step title="订å确认" :description="scope.row.orderDate"> |
| | | <template #icon> |
| | | <el-icon :color="scope.row.orderStatus === '已宿' ? '#67c23a' : '#909399'"> |
| | | <Check v-if="scope.row.orderStatus === '已宿'" /> |
| | | <Clock v-else /> |
| | | </el-icon> |
| | | </template> |
| | | </el-step> |
| | | <el-step title="è´§ç©äº¤ä»" :description="`${scope.row.deliveryProgress}%`"> |
| | | <template #icon> |
| | | <el-icon :color="scope.row.deliveryProgress === 100 ? '#67c23a' : '#409eff'"> |
| | | <Check v-if="scope.row.deliveryProgress === 100" /> |
| | | <Van v-else /> |
| | | </el-icon> |
| | | </template> |
| | | </el-step> |
| | | <el-step title="å票å¼å
·" :description="`${scope.row.invoiceProgress}%`"> |
| | | <template #icon> |
| | | <el-icon :color="scope.row.invoiceProgress === 100 ? '#67c23a' : '#e6a23c'"> |
| | | <Check v-if="scope.row.invoiceProgress === 100" /> |
| | | <Tickets v-else /> |
| | | </el-icon> |
| | | </template> |
| | | </el-step> |
| | | <el-step title="款项æ¶å" :description="`${scope.row.paymentProgress}%`"> |
| | | <template #icon> |
| | | <el-icon :color="scope.row.paymentProgress === 100 ? '#67c23a' : '#f56c6c'"> |
| | | <Check v-if="scope.row.paymentProgress === 100" /> |
| | | <Wallet v-else /> |
| | | </el-icon> |
| | | </template> |
| | | </el-step> |
| | | </el-steps> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="contractNo" label="ååç¼å·" width="150"/> |
| | | <el-table-column prop="customerName" label="客æ·åç§°" width="180"/> |
| | | <el-table-column prop="contractAmount" label="ååéé¢" width="120"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.contractAmount.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="signDate" label="ç¾è®¢æ¥æ" width="120"/> |
| | | <el-table-column label="æ§è¡è¿åº¦" width="150"> |
| | | <template #default="scope"> |
| | | <el-progress |
| | | :percentage="scope.row.executionProgress" |
| | | :color="getProgressColor(scope.row.executionProgress)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deliveryProgress" label="交ä»è¿åº¦" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.deliveryProgress }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="invoiceProgress" label="å¼ç¥¨è¿åº¦" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.invoiceProgress }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="paymentProgress" label="忬¾è¿åº¦" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.paymentProgress }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="executionStatus" label="æ§è¡ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getExecutionStatusType(scope.row.executionStatus)"> |
| | | {{ scope.row.executionStatus }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="120" fixed="right" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="handleViewContract(scope.row)">æ¥ç详æ
</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | :total="contractPagination.total" |
| | | :page="contractPagination.currentPage" |
| | | :limit="contractPagination.pageSize" |
| | | @pagination="handleContractPageChange" |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | |
| | | <!-- å岿¯ä»·åæ --> |
| | | <el-tab-pane label="å岿¯ä»·åæ" name="priceComparison"> |
| | | <el-card class="box-card"> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-select v-model="compareSearchForm.productName" placeholder="è¯·éæ©äº§å" clearable> |
| | | <el-option label="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" value="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" value="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-date-picker |
| | | v-model="compareSearchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="compareSearchForm.region" placeholder="éå®åºå" clearable> |
| | | <el-option label="åä¸å°åº" value="åä¸å°åº"></el-option> |
| | | <el-option label="ååå°åº" value="ååå°åº"></el-option> |
| | | <el-option label="ååå°åº" value="ååå°åº"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-button type="primary" @click="searchPriceComparison">æ¥è¯¢</el-button> |
| | | <el-button @click="resetCompareSearch">éç½®</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- ä»·æ ¼è¶å¿å¾ --> |
| | | <div class="chart-container"> |
| | | <div ref="priceChartRef" style="width: 100%; height: 350px;"></div> |
| | | </div> |
| | | |
| | | <!-- åå²ä»·æ ¼å表 --> |
| | | <el-table :data="priceComparisonList" border stripe v-loading="compareLoading" style="margin-top: 20px;"> |
| | | <el-table-column prop="date" label="æ¥æ" width="120"/> |
| | | <el-table-column prop="productName" label="产ååç§°" width="200"/> |
| | | <el-table-column prop="specification" label="è§æ ¼" width="120"/> |
| | | <el-table-column prop="customerName" label="客æ·" width="180"/> |
| | | <el-table-column prop="region" label="åºå" width="100"/> |
| | | <el-table-column prop="quantity" label="æ°é(å¨)" width="100" align="right"> |
| | | <template #default="scope"> |
| | | {{ scope.row.quantity.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="price" label="æäº¤åä»·" width="100"> |
| | | <template #default="scope"> |
| | | Â¥{{ scope.row.price }}/å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalAmount" label="æäº¤éé¢" width="120"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.totalAmount.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="priceChange" label="ä»·æ ¼åå¨" width="100"> |
| | | <template #default="scope"> |
| | | <span :style="{ color: scope.row.priceChange > 0 ? '#f56c6c' : scope.row.priceChange < 0 ? '#67c23a' : '#909399' }"> |
| | | {{ scope.row.priceChange > 0 ? '+' : '' }}{{ scope.row.priceChange }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="remark" label="夿³¨" show-overflow-tooltip/> |
| | | </el-table> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | |
| | | <!-- 婿¶¦åæ --> |
| | | <el-tab-pane label="婿¶¦åæ" name="profitAnalysis"> |
| | | <el-card class="box-card"> |
| | | <!-- 婿¶¦ç»è®¡å¡ç --> |
| | | <el-row :gutter="20" class="profit-stats-row"> |
| | | <el-col :span="8"> |
| | | <div class="profit-card"> |
| | | <div class="profit-header">æ»éå®é¢</div> |
| | | <div class="profit-value">Â¥{{ profitStats.totalSales.toLocaleString() }}</div> |
| | | <div class="profit-footer"> |
| | | <span>è¾ä¸æ</span> |
| | | <span :class="profitStats.salesGrowth > 0 ? 'growth-up' : 'growth-down'"> |
| | | {{ profitStats.salesGrowth > 0 ? '+' : '' }}{{ profitStats.salesGrowth }}% |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="profit-card"> |
| | | <div class="profit-header">æ»ææ¬</div> |
| | | <div class="profit-value">Â¥{{ profitStats.totalCost.toLocaleString() }}</div> |
| | | <div class="profit-footer"> |
| | | <span>ææ¬ç</span> |
| | | <span class="cost-rate">{{ profitStats.costRate }}%</span> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="profit-card"> |
| | | <div class="profit-header">æ¯å©æ¶¦</div> |
| | | <div class="profit-value profit-highlight">Â¥{{ profitStats.grossProfit.toLocaleString() }}</div> |
| | | <div class="profit-footer"> |
| | | <span>æ¯å©ç</span> |
| | | <span class="gross-profit-rate">{{ profitStats.grossProfitRate }}%</span> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æç´¢åºå --> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-select v-model="profitSearchForm.productType" placeholder="产åç±»å" clearable> |
| | | <el-option label="æ®éç¡
é
¸çæ°´æ³¥" value="æ®éç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="ç¿æ¸£ç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="å¤åç¡
é
¸çæ°´æ³¥" value="å¤åç¡
é
¸çæ°´æ³¥"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="profitSearchForm.customerName" placeholder="客æ·åç§°" clearable> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢"></el-option> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸"></el-option> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå"></el-option> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-date-picker |
| | | v-model="profitSearchForm.dateRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | style="width: 100%" |
| | | /> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-button type="primary" @click="searchProfit">æ¥è¯¢</el-button> |
| | | <el-button @click="resetProfitSearch">éç½®</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 婿¶¦åæå¾è¡¨ --> |
| | | <div class="chart-container"> |
| | | <div ref="profitChartRef" style="width: 100%; height: 350px;"></div> |
| | | </div> |
| | | |
| | | <!-- 婿¶¦æç»è¡¨ --> |
| | | <el-table :data="profitAnalysisList" border stripe v-loading="profitLoading" style="margin-top: 20px;" show-summary :summary-method="getProfitSummary"> |
| | | <el-table-column prop="orderNo" label="订åç¼å·" width="150"/> |
| | | <el-table-column prop="customerName" label="客æ·åç§°" width="180"/> |
| | | <el-table-column prop="productName" label="产ååç§°" width="200"/> |
| | | <el-table-column prop="quantity" label="æ°é(å¨)" width="100" align="right"> |
| | | <template #default="scope"> |
| | | {{ scope.row.quantity.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesPrice" label="éå®åä»·" width="100"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.salesPrice }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="costPrice" label="ææ¬åä»·" width="100"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.costPrice }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesAmount" label="éå®éé¢" width="120" align="right"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.salesAmount.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="costAmount" label="ææ¬éé¢" width="120" align="right"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.costAmount.toLocaleString() }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="grossProfit" label="æ¯å©æ¶¦" width="120" align="right"> |
| | | <template #default="scope"> |
| | | <span :style="{ color: scope.row.grossProfit > 0 ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }"> |
| | | ¥{{ scope.row.grossProfit.toLocaleString() }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="grossProfitRate" label="æ¯å©ç" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getProfitRateType(scope.row.grossProfitRate)"> |
| | | {{ scope.row.grossProfitRate }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="orderDate" label="è®¢åæ¥æ" width="120"/> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | :total="profitPagination.total" |
| | | :page="profitPagination.currentPage" |
| | | :limit="profitPagination.pageSize" |
| | | @pagination="handleProfitPageChange" |
| | | /> |
| | | </el-card> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- ä»·æ ¼çç¥å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="priceStrategyDialogVisible" :title="priceStrategyDialogTitle" width="900px" :close-on-click-modal="false"> |
| | | <el-form :model="priceStrategyForm" :rules="priceStrategyRules" ref="priceStrategyFormRef" label-width="120px"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çç¥ç±»å" prop="strategyType"> |
| | | <el-select v-model="priceStrategyForm.strategyType" placeholder="è¯·éæ©çç¥ç±»å" style="width: 100%;"> |
| | | <el-option label="ä¸å±ä»·æ ¼" value="ä¸å±ä»·æ ¼"></el-option> |
| | | <el-option label="é¶æ¢¯æ¥ä»·" value="é¶æ¢¯æ¥ä»·"></el-option> |
| | | <el-option label="ä¿éææ£" value="ä¿éææ£"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客æ·åç§°" prop="customerName"> |
| | | <el-select v-model="priceStrategyForm.customerName" placeholder="è¯·éæ©å®¢æ·" style="width: 100%;"> |
| | | <el-option label="åä¸å»ºæéå¢" value="åä¸å»ºæéå¢"></el-option> |
| | | <el-option label="é¿æ±æ··ååå
¬å¸" value="é¿æ±æ··ååå
¬å¸"></el-option> |
| | | <el-option label="æµ¦æ±æ°´æ³¥å¶åå" value="æµ¦æ±æ°´æ³¥å¶åå"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="产ååç§°" prop="productName"> |
| | | <el-select v-model="priceStrategyForm.productName" placeholder="è¯·éæ©äº§å" style="width: 100%;"> |
| | | <el-option label="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥" value="P.O 42.5æ®éç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥" value="P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥"></el-option> |
| | | <el-option label="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥" value="P.C 32.5å¤åç¡
é
¸çæ°´æ³¥"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·" prop="specification"> |
| | | <el-input v-model="priceStrategyForm.specification" placeholder="请è¾å
¥è§æ ¼åå·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºç¡ä»·æ ¼(å
/å¨)" prop="basePrice"> |
| | | <el-input-number v-model="priceStrategyForm.basePrice" :min="0" :precision="2" style="width: 100%;" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çç¥ä»·æ ¼" prop="strategyPrice"> |
| | | <el-input v-model="priceStrategyForm.strategyPrice" placeholder="å¦: Â¥350/å¨ æ 9æ" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çææ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="priceStrategyForm.startDate" |
| | | type="date" |
| | | placeholder="éæ©çææ¥æ" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¤±ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="priceStrategyForm.endDate" |
| | | type="date" |
| | | placeholder="éæ©å¤±ææ¥æ" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="çç¥è¯´æ" prop="description"> |
| | | <el-input type="textarea" v-model="priceStrategyForm.description" :rows="3" placeholder="请è¾å
¥çç¥è¯´æ" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="priceStrategyDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSavePriceStrategy">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick, watch } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Document, Van, Tickets, Wallet, Check, Clock, Search } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | import Pagination from '@/components/PIMTable/Pagination.vue' |
| | | |
| | | // æ´»å¨æ ç¾é¡µ |
| | | const activeTab = ref('priceStrategy') |
| | | |
| | | // ========== ä»·æ ¼çç¥é
ç½® ========== |
| | | const priceLoading = ref(false) |
| | | const priceSearchForm = reactive({ |
| | | customerName: '', |
| | | productType: '', |
| | | strategyType: '' |
| | | }) |
| | | |
| | | const priceStrategyList = ref([ |
| | | { |
| | | id: 1, |
| | | strategyNo: 'PS202501001', |
| | | strategyType: 'ä¸å±ä»·æ ¼', |
| | | customerName: 'åä¸å»ºæéå¢', |
| | | productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', |
| | | specification: '50kg/è¢', |
| | | basePrice: 380, |
| | | strategyPrice: 'Â¥350/å¨', |
| | | startDate: '2025-01-01', |
| | | endDate: '2025-12-31', |
| | | status: 'çæä¸', |
| | | description: 'æç¥åä½å®¢æ·ä¸å±ä¼æ ä»·æ ¼' |
| | | }, |
| | | { |
| | | id: 2, |
| | | strategyNo: 'PS202501002', |
| | | strategyType: 'é¶æ¢¯æ¥ä»·', |
| | | customerName: 'é¿æ±æ··ååå
¬å¸', |
| | | productName: 'P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥', |
| | | specification: '50kg/è¢', |
| | | basePrice: 320, |
| | | strategyPrice: '500å¨ä»¥ä¸9æ', |
| | | startDate: '2025-01-01', |
| | | endDate: '2025-06-30', |
| | | status: 'çæä¸', |
| | | description: '大æ¹ééè´é¶æ¢¯ä¼æ ' |
| | | }, |
| | | { |
| | | id: 3, |
| | | strategyNo: 'PS202501003', |
| | | strategyType: 'ä¿éææ£', |
| | | customerName: 'æµ¦æ±æ°´æ³¥å¶åå', |
| | | productName: 'P.C 32.5å¤åç¡
é
¸çæ°´æ³¥', |
| | | specification: '50kg/è¢', |
| | | basePrice: 300, |
| | | strategyPrice: '8.5æ', |
| | | startDate: '2025-01-15', |
| | | endDate: '2025-02-28', |
| | | status: 'çæä¸', |
| | | description: 'æ¥èä¿éæ´»å¨' |
| | | }, |
| | | { |
| | | id: 4, |
| | | strategyNo: 'PS202412015', |
| | | strategyType: 'ä¸å±ä»·æ ¼', |
| | | customerName: 'åä¸å»ºæéå¢', |
| | | productName: 'P.C 32.5å¤åç¡
é
¸çæ°´æ³¥', |
| | | specification: '50kg/è¢', |
| | | basePrice: 300, |
| | | strategyPrice: 'Â¥285/å¨', |
| | | startDate: '2024-10-01', |
| | | endDate: '2024-12-31', |
| | | status: 'å·²è¿æ', |
| | | description: '第åå£åº¦ä¸å±ä»·æ ¼' |
| | | } |
| | | ]) |
| | | |
| | | const pricePagination = reactive({ |
| | | total: 4, |
| | | currentPage: 1, |
| | | pageSize: 10 |
| | | }) |
| | | |
| | | const priceStrategyDialogVisible = ref(false) |
| | | const priceStrategyDialogTitle = ref('æ°å¢ä»·æ ¼çç¥') |
| | | const priceStrategyForm = reactive({ |
| | | strategyType: '', |
| | | customerName: '', |
| | | productName: '', |
| | | specification: '', |
| | | basePrice: 0, |
| | | strategyPrice: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | description: '' |
| | | }) |
| | | |
| | | const priceStrategyRules = { |
| | | strategyType: [{ required: true, message: 'è¯·éæ©çç¥ç±»å', trigger: 'change' }], |
| | | customerName: [{ required: true, message: 'è¯·éæ©å®¢æ·', trigger: 'change' }], |
| | | productName: [{ required: true, message: 'è¯·éæ©äº§å', trigger: 'change' }], |
| | | basePrice: [{ required: true, message: '请è¾å
¥åºç¡ä»·æ ¼', trigger: 'blur' }], |
| | | strategyPrice: [{ required: true, message: '请è¾å
¥çç¥ä»·æ ¼', trigger: 'blur' }], |
| | | startDate: [{ required: true, message: 'è¯·éæ©çææ¥æ', trigger: 'change' }], |
| | | endDate: [{ required: true, message: 'è¯·éæ©å¤±ææ¥æ', trigger: 'change' }] |
| | | } |
| | | |
| | | const priceStrategyFormRef = ref() |
| | | |
| | | // ========== ååæ§è¡çæ§ ========== |
| | | const contractLoading = ref(false) |
| | | const contractStats = reactive({ |
| | | totalContracts: 48, |
| | | deliveryRate: 87.5, |
| | | invoiceRate: 82.3, |
| | | paymentRate: 75.6 |
| | | }) |
| | | |
| | | const contractSearchForm = reactive({ |
| | | contractNo: '', |
| | | customerName: '', |
| | | executionStatus: '' |
| | | }) |
| | | |
| | | const contractList = ref([ |
| | | { |
| | | id: 1, |
| | | contractNo: 'CT202501001', |
| | | customerName: 'åä¸å»ºæéå¢', |
| | | contractAmount: 2850000, |
| | | signDate: '2025-01-05', |
| | | executionProgress: 85, |
| | | deliveryProgress: 90, |
| | | invoiceProgress: 85, |
| | | paymentProgress: 75, |
| | | executionStatus: 'æ§è¡ä¸', |
| | | orderStatus: '已宿', |
| | | orderDate: '2025-01-05' |
| | | }, |
| | | { |
| | | id: 2, |
| | | contractNo: 'CT202501002', |
| | | customerName: 'é¿æ±æ··ååå
¬å¸', |
| | | contractAmount: 1650000, |
| | | signDate: '2025-01-08', |
| | | executionProgress: 95, |
| | | deliveryProgress: 100, |
| | | invoiceProgress: 100, |
| | | paymentProgress: 85, |
| | | executionStatus: 'æ§è¡ä¸', |
| | | orderStatus: '已宿', |
| | | orderDate: '2025-01-08' |
| | | }, |
| | | { |
| | | id: 3, |
| | | contractNo: 'CT202501003', |
| | | customerName: 'æµ¦æ±æ°´æ³¥å¶åå', |
| | | contractAmount: 980000, |
| | | signDate: '2025-01-12', |
| | | executionProgress: 60, |
| | | deliveryProgress: 65, |
| | | invoiceProgress: 60, |
| | | paymentProgress: 50, |
| | | executionStatus: 'æ§è¡ä¸', |
| | | orderStatus: '已宿', |
| | | orderDate: '2025-01-12' |
| | | }, |
| | | { |
| | | id: 4, |
| | | contractNo: 'CT202412028', |
| | | customerName: 'åä¸å»ºæéå¢', |
| | | contractAmount: 3200000, |
| | | signDate: '2024-12-15', |
| | | executionProgress: 100, |
| | | deliveryProgress: 100, |
| | | invoiceProgress: 100, |
| | | paymentProgress: 100, |
| | | executionStatus: '已宿', |
| | | orderStatus: '已宿', |
| | | orderDate: '2024-12-15' |
| | | }, |
| | | { |
| | | id: 5, |
| | | contractNo: 'CT202501004', |
| | | customerName: 'é¿æ±æ··ååå
¬å¸', |
| | | contractAmount: 750000, |
| | | signDate: '2025-01-20', |
| | | executionProgress: 25, |
| | | deliveryProgress: 30, |
| | | invoiceProgress: 20, |
| | | paymentProgress: 0, |
| | | executionStatus: 'å¼å¸¸', |
| | | orderStatus: '已宿', |
| | | orderDate: '2025-01-20' |
| | | } |
| | | ]) |
| | | |
| | | const contractPagination = reactive({ |
| | | total: 5, |
| | | currentPage: 1, |
| | | pageSize: 10 |
| | | }) |
| | | |
| | | // ========== å岿¯ä»·åæ ========== |
| | | const compareLoading = ref(false) |
| | | const compareSearchForm = reactive({ |
| | | productName: '', |
| | | dateRange: [], |
| | | region: '' |
| | | }) |
| | | |
| | | const priceComparisonList = ref([ |
| | | { date: '2025-01-20', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: 'åä¸å»ºæéå¢', region: 'åä¸å°åº', quantity: 5000, price: 350, totalAmount: 1750000, priceChange: 0, remark: 'é¿æåä½å®¢æ·' }, |
| | | { date: '2025-01-15', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: 'æµ¦ä¸æ°åºå»ºçå
¬å¸', region: 'åä¸å°åº', quantity: 3000, price: 365, totalAmount: 1095000, priceChange: +15, remark: 'ç°æ¬¾ç°è´§' }, |
| | | { date: '2025-01-10', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: 'é¿æ±æ··ååå
¬å¸', region: 'åä¸å°åº', quantity: 8000, price: 345, totalAmount: 2760000, priceChange: -5, remark: '大æ¹é伿 ' }, |
| | | { date: '2025-01-05', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: 'æ±èå·¥ç¨éå¢', region: 'åä¸å°åº', quantity: 4500, price: 360, totalAmount: 1620000, priceChange: +10, remark: 'å·¥ç¨é¡¹ç®ä¸ç¨' }, |
| | | { date: '2024-12-28', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: 'åä¸å»ºæéå¢', region: 'åä¸å°åº', quantity: 6000, price: 355, totalAmount: 2130000, priceChange: +5, remark: 'å¹´åºå¤è´§' }, |
| | | { date: '2024-12-20', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', specification: '50kg/è¢', customerName: '䏿µ·å¸æ¿å·¥ç¨', region: 'åä¸å°åº', quantity: 10000, price: 340, totalAmount: 3400000, priceChange: -10, remark: 'æ¿åºé¡¹ç®' } |
| | | ]) |
| | | |
| | | const priceChartRef = ref(null) |
| | | let priceChart = null |
| | | |
| | | // ========== 婿¶¦åæ ========== |
| | | const profitLoading = ref(false) |
| | | const profitStats = reactive({ |
| | | totalSales: 15680000, |
| | | totalCost: 11256000, |
| | | grossProfit: 4424000, |
| | | grossProfitRate: 28.2, |
| | | salesGrowth: 12.5, |
| | | costRate: 71.8 |
| | | }) |
| | | |
| | | const profitSearchForm = reactive({ |
| | | productType: '', |
| | | customerName: '', |
| | | dateRange: [] |
| | | }) |
| | | |
| | | const profitAnalysisList = ref([ |
| | | { orderNo: 'SO202501015', customerName: 'åä¸å»ºæéå¢', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', quantity: 5000, salesPrice: 350, costPrice: 245, salesAmount: 1750000, costAmount: 1225000, grossProfit: 525000, grossProfitRate: 30.0, orderDate: '2025-01-20' }, |
| | | { orderNo: 'SO202501012', customerName: 'é¿æ±æ··ååå
¬å¸', productName: 'P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥', quantity: 3500, salesPrice: 288, costPrice: 210, salesAmount: 1008000, costAmount: 735000, grossProfit: 273000, grossProfitRate: 27.1, orderDate: '2025-01-18' }, |
| | | { orderNo: 'SO202501008', customerName: 'æµ¦æ±æ°´æ³¥å¶åå', productName: 'P.C 32.5å¤åç¡
é
¸çæ°´æ³¥', quantity: 2800, salesPrice: 255, costPrice: 185, salesAmount: 714000, costAmount: 518000, grossProfit: 196000, grossProfitRate: 27.5, orderDate: '2025-01-15' }, |
| | | { orderNo: 'SO202501005', customerName: 'åä¸å»ºæéå¢', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', quantity: 6000, salesPrice: 350, costPrice: 248, salesAmount: 2100000, costAmount: 1488000, grossProfit: 612000, grossProfitRate: 29.1, orderDate: '2025-01-10' }, |
| | | { orderNo: 'SO202501003', customerName: 'æ±èå·¥ç¨éå¢', productName: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', quantity: 4500, salesPrice: 360, costPrice: 250, salesAmount: 1620000, costAmount: 1125000, grossProfit: 495000, grossProfitRate: 30.6, orderDate: '2025-01-08' }, |
| | | { orderNo: 'SO202412025', customerName: 'é¿æ±æ··ååå
¬å¸', productName: 'P.S 32.5ç¿æ¸£ç¡
é
¸çæ°´æ³¥', quantity: 8000, salesPrice: 290, costPrice: 215, salesAmount: 2320000, costAmount: 1720000, grossProfit: 600000, grossProfitRate: 25.9, orderDate: '2024-12-28' } |
| | | ]) |
| | | |
| | | const profitPagination = reactive({ |
| | | total: 6, |
| | | currentPage: 1, |
| | | pageSize: 10 |
| | | }) |
| | | |
| | | const profitChartRef = ref(null) |
| | | let profitChart = null |
| | | |
| | | // ========== æ¹æ³ ========== |
| | | |
| | | // ä»·æ ¼çç¥ç¸å
³æ¹æ³ |
| | | const getStrategyTypeColor = (type) => { |
| | | const colorMap = { |
| | | 'ä¸å±ä»·æ ¼': 'success', |
| | | 'é¶æ¢¯æ¥ä»·': 'primary', |
| | | 'ä¿éææ£': 'warning' |
| | | } |
| | | return colorMap[type] || 'info' |
| | | } |
| | | |
| | | const searchPriceStrategy = () => { |
| | | priceLoading.value = true |
| | | setTimeout(() => { |
| | | priceLoading.value = false |
| | | }, 500) |
| | | } |
| | | |
| | | const resetPriceSearch = () => { |
| | | priceSearchForm.customerName = '' |
| | | priceSearchForm.productType = '' |
| | | priceSearchForm.strategyType = '' |
| | | } |
| | | |
| | | const handleAddPriceStrategy = () => { |
| | | priceStrategyDialogTitle.value = 'æ°å¢ä»·æ ¼çç¥' |
| | | resetPriceStrategyForm() |
| | | priceStrategyDialogVisible.value = true |
| | | } |
| | | |
| | | const handleViewPriceStrategy = (row) => { |
| | | ElMessage.info('æ¥ççç¥è¯¦æ
: ' + row.strategyNo) |
| | | } |
| | | |
| | | const handleEditPriceStrategy = (row) => { |
| | | priceStrategyDialogTitle.value = 'ç¼è¾ä»·æ ¼çç¥' |
| | | Object.assign(priceStrategyForm, row) |
| | | priceStrategyDialogVisible.value = true |
| | | } |
| | | |
| | | const handleDeletePriceStrategy = (row) => { |
| | | ElMessageBox.confirm('确认å é¤è¯¥ä»·æ ¼çç¥åï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | ElMessage.success('å 餿å') |
| | | }) |
| | | } |
| | | |
| | | const resetPriceStrategyForm = () => { |
| | | Object.keys(priceStrategyForm).forEach(key => { |
| | | if (key === 'basePrice') { |
| | | priceStrategyForm[key] = 0 |
| | | } else { |
| | | priceStrategyForm[key] = '' |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleSavePriceStrategy = () => { |
| | | priceStrategyFormRef.value.validate((valid) => { |
| | | if (valid) { |
| | | ElMessage.success('ä¿åæå') |
| | | priceStrategyDialogVisible.value = false |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handlePricePageChange = (val) => { |
| | | pricePagination.currentPage = val.page |
| | | pricePagination.pageSize = val.limit |
| | | } |
| | | |
| | | // ååæ§è¡çæ§ç¸å
³æ¹æ³ |
| | | const getExecutionStatusType = (status) => { |
| | | const statusMap = { |
| | | 'å¾
æ§è¡': 'info', |
| | | 'æ§è¡ä¸': 'primary', |
| | | '已宿': 'success', |
| | | 'å¼å¸¸': 'danger' |
| | | } |
| | | return statusMap[status] || 'info' |
| | | } |
| | | |
| | | const getProgressColor = (percentage) => { |
| | | if (percentage < 30) return '#f56c6c' |
| | | if (percentage < 70) return '#e6a23c' |
| | | return '#67c23a' |
| | | } |
| | | |
| | | const getContractStep = (row) => { |
| | | if (row.paymentProgress === 100) return 4 |
| | | if (row.invoiceProgress === 100) return 3 |
| | | if (row.deliveryProgress === 100) return 2 |
| | | if (row.orderStatus === '已宿') return 1 |
| | | return 0 |
| | | } |
| | | |
| | | const searchContract = () => { |
| | | contractLoading.value = true |
| | | setTimeout(() => { |
| | | contractLoading.value = false |
| | | }, 500) |
| | | } |
| | | |
| | | const resetContractSearch = () => { |
| | | contractSearchForm.contractNo = '' |
| | | contractSearchForm.customerName = '' |
| | | contractSearchForm.executionStatus = '' |
| | | } |
| | | |
| | | const handleViewContract = (row) => { |
| | | ElMessage.info('æ¥çåå详æ
: ' + row.contractNo) |
| | | } |
| | | |
| | | const handleContractPageChange = (val) => { |
| | | contractPagination.currentPage = val.page |
| | | contractPagination.pageSize = val.limit |
| | | } |
| | | |
| | | // å岿¯ä»·åæç¸å
³æ¹æ³ |
| | | const searchPriceComparison = () => { |
| | | compareLoading.value = true |
| | | setTimeout(() => { |
| | | compareLoading.value = false |
| | | initPriceChart() |
| | | }, 500) |
| | | } |
| | | |
| | | const resetCompareSearch = () => { |
| | | compareSearchForm.productName = '' |
| | | compareSearchForm.dateRange = [] |
| | | compareSearchForm.region = '' |
| | | } |
| | | |
| | | const initPriceChart = () => { |
| | | if (!priceChartRef.value) return |
| | | |
| | | if (priceChart) { |
| | | priceChart.dispose() |
| | | } |
| | | |
| | | priceChart = echarts.init(priceChartRef.value) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'æ°´æ³¥ä»·æ ¼è¶å¿åæ', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | formatter: '{b}<br/>{a}: Â¥{c}/å¨' |
| | | }, |
| | | legend: { |
| | | data: ['P.O 42.5æ®éç¡
é
¸çæ°´æ³¥'], |
| | | top: 30 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: ['2024-12-20', '2024-12-28', '2025-01-05', '2025-01-10', '2025-01-15', '2025-01-20'] |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'ä»·æ ¼(å
/å¨)', |
| | | min: 330, |
| | | max: 370 |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'P.O 42.5æ®éç¡
é
¸çæ°´æ³¥', |
| | | type: 'line', |
| | | data: [340, 355, 360, 345, 365, 350], |
| | | smooth: true, |
| | | itemStyle: { |
| | | color: '#409eff' |
| | | }, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: 'rgba(64, 158, 255, 0.3)' }, |
| | | { offset: 1, color: 'rgba(64, 158, 255, 0.1)' } |
| | | ]) |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | |
| | | priceChart.setOption(option) |
| | | } |
| | | |
| | | // 婿¶¦åæç¸å
³æ¹æ³ |
| | | const searchProfit = () => { |
| | | profitLoading.value = true |
| | | setTimeout(() => { |
| | | profitLoading.value = false |
| | | initProfitChart() |
| | | }, 500) |
| | | } |
| | | |
| | | const resetProfitSearch = () => { |
| | | profitSearchForm.productType = '' |
| | | profitSearchForm.customerName = '' |
| | | profitSearchForm.dateRange = [] |
| | | } |
| | | |
| | | const getProfitRateType = (rate) => { |
| | | if (rate >= 30) return 'success' |
| | | if (rate >= 25) return 'warning' |
| | | return 'danger' |
| | | } |
| | | |
| | | const getProfitSummary = (param) => { |
| | | const { columns, data } = param |
| | | const sums = [] |
| | | columns.forEach((column, index) => { |
| | | if (index === 0) { |
| | | sums[index] = 'å计' |
| | | return |
| | | } |
| | | if (['quantity', 'salesAmount', 'costAmount', 'grossProfit'].includes(column.property)) { |
| | | const values = data.map(item => Number(item[column.property])) |
| | | if (!values.every(value => isNaN(value))) { |
| | | const total = values.reduce((prev, curr) => { |
| | | const value = Number(curr) |
| | | if (!isNaN(value)) { |
| | | return prev + curr |
| | | } else { |
| | | return prev |
| | | } |
| | | }, 0) |
| | | sums[index] = column.property === 'quantity' ? total.toLocaleString() : 'Â¥' + total.toLocaleString() |
| | | } |
| | | } else if (column.property === 'grossProfitRate') { |
| | | // 计ç®å¹³åæ¯å©ç |
| | | const totalSales = data.reduce((sum, item) => sum + item.salesAmount, 0) |
| | | const totalProfit = data.reduce((sum, item) => sum + item.grossProfit, 0) |
| | | sums[index] = ((totalProfit / totalSales) * 100).toFixed(1) + '%' |
| | | } |
| | | }) |
| | | return sums |
| | | } |
| | | |
| | | const initProfitChart = () => { |
| | | if (!profitChartRef.value) return |
| | | |
| | | if (profitChart) { |
| | | profitChart.dispose() |
| | | } |
| | | |
| | | profitChart = echarts.init(profitChartRef.value) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'éå®ä¸å©æ¶¦è¶å¿åæ', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross', |
| | | crossStyle: { |
| | | color: '#999' |
| | | } |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['éå®éé¢', 'ææ¬éé¢', 'æ¯å©æ¶¦', 'æ¯å©ç'], |
| | | top: 30 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: [ |
| | | { |
| | | type: 'category', |
| | | data: ['2024-12', '2025-01'], |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | } |
| | | ], |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'éé¢(ä¸å
)', |
| | | axisLabel: { |
| | | formatter: '{value}' |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ¯å©ç(%)', |
| | | min: 0, |
| | | max: 40, |
| | | axisLabel: { |
| | | formatter: '{value}%' |
| | | } |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'éå®éé¢', |
| | | type: 'bar', |
| | | data: [820, 950], |
| | | itemStyle: { |
| | | color: '#409eff' |
| | | } |
| | | }, |
| | | { |
| | | name: 'ææ¬éé¢', |
| | | type: 'bar', |
| | | data: [605, 670], |
| | | itemStyle: { |
| | | color: '#e6a23c' |
| | | } |
| | | }, |
| | | { |
| | | name: 'æ¯å©æ¶¦', |
| | | type: 'bar', |
| | | data: [215, 280], |
| | | itemStyle: { |
| | | color: '#67c23a' |
| | | } |
| | | }, |
| | | { |
| | | name: 'æ¯å©ç', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: [26.2, 29.5], |
| | | itemStyle: { |
| | | color: '#f56c6c' |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | |
| | | profitChart.setOption(option) |
| | | } |
| | | |
| | | const handleProfitPageChange = (val) => { |
| | | profitPagination.currentPage = val.page |
| | | profitPagination.pageSize = val.limit |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // ç»ä»¶æè½½åä¸ç«å³åå§åå¾è¡¨ï¼çå¾
ç¨æ·åæ¢å°å¯¹åºæ ç¾é¡µ |
| | | }) |
| | | |
| | | // ç嬿 ç¾é¡µåæ¢ |
| | | watch(activeTab, (newVal) => { |
| | | nextTick(() => { |
| | | if (newVal === 'priceComparison') { |
| | | initPriceChart() |
| | | } else if (newVal === 'profitAnalysis') { |
| | | initProfitChart() |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | const handleTabChange = () => { |
| | | // æ ç¾é¡µåæ¢å¤ç |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .strategy-control { |
| | | padding: 0; |
| | | } |
| | | |
| | | .main-tabs { |
| | | border: none; |
| | | box-shadow: none; |
| | | } |
| | | |
| | | .main-tabs :deep(.el-tabs__content) { |
| | | padding: 0; |
| | | } |
| | | |
| | | .box-card { |
| | | border: none; |
| | | box-shadow: none; |
| | | } |
| | | |
| | | .search-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* ç»è®¡å¡çæ ·å¼ */ |
| | | .stats-row { |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .stat-card { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .stat-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 8px; |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .stat-content { |
| | | flex: 1; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | /* åå详æ
å±å¼æ ·å¼ */ |
| | | .contract-detail-expand { |
| | | padding: 30px 60px; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | .contract-detail-expand :deep(.el-step__title) { |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .contract-detail-expand :deep(.el-step__description) { |
| | | font-size: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | /* 婿¶¦ç»è®¡å¡ç */ |
| | | .profit-stats-row { |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .profit-card { |
| | | padding: 24px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 12px; |
| | | color: #fff; |
| | | box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4); |
| | | } |
| | | |
| | | .profit-card:nth-child(2) .profit-card { |
| | | background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
| | | } |
| | | |
| | | .profit-card:nth-child(3) .profit-card { |
| | | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
| | | } |
| | | |
| | | .profit-header { |
| | | font-size: 14px; |
| | | opacity: 0.9; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .profit-value { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .profit-highlight { |
| | | color: #fff; |
| | | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); |
| | | } |
| | | |
| | | .profit-footer { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-size: 13px; |
| | | opacity: 0.9; |
| | | } |
| | | |
| | | .growth-up { |
| | | color: #fff; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .growth-down { |
| | | color: #ffd04b; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .cost-rate, .gross-profit-rate { |
| | | font-weight: bold; |
| | | } |
| | | |
| | | /* å¾è¡¨å®¹å¨ */ |
| | | .chart-container { |
| | | margin: 20px 0; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | </style> |
| | | |