-
+
+
+
@@ -345,7 +383,15 @@ import type { ECharts } from 'echarts';
import BasicTable from '@/components/BasicTable/index.vue';
import CommonAttachmentModal from '@/components/MapModal/components/CommonAttachmentModal.vue';
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
-import { getAisbdbyxList, getAiChartTable, getAiPfwRecord, getAiRecord, getAiFile } from '@/api/zngj';
+import VideoPlayer from './components/VideoPlayer.vue';
+import {
+ getAisbdbyxList,
+ getAiChartTable,
+ getAiPfwRecord,
+ getAiRecord,
+ getAiFile,
+ getAiRstcd
+} from '@/api/zngj';
import { postIdUrl } from '@/api/mapModal';
import dayjs from 'dayjs';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
@@ -368,8 +414,10 @@ const jidiOptions = ref
>([]);
const searchData = ref({
baseid: props.baseid || 'all',
stnm: '',
+ rstcd: '',
month: props.month ? dayjs(props.month) : dayjs()
});
+const stcdData = ref>([]);
// ==================== 弹框状态 ====================
const attachmentModalVisible = ref(false);
@@ -431,7 +479,10 @@ const detailVideoTake = ref(20);
const detailVideoPageSizeOptions = ['20', '50', '100'];
const detailVideoTotal = ref(0);
const detailActiveFid = ref('');
-const detailActiveMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
+const detailActiveMedia = ref<{ type: string; src: string }>({
+ type: '',
+ src: ''
+});
const detailTabItems = ref([]);
const detailDate = ref('');
const detailType = ref('');
@@ -439,15 +490,53 @@ const detailDataType = ref('');
const qingpiaoModalVisible = ref(false);
const qingpiaoFids = ref([]);
-// 日历状态映射
-const calendarStatusMap: Record = {
- '0': { name: '无漂浮物', className: 'content_state2', type: 'AI_5001', dataType: '3' },
- '1': { name: '小面积漂浮物', className: 'content_state5', type: 'AI_5001', dataType: '4' },
- '2': { name: '大面积漂浮物', className: 'content_state3', type: 'AI_5001', dataType: '5' },
- '3': { name: '运行', className: 'content_state2', type: 'AI_5005', dataType: '2' },
- '4': { name: '未运行', className: 'content_state3', type: 'AI_5005', dataType: '1' },
- '5': { name: '清漂', className: 'content_state2', type: 'AI_5011', dataType: '3' },
- '6': { name: '未清漂', className: 'content_state3', type: 'AI_5011', dataType: '3' }
+// 日历状态映射(与React端一致)
+const calendarStatusMap: Record<
+ string,
+ { name: string; className: string; type: string; dataType: string }
+> = {
+ '3': {
+ name: '运行',
+ className: 'content_state2',
+ type: 'AI_5005',
+ dataType: '2'
+ },
+ '4': {
+ name: '未运行',
+ className: 'content_state3',
+ type: 'AI_5005',
+ dataType: '1'
+ },
+ '5': {
+ name: '清漂',
+ className: 'content_state2',
+ type: 'AI_5011',
+ dataType: '3'
+ },
+ '6': {
+ name: '未清漂',
+ className: 'content_state3',
+ type: 'AI_5011',
+ dataType: '3'
+ },
+ '0': {
+ name: '无漂浮物',
+ className: 'content_state2',
+ type: 'AI_5001',
+ dataType: '3'
+ },
+ '1': {
+ name: '小面积漂浮物',
+ className: 'content_state5',
+ type: 'AI_5001',
+ dataType: '4'
+ },
+ '2': {
+ name: '大面积漂浮物',
+ className: 'content_state3',
+ type: 'AI_5001',
+ dataType: '5'
+ }
};
// 函数式 ref 绑定
@@ -714,12 +803,30 @@ const tableSearchParams = computed(() => ({
}));
// ==================== 数据转换 ====================
+const flattenNestedData = (data: any[]): any[] => {
+ const result: any[] = [];
+ data.forEach((level1: any) => {
+ if (level1?.items?.length) {
+ level1.items.forEach((level2: any) => {
+ if (level2?.items?.length) {
+ level2.items.forEach((level3: any) => {
+ if (level3?.items?.length) {
+ result.push(...level3.items);
+ } else {
+ result.push(level3);
+ }
+ });
+ }
+ });
+ }
+ });
+ return result;
+};
+
const customTransform = (res: any) => {
- const dataObj = res?.data;
- const records = Array.isArray(dataObj) ? dataObj : dataObj?.data || [];
return {
- records,
- total: dataObj?.total || 0
+ records: res?.data?.data || [],
+ total: res?.data?.total || 0
};
};
@@ -802,27 +909,63 @@ const getChartData = async () => {
const params = {
filter,
group,
- groupResultFlat: true
+ sort: [
+ {
+ field: 'rstcdStepSort',
+ dir: 'asc'
+ },
+ {
+ field: 'tm',
+ dir: 'desc'
+ }
+ ]
};
const res = await getAiChartTable(params);
const dataObj = res?.data?.data ?? {};
const rawData = Array.isArray(dataObj) ? dataObj : dataObj?.data || [];
- const list = rawData.map((el: any) => {
- if (searchData.value.baseid === 'all') {
- return {
- name: el?.baseName_value || el?.baseName || '-',
- value: el?.count_baseName || el?.count_baseId || 0,
- key: el?.baseId_value || el?.baseId
- };
- }
- return {
- name: el?.ennm_value || el?.ennm || '-',
- value: el?.count_rstcd || el?.count_ennm || 0,
- key: el?.rstcd || el?.rstcd_value
- };
- });
+ const list: { name: string; value: number; key: string }[] = [];
+
+ if (searchData.value.baseid === 'all') {
+ rawData.forEach((level1: any) => {
+ if (level1?.items?.length) {
+ level1.items.forEach((level2: any) => {
+ if (level2?.items?.length) {
+ level2.items.forEach((level3: any) => {
+ if (level3?.items?.length) {
+ level3.items.forEach((item: any) => {
+ list.push({
+ name: item?.BASENAME || level3.key || '-',
+ value:
+ item?.COUNT_BASENAME ||
+ item?.COUNT_BASEID ||
+ level3.count ||
+ 0,
+ key: item?.BASEID || level2.key || ''
+ });
+ });
+ } else {
+ list.push({
+ name: level3.key || '-',
+ value: level3.count || 0,
+ key: level2.key || ''
+ });
+ }
+ });
+ }
+ });
+ }
+ });
+ } else {
+ rawData.forEach((el: any) => {
+ list.push({
+ name: el?.ennm_value || el?.ennm || '-',
+ value: el?.count_rstcd || el?.count_ennm || 0,
+ key: el?.rstcd || el?.rstcd_value || ''
+ });
+ });
+ }
chartData.value = list;
} catch (error) {
@@ -1103,64 +1246,74 @@ const fetchVideoList = async () => {
};
const res = await getAiChartTable(params);
- const dataObj = res?.data?.data ?? {};
- const rawData = dataObj?.data || [];
- videoTotal.value = dataObj?.total || 0;
+ const rData = res?.data ?? {};
+ const rawData = rData?.data || [];
+ videoTotal.value = rData?.total || rawData.length;
+
+ // 按 fid 展开(与 React 一致,支持逗号分隔的多个 fid)
+ const expandedData = rawData
+ .map((item: any) => {
+ const fids = item.fid ? item.fid.split(',') : [];
+ return fids.map((fid: string) => ({ ...item, fid: fid.trim() }));
+ })
+ .flat();
// 收集所有 fid
- const allFids: string[] = [];
- rawData.forEach((item: any) => {
- if (item.fid) {
- item.fid.split(',').forEach((f: string) => {
- const trimmed = f.trim();
- if (trimmed) allFids.push(trimmed);
- });
- }
- });
+ const allFids = expandedData
+ .filter((item: any) => item.fid)
+ .map((item: any) => item.fid)
+ .join(',');
// 调用 postIdUrl 获取媒体详情
let mediaMap: Record = {};
- if (allFids.length > 0) {
+ if (allFids) {
const formData = new FormData();
- formData.append('id', allFids.join(','));
+ formData.append('id', allFids);
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
- // 按 fid 展开为独立项
- const flatList: any[] = [];
- rawData.forEach((item: any) => {
- const fids = item.fid
- ? item.fid
- .split(',')
- .map((f: string) => f.trim())
- .filter(Boolean)
- : [];
- const tm = item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
+ // 构建列表(与 React 一致)
+ const list: any[] = [];
+ expandedData.forEach((vd: any) => {
+ const fileInfo = mediaMap[vd.fid];
+ const hasValidExtension =
+ fileInfo && ['.jpg', '.png'].includes(fileInfo.ext);
- fids.forEach((fid: string) => {
- const m = mediaMap[fid];
- const ext = m?.ext || '';
- const isVideo = ext === '.mp4';
- const isImage = ext === '.jpg' || ext === '.png';
+ let flpth = '';
+ let imgPath = '';
+ let type = '';
- flatList.push({
- ...item,
- fid,
- media: m || null,
- thumbUrl: isImage ? (baseUrl + '?' + fid + '&view=jpg') : '',
- isVideo,
- isImage,
- tm
- });
+ if (hasValidExtension !== undefined && !hasValidExtension) {
+ // 非图片(视频)
+ flpth = fileInfo.fullpath;
+ type = 'vdsp';
+ } else if (hasValidExtension !== undefined) {
+ // 图片
+ imgPath = fileInfo.fullpath;
+ type = 'img';
+ }
+
+ const tm = vd.tm ? dayjs(vd.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
+ list.push({
+ ...vd,
+ flpth,
+ imgPath,
+ flnm: `${vd.ennm || ''}${tm}`,
+ type,
+ tm
});
});
- videoList.value = flatList;
-
+ // 过滤掉无效项(与 React 一致)
+ const listMap = list.filter(
+ item => item.flpth !== '' || item.imgPath !== ''
+ );
+ videoList.value = listMap;
+ // debugger
// 默认选中第一条
- if (videoList.value.length > 0 && !activeFid.value) {
- handleSelectMedia(videoList.value[0]);
+ if (listMap.length > 0 && !activeFid.value) {
+ handleSelectMedia(listMap[0]);
}
} catch (error) {
console.error('获取视频列表失败:', error);
@@ -1170,15 +1323,20 @@ const fetchVideoList = async () => {
}
};
-// 选择媒体
+// 选择媒体(与 React 一致)
const handleSelectMedia = (item: any) => {
activeFid.value = item.fid;
- const { media, isVideo, isImage } = item;
- if (isImage) {
- activeMedia.value = { type: 'image', src: baseUrl + '?' + item.fid + '&view=jpg' };
- } else if (isVideo) {
- activeMedia.value = { type: 'video', src: baseUrl + '?' + item.fid + '&view=jpg' };
+ if (item.type === 'img') {
+ activeMedia.value = {
+ type: 'image',
+ src: baseUrl + '?' + item.fid + '&view=jpg'
+ };
+ } else if (item.type === 'vdsp') {
+ activeMedia.value = {
+ type: 'video',
+ src: baseUrl + '?' + item.fid + '&view=jpg'
+ };
} else {
activeMedia.value = { type: '', src: '' };
}
@@ -1191,58 +1349,154 @@ const handleVideoPageChange = () => {
fetchVideoList();
};
-// ==================== 日历视图函数 ====================
-// 判断是否是当前月份
-const isCurrentMonth = (current: any): boolean => {
- const selectedDate = dayjs(searchData.value.month);
- return current.month() === selectedDate.month() && current.year() === selectedDate.year();
-};
-
-// 获取某天的日历数据
-const getCalendarDataForDay = (current: any): any => {
- if (!isCurrentMonth(current)) return null;
- const day = current.format('DD');
- return calendarData.value.find((item: any) => dayjs(item.dt).format('DD') === day);
-};
-
-// 获取状态样式类
-const getStatusClass = (status: string) => {
- return calendarStatusMap[status]?.className || '';
-};
-
-// 获取状态文本
-const getStatusText = (status: string) => {
- return calendarStatusMap[status]?.name || '';
-};
-
-// 获取日历数据
-const fetchCalendarData = async () => {
- calendarLoading.value = true;
+// ==================== 获取电站列表(用于日历视图的rstcd下拉) ====================
+const colStcdData = async () => {
try {
const type = getType();
const filters: any[] = [
{ field: 'type', operator: 'eq', value: type },
- searchData.value.baseid !== 'all' ? { field: 'baseId', operator: 'eq', value: searchData.value.baseid } : null,
+ searchData.value.baseid !== 'all'
+ ? { field: 'baseId', operator: 'eq', value: searchData.value.baseid }
+ : null,
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
- value: dayjs(searchData.value.month).startOf('month').format('YYYY-MM-DD 00:00:00')
+ value: dayjs(searchData.value.month)
+ .startOf('month')
+ .format('YYYY-MM-DD 00:00:00')
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
- value: dayjs(searchData.value.month).endOf('month').format('YYYY-MM-DD 23:59:59')
+ value: dayjs(searchData.value.month)
+ .endOf('month')
+ .format('YYYY-MM-DD 23:59:59')
}
].filter(Boolean);
const params = { filter: { logic: 'and', filters } };
+ const res: any = await getAiRstcd(params);
+ const _data = res?.data?.data?.data ?? [];
+ const list = _data.map((item: any) => ({
+ label: item.stnm,
+ value: item.stcd
+ }));
- // 根据类型调用不同接口
+ // 如果当前选中的rstcd不在新列表中,重置为第一个
+ const selected = list?.filter(
+ (item: any) => item.value === searchData.value.rstcd
+ );
+ if (selected?.length === 0) {
+ searchData.value.rstcd = list?.[0]?.value ?? '';
+ }
+
+ stcdData.value = list;
+ } catch (error) {
+ console.error('获取电站列表失败:', error);
+ }
+};
+
+// ==================== 日历视图函数 ====================
+// 判断是否是当前月份
+const isCurrentMonth = (current: any): boolean => {
+ return (
+ current.month() === calendarValue.value.month() &&
+ current.year() === calendarValue.value.year()
+ );
+};
+
+// 获取某天的日历数据
+const getCalendarDataForDay = (current: any): any => {
+ return calendarData.value.find(
+ (item: any) => dayjs(item.dt).format('DD') === current.format('DD')
+ );
+};
+
+// 获取状态样式类(支持数字和字符串类型)
+const getStatusClass = (status: number | string) => {
+ const statusKey = String(status);
+ return calendarStatusMap[statusKey]?.className || '';
+};
+
+// 获取状态文本(支持数字和字符串类型)
+const getStatusText = (status: number | string) => {
+ const statusKey = String(status);
+ return calendarStatusMap[statusKey]?.name || '';
+};
+
+// 获取日历数据(参考React端逻辑)
+const fetchCalendarData = async () => {
+ calendarLoading.value = true;
+ try {
+ const type = getType();
+ const filters: any[] = [
+ // baseId过滤(支持饼图联动)
+ pieCode.value.length > 0 && searchData.value.baseid === 'all'
+ ? {
+ field: 'baseId',
+ operator: 'in',
+ dataType: 'string',
+ value: pieCode.value
+ }
+ : searchData.value.baseid !== 'all'
+ ? { field: 'baseId', operator: 'eq', value: searchData.value.baseid }
+ : null,
+ // rstcd过滤(饼图联动时)
+ pieCode.value.length > 0 && searchData.value.baseid !== 'all'
+ ? {
+ field: 'rstcd',
+ operator: 'in',
+ dataType: 'string',
+ value: pieCode.value
+ }
+ : null,
+ // type过滤
+ { field: 'type', operator: 'eq', value: type },
+ // 时间范围
+ {
+ field: 'startTime',
+ operator: 'gte',
+ dataType: 'date',
+ value: dayjs(searchData.value.month)
+ .startOf('month')
+ .format('YYYY-MM-DD 00:00:00')
+ },
+ {
+ field: 'endTime',
+ operator: 'lte',
+ dataType: 'date',
+ value: dayjs(searchData.value.month)
+ .endOf('month')
+ .format('YYYY-MM-DD 23:59:59')
+ },
+ // stnm文本搜索
+ searchData.value.stnm
+ ? { field: 'stnm', operator: 'contains', value: searchData.value.stnm }
+ : null,
+ // rstcd电站选择(日历视图下)
+ searchData.value.rstcd
+ ? {
+ field: 'rstcd',
+ operator: 'in',
+ dataType: 'string',
+ value: [searchData.value.rstcd]
+ }
+ : null
+ ].filter(Boolean);
+
+ const params = {
+ filter: { logic: 'and', filters },
+ sort: tabIndex.value === '1' ? [] : [{ field: 'tm', dir: 'desc' }]
+ };
+
+ // 根据类型调用不同接口(与React端一致:AI_5005用getAiRecord,其余用getAiPfwRecord)
const apiFn = type === 'AI_5005' ? getAiRecord : getAiPfwRecord;
const res: any = await apiFn(params);
- calendarData.value = res?.data?.data || [];
+ // 接口返回结构: { code, msg, data: [...] } - data 直接是数组,不是 res.data.data
+ const payload = res?.data || {};
+ calendarData.value = Array.isArray(payload) ? payload : payload.data || [];
} catch (error) {
console.error('获取日历数据失败:', error);
calendarData.value = [];
@@ -1254,9 +1508,15 @@ const fetchCalendarData = async () => {
// 日历面板切换
const handleCalendarPanelChange = (date: any) => {
calendarValue.value = date;
+ fetchCalendarData();
};
-// 点击日历日期
+// 日历日期选择
+const handleCalendarSelect = (date: any) => {
+ calendarValue.value = date;
+};
+
+// 点击日历日期(参考React端逻辑)
const handleCalendarDayClick = async (current: any, dayData: any) => {
const date = dayjs(current).format('YYYY-MM-DD');
const status = dayData.status;
@@ -1270,7 +1530,10 @@ const handleCalendarDayClick = async (current: any, dayData: any) => {
if (!fidStr) {
return;
}
- qingpiaoFids.value = fidStr.split(',').map((s: string) => s.trim()).filter(Boolean);
+ qingpiaoFids.value = fidStr
+ .split(',')
+ .map((s: string) => s.trim())
+ .filter(Boolean);
qingpiaoModalVisible.value = true;
return;
}
@@ -1280,18 +1543,20 @@ const handleCalendarDayClick = async (current: any, dayData: any) => {
detailType.value = statusInfo.type;
detailDataType.value = statusInfo.dataType;
- // 设置 Tab 项
- if (tabIndex.value === '1') {
+ // 设置 Tab 项(与React端一致)
+ if (tabIndex.value === '2') {
+ // 叠梁门运行识别:运行 / 未运行
+ detailTabItems.value = [
+ { key: '2', label: '运行' },
+ { key: '1', label: '未运行' }
+ ];
+ } else {
+ // 漂浮物识别:无漂浮物 / 小面积漂浮物 / 大面积漂浮物
detailTabItems.value = [
{ key: '3', label: '无漂浮物' },
{ key: '4', label: '小面积漂浮物' },
{ key: '5', label: '大面积漂浮物' }
];
- } else if (tabIndex.value === '2') {
- detailTabItems.value = [
- { key: '2', label: '运行' },
- { key: '1', label: '未运行' }
- ];
}
detailActiveKey.value = statusInfo.dataType;
@@ -1306,85 +1571,163 @@ const fetchDetailVideoList = async () => {
detailVideoLoading.value = true;
try {
const filters: any[] = [
+ // baseId过滤(支持饼图联动)
+ pieCode.value.length > 0 && searchData.value.baseid === 'all'
+ ? {
+ field: 'baseId',
+ operator: 'in',
+ dataType: 'string',
+ value: pieCode.value
+ }
+ : searchData.value.baseid !== 'all'
+ ? { field: 'baseId', operator: 'eq', value: searchData.value.baseid }
+ : null,
+ // rstcd过滤(饼图联动时)
+ pieCode.value.length > 0 && searchData.value.baseid !== 'all'
+ ? {
+ field: 'rstcd',
+ operator: 'in',
+ dataType: 'string',
+ value: pieCode.value
+ }
+ : null,
+ // type过滤
{ field: 'type', operator: 'eq', value: detailType.value },
- searchData.value.baseid !== 'all' ? { field: 'baseId', operator: 'eq', value: searchData.value.baseid } : null,
+ // 时间范围
{
field: 'tm',
operator: 'gte',
dataType: 'date',
- value: dayjs(detailDate.value).startOf('day').format('YYYY-MM-DD 00:00:00')
+ value: dayjs(detailDate.value)
+ .startOf('day')
+ .format('YYYY-MM-DD 00:00:00')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
- value: dayjs(detailDate.value).endOf('day').format('YYYY-MM-DD 23:59:59')
+ value: dayjs(detailDate.value)
+ .endOf('day')
+ .format('YYYY-MM-DD 23:59:59')
},
- { field: 'dataType', operator: 'eq', value: detailDataType.value }
+ // stnm文本搜索
+ searchData.value.stnm
+ ? { field: 'stnm', operator: 'contains', value: searchData.value.stnm }
+ : null,
+ // rstcd电站选择(日历视图下)
+ searchData.value.rstcd
+ ? {
+ field: 'rstcd',
+ operator: 'in',
+ dataType: 'string',
+ value: [searchData.value.rstcd]
+ }
+ : null
].filter(Boolean);
const params = {
filter: { logic: 'and', filters },
+ sort: tabIndex.value === '1' ? [] : [{ field: 'tm', dir: 'desc' }],
skip: (detailVideoPage.value - 1) * detailVideoTake.value,
take: detailVideoTake.value
};
const res: any = await getAiFile(params);
- const dataObj = res?.data?.data || {};
- const rawData = dataObj?.data || [];
- detailVideoTotal.value = dataObj?.total || 0;
+ console.log('二级弹框接口返回:', res);
+ const rData = res?.data ?? {};
+ const rawData = rData?.data || [];
+ console.log('提取的原始数据:', rawData);
+ detailVideoTotal.value = rData?.total || rawData.length;
+
+ // 按 fid 展开(与 React 一致,支持逗号分隔的多个 fid)
+ const expandedData = rawData
+ .map((item: any) => {
+ const fids = item.fid ? item.fid.split(',') : [];
+ return fids.map((fid: string) => ({ ...item, fid: fid.trim() }));
+ })
+ .flat();
// 收集所有 fid
- const allFids: string[] = [];
- rawData.forEach((item: any) => {
- if (item.fid) {
- item.fid.split(',').forEach((f: string) => {
- const trimmed = f.trim();
- if (trimmed) allFids.push(trimmed);
- });
- }
- });
+ const allFids = expandedData
+ .filter((item: any) => item.fid)
+ .map((item: any) => item.fid)
+ .join(',');
// 调用 postIdUrl 获取媒体详情
let mediaMap: Record = {};
- if (allFids.length > 0) {
+ if (allFids) {
const formData = new FormData();
- formData.append('id', allFids.join(','));
- const mediaRes: any = await postIdUrl(formData);
- mediaMap = mediaRes?.data || {};
+ formData.append('id', allFids);
+ console.log('请求媒体详情, allFids:', allFids);
+ try {
+ const mediaRes: any = await postIdUrl(formData);
+ console.log('媒体详情返回:', mediaRes);
+ mediaMap = mediaRes?.data || {};
+ console.log('mediaMap:', mediaMap);
+ } catch (mediaError) {
+ console.error('获取媒体详情失败:', mediaError);
+ mediaMap = {};
+ }
}
- // 按 fid 展开为独立项
- const flatList: any[] = [];
- rawData.forEach((item: any) => {
- const fids = item.fid
- ? item.fid.split(',').map((f: string) => f.trim()).filter(Boolean)
- : [];
- const tm = item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
+ // 构建列表(与 React 一致)
+ const list: any[] = [];
+ expandedData.forEach((vd: any) => {
+ const fileInfo = mediaMap[vd.fid];
+ const hasValidExtension =
+ fileInfo && ['.jpg', '.png'].includes(fileInfo.ext);
- fids.forEach((fid: string) => {
- const m = mediaMap[fid];
- const ext = m?.ext || '';
- const isVideo = ext === '.mp4';
- const isImage = ext === '.jpg' || ext === '.png';
+ let flpth = '';
+ let imgPath = '';
+ let type = '';
- flatList.push({
- ...item,
- fid,
- media: m || null,
- thumbUrl: isImage ? (baseUrl + '?' + fid + '&view=jpg') : '',
- isVideo,
- isImage,
- tm
- });
+ if (hasValidExtension !== undefined && !hasValidExtension) {
+ // 非图片(视频)
+ flpth = fileInfo.fullpath;
+ type = 'vdsp';
+ } else if (hasValidExtension !== undefined) {
+ // 图片
+ imgPath = fileInfo.fullpath;
+ type = 'img';
+ }
+
+ const tm = vd.tm ? dayjs(vd.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
+ list.push({
+ ...vd,
+ flpth,
+ imgPath,
+ flnm: `${vd.ennm || ''}${tm}`,
+ type,
+ tm
});
});
- detailVideoList.value = flatList;
+ console.log('构建的列表(过滤前):', list);
+
+ // 过滤掉无效项(与 React 一致)
+ const listMap = list.filter(
+ item => item.flpth !== '' || item.imgPath !== ''
+ );
+ console.log('过滤后的列表:', listMap);
+ detailVideoList.value = listMap;
// 默认选中第一条
- if (detailVideoList.value.length > 0) {
- handleSelectDetailMedia(detailVideoList.value[0]);
+ if (listMap.length > 0) {
+ const first = listMap[0];
+ detailActiveFid.value = first.fid;
+ if (first.type === 'img') {
+ detailActiveMedia.value = {
+ type: 'image',
+ src: baseUrl + '?' + first.fid + '&view=jpg'
+ };
+ } else if (first.type === 'vdsp') {
+ detailActiveMedia.value = {
+ type: 'video',
+ src: baseUrl + '?' + first.fid + '&view=jpg'
+ };
+ } else {
+ detailActiveMedia.value = { type: '', src: '' };
+ }
}
} catch (error) {
console.error('获取详情视频列表失败:', error);
@@ -1394,22 +1737,29 @@ const fetchDetailVideoList = async () => {
}
};
-// 选择详情媒体
+// 选择详情媒体(与 React 一致)
const handleSelectDetailMedia = (item: any) => {
detailActiveFid.value = item.fid;
- const { isVideo, isImage } = item;
- if (isImage) {
- detailActiveMedia.value = { type: 'image', src: baseUrl + '?' + item.fid + '&view=jpg' };
- } else if (isVideo) {
- detailActiveMedia.value = { type: 'video', src: baseUrl + '?' + item.fid + '&view=jpg' };
+ if (item.type === 'img') {
+ detailActiveMedia.value = {
+ type: 'image',
+ src: baseUrl + '?' + item.fid + '&view=jpg'
+ };
+ } else if (item.type === 'vdsp') {
+ detailActiveMedia.value = {
+ type: 'video',
+ src: baseUrl + '?' + item.fid + '&view=jpg'
+ };
} else {
detailActiveMedia.value = { type: '', src: '' };
}
};
// 详情分页切换
-const handleDetailVideoPageChange = () => {
+const handleDetailVideoPageChange = (page: number, pageSize: number) => {
+ detailVideoPage.value = page;
+ detailVideoTake.value = pageSize;
detailActiveFid.value = '';
detailActiveMedia.value = { type: '', src: '' };
fetchDetailVideoList();
@@ -1451,6 +1801,31 @@ watch(
{ deep: true, immediate: true }
);
+// ==================== 监听视图切换 ====================
+watch(vdListShow, () => {
+ if (vdListShow.value === '1') {
+ fetchVideoList();
+ getChartData();
+ } else if (vdListShow.value === '2') {
+ getChartData();
+ nextTick(() => {
+ const filter = buildFilter();
+ getTableInstance()?.getList(filter);
+ });
+ } else if (vdListShow.value === '3') {
+ colStcdData();
+ fetchCalendarData();
+ }
+});
+
+// ==================== 监听baseid/tabIndex/month变化,更新电站列表 ====================
+watch(
+ () => [searchData.value.baseid, tabIndex.value, searchData.value.month],
+ () => {
+ colStcdData();
+ }
+);
+
// ==================== 监听 Tab 切换 ====================
watch(tabIndex, () => {
pieCode.value = [];
@@ -1479,6 +1854,14 @@ watch(tabIndex, () => {
});
});
+// ==================== 监听详情弹窗 Tab 切换 ====================
+watch(detailActiveKey, () => {
+ if (detailModalVisible.value) {
+ detailVideoPage.value = 1;
+ fetchDetailVideoList();
+ }
+});
+
// ==================== 窗口 resize ====================
const handleResize = () => {
const dom = chartRefs.value.get(tabIndex.value);
@@ -1644,6 +2027,7 @@ onBeforeUnmount(() => {
.media-empty {
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
@@ -1724,6 +2108,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
+ flex-direction: column;
}
.sidebar-pagination {
@@ -1733,5 +2118,179 @@ onBeforeUnmount(() => {
}
}
}
+
+ .calendar-mode {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height:60vh;
+ overflow: auto;
+ padding-top:200px;
+
+ :deep(.ant-picker-calendar) {
+ width: 90%;
+ max-width: 1200px;
+ }
+
+ :deep(.ant-picker-content thead th) {
+ font-size: 0;
+ }
+
+ :deep(.ant-picker-content thead th)::before {
+ font-size: 16px;
+ font-weight: 700;
+ padding-right: 10px;
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(1))::before {
+ content: '周一';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(2))::before {
+ content: '周二';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(3))::before {
+ content: '周三';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(4))::before {
+ content: '周四';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(5))::before {
+ content: '周五';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(6))::before {
+ content: '周六';
+ }
+
+ :deep(.ant-picker-content thead tr th:nth-child(7))::before {
+ content: '周日';
+ }
+
+ :deep(.ant-picker-calendar) {
+ .ant-picker-calendar-date-value {
+ font-size: 18px !important;
+ padding-right: 8px;
+ }
+
+ .ant-picker-calendar-date {
+ border: 1px solid rgb(213, 225, 233) !important;
+ margin: 0px !important;
+ // height: 75px !important;
+ }
+
+ .ant-picker-panel {
+ .ant-picker-content {
+ thead {
+ th {
+ padding: 6px 0;
+ border: 1px solid rgb(213, 225, 233) !important;
+ }
+ }
+
+ td {
+ padding: 0;
+
+ .ant-picker-calendar-date {
+ padding: 3px;
+ border: none;
+ }
+
+ &.ant-picker-cell-selected .ant-picker-calendar-date {
+ background: transparent;
+ }
+
+ &.ant-picker-cell-today .ant-picker-calendar-date::before {
+ display: none;
+ }
+ }
+ }
+ }
+ }
+
+ .calendar-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+ padding: 2px;
+ text-align: right;
+
+ .content_state1 {
+ width: 100%;
+ background-color: #67c23a;
+ color: #000;
+ padding: 2px 0;
+ border-radius: 3px;
+ cursor: auto;
+ padding-right: 8px;
+ font-size: 12px;
+ }
+
+ .content_state4 {
+ width: 100%;
+ background-color: #e6a23c;
+ padding: 2px 0;
+ border-radius: 3px;
+ cursor: auto;
+ padding-right: 8px;
+ font-size: 12px;
+ }
+
+ .content_state2 {
+ width: 100%;
+ background-color: #e1f3d8;
+ padding: 2px 0;
+ border-radius: 3px;
+ cursor: pointer;
+ padding-right: 8px;
+ font-size: 12px;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ .content_state3 {
+ width: 100%;
+ background-color: #faecd8;
+ padding: 2px 0;
+ border-radius: 3px;
+ cursor: pointer;
+ padding-right: 8px;
+ font-size: 12px;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ .content_state5 {
+ width: 100%;
+ background-color: #faecd8;
+ padding: 2px 0;
+ border-radius: 3px;
+ cursor: pointer;
+ padding-right: 8px;
+ font-size: 12px;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+
+ .detail-empty {
+ height: 600px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
}
diff --git a/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue
new file mode 100644
index 00000000..708cc412
--- /dev/null
+++ b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/usePieChart.ts b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/usePieChart.ts
new file mode 100644
index 00000000..c6f29e19
--- /dev/null
+++ b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/usePieChart.ts
@@ -0,0 +1,119 @@
+import * as echarts from 'echarts';
+import type { ECharts, EChartsOption } from 'echarts';
+
+export interface ChartDataItem {
+ name: string;
+ value: number;
+ key: string;
+}
+
+export function usePieChart(
+ chartRefs: Map,
+ chartData: { value: ChartDataItem[] },
+ tabIndex: { value: string },
+ typeMap: Record,
+ legendCallback?: (e: any) => void
+) {
+ let chartInstance: ECharts | null = null;
+
+ const initChart = (retryCount = 0) => {
+ const dom = chartRefs.get(tabIndex.value);
+ if (!dom) {
+ if (retryCount < 10) {
+ setTimeout(() => initChart(retryCount + 1), 100);
+ }
+ return;
+ }
+
+ const rect = dom.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) {
+ if (retryCount < 10) {
+ setTimeout(() => initChart(retryCount + 1), 100);
+ }
+ return;
+ }
+
+ if (!chartInstance) {
+ chartInstance = echarts.init(dom);
+ }
+
+ const option: EChartsOption = {
+ tooltip: {
+ trigger: 'item',
+ formatter: (params: any) => `${params.marker} ${params.name}: ${params.value}(次)`
+ },
+ legend: {
+ orient: 'vertical',
+ right: 10,
+ top: 80,
+ textStyle: { fontSize: 12 }
+ },
+ series: []
+ };
+
+ if (!chartData.value || chartData.value.length === 0) {
+ chartInstance.setOption(option, true);
+ return;
+ }
+
+ const total = chartData.value.reduce((acc: number, item: ChartDataItem) => acc + (item.value || 0), 0);
+ const titleText = typeMap[tabIndex.value] || '识别(次)';
+
+ option.series = [
+ {
+ type: 'pie',
+ radius: ['26%', '35%'],
+ center: ['35%', '50%'],
+ avoidLabelOverlap: false,
+ label: {
+ show: true,
+ formatter: '{c}'
+ },
+ labelLine: {
+ show: true,
+ length: 12,
+ length2: 12
+ },
+ data: chartData.value.map((item: ChartDataItem) => ({
+ name: item.name,
+ value: item.value
+ }))
+ }
+ ];
+
+ option.title = {
+ text: String(total),
+ left: '34%',
+ top: '44%',
+ textAlign: 'center',
+ textStyle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#333'
+ },
+ subtext: titleText,
+ subtextStyle: {
+ fontSize: 12,
+ color: '#333'
+ }
+ };
+
+ chartInstance.setOption(option, true);
+
+ if (legendCallback) {
+ chartInstance.off('legendselectchanged');
+ chartInstance.on('legendselectchanged', legendCallback);
+ }
+ };
+
+ const dispose = () => {
+ chartInstance?.dispose();
+ chartInstance = null;
+ };
+
+ const resize = () => {
+ chartInstance?.resize();
+ };
+
+ return { initChart, dispose, resize };
+}
diff --git a/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/useVideoList.ts b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/useVideoList.ts
new file mode 100644
index 00000000..3fd42b36
--- /dev/null
+++ b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/composables/useVideoList.ts
@@ -0,0 +1,131 @@
+import { ref } from 'vue';
+import { postIdUrl } from '@/api/mapModal';
+import dayjs from 'dayjs';
+
+export interface VideoListItem {
+ fid: string;
+ media: any;
+ thumbUrl: string;
+ isVideo: boolean;
+ isImage: boolean;
+ tm: string;
+ ennm?: string;
+ [key: string]: any;
+}
+
+export function useVideoList(
+ fetchFn: (params: any) => Promise,
+ baseUrl: string
+) {
+ const page = ref(1);
+ const take = ref(20);
+ const total = ref(0);
+ const loading = ref(false);
+ const list = ref([]);
+ const activeFid = ref('');
+ const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
+
+ const fetchList = async (filter: any, options: any = {}) => {
+ loading.value = true;
+ try {
+ const skip = (page.value - 1) * take.value;
+ const params = { filter, skip, take: take.value, ...options };
+ const res = await fetchFn(params);
+ const dataObj = res?.data?.data ?? {};
+ const rawData = dataObj?.data || [];
+ total.value = dataObj?.total || 0;
+
+ const allFids: string[] = [];
+ rawData.forEach((item: any) => {
+ if (item.fid) {
+ item.fid.split(',').forEach((f: string) => {
+ const trimmed = f.trim();
+ if (trimmed) allFids.push(trimmed);
+ });
+ }
+ });
+
+ let mediaMap: Record = {};
+ if (allFids.length > 0) {
+ const formData = new FormData();
+ formData.append('id', allFids.join(','));
+ const mediaRes: any = await postIdUrl(formData);
+ mediaMap = mediaRes?.data || {};
+ }
+
+ const flatList: VideoListItem[] = [];
+ rawData.forEach((item: any) => {
+ const fids = item.fid
+ ? item.fid.split(',').map((f: string) => f.trim()).filter(Boolean)
+ : [];
+ const tm = item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
+
+ fids.forEach((fid: string) => {
+ const m = mediaMap[fid];
+ const ext = m?.ext || '';
+ const isVideo = ext === '.mp4';
+ const isImage = ext === '.jpg' || ext === '.png';
+
+ flatList.push({
+ ...item,
+ fid,
+ media: m || null,
+ thumbUrl: isImage ? baseUrl + '?' + fid + '&view=jpg' : '',
+ isVideo,
+ isImage,
+ tm
+ });
+ });
+ });
+
+ list.value = flatList;
+
+ if (list.value.length > 0 && !activeFid.value) {
+ selectMedia(list.value[0]);
+ }
+ } catch (error) {
+ console.error('获取视频列表失败:', error);
+ list.value = [];
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const selectMedia = (item: VideoListItem) => {
+ activeFid.value = item.fid;
+ const { isVideo, isImage } = item;
+
+ if (isImage) {
+ activeMedia.value = { type: 'image', src: baseUrl + '?' + item.fid + '&view=jpg' };
+ } else if (isVideo) {
+ activeMedia.value = { type: 'video', src: baseUrl + '?' + item.fid + '&view=jpg' };
+ } else {
+ activeMedia.value = { type: '', src: '' };
+ }
+ };
+
+ const handlePageChange = () => {
+ activeFid.value = '';
+ activeMedia.value = { type: '', src: '' };
+ };
+
+ const reset = () => {
+ page.value = 1;
+ activeFid.value = '';
+ activeMedia.value = { type: '', src: '' };
+ };
+
+ return {
+ page,
+ take,
+ total,
+ loading,
+ list,
+ activeFid,
+ activeMedia,
+ fetchList,
+ selectMedia,
+ handlePageChange,
+ reset
+ };
+}
diff --git a/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/types/index.ts b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/types/index.ts
new file mode 100644
index 00000000..55970af8
--- /dev/null
+++ b/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/types/index.ts
@@ -0,0 +1,60 @@
+export enum TabType {
+ PFW = '1',
+ DLM = '2',
+ QP = '3'
+}
+
+export enum AiType {
+ PFW = 'AI_5001',
+ DLM = 'AI_5005',
+ QP = 'AI_5011'
+}
+
+export interface ChartDataItem {
+ name: string;
+ value: number;
+ key: string;
+}
+
+export interface VideoListItem {
+ fid: string;
+ media: any;
+ thumbUrl: string;
+ isVideo: boolean;
+ isImage: boolean;
+ tm: string;
+ ennm?: string;
+ [key: string]: any;
+}
+
+export interface CalendarDayData {
+ dt: string;
+ status?: string;
+ qpStatus?: string;
+ fid?: string;
+}
+
+export interface CalendarStatusInfo {
+ name: string;
+ className: string;
+ type: string;
+ dataType: string;
+}
+
+export interface TabItem {
+ key: number;
+ value: string;
+ unit: string;
+ count: number;
+}
+
+export interface SearchData {
+ baseid: string;
+ stnm: string;
+ month: any;
+}
+
+export interface DetailTabItem {
+ key: string;
+ label: string;
+}
diff --git a/frontend/src/views/shiPinJianKong/components/LiveVideoBox.vue b/frontend/src/views/shiPinJianKong/components/LiveVideoBox.vue
index 269ba916..167bcfe5 100644
--- a/frontend/src/views/shiPinJianKong/components/LiveVideoBox.vue
+++ b/frontend/src/views/shiPinJianKong/components/LiveVideoBox.vue
@@ -32,7 +32,7 @@