Compare commits
11 Commits
2d200dd1a6
...
projects/l
| Author | SHA1 | Date | |
|---|---|---|---|
| ef230b3836 | |||
| 9f2f7036fd | |||
| 2f3586e2f2 | |||
|
|
babbe98c09 | ||
|
|
b491f24126 | ||
|
|
b76f3bfd37 | ||
|
|
51e66cf6e1 | ||
|
|
5605eeab06 | ||
|
|
20ef2b9763 | ||
|
|
7b3873f9ea | ||
|
|
80deffbb42 |
9
.env.dev
@@ -7,9 +7,14 @@ VUE_APP_TITLE = 洛玻集团驾驶舱
|
||||
# 芋道管理系统/开发环境
|
||||
# VUE_APP_BASE_API = 'http://172.16.32.18:7070'
|
||||
# VUE_APP_BASE_API = 'http://172.16.32.95:7070'
|
||||
VUE_APP_BASE_API = 'http://172.16.33.83:7070'
|
||||
# VUE_APP_BASE_API = 'http://172.16.33.83:7070'
|
||||
|
||||
# VUE_APP_BASE_API = 'http://192.168.0.35:7070'
|
||||
# 杨姗姗
|
||||
VUE_APP_BASE_API = 'http://172.16.20.218:7070'
|
||||
# 小田
|
||||
# VUE_APP_BASE_API = 'http://172.16.19.232:7070'
|
||||
# 测试
|
||||
# VUE_APP_BASE_API = 'http://192.168.0.35:8080'
|
||||
|
||||
|
||||
# 路由懒加载
|
||||
|
||||
1
.gitignore
vendored
@@ -20,3 +20,4 @@ selenium-debug.log
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
sync_luobo.bat
|
||||
@@ -47,6 +47,7 @@
|
||||
"benz-amr-recorder": "^1.1.5",
|
||||
"bpmn-js-token-simulation": "0.10.0",
|
||||
"clipboard": "2.0.8",
|
||||
"code-brick-zj": "^1.1.1",
|
||||
"core-js": "^3.26.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
"echarts": "5.4.0",
|
||||
|
||||
@@ -76,3 +76,273 @@ export function getOrderDetail(data) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getDataBackUp(data) {
|
||||
return request({
|
||||
url: "/lb/data-backup/page",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function recoverDataBackUp(data) {
|
||||
return request({
|
||||
url: "/lb/data-backup/recover",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 导出Banner Excel
|
||||
export function exportDataBackUp(data) {
|
||||
return request({
|
||||
url: "/lb/data-backup/export-excel",
|
||||
method: "post",
|
||||
data: data,
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
export function getDataBackUpDetail(data) {
|
||||
return request({
|
||||
url: "/lb/data-backup/get",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDataBackUpDetail(data) {
|
||||
return request({
|
||||
url: "/lb/data-backup/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function copyLastMonthData(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-month/copyLastMonth",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function copyLastYearData(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-year/copyLastYear",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getSalesRevenueGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/sales-revenue/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getGrossMarginGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/gross-margin/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getExpenseAnalysisGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/expense-analysis/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getSheetYieldGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/sheet-yield/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getInputOutputRateGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/input-output-rate/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getSalesRevenueFactoryData(data) {
|
||||
return request({
|
||||
url: "/lb/sales-revenue/getFactoryData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getExpenseAnalysisFactoryData(data) {
|
||||
return request({
|
||||
url: "/lb/expense-analysis/getFactoryData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getGrossMarginFactoryData(data) {
|
||||
return request({
|
||||
url: "/lb/gross-margin/getFactoryData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getSheetYieldFactoryData(data) {
|
||||
return request({
|
||||
url: "/lb/sheet-yield/getFactoryData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getInputOutputRateFactoryData(data) {
|
||||
return request({
|
||||
url: "/lb/input-output-rate/getFactoryData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getUnitPriceAnalysisGroupData(data) {
|
||||
return request({
|
||||
url: "/lb/unit-price-analysis/getGroupData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getUnitPriceAnalysisBaseData(data) {
|
||||
return request({
|
||||
url: "/lb/unit-price-analysis/getBaseData",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfitAnalysisManageList(data) {
|
||||
return request({
|
||||
url: "/lb/profit-analysis/manageList",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfitAnalysisTotalList(data) {
|
||||
return request({
|
||||
url: "/lb/profit-analysis/profitTotalList",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getCostAnalysisData(data) {
|
||||
return request({
|
||||
url: "/lb/cost-analysis/XXCostList2",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getSingleMaterialAnalysis(data) {
|
||||
return request({
|
||||
url: "/lb/cost-analysis/singleMaterialAnalysis",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getSingleMaterialCostAnalysis(data) {
|
||||
return request({
|
||||
url: "/lb/cost-analysis/singleMaterialCostAnalysis",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCalendar(data) {
|
||||
return request({
|
||||
url: "lb/index-target-month/getCalendar",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getCalendarYear(data) {
|
||||
return request({
|
||||
url: "lb/index-target-year/getCalendarYear",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getLevelStruc(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-month/getLevelStruc",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTargetMonthPage(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-month/list",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTargetMonthData(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-month/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTargetYearPage(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-year/list",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTargetYearData(data) {
|
||||
return request({
|
||||
url: "/lb/index-target-year/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getRealMonthPage(data) {
|
||||
return request({
|
||||
url: "/lb/index-real-month/list",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getRealMonthCalendar(data) {
|
||||
return request({
|
||||
url: "/lb/index-real-month/getCalendar",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function updateRealMonthData(data) {
|
||||
return request({
|
||||
url: "/lb/index-real-month/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export function getDictListData(query) {
|
||||
return request({
|
||||
url: "/system/dict-data/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
4
src/assets/icons/svg/closeSider.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
11
src/assets/icons/svg/logout.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g id="组_1" data-name="组 1" transform="translate(-970 -281.875)">
|
||||
<g id="编辑备份" transform="translate(970 281.5)">
|
||||
<rect id="矩形" width="16" height="16" transform="translate(0 0.375)" opacity="0"/>
|
||||
<path id="形状" d="M12.281,5.406a.512.512,0,0,0,1.023,0V2.559A2.559,2.559,0,0,0,10.746,0H2.559A2.559,2.559,0,0,0,0,2.559v8.188A2.559,2.559,0,0,0,2.559,13.3H5.415a.512.512,0,1,0,0-1.023H2.559a1.535,1.535,0,0,1-1.535-1.535V2.559A1.535,1.535,0,0,1,2.559,1.023h8.188a1.535,1.535,0,0,1,1.535,1.535Z" transform="translate(1.535 1.535)" fill="#3d7aff"/>
|
||||
</g>
|
||||
<g id="移出" transform="translate(978 290.608)">
|
||||
<path id="形状结合" d="M0,2.821a.746.746,0,0,0,.529.73l.118.011,3.68-.011L3.459,4.4a.656.656,0,0,0,.816,1.02l.1-.083L6.146,3.6a1.107,1.107,0,0,0,.321-.634l.012-.2,0-.026a.61.61,0,0,0-.015-.125,1.106,1.106,0,0,0-.189-.455l-.107-.129L4.39.2a.656.656,0,0,0-1.022.814L4.23,2.108.654,2.1l-.125.011A.88.88,0,0,0,0,2.821Z" transform="translate(0 0)" fill="#3d7aff"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
12
src/assets/icons/svg/openSider.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>展开备份</title>
|
||||
<g id="12-月修改-2-版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="01-运营驾驶舱展开" transform="translate(-1818.000000, -12.000000)" fill="#0B58FF">
|
||||
<g id="展开备份" transform="translate(1818.000000, 12.000000)">
|
||||
<rect id="矩形" stroke="#0B58FF" opacity="0" x="0.5" y="0.5" width="31" height="31"></rect>
|
||||
<path d="M26.9054416,1.26315789 L27.1627614,1.2715785 C27.589262,1.29963047 28.0033877,1.39768715 28.3969525,1.56449606 C28.853625,1.75530116 29.2618455,2.03033772 29.6157539,2.38424612 C29.967102,2.73559426 30.2434779,3.1465945 30.4352441,3.6024321 C30.6356746,4.07532525 30.7368421,4.57721075 30.7368421,5.09455843 L30.7368421,26.9054416 L30.7284215,27.1627614 C30.7003695,27.589262 30.6023129,28.0033877 30.4355039,28.3969525 C30.2446988,28.853625 29.9696623,29.2618455 29.6157539,29.6157539 C29.2644057,29.967102 28.8534055,30.2434779 28.3975679,30.4352441 C27.9246748,30.6356746 27.4227892,30.7368421 26.9054416,30.7368421 L5.09455843,30.7368421 L4.83723859,30.7284215 C4.41073802,30.7003695 3.99661229,30.6023129 3.60304751,30.4355039 C3.14637497,30.2446988 2.73815452,29.9696623 2.38424612,29.6157539 C2.03289798,29.2644057 1.75652209,28.8534055 1.56475593,28.3975679 C1.36432537,27.9246748 1.26315789,27.4227892 1.26315789,26.9054416 L1.26315789,5.09455843 L1.2715785,4.83723859 C1.29963047,4.41073802 1.39768715,3.99661229 1.56449606,3.60304751 C1.75530116,3.14637497 2.03033772,2.73815452 2.38424612,2.38424612 C2.73559426,2.03289798 3.1465945,1.75652209 3.6024321,1.56475593 C4.07532525,1.36432537 4.57721075,1.26315789 5.09455843,1.26315789 L26.9054416,1.26315789 Z M26.9054416,3.17216771 L5.09455843,3.17216771 L4.94438644,3.17795455 C3.95326514,3.25463661 3.17216771,4.08386243 3.17216771,5.09455843 L3.17216771,26.9054416 L3.17795455,27.0556136 C3.25463661,28.0467349 4.08386243,28.8278323 5.09455843,28.8278323 L26.9054416,28.8278323 L27.0556136,28.8220454 C28.0467349,28.7453634 28.8278323,27.9161376 28.8278323,26.9054416 L28.8278323,5.09455843 L28.8220454,4.94438644 C28.7453634,3.95326514 27.9161376,3.17216771 26.9054416,3.17216771 Z M29.2015182,9.76714413 L29.2015182,11.8615014 L2.12281432,11.8615014 L2.12281432,9.76714413 L29.2015182,9.76714413 Z" id="形状结合" fill-rule="nonzero" opacity="0.79078311"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/base/合肥.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src/assets/images/base/宜兴.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src/assets/images/base/宿迁.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src/assets/images/base/桐城.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/base/洛阳.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/base/漳州.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/base/秦皇岛.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/base/自贡.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/assets/images/bgBase/合肥.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/images/bgBase/宜兴.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
src/assets/images/bgBase/宿迁.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/images/bgBase/桐城.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
src/assets/images/bgBase/洛阳.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
src/assets/images/bgBase/漳州.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
src/assets/images/bgBase/秦皇岛.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/images/bgBase/自贡.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
src/assets/img/calendarBg.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
src/assets/img/calendarTitleBg.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/img/downArrow.png
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
src/assets/img/indicatorDetailsBg.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
src/assets/img/indicatorDetailsTitleBg.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/img/labelBg.png
Normal file
|
After Width: | Height: | Size: 810 B |
BIN
src/assets/img/opLargeBg.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
src/assets/img/operatingRevenueBg.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src/assets/img/topArrow.png
Normal file
|
After Width: | Height: | Size: 434 B |
@@ -17,7 +17,7 @@ export default {
|
||||
const vnodes = []
|
||||
|
||||
if (icon) {
|
||||
vnodes.push(<svg-icon icon-class={icon}/>)
|
||||
vnodes.push(<svg-icon icon-class={icon} />)
|
||||
}
|
||||
|
||||
if (title) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<div v-if="collapse" key="collapse" class="sidebar-logo-link">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
</div>
|
||||
<div v-else key="expand" class="sidebar-logo-link">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
|
||||
</router-link>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<template
|
||||
v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
|
||||
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
@@ -12,14 +13,8 @@
|
||||
<template slot="title">
|
||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="(child, index) in item.children"
|
||||
:key="child.path + index"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
<sidebar-item v-for="(child, index) in item.children" :key="child.path + index" :is-nest="true" :item="child"
|
||||
:base-path="resolvePath(child.path)" class="nest-menu" />
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,7 +71,7 @@ export default {
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
|
||||
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
<template>
|
||||
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<div :class="{ 'has-logo': showLogo }"
|
||||
:style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
<el-menu :default-active="activeMenu" :collapse="isCollapse"
|
||||
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
|
||||
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
||||
:unique-opened="true"
|
||||
:active-text-color="settings.theme"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
:unique-opened="true" :active-text-color="settings.theme" :collapse-transition="false" mode="vertical">
|
||||
<!-- 根据 sidebarRouters 路由,生成菜单 -->
|
||||
<sidebar-item
|
||||
v-for="(route, index) in sidebarRouters"
|
||||
:key="route.path + index"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
<sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route"
|
||||
:base-path="route.path" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,8 @@ import './tongji' // 百度统计
|
||||
import { getDicts } from "@/api/system/dict/data";
|
||||
import { getConfigKey } from "@/api/infra/config";
|
||||
import { parseTime, resetForm, handleTree, addBeginAndEndTime, divide } from "@/utils/ruoyi";
|
||||
import CodeBrickZj from "code-brick-zj";
|
||||
Vue.use(CodeBrickZj);
|
||||
import { isEmpty } from "@/utils";
|
||||
import Pagination from "@/components/Pagination";
|
||||
// 自定义表格工具扩展
|
||||
|
||||
@@ -8,6 +8,38 @@ import { isRelogin } from '@/utils/request'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
/**
|
||||
* 从菜单树中查找 jumpFlag===1 的默认跳转路径
|
||||
* 若打标在父节点则取其下第一个可见叶子路径;若在叶子则直接返回该路径
|
||||
*/
|
||||
function findDefaultPathFromMenus(menus) {
|
||||
if (!Array.isArray(menus) || menus.length === 0) return null
|
||||
|
||||
function dfs(list, parentPath = '') {
|
||||
for (const item of list) {
|
||||
if (item.visible === false) continue
|
||||
const rawPath = item.path || ''
|
||||
const currentPath = rawPath.startsWith('/')
|
||||
? rawPath
|
||||
: `${parentPath}/${rawPath}`.replace(/\/+/g, '/')
|
||||
|
||||
if (item.jumpFlag === 1) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const childPath = dfs(item.children, currentPath)
|
||||
return childPath != null ? childPath : currentPath
|
||||
}
|
||||
return currentPath
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = dfs(item.children, currentPath)
|
||||
if (found != null) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return dfs(menus)
|
||||
}
|
||||
|
||||
// 增加三方登陆 update by 芋艿
|
||||
const whiteList = ['/login', '/social-login', '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee']
|
||||
|
||||
@@ -17,7 +49,7 @@ router.beforeEach((to, from, next) => {
|
||||
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
||||
/* has token*/
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
next({ path: store.getters.defaultPath || '/' })
|
||||
NProgress.done()
|
||||
} else {
|
||||
if (store.getters.roles.length === 0) {
|
||||
@@ -31,7 +63,13 @@ router.beforeEach((to, from, next) => {
|
||||
store.dispatch('GenerateRoutes', userInfo.menus).then(accessRoutes => {
|
||||
// 根据 roles 权限生成可访问的路由表
|
||||
router.addRoutes(accessRoutes) // 动态添加可访问路由表
|
||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
|
||||
const defaultPath = findDefaultPathFromMenus(userInfo.menus) || '/'
|
||||
store.commit('permission/SET_DEFAULT_PATH', defaultPath)
|
||||
if (to.path === '/' || to.matched.length === 0) {
|
||||
next({ path: defaultPath, replace: true })
|
||||
} else {
|
||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
store.dispatch('LogOut').then(() => {
|
||||
@@ -40,7 +78,12 @@ router.beforeEach((to, from, next) => {
|
||||
})
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
if (to.path === '/') {
|
||||
const defaultPath = store.getters.defaultPath || '/'
|
||||
next({ path: defaultPath, replace: true })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,7 @@ const getters = {
|
||||
topbarRouters:state => state.permission.topbarRouters,
|
||||
defaultRoutes:state => state.permission.defaultRoutes,
|
||||
sidebarRouters:state => state.permission.sidebarRouters,
|
||||
defaultPath: state => state.permission.defaultPath,
|
||||
// 数据字典
|
||||
dict_datas: state => state.dict.dictDatas
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ const permission = {
|
||||
addRoutes: [],
|
||||
sidebarRouters: [], // 左侧边菜单的路由,被 Sidebar/index.vue 使用
|
||||
topbarRouters: [], // 顶部菜单的路由,被 TopNav/index.vue 使用
|
||||
defaultPath: '/', // 登录后默认跳转路径(由菜单 jumpFlag===1 决定)
|
||||
},
|
||||
mutations: {
|
||||
SET_DEFAULT_PATH: (state, path) => {
|
||||
state.defaultPath = path || '/'
|
||||
},
|
||||
SET_ROUTES: (state, routes) => {
|
||||
state.addRoutes = routes
|
||||
state.routes = constantRoutes.concat(routes)
|
||||
@@ -76,6 +80,20 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
|
||||
}
|
||||
}
|
||||
const needBlankLayout = [
|
||||
"/operatingRevenue",
|
||||
"/totalProfit",
|
||||
"/operatingProfit",
|
||||
"/expenseAnalysis",
|
||||
"/grossMargin",
|
||||
"/inputOutputRatio",
|
||||
"/rawSheetYield",
|
||||
"/productionCostAnalysis",
|
||||
"/unitPriceAnalysis",
|
||||
"/netPriceAnalysis",
|
||||
"/salesVolumeAnalysis",
|
||||
'/procurementGainAnalysis',
|
||||
'/fullCostAnalysis',
|
||||
// '/expenseAnalysis',
|
||||
"/cost", // cost 根路由
|
||||
"/cost/profitImpactAnalysis", // cost 子菜单(完整路径)
|
||||
];
|
||||
|
||||
@@ -81,6 +81,9 @@ export const DICT_TYPE = {
|
||||
PROMOTION_COUPON_TAKE_TYPE: 'promotion_coupon_take_type', // 优惠劵的领取方式
|
||||
PROMOTION_ACTIVITY_STATUS: 'promotion_activity_status', // 优惠活动的状态
|
||||
PROMOTION_CONDITION_TYPE: 'promotion_condition_type', // 营销的条件类型枚举
|
||||
|
||||
// ========== 模块 ==========
|
||||
LB_DW: 'lb_dw'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,3 +142,15 @@ export function getDictDataLabel(dictType, value) {
|
||||
const dict = getDictData(dictType, value);
|
||||
return dict ? dict.label : '';
|
||||
}
|
||||
|
||||
// table中用来过滤字典
|
||||
export function publicFormatter(dictTable) {
|
||||
const dictDatas = getDictDatas(dictTable)
|
||||
return function (val) {
|
||||
const arr = {}
|
||||
dictDatas.map((item) => {
|
||||
arr[item.value] = item.label
|
||||
})
|
||||
return arr?.[val]
|
||||
}
|
||||
}
|
||||
|
||||
254
src/views/home/budgetSubmissionDetails.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader top-title="预算填报" :is-full-screen="isFullScreen" @screenfullChange="screenfullChange"
|
||||
@getTimeType="handleTimeChange" :isBudget="true" />
|
||||
<div class="main-body" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:416px 1192px;
|
||||
">
|
||||
<indicatorCalendar :timeType="timeType" :calendarObj='calendarObj'/>
|
||||
<indicatorDetails :timeType="timeType" @updateLeft='getData'/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:416px 1192px;
|
||||
">
|
||||
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <div class="centerImg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1; /* 确保在 backp 之上、内容之下 */
|
||||
"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "./components/budgetHeader.vue";
|
||||
import { Sidebar } from "../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
import indicatorCalendar from "./components/budgetCalendar.vue";
|
||||
import indicatorDetails from "./components/budgetDetails.vue";
|
||||
|
||||
// import premProdStatus from "./components/premProdStatus.vue";
|
||||
import { mapState } from "vuex";
|
||||
// import operatingLineChart from "../operatingComponents/operatingLineChart";
|
||||
// import operatingLineChartCumulative from "../operatingComponents/operatingLineChartCumulative.vue";
|
||||
|
||||
import { getSalesRevenueGroupData } from '@/api/cockpit'
|
||||
import moment from "moment";
|
||||
import {getCalendar, getCalendarYear} from '@/api/cockpit';
|
||||
export default {
|
||||
name: "DayReport",
|
||||
components: {
|
||||
ReportHeader,
|
||||
indicatorCalendar,
|
||||
indicatorDetails,
|
||||
// operatingLineChartCumulative,
|
||||
// operatingLineChart,
|
||||
// premProdStatus,
|
||||
Sidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
timeType:'month',
|
||||
value: 100,
|
||||
sort:1,
|
||||
selectDate:{},
|
||||
monthData: {},
|
||||
ytdData: {},
|
||||
calendarObj:{}
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
if(this.timeType == 'month'){
|
||||
getCalendar().then((res) => {
|
||||
this.calendarObj = res.data
|
||||
})
|
||||
}else{
|
||||
getCalendarYear().then((res) => {
|
||||
this.calendarObj = res.data
|
||||
})
|
||||
}
|
||||
},
|
||||
handleTimeChange(obj) {
|
||||
console.log(obj, 'obj');
|
||||
this.timeType = obj
|
||||
this.getData()
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
// 导出
|
||||
// exportPDF() {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -99,7 +99,6 @@ export default {
|
||||
return {
|
||||
name: config.name,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: config.lineColor },
|
||||
|
||||
@@ -2,20 +2,25 @@
|
||||
<header class="report-header">
|
||||
<!-- 左侧区域:logo + 标题 -->
|
||||
<div class="left-content">
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo">
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="center-content">
|
||||
<!-- 循环 pageRoutes,不再硬编码文字 -->
|
||||
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
|
||||
<span class="item-text">{{ page.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :class="{ 'no-skew': activeIndex === index }
|
||||
" -->
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="text" class="logout-btn" :title="'退出'">
|
||||
<svg-icon style="color: #0B58FF;" icon-class="logout" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native='logout'>退出登录</el-dropdown-item>
|
||||
<el-dropdown-item @click.native='handleToggle'>切换账号</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-button type="text" class="screen-btn" @click="changeHomeSider">
|
||||
<svg-icon style="color: #0B58FF;" v-if="openSider" icon-class="closeSider" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="openSider" />
|
||||
</el-button>
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
@@ -24,16 +29,15 @@
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + label + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div>
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">日期选择</span>
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
<el-date-picker :clearable="false" v-model="date" type="month" placeholder="请选择月份" class="custom-date-picker"
|
||||
value-format="timestamp"
|
||||
style="width: 132px;height: 29px;"
|
||||
@change="emitTimeRange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -41,130 +45,155 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import {getPath} from "@/utils/ruoyi";
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' }
|
||||
openSider: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' },
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined,
|
||||
activeIndex: -1,
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中“日”)
|
||||
date: Date.now(), // 使用时间戳格式
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中"月")
|
||||
pageRoutes: [
|
||||
{ text: '营业收入', path: '/operatingRevenue' },
|
||||
{ text: '营业收入', path: '/operatingRevenue/operatingRevenueIndex' },
|
||||
{ text: '利润分析', path: '/profitAnalysis' },
|
||||
{ text: '产销率库存分析', path: '/PSIAnal' },
|
||||
{ text: '成本分析', path: '/cost/cost' },
|
||||
{ text: '驾驶舱报表', path: '/cockpit' }
|
||||
],
|
||||
// 定义时间类型配置:text=按钮文字,pickerType=选择器类型,placeholder=占位符
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 动态获取日期选择器类型
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
// 动态获取日期选择器占位符
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(path, index) {
|
||||
// 1. 跳转到对应路由
|
||||
this.$router.push(path);
|
||||
// 2. 更新activeIndex,让当前点击项高亮
|
||||
this.activeIndex = index;
|
||||
},
|
||||
changeHomeSider() { this.$emit('siderOpenChange') },
|
||||
changeFullScreen() { this.$emit('screenfullChange') },
|
||||
padZero(num) { return num < 10 ? '0' + num : num },
|
||||
|
||||
/**
|
||||
* 核心方法1:根据维度计算时间区间(首次进入时基于赋值的当月日期,计算“当月第一天0点→次月第一天0点”)
|
||||
* @returns {Object} 包含 start(开始时间)、end(结束时间)、dimension(维度)的区间对象
|
||||
*/
|
||||
* 计算时间区间
|
||||
* @returns {Object} 包含 startTime、endTime、mode、targetMonth 的区间对象
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
// 固定为月维度
|
||||
const mode = 2;
|
||||
// 初始化时间戳为0(兜底值)
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
// 存储目标月份
|
||||
let targetMonth = '';
|
||||
|
||||
const targetMoment = this.date
|
||||
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
|
||||
: defaultMoment;
|
||||
try {
|
||||
// 使用 this.date(时间戳)创建 moment 对象
|
||||
let targetMoment = this.date
|
||||
? moment(this.date) // 解析时间戳
|
||||
: moment(); // 如果 date 无效,使用当前时间
|
||||
|
||||
if (!targetMoment.isValid()) {
|
||||
console.error('无效日期:', this.date);
|
||||
return { startTime, endTime, mode };
|
||||
// 验证日期是否有效
|
||||
if (!targetMoment.isValid()) {
|
||||
console.warn('无效的日期,已使用当前月份:', this.date);
|
||||
targetMoment = moment();
|
||||
}
|
||||
|
||||
// 获取月份(数字格式,1-12)
|
||||
targetMonth = targetMoment.format('M');
|
||||
console.log('当前选择的月份:', targetMonth);
|
||||
|
||||
// 计算当月第一天00:00:00的时间戳
|
||||
startTime = targetMoment.startOf('month').valueOf();
|
||||
// 计算当月最后一天23:59:59的时间戳
|
||||
endTime = targetMoment.clone()
|
||||
.endOf('month')
|
||||
.set({
|
||||
hour: 23,
|
||||
minute: 59,
|
||||
second: 59,
|
||||
millisecond: 0 // 毫秒设为0
|
||||
})
|
||||
.valueOf();
|
||||
|
||||
// 调试输出
|
||||
console.log('月份时间范围计算结果:', {
|
||||
startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
startTimeStamp: startTime,
|
||||
endTimeStamp: endTime,
|
||||
targetMonth: targetMonth
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('计算月份时间范围时出错:', error);
|
||||
}
|
||||
|
||||
// 1. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
targetMonth
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 核心方法2:传递时间区间给父组件(首次进入时触发,传递“当月第一天0点→次月第一天0点”)
|
||||
* 传递时间区间给父组件
|
||||
*/
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"})
|
||||
console.log('当前时间区间:', timeRange);
|
||||
console.log('触发时间范围变化:', timeRange);
|
||||
},
|
||||
|
||||
async logout() {
|
||||
this.$modal.confirm('确定注销并退出系统吗?', '提示').then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
}).catch(() => {});
|
||||
},
|
||||
handleToggle() {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
|
||||
// 维度切换时:清空选择的日期
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
this.date = Date.now(); // 重置为当前时间戳
|
||||
this.emitTimeRange();
|
||||
}
|
||||
},
|
||||
dateData: {
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler(newVal) {
|
||||
console.log('dateData 变化:', newVal);
|
||||
if (newVal && (newVal.startTime || newVal.endTime)) {
|
||||
// 优先使用 startTime
|
||||
const timeStamp = newVal.startTime || newVal.endTime;
|
||||
if (timeStamp && timeStamp !== 0) {
|
||||
console.log('设置日期选择器时间为:', timeStamp);
|
||||
this.date = timeStamp; // 直接使用时间戳
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = this.padZero(now.getMonth() + 1); // 月份从0开始,+1后补零(如1月→01)
|
||||
// 赋值当月默认日期(格式:YYYY-MM,适配month类型选择器)
|
||||
this.date = `${year}-${month}`;
|
||||
// 确保选择器渲染完成后,传递“当月第一天0点→次月第一天0点”的区间
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
// 初始化默认日期为当前月份
|
||||
console.log('初始化日期选择器,当前时间戳:', this.date);
|
||||
this.$nextTick(() => {
|
||||
this.emitTimeRange();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -179,13 +208,12 @@ export default {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
justify-content: space-between;
|
||||
background: url('../../../assets/img/topTitle.png') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* 确保timeType绝对定位生效 */
|
||||
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
@@ -252,12 +280,10 @@ export default {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中,避免元素高低错位 */
|
||||
top: 42px;
|
||||
right:10px;
|
||||
right: 10px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
/* 清除间隙,让按钮与选择器紧密连接 */
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
@@ -274,7 +300,6 @@ export default {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
@@ -287,13 +312,11 @@ export default {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-20deg) !important;
|
||||
/* 统一倾斜角度,修复原30deg的错位 */
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(20deg) !important;
|
||||
/* 同步统一文字倾斜角度 */
|
||||
}
|
||||
|
||||
.dateP {
|
||||
@@ -305,11 +328,10 @@ export default {
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 70px;
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
/* 与按钮倾斜角度统一(原30deg改为25deg,避免视觉错位) */
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
@@ -319,19 +341,18 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 补充:label文字抵消倾斜(原代码遗漏,导致文字倾斜) */
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
/* 与label倾斜角度相反,确保文字正立 */
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 4px;
|
||||
gap: 20px;
|
||||
margin-right: 16px;
|
||||
justify-content: flex-end;
|
||||
gap: 21px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
@@ -345,10 +366,18 @@ export default {
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 30px;
|
||||
padding: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,25 +385,21 @@ export default {
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 132px !important;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 132px !important;
|
||||
width: 165px !important;
|
||||
text-align: center;
|
||||
padding-left: 15px !important;
|
||||
padding-right: 32px !important;
|
||||
/* 给图标留空间,避免文字被遮挡 */
|
||||
font-size: 14px !important;
|
||||
line-height: 28px !important;
|
||||
/* 行高=输入框高度,文字垂直居中 */
|
||||
color: rgba(237, 245, 253, 1) !important;
|
||||
vertical-align: middle !important;
|
||||
/* 强制文字垂直对齐 */
|
||||
clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%);
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -382,38 +407,27 @@ export default {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 2. 调整图标容器:让图标与文字在同一水平线上 */
|
||||
.el-input__prefix {
|
||||
left: auto !important;
|
||||
right: 8px !important;
|
||||
top: 50% !important;
|
||||
/* 从40%改为50%,基于输入框垂直居中 */
|
||||
transform: translateY(-50%) !important;
|
||||
/* 向上偏移自身50%,精准居中 */
|
||||
display: inline-flex !important;
|
||||
/* 让容器内图标垂直居中 */
|
||||
align-items: center !important;
|
||||
/* 图标在容器内垂直居中 */
|
||||
height: 28px !important;
|
||||
/* 容器高度=输入框高度,避免偏移 */
|
||||
}
|
||||
|
||||
/* 3. 调整图标本身:确保图标大小和对齐方式 */
|
||||
.el-input__icon {
|
||||
color: #ffffff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 28px !important;
|
||||
/* 图标行高=输入框高度,与文字对齐 */
|
||||
vertical-align: middle !important;
|
||||
/* 强制图标垂直对齐 */
|
||||
}
|
||||
|
||||
/* 4. 图标伪类:确保颜色和对齐继承 */
|
||||
.el-icon-date::before {
|
||||
color: #ffffff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: inherit !important;
|
||||
/* 继承父级行高,避免错位 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
|
||||
min: () => 0,
|
||||
max: (value) => Math.ceil(value.max),
|
||||
scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
|
||||
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
|
||||
|
||||
123
src/views/home/components/budgetCalendar.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="预算填报日历" icon="cockpitItemIcon" size="calendarBg" topSize="calendarTitleBg">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 14px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 2. .top 保持 flex,无需固定高度,自动跟随子元素拉伸 -->
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 844px;padding: 26px 16px;">
|
||||
<!-- 动态生成12个月的容器:优化flex布局,缩小行间距 -->
|
||||
<div class="month-list"
|
||||
style="display: flex; gap: 16px; flex-wrap: wrap; align-content: flex-start; row-gap: 8px;">
|
||||
<!-- 循环生成12个月:通过判断当前月份索引,添加current类 -->
|
||||
<div class="monthItem" :class="{
|
||||
'has-data': calendar.haveData,
|
||||
'current': calendar.isActive // 本月匹配current样式
|
||||
}" v-for="(calendar, index) in calendarList" :key="index">
|
||||
{{ calendar.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
timeType: {
|
||||
type: String,
|
||||
default: 'month', // 默认月份维度
|
||||
},
|
||||
calendarObj: { // 接收父组件传递的年月状态对象
|
||||
type: Object, // 注意:父组件传递的是对象,不是数组,修正props类型
|
||||
default: () => ({}) // 默认空对象,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
calendarList:[],// 日历列表
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听calendarList变化,实时更新monthList的haveData状态
|
||||
// timeType: {
|
||||
// immediate: true, // 组件挂载时立即执行一次
|
||||
// deep: true, // 深度监听对象内部属性变化
|
||||
// handler() {
|
||||
// this.updateMonthHaveData();
|
||||
// }
|
||||
// },
|
||||
calendarObj:{
|
||||
immediate: true, // 组件挂载时立即执行一次
|
||||
deep: true, // 深度监听对象内部属性变化
|
||||
handler(newVal) {
|
||||
this.updateMonthHaveData(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
// 根据月或者年维度,获取不同的接口,拿到数据,更新左侧日历框
|
||||
updateMonthHaveData(calendarObj) {
|
||||
if(this.timeType == 'month'){
|
||||
const keys = Object.keys(calendarObj);
|
||||
this.calendarList = []
|
||||
for(let i = 0; i < keys.length; i++) {
|
||||
this.calendarList.push({name:i+1+'月',haveData:calendarObj[keys[i]],isActive:i==new Date().getMonth()})
|
||||
}
|
||||
}else{
|
||||
const keys = Object.keys(calendarObj);
|
||||
this.calendarList = []
|
||||
for(let i = 0; i < keys.length; i++) {
|
||||
this.calendarList.push({name:keys[i]+'年',haveData:calendarObj[keys[i]],isActive:keys[i]==new Date().getFullYear()})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
// 月份列表容器(flex布局,自动换行)
|
||||
.month-list {
|
||||
// 内联样式已优化行间距,此处可留空或补充其他样式
|
||||
}
|
||||
|
||||
// 基础月份样式
|
||||
.monthItem {
|
||||
width: 164px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
transition: all 0.2s ease; // 过渡效果,样式切换更平滑
|
||||
border: 2px solid transparent; // 透明边框,避免选中时布局偏移
|
||||
margin: 0; // 清除默认外边距,进一步缩小缝隙
|
||||
}
|
||||
|
||||
// 有数据的样式(背景色#D1E8FF)
|
||||
.monthItem.has-data {
|
||||
background-color: #D1E8FF;
|
||||
}
|
||||
|
||||
// 无数据的样式(背景色#EFF3F8,基础样式默认值)
|
||||
.monthItem:not(.has-data) {
|
||||
background-color: #EFF3F8;
|
||||
}
|
||||
|
||||
// 本月样式(current类,边框2px solid #0B58FF)
|
||||
.monthItem.current {
|
||||
border: 2px solid #0B58FF !important;
|
||||
}
|
||||
</style>
|
||||
548
src/views/home/components/budgetDetails.vue
Normal file
@@ -0,0 +1,548 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="预算填报详情" icon="cockpitItemIcon" size="indicatorDetailsBg" topSize="indicatorDetailsTitleBg">
|
||||
<div class="kpi-content" style="padding: 14px 14px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 查询表单区域 -->
|
||||
<div class="bottom"
|
||||
style="display: flex;gap: 8px; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 64px;padding: 16px 16px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;margin-top: 10px;"></div>
|
||||
<el-form :inline="true" :model="form" class="demo-form-inline">
|
||||
<el-form-item label="所属层级">
|
||||
<el-select v-model="form.levelId" placeholder="请选择">
|
||||
<el-option v-for="item in levelLList" :key="item.id" :label="item.name" :value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="timeType === 'month' ? '填报月份' : '填报年份'">
|
||||
<!-- 根据timeType切换日期选择器类型:月/年 -->
|
||||
<el-date-picker v-model="form.date" :type="timeType" :placeholder="timeType === 'month' ? '选择月' : '选择年'"
|
||||
@change="handleDateChange">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button style="background-color: #0B58FF;" type="primary" @click="onSubmit">查询</el-button>
|
||||
<el-button type="primary" plain size="medium" @click='importExcel'>导入</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格操作区域 + 表格区域 -->
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 772px;padding: 16px 16px;flex-direction: column; gap: 8px;">
|
||||
|
||||
<!-- 只读模式:显示编辑按钮 -->
|
||||
<div v-if="!isDetail" style="display: flex;gap: 8px;align-items: center;height: 32px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;"></div>
|
||||
<div style="width: 58px;
|
||||
height: 16px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0,0,0,0.85);
|
||||
line-height: 16px;
|
||||
text-align: right;
|
||||
font-style: normal;">指标详情</div>
|
||||
<el-button style="background-color: #0B58FF;height: 32px;line-height: 10px;" type="primary"
|
||||
@click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:显示快捷操作按钮 -->
|
||||
<div v-if="isDetail" style="display: flex;gap: 8px;align-items: center;height: 32px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;"></div>
|
||||
<div
|
||||
style="width: 58px;height: 16px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 14px;color: rgba(0,0,0,0.85);line-height: 16px;text-align: right;font-style: normal;">
|
||||
快捷操作</div>
|
||||
<el-button style="height: 32px;line-height: 10px;" type="primary" plain size="medium"
|
||||
@click="copyLastMonth">复制上月/年</el-button>
|
||||
<el-button style="height: 32px;line-height: 10px;" type="primary" plain size="medium"
|
||||
@click="allUp">全部上调5%</el-button>
|
||||
<el-button style="height: 32px;line-height: 10px;" type="primary" plain size="medium"
|
||||
@click="allDown">全部下调5%</el-button>
|
||||
<el-button style="height: 32px;line-height: 10px;" type="primary" plain size="medium"
|
||||
@click="handleClear">清空配置</el-button>
|
||||
<el-button style="background-color: #0B58FF;height: 32px;line-height: 10px;" type="primary"
|
||||
@click="handleSave">保存</el-button>
|
||||
<el-button text style="height: 32px;line-height: 10px;" plain size="medium"
|
||||
@click="handleCancel">取消</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格组件:添加key属性强制刷新,绑定所有必要属性 -->
|
||||
<base-table style="height: 700px;" :maxHeight=" '700' " @emitFun="inputChange" class="right-aside"
|
||||
:table-props="tableProps" :page="form.pageNo" :limit="form.pageSize" :table-data="tableData" ref="baseTable" id='calendarTable'
|
||||
:key="tableKey"></base-table>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
|
||||
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" action="#" :disabled="upload.isUploading"
|
||||
:on-change="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip text-center" slot="tip">
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
</div>
|
||||
<div class="el-upload__tip" slot="tip">
|
||||
<el-radio-group v-model="upload.timeDim">
|
||||
<el-radio :label="2">月预算</el-radio>
|
||||
<el-radio :label="3">年预算</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
||||
<el-button @click="upload.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import { getLevelStruc, getTargetMonthPage, updateTargetMonthData, getTargetYearPage, updateTargetYearData,copyLastMonthData, copyLastYearData} from '@/api/cockpit'
|
||||
import inputArea from './inputArea.vue' // 导入输入组件
|
||||
import { publicFormatter } from '@/utils/dict';
|
||||
import {getAccessToken, getTenantId} from '@/utils/auth'
|
||||
import axios from 'axios';
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: {
|
||||
Container,
|
||||
inputArea // 注册输入组件
|
||||
},
|
||||
props: {
|
||||
timeType: {
|
||||
type: String,
|
||||
default: 'month', // 默认月份维度
|
||||
// validator: (val) => ['month', 'year'].includes(val) // 校验传入值只能是month/year
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
levelId: undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
date: undefined, // 统一存储日期(月份/年份)
|
||||
startTime: undefined, // 起始时间戳
|
||||
endTime: undefined // 结束时间戳
|
||||
},
|
||||
upload: {
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 弹出层标题
|
||||
title: "预算填报导入",
|
||||
// 是否禁用上传
|
||||
isUploading: false,
|
||||
fileList:[],
|
||||
currentFile:null,
|
||||
timeDim: 2
|
||||
},
|
||||
getDataList: null, // 动态切换的查询接口
|
||||
updateData: null, // 动态切换的更新接口
|
||||
isDetail: false, // 编辑状态标识:false=只读,true=编辑
|
||||
tableData: [], // 表格数据
|
||||
levelLList: [], // 所属层级下拉数据
|
||||
tableProps: [], // 表格列配置
|
||||
tableKey:0,// 强制表格更新
|
||||
allUpBtn: false, // 全部上调按钮状态
|
||||
allDownBtn: false // 全部下调按钮状态
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听timeType变化,动态切换接口并重新初始化
|
||||
timeType: {
|
||||
immediate: true, // 首次加载立即执行
|
||||
handler(newVal) {
|
||||
// 根据timeType切换接口
|
||||
if (newVal === 'month') {
|
||||
this.getDataList = getTargetMonthPage;
|
||||
this.updateData = updateTargetMonthData;
|
||||
} else if (newVal === 'year') {
|
||||
this.getDataList = getTargetYearPage;
|
||||
this.updateData = updateTargetYearData;
|
||||
}
|
||||
// 重新初始化日期和时间戳
|
||||
this.initDefaultDate();
|
||||
this.calculateTimeStamp();
|
||||
// 重新初始化表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 重新请求数据
|
||||
this.$nextTick(() => {
|
||||
this.getData();
|
||||
});
|
||||
}
|
||||
},
|
||||
// 监听isDetail变化,确保配置同步(双重保障)
|
||||
isDetail(newVal) {
|
||||
this.initTableProps(newVal);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 1. 初始化默认日期
|
||||
this.initDefaultDate();
|
||||
// 2. 计算对应时间戳
|
||||
this.calculateTimeStamp();
|
||||
// 3. 先初始化表格配置(优先于数据请求,避免表格空配置渲染)
|
||||
this.initTableProps(this.isDetail);
|
||||
// 4. 等待配置就绪后,再请求数据,避免异步冲突
|
||||
this.$nextTick(() => {
|
||||
this.getData();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// 清空配置
|
||||
handleClear() {
|
||||
// 清空target
|
||||
this.tableData.forEach(item => {
|
||||
item.target = null;
|
||||
})
|
||||
this.tableKey++;
|
||||
this.allDownBtn = false;
|
||||
this.allUpBtn = false;
|
||||
},
|
||||
//复制上月/上年数据(调用接口不同)
|
||||
copyLastMonth() {
|
||||
if(this.timeType === 'month') {
|
||||
this.$modal.confirm('是否确认复制上月数据?').then(() => {
|
||||
this.copyLastMonthDataPage()
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("复制成功");
|
||||
}).catch(() => { });
|
||||
}else{
|
||||
this.$modal.confirm('是否确认复制上年数据?').then(() => {
|
||||
this.copyLastYearDataPage()
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("复制成功");
|
||||
}).catch(() => { });
|
||||
}
|
||||
},
|
||||
copyLastMonthDataPage() {
|
||||
copyLastMonthData({
|
||||
levelId: this.form.levelId,
|
||||
startTime: this.form.startTime,
|
||||
endTime: this.form.endTime,
|
||||
pageSize: this.form.pageSize,
|
||||
pageNo: this.form.pageNo
|
||||
}).then((res) => {
|
||||
this.tableData = res.data.map(item => {
|
||||
// 新增unitLabel字段,存储匹配后的显示名称
|
||||
return {
|
||||
...item
|
||||
};
|
||||
});
|
||||
this.tableKey++;
|
||||
this.allDownBtn = false;
|
||||
this.allUpBtn = false;
|
||||
}).catch(err => {
|
||||
console.error('获取表格数据失败:', err);
|
||||
this.tableData = [];
|
||||
});
|
||||
},
|
||||
copyLastYearDataPage() {
|
||||
copyLastYearData({
|
||||
levelId: this.form.levelId,
|
||||
startTime: this.form.startTime,
|
||||
endTime: this.form.endTime,
|
||||
pageSize: this.form.pageSize,
|
||||
pageNo: this.form.pageNo
|
||||
}).then((res) => {
|
||||
this.tableData = res.data.map(item => {
|
||||
// 新增unitLabel字段,存储匹配后的显示名称
|
||||
return {
|
||||
...item
|
||||
};
|
||||
});
|
||||
this.tableKey++;
|
||||
this.allDownBtn = false;
|
||||
this.allUpBtn = false;
|
||||
}).catch(err => {
|
||||
console.error('获取表格数据失败:', err);
|
||||
this.tableData = [];
|
||||
});
|
||||
},
|
||||
//全部上调5%
|
||||
allUp() {
|
||||
if(this.allUpBtn || this.allDownBtn) {
|
||||
this.$modal.msgWarning("数据已调整,请先保存数据");
|
||||
return
|
||||
}
|
||||
for(let i = 0; i < this.tableData.length; i++) {
|
||||
this.tableData[i].target = (this.tableData[i].target * 1.05).toFixed(2)
|
||||
}
|
||||
// 强制表格组件刷新
|
||||
this.tableKey++;
|
||||
this.allUpBtn = true;
|
||||
},
|
||||
// 全部下调5%
|
||||
allDown() {
|
||||
if(this.allUpBtn || this.allDownBtn) {
|
||||
this.$modal.msgWarning("数据已调整,请先保存数据");
|
||||
return;
|
||||
}
|
||||
for(let i = 0; i < this.tableData.length; i++) {
|
||||
this.tableData[i].target = (this.tableData[i].target * 0.95).toFixed(2)
|
||||
}
|
||||
// 强制表格组件刷新
|
||||
this.tableKey++;
|
||||
this.allDownBtn = true;
|
||||
},
|
||||
// 查询按钮
|
||||
onSubmit() {
|
||||
// 清空原有表格数据,重新请求
|
||||
this.tableData = [];
|
||||
this.$nextTick(() => {
|
||||
this.getDataPage();
|
||||
});
|
||||
},
|
||||
// 切换到编辑模式
|
||||
handleEdit() {
|
||||
this.isDetail = true;
|
||||
// 先更新表格配置,再强制表格刷新
|
||||
this.initTableProps(this.isDetail);
|
||||
this.tableKey++;
|
||||
},
|
||||
// 保存数据(使用动态切换的updateData接口)
|
||||
handleSave() {
|
||||
this.$modal.confirm('是否确认保存数据?').then(() => {
|
||||
return this.updateData(this.tableData)
|
||||
}).then(() => {
|
||||
this.isDetail = false;
|
||||
// 重置表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 清空并重新请求数据,恢复原始状态
|
||||
this.$nextTick(() => {
|
||||
this.tableData = [];
|
||||
this.getData();
|
||||
});
|
||||
this.$modal.msgSuccess("保存成功");
|
||||
}).catch(() => { });
|
||||
},
|
||||
// 请求下拉数据和表格数据(使用动态切换的getDataList接口)
|
||||
getData() {
|
||||
if (!this.getDataList) {
|
||||
console.warn('当前时间维度异常,无法获取数据');
|
||||
return;
|
||||
}
|
||||
// 1. 请求所属层级下拉数据
|
||||
getLevelStruc().then((res) => {
|
||||
this.levelLList = res.data || [];
|
||||
this.form.levelId = this.levelLList[0].id;
|
||||
this.getDataPage()
|
||||
}).catch(err => {
|
||||
console.error('获取所属层级失败:', err);
|
||||
this.levelLList = [];
|
||||
});
|
||||
this.$emit('updateLeft')
|
||||
},
|
||||
getDataPage() {
|
||||
// 2. 请求表格分页数据(使用动态接口)
|
||||
this.getDataList({
|
||||
levelId: this.form.levelId,
|
||||
startTime: this.form.startTime,
|
||||
endTime: this.form.endTime,
|
||||
pageSize: this.form.pageSize,
|
||||
pageNo: this.form.pageNo
|
||||
}).then((res) => {
|
||||
console.log('表格数据:', res);
|
||||
this.tableData = res.data.map(item => {
|
||||
// 新增unitLabel字段,存储匹配后的显示名称
|
||||
return {
|
||||
...item
|
||||
};
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('获取表格数据失败:', err);
|
||||
this.tableData = [];
|
||||
});
|
||||
},
|
||||
// 表格单元格数据变更回调
|
||||
inputChange(val) {
|
||||
// 安全修改:判断索引是否存在,避免数组越界
|
||||
if (this.tableData[val._pageIndex - 1]) {
|
||||
this.tableData[val._pageIndex - 1][val.prop] = val[val.prop];
|
||||
// 标记数据为已修改状态
|
||||
this.tableData[val._pageIndex - 1].status = 1;
|
||||
}
|
||||
},
|
||||
// 取消编辑,恢复只读模式
|
||||
handleCancel() {
|
||||
this.isDetail = false;
|
||||
this.allUpBtn = false;
|
||||
this.allDownBtn = false;
|
||||
// 重置表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 清空并重新请求数据,恢复原始状态
|
||||
this.$nextTick(() => {
|
||||
this.tableData = [];
|
||||
this.getData();
|
||||
});
|
||||
},
|
||||
// 初始化表格列配置(核心:精准控制inputArea挂载)
|
||||
initTableProps(isEdit) {
|
||||
console.log('当前编辑状态:', isEdit, '当前时间维度:', this.timeType);
|
||||
// 基础表格列配置(只读模式使用)
|
||||
const baseTableProps = [
|
||||
{ prop: 'type', label: '指标类型', align: 'center' },
|
||||
{ prop: 'name', label: '指标名称', align: 'center' },
|
||||
{ prop: 'unit', label: '单位', align: 'center', filter: publicFormatter('lb_dw') },
|
||||
{ prop: 'target', label: '预估值', align: 'center' },
|
||||
];
|
||||
if (isEdit) {
|
||||
// 编辑模式:仅给「预估值」列添加inputArea(精准挂载,避免无效配置)
|
||||
this.tableProps = baseTableProps.map(item => {
|
||||
if (item.prop === 'target') { // 只给需要编辑的列添加子组件
|
||||
return {
|
||||
...item,
|
||||
subcomponent: inputArea // 挂载输入组件
|
||||
};
|
||||
}
|
||||
return item; // 其他列保持原有配置
|
||||
});
|
||||
} else {
|
||||
// 只读模式:深拷贝基础配置,避免引用污染
|
||||
// this.tableProps = JSON.parse(JSON.stringify(baseTableProps));
|
||||
this.tableProps = baseTableProps;
|
||||
}
|
||||
console.log('表格配置:', this.tableProps);
|
||||
},
|
||||
// 初始化默认日期(根据timeType)
|
||||
initDefaultDate() {
|
||||
const currentDate = new Date();
|
||||
this.form.date = currentDate;
|
||||
},
|
||||
// 计算时间戳(根据timeType切换:月/年)
|
||||
calculateTimeStamp(selectDate) {
|
||||
let targetDate = selectDate || this.form.date;
|
||||
if (!targetDate) {
|
||||
targetDate = new Date();
|
||||
} else {
|
||||
targetDate = new Date(targetDate);
|
||||
}
|
||||
|
||||
const year = targetDate.getFullYear();
|
||||
let month = targetDate.getMonth(); // 月份0-11
|
||||
|
||||
if (this.timeType === 'month') {
|
||||
// 月维度:当月1号 00:00:00 → 当月最后一天 23:59:59
|
||||
const startDate = new Date(year, month, 1, 0, 0, 0);
|
||||
this.form.startTime = startDate.getTime();
|
||||
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
const endDatePrecise = new Date(year, month, lastDay, 23, 59, 59);
|
||||
this.form.endTime = endDatePrecise.getTime();
|
||||
} else if (this.timeType === 'year') {
|
||||
// 年维度:当年1月1号 00:00:00 → 当年12月31号 23:59:59
|
||||
const startDate = new Date(year, 0, 1, 0, 0, 0);
|
||||
this.form.startTime = startDate.getTime();
|
||||
|
||||
const endDatePrecise = new Date(year, 11, 31, 23, 59, 59);
|
||||
this.form.endTime = endDatePrecise.getTime();
|
||||
}
|
||||
},
|
||||
// 日期选择器变更事件(统一处理月/年变更)
|
||||
handleDateChange(val) {
|
||||
if (!val) {
|
||||
// 清空选择时,重置时间戳
|
||||
this.form.startTime = undefined;
|
||||
this.form.endTime = undefined;
|
||||
return;
|
||||
}
|
||||
// 计算选中日期对应的时间戳
|
||||
this.calculateTimeStamp(val);
|
||||
},
|
||||
// 导入
|
||||
importExcel() {
|
||||
this.upload.open = true
|
||||
},
|
||||
// 文件上传中处理
|
||||
handleFileUploadProgress(file, fileList) {
|
||||
console.log('文件上传中:',file, fileList)
|
||||
this.upload.isUploading = true;
|
||||
this.upload.fileList = fileList;
|
||||
this.upload.currentFile = file.raw;
|
||||
},
|
||||
handleFileSuccess() {},
|
||||
importTemplate() {},
|
||||
// 提交上传文件
|
||||
async submitFileForm() {
|
||||
try {
|
||||
if (!this.upload.currentFile) {
|
||||
return this.$message.error('请先选择要上传的文件!')
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.upload.currentFile) // 文件字段
|
||||
formData.append('timeDim', this.upload.timeDim) // 年月维度字段
|
||||
formData.append('reportDate', this.form.endTime) // 时间维度字段
|
||||
formData.append('levelId', this.form.levelId) // 层级维度字段
|
||||
const response = await axios({
|
||||
url: process.env.VUE_APP_BASE_API + '/admin-api/lb/index-target-month/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': "Bearer " + getAccessToken(),
|
||||
'tenant-id': getTenantId(),
|
||||
},
|
||||
timeout: 30000
|
||||
})
|
||||
// 4. 处理响应结果
|
||||
if (response.data.code === 0) {
|
||||
this.$message.success('文件上传成功!')
|
||||
// 重置表单
|
||||
this.upload.fileList = []
|
||||
this.upload.timeDim = 2
|
||||
this.upload.currentFile = null
|
||||
this.upload.open = false
|
||||
this.upload.isUploading = false
|
||||
this.$refs.upload.clearFiles();
|
||||
this.getData()
|
||||
} else {
|
||||
this.$message.error(`上传失败:${response.data.msg || '未知错误'}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 5. 异常处理
|
||||
console.error('文件上传出错:', error)
|
||||
this.$message.error('上传失败!')
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
// 月份列表容器样式(保留原有配置,若无使用可忽略)
|
||||
.month-list {
|
||||
// 内联样式已优化行间距,此处可留空或补充
|
||||
}
|
||||
|
||||
// 基础月份样式(保留原有配置,若无使用可忽略)
|
||||
.monthItem {
|
||||
width: 164px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monthItem.has-data {
|
||||
background-color: #D1E8FF;
|
||||
}
|
||||
|
||||
.monthItem:not(.has-data) {
|
||||
background-color: #EFF3F8;
|
||||
}
|
||||
|
||||
.monthItem.active {
|
||||
border: 2px solid #0B58FF !important;
|
||||
}
|
||||
</style>
|
||||
409
src/views/home/components/budgetHeader.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<header class="report-header" :class="['report-header__' + size]">
|
||||
<!-- 左侧区域:标题 -->
|
||||
<div class="left-content" :style="{ marginLeft: leftMargin }">
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="text" class="logout-btn" :title="'退出'">
|
||||
<svg-icon style="color: #0B58FF;" icon-class="logout" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native='logout'>退出登录</el-dropdown-item>
|
||||
<el-dropdown-item @click.native='handleToggle'>切换账号</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-button type="text" class="return-btn" :title="'返回'" @click="handleReturn">
|
||||
<svg-icon style="color: #0B58FF;" icon-class="returnIcon" />
|
||||
</el-button>
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择区域:时间维度下拉框(月/年) + 日期选择下拉框 -->
|
||||
<div class="timeType" v-if="isBudget">
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">时间范围</span>
|
||||
</div>
|
||||
<!-- 第一步:时间维度下拉框(月/年) -->
|
||||
<el-select v-model="timeDimension" class="time-dimension-select" @change="handleDimensionChange"
|
||||
style="width: 150px; height: 29px; margin-right: 8px;">
|
||||
<el-option label="月预算" value="month" />
|
||||
<el-option label="年预算" value="year" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'; // 引入moment
|
||||
import {getPath} from "@/utils/ruoyi";
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' },
|
||||
size: { type: String, default: 'basic' },
|
||||
leftMargin: {
|
||||
type: [String, Number],
|
||||
default: '350px' // 默认值设为350px
|
||||
},
|
||||
isBudget: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
timeDimension: 'month', // 默认时间维度:月(可选值:month/year)
|
||||
selectedDate: '', // 选中的日期(格式:YYYY-MM 或 YYYY)
|
||||
dateOptions: [] // 日期下拉框选项(动态生成)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化默认时间维度和日期
|
||||
// this.initDateOptions();
|
||||
// 默认选中当前月/年
|
||||
// if (this.timeDimension === 'month') {
|
||||
// this.selectedDate = moment().format('YYYY-MM');
|
||||
// } else {
|
||||
// this.selectedDate = moment().format('YYYY');
|
||||
// }
|
||||
// this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
methods: {
|
||||
changeFullScreen() {
|
||||
this.$emit('screenfullChange');
|
||||
},
|
||||
handleReturn() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
async logout() {
|
||||
this.$modal.confirm('确定注销并退出系统吗?', '提示').then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
}).catch(() => {});
|
||||
},
|
||||
handleToggle() {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
},
|
||||
exportPDF() {
|
||||
this.$emit('exportPDF');
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化日期下拉框选项(生成近10年的年份/月份选项,可自定义范围)
|
||||
*/
|
||||
initDateOptions() {
|
||||
this.dateOptions = [];
|
||||
const currentYear = moment().year();
|
||||
const range = 10; // 生成近10年的选项
|
||||
|
||||
if (this.timeDimension === 'month') {
|
||||
// 月维度:生成近10年的所有月份(格式:YYYY-MM)
|
||||
for (let y = currentYear; y >= currentYear - range; y--) {
|
||||
for (let m = 12; m >= 1; m--) {
|
||||
const monthStr = m < 10 ? `0${m}` : `${m}`;
|
||||
const value = `${y}-${monthStr}`;
|
||||
const label = `${y}年${monthStr}月`;
|
||||
this.dateOptions.push({ value, label });
|
||||
}
|
||||
}
|
||||
} else if (this.timeDimension === 'year') {
|
||||
// 年维度:生成近10年的年份(格式:YYYY)
|
||||
for (let y = currentYear; y >= currentYear - range; y--) {
|
||||
this.dateOptions.push({
|
||||
value: `${y}`,
|
||||
label: `${y}年`
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 时间维度切换回调(月→年 / 年→月)
|
||||
*/
|
||||
handleDimensionChange() {
|
||||
// 重新初始化日期选项
|
||||
this.$emit('getTimeType', this. timeDimension)
|
||||
},
|
||||
|
||||
/**
|
||||
* 核心方法:根据选中的维度和日期,计算时间范围(时间戳格式)
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
// 初始化时间戳为0(兜底值)
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
// 时间维度对应mode(2=月,3=年,保持和原有逻辑一致)
|
||||
const mode = this.timeDimension === 'month' ? 2 : 3;
|
||||
// 默认当前时间
|
||||
const defaultMoment = moment();
|
||||
|
||||
try {
|
||||
let targetMoment;
|
||||
// 根据维度解析选中的日期
|
||||
if (this.timeDimension === 'month') {
|
||||
targetMoment = this.selectedDate ? moment(this.selectedDate, 'YYYY-MM') : defaultMoment;
|
||||
} else {
|
||||
targetMoment = this.selectedDate ? moment(this.selectedDate, 'YYYY') : defaultMoment;
|
||||
}
|
||||
|
||||
// 验证日期是否有效,无效则使用当前时间兜底
|
||||
if (!targetMoment.isValid()) {
|
||||
console.warn('无效的日期格式,已使用当前时间:', this.selectedDate);
|
||||
targetMoment = defaultMoment;
|
||||
}
|
||||
|
||||
// 根据维度计算时间范围
|
||||
if (this.timeDimension === 'month') {
|
||||
// 月维度:当月第一天00:00:00 → 当月最后一天23:59:59
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
} else if (this.timeDimension === 'year') {
|
||||
// 年维度:当年第一天00:00:00 → 当年最后一天23:59:59
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('计算时间范围时出错:', error);
|
||||
}
|
||||
|
||||
// 返回时间范围信息
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
mode
|
||||
};
|
||||
},
|
||||
|
||||
// 传递时间范围给父组件
|
||||
// emitTimeRange() {
|
||||
// const timeRange = this.calculateTimeRange();
|
||||
// this.$emit('timeRangeChange', timeRange);
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 字体引入 */
|
||||
@font-face {
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* 头部容器基础样式 */
|
||||
.report-header {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&__basic {
|
||||
background: url(../../../assets/img/topBg.png) no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psi {
|
||||
background: url(../../../assets/img/psiTopTitle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* 左侧标题区域 */
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
// margin-left: 350px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
height: 55px;
|
||||
font-family: "YouSheBiaoTiHei", sans-serif;
|
||||
font-size: 42px;
|
||||
color: #1E1651;
|
||||
line-height: 55px;
|
||||
letter-spacing: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 时间选择区域 */
|
||||
.timeType {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
top: 42px;
|
||||
right: 0px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(11, 88, 255, 1);
|
||||
line-height: 28px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timeType .item.no-skew {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-25deg) !important;
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(25deg) !important;
|
||||
}
|
||||
|
||||
.dateP {
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0B58FF;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
}
|
||||
|
||||
/* 右侧全屏按钮区域 */
|
||||
.right-content {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 10px;
|
||||
gap: 21px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
// .current-time {
|
||||
// color: #FFFFFF;
|
||||
// font-family: PingFangSC, PingFang SC;
|
||||
// font-weight: 500;
|
||||
// font-size: 22px;
|
||||
// line-height: 24px;
|
||||
// letter-spacing: 1px;
|
||||
// }
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.home-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
// margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.return-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
// margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义下拉框样式(替换原有日期选择器样式) */
|
||||
::v-deep .time-dimension-select,
|
||||
::v-deep .custom-date-select {
|
||||
height: 28px !important;
|
||||
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 28px !important;
|
||||
color: #fff !important;
|
||||
background-color: rgba(11, 88, 255, 1) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-input__icon {
|
||||
color: #fff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 28px !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item {
|
||||
font-size: 14px !important;
|
||||
padding: 6px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 时间维度下拉框额外样式 */
|
||||
::v-deep .time-dimension-select .el-input__inner {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
border-radius: 4px 0 0 4px !important;
|
||||
}
|
||||
|
||||
/* 日期选择下拉框额外样式 */
|
||||
::v-deep .custom-date-select .el-input__inner {
|
||||
border-radius: 0 4px 4px 0 !important;
|
||||
}
|
||||
</style>
|
||||
145
src/views/home/components/changeBase.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="changeBase">
|
||||
<div class="base-item" @click="handleClick(index)" v-for="(item, index) in buttonList" :key="item"
|
||||
:style="{ zIndex: activeButton === index ? 10 : 1 }">
|
||||
<img :src="activeButton === index ? imgMap.bgBase[item] : imgMap.base[item]" :alt="`${item}基地`" :title="item">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 导入基地图片(按统一命名规范)
|
||||
import baseYixing from '@/assets/images/base/宜兴.png';
|
||||
import baseZhangzhou from '@/assets/images/base/漳州.png';
|
||||
import baseZigong from '@/assets/images/base/自贡.png';
|
||||
import baseTongcheng from '@/assets/images/base/桐城.png';
|
||||
import baseLuoyang from '@/assets/images/base/洛阳.png';
|
||||
import baseHefei from '@/assets/images/base/合肥.png';
|
||||
import baseSuqian from '@/assets/images/base/宿迁.png';
|
||||
import baseQinhuangdao from '@/assets/images/base/秦皇岛.png';
|
||||
|
||||
// 导入选中态基地图片
|
||||
import bgBaseYixing from '@/assets/images/bgBase/宜兴.png';
|
||||
import bgBaseZhangzhou from '@/assets/images/bgBase/漳州.png';
|
||||
import bgBaseZigong from '@/assets/images/bgBase/自贡.png';
|
||||
import bgBaseTongcheng from '@/assets/images/bgBase/桐城.png';
|
||||
import bgBaseLuoyang from '@/assets/images/bgBase/洛阳.png';
|
||||
import bgBaseHefei from '@/assets/images/bgBase/合肥.png';
|
||||
import bgBaseSuqian from '@/assets/images/bgBase/宿迁.png';
|
||||
import bgBaseQinhuangdao from '@/assets/images/bgBase/秦皇岛.png';// 补充:江苏
|
||||
|
||||
export default {
|
||||
name: "BaseSelector",
|
||||
props: {
|
||||
factory: {
|
||||
type: [String, Number],
|
||||
default: 5, // 新映射中“宜兴”对应序号7
|
||||
validator: (val) => [5, 2, 7, 3, 8, 9, 10].includes(val) // 校验序号范围(匹配新的baseNameToIndexMap)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeButton: 5, // 初始化默认选中索引2(对应buttonList中的“宜兴”)
|
||||
buttonList: ['合肥', '桐城', '宜兴', '自贡', '漳州', '洛阳', '秦皇岛', '宿迁'], // 匹配截图顺序
|
||||
imgMap: {
|
||||
base: {
|
||||
合肥: baseHefei,
|
||||
桐城: baseTongcheng,
|
||||
宜兴: baseYixing,
|
||||
自贡: baseZigong,
|
||||
漳州: baseZhangzhou,
|
||||
洛阳: baseLuoyang,
|
||||
秦皇岛: baseQinhuangdao,
|
||||
宿迁: baseSuqian
|
||||
},
|
||||
bgBase: {
|
||||
合肥: bgBaseHefei,
|
||||
桐城: bgBaseTongcheng,
|
||||
宜兴: bgBaseYixing,
|
||||
自贡: bgBaseZigong,
|
||||
漳州: bgBaseZhangzhou,
|
||||
洛阳: bgBaseLuoyang,
|
||||
秦皇岛: bgBaseQinhuangdao,
|
||||
宿迁: bgBaseSuqian
|
||||
}
|
||||
},
|
||||
baseNameToIndexMap: { // 新的名称→序号映射
|
||||
宜兴: 7,
|
||||
漳州: 8,
|
||||
自贡: 3,
|
||||
桐城: 2,
|
||||
洛阳: 9,
|
||||
合肥: 5,
|
||||
秦皇岛: 10, // 补充
|
||||
宿迁: 6 // 补充
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 监听父组件传递的factory变化,同步本地选中索引
|
||||
factory: {
|
||||
handler(newVal) {
|
||||
// 根据新的baseNameToIndexMap,找到对应的基地名称
|
||||
const targetName = Object.keys(this.baseNameToIndexMap).find(name => this.baseNameToIndexMap[name] === newVal);
|
||||
// 根据名称找到buttonList中的索引
|
||||
const targetIndex = this.buttonList.indexOf(targetName);
|
||||
// 合法索引则更新,否则默认选中宜兴
|
||||
this.activeButton = targetIndex > -1 ? targetIndex : 2;
|
||||
console.log('当前选中基地:', this.buttonList[this.activeButton], '序号:', newVal);
|
||||
},
|
||||
immediate: true // 初始化立即执行
|
||||
},
|
||||
// 监听本地选中索引变化,向父组件发送事件
|
||||
activeButton(newVal) {
|
||||
const selectedName = this.buttonList[newVal];
|
||||
const selectedIndex = this.baseNameToIndexMap[selectedName] || 5; // 默认返回宜兴的序号7
|
||||
this.$emit('baseChange', selectedIndex);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(index) {
|
||||
this.activeButton = index;
|
||||
},
|
||||
getIndexByName(name) {
|
||||
return this.baseNameToIndexMap[name] || 5;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化时触发事件,传递默认选中的宜兴序号(7)
|
||||
this.$emit('baseChange', 5);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.changeBase {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
|
||||
.base-item {
|
||||
width: 234px;
|
||||
height: 81px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 38px;
|
||||
line-height: 54px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
margin-right: -35px; // 重叠效果,可根据需求调整
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,16 @@
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isShowTab" class="tab-group">
|
||||
<!-- 月度Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'month' }" @click="handleTabClick('month')">
|
||||
月度
|
||||
</div>
|
||||
<!-- 累计Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'total' }" @click="handleTabClick('total')">
|
||||
累计
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpitContainer-body">
|
||||
<slot>
|
||||
@@ -22,12 +32,22 @@ export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'size', 'icon', 'topSize'],
|
||||
props: ['name', 'size', 'icon', 'topSize', 'isShowTab'],
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
activeTab: 'month' // 初始化激活的Tab(支持父组件传默认值)
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
methods: {
|
||||
handleTabClick(tabType) {
|
||||
this.activeTab = tabType;
|
||||
// 向父组件派发Tab切换事件,传递当前选中的Tab类型
|
||||
this.$emit('tabChange', tabType);
|
||||
// 可选:同时传递更详细的信息(如标签名)
|
||||
// this.$emit('tabChange', { type: tabType, name: tabType === 'month' ? '月度' : '累计' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -43,35 +63,37 @@ export default {
|
||||
|
||||
.content-top {
|
||||
height: 60px;
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
// width: 547px;
|
||||
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
@@ -106,12 +128,24 @@ export default {
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__calendarTitleBg {
|
||||
background: url(../../../assets/img/calendarTitleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__indicatorDetailsTitleBg {
|
||||
background: url(../../../assets/img/indicatorDetailsTitleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
@@ -167,11 +201,31 @@ export default {
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__opLargeBg {
|
||||
background: url(../../../assets/img/opLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__calendarBg {
|
||||
background: url(../../../assets/img/calendarBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__indicatorDetailsBg {
|
||||
background: url(../../../assets/img/indicatorDetailsBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
// &__left {
|
||||
// background: url(../../../../../../../assets/img/left.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
@@ -262,4 +316,42 @@ export default {
|
||||
.container-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-group {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 10%;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
gap: 8px; // Tab之间的间距
|
||||
}
|
||||
|
||||
// Tab基础样式(统一)
|
||||
.tab-item {
|
||||
padding: 0 24px;
|
||||
width: 79px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease; // 样式切换动画
|
||||
}
|
||||
|
||||
// 未激活的Tab样式(原first-child样式)
|
||||
.tab-item:not(.active) {
|
||||
background: #ECF4FE;
|
||||
color: #0B58FF;
|
||||
}
|
||||
|
||||
// 激活的Tab样式(原last-child样式)
|
||||
.tab-item.active {
|
||||
background: #3071FF;
|
||||
color: #F9FCFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="coreItem">
|
||||
<div class="item" v-for="(item, index) in itemList" :key="index">
|
||||
<div @click="handleRoute(item.path)" class="item" v-for="(item, index) in itemList" :key="index">
|
||||
<div class="unit">{{ item.unit }}</div>
|
||||
<div class="item-content">
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
@@ -42,7 +42,7 @@
|
||||
export default {
|
||||
name: "Container",
|
||||
components: {},
|
||||
props: ["purchase"],
|
||||
props: ["purchase",'dateData'],
|
||||
data() {
|
||||
return {
|
||||
itemList: []
|
||||
@@ -60,15 +60,25 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRoute(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
transformData(rawData) {
|
||||
const dataMap = [
|
||||
{
|
||||
key: 'increase',
|
||||
unit: '本月增效额·万元'
|
||||
unit: '本月增效额·万元',
|
||||
path:'/procurementGainAnalysis/procurementGainAnalysis'
|
||||
},
|
||||
{
|
||||
key: 'totalIncrease',
|
||||
unit: '累计增效额·万元'
|
||||
unit: '累计增效额·万元',
|
||||
path: '/procurementGainAnalysis/procurementGainAnalysis'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -80,7 +90,8 @@ export default {
|
||||
unit: itemInfo.unit,
|
||||
targetValue: rawItem.target || 0,
|
||||
currentValue: rawItem.real || 0,
|
||||
progress: progress
|
||||
progress: progress,
|
||||
path: itemInfo.path
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -106,7 +117,9 @@ export default {
|
||||
background: #f9fcff;
|
||||
padding: 12px 12px 0 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 4px 12px 2px #B5CDE5;
|
||||
}
|
||||
.unit {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="barTop">
|
||||
<div class="title">销售指标趋势</div>
|
||||
<div class="legend">
|
||||
<span class="legend-item target">目标</span>
|
||||
<span class="legend-item target">预算</span>
|
||||
<span class="legend-item real">实际</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="button-line lineThree" v-if="activeButton !== 2 && activeButton !== 3"></div>
|
||||
<div class="item-button" :class="{ active: activeButton === 3 }" @click="activeButton = 3">
|
||||
双镀产品
|
||||
双镀销量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
'单价',
|
||||
'净价',
|
||||
'销量',
|
||||
'双镀面板' // 注意:数据中的 key 是“双镀面板”,按钮显示的是“双镀产品”
|
||||
'双镀销量' // 注意:数据中的 key 是“双镀面板”,按钮显示的是“双镀产品”
|
||||
]
|
||||
};
|
||||
},
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
|
||||
return [
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'circle',
|
||||
@@ -119,7 +119,6 @@ export default {
|
||||
{
|
||||
name: '实际',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Container name="采购重点指标" nameTwo="存货重点指标" icon="cockpitItemIcon" size="bottomBasic">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;">
|
||||
<coreBottomLeftItem :purchase="purchase">
|
||||
<coreBottomLeftItem :dateData="dateData" :purchase="purchase">
|
||||
</coreBottomLeftItem>
|
||||
<div class="content-right" style="background: #F9FCFF;padding: 15px 12px;">
|
||||
<base-table style="height: 180px;width: 260px;" :page="1" :limit="10" :show-index="true" :beilv="1"
|
||||
@@ -29,6 +29,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
dateData: { // 接收父组件传递的设备数据数组
|
||||
type: Object,
|
||||
default: () => { } // 默认空数组,避免报错
|
||||
},
|
||||
inventory: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
label: { backgroundColor: '#6a7985' }
|
||||
}
|
||||
},
|
||||
grid: { top: 10, bottom: 20, right: 25, left: 50 },
|
||||
grid: { top: 35, bottom: 20, right: 25, left: 70 },
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
@@ -97,32 +97,24 @@ export default {
|
||||
overflow: 'break',
|
||||
formatter: (value) => {
|
||||
const dateParts = value.split('-'); // ["2025", "07", "01"]
|
||||
if (dateParts.length < 2) return value; // 处理异常格式
|
||||
if (dateParts.length < 2) return value;
|
||||
|
||||
switch (this.dateData.mode) {
|
||||
case 1: // 日模式,显示“月-日”
|
||||
// 确保有日的部分
|
||||
return dateParts.length >= 3
|
||||
? `${dateParts[1]}月${dateParts[2]}日`
|
||||
: `${dateParts[1]}月`; // fallback
|
||||
case 2: // 月模式,显示“月”
|
||||
return `${dateParts[1]}月`;
|
||||
case 3: // 年模式,显示“年”
|
||||
return `${dateParts[0]}年`;
|
||||
default: // 默认月模式
|
||||
return `${dateParts[1]}月`;
|
||||
}
|
||||
// 去掉月份前面的0,然后加上"月"
|
||||
const month = dateParts[1].replace(/^0+/, '');
|
||||
return `${month}月`;
|
||||
}
|
||||
},
|
||||
data: this.xAxisData
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'left' },
|
||||
// type: 'value',
|
||||
name: '元/㎡',
|
||||
// nameLocation:'center',
|
||||
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'right' },
|
||||
// min: () => 0,
|
||||
// max: (value) => Math.ceil(value.max),
|
||||
// scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 12 },
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="coreItem">
|
||||
<!-- 动态生成每个 item -->
|
||||
<div class="item" v-for="(item, index) in itemList" :key="index">
|
||||
<div @click="handleDashboardClick(item.path)" class="item" v-for="(item, index) in itemList" :key="index">
|
||||
<div class="unit">{{ item.name }}</div>
|
||||
<div class="item-content">
|
||||
<!-- 左右内容容器 -->
|
||||
@@ -41,7 +41,11 @@ export default {
|
||||
cost: { // 接收父组件传递的 cost 数据对象
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
dateData: { // 接收父组件传递的 cost 数据对象
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -61,6 +65,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : 5,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 核心转换函数:将 cost 对象转换为 itemList 数组
|
||||
* @param {Object} rawData - 原始的 cost 数据对象
|
||||
@@ -69,9 +82,16 @@ export default {
|
||||
transformData(rawData) {
|
||||
// 定义费用类型映射关系(键名、显示名称、单位)
|
||||
const costMapping = [
|
||||
{ key: 'manageCost', name: '管理费用·万元' },
|
||||
{ key: 'saleCost', name: '销售费用·万元' },
|
||||
{ key: 'financeCost', name: '财务费用·万元' }
|
||||
{
|
||||
key: 'totalCost', name: '总费用·万元',
|
||||
path:"/expenseAnalysis/expenseAnalysisBase"
|
||||
},
|
||||
{
|
||||
key: 'manageCost', name: '管理费用·万元',
|
||||
path: "/expenseAnalysis/expenseAnalysisBase"
|
||||
},
|
||||
{ key: 'saleCost', name: '销售费用·万元', path: "/expenseAnalysis/expenseAnalysisBase" },
|
||||
{ key: 'financeCost', name: '财务费用·万元', path: "/expenseAnalysis/expenseAnalysisBase" }
|
||||
];
|
||||
|
||||
// 遍历映射关系,转换数据
|
||||
@@ -81,6 +101,8 @@ export default {
|
||||
|
||||
return {
|
||||
name: mappingItem.name,
|
||||
path: mappingItem.path,
|
||||
hbe: costData.hbe,
|
||||
targetValue: costData.last, // 上月值
|
||||
currentValue: costData.this // 本月值
|
||||
};
|
||||
@@ -117,7 +139,10 @@ export default {
|
||||
background: #f9fcff;
|
||||
padding: 12px 0px 17px 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 4px 12px 2px #B5CDE5;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.unit {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'inventory'">
|
||||
<!-- 存货重点指标对应的内容 -->
|
||||
<costItem :cost="cost"></costItem>
|
||||
<costItem :dateData="dateData" :cost="cost"></costItem>
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<CostsBottomBar :line="cost.line" :dateData="dateData">
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
|
||||
min: 0,
|
||||
max: (value) => Math.ceil(value.max),
|
||||
scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
|
||||
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="barTop-left" style="display: flex;">
|
||||
<div class="title">财务指标趋势</div>
|
||||
<div class="legend">
|
||||
<span class="legend-item target">目标</span>
|
||||
<span class="legend-item target">预算</span>
|
||||
<span class="legend-item real">实际</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
// 定义按钮与 line 数据中 key 的映射关系
|
||||
buttonToDataKey: [
|
||||
'营业收入',
|
||||
'经营收入', // 注意:数据中的 key 是“经营收入”,按钮显示的是“经营性利润”
|
||||
'经营性利润', // 注意:数据中的 key 是“经营收入”,按钮显示的是“经营性利润”
|
||||
'利润总额',
|
||||
'毛利率'
|
||||
]
|
||||
@@ -85,9 +85,8 @@ export default {
|
||||
|
||||
return [
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
|
||||
@@ -104,7 +103,6 @@ export default {
|
||||
{
|
||||
name: '实际',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
|
||||
|
||||
126
src/views/home/components/indicatorCalendar.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="指标填报日历" icon="cockpitItemIcon" size="calendarBg" topSize="calendarTitleBg">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 14px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 2. .top 保持 flex,无需固定高度,自动跟随子元素拉伸 -->
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 844px;padding: 26px 16px;">
|
||||
<!-- 动态生成12个月的容器:优化flex布局,缩小行间距 -->
|
||||
<div class="month-list"
|
||||
style="display: flex; gap: 16px; flex-wrap: wrap; align-content: flex-start; row-gap: 8px;">
|
||||
<!-- 循环生成12个月:通过判断当前月份索引,添加current类 -->
|
||||
<div class="monthItem" :class="{
|
||||
'has-data': month.haveData,
|
||||
'current': index === currentMonthIndex // 本月匹配current样式
|
||||
}" v-for="(month, index) in list" :key="index">
|
||||
{{ month.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
// import * as echarts from 'echarts'
|
||||
// import topItem from './operating-item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
calendarList: { // 接收父组件传递的年月状态对象
|
||||
type: Object, // 注意:父组件传递的是对象,不是数组,修正props类型
|
||||
default: () => ({}) // 默认空对象,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
list:[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 计算属性:获取当前月份对应的索引(0-11,对应1月-12月)
|
||||
currentMonthIndex() {
|
||||
// new Date().getMonth() 返回 0(1月) - 11(12月),正好匹配monthList索引
|
||||
return new Date().getMonth();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听calendarList变化,实时更新monthList的haveData状态
|
||||
calendarList: {
|
||||
immediate: true, // 组件挂载时立即执行一次
|
||||
deep: true, // 深度监听对象内部属性变化
|
||||
handler(newVal) {
|
||||
this.updateMonthHaveData(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表(若需展示图表,需在模板中添加对应 DOM)
|
||||
// this.$nextTick(() => this.updateChart())
|
||||
},
|
||||
methods: {
|
||||
// 根据calendarList更新monthList的haveData状态
|
||||
updateMonthHaveData(calendarObj) {
|
||||
if (!calendarObj || typeof calendarObj !== 'object') return;
|
||||
const keys = Object.keys(calendarObj);
|
||||
if(keys.length == 0){
|
||||
return
|
||||
}
|
||||
console.log('calendarObj',calendarObj)
|
||||
this.list = []
|
||||
for(let i = 0; i < keys.length; i++) {
|
||||
this.list.push({name:i+1+'月',haveData:calendarObj[keys[i]],isActive:i==new Date().getMonth()})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
// 月份列表容器(flex布局,自动换行)
|
||||
.month-list {
|
||||
// 内联样式已优化行间距,此处可留空或补充其他样式
|
||||
}
|
||||
|
||||
// 基础月份样式
|
||||
.monthItem {
|
||||
width: 164px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
cursor: pointer; // 鼠标悬浮手型
|
||||
transition: all 0.2s ease; // 过渡效果,样式切换更平滑
|
||||
border: 2px solid transparent; // 透明边框,避免选中时布局偏移
|
||||
margin: 0; // 清除默认外边距,进一步缩小缝隙
|
||||
}
|
||||
|
||||
// 有数据的样式(背景色#D1E8FF)
|
||||
.monthItem.has-data {
|
||||
background-color: #D1E8FF;
|
||||
}
|
||||
|
||||
// 无数据的样式(背景色#EFF3F8,基础样式默认值)
|
||||
.monthItem:not(.has-data) {
|
||||
background-color: #EFF3F8;
|
||||
}
|
||||
|
||||
// 本月样式(current类,边框2px solid #0B58FF)
|
||||
.monthItem.current {
|
||||
border: 2px solid #0B58FF !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style></style>
|
||||
424
src/views/home/components/indicatorDetails.vue
Normal file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="指标填报详情" icon="cockpitItemIcon" size="indicatorDetailsBg" topSize="indicatorDetailsTitleBg">
|
||||
<div class="kpi-content" style="padding: 14px 14px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 查询表单区域 -->
|
||||
<div class="bottom"
|
||||
style="display: flex;gap: 8px; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 64px;padding: 16px 16px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;margin-top: 10px;"></div>
|
||||
<el-form :inline="true" :model="form" class="demo-form-inline">
|
||||
<el-form-item label="所属层级">
|
||||
<el-select v-model="form.levelId" placeholder="请选择">
|
||||
<el-option v-for="item in levelLList" :key="item.id" :label="item.name" :value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="填报月份">
|
||||
<el-date-picker v-model="form.month" type="month" placeholder="选择月" @change="handleMonthChange">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button style="background-color: #0B58FF;" type="primary" @click="onSubmit">查询</el-button>
|
||||
<el-button type="primary" plain size="medium" @click='importExcel'>导入</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格操作区域 + 表格区域 -->
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 772px;padding: 16px 16px;flex-direction: column; gap: 8px;">
|
||||
|
||||
<!-- 只读模式:显示编辑按钮 -->
|
||||
<div v-if="!isDetail" style="display: flex;gap: 8px;align-items: center;height: 32px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;"></div>
|
||||
<div style="width: 58px;
|
||||
height: 16px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0,0,0,0.85);
|
||||
line-height: 16px;
|
||||
text-align: right;
|
||||
font-style: normal;">指标详情</div>
|
||||
<el-button style="background-color: #0B58FF;height: 32px;line-height: 10px;" type="primary"
|
||||
@click="handleEdit">编辑</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:显示快捷操作按钮 -->
|
||||
<div v-if="isDetail" style="display: flex;gap: 8px;align-items: center;height: 32px;">
|
||||
<div style="width: 4px;height: 16px;background: #0B58FF;border-radius: 1px;"></div>
|
||||
<div
|
||||
style="width: 58px;height: 16px;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 14px;color: rgba(0,0,0,0.85);line-height: 16px;text-align: right;font-style: normal;">
|
||||
指标详情</div>
|
||||
<el-button style="background-color: #0B58FF;height: 32px;line-height: 10px;" type="primary"
|
||||
@click="handleSave">保存</el-button>
|
||||
<el-button text style="height: 32px;line-height: 10px;" plain size="medium"
|
||||
@click="handleCancel">取消</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格组件:添加key属性强制刷新,绑定所有必要属性 -->
|
||||
<base-table style="height: 700px;" @emitFun="inputChange" class="right-aside" :table-props="tableProps"
|
||||
:page="form.pageNo" :limit="form.pageSize" :table-data="tableData" ref="baseTable"
|
||||
:key="`base-table-${isDetail}`" :maxHeight="700"
|
||||
></base-table>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
|
||||
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" action="#" :disabled="upload.isUploading"
|
||||
:on-change="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip text-center" slot="tip">
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
</div>
|
||||
<div class="el-upload__tip" slot="tip">
|
||||
</div>
|
||||
</el-upload>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
||||
<el-button @click="upload.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import { getLevelStruc, getRealMonthPage, updateRealMonthData, getDictListData, } from '@/api/cockpit'
|
||||
import inputArea from './inputArea.vue' // 导入输入组件
|
||||
import {getAccessToken, getTenantId} from '@/utils/auth'
|
||||
import axios from 'axios';
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: {
|
||||
Container,
|
||||
inputArea // 注册输入组件
|
||||
},
|
||||
props: {
|
||||
// 可保留原有props配置,若无需求可忽略
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
levelId: undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
month: undefined,
|
||||
startTime: undefined, // 当月起始时间戳(1号00:00:00)
|
||||
endTime: undefined // 当月结束时间戳(月末23:59:59)
|
||||
},
|
||||
dictData:[],
|
||||
isDetail: false, // 编辑状态标识:false=只读,true=编辑
|
||||
tableData: [], // 表格数据
|
||||
levelLList: [], // 所属层级下拉数据
|
||||
tableProps: [], // 表格列配置
|
||||
upload: {
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 弹出层标题
|
||||
title: "指标填报导入",
|
||||
// 是否禁用上传
|
||||
isUploading: false,
|
||||
fileList:[],
|
||||
currentFile:null
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 可选:监听isDetail变化,确保配置同步(双重保障)
|
||||
isDetail(newVal) {
|
||||
this.initTableProps(newVal);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 1. 初始化当前月份到日期选择器
|
||||
const currentDate = new Date();
|
||||
this.form.month = currentDate;
|
||||
// 2. 计算当月起止时间戳
|
||||
this.setMonthTimeStamp();
|
||||
// 3. 先初始化表格配置(优先于数据请求,避免表格空配置渲染)
|
||||
this.initTableProps(this.isDetail);
|
||||
// 4. 等待配置就绪后,再请求数据,避免异步冲突
|
||||
this.getDictData()
|
||||
this.$nextTick(() => {
|
||||
this.getData();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getDictData() {
|
||||
getDictListData({ pageNo: 1, pageSize: 100, dictType: 'lb_dw' }).then((res) => {
|
||||
this.dictData = res.data.list
|
||||
})
|
||||
},
|
||||
// 表格单元格数据变更回调
|
||||
inputChange(val) {
|
||||
console.log('修改的数据:', val);
|
||||
// 安全修改:判断索引是否存在,避免数组越界
|
||||
if (this.tableData[val._pageIndex - 1]) {
|
||||
this.tableData[val._pageIndex - 1][val.prop] = val[val.prop];
|
||||
// 标记数据为已修改状态
|
||||
this.tableData[val._pageIndex - 1].status = 1;
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化表格列配置(核心:精准控制inputArea挂载)
|
||||
initTableProps(isEdit) {
|
||||
console.log('当前编辑状态:', isEdit);
|
||||
|
||||
// 基础表格列配置(只读模式使用)
|
||||
const baseTableProps = [
|
||||
{ prop: 'type', label: '指标类型', align: 'center' },
|
||||
{ prop: 'name', label: '指标名称', align: 'center' },
|
||||
{ prop: 'unitLabel', label: '单位', align: 'center' },
|
||||
{ prop: 'value', label: '实际值', align: 'center' },
|
||||
];
|
||||
|
||||
if (isEdit) {
|
||||
// 编辑模式:仅给「预估值」列添加inputArea(精准挂载,避免无效配置)
|
||||
this.tableProps = baseTableProps.map(item => {
|
||||
if (item.prop === 'value') { // 只给需要编辑的列添加子组件
|
||||
return {
|
||||
...item,
|
||||
subcomponent: inputArea // 挂载输入组件
|
||||
};
|
||||
}
|
||||
return item; // 其他列保持原有配置
|
||||
});
|
||||
} else {
|
||||
// 只读模式:深拷贝基础配置,避免引用污染
|
||||
this.tableProps = JSON.parse(JSON.stringify(baseTableProps));
|
||||
}
|
||||
console.log('表格配置:', this.tableProps);
|
||||
},
|
||||
|
||||
// 切换到编辑模式
|
||||
handleEdit() {
|
||||
this.isDetail = true;
|
||||
// 先更新表格配置,再强制表格刷新(双重保障)
|
||||
this.initTableProps(this.isDetail);
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.baseTable) {
|
||||
this.$refs.baseTable.$forceUpdate(); // 强制表格组件刷新
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSave() {
|
||||
this.$modal.confirm('是否确认保存数据?').then(() => {
|
||||
return updateRealMonthData(this.tableData)
|
||||
}).then(() => {
|
||||
this.isDetail = false;
|
||||
// 重置表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 清空并重新请求数据,恢复原始状态
|
||||
this.$nextTick(() => {
|
||||
this.tableData = [];
|
||||
this.getData();
|
||||
});
|
||||
this.$modal.msgSuccess("保存成功");
|
||||
}).catch(() => { });
|
||||
},
|
||||
// 取消编辑,恢复只读模式
|
||||
handleCancel() {
|
||||
this.isDetail = false;
|
||||
// 重置表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 清空并重新请求数据,恢复原始状态
|
||||
this.$nextTick(() => {
|
||||
this.tableData = [];
|
||||
this.getData();
|
||||
});
|
||||
console.log('已取消编辑,恢复原始数据');
|
||||
},
|
||||
handleClear() {
|
||||
this.isDetail = false;
|
||||
// 重置表格配置
|
||||
this.initTableProps(this.isDetail);
|
||||
// 清空并重新请求数据,恢复原始状态
|
||||
this.$nextTick(() => {
|
||||
this.tableData = [];
|
||||
this.getData();
|
||||
});
|
||||
},
|
||||
|
||||
// 通用方法:计算指定月份(默认当前月)的起止时间戳
|
||||
setMonthTimeStamp(selectDate) {
|
||||
let targetDate;
|
||||
if (selectDate) {
|
||||
targetDate = new Date(selectDate); // 传入选中日期,计算对应月份
|
||||
} else {
|
||||
targetDate = new Date(); // 未传入,使用当前系统日期
|
||||
}
|
||||
const year = targetDate.getFullYear();
|
||||
const month = targetDate.getMonth(); // 月份0-11
|
||||
|
||||
// 计算当月1号 00:00:00 时间戳
|
||||
const startDate = new Date(year, month, 1, 0, 0, 0);
|
||||
this.form.startTime = startDate.getTime();
|
||||
|
||||
// 计算当月最后一天 23:59:59 时间戳(精准版)
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
const endDatePrecise = new Date(year, month, lastDay, 23, 59, 59);
|
||||
this.form.endTime = endDatePrecise.getTime();
|
||||
},
|
||||
|
||||
// 日期选择器变更事件:更新对应月份的起止时间戳
|
||||
handleMonthChange(val) {
|
||||
if (!val) {
|
||||
// 清空选择时,重置时间戳
|
||||
this.form.startTime = undefined;
|
||||
this.form.endTime = undefined;
|
||||
return;
|
||||
}
|
||||
// 计算选中月份的起止时间戳
|
||||
this.setMonthTimeStamp(val);
|
||||
},
|
||||
getUnitLabel(unitCode) {
|
||||
// 若字典为空或无匹配编码,返回原编码或空字符串
|
||||
if (!this.dictData || this.dictData.length === 0) {
|
||||
return unitCode || '';
|
||||
}
|
||||
// 查找匹配的字典项
|
||||
const matchItem = this.dictData.find(item => item.value == unitCode);
|
||||
// 返回匹配的label,无匹配则返回原unit编码
|
||||
return matchItem ? matchItem.label : (unitCode || '');
|
||||
},
|
||||
// 请求下拉数据和表格数据
|
||||
getData() {
|
||||
// 1. 请求所属层级下拉数据
|
||||
getLevelStruc().then((res) => {
|
||||
this.levelLList = res.data || [];
|
||||
this.form.levelId = this.levelLList[0].id;
|
||||
this.getDataPage()
|
||||
}).catch(err => {
|
||||
console.error('获取所属层级失败:', err);
|
||||
this.levelLList = [];
|
||||
});
|
||||
this.$emit('updateLeft')
|
||||
},
|
||||
getDataPage() {
|
||||
// 2. 请求表格分页数据
|
||||
getRealMonthPage({
|
||||
levelId: this.form.levelId,
|
||||
startTime: this.form.startTime,
|
||||
endTime: this.form.endTime,
|
||||
pageSize: this.form.pageSize,
|
||||
pageNo: this.form.pageNo
|
||||
}).then((res) => {
|
||||
console.log('表格数据:', res);
|
||||
this.tableData = res.data.map(item => {
|
||||
// 新增unitLabel字段,存储匹配后的显示名称
|
||||
return {
|
||||
...item,
|
||||
unitLabel: this.getUnitLabel(item.unit)
|
||||
};
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('获取表格数据失败:', err);
|
||||
this.tableData = [];
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// 查询按钮点击事件(可根据需求扩展逻辑)
|
||||
onSubmit() {
|
||||
// 清空原有表格数据,重新请求
|
||||
this.tableData = [];
|
||||
this.$nextTick(() => {
|
||||
this.getDataPage();
|
||||
});
|
||||
},
|
||||
// 导入
|
||||
importExcel() {
|
||||
this.upload.open = true
|
||||
},
|
||||
// 文件上传中处理
|
||||
handleFileUploadProgress(file, fileList) {
|
||||
console.log('文件上传中:',file, fileList)
|
||||
this.upload.isUploading = true;
|
||||
this.upload.fileList = fileList;
|
||||
this.upload.currentFile = file.raw;
|
||||
},
|
||||
handleFileSuccess() {},
|
||||
importTemplate() {},
|
||||
// 提交上传文件
|
||||
async submitFileForm() {
|
||||
try {
|
||||
if (!this.upload.currentFile) {
|
||||
return this.$message.error('请先选择要上传的文件!')
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.upload.currentFile) // 文件字段
|
||||
formData.append('reportDate', this.form.endTime) // 时间维度字段
|
||||
formData.append('levelId', this.form.levelId) // 层级维度字段
|
||||
const response = await axios({
|
||||
url: process.env.VUE_APP_BASE_API + '/admin-api/lb/index-real-month/actualIndicatorImport',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': "Bearer " + getAccessToken(),
|
||||
'tenant-id': getTenantId(),
|
||||
},
|
||||
timeout: 30000
|
||||
})
|
||||
// 4. 处理响应结果
|
||||
if (response.data.code === 0) {
|
||||
this.$message.success('文件上传成功!')
|
||||
// 重置表单
|
||||
this.upload.fileList = []
|
||||
this.upload.currentFile = null
|
||||
this.upload.open = false
|
||||
this.upload.isUploading = false
|
||||
this.$refs.upload.clearFiles();
|
||||
this.getData()
|
||||
} else {
|
||||
this.$message.error(`上传失败:${response.data.msg || '未知错误'}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 5. 异常处理
|
||||
console.error('文件上传出错:', error)
|
||||
this.$message.error('上传失败!')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
// 月份列表容器样式(保留原有配置,若无使用可忽略)
|
||||
.month-list {
|
||||
// 内联样式已优化行间距,此处可留空或补充
|
||||
}
|
||||
|
||||
// 基础月份样式(保留原有配置,若无使用可忽略)
|
||||
.monthItem {
|
||||
width: 164px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monthItem.has-data {
|
||||
background-color: #D1E8FF;
|
||||
}
|
||||
|
||||
.monthItem:not(.has-data) {
|
||||
background-color: #EFF3F8;
|
||||
}
|
||||
|
||||
.monthItem.active {
|
||||
border: 2px solid #0B58FF !important;
|
||||
}
|
||||
</style>
|
||||
38
src/views/home/components/inputArea.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
<template>
|
||||
<div class="tableInner">
|
||||
<el-input v-model="list[itemProp]" @blur="changeInput" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputArea',
|
||||
props: {
|
||||
injectData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
itemProp: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: this.injectData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeInput() {
|
||||
console.log(this.list)
|
||||
this.$emit('emitData', this.list)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.tableInner .el-input__inner {
|
||||
border: none;
|
||||
padding: 0;
|
||||
height: 33px;
|
||||
}
|
||||
</style>
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
tableData: [],
|
||||
tableProps: [
|
||||
{ prop: 'name', label: '攻坚指标', align: 'center' },
|
||||
{ prop: 'target', label: '攻坚目标', align: 'center' },
|
||||
{ prop: 'target', label: '攻坚预算', align: 'center' },
|
||||
{ prop: 'monthlyActual', label: '当月实际', align: 'center' },
|
||||
{ prop: 'accumulated', label: '累计', align: 'center', subcomponent: finishDiv },
|
||||
]
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<header class="report-header" :class="['report-header__' + size]">
|
||||
<!-- 左侧区域:标题 -->
|
||||
<div class="left-content">
|
||||
<div class="left-content" :style="{ marginLeft: leftMargin }">
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="text" class="logout-btn" :title="'退出'">
|
||||
<svg-icon style="color: #0B58FF;" icon-class="logout" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native='logout'>退出登录</el-dropdown-item>
|
||||
<el-dropdown-item @click.native='handleToggle'>切换账号</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-button type="text" class="return-btn" :title="'返回'" @click="handleReturn">
|
||||
<svg-icon style="color: #0B58FF;" icon-class="returnIcon" />
|
||||
</el-button>
|
||||
@@ -18,17 +27,12 @@
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div>
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">日期选择</span>
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" value-format="yyyy-MM-dd" :clearable="false" style="width: 132px;height: 29px;"
|
||||
@change="emitTimeRange" />
|
||||
<el-date-picker v-model="date" type="month" placeholder="请选择月份" class="custom-date-picker"
|
||||
value-format="timestamp" :clearable="false" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -36,115 +40,154 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment'; // 引入moment
|
||||
|
||||
import {getPath} from "@/utils/ruoyi";
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' },
|
||||
size: { type: String, default: 'basic' },
|
||||
leftMargin: {
|
||||
type: [String, Number],
|
||||
default: '350px' // 默认值设为350px
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined, // 存储选择的日期(字符串格式:yyyy-MM-dd/yyyy-MM/yyyy)
|
||||
date: Date.now(), // 使用当前时间戳作为初始值
|
||||
activeTime: 1, // 默认月维度(0=日,1=月,2=年)
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
this.date = Date.now();
|
||||
this.emitTimeRange();
|
||||
}
|
||||
},
|
||||
dateData: {
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler(newVal) {
|
||||
console.log('dateData 变化:', newVal);
|
||||
if (newVal && (newVal.startTime || newVal.endTime)) {
|
||||
// 优先使用 startTime,如果没有则使用 endTime
|
||||
const timeStamp = newVal.startTime || newVal.endTime;
|
||||
if (timeStamp && timeStamp !== 0) {
|
||||
console.log('设置日期选择器时间为:', timeStamp);
|
||||
this.date = timeStamp; // 直接使用时间戳
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化默认日期(当前月,格式:yyyy-MM)
|
||||
console.log(this.$router);
|
||||
this.date = moment().format('YYYY-MM');
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
// 初始化默认日期为当前月份
|
||||
console.log('初始化日期选择器,当前时间戳:', this.date);
|
||||
this.$nextTick(() => {
|
||||
this.emitTimeRange();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 清理定时器
|
||||
if (this.timeTimer) {
|
||||
clearInterval(this.timeTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeFullScreen() {
|
||||
this.$emit('screenfullChange');
|
||||
},
|
||||
handleReturn() {
|
||||
console.log(this.$router);
|
||||
|
||||
this.$router.go(-1);
|
||||
console.log('返回上一页');
|
||||
if (this.$router) {
|
||||
this.$router.go(-1);
|
||||
}
|
||||
},
|
||||
exportPDF() {
|
||||
this.$emit('exportPDF');
|
||||
async logout() {
|
||||
this.$modal.confirm('确定注销并退出系统吗?', '提示').then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
}).catch(() => {});
|
||||
},
|
||||
handleToggle() {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 核心方法:用moment计算时间范围(时间戳格式)
|
||||
* 日:选择日00:00:00 → 次日00:00:00
|
||||
* 月:当月1日00:00:00 → 次月1日00:00:00
|
||||
* 年:当年1月1日00:00:00 → 次年1月1日00:00:00
|
||||
* 计算时间范围(时间戳格式)
|
||||
* 固定为月维度:当月1日00:00:00 → 当月最后一天23:59:59
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
// 固定为月维度
|
||||
const mode = 2;
|
||||
// 初始化时间戳为0(兜底值)
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
// 存储目标月份(仅数字,如10、12)
|
||||
let targetMonth = '';
|
||||
|
||||
const targetMoment = this.date
|
||||
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
|
||||
: defaultMoment;
|
||||
try {
|
||||
// 使用 this.date(时间戳)创建 moment 对象
|
||||
let targetMoment = this.date
|
||||
? moment(this.date)
|
||||
: moment(); // 如果 date 无效,使用当前时间
|
||||
|
||||
if (!targetMoment.isValid()) {
|
||||
console.error('无效日期:', this.date);
|
||||
return { startTime, endTime, mode };
|
||||
// 验证日期是否有效
|
||||
if (!targetMoment.isValid()) {
|
||||
console.warn('无效的日期,已使用当前月份:', this.date);
|
||||
targetMoment = moment();
|
||||
}
|
||||
|
||||
// 获取月份(数字格式,1-12)
|
||||
targetMonth = targetMoment.format('M');
|
||||
console.log('当前选择的月份:', targetMonth);
|
||||
|
||||
// 计算当月第一天00:00:00的时间戳
|
||||
startTime = targetMoment.startOf('month').valueOf();
|
||||
// 计算当月最后一天23:59:59的时间戳
|
||||
endTime = targetMoment.clone()
|
||||
.endOf('month')
|
||||
.set({
|
||||
hour: 23,
|
||||
minute: 59,
|
||||
second: 59,
|
||||
millisecond: 0 // 毫秒设为0
|
||||
})
|
||||
.valueOf();
|
||||
|
||||
// 调试输出
|
||||
console.log('月份时间范围计算结果:', {
|
||||
startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
startTimeStamp: startTime,
|
||||
endTimeStamp: endTime,
|
||||
targetMonth: targetMonth
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('计算月份时间范围时出错:', error);
|
||||
}
|
||||
|
||||
// 1. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
// 返回月份相关的所有信息:时间戳、维度、月份值
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
targetMonth
|
||||
};
|
||||
},
|
||||
|
||||
// 传递时间范围给父组件
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
console.log('触发时间范围变化:', timeRange);
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +225,6 @@ export default {
|
||||
/* 左侧标题区域 */
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
margin-left: 350px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -252,7 +294,7 @@ export default {
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 70px;
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
@@ -271,62 +313,60 @@ export default {
|
||||
}
|
||||
|
||||
/* 右侧全屏按钮区域 */
|
||||
.right-content {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 10px;
|
||||
gap: 21px;
|
||||
}
|
||||
.right-content {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
margin-right: 10px;
|
||||
gap: 21px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
// .current-time {
|
||||
// color: #FFFFFF;
|
||||
// font-family: PingFangSC, PingFang SC;
|
||||
// font-weight: 500;
|
||||
// font-size: 22px;
|
||||
// line-height: 24px;
|
||||
// letter-spacing: 1px;
|
||||
// }
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
.home-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.home-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
// margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
.return-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.return-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
// margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日期选择器自定义样式 */
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 132px !important;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 132px !important;
|
||||
width: 165px !important;
|
||||
text-align: center;
|
||||
padding-left: 15px !important;
|
||||
padding-right: 32px !important;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
|
||||
@@ -102,7 +102,7 @@ export default {
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
barWidth: 14,
|
||||
@@ -185,7 +185,7 @@ export default {
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
|
||||
@@ -83,11 +83,11 @@ export default {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
},
|
||||
// min: 0,
|
||||
// max: (value) => Math.ceil((value.max || 0) * 1.1),
|
||||
scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
|
||||
@@ -83,11 +83,11 @@ export default {
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
scale: true,
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
|
||||
@@ -81,11 +81,11 @@ export default {
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
scale: true,
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
// scale: true,
|
||||
//
|
||||
splitNumber: 4,
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ itemList[0].targetValue }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
@@ -31,7 +31,9 @@
|
||||
<div class="yield" style="display: flex;justify-content: space-between;">
|
||||
<div class="progress-percent">完成率</div>
|
||||
<!-- 百分比颜色动态绑定 -->
|
||||
<div class="progress-percent">
|
||||
<div class="progress-percent" :style="{
|
||||
color: getColor(itemList[0].currentValue, itemList[0].targetValue)
|
||||
}">
|
||||
{{ itemList[0].progress }}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,12 +43,12 @@
|
||||
<!-- 循环渲染剩余的item(从索引1开始) -->
|
||||
<div class="item groupData" style="display: flex;padding: 0;" v-for="(item, index) in itemList.slice(1)"
|
||||
:key="index">
|
||||
<!-- 左侧目标值/实际值部分(不变) -->
|
||||
<!-- 左侧预算值/实际值部分(不变) -->
|
||||
<div class="left" style="display: flex;align-items: start;gap: 4px;padding: 12px 0 0 12px;">
|
||||
<div class="groupName">{{ item.unit }}</div>
|
||||
<div class="left-target">
|
||||
<div class="number">{{ item.targetValue }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="left-real">
|
||||
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
|
||||
@@ -231,7 +233,7 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
// 颜色判断核心方法:实际值≥目标值返回绿色,否则返回橙色
|
||||
// 颜色判断核心方法:实际值≥预算值返回绿色,否则返回橙色
|
||||
getColor(currentValue, targetValue) {
|
||||
return currentValue >= targetValue
|
||||
? "rgba(98, 213, 180, 1)"
|
||||
@@ -342,7 +344,7 @@ export default {
|
||||
background-image: url("../../../assets/img/order-item-bg.png");
|
||||
}
|
||||
|
||||
// 实际值 >= 目标值:绿色背景图
|
||||
// 实际值 >= 预算值:绿色背景图
|
||||
.bg-green {
|
||||
background-image: url("../../../assets/img/order-item-greenbg.png");
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
value: 0,
|
||||
proportion: 0,
|
||||
route: 'profitAnalysis',
|
||||
completed: 1 // 未达目标值,不达标
|
||||
completed: 1 // 未达预算值,不达标
|
||||
},
|
||||
{
|
||||
name: "年度",
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
value: 0,
|
||||
proportion: 0,
|
||||
route: 'profitAnalysis',
|
||||
completed: 1 // 超出目标值,达标
|
||||
completed: 1 // 超出预算值,达标
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="legend">
|
||||
<span class="legend-item-line">
|
||||
<span class="line target"></span>
|
||||
目标
|
||||
预算
|
||||
</span>
|
||||
<span class="legend-item-line">
|
||||
<span class="line real"></span>
|
||||
@@ -97,9 +97,9 @@ export default {
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
top: 35,
|
||||
bottom: 20,
|
||||
right: 25,
|
||||
right: 13,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -115,39 +115,28 @@ export default {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
// 这里可以根据需要调整标签的显示方式
|
||||
// width: 38,
|
||||
overflow: 'break',
|
||||
formatter: (value) => {
|
||||
const dateParts = value.split('-'); // ["2025", "07", "01"]
|
||||
if (dateParts.length < 2) return value; // 处理异常格式
|
||||
if (dateParts.length < 2) return value;
|
||||
|
||||
switch (this.dateData.mode) {
|
||||
case 1: // 日模式,显示“月-日”
|
||||
// 确保有日的部分
|
||||
return dateParts.length >= 3
|
||||
? `${dateParts[1]}月${dateParts[2]}日`
|
||||
: `${dateParts[1]}月`; // fallback
|
||||
case 2: // 月模式,显示“月”
|
||||
return `${dateParts[1]}月`;
|
||||
case 3: // 年模式,显示“年”
|
||||
return `${dateParts[0]}年`;
|
||||
default: // 默认月模式
|
||||
return `${dateParts[1]}月`;
|
||||
}
|
||||
// 去掉月份前面的0,然后加上"月"
|
||||
const month = dateParts[1].replace(/^0+/, '');
|
||||
return `${month}月`;
|
||||
}
|
||||
},
|
||||
data: xAxisData // 使用提取出的 X 轴数据
|
||||
data: xAxisData
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 14,
|
||||
align: 'left'
|
||||
},
|
||||
name: '元/㎡',
|
||||
// nameLocation:'center',
|
||||
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'right' },
|
||||
min: 0,
|
||||
// max: function (value) { return Math.ceil(value.max * 1.1); }, // 增加一点余量
|
||||
scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
@@ -189,7 +178,7 @@ export default {
|
||||
data: realData // 使用提取出的 "实际" 数据
|
||||
},
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'line',
|
||||
// stack: 'Total',
|
||||
symbol: 'circle',
|
||||
@@ -197,7 +186,7 @@ export default {
|
||||
lineStyle: {
|
||||
color: 'rgba(98, 213, 180, 1)', // 加深颜色
|
||||
width: 2,
|
||||
type: 'dashed' // 目标线使用虚线
|
||||
// type: 'dashed' // 目标线使用虚线
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgba(98, 213, 180, 1)',
|
||||
@@ -233,11 +222,11 @@ export default {
|
||||
/* (你的样式代码保持不变) */
|
||||
.legend {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -5px;
|
||||
right: 12px;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
/* 使用 flex 布局让两个图例项并排且对齐 */
|
||||
gap: 20px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.legend-item-line {
|
||||
@@ -248,7 +237,7 @@ export default {
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
padding-left: 24px;
|
||||
/* 为圆点和线条留出空间 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="barTop">
|
||||
<div class="title">生产指标趋势</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button" :class="{ active: activeButton === 0 }" @click="activeButton = 0">总成本</div>
|
||||
<div class="item-button" :class="{ active: activeButton === 0 }" @click="activeButton = 0">制造成本</div>
|
||||
<div class="button-line lineOne" v-if="activeButton !== 0 && activeButton !== 1"></div>
|
||||
<div class="item-button" :class="{ active: activeButton === 1 }" @click="activeButton = 1">原片成本</div>
|
||||
<div class="button-line lineTwo" v-if="activeButton !== 1 && activeButton !== 2"></div>
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
selectedChartData() {
|
||||
// 定义按钮索引与lineData中key的映射关系
|
||||
const dataKeyMap = [
|
||||
'总成本',
|
||||
'制造成本',
|
||||
'原片成本',
|
||||
'加工成本',
|
||||
'原片成品率',
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
|
||||
.lineFour {
|
||||
top: 5px;
|
||||
left: 268px;
|
||||
left: 252px;
|
||||
}
|
||||
|
||||
.item-button {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue ||0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
symbolSize: 6
|
||||
},
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 18,
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
return PlaceNames.map(place => {
|
||||
const targetItem = dataSource.find(item => item.name === place);
|
||||
return {
|
||||
targetValue: targetItem?.[`${field}Target`] || 0, // 对应指标的目标值字段(如 profitTarget)
|
||||
targetValue: targetItem?.[`${field}Target`] || 0, // 对应指标的预算值字段(如 profitTarget)
|
||||
value: targetItem?.[field] || 0, // 对应指标的实际值
|
||||
proportion: (targetItem?.[`${field}Proportion`] || 0), // 对应指标的占比(转百分比)
|
||||
completed: targetItem?.[`${field}Completed`] ?? 0 // 状态字段(复用分公司的 completed)
|
||||
|
||||
@@ -118,11 +118,11 @@ export default {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -151,7 +151,7 @@ export default {
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
scale: true,
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
|
||||
@@ -93,11 +93,11 @@ export default {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- 进度条:同步绑定类名 -->
|
||||
<div class="progress-group">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" :style="{ width: item.progress + '%' }" :class="{
|
||||
<div class="progress-bar" :style="{ width: item.progressWidth + '%' }" :class="{
|
||||
'bar-exceed': item.currentValue >= item.targetValue,
|
||||
'bar-below': item.currentValue < item.targetValue
|
||||
}"></div>
|
||||
@@ -42,7 +42,7 @@
|
||||
'percent-exceed': item.currentValue >= item.targetValue,
|
||||
'percent-below': item.currentValue < item.targetValue
|
||||
}">
|
||||
{{ item.progress }}%
|
||||
{{ item.progressDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,71 +54,148 @@
|
||||
export default {
|
||||
name: "Container",
|
||||
components: {},
|
||||
props: ["finance",'dateData'],
|
||||
props: {
|
||||
finance: {
|
||||
type: Object,
|
||||
default: () => ({}) // 明确props默认值为空对象
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// itemList: [
|
||||
// {
|
||||
// name: "营业收入·万元",
|
||||
// targetValue: 16,
|
||||
// currentValue: 17.2, // 大于目标值(绿色)
|
||||
// progress: 107.5,
|
||||
// route: 'operatingRevenue'
|
||||
// },
|
||||
// {
|
||||
// name: "经营性利润·万元",
|
||||
// targetValue: 16,
|
||||
// currentValue: 16, // 等于目标值(绿色)
|
||||
// progress: 100,
|
||||
// route: 'profitAnalysis'
|
||||
// },
|
||||
// {
|
||||
// name: "利润总额·万元",
|
||||
// targetValue: 16,
|
||||
// currentValue: 14.8, // 小于目标值(黄色)
|
||||
// progress: 92.5,
|
||||
// route: 'profitAnalysis'
|
||||
// },
|
||||
// {
|
||||
// name: "毛利率·%",
|
||||
// targetValue: 16,
|
||||
// currentValue: 15.5, // 小于目标值(黄色)
|
||||
// progress: 96.875,
|
||||
// route: 'profitAnalysis'
|
||||
// }
|
||||
// ]
|
||||
// 关键修复1:初始化itemList为空数组(必选,否则初始状态下v-for报错且数据无法响应式更新)
|
||||
itemList: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
finance: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
// 关键修复2:增强数据判断,避免空值/无效值触发错误
|
||||
if (newVal && Object.keys(newVal).length > 0) {
|
||||
// 转换数据并赋值给itemList(响应式更新)
|
||||
this.itemList = this.transformData(newVal);
|
||||
console.log('finance更新,itemList已同步', this.itemList);
|
||||
} else {
|
||||
// 当finance为空时,重置itemList为空数组
|
||||
this.itemList = [];
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
immediate: true, // 组件挂载时立即执行,初始化数据
|
||||
deep: true // 深度监听finance对象内部属性变化
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 解析rate字符串,提取百分比数值
|
||||
// 改进的 parseRateString 方法:能处理数字和字符串类型
|
||||
parseRateString(rateValue) {
|
||||
// 如果是 undefined 或 null,返回默认值
|
||||
if (rateValue === undefined || rateValue === null) {
|
||||
return { displayText: '0%', progressValue: 0 };
|
||||
}
|
||||
|
||||
// 如果是数字类型,直接处理
|
||||
if (typeof rateValue === 'number') {
|
||||
return {
|
||||
displayText: `${rateValue.toFixed(2)}%`,
|
||||
progressValue: Math.min(Math.max(rateValue, 0), 100)
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是字符串类型,使用正则表达式处理
|
||||
if (typeof rateValue === 'string') {
|
||||
// 尝试匹配百分比数字,如"减亏93%"中的93
|
||||
const match = rateValue.match(/(\d+(\.\d+)?)%/);
|
||||
if (match) {
|
||||
const percentValue = parseFloat(match[1]);
|
||||
return {
|
||||
displayText: rateValue,
|
||||
progressValue: Math.min(Math.max(percentValue, 0), 100)
|
||||
};
|
||||
}
|
||||
|
||||
// 如果没有匹配到百分比,尝试解析纯数字
|
||||
const numMatch = rateValue.match(/\d+(\.\d+)?/);
|
||||
if (numMatch) {
|
||||
const numValue = parseFloat(numMatch[0]);
|
||||
return {
|
||||
displayText: rateValue,
|
||||
progressValue: Math.min(Math.max(numValue, 0), 100)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回
|
||||
return {
|
||||
displayText: '0%',
|
||||
progressValue: 0
|
||||
};
|
||||
},
|
||||
|
||||
transformData(rawData) {
|
||||
// 定义指标映射关系,包括名称、对应的数据键和路由
|
||||
const Mapping = [
|
||||
{ key: 'operatingRevenue', name: '营业收入·万元', route: 'operatingRevenue' },
|
||||
{ key: 'operatingIncome', name: '经营性利润·万元', route: 'profitAnalysis' },
|
||||
{ key: 'totalProfit', name: '利润总额·万元', route: 'profitAnalysis' },
|
||||
{ key: 'grossMargin', name: '毛利率·%', route: 'profitAnalysis' }
|
||||
{
|
||||
key: 'operatingRevenue',
|
||||
name: '营业收入·万元',
|
||||
route: '/operatingRevenue/operatingRevenueIndex',
|
||||
isPercentage: true // 需要加%符号
|
||||
},
|
||||
{
|
||||
key: 'operatingIncome',
|
||||
name: '经营性利润·万元',
|
||||
route: '/operatingProfit/operatingProfit',
|
||||
isPercentage: false // 不需要加%符号,使用原始rate字符串
|
||||
},
|
||||
{
|
||||
key: 'totalProfit',
|
||||
name: '利润总额·万元',
|
||||
route: '/totalProfit/totalProfit',
|
||||
isPercentage: false // 不需要加%符号,使用原始rate字符串
|
||||
},
|
||||
{
|
||||
key: 'grossMargin',
|
||||
name: '毛利率·%',
|
||||
route: '/grossMargin/grossMargin',
|
||||
isPercentage: true // 需要加%符号
|
||||
}
|
||||
];
|
||||
|
||||
// 遍历映射关系,转换数据
|
||||
return Mapping.map(mappingItem => {
|
||||
const data = rawData[mappingItem.key] || { rate: 0, real: 0, target: 0 };
|
||||
// 关键修复3:兜底更严谨,避免rawData[mappingItem.key]不存在导致报错
|
||||
const data = rawData[mappingItem.key] || { rate: '0%', real: 0, target: 0 };
|
||||
// 额外兜底:避免data中的属性为undefined
|
||||
const target = data.target || 0;
|
||||
const real = data.real || 0;
|
||||
const rate = data.rate || '0%';
|
||||
|
||||
// 解析rate字符串
|
||||
const parsedRate = this.parseRateString(rate);
|
||||
|
||||
// 进度条宽度:限制在0-100之间
|
||||
const progressWidth = Math.min(Math.max(parsedRate.progressValue, 0), 100);
|
||||
|
||||
// 显示文本处理
|
||||
let progressDisplay;
|
||||
if (mappingItem.isPercentage) {
|
||||
// 对于需要加%的指标,确保有%符号
|
||||
progressDisplay = parsedRate.displayText.includes('%')
|
||||
? parsedRate.displayText
|
||||
: `${parsedRate.displayText}%`;
|
||||
} else {
|
||||
// 对于经营性利润和利润总额,直接使用原始rate字符串
|
||||
progressDisplay = parsedRate.displayText;
|
||||
}
|
||||
|
||||
return {
|
||||
name: mappingItem.name,
|
||||
targetValue: data.target,
|
||||
currentValue: data.real,
|
||||
progress: Math.round(data.rate), // 将小数率转换为百分比并四舍五入
|
||||
targetValue: target,
|
||||
currentValue: real,
|
||||
progressWidth: progressWidth, // 用于进度条宽度
|
||||
progressDisplay: progressDisplay, // 用于显示文本
|
||||
route: mappingItem.route
|
||||
};
|
||||
});
|
||||
@@ -128,9 +205,10 @@ export default {
|
||||
this.$router.push({
|
||||
path: route,
|
||||
query: {
|
||||
// 关键修复4:dateData是对象,需序列化后传递(否则路由query无法正常接收对象)
|
||||
dateData: this.dateData
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +332,7 @@ export default {
|
||||
|
||||
/* 进度条 - 实际值≥目标值(绿色) */
|
||||
.bar-exceed {
|
||||
background:rgba(98, 213, 180, 1) !important;
|
||||
background: rgba(98, 213, 180, 1) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,10 +74,10 @@ export default {
|
||||
transformData(rawData) {
|
||||
// 定义销售指标映射关系(键名、显示名称、单位、路由路径)
|
||||
const saleMapping = [
|
||||
{ key: 'unitPrice', unit: '单价·元/㎡', path: '/cost/cost' },
|
||||
{ key: 'netPrice', unit: '净价·元/㎡', path: '/cost/cost' },
|
||||
{ key: 'sales', unit: '销量·万㎡', path: 'PSIAnal' },
|
||||
{ key: 'panel', unit: '双镀面板·万㎡', path: 'PSIAnal' }
|
||||
{ key: 'unitPrice', unit: '单价·元/㎡', path: '/unitPriceAnalysis/unitPriceAnalysis' },
|
||||
{ key: 'netPrice', unit: '净价·元/㎡', path: '/netPriceAnalysis/netPriceAnalysis' },
|
||||
{ key: 'sales', unit: '销量·万㎡', path: '/salesVolumeAnalysis/salesVolumeAnalysis' },
|
||||
{ key: 'panel', unit: '双镀面板·万㎡', path: '/salesVolumeAnalysis/salesVolumeAnalysis' }
|
||||
];
|
||||
|
||||
// 遍历映射关系,转换数据
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
unit: mappingItem.unit,
|
||||
targetValue: indicatorData.target, // 目标值
|
||||
currentValue: indicatorData.real, // 实际值
|
||||
progress: Math.round(indicatorData.rate * 100), // 完成率(百分比,四舍五入)
|
||||
progress: indicatorData.rate ? Math.round(indicatorData.rate) : 0, // 完成率(百分比,四舍五入)
|
||||
path: mappingItem.path // 路由路径
|
||||
};
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 252px;
|
||||
width: 253px;
|
||||
height: 110px;
|
||||
background: #f9fcff;
|
||||
padding: 12px;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.target }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
@@ -72,28 +72,28 @@ export default {
|
||||
const dataMap = [
|
||||
{
|
||||
key: 'totalCost',
|
||||
unit: '总成本·元/㎡',
|
||||
route: 'cost/cost'
|
||||
unit: '制造成本·元/㎡',
|
||||
route: '/productionCostAnalysis/productionCostAnalysis'
|
||||
},
|
||||
{
|
||||
key: 'rawCost',
|
||||
unit: '原片成本·元/㎡',
|
||||
route: 'cost/cost'
|
||||
route: '/productionCostAnalysis/originalSheetCost'
|
||||
},
|
||||
{
|
||||
key: 'processCost',
|
||||
unit: '加工成本·元/㎡',
|
||||
route: 'cost/cost'
|
||||
route: '/productionCostAnalysis/processingCostAnalysis'
|
||||
},
|
||||
{
|
||||
key: 'rawYield',
|
||||
unit: '原片成品率·%',
|
||||
route: '' // 假设这个没有路由
|
||||
route: '/rawSheetYield/rawSheetYield' // 假设这个没有路由
|
||||
},
|
||||
{
|
||||
key: 'ioYield',
|
||||
unit: '投入产出率·%',
|
||||
route: '' // 假设这个没有路由
|
||||
route: '/inputOutputRatio/inputOutputRatio' // 假设这个没有路由
|
||||
}
|
||||
];
|
||||
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
getColor(index) {
|
||||
const { actual, target, progress } = this.itemList[index];
|
||||
|
||||
// 新增条件:如果实际值、目标值和进度都为0,则显示绿色
|
||||
// 新增条件:如果实际值、预算值和进度都为0,则显示绿色
|
||||
if (actual === 0 && target === 0 && progress === 0) {
|
||||
return "rgba(98, 213, 180, 1)"; // 绿色
|
||||
}
|
||||
@@ -181,10 +181,16 @@ export default {
|
||||
|
||||
.coreItem> :nth-child(4) {
|
||||
grid-area: item4;
|
||||
&:hover {
|
||||
box-shadow: 0px 4px 12px 2px #B5CDE5;
|
||||
}
|
||||
}
|
||||
|
||||
.coreItem> :nth-child(5) {
|
||||
grid-area: item5;
|
||||
&:hover {
|
||||
box-shadow: 0px 4px 12px 2px #B5CDE5;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
@@ -56,16 +56,16 @@ export default {
|
||||
margin-right: -35px;
|
||||
}
|
||||
|
||||
.isChecked {
|
||||
color: #F9FBFE;
|
||||
background: url(../../../assets//img/baseChecked.png) no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
// .isChecked {
|
||||
// color: #F9FBFE;
|
||||
// background: url(../../../assets//img/baseChecked.png) no-repeat;
|
||||
// background-size: cover;
|
||||
// }
|
||||
|
||||
.noChecked {
|
||||
color: rgba(30, 22, 81, 1);
|
||||
background: url(../../../assets//img/baseNoChecked.png) no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
// .noChecked {
|
||||
// color: rgba(30, 22, 81, 1);
|
||||
// background: url(../../../assets//img/baseNoChecked.png) no-repeat;
|
||||
// background-size: cover;
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number"> {{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<!-- 实际值:使用当前item的flag判断颜色 -->
|
||||
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
},
|
||||
// min: 0,
|
||||
// max: (value) => value.max > 0 ? Math.ceil(value.max * 1.1) : 100, // 留10%余量
|
||||
scale: true,
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
series: [
|
||||
// 目标数据柱状图
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 24,
|
||||
|
||||
@@ -90,11 +90,11 @@ export default {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 40,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
symbolSize: 6
|
||||
},
|
||||
{
|
||||
name: '目标',
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="content-wrapper">
|
||||
<!-- 目标值 + 分割线 + 实际值 -->
|
||||
<!-- 预算值 + 分割线 + 实际值 -->
|
||||
<div class="value-group">
|
||||
<div class="value-item">
|
||||
<div class="number">{{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="middle-line"></div>
|
||||
<div class="value-item">
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="content-wrapper">
|
||||
<!-- 目标值 + 分割线 + 实际值 -->
|
||||
<!-- 预算值 + 分割线 + 实际值 -->
|
||||
<div class="value-group">
|
||||
<div class="value-item">
|
||||
<div class="number">{{ item.value }}</div>
|
||||
<!-- <div class="title">目标值</div> -->
|
||||
<!-- <div class="title">预算值</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +130,7 @@ export default {
|
||||
// 更新 ECharts 数据(包含 flag 字段)
|
||||
this.echartData = {
|
||||
locations, // x轴:地名列表
|
||||
target, // 目标值数组
|
||||
target, // 预算值数组
|
||||
value, // 实际值数组
|
||||
proportion,
|
||||
flag
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="content-wrapper">
|
||||
<!-- 目标值 + 分割线 + 实际值 -->
|
||||
<!-- 预算值 + 分割线 + 实际值 -->
|
||||
<div class="value-group">
|
||||
<div class="value-item">
|
||||
<div class="number">{{ item.targetValue }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="middle-line"></div>
|
||||
<div class="value-item">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="value-group">
|
||||
<div class="value-item">
|
||||
<div class="number" style="color: rgba(103, 103, 103, 0.79);">{{ item.target }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="middle-line"></div>
|
||||
<!-- 当前值数字:根据 flag 变色 -->
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="content-wrapper">
|
||||
<!-- 目标值 + 分割线 + 实际值 -->
|
||||
<!-- 预算值 + 分割线 + 实际值 -->
|
||||
<div class="value-group">
|
||||
<div class="value-item">
|
||||
<div class="number">{{ item.targetValue || 0 }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="middle-line"></div>
|
||||
<div class="value-item">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue }}</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条和百分比容器:横向排列 -->
|
||||
@@ -34,12 +34,12 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条和百分比容器:横向排列 -->
|
||||
@@ -61,12 +61,12 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条和百分比容器:横向排列 -->
|
||||
@@ -88,12 +88,12 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条和百分比容器:横向排列 -->
|
||||
@@ -115,12 +115,12 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
<div class="number">16</div>
|
||||
<div class="title">目标值</div>
|
||||
<div class="title">预算值</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条和百分比容器:横向排列 -->
|
||||
|
||||
255
src/views/home/expenseAnalysis/expenseAnalysis.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" top-title="费用分析" :is-full-screen="isFullScreen" @screenfullChange="screenfullChange"
|
||||
@timeRangeChange="handleTimeChange" />
|
||||
<div class="main-body" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:1624px;
|
||||
">
|
||||
<operatingLineChart :dateData="dateData" :monthData="monthData" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<operatingLineChartCumulative :dateData="dateData" :ytdData="ytdData" />
|
||||
<!-- <keyWork /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="centerImg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1; /* 确保在 backp 之上、内容之下 */
|
||||
"></div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
// import operatingSalesRevenue from "./operatingComponents/operatingSalesRevenue";
|
||||
// import premProdStatus from "./components/premProdStatus.vue";
|
||||
import { mapState } from "vuex";
|
||||
import operatingLineChart from "../expenseAnalysisComponents/operatingLineChart";
|
||||
import operatingLineChartCumulative from "../expenseAnalysisComponents/operatingLineChartCumulative.vue";
|
||||
|
||||
import { getExpenseAnalysisGroupData } from '@/api/cockpit'
|
||||
import moment from "moment";
|
||||
export default {
|
||||
name: "DayReport",
|
||||
components: {
|
||||
ReportHeader,
|
||||
operatingLineChartCumulative,
|
||||
operatingLineChart,
|
||||
// premProdStatus,
|
||||
Sidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
value: 100,
|
||||
sort: 1,
|
||||
dateData: {},
|
||||
monthData: {},
|
||||
ytdData: {},
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
getExpenseAnalysisGroupData({
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
sort: this.sort,
|
||||
index: undefined,
|
||||
factory: undefined
|
||||
// timeDim: obj.mode
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
this.monthData = res.data.month
|
||||
this.ytdData = res.data.ytd
|
||||
|
||||
// this.saleData = res.data.SaleData
|
||||
// this.premiumProduct = res.data.premiumProduct
|
||||
// this.salesTrendMap = res.data.salesTrendMap
|
||||
// this.grossMarginTrendMap = res.data.grossMarginTrendMap
|
||||
// this.salesProportion = res.data.salesProportion ? res.data.salesProportion : {}
|
||||
})
|
||||
},
|
||||
handleTimeChange(obj) {
|
||||
console.log(obj, 'obj');
|
||||
this.dateData= obj
|
||||
this.getData()
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
// 导出
|
||||
// exportPDF() {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
330
src/views/home/expenseAnalysis/expenseAnalysisBase.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange" top-title="基地费用分析" :is-full-screen="isFullScreen"
|
||||
@screenfullChange="screenfullChange" />
|
||||
<div class="main-body" style="
|
||||
margin-top: -20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<changeBase :factory="factory" @baseChange="selectChange" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: -20px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 804px 804px;
|
||||
">
|
||||
<monthlyOverview :month="month" :monthData="monthData" :title="'月度概览'" />
|
||||
<totalOverview :ytdData="ytdData" :title="'累计概览'" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 804px 804px;
|
||||
">
|
||||
<monthlyRelatedMetrics :factory="factory" :monthAnalysis="monthAnalysis" :title="'月度·相关指标分析'" />
|
||||
<yearRelatedMetrics :factory="factory" :ytdAnalysis="ytdAnalysis" :title="'累计·相关指标分析'" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<dataTrend @handleChange="handleChange" :trend="trend" :title="'数据趋势'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="centerImg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1; /* 确保在 backp 之上、内容之下 */
|
||||
"></div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
import changeBase from "../components/changeBase.vue";
|
||||
import monthlyOverview from "../expenseAnalysisComponents/monthlyOverview.vue";
|
||||
import totalOverview from "../expenseAnalysisComponents/totalOverview.vue";
|
||||
// import totalOverview from "../operatingComponents/totalOverview.vue";
|
||||
import monthlyRelatedMetrics from "../expenseAnalysisComponents/monthlyRelatedMetrics.vue";
|
||||
import yearRelatedMetrics from "../expenseAnalysisComponents/yearRelatedMetrics.vue";
|
||||
import dataTrend from "../expenseAnalysisComponents/dataTrend.vue";
|
||||
|
||||
import profitLineChart from "../costComponents/profitLineChart.vue";
|
||||
import { mapState } from "vuex";
|
||||
import { getExpenseAnalysisFactoryData } from '@/api/cockpit'
|
||||
// import PSDO from "./components/PSDO.vue";
|
||||
// import psiLineChart from "./components/psiLineChart.vue";
|
||||
|
||||
// import coreBottomLeft from "./components/coreBottomLeft.vue";
|
||||
// import orderProgress from "./components/orderProgress.vue";
|
||||
// import keyWork from "./components/keyWork.vue";
|
||||
import moment from "moment";
|
||||
// import html2canvas from 'html2canvas'
|
||||
// import JsPDF from 'jspdf'
|
||||
export default {
|
||||
name: "DayReport",
|
||||
components: {
|
||||
ReportHeader,
|
||||
changeBase,
|
||||
profitLineChart,
|
||||
monthlyOverview,
|
||||
Sidebar,
|
||||
totalOverview,
|
||||
monthlyRelatedMetrics,
|
||||
yearRelatedMetrics,
|
||||
dataTrend
|
||||
// psiLineChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
month:'',
|
||||
value: 100,
|
||||
dateData: {},
|
||||
index: '总费用',
|
||||
monthData: undefined,
|
||||
ytdData: undefined,
|
||||
monthAnalysis: [],
|
||||
ytdAnalysis: [],
|
||||
trend: [],
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
renderList() {
|
||||
if (this.itemData && this.itemData.length > 0) {
|
||||
return this.itemData;
|
||||
}
|
||||
return this.parentItemList;
|
||||
},
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.factory = this.$route.query.factory ? Number(this.$route.query.factory) : 5
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.index = value
|
||||
this.getData()
|
||||
},
|
||||
getData() {
|
||||
const requestParams = {
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
index: this.index,
|
||||
sort: 1,
|
||||
factory: Number(this.factory)
|
||||
};
|
||||
// 调用接口
|
||||
getExpenseAnalysisFactoryData(requestParams).then((res) => {
|
||||
this.monthData = res.data.month
|
||||
this.ytdData = res.data.ytd
|
||||
this.monthAnalysis = res.data.monthAnalysis
|
||||
this.ytdAnalysis = res.data.ytdAnalysis
|
||||
this.trend = res.data.trend
|
||||
|
||||
// this.monthData = res.data.month
|
||||
});
|
||||
},
|
||||
|
||||
handleTimeChange(obj) {
|
||||
this.month = obj.targetMonth
|
||||
this.dateData = {
|
||||
startTime: obj.startTime,
|
||||
endTime: obj.endTime,
|
||||
// mode: obj.mode,
|
||||
}
|
||||
|
||||
this.getData()
|
||||
},
|
||||
selectChange(data) {
|
||||
console.log('选中的数据:', data);
|
||||
this.factory = data
|
||||
if (this.dateData.startTime && this.dateData.endTime) {
|
||||
this.getData();
|
||||
}
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
changeDate(val) {
|
||||
this.date = val;
|
||||
// this.weekDay = this.weekArr[moment(this.date).format('e')]
|
||||
// this.getData()
|
||||
if (this.date === moment().format("yyyy-MM-DD")) {
|
||||
this.loopTime();
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
},
|
||||
// 导出
|
||||
// () {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
419
src/views/home/expenseAnalysisComponents/Header.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<header class="report-header">
|
||||
<!-- 左侧区域:logo + 标题 -->
|
||||
<div class="left-content">
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="center-content">
|
||||
<!-- 循环 pageRoutes,不再硬编码文字 -->
|
||||
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
|
||||
<span class="item-text">{{ page.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :class="{ 'no-skew': activeIndex === index }
|
||||
" -->
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + label + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<!-- <div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div> -->
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined,
|
||||
activeIndex: -1,
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中“日”)
|
||||
pageRoutes: [
|
||||
{ text: '营业收入', path: '/operatingRevenue/operatingRevenueIndex' },
|
||||
{ text: '利润分析', path: '/profitAnalysis' },
|
||||
{ text: '产销率库存分析', path: '/PSIAnal' },
|
||||
{ text: '成本分析', path: '/cost/cost' },
|
||||
{ text: '驾驶舱报表', path: '/cockpit' }
|
||||
],
|
||||
// 定义时间类型配置:text=按钮文字,pickerType=选择器类型,placeholder=占位符
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 动态获取日期选择器类型
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
// 动态获取日期选择器占位符
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(path, index) {
|
||||
// 1. 跳转到对应路由
|
||||
this.$router.push(path);
|
||||
// 2. 更新activeIndex,让当前点击项高亮
|
||||
this.activeIndex = index;
|
||||
},
|
||||
changeFullScreen() { this.$emit('screenfullChange') },
|
||||
padZero(num) { return num < 10 ? '0' + num : num },
|
||||
/**
|
||||
* 核心方法1:根据维度计算时间区间(首次进入时基于赋值的当月日期,计算“当月第一天0点→次月第一天0点”)
|
||||
* @returns {Object} 包含 start(开始时间)、end(结束时间)、dimension(维度)的区间对象
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
|
||||
const targetMoment = this.date
|
||||
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
|
||||
: defaultMoment;
|
||||
|
||||
if (!targetMoment.isValid()) {
|
||||
console.error('无效日期:', this.date);
|
||||
return { startTime, endTime, mode };
|
||||
}
|
||||
|
||||
// 1. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
},
|
||||
/**
|
||||
* 核心方法2:传递时间区间给父组件(首次进入时触发,传递“当月第一天0点→次月第一天0点”)
|
||||
*/
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"})
|
||||
console.log('当前时间区间:', timeRange);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = this.padZero(now.getMonth() + 1); // 月份从0开始,+1后补零(如1月→01)
|
||||
// 赋值当月默认日期(格式:YYYY-MM,适配month类型选择器)
|
||||
this.date = `${year}-${month}`;
|
||||
// 确保选择器渲染完成后,传递“当月第一天0点→次月第一天0点”的区间
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 原有样式不变,仅补充label文字的倾斜抵消样式 */
|
||||
@font-face {
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.report-header {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: url('../../../assets/img/topTitle.png') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* 确保timeType绝对定位生效 */
|
||||
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
margin-left: 44px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
height: 55px;
|
||||
font-family: "YouSheBiaoTiHei", sans-serif;
|
||||
font-size: 42px;
|
||||
color: #1E1651;
|
||||
line-height: 55px;
|
||||
letter-spacing: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
margin-left: 70px;
|
||||
|
||||
.item {
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
background: #E1EEFC;
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #1E1651;
|
||||
line-height: 50px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 13px 16px 0px rgba(179, 217, 255, 0.43),
|
||||
0px 2px 4px 0px rgba(92, 140, 255, 0.25),
|
||||
inset 0px -43px 13px 0px rgba(255, 255, 255, 0.51);
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
}
|
||||
|
||||
.item.no-skew {
|
||||
background: none !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
color: #1E1651;
|
||||
|
||||
.item-text {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeType {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中,避免元素高低错位 */
|
||||
top: 42px;
|
||||
right:10px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
/* 清除间隙,让按钮与选择器紧密连接 */
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(11, 88, 255, 1);
|
||||
line-height: 28px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timeType .item.no-skew {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-20deg) !important;
|
||||
/* 统一倾斜角度,修复原30deg的错位 */
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(20deg) !important;
|
||||
/* 同步统一文字倾斜角度 */
|
||||
}
|
||||
|
||||
.dateP {
|
||||
position: relative;
|
||||
margin-left: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
/* 与按钮倾斜角度统一(原30deg改为25deg,避免视觉错位) */
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0B58FF;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 补充:label文字抵消倾斜(原代码遗漏,导致文字倾斜) */
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
/* 与label倾斜角度相反,确保文字正立 */
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 4px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
color: #FFFFFF;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日期选择器样式保持不变 */
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 165px !important;
|
||||
text-align: center;
|
||||
padding-left: 15px !important;
|
||||
padding-right: 32px !important;
|
||||
/* 给图标留空间,避免文字被遮挡 */
|
||||
font-size: 14px !important;
|
||||
line-height: 28px !important;
|
||||
/* 行高=输入框高度,文字垂直居中 */
|
||||
color: rgba(237, 245, 253, 1) !important;
|
||||
vertical-align: middle !important;
|
||||
/* 强制文字垂直对齐 */
|
||||
clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%);
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background-color: rgba(11, 88, 255, 1) !important;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 2. 调整图标容器:让图标与文字在同一水平线上 */
|
||||
.el-input__prefix {
|
||||
left: auto !important;
|
||||
right: 8px !important;
|
||||
top: 50% !important;
|
||||
/* 从40%改为50%,基于输入框垂直居中 */
|
||||
transform: translateY(-50%) !important;
|
||||
/* 向上偏移自身50%,精准居中 */
|
||||
display: inline-flex !important;
|
||||
/* 让容器内图标垂直居中 */
|
||||
align-items: center !important;
|
||||
/* 图标在容器内垂直居中 */
|
||||
height: 28px !important;
|
||||
/* 容器高度=输入框高度,避免偏移 */
|
||||
}
|
||||
|
||||
/* 3. 调整图标本身:确保图标大小和对齐方式 */
|
||||
.el-input__icon {
|
||||
color: #ffffff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 28px !important;
|
||||
/* 图标行高=输入框高度,与文字对齐 */
|
||||
vertical-align: middle !important;
|
||||
/* 强制图标垂直对齐 */
|
||||
}
|
||||
|
||||
/* 4. 图标伪类:确保颜色和对齐继承 */
|
||||
.el-icon-date::before {
|
||||
color: #ffffff !important;
|
||||
font-size: 16px !important;
|
||||
line-height: inherit !important;
|
||||
/* 继承父级行高,避免错位 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
271
src/views/home/expenseAnalysisComponents/container.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
|
||||
<div class="content-top" :class="['content-top__' + topSize]">
|
||||
<!-- 使用 flex 容器包裹图标和文字,实现垂直居中 -->
|
||||
<div class="title-wrapper">
|
||||
<svg-icon class="title-icon" :icon-class="icon" />
|
||||
<span class="title-text">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpitContainer-body">
|
||||
<slot>
|
||||
<div class="test-body">something test....</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'size', 'icon', 'topSize'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cockpitContainer {
|
||||
display: inline-block;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.content-top {
|
||||
height: 60px;
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
// width: 547px;
|
||||
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
&__basic {
|
||||
// width: 547px;
|
||||
background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__large {
|
||||
background: url(../../../assets/img/topTitleLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__KFAPTopTitle {
|
||||
background: url(../../../assets/img/KFAPTopTitle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiTopTitleBasic {
|
||||
background: url(../../../assets/img/psiTopTitleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
background: url(../../../assets/img/top-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__bottomBasic {
|
||||
background: url(../../../assets/img/bottom-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingBasic {
|
||||
background: url(../../../assets/img/operating-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingLarge {
|
||||
background: url(../../../assets/img/operating-large.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitTopBasic {
|
||||
background: url(../../../assets/img/profitTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitMiddleBasic {
|
||||
background: url(../../../assets/img/profitMiddleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiBasicBg {
|
||||
background: url(../../../assets/img/psiBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiMiddleBg {
|
||||
background: url(../../../assets/img/psiMiddleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingRevenueBg {
|
||||
background: url(../../../assets/img/operatingRevenueBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__costBasicBg {
|
||||
background: url(../../../assets/img/costBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
// &__left {
|
||||
// background: url(../../../../../../../assets/img/left.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__energyConsumption {
|
||||
// background: url(../../../../../../../assets/img/energyConsumption.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__left2 {
|
||||
// background: url(../../assets/left_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__left3 {
|
||||
// background: url(../../assets/left_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__mid2 {
|
||||
// background: url(../../assets/mid_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__mid3 {
|
||||
// background: url(../../assets/mid_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right1 {
|
||||
// background: url(../../assets/right_1.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right2 {
|
||||
// background: url(../../assets/right_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right3 {
|
||||
// background: url(../../assets/right_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekRight2 {
|
||||
// background: url(../../assets/week_right_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekMidTop {
|
||||
// background: url(../../assets/week-mid-top.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekMidMid {
|
||||
// background: url(../../assets/week-mid-mid.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// background: inherit;
|
||||
/* 设置模糊,不用 filter */
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.container-body {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
298
src/views/home/expenseAnalysisComponents/dataTrend.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="数据趋势" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="right" style="
|
||||
height: 191px;
|
||||
display: flex;
|
||||
width: 1595px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<dataTrendBar @changeItem="handleChange" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import dataTrendBar from "./dataTrendBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, dataTrendBar },
|
||||
props: {
|
||||
trend: {
|
||||
type: Array,
|
||||
// 默认值与实际数据结构一致(12个月)
|
||||
default: () => [
|
||||
// { title: "2025年01月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年02月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年03月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年04月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年05月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年06月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年07月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年08月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年09月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年10月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年11月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年12月", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: {
|
||||
months: [], // 月份数组(2025年01月 - 2025年12月)
|
||||
rates: [], // 每月完成率(百分比)
|
||||
reals: [], // 每月实际值
|
||||
budgets: [],// 每月预算值
|
||||
diffs: [], // 每月差值
|
||||
flags: [] // 每月达标标识(≥100 → 1,<100 → 0)
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
trend: {
|
||||
handler(newVal) {
|
||||
this.processTrendData(newVal);
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
// mounted() {
|
||||
// this.processTrendData(this.trend);
|
||||
// },
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit("handleChange", value);
|
||||
},
|
||||
/**
|
||||
* 处理趋势数据(适配12个月的数组结构)
|
||||
* @param {Array} trendData - 原始趋势数组(12个月)
|
||||
*/
|
||||
processTrendData(trendData) {
|
||||
// 数据兜底:确保是数组且长度为12
|
||||
const validTrend = Array.isArray(trendData)
|
||||
? trendData
|
||||
: {}
|
||||
|
||||
// 初始化空数组
|
||||
const months = [];
|
||||
const rates = [];
|
||||
const reals = [];
|
||||
const budgets = [];
|
||||
const diffs = [];
|
||||
const flags = [];
|
||||
|
||||
// 遍历12个月数据
|
||||
validTrend.forEach(item => {
|
||||
// 基础数据提取(兜底处理)
|
||||
const month = item.title ?? '';
|
||||
const budget = Number(item.budget) || 0;
|
||||
const real = Number(item.real) || 0;
|
||||
const rate = Number(item.rate) || 0;
|
||||
const diff = Number(item.diff) || 0;
|
||||
|
||||
// 计算达标标识(≥100 → 1,<100 → 0)
|
||||
const flag = this.getRateFlag(rate, real, budget);
|
||||
|
||||
// 填充数组
|
||||
months.push(month);
|
||||
rates.push(rate); // 转为百分比并取整
|
||||
reals.push(real);
|
||||
budgets.push(budget);
|
||||
diffs.push(diff);
|
||||
flags.push(flag);
|
||||
});
|
||||
|
||||
// 更新chartData(响应式)
|
||||
this.chartData = {
|
||||
months,
|
||||
rates,
|
||||
reals,
|
||||
budgets,
|
||||
diffs,
|
||||
flags
|
||||
};
|
||||
|
||||
console.log('处理后的趋势数据:', this.chartData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算达标标识
|
||||
* @param {Number} rate - 完成率(原始值,如1.2 → 120%)
|
||||
* @returns {Number} 1: 达标(≥100%),0: 未达标(<100%)
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
// 先处理无效值的情况
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 实际值和目标值都为0时,算作达标
|
||||
if (real === 0 && target === 0 && rate === 0) {
|
||||
return 1; // 达标
|
||||
}
|
||||
|
||||
// 其他情况:rate >= 100 或 rate === 0 时达标
|
||||
return (rate >= 100) ? 1 : 0;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 滚动容器样式 */
|
||||
.scroll-container {
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 10px 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* 设备项样式优化 */
|
||||
.proBarInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 27px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.proBarInfoEqInfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 21px;
|
||||
height: 23px;
|
||||
background: rgba(0, 106, 205, 0.22);
|
||||
backdrop-filter: blur(1.5px);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #68b5ff;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.eq-name {
|
||||
margin-left: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.eqStatus {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
line-height: 18px;
|
||||
text-align: right;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.splitLine {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
border: 1px solid #adadad;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.yield {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: #00ffff;
|
||||
line-height: 18px;
|
||||
text-align: right;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.proBarInfoEqInfoLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proBarInfoEqInfoRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proBarWrapper {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
margin-top: 6px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.proBarLine {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #acacac 100%);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.proBarLineTop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(65deg,
|
||||
rgba(53, 223, 247, 0) 0%,
|
||||
rgba(54, 220, 246, 0.92) 92%,
|
||||
#36f6e5 100%,
|
||||
#37acf5 100%);
|
||||
border-radius: 5px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* 图表相关样式 */
|
||||
.chartImgBottom {
|
||||
position: absolute;
|
||||
bottom: 45px;
|
||||
left: 58px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 57px;
|
||||
bottom: 42px;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #00e8ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局 tooltip 样式 */
|
||||
.production-status-chart-tooltip {
|
||||
background: #0a2b4f77 !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.production-status-chart-tooltip * {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
488
src/views/home/expenseAnalysisComponents/dataTrendBar.vue
Normal file
@@ -0,0 +1,488 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<!-- 新增行容器:包裹“各基地情况”和barTop -->
|
||||
<div class="header-row">
|
||||
<div class="barTop">
|
||||
<!-- 关键:新增右侧容器,包裹图例和按钮组,实现整体靠右 -->
|
||||
<div class="right-container">
|
||||
<div class="legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon line yield"></span>
|
||||
完成率
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square target"></span>
|
||||
预算
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square achieved"></span>
|
||||
实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>
|
||||
实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">类目选择</span>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<div class="item-button profit-btn active" @click.stop="isDropdownShow = !isDropdownShow">
|
||||
<span class="item-text profit-text">{{ selectedProfit || '请选择' }}</span>
|
||||
<span class="dropdown-arrow" :class="{ 'rotate': isDropdownShow }"></span>
|
||||
</div>
|
||||
<div class="dropdown-options" v-if="isDropdownShow">
|
||||
<div class="dropdown-option" v-for="(item, index) in profitOptions" :key="index"
|
||||
@click.stop="selectProfit(item)">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedProfit: null, // 选中的名称,初始为null
|
||||
profitOptions: [
|
||||
'总费用',
|
||||
'管理费用',
|
||||
'销售费用',
|
||||
'财务费用',
|
||||
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// profitOptions() {
|
||||
// return this.categoryData.map(item => item.name) || [];
|
||||
// },
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData
|
||||
},
|
||||
locations() {
|
||||
console.log('this.chartData', this.chartData);
|
||||
|
||||
return this.chartData.months
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
console.log(this.currentDataSource, 'currentDataSource');
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
name: '完成率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1, // 绑定右侧Y轴(需在子组件启用配置)
|
||||
lineStyle: {
|
||||
color: 'rgba(40, 138, 255, .5)',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgba(40, 138, 255, 1)',
|
||||
borderColor: 'rgba(40, 138, 255, 1)',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
|
||||
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rates, // 完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.budgets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: function (params) {
|
||||
const diff = data.diffs || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
// const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' }, // 顶部0px位置:阴影最强
|
||||
// { offset: 0.1, color: 'rgba(205, 215, 224, 0.4)' }, // 1px位置:阴影减弱(对应1px)
|
||||
// { offset: 0.15, color: 'rgba(205, 215, 224, 0.6)' }, // 3px位置:阴影几乎消失(对应3px扩散)
|
||||
{ offset: 0.2, color: '#ffffff' }, // 主体白色
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
// 外阴影:0px 2px 2px 0px rgba(191,203,215,0.5)
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
// 圆角:4px
|
||||
borderRadius: 4,
|
||||
// 移除边框
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
// 文字垂直居中(针对富文本)
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
// 缩小宽度和内边距,适配68px容器
|
||||
width: 'auto', // 自动宽度,替代固定40px
|
||||
padding: [5, 10, 5, 0], // 缩小内边距
|
||||
align: 'center',
|
||||
color: '#464646', // 文字灰色
|
||||
fontSize: 11, // 缩小字体,适配小尺寸
|
||||
lineHeight: 20 // 垂直居中
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(174, 239, 224, 1)' },
|
||||
{ offset: 1, color: 'rgba(118, 218, 190, 1)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
this.selectedProfit = item;
|
||||
this.isDropdownShow = false;
|
||||
this.$emit("changeItem", item);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
// 新增:头部行容器,实现一行排列
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 左右两端对齐
|
||||
align-items: center; // 垂直居中
|
||||
// width: 100%;
|
||||
margin-bottom: 8px; // 与下方图表区保留间距(可根据需求调整)
|
||||
}
|
||||
|
||||
// 各基地情况标题样式
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px; // 保留原有内边距
|
||||
white-space: nowrap; // 防止文字换行
|
||||
}
|
||||
|
||||
.barTop {
|
||||
// 移除原有flex和justify-content,由header-row控制
|
||||
width: auto; // 自适应宽度
|
||||
// 保留原有align-items,确保内部元素垂直居中
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px; // 图例项之间间距,避免重叠
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap; // 防止图例文字换行
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// 图例颜色
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
margin: 0;
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: left;
|
||||
|
||||
&.active {
|
||||
background: #3071ff;
|
||||
color: rgba(249, 252, 255, .8);
|
||||
}
|
||||
|
||||
.profit-text {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid currentColor;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.rotate {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 2px;
|
||||
width: 123px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.dropdown-option {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
color: #3071ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
src/views/home/expenseAnalysisComponents/electricityGauge.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="chartBox" style="width: 100%; height: 108px; position: relative;">
|
||||
<div :id="id" style="width: 100%; height:100%;"></div>
|
||||
<div class="bottomTip">
|
||||
<div class="precent">
|
||||
<span class="precentNum">{{ detailData.rate || 0 }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'EnergyConsumption',
|
||||
// components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// electricComu: 0,
|
||||
// steamComu: 20, // 调整为符合max范围的数值(0-8)
|
||||
// // electricity: [120, 150, 130, 180, 160, 200, 190],
|
||||
// // steam: [80, 95, 85, 110, 100, 120, 115],
|
||||
// // dates: ['1日', '2日', '3日', '4日', '5日', '6日', '7日']
|
||||
})
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => ('monthG')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// electricityChart: null,
|
||||
// steamChart: null,
|
||||
// specialTicks: [2, 4, 6, 8], // 统一的刻度显示
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detailData: {
|
||||
deep: true,
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler() {
|
||||
this.updateGauges()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initGauges()
|
||||
// window.addEventListener('resize', this.handleResize)
|
||||
this.observeContainerResize()
|
||||
},
|
||||
methods: {
|
||||
observeContainerResize() {
|
||||
// 修复:获取正确的容器(组件内的.gauge-container)
|
||||
const container = this.$el.querySelector('.gauge-container')
|
||||
if (container && window.ResizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
if (this.electricityChart) {
|
||||
this.electricityChart.resize() // 直接触发resize,无需防抖
|
||||
}
|
||||
})
|
||||
this.resizeObserver.observe(container)
|
||||
}
|
||||
},
|
||||
initGauges() {
|
||||
// console.log('this.id',this.id);
|
||||
|
||||
// 初始化电气图表实例
|
||||
const electricityDom = document.getElementById(this.id)
|
||||
if (electricityDom) {
|
||||
// 修复:正确创建并存储图表实例
|
||||
this.electricityChart = echarts.init(electricityDom)
|
||||
// 首次更新数据
|
||||
this.updateGauges()
|
||||
}
|
||||
// 蒸汽图表若未使用,可注释/删除
|
||||
// const steamDom = document.getElementById('steamGauge')
|
||||
// if (steamDom) {
|
||||
// this.steamChart = echarts.init(steamDom)
|
||||
// }
|
||||
},
|
||||
updateGauges() {
|
||||
// 修复:先判断实例是否存在,再更新配置
|
||||
if (!this.electricityChart) return
|
||||
|
||||
// 修复:兜底获取rate值,确保数值有效
|
||||
const rate = Number(this.detailData?.rate) || 0
|
||||
console.log('当前rate值:', rate); // 调试:确认rate值正确
|
||||
|
||||
// 关键:第二个参数传true,清空原有配置,强制更新
|
||||
this.electricityChart.setOption(this.getElectricityGaugeOption(rate), true)
|
||||
},
|
||||
// 用电量仪表盘配置(保留原有样式,优化数值范围)
|
||||
getElectricityGaugeOption(value) {
|
||||
const electricityGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#0B58FF' },
|
||||
{ offset: 1, color: '#32FFCD' }
|
||||
])
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
name: '月度',
|
||||
type: 'gauge',
|
||||
radius: '95', // 修复:添加%,避免数值错误
|
||||
center: ['50%', '90%'],
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
label: { show: false },
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
width: 14,
|
||||
itemStyle: { color: electricityGradient }
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
|
||||
length: '75%',
|
||||
width: 16,
|
||||
itemStyle: { color: '#288AFF' },
|
||||
offsetCenter: [0, '10%']
|
||||
},
|
||||
axisLine: {
|
||||
roundCap: true,
|
||||
lineStyle: { width: 12, color: [[1, '#E6EBF7']] }
|
||||
},
|
||||
splitLine: {
|
||||
length: 10,
|
||||
lineStyle: { width: 5, color: '#D6DAE5' },
|
||||
},
|
||||
axisTick: {
|
||||
splitNumber: 2,
|
||||
length: 6,
|
||||
lineStyle: { width: 2, color: '#D6DAE5' }
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
detail: { show: false },
|
||||
data: [{ value: value, unit: '' }] // 确保数值正确传入
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// 未使用的蒸汽仪表盘可注释/删除
|
||||
// getSteamGaugeOption(value) { ... }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.chartBox {
|
||||
.bottomTip {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
.precent {
|
||||
line-height: 3px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用电量线条颜色
|
||||
.precent::before {
|
||||
|
||||
background: linear-gradient(90deg, rgba(40, 138, 255, 0) 0%, rgba(12, 125, 254, 0.4) 100%);
|
||||
}
|
||||
|
||||
/* ::after:从 透明 到 rgba(12, 125, 254, 0.4)(90度渐变,左到右) */
|
||||
.precent::after {
|
||||
background: linear-gradient(90deg, rgba(12, 125, 254, 0.4) 0%, rgba(40, 138, 255, 0) 100%);
|
||||
}
|
||||
|
||||
// 蒸汽线条颜色
|
||||
// .steam-precent::after,
|
||||
// .steam-precent::before {
|
||||
// background: linear-gradient(90deg, rgba(11, 168, 255, 0.26) 0%, rgba(54, 239, 230, 1) 100%);
|
||||
// }
|
||||
|
||||
.precentNum {
|
||||
display: inline-block;
|
||||
// width: 52px;
|
||||
height: 22px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #0B58FF;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// 蒸汽数字颜色
|
||||
.steam-num {
|
||||
color: rgba(54, 239, 230, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||