添加模拟线上接口,转3d地球添加,点击描点出现弹框,添加数据填报过鱼倒数统计,修改公共组件 表格高度
@ -9,9 +9,17 @@ VITE_APP_BASE_API = '/dev-api'
|
||||
# 本地环境
|
||||
# VITE_APP_BASE_URL = 'http://localhost:8093'
|
||||
# 测试环境
|
||||
VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
|
||||
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
|
||||
# 汤伟
|
||||
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
|
||||
VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
|
||||
|
||||
# 测试环境线上
|
||||
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
||||
|
||||
# 线上服务名称
|
||||
VITE_APP_LYGK_SERVER = '/api/dec-lygk-base-server'
|
||||
|
||||
|
||||
|
||||
|
||||
# 开发环境导入预览地址
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"ant-design-vue": "latest",
|
||||
"axios": "^1.2.0",
|
||||
"better-scroll": "^2.4.2",
|
||||
"cesium": "^1.141.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
@ -71,6 +72,7 @@
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "latest",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-cesium": "^1.2.22",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
|
||||
7651
frontend/pnpm-lock.yaml
Normal file
19
frontend/src/api/config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// src/api/config.ts
|
||||
// 获取环境变量的辅助函数,兼容 Vite 和 Webpack
|
||||
const getEnvVar = (key: string) => {
|
||||
// Vite 使用 import.meta.env
|
||||
if (import.meta.env) {
|
||||
return import.meta.env[key];
|
||||
}
|
||||
};
|
||||
|
||||
export const SERVICE_URLS = {
|
||||
// ABC 服务基础地址
|
||||
lygk: getEnvVar('VITE_APP_LYGK_SERVER'),
|
||||
|
||||
// XYZ 服务基础地址
|
||||
eng: getEnvVar('VITE_APP_eng_SERVER'),
|
||||
|
||||
// 默认服务(如果有)
|
||||
DEFAULT: ''
|
||||
};
|
||||
@ -119,3 +119,20 @@ export function batchRemoveDraft(data: any) {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 过鱼倒数统计
|
||||
export function statistics(data: any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/statistics',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 催促
|
||||
export function batchUrgeContent(data: any) {
|
||||
return request({
|
||||
url: '/sms/batchUrgeContent',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
10
frontend/src/api/home/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request';
|
||||
import { SERVICE_URLS } from '../config'; // 引入配置
|
||||
// 基本情况介绍
|
||||
export function getBaseWbsb(data: any) {
|
||||
return request({
|
||||
url: SERVICE_URLS.lygk + '/base/wbsb/GetKendoList',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
BIN
frontend/src/assets/images/mapTip/bottom.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src/assets/images/mapTip/bottom@175.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
frontend/src/assets/images/mapTip/bottom@215.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
frontend/src/assets/images/mapTip/bottom@260.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/src/assets/images/mapTip/center.png
Normal file
|
After Width: | Height: | Size: 1001 B |
BIN
frontend/src/assets/images/mapTip/center@175.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/src/assets/images/mapTip/center@215.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/src/assets/images/mapTip/center@260.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="97px" height="107px" viewBox="0 0 97 107" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-dxsdzGuihuaNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-601.000000, -356.000000)" fill-rule="nonzero">
|
||||
<g id="map-dxsdzGuihuaNetMonitor" transform="translate(601.000000, 356.000000)">
|
||||
<path d="M48.8947368,0.0526315789 C75.4044048,0.0526315789 96.8947368,21.5429636 96.8947368,48.0526316 C96.8947368,72.3556985 78.8331163,92.4402479 55.4007117,95.6154434 L48.8947368,107 L42.388762,95.6154434 C18.9563573,92.4402479 0.894736842,72.3556985 0.894736842,48.0526316 C0.894736842,21.5429636 22.3850689,0.0526315789 48.8947368,0.0526315789 Z M48.8947368,11 C28.4311335,11 11.8421053,27.5890282 11.8421053,48.0526316 C11.8421053,68.5162349 28.4311335,85.1052632 48.8947368,85.1052632 C69.3583402,85.1052632 85.9473684,68.5162349 85.9473684,48.0526316 C85.9473684,27.5890282 69.3583402,11 48.8947368,11 Z" id="形状结合" fill="#DEE7E9"></path>
|
||||
<path d="M48.8947368,5.10526316 C25.1755602,5.10526316 5.94736842,24.333455 5.94736842,48.0526316 C5.94736842,71.7718082 25.1755602,91 48.8947368,91 C72.6139135,91 91.8421053,71.7718082 91.8421053,48.0526316 C91.8421053,24.333455 72.6139135,5.10526316 48.8947368,5.10526316 Z M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="椭圆形备份" fill="#BBCBCF"></path>
|
||||
<path d="M48.8947368,9.31578947 C27.5009697,9.31578947 10.1578947,26.6588644 10.1578947,48.0526316 C10.1578947,69.4463987 27.5009697,86.7894737 48.8947368,86.7894737 C70.288504,86.7894737 87.6315789,69.4463987 87.6315789,48.0526316 C87.6315789,26.6588644 70.288504,9.31578947 48.8947368,9.31578947 Z" id="椭圆形备份" fill="#859EA3"></path>
|
||||
<path d="M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="路径" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="97px" height="107px" viewBox="0 0 97 107" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-dxsdzYijianNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-359.000000, -356.000000)" fill-rule="nonzero">
|
||||
<g id="map-dxsdzYijianNetMonitor" transform="translate(359.000000, 356.000000)">
|
||||
<path d="M48.8947368,0.0526315789 C75.4044048,0.0526315789 96.8947368,21.5429636 96.8947368,48.0526316 C96.8947368,72.3556985 78.8331163,92.4402479 55.4007117,95.6154434 L48.8947368,107 L42.388762,95.6154434 C18.9563573,92.4402479 0.894736842,72.3556985 0.894736842,48.0526316 C0.894736842,21.5429636 22.3850689,0.0526315789 48.8947368,0.0526315789 Z M48.8947368,11 C28.4311335,11 11.8421053,27.5890282 11.8421053,48.0526316 C11.8421053,68.5162349 28.4311335,85.1052632 48.8947368,85.1052632 C69.3583402,85.1052632 85.9473684,68.5162349 85.9473684,48.0526316 C85.9473684,27.5890282 69.3583402,11 48.8947368,11 Z" id="形状结合" fill="#C0E3A4"></path>
|
||||
<path d="M48.8947368,5.10526316 C25.1755602,5.10526316 5.94736842,24.333455 5.94736842,48.0526316 C5.94736842,71.7718082 25.1755602,91 48.8947368,91 C72.6139135,91 91.8421053,71.7718082 91.8421053,48.0526316 C91.8421053,24.333455 72.6139135,5.10526316 48.8947368,5.10526316 Z M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="椭圆形备份" fill="#9AD26C"></path>
|
||||
<path d="M48.8947368,9.31578947 C27.5009697,9.31578947 10.1578947,26.6588644 10.1578947,48.0526316 C10.1578947,69.4463987 27.5009697,86.7894737 48.8947368,86.7894737 C70.288504,86.7894737 87.6315789,69.4463987 87.6315789,48.0526316 C87.6315789,26.6588644 70.288504,9.31578947 48.8947368,9.31578947 Z" id="椭圆形备份" fill="#52B303"></path>
|
||||
<path d="M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="路径" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="97px" height="107px" viewBox="0 0 97 107" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-dxsdzZaijianNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-480.000000, -356.000000)" fill-rule="nonzero">
|
||||
<g id="map-dxsdzZaijianNetMonitor" transform="translate(480.000000, 356.000000)">
|
||||
<path d="M48.8947368,0.0526315789 C75.4044048,0.0526315789 96.8947368,21.5429636 96.8947368,48.0526316 C96.8947368,72.3556985 78.8331163,92.4402479 55.4007117,95.6154434 L48.8947368,107 L42.388762,95.6154434 C18.9563573,92.4402479 0.894736842,72.3556985 0.894736842,48.0526316 C0.894736842,21.5429636 22.3850689,0.0526315789 48.8947368,0.0526315789 Z M48.8947368,11 C28.4311335,11 11.8421053,27.5890282 11.8421053,48.0526316 C11.8421053,68.5162349 28.4311335,85.1052632 48.8947368,85.1052632 C69.3583402,85.1052632 85.9473684,68.5162349 85.9473684,48.0526316 C85.9473684,27.5890282 69.3583402,11 48.8947368,11 Z" id="形状结合" fill="#B1D4F4"></path>
|
||||
<path d="M48.8947368,5.10526316 C25.1755602,5.10526316 5.94736842,24.333455 5.94736842,48.0526316 C5.94736842,71.7718082 25.1755602,91 48.8947368,91 C72.6139135,91 91.8421053,71.7718082 91.8421053,48.0526316 C91.8421053,24.333455 72.6139135,5.10526316 48.8947368,5.10526316 Z M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="椭圆形备份" fill="#7BB7ED"></path>
|
||||
<path d="M48.8947368,9.31578947 C27.5009697,9.31578947 10.1578947,26.6588644 10.1578947,48.0526316 C10.1578947,69.4463987 27.5009697,86.7894737 48.8947368,86.7894737 C70.288504,86.7894737 87.6315789,69.4463987 87.6315789,48.0526316 C87.6315789,26.6588644 70.288504,9.31578947 48.8947368,9.31578947 Z" id="椭圆形备份" fill="#4098E5"></path>
|
||||
<path d="M48.8947368,11 C69.3583402,11 85.9473684,27.5890282 85.9473684,48.0526316 C85.9473684,68.5162349 69.3583402,85.1052632 48.8947368,85.1052632 C28.4311335,85.1052632 11.8421053,68.5162349 11.8421053,48.0526316 C11.8421053,27.5890282 28.4311335,11 48.8947368,11 Z" id="路径" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
frontend/src/assets/images/mapTip/map-yingxiangtu1.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="96px" height="71px" viewBox="0 0 96 71" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-zxsdzGuihuaNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-597.000000, -488.000000)">
|
||||
<g id="map-zxsdzGuihuaNetMonitor" transform="translate(597.000000, 488.000000)">
|
||||
<path d="M0,61 L0,0 L96,0 L96,61 L53,61 L48,71 L43,61 L0,61 Z M90,6 L6,6 L6,55 L90,55 L90,6 Z" id="形状结合" fill="#DEE7E9" fill-rule="nonzero"></path>
|
||||
<polygon id="矩形" fill="#859EA3" fill-rule="nonzero" points="92 4 4 4 4 57 92 57"></polygon>
|
||||
<polygon id="路径" fill="#FFFFFF" fill-rule="nonzero" points="90 6 90 55 6 55 6 6"></polygon>
|
||||
<rect id="矩形" fill="#859EA3" x="5" y="30" width="86" height="1"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="96px" height="71px" viewBox="0 0 96 71" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-zxsdzYijianNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-365.000000, -488.000000)">
|
||||
<g id="map-zxsdzYijianNetMonitor" transform="translate(365.000000, 488.000000)">
|
||||
<path d="M0,61 L0,0 L96,0 L96,61 L53,61 L48,71 L43,61 L0,61 Z M90,6 L6,6 L6,55 L90,55 L90,6 Z" id="形状结合" fill="#C0E3A4" fill-rule="nonzero"></path>
|
||||
<polygon id="矩形" fill="#52B303" fill-rule="nonzero" points="92 4 4 4 4 57 92 57"></polygon>
|
||||
<polygon id="路径" fill="#FFFFFF" fill-rule="nonzero" points="90 6 90 55 6 55 6 6"></polygon>
|
||||
<rect id="矩形" fill="#52B303" x="5" y="30" width="86" height="1"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="96px" height="71px" viewBox="0 0 96 71" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>map-zxsdzZaijianNetMonitor</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="12.9″-iPad-Pro" transform="translate(-481.000000, -488.000000)">
|
||||
<g id="map-zxsdzZaijianNetMonitor" transform="translate(481.000000, 488.000000)">
|
||||
<path d="M0,61 L0,0 L96,0 L96,61 L53,61 L48,71 L43,61 L0,61 Z M90,6 L6,6 L6,55 L90,55 L90,6 Z" id="形状结合" fill="#B1D4F4" fill-rule="nonzero"></path>
|
||||
<polygon id="矩形" fill="#4098E5" fill-rule="nonzero" points="92 4 4 4 4 57 92 57"></polygon>
|
||||
<polygon id="路径" fill="#FFFFFF" fill-rule="nonzero" points="90 6 90 55 6 55 6 6"></polygon>
|
||||
<rect id="矩形" fill="#4098E5" x="5" y="30" width="86" height="1"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
frontend/src/assets/images/mapTip/top.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src/assets/images/mapTip/top@175.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
frontend/src/assets/images/mapTip/top@215.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
frontend/src/assets/images/mapTip/top@260.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
@ -1,38 +1,75 @@
|
||||
<template>
|
||||
<div class="basic-search-container">
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="inline" class="basic-search-form"
|
||||
@reset="handleReset" @finish="handleFinish" @values-change="handleValuesChange">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="inline"
|
||||
class="basic-search-form"
|
||||
@reset="handleReset"
|
||||
@finish="handleFinish"
|
||||
@values-change="handleValuesChange"
|
||||
>
|
||||
<div class="form-content-wrapper">
|
||||
<!-- 1. 搜索项 + 查询/重置按钮 (栅格布局) -->
|
||||
<a-row :gutter="[16, 0]" class="search-row" wrap>
|
||||
<a-col v-for="item in validSearchList" :key="item.name">
|
||||
<a-form-item :label="item.label" :name="item.name" style="width: 100%; margin-bottom: 0">
|
||||
<a-form-item
|
||||
:label="item.label"
|
||||
:name="item.name"
|
||||
style="width: 100%; margin-bottom: 0"
|
||||
>
|
||||
<!-- 1. 优先检查是否有具名插槽,或者 type 为 custom -->
|
||||
<slot v-if="$slots[item.name] || item.type === 'custom'" :name="item.name" :value="formData[item.name]"
|
||||
<slot
|
||||
v-if="$slots[item.name] || item.type === 'custom'"
|
||||
:name="item.name"
|
||||
:value="formData[item.name]"
|
||||
:onChange="(val: any) => {
|
||||
formData[item.name] = val;
|
||||
triggerManualValuesChange(item.name, val);
|
||||
}" :formModel="formData" />
|
||||
}"
|
||||
:formModel="formData"
|
||||
/>
|
||||
|
||||
<!-- 普通日期选择器 -->
|
||||
<a-date-picker v-else-if="item.type === 'DataPicker'" v-model:value="formData[item.name]"
|
||||
:picker="item.picker" :format="item.fieldProps?.format" :value-format="item.fieldProps?.valueFormat"
|
||||
:disabled-date="item.fieldProps?.disabledDate" :show-time="item.fieldProps?.showTime"
|
||||
:allow-clear="item.fieldProps?.allowClear" :presets="item.presets" style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)" />
|
||||
<a-date-picker
|
||||
v-else-if="item.type === 'DataPicker'"
|
||||
v-model:value="formData[item.name]"
|
||||
:picker="item.picker"
|
||||
:format="item.fieldProps?.format"
|
||||
:value-format="item.fieldProps?.valueFormat"
|
||||
:disabled-date="item.fieldProps?.disabledDate"
|
||||
:show-time="item.fieldProps?.showTime"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:presets="item.presets"
|
||||
style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
/>
|
||||
|
||||
<!-- 日期范围选择器 -->
|
||||
<a-range-picker v-else-if="item.type === 'RangePicker'" v-model:value="formData[item.name]"
|
||||
:picker="item.picker" :format="item.fieldProps?.format" :value-format="item.fieldProps?.valueFormat"
|
||||
:disabled-date="item.fieldProps?.disabledDate" :show-time="item.fieldProps?.showTime"
|
||||
:allow-clear="item.fieldProps?.allowClear" :presets="item.presets" style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)" />
|
||||
<a-range-picker
|
||||
v-else-if="item.type === 'RangePicker'"
|
||||
v-model:value="formData[item.name]"
|
||||
:picker="item.picker"
|
||||
:format="item.fieldProps?.format"
|
||||
:value-format="item.fieldProps?.valueFormat"
|
||||
:disabled-date="item.fieldProps?.disabledDate"
|
||||
:show-time="item.fieldProps?.showTime"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:presets="item.presets"
|
||||
style="width: 100%"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
/>
|
||||
|
||||
<!-- 普通输入框 -->
|
||||
<a-input v-else-if="!item.type || item.type === 'Input'" v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请输入'" :allow-clear="item.fieldProps?.allowClear"
|
||||
<a-input
|
||||
v-else-if="!item.type || item.type === 'Input'"
|
||||
v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请输入'"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)" />
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
|
||||
/>
|
||||
|
||||
<!-- 电站下拉框 -->
|
||||
<div class="flex gap-[10px]" v-else-if="item.type === 'waterStation'">
|
||||
@ -58,28 +95,63 @@
|
||||
</a-select-option>
|
||||
</a-select> -->
|
||||
<!-- 流域下拉框 -->
|
||||
<a-select :value="formData.hbrvcd" placeholder="请选择" @change="lyChange" show-search
|
||||
:loading="shuJuTianBaoStore.lyLoading" :filter-option="filterOption" style="width: 135px">
|
||||
<a-select-option v-for="opt in shuJuTianBaoStore.lyOption" :key="opt.hbrvcd" :value="opt.hbrvcd"
|
||||
:label="opt.hbrvnm">
|
||||
<a-select
|
||||
:value="formData.hbrvcd"
|
||||
placeholder="请选择"
|
||||
@change="lyChange"
|
||||
show-search
|
||||
:loading="shuJuTianBaoStore.lyLoading"
|
||||
:filter-option="filterOption"
|
||||
style="width: 135px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in shuJuTianBaoStore.lyOption"
|
||||
:key="opt.hbrvcd"
|
||||
:value="opt.hbrvcd"
|
||||
:label="opt.hbrvnm"
|
||||
>
|
||||
{{ opt.hbrvnm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- 电站下拉框 -->
|
||||
<a-select v-if="props.zhujianfujian == 'fu'" :value="formData.rstcd" placeholder="请选择电站"
|
||||
@change="stcdIdChange" show-search allow-clear :loading="shuJuTianBaoStore.engLoading"
|
||||
:filter-option="filterOption" style="width: 135px">
|
||||
<a-select-option v-for="opt in shuJuTianBaoStore.engOption" :key="opt.stcd" :value="opt.stcd"
|
||||
:label="opt.ennm">
|
||||
<a-select
|
||||
v-if="props.zhujianfujian == 'fu'"
|
||||
:value="formData.rstcd"
|
||||
placeholder="请选择电站"
|
||||
@change="stcdIdChange"
|
||||
show-search
|
||||
allow-clear
|
||||
:loading="shuJuTianBaoStore.engLoading"
|
||||
:filter-option="filterOption"
|
||||
style="width: 135px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in shuJuTianBaoStore.engOption"
|
||||
:key="opt.stcd"
|
||||
:value="opt.stcd"
|
||||
:label="opt.ennm"
|
||||
>
|
||||
{{ opt.ennm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- 电站下拉框 -->
|
||||
<a-select v-if="props.zhujianfujian == 'zhu'" :value="formData.stcd" placeholder="请选择电站"
|
||||
@change="stcdIdChange" show-search allow-clear :loading="shuJuTianBaoStore.engLoading"
|
||||
:filter-option="filterOption" style="width: 135px">
|
||||
<a-select-option v-for="opt in shuJuTianBaoStore.engOption" :key="opt.stcd" :value="opt.stcd"
|
||||
:label="opt.ennm">
|
||||
<a-select
|
||||
v-if="props.zhujianfujian == 'zhu'"
|
||||
:value="formData.stcd"
|
||||
placeholder="请选择电站"
|
||||
@change="stcdIdChange"
|
||||
show-search
|
||||
allow-clear
|
||||
:loading="shuJuTianBaoStore.engLoading"
|
||||
:filter-option="filterOption"
|
||||
style="width: 135px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in shuJuTianBaoStore.engOption"
|
||||
:key="opt.stcd"
|
||||
:value="opt.stcd"
|
||||
:label="opt.ennm"
|
||||
>
|
||||
{{ opt.ennm }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@ -90,21 +162,33 @@
|
||||
<!-- <div v-else-if="item.type === 'Select'">
|
||||
<div v-for="i in item.options"> {{ i[item.values?.name] }} {{ i[item.values?.value] }}</div>
|
||||
</div> -->
|
||||
<a-select v-else-if="item.type === 'Select'" v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请选择'" :allow-clear="item.fieldProps?.allowClear"
|
||||
<a-select
|
||||
v-else-if="item.type === 'Select'"
|
||||
v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请选择'"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)" show-search :filter-option="filterOption">
|
||||
<a-select-option v-for="opt in item.options" :key="opt[item.values?.value] || opt.value || opt.itemCode"
|
||||
@change="(val) => triggerManualValuesChange(item.name, val)"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="opt in item.options"
|
||||
:key="opt[item.values?.value] || opt.value || opt.itemCode"
|
||||
:value="opt[item.values?.value] || opt.value || opt.itemCode"
|
||||
:label="opt[item.values?.name] || opt.label || opt.dictName">
|
||||
:label="opt[item.values?.name] || opt.label || opt.dictName"
|
||||
>
|
||||
{{ opt[item.values?.name] || opt.label || opt.dictName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<!-- 单选框 -->
|
||||
<a-radio-group v-else-if="item.type === 'Radio'" v-model:value="formData[item.name]"
|
||||
<a-radio-group
|
||||
v-else-if="item.type === 'Radio'"
|
||||
v-model:value="formData[item.name]"
|
||||
:style="{ width: item.width ? item.width + 'px' : '200px' }"
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)">
|
||||
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
|
||||
>
|
||||
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-radio>
|
||||
@ -121,7 +205,7 @@
|
||||
<a-button v-if="showReset" @click="handleReset"> 重置 </a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col class="custom-action-col">
|
||||
<a-col class="custom-action-col !pl-0">
|
||||
<a-space>
|
||||
<slot name="actions" :form="formRef" :values="formData">
|
||||
<!-- 默认无内容 -->
|
||||
@ -256,7 +340,7 @@ const lyChange = (value: any) => {
|
||||
};
|
||||
|
||||
const stcdIdChange = (value: any) => {
|
||||
if (props.zhujianfujian == 'fu') {
|
||||
if (props.zhujianfujian == "fu") {
|
||||
formData.rstcd = value;
|
||||
shuJuTianBaoStore.getFpssOption(formData.hbrvcd, value);
|
||||
// 【关键修改】手动触发 valuesChange
|
||||
@ -267,7 +351,6 @@ const stcdIdChange = (value: any) => {
|
||||
// 【关键修改】手动触发 valuesChange
|
||||
triggerManualValuesChange("stcd", formData.stcd);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<div class="table-container" ref="tableContainerRef">
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@ -15,26 +16,22 @@
|
||||
<slot :name="slot" v-bind="scope"></slot>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, watch, nextTick } from "vue";
|
||||
import { calcTableScrollY } from "@/utils/index";
|
||||
|
||||
// --- Types ---
|
||||
interface Props {
|
||||
columns: any[];
|
||||
|
||||
scrollY?: string | number;
|
||||
// 请求数据的函数,由父组件传入
|
||||
scrollY?: string | number; // 优先使用传入的固定值
|
||||
listUrl: (params: any) => Promise<any>;
|
||||
data?: any[];
|
||||
// 是否开启行选择
|
||||
enableRowSelection?: boolean;
|
||||
// 行数据的 Key,用来优化 Table 的渲染
|
||||
rowKey?: string;
|
||||
// 外部传入的搜索/过滤参数,变化时会自动触发刷新
|
||||
searchParams?: Record<string, any>;
|
||||
// 默认每页显示数量
|
||||
defaultPageSize?: number;
|
||||
getCheckboxProps?: (record: any) => any;
|
||||
transformData?: (res: any) => { records: any[]; total: number };
|
||||
@ -43,17 +40,16 @@ interface Props {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
enableRowSelection: false,
|
||||
rowKey: "id",
|
||||
data: () => ([]),
|
||||
data: () => [],
|
||||
searchParams: () => ({}),
|
||||
defaultPageSize: 20,
|
||||
getCheckboxProps: undefined,
|
||||
transformData: undefined,
|
||||
scrollY: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
// 每次数据请求后,向父组件返回当前的查询参数和数据结果
|
||||
(e: "data-loaded", params: any, data: any): void;
|
||||
// 选中项变化
|
||||
(e: "selection-change", selectedRowKeys: string[], selectedRows: any[]): void;
|
||||
}>();
|
||||
|
||||
@ -65,15 +61,27 @@ const page = ref(1);
|
||||
const size = ref(props.defaultPageSize);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
//保存最后一次使用的 filter
|
||||
const lastFilter = ref<Record<string, any> | undefined>(undefined);
|
||||
const tableContainerRef = ref<any>(null);
|
||||
const tableScrollY = ref<number>(0); // ✅ 新增:存储容器高度
|
||||
|
||||
// --- Computed Scroll Config ---
|
||||
const scrollConfig = computed(() => {
|
||||
const config: any = {
|
||||
x: "100%",
|
||||
};
|
||||
|
||||
// 计算表格滚动高度
|
||||
const scrollConfig = computed(() => ({
|
||||
x: 'max-content',
|
||||
y: props.scrollY, // 注意:如果 scrollY 是 undefined,则不会启用垂直滚动
|
||||
}))
|
||||
// 1. 如果父组件传入了固定的 scrollY,优先使用
|
||||
if (props.scrollY !== undefined && props.scrollY !== null) {
|
||||
config.y = props.scrollY;
|
||||
}
|
||||
// 2. 否则,如果检测到容器有高度,则使用容器高度作为滚动高度
|
||||
else if (tableScrollY.value > 0) {
|
||||
config.y = tableScrollY.value;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// --- Row Selection ---
|
||||
const rowSelection = computed(() => ({
|
||||
@ -83,7 +91,9 @@ const rowSelection = computed(() => ({
|
||||
selectedRows.value = rows;
|
||||
emit("selection-change", keys, rows);
|
||||
},
|
||||
getCheckboxProps: props.getCheckboxProps ? props.getCheckboxProps : (record: any) => ({})
|
||||
getCheckboxProps: props.getCheckboxProps
|
||||
? props.getCheckboxProps
|
||||
: (record: any) => ({}),
|
||||
}));
|
||||
|
||||
// --- Pagination Config ---
|
||||
@ -98,54 +108,39 @@ const paginationConfig = computed(() => ({
|
||||
}));
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
/**
|
||||
* 获取列表数据
|
||||
* @param extraParams 额外的临时参数(可选)
|
||||
*/
|
||||
const getList = async (filter?: Record<string, any>) => {
|
||||
if(props.data.length > 0) return
|
||||
if (props.data.length > 0) return;
|
||||
loading.value = true;
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
// 如果传入了新 filter,则更新;否则保留旧的
|
||||
if (filter !== undefined) {
|
||||
lastFilter.value = filter;
|
||||
}
|
||||
try {
|
||||
// 合并基础分页参数、外部搜索参数和临时参数
|
||||
const params = {
|
||||
...props.searchParams,
|
||||
skip: page.value,
|
||||
take: size.value,
|
||||
filter: lastFilter.value,
|
||||
|
||||
// 如果后端需要 skip/take 格式,可以在此转换
|
||||
// skip: (page.value - 1) * size.value,
|
||||
// take: size.value,
|
||||
};
|
||||
|
||||
const res = await props.listUrl(params);
|
||||
let records: any[] = [];
|
||||
let totalCount: number = 0;
|
||||
|
||||
// [!code ++] 核心逻辑:如果父组件传入了 transformData,则使用父组件的逻辑
|
||||
let finalRecords: any[] = [];
|
||||
let finalTotal: number = 0;
|
||||
|
||||
if (props.transformData) {
|
||||
const result = props.transformData(res);
|
||||
records = result.records || [];
|
||||
totalCount = result.total || 0;
|
||||
finalRecords = result.records || [];
|
||||
finalTotal = result.total || 0;
|
||||
} else {
|
||||
// [!code ++] 否则使用默认逻辑
|
||||
records = res?.data?.records || res?.data || [];
|
||||
totalCount = res?.data?.total || res?.total || 0;
|
||||
finalRecords = res?.data?.records || res?.data || [];
|
||||
finalTotal = res?.data?.total || res?.total || 0;
|
||||
}
|
||||
|
||||
tableData.value = records;
|
||||
total.value = totalCount;
|
||||
|
||||
// 向父组件暴露当前请求参数和结果
|
||||
emit("data-loaded", params, { records, total: totalCount });
|
||||
|
||||
tableData.value = finalRecords;
|
||||
total.value = finalTotal;
|
||||
emit("data-loaded", params, { records: finalRecords, total: finalTotal });
|
||||
} catch (error) {
|
||||
console.error("Fetch table data error:", error);
|
||||
tableData.value = [];
|
||||
@ -155,18 +150,12 @@ const getList = async (filter?: Record<string, any>) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理分页、排序、筛选变化
|
||||
*/
|
||||
const handleTableChange = (pag: any) => {
|
||||
page.value = pag.current;
|
||||
size.value = pag.pageSize;
|
||||
getList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表格状态(回到第一页,清空选中)
|
||||
*/
|
||||
const reset = () => {
|
||||
page.value = 1;
|
||||
size.value = props.defaultPageSize;
|
||||
@ -175,9 +164,6 @@ const reset = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新当前页
|
||||
*/
|
||||
const refresh = () => {
|
||||
getList();
|
||||
};
|
||||
@ -185,60 +171,42 @@ const refresh = () => {
|
||||
const clearSelection = () => {
|
||||
selectedRowKeys.value = [];
|
||||
selectedRows.value = [];
|
||||
}
|
||||
// --- Expose Methods to Parent ---
|
||||
};
|
||||
defineExpose({
|
||||
loading,
|
||||
getList,
|
||||
reset,
|
||||
refresh,
|
||||
tableData,
|
||||
clearSelection,
|
||||
// 也可以暴露选中的数据供父组件获取
|
||||
getSelected: () => ({
|
||||
keys: selectedRowKeys.value,
|
||||
rows: selectedRows.value,
|
||||
}),
|
||||
});
|
||||
|
||||
// --- Watchers ---
|
||||
|
||||
// 监听外部搜索参数变化,自动重置页码并刷新
|
||||
watch(
|
||||
() => props.searchParams,
|
||||
() => {
|
||||
page.value = 1; // 搜索时通常重置到第一页
|
||||
page.value = 1;
|
||||
getList();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// --- Lifecycle ---
|
||||
const observer = new ResizeObserver(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
||||
});
|
||||
onMounted(() => {
|
||||
if(props.data && props.data.length > 0) tableData.value = props.data || [];
|
||||
nextTick(() => {
|
||||
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
||||
if (tableContainerRef.value) {
|
||||
observer.observe(tableContainerRef.value);
|
||||
}
|
||||
});
|
||||
if (props.data && props.data.length > 0) {
|
||||
tableData.value = props.data || [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.ant-table-wrapper) {
|
||||
height: 100%; /* 让表格容器填满父级 */
|
||||
.ant-spin-nested-loading, .ant-spin-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ant-table {
|
||||
flex: 1; /* 表格主体区域自动撑满剩余空间 */
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
.ant-table-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1; /* 表体部分撑满,实现固定表头 + 滚动内容 */
|
||||
overflow: auto !important; /* 出现滚动条的关键 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
199
frontend/src/components/gis/map.cesium.ts
Normal file
@ -0,0 +1,199 @@
|
||||
// src/components/gis/map.cesium.ts
|
||||
import * as Cesium from 'cesium';
|
||||
import type { MapInterface, layer } from './map.d';
|
||||
|
||||
// ✅ 1. 全局配置 Cesium 静态资源路径 (vite-plugin-cesium 通常会自动处理,但显式指定更保险)
|
||||
// 注意:如果你使用了 vite-plugin-cesium,这一步通常不需要,但如果报错,可以尝试取消注释下面这行
|
||||
// Cesium.buildModuleUrl.setBaseUrl('/node_modules/cesium/Build/Cesium/');
|
||||
|
||||
export class MapCesium implements MapInterface {
|
||||
private viewer: Cesium.Viewer | null = null;
|
||||
private containerId: string = '';
|
||||
|
||||
init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this.containerId = container.id;
|
||||
// ✅ 2. 初始化 Viewer
|
||||
// 在 1.141.0 中,建议先不传 imageryProvider,或者使用默认的 Ion Imagery
|
||||
// 如果不想用 Ion,可以传一个空的 ImageryProviderCollection 或者稍后移除
|
||||
this.viewer = new Cesium.Viewer(this.containerId, {
|
||||
animation: false, // 隐藏动画
|
||||
timeline: false, // 隐藏时间线
|
||||
baseLayerPicker: false, // 隐藏底图选择器
|
||||
fullscreenButton: false, // 隐藏全屏按钮
|
||||
vrButton: false, // 隐藏VR按钮
|
||||
geocoder: false, // 隐藏地址搜索框
|
||||
homeButton: false, // 隐藏首页按钮
|
||||
sceneModePicker: false, // 隐藏场景模式选择器
|
||||
navigationHelpButton: false, // 隐藏导航帮助按钮
|
||||
infoBox: false, // 隐藏信息框
|
||||
selectionIndicator: false, // 隐藏选择指示器
|
||||
shouldAnimate: true // 开启动画
|
||||
});
|
||||
|
||||
// // 清空默认图层
|
||||
const layers = this.viewer.imageryLayers;
|
||||
layers.removeAll();
|
||||
// // 添加 ArcGIS 底图(正确 URL)
|
||||
const arcgisUrl =
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/';
|
||||
|
||||
const provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
|
||||
arcgisUrl,
|
||||
{
|
||||
enablePickFeatures: false,
|
||||
// 可选:设置最大层级,避免请求过细的瓦片
|
||||
maximumLevel: 19
|
||||
}
|
||||
);
|
||||
// ✅ 6. 【关键】将 Provider 添加到图层集合中
|
||||
layers.addImageryProvider(provider);
|
||||
|
||||
// layers.addImageryProvider(provider);
|
||||
|
||||
// // ✅ 5. 场景优化
|
||||
this.viewer.scene.globe.depthTestAgainstTerrain = false;
|
||||
// // 启用高 DPI 支持(可选,根据性能需求)
|
||||
this.viewer.useBrowserRecommendedResolution = true;
|
||||
|
||||
// ✅ 6. 飞行到中国视角
|
||||
// 使用 setTimeout 确保 DOM 和 WebGL 上下文完全就绪
|
||||
// setTimeout(() => {
|
||||
// this.flyToChina();
|
||||
// resolve(this.viewer);
|
||||
// }, 100);
|
||||
} catch (error) {
|
||||
console.error('Cesium Init Critical Error:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private flyToChina() {
|
||||
if (!this.viewer) return;
|
||||
|
||||
// 取消之前的飞行动画(如果有)
|
||||
this.viewer.camera.cancelFlight();
|
||||
|
||||
this.viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(104.5, 36.5, 4000000),
|
||||
orientation: {
|
||||
heading: Cesium.Math.toRadians(0),
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0.0
|
||||
},
|
||||
duration: 2
|
||||
});
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.viewer) {
|
||||
// ✅ 关键:销毁前取消所有动画和事件
|
||||
this.viewer.camera.cancelFlight();
|
||||
this.viewer.destroy();
|
||||
this.viewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
flyTopanto(position: number[], zoom: number): void {
|
||||
if (!this.viewer || !position) return;
|
||||
const [lng, lat] = position;
|
||||
const height = 20000000 / Math.pow(2, zoom);
|
||||
|
||||
this.viewer.camera.cancelFlight();
|
||||
this.viewer.camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(
|
||||
lng,
|
||||
lat,
|
||||
height > 100 ? height : 1000
|
||||
),
|
||||
duration: 1.5
|
||||
});
|
||||
}
|
||||
|
||||
// ... 其他空实现方法保持不变 ...
|
||||
jdPanelControlShowAndHidden(baseid: String, isAll: boolean): void {}
|
||||
mdLayerShowOrHidden(): void {}
|
||||
addBaseDataLayer(layer: any): void {}
|
||||
controlBaseLayerTreeShowAndHidden(
|
||||
layerType: String,
|
||||
key: String,
|
||||
checked: boolean
|
||||
): void {}
|
||||
mdLayerTreeShowOrHidden(layerType: String, checked?: boolean): void {}
|
||||
addInitDataLayer(pointData: any[], layerType: any, mdoptions?: any): void {}
|
||||
baseLayerSwitcher(key: string): void {}
|
||||
addTertiarybasinLayer(
|
||||
layer: layer,
|
||||
fillcolor: any,
|
||||
outlineColor: any,
|
||||
datas: any
|
||||
): void {}
|
||||
removeTertiarybasinLayer(layer: layer): void {}
|
||||
zoomToggle(type: 'out' | 'in'): void {
|
||||
if (!this.viewer) return;
|
||||
const factor = 1.5;
|
||||
if (type === 'in') this.viewer.camera.zoomIn(factor);
|
||||
else this.viewer.camera.zoomOut(factor);
|
||||
}
|
||||
lengthCalculate(): void {}
|
||||
areCalculate(): void {}
|
||||
removeQueryLayer(): void {}
|
||||
// src/components/gis/map.cesium.ts
|
||||
|
||||
/**
|
||||
* 地图截图下载
|
||||
* @param fileName 可选,下载的文件名,默认为 'cesium_map.png'
|
||||
*/
|
||||
mapOutPut(fileName: string = 'cesium_map.png'): void {
|
||||
if (!this.viewer) {
|
||||
console.warn('Viewer is not initialized.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = this.viewer.canvas;
|
||||
|
||||
// ✅ 关键步骤 1: 强制渲染一帧,确保画布内容是最新的
|
||||
// 这有助于解决因缓冲区清除导致的黑屏问题
|
||||
this.viewer.render();
|
||||
|
||||
// ✅ 关键步骤 2: 尝试获取数据
|
||||
// 注意:如果存在跨域污染,这里可能会抛出 SecurityError
|
||||
const dataURL = canvas.toDataURL('image/png');
|
||||
|
||||
// 检查是否获取到了有效数据(简单的长度检查)
|
||||
if (dataURL.length < 100) {
|
||||
console.warn(
|
||||
'Canvas data is too small, likely black or empty. Check CORS settings.'
|
||||
);
|
||||
// 可以尝试 fallback 到 blob 方式,但通常 CORS 问题两者都会失败
|
||||
}
|
||||
|
||||
// 创建临时链接元素
|
||||
const link = document.createElement('a');
|
||||
link.href = dataURL;
|
||||
link.download = fileName;
|
||||
|
||||
// 触发点击事件进行下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理临时元素
|
||||
document.body.removeChild(link);
|
||||
|
||||
console.log('Map image exported successfully:', fileName);
|
||||
} catch (error) {
|
||||
console.error('Failed to export map image:', error);
|
||||
if (error instanceof DOMException && error.name === 'SecurityError') {
|
||||
console.error(
|
||||
'Security Error: The canvas has been tainted by cross-origin data. Ensure all imagery providers support CORS.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他代码 ...
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void {}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import type { layer, MapInterface } from './map.d';
|
||||
// import { MapLeaflet } from "./map.leaflet";
|
||||
import { MapOl } from './map.ol';
|
||||
import { MapCesium } from './map.cesium';
|
||||
|
||||
interface MapClassInterface extends MapInterface {
|
||||
layers: Map<string, layer>;
|
||||
@ -39,6 +40,7 @@ export class MapClass implements MapClassInterface {
|
||||
}
|
||||
// 地图初始化
|
||||
init(container: HTMLElement, rectangle?: any): Promise<any> {
|
||||
console.log(container, rectangle);
|
||||
return this.service.init(container, rectangle).then(map => {
|
||||
this.view = map;
|
||||
return map;
|
||||
@ -120,10 +122,127 @@ export class MapClass implements MapClassInterface {
|
||||
destroy(): void {
|
||||
this.service.destroy();
|
||||
}
|
||||
|
||||
// 初始化弹窗
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void {
|
||||
this.service.initPopupOverlay(popupContainer);
|
||||
}
|
||||
// 切换地图视图
|
||||
switchView(): void {
|
||||
// this.service.switchView()
|
||||
switchView(type: '2D' | '3D'): Promise<any> {
|
||||
// this.service.switchView(type);
|
||||
return new Promise((resolve, reject) => {
|
||||
const container = document.getElementById('mapContainer');
|
||||
if (!container) {
|
||||
reject(new Error('Map container not found'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 1. 销毁当前地图服务
|
||||
if (this.service) {
|
||||
try {
|
||||
this.service.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying previous service:', e);
|
||||
}
|
||||
this.service = null;
|
||||
}
|
||||
this.view = null;
|
||||
|
||||
// 2. 根据类型初始化新的地图服务
|
||||
if (type === '2D') {
|
||||
console.log('Switching to 2D (OpenLayers)');
|
||||
this.service = new MapOl();
|
||||
|
||||
this.service
|
||||
.init(container)
|
||||
.then(map => {
|
||||
this.view = map;
|
||||
resolve(map);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
} else if (type === '3D') {
|
||||
console.log('Switching to 3D (Cesium)');
|
||||
this.service = new MapCesium();
|
||||
|
||||
this.service
|
||||
.init(container)
|
||||
.then(viewer => {
|
||||
this.view = viewer;
|
||||
resolve(viewer);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
}
|
||||
// const container = this.view._container;
|
||||
// if (type === '3D' && this.view) {
|
||||
// if (type === '2D') {
|
||||
// const rectangle = this.view.camera.computeViewRectangle()
|
||||
// const heading = this.view.camera.heading
|
||||
// const height = this.view.camera.positionCartographic.height
|
||||
// const zoom = altitudeToZoom(height) > 11 ? 11 : altitudeToZoom(height)
|
||||
// const center = this.service?.getCenterPosition()
|
||||
// const west = this.transformRadian(rectangle.west)
|
||||
// const north = this.transformRadian(rectangle.north)
|
||||
// const east = this.transformRadian(rectangle.east)
|
||||
// const south = this.transformRadian(rectangle.south)
|
||||
// const bearing = this.transformRadian(heading)
|
||||
|
||||
// this.service = new MapOl();
|
||||
// this.init(document.getElementById('mapContainer'));
|
||||
// // this.service
|
||||
// .init(document.getElementById('mapContainer'))
|
||||
// .then(map => {
|
||||
// this.view = map;
|
||||
// resolve(map);
|
||||
// });
|
||||
// } else if (type === '3D') {
|
||||
// const data: {
|
||||
// _southWest: { lat: number; lng: number }
|
||||
// _northEast: { lat: number; lng: number }
|
||||
// } = this.view.getBounds()
|
||||
// const bearing = this.view.getBearing()
|
||||
// const heading = this.transformAngle(bearing)
|
||||
// //获取相机高度
|
||||
// const altitude = zoomToAltitude(this.view.getZoom())
|
||||
// //获取地图中心点
|
||||
// let lng = 0.5 * (data._ne.lng + data._sw.lng)
|
||||
// let lat = 0.5 * (data._ne.lat + data._sw.lat)
|
||||
// let center = { lng, lat }
|
||||
// this.destroy();
|
||||
|
||||
// this.service = null;
|
||||
// this.view = null;
|
||||
// this.service = new MapCesium()
|
||||
// this.service.init(container, data, center, altitude, heading).then((viewer) => {
|
||||
// this.view = viewer
|
||||
// let removeCallback = viewer.scene.globe.tileLoadProgressEvent.addEventListener((e) => {
|
||||
// if (e == 0) {
|
||||
// removeCallback()
|
||||
// removeCallback = null
|
||||
// if (viewer && !viewer.isDestroyed()) {
|
||||
// resolve(viewer)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// //容错机制,放置地图服务挂掉或者响应非常慢
|
||||
// setTimeout(() => {
|
||||
// if (removeCallback) {
|
||||
// removeCallback()
|
||||
// removeCallback = null
|
||||
// if (viewer && !viewer.isDestroyed()) {
|
||||
// resolve(viewer)
|
||||
// }
|
||||
// }
|
||||
// }, 5000)
|
||||
// })
|
||||
// }
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'%c switchView: ',
|
||||
'color: MidnightBlue; background: Aquamarine; font-size: 20px;',
|
||||
e
|
||||
);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 飞行到指定的点
|
||||
|
||||
6
frontend/src/components/gis/map.d.ts
vendored
@ -72,6 +72,12 @@ export interface MapInterface {
|
||||
mdoptions: MDOptions
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 初始化弹窗
|
||||
* @param popupContainer
|
||||
*/
|
||||
initPopupOverlay(popupContainer: HTMLDivElement): void;
|
||||
|
||||
/**
|
||||
* 初始化加载基础图层
|
||||
* @param layer
|
||||
|
||||
@ -11,6 +11,7 @@ import Stroke from 'ol/style/Stroke';
|
||||
// ✅ 新增导入
|
||||
import Icon from 'ol/style/Icon';
|
||||
import Text from 'ol/style/Text';
|
||||
7;
|
||||
|
||||
// import LayerGroup from 'ol/layer/Group';
|
||||
// import OSM from 'ol/source/OSM';
|
||||
@ -73,19 +74,13 @@ export class MapOl implements MapInterface {
|
||||
private _maskPrerender: any = null;
|
||||
private _maskPostrender: any = null;
|
||||
private BASEID: string = '';
|
||||
constructor() {
|
||||
// this.loadGeoJsonData();
|
||||
this.loadGeoJsonData1();
|
||||
}
|
||||
private async loadGeoJsonData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/data/geoJson.json');
|
||||
this.geoJsonData = await response.json();
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error);
|
||||
}
|
||||
}
|
||||
private async loadGeoJsonData2(url): Promise<any> {
|
||||
// ✅ 新增:记录当前鼠标悬停的 Feature ID
|
||||
private hoveredFeatureId: string | number | null = null;
|
||||
// ✅ 新增:用于显示自定义内容的 Overlay
|
||||
private popupOverlay: Overlay | null = null;
|
||||
private popupElement: HTMLElement | null = null;
|
||||
constructor() {}
|
||||
private async loadGeoJsonData(url): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
@ -94,27 +89,6 @@ export class MapOl implements MapInterface {
|
||||
console.error('配置加载失败:', error);
|
||||
}
|
||||
}
|
||||
private async loadGeoJsonData1(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/data/geoJson1.json');
|
||||
let res = await response.json();
|
||||
let json1 = {
|
||||
crs: res.crs,
|
||||
features: [],
|
||||
totalFeatures: res.totalFeatures,
|
||||
type: res.type
|
||||
};
|
||||
for (let i = 0; i < res.features.length; i++) {
|
||||
if (res.features[i].properties.BASEID === '12') {
|
||||
json1.features.push(res.features[i]);
|
||||
}
|
||||
}
|
||||
console.log(json1);
|
||||
this.geoJsonData1 = json1;
|
||||
} catch (error) {
|
||||
console.error('配置加载失败:', error);
|
||||
}
|
||||
}
|
||||
//地图初始化
|
||||
init(container: HTMLElement): Promise<any> {
|
||||
try {
|
||||
@ -174,25 +148,100 @@ export class MapOl implements MapInterface {
|
||||
});
|
||||
|
||||
// ✅ 新增:监听鼠标移动,动态改变光标样式
|
||||
this.map.on('pointermove', evt => {
|
||||
let lastHoveredId: string | number | null = null;
|
||||
let animationFrameId: number | null = null;
|
||||
// pointermove
|
||||
this.map.on('click', evt => {
|
||||
const pixel = evt.pixel;
|
||||
// 检测鼠标下方是否有矢量要素
|
||||
const feature = this.map?.forEachFeatureAtPixel(pixel, feature => {
|
||||
return feature;
|
||||
|
||||
// 取消之前的动画帧,确保只处理最新的一次移动
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(() => {
|
||||
let detectedFeature: Feature | undefined = undefined;
|
||||
let isHitIcon: boolean = false; // 标记是否真正命中了图标区域
|
||||
|
||||
// 1. 获取当前视口下的所有点图层
|
||||
const pointLayers = Array.from(this.pointLayerRegistry.values());
|
||||
|
||||
// 2. 遍历检测,但我们需要更精细的判断
|
||||
this.map?.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer) => {
|
||||
// 只关心点图层
|
||||
if (pointLayers.includes(layer)) {
|
||||
detectedFeature = feature as Feature;
|
||||
|
||||
// ✅ 关键优化:判断是否命中图标区域
|
||||
// OpenLayers 的 Icon 样式通常有 anchor [0.5, 0.5]
|
||||
// 我们可以获取图标的尺寸,判断鼠标是否在图标矩形范围内
|
||||
const style = (layer as VectorLayer).getStyle();
|
||||
let iconSize = { width: 0, height: 0 };
|
||||
|
||||
// 尝试从样式中获取图标尺寸(如果样式是函数,可能需要调用它,这里简化处理,假设固定大小或从 feature 获取)
|
||||
// 更通用的做法是:计算鼠标点到 Feature 坐标投影后的像素距离
|
||||
const geom = feature.getGeometry();
|
||||
if (geom && geom.getType() === 'Point') {
|
||||
const coord = (geom as Point).getCoordinates();
|
||||
const iconPixel = this.map?.getPixelFromCoordinate(coord);
|
||||
|
||||
if (iconPixel) {
|
||||
// 计算鼠标像素与图标中心像素的距离
|
||||
const dx = pixel[0] - iconPixel[0];
|
||||
const dy = pixel[1] - iconPixel[1];
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 假设图标大致大小为 30x30 或 40x40,半径设为 15-20
|
||||
// 你可以根据实际 icon 大小调整这个阈值,例如 15
|
||||
const hitRadius = 15;
|
||||
if (distance <= hitRadius) {
|
||||
isHitIcon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 找到第一个点图层要素即停止
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ hitTolerance: 0 } // 设置为 0,完全依赖上面的距离计算
|
||||
);
|
||||
|
||||
// 3. 根据是否命中图标来决定行为
|
||||
const newHoveredId =
|
||||
detectedFeature && isHitIcon ? detectedFeature.getId() : null;
|
||||
const detectedCoordinate =
|
||||
detectedFeature && isHitIcon
|
||||
? detectedFeature.getGeometry()?.getCoordinates()
|
||||
: undefined;
|
||||
|
||||
// ✅ 关键优化:立即更新状态,不再使用 setTimeout 防抖,以解决移出延时问题
|
||||
// 如果希望防止极速移动时的闪烁,可以保留极短的 debounce (如 10ms),但通常 requestAnimationFrame 已足够平滑
|
||||
if (newHoveredId !== lastHoveredId) {
|
||||
lastHoveredId = newHoveredId;
|
||||
this.hoveredFeatureId = newHoveredId;
|
||||
|
||||
// 更新光标
|
||||
const targetElement = this.map?.getTargetElement() as HTMLElement;
|
||||
if (targetElement) {
|
||||
targetElement.style.cursor = newHoveredId ? 'pointer' : '';
|
||||
}
|
||||
|
||||
// 显示/隐藏 Popup
|
||||
this.showPopup(detectedFeature, detectedCoordinate);
|
||||
|
||||
// 重绘图层以更新图标样式(如高亮)
|
||||
this.pointLayerRegistry.forEach(layer => {
|
||||
layer.changed();
|
||||
});
|
||||
|
||||
const targetElement = this.map?.getTargetElement() as HTMLElement;
|
||||
|
||||
if (targetElement) {
|
||||
if (feature) {
|
||||
// 如果鼠标下有要素,显示手型
|
||||
targetElement.style.cursor = 'pointer';
|
||||
} else {
|
||||
// 否则恢复默认
|
||||
targetElement.style.cursor = '';
|
||||
}
|
||||
// 触发地图渲染
|
||||
this.map?.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
return Promise.resolve(this.map);
|
||||
} catch (e) {
|
||||
console.log('OL Init Error', e);
|
||||
@ -293,10 +342,16 @@ export class MapOl implements MapInterface {
|
||||
...item, // 保留原始数据以便点击事件使用
|
||||
_layerKey: targetLayerKey
|
||||
});
|
||||
const uniqueId = item.stcd || item._id;
|
||||
|
||||
feature.setId(uniqueId);
|
||||
|
||||
// 将自定义属性绑定到 feature,以便样式函数访问
|
||||
feature.set('_iconUrl', iconUrl);
|
||||
feature.set('_labelText', labelText);
|
||||
if (item.popupHtml) {
|
||||
feature.set('popupHtml', item.popupHtml);
|
||||
}
|
||||
|
||||
features.push(feature);
|
||||
});
|
||||
@ -308,7 +363,68 @@ export class MapOl implements MapInterface {
|
||||
|
||||
console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
||||
}
|
||||
/**
|
||||
* 初始化 Popup Overlay
|
||||
* @param container 父组件传入的 DOM 容器
|
||||
*/
|
||||
initPopupOverlay(container: HTMLElement) {
|
||||
this.popupElement = container;
|
||||
|
||||
this.popupOverlay = new Overlay({
|
||||
element: this.popupElement,
|
||||
positioning: 'bottom-center', // 气泡在点位上方
|
||||
offset: [0, -10], // 向上偏移一点
|
||||
stopEvent: false, // 允许点击事件穿透到地图(如果需要点击地图关闭)
|
||||
autoPan: false
|
||||
// autoPan: {
|
||||
// animation: {
|
||||
// duration: 250
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
if (this.map) {
|
||||
this.map.addOverlay(this.popupOverlay);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 在 pointermove 中调用此方法显示/隐藏 Popup
|
||||
* @param feature 当前悬停的要素
|
||||
* @param coordinate 地理坐标
|
||||
*/
|
||||
showPopup(feature: Feature | undefined, coordinate: number[] | undefined) {
|
||||
if (!this.popupOverlay || !this.popupElement) return;
|
||||
|
||||
if (feature && coordinate) {
|
||||
// 1. 获取数据
|
||||
const props = feature.getProperties();
|
||||
|
||||
// 2. 生成内容 (这里假设父组件已经在 feature 上绑定了 popupHtml 或者我们在这里构建)
|
||||
// 方案 A: 如果父组件传入了 HTML 字符串
|
||||
const popupHtml = props.popupHtml || props.popup || '';
|
||||
|
||||
// 方案 B: 如果父组件希望动态更新 DOM (推荐,性能更好,支持交互)
|
||||
// 我们可以触发一个事件,让 Vue 组件去更新这个 div 的内容
|
||||
// 这里为了简单,直接演示如何填充内容
|
||||
|
||||
if (popupHtml) {
|
||||
this.popupElement.innerHTML = popupHtml;
|
||||
this.popupOverlay.setPosition(coordinate);
|
||||
this.popupElement.style.display = 'block';
|
||||
} else {
|
||||
// 如果没有预设 HTML,可以显示默认文本
|
||||
this.popupElement.innerHTML = `<div style="padding:5px; background:white; border-radius:4px;">${
|
||||
props.stnm || props.titleName || '未知'
|
||||
}</div>`;
|
||||
this.popupOverlay.setPosition(coordinate);
|
||||
this.popupElement.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// 隐藏
|
||||
this.popupOverlay.setPosition(undefined);
|
||||
this.popupElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小)
|
||||
*/
|
||||
@ -346,24 +462,33 @@ export class MapOl implements MapInterface {
|
||||
// 基础字体 12px,随缩放比例线性变化
|
||||
const fontSize = Math.max(10, Math.min(24, 12 * dynamicScale));
|
||||
|
||||
return new Style({
|
||||
const currentFeatureId = feature.getId() || (feature as any).ol_uid;
|
||||
const isHovered = this.hoveredFeatureId === currentFeatureId;
|
||||
const zIndex = isHovered ? 100 : 0;
|
||||
const styleOptions: any = {
|
||||
image: new Icon({
|
||||
src: iconUrl,
|
||||
scale: dynamicScale, // ✅ 使用动态缩放
|
||||
anchor: [0.5, 0.5], // 锚点:水平居中,底部对齐
|
||||
// 确保图标清晰
|
||||
scale: dynamicScale,
|
||||
anchor: [0.5, 0.5],
|
||||
crossOrigin: 'anonymous'
|
||||
}),
|
||||
text: new Text({
|
||||
})
|
||||
|
||||
// zIndex: zIndex // ✅ 设置样式的 zIndex
|
||||
};
|
||||
|
||||
// ✅ 4. 只有在悬停时才添加 Text 样式
|
||||
// if (isHovered && labelText) {
|
||||
styleOptions.text = new Text({
|
||||
text: labelText,
|
||||
offsetY: -20 * dynamicScale, // ✅ 偏移量也随缩放调整,保持相对位置
|
||||
font: `${fontSize}px sans-serif`, // ✅ 使用动态字体大小
|
||||
offsetY: -20 * dynamicScale,
|
||||
font: `${fontSize}px sans-serif`,
|
||||
fill: new Fill({ color: '#fff' }),
|
||||
stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }),
|
||||
textAlign: 'center',
|
||||
declutterMode: 'declutter'
|
||||
})
|
||||
declutterMode: 'declutter' // 避免标签重叠
|
||||
});
|
||||
// }
|
||||
return new Style(styleOptions);
|
||||
}
|
||||
/**
|
||||
* 初始化加载基础图层
|
||||
@ -515,7 +640,7 @@ export class MapOl implements MapInterface {
|
||||
console.log('正在请求裁切数据:', url);
|
||||
|
||||
// 等待数据加载
|
||||
const geoJsonData = await this.loadGeoJsonData2(url);
|
||||
const geoJsonData = await this.loadGeoJsonData(url);
|
||||
console.log('获取到的原始 GeoJSON:', geoJsonData);
|
||||
|
||||
// ✅ 新增:在应用遮罩前,先让地图视角聚焦到该区域
|
||||
@ -1336,9 +1461,9 @@ export class MapOl implements MapInterface {
|
||||
// duration: 动画持续时间
|
||||
// maxZoom: 限制最大缩放级别,防止放大过度导致像素化
|
||||
this.view.fit(extent, {
|
||||
padding: [50, 50, 50, 50], // 上右下左留白
|
||||
padding: [100, 200, 50, 50], // 上右下左留白
|
||||
duration: 1000, // 1秒动画
|
||||
maxZoom: 15 // 根据底图精度调整,避免过度放大
|
||||
maxZoom: 13 // 根据底图精度调整,避免过度放大
|
||||
});
|
||||
|
||||
console.log('地图视野已调整至裁切区域');
|
||||
@ -1366,6 +1491,14 @@ export class MapOl implements MapInterface {
|
||||
|
||||
// 强制重绘以移除遮罩效果
|
||||
baseLayer.changed();
|
||||
if (this.view) {
|
||||
this.view.animate({
|
||||
center: fromLonLat(CENTER_positionCN),
|
||||
zoom: INITIAL_ZOOM,
|
||||
duration: 1000 // 动画持续时间,保持与 fitViewToGeoJson 一致
|
||||
});
|
||||
console.log('地图视野已重置为初始状态');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 格式化长度输出 (米)
|
||||
@ -1385,7 +1518,10 @@ export class MapOl implements MapInterface {
|
||||
}
|
||||
|
||||
mdLayerShowOrHidden(): void {}
|
||||
switchView(): void {}
|
||||
|
||||
switchView(type): void {
|
||||
console.log(type);
|
||||
}
|
||||
fitBounds(): void {}
|
||||
/**
|
||||
* 飞行定位到指定经纬度和缩放级别
|
||||
@ -1425,31 +1561,48 @@ export class MapOl implements MapInterface {
|
||||
|
||||
this._maskPrerender = null;
|
||||
this._maskPostrender = null;
|
||||
// 2. 清除所有通过 layerRegistry 管理的图层
|
||||
if (this.layerRegistry) {
|
||||
this.layerRegistry.forEach(layer => {
|
||||
|
||||
// ✅ 关键:先清除所有图层的源,再移除图层,这能更有效地中断网络请求
|
||||
const clearLayer = (layer: any) => {
|
||||
if (layer && layer.getSource) {
|
||||
const source = layer.getSource();
|
||||
if (source && source.clear) {
|
||||
source.clear(); // 清除缓存
|
||||
}
|
||||
// 对于 WMTS/XYZ,OL 内部有 TileQueue,dispose source 有助于清理
|
||||
if (source && source.dispose) {
|
||||
// 注意:某些 OL 版本 source.dispose 可能不是公开 API,但 clear 是必须的
|
||||
}
|
||||
}
|
||||
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
// 强制解除引用
|
||||
if (layer.setMap) layer.setMap(null);
|
||||
};
|
||||
|
||||
// 2. 清除通过 layerRegistry 管理的图层
|
||||
if (this.layerRegistry) {
|
||||
this.layerRegistry.forEach(clearLayer);
|
||||
this.layerRegistry.clear();
|
||||
}
|
||||
|
||||
// ✅ 3. 清除点图层 Registry
|
||||
// 3. 清除点图层 Registry
|
||||
if (this.pointLayerRegistry) {
|
||||
this.pointLayerRegistry.forEach(layer => {
|
||||
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
this.pointLayerRegistry.forEach(clearLayer);
|
||||
this.pointLayerRegistry.clear();
|
||||
}
|
||||
|
||||
// 4. 清除地图实例
|
||||
if (this.map) {
|
||||
// 先清除交互和覆盖物
|
||||
this.map.getInteractions().clear();
|
||||
this.map.getOverlays().clear();
|
||||
|
||||
// 设置 target 为 undefined 会从 DOM 移除 canvas
|
||||
this.map.setTarget(undefined);
|
||||
|
||||
// 销毁地图实例
|
||||
this.map.dispose();
|
||||
this.map = null;
|
||||
}
|
||||
@ -1465,6 +1618,8 @@ export class MapOl implements MapInterface {
|
||||
this.measureSource = null;
|
||||
this.measureLayer = null;
|
||||
this.drawInteraction = null;
|
||||
this.geoJsonData = null;
|
||||
this.geoJsonData1 = null;
|
||||
|
||||
console.log('地图实例已销毁');
|
||||
}
|
||||
|
||||
203
frontend/src/components/iconDiv/index.scss
Normal file
@ -0,0 +1,203 @@
|
||||
.map-tooltip {
|
||||
height: auto;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding: 24px 0 25px;
|
||||
// width: 279px;
|
||||
width: 260px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 24px;
|
||||
background: url(@/assets/images/mapTip/top@260.png) center no-repeat;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 25px;
|
||||
background: url(@/assets/images/mapTip/bottom@260.png) center no-repeat;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
|
||||
&-box {
|
||||
padding: 0 12px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
bottom: 25px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background: url(@/assets/images/mapTip/center@260.png) repeat-y;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-list {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
|
||||
.__label {
|
||||
flex: 0 0 100px;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.__warn {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
margin-top: 10px;
|
||||
border-top: 1px dashed #12aaeb;
|
||||
padding: 10px 0;
|
||||
margin: 10px 0 0;
|
||||
&.__noborder {
|
||||
border: none;
|
||||
margin-top: 6px;
|
||||
}
|
||||
&.__small {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-sub {
|
||||
margin: 0;
|
||||
padding: 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
&-sub1 {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 1px;
|
||||
border-bottom: 1px dashed #12aaeb;
|
||||
margin-top: 16px;
|
||||
.__content {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgb(88 110 134);
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
&.map-tooltip-small {
|
||||
width: 175px;
|
||||
padding: 18px 0 18px;
|
||||
&::after {
|
||||
background-image: url(@/assets/images/mapTip/top@175.png);
|
||||
height: 18px;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
&::before {
|
||||
background-image: url(@/assets/images/mapTip/bottom@175.png);
|
||||
height: 18px;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
.map-tooltip-box {
|
||||
padding: 0 10px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
bottom: 18px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-image: url(@/assets/images/mapTip/center@175.png);
|
||||
filter: opacity(75%);
|
||||
}
|
||||
}
|
||||
.map-tooltip-list {
|
||||
> li {
|
||||
.__label {
|
||||
flex: 0 0 80px;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.map-tooltip-middle {
|
||||
width: 215px;
|
||||
padding: 18px 0 18px;
|
||||
&::after {
|
||||
background-image: url(@/assets/images/mapTip/top@215.png);
|
||||
height: 18px;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
&::before {
|
||||
background-image: url(@/assets/images/mapTip/bottom@215.png);
|
||||
height: 18px;
|
||||
filter: opacity(75%);
|
||||
}
|
||||
.map-tooltip-box {
|
||||
padding: 0 10px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
bottom: 18px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-image: url(@/assets/images/mapTip/center@215.png);
|
||||
filter: opacity(75%);
|
||||
}
|
||||
}
|
||||
.map-tooltip-list {
|
||||
> li {
|
||||
.__label {
|
||||
flex: 0 0 110px;
|
||||
width: 110px;
|
||||
}
|
||||
}
|
||||
&.__list2 {
|
||||
> li {
|
||||
.__label {
|
||||
flex: 0 0 90px;
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-waterQuality-subtitle {
|
||||
margin-top: 10px;
|
||||
border: 1px solid rgba(77, 129, 167);
|
||||
border-bottom: none;
|
||||
|
||||
.map-waterQuality-leftType,
|
||||
.map-waterQuality-rightType {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
1770
frontend/src/components/iconDiv/index.vue
Normal file
@ -20,9 +20,10 @@ import { ref, watch, computed } from "vue";
|
||||
import { useUiStore } from "@/store/modules/ui";
|
||||
import Calculate from "./Calculate.vue";
|
||||
import LayerController from "./LayerController.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
map: any;
|
||||
onClick:(key: any) => void
|
||||
onClick: (key: any, mapType: any) => void;
|
||||
}>();
|
||||
const map = props.map;
|
||||
|
||||
@ -90,7 +91,8 @@ const controllers: any = computed(() => [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mapType.value === "2D"
|
||||
? {
|
||||
children: [
|
||||
{
|
||||
name: "测量工具",
|
||||
@ -98,8 +100,10 @@ const controllers: any = computed(() => [
|
||||
component: Calculate,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
}
|
||||
: {},
|
||||
mapType.value === "2D"
|
||||
? {
|
||||
children: [
|
||||
{
|
||||
name: "梯级",
|
||||
@ -107,7 +111,30 @@ const controllers: any = computed(() => [
|
||||
icon: "tiji",
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
mapType.value === "3D"
|
||||
? {
|
||||
children: [
|
||||
{
|
||||
name: "电站倾斜摄影",
|
||||
key: "OSBGController",
|
||||
icon: "obliquePhotography",
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
mapType.value === "3D"
|
||||
? {
|
||||
children: [
|
||||
{
|
||||
name: "流域三维漫游",
|
||||
key: "threedRoam",
|
||||
icon: "roaming",
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
@ -117,17 +144,6 @@ const controllers: any = computed(() => [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "倾斜摄影",
|
||||
// key: "OSBGController",
|
||||
// icon: "obliquePhotography",
|
||||
// },
|
||||
// {
|
||||
// name: "三维漫游",
|
||||
// key: "threedRoam",
|
||||
// icon: "roaming",
|
||||
// },
|
||||
]);
|
||||
|
||||
// 添加全屏切换功能
|
||||
@ -146,20 +162,26 @@ const handleControllerClick = (item: any) => {
|
||||
switch (item.key) {
|
||||
case "fullScreen":
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
map.jdPanelControlShowAndHidden('01',false)
|
||||
map.jdPanelControlShowAndHidden("01", false);
|
||||
// map.mdLayerTreeShowOrHidden('fp_point',!isFullScreen.value);
|
||||
// map.controlBaseLayerTreeShowAndHidden('customBaseLayer','customBaseLayer',!isFullScreen.value);
|
||||
// map.controlBaseLayerTreeShowAndHidden('lcj_bhq','lcj_bhq',!isFullScreen.value);
|
||||
// toggleFullScreen();
|
||||
break;
|
||||
case "zoomIn":
|
||||
map.zoomToggle('in');
|
||||
map.zoomToggle("in");
|
||||
break;
|
||||
case "zoomOut":
|
||||
map.zoomToggle('out');
|
||||
map.zoomToggle("out");
|
||||
break;
|
||||
case "dim":
|
||||
mapType.value = mapType.value === "2D" ? "3D" : "2D";
|
||||
if (mapType.value === "2D") {
|
||||
uiStore.drawerOpen = true;
|
||||
} else {
|
||||
uiStore.drawerOpen = false;
|
||||
}
|
||||
props.onClick("dim", mapType.value);
|
||||
break;
|
||||
case "rightDrawer":
|
||||
// 使用全局事件切换右侧面板
|
||||
@ -167,10 +189,10 @@ const handleControllerClick = (item: any) => {
|
||||
break;
|
||||
// 可以在这里添加其他控制器的处理逻辑
|
||||
case "screenShot":
|
||||
map.mapOutPut()
|
||||
map.mapOutPut();
|
||||
break;
|
||||
case "TJ":
|
||||
props.onClick(4)
|
||||
props.onClick(4, null);
|
||||
// map.addTertiarybasinLayer(servers.Tertiarybasin, '#4DFFDD', '#92A0A5')
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -16,7 +16,7 @@ const routeKey = computed(() => router.path + Math.random());
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<!-- <GisView /> -->
|
||||
<GisView />
|
||||
<div class="gi-panels">
|
||||
<router-view v-slot="{ Component, route }" :key="routeKey">
|
||||
<transition name="router-fade" mode="out-in">
|
||||
|
||||
@ -4,17 +4,20 @@ import router from '@/router';
|
||||
import { setupStore } from '@/store';
|
||||
|
||||
import ElementPlus from 'element-plus';
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import '@/permission';
|
||||
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css' // Ant Design 全局样式重置
|
||||
import Antd from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/reset.css'; // Ant Design 全局样式重置
|
||||
import dayjs from 'dayjs'; // ant 中文语言
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
// @ts-ignore
|
||||
import 'virtual:svg-icons-register';
|
||||
// 3d地图
|
||||
import * as Cesium from 'cesium';
|
||||
import 'cesium/Build/Cesium/Widgets/widgets.css';
|
||||
|
||||
// 国际化
|
||||
import i18n from '@/lang/index';
|
||||
@ -31,7 +34,7 @@ Object.keys(directive).forEach(key => {
|
||||
app.directive(key, (directive as { [key: string]: Directive })[key]);
|
||||
});
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
app.component(key, component);
|
||||
}
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
@ -46,6 +49,7 @@ app
|
||||
.use(router)
|
||||
.use(ElementPlus)
|
||||
.use(Antd)
|
||||
.use(Antd)
|
||||
.use(WujieVue)
|
||||
.use(i18n)
|
||||
.mount('#app');
|
||||
|
||||
@ -13,26 +13,49 @@ import { ref, onMounted, watch } from "vue";
|
||||
|
||||
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
|
||||
import SidePanelItem from "@/components/SidePanelItem/index.vue";
|
||||
import { getBaseKenWbsbdoList } from "@/api/home";
|
||||
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: "jidiInfoMod",
|
||||
});
|
||||
const title_text = ref(
|
||||
"我国水能资源丰富,主要集中在金沙江、长江上游、雅砻江、黄河上游、大渡河、南盘江-红水河、乌江和西南诸河等流域,总技术可开发量约3.81亿kW,占全国技术可开发量的55.5%。截至2023年年底,雅砻江、金沙江、大渡河、乌江、长江上游、南盘江-红水河等流域已建和在建比例超80%,水能资源开发程度较高,剩余待开发水利资源主要集中在西南诸河,发展潜力巨大。"
|
||||
);
|
||||
const title_text = ref("");
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
(newVal) => {
|
||||
console.log(newVal);
|
||||
if (newVal.name == '当前全部') {
|
||||
if (newVal.name == "当前全部") {
|
||||
}
|
||||
initText();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
const initText = () => {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: "and",
|
||||
filters: [
|
||||
{
|
||||
field: "wbsCode",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: JidiSelectEventStore.selectedItem.wbsCode,
|
||||
},
|
||||
{ field: "wbsType", operator: "eq", dataType: "string", value: "PSB" },
|
||||
],
|
||||
},
|
||||
};
|
||||
getBaseKenWbsbdoList(params).then((res) => {
|
||||
console.log(res);
|
||||
title_text.value = res.data.data[0].introduce;
|
||||
});
|
||||
};
|
||||
// 页面加载时执行的逻辑
|
||||
onMounted(() => {});
|
||||
onMounted(() => {
|
||||
initText();
|
||||
console.log(JidiSelectEventStore.selectedItem);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@ -59,6 +59,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
//login
|
||||
next({ path: '/' });
|
||||
NProgress.done();
|
||||
return;
|
||||
} else {
|
||||
const hasGetUserInfo = userStore.roles.length > 0;
|
||||
if (hasGetUserInfo) {
|
||||
@ -86,10 +87,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
// ✅ 关键修复:在添加路由前,标准化所有路径
|
||||
accessRoutes = normalizeRoutes(accessRoutes);
|
||||
|
||||
console.log('Normalized Access Routes:', accessRoutes);
|
||||
|
||||
accessRoutes.forEach((route: any) => {
|
||||
console.log('Adding Route:', route.path);
|
||||
router.addRoute(route);
|
||||
});
|
||||
|
||||
|
||||
@ -96,3 +96,15 @@ svg {
|
||||
height: 98%;
|
||||
position: relative;
|
||||
}
|
||||
.table-container {
|
||||
flex: 1;
|
||||
/* 占据剩余所有空间 */
|
||||
overflow: hidden;
|
||||
/* 关键:防止表格溢出容器 */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-tooltip-inner {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -48,22 +48,22 @@ export function mix(color1: string, color2: string, weight: number) {
|
||||
|
||||
export function parseTime(time: any, cFormat: any) {
|
||||
if (arguments.length === 0) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||
let date
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
|
||||
let date;
|
||||
if (typeof time === 'undefined' || time === null || time === 'null') {
|
||||
return ''
|
||||
return '';
|
||||
} else if (typeof time === 'object') {
|
||||
date = time
|
||||
date = time;
|
||||
} else {
|
||||
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
|
||||
time = parseInt(time)
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time);
|
||||
}
|
||||
if ((typeof time === 'number') && (time.toString().length === 10)) {
|
||||
time = time * 1000
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000;
|
||||
}
|
||||
date = new Date(time)
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj: any = {
|
||||
y: date.getFullYear(),
|
||||
@ -73,16 +73,21 @@ export function parseTime(time :any, cFormat :any) {
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
};
|
||||
const time_str = format.replace(
|
||||
/{(y|m|d|h|i|s|a)+}/g,
|
||||
(result: any, key: any) => {
|
||||
let value: any = formatObj[key];
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value];
|
||||
}
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result :any, key :any) => {
|
||||
let value : any = formatObj[key]
|
||||
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0
|
||||
})
|
||||
return time_str
|
||||
return value || 0;
|
||||
}
|
||||
);
|
||||
return time_str;
|
||||
}
|
||||
export function downloadFile(obj: any, name: any, suffix: any) {
|
||||
try {
|
||||
@ -129,11 +134,11 @@ export function downloadFileByUrl(url: string, fileName?: string) {
|
||||
// 方法 1: 使用 fetch 获取 Blob (推荐,可重命名且强制下载)
|
||||
// 注意:如果 URL 跨域且服务器未配置 CORS,fetch 会失败
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
.then(blob => {
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
@ -148,8 +153,11 @@ export function downloadFileByUrl(url: string, fileName?: string) {
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
}, 100);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Fetch download failed (possibly CORS), falling back to direct link:', error);
|
||||
.catch(error => {
|
||||
console.warn(
|
||||
'Fetch download failed (possibly CORS), falling back to direct link:',
|
||||
error
|
||||
);
|
||||
|
||||
// 方法 2: 回退方案 - 直接使用 a 标签跳转
|
||||
// 这种方式对于 PDF/图片等浏览器支持预览的文件,可能会直接在新标签页打开而不是下载
|
||||
@ -178,3 +186,18 @@ Object.entries(modules).forEach(([path, module]) => {
|
||||
export const getIconPath = (icon: string): string => {
|
||||
return iconMap[icon] || '';
|
||||
};
|
||||
|
||||
// 动态计算表格滚动高度
|
||||
export const calcTableScrollY = (
|
||||
tableContainerRef: any,
|
||||
otherHeight: number = 112
|
||||
) => {
|
||||
if (!tableContainerRef) return;
|
||||
// 获取容器高度
|
||||
const containerHeight = tableContainerRef.clientHeight;
|
||||
// 估算表头(约 55px) + 分页器(约 64px) + 上下内边距容差(10px) = 约 130px
|
||||
// 如果你有筛选栏等,需要额外减去
|
||||
const height = otherHeight || 112;
|
||||
const y = containerHeight - height;
|
||||
return y > 0 ? y : undefined;
|
||||
};
|
||||
|
||||
@ -19,10 +19,21 @@ service.interceptors.request.use(
|
||||
`Expected 'config' and 'config.headers' not to be undefined`
|
||||
);
|
||||
}
|
||||
if (config.url.includes('/dec-lygk-base-server')) {
|
||||
config.headers._appid = '974975A6-47FD-4C04-9ACD-68938D2992BD';
|
||||
config.headers._isolateid = '5b34aecb-adfb-4dfc-ad95-21505a9eb388';
|
||||
config.headers._platformid = '31e6d13f-c359-4a19-8e67-b6f868f2401b';
|
||||
config.headers._sysid = '10EC2E0B-AEA9-4757-83A2-201BA1BC54E9';
|
||||
|
||||
config.headers.authorization =
|
||||
'bearer f96d0008-f77f-497a-b05d-82cded9ccd89';
|
||||
config.baseURL = '/';
|
||||
} else {
|
||||
const user = useUserStoreHook();
|
||||
if (user.Token) {
|
||||
config.headers.token = getToken();
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: any) => {
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="approval-detail-search">
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
@click="props.urgeBtn"
|
||||
:loading="btnLoading"
|
||||
:disabled="batchData.length === 0"
|
||||
>
|
||||
催促
|
||||
</a-button>
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import BasicSearch from "@/components/BasicSearch/index.vue";
|
||||
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
|
||||
|
||||
interface Props {
|
||||
batchData: any[];
|
||||
urgeBtn: () => void;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "reset", values: any): void;
|
||||
(e: "search-finish", values: any): void;
|
||||
}>();
|
||||
|
||||
const shuJuTianBaoStore = useShuJuTianBaoStore();
|
||||
const localTypeDate = ref<string>(null);
|
||||
const basicSearchRef = ref<any>();
|
||||
const btnLoading = ref<boolean>(false);
|
||||
|
||||
const initSearchData = {
|
||||
hbrvcd: "all",
|
||||
stcd: null,
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: "waterStation",
|
||||
name: "hbrvcd",
|
||||
label: "流域",
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
options: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit("search-finish", values);
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
searchData.value = { ...searchData.value, ...allValues };
|
||||
if (
|
||||
Object.keys(changedValues)[0] == "rstcd" ||
|
||||
Object.keys(changedValues)[0] == "hbrvcd"
|
||||
) {
|
||||
const formInstance = basicSearchRef.value?.formData;
|
||||
formInstance.stcd = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
localTypeDate.value = null;
|
||||
emit("reset", initSearchData);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
btnLoading,
|
||||
});
|
||||
onMounted(() => {
|
||||
emit("search-finish", initSearchData);
|
||||
shuJuTianBaoStore.getFpssOption("", "");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@ -1,239 +1,356 @@
|
||||
<template>
|
||||
<div class="guoYuDaoShuTongJi-page">
|
||||
<!-- 搜索组件 -->
|
||||
<ApprovalDetailSearch
|
||||
:guoyu-status="guoyuStatus"
|
||||
:direction="direction"
|
||||
<GuoYuDaoShuTongJiSearch
|
||||
ref="searchRef"
|
||||
:urgeBtn="urgeBtn"
|
||||
:batchData="batchData"
|
||||
@search-finish="handleDetailSearchFinish"
|
||||
@reset="handleDetailReset"
|
||||
/>
|
||||
|
||||
<!-- 表格组件 -->
|
||||
<BasicTable
|
||||
ref="detailTableRef"
|
||||
:columns="columns"
|
||||
:isOneLoad="false"
|
||||
:data="tableData"
|
||||
:list-url="getFishDraftPage"
|
||||
:scroll-y="'500px'"
|
||||
:list-url="statistics"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<!-- 判断列的 key,如果是 selection,就显示自定义内容 -->
|
||||
<span v-if="column.key === 'selection'">
|
||||
<Checkbox
|
||||
:checked="headerCheckboxState.checked"
|
||||
:indeterminate="headerCheckboxState.indeterminate"
|
||||
@change="handleHeaderSelectAll"
|
||||
/>
|
||||
</span>
|
||||
<!-- 其他列保持默认(必须写 fallback,否则其他列表头会消失) -->
|
||||
<span v-else>{{ column.title }}</span>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, h, computed, nextTick } from "vue";
|
||||
import { batchApproveByApprovalId, batchReject } from "@/api/shengPiJiLu";
|
||||
import { Tag, message } from "ant-design-vue";
|
||||
import dayjs from "dayjs";
|
||||
import { ref, onMounted, h, computed } from "vue";
|
||||
import { Checkbox, Tooltip, message, Modal } from "ant-design-vue";
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
import ApprovalDetailSearch from "../shengPiJiLu/approvalDetailSearch.vue";
|
||||
import { getDictItemsByCode } from "@/api/dict";
|
||||
import { getFishDraftPage } from "@/api/guoYuSheShiShuJuTianBao";
|
||||
import GuoYuDaoShuTongJiSearch from "./guoYuDaoShuTongJiSearch.vue";
|
||||
import { statistics, batchUrgeContent } from "@/api/guoYuSheShiShuJuTianBao";
|
||||
const searchRef = ref();
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const detailTableRef = ref();
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
const batchData = ref([]);
|
||||
const tableDataList = ref<any[]>([]); // 用于存储当前表格数据,方便查找
|
||||
|
||||
// ✅ 1. 定义获取唯一 Key 的方法 (请根据实际数据字段调整,比如 id, stcd 等)
|
||||
const getUniqueKey = (item: any) => item.id || item.stcd || JSON.stringify(item);
|
||||
|
||||
// ✅ 2. 获取同一组的所有 Key (用于联动选中)
|
||||
const getGroupKeys = (currentRecord: any) => {
|
||||
const keys: string[] = [];
|
||||
tableDataList.value.forEach((item) => {
|
||||
if (
|
||||
item.basinNames === currentRecord.basinNames &&
|
||||
item.stationNames === currentRecord.stationNames
|
||||
) {
|
||||
if (item.hasData != 1) {
|
||||
keys.push(getUniqueKey(item));
|
||||
}
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
// ✅ 3. 处理组选中逻辑
|
||||
const handleGroupSelect = (checked: boolean, groupKeys: string[]) => {
|
||||
const newSelectedKeys = new Set(selectedRowKeys.value);
|
||||
|
||||
if (checked) {
|
||||
groupKeys.forEach((key) => newSelectedKeys.add(key));
|
||||
} else {
|
||||
groupKeys.forEach((key) => newSelectedKeys.delete(key));
|
||||
}
|
||||
|
||||
selectedRowKeys.value = Array.from(newSelectedKeys);
|
||||
updateBatchData();
|
||||
};
|
||||
|
||||
// ✅ 新增:计算表头 Checkbox 的状态
|
||||
const headerCheckboxState = computed(() => {
|
||||
if (!tableDataList.value || tableDataList.value.length === 0) {
|
||||
return { checked: false, indeterminate: false };
|
||||
}
|
||||
|
||||
// 1. 只获取【有效行】(hasData != 1)的 Key
|
||||
const validItems = tableDataList.value.filter((item) => item.hasData != 1);
|
||||
const validKeys = validItems.map((item) => getUniqueKey(item));
|
||||
|
||||
// 如果没有有效行,直接返回未选中
|
||||
if (validKeys.length === 0) {
|
||||
return { checked: false, indeterminate: false };
|
||||
}
|
||||
|
||||
// 2. 计算有多少【有效行】被选中
|
||||
const selectedCount = validKeys.filter((key) => selectedRowKeys.value.includes(key))
|
||||
.length;
|
||||
|
||||
// 3. 判断状态
|
||||
const isAllSelected = selectedCount === validKeys.length;
|
||||
const isIndeterminate = selectedCount > 0 && selectedCount < validKeys.length;
|
||||
|
||||
return {
|
||||
checked: isAllSelected,
|
||||
indeterminate: isIndeterminate,
|
||||
};
|
||||
});
|
||||
const updateBatchData = () => {
|
||||
// 从 tableDataList 中找出所有 key 在 selectedRowKeys 中的完整对象
|
||||
batchData.value = tableDataList.value.filter((item) =>
|
||||
selectedRowKeys.value.includes(getUniqueKey(item))
|
||||
);
|
||||
};
|
||||
// ✅ 新增:处理表头全选/全不选
|
||||
const handleHeaderSelectAll = (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
// 选中所有有效行
|
||||
const validItems = tableDataList.value.filter((item) => item.hasData != 1);
|
||||
const allKeys = validItems.map((item) => getUniqueKey(item));
|
||||
selectedRowKeys.value = allKeys;
|
||||
} else {
|
||||
// 清空所有选中
|
||||
selectedRowKeys.value = [];
|
||||
}
|
||||
updateBatchData();
|
||||
};
|
||||
|
||||
let columns = ref([
|
||||
{
|
||||
dataIndex: "hbrvnm",
|
||||
key: "hbrvnm",
|
||||
title: "流域",
|
||||
customCell: (_, index) => {
|
||||
if (index === 0) {
|
||||
return { rowSpan: 2 };
|
||||
}
|
||||
// These two are merged into above cell
|
||||
if (index === 1) {
|
||||
return { rowSpan: 0 };
|
||||
}
|
||||
if (index === 2) {
|
||||
return { colSpan: 1 };
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "ennm",
|
||||
key: "ennm",
|
||||
title: "电站名称",
|
||||
customCell: (_, index) => {
|
||||
if (index === 0) {
|
||||
return { rowSpan: 2 };
|
||||
}
|
||||
// These two are merged into above cell
|
||||
if (index === 1) {
|
||||
return { rowSpan: 0 };
|
||||
}
|
||||
if (index === 2) {
|
||||
return { colSpan: 1 };
|
||||
}
|
||||
},
|
||||
},
|
||||
{ dataIndex: "stnm", key: "stnm", title: "月份" },
|
||||
{ dataIndex: "startTime", key: "startTime", title: "过鱼开始时间" },
|
||||
{ dataIndex: "endTime", key: "endTime", title: "过鱼结束时间" },
|
||||
{ dataIndex: "ftpName", key: "ftpName", title: "本月过鱼总数" },
|
||||
{ dataIndex: "ftpName1", key: "ftpName1", title: "联系人/电话", width: 250 },
|
||||
]);
|
||||
const tableData = ref([
|
||||
{
|
||||
hbrvnm: "黄河干流上游",
|
||||
ennm: "玛尔挡",
|
||||
stnm: "2026-03",
|
||||
ftpName: 61,
|
||||
ftpName1: "张三,13800000000",
|
||||
startTime: "2026-03-01",
|
||||
endTime: "2026-03-31",
|
||||
},
|
||||
{
|
||||
hbrvnm: "黄河干流上游",
|
||||
ennm: "玛尔挡",
|
||||
stnm: "2026-04",
|
||||
ftpName: 666,
|
||||
ftpName1: "张三,13800000000",
|
||||
startTime: "2026-04-01",
|
||||
endTime: "2026-04-31",
|
||||
},
|
||||
{
|
||||
hbrvnm: "雅鲁藏布江流域",
|
||||
ennm: "ZM",
|
||||
stnm: "2026-04",
|
||||
ftpName: 777,
|
||||
ftpName1: "李四,13800000000",
|
||||
startTime: "2026-04-01",
|
||||
endTime: "2026-04-31",
|
||||
},
|
||||
]);
|
||||
const tableRef = ref();
|
||||
const handleSearchFinish = (values: any) => {
|
||||
console.log(values);
|
||||
const filters = [
|
||||
values.status && {
|
||||
field: "status",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.status,
|
||||
},
|
||||
values.hbrvcd !== "all" && {
|
||||
field: "hbrvcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.hbrvcd,
|
||||
},
|
||||
values.stcd && {
|
||||
field: "stcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.stcd,
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
const filter = {
|
||||
logic: "and",
|
||||
filters: filters,
|
||||
};
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
const handleReset = (values) => {
|
||||
handleSearchFinish(values);
|
||||
};
|
||||
|
||||
// 审批操作日志弹框相关
|
||||
const approvalLogVisible = ref(false);
|
||||
const approvalLogTableRef = ref();
|
||||
|
||||
// 数据变更记录弹框相关
|
||||
const changeLogVisible = ref(false);
|
||||
const changeLogTableRef = ref();
|
||||
|
||||
const currentApprovalId = ref("");
|
||||
const actionTypeDict = ref([]);
|
||||
const operationTypeDict = ref([]);
|
||||
|
||||
// 详情弹框相关
|
||||
const detailVisible = ref(false);
|
||||
const detailTableRef = ref();
|
||||
const guoyuStatus = ref([]);
|
||||
const direction = ref([]);
|
||||
|
||||
// 查看详情弹框相关
|
||||
const viewModalVisible = ref(false);
|
||||
const editModalRef = ref<any>(null);
|
||||
const currentViewRecord = ref<any>(null);
|
||||
|
||||
// 媒体预览相关
|
||||
const mediaPreviewVisible = ref(false);
|
||||
const videoPreviewTitle = ref("预览");
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
type: "image" | "video" | "formVideo" | "formImage";
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
const previewList = ref<MediaItem[]>([]);
|
||||
const currentMediaIndex = ref(0);
|
||||
|
||||
// 数据转换函数
|
||||
const customTransform = (res: any) => {
|
||||
const rawRecords = res?.data?.records || [];
|
||||
const modifiedRecords = rawRecords.map((item: any) => {
|
||||
title: "选择",
|
||||
key: "selection",
|
||||
width: 60,
|
||||
align: "center",
|
||||
// ✅ 合并选择列
|
||||
customCell: (record) => {
|
||||
return {
|
||||
...item,
|
||||
picpthList: item.picpth
|
||||
? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || []
|
||||
: [],
|
||||
rowSpan: record._selectionRowSpan || 0,
|
||||
};
|
||||
},
|
||||
// ✅ 使用 h 函数渲染 Checkbox
|
||||
customRender: ({ record }) => {
|
||||
// 只有合并组的第一行才渲染 Checkbox
|
||||
if (record._selectionRowSpan && record._selectionRowSpan > 0) {
|
||||
const groupKeys = getGroupKeys(record);
|
||||
|
||||
// 判断状态
|
||||
const isAllSelected =
|
||||
groupKeys.length > 0 &&
|
||||
groupKeys.every((key) => selectedRowKeys.value.includes(key));
|
||||
const isPartiallySelected = groupKeys.some((key) =>
|
||||
selectedRowKeys.value.includes(key)
|
||||
);
|
||||
|
||||
return h(Checkbox, {
|
||||
checked: isAllSelected,
|
||||
disabled: record.hasData == 1,
|
||||
indeterminate: isPartiallySelected && !isAllSelected,
|
||||
onChange: (e: any) => handleGroupSelect(e.target.checked, groupKeys),
|
||||
});
|
||||
}
|
||||
return null; // 其他行不渲染
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "basinNames",
|
||||
key: "basinNames",
|
||||
title: "流域",
|
||||
width: 160,
|
||||
customCell: (record, index) => {
|
||||
return {
|
||||
rowSpan: record._basinRowSpan || 0,
|
||||
};
|
||||
},
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
|
||||
const str = String(text);
|
||||
// 按逗号分割(支持中文逗号和英文逗号)
|
||||
const items = str.split(/[,,]/);
|
||||
|
||||
// 如果数量超过6个,截取前6个并添加省略号
|
||||
let displayText = "";
|
||||
if (items.length > 6) {
|
||||
displayText = items.slice(0, 6).join(",") + " ...";
|
||||
} else {
|
||||
displayText = str;
|
||||
}
|
||||
|
||||
// 返回 span 标签,title 属性用于悬停显示全称
|
||||
return h(
|
||||
Tooltip,
|
||||
{
|
||||
title: str, // Tooltip 显示的全部内容
|
||||
placement: "topLeft", // 可选:提示框位置
|
||||
},
|
||||
() => h("span", displayText)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "stationNames",
|
||||
key: "stationNames",
|
||||
title: "电站名称",
|
||||
width: 160,
|
||||
customCell: (record, index) => {
|
||||
return {
|
||||
rowSpan: record._stationRowSpan || 0,
|
||||
};
|
||||
},
|
||||
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
|
||||
const str = String(text);
|
||||
// 按逗号分割(支持中文逗号和英文逗号)
|
||||
const items = str.split(/[,,]/);
|
||||
|
||||
// 如果数量超过6个,截取前6个并添加省略号
|
||||
let displayText = "";
|
||||
if (items.length > 6) {
|
||||
displayText = items.slice(0, 6).join(",") + " ...";
|
||||
} else {
|
||||
displayText = str;
|
||||
}
|
||||
|
||||
// 返回 span 标签,title 属性用于悬停显示全称
|
||||
return h(
|
||||
Tooltip,
|
||||
{
|
||||
title: str, // Tooltip 显示的全部内容
|
||||
placement: "topLeft", // 可选:提示框位置
|
||||
},
|
||||
() => h("span", displayText)
|
||||
);
|
||||
},
|
||||
},
|
||||
{ dataIndex: "reportMonth", key: "reportMonth", title: "月份", width: 80 },
|
||||
{
|
||||
dataIndex: "minStrdt",
|
||||
key: "minStrdt",
|
||||
title: "过鱼开始时间",
|
||||
width: 140,
|
||||
customRender: ({ text }) => {
|
||||
return text ? dayjs(text).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "maxEnddt",
|
||||
key: "maxEnddt",
|
||||
title: "过鱼结束时间",
|
||||
width: 140,
|
||||
customRender: ({ text }) => {
|
||||
return text ? dayjs(text).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
{ dataIndex: "totalFcnt", key: "totalFcnt", title: "本月过鱼总数", width: 140 },
|
||||
{ dataIndex: "contact", key: "contact", title: "联系人/电话", width: 250 },
|
||||
]);
|
||||
|
||||
const customTransform = (res: any) => {
|
||||
// 1. 获取原始数据数组
|
||||
let rawRecords = [];
|
||||
if (Array.isArray(res?.data)) {
|
||||
rawRecords = res.data;
|
||||
} else if (Array.isArray(res)) {
|
||||
rawRecords = res;
|
||||
} else {
|
||||
rawRecords = res?.data?.records || res?.records || [];
|
||||
}
|
||||
|
||||
if (!rawRecords || rawRecords.length === 0) {
|
||||
return { records: [], total: 0 };
|
||||
}
|
||||
|
||||
// 2. 初始化:为每条数据添加合并标记,默认都为 1
|
||||
const modifiedRecords = rawRecords.map((item: any) => ({
|
||||
...item,
|
||||
_basinRowSpan: 1,
|
||||
_stationRowSpan: 1,
|
||||
_selectionRowSpan: 1, // ✅ 新增选择列合并标记
|
||||
}));
|
||||
|
||||
// 3. 遍历计算合并行
|
||||
for (let i = 1; i < modifiedRecords.length; i++) {
|
||||
const current = modifiedRecords[i];
|
||||
const prev = modifiedRecords[i - 1];
|
||||
|
||||
// ✅ 关键修改:只有当【流域】和【电站】都相同时,才合并
|
||||
const isSameBasin = current.basinNames === prev.basinNames;
|
||||
const isSameStation = current.stationNames === prev.stationNames;
|
||||
|
||||
if (isSameBasin && isSameStation) {
|
||||
// --- 情况 A: 完全相同 -> 合并所有相关列 ---
|
||||
|
||||
// 1. 处理流域列合并
|
||||
current._basinRowSpan = 0;
|
||||
let j = i - 1;
|
||||
while (j >= 0 && modifiedRecords[j]._basinRowSpan === 0) j--;
|
||||
if (j >= 0) modifiedRecords[j]._basinRowSpan += 1;
|
||||
|
||||
// 2. 处理电站列合并
|
||||
current._stationRowSpan = 0;
|
||||
let k = i - 1;
|
||||
while (k >= 0 && modifiedRecords[k]._stationRowSpan === 0) k--;
|
||||
if (k >= 0) modifiedRecords[k]._stationRowSpan += 1;
|
||||
|
||||
// 3. ✅ 处理选择列合并
|
||||
current._selectionRowSpan = 0;
|
||||
let m = i - 1;
|
||||
while (m >= 0 && modifiedRecords[m]._selectionRowSpan === 0) m--;
|
||||
if (m >= 0) modifiedRecords[m]._selectionRowSpan += 1;
|
||||
} else {
|
||||
// --- 情况 B: 有任何一个不同 -> 不合并 ---
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 更新全局数据引用,供 getGroupKeys 使用
|
||||
tableDataList.value = modifiedRecords;
|
||||
// 4. 返回结果
|
||||
return {
|
||||
records: modifiedRecords,
|
||||
total: res?.data?.total || 0,
|
||||
total: res.data.total || 0,
|
||||
};
|
||||
};
|
||||
|
||||
// 详情搜索处理
|
||||
const handleDetailSearchFinish = (values: any) => {
|
||||
console.log("详情搜索:", values);
|
||||
const filters = [
|
||||
values.ftp && {
|
||||
field: "ftp",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.ftp,
|
||||
},
|
||||
values.strdt && {
|
||||
field: "strdt",
|
||||
field: "maxEnddt",
|
||||
operator: "gte",
|
||||
dataType: "date",
|
||||
value: values.strdt[0] + " 00:00:00",
|
||||
},
|
||||
values.strdt && {
|
||||
field: "strdt",
|
||||
field: "minStrdt",
|
||||
operator: "lte",
|
||||
dataType: "date",
|
||||
value: values.strdt[1] + " 23:59:59",
|
||||
},
|
||||
values.direction && {
|
||||
field: "direction",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.direction,
|
||||
},
|
||||
values.rstcd && {
|
||||
field: "rstcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.rstcd,
|
||||
},
|
||||
values.hbrvcd !== "all" && {
|
||||
field: "hbrvcd",
|
||||
field: "basinCode",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.hbrvcd,
|
||||
},
|
||||
approvalId.value && {
|
||||
field: "approvalId", // 字段名
|
||||
operator: "eq", // 等于操作符
|
||||
dataType: "string", // 字符串类型
|
||||
value: approvalId.value, // 从 record 中获取审批批次号
|
||||
values.rstcd && {
|
||||
field: "stationCode",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.rstcd,
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
@ -246,76 +363,45 @@ const handleDetailSearchFinish = (values: any) => {
|
||||
|
||||
// 详情重置处理
|
||||
const handleDetailReset = (values: any) => {
|
||||
console.log("详情重置:", values);
|
||||
handleDetailSearchFinish(values);
|
||||
};
|
||||
|
||||
// 显示详情弹框
|
||||
const approvalId = ref("");
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
dictNmae();
|
||||
|
||||
// 获取过鱼设施相关字典
|
||||
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
|
||||
direction.value = res.data;
|
||||
});
|
||||
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
|
||||
guoyuStatus.value = res.data;
|
||||
});
|
||||
const urgeBtn = async () => {
|
||||
Modal.confirm({
|
||||
title: "是否催促选择的用户?",
|
||||
content: h("div", {}, [
|
||||
"将对选中的 ",
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#ff4d4f", fontWeight: "bold" } },
|
||||
batchData.value.length + " 位用户"
|
||||
),
|
||||
"发送【",
|
||||
h("span", { style: { color: "#ff4d4f" } }, "过鱼数据填报催促通知"),
|
||||
"】",
|
||||
]),
|
||||
onOk: async () => {
|
||||
let ids = [];
|
||||
searchRef.value.btnLoading = true;
|
||||
detailTableRef.value.loading = true;
|
||||
batchData.value.forEach((item: any) => {
|
||||
ids.push(item.userId);
|
||||
});
|
||||
|
||||
const shenStatus = ref([]);
|
||||
const yeWuType = ref([]);
|
||||
const dictNmae = () => {
|
||||
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
|
||||
shenStatus.value = res.data;
|
||||
});
|
||||
getDictItemsByCode({ dictCode: "yeWuType" }).then((res) => {
|
||||
yeWuType.value = res.data;
|
||||
});
|
||||
// TODO: 待补充操作类型字典
|
||||
getDictItemsByCode({ dictCode: "caoType" }).then((res) => {
|
||||
res.data.forEach((item: any) => {
|
||||
if (item.itemCode == "UPDATE" || item.itemCode == "DELETE") {
|
||||
operationTypeDict.value.push(item);
|
||||
let res: any = await batchUrgeContent({ userIds: ids });
|
||||
if (res && res?.code == 0) {
|
||||
message.success("催促成功");
|
||||
detailTableRef.value.loading = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
} else {
|
||||
actionTypeDict.value.push(item);
|
||||
message.error("催促失败");
|
||||
detailTableRef.value.loading = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
const handName = (val: any, arr: any) => {
|
||||
let dictName1 = "";
|
||||
arr.forEach((item: any) => {
|
||||
if (item.itemCode == val) {
|
||||
dictName1 = item.dictName;
|
||||
}
|
||||
});
|
||||
return dictName1;
|
||||
};
|
||||
|
||||
// 搜索相关数据
|
||||
const searchData = ref<any>({
|
||||
hbrvcd: "all",
|
||||
status: "",
|
||||
});
|
||||
|
||||
// 搜索相关弹框
|
||||
const paramsTab = ref({
|
||||
sort: [
|
||||
{
|
||||
field: "applyTime",
|
||||
dir: "desc",
|
||||
needSortFlag: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 使用 computed 包装 searchParams,避免深度监听误触发
|
||||
const computedSearchParams = computed(() => ({
|
||||
sort: paramsTab.value.sort,
|
||||
}));
|
||||
};
|
||||
// 初始化
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -327,5 +413,9 @@ const computedSearchParams = computed(() => ({
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,11 +9,9 @@
|
||||
@search-finish="handleSearchFinish"
|
||||
/>
|
||||
|
||||
<div class="table-container" ref="tableContainerRef">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll-y="tableScrollY"
|
||||
:list-url="getFishDraftPage"
|
||||
:search-params="{}"
|
||||
:transform-data="customTransform"
|
||||
@ -21,14 +19,11 @@
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
|
||||
<div class="flex">
|
||||
<a-button type="link" size="small" @click="handleView(record)"
|
||||
>查看</a-button
|
||||
>
|
||||
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<!-- 预览媒体组件 -->
|
||||
<PreviewMedia
|
||||
ref="previewMediaRef"
|
||||
@ -57,7 +52,6 @@ import PreviewMedia from "@/components/previewMedia/index.vue";
|
||||
import GuoYuSheShiShuJuHistorySearch from "./guoYuSheShiShuJuHistorySearch.vue";
|
||||
import EditModal from "../guoYuSheShiShuJuTianBao/guoYuSheShiShuJuTianBaoForm.vue";
|
||||
import { getFishDraftPage } from "@/api/guoYuSheShiShuJuTianBao";
|
||||
|
||||
import { Tag } from "ant-design-vue";
|
||||
import { getDictItemsByCode } from "@/api/dict";
|
||||
|
||||
@ -311,29 +305,7 @@ const handleSearchFinish = (values: any) => {
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
const tableContainerRef = ref<HTMLElement | null>(null);
|
||||
const tableScrollY = ref<number | undefined>(undefined);
|
||||
|
||||
const calcTableScrollY = () => {
|
||||
if (!tableContainerRef.value) return;
|
||||
const containerHeight = tableContainerRef.value.clientHeight;
|
||||
const otherHeight = 112;
|
||||
const y = containerHeight - otherHeight;
|
||||
tableScrollY.value = y > 0 ? y : undefined;
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
calcTableScrollY();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
calcTableScrollY();
|
||||
if (tableContainerRef.value) {
|
||||
observer.observe(tableContainerRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
|
||||
direction.value = res.data;
|
||||
});
|
||||
@ -360,7 +332,6 @@ onMounted(() => {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -368,10 +339,4 @@ onMounted(() => {
|
||||
.search-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -118,6 +118,7 @@
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:disabled="isView"
|
||||
:disabled-date="disabledDate"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@ -328,6 +329,12 @@ const fpssOption = ref<any[]>([]);
|
||||
const imageFileList = ref<any[]>([]);
|
||||
const videoFileList = ref<any[]>([]);
|
||||
|
||||
// 禁用今天之后的日期
|
||||
const disabledDate = (current: any) => {
|
||||
// current 是 moment/dayjs 对象
|
||||
// 返回 true 表示禁用
|
||||
return current && current > dayjs().endOf("day");
|
||||
};
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
initialValues: null,
|
||||
@ -774,6 +781,7 @@ const handleOk = async () => {
|
||||
vdpth: finalVideoPaths.join(","),
|
||||
};
|
||||
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
// console.log(submitValues);
|
||||
// return;
|
||||
emit("ok", submitValues);
|
||||
} catch (error) {
|
||||
|
||||
@ -16,11 +16,9 @@
|
||||
@search-finish="handleSearchFinish"
|
||||
/>
|
||||
<!-- 主表格 -->
|
||||
<div class="table-container" ref="tableContainerRef">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll-y="tableScrollY"
|
||||
:list-url="getFishDraftPage"
|
||||
:search-params="{}"
|
||||
:enable-row-selection="true"
|
||||
@ -98,7 +96,6 @@
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<div class="buttonStyle">
|
||||
<div class="button">
|
||||
<a-tooltip title="提交数据">
|
||||
@ -225,7 +222,7 @@ import {
|
||||
} from "@/api/guoYuSheShiShuJuTianBao";
|
||||
import { Tag } from "ant-design-vue"; // 确保导入 Tag
|
||||
import { getDictItemsByCode } from "@/api/dict";
|
||||
import { fr } from "element-plus/es/locale/index.mjs";
|
||||
import { calcTableScrollY } from "@/utils/index";
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
const baseUrlApi = import.meta.env.VITE_APP_BASE_API_URL;
|
||||
@ -720,7 +717,6 @@ const fileChange = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
let res: any = await importFishZip(formData);
|
||||
console.log(res);
|
||||
if (res && res?.code == 0) {
|
||||
if (visible.value) importBtn();
|
||||
setTimeout(() => {
|
||||
@ -966,27 +962,16 @@ const handleDeleteMedia = (item: any, type: string, index: number) => {
|
||||
};
|
||||
const tableContainerRef = ref<HTMLElement | null>(null);
|
||||
const tableScrollY = ref<number | undefined>(undefined);
|
||||
// 计算表格主体高度(减去表头、分页器、内边距等)
|
||||
const calcTableScrollY = () => {
|
||||
if (!tableContainerRef.value) return;
|
||||
// 获取容器高度
|
||||
const containerHeight = tableContainerRef.value.clientHeight;
|
||||
// 估算表头(约 55px) + 分页器(约 64px) + 上下内边距容差(10px) = 约 130px
|
||||
// 如果你有筛选栏等,需要额外减去
|
||||
const otherHeight = 112;
|
||||
const y = containerHeight - otherHeight;
|
||||
tableScrollY.value = y > 0 ? y : undefined;
|
||||
};
|
||||
|
||||
// 监听容器大小变化(窗口 resize 或内容变化)
|
||||
const observer = new ResizeObserver(() => {
|
||||
calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
||||
});
|
||||
|
||||
// --- 生命周期 ---
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
calcTableScrollY();
|
||||
tableScrollY.value = calcTableScrollY(tableContainerRef.value);
|
||||
if (tableContainerRef.value) {
|
||||
observer.observe(tableContainerRef.value);
|
||||
}
|
||||
@ -1019,9 +1004,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
box-sizing: border-box;
|
||||
/* 防止 padding 撑大高度 */
|
||||
overflow: hidden;
|
||||
/* 防止整个页面出现滚动条 */
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@ -1029,15 +1012,6 @@ onMounted(() => {
|
||||
/* 搜索栏不压缩 */
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
/* 占据剩余所有空间 */
|
||||
overflow: hidden;
|
||||
/* 关键:防止表格溢出容器 */
|
||||
position: relative;
|
||||
/* 为绝对定位元素提供参考(如果需要) */
|
||||
}
|
||||
|
||||
.buttonStyle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@ -9,9 +9,6 @@
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #ftp="{ onChange }">
|
||||
<fishSearch v-model="localTypeDate" width="200px" @update:modelValue="onChange" />
|
||||
</template>
|
||||
</BasicSearch>
|
||||
</div>
|
||||
</template>
|
||||
@ -21,7 +18,6 @@ import { ref, computed, onMounted, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import BasicSearch from "@/components/BasicSearch/index.vue";
|
||||
import { DateSetting } from "@/utils/enumeration";
|
||||
import fishSearch from "@/components/fishSearch/index.vue";
|
||||
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
|
||||
|
||||
interface Props {
|
||||
@ -44,8 +40,6 @@ const initSearchData = {
|
||||
hbrvcd: "all",
|
||||
stcd: null,
|
||||
rstcd: null,
|
||||
ftp: null,
|
||||
direction: null,
|
||||
strdt: [
|
||||
dayjs().subtract(1, "month").startOf("month").format("YYYY-MM-DD"),
|
||||
dayjs().endOf("day").format("YYYY-MM-DD"),
|
||||
@ -99,14 +93,6 @@ const handleReset = () => {
|
||||
emit("reset", initSearchData);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => initSearchData.ftp,
|
||||
(newVal) => {
|
||||
localTypeDate.value = newVal || "";
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
emit("search-finish", initSearchData);
|
||||
shuJuTianBaoStore.getFpssOption("", "");
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
:columns="columns"
|
||||
:list-url="queryPageList"
|
||||
:search-params="computedSearchParams"
|
||||
:scroll-y="650"
|
||||
:enable-row-selection="true"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
@ -234,7 +233,7 @@ import {
|
||||
batchApproveByApprovalId,
|
||||
batchReject,
|
||||
} from "@/api/shengPiJiLu";
|
||||
import { Tag, message, Modal } from "ant-design-vue";
|
||||
import { Tag, Tooltip, message, Modal } from "ant-design-vue";
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
import GuoYuSheShiShuJuTianBaoSearch from "./shengPiJiLuSearch.vue";
|
||||
import ApprovalLogSearch from "./approvalLogSearch.vue";
|
||||
@ -276,14 +275,12 @@ let columns = ref([
|
||||
|
||||
// 返回 span 标签,title 属性用于悬停显示全称
|
||||
return h(
|
||||
"span",
|
||||
Tooltip,
|
||||
{
|
||||
title: str,
|
||||
style: {
|
||||
cursor: "default",
|
||||
title: str, // Tooltip 显示的全部内容
|
||||
placement: "topLeft", // 可选:提示框位置
|
||||
},
|
||||
},
|
||||
displayText
|
||||
() => h("span", displayText)
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -930,11 +927,8 @@ const handleBatchDelete = () => {
|
||||
selectedRows.value.forEach((item: any) => {
|
||||
ids.push(item.id);
|
||||
});
|
||||
const params = {
|
||||
ids: ids,
|
||||
};
|
||||
try {
|
||||
let res: any = await batchRemoveDraft(params);
|
||||
let res: any = await batchRemoveDraft(ids);
|
||||
if (res && res?.code == 0) {
|
||||
message.success("删除成功");
|
||||
tableRef.value?.clearSelection();
|
||||
@ -1006,12 +1000,6 @@ const handleBatchRejectCancel = () => {
|
||||
batchRejectForm.value.rejectReason = "";
|
||||
};
|
||||
|
||||
// 搜索相关数据
|
||||
const searchData = ref<any>({
|
||||
hbrvcd: "all",
|
||||
status: "",
|
||||
});
|
||||
|
||||
// 搜索相关弹框
|
||||
const paramsTab = ref({
|
||||
sort: [
|
||||
@ -1038,6 +1026,10 @@ const computedSearchParams = computed(() => ({
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.approval-log-modal-content {
|
||||
|
||||
@ -1014,11 +1014,10 @@ onMounted(() => {
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
width: 100%;
|
||||
height: calc(100vh - 120px);
|
||||
height: calc(100vh - 110px);
|
||||
overflow: auto;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 5px 20px 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -283,11 +283,10 @@ function handleClose() {
|
||||
pointer-events: all;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 130px);
|
||||
height: calc(100vh - 110px);
|
||||
overflow: auto;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@ -647,7 +647,6 @@ onMounted(() => {
|
||||
position: relative;
|
||||
z-index: 900;
|
||||
pointer-events: all;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
@ -665,11 +664,10 @@ onMounted(() => {
|
||||
.conductproject-bg-box {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 130px);
|
||||
height: calc(100vh - 110px);
|
||||
overflow: auto;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { UserConfig, ConfigEnv, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import path from 'path';
|
||||
import cesium from 'vite-plugin-cesium';
|
||||
|
||||
export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
// 获取 .env 环境配置文件
|
||||
@ -10,6 +11,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
cesium(),
|
||||
createSvgIconsPlugin({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
|
||||
@ -33,7 +35,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||
},
|
||||
'/geoserver': {
|
||||
// target: 'https://211.99.26.225:18085', // 地图-线上API地址
|
||||
target: 'https://211.99.26.225:18085', // 地图-线上API地址
|
||||
// target: 'http://172.16.31.112:8093', // 地图-本地API地址
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
@ -42,6 +44,16 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
'/process': {
|
||||
target: 'http://localhost:5174',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/api/dec-lygk-base-server': {
|
||||
target: 'https://211.99.26.225:12122',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: path =>
|
||||
path.replace(
|
||||
new RegExp('^/api/dec-lygk-base-server'),
|
||||
'/api/dec-lygk-base-server'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -58,6 +70,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
// 将 element-plus, lodash 等大型库单独拆分
|
||||
if (id.includes('element-plus')) return 'element-plus';
|
||||
if (id.includes('lodash')) return 'lodash';
|
||||
if (id.includes('cesium')) return 'cesium';
|
||||
// 其他 node_modules 归为 vendor
|
||||
return 'vendor';
|
||||
}
|
||||
|
||||