Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6114d989bc | |||
| 4ec6164b5c | |||
| 73ebc8b1e7 | |||
| 20d376e93e | |||
| a4409e5afe | |||
| da5a5c0d8e | |||
| a4df18890d | |||
| f432ff093c | |||
| b4a1b736c1 | |||
| a2ff005437 | |||
| 8b900ed7a1 | |||
| 20d2df9373 | |||
| 6313be9c52 | |||
| 75f745cdb2 | |||
| 3e89c0b16c | |||
| 6cfa8faa48 | |||
| a7235b05e6 | |||
| f07080397d | |||
| 10ffb2e194 |
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||
<option name="progress" value="1.0" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/screen.iml" filepath="$PROJECT_DIR$/.idea/screen.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/screen.iml
generated
Normal file
9
.idea/screen.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
982
package-lock.json
generated
982
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@kjgl77/datav-vue3": "^1.0.0",
|
||||
"axios": "^1.6.7",
|
||||
"cors": "^2.8.6",
|
||||
"echarts": "^5.6.0",
|
||||
|
||||
418
scripts/backfill-province.cjs
Normal file
418
scripts/backfill-province.cjs
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* 客户省份回填脚本
|
||||
*
|
||||
* 从 crm_customer.address 字段中提取省份信息,回填到 province 字段。
|
||||
* 匹配优先级:省份前缀 > 地级市名 > 县级市/区名
|
||||
*
|
||||
* 用法: node scripts/backfill-province.js
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2')
|
||||
|
||||
// ===== 省份名称(与 china.json 完全一致) =====
|
||||
const PROVINCES = [
|
||||
'北京市', '天津市', '上海市', '重庆市',
|
||||
'河北省', '山西省', '辽宁省', '吉林省', '黑龙江省',
|
||||
'江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省',
|
||||
'河南省', '湖北省', '湖南省', '广东省', '海南省',
|
||||
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
|
||||
'台湾省',
|
||||
'内蒙古自治区', '广西壮族自治区', '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区',
|
||||
'香港特别行政区', '澳门特别行政区'
|
||||
]
|
||||
|
||||
// ===== 省份简称 → 全称映射 =====
|
||||
const SHORT_PROVINCE = {
|
||||
'北京': '北京市', '天津': '天津市', '上海': '上海市', '重庆': '重庆市',
|
||||
'河北': '河北省', '山西': '山西省', '辽宁': '辽宁省', '吉林': '吉林省',
|
||||
'黑龙江': '黑龙江省', '江苏': '江苏省', '浙江': '浙江省', '安徽': '安徽省',
|
||||
'福建': '福建省', '江西': '江西省', '山东': '山东省', '河南': '河南省',
|
||||
'湖北': '湖北省', '湖南': '湖南省', '广东': '广东省', '海南': '海南省',
|
||||
'四川': '四川省', '贵州': '贵州省', '云南': '云南省', '陕西': '陕西省',
|
||||
'甘肃': '甘肃省', '青海': '青海省', '台湾': '台湾省',
|
||||
'内蒙古': '内蒙古自治区', '广西': '广西壮族自治区',
|
||||
'西藏': '西藏自治区', '宁夏': '宁夏回族自治区', '新疆': '新疆维吾尔自治区',
|
||||
'香港': '香港特别行政区', '澳门': '澳门特别行政区'
|
||||
}
|
||||
|
||||
// ===== 城市 → 省份映射(覆盖全部地级行政区和常见县级市) =====
|
||||
const CITY_PROVINCE = {
|
||||
|
||||
// ----- 北京市 -----
|
||||
'北京': '北京市',
|
||||
|
||||
// ----- 天津市 -----
|
||||
'天津': '天津市',
|
||||
|
||||
// ----- 上海市 -----
|
||||
'上海': '上海市',
|
||||
|
||||
// ----- 重庆市 -----
|
||||
'重庆': '重庆市',
|
||||
|
||||
// ===== 河北省 =====
|
||||
'石家庄': '河北省', '唐山': '河北省', '秦皇岛': '河北省', '邯郸': '河北省',
|
||||
'邢台': '河北省', '保定': '河北省', '张家口': '河北省', '承德': '河北省',
|
||||
'沧州': '河北省', '廊坊': '河北省', '衡水': '河北省',
|
||||
|
||||
// ===== 山西省 =====
|
||||
'太原': '山西省', '大同': '山西省', '阳泉': '山西省', '长治': '山西省',
|
||||
'晋城': '山西省', '朔州': '山西省', '晋中': '山西省', '运城': '山西省',
|
||||
'忻州': '山西省', '临汾': '山西省', '吕梁': '山西省',
|
||||
|
||||
// ===== 辽宁省 =====
|
||||
'沈阳': '辽宁省', '大连': '辽宁省', '鞍山': '辽宁省', '抚顺': '辽宁省',
|
||||
'本溪': '辽宁省', '丹东': '辽宁省', '锦州': '辽宁省', '营口': '辽宁省',
|
||||
'阜新': '辽宁省', '辽阳': '辽宁省', '盘锦': '辽宁省', '铁岭': '辽宁省',
|
||||
'朝阳': '辽宁省', '葫芦岛': '辽宁省',
|
||||
|
||||
// ===== 吉林省 =====
|
||||
'长春': '吉林省', '吉林': '吉林省', '四平': '吉林省', '辽源': '吉林省',
|
||||
'通化': '吉林省', '白山': '吉林省', '松原': '吉林省', '白城': '吉林省',
|
||||
'延边': '吉林省',
|
||||
|
||||
// ===== 黑龙江省 =====
|
||||
'哈尔滨': '黑龙江省', '齐齐哈尔': '黑龙江省', '鸡西': '黑龙江省',
|
||||
'鹤岗': '黑龙江省', '双鸭山': '黑龙江省', '大庆': '黑龙江省',
|
||||
'伊春': '黑龙江省', '佳木斯': '黑龙江省', '七台河': '黑龙江省',
|
||||
'牡丹江': '黑龙江省', '黑河': '黑龙江省', '绥化': '黑龙江省',
|
||||
'大兴安岭': '黑龙江省',
|
||||
|
||||
// ===== 江苏省 =====
|
||||
'南京': '江苏省', '无锡': '江苏省', '徐州': '江苏省', '常州': '江苏省',
|
||||
'苏州': '江苏省', '南通': '江苏省', '连云港': '江苏省', '淮安': '江苏省',
|
||||
'盐城': '江苏省', '扬州': '江苏省', '镇江': '江苏省', '泰州': '江苏省',
|
||||
'宿迁': '江苏省',
|
||||
// 江苏县级市/县
|
||||
'常熟': '江苏省', '张家港': '江苏省', '昆山': '江苏省', '太仓': '江苏省',
|
||||
'江阴': '江苏省', '宜兴': '江苏省', '丹阳': '江苏省', '扬中': '江苏省',
|
||||
'句容': '江苏省', '靖江': '江苏省', '泰兴': '江苏省', '兴化': '江苏省',
|
||||
'沭阳': '江苏省', '泗阳': '江苏省', '泗洪': '江苏省', '沛县': '江苏省',
|
||||
'丰县': '江苏省', '睢宁': '江苏省', '东海': '江苏省', '灌云': '江苏省',
|
||||
'灌南': '江苏省', '涟水': '江苏省', '洪泽': '江苏省', '盱眙': '江苏省',
|
||||
'金湖': '江苏省', '滨海': '江苏省', '阜宁': '江苏省', '射阳': '江苏省',
|
||||
'建湖': '江苏省', '东台': '江苏省', '宝应': '江苏省', '仪征': '江苏省',
|
||||
'高邮': '江苏省', '溧阳': '江苏省', '如东': '江苏省', '启东': '江苏省',
|
||||
'如皋': '江苏省', '海门': '江苏省', '海安': '江苏省',
|
||||
|
||||
// ===== 浙江省 =====
|
||||
'杭州': '浙江省', '宁波': '浙江省', '温州': '浙江省', '嘉兴': '浙江省',
|
||||
'湖州': '浙江省', '绍兴': '浙江省', '金华': '浙江省', '衢州': '浙江省',
|
||||
'舟山': '浙江省', '台州': '浙江省', '丽水': '浙江省',
|
||||
// 浙江县级市/县
|
||||
'海宁': '浙江省', '嘉善': '浙江省', '平湖': '浙江省', '桐乡': '浙江省',
|
||||
'德清': '浙江省', '长兴': '浙江省', '安吉': '浙江省', '诸暨': '浙江省',
|
||||
'上虞': '浙江省', '嵊州': '浙江省', '新昌': '浙江省', '兰溪': '浙江省',
|
||||
'义乌': '浙江省', '东阳': '浙江省', '永康': '浙江省', '武义': '浙江省',
|
||||
'浦江': '浙江省', '江山': '浙江省', '常山': '浙江省', '开化': '浙江省',
|
||||
'龙游': '浙江省', '临海': '浙江省', '温岭': '浙江省', '玉环': '浙江省',
|
||||
'三门': '浙江省', '天台': '浙江省', '仙居': '浙江省', '青田': '浙江省',
|
||||
'缙云': '浙江省', '遂昌': '浙江省', '松阳': '浙江省', '云和': '浙江省',
|
||||
'庆元': '浙江省', '景宁': '浙江省', '龙泉': '浙江省',
|
||||
|
||||
// ===== 安徽省 =====
|
||||
'合肥': '安徽省', '芜湖': '安徽省', '蚌埠': '安徽省', '淮南': '安徽省',
|
||||
'马鞍山': '安徽省', '淮北': '安徽省', '铜陵': '安徽省', '安庆': '安徽省',
|
||||
'黄山': '安徽省', '滁州': '安徽省', '阜阳': '安徽省', '宿州': '安徽省',
|
||||
'六安': '安徽省', '亳州': '安徽省', '池州': '安徽省', '宣城': '安徽省',
|
||||
// 安徽县级市/县
|
||||
'界首': '安徽省',
|
||||
|
||||
// ===== 福建省 =====
|
||||
'福州': '福建省', '厦门': '福建省', '莆田': '福建省', '三明': '福建省',
|
||||
'泉州': '福建省', '漳州': '福建省', '南平': '福建省', '龙岩': '福建省',
|
||||
'宁德': '福建省',
|
||||
|
||||
// ===== 江西省 =====
|
||||
'南昌': '江西省', '景德镇': '江西省', '萍乡': '江西省', '九江': '江西省',
|
||||
'新余': '江西省', '鹰潭': '江西省', '赣州': '江西省', '吉安': '江西省',
|
||||
'宜春': '江西省', '抚州': '江西省', '上饶': '江西省',
|
||||
|
||||
// ===== 山东省 =====
|
||||
'济南': '山东省', '青岛': '山东省', '淄博': '山东省', '枣庄': '山东省',
|
||||
'东营': '山东省', '烟台': '山东省', '潍坊': '山东省', '济宁': '山东省',
|
||||
'泰安': '山东省', '威海': '山东省', '日照': '山东省', '临沂': '山东省',
|
||||
'德州': '山东省', '聊城': '山东省', '滨州': '山东省', '菏泽': '山东省',
|
||||
// 山东县级市/县
|
||||
'莱芜': '山东省', '博兴': '山东省', '冠县': '山东省', '梁山': '山东省',
|
||||
'曹县': '山东省', '滕州': '山东省', '肥城': '山东省', '新泰': '山东省',
|
||||
'宁阳': '山东省', '东平': '山东省', '沂水': '山东省', '沂南': '山东省',
|
||||
'莒南': '山东省', '临沭': '山东省', '郯城': '山东省', '费县': '山东省',
|
||||
'平邑': '山东省', '蒙阴': '山东省', '沂源': '山东省', '临朐': '山东省',
|
||||
'昌乐': '山东省', '青州': '山东省', '诸城': '山东省', '寿光': '山东省',
|
||||
'安丘': '山东省', '高密': '山东省', '昌邑': '山东省', '曲阜': '山东省',
|
||||
'兖州': '山东省', '邹城': '山东省', '微山': '山东省', '鱼台': '山东省',
|
||||
'金乡': '山东省', '嘉祥': '山东省', '汶上': '山东省', '泗水': '山东省',
|
||||
'梁山县': '山东省',
|
||||
|
||||
// ===== 河南省 =====
|
||||
'郑州': '河南省', '开封': '河南省', '洛阳': '河南省', '平顶山': '河南省',
|
||||
'安阳': '河南省', '鹤壁': '河南省', '新乡': '河南省', '焦作': '河南省',
|
||||
'濮阳': '河南省', '许昌': '河南省', '漯河': '河南省', '三门峡': '河南省',
|
||||
'南阳': '河南省', '商丘': '河南省', '信阳': '河南省', '周口': '河南省',
|
||||
'驻马店': '河南省',
|
||||
// 河南县级市/县
|
||||
'济源': '河南省', '巩义': '河南省', '新郑': '河南省', '新密': '河南省',
|
||||
'登封': '河南省', '荥阳': '河南省', '中牟': '河南省', '偃师': '河南省',
|
||||
'孟津': '河南省', '新安': '河南省', '栾川': '河南省', '嵩县': '河南省',
|
||||
'汝阳': '河南省', '宜阳': '河南省', '洛宁': '河南省', '伊川': '河南省',
|
||||
'林州': '河南省', '滑县': '河南省', '内黄': '河南省', '汤阴': '河南省',
|
||||
'浚县': '河南省', '淇县': '河南省', '原阳': '河南省', '延津': '河南省',
|
||||
'封丘': '河南省', '长垣': '河南省', '卫辉': '河南省', '辉县': '河南省',
|
||||
'修武': '河南省', '博爱': '河南省', '武陟': '河南省', '温县': '河南省',
|
||||
'沁阳': '河南省', '孟州': '河南省', '南乐': '河南省', '范县': '河南省',
|
||||
'台前': '河南省', '禹州': '河南省', '长葛': '河南省', '鄢陵': '河南省',
|
||||
'襄城': '河南省', '舞阳': '河南省', '临颍': '河南省', '义马': '河南省',
|
||||
'灵宝': '河南省', '卢氏': '河南省', '邓州': '河南省', '镇平': '河南省',
|
||||
'内乡': '河南省', '淅川': '河南省', '西峡': '河南省', '唐河': '河南省',
|
||||
'新野': '河南省', '方城': '河南省', '社旗': '河南省', '桐柏': '河南省',
|
||||
'永城': '河南省', '虞城': '河南省', '民权': '河南省', '宁陵': '河南省',
|
||||
'睢县': '河南省', '夏邑': '河南省', '柘城': '河南省', '罗山': '河南省',
|
||||
'光山': '河南省', '新县': '河南省', '商城': '河南省', '固始': '河南省',
|
||||
'潢川': '河南省', '淮滨': '河南省', '息县': '河南省', '扶沟': '河南省',
|
||||
'西华': '河南省', '商水': '河南省', '沈丘': '河南省', '郸城': '河南省',
|
||||
'淮阳': '河南省', '太康': '河南省', '鹿邑': '河南省', '项城': '河南省',
|
||||
'西平': '河南省', '上蔡': '河南省', '平舆': '河南省', '正阳': '河南省',
|
||||
'确山': '河南省', '泌阳': '河南省', '汝南': '河南省', '遂平': '河南省',
|
||||
'新蔡': '河南省',
|
||||
|
||||
// ===== 湖北省 =====
|
||||
'武汉': '湖北省', '黄石': '湖北省', '十堰': '湖北省', '宜昌': '湖北省',
|
||||
'襄阳': '湖北省', '鄂州': '湖北省', '荆门': '湖北省', '孝感': '湖北省',
|
||||
'荆州': '湖北省', '黄冈': '湖北省', '咸宁': '湖北省', '随州': '湖北省',
|
||||
'恩施': '湖北省',
|
||||
|
||||
// ===== 湖南省 =====
|
||||
'长沙': '湖南省', '株洲': '湖南省', '湘潭': '湖南省', '衡阳': '湖南省',
|
||||
'邵阳': '湖南省', '岳阳': '湖南省', '常德': '湖南省', '张家界': '湖南省',
|
||||
'益阳': '湖南省', '郴州': '湖南省', '永州': '湖南省', '怀化': '湖南省',
|
||||
'娄底': '湖南省', '湘西': '湖南省',
|
||||
|
||||
// ===== 广东省 =====
|
||||
'广州': '广东省', '韶关': '广东省', '深圳': '广东省', '珠海': '广东省',
|
||||
'汕头': '广东省', '佛山': '广东省', '江门': '广东省', '湛江': '广东省',
|
||||
'茂名': '广东省', '肇庆': '广东省', '惠州': '广东省', '梅州': '广东省',
|
||||
'汕尾': '广东省', '河源': '广东省', '阳江': '广东省', '清远': '广东省',
|
||||
'东莞': '广东省', '中山': '广东省', '潮州': '广东省', '揭阳': '广东省',
|
||||
'云浮': '广东省',
|
||||
|
||||
// ===== 海南省 =====
|
||||
'海口': '海南省', '三亚': '海南省', '三沙': '海南省', '儋州': '海南省',
|
||||
|
||||
// ===== 四川省 =====
|
||||
'成都': '四川省', '自贡': '四川省', '攀枝花': '四川省', '泸州': '四川省',
|
||||
'德阳': '四川省', '绵阳': '四川省', '广元': '四川省', '遂宁': '四川省',
|
||||
'内江': '四川省', '乐山': '四川省', '南充': '四川省', '眉山': '四川省',
|
||||
'宜宾': '四川省', '广安': '四川省', '达州': '四川省', '雅安': '四川省',
|
||||
'巴中': '四川省', '资阳': '四川省', '阿坝': '四川省', '甘孜': '四川省',
|
||||
'凉山': '四川省',
|
||||
|
||||
// ===== 贵州省 =====
|
||||
'贵阳': '贵州省', '六盘水': '贵州省', '遵义': '贵州省', '安顺': '贵州省',
|
||||
'毕节': '贵州省', '铜仁': '贵州省', '黔西南': '贵州省', '黔东南': '贵州省',
|
||||
'黔南': '贵州省',
|
||||
|
||||
// ===== 云南省 =====
|
||||
'昆明': '云南省', '曲靖': '云南省', '玉溪': '云南省', '保山': '云南省',
|
||||
'昭通': '云南省', '丽江': '云南省', '普洱': '云南省', '临沧': '云南省',
|
||||
'楚雄': '云南省', '红河': '云南省', '文山': '云南省', '西双版纳': '云南省',
|
||||
'大理': '云南省', '德宏': '云南省', '怒江': '云南省', '迪庆': '云南省',
|
||||
|
||||
// ===== 陕西省 =====
|
||||
'西安': '陕西省', '铜川': '陕西省', '宝鸡': '陕西省', '咸阳': '陕西省',
|
||||
'渭南': '陕西省', '延安': '陕西省', '汉中': '陕西省', '榆林': '陕西省',
|
||||
'安康': '陕西省', '商洛': '陕西省',
|
||||
|
||||
// ===== 甘肃省 =====
|
||||
'兰州': '甘肃省', '嘉峪关': '甘肃省', '金昌': '甘肃省', '白银': '甘肃省',
|
||||
'天水': '甘肃省', '武威': '甘肃省', '张掖': '甘肃省', '平凉': '甘肃省',
|
||||
'酒泉': '甘肃省', '庆阳': '甘肃省', '定西': '甘肃省', '陇南': '甘肃省',
|
||||
'临夏': '甘肃省', '甘南': '甘肃省',
|
||||
|
||||
// ===== 青海省 =====
|
||||
'西宁': '青海省', '海东': '青海省', '海北': '青海省', '黄南': '青海省',
|
||||
'海南州': '青海省', '果洛': '青海省', '玉树': '青海省', '海西': '青海省',
|
||||
|
||||
// ===== 台湾省 =====
|
||||
'台北': '台湾省', '高雄': '台湾省', '台中': '台湾省',
|
||||
|
||||
// ===== 内蒙古自治区 =====
|
||||
'呼和浩特': '内蒙古自治区', '包头': '内蒙古自治区', '乌海': '内蒙古自治区',
|
||||
'赤峰': '内蒙古自治区', '通辽': '内蒙古自治区', '鄂尔多斯': '内蒙古自治区',
|
||||
'呼伦贝尔': '内蒙古自治区', '巴彦淖尔': '内蒙古自治区', '乌兰察布': '内蒙古自治区',
|
||||
'兴安': '内蒙古自治区', '锡林郭勒': '内蒙古自治区', '阿拉善': '内蒙古自治区',
|
||||
|
||||
// ===== 广西壮族自治区 =====
|
||||
'南宁': '广西壮族自治区', '柳州': '广西壮族自治区', '桂林': '广西壮族自治区',
|
||||
'梧州': '广西壮族自治区', '北海': '广西壮族自治区', '防城港': '广西壮族自治区',
|
||||
'钦州': '广西壮族自治区', '贵港': '广西壮族自治区', '玉林': '广西壮族自治区',
|
||||
'百色': '广西壮族自治区', '贺州': '广西壮族自治区', '河池': '广西壮族自治区',
|
||||
'来宾': '广西壮族自治区', '崇左': '广西壮族自治区',
|
||||
|
||||
// ===== 西藏自治区 =====
|
||||
'拉萨': '西藏自治区', '日喀则': '西藏自治区', '昌都': '西藏自治区',
|
||||
'林芝': '西藏自治区', '山南': '西藏自治区', '那曲': '西藏自治区',
|
||||
'阿里': '西藏自治区',
|
||||
|
||||
// ===== 宁夏回族自治区 =====
|
||||
'银川': '宁夏回族自治区', '石嘴山': '宁夏回族自治区', '吴忠': '宁夏回族自治区',
|
||||
'固原': '宁夏回族自治区', '中卫': '宁夏回族自治区',
|
||||
|
||||
// ===== 新疆维吾尔自治区 =====
|
||||
'乌鲁木齐': '新疆维吾尔自治区', '克拉玛依': '新疆维吾尔自治区',
|
||||
'吐鲁番': '新疆维吾尔自治区', '哈密': '新疆维吾尔自治区',
|
||||
'昌吉': '新疆维吾尔自治区', '博尔塔拉': '新疆维吾尔自治区',
|
||||
'巴音郭楞': '新疆维吾尔自治区', '阿克苏': '新疆维吾尔自治区',
|
||||
'克孜勒苏': '新疆维吾尔自治区', '喀什': '新疆维吾尔自治区',
|
||||
'和田': '新疆维吾尔自治区', '伊犁': '新疆维吾尔自治区',
|
||||
'塔城': '新疆维吾尔自治区', '阿勒泰': '新疆维吾尔自治区',
|
||||
'石河子': '新疆维吾尔自治区', '阿拉尔': '新疆维吾尔自治区',
|
||||
'图木舒克': '新疆维吾尔自治区', '五家渠': '新疆维吾尔自治区',
|
||||
'北屯': '新疆维吾尔自治区', '铁门关': '新疆维吾尔自治区',
|
||||
'双河': '新疆维吾尔自治区', '可克达拉': '新疆维吾尔自治区',
|
||||
'昆玉': '新疆维吾尔自治区', '胡杨河': '新疆维吾尔自治区',
|
||||
|
||||
// ===== 香港特别行政区 =====
|
||||
'香港': '香港特别行政区',
|
||||
|
||||
// ===== 澳门特别行政区 =====
|
||||
'澳门': '澳门特别行政区',
|
||||
}
|
||||
|
||||
/**
|
||||
* 从地址中提取省份名称
|
||||
*/
|
||||
function extractProvince(address) {
|
||||
if (!address) return ''
|
||||
|
||||
// 清理地址前缀
|
||||
let addr = address.trim()
|
||||
// 去掉 "地址:" 等前缀
|
||||
addr = addr.replace(/^地址[::]?\s*/, '')
|
||||
// 去掉首尾空格
|
||||
addr = addr.trim()
|
||||
|
||||
// ---- 特殊模式 1: "中国(山东)自由贸易试验区..." ----
|
||||
const chinaMatch = addr.match(/^中国\((\S+?)\)/)
|
||||
if (chinaMatch) {
|
||||
const shortName = chinaMatch[1]
|
||||
if (SHORT_PROVINCE[shortName]) return SHORT_PROVINCE[shortName]
|
||||
}
|
||||
|
||||
// ---- 模式 2: 地址以完整省份名开头 ----
|
||||
// 按长度降序排序(避免"宁夏回族自治区"被"宁夏"提前匹配)
|
||||
const sortedProvinces = [...PROVINCES].sort((a, b) => b.length - a.length)
|
||||
for (const p of sortedProvinces) {
|
||||
if (addr.startsWith(p)) return p
|
||||
}
|
||||
|
||||
// ---- 模式 3: 地址以省份简称开头(如"山东济南市"、"河南洛阳") ----
|
||||
const sortedShorts = Object.keys(SHORT_PROVINCE).sort((a, b) => b.length - a.length)
|
||||
for (const s of sortedShorts) {
|
||||
if (addr.startsWith(s)) return SHORT_PROVINCE[s]
|
||||
}
|
||||
|
||||
// ---- 模式 4: 地址以城市名开头 ----
|
||||
// 按长度降序排序,避免"张家港"被"张家"错误匹配
|
||||
const sortedCities = Object.keys(CITY_PROVINCE).sort((a, b) => b.length - a.length)
|
||||
for (const city of sortedCities) {
|
||||
if (addr.startsWith(city)) return CITY_PROVINCE[city]
|
||||
}
|
||||
|
||||
// ---- 模式 5: 地址以区号/代码开头(如 [370000/370800/370829]) ----
|
||||
// 行政区划代码前两位 = 省代码
|
||||
const codeMatch = addr.match(/^\[?(\d{2})/)
|
||||
if (codeMatch) {
|
||||
const codeMap = {
|
||||
'11': '北京市', '12': '天津市', '13': '河北省', '14': '山西省',
|
||||
'15': '内蒙古自治区', '21': '辽宁省', '22': '吉林省', '23': '黑龙江省',
|
||||
'31': '上海市', '32': '江苏省', '33': '浙江省', '34': '安徽省',
|
||||
'35': '福建省', '36': '江西省', '37': '山东省', '41': '河南省',
|
||||
'42': '湖北省', '43': '湖南省', '44': '广东省', '45': '广西壮族自治区',
|
||||
'46': '海南省', '50': '重庆市', '51': '四川省', '52': '贵州省',
|
||||
'53': '云南省', '54': '西藏自治区', '61': '陕西省', '62': '甘肃省',
|
||||
'63': '青海省', '64': '宁夏回族自治区', '65': '新疆维吾尔自治区',
|
||||
'71': '台湾省', '81': '香港特别行政区', '82': '澳门特别行政区',
|
||||
}
|
||||
const code = codeMatch[1]
|
||||
if (codeMap[code]) return codeMap[code]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// ---- 主流程 ----
|
||||
async function main() {
|
||||
const conn = mysql.createConnection({
|
||||
host: '140.143.206.120', port: 13306,
|
||||
user: 'klp', password: 'KeLunPu@123',
|
||||
database: 'klp-oa-test', connectTimeout: 10000,
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: true
|
||||
})
|
||||
|
||||
const connPromise = conn.promise()
|
||||
|
||||
// 查询需要回填的客户
|
||||
const [rows] = await connPromise.query(
|
||||
"SELECT customer_id, company_name, address FROM crm_customer WHERE del_flag = 0 AND (province IS NULL OR province = '') AND address IS NOT NULL AND address != '' ORDER BY customer_id"
|
||||
)
|
||||
|
||||
console.log(`共 ${rows.length} 条客户记录需要回填省份\n`)
|
||||
|
||||
let matched = 0
|
||||
let unmatched = 0
|
||||
const unmatchedList = []
|
||||
const updates = []
|
||||
|
||||
for (const row of rows) {
|
||||
const province = extractProvince(row.address)
|
||||
if (province) {
|
||||
updates.push({ customer_id: row.customer_id, province, company_name: row.company_name, address: row.address })
|
||||
matched++
|
||||
} else {
|
||||
unmatched++
|
||||
unmatchedList.push({ company_name: row.company_name, address: row.address })
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`匹配成功: ${matched}`)
|
||||
console.log(`无法匹配: ${unmatched}\n`)
|
||||
|
||||
if (unmatchedList.length > 0) {
|
||||
console.log('=== 无法匹配的地址 ===')
|
||||
unmatchedList.forEach(u => console.log(` ${u.company_name} | ${u.address}`))
|
||||
console.log('')
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
if (updates.length > 0) {
|
||||
console.log('正在回填数据库...')
|
||||
const sql = 'UPDATE crm_customer SET province = ? WHERE customer_id = ?'
|
||||
let success = 0
|
||||
for (const u of updates) {
|
||||
try {
|
||||
await connPromise.query(sql, [u.province, u.customer_id])
|
||||
success++
|
||||
} catch (err) {
|
||||
console.log(` 更新失败 ID=${u.customer_id}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
console.log(`回填完成: ${success}/${updates.length} 条已更新`)
|
||||
}
|
||||
|
||||
// 统计各省份分布
|
||||
const [stats] = await connPromise.query(
|
||||
"SELECT province, COUNT(*) as cnt FROM crm_customer WHERE del_flag = 0 AND province != '' GROUP BY province ORDER BY cnt DESC"
|
||||
)
|
||||
console.log('\n=== 省份分布 ===')
|
||||
stats.forEach(s => console.log(` ${s.province}: ${s.cnt}`))
|
||||
|
||||
await connPromise.end()
|
||||
console.log('\n完成!')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('脚本执行失败:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -813,7 +813,8 @@ app.get('/api/screens', async (req, res) => {
|
||||
{ id: 2, name: '订单大屏', path: '/dashboard/order', status: 'stopped', createTime: '2026-05-11 14:30:00' },
|
||||
{ id: 3, name: '成本大屏', path: '/dashboard/cost', status: 'running', createTime: '2026-05-12 09:00:00' },
|
||||
{ id: 4, name: '能源大屏', path: '/dashboard/energy', status: 'running', createTime: '2026-05-13 11:00:00' },
|
||||
{ id: 5, name: '酸轧数据大屏', path: '/dashboard/acid-rolling', status: 'running', createTime: '2026-05-14 08:00:00' }
|
||||
{ id: 5, name: '酸轧数据大屏', path: '/dashboard/acid-rolling', status: 'running', createTime: '2026-05-14 08:00:00' },
|
||||
{ id: 6, name: '异常钢卷大屏', path: '/abnormal-coil', status: 'running', createTime: '2026-06-03 10:00:00' }
|
||||
]
|
||||
sendResponse(res, screens)
|
||||
})
|
||||
@@ -849,7 +850,8 @@ const mockMenuList = [
|
||||
{ id: 25, path: 'acid-rolling', name: 'AcidRolling', component: 'views/screens/acid-rolling/index.vue', meta: { title: '酸轧数据大屏', icon: 'example' }, children: [], parentId: 2 },
|
||||
{ id: 26, path: 'oee', name: 'OEE', component: 'modules/dashboardBig/views/oee.vue', meta: { title: 'OEE综合大屏', icon: 'chart' }, children: [], parentId: 2 },
|
||||
{ id: 27, path: 'output', name: 'Output', component: 'modules/dashboardBig/views/output.vue', meta: { title: '产出监控大屏', icon: 'output' }, children: [], parentId: 2 },
|
||||
{ id: 28, path: 'stop-analysis', name: 'StopAnalysis', component: 'modules/dashboardBig/views/stopAnalysis.vue', meta: { title: '停机分析大屏', icon: 'stop' }, children: [], parentId: 2 }
|
||||
{ id: 28, path: 'stop-analysis', name: 'StopAnalysis', component: 'modules/dashboardBig/views/stopAnalysis.vue', meta: { title: '停机分析大屏', icon: 'stop' }, children: [], parentId: 2 },
|
||||
{ id: 29, path: 'abnormal-coil', name: 'AbnormalCoil', component: 'views/screens/abnormal-coil/index.vue', meta: { title: '异常钢卷大屏', icon: 'example' }, children: [], parentId: 2 }
|
||||
], parentId: 0 },
|
||||
{ id: 3, path: '/screen-manage', name: 'ScreenManage', component: '', meta: { title: '大屏管理', icon: 'pie-chart' }, children: [
|
||||
{ id: 31, path: '', name: 'ScreenList', component: 'views/screens/list.vue', meta: { title: '大屏列表', icon: 'list' }, children: [], parentId: 3 },
|
||||
|
||||
@@ -10,11 +10,31 @@ const service = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
// 从 Cookies 获取 Token(与 klp-ui 保持一致)
|
||||
const getToken = () => {
|
||||
const name = 'Admin-Token'
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) return parts.pop().split(';').shift()
|
||||
return null
|
||||
}
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
let token = getToken()
|
||||
// ========== 测试用:临时硬编码 Token ==========
|
||||
// 如果没有从 Cookies 获取到 Token,使用测试 Token
|
||||
if (!token) {
|
||||
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxIiwicm5TdHIiOiJGM2w5Nm5ncGV4ZDA4d1BUYWlvVmFreWhCUDdlc2gyeCIsInVzZXJJZCI6MX0.aP06S-5aJtukzMjmYW3d5zK3v10TOKcdNpROniv5lr8'
|
||||
}
|
||||
// ========== 测试用结束 ==========
|
||||
|
||||
console.log('请求URL:', config.url)
|
||||
console.log('使用的Token:', token ? '已设置' : '未设置')
|
||||
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
// 尝试两种格式:Bearer 和 直接使用Token
|
||||
config.headers['Authorization'] = token // 直接使用Token(不带Bearer前缀)
|
||||
}
|
||||
return config
|
||||
},
|
||||
@@ -25,15 +45,36 @@ service.interceptors.request.use(
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code !== 200) {
|
||||
console.error('请求失败:', res.message)
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
console.log('响应数据:', response.data)
|
||||
|
||||
let res = response.data
|
||||
|
||||
// 如果响应数据为空
|
||||
if (!res) {
|
||||
console.error('响应数据为空')
|
||||
return Promise.reject(new Error('响应数据为空'))
|
||||
}
|
||||
return res.data
|
||||
|
||||
// 如果是字符串,尝试解析
|
||||
if (typeof res === 'string') {
|
||||
try {
|
||||
res = JSON.parse(res)
|
||||
} catch {
|
||||
console.error('响应数据不是JSON格式:', res)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// 检查code
|
||||
if (res.code !== undefined && res.code !== 200) {
|
||||
console.error('请求失败 [code=' + res.code + ']:', res.message || res.msg)
|
||||
return Promise.reject(new Error(res.message || res.msg || '请求失败'))
|
||||
}
|
||||
|
||||
return res.data !== undefined ? res.data : res
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
console.error('请求错误:', error.response?.data || error.message || error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
149
src/api/sales.js
Normal file
149
src/api/sales.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ===== Mock 数据(后端无认证时兜底) =====
|
||||
const mockSummary = {
|
||||
totalOrderCount: 450,
|
||||
totalSalesAmount: 98600000,
|
||||
completedOrderCount: 320,
|
||||
completedSalesAmount: 72300000,
|
||||
totalUnpaidAmount: 26300000,
|
||||
avgOrderAmount: 219000
|
||||
}
|
||||
|
||||
const mockSalesmanStats = [
|
||||
{ salesmanName: '张伟', totalAmount: 12560000 },
|
||||
{ salesmanName: '李强', totalAmount: 9820000 },
|
||||
{ salesmanName: '王芳', totalAmount: 8750000 },
|
||||
{ salesmanName: '赵磊', totalAmount: 6540000 },
|
||||
{ salesmanName: '刘洋', totalAmount: 5210000 },
|
||||
{ salesmanName: '陈静', totalAmount: 3980000 },
|
||||
{ salesmanName: '杨光', totalAmount: 2850000 }
|
||||
]
|
||||
|
||||
const mockLevelStats = [
|
||||
{ customerLevel: '高', count: 400 },
|
||||
{ customerLevel: '中', count: 44 },
|
||||
{ customerLevel: '低', count: 1 },
|
||||
{ customerLevel: 'VIP', count: 1 }
|
||||
]
|
||||
|
||||
const mockIndustryStats = [
|
||||
{ industry: '制造业', count: 168 },
|
||||
{ industry: '贸易', count: 165 },
|
||||
{ industry: '加工业', count: 115 },
|
||||
{ industry: '互联网', count: 1 }
|
||||
]
|
||||
|
||||
const mockOrders = [
|
||||
{ orderNo: 'ORD20260515001', customer: '周口钢铁', product: '冷轧卷', amount: 125000, status: '生产中', time: '10:30' },
|
||||
{ orderNo: 'ORD20260515002', customer: '南阳重工', product: '镀锌板', amount: 89000, status: '已完成', time: '09:45' },
|
||||
{ orderNo: 'ORD20260515003', customer: '洛阳机械', product: '酸洗板', amount: 156000, status: '待生产', time: '11:20' },
|
||||
{ orderNo: 'ORD20260515004', customer: '开封汽配', product: '冷轧卷', amount: 67000, status: '生产中', time: '08:15' },
|
||||
{ orderNo: 'ORD20260515005', customer: '商丘金属', product: '镀铬板', amount: 45000, status: '已完成', time: '07:30' },
|
||||
{ orderNo: 'ORD20260515006', customer: '山东福安德', product: '镀锌板', amount: 234000, status: '生产中', time: '14:20' },
|
||||
{ orderNo: 'ORD20260515007', customer: '临沂屹钢', product: '冷轧卷', amount: 187000, status: '待生产', time: '13:45' },
|
||||
{ orderNo: 'ORD20260515008', customer: '江苏永腾', product: '酸洗板', amount: 92000, status: '已完成', time: '15:10' },
|
||||
{ orderNo: 'ORD20260515009', customer: '武汉欣航晟', product: '镀锌板', amount: 76000, status: '生产中', time: '16:30' },
|
||||
{ orderNo: 'ORD20260515010', customer: '天津盛盈', product: '冷轧卷', amount: 198000, status: '待生产', time: '17:00' },
|
||||
{ orderNo: 'ORD20260515011', customer: '河南宏之澳', product: '镀铬板', amount: 54000, status: '生产中', time: '09:20' },
|
||||
{ orderNo: 'ORD20260515012', customer: '上海圳洋', product: '镀锌板', amount: 312000, status: '已完成', time: '11:50' },
|
||||
{ orderNo: 'ORD20260515013', customer: '许昌涵博', product: '酸洗板', amount: 43000, status: '待生产', time: '08:40' },
|
||||
{ orderNo: 'ORD20260515014', customer: '济宁钰昌', product: '冷轧卷', amount: 88000, status: '生产中', time: '14:10' },
|
||||
{ orderNo: 'ORD20260515015', customer: '无锡昌德', product: '镀锌板', amount: 165000, status: '已完成', time: '10:00' }
|
||||
]
|
||||
|
||||
const withMock = (apiFn, mockData) => {
|
||||
return async (params) => {
|
||||
try {
|
||||
const res = await apiFn(params)
|
||||
// request.js 在 401 时返回 [],此时降级为 mock
|
||||
if (Array.isArray(res) && res.length === 0) return { data: mockData }
|
||||
return { data: (res && res.data !== undefined) ? res.data : res }
|
||||
} catch {
|
||||
return { data: mockData }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销售汇总指标
|
||||
*/
|
||||
export const getSalesSummary = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/summary', method: 'get', params }),
|
||||
mockSummary
|
||||
)
|
||||
|
||||
/**
|
||||
* 销售员统计排行
|
||||
*/
|
||||
export const getSalesmanStats = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/salesmanStats', method: 'get', params }),
|
||||
mockSalesmanStats
|
||||
)
|
||||
|
||||
/**
|
||||
* 客户等级分布
|
||||
*/
|
||||
export const getCustomerLevelStats = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/customerLevelStats', method: 'get', params }),
|
||||
mockLevelStats
|
||||
)
|
||||
|
||||
/**
|
||||
* 行业分布统计
|
||||
*/
|
||||
export const getIndustryStats = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/industryStats', method: 'get', params }),
|
||||
mockIndustryStats
|
||||
)
|
||||
|
||||
/**
|
||||
* 订单明细列表
|
||||
*/
|
||||
export const getOrderDetails = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/orderDetails', method: 'get', params }),
|
||||
{ rows: mockOrders, total: mockOrders.length }
|
||||
)
|
||||
|
||||
/**
|
||||
* 完整销售报表(一次性返回所有数据)
|
||||
*/
|
||||
export function getFullSalesReport(params) {
|
||||
return request({
|
||||
url: '/crm/salesReport/fullReport',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 省份热力地图数据 =====
|
||||
const mockProvinceStats = [
|
||||
{ name: '山东省', customerCount: 124, orderCount: 2800, salesAmount: 31000000 },
|
||||
{ name: '河南省', customerCount: 103, orderCount: 2300, salesAmount: 25500000 },
|
||||
{ name: '江苏省', customerCount: 35, orderCount: 850, salesAmount: 9500000 },
|
||||
{ name: '天津市', customerCount: 17, orderCount: 420, salesAmount: 4800000 },
|
||||
{ name: '河北省', customerCount: 12, orderCount: 310, salesAmount: 3600000 },
|
||||
{ name: '上海市', customerCount: 10, orderCount: 260, salesAmount: 3200000 },
|
||||
{ name: '湖北省', customerCount: 7, orderCount: 180, salesAmount: 2100000 },
|
||||
{ name: '辽宁省', customerCount: 6, orderCount: 150, salesAmount: 1700000 },
|
||||
{ name: '浙江省', customerCount: 5, orderCount: 130, salesAmount: 1500000 },
|
||||
{ name: '安徽省', customerCount: 4, orderCount: 100, salesAmount: 1150000 },
|
||||
{ name: '四川省', customerCount: 4, orderCount: 90, salesAmount: 1000000 },
|
||||
{ name: '湖南省', customerCount: 3, orderCount: 75, salesAmount: 850000 },
|
||||
{ name: '福建省', customerCount: 3, orderCount: 70, salesAmount: 800000 },
|
||||
{ name: '山西省', customerCount: 2, orderCount: 50, salesAmount: 550000 },
|
||||
{ name: '陕西省', customerCount: 2, orderCount: 45, salesAmount: 500000 },
|
||||
{ name: '江西省', customerCount: 2, orderCount: 40, salesAmount: 450000 },
|
||||
{ name: '北京市', customerCount: 1, orderCount: 25, salesAmount: 300000 },
|
||||
{ name: '广东省', customerCount: 1, orderCount: 20, salesAmount: 250000 },
|
||||
{ name: '海南省', customerCount: 1, orderCount: 15, salesAmount: 180000 },
|
||||
{ name: '广西壮族自治区', customerCount: 1, orderCount: 10, salesAmount: 120000 }
|
||||
]
|
||||
|
||||
/**
|
||||
* 省份热力统计数据
|
||||
*/
|
||||
export const getProvinceStats = withMock(
|
||||
(params) => request({ url: '/crm/salesReport/provinceStats', method: 'get', params }),
|
||||
mockProvinceStats
|
||||
)
|
||||
29
src/api/wip.js
Normal file
29
src/api/wip.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询钢卷物料列表(与工序页面相同的数据源)
|
||||
export function listMaterialCoil(query) {
|
||||
return request({
|
||||
url: '/wms/materialCoil/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 批量查询钢卷物料(按 coilIds)
|
||||
export function listMaterialCoilByPost(data) {
|
||||
return request({
|
||||
url: '/wms/materialCoil/listByPost',
|
||||
method: 'post',
|
||||
data: data,
|
||||
params: { pageNum: 1, pageSize: 500 }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询钢卷待操作列表
|
||||
export function listPendingAction(query) {
|
||||
return request({
|
||||
url: '/wms/coilPendingAction/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
136
src/api/wms/actualWarehouse.js
Normal file
136
src/api/wms/actualWarehouse.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
|
||||
// 查询实际库区/库位自关联列表
|
||||
export function listActualWarehouse(query) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取完整三级目录
|
||||
export function listActualWarehouseTree(query) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/tree',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询实际库区/库位自关联详细
|
||||
export function getActualWarehouse(actualWarehouseId) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/' + actualWarehouseId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增实际库区/库位自关联
|
||||
export function addActualWarehouse(data) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 导入实际库区/库位
|
||||
export function importActualWarehouse(data) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/importData',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
// 批量新增三级目录
|
||||
export function createActualWarehouseHierarchy(data) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/hierarchy',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改实际库区/库位自关联
|
||||
export function updateActualWarehouse(data) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除实际库区/库位自关联
|
||||
export function delActualWarehouse(actualWarehouseId) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/' + actualWarehouseId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取两级的树结构
|
||||
export function treeActualWarehouseTwoLevel(query) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/levelTwo',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function generateLocations(data) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/generateLocations',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割库区
|
||||
*/
|
||||
export function splitActualWarehouse(payload) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/split',
|
||||
method: 'post',
|
||||
timeout: 100000,
|
||||
data: {
|
||||
action: 1,
|
||||
splitType: 0,
|
||||
...payload
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并库区
|
||||
*/
|
||||
export function mergeActualWarehouse(payload) {
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/merge',
|
||||
method: 'post',
|
||||
timeout: 100000,
|
||||
data: {
|
||||
action: 0,
|
||||
splitType: 0,
|
||||
...payload
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放库位
|
||||
*/
|
||||
export function forceReleaseLocation(actualWarehouseId) {
|
||||
if (!actualWarehouseId) {
|
||||
throw new Error('actualWarehouseId is required');
|
||||
}
|
||||
return request({
|
||||
url: '/wms/actualWarehouse/release/' + actualWarehouseId,
|
||||
method: 'put',
|
||||
timeout: 100000,
|
||||
})
|
||||
}
|
||||
117
src/components/DataVLayout.vue
Normal file
117
src/components/DataVLayout.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<dv-full-screen-container class="datav-container" auto-fit>
|
||||
<div class="datav-wrapper">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="datav-header">
|
||||
<dv-decoration-10 style="width: 400px; height: 40px" />
|
||||
<div class="header-title">
|
||||
<dv-decoration-9 :reverse="true" />
|
||||
<span class="title-text">{{ title || '数据可视化大屏' }}</span>
|
||||
<dv-decoration-9 />
|
||||
</div>
|
||||
<dv-decoration-10 style="width: 400px; height: 40px" :reverse="true" />
|
||||
</div>
|
||||
|
||||
<!-- 左侧装饰 -->
|
||||
<div class="decoration decoration-left">
|
||||
<dv-decoration-3 style="width: 200px; height: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧装饰 -->
|
||||
<div class="decoration decoration-right">
|
||||
<dv-decoration-3 style="width: 200px; height: 100%" :reverse="true" />
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区域 -->
|
||||
<div class="datav-main">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- 底部装饰 -->
|
||||
<div class="datav-footer">
|
||||
<dv-decoration-2 style="width: 100%; height: 30px" />
|
||||
</div>
|
||||
</div>
|
||||
</dv-full-screen-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.datav-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0a1428;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.datav-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.datav-header {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.title-text {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
letter-spacing: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
bottom: 30px;
|
||||
width: 15px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.decoration-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.decoration-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.datav-main {
|
||||
flex: 1;
|
||||
padding: 10px 30px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.datav-footer {
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -14,9 +14,9 @@
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
@@ -36,4 +36,4 @@
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<nav class="navbar">
|
||||
<div class="navbar-left">
|
||||
<hamburger @toggle-click="toggleSideBar" />
|
||||
<span class="navbar-title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<button class="action-btn refresh-btn" @click="handleRefresh" title="刷新数据">
|
||||
@@ -54,23 +53,23 @@ onUnmounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 60px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.95) 0%, rgba(0, 212, 255, 0.9) 50%, rgba(0, 168, 204, 0.95) 100%);
|
||||
height: 50px;
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { Monitor, PieChart, Document, Bell } from '@element-plus/icons-vue'
|
||||
import { Monitor, PieChart, Document, Bell, Coin } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
@@ -39,13 +39,15 @@ const activeMenu = computed(() => route.path)
|
||||
|
||||
const iconMap = {
|
||||
'monitor': Monitor,
|
||||
'example': PieChart,
|
||||
'order': Document,
|
||||
'cost': PieChart,
|
||||
'energy': Monitor,
|
||||
'oee': PieChart,
|
||||
'output': PieChart,
|
||||
'stop': Bell
|
||||
'stop': Bell,
|
||||
'sales': Coin,
|
||||
'heatmap': Monitor,
|
||||
'warning': Bell
|
||||
}
|
||||
|
||||
const getIcon = (iconName) => {
|
||||
@@ -53,14 +55,18 @@ const getIcon = (iconName) => {
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/dashboard/demo', meta: { title: '示例大屏', icon: 'example' } },
|
||||
{ path: '/sales', meta: { title: '销售信息大屏', icon: 'sales' } },
|
||||
{ path: '/sales-heatmap', meta: { title: '销售热力地图', icon: 'heatmap' } },
|
||||
{ path: '/dashboard/order', meta: { title: '订单大屏', icon: 'order' } },
|
||||
{ path: '/dashboard/cost', meta: { title: '成本大屏', icon: 'cost' } },
|
||||
{ path: '/dashboard/energy', meta: { title: '能源大屏', icon: 'energy' } },
|
||||
{ path: '/dashboard/oee', meta: { title: 'OEE综合大屏', icon: 'oee' } },
|
||||
{ path: '/dashboard/output', meta: { title: '产出监控大屏', icon: 'output' } },
|
||||
{ path: '/dashboard/stop-analysis', meta: { title: '停机分析大屏', icon: 'stop' } },
|
||||
{ path: '/dashboard/acid-rolling', meta: { title: '酸轧数据大屏', icon: 'example' } }
|
||||
{ path: '/dashboard/acid-rolling', meta: { title: '酸轧数据大屏', icon: 'example' } },
|
||||
{ path: '/scrap-coil', meta: { title: '次品钢卷大屏', icon: 'warning' } },
|
||||
{ path: '/warehouse-overview', meta: { title: '库区总览大屏', icon: 'example' } },
|
||||
{ path: '/wip', meta: { title: 'WIP在产大屏', icon: 'monitor' } }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './assets/styles/index.scss'
|
||||
import DatavVue3 from '@kjgl77/datav-vue3'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -22,4 +23,5 @@ app.config.errorHandler = (err, instance, info) => {
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(ElementPlus)
|
||||
app.use(DatavVue3)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,540 +0,0 @@
|
||||
<template>
|
||||
<div class="screen-wrapper">
|
||||
<div class="screen-content">
|
||||
<header class="screen-header">
|
||||
<h1 class="title">大数据可视化平台</h1>
|
||||
<div class="header-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
<button v-if="isFullscreen" class="exit-fullscreen-btn" @click="exitFullscreen" title="退出全屏">
|
||||
<span>⛶ 退出全屏</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card" v-for="card in cards" :key="card.title">
|
||||
<div class="card-header">{{ card.title }}</div>
|
||||
<div class="kpi-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="kpi-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">产量趋势</div>
|
||||
<div ref="lineChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">运行状态</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">班组排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingList" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
<span class="unit">吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-2">
|
||||
<div class="box-header">实时告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarmList" :key="alarm.time">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const currentTime = ref('')
|
||||
const isFullscreen = ref(false)
|
||||
const lineChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
let lineChart = null
|
||||
let pieChart = null
|
||||
let timeInterval = null
|
||||
let resizeObserver = null
|
||||
let fullscreenChangeHandler = null
|
||||
|
||||
const cards = ref([
|
||||
{ title: '总产量', value: '12,580', unit: '吨', color: '#00d4ff' },
|
||||
{ title: '日产量', value: '1,258', unit: '吨', color: '#00ff88' },
|
||||
{ title: '运行效率', value: '96.8', unit: '%', color: '#7c63ff' },
|
||||
{ title: '良品率', value: '99.2', unit: '%', color: '#ff9f43' }
|
||||
])
|
||||
|
||||
const rankingList = ref([
|
||||
{ name: '甲班', value: '3,200' },
|
||||
{ name: '乙班', value: '2,980' },
|
||||
{ name: '丙班', value: '2,850' },
|
||||
{ name: '丁班', value: '2,720' }
|
||||
])
|
||||
|
||||
const alarmList = ref([
|
||||
{ icon: '✅', title: '系统运行正常', time: '15:30:00', level: 'success' },
|
||||
{ icon: '⚠️', title: '速度损失告警', time: '14:25:00', level: 'warning' },
|
||||
{ icon: '🔴', title: '设备故障停机', time: '13:15:00', level: 'danger' }
|
||||
])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (lineChartRef.value) {
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00'],
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [{
|
||||
name: '产量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [820, 932, 901, 1234, 1290, 1330],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0, 212, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#00d4ff', width: 2 },
|
||||
itemStyle: { color: '#00d4ff' }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}% ({d}%)',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 65, name: '运行中', itemStyle: { color: '#00ff88' } },
|
||||
{ value: 20, name: '待机', itemStyle: { color: '#ffd43b' } },
|
||||
{ value: 10, name: '故障', itemStyle: { color: '#ff6b6b' } },
|
||||
{ value: 5, name: '维护', itemStyle: { color: '#00d4ff' } }
|
||||
],
|
||||
label: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
nextTick(() => {
|
||||
lineChart?.resize()
|
||||
pieChart?.resize()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
handleResize()
|
||||
})
|
||||
const container = document.querySelector('.screen-wrapper')
|
||||
if (container) {
|
||||
resizeObserver.observe(container)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('refresh-data', handleRefresh)
|
||||
|
||||
fullscreenChangeHandler = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
}
|
||||
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
})
|
||||
|
||||
const handleRefresh = () => {
|
||||
initCharts()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
timeInterval = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('refresh-data', handleRefresh)
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver = null
|
||||
}
|
||||
if (fullscreenChangeHandler) {
|
||||
document.removeEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
fullscreenChangeHandler = null
|
||||
}
|
||||
if (lineChart) {
|
||||
lineChart.dispose()
|
||||
lineChart = null
|
||||
}
|
||||
if (pieChart) {
|
||||
pieChart.dispose()
|
||||
pieChart = null
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 确保清理完成
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 18px;
|
||||
color: #00d4ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.exit-fullscreen-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.5);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 107, 0.3);
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.kpi-card {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.chart-box {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.flex-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%);
|
||||
padding: 12px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 12px;
|
||||
background: #3a5a8a;
|
||||
color: #a0c4e8;
|
||||
|
||||
&.rank-1 {
|
||||
background: linear-gradient(135deg, #ffd43b, #ff9f43);
|
||||
color: #0a1428;
|
||||
}
|
||||
&.rank-2 {
|
||||
background: linear-gradient(135deg, #a0c4e8, #74c0fc);
|
||||
color: #0a1428;
|
||||
}
|
||||
&.rank-3 {
|
||||
background: linear-gradient(135deg, #ffa94d, #ff6b6b);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 13px;
|
||||
color: #a0c4e8;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
border-left-width: 4px;
|
||||
|
||||
&.success {
|
||||
border-color: #00ff88;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: #ffd43b;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
|
||||
.alarm-title {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ export const constantRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
redirect: '/dashboard/demo',
|
||||
redirect: '/dashboard/acid-rolling',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
@@ -12,12 +12,6 @@ export const constantRoutes = [
|
||||
component: () => import('@/layout/index.vue'),
|
||||
meta: { title: '数据大屏', icon: 'monitor' },
|
||||
children: [
|
||||
{
|
||||
path: 'demo',
|
||||
name: 'Demo',
|
||||
component: () => import('@/modules/dashboardBig/views/index.vue'),
|
||||
meta: { title: '示例大屏', icon: 'example' }
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
name: 'Order',
|
||||
@@ -61,6 +55,36 @@ export const constantRoutes = [
|
||||
meta: { title: '酸轧数据大屏', icon: 'example' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/warehouse-overview',
|
||||
name: 'WarehouseOverview',
|
||||
component: () => import('@/views/screens/warehouse-overview/index.vue'),
|
||||
meta: { title: '库区总览大屏' }
|
||||
},
|
||||
{
|
||||
path: '/sales',
|
||||
name: 'SalesOverview',
|
||||
component: () => import('@/views/screens/sales/index.vue'),
|
||||
meta: { title: '销售信息大屏' }
|
||||
},
|
||||
{
|
||||
path: '/wip',
|
||||
name: 'WipOverview',
|
||||
component: () => import('@/views/screens/wip/index.vue'),
|
||||
meta: { title: '在产大屏' }
|
||||
},
|
||||
{
|
||||
path: '/sales-heatmap',
|
||||
name: 'SalesHeatmap',
|
||||
component: () => import('@/views/screens/sales-heatmap/index.vue'),
|
||||
meta: { title: '销售热力地图大屏' }
|
||||
},
|
||||
{
|
||||
path: '/scrap-coil',
|
||||
name: 'ScrapCoil',
|
||||
component: () => import('@/views/screens/scrap-coil/index.vue'),
|
||||
meta: { title: '次品钢卷大屏' }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -79,4 +103,4 @@ export function resetRouter() {
|
||||
router.matcher = newRouter.matcher
|
||||
}
|
||||
|
||||
export default router
|
||||
export default router
|
||||
|
||||
@@ -36,7 +36,7 @@ const actions = {
|
||||
function filterAsyncRoutes(routes) {
|
||||
return routes.filter(route => {
|
||||
if (!route.component || route.component === '') {
|
||||
route.component = () => import('@/modules/dashboardBig/views/index.vue')
|
||||
route.component = () => import('@/views/screens/acid-rolling/index.vue')
|
||||
} else if (route.component !== 'Layout') {
|
||||
route.component = loadComponent(route.component)
|
||||
}
|
||||
@@ -51,16 +51,16 @@ function loadComponent(componentPath) {
|
||||
const path = componentPath.replace(/^\//, '').replace(/\.vue$/, '')
|
||||
const componentMap = {
|
||||
'Layout': () => import('@/layout/index.vue'),
|
||||
'modules/dashboardBig/views/index': () => import('@/modules/dashboardBig/views/index.vue'),
|
||||
'modules/dashboardBig/views/order': () => import('@/modules/dashboardBig/views/order.vue'),
|
||||
'modules/dashboardBig/views/cost': () => import('@/modules/dashboardBig/views/cost.vue'),
|
||||
'modules/dashboardBig/views/energy': () => import('@/modules/dashboardBig/views/energy.vue'),
|
||||
'modules/dashboardBig/views/oee': () => import('@/modules/dashboardBig/views/oee.vue'),
|
||||
'modules/dashboardBig/views/output': () => import('@/modules/dashboardBig/views/output.vue'),
|
||||
'modules/dashboardBig/views/stopAnalysis': () => import('@/modules/dashboardBig/views/stopAnalysis.vue'),
|
||||
'screens/acid-rolling/index': () => import('@/views/screens/acid-rolling/index.vue')
|
||||
'screens/acid-rolling/index': () => import('@/views/screens/acid-rolling/index.vue'),
|
||||
'screens/scrap-coil/index': () => import('@/views/screens/scrap-coil/index.vue')
|
||||
}
|
||||
return componentMap[path] || (() => import('@/modules/dashboardBig/views/index.vue'))
|
||||
return componentMap[path] || (() => import('@/views/screens/acid-rolling/index.vue'))
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// klp-oa线上API地址(测试环境)
|
||||
const baseURL = 'http://140.143.206.120:8080'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 15000
|
||||
baseURL: baseURL,
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// 从 Cookies 获取 Token(与 klp-ui 保持一致)
|
||||
const getToken = () => {
|
||||
const name = 'Admin-Token'
|
||||
const value = `; ${document.cookie}`
|
||||
const parts = value.split(`; ${name}=`)
|
||||
if (parts.length === 2) return parts.pop().split(';').shift()
|
||||
return null
|
||||
}
|
||||
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// 携带Token
|
||||
let token = getToken()
|
||||
|
||||
// ========== 测试用:临时硬编码 Token ==========
|
||||
// 如果没有从 Cookies 获取到 Token,使用测试 Token
|
||||
if (!token) {
|
||||
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxIiwicm5TdHIiOiJGM2w5Nm5ncGV4ZDA4d1BUYWlvVmFreWhCUDdlc2gyeCIsInVzZXJJZCI6MX0.aP06S-5aJtukzMjmYW3d5zK3v10TOKcdNpROniv5lr8'
|
||||
}
|
||||
// ========== 测试用结束 ==========
|
||||
|
||||
console.log('请求URL:', config.url)
|
||||
console.log('使用的Token:', token ? '已设置' : '未设置')
|
||||
|
||||
if (token) {
|
||||
// 直接使用Token(不带Bearer前缀)
|
||||
config.headers['Authorization'] = "Bearer " + token
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
@@ -18,16 +47,22 @@ service.interceptors.request.use(
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code === 200) {
|
||||
return res.data
|
||||
} else {
|
||||
console.error('Response error:', res.message)
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
// 兼容多种返回格式
|
||||
if (res && res.code !== undefined) {
|
||||
if (res.code === 200) {
|
||||
return res.data !== undefined ? res.data : res
|
||||
} else {
|
||||
console.warn('API error:', res.message)
|
||||
return res.data || []
|
||||
}
|
||||
}
|
||||
// 如果没有 code 字段,直接返回数据
|
||||
return res
|
||||
},
|
||||
error => {
|
||||
console.error('Response error:', error.message)
|
||||
return Promise.reject(error)
|
||||
// 返回空数组作为默认值,避免页面崩溃
|
||||
return Promise.resolve([])
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
465
src/views/screens/sales-heatmap/index.vue
Normal file
465
src/views/screens/sales-heatmap/index.vue
Normal file
@@ -0,0 +1,465 @@
|
||||
<template>
|
||||
<div class="heatmap-screen">
|
||||
<!-- 全屏动态背景 -->
|
||||
<div class="dynamic-bg">
|
||||
<div class="grid-container">
|
||||
<div class="grid-lines horizontal"></div>
|
||||
<div class="grid-lines vertical"></div>
|
||||
</div>
|
||||
<div class="particles-container">
|
||||
<div v-for="i in 20" :key="i" class="particle" :style="getParticleStyle(i)"></div>
|
||||
</div>
|
||||
<div class="glow-globe globe-1"></div>
|
||||
<div class="glow-globe globe-2"></div>
|
||||
<div class="glow-globe globe-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- 四角装饰 -->
|
||||
<div class="corner-decor corner-tl"></div>
|
||||
<div class="corner-decor corner-tr"></div>
|
||||
<div class="corner-decor corner-bl"></div>
|
||||
<div class="corner-decor corner-br"></div>
|
||||
<div class="side-light side-light-left"></div>
|
||||
<div class="side-light side-light-right"></div>
|
||||
|
||||
<div class="scale-wrapper" :style="scaleWrapperStyle">
|
||||
<div class="screen-content">
|
||||
<!-- 头部标题 -->
|
||||
<header class="screen-header">
|
||||
<dv-border-box-1 class="title-border">
|
||||
<div class="title-box">
|
||||
<span class="title-icon">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" color="#00d4ff">
|
||||
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="0.8" opacity="0.3"/>
|
||||
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.15"/>
|
||||
<path d="M12,12 L12,3 A9,9 0 0,1 21,12 Z" fill="currentColor" opacity="0.2">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<circle cx="12" cy="12" r="1.5" fill="#00d4ff" opacity="0.9"/>
|
||||
<circle cx="12" cy="12" r="3" fill="none" stroke="#00d4ff" stroke-width="0.5" opacity="0.3">
|
||||
<animate attributeName="r" values="3;6;3" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.3;0;0.3" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
</span>
|
||||
<h1 class="screen-title">销售客户热力地图</h1>
|
||||
<span class="subtitle">Sales Customer Heatmap</span>
|
||||
<span class="live-indicator">
|
||||
<span class="live-dot"></span>
|
||||
<span class="live-text">实时</span>
|
||||
</span>
|
||||
<span class="clock-text">{{ currentDate }}</span>
|
||||
</div>
|
||||
</dv-border-box-1>
|
||||
</header>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="screen-body">
|
||||
<!-- 中国热力地图 -->
|
||||
<div class="map-area">
|
||||
<div class="map-panel">
|
||||
<div class="map-wrap">
|
||||
<div ref="mapChartRef" class="map-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计卡片 -->
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-card" v-for="k in kpiList" :key="k.label">
|
||||
<div class="kpi-label">{{ k.label }}</div>
|
||||
<div class="kpi-value" :style="{ color: k.color }">{{ k.value }}</div>
|
||||
<div class="kpi-unit">{{ k.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import chinaMap from 'echarts-china-map/lib/china.json'
|
||||
import { getProvinceStats } from '@/api/sales'
|
||||
echarts.registerMap('china', chinaMap)
|
||||
|
||||
const currentDate = ref('')
|
||||
const scaleRatio = ref(1)
|
||||
let timeInterval = null
|
||||
|
||||
const provinceData = ref([])
|
||||
|
||||
const kpiList = computed(() => [
|
||||
{ label: '覆盖省份', value: provinceData.value.length, unit: '省', color: '#00d4ff' },
|
||||
{ label: '总客户数', value: provinceData.value.reduce((s, d) => s + d.customerCount, 0), unit: '个', color: '#7c63ff' },
|
||||
{ label: '总订单数', value: provinceData.value.reduce((s, d) => s + d.orderCount, 0), unit: '单', color: '#00ff88' },
|
||||
{ label: '总销售额', value: provinceData.value.reduce((s, d) => s + Number(d.salesAmount), 0).toLocaleString(), unit: '元', color: '#f0ad4e' }
|
||||
])
|
||||
|
||||
// ===== 缩放 =====
|
||||
const scaleWrapperStyle = computed(() => ({
|
||||
transform: `scale(${scaleRatio.value})`
|
||||
}))
|
||||
|
||||
const getParticleStyle = (index) => ({
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${index * 0.4}s`,
|
||||
animationDuration: `${8 + Math.random() * 6}s`
|
||||
})
|
||||
|
||||
const updateScale = () => {
|
||||
const w = window.innerWidth
|
||||
const h = window.innerHeight
|
||||
scaleRatio.value = Math.min(w / 1920, h / 1080)
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
currentDate.value = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 地图 =====
|
||||
const mapChartRef = ref(null)
|
||||
let mapChart = null
|
||||
|
||||
const initChart = () => {
|
||||
if (mapChartRef.value && !mapChart) {
|
||||
mapChart = echarts.init(mapChartRef.value)
|
||||
}
|
||||
}
|
||||
|
||||
const updateMapChart = (data) => {
|
||||
if (!mapChart) return
|
||||
const raw = Array.isArray(data) ? data : []
|
||||
const list = raw.map(d => ({
|
||||
name: d.province,
|
||||
value: Number(d.salesAmount) || 0,
|
||||
customerCount: d.customerCount || 0,
|
||||
orderCount: d.orderCount || 0,
|
||||
salesAmount: Number(d.salesAmount) || 0
|
||||
}))
|
||||
const maxVal = Math.max(...list.map(d => d.value), 10)
|
||||
mapChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (p) => {
|
||||
const d = p.data || {}
|
||||
if (!d || !d.customerCount) return p.name + '<br/>暂无数据'
|
||||
return p.name +
|
||||
'<br/>客户数:' + d.customerCount + ' 个' +
|
||||
'<br/>订单数:' + d.orderCount + ' 单' +
|
||||
'<br/>销售额:' + d.salesAmount.toLocaleString() + ' 元'
|
||||
},
|
||||
backgroundColor: 'rgba(10,20,40,0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: maxVal,
|
||||
left: 10,
|
||||
bottom: 5,
|
||||
text: ['多', '少'],
|
||||
textStyle: { color: 'rgba(255,255,255,0.5)', fontSize: 10 },
|
||||
inRange: { color: ['rgba(0,212,255,0.15)', 'rgba(0,212,255,0.4)', '#00d4ff', '#7c63ff', '#ff6b81'] },
|
||||
calculable: false,
|
||||
itemWidth: 12,
|
||||
itemHeight: 80
|
||||
},
|
||||
series: [{
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
roam: false,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
itemStyle: {
|
||||
borderColor: 'rgba(0,212,255,0.2)',
|
||||
borderWidth: 0.5,
|
||||
areaColor: 'rgba(10,20,40,0.6)'
|
||||
},
|
||||
emphasis: {
|
||||
label: { color: '#fff', fontSize: 13 },
|
||||
itemStyle: {
|
||||
areaColor: 'rgba(0,212,255,0.3)',
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0,212,255,0.2)'
|
||||
}
|
||||
},
|
||||
data: list
|
||||
}]
|
||||
})
|
||||
startCarousel()
|
||||
}
|
||||
|
||||
// 地图轮播
|
||||
let mapTimer = null
|
||||
let mapIndex = 0
|
||||
|
||||
const startCarousel = () => {
|
||||
stopCarousel()
|
||||
const list = provinceData.value
|
||||
if (!mapChart || list.length === 0) return
|
||||
mapIndex = 0
|
||||
mapTimer = setInterval(() => {
|
||||
mapChart.dispatchAction({ type: 'downplay', seriesIndex: 0 })
|
||||
mapChart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: mapIndex })
|
||||
mapChart.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: mapIndex })
|
||||
mapIndex = (mapIndex + 1) % list.length
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
const stopCarousel = () => {
|
||||
if (mapTimer) { clearInterval(mapTimer); mapTimer = null }
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
mapChart?.resize()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await getProvinceStats()
|
||||
const list = Array.isArray(res) ? res : (res?.data || [])
|
||||
provinceData.value = list
|
||||
return list
|
||||
} catch (e) {
|
||||
console.error('省份数据加载失败:', e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 生命周期 =====
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
nextTick(() => {
|
||||
updateScale()
|
||||
initChart()
|
||||
loadData().then(list => {
|
||||
if (list.length > 0) {
|
||||
updateMapChart(list)
|
||||
}
|
||||
})
|
||||
})
|
||||
window.addEventListener('resize', updateScale)
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
stopCarousel()
|
||||
if (mapChart) { mapChart.dispose(); mapChart = null }
|
||||
window.removeEventListener('resize', updateScale)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ===== 整体容器 ===== */
|
||||
.heatmap-screen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #050a15 0%, #0a1428 50%, #0d1b34 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scale-wrapper {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 14px 18px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ===== 动态背景 ===== */
|
||||
.dynamic-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-lines { position: absolute; inset: 0; }
|
||||
.grid-lines::before { content: ''; position: absolute; inset: 0; }
|
||||
.grid-lines.horizontal::before { background: repeating-linear-gradient(90deg, transparent, transparent 95px, rgba(0,212,255,0.04) 95px, rgba(0,212,255,0.04) 96px); }
|
||||
.grid-lines.vertical::before { background: repeating-linear-gradient(0deg, transparent, transparent 95px, rgba(0,212,255,0.04) 95px, rgba(0,212,255,0.04) 96px); }
|
||||
.particles-container { position: absolute; inset: 0; overflow: hidden; }
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 3px; height: 3px;
|
||||
background: radial-gradient(circle, #00d4ff 0%, rgba(0,212,255,0.4) 50%, transparent 100%);
|
||||
border-radius: 50%;
|
||||
animation: particleFloat 12s ease-in-out infinite;
|
||||
box-shadow: 0 0 6px #00d4ff, 0 0 16px rgba(0,212,255,0.4);
|
||||
}
|
||||
@keyframes particleFloat {
|
||||
0%,100% { top: 100%; transform: translateX(0) scale(0.3); opacity: 0; }
|
||||
10% { top: 85%; transform: translateX(0) scale(1); opacity: 1; }
|
||||
50% { top: 45%; transform: translateX(25px) scale(1.2); }
|
||||
85% { opacity: 0.6; top: 10%; transform: translateX(-15px) scale(0.7); }
|
||||
100% { top: -5%; opacity: 0; }
|
||||
}
|
||||
.glow-globe { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.2; animation: globePulse 8s ease-in-out infinite; }
|
||||
.glow-globe.globe-1 { width: 350px; height: 350px; background: radial-gradient(circle, rgba(0,212,255,0.3) 0%, transparent 70%); top: 15%; right: 15%; }
|
||||
.glow-globe.globe-2 { width: 300px; height: 300px; background: radial-gradient(circle, rgba(124,99,255,0.25) 0%, transparent 70%); bottom: 20%; left: 8%; animation-delay: 2.5s; }
|
||||
.glow-globe.globe-3 { width: 280px; height: 280px; background: radial-gradient(circle, rgba(0,255,136,0.15) 0%, transparent 70%); top: 45%; left: 50%; transform: translate(-50%,-50%); animation-delay: 5s; }
|
||||
@keyframes globePulse { 0%,100% { transform: scale(1); opacity: 0.2; } 50% { transform: scale(1.25); opacity: 0.35; } }
|
||||
|
||||
/* 四角装饰 */
|
||||
.corner-decor { position: fixed; z-index: 5; width: 40px; height: 40px; pointer-events: none; }
|
||||
.corner-decor::before, .corner-decor::after { content: ''; position: absolute; background: #00d4ff; box-shadow: 0 0 8px rgba(0,212,255,0.4); }
|
||||
.corner-tl { top: 10px; left: 10px; }
|
||||
.corner-tl::before { top: 0; left: 0; width: 2px; height: 100%; }
|
||||
.corner-tl::after { top: 0; left: 0; width: 100%; height: 2px; }
|
||||
.corner-tr { top: 10px; right: 10px; }
|
||||
.corner-tr::before { top: 0; right: 0; width: 2px; height: 100%; }
|
||||
.corner-tr::after { top: 0; right: 0; width: 100%; height: 2px; }
|
||||
.corner-bl { bottom: 10px; left: 10px; }
|
||||
.corner-bl::before { bottom: 0; left: 0; width: 2px; height: 100%; }
|
||||
.corner-bl::after { bottom: 0; left: 0; width: 100%; height: 2px; }
|
||||
.corner-br { bottom: 10px; right: 10px; }
|
||||
.corner-br::before { bottom: 0; right: 0; width: 2px; height: 100%; }
|
||||
.corner-br::after { bottom: 0; right: 0; width: 100%; height: 2px; }
|
||||
|
||||
/* 侧边光柱 */
|
||||
.side-light { position: fixed; top: 0; bottom: 0; width: 1px; z-index: 5; pointer-events: none; }
|
||||
.side-light::before { content: ''; position: absolute; top: 0; width: 1px; height: 100%; }
|
||||
.side-light-left { left: 0; }
|
||||
.side-light-left::before { left: 0; background: linear-gradient(180deg, transparent, #00d4ff, transparent); box-shadow: 0 0 10px rgba(0,212,255,0.3),0 0 30px rgba(0,212,255,0.15); }
|
||||
.side-light-right { right: 0; }
|
||||
.side-light-right::before { right: 0; background: linear-gradient(180deg, transparent, #7c63ff, transparent); box-shadow: 0 0 10px rgba(124,99,255,0.3),0 0 30px rgba(124,99,255,0.15); }
|
||||
|
||||
/* ===== 头部标题 ===== */
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.title-box {
|
||||
padding: 10px 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
}
|
||||
.title-icon { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; }
|
||||
.screen-title {
|
||||
font-size: 28px;
|
||||
color: #00d4ff;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0,212,255,0.8), 0 0 40px rgba(0,212,255,0.4);
|
||||
letter-spacing: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.subtitle { font-size: 12px; color: #6a8cb5; letter-spacing: 1px; }
|
||||
.live-indicator { display: flex; align-items: center; gap: 5px; margin-left: auto; padding: 2px 10px; border: 1px solid rgba(0,255,136,0.3); border-radius: 12px; background: rgba(0,255,136,0.08); }
|
||||
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: #00ff88; box-shadow: 0 0 6px #00ff88,0 0 12px rgba(0,255,136,0.4); animation: liveDotPulse 1.5s ease-in-out infinite; }
|
||||
@keyframes liveDotPulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.7); } }
|
||||
.live-text { font-size: 11px; color: #00ff88; letter-spacing: 1px; }
|
||||
.clock-text { font-size: 14px; color: #00d4ff; font-family: 'Courier New', monospace; letter-spacing: 1px; margin-left: 4px; }
|
||||
|
||||
/* ===== 主内容 ===== */
|
||||
.screen-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ===== 地图区域 ===== */
|
||||
.map-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: rgba(10, 20, 40, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ===== KPI 卡片 ===== */
|
||||
.kpi-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 6px 8px;
|
||||
background: rgba(10, 20, 40, 0.6);
|
||||
border: 1px solid rgba(0, 212, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-shadow: 0 0 12px currentColor;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
1327
src/views/screens/sales/index.vue
Normal file
1327
src/views/screens/sales/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1048
src/views/screens/scrap-coil/index.vue
Normal file
1048
src/views/screens/scrap-coil/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
2546
src/views/screens/warehouse-overview/index.vue
Normal file
2546
src/views/screens/warehouse-overview/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1660
src/views/screens/wip/index.vue
Normal file
1660
src/views/screens/wip/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,31 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/da': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/pocket': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/l2': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/wms': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/oee': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/crm': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user