添加公共弹框,实时视频,全景影像,监测数据,预警提示,查看报告,批复文件,公共弹框逻辑修改
This commit is contained in:
parent
34a5588899
commit
ebf6a57a31
@ -7,12 +7,12 @@ 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://localhost:8093'
|
||||
# 测试环境
|
||||
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
|
||||
# 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_BASE_URL = 'http://192.168.1.162:8093'
|
||||
|
||||
# 测试环境线上
|
||||
@ -23,8 +23,9 @@ VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
||||
|
||||
# 开发环境导入预览地址
|
||||
VITE_APP_BASE_API_URL = 'http://10.84.121.21:8093'
|
||||
## 开发环境预览 图片视频地址
|
||||
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'
|
||||
## 开发环境 附件服务地址
|
||||
VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125'
|
||||
attachment
|
||||
|
||||
# 地图服务地址
|
||||
VITE_APP_MAP_URL = 'https://211.99.26.225:18085'
|
||||
|
||||
@ -3,10 +3,17 @@ NODE_ENV='production'
|
||||
|
||||
VITE_APP_TITLE = 'qgc-buji-web'
|
||||
VITE_APP_PORT = 3000
|
||||
VITE_APP_BASE_API = '/prod-api'
|
||||
VITE_APP_BASE_API = '/'
|
||||
## 生产环境API地址
|
||||
VITE_APP_BASE_URL = 'http://localhost:8093'
|
||||
## 生产环境导入预览地址
|
||||
VITE_APP_BASE_API_URL = 'https://211.99.26.225:12130/prod-api'
|
||||
## 生产环境预览 图片视频地址
|
||||
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'
|
||||
|
||||
## 开发环境 附件服务地址
|
||||
VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125'
|
||||
attachment
|
||||
|
||||
# 地图服务地址
|
||||
VITE_APP_MAP_URL = 'https://211.99.26.225:18085'
|
||||
@ -37,6 +37,7 @@
|
||||
"ol": "^10.8.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pdfjs-dist": "^6.0.227",
|
||||
"pinia": "^2.0.12",
|
||||
"screenfull": "^6.0.0",
|
||||
"sortablejs": "^1.14.0",
|
||||
|
||||
@ -89,6 +89,9 @@ importers:
|
||||
path-to-regexp:
|
||||
specifier: ^6.2.0
|
||||
version: 6.3.0
|
||||
pdfjs-dist:
|
||||
specifier: ^6.0.227
|
||||
version: 6.0.227
|
||||
pinia:
|
||||
specifier: ^2.0.12
|
||||
version: 2.3.1(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
||||
@ -595,6 +598,81 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@1.0.0':
|
||||
resolution: {integrity: sha512-3hNKJObUK7JsCF9aJlVCs1J0/KE/gGfZNeK8MO1ge6bB3aicr5walGme9t9No1f/oyk9GgvdAT/rjSdsx3gbIw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@1.0.0':
|
||||
resolution: {integrity: sha512-ZIja19/BiGz2puhki+WUYSRriwFeFJ8Mi9eK3hZdSS85w4Y60cuEAJVhMCfKwswQkKkUtrnzdKMBuO7TupvexA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@1.0.0':
|
||||
resolution: {integrity: sha512-hImggWc82jqZVpEsFR9S7PE9OQYjq/H/D7vwCGB6X1jRH+UVBP1+1niJTPBOat1B154T6GKK7/kcFtoWgjgFzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.0':
|
||||
resolution: {integrity: sha512-hlJRy6d+kWLKVOG/+1rEvNQVURZ0DxxRPJsLmEWwhwiXZUJc0BF5o9esALHSEP4CoJK4wChRtj3hnyBgVx2oWA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.0.0':
|
||||
resolution: {integrity: sha512-5Hru4T3RXkosRQafcjelv7AUzw9mXqmGYsxnzeDDOWveFCJyEPMSJltvGCM+jfH98seOCbfwm9KyFg6Jm5FhAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.0.0':
|
||||
resolution: {integrity: sha512-LTUl9jS8WsLSUGaxQZKQkxfluOJRpgvBuxxdM4pYcjib+di8AU4OzQc6+L6SzGMLcKc9H0RAjojRatBhTMqYdg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.0':
|
||||
resolution: {integrity: sha512-Iz931SAZf+WVDzpjk52Q3ffW3zw0YflFwEZMgs036Wfu1kX/LrwT9wGjsuSqyduqefUkl91/vTdAjn8hQu5ezA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.0.0':
|
||||
resolution: {integrity: sha512-pFEQ5eFK4JusgN1K6KkO9DKP/Hi1WMJOkF8Ch03/khTc4bFbCKkCCsJG4YcOMOW9bI4XbT2/eMAWxhO0xaWgPA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@1.0.0':
|
||||
resolution: {integrity: sha512-jnvr8NrLHiZ3NCiOKWqDbkI4Ah+QDrqtZ+sddPZBltEb1mQ2coSvCSJYfict+oAwcm0c970oTmVySpjKP/lnaA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.0':
|
||||
resolution: {integrity: sha512-y2j9/Gfd5joqiqxdP/L1smqjQ+uAx3C4N0EC7bDHrnZEEH8ToM/OC5p3uHvtj4Lq591aHj+ArL01UDLNwT5HgQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.0.0':
|
||||
resolution: {integrity: sha512-qwdhh9N6Gge/hC4pL9S1tQp0iKwhSl/dYjg7+RGp9k26iRGRi5MqqUyKGOXIWli0zOcuy5Y2wIH/jk2ry6i/jA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@1.0.0':
|
||||
resolution: {integrity: sha512-Jqxcy1XOIqj+lH9sl1GT+il6GR3uQv13vI2mrwubP3uT8Olak2ClDrK2RnxlQKjwv8BRr4b3ug0YR7c6hBX8wg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -2864,6 +2942,10 @@ packages:
|
||||
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||
hasBin: true
|
||||
|
||||
pdfjs-dist@6.0.227:
|
||||
resolution: {integrity: sha512-/P6M4SXw+70waMVLUM7rdRtvo+dEzqE1t6W/zQNvBETo2MaRa5rrvCcAYdfWGiUzadTgM0lJmRApUrW0d9zgKg==}
|
||||
engines: {node: '>=22.13.0 || >=24'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@ -4323,6 +4405,54 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@napi-rs/canvas-android-arm64@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@1.0.0':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 1.0.0
|
||||
'@napi-rs/canvas-darwin-arm64': 1.0.0
|
||||
'@napi-rs/canvas-darwin-x64': 1.0.0
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 1.0.0
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 1.0.0
|
||||
'@napi-rs/canvas-linux-arm64-musl': 1.0.0
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 1.0.0
|
||||
'@napi-rs/canvas-linux-x64-gnu': 1.0.0
|
||||
'@napi-rs/canvas-linux-x64-musl': 1.0.0
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 1.0.0
|
||||
'@napi-rs/canvas-win32-x64-msvc': 1.0.0
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@ -6817,6 +6947,10 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-protobuf-schema: 2.1.0
|
||||
|
||||
pdfjs-dist@6.0.227:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 1.0.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
@ -5,4 +5,60 @@ export function getStcdDetail(url: string, stcd: string) {
|
||||
method: 'get',
|
||||
params: { stcd }
|
||||
});
|
||||
}
|
||||
}
|
||||
// 公共接口 - post
|
||||
export function queryPostUrlList(url: string, data: any) {
|
||||
return request({
|
||||
url: url,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 公共接口 - 获取预览地址
|
||||
export function getIdUrl(params: any) {
|
||||
return request({
|
||||
url: import.meta.env.VITE_APP_ATTACHMENT_URL + '/get',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
// 全景影像
|
||||
export function getPanoramaList(data: any) {
|
||||
return request({
|
||||
url: '/eq/msstbprpt/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 实时视频
|
||||
export function getVideoList(data: any) {
|
||||
return request({
|
||||
url: '/vd/vmsstbprpt/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 实时视频单个查询
|
||||
export function getVideoDetail(data: any) {
|
||||
return request({
|
||||
url: '/vd/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 实时视频 回放
|
||||
export function getVideoPlayDetail(data: any) {
|
||||
return request({
|
||||
url: '/vd/runData/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 监测数据 - 电站
|
||||
export function getMonitorData(data: any) {
|
||||
return request({
|
||||
url: '/eq/data/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 基地下拉列表
|
||||
export function getBaseDropdown(data:any) {
|
||||
export function getBaseDropdown(data: any) {
|
||||
return request({
|
||||
url: '/env/hydrobase/dropdown',
|
||||
method: 'get',
|
||||
@ -9,7 +9,7 @@ export function getBaseDropdown(data:any) {
|
||||
});
|
||||
}
|
||||
// 流域下拉列表
|
||||
export function getSelectForDropdown(data:any) {
|
||||
export function getSelectForDropdown(data: any) {
|
||||
return request({
|
||||
url: '/env/hbrv/selectForDropdown',
|
||||
method: 'get',
|
||||
@ -17,7 +17,7 @@ export function getSelectForDropdown(data:any) {
|
||||
});
|
||||
}
|
||||
//电站下拉列表
|
||||
export function getEngInfoDropdown(data:any) {
|
||||
export function getEngInfoDropdown(data: any) {
|
||||
return request({
|
||||
url: '/env/engInfo/dropdown',
|
||||
method: 'post',
|
||||
@ -25,7 +25,7 @@ export function getEngInfoDropdown(data:any) {
|
||||
});
|
||||
}
|
||||
// 重新验证并更新行数据
|
||||
export function revalidateAndUpdateRow(data:any) {
|
||||
export function revalidateAndUpdateRow(data: any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/revalidateAndUpdateRow',
|
||||
method: 'post',
|
||||
@ -33,7 +33,7 @@ export function revalidateAndUpdateRow(data:any) {
|
||||
});
|
||||
}
|
||||
// 删除 行数据
|
||||
export function deleteRowById(data:any) {
|
||||
export function deleteRowById(data: any) {
|
||||
return request({
|
||||
url: '/data/fishDraft/deleteRowById',
|
||||
method: 'post',
|
||||
@ -42,7 +42,7 @@ export function deleteRowById(data:any) {
|
||||
}
|
||||
|
||||
//过鱼设施下拉列表
|
||||
export function getFpssDropdown(params:any) {
|
||||
export function getFpssDropdown(params: any) {
|
||||
return request({
|
||||
url: '/env/fpss/dropdown',
|
||||
method: 'get',
|
||||
@ -57,7 +57,7 @@ export function getFishDictoryDropdown() {
|
||||
});
|
||||
}
|
||||
// 类似鱼类名称下拉列表
|
||||
export function getSimilarFishDictoryDropdown(params:any) {
|
||||
export function getSimilarFishDictoryDropdown(params: any) {
|
||||
return request({
|
||||
url: '/env/fishDictory/similar',
|
||||
method: 'get',
|
||||
@ -65,11 +65,11 @@ export function getSimilarFishDictoryDropdown(params:any) {
|
||||
});
|
||||
}
|
||||
// 上传文件
|
||||
export function uploadFile(data:any) {
|
||||
export function uploadFile(data: any) {
|
||||
return request({
|
||||
url: import.meta.env.VITE_APP_PREVIEW_URL + '/upload',
|
||||
url: import.meta.env.VITE_APP_ATTACHMENT_URL + '/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div class="table-container" ref="tableContainerRef">
|
||||
<a-table size="small" :loading="loading" :row-selection="enableRowSelection ? rowSelection : undefined"
|
||||
:data-source="tableData" :columns="enhancedColumns" :pagination="paginationConfig" :scroll="scrollConfig"
|
||||
:row-key="rowKey" @change="handleTableChange">
|
||||
<a-table
|
||||
size="small"
|
||||
:loading="loading"
|
||||
:row-selection="enableRowSelection ? rowSelection : undefined"
|
||||
:data-source="tableData"
|
||||
:columns="enhancedColumns"
|
||||
: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>
|
||||
@ -12,15 +20,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, nextTick, h } from "vue";
|
||||
import { Tooltip } from "ant-design-vue";
|
||||
import { calcTableScrollY } from "@/utils/index";
|
||||
import { ref, computed, onMounted, watch, nextTick, h } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
// --- Types ---
|
||||
interface Props {
|
||||
columns: any[];
|
||||
scrollY?: string | number; // 优先使用传入的固定值
|
||||
listUrl: (params: any) => Promise<any>;
|
||||
scrollX?: string | number; // 横向滚动宽度
|
||||
listUrl?: (params: any) => Promise<any>;
|
||||
data?: any[];
|
||||
enableRowSelection?: boolean;
|
||||
rowKey?: string;
|
||||
@ -37,7 +46,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
enableRowSelection: false,
|
||||
rowKey: "id",
|
||||
rowKey: 'id',
|
||||
isPage: true,
|
||||
data: () => [],
|
||||
searchParams: () => ({}),
|
||||
@ -45,16 +54,19 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
getCheckboxProps: undefined,
|
||||
transformData: undefined,
|
||||
scrollY: undefined,
|
||||
emptyPlaceholder: "-",
|
||||
emptyPlaceholder: '-',
|
||||
processEmptyValues: true, // 默认开启
|
||||
enableEllipsis: true, // 默认开启
|
||||
enableSort: false, // 默认关闭,确保旧代码不受影响
|
||||
enableSort: false // 默认关闭,确保旧代码不受影响
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "data-loaded", params: any, data: any): void;
|
||||
(e: "selection-change", selectedRowKeys: string[], selectedRows: any[]): void;
|
||||
(e: "sort-change", sortInfo: { field: string; order: 'ascend' | 'descend' | null }): void;
|
||||
(e: 'data-loaded', params: any, data: any): void;
|
||||
(e: 'selection-change', selectedRowKeys: string[], selectedRows: any[]): void;
|
||||
(
|
||||
e: 'sort-change',
|
||||
sortInfo: { field: string; order: 'ascend' | 'descend' | null }
|
||||
): void;
|
||||
}>();
|
||||
|
||||
// --- State ---
|
||||
@ -72,19 +84,23 @@ const tableScrollY = ref<number>(0); // ✅ 新增:存储容器高度
|
||||
// --- Computed Scroll Config ---
|
||||
const scrollConfig = computed(() => {
|
||||
const config: any = {
|
||||
x: "100%",
|
||||
y: 0,
|
||||
x: '100%',
|
||||
y: 0
|
||||
};
|
||||
|
||||
// // 1. 如果父组件传入了固定的 scrollY,优先使用
|
||||
// 1. 如果父组件传入了固定的 scrollX,优先使用
|
||||
if (props.scrollX !== undefined && props.scrollX !== null) {
|
||||
config.x = props.scrollX;
|
||||
}
|
||||
|
||||
// 2. 如果父组件传入了固定的 scrollY,优先使用
|
||||
if (props.scrollY !== undefined && props.scrollY !== null) {
|
||||
config.y = props.scrollY;
|
||||
}
|
||||
// 2. 否则,如果检测到容器有高度,则使用容器高度作为滚动高度
|
||||
// 3. 否则,如果检测到容器有高度,则使用容器高度作为滚动高度
|
||||
else if (tableScrollY.value > 0) {
|
||||
config.y = tableScrollY.value;
|
||||
}
|
||||
console.log(config);
|
||||
|
||||
return config;
|
||||
});
|
||||
@ -95,11 +111,11 @@ const rowSelection = computed(() => ({
|
||||
onChange: (keys: string[], rows: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedRows.value = rows;
|
||||
emit("selection-change", keys, rows);
|
||||
emit('selection-change', keys, rows);
|
||||
},
|
||||
getCheckboxProps: props.getCheckboxProps
|
||||
? props.getCheckboxProps
|
||||
: (record: any) => ({}),
|
||||
: (record: any) => ({})
|
||||
}));
|
||||
|
||||
// --- Pagination Config ---
|
||||
@ -110,7 +126,7 @@ const paginationConfig = computed(() => ({
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
pageSizeOptions: ["20", "50", "100"],
|
||||
pageSizeOptions: ['20', '50', '100']
|
||||
}));
|
||||
|
||||
// --- Methods ---
|
||||
@ -128,7 +144,7 @@ const getList = async (filter?: Record<string, any>) => {
|
||||
...props.searchParams,
|
||||
skip: page.value,
|
||||
take: size.value,
|
||||
filter: lastFilter.value,
|
||||
filter: lastFilter.value
|
||||
};
|
||||
if (!props.isPage) {
|
||||
delete params.take;
|
||||
@ -151,9 +167,9 @@ const getList = async (filter?: Record<string, any>) => {
|
||||
|
||||
tableData.value = processData(finalRecords);
|
||||
total.value = finalTotal;
|
||||
emit("data-loaded", params, { records: finalRecords, total: finalTotal });
|
||||
emit('data-loaded', params, { records: finalRecords, total: finalTotal });
|
||||
} catch (error) {
|
||||
console.error("Fetch table data error:", error);
|
||||
console.error('Fetch table data error:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
@ -166,14 +182,13 @@ const handleTableChange = (pag: any, filters: any, sorter: any) => {
|
||||
size.value = pag.pageSize;
|
||||
if (props.enableSort) {
|
||||
if (sorter && sorter.field) {
|
||||
emit("sort-change", { field: sorter.field, order: sorter.order });
|
||||
emit('sort-change', { field: sorter.field, order: sorter.order });
|
||||
} else {
|
||||
emit("sort-change", { field: '', order: null });
|
||||
emit('sort-change', { field: '', order: null });
|
||||
}
|
||||
}else {
|
||||
} else {
|
||||
getList();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
@ -201,8 +216,8 @@ defineExpose({
|
||||
clearSelection,
|
||||
getSelected: () => ({
|
||||
keys: selectedRowKeys.value,
|
||||
rows: selectedRows.value,
|
||||
}),
|
||||
rows: selectedRows.value
|
||||
})
|
||||
});
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
@ -218,7 +233,7 @@ const processData = (records: any[]) => {
|
||||
for (const key in processedRecord) {
|
||||
if (Object.prototype.hasOwnProperty.call(processedRecord, key)) {
|
||||
const val = processedRecord[key];
|
||||
if (val === null || val === undefined || val === "") {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
processedRecord[key] = props.emptyPlaceholder;
|
||||
}
|
||||
}
|
||||
@ -241,15 +256,21 @@ const enhancedColumns = computed(() => {
|
||||
ellipsis: true, // 启用 Ant Design Vue 原生的 ellipsis 支持
|
||||
customRender: ({ text }: any) => {
|
||||
const content = String(text ?? props.emptyPlaceholder);
|
||||
return h(
|
||||
Tooltip,
|
||||
{ title: content },
|
||||
() => h('div', {
|
||||
class: 'table-cell-ellipsis',
|
||||
style: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }
|
||||
}, content)
|
||||
return h(Tooltip, { title: content }, () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'table-cell-ellipsis',
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
},
|
||||
content
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@ -263,8 +284,24 @@ onMounted(() => {
|
||||
});
|
||||
if (props.data && props.data.length > 0) {
|
||||
tableData.value = processData(props.data);
|
||||
total.value = props.data.length;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 data prop 变化
|
||||
watch(
|
||||
() => props.data,
|
||||
newData => {
|
||||
if (newData && newData.length > 0) {
|
||||
tableData.value = processData(newData);
|
||||
total.value = newData.length;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
264
frontend/src/components/MapModal/components/Attachment.vue
Normal file
264
frontend/src/components/MapModal/components/Attachment.vue
Normal file
@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="attachment-container">
|
||||
<!-- 左侧文件列表 -->
|
||||
<div v-loading="loading" class="file-list" v-if="fileList.length > 0">
|
||||
<div
|
||||
v-for="file in fileList"
|
||||
:key="file.id"
|
||||
:class="['file-item', { active: selectedFile?.id === file.id }]"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
<span class="file-name" :title="file.name">{{ file.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧PDF预览 -->
|
||||
<div class="pdf-preview">
|
||||
<div v-if="showDownload && selectedFile" class="download-bar">
|
||||
<button class="download-btn" @click="downloadFile(selectedFile)">
|
||||
<span class="download-icon">⬇️</span>
|
||||
<span>下载文件</span>
|
||||
</button>
|
||||
</div>
|
||||
<pdf-view :file-id="selectedFile?.id || ''" />
|
||||
<a-empty
|
||||
v-show="!loading && fileList.length === 0"
|
||||
class="empty-state"
|
||||
:description="
|
||||
props.tabKey == 'attachment' ? '暂无数据' : '暂无批复文件'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, watch, computed } from 'vue';
|
||||
import PdfView from '@/components/pdf/pdfView.vue';
|
||||
import { queryPostUrlList, getIdUrl } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
const modelStore = useModelStore();
|
||||
const hasLoaded = ref(false); // 标记是否已经请求过
|
||||
|
||||
interface FileItem {
|
||||
id: string | number;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showDownload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tabKey: {
|
||||
type: String,
|
||||
default: 'attachment'
|
||||
},
|
||||
activeKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const fileList = reactive<FileItem[]>([]);
|
||||
const selectedFile = ref<FileItem | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const selectFile = (file: FileItem) => {
|
||||
selectedFile.value = file;
|
||||
};
|
||||
|
||||
const downloadFile = (file: FileItem) => {
|
||||
window.open(file.url, '_blank');
|
||||
};
|
||||
|
||||
// 加载 查看报告
|
||||
const loadReportData = () => {
|
||||
if (hasLoaded.value) return;
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
loading.value = true;
|
||||
queryPostUrlList(props.url, params).then(res => {
|
||||
const ids = res.data?.data?.map((item: any) => item) || [];
|
||||
if (ids.length === 0) {
|
||||
hasLoaded.value = true;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
getIdUrl({
|
||||
id: ids.join(',')
|
||||
}).then(res1 => {
|
||||
const data = res1?.data || {};
|
||||
const fileArray = Object.keys(data).map(key => ({
|
||||
id: key,
|
||||
name: data[key].name,
|
||||
url: data[key].fullpath
|
||||
}));
|
||||
loading.value = false;
|
||||
hasLoaded.value = true;
|
||||
updateFileList(fileArray);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 加载批复文件
|
||||
const loadApprovalData = () => {
|
||||
if (hasLoaded.value) return;
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
loading.value = true;
|
||||
queryPostUrlList(props.url, params).then(res => {
|
||||
const data = res.data?.data || [];
|
||||
const fileArray = data.map((item: any) => ({
|
||||
id: item.flpath.split('?')[1] || '',
|
||||
name: item.approvalFileNo || item.approvalFileTitle,
|
||||
url: item.flpath || ''
|
||||
}));
|
||||
hasLoaded.value = true;
|
||||
updateFileList(fileArray);
|
||||
});
|
||||
};
|
||||
|
||||
const updateFileList = (files: FileItem[]) => {
|
||||
fileList.length = 0;
|
||||
files.forEach(file => {
|
||||
fileList.push(file);
|
||||
});
|
||||
if (files.length > 0) {
|
||||
selectedFile.value = files[0];
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const initReportList = () => {
|
||||
const type = props.tabKey;
|
||||
if (type === 'approval') {
|
||||
loadApprovalData();
|
||||
} else if (type === 'attachment') {
|
||||
loadReportData();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active) {
|
||||
initReportList();
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attachment-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧文件列表 */
|
||||
.file-list {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
&:hover {
|
||||
background-color: #e8f4fd;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #005293;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 右侧PDF预览 */
|
||||
.pdf-preview {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.download-bar {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
@ -1,16 +1,31 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="modal-baseic-container" >
|
||||
<div class="modal-baseic-container">
|
||||
<a-row class="w-[100%]" v-if="!loading">
|
||||
<a-col :span="!item.visible ? 24 : (item.filed == '' ? 12 : 12)" v-for="(item, index) in tabledata"
|
||||
:key="item.key">
|
||||
<a-col
|
||||
:span="!item.visible ? 24 : item.filed == '' ? 12 : 12"
|
||||
v-for="(item, index) in tabledata"
|
||||
:key="item.key"
|
||||
>
|
||||
<div class="col-title" v-if="!item.visible">
|
||||
{{ item.name }}:
|
||||
<div v-if="modelStore.isBasicEdit && index == 0">
|
||||
<a-button v-if="!isEdit" type="primary" size="small" class="mr-2" @click="editBtn">
|
||||
<a-button
|
||||
v-if="!isEdit"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
@click="editBtn"
|
||||
>
|
||||
编辑信息
|
||||
</a-button>
|
||||
<a-button v-else type="primary" size="small" class="mr-2" @click="saveBtn">
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
@click="saveBtn"
|
||||
>
|
||||
保存信息
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" @click="recordBtn">
|
||||
@ -19,23 +34,46 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-left" v-if="item.visible && item.filed != ''">
|
||||
<a-tooltip :title="item.name.length > 10 ? item.name : ''">{{ item.name }}:
|
||||
<a-tooltip :title="item.name.length > 10 ? item.name : ''"
|
||||
>{{ item.name }}:
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="col-right" v-if="item.visible">
|
||||
<a-tooltip :title="!isEdit ? item.vlsr : ''">
|
||||
<span v-if="!isEdit && item.filed != ''">{{ item.value || "-" }}</span>
|
||||
<span v-if="!isEdit && item.filed != ''">{{
|
||||
item.value || '-'
|
||||
}}</span>
|
||||
<span v-else>
|
||||
<!-- {{ item.value }} -->
|
||||
<a-input v-if="item.type == 'input'" v-model:value="item.value" :placeholder="item.name"
|
||||
size="small"></a-input>
|
||||
<a-select v-else-if="item.type == 'select'" v-model:value="item.value" :placeholder="item.name"
|
||||
size="small" class="w-full"></a-select>
|
||||
<a-input
|
||||
v-if="item.type == 'input'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
></a-input>
|
||||
<a-select
|
||||
v-else-if="item.type == 'select'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
class="w-full"
|
||||
></a-select>
|
||||
<!-- <div v-else-if="item.type == 'date'">{{ item.value }}2222</div> -->
|
||||
<a-date-picker v-else-if="item.type == 'date'" v-model:value="item.value" :format="item.format"
|
||||
:value-format="item.format" :picker="item.picker" class="w-full"></a-date-picker>
|
||||
<a-input-number v-else-if="item.type == 'number'" v-model:value="item.value" :placeholder="item.name"
|
||||
size="small" class="w-full"></a-input-number>
|
||||
<a-date-picker
|
||||
v-else-if="item.type == 'date'"
|
||||
v-model:value="item.value"
|
||||
:format="item.format"
|
||||
:value-format="item.format"
|
||||
:picker="item.picker"
|
||||
class="w-full"
|
||||
></a-date-picker>
|
||||
<a-input-number
|
||||
v-else-if="item.type == 'number'"
|
||||
v-model:value="item.value"
|
||||
:placeholder="item.name"
|
||||
size="small"
|
||||
class="w-full"
|
||||
></a-input-number>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@ -43,13 +81,22 @@
|
||||
</a-row>
|
||||
|
||||
<div v-if="isLogo" class="img_box_one">
|
||||
<div class="img_box">
|
||||
<div class="img_box_img">
|
||||
<a-image style="width: 100%;" :src="baseUrl + '/?' + data2.msStbprpT.logo" />
|
||||
</div>
|
||||
<div class="img_box">
|
||||
<div class="img_box_img">
|
||||
<a-image
|
||||
style="width: 100%"
|
||||
:src="baseUrl + '/?' + data2.msStbprpT.logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-modal v-model:open="visible" title="修改记录" :footer="null" width="900px" :zIndex="2000">
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
title="修改记录"
|
||||
:footer="null"
|
||||
width="900px"
|
||||
:zIndex="2000"
|
||||
>
|
||||
123
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -57,104 +104,121 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import dayjs from "dayjs";
|
||||
import { ref, onMounted, nextTick,watch } from "vue";
|
||||
import { BasicColumns, wtPointColumns, FhWpPointColumns, FhPointColumns, FhZQPointColumns, FPPointColumns, FBPointColumns, VaPointColumns, VPPointColumns, wePointColumns, waPointColumns, DwPointColumns, DwFivePointColumns, DwSixPointColumns, DwOnePointColumns, StinfoAiVidoPointColumns } from "../column.config";
|
||||
import { useModelStore } from "@/store/modules/model";
|
||||
import { getStcdDetail } from "@/api/mapModal"
|
||||
import dayjs from 'dayjs';
|
||||
import { ref, onMounted, nextTick, watch, computed } from 'vue';
|
||||
import {
|
||||
BasicColumns,
|
||||
wtPointColumns,
|
||||
FhWpPointColumns,
|
||||
FhPointColumns,
|
||||
FhZQPointColumns,
|
||||
FPPointColumns,
|
||||
FBPointColumns,
|
||||
VaPointColumns,
|
||||
VPPointColumns,
|
||||
wePointColumns,
|
||||
waPointColumns,
|
||||
DwPointColumns,
|
||||
DwFivePointColumns,
|
||||
DwSixPointColumns,
|
||||
DwOnePointColumns,
|
||||
StinfoAiVidoPointColumns
|
||||
} from '../column.config';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { getStcdDetail } from '@/api/mapModal';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
const modelStore = useModelStore();
|
||||
const hasLoaded = ref(false); // 标记是否已经请求过
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: ''
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
const visible = ref(false);
|
||||
const isLogo = ref(false)
|
||||
const isLogo = ref(false);
|
||||
const loading = ref(false);
|
||||
const data2: any = ref({})
|
||||
const data2: any = ref({});
|
||||
const tabledata = ref([]);
|
||||
console.log(BasicColumns);
|
||||
const columnsConfig = ref([
|
||||
{
|
||||
type: "ENG",
|
||||
columns: BasicColumns,
|
||||
type: 'ENG',
|
||||
columns: BasicColumns
|
||||
},
|
||||
{
|
||||
type: "wt_point",
|
||||
columns: wtPointColumns,
|
||||
type: 'wt_point',
|
||||
columns: wtPointColumns
|
||||
},
|
||||
{
|
||||
type: "fh_wq_point",
|
||||
columns: FhWpPointColumns,
|
||||
type: 'fh_wq_point',
|
||||
columns: FhWpPointColumns
|
||||
},
|
||||
{
|
||||
type: "fh_point",
|
||||
columns: FhPointColumns,
|
||||
type: 'fh_point',
|
||||
columns: FhPointColumns
|
||||
},
|
||||
{
|
||||
type: "fh_zq_point",
|
||||
columns: FhZQPointColumns,
|
||||
},//
|
||||
type: 'fh_zq_point',
|
||||
columns: FhZQPointColumns
|
||||
}, //
|
||||
{
|
||||
type: "fp_point",
|
||||
columns: FPPointColumns,
|
||||
type: 'fp_point',
|
||||
columns: FPPointColumns
|
||||
},
|
||||
{
|
||||
type: "fb_point",
|
||||
columns: FBPointColumns,
|
||||
type: 'fb_point',
|
||||
columns: FBPointColumns
|
||||
},
|
||||
{
|
||||
type: "va_point",
|
||||
columns: VaPointColumns,
|
||||
|
||||
type: 'va_point',
|
||||
columns: VaPointColumns
|
||||
},
|
||||
{
|
||||
type: "vp_point",
|
||||
columns: VPPointColumns,
|
||||
type: 'vp_point',
|
||||
columns: VPPointColumns
|
||||
},
|
||||
{
|
||||
type: "we_point",
|
||||
columns: wePointColumns,
|
||||
type: 'we_point',
|
||||
columns: wePointColumns
|
||||
},
|
||||
{
|
||||
type: "wa_point",
|
||||
columns: waPointColumns,
|
||||
type: 'wa_point',
|
||||
columns: waPointColumns
|
||||
},
|
||||
{
|
||||
type: "stinfo_ai_video_point",
|
||||
columns: StinfoAiVidoPointColumns,
|
||||
type: 'stinfo_ai_video_point',
|
||||
columns: StinfoAiVidoPointColumns
|
||||
},
|
||||
{
|
||||
type: "eng_alarm_point",
|
||||
columns: BasicColumns,
|
||||
type: 'eng_alarm_point',
|
||||
columns: BasicColumns
|
||||
},
|
||||
{
|
||||
type: "fprd_point",
|
||||
columns: StinfoAiVidoPointColumns,
|
||||
|
||||
type: 'fprd_point',
|
||||
columns: StinfoAiVidoPointColumns
|
||||
},
|
||||
{
|
||||
type: "dw_point",
|
||||
columns: DwPointColumns,
|
||||
type: 'dw_point',
|
||||
columns: DwPointColumns
|
||||
},
|
||||
{
|
||||
type: "DW_5",
|
||||
columns: DwFivePointColumns,
|
||||
type: 'DW_5',
|
||||
columns: DwFivePointColumns
|
||||
},
|
||||
{
|
||||
type: "DW_6",
|
||||
columns: DwSixPointColumns,
|
||||
type: 'DW_6',
|
||||
columns: DwSixPointColumns
|
||||
},
|
||||
{
|
||||
type: "DW_1",
|
||||
columns: DwOnePointColumns,
|
||||
},
|
||||
|
||||
|
||||
type: 'DW_1',
|
||||
columns: DwOnePointColumns
|
||||
}
|
||||
|
||||
//DW_1
|
||||
]);
|
||||
@ -162,51 +226,58 @@ const isEdit = ref(false);
|
||||
const editBtn = () => {
|
||||
nextTick(() => {
|
||||
isEdit.value = true;
|
||||
tabledata.value.forEach((item) => {
|
||||
if (item.type == "date") item.value = dayjs(item.value).format(item.format);
|
||||
tabledata.value.forEach(item => {
|
||||
if (item.type == 'date')
|
||||
item.value = dayjs(item.value).format(item.format);
|
||||
});
|
||||
});
|
||||
};
|
||||
const saveBtn = () => {
|
||||
isEdit.value = false;
|
||||
console.log(tabledata.value);
|
||||
};
|
||||
const recordBtn = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
const getData = async () => {
|
||||
if (hasLoaded.value) return;
|
||||
loading.value = true;
|
||||
console.log(props.url);
|
||||
console.log(modelStore.params.stcd);
|
||||
if (props.url && modelStore.params.stcd) {
|
||||
let res: any = await getStcdDetail(props.url, modelStore.params.stcd)
|
||||
console.log(res);
|
||||
let res: any = await getStcdDetail(props.url, modelStore.params.stcd);
|
||||
if (res.success) {
|
||||
let data = res.data
|
||||
data2.value = data
|
||||
|
||||
let columns = [];
|
||||
columns = columnsConfig.value.find((item) => item.type == modelStore.params.sttp)
|
||||
.columns;
|
||||
let data = res.data;
|
||||
data2.value = data;
|
||||
|
||||
columns.forEach((item) => {
|
||||
let columns = [];
|
||||
columns = columnsConfig.value.find(
|
||||
item => item.type == modelStore.params.sttp
|
||||
).columns;
|
||||
|
||||
columns.forEach(item => {
|
||||
if (item.visible) {
|
||||
if (item.type == "date") {
|
||||
if (data.msStbprpT[item.filed] && data.msStbprpT[item.filed] !== "-") {
|
||||
item.value = dayjs(data.msStbprpT[item.filed] || "").format(item.format);
|
||||
if (item.type == 'date') {
|
||||
if (
|
||||
data.msStbprpT[item.filed] &&
|
||||
data.msStbprpT[item.filed] !== '-'
|
||||
) {
|
||||
item.value = dayjs(data.msStbprpT[item.filed] || '').format(
|
||||
item.format
|
||||
);
|
||||
} else {
|
||||
item.value = null;
|
||||
}
|
||||
} else {
|
||||
if (item.type == "number") {
|
||||
if (item.type == 'number') {
|
||||
if (data.msStbprpT[item.filed]) {
|
||||
if (item.filed == "ttpwr") {
|
||||
if (item.filed == 'ttpwr') {
|
||||
item.value =
|
||||
(Number(data.msStbprpT[item.filed] || 0) / 10).toFixed(item.toFixed) ||
|
||||
null;
|
||||
(Number(data.msStbprpT[item.filed] || 0) / 10).toFixed(
|
||||
item.toFixed
|
||||
) || null;
|
||||
} else {
|
||||
item.value =
|
||||
Number(data.msStbprpT[item.filed] || 0).toFixed(item.toFixed) || null;
|
||||
Number(data.msStbprpT[item.filed] || 0).toFixed(
|
||||
item.toFixed
|
||||
) || null;
|
||||
}
|
||||
} else {
|
||||
item.value = null;
|
||||
@ -215,59 +286,54 @@ const getData = async () => {
|
||||
item.value = data.msStbprpT[item.filed] || null;
|
||||
}
|
||||
}
|
||||
let stcdItem = data.stcdValVoList.find((stcdItem) => {
|
||||
let str = "";
|
||||
let stcdItem = data.stcdValVoList.find(stcdItem => {
|
||||
let str = '';
|
||||
let arr = [
|
||||
"baseName",
|
||||
"addvcdName",
|
||||
"hbrvcdName",
|
||||
"bldsttCcodeName",
|
||||
"dvtpName",
|
||||
"rgcpName",
|
||||
'baseName',
|
||||
'addvcdName',
|
||||
'hbrvcdName',
|
||||
'bldsttCcodeName',
|
||||
'dvtpName',
|
||||
'rgcpName'
|
||||
];
|
||||
if (arr.includes(item.filed)) {
|
||||
// 删除name后缀
|
||||
str = item.filed.replace("Name", "");
|
||||
str = item.filed.replace('Name', '');
|
||||
} else {
|
||||
str = item.filed;
|
||||
}
|
||||
return stcdItem.attrKey == str;
|
||||
});
|
||||
if (stcdItem && stcdItem.attrKey == "rgcp") {
|
||||
console.log(stcdItem);
|
||||
if (stcdItem && stcdItem.attrKey == 'rgcp') {
|
||||
}
|
||||
if (stcdItem && stcdItem.vlsr) {
|
||||
item.vlsr = stcdItem.vlsr;
|
||||
}
|
||||
// console.log(stcdItem);
|
||||
}
|
||||
});
|
||||
tabledata.value = columns;
|
||||
loading.value = false;
|
||||
hasLoaded.value = true;
|
||||
} else {
|
||||
loading.value = false;
|
||||
}
|
||||
if (data2.value.msStbprpT.logo) {
|
||||
isLogo.value = true
|
||||
} else {
|
||||
isLogo.value = false
|
||||
}
|
||||
|
||||
if (data2.value.msStbprpT.logo) {
|
||||
isLogo.value = true;
|
||||
} else {
|
||||
isLogo.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
}, 1000);
|
||||
});
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
//监听modelStore.params.stcd数据变化,刷新数据
|
||||
watch(
|
||||
() => modelStore.params.stcd,
|
||||
@ -288,7 +354,7 @@ watch(
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.img_box_one{
|
||||
.img_box_one {
|
||||
width: 40%;
|
||||
// height: 80%;
|
||||
}
|
||||
@ -306,20 +372,12 @@ watch(
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(0, 127, 204, 0.65);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ant-row {
|
||||
border-left: 1px solid rgba(0, 127, 204, 0.65);
|
||||
border-top: 1px solid rgba(0, 127, 204, 0.65);
|
||||
height: 100%;
|
||||
.ant-col:nth-last-child(-n + 2) {
|
||||
|
||||
.col-left,
|
||||
.col-right {
|
||||
// border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col {
|
||||
|
||||
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="early-warning-alert">
|
||||
<a-tabs v-model:activeKey="activeTabKey" @change="handleTabChange">
|
||||
<a-tab-pane v-for="tab in tabsItems" :key="tab.key" :tab="tab.name">
|
||||
<div class="table-wrapper">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="currentColumns"
|
||||
:list-url="fetchTableData"
|
||||
:search-params="searchParams"
|
||||
:enable-sort="true"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { queryPostUrlList } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
|
||||
const props = defineProps<{
|
||||
tabsItems: Array<{
|
||||
name: string;
|
||||
key: string;
|
||||
type: string;
|
||||
hiddenChart: boolean;
|
||||
tableUrl: string;
|
||||
}>;
|
||||
isActive: boolean;
|
||||
}>();
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const activeTabKey = ref(props.tabsItems?.[0]?.key || '');
|
||||
const hasLoaded = ref(false);
|
||||
const tableRef = ref();
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = computed(() => ({
|
||||
...modelStore.params
|
||||
}));
|
||||
|
||||
// 不同 tab 的表头配置
|
||||
const columnsMap: Record<string, any[]> = {
|
||||
DesignParameterChangePrompt: [
|
||||
{
|
||||
title: '重大变动类型',
|
||||
dataIndex: 'majorChangeType',
|
||||
key: 'majorChangeType'
|
||||
},
|
||||
{ title: '依据阶段', dataIndex: 'basisStage', key: 'basisStage' },
|
||||
{
|
||||
title: '有重大变动的阶段',
|
||||
dataIndex: 'changedStage',
|
||||
key: 'changedStage'
|
||||
},
|
||||
{
|
||||
title: '依据阶段数据',
|
||||
dataIndex: 'basisStageData',
|
||||
key: 'basisStageData'
|
||||
},
|
||||
{
|
||||
title: '重大变动阶段数据',
|
||||
dataIndex: 'changedStageData',
|
||||
key: 'changedStageData'
|
||||
},
|
||||
{ title: '预警描述', dataIndex: 'warningDesc', key: 'warningDesc' }
|
||||
],
|
||||
ImplementEarlyWarning: [
|
||||
{ title: '设施名称', dataIndex: 'stnm', key: 'stnm' },
|
||||
{ title: '行政区', dataIndex: 'addvnm', key: 'addvnm' },
|
||||
{ title: '流域', dataIndex: 'rvnm', key: 'rvnm' },
|
||||
{
|
||||
title: '可研(核准)',
|
||||
dataIndex: 'feasibilityApproval',
|
||||
key: 'feasibilityApproval'
|
||||
},
|
||||
{
|
||||
title: '预可是否设计',
|
||||
dataIndex: 'preFeasibilityDesign',
|
||||
key: 'preFeasibilityDesign'
|
||||
},
|
||||
{
|
||||
title: '环评是否有要求',
|
||||
dataIndex: 'eiarqst',
|
||||
key: 'eiarqst'
|
||||
},
|
||||
{ title: '专项设计', dataIndex: 'specialDesign', key: 'specialDesign' },
|
||||
{ title: '建设', dataIndex: 'isbuild', key: 'isbuild' },
|
||||
{ title: '证明材料', dataIndex: 'proofMaterial', key: 'proofMaterial' }
|
||||
],
|
||||
ConstructionEarlyWarning: [
|
||||
{ title: '设施名称', dataIndex: 'facilityName', key: 'facilityName' },
|
||||
{ title: '行政区', dataIndex: 'district', key: 'district' },
|
||||
{ title: '流域', dataIndex: 'basin', key: 'basin' },
|
||||
{
|
||||
title: '可研(核准)',
|
||||
dataIndex: 'feasibilityApproval',
|
||||
key: 'feasibilityApproval'
|
||||
},
|
||||
{
|
||||
title: '预可是否设计',
|
||||
dataIndex: 'preFeasibilityDesign',
|
||||
key: 'preFeasibilityDesign'
|
||||
},
|
||||
{
|
||||
title: '环评是否有要求',
|
||||
dataIndex: 'eiaRequirement',
|
||||
key: 'eiaRequirement'
|
||||
},
|
||||
{ title: '专项设计', dataIndex: 'specialDesign', key: 'specialDesign' },
|
||||
{ title: '建设', dataIndex: 'construction', key: 'construction' },
|
||||
{ title: '证明材料', dataIndex: 'proofMaterial', key: 'proofMaterial' }
|
||||
],
|
||||
RunEarlyWarning: [
|
||||
{ title: '设施名称', dataIndex: 'stnm', key: 'stnm' },
|
||||
{ title: '所属行政区域', dataIndex: 'addvnm', key: 'addvnm' },
|
||||
{ title: '运行状态', dataIndex: 'status', key: 'status' }
|
||||
],
|
||||
ReleaseEarlyWarning: [
|
||||
{ title: '时间', dataIndex: 'releaseTime', key: 'releaseTime' },
|
||||
{ title: '鱼类', dataIndex: 'fishType', key: 'fishType' },
|
||||
{ title: '放鱼规格(cm)', dataIndex: 'fishSpec', key: 'fishSpec' },
|
||||
{ title: '放鱼数量(尾)', dataIndex: 'fishCount', key: 'fishCount' },
|
||||
{ title: '放流地点', dataIndex: 'releaseLocation', key: 'releaseLocation' },
|
||||
{ title: '所在河流', dataIndex: 'river', key: 'river' },
|
||||
{ title: '鱼龄(年)', dataIndex: 'fishAge', key: 'fishAge' },
|
||||
{ title: '标记方式', dataIndex: 'markMethod', key: 'markMethod' },
|
||||
{ title: '标记种类', dataIndex: 'markType', key: 'markType' },
|
||||
{ title: '标记数量(尾)', dataIndex: 'markCount', key: 'markCount' },
|
||||
{ title: '运行状态', dataIndex: 'runStatus', key: 'runStatus' }
|
||||
]
|
||||
};
|
||||
|
||||
// 当前 tab 的列配置
|
||||
const currentColumns = computed(() => {
|
||||
return columnsMap[activeTabKey.value] || [];
|
||||
});
|
||||
|
||||
// 当前 tab 的接口地址
|
||||
const currentTableUrl = computed(() => {
|
||||
const tab = props.tabsItems.find(item => item.key === activeTabKey.value);
|
||||
return tab?.tableUrl || '';
|
||||
});
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async (params: any) => {
|
||||
if (!currentTableUrl.value) {
|
||||
return { data: { records: [], total: 0 } };
|
||||
}
|
||||
return await queryPostUrlList(currentTableUrl.value, params);
|
||||
};
|
||||
|
||||
// tab 切换
|
||||
const handleTabChange = (key: string) => {
|
||||
activeTabKey.value = key;
|
||||
// 切换 tab 时刷新表格数据
|
||||
if (tableRef.value) {
|
||||
tableRef.value.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 isActive 变化,首次激活时请求数据
|
||||
watch(
|
||||
() => props.isActive,
|
||||
newVal => {
|
||||
if (newVal && !hasLoaded.value) {
|
||||
hasLoaded.value = true;
|
||||
// 首次加载当前 tab 数据
|
||||
setTimeout(() => {
|
||||
if (tableRef.value) {
|
||||
// tableRef.value.getList();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听 tabsItems 变化,初始化 activeTabKey
|
||||
watch(
|
||||
() => props.tabsItems,
|
||||
newTabs => {
|
||||
if (newTabs && newTabs.length > 0 && !activeTabKey.value) {
|
||||
activeTabKey.value = newTabs[0].key;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.early-warning-alert {
|
||||
.table-wrapper {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
609
frontend/src/components/MapModal/components/MonitorInfo.vue
Normal file
609
frontend/src/components/MapModal/components/MonitorInfo.vue
Normal file
@ -0,0 +1,609 @@
|
||||
<template>
|
||||
<div class="monitor-info">
|
||||
<!-- Tabs 区域 -->
|
||||
<a-tabs v-model:activeKey="activeTabKey">
|
||||
<a-tab-pane v-for="tab in tabsList" :key="tab.key" :tab="tab.name" />
|
||||
<template #rightExtra>
|
||||
<div class="search-bar">
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
:format="dateFormat"
|
||||
:show-time="showTime ? showTimeConfig : false"
|
||||
:allowClear="false"
|
||||
:disabled-date="disabledDate"
|
||||
>
|
||||
</a-range-picker>
|
||||
<a-button type="primary" class="search-btn" @click="handleSearch">
|
||||
查询
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div v-if="activeTabKey === 'dzxq.tabs.jcsj'" class="tab-content">
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<!-- 左侧图表 + 右侧表格 -->
|
||||
<div class="content-body">
|
||||
<div class="chart-wrapper" ref="chartRef"></div>
|
||||
<div class="table-wrapper">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="480"
|
||||
:scrollX="980"
|
||||
:columns="currentColumns"
|
||||
:data="tableData"
|
||||
:isPage="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { getMonitorData } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const hasLoaded = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const activeTabKey = ref('dzxq.tabs.jcsj');
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
const tableRef = ref<any>(null);
|
||||
const tableData = ref<any[]>([]);
|
||||
|
||||
// ==================== Tab 配置 ====================
|
||||
const tabsConfig: Record<string, any[]> = {
|
||||
'dzxq.tabs.jcsj': [{ name: '电站运行过程线', key: 'dzxq.tabs.jcsj' }]
|
||||
};
|
||||
|
||||
const tabsList = computed(() => {
|
||||
return tabsConfig[props.code] || tabsConfig['dzxq.tabs.jcsj'];
|
||||
});
|
||||
|
||||
// ==================== 时间选择器配置 ====================
|
||||
const timeConfig: Record<string, { showTime: boolean; format: string }> = {
|
||||
'dzxq.tabs.jcsj': { showTime: true, format: 'YYYY-MM-DD HH:mm' }
|
||||
};
|
||||
|
||||
const currentTimeConfig = computed(() => {
|
||||
return timeConfig[props.code] || timeConfig['dzxq.tabs.jcsj'];
|
||||
});
|
||||
|
||||
const showTime = computed(() => currentTimeConfig.value.showTime);
|
||||
const dateFormat = computed(() => currentTimeConfig.value.format);
|
||||
|
||||
// 时分秒配置(不选秒)
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
|
||||
// 初始化日期范围:当前日期到前一个月
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
const endDate = dayjs();
|
||||
const startDate = dayjs().subtract(1, 'month');
|
||||
return [startDate, endDate];
|
||||
};
|
||||
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// 禁用超过当前日期的选项
|
||||
const disabledDate = (current: Dayjs) => {
|
||||
return current && current.isAfter(dayjs(), 'day');
|
||||
};
|
||||
|
||||
// ==================== 表格配置 ====================
|
||||
const tableColumnsConfig: Record<string, any[]> = {
|
||||
'dzxq.tabs.jcsj': [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
width: 180,
|
||||
fixed: 'left',
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||
},
|
||||
{
|
||||
title: '坝上水位(m)',
|
||||
dataIndex: 'rz',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
|
||||
},
|
||||
{
|
||||
title: '坝下水位(m)',
|
||||
dataIndex: 'dz',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
|
||||
},
|
||||
{
|
||||
title: '入库流量(m³/s)',
|
||||
dataIndex: 'qi',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Math.round(Number(text)) : '-'
|
||||
},
|
||||
{
|
||||
title: '出库流量(m³/s)',
|
||||
dataIndex: 'qo',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text) : '-'
|
||||
},
|
||||
{
|
||||
title: '生态流量(m³/s)',
|
||||
dataIndex: 'qec',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||||
},
|
||||
{
|
||||
title: '生态流量限值(m³/s)',
|
||||
dataIndex: 'qecLimit',
|
||||
width: 150,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const currentColumns = computed(() => {
|
||||
return tableColumnsConfig[props.code] || tableColumnsConfig['dzxq.tabs.jcsj'];
|
||||
});
|
||||
|
||||
// ==================== 图表图例配置 ====================
|
||||
const chartLegendConfig: Record<string, string[]> = {
|
||||
'dzxq.tabs.jcsj': [
|
||||
'坝上水位',
|
||||
'坝下水位',
|
||||
'入库流量',
|
||||
'出库流量',
|
||||
'生态流量',
|
||||
'生态流量限值'
|
||||
]
|
||||
};
|
||||
|
||||
const chartLegendData = computed(() => {
|
||||
return chartLegendConfig[props.code] || chartLegendConfig['dzxq.tabs.jcsj'];
|
||||
});
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
const fetchData = async () => {
|
||||
if (!dateRange.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
field: 'tm',
|
||||
dir: 'asc'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await getMonitorData(params);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
// 数据按时间倒序显示(最新的在前,1月1号最后)
|
||||
const reversedData = [...rawData].reverse();
|
||||
tableData.value = reversedData;
|
||||
updateChart(rawData); // 图表仍按升序渲染
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
tableData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查询按钮点击事件
|
||||
const handleSearch = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// ==================== 图表相关 ====================
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 按时间升序
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
|
||||
const xAxisData = sorted.map(item => dayjs(item.tm).format('HH:mm'));
|
||||
const rzData = sorted.map(item => item.rz);
|
||||
const dzData = sorted.map(item => item.dz);
|
||||
const qiData = sorted.map(item => item.qi);
|
||||
const qoData = sorted.map(item => item.qo);
|
||||
const qecData = sorted.map(item => item.qec);
|
||||
const qecLimitData = sorted.map(item => item.qecLimit);
|
||||
|
||||
// 计算水位 Y 轴最大值(左侧)
|
||||
const waterValues = [...rzData, ...dzData].filter(
|
||||
v => v !== null && v !== undefined
|
||||
);
|
||||
const waterMax = waterValues.length > 0 ? Math.max(...waterValues) : 10;
|
||||
const waterYMax = Math.ceil(waterMax * 1.1);
|
||||
const waterYMin = Math.floor(Math.min(...waterValues) * 0.95);
|
||||
|
||||
// 计算流量 Y 轴最大值(右侧)
|
||||
const flowValues = [...qiData, ...qoData, ...qecData, ...qecLimitData].filter(
|
||||
v => v !== null && v !== undefined
|
||||
);
|
||||
const flowMax = flowValues.length > 0 ? Math.max(...flowValues) : 10;
|
||||
const flowYMax = Math.ceil(flowMax * 1.1);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const fullTime = sorted[dataIndex]?.tm
|
||||
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '';
|
||||
let html = `<div style="font-size:14px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((param: any) => {
|
||||
const value = param.value != null ? param.value : '-';
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span style="font-size:14px;">${param.seriesName}: ${value}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: 10,
|
||||
data: chartLegendData.value,
|
||||
textStyle: { fontSize: 12 },
|
||||
selected: {
|
||||
坝上水位: false,
|
||||
坝下水位: false,
|
||||
入库流量: true,
|
||||
出库流量: true,
|
||||
生态流量: false,
|
||||
生态流量限值: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 60,
|
||||
top: 80,
|
||||
bottom: 60
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
// 左侧 Y 轴:水位
|
||||
{
|
||||
name: '水位',
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9,
|
||||
boundaryGap: ['10%', 0]
|
||||
},
|
||||
// 右侧 Y 轴:流量
|
||||
{
|
||||
name: '流量',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9,
|
||||
boundaryGap: ['10%', 0]
|
||||
}
|
||||
],
|
||||
series: [
|
||||
// 水位系列(使用左侧 Y 轴)
|
||||
{
|
||||
name: '坝上水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: rzData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#56C2E3', width: 2 },
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '坝下水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: dzData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#7399C6', width: 2 },
|
||||
itemStyle: { color: '#7399C6' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
// 流量系列(使用右侧 Y 轴)
|
||||
{
|
||||
name: '入库流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qiData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#4B79AB', width: 2 },
|
||||
itemStyle: { color: '#4B79AB' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '出库流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qoData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#78C300', width: 2 },
|
||||
itemStyle: { color: '#78C300' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '生态流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qecData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#00A050', width: 2 },
|
||||
itemStyle: { color: '#00A050' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '生态流量限值',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qecLimitData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#F7A737', width: 2, type: 'dashed' },
|
||||
itemStyle: { color: '#F7A737' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
const destroyChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
const initData = () => {
|
||||
if (hasLoaded.value) return;
|
||||
fetchData();
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
}, 200);
|
||||
hasLoaded.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => modelStore.params.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (newStcd && newStcd !== oldStcd) {
|
||||
console.log('【MonitorInfo】stcd 变化,重新加载数据:', newStcd);
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyChart();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-info {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.search-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
width: 100%;
|
||||
|
||||
.content-body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 600px;
|
||||
|
||||
.chart-wrapper {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-width: 500px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
frontend/src/components/MapModal/components/PanoramaInfo.vue
Normal file
140
frontend/src/components/MapModal/components/PanoramaInfo.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="panorama-info-view-container">
|
||||
<a-spin :spinning="loading">
|
||||
<iframe
|
||||
v-show="iframeUrl != ''"
|
||||
:src="iframeUrl"
|
||||
class="panorama-info-iframe"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<a-select
|
||||
v-show="iframeUrl != ''"
|
||||
class="panorama-info-select"
|
||||
v-model:value="selectedOption"
|
||||
:options="options"
|
||||
:fieldNames="{
|
||||
label: 'name',
|
||||
value: 'id'
|
||||
}"
|
||||
placeholder="请选择文件"
|
||||
@change="handleSelectChange"
|
||||
/>
|
||||
<a-empty
|
||||
v-show="!loading && iframeUrl == ''"
|
||||
class="empty-state"
|
||||
description="暂无数据"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { getPanoramaList, getIdUrl } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const hasLoaded = ref(false); // 标记是否已经请求过
|
||||
const loading = ref(true); // 标记是否正在加载中
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
const selectedOption = ref('');
|
||||
const options = ref([]);
|
||||
const iframeUrl = ref('');
|
||||
const handleSelectChange = value => {
|
||||
options.value.forEach(item => {
|
||||
if (item.id === value) {
|
||||
iframeUrl.value = item.newUrl;
|
||||
}
|
||||
});
|
||||
};
|
||||
const initPanoramaInfoList = () => {
|
||||
if (hasLoaded.value) return;
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['url', 'stnm']
|
||||
};
|
||||
loading.value = true;
|
||||
getPanoramaList(params).then(res => {
|
||||
if (res.data.data[0].url) {
|
||||
getIdUrl({
|
||||
id: res.data.data[0].url
|
||||
}).then(res1 => {
|
||||
hasLoaded.value = true; // 标记为已请求过
|
||||
loading.value = false;
|
||||
const data = res1?.data || {};
|
||||
const fileArray = Object.keys(data).map(key => ({
|
||||
id: key,
|
||||
name: data[key].name,
|
||||
oldUrl: data[key].fullpath,
|
||||
newUrl:
|
||||
import.meta.env.VITE_APP_ATTACHMENT_URL +
|
||||
'/Pano/Show/' +
|
||||
data[key].fullpath.split('?')[1]
|
||||
}));
|
||||
options.value = fileArray;
|
||||
iframeUrl.value = options.value[0].newUrl;
|
||||
selectedOption.value = options.value[0].id;
|
||||
});
|
||||
} else {
|
||||
hasLoaded.value = true; // 标记为已请求过
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
initPanoramaInfoList();
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.panorama-info-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.panorama-info-select {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.panorama-info-iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 600px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
监测数据
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -1,682 +0,0 @@
|
||||
<template>
|
||||
<div class="body_one">
|
||||
<div class="search_one">
|
||||
<div class="search_left">
|
||||
<div>鱼类选择:</div>
|
||||
<a-select v-model:value="selectValue" mode="multiple" style="width: 260px;" placeholder=" "
|
||||
:options="selectOption" @change="handleChange"></a-select>
|
||||
</div>
|
||||
<div>
|
||||
<a-range-picker v-model:value="rangePicker" format="YYYY-MM-DD" :disabled-date="disabledDate"
|
||||
:allow-clear="false" @change="handleDateChange">
|
||||
<template #renderExtraFooter>
|
||||
<div class="quick-date-options">
|
||||
<a-button size="small" @click="setQuickRange(7)">最近七天</a-button>
|
||||
<a-button size="small" @click="setQuickRange(30)">最近一个月</a-button>
|
||||
<a-button size="small" @click="setQuickRange(90)">最近三个月</a-button>
|
||||
<a-button size="small" @click="setQuickRange(180)">最近六个月</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body_body">
|
||||
<div class="echarts" ref="chartRef"></div>
|
||||
<div class="table">
|
||||
<BasicTable ref="tableRef" :scrollY="460" :columns="columns" :list-url="fishGetKendoListCust"
|
||||
:search-params="{}" :transform-data="customTransform">
|
||||
<!-- 同期对比列的自定义渲染 -->
|
||||
<template #contrast="{ record }">
|
||||
<span v-if="record.wt && record.beforeWt"
|
||||
:style="{ color: (record.wt - record.beforeWt) > 0 ? 'rgb(255, 85, 0)' : 'rgb(135, 208, 104)' }">
|
||||
{{ (record.wt - record.beforeWt) > 0 ? '+' : '' }}{{ (record.wt -
|
||||
record.beforeWt).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onUnmounted, computed } from "vue";
|
||||
import * as echarts from 'echarts';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useModelStore } from "@/store/modules/model";
|
||||
import { infoGetKendoListCust, fishGetKendoListCust } from "@/api/sw";
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
const modelStore = useModelStore();
|
||||
const selectOption = ref([])
|
||||
const selectValue = ref([])
|
||||
const tableRef = ref<any>(null);
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
const chartData = ref<any[]>([]);
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'dt',
|
||||
width: 180,
|
||||
customRender: ({ text }: any) => text ? dayjs(text).format('YYYY-MM-DD') : '-'
|
||||
},
|
||||
{
|
||||
title: '水温°C',
|
||||
dataIndex: 'wt',
|
||||
customRender: ({ text }: any) => text !== undefined && text !== null ? text : '-'
|
||||
},
|
||||
{
|
||||
title: '上一年水温',
|
||||
dataIndex: 'beforeWt',
|
||||
customRender: ({ text }: any) => text !== undefined && text !== null ? text : '-'
|
||||
},
|
||||
{
|
||||
title: '同期对比',
|
||||
dataIndex: 'contrast',
|
||||
width: 120,
|
||||
slots: { customRender: 'contrast' }
|
||||
},
|
||||
{
|
||||
title: '适宜产卵鱼',
|
||||
dataIndex: 'spawnFish',
|
||||
width: 200,
|
||||
customRender: ({ record }: any) => {
|
||||
if (record.fishList && Array.isArray(record.fishList) && record.fishList.length > 0) {
|
||||
return record.fishList.map(fish => fish.name).join('、');
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
];
|
||||
// 初始化日期范围:7天前到当天
|
||||
const initDateRange = () => {
|
||||
const endDate = dayjs();
|
||||
const startDate = dayjs().subtract(7, 'day');
|
||||
return [startDate, endDate] as [Dayjs, Dayjs];
|
||||
};
|
||||
|
||||
const rangePicker = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
// 禁用超过当前日期的选项
|
||||
const disabledDate = (current: Dayjs) => {
|
||||
// 不能选择今天之后的日期
|
||||
return current && current.isAfter(dayjs(), 'day');
|
||||
};
|
||||
|
||||
// 设置快捷日期范围
|
||||
const setQuickRange = (days: number) => {
|
||||
const endDate = dayjs();
|
||||
const startDate = dayjs().subtract(days, 'day');
|
||||
rangePicker.value = [startDate, endDate];
|
||||
// 手动触发变更事件
|
||||
handleDateChange(rangePicker.value, [
|
||||
startDate.format('YYYY-MM-DD'),
|
||||
endDate.format('YYYY-MM-DD')
|
||||
]);
|
||||
};
|
||||
|
||||
// 日期改变时的处理方法(内容待定)
|
||||
const handleDateChange = (dates: any, dateStrings: string[]) => {
|
||||
console.log('日期改变:', dates, dateStrings);
|
||||
getEchartsData()
|
||||
gerTableData()
|
||||
// TODO: 在此处添加日期改变后的业务逻辑
|
||||
};
|
||||
// 下拉框数据
|
||||
const getSelectOption = async () => {
|
||||
//
|
||||
// modelStore.params.date
|
||||
let params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "stcd",
|
||||
"operator": "eq",
|
||||
"dataType": "string",
|
||||
"value": modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
let res = await infoGetKendoListCust(params)
|
||||
let data = res?.data?.data || res?.data
|
||||
console.log(res)
|
||||
selectOption.value = data.map(item => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
pretemp: item.pretemp,
|
||||
}
|
||||
})
|
||||
// 根据数据长度智能选择ID:2个及以上取前2个,少于2个取第1个
|
||||
selectValue.value = data.length >= 2 ? data.slice(0, 2).map(item => item.id) : (data.length > 0 ? [data[0].id] : [])
|
||||
}
|
||||
//echarts 数据获取
|
||||
const getEchartsData = async () => {
|
||||
const params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "stcd",
|
||||
"operator": "eq",
|
||||
"dataType": "string",
|
||||
"value": modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
"field": "startTime",
|
||||
"operator": "gte",
|
||||
"dataType": "date",
|
||||
"value": rangePicker.value[0].format('YYYY-MM-DD') + " 00:00:00"
|
||||
},
|
||||
{
|
||||
"field": "endTime",
|
||||
"operator": "lte",
|
||||
"dataType": "date",
|
||||
"value": rangePicker.value[1].format('YYYY-MM-DD') + " 23:59:59"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"field": "dt",
|
||||
"dir": "desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
let res = await fishGetKendoListCust(params)
|
||||
console.log('ECharts原始数据:', res)
|
||||
|
||||
// 存储原始数据到响应式变量
|
||||
const rawData = res?.data?.data || res?.data?.records || []
|
||||
chartData.value = rawData
|
||||
|
||||
// 延迟更新图表,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
updateChart()
|
||||
}, 100)
|
||||
}
|
||||
//table 数据获取
|
||||
const gerTableData = async () => {
|
||||
const filter = {
|
||||
logic: 'and',
|
||||
"filters": [
|
||||
{
|
||||
"field": "stcd",
|
||||
"operator": "eq",
|
||||
"dataType": "string",
|
||||
"value": modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
"field": "startTime",
|
||||
"operator": "gte",
|
||||
"dataType": "date",
|
||||
"value": rangePicker.value[0].format('YYYY-MM-DD') + " 00:00:00"
|
||||
},
|
||||
{
|
||||
"field": "endTime",
|
||||
"operator": "lte",
|
||||
"dataType": "date",
|
||||
"value": rangePicker.value[1].format('YYYY-MM-DD') + " 23:59:59"
|
||||
}
|
||||
]
|
||||
};
|
||||
tableRef.value?.getList(filter);
|
||||
}
|
||||
|
||||
const handleChange = (value: any) => {
|
||||
console.log(value)
|
||||
getEchartsData()
|
||||
// gerTableData()
|
||||
}
|
||||
const customTransform = (res: any) => {
|
||||
console.log('表格数据:', res);
|
||||
return {
|
||||
records: res?.data?.data || [],
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
// 过滤和处理图表数据
|
||||
const processChartData = (rawData: any[]) => {
|
||||
if (!rawData || !Array.isArray(rawData)) return [];
|
||||
|
||||
// 过滤掉 wt 或 beforeWt 为 null 的记录
|
||||
const filtered = rawData.filter(item =>
|
||||
item.wt !== null && item.wt !== undefined
|
||||
);
|
||||
// && item.beforeWt !== null && item.beforeWt !== undefined
|
||||
|
||||
// 按 dt 升序排序
|
||||
return filtered.sort((a, b) =>
|
||||
new Date(a.dt).getTime() - new Date(b.dt).getTime()
|
||||
);
|
||||
};
|
||||
|
||||
// 计算 Y 轴最大值(向上取整到个位)
|
||||
const calculateYAxisMax = (data: any[]) => {
|
||||
if (!data || data.length === 0) return 10;
|
||||
|
||||
const allValues = [
|
||||
...data.map(item => item.wt),
|
||||
...data.map(item => item.beforeWt)
|
||||
].filter(v => v !== null && v !== undefined);
|
||||
|
||||
const max = Math.max(...allValues);
|
||||
return Math.ceil(max);
|
||||
};
|
||||
|
||||
// 生成随机颜色(HSL转HEX)
|
||||
const generateRandomColor = () => {
|
||||
const hue = Math.random() * 360; // 色相:0-360°
|
||||
const saturation = 40 + Math.random() * 30; // 饱和度:40%-70%
|
||||
const lightness = 45 + Math.random() * 20; // 亮度:45%-65%
|
||||
|
||||
// HSL转RGB
|
||||
const c = (1 - Math.abs(2 * lightness / 100 - 1)) * saturation / 100;
|
||||
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
|
||||
const m = lightness / 100 - c / 2;
|
||||
|
||||
let r = 0, g = 0, b = 0;
|
||||
|
||||
if (hue < 60) { r = c; g = x; b = 0; }
|
||||
else if (hue < 120) { r = x; g = c; b = 0; }
|
||||
else if (hue < 180) { r = 0; g = c; b = x; }
|
||||
else if (hue < 240) { r = 0; g = x; b = c; }
|
||||
else if (hue < 300) { r = x; g = 0; b = c; }
|
||||
else { r = c; g = 0; b = x; }
|
||||
|
||||
// RGB转HEX
|
||||
const toHex = (value: number) => {
|
||||
const hex = Math.round((value + m) * 255).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
};
|
||||
|
||||
// 计算选中鱼类的温度范围数组
|
||||
const calculateTempRanges = () => {
|
||||
if (!selectValue.value || selectValue.value.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 从 selectOption 中找到选中的鱼类
|
||||
const selectedFish = selectOption.value.filter(fish =>
|
||||
selectValue.value.includes(fish.value)
|
||||
);
|
||||
|
||||
if (selectedFish.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 为每个鱼类生成温度范围和随机颜色
|
||||
return selectedFish
|
||||
.map(fish => {
|
||||
if (!fish.pretemp || !Array.isArray(fish.pretemp) || fish.pretemp.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const min = parseFloat(fish.pretemp[0]);
|
||||
const max = parseFloat(fish.pretemp[1]);
|
||||
|
||||
if (isNaN(min) || isNaN(max)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: fish.label,
|
||||
min,
|
||||
max,
|
||||
color: generateRandomColor()
|
||||
};
|
||||
})
|
||||
.filter(item => item !== null);
|
||||
};
|
||||
|
||||
// 生成 ECharts 配置
|
||||
const getChartOption = computed(() => {
|
||||
const data = processChartData(chartData.value);
|
||||
|
||||
if (data.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// const yAxisMax = calculateYAxisMax(data);
|
||||
const tempRanges = calculateTempRanges();
|
||||
const dataMax = calculateYAxisMax(data);
|
||||
const tempMax = tempRanges.length > 0
|
||||
? Math.max(...tempRanges.map(r => r.max))
|
||||
: 0;
|
||||
const yAxisMax = Math.max(dataMax, tempMax);
|
||||
// 提取时间轴和数据
|
||||
const xAxisData = data.map(item => dayjs(item.dt).format('YYYY-MM-DD'));
|
||||
const wtData = data.map(item => item.wt);
|
||||
const beforeWtData = data.map(item => item.beforeWt);
|
||||
|
||||
// 构建 markArea 配置(为每个鱼类生成独立的背景区域)
|
||||
const markAreaConfig = tempRanges.length > 0 ? {
|
||||
silent: true,
|
||||
data: tempRanges.map(range => [
|
||||
{
|
||||
yAxis: range.min,
|
||||
name: `${range.name}: ${range.min}-${range.max}°C`,
|
||||
itemStyle: { color: `${range.color}26` }
|
||||
},
|
||||
{
|
||||
yAxis: range.max
|
||||
}
|
||||
])
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
|
||||
const date = params[0].axisValue;
|
||||
let html = `<div style="font-weight:bold;margin-bottom:8px;">${date}</div>`;
|
||||
|
||||
params.forEach((param: any) => {
|
||||
const dataIndex = param.dataIndex;
|
||||
const record = data[dataIndex];
|
||||
const value = param.value != null ? param.value : '-';
|
||||
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span>${param.seriesName}: ${value}°C</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 10,
|
||||
left: 'center',
|
||||
data: ['现在水温', '上一年同期水温'],
|
||||
textStyle: {
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 40,
|
||||
top: 60,
|
||||
bottom: 50
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#000000'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: (() => {
|
||||
const dataLen = xAxisData.length;
|
||||
if (dataLen <= 8) return 0; // 数据少时全部显示
|
||||
|
||||
// 计算最大允许的间隔,确保首尾都能显示
|
||||
// 需要显示的首尾2个 + 中间最多6个 = 总共8个标签
|
||||
const maxLabels = 8;
|
||||
const minInterval = Math.ceil((dataLen - 1) / (maxLabels - 1));
|
||||
|
||||
// 如果原有计算的间隔更大,使用原有逻辑;否则使用最小间隔
|
||||
const calculatedInterval = Math.floor((dataLen - 1) / 7);
|
||||
return Math.max(calculatedInterval, minInterval);
|
||||
})(),
|
||||
fontSize: 12,
|
||||
rotate: 0,
|
||||
margin: 10
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#bfbfbf',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '水温(°C)',
|
||||
min: 0,
|
||||
max: yAxisMax,
|
||||
scale: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#000000',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
length: 4
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#bfbfbf',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '现在水温',
|
||||
type: 'line',
|
||||
data: wtData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: '#009dff', width: 2 },
|
||||
itemStyle: { color: '#009dff' },
|
||||
connectNulls: false,
|
||||
// 移除 markArea
|
||||
},
|
||||
{
|
||||
name: '上一年同期水温',
|
||||
type: 'line',
|
||||
data: beforeWtData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: '#fee588', width: 2 },
|
||||
itemStyle: { color: '#fee588' },
|
||||
connectNulls: false
|
||||
},
|
||||
// 新增辅助系列,专门承载 markArea
|
||||
{
|
||||
name: '温度区间背景', // 名字不会被放入 legend,所以不显示
|
||||
type: 'line',
|
||||
data: new Array(xAxisData.length).fill(0), // 假数据,保证坐标匹配
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: { opacity: 0 },
|
||||
itemStyle: { opacity: 0 },
|
||||
legendHoverLink: false, // 图例悬停时不联动
|
||||
silent: true, // 不响应鼠标事件
|
||||
markArea: markAreaConfig // 温度区间背景绑定于此
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
title: '导出图片',
|
||||
type: 'png',
|
||||
pixelRatio: 2
|
||||
}
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart();
|
||||
|
||||
// 监听窗口resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
const option = getChartOption.value;
|
||||
if (Object.keys(option).length > 0) {
|
||||
chartInstance.setOption(option, true);
|
||||
} else {
|
||||
// 如果没有数据,清空图表或显示空状态
|
||||
chartInstance.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理窗口resize
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
// 销毁图表
|
||||
const destroyChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getSelectOption()
|
||||
getEchartsData()
|
||||
gerTableData()
|
||||
// 初始化图表
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
}, 200)
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyChart()
|
||||
});
|
||||
watch(
|
||||
() => modelStore.params.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
// 避免初始化时的无效触发
|
||||
if (newStcd && newStcd !== oldStcd) {
|
||||
console.log('【BasicInfo】stcd 变化,重新加载数据:', newStcd);
|
||||
getSelectOption()
|
||||
getEchartsData()
|
||||
gerTableData()
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.body_one {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
|
||||
.search_one {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.search_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.body_body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 540px;
|
||||
|
||||
.echarts {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-date-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
.ant-btn {
|
||||
border-color: #1890ff !important;
|
||||
color: #1890ff !important;
|
||||
background-color: #e6f7ff !important;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
border-color: #40a9ff !important;
|
||||
color: #40a9ff !important;
|
||||
background-color: #bae7ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,779 @@
|
||||
<template>
|
||||
<div class="body_one">
|
||||
<div class="search_one">
|
||||
<div class="search_left">
|
||||
<div>鱼类选择:</div>
|
||||
<a-select
|
||||
v-model:value="selectValue"
|
||||
mode="multiple"
|
||||
style="width: 260px"
|
||||
placeholder=" "
|
||||
:options="selectOption"
|
||||
@change="handleChange"
|
||||
></a-select>
|
||||
</div>
|
||||
<div>
|
||||
<a-range-picker
|
||||
v-model:value="rangePicker"
|
||||
format="YYYY-MM-DD"
|
||||
:disabled-date="disabledDate"
|
||||
:allow-clear="false"
|
||||
@change="handleDateChange"
|
||||
>
|
||||
<template #renderExtraFooter>
|
||||
<div class="quick-date-options">
|
||||
<a-button size="small" @click="setQuickRange(7)"
|
||||
>最近七天</a-button
|
||||
>
|
||||
<a-button size="small" @click="setQuickRange(30)"
|
||||
>最近一个月</a-button
|
||||
>
|
||||
<a-button size="small" @click="setQuickRange(90)"
|
||||
>最近三个月</a-button
|
||||
>
|
||||
<a-button size="small" @click="setQuickRange(180)"
|
||||
>最近六个月</a-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</a-range-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body_body">
|
||||
<div class="echarts" ref="chartRef"></div>
|
||||
<div class="table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="460"
|
||||
:columns="columns"
|
||||
:list-url="fishGetKendoListCust"
|
||||
:search-params="{}"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<!-- 同期对比列的自定义渲染 -->
|
||||
<template #contrast="{ record }">
|
||||
<span
|
||||
v-if="record.wt && record.beforeWt"
|
||||
:style="{
|
||||
color:
|
||||
record.wt - record.beforeWt > 0
|
||||
? 'rgb(255, 85, 0)'
|
||||
: 'rgb(135, 208, 104)'
|
||||
}"
|
||||
>
|
||||
{{ record.wt - record.beforeWt > 0 ? '+' : ''
|
||||
}}{{ (record.wt - record.beforeWt).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onUnmounted, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { infoGetKendoListCust, fishGetKendoListCust } from '@/api/sw';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
const modelStore = useModelStore();
|
||||
const selectOption = ref([]);
|
||||
const selectValue = ref([]);
|
||||
const tableRef = ref<any>(null);
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
const hasLoaded = ref(false); // 标记是否已经请求过
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'dt',
|
||||
width: 180,
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD') : '-'
|
||||
},
|
||||
{
|
||||
title: '水温°C',
|
||||
dataIndex: 'wt',
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? text : '-'
|
||||
},
|
||||
{
|
||||
title: '上一年水温',
|
||||
dataIndex: 'beforeWt',
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? text : '-'
|
||||
},
|
||||
{
|
||||
title: '同期对比',
|
||||
dataIndex: 'contrast',
|
||||
width: 120,
|
||||
slots: { customRender: 'contrast' }
|
||||
},
|
||||
{
|
||||
title: '适宜产卵鱼',
|
||||
dataIndex: 'spawnFish',
|
||||
width: 200,
|
||||
customRender: ({ record }: any) => {
|
||||
if (
|
||||
record.fishList &&
|
||||
Array.isArray(record.fishList) &&
|
||||
record.fishList.length > 0
|
||||
) {
|
||||
return record.fishList.map(fish => fish.name).join('、');
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
];
|
||||
// 初始化日期范围:7天前到当天
|
||||
const initDateRange = () => {
|
||||
const endDate = dayjs();
|
||||
const startDate = dayjs().subtract(7, 'day');
|
||||
return [startDate, endDate] as [Dayjs, Dayjs];
|
||||
};
|
||||
|
||||
const rangePicker = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
// 禁用超过当前日期的选项
|
||||
const disabledDate = (current: Dayjs) => {
|
||||
// 不能选择今天之后的日期
|
||||
return current && current.isAfter(dayjs(), 'day');
|
||||
};
|
||||
|
||||
// 设置快捷日期范围
|
||||
const setQuickRange = (days: number) => {
|
||||
const endDate = dayjs();
|
||||
const startDate = dayjs().subtract(days, 'day');
|
||||
rangePicker.value = [startDate, endDate];
|
||||
// 手动触发变更事件
|
||||
handleDateChange(rangePicker.value, [
|
||||
startDate.format('YYYY-MM-DD'),
|
||||
endDate.format('YYYY-MM-DD')
|
||||
]);
|
||||
};
|
||||
|
||||
// 日期改变时的处理方法(内容待定)
|
||||
const handleDateChange = (dates: any, dateStrings: string[]) => {
|
||||
console.log('日期改变:', dates, dateStrings);
|
||||
getEchartsData();
|
||||
gerTableData();
|
||||
// TODO: 在此处添加日期改变后的业务逻辑
|
||||
};
|
||||
// 下拉框数据
|
||||
const getSelectOption = async () => {
|
||||
//
|
||||
// modelStore.params.date
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
let res = await infoGetKendoListCust(params);
|
||||
let data = res?.data?.data || res?.data;
|
||||
console.log(res);
|
||||
selectOption.value = data.map(item => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
pretemp: item.pretemp
|
||||
};
|
||||
});
|
||||
// 根据数据长度智能选择ID:2个及以上取前2个,少于2个取第1个
|
||||
selectValue.value =
|
||||
data.length >= 2
|
||||
? data.slice(0, 2).map(item => item.id)
|
||||
: data.length > 0
|
||||
? [data[0].id]
|
||||
: [];
|
||||
};
|
||||
//echarts 数据获取
|
||||
const getEchartsData = async () => {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: rangePicker.value[0].format('YYYY-MM-DD') + ' 00:00:00'
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: rangePicker.value[1].format('YYYY-MM-DD') + ' 23:59:59'
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
field: 'dt',
|
||||
dir: 'desc'
|
||||
}
|
||||
]
|
||||
};
|
||||
let res = await fishGetKendoListCust(params);
|
||||
console.log('ECharts原始数据:', res);
|
||||
|
||||
// 存储原始数据到响应式变量
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
chartData.value = rawData;
|
||||
|
||||
// 延迟更新图表,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
updateChart();
|
||||
}, 100);
|
||||
};
|
||||
//table 数据获取
|
||||
const gerTableData = async () => {
|
||||
const filter = {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: rangePicker.value[0].format('YYYY-MM-DD') + ' 00:00:00'
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: rangePicker.value[1].format('YYYY-MM-DD') + ' 23:59:59'
|
||||
}
|
||||
]
|
||||
};
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
const handleChange = (value: any) => {
|
||||
console.log(value);
|
||||
getEchartsData();
|
||||
// gerTableData()
|
||||
};
|
||||
const customTransform = (res: any) => {
|
||||
console.log('表格数据:', res);
|
||||
return {
|
||||
records: res?.data?.data || [],
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
// 过滤和处理图表数据
|
||||
const processChartData = (rawData: any[]) => {
|
||||
if (!rawData || !Array.isArray(rawData)) return [];
|
||||
|
||||
// 过滤掉 wt 或 beforeWt 为 null 的记录
|
||||
const filtered = rawData.filter(
|
||||
item => item.wt !== null && item.wt !== undefined
|
||||
);
|
||||
// && item.beforeWt !== null && item.beforeWt !== undefined
|
||||
|
||||
// 按 dt 升序排序
|
||||
return filtered.sort(
|
||||
(a, b) => new Date(a.dt).getTime() - new Date(b.dt).getTime()
|
||||
);
|
||||
};
|
||||
|
||||
// 计算 Y 轴最大值(向上取整到个位)
|
||||
const calculateYAxisMax = (data: any[]) => {
|
||||
if (!data || data.length === 0) return 10;
|
||||
|
||||
const allValues = [
|
||||
...data.map(item => item.wt),
|
||||
...data.map(item => item.beforeWt)
|
||||
].filter(v => v !== null && v !== undefined);
|
||||
|
||||
const max = Math.max(...allValues);
|
||||
return Math.ceil(max);
|
||||
};
|
||||
|
||||
// 生成随机颜色(HSL转HEX)
|
||||
const generateRandomColor = () => {
|
||||
const hue = Math.random() * 360; // 色相:0-360°
|
||||
const saturation = 40 + Math.random() * 30; // 饱和度:40%-70%
|
||||
const lightness = 45 + Math.random() * 20; // 亮度:45%-65%
|
||||
|
||||
// HSL转RGB
|
||||
const c = ((1 - Math.abs((2 * lightness) / 100 - 1)) * saturation) / 100;
|
||||
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
||||
const m = lightness / 100 - c / 2;
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
|
||||
if (hue < 60) {
|
||||
r = c;
|
||||
g = x;
|
||||
b = 0;
|
||||
} else if (hue < 120) {
|
||||
r = x;
|
||||
g = c;
|
||||
b = 0;
|
||||
} else if (hue < 180) {
|
||||
r = 0;
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (hue < 240) {
|
||||
r = 0;
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (hue < 300) {
|
||||
r = x;
|
||||
g = 0;
|
||||
b = c;
|
||||
} else {
|
||||
r = c;
|
||||
g = 0;
|
||||
b = x;
|
||||
}
|
||||
|
||||
// RGB转HEX
|
||||
const toHex = (value: number) => {
|
||||
const hex = Math.round((value + m) * 255).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
};
|
||||
|
||||
// 计算选中鱼类的温度范围数组
|
||||
const calculateTempRanges = () => {
|
||||
if (!selectValue.value || selectValue.value.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 从 selectOption 中找到选中的鱼类
|
||||
const selectedFish = selectOption.value.filter(fish =>
|
||||
selectValue.value.includes(fish.value)
|
||||
);
|
||||
|
||||
if (selectedFish.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 为每个鱼类生成温度范围和随机颜色
|
||||
return selectedFish
|
||||
.map(fish => {
|
||||
if (
|
||||
!fish.pretemp ||
|
||||
!Array.isArray(fish.pretemp) ||
|
||||
fish.pretemp.length !== 2
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const min = parseFloat(fish.pretemp[0]);
|
||||
const max = parseFloat(fish.pretemp[1]);
|
||||
|
||||
if (isNaN(min) || isNaN(max)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: fish.label,
|
||||
min,
|
||||
max,
|
||||
color: generateRandomColor()
|
||||
};
|
||||
})
|
||||
.filter(item => item !== null);
|
||||
};
|
||||
|
||||
// 生成 ECharts 配置
|
||||
const getChartOption = computed(() => {
|
||||
const data = processChartData(chartData.value);
|
||||
|
||||
if (data.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// const yAxisMax = calculateYAxisMax(data);
|
||||
const tempRanges = calculateTempRanges();
|
||||
const dataMax = calculateYAxisMax(data);
|
||||
const tempMax =
|
||||
tempRanges.length > 0 ? Math.max(...tempRanges.map(r => r.max)) : 0;
|
||||
const yAxisMax = Math.max(dataMax, tempMax);
|
||||
// 提取时间轴和数据
|
||||
const xAxisData = data.map(item => dayjs(item.dt).format('YYYY-MM-DD'));
|
||||
const wtData = data.map(item => item.wt);
|
||||
const beforeWtData = data.map(item => item.beforeWt);
|
||||
|
||||
// 构建 markArea 配置(为每个鱼类生成独立的背景区域)
|
||||
const markAreaConfig =
|
||||
tempRanges.length > 0
|
||||
? {
|
||||
silent: true,
|
||||
data: tempRanges.map(range => [
|
||||
{
|
||||
yAxis: range.min,
|
||||
name: `${range.name}: ${range.min}-${range.max}°C`,
|
||||
itemStyle: { color: `${range.color}26` }
|
||||
},
|
||||
{
|
||||
yAxis: range.max
|
||||
}
|
||||
])
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
|
||||
const date = params[0].axisValue;
|
||||
let html = `<div style="font-weight:bold;margin-bottom:8px;">${date}</div>`;
|
||||
|
||||
params.forEach((param: any) => {
|
||||
const dataIndex = param.dataIndex;
|
||||
const record = data[dataIndex];
|
||||
const value = param.value != null ? param.value : '-';
|
||||
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span>${param.seriesName}: ${value}°C</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 10,
|
||||
left: 'center',
|
||||
data: ['现在水温', '上一年同期水温'],
|
||||
textStyle: {
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 40,
|
||||
top: 60,
|
||||
bottom: 50
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#000000'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: (() => {
|
||||
const dataLen = xAxisData.length;
|
||||
if (dataLen <= 8) return 0; // 数据少时全部显示
|
||||
|
||||
// 计算最大允许的间隔,确保首尾都能显示
|
||||
// 需要显示的首尾2个 + 中间最多6个 = 总共8个标签
|
||||
const maxLabels = 8;
|
||||
const minInterval = Math.ceil((dataLen - 1) / (maxLabels - 1));
|
||||
|
||||
// 如果原有计算的间隔更大,使用原有逻辑;否则使用最小间隔
|
||||
const calculatedInterval = Math.floor((dataLen - 1) / 7);
|
||||
return Math.max(calculatedInterval, minInterval);
|
||||
})(),
|
||||
fontSize: 12,
|
||||
rotate: 0,
|
||||
margin: 10
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#bfbfbf',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '水温(°C)',
|
||||
min: 0,
|
||||
max: yAxisMax,
|
||||
scale: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#000000',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
length: 4
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#bfbfbf',
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '现在水温',
|
||||
type: 'line',
|
||||
data: wtData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: '#009dff', width: 2 },
|
||||
itemStyle: { color: '#009dff' },
|
||||
connectNulls: false
|
||||
// 移除 markArea
|
||||
},
|
||||
{
|
||||
name: '上一年同期水温',
|
||||
type: 'line',
|
||||
data: beforeWtData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: '#fee588', width: 2 },
|
||||
itemStyle: { color: '#fee588' },
|
||||
connectNulls: false
|
||||
},
|
||||
// 新增辅助系列,专门承载 markArea
|
||||
{
|
||||
name: '温度区间背景', // 名字不会被放入 legend,所以不显示
|
||||
type: 'line',
|
||||
data: new Array(xAxisData.length).fill(0), // 假数据,保证坐标匹配
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
lineStyle: { opacity: 0 },
|
||||
itemStyle: { opacity: 0 },
|
||||
legendHoverLink: false, // 图例悬停时不联动
|
||||
silent: true, // 不响应鼠标事件
|
||||
markArea: markAreaConfig // 温度区间背景绑定于此
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
title: '导出图片',
|
||||
type: 'png',
|
||||
pixelRatio: 2
|
||||
}
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart();
|
||||
|
||||
// 监听窗口resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
const option = getChartOption.value;
|
||||
if (Object.keys(option).length > 0) {
|
||||
chartInstance.setOption(option, true);
|
||||
} else {
|
||||
// 如果没有数据,清空图表或显示空状态
|
||||
chartInstance.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理窗口resize
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
// 销毁图表
|
||||
const destroyChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
const initData = () => {
|
||||
if (hasLoaded.value) return;
|
||||
getSelectOption();
|
||||
getEchartsData();
|
||||
gerTableData();
|
||||
// 初始化图表
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
}, 200);
|
||||
hasLoaded.value = true; // 标记为已请求过
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
// onMounted(() => {
|
||||
// getSelectOption()
|
||||
// getEchartsData()
|
||||
// gerTableData()
|
||||
// // 初始化图表
|
||||
// setTimeout(() => {
|
||||
// initChart()
|
||||
// }, 200)
|
||||
// });
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyChart();
|
||||
});
|
||||
watch(
|
||||
() => modelStore.params.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
// 避免初始化时的无效触发
|
||||
if (newStcd && newStcd !== oldStcd) {
|
||||
console.log('【BasicInfo】stcd 变化,重新加载数据:', newStcd);
|
||||
getSelectOption();
|
||||
getEchartsData();
|
||||
gerTableData();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.body_one {
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
|
||||
.search_one {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.search_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.body_body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 540px;
|
||||
|
||||
.echarts {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-date-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
.ant-btn {
|
||||
border-color: #1890ff !important;
|
||||
color: #1890ff !important;
|
||||
background-color: #e6f7ff !important;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
border-color: #40a9ff !important;
|
||||
color: #40a9ff !important;
|
||||
background-color: #bae7ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
674
frontend/src/components/MapModal/components/videoInfo.vue
Normal file
674
frontend/src/components/MapModal/components/videoInfo.vue
Normal file
@ -0,0 +1,674 @@
|
||||
<template>
|
||||
<div class="video-info-container">
|
||||
<a-row class="video-info-row" type="flex" style="height: 100%" :gutter="16">
|
||||
<!-- 左侧列表 15% -->
|
||||
<a-col flex="15%" class="left-panel">
|
||||
<div class="panel-header"><a-divider type="vertical" />视频列表</div>
|
||||
<div class="list-container">
|
||||
<a-spin :spinning="loading">
|
||||
<div
|
||||
v-for="item in videoList"
|
||||
:key="item.stcd"
|
||||
class="list-item"
|
||||
:class="{ active: currentVideo?.stcd === item.stcd }"
|
||||
@click="handleSelectVideo(item)"
|
||||
:title="item.stnm"
|
||||
>
|
||||
{{ item.stnm }}
|
||||
</div>
|
||||
<a-empty
|
||||
v-if="!loading && videoList.length === 0"
|
||||
description="暂无数据"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 中间视频区域 55% -->
|
||||
<a-col flex="55%" class="center-panel">
|
||||
<!-- 视频宫格 -->
|
||||
<div class="video-grid" :class="`layout-${viewLayout}`">
|
||||
<div
|
||||
v-for="index in parseInt(viewLayout)"
|
||||
:key="index"
|
||||
class="video-item"
|
||||
>
|
||||
<LiveVideoBox
|
||||
:video-data="videoSlots[index - 1]"
|
||||
:monitor-type="'live'"
|
||||
:show-title="viewLayout !== '1'"
|
||||
height="100%"
|
||||
@close="handleCloseVideo(index - 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧录像列表 30% -->
|
||||
<a-col flex="30%" class="right-panel">
|
||||
<div class="panel-header">
|
||||
<a-divider type="vertical" />
|
||||
回放列表
|
||||
<!-- 右上角宫格切换 -->
|
||||
<div class="layout-switcher">
|
||||
<a-radio-group v-model:value="viewLayout" button-style="solid">
|
||||
<a-radio-button value="1">
|
||||
<i class="iconfont icon-view1"></i>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="4">
|
||||
<i class="iconfont icon-view4"></i>
|
||||
</a-radio-button>
|
||||
<a-radio-button value="9">
|
||||
<i class="iconfont icon-view9"></i>
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择 -->
|
||||
<div class="date-picker">
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.days"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 录像列表 -->
|
||||
<div class="record-list">
|
||||
<a-spin :spinning="recordLoading">
|
||||
<div v-if="paginatedRecords.length === 0" class="empty-record">
|
||||
<a-empty />
|
||||
</div>
|
||||
<div v-else class="record-grid">
|
||||
<div
|
||||
class="record-item"
|
||||
v-for="item in paginatedRecords"
|
||||
:key="item.id"
|
||||
@click="handlePlayRecord(item)"
|
||||
>
|
||||
<div class="record-image-wrapper">
|
||||
<a-image :src="item.imgPath" :preview="false" alt="" />
|
||||
<div class="play-btn">
|
||||
<i class="icon iconfont icon-play"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="record-info">
|
||||
<div class="record-name">{{ item.name }}</div>
|
||||
<div class="record-time">
|
||||
{{ dayjs(item.time).format('YYYY-MM-DD HH') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="recordTotal"
|
||||
:pageSize="pageSize"
|
||||
size="small"
|
||||
show-less-items
|
||||
/>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
getVideoList,
|
||||
getVideoDetail,
|
||||
getVideoPlayDetail
|
||||
} from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const hasLoaded = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordLoading = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// 左侧视频列表数据
|
||||
const videoList = ref<any[]>([]);
|
||||
|
||||
// 中间视频数据
|
||||
const videoSlots = ref<any[]>(Array(9).fill(null));
|
||||
|
||||
// 当前播放的视频
|
||||
const currentVideo = ref<any>(null);
|
||||
|
||||
// 宫格布局
|
||||
const viewLayout = ref<'1' | '4' | '9'>('1');
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref<[Dayjs, Dayjs]>([dayjs().subtract(7, 'day'), dayjs()]);
|
||||
|
||||
// 右侧录像列表
|
||||
const recordList = ref<any[]>([]);
|
||||
const recordTotal = ref(0);
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1);
|
||||
const pageSize = 4; // 每页4个,一行2个,共2行
|
||||
|
||||
// 当前选中的stcd(用于右侧接口)
|
||||
const selectedStcd = ref<string>('');
|
||||
|
||||
// 分页后的录像数据
|
||||
const paginatedRecords = computed(() => {
|
||||
return recordList.value;
|
||||
});
|
||||
|
||||
// 加载右侧录像列表
|
||||
const loadRecordList = async () => {
|
||||
if (!selectedStcd.value) {
|
||||
recordList.value = [];
|
||||
recordTotal.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
recordLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'in',
|
||||
value: [selectedStcd.value]
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD 00:00:00')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD 23:59:59')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'desc' }],
|
||||
skip: (currentPage.value - 1) * pageSize,
|
||||
take: pageSize
|
||||
};
|
||||
|
||||
const res: any = await getVideoPlayDetail(params);
|
||||
if (res?.data?.data) {
|
||||
recordList.value = res.data.data;
|
||||
recordTotal.value = res.data.total || 0;
|
||||
} else {
|
||||
recordList.value = [];
|
||||
recordTotal.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
recordList.value = [];
|
||||
recordTotal.value = 0;
|
||||
} finally {
|
||||
recordLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
if (hasLoaded.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{ field: 'sttpCode', operator: 'startswith', value: 'VD_' },
|
||||
{
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'fhstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
},
|
||||
{
|
||||
field: 'stCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.params.stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['stcd', 'stnm', 'sttpCode', 'sttpName', 'playWay', 'url'],
|
||||
sort: [{ field: 'stnm', dir: 'asc' }]
|
||||
};
|
||||
const res: any = await getVideoList(params);
|
||||
if (res?.data?.data) {
|
||||
videoList.value = res.data.data;
|
||||
// 默认选中第一条并加载右侧录像
|
||||
if (res.data.data.length > 0) {
|
||||
const firstItem = res.data.data[0];
|
||||
currentVideo.value = firstItem;
|
||||
selectedStcd.value = firstItem.stcd;
|
||||
// 中间显示第一条视频
|
||||
videoSlots.value[0] = {
|
||||
flnm: firstItem.stnm,
|
||||
stnm: firstItem.stnm,
|
||||
url: firstItem.url,
|
||||
key: firstItem.stcd,
|
||||
...firstItem
|
||||
};
|
||||
loadRecordList();
|
||||
}
|
||||
}
|
||||
hasLoaded.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取视频列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 选择视频播放
|
||||
const handleSelectVideo = (item: any) => {
|
||||
currentVideo.value = item;
|
||||
selectedStcd.value = item.stcd;
|
||||
currentPage.value = 1; // 重置分页
|
||||
|
||||
// 只有在 viewLayout 为 1 时才替换中间视频
|
||||
if (viewLayout.value === '1') {
|
||||
const videoData = {
|
||||
flnm: item.stnm,
|
||||
stnm: item.stnm,
|
||||
url: item.url,
|
||||
key: item.stcd,
|
||||
...item
|
||||
};
|
||||
videoSlots.value[0] = videoData;
|
||||
// 清空其他槽位
|
||||
for (let i = 1; i < videoSlots.value.length; i++) {
|
||||
videoSlots.value[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新右侧录像列表
|
||||
loadRecordList();
|
||||
};
|
||||
|
||||
// 关闭视频
|
||||
const handleCloseVideo = (index: number) => {
|
||||
videoSlots.value[index] = null;
|
||||
};
|
||||
|
||||
// 播放录像
|
||||
const handlePlayRecord = (record: any) => {
|
||||
const videoData = {
|
||||
flnm: record.name,
|
||||
stnm: record.name,
|
||||
url: record.url,
|
||||
key: record.id,
|
||||
...record
|
||||
};
|
||||
|
||||
// 找第一个空位
|
||||
const emptyIndex = videoSlots.value.findIndex(
|
||||
(v, i) => i < parseInt(viewLayout.value) && !v
|
||||
);
|
||||
if (emptyIndex !== -1) {
|
||||
videoSlots.value[emptyIndex] = videoData;
|
||||
} else {
|
||||
// 满了替换第一个
|
||||
videoSlots.value[0] = videoData;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听布局变化,调整视频数组长度
|
||||
watch(viewLayout, newLayout => {
|
||||
const maxCount = parseInt(newLayout);
|
||||
while (videoSlots.value.length < maxCount) {
|
||||
videoSlots.value.push(null);
|
||||
}
|
||||
|
||||
// 切换到 1 宫格时,确保中间显示视频
|
||||
if (newLayout === '1') {
|
||||
const firstVideo = videoSlots.value.find(
|
||||
v => v !== null && v !== undefined
|
||||
);
|
||||
if (firstVideo) {
|
||||
// 中间有视频,保留第一个
|
||||
videoSlots.value[0] = firstVideo;
|
||||
for (let i = 1; i < videoSlots.value.length; i++) {
|
||||
videoSlots.value[i] = null;
|
||||
}
|
||||
} else if (currentVideo.value) {
|
||||
// 中间没有视频,显示左侧选中的
|
||||
videoSlots.value[0] = {
|
||||
flnm: currentVideo.value.stnm,
|
||||
stnm: currentVideo.value.stnm,
|
||||
url: currentVideo.value.url,
|
||||
key: currentVideo.value.stcd,
|
||||
...currentVideo.value
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听日期范围变化,重新加载录像列表
|
||||
watch(dateRange, () => {
|
||||
currentPage.value = 1;
|
||||
loadRecordList();
|
||||
});
|
||||
|
||||
// 监听页码变化,重新加载录像列表
|
||||
watch(currentPage, () => {
|
||||
loadRecordList();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
initData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.video-info-container {
|
||||
width: 100%;
|
||||
height: 630px;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
|
||||
.video-info-row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 左侧面板
|
||||
.left-panel {
|
||||
height: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.panel-header {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
:deep(.ant-divider) {
|
||||
margin-right: 6px;
|
||||
height: 1.2em !important;
|
||||
border-left: 3px solid rgb(47, 107, 152) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
|
||||
.list-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #005293;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中间面板
|
||||
.center-panel {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.video-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
&.layout-1 {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&.layout-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&.layout-9 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.video-item {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧面板
|
||||
.right-panel {
|
||||
height: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.panel-header {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:deep(.ant-divider) {
|
||||
margin-right: 6px;
|
||||
height: 1.2em !important;
|
||||
border-left: 3px solid rgb(47, 107, 152) !important;
|
||||
}
|
||||
.layout-switcher {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
|
||||
:deep(.ant-radio-group) {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
|
||||
.empty-record {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.record-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
.record-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.record-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-bottom: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
:deep(.ant-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: rgba(12, 51, 88, 0.8);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
i {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-info {
|
||||
height: 50px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
.record-name {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
padding: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,48 +1,108 @@
|
||||
<template>
|
||||
<a-modal :open="visible" :title="title" width="80vw" :footer="null" :closable="true" @cancel="handleClose"
|
||||
:destroyOnClose="true" class="map-modal">
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="title"
|
||||
width="80vw"
|
||||
:footer="null"
|
||||
:closable="true"
|
||||
@cancel="handleClose"
|
||||
:destroyOnClose="true"
|
||||
class="map-modal"
|
||||
>
|
||||
<div v-if="modelStore.showStcdSelector" class="stcd-selector-wrapper">
|
||||
<a-select v-model:value="modelStore.params.stcd" placeholder="请选择测站" style="width: 240px; margin-bottom: 12px;"
|
||||
:options="modelStore.stcdOptions" show-search :filter-option="filterOption" @change="handleStcdChange">
|
||||
<a-select
|
||||
v-model:value="modelStore.params.stcd"
|
||||
placeholder="请选择测站"
|
||||
style="width: 240px; margin-bottom: 12px"
|
||||
:options="modelStore.stcdOptions"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
@change="handleStcdChange"
|
||||
>
|
||||
</a-select>
|
||||
</div>
|
||||
<a-tabs :active-key="currentActiveKey" @change="onTabChange">
|
||||
<a-tab-pane v-for="tab in tabsConfig" :key="tab.key" :tab="tab.name">
|
||||
<div class="content">
|
||||
<!-- 基本信息组件 -->
|
||||
<BasicInfo v-if="currentActiveKey === 'basicInfo'" :url="tab.url" />
|
||||
<!-- 水温-监测数据 -->
|
||||
<WaterTemperature v-if="currentActiveKey === 'WaterTemperature'" />
|
||||
<!-- 水温-鱼类繁殖适宜性分析 -->
|
||||
<WaterTemperatureContrast v-if="currentActiveKey === 'WaterTemperatureRep'" />
|
||||
<!-- 地图组件 -->
|
||||
<!-- <MapView v-else-if="currentActiveKey === 'mapView'" :data="modalData" /> -->
|
||||
<!-- 周边配套组件 -->
|
||||
<!-- <SurroundingInfo
|
||||
v-else-if="currentActiveKey === 'surrounding'"
|
||||
:data="modalData"
|
||||
/> -->
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<template #rightExtra>
|
||||
<a-tooltip :title="!isEngConfig ? '' : '该电站无专题配置'">
|
||||
<a-button type="primary" :disabled="isEngConfig">
|
||||
<i class="icon iconfont icon-topic mr-[5px]"></i>
|
||||
电站专题
|
||||
</a-button></a-tooltip>
|
||||
</a-button></a-tooltip
|
||||
>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<!-- 内容区域 - 使用动态组件 + keep-alive 实现按需加载和缓存 -->
|
||||
<keep-alive>
|
||||
<div class="content">
|
||||
<!-- 基本信息 -->
|
||||
<BasicInfo
|
||||
v-show="currentActiveKey === 'basicInfo'"
|
||||
:url="getTabUrl('basicInfo')"
|
||||
:is-active="currentActiveKey === 'basicInfo'"
|
||||
/>
|
||||
<!-- 实时视频 -->
|
||||
<VideoInfo
|
||||
v-show="currentActiveKey === 'videoInfo'"
|
||||
:is-active="currentActiveKey === 'videoInfo'"
|
||||
/>
|
||||
<!-- 全景影像 -->
|
||||
<PanoramaInfo
|
||||
v-show="currentActiveKey === 'panoramaInfo'"
|
||||
:url="getTabUrl('panoramaInfo')"
|
||||
:is-active="currentActiveKey === 'panoramaInfo'"
|
||||
/>
|
||||
<!-- 监测数据 -->
|
||||
<MonitorInfo
|
||||
v-show="currentActiveKey === 'monitorInfo'"
|
||||
:is-active="currentActiveKey === 'monitorInfo'"
|
||||
:code="getTabCode('monitorInfo')"
|
||||
/>
|
||||
<!-- 预警提示 -->
|
||||
<EarlyWarningAlert
|
||||
v-show="currentActiveKey === 'tableTabs'"
|
||||
:tabs-items="getTabChildren('tableTabs')"
|
||||
:is-active="currentActiveKey === 'tableTabs'"
|
||||
/>
|
||||
<!-- 鱼类繁殖适宜性分析 -->
|
||||
<WaterTemperatureRep
|
||||
v-show="currentActiveKey === 'WaterTemperatureRep'"
|
||||
:is-active="currentActiveKey === 'WaterTemperatureRep'"
|
||||
/>
|
||||
<!-- 查看报告 -->
|
||||
<Attachment
|
||||
v-show="currentActiveKey === 'attachment'"
|
||||
:is-active="currentActiveKey === 'attachment'"
|
||||
tabKey="attachment"
|
||||
:show-download="false"
|
||||
:url="getTabUrl('attachment')"
|
||||
/>
|
||||
<!-- 批复文件 -->
|
||||
<Attachment
|
||||
v-show="currentActiveKey === 'approval'"
|
||||
:is-active="currentActiveKey === 'approval'"
|
||||
tabKey="approval"
|
||||
:show-download="true"
|
||||
:url="getTabUrl('approval')"
|
||||
/>
|
||||
</div>
|
||||
</keep-alive>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, reactive, computed } from 'vue';
|
||||
// 导入预定义的 Tab 内容组件
|
||||
import BasicInfo from "./components/BasicInfo.vue";
|
||||
import WaterTemperature from "./components/WaterTemperature.vue";
|
||||
import WaterTemperatureContrast from "./components/WaterTemperatureContrast.vue";
|
||||
import { useModelStore } from "@/store/modules/model";
|
||||
import { handleTabs } from "./setting.config";
|
||||
import BasicInfo from './components/BasicInfo.vue'; // 基本信息
|
||||
import VideoInfo from './components/videoInfo.vue'; // 实时视频
|
||||
import PanoramaInfo from './components/PanoramaInfo.vue'; // 全景影像
|
||||
import MonitorInfo from './components/MonitorInfo.vue'; // 监测数据
|
||||
import EarlyWarningAlert from './components/EarlyWarningAlert.vue'; // 预警提示
|
||||
import Attachment from './components/Attachment.vue'; // 查看报告/批复文件
|
||||
import WaterTemperatureRep from './components/WaterTemperatureRep.vue'; // 水温对比数据
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { handleTabs } from './setting.config';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const tabsConfig = ref([]);
|
||||
@ -57,7 +117,22 @@ interface TabItem {
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 获取 Tab 配置项的 URL
|
||||
const getTabUrl = (key: string) => {
|
||||
const tab = tabsConfig.value.find((item: any) => item.key === key);
|
||||
return tab?.url || '';
|
||||
};
|
||||
// 获取 Tab 配置项的子 Tab
|
||||
const getTabChildren = (key: string) => {
|
||||
console.log(tabsConfig.value);
|
||||
const tab = tabsConfig.value.find((item: any) => item.key === key);
|
||||
return tab?.tabs || [];
|
||||
};
|
||||
// 获取 Tab 配置项的 code
|
||||
const getTabCode = (key: string) => {
|
||||
const tab = tabsConfig.value.find((item: any) => item.key === key);
|
||||
return tab?.code || '';
|
||||
};
|
||||
// 定义 Props
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@ -69,17 +144,17 @@ const props = defineProps<{
|
||||
// 定义 Emits
|
||||
// 添加 'update:activeKey' 以支持 v-model:active-key
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "change", key: string): void;
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'change', key: string): void;
|
||||
}>();
|
||||
|
||||
// 内部维护的 activeKey
|
||||
const currentActiveKey = ref<string>("");
|
||||
const currentActiveKey = ref<string>('');
|
||||
|
||||
// 监听外部传入的 activeKey 变化,同步到内部状态
|
||||
watch(
|
||||
() => props.activeKey,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
if (newVal && newVal !== currentActiveKey.value) {
|
||||
currentActiveKey.value = newVal;
|
||||
}
|
||||
@ -88,7 +163,8 @@ watch(
|
||||
);
|
||||
watch(
|
||||
() => modelStore.params,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
if (!newVal || Object.keys(newVal).length === 0) return;
|
||||
console.log(newVal);
|
||||
tabsConfig.value = handleTabs(newVal);
|
||||
let value = tabsConfig.value.find((item: any) => item.default);
|
||||
@ -101,12 +177,13 @@ const onTabChange = (key: string) => {
|
||||
currentActiveKey.value = key;
|
||||
// 通知父组件更新 activeKey (支持 v-model)
|
||||
// 通知父组件发生了切换事件 (用于业务逻辑,如加载数据)
|
||||
emit("change", key);
|
||||
emit('change', key);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit("update:visible", false);
|
||||
emit('update:visible', false);
|
||||
modelStore.params = {};
|
||||
// 清理状态,避免影响下次打开
|
||||
modelStore.showStcdSelector = false;
|
||||
modelStore.stcdOptions = [];
|
||||
@ -116,7 +193,7 @@ const handleClose = () => {
|
||||
const modalData = ref(props.data);
|
||||
watch(
|
||||
() => props.data,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
modalData.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
@ -20,12 +20,12 @@ const ENGTabs: Array<any> = [
|
||||
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
|
||||
default: true // 默认显示
|
||||
},
|
||||
{
|
||||
name: '阶段属性',
|
||||
key: 'basicFilter',
|
||||
type: 'basicFilter',
|
||||
url: '/eng/engBasisInfo/getEngBaseInfo'
|
||||
},
|
||||
// {
|
||||
// name: '阶段属性',
|
||||
// key: 'basicFilter',
|
||||
// type: 'basicFilter',
|
||||
// url: '/eng/engBasisInfo/getEngBaseInfo'
|
||||
// },
|
||||
{
|
||||
name: '实时视频',
|
||||
key: 'videoInfo',
|
||||
@ -92,12 +92,14 @@ const ENGTabs: Array<any> = [
|
||||
{
|
||||
name: '查看报告',
|
||||
key: 'attachment',
|
||||
type: 'attachment'
|
||||
type: 'attachment',
|
||||
url: '/eq/fid/GetKendoListCust'
|
||||
},
|
||||
{
|
||||
name: '批复文件',
|
||||
key: 'approval',
|
||||
type: 'approval'
|
||||
type: 'approval',
|
||||
url: '/eng/base/eiaapproval/GetKendoList'
|
||||
}
|
||||
// {
|
||||
// name: "特征曲线",
|
||||
@ -296,7 +298,7 @@ const FPTabs: Array<any> =
|
||||
// type: 'AIYXSB',
|
||||
// }
|
||||
// ]
|
||||
// :
|
||||
// :
|
||||
[
|
||||
{
|
||||
name: '基础信息',
|
||||
@ -333,7 +335,7 @@ const FPTabs: Array<any> =
|
||||
name: '运行情况',
|
||||
key: 'FishFacilityRunState', //Normal1
|
||||
type: 'FishFacilityRunState', //Normal
|
||||
code: 'gyss.tabs.yxqk',
|
||||
code: 'gyss.tabs.yxqk'
|
||||
// tabs: [
|
||||
// {
|
||||
// name: '',
|
||||
@ -349,9 +351,9 @@ const FPTabs: Array<any> =
|
||||
name: '综合分析',
|
||||
key: 'FishZHFX',
|
||||
type: 'FishZHFX',
|
||||
code: "gyss.tabs.zhfx"
|
||||
},
|
||||
]
|
||||
code: 'gyss.tabs.zhfx'
|
||||
}
|
||||
];
|
||||
|
||||
// // 过鱼设施在建状态
|
||||
// const FPTabs1: Array<any> = [
|
||||
@ -405,7 +407,7 @@ const FBTabs: Array<any> = [
|
||||
name: '过程图',
|
||||
key: 'ProcessDiagram',
|
||||
type: 'ProcessDiagram',
|
||||
code: "ylzzz.tabs.gct"
|
||||
code: 'ylzzz.tabs.gct'
|
||||
},
|
||||
{
|
||||
name: '增殖放流情况',
|
||||
@ -457,7 +459,7 @@ const FBTabs: Array<any> = [
|
||||
// key: "panoramaInfo",
|
||||
// type: "panorama"
|
||||
// },
|
||||
]
|
||||
];
|
||||
|
||||
// const FBTabs1: Array<any> = [
|
||||
// {
|
||||
@ -590,7 +592,7 @@ const FHTabs: Array<any> = [
|
||||
{
|
||||
name: '流量监测',
|
||||
key: 'FlowMeasure',
|
||||
type: 'FlowMeasure',
|
||||
type: 'FlowMeasure'
|
||||
// tabs: [
|
||||
// {
|
||||
// url: '/wmp-eng-server/eng/river/GetKendoListCust',
|
||||
@ -622,7 +624,7 @@ const FHTabs: Array<any> = [
|
||||
// key: "panoramaInfo",
|
||||
// type: "panorama"
|
||||
// }
|
||||
]
|
||||
];
|
||||
// const ZQTabs: Array<any> = [
|
||||
// {
|
||||
// name: '基础信息',
|
||||
@ -675,7 +677,7 @@ const WQFBTabs: Array<any> = [
|
||||
type: 'video',
|
||||
url: '/video/dataStcdFrame/getVideoMonitorList'
|
||||
}
|
||||
]
|
||||
];
|
||||
// const WQTabs: Array<any> = [
|
||||
// ...WQFBTabs,
|
||||
// {
|
||||
@ -723,7 +725,7 @@ const FLOWTabs: Array<any> = [
|
||||
key: 'FlowMeasure',
|
||||
type: 'FlowMeasure'
|
||||
}
|
||||
]
|
||||
];
|
||||
// //水温监测站 √
|
||||
const WTTabs: Array<any> = [
|
||||
{
|
||||
@ -748,9 +750,9 @@ const WTTabs: Array<any> = [
|
||||
name: '鱼类繁殖适宜性分析',
|
||||
key: 'WaterTemperatureRep',
|
||||
type: 'WaterTemperatureRep',
|
||||
code: 'swjc.tabs.ylfzsyxfx',
|
||||
code: 'swjc.tabs.ylfzsyxfx'
|
||||
}
|
||||
]
|
||||
];
|
||||
// //垂向水温 √
|
||||
// const WTTabs1: Array<any> = [
|
||||
// {
|
||||
@ -774,7 +776,7 @@ const DWTabs: Array<any> = [
|
||||
type: 'basic',
|
||||
default: true, // 默认显示
|
||||
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
|
||||
},
|
||||
}
|
||||
// {
|
||||
// name: '实时视频',
|
||||
// key: 'videoInfo',
|
||||
@ -791,7 +793,7 @@ const DWTabs: Array<any> = [
|
||||
// key: 'AIYXSB',
|
||||
// type: 'AIYXSB',
|
||||
// }
|
||||
]
|
||||
];
|
||||
// //低温水减缓设施这是旧的
|
||||
// const DWTabs2: Array<any> = [
|
||||
// {
|
||||
@ -834,7 +836,7 @@ const WETabs: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
{
|
||||
name: '监测数据',
|
||||
@ -845,7 +847,7 @@ const WETabs: Array<any> = [
|
||||
name: '调查鱼类',
|
||||
type: 'table',
|
||||
key: 'weFish',
|
||||
// tableUrl: getUrl('/wmp-env-server/env/we/fisht/GetKendoListCust'),
|
||||
// tableUrl: getUrl('/wmp-env-server/env/we/fisht/GetKendoListCust'),
|
||||
hiddenChart: true,
|
||||
// filter: NormalWEFilter,
|
||||
filterProps: {
|
||||
@ -887,7 +889,7 @@ const WETabs: Array<any> = [
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
];
|
||||
//鱼类调查装置
|
||||
const DEVICETABS: Array<any> = [
|
||||
{
|
||||
@ -895,9 +897,9 @@ const DEVICETABS: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
|
||||
default: true, // 默认显示
|
||||
},
|
||||
]
|
||||
default: true // 默认显示
|
||||
}
|
||||
];
|
||||
// //电站告警
|
||||
const DZGJ: Array<any> = [
|
||||
{
|
||||
@ -905,7 +907,7 @@ const DZGJ: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
{
|
||||
name: '监测数据',
|
||||
@ -958,7 +960,7 @@ const DZGJ: Array<any> = [
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
//珍稀植物园
|
||||
const VPTabs: Array<any> = [
|
||||
@ -967,7 +969,7 @@ const VPTabs: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
// {
|
||||
// name: '实时视频',
|
||||
@ -1011,7 +1013,7 @@ const VPTabs: Array<any> = [
|
||||
// type: "FishSystemRunState", //Normal
|
||||
// tabs: []
|
||||
// }
|
||||
]
|
||||
];
|
||||
// //视频站
|
||||
const videoTabs: Array<any> = [
|
||||
{
|
||||
@ -1019,7 +1021,7 @@ const videoTabs: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
{
|
||||
name: '实时视频',
|
||||
@ -1027,7 +1029,7 @@ const videoTabs: Array<any> = [
|
||||
type: 'video',
|
||||
url: '/video/dataStcdFrame/getVideoMonitorList'
|
||||
}
|
||||
]
|
||||
];
|
||||
// //视频站
|
||||
// const noLiveVideoTabs: Array<any> = [
|
||||
// {
|
||||
@ -1050,7 +1052,7 @@ const VATabs: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
// {
|
||||
// name: '实时视频',
|
||||
@ -1076,7 +1078,7 @@ const VATabs: Array<any> = [
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
// // //河道水情站
|
||||
// // const hdsqz: Array<any> = [
|
||||
@ -1177,20 +1179,19 @@ const WATabs: Array<any> = [
|
||||
key: 'basicInfo',
|
||||
type: 'basic',
|
||||
url: '/bbi/siteBipc/getSiteBasicInfo',
|
||||
default: true, // 默认显示
|
||||
default: true // 默认显示
|
||||
},
|
||||
{
|
||||
name: '现场录像',
|
||||
key: 'liveFootage',
|
||||
type: 'liveFootage',
|
||||
type: 'liveFootage'
|
||||
},
|
||||
{
|
||||
name: '现场图片',
|
||||
key: 'onSitePhoto',
|
||||
type: 'onSitePhoto',
|
||||
},
|
||||
|
||||
]
|
||||
type: 'onSitePhoto'
|
||||
}
|
||||
];
|
||||
// //mway : 1-是人工 , 2-是自动 dtinType: 0-自建 1-国家 2-人工
|
||||
const handleTabs = (modaldata: any) => {
|
||||
if (!modaldata?.sttp) return;
|
||||
@ -1206,7 +1207,7 @@ const handleTabs = (modaldata: any) => {
|
||||
case 'FH_POINT':
|
||||
if (modaldata.show) {
|
||||
return FHTabs;
|
||||
}else{
|
||||
} else {
|
||||
let FHTabsTwo = FHTabs.slice(0, 1);
|
||||
return FHTabsTwo;
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ declare global {
|
||||
__mapMode?: string; // 建议同时声明代码中用到的其他全局变量
|
||||
}
|
||||
}
|
||||
console.log(import.meta.env.VITE_APP_PREVIEW_URL);
|
||||
console.log(import.meta.env.VITE_APP_ATTACHMENT_URL);
|
||||
|
||||
/**
|
||||
* 根据镜头高度获取地图级别
|
||||
@ -60,11 +60,7 @@ export const legendData2Obj = (data: any[]) => {
|
||||
* @param index - item的索引
|
||||
* @param labelType - 标签类型
|
||||
*/
|
||||
export const appendOffsetPropties = (
|
||||
item: any,
|
||||
index: any,
|
||||
labelType = 2
|
||||
) => {
|
||||
export const appendOffsetPropties = (item: any, index: any, labelType = 2) => {
|
||||
let drawDotImg = null;
|
||||
let offset = null;
|
||||
if (labelType == 2) {
|
||||
|
||||
@ -144,6 +144,7 @@ export class MapOl implements MapInterface {
|
||||
|
||||
this.view.on('change:resolution', () => {});
|
||||
this.map.on('click', evt => {
|
||||
this.popupManager.showPopup(undefined, undefined);
|
||||
this.popupManager.handleMapClick(evt.pixel, detectedFeature => {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = detectedFeature.values_;
|
||||
|
||||
51
frontend/src/components/pdf/pdfView.vue
Normal file
51
frontend/src/components/pdf/pdfView.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="pdf-view-container">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="iframeUrl"
|
||||
class="pdf-iframe"
|
||||
frameborder="0"
|
||||
:title="fileId"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
fileId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
const iframeUrl = computed(() => {
|
||||
if (!props.fileId) return '';
|
||||
return (
|
||||
import.meta.env.VITE_APP_ATTACHMENT_URL +
|
||||
`/ShowDocument?fileKey=${props.fileId}&backgroundColor=transparent`
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pdf-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.pdf-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,65 +1,82 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="设施类型介绍">
|
||||
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<div class="carousel-track" :class="{ 'no-transition': isTransitioning }"
|
||||
:style="{ transform: `translateX(-${currentIndex * 100}%)` }">
|
||||
<!-- 遍历所有媒体项(包含克隆项) -->
|
||||
<div v-for="(item, index) in renderMediaData" :key="index" class="carousel-item"
|
||||
@click="handleItemClick(originalMediaData)">
|
||||
<!-- 图片 -->
|
||||
<img v-if="item.url" :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<!-- <div class="text">{{ item.text }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span v-for="(dot, index) in originalMediaData" :key="index" class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }" @click="goToSlide(index)"></span>
|
||||
</div>
|
||||
<SidePanelItem title="设施类型介绍">
|
||||
<div
|
||||
class="container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<div
|
||||
class="carousel-track"
|
||||
:class="{ 'no-transition': isTransitioning }"
|
||||
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
|
||||
>
|
||||
<!-- 遍历所有媒体项(包含克隆项) -->
|
||||
<div
|
||||
v-for="(item, index) in renderMediaData"
|
||||
:key="index"
|
||||
class="carousel-item"
|
||||
@click="handleItemClick(originalMediaData)"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<img v-if="item.url" :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<!-- <div class="text">{{ item.text }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
|
||||
<div class="description-text" @click="handleItemClick(originalMediaData)">
|
||||
{{ currentDescription }}
|
||||
</div>
|
||||
|
||||
</SidePanelItem>
|
||||
<!-- 设施详情弹框 -->
|
||||
<a-modal v-model:open="modalVisible" :title="'设施类型介绍'" width="1536px" :footer="null">
|
||||
<!-- 你在这里编写弹框内容 -->
|
||||
<div v-if="currentItem">
|
||||
<ArtsDetail :dataSource='currentItem' :index="getCurrentRealIndex()" />
|
||||
</div>
|
||||
</a-modal>
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span
|
||||
v-for="(dot, index) in originalMediaData"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }"
|
||||
@click="goToSlide(index)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
|
||||
<div class="description-text" @click="handleItemClick(originalMediaData)">
|
||||
{{ currentDescription }}
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
<!-- 设施详情弹框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="'设施类型介绍'"
|
||||
width="1536px"
|
||||
:footer="null"
|
||||
>
|
||||
<!-- 你在这里编写弹框内容 -->
|
||||
<div v-if="currentItem">
|
||||
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
|
||||
import { sttpbGetKendoList } from "@/api/sw";
|
||||
import { sttpbGetKendoList } from '@/api/sw';
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'diwenshuijianhuansheshileixingzuchengjijieruqingkuang'
|
||||
name: 'diwenshuijianhuansheshileixingzuchengjijieruqingkuang'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 原始媒体数据(3条测试数据)
|
||||
const originalMediaData = ref<MediaItem[]>([
|
||||
|
||||
]);
|
||||
const originalMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
||||
@ -81,252 +98,250 @@ const modalVisible = ref(false);
|
||||
const currentItem = ref<any>(null);
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
};
|
||||
|
||||
// 启动自动轮播
|
||||
const startAutoPlay = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 计算当前显示的描述文字
|
||||
const currentDescription = computed(() => {
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
const getListData = async () => {
|
||||
try {
|
||||
const params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
{
|
||||
"field": "fullPath",
|
||||
"operator": "contains",
|
||||
"dataType": "string",
|
||||
"value": "DW,DW"
|
||||
},
|
||||
{
|
||||
"field": "logo",
|
||||
"operator": "isnotnull",
|
||||
"dataType": "string"
|
||||
},
|
||||
{
|
||||
"field": "introduce",
|
||||
"operator": "isnotnull",
|
||||
"dataType": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
let res = await sttpbGetKendoList(params);
|
||||
let data = res?.data?.data || res?.data || [];
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'fullPath',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: 'DW,DW'
|
||||
},
|
||||
{
|
||||
field: 'logo',
|
||||
operator: 'isnotnull',
|
||||
dataType: 'string'
|
||||
},
|
||||
{
|
||||
field: 'introduce',
|
||||
operator: 'isnotnull',
|
||||
dataType: 'string'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
let res = await sttpbGetKendoList(params);
|
||||
let data = res?.data?.data || res?.data || [];
|
||||
|
||||
// 将接口返回的数据转换为轮播图格式
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
originalMediaData.value = data.map((item: any) => ({
|
||||
text: item.sttpName,
|
||||
description: item.introduce,
|
||||
url: item.logo ? `${baseUrl}/?${item.logo}` : ''
|
||||
}));
|
||||
console.log(originalMediaData.value);
|
||||
} catch (error) {
|
||||
|
||||
originalMediaData.value = [];
|
||||
} finally {
|
||||
|
||||
}
|
||||
// 将接口返回的数据转换为轮播图格式
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
originalMediaData.value = data.map((item: any) => ({
|
||||
text: item.sttpName,
|
||||
description: item.introduce,
|
||||
url: item.logo ? `${baseUrl}/?${item.logo}` : ''
|
||||
}));
|
||||
console.log(originalMediaData.value);
|
||||
} catch (error) {
|
||||
originalMediaData.value = [];
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理项目点击事件
|
||||
const handleItemClick = (item: any) => {
|
||||
console.log('轮播图项目被点击:', item,getCurrentRealIndex());
|
||||
currentItem.value = item;
|
||||
modalVisible.value = true;
|
||||
console.log('轮播图项目被点击:', item, getCurrentRealIndex());
|
||||
currentItem.value = item;
|
||||
modalVisible.value = true;
|
||||
};
|
||||
// 页面加载时执行
|
||||
onMounted(async () => {
|
||||
// 先获取数据
|
||||
await getListData();
|
||||
// 数据加载完成后初始化渲染数组
|
||||
initRenderData();
|
||||
// 最后启动自动轮播
|
||||
startAutoPlay();
|
||||
// 先获取数据
|
||||
await getListData();
|
||||
// 数据加载完成后初始化渲染数组
|
||||
initRenderData();
|
||||
// 最后启动自动轮播
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
// 获取当前真实的索引(排除克隆项的影响)
|
||||
const getCurrentRealIndex = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
|
||||
return realIndex;
|
||||
return realIndex;
|
||||
};
|
||||
|
||||
// 跳转到指定幻灯片
|
||||
const goToSlide = (targetIndex: number) => {
|
||||
if (isTransitioning.value) return;
|
||||
if (isTransitioning.value) return;
|
||||
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.pagination-dots-fixed {
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 10; // 确保在文字上方
|
||||
|
||||
.dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: #D8D8D8;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&.active {
|
||||
background-color: #005293;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.pagination-dots-fixed {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 10; // 确保在文字上方
|
||||
|
||||
.dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: #d8d8d8;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&.active {
|
||||
background-color: #005293;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 独立的文字说明区域
|
||||
.description-text {
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div class="live-video-box" :style="{ height }">
|
||||
<div
|
||||
class="live-video-box"
|
||||
:class="{ 'no-title': !showTitle }"
|
||||
:style="{ height }"
|
||||
>
|
||||
<!-- 有视频时显示播放器 -->
|
||||
<div v-if="videoUrl" class="video-container">
|
||||
<!-- 视频标题和关闭按钮 -->
|
||||
<div class="live-video-title">
|
||||
<div v-if="showTitle" class="live-video-title">
|
||||
{{ videoData?.flnm }}
|
||||
<div class="title-actions">
|
||||
<CloseOutlined @click="handleClose" />
|
||||
@ -21,11 +25,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from "vue";
|
||||
import { CloseOutlined } from "@ant-design/icons-vue";
|
||||
import videojs from "video.js";
|
||||
import "video.js/dist/video-js.css";
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
import { CloseOutlined } from '@ant-design/icons-vue';
|
||||
import videojs from 'video.js';
|
||||
import 'video.js/dist/video-js.css';
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
|
||||
// 注册 HLS 支持(video.js 8.x 内置了 HLS 支持)
|
||||
|
||||
@ -37,11 +41,17 @@ interface VideoData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
videoData?: VideoData | null;
|
||||
height?: string;
|
||||
monitorType?: "live" | "record"; // 监控类型:live-实时视频, record-录像
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoData?: VideoData | null;
|
||||
height?: string;
|
||||
monitorType?: 'live' | 'record'; // 监控类型:live-实时视频, record-录像
|
||||
showTitle?: boolean; // 是否显示标题栏
|
||||
}>(),
|
||||
{
|
||||
showTitle: true
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [nodeKey?: string];
|
||||
@ -53,7 +63,7 @@ let player: any = null;
|
||||
|
||||
// 获取第一个有效的URL
|
||||
const videoUrl = computed(() => {
|
||||
if (!props.videoData) return "";
|
||||
if (!props.videoData) return '';
|
||||
|
||||
// 优先使用url字段
|
||||
if (props.videoData.url) return props.videoData.url;
|
||||
@ -67,7 +77,7 @@ const videoUrl = computed(() => {
|
||||
return props.videoData.urls[0];
|
||||
}
|
||||
|
||||
return "";
|
||||
return '';
|
||||
});
|
||||
|
||||
// 初始化 video.js 播放器
|
||||
@ -81,49 +91,49 @@ const initPlayer = () => {
|
||||
}
|
||||
try {
|
||||
// 创建 video 元素
|
||||
const videoElement = document.createElement("video");
|
||||
videoElement.className = "video-js vjs-default-skin vjs-big-play-centered";
|
||||
videoElement.setAttribute("id", playerId);
|
||||
videoElement.setAttribute("playsinline", "playsinline");
|
||||
videoElement.setAttribute("webkit-playsinline", "webkit-playsinline");
|
||||
const videoElement = document.createElement('video');
|
||||
videoElement.className = 'video-js vjs-default-skin vjs-big-play-centered';
|
||||
videoElement.setAttribute('id', playerId);
|
||||
videoElement.setAttribute('playsinline', 'playsinline');
|
||||
videoElement.setAttribute('webkit-playsinline', 'webkit-playsinline');
|
||||
|
||||
// 将 video 元素添加到容器
|
||||
const container = document.getElementById(playerId);
|
||||
if (container) {
|
||||
container.innerHTML = "";
|
||||
container.innerHTML = '';
|
||||
container.appendChild(videoElement);
|
||||
}
|
||||
|
||||
// 根据 monitorType 判断是否为直播流
|
||||
// live: 实时视频流(m3u8),显示直播UI
|
||||
// record: 录像文件,显示进度条
|
||||
const isLiveStream = props.monitorType === "live";
|
||||
const isLiveStream = props.monitorType === 'live';
|
||||
|
||||
// 根据 URL 后缀或监控类型确定视频 MIME Type
|
||||
const getVideoType = (url: string): string => {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
|
||||
// 优先根据 URL 后缀判断
|
||||
if (lowerUrl.includes(".m3u8")) {
|
||||
return "application/x-mpegURL"; // HLS 流
|
||||
} else if (lowerUrl.includes(".mp4")) {
|
||||
return "video/mp4";
|
||||
} else if (lowerUrl.includes(".webm")) {
|
||||
return "video/webm";
|
||||
} else if (lowerUrl.includes(".ogg") || lowerUrl.includes(".ogv")) {
|
||||
return "video/ogg";
|
||||
} else if (lowerUrl.includes(".avi")) {
|
||||
return "video/x-msvideo";
|
||||
} else if (lowerUrl.includes(".mov")) {
|
||||
return "video/quicktime";
|
||||
} else if (lowerUrl.includes(".wmv")) {
|
||||
return "video/x-ms-wmv";
|
||||
} else if (lowerUrl.includes(".flv")) {
|
||||
return "video/x-flv";
|
||||
if (lowerUrl.includes('.m3u8')) {
|
||||
return 'application/x-mpegURL'; // HLS 流
|
||||
} else if (lowerUrl.includes('.mp4')) {
|
||||
return 'video/mp4';
|
||||
} else if (lowerUrl.includes('.webm')) {
|
||||
return 'video/webm';
|
||||
} else if (lowerUrl.includes('.ogg') || lowerUrl.includes('.ogv')) {
|
||||
return 'video/ogg';
|
||||
} else if (lowerUrl.includes('.avi')) {
|
||||
return 'video/x-msvideo';
|
||||
} else if (lowerUrl.includes('.mov')) {
|
||||
return 'video/quicktime';
|
||||
} else if (lowerUrl.includes('.wmv')) {
|
||||
return 'video/x-ms-wmv';
|
||||
} else if (lowerUrl.includes('.flv')) {
|
||||
return 'video/x-flv';
|
||||
}
|
||||
|
||||
// 如果 URL 中没有明确后缀,根据监控类型判断
|
||||
return isLiveStream ? "application/x-mpegURL" : "video/mp4";
|
||||
return isLiveStream ? 'application/x-mpegURL' : 'video/mp4';
|
||||
};
|
||||
|
||||
const videoType = getVideoType(videoUrl.value);
|
||||
@ -134,56 +144,56 @@ const initPlayer = () => {
|
||||
muted: true,
|
||||
controls: true,
|
||||
fluid: false,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
preload: "auto",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
preload: 'auto',
|
||||
playbackRates: [0.5, 1, 1.5, 2],
|
||||
liveui: isLiveStream, // 直播流显示直播UI
|
||||
// 录像模式下禁用直播相关功能
|
||||
inactivityTimeout: 0, // 禁用不活动超时
|
||||
controlBar: {
|
||||
children: [
|
||||
"playToggle",
|
||||
"volumePanel",
|
||||
"currentTimeDisplay",
|
||||
"timeDivider",
|
||||
"durationDisplay",
|
||||
"progressControl",
|
||||
"liveDisplay",
|
||||
"remainingTimeDisplay",
|
||||
"customControlSpacer",
|
||||
"playbackRateMenuButton",
|
||||
"fullscreenToggle",
|
||||
],
|
||||
'playToggle',
|
||||
'volumePanel',
|
||||
'currentTimeDisplay',
|
||||
'timeDivider',
|
||||
'durationDisplay',
|
||||
'progressControl',
|
||||
'liveDisplay',
|
||||
'remainingTimeDisplay',
|
||||
'customControlSpacer',
|
||||
'playbackRateMenuButton',
|
||||
'fullscreenToggle'
|
||||
]
|
||||
},
|
||||
html5: {
|
||||
hls: {
|
||||
enableLowInitialPlaylist: true,
|
||||
smoothQualityChange: true,
|
||||
},
|
||||
smoothQualityChange: true
|
||||
}
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
src: videoUrl.value,
|
||||
type: videoType, // 指定视频类型
|
||||
},
|
||||
],
|
||||
type: videoType // 指定视频类型
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 如果是录像模式,隐藏直播指示器
|
||||
if (!isLiveStream) {
|
||||
player.ready(() => {
|
||||
const liveDisplay = player.controlBar.getChild("liveDisplay");
|
||||
const liveDisplay = player.controlBar.getChild('liveDisplay');
|
||||
if (liveDisplay) {
|
||||
liveDisplay.el().style.display = "none";
|
||||
liveDisplay.el().style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听错误事件
|
||||
player.on("error", () => {
|
||||
player.on('error', () => {
|
||||
const error = player.error();
|
||||
console.error("播放器错误:", error);
|
||||
console.error('播放器错误:', error);
|
||||
|
||||
// 保持最后一帧画面,不显示错误提示
|
||||
// 在后台静默重试
|
||||
@ -197,7 +207,7 @@ const initPlayer = () => {
|
||||
try {
|
||||
player.src({
|
||||
src: videoUrl.value,
|
||||
type: videoType, // 指定视频类型
|
||||
type: videoType // 指定视频类型
|
||||
});
|
||||
player.load();
|
||||
player.play();
|
||||
@ -214,16 +224,16 @@ const initPlayer = () => {
|
||||
});
|
||||
|
||||
// 监听加载完成
|
||||
player.on("loadeddata", () => {});
|
||||
player.on('loadeddata', () => {});
|
||||
|
||||
// 监听播放状态
|
||||
player.on("play", () => {});
|
||||
player.on('play', () => {});
|
||||
|
||||
// 监听暂停
|
||||
player.on("pause", () => {});
|
||||
player.on('pause', () => {});
|
||||
|
||||
// 监听等待(缓冲)
|
||||
player.on("waiting", () => {});
|
||||
player.on('waiting', () => {});
|
||||
} catch (error) {
|
||||
// 静默处理错误
|
||||
}
|
||||
@ -232,7 +242,7 @@ const initPlayer = () => {
|
||||
// 关闭视频
|
||||
const handleClose = () => {
|
||||
// 传递节点的 key 给父组件
|
||||
emit("close", props.videoData?.key);
|
||||
emit('close', props.videoData?.key);
|
||||
};
|
||||
|
||||
// 监听视频URL变化
|
||||
@ -323,6 +333,12 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&.no-title {
|
||||
.video-container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
:list-url="statistics"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<template #bodyCell="{ column }">
|
||||
<!-- 判断列的 key,如果是 selection,就显示自定义内容 -->
|
||||
<span v-if="column.key === 'selection'">
|
||||
<Checkbox
|
||||
@ -32,12 +32,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 GuoYuDaoShuTongJiSearch from "./guoYuDaoShuTongJiSearch.vue";
|
||||
import { statistics, batchUrgeContent } from "@/api/guoYuSheShiShuJuTianBao";
|
||||
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 GuoYuDaoShuTongJiSearch from './guoYuDaoShuTongJiSearch.vue';
|
||||
import { statistics, batchUrgeContent } from '@/api/guoYuSheShiShuJuTianBao';
|
||||
const searchRef = ref();
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const detailTableRef = ref();
|
||||
@ -46,12 +46,13 @@ const batchData = ref([]);
|
||||
const tableDataList = ref<any[]>([]); // 用于存储当前表格数据,方便查找
|
||||
|
||||
// ✅ 1. 定义获取唯一 Key 的方法 (请根据实际数据字段调整,比如 id, stcd 等)
|
||||
const getUniqueKey = (item: any) => item.id || item.stcd || JSON.stringify(item);
|
||||
const getUniqueKey = (item: any) =>
|
||||
item.id || item.stcd || JSON.stringify(item);
|
||||
|
||||
// ✅ 2. 获取同一组的所有 Key (用于联动选中)
|
||||
const getGroupKeys = (currentRecord: any) => {
|
||||
const keys: string[] = [];
|
||||
tableDataList.value.forEach((item) => {
|
||||
tableDataList.value.forEach(item => {
|
||||
if (
|
||||
item.basinNames === currentRecord.basinNames &&
|
||||
item.stationNames === currentRecord.stationNames
|
||||
@ -69,9 +70,9 @@ const handleGroupSelect = (checked: boolean, groupKeys: string[]) => {
|
||||
const newSelectedKeys = new Set(selectedRowKeys.value);
|
||||
|
||||
if (checked) {
|
||||
groupKeys.forEach((key) => newSelectedKeys.add(key));
|
||||
groupKeys.forEach(key => newSelectedKeys.add(key));
|
||||
} else {
|
||||
groupKeys.forEach((key) => newSelectedKeys.delete(key));
|
||||
groupKeys.forEach(key => newSelectedKeys.delete(key));
|
||||
}
|
||||
|
||||
selectedRowKeys.value = Array.from(newSelectedKeys);
|
||||
@ -85,8 +86,8 @@ const headerCheckboxState = computed(() => {
|
||||
}
|
||||
|
||||
// 1. 只获取【有效行】(hasData != 1)的 Key
|
||||
const validItems = tableDataList.value.filter((item) => item.hasData != 1);
|
||||
const validKeys = validItems.map((item) => getUniqueKey(item));
|
||||
const validItems = tableDataList.value.filter(item => item.hasData != 1);
|
||||
const validKeys = validItems.map(item => getUniqueKey(item));
|
||||
|
||||
// 如果没有有效行,直接返回未选中
|
||||
if (validKeys.length === 0) {
|
||||
@ -94,8 +95,9 @@ const headerCheckboxState = computed(() => {
|
||||
}
|
||||
|
||||
// 2. 计算有多少【有效行】被选中
|
||||
const selectedCount = validKeys.filter((key) => selectedRowKeys.value.includes(key))
|
||||
.length;
|
||||
const selectedCount = validKeys.filter(key =>
|
||||
selectedRowKeys.value.includes(key)
|
||||
).length;
|
||||
|
||||
// 3. 判断状态
|
||||
const isAllSelected = selectedCount === validKeys.length;
|
||||
@ -103,12 +105,12 @@ const headerCheckboxState = computed(() => {
|
||||
|
||||
return {
|
||||
checked: isAllSelected,
|
||||
indeterminate: isIndeterminate,
|
||||
indeterminate: isIndeterminate
|
||||
};
|
||||
});
|
||||
const updateBatchData = () => {
|
||||
// 从 tableDataList 中找出所有 key 在 selectedRowKeys 中的完整对象
|
||||
batchData.value = tableDataList.value.filter((item) =>
|
||||
batchData.value = tableDataList.value.filter(item =>
|
||||
selectedRowKeys.value.includes(getUniqueKey(item))
|
||||
);
|
||||
};
|
||||
@ -117,8 +119,8 @@ 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));
|
||||
const validItems = tableDataList.value.filter(item => item.hasData != 1);
|
||||
const allKeys = validItems.map(item => getUniqueKey(item));
|
||||
selectedRowKeys.value = allKeys;
|
||||
} else {
|
||||
// 清空所有选中
|
||||
@ -129,14 +131,14 @@ const handleHeaderSelectAll = (e: any) => {
|
||||
|
||||
let columns = ref([
|
||||
{
|
||||
title: "选择",
|
||||
key: "selection",
|
||||
title: '选择',
|
||||
key: 'selection',
|
||||
width: 60,
|
||||
align: "center",
|
||||
align: 'center',
|
||||
// ✅ 合并选择列
|
||||
customCell: (record) => {
|
||||
customCell: record => {
|
||||
return {
|
||||
rowSpan: record._selectionRowSpan || 0,
|
||||
rowSpan: record._selectionRowSpan || 0
|
||||
};
|
||||
},
|
||||
// ✅ 使用 h 函数渲染 Checkbox
|
||||
@ -148,8 +150,8 @@ let columns = ref([
|
||||
// 判断状态
|
||||
const isAllSelected =
|
||||
groupKeys.length > 0 &&
|
||||
groupKeys.every((key) => selectedRowKeys.value.includes(key));
|
||||
const isPartiallySelected = groupKeys.some((key) =>
|
||||
groupKeys.every(key => selectedRowKeys.value.includes(key));
|
||||
const isPartiallySelected = groupKeys.some(key =>
|
||||
selectedRowKeys.value.includes(key)
|
||||
);
|
||||
|
||||
@ -157,33 +159,33 @@ let columns = ref([
|
||||
checked: isAllSelected,
|
||||
disabled: record.hasData == 1,
|
||||
indeterminate: isPartiallySelected && !isAllSelected,
|
||||
onChange: (e: any) => handleGroupSelect(e.target.checked, groupKeys),
|
||||
onChange: (e: any) => handleGroupSelect(e.target.checked, groupKeys)
|
||||
});
|
||||
}
|
||||
return null; // 其他行不渲染
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: "basinNames",
|
||||
key: "basinNames",
|
||||
title: "流域",
|
||||
dataIndex: 'basinNames',
|
||||
key: 'basinNames',
|
||||
title: '流域',
|
||||
width: 160,
|
||||
customCell: (record, index) => {
|
||||
return {
|
||||
rowSpan: record._basinRowSpan || 0,
|
||||
rowSpan: record._basinRowSpan || 0
|
||||
};
|
||||
},
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
|
||||
const str = String(text);
|
||||
// 按逗号分割(支持中文逗号和英文逗号)
|
||||
const items = str.split(/[,,]/);
|
||||
|
||||
// 如果数量超过6个,截取前6个并添加省略号
|
||||
let displayText = "";
|
||||
let displayText = '';
|
||||
if (items.length > 6) {
|
||||
displayText = items.slice(0, 6).join(",") + " ...";
|
||||
displayText = items.slice(0, 6).join(',') + ' ...';
|
||||
} else {
|
||||
displayText = str;
|
||||
}
|
||||
@ -193,34 +195,34 @@ let columns = ref([
|
||||
Tooltip,
|
||||
{
|
||||
title: str, // Tooltip 显示的全部内容
|
||||
placement: "topLeft", // 可选:提示框位置
|
||||
placement: 'topLeft' // 可选:提示框位置
|
||||
},
|
||||
() => h("span", displayText)
|
||||
() => h('span', displayText)
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: "stationNames",
|
||||
key: "stationNames",
|
||||
title: "电站名称",
|
||||
dataIndex: 'stationNames',
|
||||
key: 'stationNames',
|
||||
title: '电站名称',
|
||||
width: 160,
|
||||
customCell: (record, index) => {
|
||||
return {
|
||||
rowSpan: record._stationRowSpan || 0,
|
||||
rowSpan: record._stationRowSpan || 0
|
||||
};
|
||||
},
|
||||
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
|
||||
const str = String(text);
|
||||
// 按逗号分割(支持中文逗号和英文逗号)
|
||||
const items = str.split(/[,,]/);
|
||||
|
||||
// 如果数量超过6个,截取前6个并添加省略号
|
||||
let displayText = "";
|
||||
let displayText = '';
|
||||
if (items.length > 6) {
|
||||
displayText = items.slice(0, 6).join(",") + " ...";
|
||||
displayText = items.slice(0, 6).join(',') + ' ...';
|
||||
} else {
|
||||
displayText = str;
|
||||
}
|
||||
@ -230,33 +232,38 @@ let columns = ref([
|
||||
Tooltip,
|
||||
{
|
||||
title: str, // Tooltip 显示的全部内容
|
||||
placement: "topLeft", // 可选:提示框位置
|
||||
placement: 'topLeft' // 可选:提示框位置
|
||||
},
|
||||
() => h("span", displayText)
|
||||
() => h('span', displayText)
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{ dataIndex: "reportMonth", key: "reportMonth", title: "月份", width: 80 },
|
||||
{ dataIndex: 'reportMonth', key: 'reportMonth', title: '月份', width: 80 },
|
||||
{
|
||||
dataIndex: "minStrdt",
|
||||
key: "minStrdt",
|
||||
title: "过鱼开始时间",
|
||||
dataIndex: 'minStrdt',
|
||||
key: 'minStrdt',
|
||||
title: '过鱼开始时间',
|
||||
width: 140,
|
||||
customRender: ({ text }) => {
|
||||
return text ? dayjs(text).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
return text ? dayjs(text).format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: "maxEnddt",
|
||||
key: "maxEnddt",
|
||||
title: "过鱼结束时间",
|
||||
dataIndex: 'maxEnddt',
|
||||
key: 'maxEnddt',
|
||||
title: '过鱼结束时间',
|
||||
width: 140,
|
||||
customRender: ({ text }) => {
|
||||
return text ? dayjs(text).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
return text ? dayjs(text).format('YYYY-MM-DD') : '';
|
||||
}
|
||||
},
|
||||
{ dataIndex: "totalFcnt", key: "totalFcnt", title: "本月过鱼总数", width: 140 },
|
||||
{ dataIndex: "contact", key: "contact", title: "联系人/电话", width: 250 },
|
||||
{
|
||||
dataIndex: 'totalFcnt',
|
||||
key: 'totalFcnt',
|
||||
title: '本月过鱼总数',
|
||||
width: 140
|
||||
},
|
||||
{ dataIndex: 'contact', key: 'contact', title: '联系人/电话', width: 250 }
|
||||
]);
|
||||
|
||||
const customTransform = (res: any) => {
|
||||
@ -279,7 +286,7 @@ const customTransform = (res: any) => {
|
||||
...item,
|
||||
_basinRowSpan: 1,
|
||||
_stationRowSpan: 1,
|
||||
_selectionRowSpan: 1, // ✅ 新增选择列合并标记
|
||||
_selectionRowSpan: 1 // ✅ 新增选择列合并标记
|
||||
}));
|
||||
|
||||
// 3. 遍历计算合并行
|
||||
@ -321,7 +328,7 @@ const customTransform = (res: any) => {
|
||||
// 4. 返回结果
|
||||
return {
|
||||
records: modifiedRecords,
|
||||
total: res.data.total || 0,
|
||||
total: res.data.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
@ -329,34 +336,34 @@ const customTransform = (res: any) => {
|
||||
const handleDetailSearchFinish = (values: any) => {
|
||||
const filters = [
|
||||
values.strdt && {
|
||||
field: "maxEnddt",
|
||||
operator: "gte",
|
||||
dataType: "date",
|
||||
value: values.strdt[0] + " 00:00:00",
|
||||
field: 'maxEnddt',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[0] + ' 00:00:00'
|
||||
},
|
||||
values.strdt && {
|
||||
field: "minStrdt",
|
||||
operator: "lte",
|
||||
dataType: "date",
|
||||
value: values.strdt[1] + " 23:59:59",
|
||||
field: 'minStrdt',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[1] + ' 23:59:59'
|
||||
},
|
||||
values.hbrvcd !== "all" && {
|
||||
field: "basinCode",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.hbrvcd,
|
||||
values.hbrvcd !== 'all' && {
|
||||
field: 'basinCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.hbrvcd
|
||||
},
|
||||
values.rstcd && {
|
||||
field: "stationCode",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.rstcd,
|
||||
},
|
||||
field: 'stationCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
}
|
||||
].filter(Boolean);
|
||||
|
||||
const filter = {
|
||||
logic: "and",
|
||||
filters: filters,
|
||||
logic: 'and',
|
||||
filters: filters
|
||||
};
|
||||
detailTableRef.value?.getList(filter);
|
||||
};
|
||||
@ -367,17 +374,17 @@ const handleDetailReset = (values: any) => {
|
||||
};
|
||||
const urgeBtn = async () => {
|
||||
Modal.confirm({
|
||||
title: "是否催促选择的用户?",
|
||||
content: h("div", {}, [
|
||||
"将对选中的 ",
|
||||
title: '是否催促选择的用户?',
|
||||
content: h('div', {}, [
|
||||
'将对选中的 ',
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#ff4d4f", fontWeight: "bold" } },
|
||||
batchData.value.length + " 位用户"
|
||||
'span',
|
||||
{ style: { color: '#ff4d4f', fontWeight: 'bold' } },
|
||||
batchData.value.length + ' 位用户'
|
||||
),
|
||||
"发送【",
|
||||
h("span", { style: { color: "#ff4d4f" } }, "过鱼数据填报催促通知"),
|
||||
"】",
|
||||
'发送【',
|
||||
h('span', { style: { color: '#ff4d4f' } }, '过鱼数据填报催促通知'),
|
||||
'】'
|
||||
]),
|
||||
onOk: async () => {
|
||||
let ids = [];
|
||||
@ -389,15 +396,15 @@ const urgeBtn = async () => {
|
||||
|
||||
let res: any = await batchUrgeContent({ userIds: ids });
|
||||
if (res && res?.code == 0) {
|
||||
message.success("催促成功");
|
||||
message.success('催促成功');
|
||||
detailTableRef.value.loading = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
} else {
|
||||
message.error("催促失败");
|
||||
message.error('催促失败');
|
||||
detailTableRef.value.loading = false;
|
||||
searchRef.value.btnLoading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
// 初始化
|
||||
|
||||
@ -17,9 +17,13 @@
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
|
||||
<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>
|
||||
@ -46,16 +50,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, h, nextTick } from "vue";
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
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";
|
||||
import { ref, computed, onMounted, h, nextTick } from 'vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
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';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
|
||||
interface FormData {
|
||||
[key: string]: any;
|
||||
@ -77,73 +81,86 @@ const guoyuStatus = ref<any>([]);
|
||||
|
||||
const baseColumnsConfig: ColumnConfig[] = [
|
||||
{
|
||||
dataIndex: "hbrvnm",
|
||||
key: "hbrvnm",
|
||||
title: "流域",
|
||||
dataIndex: 'hbrvnm',
|
||||
key: 'hbrvnm',
|
||||
title: '流域',
|
||||
width: 120,
|
||||
fixed: "left",
|
||||
fixed: 'left'
|
||||
},
|
||||
{ dataIndex: "ennm", key: "ennm", title: "电站名称", width: 120, fixed: "left" },
|
||||
{ dataIndex: "stnm", key: "stnm", title: "过鱼设施名称", width: 150, fixed: "left" },
|
||||
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
|
||||
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
|
||||
{
|
||||
dataIndex: "isfs",
|
||||
key: "isfs",
|
||||
title: "是否鱼苗",
|
||||
dataIndex: 'ennm',
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
width: 120,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
dataIndex: 'stnm',
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
width: 150,
|
||||
fixed: 'left'
|
||||
},
|
||||
{ dataIndex: 'strdt', key: 'strdt', title: '过鱼时间', width: 150 },
|
||||
{ dataIndex: 'ftpName', key: 'ftpName', title: '鱼种类', width: 120 },
|
||||
{
|
||||
dataIndex: 'isfs',
|
||||
key: 'isfs',
|
||||
title: '是否鱼苗',
|
||||
width: 74,
|
||||
customRender: ({ text }: any) => {
|
||||
const isYes = text === 1 || text === "1";
|
||||
const isYes = text === 1 || text === '1';
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
color: isYes ? "success" : "error",
|
||||
style: { margin: 0 },
|
||||
color: isYes ? 'success' : 'error',
|
||||
style: { margin: 0 }
|
||||
},
|
||||
() => (isYes ? "是" : "否")
|
||||
() => (isYes ? '是' : '否')
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: "direction",
|
||||
key: "direction",
|
||||
title: "游向",
|
||||
dataIndex: 'direction',
|
||||
key: 'direction',
|
||||
title: '游向',
|
||||
width: 80,
|
||||
customRender: ({ text }: any) =>
|
||||
direction.value.find((item: any) => item.itemCode === text)?.dictName || "-",
|
||||
direction.value.find((item: any) => item.itemCode === text)?.dictName ||
|
||||
'-'
|
||||
},
|
||||
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
|
||||
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
|
||||
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
|
||||
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
|
||||
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
|
||||
{ dataIndex: 'fcnt', key: 'fcnt', title: '过鱼数量(尾)', width: 120 },
|
||||
{ dataIndex: 'fsz', key: 'fsz', title: '体长(cm)', width: 110 },
|
||||
{ dataIndex: 'fwet', key: 'fwet', title: '平均体重(g)', width: 110 },
|
||||
{ dataIndex: 'wt', key: 'wt', title: '水温(℃)', width: 80 },
|
||||
{ dataIndex: 'tm', key: 'tm', title: '填报时间', width: 150 },
|
||||
{
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
title: "状态",
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
title: '状态',
|
||||
width: 74,
|
||||
customRender: ({ text }: any) => {
|
||||
let data = guoyuStatus.value.find((item: any) => item.itemCode === text);
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
color: data?.custom1 || "error",
|
||||
color: data?.custom1 || 'error'
|
||||
},
|
||||
() => data?.dictName || "-"
|
||||
() => data?.dictName || '-'
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const editModalVisible = ref(false);
|
||||
const currentRecord = ref<FormData | null>(null);
|
||||
|
||||
const mediaPreviewVisible = ref(false);
|
||||
const videoPreviewTitle = ref("视频预览");
|
||||
const videoPreviewTitle = ref('视频预览');
|
||||
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
type: "image" | "video";
|
||||
type: 'image' | 'video';
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
@ -152,35 +169,35 @@ const previewList = ref<MediaItem[]>([]);
|
||||
const previewListIndex = ref(0);
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
...baseColumnsConfig.map((col) => {
|
||||
if (col.dataIndex === "level5") {
|
||||
...baseColumnsConfig.map(col => {
|
||||
if (col.dataIndex === 'level5') {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
if (col.dataIndex === "level6") {
|
||||
if (col.dataIndex === 'level6') {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
return { ...col, visible: true };
|
||||
}),
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
dataIndex: "action",
|
||||
fixed: "right",
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
@ -195,26 +212,26 @@ const editModalCancel = () => {
|
||||
|
||||
const handlePreviewClick = (record: any, type: string, index: number) => {
|
||||
const mixedList: MediaItem[] = [];
|
||||
if (type === "image") {
|
||||
videoPreviewTitle.value = "图片预览";
|
||||
if (type === 'image') {
|
||||
videoPreviewTitle.value = '图片预览';
|
||||
const nameList = JSON.parse(JSON.stringify(record)).picpthList;
|
||||
nameList.forEach((item: any) => {
|
||||
mixedList.push({
|
||||
id: record.id,
|
||||
type: "image",
|
||||
type: 'image',
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
url: item.url
|
||||
});
|
||||
});
|
||||
} else if (type === "video") {
|
||||
videoPreviewTitle.value = "视频预览";
|
||||
} else if (type === 'video') {
|
||||
videoPreviewTitle.value = '视频预览';
|
||||
const nameList = JSON.parse(JSON.stringify(record)).vdpthList;
|
||||
nameList.forEach((item: any) => {
|
||||
mixedList.push({
|
||||
id: record.id,
|
||||
type: "video",
|
||||
type: 'video',
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
url: item.url
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -232,88 +249,89 @@ const customTransform = (res: any) => {
|
||||
return {
|
||||
...item,
|
||||
picpthList: item.picpth
|
||||
? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || []
|
||||
: [],
|
||||
? item.picpth.split(',').map((item: string) => baseUrl + '/?' + item) ||
|
||||
[]
|
||||
: []
|
||||
};
|
||||
});
|
||||
return {
|
||||
records: modifiedRecords,
|
||||
total: res?.data?.total || 0,
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
const handleReset = (values) => {
|
||||
const handleReset = values => {
|
||||
handleSearchFinish(values);
|
||||
};
|
||||
|
||||
const handleSearchFinish = (values: any) => {
|
||||
const filters = [
|
||||
values.ftp && {
|
||||
field: "ftp",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.ftp,
|
||||
field: 'ftp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.ftp
|
||||
},
|
||||
{
|
||||
field: "strdt",
|
||||
operator: "gte",
|
||||
dataType: "date",
|
||||
value: values.strdt[0] + " 00:00:00",
|
||||
field: 'strdt',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[0] + ' 00:00:00'
|
||||
},
|
||||
{
|
||||
field: "strdt",
|
||||
operator: "lte",
|
||||
dataType: "date",
|
||||
value: values.strdt[1] + " 23:59:59",
|
||||
field: 'strdt',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[1] + ' 23:59:59'
|
||||
},
|
||||
values.direction && {
|
||||
field: "direction",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.direction,
|
||||
field: 'direction',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.direction
|
||||
},
|
||||
{
|
||||
field: "status",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: "APPROVED",
|
||||
field: 'status',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'APPROVED'
|
||||
},
|
||||
values.stcd && {
|
||||
field: "stcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.stcd,
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
values.rstcd && {
|
||||
field: "rstcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.rstcd,
|
||||
},
|
||||
values.hbrvcd !== "all" && {
|
||||
field: "hbrvcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.hbrvcd,
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
},
|
||||
values.hbrvcd !== 'all' && {
|
||||
field: 'hbrvcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.hbrvcd
|
||||
}
|
||||
].filter(Boolean);
|
||||
|
||||
const filter = {
|
||||
logic: "and",
|
||||
filters: filters,
|
||||
logic: 'and',
|
||||
filters: filters
|
||||
};
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
|
||||
getDictItemsByCode({ dictCode: 'direction' }).then(res => {
|
||||
direction.value = res.data;
|
||||
});
|
||||
|
||||
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
|
||||
getDictItemsByCode({ dictCode: 'approvalStatus' }).then(res => {
|
||||
guoyuStatus.value.length = 0;
|
||||
res.data.forEach((item: any) => {
|
||||
if (item.itemCode == "APPROVED") {
|
||||
if (item.itemCode == 'APPROVED') {
|
||||
guoyuStatus.value.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
@ -240,7 +240,11 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12" class="imgupload" :class="{ imgupload_hidden: isView }">
|
||||
<a-col
|
||||
:span="12"
|
||||
class="imgupload"
|
||||
:class="{ imgupload_hidden: isView }"
|
||||
>
|
||||
<a-form-item label="图片" name="picpth">
|
||||
<a-upload
|
||||
v-model:file-list="imageFileList"
|
||||
@ -281,7 +285,10 @@
|
||||
上传视频 (MP4)
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<a-button v-else-if="videoFileList.length > 0" @click="handleVideoPreview">
|
||||
<a-button
|
||||
v-else-if="videoFileList.length > 0"
|
||||
@click="handleVideoPreview"
|
||||
>
|
||||
点击预览视频
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
@ -292,19 +299,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { Upload, message } from "ant-design-vue";
|
||||
import type { UploadProps } from "ant-design-vue";
|
||||
import type { Rule } from "ant-design-vue/es/form";
|
||||
import { UploadOutlined, PlusOutlined } from "@ant-design/icons-vue";
|
||||
import fishSearch from "@/components/fishSearch/index.vue";
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { Upload, message } from 'ant-design-vue';
|
||||
import type { UploadProps } from 'ant-design-vue';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { UploadOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import fishSearch from '@/components/fishSearch/index.vue';
|
||||
import {
|
||||
getSelectForDropdown,
|
||||
getEngInfoDropdown,
|
||||
getFpssDropdown,
|
||||
uploadFile,
|
||||
} from "@/api/select";
|
||||
uploadFile
|
||||
} from '@/api/select';
|
||||
|
||||
// 定义 Props
|
||||
interface Props {
|
||||
@ -333,12 +340,12 @@ const videoFileList = ref<any[]>([]);
|
||||
const disabledDate = (current: any) => {
|
||||
// current 是 moment/dayjs 对象
|
||||
// 返回 true 表示禁用
|
||||
return current && current > dayjs().endOf("day");
|
||||
return current && current > dayjs().endOf('day');
|
||||
};
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
initialValues: null,
|
||||
loading: false,
|
||||
loading: false
|
||||
});
|
||||
// 获取基地
|
||||
// const getBaseDropdownSelect = async () => {
|
||||
@ -363,11 +370,11 @@ const getHbrvcdDropdownSelect = async () => {
|
||||
const res = await getSelectForDropdown({});
|
||||
let list = res.data || [];
|
||||
if (list && list.length > 0) {
|
||||
list = list.filter((item: any) => item.hbrvcd !== "all");
|
||||
list = list.filter((item: any) => item.hbrvcd !== 'all');
|
||||
}
|
||||
hbrvcdOption.value = list;
|
||||
} catch (error) {
|
||||
console.error("获取流域列表失败:", error);
|
||||
console.error('获取流域列表失败:', error);
|
||||
} finally {
|
||||
hbrvcdLoading.value = false;
|
||||
}
|
||||
@ -390,7 +397,7 @@ const getEngInfoDropdownSelect = async (hbrvcd: string) => {
|
||||
const res = await getEngInfoDropdown({ hbrvcd });
|
||||
engOption.value = res.data;
|
||||
} catch (error) {
|
||||
console.error("获取电站列表失败", error);
|
||||
console.error('获取电站列表失败', error);
|
||||
} finally {
|
||||
engLoading.value = false;
|
||||
}
|
||||
@ -405,28 +412,34 @@ const getFpssDropdownSelect = async (rstcd: string, hbrvcd: string) => {
|
||||
const res = await getFpssDropdown({ rstcd, hbrvcd });
|
||||
fpssOption.value = res.data;
|
||||
} catch (error) {
|
||||
console.error("获取流量列表失败", error);
|
||||
console.error('获取流量列表失败', error);
|
||||
} finally {
|
||||
fpssLoading.value = false;
|
||||
}
|
||||
};
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit("update:visible", val),
|
||||
set: val => emit('update:visible', val)
|
||||
});
|
||||
// 定义 Emits
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "cancel"): void;
|
||||
(e: "ok", values: any): void;
|
||||
(e: "preview-click", record: any, type: string, index: number, action: string): void;
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'cancel'): void;
|
||||
(e: 'ok', values: any): void;
|
||||
(
|
||||
e: 'preview-click',
|
||||
record: any,
|
||||
type: string,
|
||||
index: number,
|
||||
action: string
|
||||
): void;
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
// 1. 新增用于存储手动验证错误信息的响应式变量
|
||||
const bodyLengthError = ref<string>("");
|
||||
const weightError = ref<string>("");
|
||||
const bodyLengthError = ref<string>('');
|
||||
const weightError = ref<string>('');
|
||||
|
||||
// 表单数据模型
|
||||
const defaultFormData = reactive({
|
||||
@ -447,7 +460,7 @@ const defaultFormData = reactive({
|
||||
bodyLengthMin: undefined,
|
||||
bodyLengthMax: undefined,
|
||||
weightMin: undefined,
|
||||
weightMax: undefined,
|
||||
weightMax: undefined
|
||||
});
|
||||
const formData: any = reactive({ ...defaultFormData });
|
||||
const filterOption = (inputValue: string, option: any) => {
|
||||
@ -456,13 +469,13 @@ const filterOption = (inputValue: string, option: any) => {
|
||||
};
|
||||
// 验证规则
|
||||
const rules: Record<string, Rule[]> = {
|
||||
hbrvcd: [{ required: true, message: "请选择流域", trigger: "change" }],
|
||||
rstcd: [{ required: true, message: "请选择电站", trigger: "change" }],
|
||||
stcd: [{ required: true, message: "请选择过鱼设施", trigger: "change" }],
|
||||
strdt: [{ required: true, message: "请选择过鱼时间", trigger: "change" }],
|
||||
ftp: [{ required: true, message: "请选择鱼种类", trigger: "change" }],
|
||||
direction: [{ required: true, message: "请选择游向", trigger: "change" }],
|
||||
fcnt: [{ required: true, message: "请输入过鱼数量", trigger: "change" }],
|
||||
hbrvcd: [{ required: true, message: '请选择流域', trigger: 'change' }],
|
||||
rstcd: [{ required: true, message: '请选择电站', trigger: 'change' }],
|
||||
stcd: [{ required: true, message: '请选择过鱼设施', trigger: 'change' }],
|
||||
strdt: [{ required: true, message: '请选择过鱼时间', trigger: 'change' }],
|
||||
ftp: [{ required: true, message: '请选择鱼种类', trigger: 'change' }],
|
||||
direction: [{ required: true, message: '请选择游向', trigger: 'change' }],
|
||||
fcnt: [{ required: true, message: '请输入过鱼数量', trigger: 'change' }]
|
||||
};
|
||||
|
||||
// 计算是否为编辑模式
|
||||
@ -473,15 +486,15 @@ const validateBodyLength = () => {
|
||||
const max = formData.bodyLengthMax;
|
||||
|
||||
// 重置错误
|
||||
bodyLengthError.value = "";
|
||||
bodyLengthError.value = '';
|
||||
|
||||
// 判断是否有值 (排除 undefined, null, 和空字符串)
|
||||
const hasMin = min !== undefined && min !== null && min !== "";
|
||||
const hasMax = max !== undefined && max !== null && max !== "";
|
||||
const hasMin = min !== undefined && min !== null && min !== '';
|
||||
const hasMax = max !== undefined && max !== null && max !== '';
|
||||
|
||||
// 1. 如果只填了一个值,提示两个都必填
|
||||
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
|
||||
bodyLengthError.value = "最小体长和最大体长均需填写";
|
||||
bodyLengthError.value = '最小体长和最大体长均需填写';
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -489,7 +502,7 @@ const validateBodyLength = () => {
|
||||
if (hasMin && hasMax) {
|
||||
// 允许相等,只禁止 min > max
|
||||
if (Number(min) > Number(max)) {
|
||||
bodyLengthError.value = "最小体长不能大于最大体长";
|
||||
bodyLengthError.value = '最小体长不能大于最大体长';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -502,15 +515,15 @@ const validateWeight = () => {
|
||||
const max = formData.weightMax;
|
||||
|
||||
// 重置错误
|
||||
weightError.value = "";
|
||||
weightError.value = '';
|
||||
|
||||
// 判断是否有值
|
||||
const hasMin = min !== undefined && min !== null && min !== "";
|
||||
const hasMax = max !== undefined && max !== null && max !== "";
|
||||
const hasMin = min !== undefined && min !== null && min !== '';
|
||||
const hasMax = max !== undefined && max !== null && max !== '';
|
||||
|
||||
// 1. 如果只填了一个值,提示两个都必填
|
||||
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
|
||||
weightError.value = "最小体重和最大体重均需填写";
|
||||
weightError.value = '最小体重和最大体重均需填写';
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -518,7 +531,7 @@ const validateWeight = () => {
|
||||
if (hasMin && hasMax) {
|
||||
// 允许相等,只禁止 min > max
|
||||
if (Number(min) > Number(max)) {
|
||||
weightError.value = "最小体重不能大于最大体重";
|
||||
weightError.value = '最小体重不能大于最大体重';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -526,20 +539,20 @@ const validateWeight = () => {
|
||||
return true;
|
||||
};
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload: UploadProps["beforeUpload"] = (file) => {
|
||||
const beforeImageUpload: UploadProps['beforeUpload'] = file => {
|
||||
const isJpgOrPng =
|
||||
file.type === "image/jpeg" ||
|
||||
file.type === "image/png" ||
|
||||
file.name.toLowerCase().endsWith(".jpg") ||
|
||||
file.name.toLowerCase().endsWith(".jpeg") ||
|
||||
file.name.toLowerCase().endsWith(".png");
|
||||
file.type === 'image/jpeg' ||
|
||||
file.type === 'image/png' ||
|
||||
file.name.toLowerCase().endsWith('.jpg') ||
|
||||
file.name.toLowerCase().endsWith('.jpeg') ||
|
||||
file.name.toLowerCase().endsWith('.png');
|
||||
if (!isJpgOrPng) {
|
||||
message.error("只能上传 JPG/PNG/JPEG 格式的图片!");
|
||||
message.error('只能上传 JPG/PNG/JPEG 格式的图片!');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 10; // 限制10M,可根据需求调整
|
||||
if (!isLt2M) {
|
||||
message.error("图片大小不能超过 10MB!");
|
||||
message.error('图片大小不能超过 10MB!');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
// 返回 false 阻止自动上传,我们手动处理
|
||||
@ -547,15 +560,16 @@ const beforeImageUpload: UploadProps["beforeUpload"] = (file) => {
|
||||
};
|
||||
|
||||
// 视频上传前校验
|
||||
const beforeVideoUpload: UploadProps["beforeUpload"] = (file) => {
|
||||
const isMp4 = file.type === "video/mp4" || file.name.toLowerCase().endsWith(".mp4");
|
||||
const beforeVideoUpload: UploadProps['beforeUpload'] = file => {
|
||||
const isMp4 =
|
||||
file.type === 'video/mp4' || file.name.toLowerCase().endsWith('.mp4');
|
||||
if (!isMp4) {
|
||||
message.error("只能上传 MP4 格式的视频!");
|
||||
message.error('只能上传 MP4 格式的视频!');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt50M = file.size / 1024 / 1024 < 20; // 限制20M,可根据需求调整
|
||||
if (!isLt50M) {
|
||||
message.error("视频大小不能超过 20MB!");
|
||||
message.error('视频大小不能超过 20MB!');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
// 返回 false 阻止自动上传
|
||||
@ -587,7 +601,7 @@ const initForm = () => {
|
||||
// --- 编辑模式:回填数据 ---
|
||||
const values = props.initialValues;
|
||||
if (values.fwet) {
|
||||
const weights = values.fwet.split("~");
|
||||
const weights = values.fwet.split('~');
|
||||
if (weights.length === 2) {
|
||||
formData.weightMin = weights[0];
|
||||
formData.weightMax = weights[1];
|
||||
@ -601,7 +615,7 @@ const initForm = () => {
|
||||
}
|
||||
|
||||
if (values.fsz) {
|
||||
const sizes = values.fsz.split("~");
|
||||
const sizes = values.fsz.split('~');
|
||||
if (sizes.length === 2) {
|
||||
formData.bodyLengthMin = sizes[0];
|
||||
formData.bodyLengthMax = sizes[1];
|
||||
@ -616,35 +630,37 @@ const initForm = () => {
|
||||
if (values.picpth) {
|
||||
const paths = Array.isArray(values.picpth)
|
||||
? values.picpth
|
||||
: values.picpth.split(",");
|
||||
: values.picpth.split(',');
|
||||
imageFileList.value = paths.map((path: string, index: number) => ({
|
||||
uid: `-${index}`,
|
||||
name: path.split("/").pop() || `image-${index}`,
|
||||
status: "done",
|
||||
name: path.split('/').pop() || `image-${index}`,
|
||||
status: 'done',
|
||||
path: path,
|
||||
url: import.meta.env.VITE_APP_PREVIEW_URL + "/?" + path, // 这里需要是完整的可访问URL
|
||||
url: import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + path // 这里需要是完整的可访问URL
|
||||
}));
|
||||
}
|
||||
|
||||
// 处理视频回显
|
||||
if (values.vdpth) {
|
||||
const paths = Array.isArray(values.vdpth) ? values.vdpth : values.vdpth.split(",");
|
||||
const paths = Array.isArray(values.vdpth)
|
||||
? values.vdpth
|
||||
: values.vdpth.split(',');
|
||||
videoFileList.value = paths.map((path: string, index: number) => ({
|
||||
uid: `-${index}`,
|
||||
name: path.split("/").pop() || `image-${index}`,
|
||||
status: "done",
|
||||
name: path.split('/').pop() || `image-${index}`,
|
||||
status: 'done',
|
||||
path: path,
|
||||
url: import.meta.env.VITE_APP_PREVIEW_URL + "/?" + path, // 这里需要是完整的可访问URL
|
||||
url: import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + path // 这里需要是完整的可访问URL
|
||||
}));
|
||||
}
|
||||
// 回填其他普通字段
|
||||
Object.keys(formData).forEach((key) => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
// 跳过已经特殊处理的字段
|
||||
if (
|
||||
key === "weightMin" ||
|
||||
key === "weightMax" ||
|
||||
key === "bodyLengthMin" ||
|
||||
key === "bodyLengthMax"
|
||||
key === 'weightMin' ||
|
||||
key === 'weightMax' ||
|
||||
key === 'bodyLengthMin' ||
|
||||
key === 'bodyLengthMax'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -660,12 +676,24 @@ const initForm = () => {
|
||||
|
||||
// 视频预览
|
||||
const handleVideoPreview = () => {
|
||||
emit("preview-click", { vdpthList: videoFileList.value }, "video", 0, "preview");
|
||||
emit(
|
||||
'preview-click',
|
||||
{ vdpthList: videoFileList.value },
|
||||
'video',
|
||||
0,
|
||||
'preview'
|
||||
);
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
const handleImagePreview = async (file: any) => {
|
||||
emit("preview-click", { picpthList: imageFileList.value }, "image", 0, "preview");
|
||||
emit(
|
||||
'preview-click',
|
||||
{ picpthList: imageFileList.value },
|
||||
'image',
|
||||
0,
|
||||
'preview'
|
||||
);
|
||||
};
|
||||
// 确认操作
|
||||
const handleOk = async () => {
|
||||
@ -674,7 +702,7 @@ const handleOk = async () => {
|
||||
const isBodyLenValid = validateBodyLength();
|
||||
const isWeightValid = validateWeight();
|
||||
if (!isBodyLenValid || !isWeightValid) {
|
||||
message.error("请检查体长或体重填写是否正确");
|
||||
message.error('请检查体长或体重填写是否正确');
|
||||
return;
|
||||
}
|
||||
// 验证表单
|
||||
@ -683,16 +711,16 @@ const handleOk = async () => {
|
||||
let uploadedPicPaths: any[] = [];
|
||||
// 过滤出新生成的文件(没有 url 属性的通常是新选择的文件)
|
||||
const newImageFiles = imageFileList.value.filter(
|
||||
(file) => !file.url && file.originFileObj
|
||||
file => !file.url && file.originFileObj
|
||||
);
|
||||
|
||||
localLoading.value = true;
|
||||
if (newImageFiles.length > 0) {
|
||||
// 这里需要循环上传,或者使用 Promise.all 并行上传
|
||||
// 伪代码:假设 uploadFile 返回 { data: { url: '...' } }
|
||||
const uploadPromises = newImageFiles.map((file) => {
|
||||
const uploadPromises = newImageFiles.map(file => {
|
||||
const formDataUpload = new FormData();
|
||||
formDataUpload.append("file", file.originFileObj);
|
||||
formDataUpload.append('file', file.originFileObj);
|
||||
return uploadFile(formDataUpload); // 替换为真实的上传接口调用
|
||||
});
|
||||
|
||||
@ -701,30 +729,30 @@ const handleOk = async () => {
|
||||
path: item.message,
|
||||
uid: `-${index}`,
|
||||
name: item.fileMD5,
|
||||
status: "done",
|
||||
url: item.fullFilePath, // 这里需要是完整的可访问URL
|
||||
status: 'done',
|
||||
url: item.fullFilePath // 这里需要是完整的可访问URL
|
||||
}));
|
||||
}
|
||||
|
||||
// 合并已存在的图片路径(编辑模式下回显的)和新上传的
|
||||
const existingPics = imageFileList.value
|
||||
.filter((file) => file.path)
|
||||
.map((file) => file.path);
|
||||
.filter(file => file.path)
|
||||
.map(file => file.path);
|
||||
const uploadedPicPaths1 = uploadedPicPaths
|
||||
.filter((file) => file.path)
|
||||
.map((file) => file.path);
|
||||
.filter(file => file.path)
|
||||
.map(file => file.path);
|
||||
const finalPicPaths = [...existingPics, ...uploadedPicPaths1];
|
||||
|
||||
// 2. 处理视频上传
|
||||
let uploadedVideoPath = [];
|
||||
const newVideoFiles = videoFileList.value.filter(
|
||||
(file) => !file.url && file.originFileObj
|
||||
file => !file.url && file.originFileObj
|
||||
);
|
||||
|
||||
if (newVideoFiles.length > 0) {
|
||||
const uploadPromises = newVideoFiles.map((file) => {
|
||||
const uploadPromises = newVideoFiles.map(file => {
|
||||
const formDataUpload = new FormData();
|
||||
formDataUpload.append("file", file.originFileObj);
|
||||
formDataUpload.append('file', file.originFileObj);
|
||||
return uploadFile(formDataUpload); // 替换为真实的上传接口调用
|
||||
});
|
||||
const results = await Promise.all(uploadPromises);
|
||||
@ -732,31 +760,34 @@ const handleOk = async () => {
|
||||
path: item.message,
|
||||
uid: `-${index}`,
|
||||
name: item.fileMD5,
|
||||
status: "done",
|
||||
url: item.fullFilePath, // 这里需要是完整的可访问URL
|
||||
status: 'done',
|
||||
url: item.fullFilePath // 这里需要是完整的可访问URL
|
||||
}));
|
||||
}
|
||||
const existingVideos = videoFileList.value
|
||||
.filter((file) => file.path)
|
||||
.map((file) => file.path);
|
||||
.filter(file => file.path)
|
||||
.map(file => file.path);
|
||||
const uploadedVideoPaths1 = uploadedVideoPath
|
||||
.filter((file) => file.path)
|
||||
.map((file) => file.path);
|
||||
.filter(file => file.path)
|
||||
.map(file => file.path);
|
||||
const finalVideoPaths = [...existingVideos, ...uploadedVideoPaths1];
|
||||
|
||||
let fwet = "";
|
||||
let fwet = '';
|
||||
if (
|
||||
formData.weightMin == formData.weightMax &&
|
||||
formData.weightMin != undefined &&
|
||||
formData.weightMax != undefined
|
||||
) {
|
||||
fwet = formData.weightMin;
|
||||
} else if (formData.weightMin == undefined && formData.weightMax == undefined) {
|
||||
fwet = "";
|
||||
} else if (
|
||||
formData.weightMin == undefined &&
|
||||
formData.weightMax == undefined
|
||||
) {
|
||||
fwet = '';
|
||||
} else {
|
||||
fwet = formData.weightMin + "~" + formData.weightMax;
|
||||
fwet = formData.weightMin + '~' + formData.weightMax;
|
||||
}
|
||||
let fsz = "";
|
||||
let fsz = '';
|
||||
if (
|
||||
formData.bodyLengthMin == formData.bodyLengthMax &&
|
||||
formData.bodyLengthMin != undefined &&
|
||||
@ -767,26 +798,26 @@ const handleOk = async () => {
|
||||
formData.bodyLengthMin == undefined &&
|
||||
formData.bodyLengthMax == undefined
|
||||
) {
|
||||
fsz = "";
|
||||
fsz = '';
|
||||
} else {
|
||||
fsz = formData.bodyLengthMin + "~" + formData.bodyLengthMax;
|
||||
fsz = formData.bodyLengthMin + '~' + formData.bodyLengthMax;
|
||||
}
|
||||
// 准备提交数据
|
||||
const submitValues = {
|
||||
...formData,
|
||||
fwet: fwet,
|
||||
fsz: fsz,
|
||||
strdt: formData.strdt + " 00:00:00",
|
||||
picpth: finalPicPaths.join(","), // 根据后端要求,可能是数组或逗号分隔字符串
|
||||
vdpth: finalVideoPaths.join(","),
|
||||
strdt: formData.strdt + ' 00:00:00',
|
||||
picpth: finalPicPaths.join(','), // 根据后端要求,可能是数组或逗号分隔字符串
|
||||
vdpth: finalVideoPaths.join(',')
|
||||
};
|
||||
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
if (!formData.id) submitValues.tm = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
// console.log(submitValues);
|
||||
// return;
|
||||
emit("ok", submitValues);
|
||||
emit('ok', submitValues);
|
||||
} catch (error) {
|
||||
console.error("Validate Failed:", error);
|
||||
message.error("请检查表单填写是否正确");
|
||||
console.error('Validate Failed:', error);
|
||||
message.error('请检查表单填写是否正确');
|
||||
}
|
||||
};
|
||||
// 重置表单
|
||||
@ -796,8 +827,8 @@ const resetForm = () => {
|
||||
}
|
||||
Object.assign(formData, defaultFormData);
|
||||
// 清空手动验证的错误信息
|
||||
bodyLengthError.value = "";
|
||||
weightError.value = "";
|
||||
bodyLengthError.value = '';
|
||||
weightError.value = '';
|
||||
// 清空文件列表
|
||||
imageFileList.value = [];
|
||||
videoFileList.value = [];
|
||||
@ -805,14 +836,14 @@ const resetForm = () => {
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
resetForm();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
newVisible => {
|
||||
if (newVisible) {
|
||||
// 弹窗打开时,初始化数据
|
||||
// getBaseDropdownSelect();// 基地
|
||||
@ -826,7 +857,7 @@ watch(
|
||||
);
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
localLoading,
|
||||
localLoading
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,18 +1,37 @@
|
||||
<template>
|
||||
<div class="guoYuSheShiShuJuTianBao-search">
|
||||
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData" :zhujianfujian="'fu'"
|
||||
@reset="handleReset" @finish="onSearchFinish" @values-change="onValuesChange">
|
||||
<BasicSearch
|
||||
ref="basicSearchRef"
|
||||
:searchList="searchList"
|
||||
:initial-values="initSearchData"
|
||||
:zhujianfujian="'fu'"
|
||||
@reset="handleReset"
|
||||
@finish="onSearchFinish"
|
||||
@values-change="onValuesChange"
|
||||
>
|
||||
<template #ftp="{ onChange }">
|
||||
<fishSearch v-model="localTypeDate" width="200px" @update:modelValue="onChange" />
|
||||
<fishSearch
|
||||
v-model="localTypeDate"
|
||||
width="200px"
|
||||
@update:modelValue="onChange"
|
||||
/>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-tooltip title="手动新增">
|
||||
<a-button v-hasPerm="['sjtb:import-add']" @click="props.handleAdd" type="primary">
|
||||
<a-button
|
||||
v-hasPerm="['sjtb:import-add']"
|
||||
@click="props.handleAdd"
|
||||
type="primary"
|
||||
>
|
||||
手动新增
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="数据批量导入zip">
|
||||
<a-button v-hasPerm="['sjtb:import-add']" @click="props.importBtn" type="primary">
|
||||
<a-button
|
||||
v-hasPerm="['sjtb:import-add']"
|
||||
@click="props.importBtn"
|
||||
type="primary"
|
||||
>
|
||||
数据批量导入zip
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
@ -69,7 +88,10 @@
|
||||
</a-tooltip>
|
||||
<a-tooltip title="操作手册下载">
|
||||
<a-button>
|
||||
<a href="/file/过鱼设施数据填报操作手册.docx" download="过鱼设施数据填报操作手册.docx">
|
||||
<a
|
||||
href="/file/过鱼设施数据填报操作手册.docx"
|
||||
download="过鱼设施数据填报操作手册.docx"
|
||||
>
|
||||
操作手册下载
|
||||
</a>
|
||||
</a-button>
|
||||
@ -80,18 +102,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import {
|
||||
SaveOutlined,
|
||||
CheckSquareOutlined,
|
||||
QuestionOutlined,
|
||||
} from "@ant-design/icons-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";
|
||||
import { downloadFileByUrl } from "@/utils/index";
|
||||
QuestionOutlined
|
||||
} from '@ant-design/icons-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';
|
||||
import { downloadFileByUrl } from '@/utils/index';
|
||||
|
||||
interface Props {
|
||||
direction: any[];
|
||||
@ -109,14 +131,14 @@ const shuJuTianBaoStore = useShuJuTianBaoStore();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "reset", values: any): void;
|
||||
(e: "searchFinish", values: any): void;
|
||||
(e: 'reset', values: any): void;
|
||||
(e: 'searchFinish', values: any): void;
|
||||
}>();
|
||||
const localTypeDate = ref<string>(null);
|
||||
const basicSearchRef = ref<any>();
|
||||
|
||||
const initSearchData = {
|
||||
hbrvcd: "all",
|
||||
hbrvcd: 'all',
|
||||
// baseId: "all",
|
||||
stcd: null,
|
||||
rstcd: null,
|
||||
@ -124,88 +146,88 @@ const initSearchData = {
|
||||
status: 'DRAFT',
|
||||
direction: null,
|
||||
strdt: [
|
||||
dayjs().subtract(1, 'month').startOf('month').format("YYYY-MM-DD"),
|
||||
dayjs().format("YYYY-MM-DD"),
|
||||
],
|
||||
dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD')
|
||||
]
|
||||
};
|
||||
|
||||
const searchData = ref<any>({ ...initSearchData });
|
||||
const searchList: any = computed(() => [
|
||||
{
|
||||
type: "waterStation",
|
||||
name: "hbrvcd",
|
||||
label: "流域",
|
||||
type: 'waterStation',
|
||||
name: 'hbrvcd',
|
||||
label: '流域',
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
allowClear: true
|
||||
},
|
||||
options: [],
|
||||
options: []
|
||||
},
|
||||
{
|
||||
type: "Select",
|
||||
name: "stcd",
|
||||
label: "过鱼设施",
|
||||
values: { name: "stnm", value: "stcd" },
|
||||
type: 'Select',
|
||||
name: 'stcd',
|
||||
label: '过鱼设施',
|
||||
values: { name: 'stnm', value: 'stcd' },
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
allowClear: true
|
||||
},
|
||||
options: shuJuTianBaoStore.fpssOption,
|
||||
options: shuJuTianBaoStore.fpssOption
|
||||
},
|
||||
{
|
||||
type: "Select",
|
||||
name: "direction",
|
||||
label: "游向",
|
||||
type: 'Select',
|
||||
name: 'direction',
|
||||
label: '游向',
|
||||
width: 120,
|
||||
options: props.direction,
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "custom",
|
||||
name: "ftp",
|
||||
label: "鱼种类",
|
||||
type: 'custom',
|
||||
name: 'ftp',
|
||||
label: '鱼种类',
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
allowClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
width: 120,
|
||||
type: "Select",
|
||||
name: "status",
|
||||
label: "数据状态",
|
||||
type: 'Select',
|
||||
name: 'status',
|
||||
label: '数据状态',
|
||||
fieldProps: {
|
||||
allowClear: true,
|
||||
allowClear: true
|
||||
},
|
||||
options: props.guoyuStatus,
|
||||
options: props.guoyuStatus
|
||||
},
|
||||
{
|
||||
span: 12,
|
||||
type: "RangePicker",
|
||||
name: "strdt",
|
||||
label: "过鱼时间",
|
||||
picker: "date",
|
||||
type: 'RangePicker',
|
||||
name: 'strdt',
|
||||
label: '过鱼时间',
|
||||
picker: 'date',
|
||||
fieldProps: {
|
||||
format: "YYYY-MM-DD",
|
||||
valueFormat: "YYYY-MM-DD",
|
||||
allowClear: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
allowClear: false
|
||||
},
|
||||
presets: DateSetting.RangeButton.days,
|
||||
},
|
||||
presets: DateSetting.RangeButton.days
|
||||
}
|
||||
]);
|
||||
// --- Methods ---
|
||||
|
||||
// 2. 搜索表单逻辑
|
||||
const onSearchFinish = (values: any) => {
|
||||
emit("searchFinish", values);
|
||||
emit('searchFinish', values);
|
||||
};
|
||||
|
||||
const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
searchData.value = { ...searchData.value, ...allValues };
|
||||
// 同步更新本地 searchData,以便其他逻辑使用
|
||||
if (
|
||||
Object.keys(changedValues)[0] == "rstcd" ||
|
||||
Object.keys(changedValues)[0] == "baseId" ||
|
||||
Object.keys(changedValues)[0] == "hbrvcd"
|
||||
Object.keys(changedValues)[0] == 'rstcd' ||
|
||||
Object.keys(changedValues)[0] == 'baseId' ||
|
||||
Object.keys(changedValues)[0] == 'hbrvcd'
|
||||
) {
|
||||
const formInstance = basicSearchRef.value?.formData;
|
||||
formInstance.stcd = null;
|
||||
@ -214,24 +236,28 @@ const onValuesChange = (changedValues: any, allValues: any) => {
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
downloadFileByUrl(import.meta.env.VITE_APP_PREVIEW_URL + "/?20260429090252740092641634938251", "过鱼设施数据填报模板");
|
||||
downloadFileByUrl(
|
||||
import.meta.env.VITE_APP_ATTACHMENT_URL +
|
||||
'/?20260429090252740092641634938251',
|
||||
'过鱼设施数据填报模板'
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
localTypeDate.value = null;
|
||||
emit("reset", initSearchData);
|
||||
emit('reset', initSearchData);
|
||||
};
|
||||
watch(
|
||||
() => initSearchData.ftp,
|
||||
(newVal) => {
|
||||
localTypeDate.value = newVal || "";
|
||||
newVal => {
|
||||
localTypeDate.value = newVal || '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {
|
||||
emit("searchFinish", initSearchData);
|
||||
emit('searchFinish', initSearchData);
|
||||
|
||||
shuJuTianBaoStore.getFpssOption("", "");
|
||||
shuJuTianBaoStore.getFpssOption('', '');
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@ -28,7 +28,9 @@
|
||||
>
|
||||
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
|
||||
<template
|
||||
v-if="column.key === 'action' || column.dataIndex === 'action'"
|
||||
>
|
||||
<div class="flex">
|
||||
<a-button
|
||||
v-hasPerm="['sjtb:import-add']"
|
||||
@ -68,9 +70,12 @@
|
||||
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') ||
|
||||
(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
|
||||
@ -111,7 +116,11 @@
|
||||
</div>
|
||||
<div class="button">
|
||||
<a-tooltip title="全部提交">
|
||||
<a-button v-hasPerm="['sjtb:import-add']" @click="allSubmitBtn" size="large">
|
||||
<a-button
|
||||
v-hasPerm="['sjtb:import-add']"
|
||||
@click="allSubmitBtn"
|
||||
size="large"
|
||||
>
|
||||
全部提交
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
@ -156,7 +165,7 @@
|
||||
:direction="direction"
|
||||
@preview-click="handlePreviewClick"
|
||||
v-model:file-loading="fileLoading"
|
||||
@update:file-table-data="(val) => (fileTableData = val)"
|
||||
@update:file-table-data="val => (fileTableData = val)"
|
||||
/>
|
||||
<template #footer>
|
||||
<a-button key="back" @click="handleCustomCancel">取消导入</a-button>
|
||||
@ -197,14 +206,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, h, nextTick, watch } from "vue";
|
||||
import { message, Modal } from "ant-design-vue"; // 假设使用 ant-design-vue
|
||||
import BasicTable from "@/components/BasicTable/index.vue";
|
||||
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
|
||||
import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue";
|
||||
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
|
||||
import PreviewMedia from "@/components/previewMedia/index.vue";
|
||||
import { checkPerm } from "@/directive/permission";
|
||||
import { ref, computed, onMounted, h, nextTick, watch } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue'; // 假设使用 ant-design-vue
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import GuoYuSheShiShuJuTianBaoSearch from './guoYuSheShiShuJuTianBaoSearch.vue';
|
||||
import GuoYuSheShiShuJuTianBaoTable from './guoYuSheShiShuJuTianBaoTable.vue';
|
||||
import EditModal from './guoYuSheShiShuJuTianBaoForm.vue';
|
||||
import PreviewMedia from '@/components/previewMedia/index.vue';
|
||||
import { checkPerm } from '@/directive/permission';
|
||||
import {
|
||||
getFishDraftPage,
|
||||
addFishDraft,
|
||||
@ -218,13 +227,13 @@ import {
|
||||
checkImportStatus,
|
||||
batchSaveDraft,
|
||||
getLastImportResult,
|
||||
batchApproveAll,
|
||||
} from "@/api/guoYuSheShiShuJuTianBao";
|
||||
import { Tag } from "ant-design-vue"; // 确保导入 Tag
|
||||
import { getDictItemsByCode } from "@/api/dict";
|
||||
import { calcTableScrollY } from "@/utils/index";
|
||||
batchApproveAll
|
||||
} from '@/api/guoYuSheShiShuJuTianBao';
|
||||
import { Tag } from 'ant-design-vue'; // 确保导入 Tag
|
||||
import { getDictItemsByCode } from '@/api/dict';
|
||||
import { calcTableScrollY } from '@/utils/index';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
const baseUrlApi = import.meta.env.VITE_APP_BASE_API_URL;
|
||||
// --- 类型定义 ---
|
||||
interface FormData {
|
||||
@ -247,62 +256,75 @@ const guoyuStatus = ref<any>([]);
|
||||
// --- 基础配置 ---
|
||||
const baseColumnsConfig: ColumnConfig[] = [
|
||||
{
|
||||
dataIndex: "hbrvnm",
|
||||
key: "hbrvnm",
|
||||
title: "流域",
|
||||
dataIndex: 'hbrvnm',
|
||||
key: 'hbrvnm',
|
||||
title: '流域',
|
||||
width: 120,
|
||||
fixed: "left",
|
||||
fixed: 'left'
|
||||
},
|
||||
{ dataIndex: "ennm", key: "ennm", title: "电站名称", width: 120, fixed: "left" },
|
||||
{ dataIndex: "stnm", key: "stnm", title: "过鱼设施名称", width: 150, fixed: "left" },
|
||||
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
|
||||
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
|
||||
{
|
||||
dataIndex: "isfs",
|
||||
key: "isfs",
|
||||
title: "是否鱼苗",
|
||||
dataIndex: 'ennm',
|
||||
key: 'ennm',
|
||||
title: '电站名称',
|
||||
width: 120,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
dataIndex: 'stnm',
|
||||
key: 'stnm',
|
||||
title: '过鱼设施名称',
|
||||
width: 150,
|
||||
fixed: 'left'
|
||||
},
|
||||
{ dataIndex: 'strdt', key: 'strdt', title: '过鱼时间', width: 150 },
|
||||
{ dataIndex: 'ftpName', key: 'ftpName', title: '鱼种类', width: 120 },
|
||||
{
|
||||
dataIndex: 'isfs',
|
||||
key: 'isfs',
|
||||
title: '是否鱼苗',
|
||||
width: 74,
|
||||
customRender: ({ text }: any) => {
|
||||
const isYes = text === 1 || text === "1";
|
||||
const isYes = text === 1 || text === '1';
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
color: isYes ? "success" : "error", // Antdv Tag 的颜色预设
|
||||
style: { margin: 0 }, // 去除默认 margin,使其在表格中对齐更好
|
||||
color: isYes ? 'success' : 'error', // Antdv Tag 的颜色预设
|
||||
style: { margin: 0 } // 去除默认 margin,使其在表格中对齐更好
|
||||
},
|
||||
() => (isYes ? "是" : "否")
|
||||
() => (isYes ? '是' : '否')
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
dataIndex: "direction",
|
||||
key: "direction",
|
||||
title: "游向",
|
||||
dataIndex: 'direction',
|
||||
key: 'direction',
|
||||
title: '游向',
|
||||
width: 80,
|
||||
customRender: ({ text }: any) =>
|
||||
direction.value.find((item: any) => item.itemCode === text)?.dictName || "-",
|
||||
direction.value.find((item: any) => item.itemCode === text)?.dictName ||
|
||||
'-'
|
||||
},
|
||||
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
|
||||
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
|
||||
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
|
||||
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
|
||||
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
|
||||
{ dataIndex: 'fcnt', key: 'fcnt', title: '过鱼数量(尾)', width: 120 },
|
||||
{ dataIndex: 'fsz', key: 'fsz', title: '体长(cm)', width: 110 },
|
||||
{ dataIndex: 'fwet', key: 'fwet', title: '平均体重(g)', width: 110 },
|
||||
{ dataIndex: 'wt', key: 'wt', title: '水温(℃)', width: 80 },
|
||||
{ dataIndex: 'tm', key: 'tm', title: '填报时间', width: 150 },
|
||||
{
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
title: "状态",
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
title: '状态',
|
||||
width: 74,
|
||||
customRender: ({ text }: any) => {
|
||||
let data = guoyuStatus.value.find((item: any) => item.itemCode === text);
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
color: data?.custom1 || "error", // Antdv Tag 的颜色预设
|
||||
color: data?.custom1 || 'error' // Antdv Tag 的颜色预设
|
||||
},
|
||||
() => data?.dictName || "-"
|
||||
() => data?.dictName || '-'
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// --- 状态定义 ---
|
||||
@ -316,10 +338,10 @@ const currentRecord = ref<FormData | null>(null);
|
||||
|
||||
const mediaPreviewVisible = ref(false);
|
||||
const isPreviewMediaDelete = ref(false);
|
||||
const videoPreviewTitle = ref("视频预览");
|
||||
const videoPreviewTitle = ref('视频预览');
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
type: "image" | "video";
|
||||
type: 'image' | 'video';
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
@ -333,41 +355,41 @@ const batchData = ref<any[]>([]);
|
||||
|
||||
const modalTableRef = ref<any>(null);
|
||||
const fileLoading = ref(false);
|
||||
const taskId = ref<string>("");
|
||||
const taskId = ref<string>('');
|
||||
|
||||
// 主表格 Columns
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
...baseColumnsConfig.map((col) => {
|
||||
if (col.dataIndex === "level5") {
|
||||
...baseColumnsConfig.map(col => {
|
||||
if (col.dataIndex === 'level5') {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
// 实际应渲染 Icon 和点击事件,此处简化
|
||||
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
if (col.dataIndex === "level6") {
|
||||
if (col.dataIndex === 'level6') {
|
||||
return {
|
||||
...col,
|
||||
customRender: ({ text }: any) => {
|
||||
if (!text) return "-";
|
||||
if (!text) return '-';
|
||||
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
return { ...col, visible: true };
|
||||
}),
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
dataIndex: "action",
|
||||
fixed: "right",
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
dataIndex: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
align: "center",
|
||||
},
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
@ -380,7 +402,7 @@ const handleAdd = () => {
|
||||
};
|
||||
// 修改
|
||||
const handleEdit = (record: any, type: string) => {
|
||||
if (type == "view") {
|
||||
if (type == 'view') {
|
||||
isView.value = true;
|
||||
} else {
|
||||
isView.value = false;
|
||||
@ -392,14 +414,14 @@ const handleEdit = (record: any, type: string) => {
|
||||
// 单个/批量 删除过鱼数据
|
||||
const handleDelete = (ids: any[]) => {
|
||||
Modal.confirm({
|
||||
title: "是否确认 删除 选中数据吗?",
|
||||
title: '是否确认 删除 选中数据吗?',
|
||||
onOk: async () => {
|
||||
let res: any = await delFishDraft(ids);
|
||||
if (res && res?.code == 0) {
|
||||
message.success("删除成功");
|
||||
message.success('删除成功');
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
// 批量删除 - 按钮
|
||||
@ -409,14 +431,14 @@ const batchDelBtn = () => {
|
||||
//单个/ 批量提交过鱼数据
|
||||
const handleSubmit = (ids: any[]) => {
|
||||
Modal.confirm({
|
||||
title: "是否 提交 选中数据吗?",
|
||||
title: '是否 提交 选中数据吗?',
|
||||
onOk: async () => {
|
||||
let res: any = await submitFishDraft(ids);
|
||||
if (res && res?.code == 0) {
|
||||
message.success("提交成功");
|
||||
message.success('提交成功');
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
// 批量提交-按钮
|
||||
@ -426,58 +448,61 @@ const submitBtn = async () => {
|
||||
//全部提交按钮
|
||||
const allSubmitBtn = async () => {
|
||||
if (tableRef.value.tableData.length === 0) {
|
||||
message.warning("请先添加数据!");
|
||||
message.warning('请先添加数据!');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: "是否 提交 全部数据吗?",
|
||||
title: '是否 提交 全部数据吗?',
|
||||
onOk: async () => {
|
||||
let res: any = await batchApproveAll();
|
||||
if (res && res?.code == 0) {
|
||||
message.success("提交成功");
|
||||
message.success('提交成功');
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 单个/ 批量审批过鱼数据
|
||||
const handleSuccess = (ids: any[]) => {
|
||||
let rejectReason = "";
|
||||
let rejectReason = '';
|
||||
Modal.confirm({
|
||||
title: "是否确认 审批通过 选中数据?",
|
||||
title: '是否确认 审批通过 选中数据?',
|
||||
// 使用 h 函数创建内容
|
||||
content: h("div", { style: "margin-top: 10px;" }, [
|
||||
h("div", { style: "margin-bottom: 8px;" }, [
|
||||
h("span", { style: "color: red;" }, "* "),
|
||||
h("span", "审批原因:"),
|
||||
content: h('div', { style: 'margin-top: 10px;' }, [
|
||||
h('div', { style: 'margin-bottom: 8px;' }, [
|
||||
h('span', { style: 'color: red;' }, '* '),
|
||||
h('span', '审批原因:')
|
||||
]),
|
||||
h("textarea", {
|
||||
class: "ant-input", // 使用 antdv 的样式类,使其看起来像 Antdv 输入框
|
||||
placeholder: "请输入审批原因",
|
||||
h('textarea', {
|
||||
class: 'ant-input', // 使用 antdv 的样式类,使其看起来像 Antdv 输入框
|
||||
placeholder: '请输入审批原因',
|
||||
rows: 4,
|
||||
style: {
|
||||
width: "100%",
|
||||
resize: "none",
|
||||
border: "1px solid #d9d9d9",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
width: '100%',
|
||||
resize: 'none',
|
||||
border: '1px solid #d9d9d9',
|
||||
padding: '8px',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
// 监听输入事件,更新局部变量
|
||||
onInput: (e: Event) => {
|
||||
rejectReason = (e.target as HTMLTextAreaElement).value;
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]),
|
||||
okText: "确认审批",
|
||||
cancelText: "取消",
|
||||
okText: '确认审批',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
let res: any = await successFishDraft({ ids: ids, approveComment: rejectReason });
|
||||
let res: any = await successFishDraft({
|
||||
ids: ids,
|
||||
approveComment: rejectReason
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("审批成功");
|
||||
message.success('审批成功');
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
// 批量审批-按钮
|
||||
@ -487,55 +512,59 @@ const successBtn = async () => {
|
||||
|
||||
// 批量驳回过鱼数据
|
||||
const handleReject = (id: any) => {
|
||||
let rejectReason = "";
|
||||
let rejectReason = '';
|
||||
Modal.confirm({
|
||||
title: "是否确认 驳回 选中数据?",
|
||||
title: '是否确认 驳回 选中数据?',
|
||||
// 使用 h 函数创建内容
|
||||
content: h("div", { style: "margin-top: 10px;" }, [
|
||||
h("div", { style: "margin-bottom: 8px;" }, [
|
||||
h("span", { style: "color: red;" }, "* "),
|
||||
h("span", "驳回原因:"),
|
||||
content: h('div', { style: 'margin-top: 10px;' }, [
|
||||
h('div', { style: 'margin-bottom: 8px;' }, [
|
||||
h('span', { style: 'color: red;' }, '* '),
|
||||
h('span', '驳回原因:')
|
||||
]),
|
||||
h("textarea", {
|
||||
class: "ant-input", // 使用 antdv 的样式类,使其看起来像 Antdv 输入框
|
||||
placeholder: "请输入驳回原因",
|
||||
h('textarea', {
|
||||
class: 'ant-input', // 使用 antdv 的样式类,使其看起来像 Antdv 输入框
|
||||
placeholder: '请输入驳回原因',
|
||||
rows: 4,
|
||||
style: {
|
||||
width: "100%",
|
||||
resize: "none",
|
||||
border: "1px solid #d9d9d9",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
width: '100%',
|
||||
resize: 'none',
|
||||
border: '1px solid #d9d9d9',
|
||||
padding: '8px',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
// 监听输入事件,更新局部变量
|
||||
onInput: (e: Event) => {
|
||||
rejectReason = (e.target as HTMLTextAreaElement).value;
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]),
|
||||
okText: "确认驳回",
|
||||
cancelText: "取消",
|
||||
okText: '确认驳回',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
// 校验驳回原因不能为空
|
||||
if (!rejectReason || rejectReason.trim() === "") {
|
||||
message.warning("请输入驳回原因");
|
||||
if (!rejectReason || rejectReason.trim() === '') {
|
||||
message.warning('请输入驳回原因');
|
||||
return Promise.reject(); // 阻止 Modal 关闭
|
||||
}
|
||||
|
||||
let res: any = await rejectFishDraft({ id: id, rejectReason: rejectReason });
|
||||
let res: any = await rejectFishDraft({
|
||||
id: id,
|
||||
rejectReason: rejectReason
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("驳回成功");
|
||||
message.success('驳回成功');
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
// 多选禁用
|
||||
const getCheckboxProps = (record: any) => {
|
||||
return {
|
||||
disabled: [!checkPerm(["sjtb:edit-review"]) && "PENDING", "APPROVED"].includes(
|
||||
record.status
|
||||
),
|
||||
disabled: [
|
||||
!checkPerm(['sjtb:edit-review']) && 'PENDING',
|
||||
'APPROVED'
|
||||
].includes(record.status)
|
||||
};
|
||||
};
|
||||
// 多选
|
||||
@ -551,10 +580,10 @@ const handleEditSubmit = async (values: FormData) => {
|
||||
if (currentRecord.value) {
|
||||
// 编辑逻辑
|
||||
let res: any = await editFishDraft({
|
||||
...values,
|
||||
...values
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("编辑成功");
|
||||
message.success('编辑成功');
|
||||
editModalVisible.value = false;
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
@ -562,10 +591,10 @@ const handleEditSubmit = async (values: FormData) => {
|
||||
} else {
|
||||
// 新增逻辑
|
||||
let res: any = await addFishDraft({
|
||||
...values,
|
||||
...values
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
message.success("新增成功");
|
||||
message.success('新增成功');
|
||||
editModalVisible.value = false;
|
||||
tableRef.value?.refresh();
|
||||
}
|
||||
@ -575,7 +604,7 @@ const handleEditSubmit = async (values: FormData) => {
|
||||
// 提交导入
|
||||
const handleModalOk = () => {
|
||||
if (modalTableRef.value.editingData != undefined) {
|
||||
message.warning("请点击保存后提交数据!");
|
||||
message.warning('请点击保存后提交数据!');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < fileTableData.value.length; i++) {
|
||||
@ -584,95 +613,95 @@ const handleModalOk = () => {
|
||||
// message.warning("请检查流域,流域填写有误!");
|
||||
// return;
|
||||
// }
|
||||
if (fileTableData.value[i].warnings.includes("hbrvcd")) {
|
||||
message.warning("请检查流域,流域填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('hbrvcd')) {
|
||||
message.warning('请检查流域,流域填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("rstcd")) {
|
||||
message.warning("请检查电站,电站填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('rstcd')) {
|
||||
message.warning('请检查电站,电站填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("stcd")) {
|
||||
message.warning("请检查过鱼设施,过鱼设施填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('stcd')) {
|
||||
message.warning('请检查过鱼设施,过鱼设施填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("strdt")) {
|
||||
message.warning("请检查过鱼时间,过鱼时间填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('strdt')) {
|
||||
message.warning('请检查过鱼时间,过鱼时间填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("ftp")) {
|
||||
message.warning("请检查鱼种类,鱼种类填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('ftp')) {
|
||||
message.warning('请检查鱼种类,鱼种类填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("direction")) {
|
||||
message.warning("请检查游向,游向填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('direction')) {
|
||||
message.warning('请检查游向,游向填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("isfs")) {
|
||||
message.warning("请检查是否鱼苗,是否鱼苗填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('isfs')) {
|
||||
message.warning('请检查是否鱼苗,是否鱼苗填写有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].warnings.includes("fcnt")) {
|
||||
message.warning("请检查过鱼数量,过鱼数量填写有误!");
|
||||
if (fileTableData.value[i].warnings.includes('fcnt')) {
|
||||
message.warning('请检查过鱼数量,过鱼数量填写有误!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (fileTableData.value[i].picpthsWarnings?.length > 0) {
|
||||
message.warning("请检查图片,图片路径有误!");
|
||||
message.warning('请检查图片,图片路径有误!');
|
||||
return;
|
||||
}
|
||||
if (fileTableData.value[i].vdpthsWarnings?.length > 0) {
|
||||
message.warning("请检查视频,视频路径有误!");
|
||||
message.warning('请检查视频,视频路径有误!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
Modal.confirm({
|
||||
title: "是否提交导入数据?",
|
||||
title: '是否提交导入数据?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
let res: any = await batchSaveDraft({
|
||||
taskId: taskId.value,
|
||||
taskId: taskId.value
|
||||
});
|
||||
if (res && res?.code == 0) {
|
||||
fileTableData.value = [];
|
||||
taskId.value = "";
|
||||
taskId.value = '';
|
||||
tableRef.value?.refresh();
|
||||
visible.value = false;
|
||||
message.success("导入成功");
|
||||
message.success('导入成功');
|
||||
} else {
|
||||
message.error("导入失败,请检查数据是否正确");
|
||||
message.error('导入失败,请检查数据是否正确');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("导入数据失败");
|
||||
message.error('导入数据失败');
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭导入弹窗
|
||||
const handleCustomCancel = () => {
|
||||
Modal.confirm({
|
||||
title: "是否取消导入数据?",
|
||||
content: "未提交的数据将丢失",
|
||||
okText: "确定",
|
||||
title: '是否取消导入数据?',
|
||||
content: '未提交的数据将丢失',
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
// 可选:调用取消接口
|
||||
let res: any = await cancelImportTask({ taskId: taskId.value });
|
||||
if (res && res?.code == 0) {
|
||||
fileTableData.value = [];
|
||||
taskId.value = "";
|
||||
message.success("取消成功");
|
||||
taskId.value = '';
|
||||
message.success('取消成功');
|
||||
tableRef.value?.refresh();
|
||||
visible.value = false;
|
||||
} else {
|
||||
message.error("取消失败");
|
||||
message.error('取消失败');
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
const importBtn = async () => {
|
||||
let res: any = await checkImportStatus();
|
||||
taskId.value = "";
|
||||
taskId.value = '';
|
||||
if (res?.code == 0) {
|
||||
const { hasImportingTask, currentTask } = res?.data || {};
|
||||
if (currentTask) {
|
||||
@ -681,17 +710,17 @@ const importBtn = async () => {
|
||||
if (hasImportingTask) {
|
||||
visible.value = true;
|
||||
fileLoading.value = true;
|
||||
if (currentTask.status == "UPLOADED") {
|
||||
if (currentTask.status == 'UPLOADED') {
|
||||
if (visible.value) {
|
||||
// message.warning(currentTask.statusText)
|
||||
setTimeout(() => {
|
||||
if (visible.value) importBtn();
|
||||
}, 5000);
|
||||
}
|
||||
} else if (currentTask.status == "FAILED") {
|
||||
} else if (currentTask.status == 'FAILED') {
|
||||
message.error(currentTask.statusText);
|
||||
fileLoading.value = false;
|
||||
} else if (currentTask.status == "VALIDATED") {
|
||||
} else if (currentTask.status == 'VALIDATED') {
|
||||
nextTick(async () => {
|
||||
modalTableRef.value.editingRowIndex = null;
|
||||
getFileList();
|
||||
@ -701,7 +730,7 @@ const importBtn = async () => {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
} else {
|
||||
message.error("导入查询失败");
|
||||
message.error('导入查询失败');
|
||||
}
|
||||
};
|
||||
const getFileList = async () => {
|
||||
@ -715,7 +744,7 @@ const fileChange = (file: File) => {
|
||||
fileLoading.value = true;
|
||||
fileTableData.value = [];
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append('file', file);
|
||||
let res: any = await importFishZip(formData);
|
||||
if (res && res?.code == 0) {
|
||||
if (visible.value) importBtn();
|
||||
@ -734,19 +763,19 @@ const handleFileSelect = (e: Event) => {
|
||||
// 校验文件大小 (300MB)
|
||||
const maxSize = 300 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
message.error("文件大小不能超过300MB");
|
||||
message.error('文件大小不能超过300MB');
|
||||
resetFileInput();
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验文件类型
|
||||
const isZip =
|
||||
file.name.toLowerCase().endsWith(".zip") ||
|
||||
file.type === "application/zip" ||
|
||||
file.type === "application/x-zip-compressed";
|
||||
file.name.toLowerCase().endsWith('.zip') ||
|
||||
file.type === 'application/zip' ||
|
||||
file.type === 'application/x-zip-compressed';
|
||||
|
||||
if (!isZip) {
|
||||
message.error("请选择.zip格式的压缩包");
|
||||
message.error('请选择.zip格式的压缩包');
|
||||
resetFileInput();
|
||||
return;
|
||||
}
|
||||
@ -756,7 +785,7 @@ const handleFileSelect = (e: Event) => {
|
||||
|
||||
const resetFileInput = () => {
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = "";
|
||||
fileInputRef.value.value = '';
|
||||
}
|
||||
};
|
||||
const fileTableaAnalysis = (res: any) => {
|
||||
@ -771,7 +800,7 @@ const fileTableaAnalysis = (res: any) => {
|
||||
return keyA - keyB;
|
||||
});
|
||||
|
||||
list.forEach((item) => {
|
||||
list.forEach(item => {
|
||||
data.push({
|
||||
...item.data,
|
||||
rowIndex: item.rowIndex,
|
||||
@ -779,13 +808,13 @@ const fileTableaAnalysis = (res: any) => {
|
||||
vdpthsWarnings: item.vdpthsWarnings,
|
||||
picpthList: item.picpthList,
|
||||
picpthsWarnings: item.picpthsWarnings,
|
||||
warnings: item.warnings,
|
||||
warnings: item.warnings
|
||||
});
|
||||
});
|
||||
fileTableData.value = data || [];
|
||||
fileLoading.value = false;
|
||||
};
|
||||
const handleReset = (values) => {
|
||||
const handleReset = values => {
|
||||
handleSearchFinish(values);
|
||||
};
|
||||
// 自定义数据转换函数
|
||||
@ -795,67 +824,68 @@ const customTransform = (res: any) => {
|
||||
return {
|
||||
...item,
|
||||
picpthList: item.picpth
|
||||
? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || []
|
||||
: [],
|
||||
? item.picpth.split(',').map((item: string) => baseUrl + '/?' + item) ||
|
||||
[]
|
||||
: []
|
||||
};
|
||||
});
|
||||
return {
|
||||
records: modifiedRecords,
|
||||
total: res?.data?.total || 0,
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
// 搜索-按钮
|
||||
const handleSearchFinish = (values: any) => {
|
||||
const filters = [
|
||||
values.ftp && {
|
||||
field: "ftp",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.ftp,
|
||||
field: 'ftp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.ftp
|
||||
},
|
||||
{
|
||||
field: "strdt",
|
||||
operator: "gte",
|
||||
dataType: "date",
|
||||
value: values.strdt[0] + " 00:00:00",
|
||||
field: 'strdt',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[0] + ' 00:00:00'
|
||||
},
|
||||
{
|
||||
field: "strdt",
|
||||
operator: "lte",
|
||||
dataType: "date",
|
||||
value: values.strdt[1] + " 23:59:59",
|
||||
field: 'strdt',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: values.strdt[1] + ' 23:59:59'
|
||||
},
|
||||
values.direction && {
|
||||
field: "direction",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.direction,
|
||||
field: 'direction',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.direction
|
||||
},
|
||||
// status: 'DRAFT',
|
||||
values.status && {
|
||||
field: "status",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.status,
|
||||
field: 'status',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.status
|
||||
},
|
||||
values.stcd && {
|
||||
field: "stcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.stcd,
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.stcd
|
||||
},
|
||||
values.rstcd && {
|
||||
field: "rstcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.rstcd,
|
||||
},
|
||||
values.hbrvcd !== "all" && {
|
||||
field: "hbrvcd",
|
||||
operator: "eq",
|
||||
dataType: "string",
|
||||
value: values.hbrvcd,
|
||||
field: 'rstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.rstcd
|
||||
},
|
||||
values.hbrvcd !== 'all' && {
|
||||
field: 'hbrvcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: values.hbrvcd
|
||||
}
|
||||
// values.baseId !== "all" && {
|
||||
// field: "baseId",
|
||||
// operator: "eq",
|
||||
@ -865,65 +895,70 @@ const handleSearchFinish = (values: any) => {
|
||||
].filter(Boolean);
|
||||
|
||||
const filter = {
|
||||
logic: "and",
|
||||
filters: filters,
|
||||
logic: 'and',
|
||||
filters: filters
|
||||
};
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
// 处理预览点击 (由子组件触发)
|
||||
const handlePreviewClick = (record: any, type: string, index: number, action: string) => {
|
||||
const handlePreviewClick = (
|
||||
record: any,
|
||||
type: string,
|
||||
index: number,
|
||||
action: string
|
||||
) => {
|
||||
const mixedList: MediaItem[] = [];
|
||||
if (action === "delete") {
|
||||
if (action === 'delete') {
|
||||
isPreviewMediaDelete.value = true;
|
||||
tablePreviewRecord.value = record;
|
||||
} else if (action === "preview") {
|
||||
} else if (action === 'preview') {
|
||||
isPreviewMediaDelete.value = false;
|
||||
}
|
||||
if (type === "image") {
|
||||
videoPreviewTitle.value = "图片预览";
|
||||
if (type === 'image') {
|
||||
videoPreviewTitle.value = '图片预览';
|
||||
const nameList = record.picpthList;
|
||||
|
||||
nameList.forEach((item: any) => {
|
||||
let url = "";
|
||||
if (action === "preview") {
|
||||
let url = '';
|
||||
if (action === 'preview') {
|
||||
url = item.url;
|
||||
} else if (action === "delete") {
|
||||
} else if (action === 'delete') {
|
||||
url =
|
||||
baseUrlApi +
|
||||
"/data/fishDraft/previewFile?filename=" +
|
||||
'/data/fishDraft/previewFile?filename=' +
|
||||
item.name +
|
||||
"&taskId=" +
|
||||
'&taskId=' +
|
||||
taskId.value +
|
||||
"&type=1";
|
||||
'&type=1';
|
||||
}
|
||||
mixedList.push({
|
||||
id: record.id,
|
||||
type: "image",
|
||||
type: 'image',
|
||||
name: item.name,
|
||||
url: url,
|
||||
url: url
|
||||
});
|
||||
});
|
||||
} else if (type === "video") {
|
||||
videoPreviewTitle.value = "视频预览";
|
||||
} else if (type === 'video') {
|
||||
videoPreviewTitle.value = '视频预览';
|
||||
const nameList = record.vdpthList;
|
||||
nameList.forEach((item: any) => {
|
||||
let url = "";
|
||||
if (action === "preview") {
|
||||
let url = '';
|
||||
if (action === 'preview') {
|
||||
url = item.url;
|
||||
} else if (action === "delete") {
|
||||
} else if (action === 'delete') {
|
||||
url =
|
||||
baseUrlApi +
|
||||
"/data/fishDraft/previewFile?filename=" +
|
||||
'/data/fishDraft/previewFile?filename=' +
|
||||
item.name +
|
||||
"&taskId=" +
|
||||
'&taskId=' +
|
||||
taskId.value +
|
||||
"&type=2";
|
||||
'&type=2';
|
||||
}
|
||||
mixedList.push({
|
||||
id: record.id,
|
||||
type: "video",
|
||||
type: 'video',
|
||||
name: item.name, // 视频通常没有单独的名称字段,可根据需要调整
|
||||
url: url,
|
||||
url: url
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -938,15 +973,16 @@ const handlePreviewClick = (record: any, type: string, index: number, action: st
|
||||
|
||||
const handleDeleteMedia = (item: any, type: string, index: number) => {
|
||||
previewList.value.splice(index, 1);
|
||||
if (type == "图片预览") {
|
||||
if (type == '图片预览') {
|
||||
if (previewList.value.length == 0) {
|
||||
tablePreviewRecord.value.picpthList = [];
|
||||
tablePreviewRecord.value.picpthsWarnings = [];
|
||||
} else {
|
||||
tablePreviewRecord.value.picpthList.splice(index, 1);
|
||||
tablePreviewRecord.value.picpthsWarnings = tablePreviewRecord.value.picpthsWarnings.filter(
|
||||
(warningName: any) => warningName !== item.name
|
||||
);
|
||||
tablePreviewRecord.value.picpthsWarnings =
|
||||
tablePreviewRecord.value.picpthsWarnings.filter(
|
||||
(warningName: any) => warningName !== item.name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (previewList.value.length == 0) {
|
||||
@ -954,9 +990,10 @@ const handleDeleteMedia = (item: any, type: string, index: number) => {
|
||||
tablePreviewRecord.value.vdpthsWarnings = [];
|
||||
} else {
|
||||
tablePreviewRecord.value.vdpthList.splice(index, 1);
|
||||
tablePreviewRecord.value.vdpthsWarnings = tablePreviewRecord.value.vdpthsWarnings.filter(
|
||||
(warningName: any) => warningName !== item.name
|
||||
);
|
||||
tablePreviewRecord.value.vdpthsWarnings =
|
||||
tablePreviewRecord.value.vdpthsWarnings.filter(
|
||||
(warningName: any) => warningName !== item.name
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -976,13 +1013,13 @@ onMounted(() => {
|
||||
observer.observe(tableContainerRef.value);
|
||||
}
|
||||
});
|
||||
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
|
||||
getDictItemsByCode({ dictCode: 'direction' }).then(res => {
|
||||
direction.value = res.data;
|
||||
});
|
||||
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
|
||||
getDictItemsByCode({ dictCode: 'approvalStatus' }).then(res => {
|
||||
guoyuStatus.value.length = 0;
|
||||
res.data.forEach((item: any) => {
|
||||
if (item.itemCode != "APPROVED") {
|
||||
if (item.itemCode != 'APPROVED') {
|
||||
guoyuStatus.value.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user