WholeProcessPlatform/frontend/src/modules/yunXingGaoJIng/Aisbdbyx/AISBTK.vue
2026-07-06 10:27:41 +08:00

2306 lines
60 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="aisbtk-page">
<!-- 搜索表单 -->
<a-form layout="inline" class="search-form">
<a-form-item label="基地选择">
<a-select
v-model:value="searchData.baseid"
placeholder="请选择基地"
style="width: 180px"
:options="jidiOptions"
@change="handleBaseChange"
/>
</a-form-item>
<a-form-item v-if="vdListShow !== '3'" label="电站名称">
<a-input
v-model:value="searchData.stnm"
placeholder="请输入电站"
allow-clear
style="width: 180px"
/>
</a-form-item>
<a-form-item v-else label="电站选择">
<a-select
v-model:value="searchData.rstcd"
placeholder="请选择电站"
style="width: 180px"
:options="stcdData"
allow-clear
/>
</a-form-item>
<a-form-item label="时间">
<a-date-picker
v-model:value="searchData.month"
picker="month"
format="YYYY-MM"
:allow-clear="false"
style="width: 150px"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
</a-form-item>
</a-form>
<!-- Tabs + 图表表格 -->
<a-tabs v-model:activeKey="tabIndex" class="aisbtk-tabs">
<!-- 视图切换按钮 -->
<div class="view-switch-buttons">
<a-tooltip placement="top" title="视频">
<div
class="view-btn"
:class="{ active: vdListShow === '1' }"
@click="vdListShow = '1'"
>
<i class="icon iconfont icon-video"></i>
</div>
</a-tooltip>
<a-tooltip placement="top" title="数据列表">
<div
class="view-btn"
:class="{ active: vdListShow === '2' }"
@click="vdListShow = '2'"
>
<i class="icon iconfont icon-dataTable"></i>
</div>
</a-tooltip>
<a-tooltip placement="top" title="日历">
<div
class="view-btn"
:class="{ active: vdListShow === '3' }"
@click="vdListShow = '3'"
>
<i class="icon iconfont icon-calendar"></i>
</div>
</a-tooltip>
</div>
<a-tab-pane
v-for="item in tabDisplayList"
:key="String(item.key)"
:tab="item.tabLabel"
>
<!-- 数据列表视图(饼图+表格) -->
<div v-show="vdListShow === '2'" class="chart-table-layout">
<!-- 左侧饼图 -->
<div class="chart-section">
<a-spin :spinning="chartLoading">
<div
:ref="(el: any) => setChartRef(el, String(item.key))"
class="chart-container"
></div>
</a-spin>
<div
v-if="!chartLoading && chartData.length === 0"
class="chart-empty-overlay"
>
<a-empty description="暂无数据" />
</div>
</div>
<!-- 右侧表格 -->
<div class="table-section">
<BasicTable
:ref="(el: any) => setTableRef(el, String(item.key))"
:columns="currentColumns"
:scroll-y="450"
:scroll-x="600"
:list-url="getAiChartTable"
:search-params="tableSearchParams"
:transform-data="customTransform"
>
<template #tm="{ record }">
{{ record.tm ? dayjs(record.tm).format('YYYY-MM-DD') : '-' }}
</template>
<template #fid="{ record }">
<a-button
type="link"
style="padding: 4px"
:disabled="!record.fid"
@click="handleViewAttachment(record.fid)"
>
查看附件
</a-button>
</template>
<template #isBigDebrisArea="{ record }">
<span v-if="record.isBigDebrisArea === 1" style="color: red"
>是</span
>
<span
v-else-if="record.isBigDebrisArea === 0"
style="color: green"
>否</span
>
<span v-else>-</span>
</template>
<template #aiVal="{ record }">
{{
record.aiVal === 'open'
? '开启'
: record.aiVal === 'close'
? '关闭'
: record.aiVal ?? '-'
}}
</template>
</BasicTable>
</div>
</div>
<!-- 视频视图 -->
<div v-show="vdListShow === '1'" class="chart-table-layout">
<!-- 左侧饼图 -->
<div class="chart-section">
<a-spin :spinning="chartLoading">
<div
:ref="(el: any) => setVideoChartRef(el, String(item.key))"
class="chart-container"
></div>
</a-spin>
<div
v-if="!chartLoading && chartData.length === 0"
class="chart-empty-overlay"
>
<a-empty description="暂无数据" />
</div>
</div>
<!-- 右侧视频列表 -->
<div class="video-section">
<div class="video-mode">
<div class="video-main" v-loading="videoLoading">
<div
v-if="activeMedia.type === 'image' && activeMedia.src"
class="media-display"
>
<img
:src="activeMedia.src"
style="width: 100%; height: 100%; object-fit: contain"
/>
</div>
<div
v-else-if="activeMedia.type === 'video' && activeMedia.src"
class="media-display"
>
<LiveVideoBox
:videoData="{ url: activeMedia.src }"
height="100%"
:showTitle="false"
/>
</div>
<div v-else class="media-empty">
<a-empty description="请选择右侧数据" />
</div>
</div>
<div class="video-sidebar" v-loading="videoLoading">
<div class="sidebar-list" v-if="videoList.length > 0">
<div
v-for="item in videoList"
:key="item.fid"
class="sidebar-item"
:class="{ active: activeFid === item.fid }"
@click="handleSelectMedia(item)"
>
<!-- {{ item }}12132131313 -->
<div class="item-thumb">
<img v-if="item.type === 'img'" :src="item.imgPath" />
<div
v-else-if="item.type === 'vdsp'"
class="thumb-placeholder"
>
<img :src="videoCover" />
</div>
<span v-else class="no-media">无媒体</span>
</div>
<div class="item-time">{{ item.stName }}{{ item.tm }}</div>
</div>
</div>
<a-empty v-else description="暂无数据" class="sidebar-empty" />
</div>
</div>
<div
class="sidebar-pagination"
style="margin-top: 10px; display: flex; justify-content: end"
>
<a-pagination
v-model:current="videoPage"
v-model:pageSize="videoTake"
:page-size-options="videoPageSizeOptions"
:total="videoTotal"
show-size-changer
size="small"
@change="handleVideoPageChange"
/>
</div>
</div>
</div>
<!-- 日历视图 -->
<div v-show="vdListShow === '3'" class="calendar-mode">
<a-spin :spinning="calendarLoading">
<a-calendar
v-model:value="calendarValue"
@panelChange="handleCalendarPanelChange"
@select="handleCalendarSelect"
>
<template #headerRender="{ current }"> </template>
<template #dateCellRender="{ current }">
<div class="calendar-content">
<template v-if="isCurrentMonth(current)">
<div
class="content_state1"
v-if="getCalendarDataForDay(current)"
>
监测
</div>
<div class="content_state4" v-else>未监测</div>
<!-- 漂浮物识别和清漂识别:显示漂浮物状态和清漂状态 -->
<template v-if="tabIndex === '1' || tabIndex === '3'">
<div
v-if="getCalendarDataForDay(current)?.status"
:class="
getStatusClass(getCalendarDataForDay(current)?.status)
"
@click.stop="
handleCalendarDayClick(
current,
getCalendarDataForDay(current)
)
"
>
{{
getStatusText(getCalendarDataForDay(current)?.status)
}}
</div>
<div
v-if="getCalendarDataForDay(current)?.qpStatus"
:class="
getStatusClass(
getCalendarDataForDay(current)?.qpStatus
)
"
@click.stop="
handleCalendarDayClick(
current,
getCalendarDataForDay(current)
)
"
>
{{
getStatusText(
getCalendarDataForDay(current)?.qpStatus
)
}}
</div>
</template>
<!-- 叠梁门运行识别:只显示运行状态 -->
<template v-else-if="tabIndex === '2'">
<div
v-if="getCalendarDataForDay(current)?.status"
:class="
getStatusClass(getCalendarDataForDay(current)?.status)
"
@click.stop="
handleCalendarDayClick(
current,
getCalendarDataForDay(current)
)
"
>
{{
getStatusText(getCalendarDataForDay(current)?.status)
}}
</div>
</template>
</template>
</div>
</template>
</a-calendar>
</a-spin>
</div>
</a-tab-pane>
</a-tabs>
<!-- 附件预览弹框 -->
<CommonAttachmentModal
v-model:open="attachmentModalVisible"
:fid="currentAttachmentFid"
title="查看附件"
/>
<!-- 日历详情弹窗 -->
<a-modal
v-model:open="detailModalVisible"
title="视频详情"
width="80vw"
:footer="null"
destroyOnClose
:teleport="false"
:bodyStyle="{ height: '70vh' }"
>
<a-tabs v-model:activeKey="detailActiveKey">
<a-tab-pane
v-for="item in detailTabItems"
:key="item.key"
:tab="item.label"
>
<div v-show="detailActiveKey === item.key">
<VideoPlayer
:list="detailVideoList"
:activeFid="detailActiveFid"
:activeMedia="detailActiveMedia"
:loading="detailVideoLoading"
:page="detailVideoPage"
:pageSize="detailVideoTake"
:pageSizeOptions="detailVideoPageSizeOptions"
:total="detailVideoTotal"
@select="handleSelectDetailMedia"
@pageChange="handleDetailVideoPageChange"
/>
</div>
</a-tab-pane>
</a-tabs>
</a-modal>
<!-- 清漂附件预览弹框 -->
<CommonAttachmentModal
v-model:open="qingpiaoModalVisible"
:fid="qingpiaoFids.join(',')"
title="附件预览"
/>
</div>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick
} from 'vue';
import * as echarts from 'echarts';
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 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';
import videoCover from '@/assets/images/videFm.jpg';
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// ==================== Props ====================
const props = defineProps<{
month?: string;
tabIdx?: string;
baseid?: string;
}>();
// ==================== Store ====================
const JidiSelectEventStore = useJidiSelectEventStore();
// ==================== 搜索状态 ====================
const jidiOptions = ref<Array<{ label: string; value: string }>>([]);
const searchData = ref({
baseid: props.baseid || 'all',
stnm: '',
rstcd: '',
month: props.month ? dayjs(props.month) : dayjs()
});
const stcdData = ref<Array<{ label: string; value: string }>>([]);
// ==================== 弹框状态 ====================
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
// ==================== Tabs 状态 ====================
const tabIndex = ref(props.tabIdx ?? '1');
const tabList = ref<any[]>([
{ key: 1, value: '漂浮物识别', unit: '次', count: 0 },
{ key: 3, value: '清漂识别', unit: '次', count: 0 },
{ key: 2, value: '叠梁门运行识别', unit: '次', count: 0 }
]);
// ==================== 视图切换状态 ====================
const vdListShow = ref<'1' | '2' | '3'>('2');
// Tab 显示列表(根据视图类型动态计算标题)
const tabDisplayList = computed(() =>
tabList.value.map(item => ({
...item,
tabLabel:
vdListShow.value === '3'
? item.value
: `${item.value}(${item.count}${item.unit})`
}))
);
// ==================== 图表状态 ====================
const chartRefs = ref<Map<string, HTMLElement>>(new Map());
let chartInstance: ECharts | null = null;
const chartData = ref<any[]>([]);
const chartLoading = ref(true);
const pieCode = ref<string[]>([]);
// ==================== 视频视图状态 ====================
const videoPage = ref(1);
const videoTake = ref(20);
const videoPageSizeOptions = ['20', '50', '100'];
const videoTotal = ref(0);
const videoLoading = ref(false);
const videoList = ref<any[]>([]);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
// 视频视图饼图状态
const videoChartRefs = ref<Map<string, HTMLElement>>(new Map());
let videoChartInstance: ECharts | null = null;
// ==================== 日历视图状态 ====================
const calendarLoading = ref(false);
const calendarData = ref<any[]>([]);
const calendarValue = ref(searchData.value.month);
const detailModalVisible = ref(false);
const detailActiveKey = ref('');
const detailVideoLoading = ref(false);
const detailVideoList = ref<any[]>([]);
const detailVideoPage = ref(1);
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 detailTabItems = ref<any[]>([]);
const detailDate = ref('');
const detailType = ref('');
const detailDataType = computed(() => detailActiveKey.value);
const qingpiaoModalVisible = ref(false);
const qingpiaoFids = ref<string[]>([]);
// 日历状态映射与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 绑定
const setChartRef = (el: any, key: string) => {
if (el instanceof HTMLElement) {
chartRefs.value.set(key, el);
}
};
const setVideoChartRef = (el: any, key: string) => {
if (el instanceof HTMLElement) {
videoChartRefs.value.set(key, el);
}
};
// ==================== 表格引用 ====================
const tableRefs = ref<Map<string, any>>(new Map());
const setTableRef = (el: any, key: string) => {
if (el && !(el instanceof HTMLElement)) {
tableRefs.value.set(key, el);
}
};
// 安全获取 table 实例
const getTableInstance = () => {
return tableRefs.value.get(tabIndex.value) || null;
};
// ==================== 表格列配置 ====================
const pfwColumns = [
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
fixed: 'left' as const,
width: 120,
ellipsis: true
},
{
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
width: 150,
ellipsis: true
},
{
key: 'hbRvcdName',
title: '所在河流',
dataIndex: 'hbRvcdName',
width: 120,
ellipsis: true
},
{
key: 'tm',
title: '时间',
dataIndex: 'tm',
width: 150,
ellipsis: true,
slots: { customRender: 'tm' }
},
{
key: 'aiVal',
title: '漂浮物面积',
dataIndex: 'aiVal',
width: 120,
ellipsis: true
},
{
key: 'isBigDebrisArea',
title: '是否为大面积堆积',
dataIndex: 'isBigDebrisArea',
width: 140,
ellipsis: true,
slots: { customRender: 'isBigDebrisArea' }
},
{
key: 'fid',
title: '操作',
dataIndex: 'fid',
width: 100,
ellipsis: true,
slots: { customRender: 'fid' }
}
];
const dlmColumns = [
{
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
fixed: 'left' as const,
width: 150,
ellipsis: true
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
fixed: 'left' as const,
width: 120,
ellipsis: true
},
{
key: 'tm',
title: '操作时间',
dataIndex: 'tm',
width: 150,
ellipsis: true,
slots: { customRender: 'tm' }
},
{
key: 'aiVal',
title: '操作状态',
dataIndex: 'aiVal',
width: 120,
ellipsis: true,
slots: { customRender: 'aiVal' }
},
{
key: 'fid',
title: '操作',
dataIndex: 'fid',
width: 100,
ellipsis: true,
slots: { customRender: 'fid' }
}
];
const dlmCntColumns = [
{
key: 'hbRvcdName',
title: '所属河流',
dataIndex: 'hbRvcdName',
fixed: 'left' as const,
width: 150,
ellipsis: true
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
fixed: 'left' as const,
width: 120,
ellipsis: true
},
{
key: 'tm',
title: '清漂时间',
dataIndex: 'tm',
width: 150,
ellipsis: true,
slots: { customRender: 'tm' }
},
{
key: 'fid',
title: '操作',
dataIndex: 'fid',
width: 100,
ellipsis: true,
slots: { customRender: 'fid' }
}
];
const currentColumns = computed(() => {
switch (tabIndex.value) {
case '1':
return pfwColumns;
case '2':
return dlmColumns;
case '3':
return dlmCntColumns;
default:
return pfwColumns;
}
});
// ==================== 获取 type ====================
const getType = () => {
switch (tabIndex.value) {
case '1':
return 'AI_5001';
case '2':
return 'AI_5005';
case '3':
return 'AI_5011';
default:
return 'AI_5001';
}
};
// ==================== 构建过滤条件 ====================
const buildFilter = () => {
const filters: any[] = [
{
field: 'type',
operator: 'eq',
value: getType()
},
pieCode.value.length > 0 && searchData.value.baseid === 'all'
? {
field: 'baseId',
operator: 'in',
dataType: 'string',
value: pieCode.value
}
: null,
pieCode.value.length > 0 && searchData.value.baseid !== 'all'
? {
field: 'rstcd',
operator: 'in',
dataType: 'string',
value: pieCode.value
}
: null,
searchData.value.baseid !== 'all' && pieCode.value.length === 0
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: searchData.value.baseid
}
: null,
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(searchData.value.month)
.startOf('month')
.format('YYYY-MM-DD 00:00:00')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(searchData.value.month)
.endOf('month')
.format('YYYY-MM-DD 23:59:59')
},
searchData.value.stnm
? {
field: 'stnm',
operator: 'contains',
value: searchData.value.stnm
}
: null
].filter(Boolean);
return {
logic: 'and',
filters
};
};
// ==================== 表格搜索参数 ====================
const tableSearchParams = computed(() => ({
sort: [
{ field: 'rstcdStepSort', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
],
group: [],
select: [],
groupResultFlat: false
}));
// ==================== 数据转换 ====================
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) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
// ==================== 获取 Tab 数量统计 ====================
const getTabList = async () => {
try {
const params = {
filter: {
logic: 'and',
filters: [
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')
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: dayjs(searchData.value.month)
.endOf('month')
.format('YYYY-MM-DD 23:59:59')
},
searchData.value.stnm
? {
field: 'stnm',
operator: 'contains',
value: searchData.value.stnm
}
: null
].filter(Boolean)
}
};
const res = await getAisbdbyxList(params);
const data = res?.data ?? {};
tabList.value = [
{ key: 1, value: '漂浮物识别', unit: '次', count: data?.pfwCnt ?? 0 },
{ key: 3, value: '清漂识别', unit: '次', count: data?.pfwqlCnt ?? 0 },
{ key: 2, value: '叠梁门运行识别', unit: '次', count: data?.dlmCnt ?? 0 }
];
} catch (error) {
console.error('获取Tab统计失败:', error);
tabList.value = [
{ key: 1, value: '漂浮物识别', unit: '次', count: 0 },
{ key: 3, value: '清漂识别', unit: '次', count: 0 },
{ key: 2, value: '叠梁门运行识别', unit: '次', count: 0 }
];
}
};
// ==================== 获取饼图数据 ====================
const getChartData = async () => {
chartLoading.value = true;
try {
const filter = buildFilter();
const group =
searchData.value.baseid === 'all'
? [
{ dir: 'asc', field: 'baseStepSort' },
{ dir: 'asc', field: 'baseId' },
{ dir: 'asc', field: 'baseName' }
]
: [
{ dir: 'asc', field: 'rstcd' },
{ dir: 'asc', field: 'ennm' }
];
const params = {
filter,
group,
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: { 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) {
console.error('获取饼图数据失败:', error);
chartData.value = [];
} finally {
chartLoading.value = false;
// 等待 DOM 更新后再初始化图表
await nextTick();
setTimeout(() => {
initChart();
initVideoChart();
}, 50);
}
};
// ==================== 初始化饼图 ====================
const initChart = (retryCount = 0) => {
// 根据当前 tabIndex 获取对应 tab 的 DOM
const dom = chartRefs.value.get(tabIndex.value);
if (!dom) {
console.warn(
`[initChart] DOM not found for tab ${tabIndex.value}, retrying...`
);
if (retryCount < 10) {
setTimeout(() => initChart(retryCount + 1), 100);
}
return;
}
// 检查 DOM 是否可见(宽高不为 0
const rect = dom.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
if (retryCount < 10) {
console.warn(
`[initChart] DOM has 0 dimensions, retrying in 100ms... (attempt ${
retryCount + 1
}/10)`
);
setTimeout(() => initChart(retryCount + 1), 100);
} else {
console.error(
'[initChart] DOM still has 0 dimensions after 10 retries, giving up'
);
}
return;
}
if (!chartInstance) {
chartInstance = echarts.init(dom);
}
const option: echarts.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: any) => acc + (item.value || 0),
0
);
const typeMap: Record<string, string> = {
'1': '清漂(次)',
'2': '叠梁门开启(次)',
'3': '清漂识别(次)'
};
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: any) => ({
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);
// 图例点击联动过滤表格
chartInstance.off('legendselectchanged');
chartInstance.on('legendselectchanged', (e: any) => {
const trueRivers = Object.keys(e.selected).filter(
(key: string) => e.selected[key]
);
const allFalse = Object.values(e.selected).every(
(value: any) => value === false
);
const selectedKeys = chartData.value
.filter((item: any) => trueRivers.includes(item.name))
.map((item: any) => item.key);
pieCode.value = allFalse ? [] : selectedKeys;
nextTick(() => {
const filter = buildFilter();
getTableInstance()?.getList(filter);
});
});
};
// ==================== 初始化视频视图饼图 ====================
const initVideoChart = (retryCount = 0) => {
const dom = videoChartRefs.value.get(tabIndex.value);
if (!dom) {
if (retryCount < 10) {
setTimeout(() => initVideoChart(retryCount + 1), 100);
}
return;
}
const rect = dom.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
if (retryCount < 10) {
setTimeout(() => initVideoChart(retryCount + 1), 100);
}
return;
}
if (!videoChartInstance) {
videoChartInstance = echarts.init(dom);
}
const option: echarts.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) {
videoChartInstance.setOption(option, true);
return;
}
const total = chartData.value.reduce(
(acc: number, item: any) => acc + (item.value || 0),
0
);
const typeMap: Record<string, string> = {
'1': '清漂(次)',
'2': '叠梁门开启(次)',
'3': '清漂识别(次)'
};
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: any) => ({
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'
}
};
videoChartInstance.setOption(option, true);
// 图例点击联动过滤视频列表
videoChartInstance.off('legendselectchanged');
videoChartInstance.on('legendselectchanged', (e: any) => {
const trueRivers = Object.keys(e.selected).filter(
(key: string) => e.selected[key]
);
const allFalse = Object.values(e.selected).every(
(value: any) => value === false
);
const selectedKeys = chartData.value
.filter((item: any) => trueRivers.includes(item.name))
.map((item: any) => item.key);
pieCode.value = allFalse ? [] : selectedKeys;
nextTick(() => {
videoPage.value = 1;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
fetchVideoList();
});
});
};
// ==================== 获取视频列表 ====================
const fetchVideoList = async () => {
videoLoading.value = true;
try {
const filter = buildFilter();
const skip = (videoPage.value - 1) * videoTake.value;
const params = {
filter,
skip,
take: videoTake.value,
sort: [
{ field: 'rstcdStepSort', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
]
};
const res = await getAiChartTable(params);
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 = expandedData
.filter((item: any) => item.fid)
.map((item: any) => item.fid)
.join(',');
// 调用 postIdUrl 获取媒体详情
let mediaMap: Record<string, any> = {};
if (allFids) {
const formData = new FormData();
formData.append('id', allFids);
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
// 构建列表(与 React 一致)
const list: any[] = [];
expandedData.forEach((vd: any) => {
const fileInfo = mediaMap[vd.fid];
const hasValidExtension =
fileInfo && ['.jpg', '.png'].includes(fileInfo.ext);
let flpth = '';
let imgPath = '';
let type = '';
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
});
});
// 过滤掉无效项(与 React 一致)
const listMap = list.filter(
item => item.flpth !== '' || item.imgPath !== ''
);
videoList.value = listMap;
// debugger
// 默认选中第一条
if (listMap.length > 0 && !activeFid.value) {
handleSelectMedia(listMap[0]);
}
} catch (error) {
console.error('获取视频列表失败:', error);
videoList.value = [];
} finally {
videoLoading.value = false;
}
};
// 选择媒体(与 React 一致)
const handleSelectMedia = (item: any) => {
activeFid.value = item.fid;
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: '' };
}
};
// 分页切换
const handleVideoPageChange = () => {
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
fetchVideoList();
};
// ==================== 获取电站列表用于日历视图的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,
{
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')
}
].filter(Boolean);
const params = { filter: { logic: 'and', filters } };
const res: any = await getAiRstcd(params);
const _data = res?.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);
// 接口返回结构: { 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 = [];
} finally {
calendarLoading.value = false;
}
};
// 日历面板切换
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;
const statusInfo = calendarStatusMap[status];
if (!statusInfo) return;
// 清漂识别直接打开附件预览
if (tabIndex.value === '3') {
const fidStr = dayData.fid;
if (!fidStr) {
return;
}
qingpiaoFids.value = fidStr
.split(',')
.map((s: string) => s.trim())
.filter(Boolean);
qingpiaoModalVisible.value = true;
return;
}
// 漂浮物/叠梁门打开视频详情弹窗
detailDate.value = date;
detailType.value = statusInfo.type;
// 设置 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: '大面积漂浮物' }
];
}
detailActiveKey.value = statusInfo.dataType;
detailModalVisible.value = true;
// 获取视频数据
fetchDetailVideoList();
};
// 获取详情视频列表
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 },
// dataType过滤
{ field: 'dataType', operator: 'eq', value: detailDataType.value },
// 时间范围
{
field: 'tm',
operator: 'gte',
dataType: 'date',
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')
},
// 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);
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 = expandedData
.filter((item: any) => item.fid)
.map((item: any) => item.fid)
.join(',');
// 调用 postIdUrl 获取媒体详情
let mediaMap: Record<string, any> = {};
if (allFids) {
const formData = new FormData();
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 = {};
}
}
// 构建列表(与 React 一致)
const list: any[] = [];
expandedData.forEach((vd: any) => {
const fileInfo = mediaMap[vd.fid];
const hasValidExtension =
fileInfo && ['.jpg', '.png'].includes(fileInfo.ext);
let flpth = '';
let imgPath = '';
let type = '';
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
});
});
console.log('构建的列表(过滤前):', list);
// 过滤掉无效项(与 React 一致)
const listMap = list.filter(
item => item.flpth !== '' || item.imgPath !== ''
);
console.log('过滤后的列表:', listMap);
detailVideoList.value = listMap;
// 默认选中第一条
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);
detailVideoList.value = [];
} finally {
detailVideoLoading.value = false;
}
};
// 选择详情媒体(与 React 一致)
const handleSelectDetailMedia = (item: any) => {
detailActiveFid.value = item.fid;
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 = (page: number, pageSize: number) => {
detailVideoPage.value = page;
detailVideoTake.value = pageSize;
detailActiveFid.value = '';
detailActiveMedia.value = { type: '', src: '' };
fetchDetailVideoList();
};
// ==================== 事件处理 ====================
const handleBaseChange = () => {
pieCode.value = [];
handleSearch();
};
const handleSearch = () => {
getTabList();
getChartData();
videoPage.value = 1;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
fetchVideoList();
fetchCalendarData();
nextTick(() => {
const filter = buildFilter();
getTableInstance()?.getList(filter);
});
};
const handleViewAttachment = (fid: string) => {
currentAttachmentFid.value = fid;
attachmentModalVisible.value = true;
};
// ==================== 监听基地变化 ====================
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
searchData.value.baseid = newVal.wbsCode;
}
},
{ 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();
}
);
// ==================== 监听月份变化,同步日历显示 ====================
watch(
() => searchData.value.month,
(newMonth) => {
calendarValue.value = newMonth;
}
);
// ==================== 监听 Tab 切换 ====================
watch(tabIndex, () => {
pieCode.value = [];
// 清空视频状态
videoPage.value = 1;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
// 销毁旧实例,避免 setOption 到已隐藏的 DOM
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
if (videoChartInstance) {
videoChartInstance.dispose();
videoChartInstance = null;
}
// 等待新 tab DOM 渲染后再请求数据
nextTick(() => {
getChartData();
fetchVideoList();
fetchCalendarData();
nextTick(() => {
const filter = buildFilter();
getTableInstance()?.getList(filter);
});
});
});
// ==================== 监听详情弹窗 Tab 切换 ====================
watch(detailActiveKey, () => {
if (detailModalVisible.value) {
detailVideoPage.value = 1;
fetchDetailVideoList();
}
});
// ==================== 窗口 resize ====================
const handleResize = () => {
const dom = chartRefs.value.get(tabIndex.value);
if (dom) {
chartInstance?.resize();
}
};
// ==================== 生命周期 ====================
onMounted(() => {
// 初始化基地选项
const jidiList = JidiSelectEventStore.jidiData || [];
jidiOptions.value = jidiList.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
getTabList();
getChartData();
fetchVideoList();
fetchCalendarData();
nextTick(() => {
const filter = buildFilter();
getTableInstance()?.getList(filter);
});
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
});
</script>
<style lang="scss" scoped>
.aisbtk-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.search-form {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 12px;
}
.aisbtk-tabs {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
:deep(.ant-tabs-content) {
height: 100%;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
}
}
.view-switch-buttons {
position: absolute;
right: 0;
top: -60px;
display: flex;
z-index: 10;
.view-btn {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
border: 1px solid #ccdae7;
border-radius: 2px;
cursor: pointer;
color: #333;
background: transparent;
transition: all 0.3s;
&:hover {
border-color: #2f6b98;
color: #2f6b98;
}
&.active {
background: #2f6b98;
color: #ffffff;
border-color: #2f6b98;
}
}
}
.view-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.chart-table-layout {
display: flex;
gap: 16px;
height: 100%;
.chart-section {
width: 35%;
min-width: 300px;
position: relative;
.chart-container {
width: 100%;
height: 480px;
}
.chart-empty-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
background: rgba(255, 255, 255, 0.8);
}
}
.table-section {
flex: 1;
min-width: 0;
}
.video-section {
flex: 1;
min-width: 0;
}
}
.video-mode {
display: flex;
gap: 12px;
width: 100%;
height: 520px;
.video-main {
flex: 0 0 80%;
background: #000;
border-radius: 4px;
overflow: hidden;
.media-display {
width: 100%;
height: 100%;
}
.media-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
}
.video-sidebar {
flex: 0 0 20%;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
.sidebar-list {
flex: 1;
overflow-y: auto;
.sidebar-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
}
.item-thumb {
width: 100%;
height: 110px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
.no-media {
font-size: 12px;
color: #999;
}
}
}
.item-time {
margin-top: 6px;
font-size: 12px;
color: #666;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.sidebar-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.sidebar-pagination {
padding: 8px;
text-align: center;
border-top: 1px solid #e8e8e8;
}
}
}
.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;
}
}
</style>