添加模拟线上接口,转3d地球添加,点击描点出现弹框,添加数据填报过鱼倒数统计,修改公共组件 表格高度

This commit is contained in:
扈兆增 2026-05-15 17:38:03 +08:00
parent f2767bb5b5
commit cd59030f07
53 changed files with 12725 additions and 50921 deletions

View File

@ -3,15 +3,23 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV='development'
VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_TITLE = '水电水利建设项目全过程环 境管理信息平台'
VITE_APP_PORT = 3000
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'
# 开发环境导入预览地址

View File

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

File diff suppressed because it is too large Load Diff

View 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: ''
};

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

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

View File

@ -1,40 +1,37 @@
<template>
<a-table
size="small"
:loading="loading"
:row-selection="enableRowSelection ? rowSelection : undefined"
:data-source="tableData"
:columns="columns"
:pagination="paginationConfig"
:scroll="scrollConfig "
:row-key="rowKey"
@change="handleTableChange"
>
<!-- 透传插槽支持自定义列内容 -->
<template v-for="slot in Object.keys($slots)" #[slot]="scope" :key="slot">
<slot :name="slot" v-bind="scope"></slot>
</template>
</a-table>
<div class="table-container" ref="tableContainerRef">
<a-table
size="small"
:loading="loading"
:row-selection="enableRowSelection ? rowSelection : undefined"
:data-source="tableData"
:columns="columns"
:pagination="paginationConfig"
:scroll="scrollConfig"
:row-key="rowKey"
@change="handleTableChange"
>
<!-- 透传插槽支持自定义列内容 -->
<template v-for="slot in Object.keys($slots)" #[slot]="scope" :key="slot">
<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>

File diff suppressed because it is too large Load Diff

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

View File

@ -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();
}
});
}
// 飞行到指定的点

View File

@ -72,6 +72,12 @@ export interface MapInterface {
mdoptions: MDOptions
): void;
/**
*
* @param popupContainer
*/
initPopupOverlay(popupContainer: HTMLDivElement): void;
/**
*
* @param layer

View File

@ -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,24 +148,99 @@ 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;
});
const targetElement = this.map?.getTargetElement() as HTMLElement;
if (targetElement) {
if (feature) {
// 如果鼠标下有要素,显示手型
targetElement.style.cursor = 'pointer';
} else {
// 否则恢复默认
targetElement.style.cursor = '';
}
// 取消之前的动画帧,确保只处理最新的一次移动
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();
});
// 触发地图渲染
this.map?.render();
}
});
});
return Promise.resolve(this.map);
} catch (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({
text: labelText,
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'
})
// zIndex: zIndex // ✅ 设置样式的 zIndex
};
// ✅ 4. 只有在悬停时才添加 Text 样式
// if (isHovered && labelText) {
styleOptions.text = new Text({
text: labelText,
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' // 避免标签重叠
});
// }
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 => {
if (this.map && this.map.getLayers().getArray().includes(layer)) {
this.map.removeLayer(layer);
// ✅ 关键:先清除所有图层的源,再移除图层,这能更有效地中断网络请求
const clearLayer = (layer: any) => {
if (layer && layer.getSource) {
const source = layer.getSource();
if (source && source.clear) {
source.clear(); // 清除缓存
}
});
// 对于 WMTS/XYZOL 内部有 TileQueuedispose 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('地图实例已销毁');
}

View 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%;
}
}
}

File diff suppressed because it is too large Load Diff

View 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,24 +91,50 @@ const controllers: any = computed(() => [
},
],
},
{
children: [
{
name: "测量工具",
key: "Calculate",
component: Calculate,
},
],
},
{
children: [
{
name: "梯级",
key: "TJ",
icon: "tiji",
},
],
},
mapType.value === "2D"
? {
children: [
{
name: "测量工具",
key: "Calculate",
component: Calculate,
},
],
}
: {},
mapType.value === "2D"
? {
children: [
{
name: "梯级",
key: "TJ",
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",
// },
]);
//
@ -145,21 +161,27 @@ const controllers: any = computed(() => [
const handleControllerClick = (item: any) => {
switch (item.key) {
case "fullScreen":
isFullScreen.value = !isFullScreen.value;
map.jdPanelControlShowAndHidden('01',false)
// map.mdLayerTreeShowOrHidden('fp_point',!isFullScreen.value);
isFullScreen.value = !isFullScreen.value;
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:

View File

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

View File

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

View File

@ -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">
@ -43,7 +66,7 @@ onMounted(() => {});
}
.basic_body1 {
width: 100%;
line-break: anywhere;
width: 100%;
line-break: anywhere;
}
</style>

View File

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

View File

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

View File

@ -46,26 +46,26 @@ export function mix(color1: string, color2: string, weight: number) {
return '#' + rStr + gStr + bStr;
}
export function parseTime(time :any, cFormat :any) {
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 = {
const formatObj: any = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
@ -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 ] }
if (result.length > 0 && value < 10) {
value = '0' + 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;
}
return value || 0;
}
return value || 0
})
return time_str
);
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 跨域且服务器未配置 CORSfetch 会失败
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;
};

View File

@ -19,9 +19,20 @@ service.interceptors.request.use(
`Expected 'config' and 'config.headers' not to be undefined`
);
}
const user = useUserStoreHook();
if (user.Token) {
config.headers.token = getToken();
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;
},

View File

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

View File

@ -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: "选择",
key: "selection",
width: 60,
align: "center",
//
customCell: (record) => {
return {
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: "流域",
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 };
width: 160,
customCell: (record, index) => {
return {
rowSpan: record._basinRowSpan || 0,
};
},
customRender: ({ text }: any) => {
if (!text) return "-";
const str = String(text);
//
const items = str.split(/[,]/);
// 66
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: "ennm",
key: "ennm",
dataIndex: "stationNames",
key: "stationNames",
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 };
width: 160,
customCell: (record, index) => {
return {
rowSpan: record._stationRowSpan || 0,
};
},
customRender: ({ text }: any) => {
if (!text) return "-";
const str = String(text);
//
const items = str.split(/[,]/);
// 66
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: "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 },
{ 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 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) => {
return {
...item,
picpthList: item.picpth
? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || []
: [],
};
});
// 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 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 approvalId = ref("");
//
onMounted(() => {
dictNmae();
//
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
direction.value = res.data;
});
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
guoyuStatus.value = res.data;
});
});
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>

View File

@ -9,26 +9,21 @@
@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"
>
<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
>
</div>
</template>
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="getFishDraftPage"
:search-params="{}"
:transform-data="customTransform"
>
<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>
</div>
</template>
</BasicTable>
</div>
</template>
</BasicTable>
<!-- 预览媒体组件 -->
<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>

View File

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

View File

@ -16,89 +16,86 @@
@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"
:get-checkbox-props="getCheckboxProps"
:transform-data="customTransform"
@selection-change="handleSelectionChange"
>
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
size="small"
@click="handleSubmit([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>提交</a-button
>
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>编辑</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="record.status === 'PENDING'"
>编辑</a-button
>
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
danger
size="small"
@click="handleDelete([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>删除</a-button
>
<a-button
type="link"
size="small"
@click="handleEdit(record, 'view')"
v-if="
(checkPerm(['sjtb:edit-review']) && record.status === 'DRAFT') ||
(checkPerm(['sjtb:import-add']) && record.status === 'PENDING') ||
(checkPerm(['sjtb:edit-review']) && record.status === 'REJECTED') ||
record.status === 'APPROVED'
"
>查看</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleSuccess([record.id])"
v-if="record.status === 'PENDING'"
>审批</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
danger
size="small"
@click="handleReject(record.id)"
v-if="record.status === 'PENDING'"
>驳回</a-button
>
</div>
</template>
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="getFishDraftPage"
:search-params="{}"
:enable-row-selection="true"
:get-checkbox-props="getCheckboxProps"
:transform-data="customTransform"
@selection-change="handleSelectionChange"
>
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
size="small"
@click="handleSubmit([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>提交</a-button
>
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>编辑</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="record.status === 'PENDING'"
>编辑</a-button
>
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
danger
size="small"
@click="handleDelete([record.id])"
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>删除</a-button
>
<a-button
type="link"
size="small"
@click="handleEdit(record, 'view')"
v-if="
(checkPerm(['sjtb:edit-review']) && record.status === 'DRAFT') ||
(checkPerm(['sjtb:import-add']) && record.status === 'PENDING') ||
(checkPerm(['sjtb:edit-review']) && record.status === 'REJECTED') ||
record.status === 'APPROVED'
"
>查看</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleSuccess([record.id])"
v-if="record.status === 'PENDING'"
>审批</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
danger
size="small"
@click="handleReject(record.id)"
v-if="record.status === 'PENDING'"
>驳回</a-button
>
</div>
</template>
</BasicTable>
</div>
</template>
</BasicTable>
<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;

View File

@ -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("", "");

View File

@ -15,7 +15,6 @@
:columns="columns"
:list-url="queryPageList"
:search-params="computedSearchParams"
:scroll-y="650"
:enable-row-selection="true"
@selection-change="handleSelectionChange"
>
@ -36,8 +35,8 @@
{{ handName(record.bizType, yeWuType) }}
</template>
<!-- <template v-if="column.dataIndex === 'status'">
{{ handName(record.status, shenStatus) }}
</template> -->
{{ handName(record.status, shenStatus) }}
</template> -->
</template>
</BasicTable>
@ -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 {

View File

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

View File

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

View File

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

View File

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