Compare commits

19 Commits
master ... main

Author SHA1 Message Date
jhd
6114d989bc 次品钢卷大屏初版 2026-06-04 13:24:39 +08:00
jhd
4ec6164b5c 热力地图大屏优化 2026-06-02 18:23:02 +08:00
jhd
73ebc8b1e7 地图获取省份逻辑 2026-06-02 15:44:00 +08:00
jhd
20d376e93e 销售信息大屏初版 2026-06-01 15:43:46 +08:00
jhd
a4409e5afe wip在产大屏初版优化 2026-05-30 22:31:26 +08:00
jhd
da5a5c0d8e wip在产大屏初版 2026-05-29 16:45:12 +08:00
jhd
a4df18890d wip在产大屏 2026-05-29 16:26:59 +08:00
jhd
f432ff093c wip在产大屏 2026-05-29 15:53:25 +08:00
jhd
b4a1b736c1 大屏优化 2026-05-28 14:23:14 +08:00
jhd
a2ff005437 大屏优化 2026-05-28 14:09:08 +08:00
jhd
8b900ed7a1 大屏优化 2026-05-27 17:05:49 +08:00
jhd
20d2df9373 合并远程分支 2026-05-27 12:00:54 +08:00
jhd
6313be9c52 大屏判断逻辑修改 2026-05-27 11:54:52 +08:00
jhd
75f745cdb2 大屏判断逻辑修改 2026-05-27 10:58:17 +08:00
jhd
3e89c0b16c 大屏终版 2026-05-26 20:32:15 +08:00
jhd
6cfa8faa48 大屏样式修改 2026-05-26 15:14:08 +08:00
jhd
a7235b05e6 库存总览初版大屏 2026-05-25 18:20:37 +08:00
jhd
f07080397d 库存总览初版大屏 2026-05-25 18:12:10 +08:00
10ffb2e194 feat:修改 2026-05-25 11:41:57 +08:00
30 changed files with 8584 additions and 1101 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

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

View 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,
})
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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([])
}
)

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

8
tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

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