Compare commits

...

46 Commits

Author SHA1 Message Date
d8abdfbfec bug修改 2026-04-14 15:27:06 +08:00
446078f558 修改bug 2026-04-14 13:54:05 +08:00
7135ab0e4b 达标函数删除,图表label置于最顶层 2026-04-14 10:17:38 +08:00
0d74e762ce 预算和指标填报上传样式修改 2026-04-10 16:00:39 +08:00
24102f0d0d 预算和指标填报上传样式修改 2026-04-10 10:47:58 +08:00
bdd73b8868 预算和指标填报 2026-04-10 08:41:39 +08:00
0c8bd440ae 制造成本分析接口+单位修改 2026-04-08 15:26:20 +08:00
ed0fd63474 应收账款等4个新增页面 2026-04-03 16:14:28 +08:00
9d91188b98 营业收入-全成本分析页面修改 2026-04-03 09:10:04 +08:00
f0ac88af3d 驾驶舱&基础信息配置 2026-04-01 15:04:37 +08:00
161d6a1bdf 运营驾驶舱对接 2026-03-31 15:13:13 +08:00
05fe91618c 运营驾驶舱修改部分 2026-03-27 11:12:13 +08:00
b85ceb2542 merge B 2026-03-27 10:06:27 +08:00
9b0a768216 指标和预算填报删除指标类型 2026-03-27 08:36:02 +08:00
e770dc4fed 制造成本分析修改 2026-03-26 09:52:35 +08:00
4f7466bb29 制造成本分析修改 2026-03-25 14:10:27 +08:00
bb66f97b95 修改集团及基地图表样式 2026-03-20 16:13:53 +08:00
ee4fdbd45b 修改icon提示 2026-03-20 10:25:15 +08:00
5465a43bcc 去掉默认的账密 2026-03-19 09:13:27 +08:00
2465f89d26 集团页面默认值 2026-03-16 15:16:16 +08:00
09c7fd1f63 修复切换展示顺序,完成率不一起切换的问题 2026-03-13 15:42:39 +08:00
195979b3e0 需改样式,图表右侧缩进 2026-03-13 14:47:54 +08:00
0e6caec8d8 样坏修改 2026-03-13 14:03:58 +08:00
acc327212e 修改 2026-03-13 10:11:42 +08:00
dd1f639c77 集团页面左侧图样式修改 2026-03-13 10:02:23 +08:00
b36acbf1e6 制造成本分析下所有单项页面修改 2026-03-13 09:44:43 +08:00
660bc4b58b 修改bug 2026-03-12 15:07:18 +08:00
2a316f89c6 修改禅道bug 2026-03-11 15:59:57 +08:00
4d3b2b13b8 驾驶舱报表&指标填报&接口添加loading 2026-03-11 09:54:39 +08:00
1d5af53e1a 权限跳转修改+预算填报修改 2026-03-10 08:53:03 +08:00
d379d7bb5b 预算填报页面添加字段 2026-03-09 15:35:58 +08:00
418c29095b 页面权限 2026-03-09 15:02:09 +08:00
ef230b3836 登录后跳转到打标页面 2026-03-06 09:28:06 +08:00
9f2f7036fd 增加退出登录 2026-03-05 16:09:07 +08:00
2f3586e2f2 预算&指标填报 2026-03-05 11:12:34 +08:00
‘937886381’
babbe98c09 xiugai 2026-01-12 16:07:02 +08:00
‘937886381’
b491f24126 修改 2026-01-09 17:53:34 +08:00
‘937886381’
b76f3bfd37 修改 2026-01-07 16:47:49 +08:00
‘937886381’
51e66cf6e1 修改 2026-01-06 17:09:52 +08:00
‘937886381’
5605eeab06 xiugai 2026-01-06 13:48:11 +08:00
‘937886381’
20ef2b9763 修改 2025-12-30 19:36:05 +08:00
‘937886381’
7b3873f9ea 修改 2025-12-30 09:04:48 +08:00
‘937886381’
80deffbb42 新增页面 2025-12-25 16:48:17 +08:00
‘937886381’
2d200dd1a6 修改 2025-12-09 16:55:39 +08:00
‘937886381’
a907c7273e 修改 2025-11-25 09:30:51 +08:00
‘937886381’
694beb5851 修改 2025-11-24 14:10:46 +08:00
619 changed files with 129381 additions and 4421 deletions

View File

@@ -5,8 +5,18 @@ ENV = 'development'
VUE_APP_TITLE = 洛玻集团驾驶舱
# 芋道管理系统/开发环境
VUE_APP_BASE_API = 'http://172.16.32.18:7070'
# VUE_APP_BASE_API = 'http://192.168.0.35:7070'
# 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.20.218:7070'
# 小田
# VUE_APP_BASE_API = 'http://172.16.19.232:7070'
# 测试
VUE_APP_BASE_API = 'http://192.168.0.35:8080'
# 闫阳
# VUE_APP_BASE_API = 'http://172.16.19.131:7070'
# 路由懒加载
@@ -22,4 +32,4 @@ VUE_APP_CAPTCHA_ENABLE = true
VUE_APP_DOC_ENABLE = true
# 百度统计
VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab
# VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab

View File

@@ -1,16 +1,20 @@
# 生产环境配置
NODE_ENV = 'production'
ENV = 'production'
# 页面标题
VUE_APP_TITLE = 洛玻集团驾驶舱
# 芋道管理系统/生产环境
VUE_APP_BASE_API = 'http://192.168.0.35:7070'
# VUE_APP_BASE_API = '/prod-api'
VUE_APP_BASE_API = ''
# 根据服务器或域名修改
# PUBLIC_PATH = 'http://192.168.0.35:7070'
# PUBLIC_PATH = 'http://my-pi.com:8888/yudao-admin/'
# PUBLIC_PATH = 'http://192.168.0.33:8888/'
PUBLIC_PATH = ''
# 二级部署路径
# VUE_APP_APP_NAME ='yudao-admin'
VUE_APP_APP_NAME ='yudao-admin'
# 多租户的开关
VUE_APP_TENANT_ENABLE = true

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ selenium-debug.log
*.local
package-lock.json
sync_luobo.bat

View File

@@ -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",

View File

@@ -5,6 +5,10 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Cache" content="no-cache">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
<!--[if lt IE 11]><script>window.location.href='html/ie.html';</script><![endif]-->

109
src/api/basicInfoConfig.js Normal file
View File

@@ -0,0 +1,109 @@
import request from "@/utils/request";
// 产品信息
export function getProductConfigPage(data) {
return request({
url: "/lb/product-config/page",
method: "get",
params: data,
});
}
export function getProductConfig(data) {
return request({
url: "/lb/product-config/get",
method: "get",
params: data,
});
}
export function addProductConfig(data) {
return request({
url: "/lb/product-config/create",
method: "post",
data: data,
});
}
export function updateProductConfig(data) {
return request({
url: "/lb/product-config/update",
method: "put",
data: data,
});
}
export function delProductConfig(data) {
return request({
url: "/lb/product-config/delete",
method: "delete",
params: data,
});
}
// 集团重点工作
export function getImportantWorkConfigPage(data) {
return request({
url: "/lb/important-work-config/page",
method: "get",
params: data,
});
}
export function getImportantWorkConfig(data) {
return request({
url: "/lb/important-work-config/get",
method: "get",
params: data,
});
}
export function addImportantWorkConfig(data) {
return request({
url: "/lb/important-work-config/create",
method: "post",
data: data,
});
}
export function updateImportantWorkConfig(data) {
return request({
url: "/lb/important-work-config/update",
method: "put",
data: data,
});
}
export function delImportantWorkConfig(data) {
return request({
url: "/lb/important-work-config/delete",
method: "delete",
params: data,
});
}
// 客户信息配置
export function getCustomerConfigPage(data) {
return request({
url: "/lb/customer-config/page",
method: "get",
params: data,
});
}
export function getCustomerConfig(data) {
return request({
url: "/lb/customer-config/get",
method: "get",
params: data,
});
}
export function addCustomerConfig(data) {
return request({
url: "/lb/customer-config/create",
method: "post",
data: data,
});
}
export function updateCustomerConfig(data) {
return request({
url: "/lb/customer-config/update",
method: "put",
data: data,
});
}
export function delCustomerConfig(data) {
return request({
url: "/lb/customer-config/delete",
method: "delete",
params: data,
});
}

View File

@@ -43,3 +43,359 @@ export function getAccountSumaryPage(data) {
params: data,
});
}
export function getProfitImpactList(data) {
return request({
url: "/lb/cost-analysis/profitImpactList",
method: "post",
data: data,
});
}
export function getSalesRevenueData(data) {
return request({
url: "lb/sales-revenue/getData",
method: "post",
data: data,
});
}
export function getOperateCockpit(data) {
return request({
url: "/lb/operate-cockpit/get",
method: "post",
data: data,
});
}
export function getOrderDetail(data) {
return request({
url: "/lb/operate-cockpit/getOrderDetail",
method: "post",
data: 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 importTemplateYS(data) {
return request({
url: '/lb/index-target-month/export',
method: "post",
data: data,
responseType: 'blob'
})
}
// 指标填报模板下载/导出
export function importTemplateZB(data) {
return request({
url: '/lb/index-real-month/export',
method: "post",
data: data,
responseType: 'blob'
})
}
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,
});
}
export function getAccountsReceivableData(data) {
return request({
url: "/lb/accounts-receivable/getGroupData",
method: "post",
data: data,
});
}
export function getInventoryData(data) {
return request({
url: "/lb/inventory/getGroupData",
method: "post",
data: data,
});
}
export function getElectricityCostAnalysisData(data) {
return request({
url: "/lb/electricity-cost-analysis/getGroupData",
method: "post",
data: data,
});
}
export function getDepreciationAnalysisData(data) {
return request({
url: "/lb/depreciation-analysis/getGroupData",
method: "post",
data: data,
});
}
export function getElectricityCostAnalysisFData(data) {
return request({
url: "/lb/electricity-cost-analysis/getFactoryData",
method: "post",
data: data,
});
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,6 +1,6 @@
<?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>
<title>全屏</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="01-运营驾驶舱" transform="translate(-1864.000000, -12.000000)" fill="#0B58FF">
<g id="icon/可视化/全屏" transform="translate(1864.000000, 12.000000)">

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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

View 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

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1763973887412" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5048" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M538.622846 950.291255c257.534448-171.658591 308.738643-462.119963-75.504648-453.027887l0 225.514173L121.893676 381.529483 463.118198 40.281425l0 220.701564c218.259956-5.67321 470.02705 96.270601 470.02705 280.69596C933.146271 813.119105 768.337691 874.762047 538.622846 950.291255" fill="#1296db" p-id="5049"></path></svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@@ -0,0 +1,24 @@
<?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="页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon和插图" transform="translate(-339.000000, -683.000000)" fill-rule="nonzero">
<g id="检测数据" transform="translate(339.000000, 683.000000)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
<g id="编组-5" transform="translate(1.358796, 1.358796)">
<path d="M14.6412037,0 C6.56329821,0 0,6.56329821 0,14.6412037 C0,22.7191092 6.56329821,29.2824074 14.6412037,29.2824074 C22.7191092,29.2824074 29.2824074,22.7191092 29.2824074,14.6412037 C29.2824074,6.56329821 22.7191092,0 14.6412037,0 Z" id="形状" fill="#ECF5FE"></path>
<g id="烧瓶,实验,化学,科学" transform="translate(4.362930, 5.001581)">
<rect id="矩形" fill="#000000" opacity="0" x="1.95940783" y="1.320757" width="16.6377315" height="16.6377315"></rect>
<g>
<rect id="矩形" fill="#000000" opacity="0" x="2.83308197" y="2.32185598" width="14.8903832" height="14.8903832"></rect>
<g id="还原画布" fill="#3E6AF7">
<rect id="矩形" opacity="0" x="1.23578458" y="0.724558589" width="18.084978" height="18.084978"></rect>
<path d="M17.6932207,6.74823569 L13.2296336,6.74823569 C13.0215873,6.74823569 12.8513571,6.54528408 12.8513571,6.29724746 L12.8513571,1.97232623 C12.8513571,1.72428962 13.0215542,1.521338 13.2296171,1.521338 L17.6932207,1.521338 C17.901267,1.521338 18.0714972,1.72428962 18.0714972,1.97232623 L18.0714972,6.29722771 C18.0714972,6.54526433 17.9031719,6.74823569 17.6932207,6.74823569 L17.6932207,6.74823569 Z M6.87068348,17.3028361 L2.40704671,17.3028361 C2.19900041,17.3028361 2.02877016,17.0998845 2.02877016,16.8518479 L2.02877016,12.5269266 C2.02877016,12.27889 2.19900041,12.0759384 2.40704671,12.0759384 L6.87068348,12.0759384 C7.07872978,12.0759384 7.24896003,12.2788703 7.24896003,12.5269266 L7.24896003,16.8518479 C7.24896003,17.1021358 7.07872978,17.3028361 6.87068348,17.3028361 Z M20.4661601,11.5941637 L19.1062728,9.97893922 C19.0135961,9.86923455 18.8717403,9.82573697 18.7412476,9.85222312 C18.680722,9.86356959 18.6239731,9.89194406 18.574794,9.93355332 L16.8858257,11.3558546 C16.7269585,11.4901405 16.7061539,11.7284496 16.8404398,11.8892217 C16.9160885,11.9781052 17.0220165,12.0235076 17.1298163,12.0235076 C17.2149231,12.0235076 17.3019183,11.9951332 17.3738069,11.9346076 L18.4537759,11.0267571 C18.4518711,11.0418803 18.4518711,11.0551316 18.4518711,11.0702547 C18.4518711,13.3493556 17.7350555,15.2804262 16.3789447,16.6535483 C15.0077109,18.0437151 13.0596123,18.7775586 10.7445837,18.7775586 C10.5365374,18.7775586 10.3663072,18.9477723 10.3663072,19.1558352 C10.3663072,19.3638981 10.5365208,19.5340952 10.7445837,19.5340952 C13.2657703,19.5340952 15.3992222,18.7227146 16.9160885,17.1850271 C17.6556136,16.4360439 18.2287016,15.5319866 18.6183247,14.4993084 C18.9890314,13.5157926 19.1857312,12.4301421 19.2065193,11.2707477 L19.8912003,12.0840332 C19.9668656,12.1729167 20.072777,12.2183192 20.1805768,12.2183192 C20.2657002,12.2183192 20.3526954,12.1899447 20.4245674,12.1294191 C20.5815463,11.9932449 20.602351,11.7549358 20.4661767,11.5941637 L20.4661601,11.5941637 Z M9.98803061,0 C8.67730576,0 7.47441384,0.202364785 6.41524944,0.603334282 C5.37500136,0.996733941 4.47470417,1.57928016 3.74086058,2.33204005 C2.34880553,3.76191116 1.58658748,5.7724236 1.52796679,8.16311747 L0.622004647,7.3990111 C0.461232559,7.2647086 0.222923469,7.28551323 0.0886375303,7.444397 C-0.0456484081,7.6032642 -0.0248437777,7.84347818 0.134023428,7.97776411 L1.82300826,9.40006538 C1.88255245,9.4508878 1.95677081,9.48136671 2.03484776,9.48706054 C2.04619423,9.48706054 2.05565239,9.48896542 2.06699887,9.48896542 C2.17481522,9.48896542 2.28261501,9.44357953 2.35637537,9.35466292 L3.71626275,7.73945502 C3.85054869,7.58057125 3.82974406,7.34037384 3.67087685,7.20608791 C3.51200965,7.07180197 3.27179567,7.0926066 3.13750974,7.2514738 L2.28263157,8.26714062 C2.32044763,6.0334422 3.00890532,4.16667386 4.28178097,2.86163053 C4.93808755,2.18640764 5.74570807,1.66627531 6.681933,1.3125966 C7.65597399,0.943778206 8.76809412,0.758424851 9.98803061,0.758424851 C10.1960769,0.758424851 10.3663072,0.588211171 10.3663072,0.380164867 C10.3663072,0.172118563 10.1979652,0 9.98803061,0 Z" id="形状" stroke="#3E6AF7" stroke-width="0.7"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,6 +1,6 @@
<!--?xml version="1.0" encoding="UTF-8"?-->
<svg width="100%" height="100%" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
<title>icon/可视化/退出全屏</title>
<title>退出全屏</title>
<g id="00首页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1802.000000, -40.000000)" fill="#0B58FF" id="icon/可视化/退出全屏">
<g transform="translate(1802.000000, 40.000000)">

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
src/assets/img/labelBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 680 KiB

BIN
src/assets/img/topArrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -143,7 +143,7 @@ h6 {
.pagination-container .el-pagination {
right: 0;
position: absolute;
position: absolute !important;
}
@media (max-width: 768px) {

View File

@@ -9,7 +9,7 @@
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<img :src="require(`../../assets/images/choicepart/avatar.png`)" class="user-avatar">
<span v-if="nickname" class="user-nickname">{{ nickname }}</span>
<i class="el-icon-caret-bottom" />
</div>
@@ -188,7 +188,7 @@ export default {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
top: 18px;
font-size: 12px;
}
}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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";
// 自定义表格工具扩展

View File

@@ -8,6 +8,48 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
// 从菜单树中查找第一个可见叶子菜单路径(按接口返回顺序)
function findFirstLeafPathFromMenus(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.children && item.children.length > 0) {
const found = dfs(item.children, currentPath)
if (found != null) return found
} else {
return currentPath
}
}
return null
}
return dfs(menus)
}
function findFirstLeafPathFromRoutes(routes) {
if (!Array.isArray(routes) || routes.length === 0) return null
const stack = [...routes]
while (stack.length) {
const route = stack.shift()
if (!route || route.hidden === true) continue
if (Array.isArray(route.children) && route.children.length > 0) {
stack.unshift(...route.children)
continue
}
const p = route.path || ''
if (typeof p === 'string' && p.startsWith('/') && p !== '/404') {
return p
}
}
return null
}
// 增加三方登陆 update by 芋艿
const whiteList = ['/login', '/social-login', '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee']
@@ -17,21 +59,35 @@ 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) {
isRelogin.show = true
// 获取字典数据 add by 芋艿
store.dispatch('dict/loadDictDatas')
// 获取部门权限
store.dispatch('GetLevel')
// 判断当前用户是否已拉取完 user_info 信息
store.dispatch('GetInfo').then(userInfo => {
isRelogin.show = false
// 触发 GenerateRoutes 事件时,将 menus 菜单树传递进去
store.dispatch('GenerateRoutes', userInfo.menus).then(accessRoutes => {
store.dispatch('GenerateRoutes', userInfo.menus).then(accessRoutes => {
// 根据 roles 权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
const menuDefaultPath = findFirstLeafPathFromMenus(userInfo.menus)
const routeFallbackPath = findFirstLeafPathFromRoutes(accessRoutes)
const defaultPath = menuDefaultPath || routeFallbackPath || '/'
store.dispatch('SetDefaultPath', defaultPath)
// 仅当目标为根路径 '/' 时跳默认页;否则保持当前路径(含刷新场景),避免刷新被误判为“未匹配”而跳到默认页
if (to.path === '/') {
const matched = router.match(defaultPath)
const hasMatch = matched && Array.isArray(matched.matched) && matched.matched.length > 0
const safePath = hasMatch ? defaultPath : (routeFallbackPath || '/')
next({ path: safePath, replace: true })
} else {
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成当前页刷新时保留 to 的路径
}
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
@@ -40,7 +96,12 @@ router.beforeEach((to, from, next) => {
})
})
} else {
next()
if (to.path === '/') {
const defaultPath = store.getters.defaultPath || '/'
next({ path: defaultPath, replace: true })
} else {
next()
}
}
}
} else {

View File

@@ -64,13 +64,13 @@ export const constantRoutes = [
component: (resolve) => require(["@/views/error/401"], resolve),
hidden: true,
},
{
path: "/",
// component: () => import('@/views/choicePart'),
component: () => import("@/views/home"),
hidden: true,
meta: { requireToken: true },
},
// {
// path: "/",
// // component: () => import('@/views/choicePart'),
// component: () => import("@/views/home"),
// hidden: true,
// meta: { requireToken: true },
// },
// {
// path: "/operatingRevenue",
// // component: () => import('@/views/choicePart'),

View File

@@ -17,7 +17,10 @@ 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
dict_datas: state => state.dict.dictDatas,
// 部门层级
levelList: state => state.user.levelList
}
export default getters

View File

@@ -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)
@@ -48,6 +52,9 @@ const permission = {
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
},
SetDefaultPath({commit}, path) {
commit('SET_DEFAULT_PATH', path)
}
}
}
@@ -76,6 +83,20 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
}
}
const needBlankLayout = [
"/operatingRevenue",
"/totalProfit",
"/operatingProfit",
"/expenseAnalysis",
"/grossMargin",
"/inputOutputRatio",
"/rawSheetYield",
"/productionCostAnalysis",
"/unitPriceAnalysis",
"/netPriceAnalysis",
"/salesVolumeAnalysis",
'/procurementGainAnalysis',
'/fullCostAnalysis',
'/electricityCostAnalysis',
"/cost", // cost 根路由
"/cost/profitImpactAnalysis", // cost 子菜单(完整路径)
];

View File

@@ -1,5 +1,6 @@
import {login, logout, getInfo, socialLogin, smsLogin} from '@/api/login'
import {setToken, removeToken} from '@/utils/auth'
import {getLevelStruc} from '@/api/cockpit'
const user = {
state: {
@@ -7,7 +8,8 @@ const user = {
name: '',
avatar: '',
roles: [],
permissions: []
permissions: [],
levelList:[]
},
mutations: {
@@ -28,6 +30,9 @@ const user = {
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_LEVEL_LIST: (state, levelList) => {
state.levelList = levelList
}
},
@@ -122,7 +127,21 @@ const user = {
})
})
},
// 获取层级
GetLevel({ commit, state }) {
return new Promise((resolve, reject) => {
getLevelStruc().then(res => {
// 如果未加载到数据,则直接返回
if (!res || !res.data) {
return;
}
commit('SET_LEVEL_LIST', res.data)
resolve()
})
}).catch(error => {
reject(error)
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {

View File

@@ -81,6 +81,12 @@ 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',
PROCESS:'process',
IMPORTANT_WORK_METHOD:'important_work_method',
TARGET_PROPERTY:'target_property'
}
/**
@@ -139,3 +145,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]
}
}

View File

@@ -1,5 +1,5 @@
import axios from 'axios'
import {Message, MessageBox, Notification} from 'element-ui'
import {Message, MessageBox, Notification, Loading} from 'element-ui'
import store from '@/store'
import {getAccessToken, getRefreshToken, getTenantId, setToken, getVisitTenantId} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
@@ -31,8 +31,36 @@ const service = axios.create({
// 禁用 Cookie 等信息
withCredentials: false,
})
let loadingInstance = null
function startLoading() {
loadingInstance = Loading.service({
fullscreen: false,
text: '拼命加载中...',
background: 'rgba(0, 0, 0, 0.1)'
})
}
function endLoading() {
loadingInstance.close()
}
let needLoadingRequestCount = 0
function showFullScreenLoading() {
if (needLoadingRequestCount === 0) {
startLoading()
}
needLoadingRequestCount++
}
function tryHideFullScreenLoading() {
if (needLoadingRequestCount <= 0) return
needLoadingRequestCount--
if (needLoadingRequestCount === 0) {
endLoading()
}
}
// request拦截器
service.interceptors.request.use(config => {
showFullScreenLoading()
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getAccessToken() && !isToken) {
@@ -88,12 +116,14 @@ service.interceptors.request.use(config => {
}
return config
}, error => {
tryHideFullScreenLoading()
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(async res => {
tryHideFullScreenLoading()
let { data } = res
// 未设置状态码则默认成功状态
// 二进制数据则直接返回,例如说 Excel 导出
@@ -202,6 +232,7 @@ service.interceptors.response.use(async res => {
}
}, error => {
console.log('err' + error)
tryHideFullScreenLoading()
let {message} = error;
if (message === "Network Error") {
message = "后端接口连接异常";

View File

@@ -17,8 +17,8 @@
<div class="bullshit__info">
对不起您正在寻找的页面不存在尝试检查URL的错误然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容
</div>
<router-link to="/" class="bullshit__return-home">
返回
<router-link to="/login" class="bullshit__return-home">
返回登录
</router-link>
</div>
</div>

View File

@@ -143,12 +143,6 @@ export default {
this.destroy();
},
mounted() {
const startTime = moment().startOf("week").format("YYYY-MM-DD");
const endTime = moment().format("YYYY-MM-DD");
console.log(this.date, "date");
this.date = [startTime, endTime];
// this.weekDay = this.weekArr[moment(this.date).format('e')]
// this.getData()
const _this = this;
_this.beilv = document.documentElement.clientWidth / 1920;
window.onresize = () => {
@@ -161,9 +155,9 @@ export default {
methods: {
getData(obj) {
getProductSaleAnalysis({
startTime: 1762704000000,
endTime: 1762790400000,
mode: 1
startTime: obj.startTime,
endTime: obj.endTime,
mode: obj.mode,
}).then((res) => {
console.log(res);
this.productSaleData = [

View File

@@ -0,0 +1,214 @@
<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" />
</div>
</div>
</div>
</div>
</template>
<script>
import ReportHeader from "../components/noRouterHeader.vue";
import { Sidebar } from "../../../layout/components";
import screenfull from "screenfull";
import { mapState } from "vuex";
import operatingLineChart from "../accountsReceivableComponents/operatingLineChart";
import operatingLineChartCumulative from "../accountsReceivableComponents/operatingLineChartCumulative.vue";
import { getAccountsReceivableData } from '@/api/cockpit'
export default {
name: "AccountsReceivable",
components: {
ReportHeader,
operatingLineChartCumulative,
operatingLineChart,
Sidebar,
},
data() {
return {
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
isFullScreen: false,
timer: null,
beilv: 1,
value: 100,
sort:1,
selectDate:{},
monthData: {},
ytdData:{},
dateData: {},
};
},
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() {
getAccountsReceivableData({
startTime: this.dateData.startTime,
endTime: this.dateData.endTime,
sort: this.sort,
index: undefined,
factory: undefined
}).then((res) => {
this.monthData= res.data.month
this.ytdData = res.data.ytd
})
},
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);
}
},
};
</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>

View 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>

View File

@@ -0,0 +1,282 @@
<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;
}
&__opLargeBg {
background: url(../../../assets/img/opLargeBg.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>

View File

@@ -0,0 +1,226 @@
<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>

View File

@@ -0,0 +1,513 @@
<template>
<div class="coreBar">
<div class="header-row">
<div class="base-title">各基地情况</div>
<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">{{ selectedSort || '请选择' }}</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.label }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingLineBarSale.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { operatingLineBar },
props: ["chartData", 'dateData'],
emits: ['sort-change'], // 声明事件Vue3 推荐)
data() {
return {
activeButton: 0,
isDropdownShow: false,
selectedSort: '实际值:高~低', // 选中的label
selectedSortValue: 1, // 选中的value用于排序逻辑
profitOptions: [
{ label: '实际值:高~低', value: 1 },
{ label: '实际值:低~高', value: 2 },
{ label: '完成率:高~低', value: 3 },
{ label: '完成率:低~高', value: 4 },
]
};
},
computed: {
// 排序后的数据源核心根据selectedSortValue重新排序
currentDataSource() {
if (!this.chartData?.factory) return {};
// 深拷贝原始数据,避免修改原数据
const factory = JSON.parse(JSON.stringify(this.chartData.factory));
if (!factory.locations.length || !this.selectedSortValue) return factory;
// 构建带索引的数组,方便同步所有字段排序
const dataWithIndex = factory.locations.map((name, index) => ({
index,
name,
real: factory.reals[index],
target: factory.targets[index],
rate: factory.rates[index],
diff: factory.diff[index],
flag: factory.flags[index]
}));
// 根据选中的排序规则排序
switch (this.selectedSortValue) {
case 1: // 实际值:高~低
dataWithIndex.sort((a, b) => b.real - a.real);
break;
case 2: // 实际值:低~高
dataWithIndex.sort((a, b) => a.real - b.real);
break;
case 3: // 预算值:高~低
dataWithIndex.sort((a, b) => b.rate - a.rate);
break;
case 4: // 预算值:低~高
dataWithIndex.sort((a, b) => a.rate - b.rate);
break;
default:
return factory;
}
// 同步更新所有数组
factory.locations = dataWithIndex.map(item => item.name);
factory.reals = dataWithIndex.map(item => item.real);
factory.targets = dataWithIndex.map(item => item.target);
factory.rates = dataWithIndex.map(item => item.rate);
factory.diff = dataWithIndex.map(item => item.diff);
factory.flags = dataWithIndex.map(item => item.flag);
return factory;
},
locations() {
return this.currentDataSource.locations || [];
},
// 最终传递给图表的排序后数据
chartD() {
const data = this.currentDataSource;
const salesData = {
allPlaceNames: this.locations,
series: [
// 完成率(折线图)
{
name: '完成率',
type: 'line',
yAxisIndex: 1,
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
},
// 目标(柱状图)
{
name: '预算',
type: 'bar',
yAxisIndex: 0,
barWidth: 40,
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.targets || []
},
// 实际(柱状图)
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 40,
label: {
show: false
},
itemStyle: {
color: (params) => {
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 || []
},
// 实际差值标签独立scatter系列zlevel=1确保标签在最上层
{
name: '__实际差值标签',
type: 'scatter',
yAxisIndex: 0,
zlevel: 1,
symbolSize: 0,
tooltip: {
show: false
},
data: (data.reals || []).map((value, index) => ({
value: value,
label: {
show: true,
position: 'top',
offset: [32, 0],
width: 100,
height: 22,
formatter: () => {
const diff = data.diff || [];
const flags = data.flags || [];
const currentDiff = diff[index] || 0;
const currentFlag = flags[index] || 0;
if (currentFlag === 1) {
return `{achieved|${currentDiff}}{text|差值}`;
} else {
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)' },
{ offset: 0.2, color: '#ffffff' },
{ offset: 1, color: '#ffffff' }
]
},
shadowColor: 'rgba(191,203,215,0.5)',
shadowBlur: 2,
shadowOffsetX: 0,
shadowOffsetY: 2,
borderRadius: 4,
borderColor: '#BFCBD577',
borderWidth: 0,
lineHeight: 26,
rich: {
text: {
width: 'auto',
padding: [5, 10, 5, 0],
align: 'center',
color: '#464646',
fontSize: 14,
},
achieved: {
width: 'auto',
padding: [5, 0, 5, 10],
align: 'center',
color: '#76DABE',
fontSize: 14,
},
unachieved: {
width: 'auto',
padding: [5, 0, 5, 10],
align: 'center',
color: '#F9A44A',
fontSize: 14,
}
}
}
}))
}
]
};
return salesData;
}
},
methods: {
selectProfit(item) {
// 更新选中的label和value
this.selectedSort = item.label;
this.selectedSortValue = item.value;
this.isDropdownShow = false;
// 向父组件传递排序事件(可选,保持原有逻辑)
this.$emit('sort-change', item.value);
}
},
// 监听父组件传入的chartData变化重置选中状态可选
watch: {
'chartData.factory': {
handler() {
// 若需要切换数据源后重置排序,可取消注释
// this.selectedSort = null;
// this.selectedSortValue = null;
},
deep: true
}
}
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.header-row {
display: flex;
justify-content: space-between;
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 {
width: auto;
align-items: center;
gap: 16px;
.right-container {
display: flex;
align-items: center;
gap: 24px;
margin-right: 46px;
}
.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);
}
.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;
fontSize: 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;
fontSize: 12px;
color: #333;
cursor: pointer;
text-align: left;
letter-spacing: 1px;
&:hover {
background: #f5f7fa;
color: #3071ff;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<div ref="cockpitEffChipBottom" id="cockpitEffChipBottom" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
chartData: {
type: Object,
default: () => ({
series: [],
allPlaceNames: []
}),
// 校验数据格式
validator: (value) => {
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
}
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChipBottom;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
const unit = item.seriesName === '完成率' ? '%' : (
['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
);
html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
});
return html;
}
},
grid: {
top: 30,
bottom: 30,
right: 20,
left: 60,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
splitNumber: 4,
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
// min: 0,
// max: (value) => Math.ceil((value.max || 0) * 1.1),
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
// 右侧Y轴利润占比百分比
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
// min: 0,
// max: 100,
scale:true,
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,172 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
chartData: {
type: Object,
default: () => ({
series: [],
allPlaceNames: []
}),
// 校验数据格式
validator: (value) => {
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
}
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// formatter: (params) => {
// let html = `${params[0].axisValue}<br/>`;
// params.forEach(item => {
// const unit = item.seriesName === '完成率' ? '%' : (
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
// );
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
// });
// return html;
// }
},
grid: {
top: 30,
bottom: 30,
right: 20,
left: 60,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
// 右侧Y轴利润占比百分比
// {
// type: 'value',
// nameTextStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12,
// align: 'left'
// },
// min: 0,
// max: 100,
// axisTick: { show: false },
// axisLabel: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12,
// formatter: '{value}%'
// },
// splitLine: { show: false },
// axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
// splitNumber: 4
// }
],
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,215 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null, // 存储图表实例
resizeHandler: null, // 存储resize事件处理函数
// 核心:基地名称与序号的映射表(固定顺序)
baseNameToIndexMap: {
'宜兴': 7,
'漳州': 8,
'自贡': 3,
'桐城': 2,
'洛阳': 9,
'合肥': 5,
'宿迁': 6,
'秦皇岛': 10
}
};
},
props: {
chartData: {
type: Object,
default: () => ({}),
},
dateData: {
type: Object,
default: () => ({}),
},
},
mounted() {
this.$nextTick(() => {
this.initChart(); // 初始化图表(只执行一次)
this.updateChart(); // 更新图表数据
});
},
watch: {
chartData: {
handler() {
console.log(this.chartData, 'chartData');
this.updateChart(); // 仅更新数据,不重新创建实例
},
deep: true,
immediate: true
},
},
beforeDestroy() {
// 组件销毁时清理资源
this.destroyChart();
},
methods: {
// 初始化图表只在mounted中执行一次
initChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
// 只创建一次图表实例
this.myChart = echarts.init(chartDom);
// 绑定点击事件(只绑定一次,永久生效)
this.myChart.getZr().on('click', (params) => {
console.log('params', params);
// 提取点击的基地名称
// const itemName = params.name;
let itemName = undefined
// 根据映射表获取对应的序号未匹配到则返回0或其他默认值
let pointInPixel = [params.offsetX, params.offsetY];
if (this.myChart.containPixel('grid', pointInPixel)) {
let pointInGrid = this.myChart.convertFromPixel({
seriesIndex: 0
}, pointInPixel);
let xIndex = pointInGrid[0]; //索引
let handleIndex = Number(xIndex);
let seriesObj = this.myChart.getOption(); //图表object对象
var op = this.myChart.getOption();
//获得图表中点击的列
itemName = op.xAxis[0].data[handleIndex]; //获取点击的列名
console.log(itemName, 'monthmonthmonth');
console.log(handleIndex, seriesObj);
};
const baseIndex = this.baseNameToIndexMap[itemName] || 0;
console.log(`你点击了【${itemName}】(序号:${baseIndex})`);
if (itemName === undefined) {
return;
}
});
// 定义resize处理函数命名函数方便移除
this.resizeHandler = () => {
this.myChart && this.myChart.resize();
};
// 绑定resize事件只绑定一次
window.addEventListener('resize', this.resizeHandler);
},
// 更新图表数据(数据变化时执行)
updateChart() {
if (!this.myChart) {
return; // 实例未初始化则返回
}
const { allPlaceNames, series } = this.chartData || {};
const xData = allPlaceNames || [];
const chartSeries = series || [];
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
top: 30,
bottom: 5,
right: 20,
left: 25,
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0],
// 可选X轴标签显示“序号+名称”如“1 宜兴”)
// formatter: (value) => {
// const index = this.baseNameToIndexMap[value] || '';
// return index ? `${index} ${value}` : value;
// }
},
data: xData
}
],
yAxis: [
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
},
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: chartSeries
};
// 只更新配置,不重新创建实例
this.myChart.setOption(option, true); // 第二个参数true表示清空原有配置避免数据残留
},
// 销毁图表资源
destroyChart() {
// 移除resize事件
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
// 销毁图表实例
if (this.myChart) {
this.myChart.dispose();
this.myChart = null;
}
}
}
};
</script>

View File

@@ -0,0 +1,172 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 380px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
chartData: {
type: Object,
default: () => ({
}),
// 校验数据格式
// validator: (value) => {
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
// }
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
console.log('chartData', this.chartData);
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
console.log('xData', xData);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// formatter: (params) => {
// let html = `${params[0].axisValue}<br/>`;
// params.forEach(item => {
// const unit = item.seriesName === '完成率' ? '%' : (
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
// );
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
// });
// return html;
// }
},
grid: {
top: 30,
bottom:20,
right: 10,
left: 25,
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
},
// 右侧Y轴利润占比百分比
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
// min: 0,
// max: 100,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
//
splitNumber: 4,
}
],
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,165 @@
<template>
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 200px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
refName: {
type: String,
default: () => 'cockpitEffChip',
// 校验数据格式
// validator: (value) => {
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
// }
},
chartData: {
type: Object,
default: () => ({
}),
// 校验数据格式
// validator: (value) => {
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
// }
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData, 'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs[this.refName];
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
console.log('chartData', this.chartData);
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
console.log('xData', xData);
const option = {
tooltip: {
trigger: 'axis',
// axisPointer: {
// type: 'cross',
// label: {
// backgroundColor: '#6a7985'
// }
// },
// formatter: (params) => {
// let html = `${params[0].axisValue}<br/>`;
// params.forEach(item => {
// const unit = item.seriesName === '完成率' ? '%' : (
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
// );
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
// });
// return html;
// }
},
grid: {
top: 20,
bottom: 30,
right: 20,
left: 5,
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: {
type: 'value',
// name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
axisLine: {
show: true, // 显示Y轴轴线关键
lineStyle: {
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
width: 1, // 轴线宽度
type: 'solid' // 实线可选dashed虚线、dotted点线
}
},
splitNumber: 2,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
// axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
},
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,331 @@
<template>
<div style="flex: 1">
<Container name="当月数据对比" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
<div class="left" style="
height: 380px;
display: flex;
width: 348px;
background-color: rgba(249, 252, 255, 1);
flex-direction: column;
">
<div style="
padding: 16px 16px 5px 16px;
line-height: 18px;
font-size: 18px;
color: #000000;
letter-spacing: 1px;
font-style: normal;
">
集团情况
</div>
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
</div>
<operatingTopBar :chartData="chartData" />
</div>
<div class="right" style="
height: 380px;
display: flex;
width: 1220px;
background-color: rgba(249, 252, 255, 1);
">
<operatingBar :dateData="dateData" :chartData="chartData" @sort-change="sortChange" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from "../components/container.vue";
import operatingBar from "./operatingBar.vue";
import operatingTopBar from "./operatingTopBar.vue";
export default {
name: "ProductionStatus",
components: { Container, operatingBar, operatingTopBar },
props: {
monthData: {
type: Object,
default: () => ({
group: {
rate: 0,
diff: 0,
real: 0,
target: 0
},
factory: []
}),
required: true
},
dateData: {
type: Object,
default: () => ({
}),
},
},
data() {
return {
chartData: null, // 初始化 chartData 为 null
groupData: {}, // 集团数据
factoryData: [] // 工厂数据
};
},
watch: {
monthData: {
handler() {
this.processChartData();
},
immediate: true,
deep: true,
},
},
methods: {
sortChange(value) {
this.$emit('sort-change', value);
},
/**
* 核心处理函数:在所有数据都准备好后,才组装 chartData
*/
processChartData() {
// 1. 处理集团数据 - 提取各字段到对应数组
this.groupData = this.monthData.group || { rate: 0, diff: 0, real: 0, target: 0 };
// 集团各维度数据数组(单条数据,对应凯盛新能)
const groupTarget = [this.groupData.target]; // 预算值数组
const groupDiff = [this.groupData.diff]; // 差值数组
const groupReal = [this.groupData.real]; // 实际值数组
const groupRate = [this.groupData.rate]; // 完成率数组
// 新增集团rate对应的flag
const groupFlag = [this.groupData.rate >= 100 ? 1 : 0];
console.log('集团数据数组:', {
groupTarget,
groupDiff,
groupReal,
groupRate,
groupFlag,
rawGroupData: this.groupData
});
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
this.factoryData = this.monthData.factory || [];
// 提取工厂名称数组
const factoryNames = this.factoryData.map(item => item.title || '');
// 提取工厂各维度数据数组
const factoryBudget = this.factoryData.map(item => item.budget || 0);
const factoryReal = this.factoryData.map(item => item.real || 0);
const factoryRate = this.factoryData.map(item => item.rate || 0);
const factoryDiff = this.factoryData.map(item => item.diff || 0);
// 新增每个工厂rate对应的flag数组
const factoryFlags = this.factoryData.map(item => item.rate >= 100 ? 1 : 0);
// 3. 组装最终的chartData供子组件使用
this.chartData = {
// 集团数据(对应凯盛新能)
group: {
locations: ['凯盛新能'], // 集团名称
targets: groupTarget, // 集团预算值数组
diff: groupDiff, // 集团差值数组
reals: groupReal, // 集团实际值数组
rate: groupRate, // 集团完成率数组
flags: groupFlag // 新增集团rate对应的flag
},
// 工厂数据
factory: {
locations: factoryNames, // 工厂名称数组
targets: factoryBudget, // 工厂预算数组
reals: factoryReal, // 工厂实际值数组
rates: factoryRate, // 工厂完成率数组
diff: factoryDiff, // 工厂差值数组
flags: factoryFlags // 新增工厂rate对应的flags数组
},
// 原始数据备份(方便后续使用)
rawData: {
group: this.groupData,
factory: this.factoryData
}
};
console.log('最终处理后的图表数据:', this.chartData);
},
},
};
</script>
<style lang="scss" scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-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 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<div style="flex: 1">
<Container name="累计数据对比" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
<div class="left" style="
height: 380px;
display: flex;
width: 348px;
background-color: rgba(249, 252, 255, 1);
flex-direction: column;
">
<div style="
padding: 16px 16px 5px 16px;
line-height: 18px;
font-size: 18px;
color: #000000;
letter-spacing: 1px;
font-style: normal;
">
集团情况
</div>
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
</div>
<operatingTopBar :chartData="chartData" />
</div>
<div class="right" style="
height: 380px;
display: flex;
width: 1220px;
background-color: rgba(249, 252, 255, 1);
">
<operatingBar :dateData="dateData" @sort-change="sortChange" :chartData="chartData" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from "../components/container.vue";
import operatingBar from "./operatingBar.vue";
import operatingTopBar from "./operatingTopBar.vue";
export default {
name: "ProductionStatus",
components: { Container, operatingBar, operatingTopBar },
props: {
salesTrendMap: {
type: Object,
default: () => ({}),
},
ytdData: {
type: Object,
default: () => ({}),
},
dateData: {
type: Object,
default: () => ({
}),
},
},
data() {
return {
chartData: null, // 初始化 chartData 为 null
groupData: {}, // 集团数据
factoryData: [] // 工厂数据
};
},
watch: {
ytdData: {
handler() {
this.processChartData();
},
immediate: true,
deep: true,
},
},
methods: {
sortChange(value) {
this.$emit('sort-change', value);
},
/**
* 核心处理函数:在所有数据都准备好后,才组装 chartData
*/
processChartData() {
// 1. 处理集团数据 - 提取各字段到对应数组
this.groupData = this.ytdData.group || { rate: 0, diff: 0, real: 0, target: 0 };
// 集团各维度数据数组(单条数据,对应凯盛新能)
const groupTarget = [this.groupData.target]; // 预算值数组
const groupDiff = [this.groupData.diff]; // 差值数组
const groupReal = [this.groupData.real]; // 实际值数组
const groupRate = [this.groupData.rate]; // 完成率数组
// 新增集团rate对应的flag
const groupFlag = [this.groupData.rate >= 100 ? 1 : 0];
console.log('集团数据数组:', {
groupTarget,
groupDiff,
groupReal,
groupRate,
groupFlag,
rawGroupData: this.groupData
});
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
this.factoryData = this.ytdData.factory || [];
// 提取工厂名称数组
const factoryNames = this.factoryData.map(item => item.title || '');
// 提取工厂各维度数据数组
const factoryBudget = this.factoryData.map(item => item.budget || 0);
const factoryReal = this.factoryData.map(item => item.real || 0);
const factoryRate = this.factoryData.map(item => item.rate || 0);
const factoryDiff = this.factoryData.map(item => item.diff || 0);
// 新增每个工厂rate对应的flag数组
const factoryFlags = this.factoryData.map(item => item.rate >= 100 ? 1 : 0);
// 3. 组装最终的chartData供子组件使用
this.chartData = {
// 集团数据(对应凯盛新能)
group: {
locations: ['凯盛新能'], // 集团名称
targets: groupTarget, // 集团预算值数组
diff: groupDiff, // 集团差值数组
reals: groupReal, // 集团实际值数组
rate: groupRate, // 集团完成率数组
flags: groupFlag // 新增集团rate对应的flag
},
// 工厂数据
factory: {
locations: factoryNames, // 工厂名称数组
targets: factoryBudget, // 工厂预算数组
reals: factoryReal, // 工厂实际值数组
rates: factoryRate, // 工厂完成率数组
diff: factoryDiff, // 工厂差值数组
flags: factoryFlags // 新增工厂rate对应的flags数组
},
// 原始数据备份(方便后续使用)
rawData: {
group: this.groupData,
factory: this.factoryData
}
};
console.log('最终处理后的图表数据:', this.chartData);
},
},
};
</script>
<style lang="scss" scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-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 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,337 @@
<template>
<div class="coreBar">
<div class="lineBottom" style="height: 320px; width: 100%">
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingLineBarSaleGroup.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { operatingLineBar },
props: ["chartData"],
data() {
return {
activeButton: 0,
};
},
computed: {
currentDataSource() {
console.log('yyyy', this.chartData);
return this.chartData.group
},
locations() {
console.log('this.chartData', this.chartData);
return this.chartData.group.locations
},
// 根据按钮切换生成对应的 chartData
chartD() {
// 销量场景数据
const data = this.currentDataSource;
const diff = this.currentDataSource.diff[0]
const rate = this.currentDataSource.rate[0]
const salesData = {
allPlaceNames: this.locations,
series: [
{
name: '预算',
type: 'bar',
yAxisIndex: 0, // 左侧Y轴万元
label: {
show: true,
position: 'top'
},
barWidth: 65,
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.targets // 目标销量(万元)
},
// 3. 实际(柱状图,含达标状态)
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
label: {
show: true,
position: 'top'
},
barWidth: 65,
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 // 实际销量(万元)
}
]
};
// 毛利率场景数据
const grossProfitData = {
series: [
// 1. 完成率(折线图)
{
name: '完成率',
type: 'line',
yAxisIndex: 1,
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: [106.7, 96.9, 106.5, 106.1, 93.8, 105.9], // 毛利率完成率(%
symbol: 'circle',
symbolSize: 6
},
// 2. 目标(柱状图)
{
name: '预算',
type: 'bar',
yAxisIndex: 0,
barWidth: 65,
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: [30, 32, 31, 33, 32, 34] // 目标毛利率(万元)
},
// 3. 实际(柱状图)
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 65,
itemStyle: {
color: (params) => {
const safeFlag = [1, 0, 1, 1, 0, 1]; // 达标状态
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: [32, 31, 33, 35, 30, 36] // 实际毛利率(万元)
}
]
};
// 根据按钮状态返回对应数据
return this.activeButton === 0 ? salesData : grossProfitData;
}
},
methods: {},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
align-items: center; // 垂直居中,避免上下错位
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
width: 100%; // 确保占满父容器,实现两端对齐
.title {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
// 标题固定在左侧,不挤压右侧空间
white-space: nowrap;
}
// 1. 右侧容器:包裹图例和按钮组,整体靠右
.right-container {
display: flex;
align-items: center; // 图例和按钮组垂直居中
gap: 24px; // 图例与按钮组的间距,避免贴紧
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
}
// 2. 图例:在右侧容器内横向排列
.legend {
display: flex;
gap: 16px; // 图例项之间间距,避免重叠
align-items: center;
// 移除原margin-left避免位置偏移
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;
width: 283px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
// 移除原margin-right由右侧容器统一控制
margin: 0;
.item-button {
cursor: pointer;
width: 142px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
letter-spacing: 8px; // 确保文字间距生效
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
}
.item-button.active {
width: 142px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<el-form ref="form" :rules="rules" label-width="110px" :model="form">
<el-row>
<el-col :span="24">
<el-form-item label="客户名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="客户编码" prop="code">
<el-input v-model="form.code"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="所属基地" prop="factoryList">
<el-select v-model="form.factoryList" placeholder="请选择" style="width: 100%;" multiple>
<el-option v-for='item in facList' :key='item.value' :label="item.name" :value= 'item.value' ></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import { addCustomerConfig, updateCustomerConfig, getCustomerConfig } from '@/api/basicInfoConfig'
export default {
name: 'CustomerInfoAdd',
data() {
return {
form: {
id: '',
name: '',
code: '',
factoryList:'',
},
isEdit: false, //是否是编辑
rules: {
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
factoryList: [{ required: true, message: '请选择所属基地', trigger: 'change' }],
},
facList:[
{name:'宜兴',value:7},
{name:'漳州',value:8},
{name:'自贡',value:3},
{name:'桐城',value:2},
{name:'洛阳',value:9},
{name:'合肥',value:5},
{name:'宿迁',value:6},
{name:'秦皇岛',value:10}
]
}
},
methods: {
init(id) {
if (id) {
this.isEdit = true
this.form.id = id
getCustomerConfig({id}).then((res) => {
if (res.code === 0) {
this.form = res.data
}
})
}
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.isEdit) {
//编辑
updateCustomerConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
} else {
addCustomerConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
}
} else {
return false
}
})
},
formClear() {
this.$refs.form.resetFields()
this.isEdit = false
}
}
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<el-form ref="form" :rules="rules" label-width="120px" :model="form">
<el-row>
<el-col :span="24">
<el-form-item label="重点工作" prop="importantWork">
<el-input v-model="form.importantWork"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="单位" prop="unit">
<el-select v-model="form.unit" placeholder="请选择" style="width: 100%;">
<el-option v-for='item in unitList' :key='item.value' :label="item.label" :value= 'item.value' ></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="所属年份" prop="time">
<el-date-picker
v-model="form.time"
type="year"
placeholder="选择年"
style="width: 100%;">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="累计值计算方式" prop="calculateMethod">
<el-select v-model="form.calculateMethod" placeholder="请选择" style="width: 100%;">
<el-option v-for='item in calculateMethodList' :key='item.value' :label="item.label" :value= 'item.value' ></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="数值目标属性" prop="targetProperty">
<el-select v-model="form.targetProperty" placeholder="请选择" style="width: 100%;">
<el-option v-for='item in targetPropertyList' :key='item.value' :label="item.label" :value= 'item.value' ></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import { addImportantWorkConfig, updateImportantWorkConfig, getImportantWorkConfig } from '@/api/basicInfoConfig'
import {getDictDatas } from '@/utils/dict'
import moment from 'moment';
export default {
name: 'groupKeyAdd',
data() {
return {
form: {
id: '',
importantWork: '',
unit: '',
time:'',
calculateMethod:'',
targetProperty:'',
},
isEdit: false, //是否是编辑
rules: {
importantWork: [{ required: true, message: '请输入重点工作', trigger: 'blur' }],
unit: [{ required: true, message: '请选择单位', trigger: 'change' }],
time: [{ required: true, message: '请选择所属年份', trigger: 'change' }],
calculateMethod: [{ required: true, message: '请选择累计值计算方式', trigger: 'change' }],
targetProperty: [{ required: true, message: '请选择数值目标属性', trigger: 'change' }],
},
unitList:getDictDatas('lb_dw'),
calculateMethodList:getDictDatas('important_work_method'),
targetPropertyList:getDictDatas('target_property')
}
},
methods: {
init(id) {
if (id) {
this.isEdit = true
this.form.id = id
getImportantWorkConfig({id}).then((res) => {
if (res.code === 0) {
this.form = res.data
}
})
}
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
this.form.time = moment(this.form.time).endOf('year').endOf('month').endOf('day').unix() * 1000
if (this.isEdit) {
//编辑
updateImportantWorkConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
} else {
addImportantWorkConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
}
} else {
return false
}
})
},
formClear() {
this.$refs.form.resetFields()
this.isEdit = false
}
}
}
</script>

View File

@@ -0,0 +1,88 @@
<template>
<el-form ref="form" :rules="rules" label-width="110px" :model="form">
<el-row>
<el-col :span="24">
<el-form-item label="产品名称" prop="product">
<el-input v-model="form.product"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="规格" prop="spec">
<el-input v-model="form.spec"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="工艺" prop="process">
<el-select v-model="form.process" placeholder="请选择" style="width: 100%;">
<el-option v-for='item in processList' :key='item.value' :label="item.label" :value= 'item.value' ></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import { addProductConfig, updateProductConfig, getProductConfig } from '@/api/basicInfoConfig'
import {getDictDatas } from '@/utils/dict'
export default {
name: 'ProductInfoAdd',
data() {
return {
form: {
id: '',
product: '',
spec: '',
process:'',
},
isEdit: false, //是否是编辑
rules: {
product: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
},
processList:getDictDatas('process')
}
},
methods: {
init(id) {
if (id) {
this.isEdit = true
this.form.id = id
getProductConfig({id}).then((res) => {
if (res.code === 0) {
this.form = res.data
}
})
}else{
this.form.id = ''
}
},
submitForm() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.isEdit) {
//编辑
updateProductConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
} else {
addProductConfig({ ...this.form }).then((res) => {
if (res.code === 0) {
this.$modal.msgSuccess("操作成功");
this.$emit('successSubmit')
}
})
}
} else {
return false
}
})
},
formClear() {
this.$refs.form.resetFields()
this.isEdit = false
}
}
}
</script>

View File

@@ -0,0 +1,192 @@
<template>
<div class="app-container">
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
@clickBtn="handleClick" />
</base-table>
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
<!-- 新增 -->
<base-dialog
:dialogTitle="addOrEditTitle"
:dialogVisible="centervisible"
@cancel="handleCancel"
@confirm="handleConfirm"
:before-close="handleCancel"
width="50%">
<customer-info-add ref="customerList" @successSubmit="successSubmit" />
</base-dialog>
</div>
</template>
<script>
import CustomerInfoAdd from './components/customerInfoAdd.vue';
import { getCustomerConfigPage,delCustomerConfig } from '@/api/basicInfoConfig';
const tableProps = [
{
prop: 'name',
label: '客户名称'
},
{
prop: 'code',
label: '客户编码'
},
{
prop: 'factoryNameList',
label: '所属基地',
filter: (val) => val ? val.join(',') :'-'
},
];
export default {
name: 'CustomerInfoConfiguration',
components:{CustomerInfoAdd},
data () {
return {
formConfig: [
{
type: 'select',
label: '所属基地',
selectOptions: [
{ id: 2, name: '桐城' },
{ id: 3, name: '自贡' },
{ id: 5, name: '合肥' },
{ id: 6, name: '宿迁' },
{ id: 7, name: '宜兴' },
{ id: 8, name: '漳州' },
{ id: 9, name: '洛阳' },
{ id: 10, name: '秦皇岛' },
],
param: 'factory',
onchange: true
},
{
type: 'input',
label: '客户名称',
placeholder: '客户名称',
param: 'name'
},
{
type: 'input',
label: '客户编码',
placeholder: '客户编码',
param: 'code'
},
{
type: 'button',
btnName: '查询',
name: 'search',
color: 'primary',
},
{
type: 'separate',
},
{
type: 'button',
btnName: '新增',
name: 'add',
color: 'success',
plain: true
}
],
listQuery:{
factory:'',
name:'',
code:'',
pageSize:20,
pageNo:1
},
total:0,
tableData:[],
tableProps,
tableBtn:[
{
type: 'edit',
btnName: '编辑',
},
{
type: 'delete',
btnName: '删除',
},
],
addOrEditTitle: '',
centervisible: false,
}
},
mounted() {
this.getDataList();
},
methods: {
buttonClick(val) {
console.log(val)
switch (val.btnName) {
case 'search':
this.listQuery.name = val.name
this.listQuery.code = val.code
this.listQuery.factory = val.factory
this.listQuery.pageNo = 1;
this.getDataList();
break;
case 'add':
this.addOrEditTitle = '新增';
this.$nextTick(() => {
this.$refs.customerList.init();
});
this.centervisible = true;
break;
}
},
getDataList() {
getCustomerConfigPage({...this.listQuery}).then(res => {
if (res.code === 0) {
this.tableData = res.data.list;
this.total = res.data.total;
}else{
this.tableData = [];
this.total = 0;
}
})
},
handleClick(val) {
switch (val.type) {
case 'edit':
this.addOrEditTitle = '编辑';
this.$nextTick(() => {
this.$refs.customerList.init(val.data.id);
});
this.centervisible = true;
break;
default:
this.handleDelete(val.data);
}
},
handleCancel() {
this.$refs.customerList.formClear();
this.centervisible = false;
this.addOrEditTitle = '';
},
handleConfirm() {
this.$refs.customerList.submitForm();
},
successSubmit() {
this.handleCancel();
this.getDataList();
},
/** 删除按钮操作 */
handleDelete(row) {
let _this = this;
_this.$modal.confirm('确定删除客户"' + row.name + '吗?').then(function() {
delCustomerConfig({id:row.id}).then(response => {
_this.$modal.msgSuccess("删除成功");
_this.getDataList();
})
}).catch(() => {});
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<div class="app-container">
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
@clickBtn="handleClick" />
</base-table>
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
<!-- 新增 -->
<base-dialog
:dialogTitle="addOrEditTitle"
:dialogVisible="centervisible"
@cancel="handleCancel"
@confirm="handleConfirm"
:before-close="handleCancel"
width="50%">
<group-key-add ref="groupKey" @successSubmit="successSubmit" />
</base-dialog>
</div>
</template>
<script>
import GroupKeyAdd from './components/groupKeyAdd.vue';
import { getImportantWorkConfigPage,delImportantWorkConfig } from '@/api/basicInfoConfig';
import { publicFormatter } from '@/utils/dict';
import moment from 'moment';
const tableProps = [
{
prop: 'importantWork',
label: '重点工作'
},
{
prop: 'unit',
label: '单位',
filter: publicFormatter('lb_dw')
},
{
prop: 'time',
label: '所属年份',
filter: (val) => val ? moment(val).format('YYYY') : '-'
},
{
prop: 'calculateMethod',
label: '累计值计算方式',
filter: publicFormatter('important_work_method')
},
{
prop: 'targetProperty',
label: '数值目标属性',
filter: publicFormatter('target_property')
},
];
export default {
name: 'GroupKeyTaskConfiguration',
components:{GroupKeyAdd},
data () {
return {
formConfig: [
{
type: 'input',
label: '重点工作',
placeholder: '重点工作',
param: 'importantWork'
},
{
type: 'datePicker',
label: '所属年份',
dateType: 'year',
format: 'yyyy',
valueFormat: 'yyyy',
placeholder: '所属年份',
param: 'time',
width: 150
},
{
type: 'button',
btnName: '查询',
name: 'search',
color: 'primary',
},
{
type: 'separate',
},
{
type: 'button',
btnName: '新增',
name: 'add',
color: 'success',
plain: true
}
],
listQuery:{
importantWork:'',
time:'',
pageSize:20,
pageNo:1
},
total:0,
tableData:[],
tableProps,
tableBtn:[
{
type: 'edit',
btnName: '编辑',
},
{
type: 'delete',
btnName: '删除',
},
],
addOrEditTitle: '',
centervisible: false,
}
},
mounted() {
this.$refs.searchBarForm.formInline.time = moment().endOf('year').endOf('month').endOf('day').format('YYYY-MM-DD HH:mm:ss')
this.listQuery.time = moment().endOf('year').endOf('month').endOf('day').format('YYYY-MM-DD HH:mm:ss')
this.getDataList();
},
methods: {
buttonClick(val) {
switch (val.btnName) {
case 'search':
this.listQuery.importantWork = val.importantWork;
this.listQuery.time = val.time ? moment(val.time).endOf('year').endOf('month').endOf('day').format('YYYY-MM-DD HH:mm:ss') : '';
this.listQuery.pageNo = 1;
this.getDataList();
break;
case 'add':
this.addOrEditTitle = '新增';
this.$nextTick(() => {
this.$refs.groupKey.init();
});
this.centervisible = true;
break;
}
},
getDataList() {
getImportantWorkConfigPage({...this.listQuery}).then(res => {
if (res.code === 0) {
this.tableData = res.data.list;
this.total = res.data.total;
}else{
this.tableData = [];
this.total = 0;
}
})
},
handleClick(val) {
switch (val.type) {
case 'edit':
this.addOrEditTitle = '编辑';
this.$nextTick(() => {
this.$refs.groupKey.init(val.data.id);
});
this.centervisible = true;
break;
default:
this.handleDelete(val.data);
}
},
handleCancel() {
this.$refs.groupKey.formClear();
this.centervisible = false;
this.addOrEditTitle = '';
},
handleConfirm() {
this.$refs.groupKey.submitForm();
},
successSubmit() {
this.handleCancel();
this.getDataList();
},
/** 删除按钮操作 */
handleDelete(row) {
let _this = this;
_this.$modal.confirm('确定删除重点工作"' + row.importantWork + '吗?').then(function() {
delImportantWorkConfig({id:row.id}).then(response => {
_this.$modal.msgSuccess("删除成功");
_this.getDataList();
})
}).catch(() => {});
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<div class="app-container">
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
@clickBtn="handleClick" />
</base-table>
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
<!-- 新增 -->
<base-dialog
:dialogTitle="addOrEditTitle"
:dialogVisible="centervisible"
@cancel="handleCancel"
@confirm="handleConfirm"
:before-close="handleCancel"
width="50%">
<product-info-add ref="prodectInfo" @successSubmit="successSubmit" />
</base-dialog>
</div>
</template>
<script>
import ProductInfoAdd from './components/productInfoAdd.vue';
import { getProductConfigPage,delProductConfig } from '@/api/basicInfoConfig';
import { publicFormatter } from '@/utils/dict';
const tableProps = [
{
prop: 'product',
label: '产品名称'
},
{
prop: 'process',
label: '工艺',
filter: publicFormatter('process')
},
{
prop: 'spec',
label: '规格'
},
];
export default {
name: 'ProductInfoConfiguration',
components:{ProductInfoAdd},
data () {
return {
formConfig: [
{
type: 'input',
label: '产品名称',
placeholder: '产品名称',
param: 'product'
},
{
type: 'button',
btnName: '查询',
name: 'search',
color: 'primary',
},
{
type: 'separate',
},
{
type: 'button',
btnName: '新增',
name: 'add',
color: 'success',
plain: true
}
],
listQuery:{
product:'',
pageSize:20,
pageNo:1
},
total:0,
tableData:[],
tableProps,
tableBtn:[
{
type: 'edit',
btnName: '编辑',
},
{
type: 'delete',
btnName: '删除',
},
],
addOrEditTitle: '',
centervisible: false,
}
},
mounted() {
this.getDataList();
},
methods: {
buttonClick(val) {
switch (val.btnName) {
case 'search':
this.listQuery.product = val.product;
this.listQuery.pageNo = 1;
this.getDataList();
break;
case 'add':
this.addOrEditTitle = '新增';
this.$nextTick(() => {
this.$refs.prodectInfo.init();
});
this.centervisible = true;
break;
}
},
getDataList() {
getProductConfigPage({
...this.listQuery
}).then(res => {
if (res.code === 0) {
this.tableData = res.data.list;
this.total = res.data.total;
}else{
this.tableData = [];
this.total = 0;
}
})
},
handleClick(val) {
switch (val.type) {
case 'edit':
this.addOrEditTitle = '编辑';
this.$nextTick(() => {
this.$refs.prodectInfo.init(val.data.id);
});
this.centervisible = true;
break;
default:
this.handleDelete(val.data);
}
},
handleCancel() {
this.$refs.prodectInfo.formClear();
this.centervisible = false;
this.addOrEditTitle = '';
},
handleConfirm() {
this.$refs.prodectInfo.submitForm();
},
successSubmit() {
this.handleCancel();
this.getDataList();
},
/** 删除按钮操作 */
handleDelete(row) {
let _this = this;
_this.$modal.confirm('确定删除产品"' + row.product + '吗?').then(function() {
delProductConfig({id:row.id}).then(response => {
_this.getDataList();
_this.$modal.msgSuccess("删除成功");
})
}).catch(() => {});
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,260 @@
<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:186px 1422px;
">
<indicatorCalendar :timeType="timeType" :calendarObj='calendarObj'/>
<indicatorDetails :timeType="timeType" @updateLeft='getData' @updateLevel='getLevel'/>
</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:{},
levelId: null
};
},
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({levelId: this.levelId}).then((res) => {
this.calendarObj = res.data
})
}else{
getCalendarYear({levelId: this.levelId}).then((res) => {
this.calendarObj = res.data
})
}
},
// 层级变动
getLevel(id) {
this.levelId = id
this.getData()
},
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>

View File

@@ -10,10 +10,10 @@
</div>
</div>
</div>
<div class="lineBottom" style="height: 214px; width: 100%">
<!-- 传递 series 数据给子组件添加 key 确保数据更新时重新渲染 -->
<coreLineChart style="height: 214px; width: 680px" :chart-series="chartSeries"
:key="JSON.stringify(chartSeries)" />
<div class="lineBottom" style="height: 213px; width: 100%">
<!-- 传递动态生成的 series 数据 xAxis 数据给子组件 -->
<coreLineChart style="height: 213px; width: 680px" :chart-series="chartSeries" :x-axis-data="xAxisData"
:dateData="dateData" />
</div>
</div>
</template>
@@ -25,80 +25,100 @@ import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
props: {
line: { // 接收父组件传递的 cost 数据对象
type: Object,
default: () => ({})
},
dateData: {
type: Object,
default: () => ({})
}
},
data() {
return {
activeButton: undefined,
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 定义要传递的 series 数据(对应图例的三个费用类型)
chartSeries: [
{
// 图表样式配置项,可以抽离出来方便管理
chartConfig: {
'管理费用': {
name: '管理费用',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(11, 88, 255, .5)' },
itemStyle: {
color: 'rgba(11, 88, 255, .5)',
borderColor: 'rgba(11, 88, 255, 1)',
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(11, 88, 255, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
},
data: [140, 232, 101, 264, 90, 340] // 与 xAxis 数据长度一致6个月份
lineColor: 'rgba(11, 88, 255, .5)',
itemColor: 'rgba(11, 88, 255, .5)',
borderColor: 'rgba(11, 88, 255, 1)',
areaGradient: [
{ offset: 0, color: 'rgba(11, 88, 255, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]
},
{
'销售费用': {
name: '销售费用',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(54, 181, 138, .5)' },
itemStyle: {
color: 'rgba(54, 181, 138, .5)',
borderColor: 'rgba(54, 181, 138, 1)',
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(54, 181, 138, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
},
data: [120, 282, 111, 234, 220, 340]
lineColor: 'rgba(54, 181, 138, .5)',
itemColor: 'rgba(54, 181, 138, .5)',
borderColor: 'rgba(54, 181, 138, 1)',
areaGradient: [
{ offset: 0, color: 'rgba(54, 181, 138, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]
},
{
'财务费用': {
name: '财务费用',
lineColor: 'rgba(255, 132, 0, .5)',
itemColor: 'rgba(255, 132, 0, .5)',
borderColor: 'rgba(255, 132, 0, 1)',
areaGradient: [
{ offset: 0, color: 'rgba(255, 132, 0, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]
}
}
};
},
computed: {
// 动态生成 X 轴数据
xAxisData() {
// 从 cost.line 中获取任意一个有数据的键的 keys 作为 X 轴
const lineData = this.line || {};
const firstKey = Object.keys(lineData)[0];
return firstKey ? Object.keys(lineData[firstKey].real) : [];
},
// 动态生成 series 数据
chartSeries() {
const lineData = this.line || {};
const xAxisKeys = this.xAxisData;
// 如果没有 X 轴数据,则返回空数组
if (xAxisKeys.length === 0) {
return {};
}
let obj = {
unit:'万元',
series:[]
}
// 遍历配置项,生成 series
obj.series = Object.keys(this.chartConfig).map(key => {
const config = this.chartConfig[key];
// 确保数据顺序和 X 轴一致
const dataValues = xAxisKeys.map(date => lineData[key] ? lineData[key].real[date] : 0);
return {
name: config.name,
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, .5)' },
symbolSize: 6,
lineStyle: { color: config.lineColor },
itemStyle: {
color: 'rgba(255, 132, 0, .5)',
borderColor: 'rgba(255, 132, 0, 1)',
color: config.itemColor,
borderColor: config.borderColor,
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, config.areaGradient),
},
data: [90, 150, 180, 120, 250, 280]
}
]
};
data: dataValues
};
});
return obj;
}
},
methods: {}
};
@@ -110,6 +130,7 @@ export default {
display: flex;
flex-direction: column;
padding: 12px;
margin: 7px 0;
.barTop {
display: flex;

View File

@@ -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,150 +29,171 @@
<!-- 时间选择区域//年按钮 + 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>
</template>
<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 = '';
// 处理选择的日期转为moment对象兼容不同选择器格式
console.log('this.date', this.date);
try {
// 使用 this.date时间戳创建 moment 对象
let targetMoment = this.date
? moment(this.date) // 解析时间戳
: moment(); // 如果 date 无效,使用当前时间
const targetMoment = this.date
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
: defaultMoment;
// 验证日期是否有效
if (!targetMoment.isValid()) {
console.warn('无效的日期,已使用当前月份:', this.date);
targetMoment = moment();
}
// 验证日期有效性
if (!targetMoment.isValid()) {
console.error('无效日期:', this.date);
return { startTime, endTime, mode };
// 获取月份数字格式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. 日维度当天0点 → 次日0点
if (this.activeTime === 0) {
startTime = targetMoment.startOf('day').valueOf(); // 当天00:00:00 时间戳
endTime = targetMoment.add(1, 'day').startOf('day').valueOf(); // 次日00:00:00 时间戳
}
// 2. 月维度当月1日0点 → 次月1日0点
else if (this.activeTime === 1) {
startTime = targetMoment.startOf('month').valueOf(); // 当月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'month').startOf('month').valueOf(); // 次月1日00:00:00 时间戳
}
// 3. 年维度当年1月1日0点 → 次年1月1日0点
else if (this.activeTime === 2) {
startTime = targetMoment.startOf('year').valueOf(); // 当年1月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'year').startOf('year').valueOf(); // 次年1月1日00:00:00 时间戳
}
// 调试输出(格式化显示,便于验证)
console.log('时间范围计算结果:', {
return {
startTime,
endTime,
mode,
startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
startTimeStamp: startTime,
endTimeStamp: endTime
});
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>
@@ -182,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;
@@ -255,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 {
@@ -277,7 +300,6 @@ export default {
text-align: center;
cursor: pointer;
overflow: hidden;
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
}
.timeType .item .item-text {
@@ -290,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 {
@@ -308,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;
@@ -322,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 {
@@ -348,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;
}
}
@@ -359,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;
@@ -385,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>

View File

@@ -1,6 +1,6 @@
<template>
<div style="">
<Container name="利润主要影响因素" icon="cockpitItemIcon" size="profitMiddleBasic" topSize="KFAPTopTitle">
<Container name="利润主要影响因素·万元" icon="cockpitItemIcon" size="profitMiddleBasic" topSize="KFAPTopTitle">
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="left" style="width: 382px;">
<top-item :itemList="targetItemList" />

View File

@@ -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' } },

View File

@@ -1,18 +1,11 @@
<!--
* @Date: 2020-12-14 09:07:03
* @LastEditors: zhp
* @LastEditTime: 2024-09-05 09:50:14
* @FilePath: \mt-bus-fe\src\views\OperationalOverview\components\baseTable.vue
* @Description:
-->
<template>
<div class="visual-base-table-container">
<el-table :max-height="maxHeight" ref="scroll_Table" @mouseenter.native="autoScroll(true)"
@mouseleave.native="autoScroll(false)" v-loading="isLoading"
:header-cell-style="{ background: 'rgba(218, 226, 237, 1)', color: 'rgba(0, 0, 0, .6)',padding:'3px 2px'}" :row-style="setRowStyle"
:data="renderData" border style="width: 100%; background: transparent">
<el-table-column v-if="page && limit && showIndex" prop="_pageIndex" label="序号" :width="70" align="center" />
<el-table-column v-for="item in renderTableHeadList" :key="item.prop" :show-overflow-tooltip="showOverflow"
<el-table-column v-if="page && limit && showIndex" prop="_pageIndex" label="序号" :width="60" align="center" />
<el-table-column v-for="item in renderTableHeadList" :key="item.prop" :show-overflow-tooltip="showOverflow" :width='item.width || ""'
v-bind="item">
<template slot-scope="scope">
@@ -39,7 +32,7 @@ export default {
maxHeight: {
type: [Number, String], // 支持数字如300或字符串如'300px'
required: false,
default: 200 // 原固定值,作为默认 fallback
default: 200 // 原固定值,作为默认 fallback
},
tableData: {
type: Array,

View File

@@ -6,6 +6,8 @@
<span class="title-text">
{{ name }}
</span>
<svg-icon @click="handleShow" v-if="tableShow" class="title-icon" style="position: absolute;right: 20;" :icon-class="'orderReturn'" />
</div>
</div>
<div class="container-body">
@@ -21,12 +23,16 @@ export default {
name: 'Container',
components: {},
// eslint-disable-next-line vue/require-prop-types
props: ['name', 'size', 'icon', 'nameTwo'],
props: ['name', 'size', 'icon', 'nameTwo','tableShow'],
data() {
return {};
},
computed: {},
methods: {},
methods: {
handleShow() {
this.$emit('handleShow',false)
}
},
};
</script>
@@ -83,7 +89,7 @@ export default {
position: absolute;
top: 0;
left: 0;
width: 100%;
width: 50%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;

View File

@@ -0,0 +1,122 @@
<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="width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 844px;padding: 26px 0px;">
<!-- 动态生成12个月的容器优化flex布局缩小行间距 -->
<div class="month-list">
<!-- 循环生成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: 100px;
height: 57px;
border-radius: 4px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 20px;
color: rgba(0, 0, 0, 0.85);
line-height: 57px;
text-align: center;
font-style: normal;
transition: all 0.2s ease; // 过渡效果,样式切换更平滑
border: 2px solid transparent; // 透明边框,避免选中时布局偏移
margin: 0 auto 10px; // 清除默认外边距,进一步缩小缝隙
}
// 有数据的样式(背景色#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>

View File

@@ -0,0 +1,659 @@
<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="请选择" @change='handleLevelChange'>
<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='downLoadExcel'>模板下载</el-button>
<el-button type="primary" plain size="medium" @click='importExcel'>导入</el-button>
<el-button type="primary" plain size="medium" @click='exportExcel'>导出</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 @close="handleImportDialogClose">
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" action="#" :disabled="upload.httpUploading"
: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>仅允许导入xlsxlsx格式文件</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 v-if="upload.httpUploading" class="upload-progress-wrap">
<div class="upload-progress-track">
<div class="upload-progress-bar"></div>
</div>
<span class="upload-progress-text">正在上传</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="upload.httpUploading" :disabled="upload.httpUploading" @click="submitFileForm"> </el-button>
<el-button :disabled="upload.httpUploading" @click="cancelBtn"> </el-button>
</div>
</el-dialog>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import { getLevelStruc, getTargetMonthPage, updateTargetMonthData, getTargetYearPage, updateTargetYearData,copyLastMonthData, copyLastYearData,importTemplateYS} from '@/api/cockpit'
import inputArea from './inputArea.vue' // 导入输入组件
import { publicFormatter } from '@/utils/dict';
import {getAccessToken, getTenantId} from '@/utils/auth'
import axios from 'axios';
import moment from 'moment';
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: "预算填报导入",
fileList:[],
currentFile:null,
timeDim: 2,
// HTTP 上传中(点击确定后 axios 上传,展示不确定进度条)
httpUploading: false
},
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.getDataPage();
});
this.$modal.msgSuccess("保存成功");
this.$emit('updateLeft')
}).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.$emit('updateLevel', 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 = [];
});
},
handleLevelChange(id) {
this.form.levelId = id;
this.getDataPage()
this.$emit('updateLevel', id)
},
// 表格单元格数据变更回调
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.getDataPage();
});
},
// 初始化表格列配置核心精准控制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);
// 重新请求数据
this.getDataPage();
},
// 导入
importExcel() {
this.upload.httpUploading = false
this.upload.open = true
},
downLoadExcel() {
importTemplateYS({
levelId:this.form.levelId,
startTime: this.form.startTime,
endTime: this.form.endTime,
pageSize: 1000,
template:1,
pageNo: this.form.pageNo,
timeDim:this.timeType === 'month'?2:3
}).then(response => {
let fileName = '';
let factoryName = this.levelLList.filter(item => item.id == this.form.levelId)[0].name;
if (this.timeType === 'month') {
fileName = factoryName+'_月_预算导入模板.xls';
}else{
fileName = factoryName+'_年_预算导入模板.xls';
}
this.$download.excel(response, fileName);
});
},
exportExcel() {
importTemplateYS({
levelId:this.form.levelId,
startTime: this.form.startTime,
endTime: this.form.endTime,
pageSize: 1000,
template:0,
pageNo: this.form.pageNo,
timeDim:this.timeType === 'month'?2:3
}).then(response => {
let mon = this.timeType=== 'month'?moment(this.form.endTime).format('YYYYMM'):moment(this.form.endTime).format('YYYY');
let factoryName = this.levelLList.filter(item => item.id == this.form.levelId)[0].name;
let fileName = this.timeType=== 'month'?mon + factoryName + '月度预算表.xls':mon + factoryName + '年度预算表.xls';
this.$download.excel(response, fileName);
});
},
// 文件上传中处理
handleFileUploadProgress(file, fileList) {
console.log('文件上传中:',file, fileList)
this.upload.fileList = fileList;
this.upload.currentFile = file.raw;
},
handleFileSuccess() {},
importTemplate() {},
// 提交上传文件
async submitFileForm() {
if (!this.upload.currentFile) {
return this.$message.error('请先选择要上传的文件!')
}
if (this.upload.httpUploading) {
return
}
this.upload.httpUploading = true
try {
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: 300000
})
// 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.$refs.upload.clearFiles();
this.getDataPage();
this.$emit('updateLeft')
} else {
this.$message.error(`上传失败:${response.data.msg || '未知错误'}`)
}
} catch (error) {
// 5. 异常处理
console.error('文件上传出错:', error)
this.$message.error('上传失败!')
} finally {
this.upload.httpUploading = false
}
},
cancelBtn() {
if (this.upload.httpUploading) {
return
}
this.upload.open = false
},
handleImportDialogClose() {
this.upload.currentFile = null
this.upload.fileList = []
this.$nextTick(() => {
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
})
}
}
}
</script>
<style lang='scss' scoped>
.upload-progress-wrap {
margin-top: 12px;
}
.upload-progress-track {
height: 8px;
border-radius: 4px;
background: rgba(11, 88, 255, 0.12);
overflow: hidden;
position: relative;
}
.upload-progress-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 38%;
border-radius: 4px;
background: linear-gradient(90deg, #0b58ff, #5b9aff, #0b58ff);
animation: upload-indeterminate-slide 1.35s ease-in-out infinite;
}
@keyframes upload-indeterminate-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(320%);
}
}
.upload-progress-text {
display: block;
margin-top: 8px;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
text-align: center;
}
// 月份列表容器样式(保留原有配置,若无使用可忽略)
.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>

View 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;
// 时间维度对应mode2=月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>

View File

@@ -0,0 +1,134 @@
<template>
<div class="changeBase">
<div class="base-item" @click="handleClick(item.id)" v-for="(item) in buttonLevelList" :key="item.id"
:style="{ zIndex: activeButton === item.id ? 10 : 1 }">
<img :src="activeButton === item.id ? item.bgImg : item.img" :alt="`${item.name}基地`" :title="item.name">
</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: undefined,
validator: (val) => [5, 2, 7, 3, 8, 9, 10, 6].includes(val) // 校验序号范围匹配新的baseNameToIndexMap
}
},
// 计算属性响应式levelList变化时会自动更新
computed: {
buttonLevelList() {
// 核心:通过$store.getters获取定义的getter
let arr = []
this.$store.getters.levelList.forEach(item => {
this.buttonList.forEach(item2 => {
if (item2.id === item.id) {
arr.push(item2)
}
})
})
this.activeButton = arr[0].id
return arr
}
},
data() {
return {
activeButton: undefined,
buttonList:[
{id: 5, name: '合肥', img: baseHefei, bgImg: bgBaseHefei},
{id: 2, name: '桐城', img: baseTongcheng, bgImg: bgBaseTongcheng},
{id: 7, name: '宜兴', img: baseYixing, bgImg: bgBaseYixing},
{id: 3, name: '自贡', img: baseZigong, bgImg: bgBaseZigong},
{id: 8, name: '漳州', img: baseZhangzhou, bgImg: bgBaseZhangzhou},
{id: 9, name: '洛阳', img: baseLuoyang, bgImg: bgBaseLuoyang},
{id: 10, name: '秦皇岛', img: baseQinhuangdao, bgImg: bgBaseQinhuangdao},
{id: 6, name: '宿迁', img: baseSuqian, bgImg: bgBaseSuqian}
],
};
},
watch: {
// 监听父组件传递的factory变化同步本地选中索引
factory: {
handler(newVal) {
console.log('watch factory=======================:', newVal);
if (newVal) {
this.activeButton = 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 // 初始化立即执行
}
},
methods: {
handleClick(id) {
this.activeButton = id;
this.$emit('baseChange', id);
}
},
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>

View File

@@ -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: 25px;
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>

View File

@@ -1,16 +1,16 @@
<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">
<div class="number" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">
<div class="number" :style="{ color: getTargetColor(item.progress) }">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
@@ -21,15 +21,15 @@
<div class="progress-container">
<div class="progress-bar" :style="{
width: item.progress + '%',
background: getTargetColor(item.currentValue, item.targetValue)
background: getTargetColor(item.progress)
}"></div>
</div>
</div>
<div class="yield" style="display: flex;justify-content: space-between;">
<div class="progress-percent" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">完成率
<div class="progress-percent" :style="{ color: getTargetColor(item.progress) }">完成率
</div>
<div class="progress-percent" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">
<div class="progress-percent" :style="{ color: getTargetColor(item.progress) }">
{{ item.progress }}%
</div>
</div>
@@ -42,42 +42,61 @@
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
props: ["purchase",'dateData'],
data() {
return {
progress: 90,
itemList: [
{
unit: "本月增效额·万元",
targetValue: 16,
currentValue: 14.5,
progress: 90
},
{
unit: "累计增效额·万元",
targetValue: 16,
currentValue: 15.2,
progress: 85
},
// {
// unit: "销量·万㎡",
// targetValue: 20,
// currentValue: 16,
// progress: 80
// },
// {
// unit: "双镀面板·万㎡",
// targetValue: 15,
// currentValue: 13.8,
// progress: 92
// }
]
itemList: []
};
},
computed: {},
watch: {
purchase: {
handler(newVal) {
if (newVal) {
this.itemList = this.transformData(newVal);
}
},
immediate: true,
deep: true
}
},
methods: {
getTargetColor(currentValue, targetValue) {
return currentValue >= targetValue
handleRoute(path) {
this.$router.push({
path: path,
query: {
dateData: this.dateData
}
})
},
transformData(rawData) {
const dataMap = [
{
key: 'increase',
unit: '本月增效额·万元',
path:'/procurementGainAnalysis/procurementGainAnalysis'
},
{
key: 'totalIncrease',
unit: '累计增效额·万元',
path: '/procurementGainAnalysis/procurementGainAnalysis'
}
];
return dataMap.map(itemInfo => {
const rawItem = rawData[itemInfo.key] || {};
const progress = rawItem.rate || 0;
return {
unit: itemInfo.unit,
targetValue: rawItem.target || 0,
currentValue: rawItem.real || 0,
progress: progress,
path: itemInfo.path
};
});
},
getTargetColor(progress) {
return progress >= 100
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
}
@@ -98,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;

View File

@@ -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,14 +23,14 @@
</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>
<div class="lineBottom" style="height: 219px; width: 100%">
<!-- 传递当前选中的 series 数据给子组件key 确保数据更新时重新渲染 -->
<coreLineChart style="height: 219px; width: 500px" :chart-series="currentSeries"
:key="activeButton + JSON.stringify(currentSeries)" />
<coreLineChart style="height: 219px; width: 500px" :chart-series="currentSeries" :x-axis-data="xAxisData"
:dateData="dateData" :key="activeButton + JSON.stringify(currentSeries)" />
</div>
</div>
</template>
@@ -42,204 +42,103 @@ import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
props: {
line: { // 接收父组件传递的 sale 数据对象
type: Object,
default: () => ({})
},
dateData: { // 接收父组件传递的 sale 数据对象
type: Object,
default: () => ({})
}
},
data() {
return {
activeButton: 0,
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 4个按钮对应的 series 数据(目标+实际两条线)
seriesMap: [
// 0: 单价(元/m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: 2
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [16, 16.2, 15.8, 16.1, 15.9, 16] // 6-11月目标数据
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: 'rgba(255, 132, 0, 1)',
borderWidth: 2,
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [14.5, 14.8, 15.2, 14.6, 15, 14.7] // 6-11月实际数据
}
],
// 1: 净价(元/m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [16, 16.1, 15.9, 16.2, 16, 16.1]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [15.2, 15.5, 15.3, 15.6, 15.4, 15.5]
}
],
// 2: 销量万m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [20, 20.5, 19.8, 21, 20.2, 20.8]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [16, 16.8, 17.2, 16.5, 17, 17.5]
}
],
// 3: 双镀产品万m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [15, 15.2, 14.8, 15.5, 15.1, 15.3]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [13.8, 14.2, 14, 14.5, 14.3, 14.6]
}
]
// 定义按钮与 line 数据中 key 的映射关系
buttonToDataKey: [
{name:'单价',unit:'元/㎡'},
{name:'净价',unit:'元/㎡'},
{name:'销量',unit:'万㎡'},
{name:'双镀销量',unit:'万㎡'}
]
};
},
computed: {
// 动态生成 X 轴数据
xAxisData() {
const lineData = this.line || {};
// 获取当前激活按钮对应的数据
const currentDataKey = this.buttonToDataKey[this.activeButton].name;
const currentIndicatorData = lineData[currentDataKey];
// 使用 'target' 的键作为 x 轴,如果 'target' 不存在,则使用 'real' 的键
if (currentIndicatorData && currentIndicatorData.target) {
return Object.keys(currentIndicatorData.target);
} else if (currentIndicatorData && currentIndicatorData.real) {
return Object.keys(currentIndicatorData.real);
}
return [];
},
// 根据激活按钮动态返回对应 series 数据
currentSeries() {
return this.seriesMap[this.activeButton] || [];
const lineData = this.line || {};
const currentDataKey = this.buttonToDataKey[this.activeButton].name;
const chartData = lineData[currentDataKey];
if (!chartData) {
return {};
}
// 提取目标和实际数据的值,并确保顺序与 X 轴一致
const xAxisKeys = this.xAxisData;
const targetDataValues = xAxisKeys.map(date => chartData.target ? chartData.target[date] : 0);
const realDataValues = xAxisKeys.map(date => chartData.real ? chartData.real[date] : 0);
let obj = {
unit:this.buttonToDataKey[this.activeButton].unit,
series:[
{
name: '预算',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: 2
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: targetDataValues
},
{
name: '实际',
type: 'line',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: 'rgba(255, 132, 0, 1)',
borderWidth: 2,
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: realDataValues
}
]}
return obj;
}
}
};

View File

@@ -3,11 +3,11 @@
<Container name="采购重点指标" nameTwo="存货重点指标" icon="cockpitItemIcon" size="bottomBasic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;">
<coreBottomLeftItem>
<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"
:tableConfig="tableProps" :table-data="maintenanceTasks" />
<base-table style="height: 221px;width: 260px;" :page="1" :limit="10" :show-index="true" :beilv="1"
:tableConfig="tableProps" :table-data="maintenanceTasks" :maxHeight='220'/>
</div>
</div>
</Container>
@@ -25,11 +25,15 @@ export default {
components: { Container, coreBottomLeftItem, baseTable },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
purchase: { // 接收父组件传递的设备数据数组
type: Object,
default: () => {} // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
dateData: { // 接收父组件传递的设备数据数组
type: Object,
default: () => { } // 默认空数组,避免报错
},
inventory: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
@@ -37,38 +41,35 @@ export default {
data() {
return {
maintenanceTasks: [
{ id: 1, eqName: '纯碱', taskName: '1313,252', },
{ id: 2, eqName: '硅砂', taskName: '14,252', },
{ id: 2, eqName: '白云石', taskName: '23,252', },
{ id: 2, eqName: '石灰石', taskName: '34,421', },
{ id: 2, eqName: '氧化铝', taskName: '1,251.34', },
{ id: 2, eqName: '氢氧化铝', taskName: '14,252', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
{ id: 1, name: '纯碱', number: '1313,252', },
{ id: 2, name: '硅砂', number: '14,252', },
{ id: 3, name: '白云石', number: '23,252', },
{ id: 4, name: '石灰石', number: '34,421', },
{ id: 5, name: '氧化铝', number: '1,251.34', },
{ id: 6, name: '氢氧化铝', number: '14,252', }
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{ prop: 'eqName', label: '物料', align: 'left' },
{ prop: 'taskName', label: '库存/吨', align: 'left' },
{ prop: 'name', label: '物料', align: 'left' },
{ prop: 'number', label: '库存/吨', align: 'left' },
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
inventory: {
handler(newInventoryData) {
// 当 inventory 数据变化时,执行转换函数
this.maintenanceTasks = this.transformInventoryData(newInventoryData);
},
deep: true // 若对象内属性变化需触发,需加 deep: true
immediate: true, // 组件初始化时立即执行一次
deep: true // 深度监听对象内部属性的变化
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
// 组件挂载时,如果有初始 inventory 数据,也执行一次转换
if (this.inventory) {
this.maintenanceTasks = this.transformInventoryData(this.inventory);
}
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
@@ -78,115 +79,25 @@ export default {
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
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' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
/**
* 核心转换函数:将 inventory 对象转换为 maintenanceTasks 数组
* @param {Object} inventoryData - 格式如: { "纯碱": 0, "硅砂": 0, ... }
* @returns {Array} - 格式如: [ { id: 1, name: '纯碱', number: '0' }, ... ]
*/
transformInventoryData(inventoryData) {
// 检查输入是否为有效的非空对象
if (!inventoryData || typeof inventoryData !== 'object' || Object.keys(inventoryData).length === 0) {
return []; // 如果无效,返回空数组
}
this.chart.setOption(option)
// 使用 Object.entries() 和 map() 进行转换
return Object.entries(inventoryData).map(([name, value], index) => {
return {
id: index + 1, // id 从 1 开始自增
name: name, // 物料名称
number: value ? String(value) : '-' // 将数值转换为字符串,以匹配 "number" 字段
};
});
}
}
}

View File

@@ -1,16 +1,15 @@
<template>
<div style="flex: 1">
<Container name="销售重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic">
<Container name="销售重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic" :isShowTab='true' @tabChange='tabChange'>
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item />
<top-item :sale="saleData" :dateData="dateData" />
</div>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<coreBottomBar />
<coreBottomBar :line="sale.line" :dateData="dateData" />
</div>
</div>
@@ -19,7 +18,6 @@
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './top-item.vue'
import coreBottomBar from './coreBottomBar.vue'
@@ -28,149 +26,47 @@ export default {
components: { Container, topItem, coreBottomBar },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
sale: { // 接收父组件传递的设备数据数组
type: Object,
default: () => ({})
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
dateData: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null
chart: null,
saleData:{},
currentTap:'month'
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
sale: {
handler(newVal) {
if(this.currentTap === 'month') {
this.saleData = this.sale.mon
}else{
this.saleData = this.sale.total
}
},
deep: true // 若对象内属性变化需触发,需加 deep: true
deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
this.saleData = this.sale.month
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
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' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
tabChange(val) {
if(val === 'month') {
this.currentTap = 'month'
this.saleData = this.sale.mon
}else{
this.currentTap = 'total'
this.saleData = this.sale.total
}
this.chart.setOption(option)
console.log(this.saleData);
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="height: 219px; width: 100%;"></div>
<div ref="cockpitEffChip" id="coreLineChart" style="height: 184px; width: 100%;"></div>
</template>
<script>
@@ -11,10 +11,17 @@ export default {
// 接收父组件传递的 series 数据
props: {
chartSeries: {
type: Object,
default: () => {{}} // 默认空数组,避免报错
},
xAxisData: {
type: Array,
required: true,
default: () => [] // 默认空数组,避免报错
}
},
dateData: {
type: Object,
default: () => {} // 默认空数组,避免报错
},
},
data() {
return {
@@ -70,7 +77,7 @@ export default {
label: { backgroundColor: '#6a7985' }
}
},
grid: { top: 10, bottom: 20, right: 25, left: 50 },
grid: { top: 35, bottom: 3, right: 15, left: 18, containLabel: true},
xAxis: [
{
type: 'category',
@@ -85,24 +92,29 @@ export default {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
// width: 38,
overflow: 'break',
formatter: (value) => {
const dateParts = value.split('-'); // ["2025", "07", "01"]
if (dateParts.length < 2) return value;
// 去掉月份前面的0然后加上"月"
const month = dateParts[1].replace(/^0+/, '');
return `${month}`;
}
},
data: ['6月', '7月', '8月', '9月', '10月', '11月']
data: this.xAxisData
}
],
yAxis: {
type: 'value',
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'left' },
// min: () => 0,
// max: (value) => Math.ceil(value.max),
// scale: true,
name: this.chartSeries.unit,
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'right' },
axisTick: { show: false },
axisLabel: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 12 },
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
},
series: this.chartSeries // 使用父组件传递的 series 数据
series: this.chartSeries.series // 使用父组件传递的 series 数据
};
this.myChart.setOption(option, true); // 第二个参数 true 表示替换现有配置

View File

@@ -1,14 +1,14 @@
<template>
<div class="coreItem">
<!-- 动态生成每个 item -->
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.unit }}</div>
<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">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue }}</div>
<div class="title">上月</div>
<div class="title">{{currentTap==='month'?'上月':'上年'}}</div>
</div>
<div class="line"></div>
<div class="right">
@@ -16,7 +16,7 @@
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ item.currentValue }}
</div>
<div class="title">本月</div>
<div class="title">{{currentTap==='month'?'本月':'本年'}}</div>
</div>
</div>
<div class="line"></div>
@@ -24,7 +24,7 @@
<div class="right">
<!-- 环比额计算差值并动态绑定颜色 -->
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ get环比额(item.currentValue, item.targetValue) }}
{{ item.thbe }}
</div>
<div class="title">环比额</div>
</div>
@@ -37,46 +37,101 @@
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
props: {
cost: { // 接收父组件传递的 cost 数据对象
type: Object,
default: () => ({})
},
dateData: { // 接收父组件传递的 cost 数据对象
type: Object,
default: () => ({})
},
currentTap: {
type: String,
default: 'month'
}
},
data() {
return {
itemList: [
{
unit: "管理费用·万元",
targetValue: 16, // 上月值
currentValue: 14.5, // 本月值(小于上月,应显示橙色)
},
{
unit: "销售费用·万元",
targetValue: 16,
currentValue: 17, // 大于上月,应显示绿色
},
{
unit: "财务费用·万元",
targetValue: 16,
currentValue: 16, // 等于上月,应显示绿色
},
]
itemList: [] // 初始化为空数组,等待数据加载
};
},
watch: {
// 监听 cost 数据变化,实时更新 itemList
cost: {
handler(newVal) {
if (newVal) {
this.itemList = this.transformData(newVal);
}
},
immediate: true, // 组件初始化时立即执行一次
deep: true // 深度监听对象内部属性变化
}
},
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 数据对象
* @returns {Array} - 转换后的 itemList 数组
*/
transformData(rawData) {
// 定义费用类型映射关系(键名、显示名称、单位)
const costMapping = [
{
key: 'totalCost', name: '总费用·万元',
path:"/expenseAnalysis/expenseAnalysisBase"
},
{
key: 'manageCost', name: '管理费用·万元',
path: "/expenseAnalysis/expenseAnalysisBase"
},
{ key: 'saleCost', name: '销售费用·万元', path: "/expenseAnalysis/expenseAnalysisBase" },
{ key: 'financeCost', name: '财务费用·万元', path: "/expenseAnalysis/expenseAnalysisBase" }
];
// 遍历映射关系,转换数据
return costMapping.map(mappingItem => {
// 获取对应费用类型的数据,若不存在则使用默认值
const costData = rawData[mappingItem.key] || { last: 0, this: 0, thbe: 0 };
return {
name: mappingItem.name,
path: mappingItem.path,
thbe: costData.thbe,
targetValue: costData.last, // 上月值
currentValue: costData.this // 本月值
};
});
},
// 颜色判断:本月 >= 上月 绿色,否则 橙色
getColor(current, target) {
return current >= target
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
? "rgba(98, 213, 180, 1)" // 绿色:增长或持平
: "rgba(249, 164, 74, 1)"; // 橙色:下降
},
// 计算环比额(本月 - 上月),保留一位小数
get环比额(current, target) {
const diff = current - target;
// 正数加"+"号,负数和零保持原样
return diff > 0 ? `${diff.toFixed(1)}` : diff.toFixed(1);
}
// getData(current, target) {
// const diff = current - target;
// // 正数加"+"号,负数和零保持原样
// return diff > 0 ? `+${diff.toFixed(1)}` : diff.toFixed(1);
// }
},
};
</script>
<style scoped lang="scss">
/* 样式保持不变 */
.coreItem {
display: flex;
gap: 8px;
@@ -88,7 +143,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;

View File

@@ -1,29 +1,26 @@
<template>
<div style="flex: 1">
<!-- 传入点击切换的状态到Container组件 -->
<Container name="财务重点指标" nameTwo="费用重点指标" icon="cockpitItemIcon" size="topBasic" @switchTab="handleTabSwitch">
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;flex-direction: column;">
<Container name="财务重点指标" nameTwo="费用重点指标" icon="cockpitItemIcon" size="topBasic" @switchTab="handleTabSwitch" @tabChange='tabChange'>
<div class="bottom-left-content" style="display: flex;gap: 0px;padding: 14px 16px;flex-direction: column;">
<!-- 根据activeTab状态切换显示采购/存货内容 -->
<template v-if="activeTab === 'purchase'">
<!-- 采购重点指标应的内容 -->
<coreBottomLeftItem></coreBottomLeftItem>
<!-- 财务重点指标应的内容 -->
<coreBottomLeftItem :dateData="dateData" :finance="financeData"></coreBottomLeftItem>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<coreBottomBar></coreBottomBar>
<coreBottomBar :dateData="dateData" :line="finance.line"></coreBottomBar>
</div>
</template>
<template v-else-if="activeTab === 'inventory'">
<!-- 存货重点指标对应的内容 -->
<costItem></costItem>
<!-- 费用重点指标对应的内容 -->
<costItem :dateData="dateData" :cost="costData" :currentTap='currentTap'></costItem>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<CostsBottomBar>
style="display: flex; width: 100%;margin-top: 4px;background-color: rgba(249, 252, 255, 1);">
<CostsBottomBar :line="cost.line" :dateData="dateData">
</CostsBottomBar>
</div>
</template>
<!-- 图表容器根据状态控制显示若需展示图表 -->
<!-- <div id="productionStatusChart" style="height: 200px; margin-top: 12px;" v-if="showChart"></div> -->
</div>
</Container>
</div>
@@ -41,11 +38,15 @@ export default {
name: 'ProductionStatus',
components: { Container, coreBottomLeftItem, coreBottomBar, costItem, CostsBottomBar },
props: {
leftEqInfoData: {
type: Array,
default: () => []
finance: {
type: Object,
default: () => {}
},
productionOverviewVo: {
cost: {
type: Object,
default: () => ({})
},
dateData: {
type: Object,
default: () => ({})
}
@@ -54,7 +55,10 @@ export default {
return {
activeTab: 'purchase', // 激活的标签purchase=采购inventory=存货
showChart: true, // 控制图表是否显示
chart: null // 图表实例
chart: null, // 图表实例
financeData:{},
costData:{},
currentTap: 'month'
}
},
watch: {
@@ -67,7 +71,28 @@ export default {
this.updateChart()
},
deep: true
},
finance: {
handler() {
if(this.currentTap === 'month') {
this.financeData = this.finance.mon
}else{
this.financeData = this.finance.total
}
},
deep: true
},
cost: {
handler() {
if(this.currentTap === 'month') {
this.costData = this.cost.mon
}else{
this.costData = this.cost.total
}
},
deep: true
}
},
mounted() {
// 初始化图表
@@ -81,6 +106,17 @@ export default {
}
},
methods: {
tabChange(val) {
if(val === 'month') {
this.currentTap = 'month'
this.financeData = this.finance.mon
this.costData = this.cost.mon
}else{
this.currentTap = 'total'
this.financeData = this.finance.total
this.costData = this.cost.total
}
},
// 处理标题点击切换标签
handleTabSwitch(tabType) {
this.activeTab = tabType // tabType由Container组件传递'purchase'或'inventory'
@@ -140,7 +176,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' } },

View File

@@ -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>
@@ -27,12 +27,22 @@
<div class="item-button" :class="{ active: activeButton === 3 }" @click="activeButton = 3">
毛利率
</div>
<div class="button-line lineFour" v-if="activeButton !== 3 && activeButton !== 4"></div>
<div class="item-button" :class="{ active: activeButton === 4 }" @click="activeButton = 4">
应收账款
</div>
<div class="button-line lineFive" v-if="activeButton !== 4 && activeButton !== 5"></div>
<div class="item-button" :class="{ active: activeButton === 5 }" @click="activeButton = 5">
存货
</div>
</div>
</div>
<div class="lineBottom" style="height: 210px; width: 100%">
<!-- 传递当前选中的 series 数据给子组件 -->
<coreLineChart style="height: 210px; width: 680px" :chart-series="currentSeries"
:key="activeButton + JSON.stringify(currentSeries)" />
<div class="lineBottom" style="height: 184px; width: 100%">
<!-- 传递当前选中的 series 数据 xAxis 数据给子组件 -->
<coreLineChart style="height: 184px; width: 680px" :chart-series="currentSeries" :x-axis-data="xAxisData"
:dateData="dateData" />
</div>
</div>
</template>
@@ -44,197 +54,93 @@ import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
props: {
line: { // 接收父组件传递的 line 数据
type: Object,
default: () => ({})
},
dateData: { // 接收父组件传递的 line 数据
type: Object,
default: () => ({})
},
},
data() {
return {
activeButton: 0, // 初始激活第一个按钮索引0
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 存储4个按钮对应的 series 数据(每个按钮对应「目标+实际」两条线)
seriesMap: [
// 0: 营业收入
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: 'rgba(91, 230, 190, 1)',
borderWidth: 2
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [500, 620, 580, 720, 650, 800] // 6-11月目标数据
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: 'rgba(255, 132, 0, 1)',
borderWidth: 2
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [480, 590, 610, 680, 700, 750] // 6-11月实际数据
}
],
// 1: 经营性利润
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [150, 180, 160, 200, 190, 220]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [140, 170, 180, 190, 210, 200]
}
],
// 2: 利润总额
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [120, 150, 140, 170, 160, 190]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [110, 140, 150, 160, 180, 170]
}
],
// 3: 毛利率(百分比数据)
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [35, 36, 35.5, 37, 36.5, 38]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [34, 35.5, 36, 36.2, 37, 37.5]
}
]
// 定义按钮与 line 数据中 key 的映射关系
buttonToDataKey: [
{name:'营业收入',unit:'万元'},
{name:'经营性利润',unit:'万元'},
{name:'利润总额',unit:'万元'},
{name:'毛利率',unit:'%'},
{name:'应收账款',unit:'万元'},
{name:'存货',unit:'万元'}
]
};
},
computed: {
// 根据当前激活的按钮,返回对应的 series 数据
// 根据当前激活的按钮,动态生成对应的 series 数据
currentSeries() {
return this.seriesMap[this.activeButton] || [];
const dataKey = this.buttonToDataKey[this.activeButton].name;
const chartData = this.line[dataKey];
if (!chartData) {
return {};
}
// 提取目标和实际数据的值
const targetDataValues = Object.values(chartData.target || {});
const realDataValues = Object.values(chartData.real || {});
let obj = {
unit: this.buttonToDataKey[this.activeButton].unit,
series:[
{
name: '预算',
type: 'line',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: { color: 'rgba(91, 230, 190, 1)', borderColor: 'rgba(91, 230, 190, 1)', borderWidth: 2 },
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: targetDataValues
},
{
name: '实际',
type: 'line',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: { color: 'rgba(255, 132, 0, 1)', borderColor: 'rgba(255, 132, 0, 1)', borderWidth: 2 },
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: realDataValues
}
]}
return obj;
},
// 提取 x 轴数据(日期)
xAxisData() {
const dataKey = this.buttonToDataKey[this.activeButton].name;
const chartData = this.line[dataKey];
// 使用 'target' 的键作为 x 轴,如果 'target' 不存在,则使用 'real' 的键
if (chartData && chartData.target) {
return Object.keys(chartData.target);
} else if (chartData && chartData.real) {
return Object.keys(chartData.real);
}
return [];
}
},
methods: {}
@@ -311,7 +217,7 @@ export default {
display: flex;
position: relative;
gap: 2px;
width: 252px;
width: 350px;
align-items: center;
height: 24px;
background: #ecf4fe;
@@ -325,18 +231,27 @@ export default {
}
.lineOne {
top: 5px;
top: 6px;
left: 59px;
}
.lineTwo {
top: 5px;
left: 134px;
top: 6px;
left: 131px;
}
.lineThree {
top: 5px;
left: 193px;
top: 6px;
left: 190px;
}
.lineFour {
top: 6px;
left: 238px;
}
.lineFive {
top: 6px;
left: 302px;
}
.item-button {

View File

@@ -16,7 +16,17 @@
<span class="title-text">{{ nameTwo }}</span>
</div>
<span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span>
<!-- <span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span> -->
<div 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="container-body">
@@ -40,10 +50,18 @@ export default {
return {
// 初始状态左侧不透明1右侧透明0.3
isLeftTransparent: true, // 左侧透明度状态true=1false=0.3
isRightTransparent: false // 右侧透明度状态true=1false=0.3
isRightTransparent: false, // 右侧透明度状态true=1false=0.3
activeTab: 'month', // 默认激活的Tab为月度
};
},
methods: {
handleTabClick(tabType) {
this.activeTab = tabType;
// 向父组件派发Tab切换事件传递当前选中的Tab类型
this.$emit('tabChange', tabType);
// 可选:同时传递更详细的信息(如标签名)
// this.$emit('tabChange', { type: tabType, name: tabType === 'month' ? '月度' : '累计' });
},
// 点击左侧标题:左侧保持不透明,右侧变透明,并派发采购标签事件
handleLeftClick() {
this.isLeftTransparent = true; // 左侧不透明
@@ -151,7 +169,7 @@ export default {
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
position: absolute;
top: 0;
right: 0;
left: 240px;
z-index: 10;
overflow: hidden;
cursor: pointer;
@@ -258,4 +276,41 @@ export default {
// text-align: left;
// font-style: normal;
}
.tab-group {
display: inline-flex;
position: absolute;
right: 5%;
top:20px;
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>

View File

@@ -1,7 +1,7 @@
<template>
<!-- 显示累计值并绑定颜色类 -->
<div class="accumulated-value" :class="injectData.status">
{{ injectData.accumulated }} <!-- 显示累计数据 -->
{{ injectData.total }} <!-- 显示累计数据 -->
</div>
</template>

View File

@@ -0,0 +1,157 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 216px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
lineData: {
type: Object,
default: () => ({}),
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
lineData: {
handler() {
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const entries = Object.entries(this.lineData);
entries.sort((item1, item2) => item2[1] - item1[1]);
const sortedObj = Object.fromEntries(entries);
let xData = Object.keys(sortedObj);
let yData = Object.values(sortedObj);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
},
grid: {
top: 30,
bottom: 30,
right: 20,
left: 60,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data:xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
name: 'kcal/kg',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
],
series: [
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 40,
label: {
show: true,
position: 'top'
},
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: yData
}
]
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,125 @@
<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="width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);height: 844px;padding: 26px 0px;">
<!-- 动态生成12个月的容器优化flex布局缩小行间距 -->
<div class="month-list">
<!-- 循环生成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: 100px;
height: 57px;
border-radius: 4px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 20px;
color: rgba(0, 0, 0, 0.85);
line-height: 57px;
text-align: center;
font-style: normal;
cursor: pointer; // 鼠标悬浮手型
transition: all 0.2s ease; // 过渡效果,样式切换更平滑
border: 2px solid transparent; // 透明边框,避免选中时布局偏移
margin: 0 auto 10px; // 清除默认外边距,进一步缩小缝隙
}
// 有数据的样式(背景色#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>

Some files were not shown because too many files have changed in this diff Show More