添加数据查询 基本信息 环保数据
This commit is contained in:
parent
d177ebec69
commit
0364558f17
@ -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: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://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'
|
||||
>>>>>>> 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:
|
||||
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
||||
|
||||
@ -184,6 +184,7 @@
|
||||
</div> -->
|
||||
<a-select
|
||||
v-else-if="item.type === 'Select'"
|
||||
:key="`${item.name}-${item.options?.length ?? 'loading'}`"
|
||||
v-model:value="formData[item.name]"
|
||||
:placeholder="item.placeholder || '请选择'"
|
||||
:allow-clear="item.fieldProps?.allowClear"
|
||||
|
||||
@ -74,12 +74,12 @@ const videoPreviewVisible = ref(false);
|
||||
const previewVideoUrl = ref('');
|
||||
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = url;
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
previewVideoUrl.value = url;
|
||||
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
@ -134,12 +134,12 @@ const filterFtpOption = (input: string, option: any) => {
|
||||
};
|
||||
|
||||
const openImagePreview = (url: string) => {
|
||||
previewImageUrl.value = url;
|
||||
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
const openVideoPreview = (url: string) => {
|
||||
currentVideoUrl.value = url;
|
||||
currentVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||
videoPreviewVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
137
frontend/src/composables/useTimeScale.ts
Normal file
137
frontend/src/composables/useTimeScale.ts
Normal 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
|
||||
};
|
||||
}
|
||||
150
frontend/src/utils/buildTimeFilters.ts
Normal file
150
frontend/src/utils/buildTimeFilters.ts
Normal 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 固定 MON,year 和 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;
|
||||
}
|
||||
246
frontend/src/utils/exportExcel.ts
Normal file
246
frontend/src/utils/exportExcel.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user