添加数据查询 基本信息 环保数据

This commit is contained in:
扈兆增 2026-06-25 10:21:53 +08:00
parent d177ebec69
commit 0364558f17
33 changed files with 8971 additions and 16 deletions

View File

@ -12,21 +12,12 @@ 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:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟 # 汤伟
<<<<<<< Updated upstream
<<<<<<< HEAD
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
# 李林
VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
=======
VITE_APP_BASE_URL = 'http://10.84.121.127:8093' VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
# VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
>>>>>>> fa4e1bc194446adb8adfd7f6404be1fbb50b31c3
=======
# VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
VITE_APP_BASE_URL = 'http://192.168.1.168:8093'
# 李林 # 李林
# VITE_APP_BASE_URL = 'http://10.84.121.43:8093' # VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
>>>>>>> Stashed changes # VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
# 李林
# VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
# 测试环境线上10.84.121.122: # 测试环境线上10.84.121.122:
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'

View File

@ -184,6 +184,7 @@
</div> --> </div> -->
<a-select <a-select
v-else-if="item.type === 'Select'" v-else-if="item.type === 'Select'"
:key="`${item.name}-${item.options?.length ?? 'loading'}`"
v-model:value="formData[item.name]" v-model:value="formData[item.name]"
:placeholder="item.placeholder || '请选择'" :placeholder="item.placeholder || '请选择'"
:allow-clear="item.fieldProps?.allowClear" :allow-clear="item.fieldProps?.allowClear"

View File

@ -74,12 +74,12 @@ const videoPreviewVisible = ref(false);
const previewVideoUrl = ref(''); const previewVideoUrl = ref('');
const openImagePreview = (url: string) => { const openImagePreview = (url: string) => {
previewImageUrl.value = url; previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
imagePreviewVisible.value = true; imagePreviewVisible.value = true;
}; };
const openVideoPreview = (url: string) => { const openVideoPreview = (url: string) => {
previewVideoUrl.value = url; previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
videoPreviewVisible.value = true; videoPreviewVisible.value = true;
}; };

View File

@ -134,12 +134,12 @@ const filterFtpOption = (input: string, option: any) => {
}; };
const openImagePreview = (url: string) => { const openImagePreview = (url: string) => {
previewImageUrl.value = url; previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
imagePreviewVisible.value = true; imagePreviewVisible.value = true;
}; };
const openVideoPreview = (url: string) => { const openVideoPreview = (url: string) => {
currentVideoUrl.value = url; currentVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
videoPreviewVisible.value = true; videoPreviewVisible.value = true;
}; };

View File

@ -0,0 +1,137 @@
import { ref, computed } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import { DateSetting } from '@/utils/enumeration';
export interface UseTimeScaleOptions {
defaultTimeScale?: string;
onSearchFinish?: (values: any) => void;
onReset?: (values: any) => void;
}
export function useTimeScale(options: UseTimeScaleOptions = {}) {
const defaultTimeScale = options.defaultTimeScale || 'tm';
const currentTimeScale = ref(defaultTimeScale);
const jcdt = ref({
min: null as string | null,
max: null as string | null
});
// 根据时间尺度动态切换 picker 类型
const timePicker = computed(() => {
if (currentTimeScale.value === 'tm') return 'date';
if (currentTimeScale.value === 'dt') return 'date';
return 'month';
});
// 根据时间尺度动态切换 format
const timeFormat = computed(() => {
if (currentTimeScale.value === 'tm') return 'YYYY-MM-DD HH:mm';
if (currentTimeScale.value === 'dt') return 'YYYY-MM-DD';
return 'YYYY-MM';
});
// 小时模式需要 showTime 配置
const timeShowTime = computed(() => {
if (currentTimeScale.value === 'tm') return { format: 'HH:mm' };
return undefined;
});
// 设置默认时间范围
const setDefaultTimeRange = (timeScale: string) => {
const now = dayjs();
const today = now.format('YYYY-MM-DD');
if (timeScale === 'tm') {
// 小时:默认前七天,使用当前时分秒
jcdt.value = {
min: `${dayjs().subtract(6, 'day').format('YYYY-MM-DD')} ${now.format(
'HH:mm:ss'
)}`,
max: `${today} ${now.format('HH:mm:ss')}`
};
} else if (timeScale === 'dt') {
// 日:默认前一个月
jcdt.value = {
min: `${dayjs().subtract(1, 'month').format('YYYY-MM-DD')} 00:00:00`,
max: `${today} 23:59:59`
};
} else {
// 月默认前4个月
jcdt.value = {
min: `${dayjs().subtract(4, 'month').format('YYYY-MM')}-01 00:00:00`,
max: `${today} 23:59:59`
};
}
};
const handleRangeClick = (value: any) => {
jcdt.value = {
min: dayjs(value.value[0]).format('YYYY-MM-DD HH:mm:ss'),
max: dayjs(value.value[1]).format('YYYY-MM-DD HH:mm:ss')
};
};
// 禁用起始时间之前的日期
const disabledEndDate = (current: Dayjs) => {
if (!jcdt.value.min) return false;
return current && current < dayjs(jcdt.value.min).endOf('day');
};
// 处理搜索完成
const handleSearchFinish = (values: any) => {
values.jcdt = {
min: values.jcdt?.min || jcdt.value.min,
max: values.jcdt?.max || jcdt.value.max
};
if (jcdt.value.min) values.jcdt.min = dayjs(jcdt.value.min);
if (jcdt.value.max) values.jcdt.max = dayjs(jcdt.value.max);
// 强制使用当前 timeScale不被传入值覆盖
values.timeScale = currentTimeScale.value;
options.onSearchFinish?.(values);
};
// 处理值变化
const handleValuesChange = (changedValues: any, allValues: any) => {
if (
changedValues.timeScale &&
changedValues.timeScale !== currentTimeScale.value
) {
currentTimeScale.value = changedValues.timeScale;
setDefaultTimeRange(changedValues.timeScale);
}
};
// 处理重置
const handleReset = () => {
jcdt.value = {
min: null,
max: null
};
currentTimeScale.value = defaultTimeScale;
setDefaultTimeRange(defaultTimeScale);
options.onReset?.({
timeScale: defaultTimeScale
});
};
// 初始化
const init = () => {
setDefaultTimeRange(defaultTimeScale);
};
return {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
};
}

View File

@ -0,0 +1,150 @@
import dayjs from 'dayjs';
interface TimeFilterValues {
timeScale: string;
jcdt: {
min: any;
max: any;
};
}
/**
*
* @param values - timeScale jcdt
* @returns
*/
export function buildTimeFilters(values: TimeFilterValues) {
const { timeScale, jcdt } = values;
const filters: any[] = [];
if (!jcdt || !jcdt.min || !jcdt.max) return filters;
if (timeScale === 'tm') {
// 小时:传 tm拆成 gte 和 lte
filters.push({
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(jcdt.min).format('YYYY-MM-DD HH:mm:ss')
});
filters.push({
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(jcdt.max).format('YYYY-MM-DD HH:mm:ss')
});
} else if (timeScale === 'dt') {
// 日:传 dt拆成 gte 和 lte
filters.push({
field: 'dt',
operator: 'gte',
dataType: 'date',
value: dayjs(jcdt.min).format('YYYY-MM-DD HH:mm:ss')
});
filters.push({
field: 'dt',
operator: 'lte',
dataType: 'date',
value: dayjs(jcdt.max).format('YYYY-MM-DD HH:mm:ss')
});
} else if (timeScale === 'month') {
// 月drtp 固定 MONyear 和 month 数组
const minDate = dayjs(jcdt.min);
const maxDate = dayjs(jcdt.max);
const minYear = minDate.year();
const maxYear = maxDate.year();
// 同年:传 drtp=MON + year + month 数组
if (minYear === maxYear) {
filters.push({
field: 'drtp',
operator: 'eq',
dataType: 'string',
value: 'MON'
});
filters.push({
field: 'year',
operator: 'eq',
dataType: 'string',
value: String(minYear)
});
const months: number[] = [];
let m = minDate.month() + 1;
const endMonth = maxDate.month() + 1;
while (true) {
months.push(m);
if (m === endMonth) break;
m = m >= 12 ? 1 : m + 1;
if (months.length > 12) break;
}
filters.push({
field: 'month',
operator: 'in',
dataType: 'number',
value: months
});
} else {
// 跨年:拆分成两个 AND 组合,用 OR 连接,不传 drtp
const firstYearMonths: number[] = [];
let m1 = minDate.month() + 1;
while (m1 <= 12) {
firstYearMonths.push(m1);
m1++;
}
filters.push({
logic: 'and',
filters: [
{
field: 'year',
operator: 'eq',
dataType: 'string',
value: String(minYear)
},
{
field: 'month',
operator: 'in',
dataType: 'number',
value: firstYearMonths
}
]
});
const secondYearMonths: number[] = [];
let m2 = 1;
const endMonth = maxDate.month() + 1;
while (m2 <= endMonth) {
secondYearMonths.push(m2);
m2++;
}
filters.push({
logic: 'and',
filters: [
{
field: 'year',
operator: 'eq',
dataType: 'string',
value: String(maxYear)
},
{
field: 'month',
operator: 'in',
dataType: 'number',
value: secondYearMonths
}
]
});
// 将 filters 数组转换为 OR 逻辑
return [
{
logic: 'or',
filters: filters
}
];
}
}
return filters;
}

View File

@ -0,0 +1,246 @@
/**
* Excel
* exceljs
*/
import * as ExcelJS from 'exceljs';
export interface ExportColumn {
title: string;
dataIndex: string;
customRender?: (params: { text: any; record: any; index: number }) => any;
/** 是否在导出时排除该列 */
excludeFromExport?: boolean;
}
export interface ExportParams {
/** 导出数据列表 */
data: any[];
/** 表格列配置 */
columns: ExportColumn[];
/** 导出文件名(不含扩展名) */
fileName?: string;
/** 表头主标题 */
tbtitle?: string;
/** 表头副标题 */
tbsub?: string;
/** 导出回调 */
callback?: (message: string | boolean) => void;
}
/**
* Excel
*/
function downloadExcel({
filename,
sheets,
callback
}: {
filename: string;
sheets: {
sheetName: string;
columns: ExportColumn[];
dataSource: any[];
tbtitle?: string;
tbsub?: string;
}[];
callback?: (message: string | boolean) => void;
}) {
try {
const workbook = new ExcelJS.Workbook();
for (const sheet of sheets) {
const { columns, dataSource, tbtitle, tbsub, sheetName } = sheet;
// 添加 sheet
const worksheet = workbook.addWorksheet(sheetName || 'Sheet1', {
properties: { tabColor: { argb: 'FFC0000' } }
});
// 固定列宽(根据定义宽度计算导出宽度)
worksheet.columns = columns.map(col => {
const definedWidth = col.width || 150;
// < 200: 29.22, 200-300: 39.22, 300-400: 59.22
const exportWidth = 29.22 + Math.max(0, (definedWidth - 150) / 50) * 10;
return { width: exportWidth };
});
// 样式定义
const headerStyle: Partial<ExcelJS.Style> = {
font: { bold: true, size: 14, name: '微软雅黑' },
alignment: {
wrapText: true,
vertical: 'middle',
horizontal: 'left'
}
};
const dataStyle: Partial<ExcelJS.Style> = {
font: { size: 12, name: '微软雅黑' },
alignment: {
wrapText: true,
vertical: 'middle',
horizontal: 'left'
}
};
// 当前行号
let currentRow = 1;
// 写入表头标题(如果有)
if (tbtitle) {
const titleRow = worksheet.getRow(currentRow);
titleRow.getCell(1).value = tbtitle;
titleRow.getCell(2).value = tbsub || '';
currentRow++;
}
// 写入表头行
const headerRow = worksheet.getRow(currentRow);
columns.forEach((col, index) => {
headerRow.getCell(index + 1).value = col.title;
headerRow.getCell(index + 1).style = headerStyle;
});
// 冻结表头行
worksheet.views = [{ state: 'frozen', ySplit: currentRow }];
currentRow++;
// 写入数据行
dataSource.forEach(record => {
const row = worksheet.getRow(currentRow);
columns.forEach((col, index) => {
const { dataIndex, customRender } = col;
if (!dataIndex) return;
let value = record[dataIndex];
// 如果有 customRender优先使用它处理后的值
if (customRender) {
try {
const renderResult = customRender({
text: value,
record,
index: currentRow - (tbtitle ? 3 : 2)
});
value = renderResult !== undefined ? renderResult : value;
} catch {
// customRender 执行失败,使用原始值
}
}
if (value === null || value === undefined || value === '') {
value = '-';
}
const cell = row.getCell(index + 1);
cell.value = value;
// 所有单元格统一应用样式,确保 - 占位符也居中对齐
cell.style = dataStyle;
});
currentRow++;
});
}
// 生成 Excel 文件并下载
workbook.xlsx
.writeBuffer()
.then(buffer => {
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
callback?.(true);
})
.catch(error => {
console.error('导出 Excel 失败:', error);
callback?.(false);
});
} catch (error) {
console.error('导出 Excel 失败:', error);
callback?.(false);
}
}
/**
*
*/
function resolveColumnTitle(col: ExportColumn): string | null {
const title = col.title;
if (typeof title === 'function') {
try {
const result = title();
// 如果函数返回的是字符串,使用它
if (typeof result === 'string') return result;
// 如果返回的是 VNode 或其他类型,无法导出标题
return null;
} catch {
return null;
}
}
if (typeof title === 'string') return title;
return null;
}
/**
*
*/
function getNestedValue(obj: any, path: string): any {
if (!path || !path.includes('.')) return obj?.[path];
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
* Excel
*/
export function exportTable({
data,
columns,
fileName = '导出数据',
tbtitle,
tbsub,
callback
}: ExportParams) {
if (!data || data.length === 0) {
callback?.('没有可导出的数据');
return;
}
// 过滤掉不需要导出的列,并解析函数类型的 title
const exportColumns: ExportColumn[] = [];
for (const col of columns) {
if (!col.dataIndex || col.dataIndex === 'action' || col.excludeFromExport) {
continue;
}
const resolvedTitle = resolveColumnTitle(col);
// 如果没有解析出标题,跳过该列
if (!resolvedTitle) continue;
exportColumns.push({
...col,
title: resolvedTitle
});
}
downloadExcel({
filename: fileName,
sheets: [
{
sheetName: fileName,
columns: exportColumns,
dataSource: data,
tbtitle,
tbsub
}
],
callback
});
}

View File

@ -0,0 +1,142 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
basin: null,
approvalFileNo: null,
ecologicalFlow: null,
fishPassageMeasures: null,
envMonitoringRequire: null,
bldsttCcode: null
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Input',
name: 'approvalFileNo',
label: '批复文号',
width: 140,
placeholder: '请输入批复文号',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Input',
name: 'ecologicalFlow',
label: '生态流量',
width: 140,
placeholder: '请输入生态流量',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Input',
name: 'fishPassageMeasures',
label: '过鱼措施',
width: 140,
placeholder: '请输入过鱼措施',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Input',
name: 'envMonitoringRequire',
label: '环境监测要求',
width: 170,
placeholder: '请输入环境监测要求',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'bldsttCcode',
label: '建设状态',
width: 140,
fieldProps: {},
options: [
{ label: '全部', value: '' },
{ label: '未建', value: '0' },
{ label: '在建', value: '1' },
{ label: '已建', value: '2' }
]
}
]);
const onSearchFinish = (values: any) => {
emit('search-finish', values);
};
const exportBtn = () => {
emit('export-btn');
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
};
const handleReset = () => {
emit('reset', initSearchData);
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
emit('search-finish', initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,305 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<ApprovalSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="columns"
:list-url="getApprovalStatusList"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue';
import dayjs from 'dayjs';
import ApprovalSearch from './ApprovalSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getApprovalStatusList } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
const tableScrollX = computed(() =>
Math.max(
columns.reduce((sum: number, col: any) => sum + (col.width || 180), 0),
600
)
);
const columns: any = [
{
key: 'index',
align: 'center',
title: '序号',
dataIndex: 'index',
visible: true,
width: 60,
fixed: 'left',
customRender: ({ text, record, index }) => index + 1,
excludeFromExport: true
},
{
key: 'basin',
title: '流域',
dataIndex: 'basin',
visible: true,
width: 150,
fixed: 'left',
ellipsis: true
},
{
key: 'river',
title: '河流',
dataIndex: 'river',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'hydropowerStation',
title: '电站',
dataIndex: 'hydropowerStation',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'bldsttCName',
title: '建设状态',
dataIndex: 'bldsttCName',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'approvalFileNo',
title: '批复文号',
dataIndex: 'approvalFileNo',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'ecologicalFlow',
title: '生态流量',
dataIndex: 'ecologicalFlow',
visible: true,
width: 120,
ellipsis: true
},
{
key: 'waterEnvProtection',
title: '水环境保护措施',
dataIndex: 'waterEnvProtection',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'coldWaterMitigation',
title: '低温水减缓措施',
dataIndex: 'coldWaterMitigation',
visible: true,
width: 180,
ellipsis: true
},
{
key: 'habitatProtection',
title: '栖息地保护',
dataIndex: 'habitatProtection',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'fishPassageMeasures',
title: '过鱼措施',
dataIndex: 'fishPassageMeasures',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'fishStocking',
title: '鱼类增殖放流',
dataIndex: 'fishStocking',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'terrestrialEcoSoilCons',
title: '陆生生态保护/水土保持措施',
dataIndex: 'terrestrialEcoSoilCons',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'rarePlantProtection',
title: '珍稀植物保护措施',
dataIndex: 'rarePlantProtection',
visible: true,
width: 180,
ellipsis: true
},
{
key: 'animalRescue',
title: '动物救助',
dataIndex: 'animalRescue',
visible: true,
width: 120,
ellipsis: true
},
{
key: 'resettlementMeasures',
title: '移民安置',
dataIndex: 'resettlementMeasures',
visible: true,
width: 120,
ellipsis: true
},
{
key: 'constructionPollutionCtrl',
title: '施工期防治措施或其他环境保护措施',
dataIndex: 'constructionPollutionCtrl',
visible: true,
width: 250,
ellipsis: true
},
{
key: 'groundwaterGeoProtect',
title: '地下水及地质环境保护措施',
dataIndex: 'groundwaterGeoProtect',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'scenicAreaProtection',
title: '风景名胜区保护措施',
dataIndex: 'scenicAreaProtection',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'envMonitoringRequire',
title: '环境监测要求',
dataIndex: 'envMonitoringRequire',
visible: true,
width: 150,
ellipsis: true
}
];
const onSearchFinish = (values: any) => {
currentSearchParams.value = values;
initTable(values);
};
const onReset = (values: any) => {
currentSearchParams.value = values;
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `批复文件数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const buildSearchParams = (values: any) => {
return {
logic: 'and',
filters: [
values.basin
? {
field: 'basin',
operator: 'contains',
dataType: 'string',
value: values.basin
}
: null,
values.approvalFileNo
? {
field: 'approvalFileNo',
operator: 'contains',
dataType: 'string',
value: values.approvalFileNo
}
: null,
values.ecologicalFlow
? {
field: 'ecologicalFlow',
operator: 'contains',
dataType: 'string',
value: values.ecologicalFlow
}
: null,
values.fishPassageMeasures
? {
field: 'fishPassageMeasures',
operator: 'contains',
dataType: 'string',
value: values.fishPassageMeasures
}
: null,
values.envMonitoringRequire
? {
field: 'envMonitoringRequire',
operator: 'contains',
dataType: 'string',
value: values.envMonitoringRequire
}
: null,
values.bldsttCcode
? {
field: 'bldsttCcode',
operator: 'eq',
dataType: 'string',
value: values.bldsttCcode
}
: null
].filter(Boolean)
};
};
const initTable = (values: any) => {
const params = buildSearchParams(values);
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,310 @@
<template>
<div class="basic-data-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #ttpwr>
<a-input-number
v-model:value="ttpwr.min"
:min="0"
placeholder="最小装机"
/>
~
<a-form-item-rest>
<a-input-number
v-model:value="ttpwr.max"
:min="0"
placeholder="最大装机"
@change="handleChange"
/>
</a-form-item-rest>
</template>
<template #jcdt>
<a-date-picker
v-model:value="jcdt.min"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
v-model:value="jcdt.max"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #dvtp>
<a-checkbox-group v-model:value="dvtp" :options="dvtpList" />
</template>
<template #actions>
<a-button :loading="btnLoading" @click="exportBtn">导出</a-button>
<a-button @click="customFilterBtn">自定义筛选列</a-button>
<a-button @click="hideBtn">{{ isHide ? '展开' : '隐藏' }}</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import dayjs, { type Dayjs } from 'dayjs';
import { message } from 'ant-design-vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'custom-filter-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
interface Props {
topHynmList: any[];
hycdList: any[];
topHynmLoading: boolean;
hycdLoading: boolean;
}
const props = withDefaults(defineProps<Props>(), {
topHynmList: () => [],
hycdList: () => [],
topHynmLoading: false,
hycdLoading: false
});
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const ttpwr = ref({
min: null,
max: null
});
const jcdt = ref({
min: null,
max: null
});
const dvtpList = ref<any[]>([
{ label: '堤坝式', value: '0' },
{ label: '引水式', value: '1' },
{ label: '混合式', value: '2' }
]);
const dvtp = ref([]);
const initSearchData = {
rvcd: 'all',
rstcd: null,
topHynm: null,
hycd: null,
baseId: null,
hbrvcd: null,
bldsttCcode: null,
dtin: 'all'
};
const isHide = ref<boolean>(false);
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'topHynm',
label: '所属集团',
width: 140,
fieldProps: {
allowClear: true,
placeholder: '请选择所属集团',
dropdownStyle: { maxHeight: 400, overflow: 'auto', width: 200 }
},
loading: props.topHynmLoading,
options: props.topHynmList.map((item: any) => ({
label: item.hynm,
value: item.hycd
}))
},
{
type: 'Select',
name: 'hycd',
label: '所属公司',
width: 140,
fieldProps: {
allowClear: true,
placeholder: '请选择所属公司',
dropdownStyle: { maxHeight: 400, overflow: 'auto', width: 200 }
},
loading: props.hycdLoading,
options: props.hycdList.map((item: any) => ({
label: item.hynm,
value: item.hycd
}))
},
{
col: isHide.value ? 0 : null,
type: 'custom',
name: 'ttpwr',
label: '装机容量(万kW)',
width: 140,
fieldProps: {
allowClear: true
},
options: []
},
{
col: isHide.value ? 0 : null,
type: 'custom',
name: 'jcdt',
label: '建设时间',
width: 140,
fieldProps: {
allowClear: true
},
options: []
},
{
col: isHide.value ? 0 : null,
type: 'Select',
name: 'bldsttCcode',
label: '建设状态',
width: 80,
fieldProps: {},
options: [
{ label: '全部', value: '' },
{ label: '未建', value: '0' },
{ label: '在建', value: '1' },
{ label: '已建', value: '2' }
]
},
{
col: isHide.value ? 0 : null,
type: 'Radio',
name: 'dtin',
label: '接入状态',
width: 240,
options: [
{ label: '所有', value: 'all' },
{ label: '未接入', value: '0' },
{ label: '已接入', value: '1' }
]
},
{
col: isHide.value ? 0 : null,
type: 'custom',
name: 'dvtp',
label: '开发方式',
width: 240
}
]);
const handleChange = (value: any) => {
if (ttpwr.value.min > value) {
message.warning('最小装机容量不能大于最大装机容量');
ttpwr.value.max = null;
}
};
const handleRangeClick = (value: any) => {
jcdt.value = {
min: dayjs(value.value[0]).format('YYYY-MM-DD HH:mm:ss'),
max: dayjs(value.value[1]).format('YYYY-MM-DD HH:mm:ss')
};
};
//
const disabledEndDate = (current: Dayjs) => {
if (!jcdt.value.min) return false;
return current && current < dayjs(jcdt.value.min).endOf('day');
};
const onSearchFinish = (values: any) => {
values.ttpwr = ttpwr.value;
if (jcdt.value.min)
values.jcdt.min = dayjs(jcdt.value.min).format('YYYY-MM-DD') + ' 00:00:00';
if (jcdt.value.max)
values.jcdt.max = dayjs(jcdt.value.max).format('YYYY-MM-DD') + ' 00:00:00';
values.dvtp = dvtp.value;
emit('search-finish', { ...values });
};
const hideBtn = () => {
isHide.value = !isHide.value;
};
const customFilterBtn = () => {
emit('custom-filter-btn');
};
const exportBtn = () => {
emit('export-btn');
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
};
const handleReset = () => {
ttpwr.value = {
min: null,
max: null
};
jcdt.value = {
min: null,
max: null
};
dvtp.value = [];
emit('reset', initSearchData);
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
emit('search-finish', { ...initSearchData, ...ttpwr.value, ...jcdt.value });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,297 @@
<template>
<a-modal
:open="visible"
title="自定义数据列"
:width="1280"
:style="{ maxWidth: '80vw' }"
:footer="null"
@cancel="handleClose"
:destroyOnClose="true"
class="custom-column-modal"
>
<div class="custom-column-content">
<div
v-for="(group, idx) in groupedColumns"
:key="idx"
class="column-group"
>
<!-- 分组标题行 -->
<div class="group-header">
<span class="group-title">{{ group.name }}</span>
<div class="group-actions">
<a-checkbox
:checked="isGroupAllSelected(group)"
:indeterminate="isGroupIndeterminate(group)"
:disabled="isGroupAllDisabled(group)"
@change="e => handleGroupSelectAll(group, e.target.checked)"
>
全选
</a-checkbox>
<a-checkbox
:disabled="isGroupAllDisabled(group)"
@change="() => handleGroupSelectInverse(group)"
>
反选
</a-checkbox>
</div>
</div>
<!-- 子项列表 -->
<div class="group-items">
<a-checkbox
v-for="item in group.children"
:key="item.filed"
:checked="tempSelectedKeys.includes(item.filed)"
:disabled="item.disabled"
@change="e => handleItemChange(item, e.target.checked)"
>
{{ item.name }}
</a-checkbox>
</div>
</div>
</div>
<div class="modal-footer">
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" @click="handleConfirm">确定</a-button>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { BasicColumns } from '@/components/MapModal/column.config';
interface ColumnItem {
name: string;
filed: string;
disabled?: boolean;
}
interface ColumnGroup {
name: string;
children: ColumnItem[];
}
interface Props {
visible: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'confirm', keys: string[]): void;
}>();
const groupedColumns = ref<ColumnGroup[]>([]);
const tempSelectedKeys = ref<string[]>([]);
// BasicColumns
const buildGroupedColumns = () => {
const groups: ColumnGroup[] = [];
let currentGroup: ColumnGroup | null = null;
for (const col of BasicColumns) {
if (!col.name && !col.filed) continue;
if (col.visible === false && col.name) {
currentGroup = {
name: col.name,
children: []
};
groups.push(currentGroup);
} else if (col.name && col.filed) {
if (!currentGroup) {
currentGroup = {
name: '基础信息',
children: []
};
groups.push(currentGroup);
}
const isStationName = col.filed === 'ennm';
currentGroup.children.push({
name: col.name,
filed: col.filed,
disabled: isStationName
});
}
}
return groups;
};
// filed
const STATION_NAME_FILED = 'ennm';
// +
const initSelectedKeys = () => {
const allKeys: string[] = [STATION_NAME_FILED];
for (const group of groupedColumns.value) {
for (const child of group.children) {
if (!child.disabled && child.filed !== STATION_NAME_FILED) {
allKeys.push(child.filed);
}
}
}
tempSelectedKeys.value = [...allKeys];
};
//
const isGroupAllSelected = (group: ColumnGroup) => {
const selectable = group.children.filter(child => !child.disabled);
if (selectable.length === 0) return false;
return selectable.every(child =>
tempSelectedKeys.value.includes(child.filed)
);
};
//
const isGroupIndeterminate = (group: ColumnGroup) => {
const selectable = group.children.filter(child => !child.disabled);
if (selectable.length === 0) return false;
const selectedCount = selectable.filter(child =>
tempSelectedKeys.value.includes(child.filed)
).length;
return selectedCount > 0 && selectedCount < selectable.length;
};
//
const isGroupAllDisabled = (group: ColumnGroup) => {
return group.children.every(child => child.disabled);
};
// /
const handleGroupSelectAll = (group: ColumnGroup, checked: boolean) => {
const selectableFiled = group.children
.filter(child => !child.disabled)
.map(child => child.filed);
if (checked) {
for (const filed of selectableFiled) {
if (!tempSelectedKeys.value.includes(filed)) {
tempSelectedKeys.value.push(filed);
}
}
} else {
const filedSet = new Set(selectableFiled);
tempSelectedKeys.value = tempSelectedKeys.value.filter(
filed => !filedSet.has(filed)
);
}
};
//
const handleGroupSelectInverse = (group: ColumnGroup) => {
for (const child of group.children) {
if (child.disabled) continue;
const index = tempSelectedKeys.value.indexOf(child.filed);
if (index > -1) {
tempSelectedKeys.value.splice(index, 1);
} else {
tempSelectedKeys.value.push(child.filed);
}
}
};
//
const handleItemChange = (item: ColumnItem, checked: boolean) => {
if (checked) {
if (!tempSelectedKeys.value.includes(item.filed)) {
tempSelectedKeys.value.push(item.filed);
}
} else {
tempSelectedKeys.value = tempSelectedKeys.value.filter(
filed => filed !== item.filed
);
}
};
const handleClose = () => {
emit('update:visible', false);
};
const handleConfirm = () => {
//
const keys = [...new Set([STATION_NAME_FILED, ...tempSelectedKeys.value])];
emit('confirm', keys);
emit('update:visible', false);
};
watch(
() => props.visible,
newVal => {
if (newVal) {
groupedColumns.value = buildGroupedColumns();
initSelectedKeys();
}
}
);
</script>
<style lang="scss" scoped>
.custom-column-modal {
:deep(.ant-modal-header) {
background-color: #005294;
.ant-modal-title {
color: #fff;
font-weight: 600;
}
}
.custom-column-content {
max-height: 500px;
overflow-y: auto;
padding: 8px 0;
.column-group {
margin-bottom: 16px;
border: 1px solid #e8e8e8;
border-radius: 4px;
padding: 12px;
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
.group-title {
font-weight: 600;
font-size: 14px;
color: #333;
}
.group-actions {
display: flex;
gap: 12px;
:deep(.ant-checkbox-wrapper) {
margin-right: 0;
}
}
}
.group-items {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
:deep(.ant-checkbox-wrapper) {
margin: 0;
font-size: 13px;
}
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<!-- 第一步输入数据来源 -->
<a-modal
v-model:open="dataSourceVisible"
title="删除电站"
ok-text="确定"
cancel-text="取消"
@ok="handleDataSourceConfirm"
>
<div>
<p style="color: red; margin-bottom: 8px;">
请输入数据来源以继续删除操作
</p>
<a-input
v-model:value="dataSource"
placeholder="请输入数据来源"
/>
</div>
</a-modal>
<!-- 第二步确认删除 -->
<a-modal
v-model:open="confirmVisible"
title="确认删除"
ok-text="确认删除"
cancel-text="取消"
:ok-button-props="{ danger: true }"
@ok="handleFinalDelete"
>
<div>
<p style="color: red; font-weight: bold;">
请慎重操作此操作不可恢复
</p>
<p>电站名称{{ deleteRecord?.stnm }}</p>
<p>数据来源{{ dataSource }}</p>
<p>确定要删除该电站吗</p>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { deletePowerInfo } from '@/api/DataQueryMenuModule';
const emit = defineEmits(['success']);
const dataSourceVisible = ref(false);
const confirmVisible = ref(false);
const dataSource = ref('');
const deleteRecord = ref<any>(null);
const onSuccess = ref<Function>(() => {});
//
const open = (record: any, callback: Function) => {
deleteRecord.value = record;
dataSource.value = '';
dataSourceVisible.value = true;
onSuccess.value = callback;
};
//
const handleDataSourceConfirm = () => {
if (!dataSource.value || dataSource.value.trim() === '') {
message.error('请输入数据来源');
return;
}
dataSourceVisible.value = false;
confirmVisible.value = true;
};
//
const handleFinalDelete = async () => {
try {
const res = await deletePowerInfo({ stcd: deleteRecord.value.stcd });
if (res?.code === 200 || res?.success) {
message.success('删除成功');
confirmVisible.value = false;
onSuccess.value();
} else {
message.error(res?.msg || '删除失败');
}
} catch (error) {
message.error('删除失败,请重试');
}
};
defineExpose({ open });
</script>

View File

@ -0,0 +1,251 @@
<template>
<a-modal
v-model:open="visible"
title="编辑电站"
width="80vw"
:confirm-loading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
class="max-h-[70vh] overflow-y-auto pr-4"
>
<a-row :gutter="16">
<template v-for="(col, index) in formColumns" :key="index">
<!-- 分组标题 -->
<template v-if="col.isTitle">
<a-col :span="24">
<div class="form-group-title">{{ col.name }}</div>
</a-col>
</template>
<!-- 表单字段 -->
<a-col v-else :span="8">
<a-form-item
:label="col.name"
:name="col.filed"
:rules="getRules(col)"
>
<a-form-item-rest>
<!-- 输入框 -->
<a-input
v-if="col.type === 'input'"
v-model:value="formData[col.filed]"
:placeholder="`请输入${col.name}`"
/>
<!-- 数字 -->
<a-input-number
v-else-if="col.type === 'number'"
v-model:value="formData[col.filed]"
:placeholder="`请输入${col.name}`"
:precision="col.toFixed"
style="width: 100%"
/>
<!-- 日期 -->
<a-date-picker
v-else-if="col.type === 'date'"
v-model:value="formData[col.filed]"
:format="col.format || 'YYYY-MM-DD'"
:value-format="col.format || 'YYYY-MM-DD'"
:placeholder="`请选择${col.name}`"
style="width: 100%"
/>
<!-- 下拉选择 - 所属集团 -->
<a-select
v-else-if="col.type === 'select' && col.filed === 'topHynm'"
v-model:value="formData[col.filed]"
:placeholder="`请选择${col.name}`"
allow-clear
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="item in topHynmList"
:key="item.hycd"
:label="item.hynm"
:value="item.hycd"
>
{{ item.hynm }}
</a-select-option>
</a-select>
<!-- 下拉选择 - 所属公司 -->
<a-select
v-else-if="col.type === 'select' && col.filed === 'hynm'"
v-model:value="formData[col.filed]"
:placeholder="`请选择${col.name}`"
allow-clear
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="item in hycdList"
:key="item.hycd"
:label="item.hynm"
:value="item.hycd"
>
{{ item.hynm }}
</a-select-option>
</a-select>
<!-- 下拉选择 - 开发方式 -->
<a-select
v-else-if="col.type === 'select' && col.filed === 'dvtpName'"
v-model:value="formData[col.filed]"
:placeholder="`请选择${col.name}`"
allow-clear
>
<a-select-option
v-for="item in dvtpList"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
<!-- 下拉选择 - 其他 -->
<a-select
v-else-if="col.type === 'select'"
v-model:value="formData[col.filed]"
:placeholder="`请选择${col.name}`"
allow-clear
>
<!-- 这里可以根据需要添加选项 -->
</a-select>
<!-- 默认输入框 -->
<a-input
v-else
v-model:value="formData[col.filed]"
:placeholder="`请输入${col.name}`"
/>
</a-form-item-rest>
</a-form-item>
</a-col>
</template>
</a-row>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import { BasicColumns } from '@/components/MapModal/column.config';
import { updatePowerInfo } from '@/api/DataQueryMenuModule';
const props = defineProps<{
open: boolean;
record?: any;
topHynmList?: any[];
hycdList?: any[];
}>();
const emit = defineEmits(['update:open', 'success']);
const visible = computed({
get: () => props.open,
set: val => emit('update:open', val)
});
const formRef = ref();
const confirmLoading = ref(false);
const formData = ref<any>({});
//
const dvtpList = ref<any[]>([
{ label: '堤坝式', value: '0' },
{ label: '引水式', value: '1' },
{ label: '混合式', value: '2' }
]);
// BasicColumns visible: false
const formColumns = computed(() => {
return BasicColumns.filter(col => col.filed !== '' && col.type !== '').map(
col => ({
...col,
isTitle: !col.visible
})
);
});
//
const getRules = (col: any) => {
const rules: any[] = [];
//
if (col.type === 'number') {
rules.push({
type: 'number',
message: `${col.name}必须是数字`
});
}
return rules;
};
const filterOption = (inputValue: string, option: any) => {
const label = option.label || '';
const keyword = inputValue || '';
return label.includes(keyword);
};
// "-"null
const convertDashToNull = (obj: any) => {
const result: any = {};
for (const key in obj) {
if (obj[key] === '-' || obj[key] === '') {
result[key] = null;
} else {
result[key] = obj[key];
}
}
return result;
};
// record
watch(
() => props.record,
newRecord => {
if (newRecord) {
formData.value = convertDashToNull({ ...newRecord });
}
},
{ deep: true }
);
const handleOk = async () => {
try {
await formRef.value?.validateFields();
confirmLoading.value = true;
// "-"null
const submitData = convertDashToNull(formData.value);
const res = await updatePowerInfo(submitData);
if (res?.code === 200 || res?.success) {
message.success('编辑成功');
visible.value = false;
emit('success');
} else {
message.error(res?.msg || '编辑失败');
}
} catch (error) {
console.error('验证失败:', error);
} finally {
confirmLoading.value = false;
}
};
const handleCancel = () => {
visible.value = false;
formRef.value?.resetFields();
};
</script>
<style scoped lang="scss">
.form-group-title {
font-size: 16px;
font-weight: 600;
color: #333;
padding: 12px 0;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<div class="approval-detail-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #actions>
<a-button @click="exportBtn" :loading="exportLoading">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import moment from 'moment';
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
interface Props {
exportBtn: () => void;
fish_ptype: any[];
familyList: any[];
ptypeLoading?: boolean;
familyLoading?: boolean;
exportLoading?: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const localTypeDate = ref<string>(null);
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const initSearchData = {
rvcd: 'all',
stcd: null,
ptype: 'all',
family: '全部'
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'ptype',
label: '保护类别',
fieldProps: {
allowClear: false
},
loading: props.ptypeLoading,
options: props.fish_ptype.map((item: any) => ({
label: item.dictName,
value: item.itemCode
}))
},
{
type: 'Select',
name: 'family',
label: '科',
fieldProps: {
allowClear: false
},
loading: props.familyLoading,
options: props.familyList.map((item: any) => ({
label: item.key,
value: item.key
}))
},
{
type: 'Input',
name: 'name',
label: '',
placeholder: '请输入鱼类名称',
fieldProps: {}
},
{
type: 'Input',
name: 'alias',
label: '',
placeholder: '请输入鱼类俗名',
fieldProps: {}
}
]);
const onSearchFinish = (values: any) => {
emit('search-finish', values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
if (
Object.keys(changedValues)[0] == 'rstcd' ||
Object.keys(changedValues)[0] == 'rvcd'
) {
const formInstance = basicSearchRef.value?.formData;
formInstance.stcd = null;
}
};
const handleReset = () => {
localTypeDate.value = null;
emit('reset', initSearchData);
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
emit('search-finish', initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,606 @@
<template>
<div class="w-full h-full">
<!-- 搜索组件 -->
<FishResourceSearch
:fish_ptype="fish_ptype_list"
:familyList="familyList"
:ptypeLoading="ptypeLoading"
:familyLoading="familyLoading"
:exportLoading="exportLoading"
:exportBtn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
ref="searchRef"
/>
<a-row type="flex" class="w-full h-full" style="height: calc(100% - 80px)">
<a-col flex="75%" class="pr-4 h-full flex overflow-auto">
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="columns"
:list-url="getFishResourceZYList"
:transformData="transformData"
:enable-row-highlight="true"
@row-click="handleRowClick"
>
</BasicTable>
</a-col>
<a-col flex="25%" class="overflow-hidden">
<div
ref="rightPanelRef"
class="bg-[#fafafa] pt-[10px] pr-[15px] pl-[15px]"
style="height: calc(100% - 40px); overflow: hidden"
>
<a-spin :spinning="rightPanelLoading">
<div style="display: flex; align-items: center">
<div
style="
width: 4px;
height: 18px;
background-color: rgb(47, 107, 152);
margin-right: 10px;
"
></div>
<div style="font-size: 18px; color: rgba(0, 0, 0, 0.85)">
{{ fishResource.name }}
</div>
</div>
<div
:style="{ width: carouselWidth + 'px' }"
class="mt-[10px] px-[10px]"
>
<a-carousel
arrows
v-if="fishResource.logoList.length > 0"
class="w-full h-[306px] slick-arrow"
>
<template #prevArrow>
<div
class="custom-slick-arrow"
style="left: 10px; z-index: 1"
>
<left-circle-outlined />
</div>
</template>
<template #nextArrow>
<div class="custom-slick-arrow" style="right: 10px">
<right-circle-outlined />
</div>
</template>
<div v-for="(img, index) in fishResource.logoList" :key="index">
<a-image :src="img" class="fishResource-image" />
</div>
</a-carousel>
<a-empty v-else description="暂无图片" class="h-[306px]" />
</div>
<div
class="w-full px-[10px]"
style="
margin-top: 10px;
font-size: 18px;
color: rgba(0, 0, 0, 0.85);
max-height: 440px;
overflow-y: scroll;
"
>
{{ fishResource.shapedesc }}
</div>
</a-spin>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import {
ref,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick,
h
} from 'vue';
import dayjs from 'dayjs';
import FishResourceSearch from './FishResourceSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
import { getDictItemsByCode } from '@/api/dict';
import {
getFishResourceList,
getFishResourceZYList
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const fishResource = ref<any>({
name: '',
logoList: [],
shapedesc: ''
});
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
const rightPanelRef = ref<HTMLElement | null>(null);
const carouselWidth = ref(0);
const tableScrollY = ref<string | number>(0);
const updateCarouselWidth = () => {
if (rightPanelRef.value) {
const rect = rightPanelRef.value.getBoundingClientRect();
carouselWidth.value = rect.width - 20; // px-[10px] padding
}
};
const updateTableScrollY = () => {
nextTick(() => {
tableScrollY.value = calcTableScrollY(rightPanelRef.value);
});
};
const resizeObserver = new ResizeObserver(() => {
updateCarouselWidth();
updateTableScrollY();
});
const tableScrollX = computed(() =>
Math.max(
columns.reduce((sum: number, col: any) => sum + (col.width || 180), 0),
600
)
);
const columns: any = [
{
key: 'index',
title: '序号',
dataIndex: 'index',
visible: true,
width: 80,
fixed: 'left'
},
{
key: 'name',
title: '中文名称',
dataIndex: 'name',
visible: true,
width: 150,
ellipsis: true,
fixed: 'left',
customRender: ({ text }: any) => {
return text
? h('span', { style: { color: '#2f6b98', cursor: 'pointer' } }, text)
: '-';
}
},
{
key: 'nameEn',
title: '拉丁学名',
dataIndex: 'nameEn',
visible: true,
width: 200,
ellipsis: true,
fixed: 'left'
},
{
key: 'rvcdName',
title: '所属流域',
dataIndex: 'rvcdName',
visible: true,
width: 100,
ellipsis: true,
fixed: 'left'
},
{
key: 'typeName',
title: '分类',
dataIndex: 'typeName',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'species',
title: '种',
dataIndex: 'species',
visible: true,
width: 150,
ellipsis: true
},
{
key: 'genus',
title: '属',
dataIndex: 'genus',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'family',
title: '科',
dataIndex: 'family',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'orders',
title: '目',
dataIndex: 'orders',
visible: true,
width: 150,
ellipsis: true
},
// {
// "key": "nameEn",
// "title": "",
// "dataIndex": "nameEn",
// "visible": true,
// width: 200,
// ellipsis: true,
// },
{
key: 'alias',
title: '俗名',
dataIndex: 'alias',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'fsz',
title: '成鱼大小(cm)',
dataIndex: 'fsz',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'shapedesc',
title: '形态描述',
dataIndex: 'shapedesc',
visible: true,
width: 300,
ellipsis: true
},
{
key: 'habitation',
title: '栖息习性',
dataIndex: 'habitation',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'habitMigrat',
title: '洄游习性',
dataIndex: 'habitMigrat',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'feedingHabit',
title: '摄食习性',
dataIndex: 'feedingHabit',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'food',
title: '主要食物',
dataIndex: 'food',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'timeFeed',
title: '觅食时段',
dataIndex: 'timeFeed',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'spawnMonth',
title: '产卵期(月)',
dataIndex: 'spawnMonth',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'orignDate',
title: '产地及产期',
dataIndex: 'orignDate',
visible: true,
width: 300,
ellipsis: true
},
{
key: 'description',
title: '内容',
dataIndex: 'description',
visible: true,
width: 200,
ellipsis: true
},
{
key: 'pretemp',
title: '适宜温度',
dataIndex: 'pretemp',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'flowRate',
title: '适宜流速',
dataIndex: 'flowRate',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'depth',
title: '适宜水深(m)',
dataIndex: 'depth',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'wqtq',
title: '水质要求',
dataIndex: 'wqtq',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'spawnCharact',
title: '型',
dataIndex: 'spawnCharact',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'botmMater',
title: '地质',
dataIndex: 'botmMater',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'ptypeName',
title: '保护类型',
dataIndex: 'ptypeName',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'specOriginName',
title: '物种来源',
dataIndex: 'specOriginName',
visible: true,
width: 100,
ellipsis: true
},
{
key: 'vlsr',
title: '数据来源',
dataIndex: 'vlsr',
visible: true,
width: 150,
ellipsis: true
}
// {
// "key": "inffile",
// "title": "",
// "dataIndex": "inffile",
// "visible": true,
// width: 100,
// ellipsis: true,
// "fixed": "right",
// },
];
const tableRef = ref();
const searchRef = ref();
const exportLoading = ref(false);
const currentSearchParams = ref<any>({});
const fish_ptype_list = ref<any>([]);
const familyList = ref<any>([]);
const ptypeLoading = ref(false);
const familyLoading = ref(false);
const rightPanelLoading = ref(false);
//
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `鱼类资源_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const handleRowClick = (record: any) => {
rightPanelLoading.value = true;
// inffile
let logoList: string[] = [];
if (record.inffile && record.inffile !== '-') {
// URL
logoList = record.inffile
.split(',')
.map((item: string) => item.trim())
.filter((item: string) => item && item !== '-' && item.length > 0)
.map((item: string) => `${baseUrl}/?${item}&view=jpg`);
}
fishResource.value = {
name: record.name,
logoList,
shapedesc: record.shapedesc
};
// loading
setTimeout(() => {
rightPanelLoading.value = false;
}, 200);
};
const onSearchFinish = (values: any) => {
fishResource.value = {
name: '',
logoList: [],
shapedesc: ''
};
console.log(values);
initTable(values);
};
const transformData = (res: any) => {
console.log(res);
if (res?.data?.data) {
handleRowClick(res?.data?.data[0]);
}
return res?.data || [];
};
//
const onReset = (values: any) => {
currentSearchParams.value = values;
initTable(values);
};
const initTable = (values: any) => {
const params = buildSearchParams(values);
currentSearchParams.value = params;
tableRef.value.getList(params);
};
const buildSearchParams = (values: any) => {
return {
logic: 'and',
filters: [
values.ptype && values.ptype !== 'all'
? {
field: 'ptype',
operator: 'eq',
dataType: 'string',
value: values.ptype
}
: null,
values.family && values.family !== 'all' && values.family !== '全部'
? {
field: 'family',
operator: 'eq',
dataType: 'string',
value: values.family
}
: null,
values.name
? {
field: 'name',
operator: 'contains',
dataType: 'string',
value: values.name
}
: null,
values.alias
? {
field: 'alias',
operator: 'contains',
dataType: 'string',
value: values.alias
}
: null
].filter(Boolean)
};
};
const init = () => {
ptypeLoading.value = true;
familyLoading.value = true;
getDictItemsByCode({ dictCode: 'FISH_PTYPE' }).then(res => {
res.data.unshift({
itemCode: 'all',
dictName: '全部'
});
fish_ptype_list.value = res.data;
ptypeLoading.value = false;
});
const params = {
group: [
{
field: 'family',
dir: 'asc'
}
]
};
getFishResourceList(params).then(res => {
res?.data?.data.unshift({
count: '全部',
key: '全部'
});
familyList.value = res?.data?.data || [];
familyLoading.value = false;
});
};
onMounted(() => {
init();
nextTick(() => {
updateCarouselWidth();
updateTableScrollY();
if (rightPanelRef.value) {
resizeObserver.observe(rightPanelRef.value);
}
});
});
onBeforeUnmount(() => {
resizeObserver.disconnect();
});
</script>
<style scoped lang="scss">
:deep(.slick-slide) {
text-align: center;
overflow: hidden;
}
:deep(.slick-arrow.custom-slick-arrow) {
width: 25px;
height: 25px;
font-size: 25px;
color: #fff;
background-color: rgba(31, 45, 61, 0.11);
transition: ease all 0.3s;
opacity: 0.3;
z-index: 1;
}
:deep(.slick-arrow.custom-slick-arrow:before) {
display: none;
}
:deep(.slick-arrow.custom-slick-arrow:hover) {
color: #fff;
opacity: 0.5;
}
:deep(.slick-slide h3) {
color: #fff;
}
:deep(.fishResource-image) {
width: 100%;
height: 306px;
object-fit: cover;
}
</style>

View File

@ -0,0 +1,324 @@
<template>
<div class="export-container">
<a-form
ref="formRef"
:model="formData"
:labelCol="{ span: 6 }"
:wrapperCol="{ span: 18 }"
class="export-form"
>
<!-- 时间维度 -->
<a-form-item label="时间维度" name="timeDimension">
<a-radio-group v-model:value="formData.timeDimension">
<a-radio value="month">月度统计</a-radio>
<a-radio value="year">年统计</a-radio>
</a-radio-group>
</a-form-item>
<!-- 月度统计 -->
<template v-if="formData.timeDimension === 'month'">
<a-form-item label="统计监测要素" name="monitorElements">
<a-checkbox-group v-model:value="formData.monitorElements">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="选择水电站">
<a-row :gutter="16">
<a-col :span="12">
<a-select
v-model:value="formData.rvcd"
placeholder="请选择流域"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="rvcdLoading"
@change="handleRvcdChange"
style="width: 100%"
>
<a-select-option
v-for="item in rvcdOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-col>
<a-col :span="12">
<a-select
v-model:value="formData.stations"
placeholder="请选择水电站"
mode="multiple"
allow-clear
show-search
:filter-option="filterOption"
:loading="stationLoading"
style="width: 100%"
>
<a-select-option
v-for="item in stationOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
<a-form-item label="统计月份" name="statMonths">
<a-tree-select
v-model:value="formData.statMonths"
:tree-data="monthTreeData"
placeholder="请选择统计月份"
tree-checkable
:fieldNames="{
label: 'title',
value: 'value',
children: 'children'
}"
tree-node-filter-prop="title"
show-search
allow-clear
style="width: 100%"
/>
</a-form-item>
<a-form-item label="数据来源" name="dataSource">
<a-radio-group v-model:value="formData.dataSource">
<a-radio value="day">日统计表</a-radio>
<a-radio value="hour">小时表</a-radio>
</a-radio-group>
</a-form-item>
</template>
<!-- 年统计 -->
<template v-if="formData.timeDimension === 'year'">
<a-form-item label="统计内容" name="yearStatContent">
<a-checkbox-group v-model:value="formData.yearStatContent">
<a-checkbox value="flow">生态流量</a-checkbox>
<a-checkbox value="waterQuality">水质监测</a-checkbox>
<a-checkbox value="verticalTemp">垂向水温</a-checkbox>
<a-checkbox value="surfaceTemp">表层水温</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="统计年份" name="statYears">
<a-tree-select
v-model:value="formData.statYears"
:tree-data="yearTreeData"
placeholder="请选择统计年份"
tree-checkable
:fieldNames="{
label: 'title',
value: 'value',
children: 'children'
}"
tree-node-filter-prop="title"
show-search
allow-clear
style="width: 100%"
/>
</a-form-item>
<a-form-item label="显示维度统计" name="dimensionStat">
<a-input
v-model:value="formData.dimensionStat"
placeholder="请输入显示维度统计"
allow-clear
/>
</a-form-item>
<a-form-item label="内容" name="content">
<a-input
v-model:value="formData.content"
placeholder="请输入内容"
allow-clear
/>
</a-form-item>
</template>
<!-- 导出按钮 -->
<a-form-item :wrapperCol="{ offset: 6, span: 18 }">
<a-button type="primary" :loading="exportLoading" @click="handleExport">
导出
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { getSelectForDropdown, getEngInfoDropdown } from '@/api/select';
const formRef = ref<any>();
const exportLoading = ref(false);
const rvcdLoading = ref(false);
const stationLoading = ref(false);
//
const rvcdOptions = ref<{ label: string; value: string }[]>([]);
//
const stationOptions = ref<{ label: string; value: string }[]>([]);
//
const formData = reactive({
timeDimension: 'month' as 'month' | 'year',
//
monitorElements: [] as string[],
rvcd: [] as string[],
stations: [] as string[],
statMonths: [] as string[],
dataSource: 'day' as 'day' | 'hour',
//
yearStatContent: [] as string[],
statYears: [] as string[],
dimensionStat: '',
content: ''
});
//
const monthTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const years: any[] = [];
for (let y = currentYear; y >= currentYear - 5; y--) {
years.push({
title: `${y}`,
value: `year_${y}`,
children: Array.from({ length: 12 }, (_, i) => ({
title: `${y}${i + 1}`,
value: `${y}-${String(i + 1).padStart(2, '0')}`
}))
});
}
return years;
});
//
const yearTreeData = computed(() => {
const currentYear = new Date().getFullYear();
const decades: any[] = [];
const startDecade = Math.floor((currentYear - 20) / 10) * 10;
const endDecade = Math.floor(currentYear / 10) * 10;
for (let d = endDecade; d >= startDecade; d -= 10) {
const years: any[] = [];
for (let y = d; y < d + 10 && y <= currentYear; y++) {
years.push({ title: `${y}`, value: `${y}` });
}
if (years.length > 0) {
decades.push({
title: `${d}s`,
value: `decade_${d}`,
children: years
});
}
}
return decades;
});
//
const filterOption = (input: string, option: any) => {
return (option?.label || '').toLowerCase().includes(input.toLowerCase());
};
//
const loadRvcdOptions = async () => {
rvcdLoading.value = true;
try {
const res = await getSelectForDropdown({});
if (res?.data) {
rvcdOptions.value = (res.data || []).map((item: any) => ({
label: item.rvnm || item.label || item.name,
value: item.rvcd || item.value || item.id
}));
}
} catch (e) {
console.error('获取流域列表失败', e);
} finally {
rvcdLoading.value = false;
}
};
//
const loadStationOptions = async (rvcds?: string[]) => {
stationLoading.value = true;
try {
const params: any = {};
if (rvcds && rvcds.length > 0) {
params.rvcds = rvcds;
}
const res = await getEngInfoDropdown(params);
if (res?.data) {
stationOptions.value = (res.data || []).map((item: any) => ({
label: item.engnm || item.stnm || item.label || item.name,
value: item.stcd || item.engId || item.value || item.id
}));
}
} catch (e) {
console.error('获取水电站列表失败', e);
} finally {
stationLoading.value = false;
}
};
//
const handleRvcdChange = (values: string[]) => {
formData.stations = [];
loadStationOptions(values);
};
//
const handleExport = () => {
exportLoading.value = true;
try {
if (formData.timeDimension === 'month') {
console.log('月度统计导出参数:', {
monitorElements: formData.monitorElements,
rvcd: formData.rvcd,
stations: formData.stations,
statMonths: formData.statMonths,
dataSource: formData.dataSource
});
} else {
console.log('年统计导出参数:', {
yearStatContent: formData.yearStatContent,
statYears: formData.statYears,
dimensionStat: formData.dimensionStat,
content: formData.content
});
}
message.success('导出任务已提交');
} catch (e) {
message.error('导出失败');
} finally {
exportLoading.value = false;
}
};
onMounted(() => {
loadRvcdOptions();
loadStationOptions();
});
</script>
<style scoped lang="scss">
.export-container {
padding: 20px;
max-width: 800px;
.export-form {
:deep(.ant-form-item) {
margin-bottom: 20px;
}
}
}
</style>

View File

@ -0,0 +1,255 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[120px]"
v-model:value="jcdt.min"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[120px]"
v-model:value="jcdt.max"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import dayjs from 'dayjs';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
import { getFishReleaseMonitorSectionList } from '@/api/DataQueryMenuModule';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
mway: '1'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const crossSectionListLoading = ref<any>(false);
const crossSectionList = ref<any>([]);
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'stcd',
label: '过鱼设施名称',
width: 200,
placeholder: '请选择过鱼设施名称',
fieldProps: {
allowClear: true
},
loading: crossSectionListLoading.value,
options: crossSectionList.value.map(item => ({
label: item.stnm,
value: item.stcd
}))
},
{
type: 'Radio',
name: 'mway',
label: '数据类型',
width: 140,
options: [
{
label: '人工',
value: '1'
},
{
label: '自动',
value: '2'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const initCrossSectionList = params => {
const filters = [];
if (params) {
if (params.rvcd != '' && params.rvcd != 'all') {
filters.push({
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: params.rvcd
});
}
if (params.rstcd != '' && params.rstcd != null) {
filters.push({
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: params.rstcd
});
}
}
crossSectionListLoading.value = true;
getFishReleaseMonitorSectionList({
filter: {
logic: 'and',
filters: filters
},
select: ['stcd', 'stnm', 'wtDeviceType', 'dtinType', 'mway']
}).then(res => {
if (res.data?.data) {
crossSectionList.value = res.data?.data;
}
crossSectionListLoading.value = false;
});
};
const onValuesChange = (changedValues: any, allValues: any) => {
//
if (changedValues.rvcd || changedValues.rstcd !== undefined) {
allValues.stcd = null;
if (basicSearchRef.value?.formData) {
basicSearchRef.value.formData.stcd = null;
}
initCrossSectionList(allValues);
}
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
const now = dayjs();
jcdt.value = {
min: `${now
.subtract(5, 'year')
.startOf('year')
.format('YYYY-MM-DD')} 00:00:00`,
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
};
setDefaultTimeRange(initSearchData.timeScale);
// 使 initSearchData
handleSearchFinish({
...initSearchData
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
//
const now = dayjs();
jcdt.value = {
min: `${now.subtract(5, 'year').format('YYYY-MM-DD')} 00:00:00`,
max: `${now.format('YYYY-MM-DD')} 23:59:59`
};
initCrossSectionList();
// 使 handleSearchFinish jcdt
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,549 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<FishDataSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="tableColumns"
:list-url="getFishReleaseMonitorList"
:searchParams="{
sort: sort
}"
>
</BasicTable>
<!-- 图片预览弹框 -->
<a-modal
v-model:open="imagePreviewVisible"
title="图片预览"
:footer="null"
width="800px"
>
<div style="text-align: center">
<img :src="previewImageUrl" style="width: 100%; height: 600px" />
</div>
</a-modal>
<!-- 视频预览弹框 -->
<a-modal
v-model:open="videoPreviewVisible"
title="视频预览"
:footer="null"
width="800px"
>
<div style="text-align: center">
<video
:src="previewVideoUrl"
controls
style="width: 100%; height: 600px"
></video>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import { Button } from 'ant-design-vue';
import FishDataSearch from './FishDataSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getFishReleaseMonitorList } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const sort = ref<any>([
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'rstcdStepSort',
dir: 'asc'
},
{
field: 'ttpwr',
dir: 'asc'
},
{
field: 'tm',
dir: 'desc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
//
const imagePreviewVisible = ref(false);
const previewImageUrl = ref('');
//
const videoPreviewVisible = ref(false);
const previewVideoUrl = ref('');
const openImagePreview = (url: string) => {
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
imagePreviewVisible.value = true;
};
const openVideoPreview = (url: string) => {
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
videoPreviewVisible.value = true;
};
const tableScrollX = computed(() =>
Math.max(
tableColumns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
//
const columnsManual: any[] = [
{
key: 'stnm',
title: '电站名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
merge: true
},
{
key: 'baseName',
title: '所属基地',
dataIndex: 'baseName',
sort: true,
width: 120
},
{
key: 'stnm',
title: '过鱼设施名称',
dataIndex: 'stnm',
sort: true,
width: 200
},
{
key: 'strdt',
title: '开始时间',
dataIndex: 'strdt',
sort: true,
width: 100,
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
},
{
key: 'enddt',
title: '结束时间',
dataIndex: 'enddt',
sort: true,
width: 100,
customRender: ({ text }) => dayjs(text).format('YYYY-MM-DD') || '-'
},
{
key: 'tm',
title: '填报时间',
dataIndex: 'tm',
sort: true,
width: 160
},
{
key: 'ftp',
title: '鱼种类',
dataIndex: 'ftp',
sort: true,
width: 120
},
{
key: 'fcnt',
title: '过鱼数量(尾)',
dataIndex: 'fcnt',
sort: true,
width: 120
},
{
key: 'directionName',
title: '鱼游向',
dataIndex: 'directionName',
sort: true,
width: 120
},
{
key: 'fsz',
title: '鱼类规格',
dataIndex: 'fsz',
sort: true,
width: 160
},
{
key: 'firstimgurl',
title: '图片',
dataIndex: 'firstimgurl',
align: 'center',
width: 110,
customRender: ({ text }: any) => {
const hasValidUrl =
text &&
text !== '-' &&
text.trim() !== '' &&
text !== 'null' &&
text !== 'undefined';
return h(
Button,
{
type: 'link',
disabled: !hasValidUrl,
style: {
color: !hasValidUrl ? '#ccc' : '#1890ff',
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
},
onClick: () => {
if (hasValidUrl) {
openImagePreview(text);
}
}
},
() => '查看图片'
);
}
},
{
key: 'videourl',
title: '视频',
dataIndex: 'videourl',
align: 'center',
width: 110,
customRender: ({ text }: any) => {
const hasValidUrl =
text &&
text !== '-' &&
text.trim() !== '' &&
text !== 'null' &&
text !== 'undefined';
return h(
Button,
{
type: 'link',
disabled: !hasValidUrl,
style: {
color: !hasValidUrl ? '#ccc' : '#1890ff',
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
},
onClick: () => {
if (hasValidUrl) {
openVideoPreview(text);
}
}
},
() => '查看视频'
);
}
}
];
//
const columnsAuto: any[] = [
{
key: 'stnm',
title: '电站名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
merge: true
},
{
key: 'baseName',
title: '所属基地',
dataIndex: 'baseName',
sort: true,
width: 120
},
{
key: 'stnm',
title: '过鱼设施名称',
dataIndex: 'stnm',
sort: true,
width: 200
},
{
key: 'tm',
title: '时间',
dataIndex: 'tm',
sort: true,
width: 160
},
{
key: 'ftp',
title: '鱼种类',
dataIndex: 'ftp',
sort: true,
width: 120
},
{
key: 'length',
title: '鱼长度(cm)',
dataIndex: 'length',
sort: true,
width: 120
},
{
key: 'width',
title: '鱼宽度(cm)',
dataIndex: 'width',
sort: true,
width: 120,
customRender: ({ text }: any) => {
const num = parseFloat(text);
return isNaN(num) ? '-' : num.toFixed(2);
}
},
{
key: 'fishspeed',
title: '鱼速度(m/s)',
dataIndex: 'fishspeed',
sort: true,
width: 80,
customRender: ({ text }: any) => {
const num = parseFloat(text);
return isNaN(num) ? '-' : num.toFixed(2);
}
},
{
key: 'directionName',
title: '鱼游向',
dataIndex: 'directionName',
sort: true,
width: 160
},
{
key: 'temperature',
title: '水温(℃)',
dataIndex: 'temperature',
sort: true,
width: 160,
customRender: ({ text }: any) => {
const num = parseFloat(text);
return isNaN(num) ? '-' : num.toFixed(1);
}
},
{
key: 'speed',
title: '流速(m/s)',
dataIndex: 'speed',
sort: true,
width: 160,
customRender: ({ text }: any) => {
const num = parseFloat(text);
return isNaN(num) ? '-' : num.toFixed(2);
}
},
{
key: 'fcnt',
title: '过鱼数量(尾)',
dataIndex: 'fcnt',
sort: true,
width: 120
},
{
key: 'channelno',
title: '过鱼通道',
dataIndex: 'channelno',
sort: true,
width: 140
},
{
key: 'firstimgurl',
title: '轮廓图',
dataIndex: 'firstimgurl',
align: 'center',
width: 110,
customRender: ({ text }: any) => {
const hasValidUrl =
text &&
text !== '-' &&
text.trim() !== '' &&
text !== 'null' &&
text !== 'undefined';
return h(
Button,
{
type: 'link',
disabled: !hasValidUrl,
style: {
color: !hasValidUrl ? '#ccc' : '#1890ff',
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
},
onClick: () => {
if (hasValidUrl) {
openImagePreview(text);
}
}
},
() => '查看图片'
);
}
},
{
key: 'videourl',
title: '短视频',
dataIndex: 'videourl',
align: 'center',
width: 110,
customRender: ({ text }: any) => {
const hasValidUrl =
text &&
text !== '-' &&
text.trim() !== '' &&
text !== 'null' &&
text !== 'undefined';
return h(
Button,
{
type: 'link',
disabled: !hasValidUrl,
style: {
color: !hasValidUrl ? '#ccc' : '#1890ff',
cursor: !hasValidUrl ? 'not-allowed' : 'pointer'
},
onClick: () => {
if (hasValidUrl) {
openVideoPreview(text);
}
}
},
() => '查看视频'
);
}
}
];
//
const tableColumns = computed(() => {
const mway = currentSearchParams.value.mway;
return mway === '2' ? columnsAuto : columnsManual;
});
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
// prop getList
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `过鱼监测数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
console.log(values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.mway
? {
field: 'mway',
operator: 'eq',
dataType: 'string',
value: values.mway
}
: null
].filter(Boolean); //
if (values.stcd) {
filters.push({
logic: 'or',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
},
{
field: 'stcode',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
]
});
}
if (values.jcdt) {
filters.push(
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
}
);
}
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,184 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
format="YYYY"
value-format="YYYY-MM-DD HH:mm:ss"
picker="year"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
format="YYYY"
value-format="YYYY-MM-DD HH:mm:ss"
picker="year"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import dayjs from 'dayjs';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
init
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const onValuesChange = (changedValues: any, allValues: any) => {
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
//
const now = dayjs();
jcdt.value = {
min: `${now
.subtract(1, 'year')
.startOf('year')
.format('YYYY-MM-DD')} 00:00:00`,
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
};
// 使 initSearchData
handleSearchFinish({
...initSearchData
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
//
const now = dayjs();
jcdt.value = {
min: `${now
.subtract(1, 'year')
.startOf('year')
.format('YYYY-MM-DD')} 00:00:00`,
max: `${now.endOf('year').format('YYYY-MM-DD')} 23:59:59`
};
// 使 handleSearchFinish jcdt
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,208 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<FishReleaseSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="columns"
:list-url="getFishReleaseList"
:searchParams="{
sort: sort
}"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import FishReleaseSearch from './FishReleaseSearch.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getFishReleaseList } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const sort = ref<any>([
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'flyr',
dir: 'asc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
const tableScrollX = computed(() =>
Math.max(
columns.reduce((sum: number, col: any) => sum + (col.width || 180), 0),
600
)
);
//
const columns: any[] = [
{
key: 'stnm',
title: '电站名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
ellipsis: true,
merge: true
},
{
key: 'baseName',
title: '所属基地',
dataIndex: 'baseName',
sort: true,
width: 160,
merge: true,
mergeDeps: ['stnm']
},
{
key: 'flyr',
title: '放鱼年份',
dataIndex: 'flyr',
sort: true,
width: 160,
merge: true,
mergeDeps: ['stnm']
},
{
key: 'totalFcnt',
title: '放流总量(万尾)',
dataIndex: 'totalFcnt',
sort: true,
width: 160,
merge: true,
mergeDeps: ['stnm'],
customRender: ({ record }: any) =>
(record.totalFcnt / 10000).toFixed(4) || '-'
},
{
key: 'release',
title: '放流来源',
dataIndex: 'release',
merge: true,
mergeDeps: ['stnm'],
width: 250
},
{
key: 'ftpName',
title: '放鱼种类',
dataIndex: 'ftpName',
width: 100
},
{
key: 'fcnt',
title: '数量(万尾)',
dataIndex: 'fcnt',
width: 100,
customRender: ({ record }: any) => (record.fcnt / 10000).toFixed(4) || '-'
}
];
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
// prop getList
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `鱼类放鱼数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
console.log('values', values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null
].filter(Boolean); //
if (values.jcdt) {
filters.push(
{
field: 'flyr',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'flyr',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
}
);
}
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,213 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
basin: null,
ruleType: null,
timeScale: 'tm'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'ruleType',
label: '规则类型',
width: 160,
placeholder: '请选择规则类型',
fieldProps: {
allowClear: false
},
options: [
{ label: '全部', value: '' },
{ label: '环评要求', value: 'sfdb' },
{ label: '水利部要求', value: 'mwrSfdb' },
{ label: '多年平均流量10%', value: 'avqSfdb' },
{ label: '无生态流量要求', value: 'wstllyq' }
]
},
{
type: 'Select',
name: 'timeScale',
label: '时间统计尺度',
width: 80,
placeholder: '请选择时间统计尺度',
fieldProps: {
allowClear: false
},
options: [
{
label: '小时',
value: 'tm'
},
{
label: '日',
value: 'dt'
},
{
label: '月',
value: 'month'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const onValuesChange = (changedValues: any, allValues: any) => {
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
setDefaultTimeRange(initSearchData.timeScale);
// jcdt
handleSearchFinish({
...initSearchData,
timeScale: currentTimeScale.value
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,473 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<FlowDataSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="displayColumns"
:list-url="currentListUrl"
:searchParams="{
sort: sort
}"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import FlowDataSearch from './FlowDataSearch.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getFlowDataList,
getFlowDataDayList,
getFlowDataMonthList
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
// timeScale API
const currentListUrl = computed(() => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'dt') return getFlowDataDayList;
if (timeScale === 'month') return getFlowDataMonthList;
return getFlowDataList; // tm
});
const tableScrollX = computed(() =>
Math.max(
displayColumns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
//
const baseColumns: any = [
{
key: 'stnm',
title: '电站名称',
dataIndex: 'stnm',
visible: true,
width: 150,
sort: true,
fixed: 'left',
ellipsis: true
},
{
key: 'baseName',
title: '基地',
dataIndex: 'baseName',
visible: true,
width: 150,
sort: true,
fixed: 'left',
ellipsis: true
},
{
key: 'tm',
title: '时间',
dataIndex: 'tm',
visible: true,
width: 150,
sort: true,
fixed: 'left',
ellipsis: true
},
{
key: 'qi',
title: '入库流量(m³/s)',
dataIndex: 'qi',
visible: true,
width: 150,
sort: true,
ellipsis: true,
customRender: ({ text }) => {
return text ? Number(text).toFixed(1) : '-';
}
},
{
key: 'qec',
title: '生态流量值(m³/s)',
dataIndex: 'qec',
visible: true,
sort: true,
width: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? Number(text).toFixed(1) : '-';
}
}
];
//
const ruleColumnsMap: any = {
'': [
// -
{
key: 'qecLimit',
title: '生态环境部限值要求(m³/s)',
dataIndex: 'qecLimit',
visible: true,
sort: true,
width: 200,
ellipsis: true
},
{
key: 'sfdb',
// title: '',
dataIndex: 'sfdbName',
visible: true,
sort: true,
width: 170,
title: () =>
h('span', { style: 'display:flex;align-items:center;gap:4px' }, [
'生态流量是否达标',
h(
Tooltip,
{
title: h('ol', {}, [
h('li', {}, '达标判定规则:'),
h(
'li',
{},
'a)环评中对生态流量限值有明确要求的,按环评要求评判,'
),
h(
'li',
{},
'b)环评中未对生态流量限值有明确要求的,按水利部要求评判,'
),
h(
'li',
{},
'c)无环评要求、无水利部要求的按多年平均流量的10%作为评判依据,'
),
h('li', {}, 'd)当来水不足时,生态流量不小于入库流量判定达标,')
])
},
{
default: () =>
h('i', {
class: 'iconfont icon-iconQuestion'
})
}
)
])
},
{
key: 'mwrLimit',
title: '水利部限值要求(m³/s)',
dataIndex: 'mwrLimit',
visible: true,
width: 180,
sort: true,
ellipsis: true
},
{
key: 'mwrSfdb',
dataIndex: 'mwrSfdbName',
visible: true,
sort: true,
width: 170,
title: () =>
h('span', { style: 'display:flex;align-items:center;gap:4px' }, [
'水利部要求是否达标',
h(
Tooltip,
{
title: h('ol', {}, [
h('li', {}, '达标判定规则:'),
h(
'li',
{},
'a)环评中对生态流量限值有明确要求的,按环评要求评判,'
),
h(
'li',
{},
'b)环评中未对生态流量限值有明确要求的,按水利部要求评判,'
),
h(
'li',
{},
'c)无环评要求、无水利部要求的按多年平均流量的10%作为评判依据,'
),
h('li', {}, 'd)当来水不足时,生态流量不小于入库流量判定达标,')
])
},
{
default: () =>
h('i', {
class: 'iconfont icon-iconQuestion'
})
}
)
])
},
{
key: 'avqLimit',
title: '多年平均流量(m³/s)',
dataIndex: 'avqLimit',
visible: true,
sort: true,
width: 150
},
{
key: 'avqSfdb',
title: '多年平均流量*10%是否达标',
dataIndex: 'avqSfdbName',
visible: true,
sort: true,
width: 210,
title: () =>
h('span', { style: 'display:flex;align-items:center;gap:4px' }, [
'多年平均流量*10%是否达标',
h(
Tooltip,
{
title: h('ol', {}, [
h('li', {}, '达标判定规则:'),
h(
'li',
{},
'a)环评中对生态流量限值有明确要求的,按环评要求评判,'
),
h(
'li',
{},
'b)环评中未对生态流量限值有明确要求的,按水利部要求评判,'
),
h(
'li',
{},
'c)无环评要求、无水利部要求的按多年平均流量的10%作为评判依据,'
),
h('li', {}, 'd)当来水不足时,生态流量不小于入库流量判定达标,')
])
},
{
default: () =>
h('i', {
class: 'iconfont icon-iconQuestion'
})
}
)
])
}
],
sfdb: [
//
{
key: 'qecLimit',
title: '生态环境部限值要求(m³/s)',
dataIndex: 'qecLimit',
visible: true,
sort: true,
width: 200,
ellipsis: true
},
{
key: 'sfdb',
title: '生态流量是否达标',
dataIndex: 'sfdbName',
visible: true,
sort: true,
width: 150
}
],
mwrSfdb: [
//
{
key: 'mwrLimit',
title: '水利部限值要求(m³/s)',
dataIndex: 'mwrLimit',
visible: true,
width: 180,
sort: true,
ellipsis: true
},
{
key: 'mwrSfdb',
title: '水利部要求是否达标',
dataIndex: 'mwrSfdbName',
visible: true,
sort: true,
width: 150
}
],
avqSfdb: [
// 10%
{
key: 'avqLimit',
title: '多年平均流量(m³/s)',
dataIndex: 'avqLimit',
visible: true,
sort: true,
width: 150
},
{
key: 'avqSfdb',
title: '多年平均流量*10%是否达标',
dataIndex: 'avqSfdbName',
visible: true,
sort: true,
width: 200
}
],
wstllyq: [] // -
};
// ruleType
const displayColumns = computed(() => {
const ruleType = currentSearchParams.value.ruleType ?? '';
const ruleColumns = ruleColumnsMap[ruleType] || ruleColumnsMap[''];
return [...baseColumns, ...ruleColumns];
});
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `生态流量运行数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
// ruleType
const buildRuleTypeFilters = (ruleType: string) => {
//
if (ruleType === '' || ruleType === undefined) return [];
//
if (ruleType === 'sfdb') {
return [{ field: 'sfdb', operator: 'neq', dataType: 'string', value: '3' }];
}
//
if (ruleType === 'mwrSfdb') {
return [
{ field: 'mwrSfdb', operator: 'neq', dataType: 'string', value: '3' }
];
}
// 10%
if (ruleType === 'avqSfdb') {
return [
{ field: 'avqSfdb', operator: 'neq', dataType: 'string', value: '3' }
];
}
//
if (ruleType === 'wstllyq') {
return [
{ field: 'sfdb', operator: 'eq', dataType: 'string', value: '3' },
{ field: 'mwrSfdb', operator: 'eq', dataType: 'string', value: '3' },
{ field: 'avqSfdb', operator: 'eq', dataType: 'string', value: '3' }
];
}
return [];
};
const initTable = (values: any) => {
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
...buildRuleTypeFilters(values.ruleType),
...buildTimeFilters(values)
].filter(Boolean);
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,265 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
import { getFlowStationSectionList } from '@/api/DataQueryMenuModule';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
timeScale: 'tm'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const crossSectionListLoading = ref<any>(false);
const crossSectionList = ref<any>([]);
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'stcd',
label: '流量站',
width: 180,
placeholder: '请选择流量站',
fieldProps: {
allowClear: true
},
loading: crossSectionListLoading.value,
options: crossSectionList.value.map(item => ({
label: item.stnm,
value: item.stcd
}))
},
{
type: 'Select',
name: 'timeScale',
label: '时间统计尺度',
width: 80,
placeholder: '请选择时间统计尺度',
fieldProps: {
allowClear: false
},
options: [
{
label: '小时',
value: 'tm'
},
{
label: '日',
value: 'dt'
},
{
label: '月',
value: 'month'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const initCrossSectionList = params => {
const filters = [
{
field: 'sttpFullPath',
operator: 'eq',
dataType: 'string',
value: 'ZQ'
}
];
if (params) {
if (params.rvcd != '' && params.rvcd != 'all') {
filters.push({
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: params.rvcd
});
}
if (params.rstcd != '' && params.rstcd != null) {
filters.push({
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: params.rstcd
});
}
}
crossSectionListLoading.value = true;
getFlowStationSectionList({
filter: {
logic: 'and',
filters: filters
},
select: ['stcd', 'stnm', 'wtDeviceType', 'dtinType', 'mway']
}).then(res => {
if (res.data?.data) {
crossSectionList.value = res.data?.data;
}
crossSectionListLoading.value = false;
});
};
const onValuesChange = (changedValues: any, allValues: any) => {
//
if (changedValues.rvcd || changedValues.rstcd !== undefined) {
allValues.stcd = null;
if (basicSearchRef.value?.formData) {
basicSearchRef.value.formData.stcd = null;
}
initCrossSectionList(allValues);
}
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
setDefaultTimeRange(initSearchData.timeScale);
// 使 initSearchData
handleSearchFinish({
...initSearchData,
timeScale: currentTimeScale.value
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
initCrossSectionList();
// 使 handleSearchFinish jcdt
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,252 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<FlowStationSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="Columns"
:list-url="currentListUrl"
:searchParams="{
sort: sort
}"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import FlowStationSearch from './FlowStationSearch.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getFlowStationList,
getFlowStationDayList,
getFlowStationMonthList
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
// timeScale API
const currentListUrl = computed(() => {
const timeScale = currentSearchParams.value.timeScale;
console.log('timeScale:', timeScale);
if (timeScale === 'dt') return getFlowStationDayList;
if (timeScale === 'month') return getFlowStationMonthList;
return getFlowStationList; // tm
});
const tableScrollX = computed(() =>
Math.max(
Columns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
//
const allColumns: any[] = [
{
key: 'stnm',
title: '站点名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
key: 'stlc',
title: '站址',
dataIndex: 'stlc',
width: 200
},
{
key: 'baseName',
title: '所属基地',
dataIndex: 'baseName',
width: 160
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
width: 160
},
{
key: 'z',
title: '水位(m)',
dataIndex: 'z',
width: 100,
customRender: ({ record }: any) => record.z?.toFixed(2) || '-'
},
{
key: 'q',
title: '流量(m³/s)',
dataIndex: 'q',
width: 100,
customRender: ({ record }: any) => record.q?.toFixed(2) || '-'
},
{
key: 'v',
title: '流速(m³/s)',
dataIndex: 'v',
width: 100,
customRender: ({ record }: any) => record.v?.toFixed(2) || '-'
}
];
// timeScale
const timeColumn = computed(() => ({
key: 'tm',
title: () => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'month') return '月';
if (timeScale === 'dt') return '时间';
return '时间';
},
dataIndex: 'tm',
visible: true,
width: 150,
sort: true,
ellipsis: true,
customRender: ({ record }: any) => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'dt') {
return dayjs(record.dt).format('YYYY-MM-DD');
}
if (timeScale === 'month') {
const year = record.year ?? '-';
const month = record.month ?? '-';
return `${year}-${String(month).padStart(2, '0')}`;
}
return record.tm || '-';
}
}));
//
const Columns = computed(() => {
const result = [...allColumns];
//
const baseIndex = result.findIndex(c => c.key === 'stlc');
result.splice(baseIndex + 1, 0, timeColumn.value);
return result;
});
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
// prop getList
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `流量站数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
console.log(values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.stcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
: null,
...buildTimeFilters(values)
].filter(Boolean); //
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,265 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
import { getSurfaceTempSectionList } from '@/api/DataQueryMenuModule';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
timeScale: 'tm'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const crossSectionListLoading = ref<any>(false);
const crossSectionList = ref<any>([]);
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'stcd',
label: '断面',
width: 180,
placeholder: '请选择断面',
fieldProps: {
allowClear: true
},
loading: crossSectionListLoading.value,
options: crossSectionList.value.map(item => ({
label: item.stnm,
value: item.stcd
}))
},
{
type: 'Select',
name: 'timeScale',
label: '时间统计尺度',
width: 80,
placeholder: '请选择时间统计尺度',
fieldProps: {
allowClear: false
},
options: [
{
label: '小时',
value: 'tm'
},
{
label: '日',
value: 'dt'
},
{
label: '月',
value: 'month'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const initCrossSectionList = params => {
const filters = [
{
field: 'sttpFullPath',
operator: 'contains',
dataType: 'string',
value: 'ENV,ENVM,WT,WTRV'
}
];
if (params) {
if (params.rvcd != '' && params.rvcd != 'all') {
filters.push({
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: params.rvcd
});
}
if (params.rstcd != '' && params.rstcd != null) {
filters.push({
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: params.rstcd
});
}
}
crossSectionListLoading.value = true;
getSurfaceTempSectionList({
filter: {
logic: 'and',
filters: filters
},
select: ['stcd', 'stnm', 'wtDeviceType', 'dtinType', 'mway']
}).then(res => {
if (res.data?.data) {
crossSectionList.value = res.data?.data;
}
crossSectionListLoading.value = false;
});
};
const onValuesChange = (changedValues: any, allValues: any) => {
//
if (changedValues.rvcd || changedValues.rstcd !== undefined) {
allValues.stcd = null;
if (basicSearchRef.value?.formData) {
basicSearchRef.value.formData.stcd = null;
}
initCrossSectionList(allValues);
}
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
setDefaultTimeRange(initSearchData.timeScale);
// 使 initSearchData
handleSearchFinish({
...initSearchData,
timeScale: currentTimeScale.value
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
initCrossSectionList();
// 使 handleSearchFinish jcdt
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,224 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<SurfaceTempSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="Columns"
:list-url="currentListUrl"
:searchParams="{
sort: sort
}"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import SurfaceTempSearch from './SurfaceTempSearch.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getSurfaceTempList,
getSurfaceTempDayList,
getSurfaceTempMonthList
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
// timeScale API
const currentListUrl = computed(() => {
const timeScale = currentSearchParams.value.timeScale;
console.log('timeScale:', timeScale);
if (timeScale === 'dt') return getSurfaceTempDayList;
if (timeScale === 'month') return getSurfaceTempMonthList;
return getSurfaceTempList; // tm
});
const tableScrollX = computed(() =>
Math.max(
Columns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
//
const allColumns: any[] = [
{
key: 'stnm',
title: '断面名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
ellipsis: true
},
{
key: 'wt',
title: '水温(℃)',
dataIndex: 'wt',
visible: true,
width: 200,
sort: true,
fixed: 'left',
customRender: ({ record }: any) => record.wt?.toFixed(1) || '-',
ellipsis: true
}
];
// timeScale
const timeColumn = computed(() => ({
key: 'tm',
title: () => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'month') return '月';
if (timeScale === 'dt') return '时间';
return '时间';
},
dataIndex: 'tm',
visible: true,
width: 150,
sort: true,
ellipsis: true,
customRender: ({ record }: any) => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'dt') {
return dayjs(record.dt).format('YYYY-MM-DD');
}
if (timeScale === 'month') {
const year = record.year ?? '-';
const month = record.month ?? '-';
return `${year}-${String(month).padStart(2, '0')}`;
}
return record.tm || '-';
}
}));
//
const Columns = computed(() => {
const result = [...allColumns];
//
const baseIndex = result.findIndex(c => c.key === 'stnm');
result.splice(baseIndex + 1, 0, timeColumn.value);
return result;
});
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
// prop getList
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `表层水温数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
console.log(values);
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.stcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
: null,
...buildTimeFilters(values)
].filter(Boolean); //
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,271 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
import { getSurfaceTempSectionList } from '@/api/DataQueryMenuModule';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
timeScale: 'tm'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const crossSectionListLoading = ref<any>(false);
const crossSectionList = ref<any>([]);
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'stcd',
label: '测站/断面名称',
width: 180,
placeholder: '请选择测站/断面名称',
fieldProps: {
allowClear: true
},
loading: crossSectionListLoading.value,
options: crossSectionList.value.map(item => ({
label: item.stnm,
value: item.stcd
}))
},
{
type: 'Select',
name: 'timeScale',
label: '时间统计尺度',
width: 80,
placeholder: '请选择时间统计尺度',
fieldProps: {
allowClear: false
},
options: [
{
label: '小时',
value: 'tm'
},
{
label: '日',
value: 'dt'
},
{
label: '月',
value: 'month'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const initCrossSectionList = params => {
const filters = [
{
field: 'sttpFullPath',
operator: 'contains',
dataType: 'string',
value: 'ENV,ENVM,WT,WTVT'
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTVT'
}
];
if (params) {
if (params.rvcd != '' && params.rvcd != 'all') {
filters.push({
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: params.rvcd
});
}
if (params.rstcd != '' && params.rstcd != null) {
filters.push({
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: params.rstcd
});
}
}
crossSectionListLoading.value = true;
getSurfaceTempSectionList({
filter: {
logic: 'and',
filters: filters
},
select: ['stcd', 'stnm', 'wtDeviceType', 'dtinType', 'mway']
}).then(res => {
if (res.data?.data) {
crossSectionList.value = res.data?.data;
}
crossSectionListLoading.value = false;
});
};
const onValuesChange = (changedValues: any, allValues: any) => {
//
if (changedValues.rvcd || changedValues.rstcd !== undefined) {
allValues.stcd = null;
if (basicSearchRef.value?.formData) {
basicSearchRef.value.formData.stcd = null;
}
initCrossSectionList(allValues);
}
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
setDefaultTimeRange(initSearchData.timeScale);
// 使 initSearchData
handleSearchFinish({
...initSearchData,
timeScale: currentTimeScale.value
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
initCrossSectionList();
// 使 handleSearchFinish jcdt
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,624 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<VerticalTempSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 垂向水温列勾选 -->
<div class="tab-checkbox-cxsw px-2">
<div class="checkbox-title">水深:</div>
<div class="checkbox-group">
<a-checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAllToggle"
class="select-all-checkbox"
>
全选
</a-checkbox>
<a-checkbox-group
v-model:value="selectedColumns"
:options="checkboxOptions"
@change="handleCheckboxChange"
/>
</div>
</div>
<!-- 图表和表格区域 -->
<div class="w-full h-full flex-1 flex overflow-hidden px-2 pb-2">
<!-- 左侧图表 -->
<div class="w-[50%] h-full relative">
<div ref="chartRef" class="w-full h-full"></div>
<a-empty
v-if="!chartData || chartData.length === 0"
description="暂无数据"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
/>
</div>
<!-- 右侧表格 -->
<div class="w-[50%] h-[100%] border-l border-[#dcdfe6] ml-2 pl-2 flex">
<BasicTable
ref="tableRef"
row-key="dt"
:scrollY="tableScrollY"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getVerticalList"
:transformData="transformData"
:searchParams="{
sort: sort
}"
:enable-row-selection="true"
:enable-row-highlight="false"
:min-selection-count="1"
@selection-change="handleCxswSelectionChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
computed,
onMounted,
nextTick,
watch,
onBeforeUnmount
} from 'vue';
import dayjs from 'dayjs';
import * as echarts from 'echarts';
import VerticalTempSearch from './VerticalTempSearch.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getVerticalList } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const sort = ref<any>([
{
field: 'dt',
dir: 'asc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
const tableData = ref<any[]>([]);
//
const chartRef = ref<HTMLElement>();
let chartInstance: echarts.ECharts | null = null;
const chartData = ref<any[]>([]);
const selectedRowsForChart = ref<any[]>([]);
//
const columns = ref<any[]>([]);
const selectedColumns = ref<string[]>([]);
const checkboxOptions = ref<any[]>([]);
const isAllSelected = computed(() => {
return (
checkboxOptions.value.length > 0 &&
selectedColumns.value.length === checkboxOptions.value.length
);
});
const isIndeterminate = computed(() => {
const total = checkboxOptions.value.length;
const sel = selectedColumns.value.length;
return sel > 0 && sel < total;
});
const handleCheckboxChange = (values: string[]) => {
selectedColumns.value = values;
//
if (selectedRowsForChart.value.length > 0) {
nextTick(() => {
updateChart(selectedRowsForChart.value);
});
}
};
const handleSelectAllToggle = (e: any) => {
if (e.target.checked) {
selectedColumns.value = checkboxOptions.value.map((opt: any) => opt.value);
} else {
selectedColumns.value = [];
}
// /
if (selectedRowsForChart.value.length > 0) {
nextTick(() => {
updateChart(selectedRowsForChart.value);
});
}
};
//
const tableColumns = computed(() => {
const fixedCols = [
{
title: '测站名称',
dataIndex: 'stnm',
width: 150,
fixed: 'left'
},
{
title: '时间',
dataIndex: 'dt',
width: 140,
fixed: 'left',
customRender: ({ text }: any) =>
text
? dayjs(text).format(
currentSearchParams.value.timeScale === 'month'
? 'YYYY-MM'
: currentSearchParams.value.timeScale === 'dt'
? 'YYYY-MM-DD'
: 'YYYY-MM-DD HH:mm:ss'
)
: '-'
}
];
const dataCols = columns.value
.filter((col: any) => selectedColumns.value.includes(col.dataIndex))
.map((col: any) => ({
title: col.title,
dataIndex: col.dataIndex,
width: 120,
customRender: ({ record }: any) => {
const val = record.dataList?.[col.dataIndex];
return val !== undefined && val !== null
? parseFloat(val).toFixed(1)
: '-';
}
}));
return [...fixedCols, ...dataCols];
});
const tableScrollX = computed(() => {
const cols = tableColumns.value;
const total = cols.reduce((s, c) => s + (c.width || 100), 0);
return total > 600 ? total : undefined;
});
//
const defaultSelectedRowKeys = computed(() => {
if (tableData.value && tableData.value.length > 0) {
return [tableData.value[0].dt];
}
return [];
});
//
const COLORS = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc',
'#5470c6',
'#73c0de',
'#fac858',
'#91cc75',
'#ee6666',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc',
'#5470c6',
'#73c0de'
];
//
const updateChart = (selectedRows: any[]) => {
if (!chartInstance) return;
if (!selectedRows || selectedRows.length === 0) {
chartInstance.clear();
return;
}
const series = selectedRows.map((row, index) => {
const data = Object.entries(row.dataList || {})
.filter(([depth]) => selectedColumns.value.includes(depth))
.map(([depth, temp]) => [parseFloat(temp as string), parseFloat(depth)])
.sort((a, b) => a[1] - b[1]);
return {
name: dayjs(row.dt).format(
currentSearchParams.value.timeScale === 'month'
? 'YYYY-MM'
: currentSearchParams.value.timeScale === 'dt'
? 'YYYY-MM-DD'
: 'YYYY-MM-DD HH:mm'
),
type: 'line',
data,
smooth: false,
symbol: 'circle',
symbolSize: 4,
lineStyle: { width: 2, color: COLORS[index % COLORS.length] },
itemStyle: { color: COLORS[index % COLORS.length] }
};
});
const option = {
title: {
text: '垂向水温分布',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
formatter: (value: any) => {
if (value.axisDimension == 'x') {
return value.value.toFixed(1) + '℃';
} else if (value.axisDimension == 'y') {
return value.value.toFixed(2) + 'm';
}
return value.value;
}
}
},
formatter: (params: any) => {
let html = params[0].seriesName + '\n';
params.forEach((item: any) => {
html += `<div style="display:flex;"><div style="width:40px;">${item.data[1]}m :</div> <div>${item.data[0]}℃</div></div>`;
});
return html;
}
},
legend: {
type: 'scroll',
top: 40,
data: series.map(s => s.name),
textStyle: { fontSize: 12 }
},
grid: {
left: '8%',
right: 70,
top: '15%',
bottom: '8%',
containLabel: false
},
xAxis: {
position: 'top',
name: '水温 (℃)',
nameLocation: 'end',
type: 'value',
scale: true,
boundaryGap: ['10%', '10%'],
axisLabel: {
fontSize: 12,
color: '#000000',
formatter: function (value) {
return value.toFixed(1) + '℃';
}
},
splitLine: { show: true }
},
yAxis: {
name: '水深 (m)',
type: 'value',
inverse: true,
nameLocation: 'end',
min: 0,
axisLabel: {
fontSize: 12,
color: '#000000',
formatter: function (value) {
return value + 'm';
}
},
splitLine: { show: true }
},
series,
toolbox: {
show: true,
feature: {
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
},
right: 20,
top: 10
},
dataZoom: {
type: 'inside',
xAxisIndex: [0],
throttle: 50,
start: 0,
end: 100
}
};
chartInstance.setOption(option, true);
};
//
const handleCxswSelectionChange = (
selectedRowKeys: string[],
selectedRows: any[]
) => {
selectedRowsForChart.value = selectedRows;
updateChart(selectedRows);
};
//
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `垂向水温数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.stcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
: null
].filter(Boolean);
if (values.timeScale == 'tm' || values.timeScale == 'dt') {
filters.push(
{
field: 'drtp',
operator: 'eq',
dataType: 'string',
value: values.timeScale == 'tm' ? 'HOUR' : 'DAY'
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
}
);
} else if (values.timeScale == 'month') {
filters.push(
{
field: 'drtp',
operator: 'eq',
dataType: 'string',
value: 'MON'
},
{
field: 'startYear',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY')
},
{
field: 'startMonth',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('MM')
},
{
field: 'endYear',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY')
},
{
field: 'endMonth',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('MM')
}
);
}
const params = {
logic: 'and',
filters
};
//
selectedColumns.value = [];
checkboxOptions.value = [];
columns.value = [];
selectedRowsForChart.value = [];
if (chartInstance) chartInstance.clear();
tableRef.value.getList(params);
};
const transformData = (data: any) => {
const apiColumns = data?.data?.data?.[0]?.columns || [];
const dataSource = data?.data?.data?.[0]?.dataSource || [];
//
const dynamicColumns = apiColumns.filter(
(item: any) => item.key !== 'stnm' && item.key !== 'dt'
);
columns.value = dynamicColumns;
checkboxOptions.value = dynamicColumns.map((col: any) => ({
label: col.key + 'm',
value: col.dataIndex
}));
selectedColumns.value = checkboxOptions.value.map((opt: any) => opt.value);
// dataList
const formattedData = dataSource
.map((row: any) => {
const dataList: any = {};
dynamicColumns.forEach((col: any) => {
dataList[col.dataIndex] = row.dataList[col.dataIndex];
});
return {
stnm: row.stnm,
dt: row.dt,
dataList
};
})
.reverse();
const total = data?.data?.data?.[0]?.total || 0;
tableData.value = formattedData;
//
if (formattedData.length > 0) {
selectedRowsForChart.value = [formattedData[0]];
nextTick(() => {
chartData.value = JSON.parse(JSON.stringify(formattedData));
updateChart([formattedData[0]]);
//
tableRef.value?.setSelectedRowKeys?.([formattedData[0].dt]);
});
} else {
chartData.value = [];
if (chartInstance) chartInstance.clear();
}
console.log(formattedData);
return {
records: formattedData,
total
};
};
//
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
// ResizeObserver
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
//
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
});
//
if (chartRef.value) {
resizeObserver = new ResizeObserver(() => {
if (chartInstance) chartInstance.resize();
});
resizeObserver.observe(chartRef.value);
}
//
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<style scoped lang="scss">
.tab-checkbox-cxsw {
display: flex;
gap: 8px;
padding-top: 8px;
padding-bottom: 8px;
.select-all-checkbox {
flex-shrink: 0;
}
.checkbox-title {
width: 60px;
flex-shrink: 0;
padding-right: 10px;
font-weight: 500;
white-space: nowrap;
text-align: right;
}
.checkbox-group {
max-height: 68px;
overflow-y: auto;
}
}
//
:deep(.ant-table-tbody .ant-table-row:hover > td) {
background-color: transparent !important;
}
:deep(.ant-table-tbody .ant-table-row-selected > td) {
background-color: transparent !important;
}
:deep(.ant-table-tbody .ant-table-row > td) {
cursor: default !important;
}
</style>

View File

@ -0,0 +1,286 @@
<template>
<div class="approval-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:zhujianfujian="'fu'"
@reset="handleResetWrapper"
@finish="onSearchFinish"
@values-change="onValuesChange"
><template #jcdt>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.min"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
placeholder="起始时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
<a-form-item-rest>
<a-date-picker
class="w-[160px]"
v-model:value="jcdt.max"
:format="timeFormat"
value-format="YYYY-MM-DD HH:mm:ss"
:picker="timePicker"
:showTime="timeShowTime"
:disabledDate="disabledEndDate"
placeholder="结束时间"
:showToday="false"
>
<template #renderExtraFooter>
<div class="flex items-center flex-wrap px-2">
<span
v-for="item in DateSetting.RangeButton.days1"
:key="item"
@click="handleRangeClick(item)"
>
<a class="text-[#1890ff] mr-1"> {{ item.label }}</a>
</span>
</div>
</template>
</a-date-picker>
</a-form-item-rest>
</template>
<template #actions>
<a-button :loading="exportLoading" @click="exportBtn">导出</a-button>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
import { DateSetting } from '@/utils/enumeration';
import { useTimeScale } from '@/composables/useTimeScale';
import { getWaterDataSectionList } from '@/api/DataQueryMenuModule';
const emit = defineEmits<{
(e: 'export-btn'): void;
(e: 'reset', values: any): void;
(e: 'search-finish', values: any): void;
}>();
const basicSearchRef = ref<any>();
const btnLoading = ref<boolean>(false);
const props = defineProps<{
exportLoading?: boolean;
}>();
const initSearchData = {
rvcd: 'all',
rstcd: null,
stcd: null,
mway: null,
timeScale: 'tm'
};
const searchData = ref<any>({ ...initSearchData });
const {
jcdt,
currentTimeScale,
timePicker,
timeFormat,
timeShowTime,
handleRangeClick,
disabledEndDate,
handleSearchFinish,
handleValuesChange,
handleReset,
init,
setDefaultTimeRange
} = useTimeScale({
defaultTimeScale: initSearchData.timeScale,
onSearchFinish: values => emit('search-finish', values),
onReset: () => emit('reset', initSearchData)
});
const crossSectionListLoading = ref<any>(false);
const crossSectionList = ref<any>([]);
const searchList: any = computed(() => [
{
type: 'waterStation',
name: 'rvcd',
label: '流域',
placeholder: '请输入流域名称',
fieldProps: {
allowClear: true
},
options: []
},
{
type: 'Select',
name: 'mway',
label: '监测类型',
width: 100,
placeholder: '请选择监测类型',
fieldProps: {
allowClear: false
},
options: [
{ label: '全部', value: '' },
{ label: '人工', value: '1' },
{ label: '自动', value: '2' }
]
},
{
type: 'Select',
name: 'stcd',
label: '断面',
width: 180,
placeholder: '请选择断面',
fieldProps: {
allowClear: true
},
loading: crossSectionListLoading.value,
options: crossSectionList.value.map(item => ({
label: item.stnm,
value: item.stcd
}))
},
{
type: 'Select',
name: 'timeScale',
label: '时间统计尺度',
width: 80,
placeholder: '请选择时间统计尺度',
fieldProps: {
allowClear: false
},
options: [
{
label: '小时',
value: 'tm'
},
{
label: '日',
value: 'dt'
},
{
label: '月',
value: 'month'
}
]
},
{
type: 'custom',
name: 'jcdt',
label: '时间',
fieldProps: {
allowClear: false
}
}
]);
const onSearchFinish = (values: any) => {
handleSearchFinish(values);
};
const exportBtn = () => {
emit('export-btn');
};
const initCrossSectionList = params => {
const filters = [
{
field: 'sttpCode',
operator: 'in',
dataType: 'string',
value: ['WQ', 'WQFB']
}
];
if (params) {
if (params.rvcd != '' && params.rvcd != 'all') {
filters.push({
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: params.rvcd
});
}
if (params.rstcd != '' && params.rstcd != null) {
filters.push({
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: params.rstcd
});
}
}
crossSectionListLoading.value = true;
getWaterDataSectionList({
filter: {
logic: 'and',
filters: filters
},
select: ['stcd', 'stnm'],
sort: [
{
field: 'ttpwr',
dir: 'desc'
}
]
}).then(res => {
if (res.data?.data) {
crossSectionList.value = res.data?.data;
}
crossSectionListLoading.value = false;
});
};
const onValuesChange = (changedValues: any, allValues: any) => {
//
if (changedValues.rvcd || changedValues.rstcd !== undefined) {
allValues.stcd = null;
if (basicSearchRef.value?.formData) {
basicSearchRef.value.formData.stcd = null;
}
initCrossSectionList(allValues);
}
handleValuesChange(changedValues, allValues);
searchData.value = { ...searchData.value, ...allValues };
};
const handleResetWrapper = () => {
//
currentTimeScale.value = initSearchData.timeScale;
setDefaultTimeRange(initSearchData.timeScale);
// jcdt
handleSearchFinish({
...initSearchData,
timeScale: currentTimeScale.value
});
};
defineExpose({
btnLoading,
searchData
});
onMounted(() => {
init();
initCrossSectionList();
handleSearchFinish({ ...initSearchData });
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,359 @@
<template>
<div class="w-full h-full flex flex-col">
<!-- 搜索组件 -->
<WaterDataSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 表格组件 -->
<BasicTable
ref="tableRef"
:scrollX="tableScrollX"
:scrollY="tableScrollY"
:columns="Columns"
:list-url="currentListUrl"
:searchParams="{
sort: sort
}"
>
</BasicTable>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, h } from 'vue';
import dayjs from 'dayjs';
import WaterDataSearch from './WaterDataSearch.vue';
import { Tooltip } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getWaterDataList,
getWaterDataDayList,
getWaterDataMonthList
} from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
import { buildTimeFilters } from '@/utils/buildTimeFilters';
const sort = ref<any>([
{
field: 'qecLimit',
dir: 'asc'
},
{
field: 'baseStepSort',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
},
{
field: 'year',
dir: 'desc'
},
{
field: 'month',
dir: 'desc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
// timeScale API
const currentListUrl = computed(() => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'dt') return getWaterDataDayList;
if (timeScale === 'month') return getWaterDataMonthList;
return getWaterDataList; // tm
});
const tableScrollX = computed(() =>
Math.max(
Columns.value.reduce(
(sum: number, col: any) => sum + (col.width || 180),
0
),
600
)
);
//
const allColumns: any[] = [
{
key: 'stnm',
title: '测站/断面名称',
dataIndex: 'stnm',
visible: true,
width: 200,
sort: true,
fixed: 'left',
ellipsis: true
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
visible: true,
width: 120,
ellipsis: true
},
{
key: 'baseName',
title: '基地',
dataIndex: 'baseName',
visible: true,
width: 150,
ellipsis: true
},
{
title: '地表水等级',
children: [
{
key: 'wwqtgName',
title: '水质要求',
dataIndex: 'wwqtgName',
width: 100,
customRender: ({ record }: any) => {
return record.wwqtgName || '-';
}
},
{
key: 'wqgrdName',
title: '水质等级',
dataIndex: 'wqgrdName',
width: 100,
customRender: ({ record }: any) => {
return record.wqgrdName || '-';
}
}
]
},
{
title: '污染物监测指标',
children: [
{
key: 'codcr',
title: '化学需氧量(mg/L)',
dataIndex: 'codcr',
sort: true,
width: 150,
customRender: ({ record }: any) => {
return record.codcr || '-';
}
},
{
key: 'nh3n',
title: '氨氮(mg/L)',
dataIndex: 'nh3n',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.nh3n || '-';
}
},
{
key: 'tp',
title: '总磷(mg/L)',
dataIndex: 'tp',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.tp || '-';
}
},
{
key: 'tn',
title: '总氮(mg/L)',
dataIndex: 'tn',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.tn || '-';
}
}
]
},
{
title: '五参数',
children: [
{
key: 'cond',
title: '电导率(μS/cm)',
dataIndex: 'cond',
sort: true,
width: 120,
customRender: ({ record }: any) => {
return record.cond || '-';
}
},
{
key: 'dox',
title: '溶解氧(mg/L)',
dataIndex: 'dox',
sort: true,
width: 120,
customRender: ({ record }: any) => {
return record.dox || '-';
}
},
{
key: 'ph',
title: 'pH值',
dataIndex: 'ph',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.ph || '-';
}
},
{
key: 'tu',
title: '浊度(NTU)',
dataIndex: 'tu',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.tu || '-';
}
},
{
key: 'wtmp',
title: '水温(℃)',
dataIndex: 'wtmp',
sort: true,
width: 100,
customRender: ({ record }: any) => {
return record.wtmp || '-';
}
}
]
}
];
// timeScale
const timeColumn = computed(() => ({
key: 'tm',
title: () => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'month') return '月';
if (timeScale === 'dt') return '时间';
return '时间';
},
dataIndex: 'tm',
visible: true,
width: 150,
sort: true,
ellipsis: true,
customRender: ({ record }: any) => {
const timeScale = currentSearchParams.value.timeScale;
if (timeScale === 'dt') {
return dayjs(record.dt).format('YYYY-MM-DD');
}
if (timeScale === 'month') {
const year = record.year ?? '-';
const month = record.month ?? '-';
return `${year}-${String(month).padStart(2, '0')}`;
}
return record.tm || '-';
}
}));
//
const Columns = computed(() => {
const result = [...allColumns];
//
const baseIndex = result.findIndex(c => c.key === 'baseName');
result.splice(baseIndex + 1, 0, timeColumn.value);
return result;
});
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `水质监测数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const initTable = (values: any) => {
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.mway
? {
field: 'mway',
operator: 'eq',
dataType: 'string',
value: values.mway
}
: null,
values.stcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
: null,
...buildTimeFilters(values)
].filter(Boolean); //
const params = {
logic: 'and',
filters
};
tableRef.value.getList(params);
};
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,269 @@
<template>
<a-modal
title="选中配置"
v-model:open="modalVisible"
:footer="null"
width="1080px"
:destroy-on-close="true"
@cancel="handleCancel"
>
<div class="mb-4">
<a-button type="primary" @click="handleAdd">新增选中配置</a-button>
</div>
<a-table
:columns="columns"
:data-source="tableData"
:loading="tableLoading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #action="{ record }">
<div class="flex gap-[6px]">
<a-button type="link" size="small" @click="handleEdit(record)"
>编辑</a-button
>
<a-button
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
>
</div>
</template>
</a-table>
<LegendDataSelectedConfigForm
ref="configFormRef"
v-model:visible="formVisible"
:initial-values="currentConfig"
:loading="formLoading"
@cancel="handleFormCancel"
@ok="handleFormSubmit"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, h } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
getAllMapLegendModuleTree,
saveMapLegendModule,
deleteMapLegendModule
} from '@/api/system/map/LegendStructure';
import LegendDataSelectedConfigForm from './LegendDataSelectedConfigForm.vue';
interface Props {
visible: boolean;
legendId?: string;
legendType?: string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
legendId: '',
legendType: ''
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'close'): void;
}>();
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
const columns = [
{
title: '系统名称',
dataIndex: 'systemName',
key: 'systemName',
width: 180,
customRender: ({ record }: any) => {
return record.systemName || record.systemId || '-';
}
},
{
title: '菜单',
dataIndex: 'moduleName',
key: 'moduleName',
width: 180,
customRender: ({ record }: any) => {
return record.moduleName || record.moduleId || '-';
}
},
{
title: '是否默认展示',
dataIndex: 'enable',
key: 'enable',
width: 150,
customRender: ({ record }: any) => {
const text =
record.enable === 1 ? '是' : record.enable === 0 ? '否' : '-';
const color =
record.enable === 1
? '#52c41a'
: record.enable === 0
? '#ff4d4f'
: '#999';
return h('span', { style: { color } }, text);
}
},
{
title: '排序',
dataIndex: 'orderIndex',
key: 'orderIndex',
width: 100
},
{
title: '操作',
key: 'action',
width: 120,
slots: { customRender: 'action' }
}
];
const tableData = ref<any[]>([]);
const tableLoading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${total}`
});
//
const configFormRef = ref();
const formVisible = ref(false);
const formLoading = ref(false);
const currentConfig = ref<any | null>(null);
const handleCancel = () => {
emit('update:visible', false);
emit('close');
};
//
const fetchTableData = async () => {
if (!props.legendId) return;
tableLoading.value = true;
try {
const res = await getAllMapLegendModuleTree({
skip: pagination.current - 1,
take: pagination.pageSize,
sort: [
{
field: 'orderIndex',
dir: 'asc'
}
],
filter: {
logic: 'and',
filters: [
{
field: 'resId',
operator: 'eq',
dataType: 'string',
value: props.legendId
}
]
}
});
tableData.value = res.data?.data || [];
pagination.total = res.data?.total || 0;
} catch (error) {
message.error('获取数据失败');
tableData.value = [];
} finally {
tableLoading.value = false;
}
};
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchTableData();
};
//
const handleAdd = () => {
currentConfig.value = null;
formVisible.value = true;
};
//
const handleEdit = (record: any) => {
if (record.moduleId == 'tycd') {
record.universal = 1;
} else {
record.universal = 0;
}
currentConfig.value = { ...record };
formVisible.value = true;
};
//
const handleDelete = (record: any) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的记录吗?',
zIndex: 2100,
onOk: async () => {
try {
let res = await deleteMapLegendModule([record.id]);
message.success('删除成功');
fetchTableData();
} catch (error) {
message.error('删除失败');
return;
}
}
});
};
//
const handleFormCancel = () => {
currentConfig.value = null;
formVisible.value = false;
};
//
const handleFormSubmit = async (values: any) => {
formLoading.value = true;
try {
//
let res = await saveMapLegendModule({
...values,
resId: props.legendId,
resType: props.legendType
});
//
message.success('保存成功');
formVisible.value = false;
fetchTableData();
} catch (error) {
message.error('保存失败');
} finally {
formLoading.value = false;
}
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
pagination.current = 1;
fetchTableData();
}
},
{ immediate: false }
);
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,190 @@
<template>
<a-modal
:title="isEdit ? '编辑选中配置' : '新增选中配置'"
v-model:open="modalVisible"
:confirm-loading="loading"
width="500px"
:destroy-on-close="true"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
name="selected_config_form"
:labelCol="{ span: 6 }"
>
<a-form-item label="是否通用菜单" name="universal">
<a-radio-group
v-model:value="formData.universal"
@change="formData.universal === 1 ? (formData.moduleId = null) : null"
>
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="所属菜单"
name="moduleId"
v-if="formData.universal === 0"
>
<a-tree-select
v-model:value="formData.moduleId"
:tree-data="menuTreeData"
:field-names="{ label: 'name', value: 'id', children: 'children' }"
tree-node-filter-prop="name"
style="width: 100%"
placeholder="请选择所属菜单"
allow-clear
show-search
tree-default-expand-all
/>
</a-form-item>
<a-form-item label="是否选中" name="enable">
<a-radio-group v-model:value="formData.enable">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序" name="orderIndex">
<a-input-number
v-model:value="formData.orderIndex"
style="width: 100%"
placeholder="请输入排序"
:min="0"
:step="1"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { listRoutes } from '@/api/menu';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
const formRef = ref();
const menuTreeData = ref<any[]>([]);
const defaultFormData = reactive({
id: undefined,
legendId: undefined,
moduleId: undefined,
enable: 1,
orderIndex: 0,
universal: 0
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
universal: [
{ required: true, message: '请选择是否通用菜单', trigger: 'change' }
],
moduleId: [{ required: true, message: '请选择所属菜单', trigger: 'change' }],
enable: [{ required: true, message: '请选择是否选中', trigger: 'change' }],
orderIndex: [{ required: true, message: '请输入排序', trigger: 'blur' }]
};
const filterOption = (input: string, option: any) => {
console.log('filterOption', input, option);
if (!option.name) return false;
return option.name.indexOf(input) !== -1;
};
const isEdit = computed(() => !!props.initialValues);
//
const fetchMenuTree = async () => {
try {
const res = await listRoutes();
console.log(res);
menuTreeData.value = res || [];
} catch (error) {
console.error('获取菜单数据失败:', error);
}
};
const handleOk = async () => {
try {
await formRef.value.validate();
const values = { ...formData, systemId: 'qgc', systemName: 'qgc' };
if (values.universal === 0) {
delete values.universal;
} else {
delete values.universal;
values.moduleId = 'tycd';
}
console.log(values);
emit('ok', values);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
resetForm();
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
};
const initForm = () => {
if (props.initialValues) {
Object.keys(formData).forEach(key => {
if (props.initialValues.hasOwnProperty(key)) {
formData[key] = props.initialValues[key];
}
});
} else {
resetForm();
}
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
initForm();
if (menuTreeData.value.length === 0) {
fetchMenuTree();
}
}
},
{ immediate: false }
);
</script>
<style scoped lang="scss"></style>