添加数据查询 基本信息 环保数据
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:8093'
|
||||||
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
|
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
|
||||||
# 汤伟
|
# 汤伟
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< HEAD
|
|
||||||
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
|
|
||||||
# 李林
|
|
||||||
VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
|
|
||||||
=======
|
|
||||||
VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
|
VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
|
||||||
# VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
|
|
||||||
>>>>>>> fa4e1bc194446adb8adfd7f6404be1fbb50b31c3
|
|
||||||
=======
|
|
||||||
# VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
|
|
||||||
VITE_APP_BASE_URL = 'http://192.168.1.168:8093'
|
|
||||||
# 李林
|
# 李林
|
||||||
# VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
|
# VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
|
||||||
>>>>>>> Stashed changes
|
# VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
|
||||||
|
# 李林
|
||||||
|
# VITE_APP_BASE_URL = 'http://10.84.121.43:8093'
|
||||||
|
|
||||||
# 测试环境线上10.84.121.122:
|
# 测试环境线上10.84.121.122:
|
||||||
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
|
||||||
|
|||||||
@ -184,6 +184,7 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
<a-select
|
<a-select
|
||||||
v-else-if="item.type === 'Select'"
|
v-else-if="item.type === 'Select'"
|
||||||
|
:key="`${item.name}-${item.options?.length ?? 'loading'}`"
|
||||||
v-model:value="formData[item.name]"
|
v-model:value="formData[item.name]"
|
||||||
:placeholder="item.placeholder || '请选择'"
|
:placeholder="item.placeholder || '请选择'"
|
||||||
:allow-clear="item.fieldProps?.allowClear"
|
:allow-clear="item.fieldProps?.allowClear"
|
||||||
|
|||||||
@ -74,12 +74,12 @@ const videoPreviewVisible = ref(false);
|
|||||||
const previewVideoUrl = ref('');
|
const previewVideoUrl = ref('');
|
||||||
|
|
||||||
const openImagePreview = (url: string) => {
|
const openImagePreview = (url: string) => {
|
||||||
previewImageUrl.value = url;
|
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||||
imagePreviewVisible.value = true;
|
imagePreviewVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openVideoPreview = (url: string) => {
|
const openVideoPreview = (url: string) => {
|
||||||
previewVideoUrl.value = url;
|
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||||
videoPreviewVisible.value = true;
|
videoPreviewVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -134,12 +134,12 @@ const filterFtpOption = (input: string, option: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openImagePreview = (url: string) => {
|
const openImagePreview = (url: string) => {
|
||||||
previewImageUrl.value = url;
|
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||||
imagePreviewVisible.value = true;
|
imagePreviewVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openVideoPreview = (url: string) => {
|
const openVideoPreview = (url: string) => {
|
||||||
currentVideoUrl.value = url;
|
currentVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url;
|
||||||
videoPreviewVisible.value = true;
|
videoPreviewVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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