导入流程走通,预览导入图片和视频

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
扈兆增 2026-04-27 19:11:22 +08:00
parent 05c4cd68c9
commit 10c3c33ad0
10 changed files with 493 additions and 148 deletions

View File

@ -6,3 +6,4 @@ NODE_ENV='development'
VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台' VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000 VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'

View File

@ -4,3 +4,4 @@ NODE_ENV='production'
VITE_APP_TITLE = 'qgc-buji-web' VITE_APP_TITLE = 'qgc-buji-web'
VITE_APP_PORT = 3000 VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/prod-api' VITE_APP_BASE_API = '/prod-api'
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'

View File

@ -79,6 +79,7 @@
placeholder="请选择" placeholder="请选择"
@change="dataDimensionDataChange" @change="dataDimensionDataChange"
show-search show-search
allow-clear
:loading="shuJuTianBaoStore.baseLoading" :loading="shuJuTianBaoStore.baseLoading"
:filter-option="filterOption" :filter-option="filterOption"
style="width: 135px" style="width: 135px"
@ -97,6 +98,7 @@
placeholder="请选择电站" placeholder="请选择电站"
@change="stcdIdChange" @change="stcdIdChange"
show-search show-search
allow-clear
:loading="shuJuTianBaoStore.engLoading" :loading="shuJuTianBaoStore.engLoading"
:filter-option="filterOption" :filter-option="filterOption"
style="width: 135px" style="width: 135px"

View File

@ -34,6 +34,7 @@ interface Props {
// //
defaultPageSize?: number; defaultPageSize?: number;
getCheckboxProps?: (record: any) => any; getCheckboxProps?: (record: any) => any;
transformData?: (res: any) => { records: any[]; total: number };
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -41,7 +42,8 @@ const props = withDefaults(defineProps<Props>(), {
rowKey: "id", rowKey: "id",
searchParams: () => ({}), searchParams: () => ({}),
defaultPageSize: 20, defaultPageSize: 20,
getCheckboxProps: undefined getCheckboxProps: undefined,
transformData: undefined,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -101,20 +103,29 @@ const getList = async (filter?: Record<string, any>) => {
// skip: (page.value - 1) * size.value, // skip: (page.value - 1) * size.value,
// take: size.value, // take: size.value,
}; };
console.log(params);
const res = await props.listUrl(params); const res = await props.listUrl(params);
let records: any[] = [];
let totalCount: number = 0;
// [!code ++] transformData使
if (props.transformData) {
const result = props.transformData(res);
records = result.records || [];
totalCount = result.total || 0;
} else {
// [!code ++] 使
records = res?.data?.records || res?.data || [];
totalCount = res?.data?.total || res?.total || 0;
}
// { data: { records: [], total: 0 } }
//
const records = res?.data?.records || res?.data?.list || res?.data || [];
const totalCount = res?.data?.total || res?.total || 0;
tableData.value = records; tableData.value = records;
total.value = totalCount; total.value = totalCount;
// //
emit("data-loaded", params, { records, total: totalCount }); emit("data-loaded", params, { records, total: totalCount });
} catch (error) { } catch (error) {
console.error("Fetch table data error:", error); console.error("Fetch table data error:", error);
tableData.value = []; tableData.value = [];

View File

@ -56122,41 +56122,42 @@ const fetchPointData = _.debounce(async () => {
// } // }
// } // }
// //
// mapClass.addBaseDataLayer({ mapClass.addBaseDataLayer({
// id: "customBaseLayer", id: "customBaseLayer",
// key: "customBaseLayer", key: "customBaseLayer",
// type: "wmts", type: "wmts",
// name: "qgc_sx_gjjdx_arcgistiles_l13", name: "qgc_sx_gjjdx_arcgistiles_l13",
// urlType: "gisurl", urlType: "gisurl",
// url: url:
// "/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=qgc_qsj_arcgistiles_l13&STYLE=&TILEMATRIX=EPSG:3857_qgc_qsj_arcgistiles_l13:{z}&TILEMATRIXSET=EPSG:3857_qgc_qsj_arcgistiles_l13&FORMAT=image/png&TILECOL={x}&TILEROW={y}", "/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=qgc_qsj_arcgistiles_l13&STYLE=&TILEMATRIX=EPSG:3857_qgc_qsj_arcgistiles_l13:{z}&TILEMATRIXSET=EPSG:3857_qgc_qsj_arcgistiles_l13&FORMAT=image/png&TILECOL={x}&TILEROW={y}",
// url_3d: url_3d:
// "/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=qgc_sx_gjjdx_arcgistiles_l13&STYLE=&TILEMATRIX=EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13:{z}&TILEMATRIXSET=EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13&FORMAT=image/png&TILECOL={x}&TILEROW={y}", "/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=qgc_sx_gjjdx_arcgistiles_l13&STYLE=&TILEMATRIX=EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13:{z}&TILEMATRIXSET=EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13&FORMAT=image/png&TILECOL={x}&TILEROW={y}",
// matrixIds_index: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], matrixIds_index: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
// tileMatrixSetID: "EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13", tileMatrixSetID: "EPSG:3857_qgc_sx_gjjdx_arcgistiles_l13",
// }); });
// setTimeout(() => { // setTimeout(() => {
mapClass.addBaseDataLayer({ //
"id": "hydropBase", // mapClass.addBaseDataLayer({
"key": "hydropBase", // "id": "hydropBase",
"urlType": "gisurl", // "key": "hydropBase",
"url": "https://211.99.26.225:18085/geoserver/gwc/service/tms/1.0.0/qgc%3AstationEra1117@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf", // "urlType": "gisurl",
"geojson_url": "https://211.99.26.225:18085/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:stationEra1117&maxFeatures=50&outputFormat=application/json&token=bearer a9a0f227-1df3-4e68-b380-2eca5bb49bd1", // "url": "https://211.99.26.225:18085/geoserver/gwc/service/tms/1.0.0/qgc%3AstationEra1117@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf",
"url_3d": "https://211.99.26.225:18085/geoserver/qgc/wms", // "geojson_url": "https://211.99.26.225:18085/geoserver/qgc/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=qgc:stationEra1117&maxFeatures=50&outputFormat=application/json&token=bearer a9a0f227-1df3-4e68-b380-2eca5bb49bd1",
"_layer": "stationEra1117", // "url_3d": "https://211.99.26.225:18085/geoserver/qgc/wms",
"layers": "qgc:stationEra1117", // "_layer": "stationEra1117",
"rasteropacity": 0.5, // "layers": "qgc:stationEra1117",
"visible": true, // "rasteropacity": 0.5,
"minZoom": 0, // "visible": true,
"maxZoom": 20, // "minZoom": 0,
"type": "vector", // "maxZoom": 20,
"layerType": "line", // "type": "vector",
"paint": { // "layerType": "line",
"line-color": "#C5C6F3", // "paint": {
"line-width": 1, // "line-color": "#C5C6F3",
"line-opacity": 1 // "line-width": 1,
} // "line-opacity": 1
}) // }
// })
// }, 2000); // }, 2000);
// //

View File

@ -439,7 +439,6 @@ export class MapOl implements MapInterface {
if (layer.key === 'hydropBase') { if (layer.key === 'hydropBase') {
// this.hydropBaseConfig = layer; // this.hydropBaseConfig = layer;
} }
console.log(this.geoJsonData1)
// ✅ 1. 创建矢量源,关键是要配置投影转换 // ✅ 1. 创建矢量源,关键是要配置投影转换
const vectorSource = new VectorSource({ const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(this.geoJsonData1, { features: new GeoJSON().readFeatures(this.geoJsonData1, {

View File

@ -25,6 +25,7 @@
placeholder="请选择流域" placeholder="请选择流域"
:disabled="isView" :disabled="isView"
show-search show-search
allowClear
:filter-option="filterOption" :filter-option="filterOption"
@change="baseChange" @change="baseChange"
> >
@ -47,6 +48,7 @@
placeholder="请选择电站名称" placeholder="请选择电站名称"
:disabled="isView" :disabled="isView"
show-search show-search
allowClear
:filter-option="filterOption" :filter-option="filterOption"
@change="engChange" @change="engChange"
> >
@ -72,6 +74,7 @@
placeholder="请选择过鱼设施" placeholder="请选择过鱼设施"
:disabled="isView" :disabled="isView"
show-search show-search
allowClear
:filter-option="filterOption" :filter-option="filterOption"
> >
<a-select-option <a-select-option

View File

@ -50,6 +50,11 @@
批量审批 批量审批
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip title="下载模板">
<a-button v-hasPerm="['sjtb:import-add']">
下载模板
</a-button>
</a-tooltip>
<a-tooltip placement="leftBottom"> <a-tooltip placement="leftBottom">
<template #title> <template #title>
<div>1.</div> <div>1.</div>
@ -184,7 +189,10 @@ const onSearchFinish = (values: any) => {
const onValuesChange = (changedValues: any, allValues: any) => { const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues }; searchData.value = { ...searchData.value, ...allValues };
// searchData便使 // searchData便使
if (changedValues.rstcd || changedValues.baseId) { if (
Object.keys(changedValues)[0] == "rstcd" ||
Object.keys(changedValues)[0] == "baseId"
) {
shuJuTianBaoStore.getFpssOption( shuJuTianBaoStore.getFpssOption(
allValues.baseId == "all" ? "" : allValues.baseId, allValues.baseId == "all" ? "" : allValues.baseId,
allValues.rstcd allValues.rstcd

View File

@ -4,8 +4,8 @@
:loading="fileLoading" :loading="fileLoading"
:data-source="fileTableData" :data-source="fileTableData"
:columns="modalColumns" :columns="modalColumns"
:scroll="{ y: '100%', x: '100%' }" :scroll="{ y: 500, x: '100%' }"
pagination="false" :pagination="false"
:row-key="(record, index) => index" :row-key="(record, index) => index"
> >
<template #bodyCell="{ column, record, index }"> <template #bodyCell="{ column, record, index }">
@ -35,13 +35,13 @@
" "
> >
<div style="color: red; display: flex; align-items: center"> <div style="color: red; display: flex; align-items: center">
<span>{{record[column.dataIndex] }}</span> <span>{{ record[column.dataIndex] }}</span>
<exclamation-circle-outlined style="margin-left: 4px" /> <exclamation-circle-outlined style="margin-left: 4px" />
</div> </div>
</template> </template>
<!-- 3. 编辑状态下的单元格 (绑定到 editingData) --> <!-- 3. 编辑状态下的单元格 (绑定到 editingData) -->
<template v-else-if="isEditing(index)"> <template v-else-if="isEditing(index) && column.dataIndex != 'picpth' && column.dataIndex != 'vdpth'">
<template v-if="column.dataIndex === 'baseName'"> <template v-if="column.dataIndex === 'baseName'">
<a-select <a-select
v-model:value="editingData.baseId" v-model:value="editingData.baseId"
@ -196,17 +196,28 @@
/> />
</div> </div>
</template> </template>
<!-- 其他默认文本输入 -->
<template v-else>
<a-input v-model:value="editingData[column.dataIndex]" size="small" />
</template>
</template> </template>
<!-- 4. 普通展示状态 (直接显示 record) --> <template
<!-- <template v-else> v-else-if="column.dataIndex === 'picpth'"
{{ getDisplayValue(column.dataIndex, record) }} >
</template> --> <div class="preview" v-for="(item, index) in record.picpthList">
<div class="text" :class="{'text_warning': record.picpthsWarnings.includes(item.name)}" @click="emit('preview-click', record, 'image' ,index)">
{{ item.name }}
</div>
</div>
<div v-if="record.picpthList.length==0">暂无图片</div>
</template>
<template
v-else-if="column.dataIndex === 'vdpth'"
>
<div class="preview" v-for="(item, index) in record.vdpthList">
<div class="text" :class="{'text_warning': record.vdpthsWarnings.includes(item.name)}" @click="emit('preview-click', record, 'vdpth' ,index)">
{{ item.name }}
</div>
</div>
<div v-if="record.vdpthList.length==0">暂无视频</div>
</template>
</template> </template>
</a-table> </a-table>
</template> </template>
@ -217,7 +228,8 @@ import { message, Tag } from "ant-design-vue";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue"; import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import fishSearch from "@/components/fishSearch/index.vue"; import fishSearch from "@/components/fishSearch/index.vue";
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select"; import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
import dayjs from "dayjs"; import { CloseCircleOutlined } from "@ant-design/icons-vue";
import { es } from "element-plus/es/locale/index.mjs";
const props: any = defineProps({ const props: any = defineProps({
fileTableData: { type: Array, default: () => [] }, fileTableData: { type: Array, default: () => [] },
@ -225,7 +237,7 @@ const props: any = defineProps({
direction: { type: Array, default: () => [] }, direction: { type: Array, default: () => [] },
}); });
const emit = defineEmits(["update:fileTableData"]); const emit = defineEmits(["update:fileTableData", "preview-click"]);
// --- --- // --- ---
const editingRowIndex = ref<number | null>(null); const editingRowIndex = ref<number | null>(null);
@ -236,9 +248,27 @@ const rowStates = reactive<Record<number, any>>({});
const editingData = ref<any>(null); const editingData = ref<any>(null);
const modalColumns = ref([ const modalColumns = ref([
{ dataIndex: "baseName", key: "baseName", dataIndexKey: "baseId", title: "流域", width: 140 }, {
{ dataIndex: "ennm", key: "ennm", dataIndexKey: "rstcd", title: "电站名称", width: 140 }, dataIndex: "baseName",
{ dataIndex: "stnm", key: "stnm", dataIndexKey: "stcd", title: "过鱼设施名称", width: 150 }, key: "baseName",
dataIndexKey: "baseId",
title: "流域",
width: 140,
},
{
dataIndex: "ennm",
key: "ennm",
dataIndexKey: "rstcd",
title: "电站名称",
width: 140,
},
{
dataIndex: "stnm",
key: "stnm",
dataIndexKey: "stcd",
title: "过鱼设施名称",
width: 150,
},
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 190 }, { dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 190 },
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 }, { dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
{ {
@ -258,17 +288,15 @@ const modalColumns = ref([
key: "direction", key: "direction",
title: "游向", title: "游向",
width: 120, width: 120,
customRender: ({ text }: any) => { customRender: ({ text }: any) => props.direction.find((item: any) => item.itemCode === text)?.dictName || "-"
console.log(props.direction) ,
return props.direction.find((item: any) => item.itemCode === text)?.dictName || "-"
}
}, },
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 }, { dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 160 }, { dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 160 },
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 160 }, { dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 160 },
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 }, { dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
{ dataIndex: "picpth", key: "level5", title: "图片", width: 100 }, { dataIndex: "picpth", key: "picpth", title: "图片", width: 160 },
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 }, { dataIndex: "vdpth", key: "vdpth", title: "视频", width: 160 },
{ {
title: "操作", title: "操作",
key: "action", key: "action",
@ -309,12 +337,14 @@ const ensureRowState = (index: number) => {
// --- ( editingData) --- // --- ( editingData) ---
const handleBaseChange = async (baseId: string, index: number) => { const handleBaseChange = async (baseId: string, index: number) => {
console.log(baseId) console.log(baseId);
editingData.value.baseName = baseOptions.value.find( editingData.value.baseName = baseOptions.value.find(
(item: any) => item.baseid == baseId (item: any) => item.baseid == baseId
)?.basename; )?.basename;
if (baseId && editingData.value._warnings) { if (baseId && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'baseName'); editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "baseName"
);
} }
const state = ensureRowState(index); const state = ensureRowState(index);
// //
@ -338,7 +368,9 @@ const handleBaseChange = async (baseId: string, index: number) => {
const handleEngChange = async (rstcd: string, index: number) => { const handleEngChange = async (rstcd: string, index: number) => {
const state = ensureRowState(index); const state = ensureRowState(index);
if (rstcd && editingData.value._warnings) { if (rstcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'ennm'); editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "ennm"
);
} }
editingData.value.ennm = state.engOptions.find( editingData.value.ennm = state.engOptions.find(
(item: any) => item.stcd === rstcd (item: any) => item.stcd === rstcd
@ -360,7 +392,9 @@ const handleEngChange = async (rstcd: string, index: number) => {
const handleFpssChange = (stcd: string, index: number) => { const handleFpssChange = (stcd: string, index: number) => {
const state = ensureRowState(index); const state = ensureRowState(index);
if (stcd && editingData.value._warnings) { if (stcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter((w: string) => w !== 'stnm'); editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "stnm"
);
} }
editingData.value.stnm = state.fpssOptions.find( editingData.value.stnm = state.fpssOptions.find(
(item: any) => item.stcd === stcd (item: any) => item.stcd === stcd
@ -483,22 +517,36 @@ const handleFtpChange = (val: any, opt: any) => {
editingData.value.ftpName = opt.name; editingData.value.ftpName = opt.name;
}; };
// --- ---
// --- ---
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0; return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
}; };
//
const getDisplayValue = (dataIndex: string, record: any) => {
const val = record[dataIndex];
if (val === undefined || val === null) return "-";
if (dataIndex === "isfs") return val === 1 ? "是" : "否";
if (dataIndex === "strdt") return val ? dayjs(val).format("YYYY-MM-DD HH:mm:ss") : "-";
return val;
};
defineExpose({ defineExpose({
editingRowIndex, editingRowIndex,
editingData, editingData,
}); });
</script> </script>
<style lang="scss" scoped>
.preview {
width: 100%;
position: relative;
cursor: pointer;
color: #1890ff;
&:hover {
color: #40a9ff;
}
display: flex;
justify-content: justify-between;
align-items: center;
.text {
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text_warning {
color: red;
}
}
</style>

View File

@ -20,7 +20,8 @@
:list-url="getFishDraftPage" :list-url="getFishDraftPage"
:search-params="{}" :search-params="{}"
:enable-row-selection="true" :enable-row-selection="true"
:get-checkbox-props="getCheckboxProps" :get-checkbox-props="getCheckboxProps"
:transform-data="customTransform"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<!-- 使用 bodyCell 插槽自定义单元格渲染 --> <!-- 使用 bodyCell 插槽自定义单元格渲染 -->
@ -94,18 +95,17 @@
cancel-text="取消导入" cancel-text="取消导入"
:width="1500" :width="1500"
v-model:open="visible" v-model:open="visible"
maskClosable="false" :maskClosable="false"
:confirm-loading="fileLoading" :confirm-loading="fileLoading"
> >
<div class="w-full h-[500px]">
<GuoYuSheShiShuJuTianBaoTable <GuoYuSheShiShuJuTianBaoTable
ref="modalTableRef" ref="modalTableRef"
:fileLoading="fileLoading" :fileLoading="fileLoading"
:fileTableData="fileTableData" :fileTableData="fileTableData"
:direction="direction" :direction="direction"
@update:file-table-data="(val) => fileTableData = val" @preview-click="handlePreviewClick"
@update:file-table-data="(val) => (fileTableData = val)"
/> />
</div>
<template #footer> <template #footer>
<a-button key="back" @click="handleCustomCancel">取消导入</a-button> <a-button key="back" @click="handleCustomCancel">取消导入</a-button>
<a-button <a-button
@ -120,7 +120,6 @@
</a-modal> </a-modal>
<!-- 新增/编辑 Modal (对应 React EditModal) --> <!-- 新增/编辑 Modal (对应 React EditModal) -->
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
<EditModal <EditModal
v-model:visible="editModalVisible" v-model:visible="editModalVisible"
:is-view="isView" :is-view="isView"
@ -131,23 +130,104 @@
@ok="handleEditSubmit" @ok="handleEditSubmit"
/> />
<!-- 视频预览 Modal --> <!-- 媒体预览 Modal -->
<a-modal <a-modal
title="视频预览" v-model:open="mediaPreviewVisible"
v-model:open="videoPreviewVisible" :title="videoPreviewTitle"
:footer="null" :footer="null"
width="800px" width="900px"
@cancel="closeVideoPreview" @cancel="closeMediaPreview"
> >
<video <div class="flex h-[60vh] gap-4">
v-if="currentVideoUrl" <!-- 左侧混合列表 (图片+视频) -->
controls <div class="w-1/4 overflow-y-auto border-r pr-2 media-list-container">
autoplay <div
style="width: 100%" v-for="(item, index) in previewList"
:src="currentVideoUrl" :key="index"
> class="mb-2 cursor-pointer list-item"
您的浏览器不支持视频播放 :class="{ select: index === currentMediaIndex }"
</video> @click="currentMediaIndex = index"
>
<span class="file-name">{{ item.name }}</span>
<!-- 删除按钮 -->
<div class="list-item-delete" @click.stop="handleDeleteMedia(item, index)">
<CloseCircleOutlined />
</div>
</div>
</div>
<!-- 右侧动态预览区域 -->
<div
class="w-3/4 flex flex-col items-center justify-center bg-gray-100 relative p-4"
>
<!-- 图片预览 -->
<a-image
v-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'image'
"
:src="currentMediaItem.url"
class="max-w-full max-h-full object-contain"
:preview="{ visible: false }"
/>
<div
v-else-if="
currentMediaItem &&
currentMediaItem.url == '' &&
currentMediaItem.type === 'image'
"
class="text-gray-400"
>
暂无图片预览
</div>
<!-- 视频预览 -->
<video
v-else-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'video'
"
:src="currentMediaItem.url"
controls
autoplay
class="max-w-full max-h-[50vh]"
>
您的浏览器不支持视频播放
</video>
<div
v-else-if="
currentMediaItem &&
currentMediaItem.url == '' &&
currentMediaItem.type === 'video'
"
class="text-gray-400"
>
暂无视频预览
</div>
<!-- 底部切换控制 -->
<div class="absolute bottom-4 flex gap-4 z-10">
<a-button
shape="circle"
:icon="h(LeftOutlined)"
@click="prevMedia"
:disabled="currentMediaIndex === 0"
/>
<span class="self-center text-gray-600 bg-white px-2 rounded shadow-sm">
{{ currentMediaIndex + 1 }} / {{ previewList.length }}
</span>
<a-button
shape="circle"
:icon="h(RightOutlined)"
@click="nextMedia"
:disabled="currentMediaIndex === previewList.length - 1"
/>
</div>
</div>
</div>
</a-modal> </a-modal>
</div> </div>
</template> </template>
@ -155,10 +235,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted, h, nextTick } from "vue"; import { ref, computed, onMounted, h, nextTick } from "vue";
import { message, Modal } from "ant-design-vue"; // 使 ant-design-vue import { message, Modal } from "ant-design-vue"; // 使 ant-design-vue
import { LeftOutlined, RightOutlined, CloseCircleOutlined } from "@ant-design/icons-vue"; //
import BasicTable from "@/components/BasicTable/index.vue"; import BasicTable from "@/components/BasicTable/index.vue";
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue"; import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue"; import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue";
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue"; import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
import { import {
getFishDraftPage, getFishDraftPage,
addFishDraft, addFishDraft,
@ -172,12 +254,14 @@ import {
checkImportStatus, checkImportStatus,
batchSaveDraft, batchSaveDraft,
getLastImportResult, getLastImportResult,
markImportTaskSuccess markImportTaskSuccess,
} from "@/api/guoYuSheShiShuJuTianBao"; } from "@/api/guoYuSheShiShuJuTianBao";
import { Tag } from "ant-design-vue"; // Tag import { Tag } from "ant-design-vue"; // Tag
import { getDictItemsByCode } from "@/api/dict"; import { getDictItemsByCode } from "@/api/dict";
import dayjs from "dayjs";
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue' // import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
// --- --- // --- ---
interface FormData { interface FormData {
[key: string]: any; [key: string]: any;
@ -238,14 +322,12 @@ const baseColumnsConfig: ColumnConfig[] = [
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 }, { dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 }, { dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 }, { dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
{ dataIndex: "picpth", key: "level5", title: "图片", width: 100 },
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 },
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 }, { dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
{ {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
title: "状态", title: "状态",
width: 100, width: 74,
customRender: ({ text }: any) => { customRender: ({ text }: any) => {
let data = guoyuStatus.value.find((item: any) => item.itemCode === text); let data = guoyuStatus.value.find((item: any) => item.itemCode === text);
return h( return h(
@ -268,9 +350,19 @@ const isView = ref(false);
const currentRecord = ref<FormData | null>(null); const currentRecord = ref<FormData | null>(null);
const submitLoading = ref(false); const submitLoading = ref(false);
// const mediaPreviewVisible = ref(false);
const videoPreviewVisible = ref(false); const videoPreviewTitle = ref("视频预览");
const currentVideoUrl = ref<string>(""); interface MediaItem {
type: "image" | "video";
name: string;
url: string;
}
const previewList = ref<MediaItem[]>([]);
const currentMediaIndex = ref(0);
const tablePreviewRecord = ref<any>({});
//
const currentMediaItem = computed(() => previewList.value[currentMediaIndex.value]);
// //
const fileTableData = ref<any[]>([]); const fileTableData = ref<any[]>([]);
@ -460,11 +552,14 @@ const handleReject = (id: any) => {
}; };
// //
const getCheckboxProps = (record: any) => { const getCheckboxProps = (record: any) => {
console.log(record)
return { return {
disabled: record.status === 'SUBMITTED' || record.status === 'APPROVED', disabled: ['SUBMITTED', 'APPROVED'].includes(record.status),
}; };
}; };
const handleDataLoaded = (params: any, data: any) => {
console.log(params, data);
return
};
// //
const handleSelectionChange = (keys: any) => { const handleSelectionChange = (keys: any) => {
batchData.value = keys; batchData.value = keys;
@ -506,8 +601,10 @@ const isRowDataEqual = (oldRow: any, newRow: any, fields: string[]): boolean =>
let newVal = newRow?.[field]; let newVal = newRow?.[field];
// //
if ((oldVal === undefined || oldVal === null || oldVal === '') && if (
(newVal === undefined || newVal === null || newVal === '')) { (oldVal === undefined || oldVal === null || oldVal === "") &&
(newVal === undefined || newVal === null || newVal === "")
) {
continue; continue;
} }
@ -535,9 +632,9 @@ const checkTableDataChanges = () => {
for (let i = 0; i < newData.length; i++) { for (let i = 0; i < newData.length; i++) {
const oldRow = oldData[i]; const oldRow = oldData[i];
const newRow = newData[i]; const newRow = newData[i];
// +1 // +1
if (!isRowDataEqual(oldRow, newRow, ['baseId'])) { if (!isRowDataEqual(oldRow, newRow, ["baseId"])) {
changedCount++; changedCount++;
} }
} }
@ -547,30 +644,46 @@ const checkTableDataChanges = () => {
// //
const handleModalOk = () => { const handleModalOk = () => {
console.log(orgFileTableData.value) console.log(orgFileTableData.value);
console.log(fileTableData.value) console.log(fileTableData.value);
console.log(modalTableRef.value.editingData) console.log(modalTableRef.value.editingData);
if (modalTableRef.value.editingData != undefined) { if (modalTableRef.value.editingData != undefined) {
message.warning("请点击保存后提交数据!"); message.warning("请点击保存后提交数据!");
return
}
const { hasChanged, changedCount } = checkTableDataChanges();
// 3.
if (!hasChanged) {
message.info("数据未发生任何变化,无需提交");
return; return;
} }
console.log(123) for (let i = 0; i < fileTableData.value.length; i++) {
// if (fileTableData.value[i]._warnings?.length > 0) {
// message.warning("");
// return;
// }
if (fileTableData.value[i].picpthsWarnings?.length > 0) {
message.warning("请检查图片,图片路径有误!");
return;
}
if (fileTableData.value[i].vdpthsWarnings?.length > 0) {
message.warning("请检查视频,视频路径有误!");
return;
}
}
fileTableData.value.forEach((item) => {
item.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
});
// const { hasChanged, changedCount } = checkTableDataChanges();
// // 3.
// if (!hasChanged) {
// message.info("");
// return;
// }
Modal.confirm({ Modal.confirm({
title: "是否提交导入数据?", title: "是否提交导入数据?",
onOk: async () => { onOk: async () => {
let res: any = await batchSaveDraft(fileTableData.value); let res: any = await batchSaveDraft(fileTableData.value);
if (res && res?.code == 0) { if (res && res?.code == 0) {
message.success("导入成功"); message.success("导入成功");
visible.value = false;
tableRef.value?.getList(); tableRef.value?.getList();
await markImportTaskSuccess({id: taskId.value}); visible.value = false;
await markImportTaskSuccess({ id: taskId.value });
} else { } else {
message.error("导入失败,请检查数据是否正确"); message.error("导入失败,请检查数据是否正确");
} }
@ -586,7 +699,7 @@ const handleCustomCancel = () => {
okText: "确定", okText: "确定",
onOk: async () => { onOk: async () => {
// //
let res: any = await cancelImportTask({taskId: taskId.value}); let res: any = await cancelImportTask({ taskId: taskId.value });
if (res && res?.code == 0) { if (res && res?.code == 0) {
message.success("取消成功"); message.success("取消成功");
tableRef.value?.getList(); tableRef.value?.getList();
@ -600,7 +713,7 @@ const importBtn = async () => {
let res: any = await checkImportStatus(); let res: any = await checkImportStatus();
taskId.value = ""; taskId.value = "";
if (res?.code == 0) { if (res?.code == 0) {
const { hasImportingTask, currentTask} = res?.data || {}; const { hasImportingTask, currentTask } = res?.data || {};
if (currentTask) { if (currentTask) {
taskId.value = currentTask.id; taskId.value = currentTask.id;
} }
@ -626,26 +739,28 @@ const importBtn = async () => {
} }
}; };
// //
const fileChange = async (file: File) => { const fileChange = (file: File) => {
try { try {
visible.value = true; nextTick(async () => {
fileLoading.value = true; visible.value = true;
const formData = new FormData(); fileLoading.value = true;
formData.append("file", file); fileTableData.value = [];
let res: any = await importFishZip(formData); const formData = new FormData();
const { code } = res.data || {}; formData.append("file", file);
if (code == 1) { let res: any = await importFishZip(formData);
message.error("导入失败"); const { code } = res.data || {};
} else { if (code == 1) {
taskId.value = res.data.taskId; message.error("导入失败");
message.success("导入成功"); } else {
fileTableaAnalysis(res, "file"); taskId.value = res.data.taskId;
} message.success("导入成功");
fileTableaAnalysis(res, "file");
}
});
} catch (error) { } catch (error) {
} finally { } finally {
fileLoading.value = false; fileLoading.value = false;
} }
}; };
// //
const handleFileSelect = (e: Event) => { const handleFileSelect = (e: Event) => {
@ -689,10 +804,13 @@ const fileTableaAnalysis = (res: any, type: string) => {
} else { } else {
list = res.data.result.failedRowDetails; list = res.data.result.failedRowDetails;
} }
console.log(list);
list.forEach((item) => { list.forEach((item) => {
data.push({ data.push({
...item.data, ...item.data,
vdpthList: item.vdpthList,
vdpthsWarnings: item.vdpthsWarnings,
picpthList: item.picpthList,
picpthsWarnings: item.picpthsWarnings,
_warnings: item.warnings, _warnings: item.warnings,
}); });
}); });
@ -703,6 +821,20 @@ const fileTableaAnalysis = (res: any, type: string) => {
const handleReset = (values) => { const handleReset = (values) => {
handleSearchFinish(values); handleSearchFinish(values);
}; };
//
const customTransform = (res: any) => {
const rawRecords = res?.data?.records || [];
const modifiedRecords = rawRecords.map((item: any) => {
return {
...item,
picpthList: item.picpth ? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || [] : [],
};
});
return {
records: modifiedRecords,
total: res?.data?.total || 0,
};
};
// - // -
const handleSearchFinish = (values: any) => { const handleSearchFinish = (values: any) => {
const filters = [ const filters = [
@ -762,10 +894,100 @@ const handleSearchFinish = (values: any) => {
}; };
tableRef.value?.getList(filter); tableRef.value?.getList(filter);
}; };
// ()
const handlePreviewClick = (record: any, type: string, index: number) => {
const mixedList: MediaItem[] = [];
tablePreviewRecord.value = record;
if (type === "image") {
videoPreviewTitle.value = "图片预览";
const nameList = record.picpthList;
nameList.forEach((item: any) => {
mixedList.push({
type: "image",
name: item.name,
url: item.value ? `${baseUrl}/?${item.value}` : "",
});
});
} else {
videoPreviewTitle.value = "视频预览";
const nameList = record.vdpthList;
nameList.forEach((item: any) => {
mixedList.push({
type: "video",
name: item.name, //
url: item.value ? `${baseUrl}/?${item.value}` : "",
});
});
}
const closeVideoPreview = () => { mediaPreviewVisible.value = true;
videoPreviewVisible.value = false; currentMediaIndex.value = index;
currentVideoUrl.value = ""; nextTick(() => {
previewList.value = mixedList;
});
};
//
const prevMedia = () => {
if (currentMediaIndex.value > 0) {
currentMediaIndex.value--;
}
};
const nextMedia = () => {
if (currentMediaIndex.value < previewList.value.length - 1) {
currentMediaIndex.value++;
}
};
//
const handleDeleteMedia = (item: any, index: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要从预览列表中移除该项吗?",
onOk: () => {
previewList.value.splice(index, 1);
console.log(previewList.value);
if (videoPreviewTitle.value == "图片预览") {
if (previewList.value.length == 0) {
tablePreviewRecord.value.picpthList = [];
tablePreviewRecord.value.picpthsWarnings = [];
} else {
tablePreviewRecord.value.picpthList.splice(index, 1);
tablePreviewRecord.value.picpthsWarnings = tablePreviewRecord.value.picpthsWarnings.filter(
(warningName: any) => warningName !== item.name
);
}
} else {
if (previewList.value.length == 0) {
tablePreviewRecord.value.vdpthList = [];
tablePreviewRecord.value.vdpthsWarnings = [];
} else {
tablePreviewRecord.value.vdpthList.splice(index, 1);
tablePreviewRecord.value.vdpthsWarnings = tablePreviewRecord.value.vdpthsWarnings.filter(
(warningName: any) => warningName !== item.name
);
}
}
if (previewList.value.length === 0) {
mediaPreviewVisible.value = false;
} else {
//
if (index <= currentMediaIndex.value) {
currentMediaIndex.value = Math.max(0, currentMediaIndex.value - 1);
}
}
},
});
};
const closeMediaPreview = () => {
mediaPreviewVisible.value = false;
nextTick(() => {
previewList.value = [];
currentMediaIndex.value = 0;
});
}; };
// --- --- // --- ---
@ -786,4 +1008,53 @@ onMounted(() => {
background-color: #ffffff; background-color: #ffffff;
padding: 20px; padding: 20px;
} }
.media-list-container {
background-color: rgb(234, 241, 251);
padding: 10px;
.list-item {
width: 100%;
height: 36px;
line-height: 36px;
padding: 0 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
&.select {
color: #ffffff;
background-color: rgb(37, 93, 138);
}
.list-item-delete {
color: red;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
margin-left: 8px;
&:hover {
transform: scale(1.1);
}
}
&:hover .list-item-delete {
opacity: 1;
}
}
}
</style> </style>