鱼种类修改,导入修改,新增修改

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
扈兆增 2026-04-28 19:27:42 +08:00
parent 10c3c33ad0
commit 7e383e35fe
14 changed files with 558 additions and 126 deletions

View File

@ -6,4 +6,7 @@ NODE_ENV='development'
VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
## 开发环境API地址
VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
## 开发环境预览 图片视频地址
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'

View File

@ -4,4 +4,7 @@ NODE_ENV='production'
VITE_APP_TITLE = 'qgc-buji-web'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/prod-api'
## 生产环境API地址
VITE_APP_BASE_URL = 'http://localhost:8093'
## 生产环境预览 图片视频地址
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'

BIN
frontend/dist.rar Normal file

Binary file not shown.

View File

@ -81,7 +81,7 @@ export function checkImportStatus() {
});
}
// 查询用户导入导入结果
// 查询用户导入结果
export function getLastImportResult() {
return request({
url: '/data/fishDraft/getLastImportResult',

View File

@ -9,11 +9,11 @@ export function getBaseDropdown(data:any) {
});
}
//电站下拉列表
export function getEngInfoDropdown(params:any) {
export function getEngInfoDropdown(data:any) {
return request({
url: '/env/engInfo/dropdown',
method: 'get',
params
method: 'post',
data
});
}
//过鱼设施下拉列表
@ -31,3 +31,12 @@ export function getFishDictoryDropdown() {
method: 'get'
});
}
// 上传文件
export function uploadFile(data:any) {
return request({
url: import.meta.env.VITE_APP_PREVIEW_URL + '/upload',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data' },
});
}

View File

@ -7,13 +7,12 @@
:loading="loading"
@change="handleChange"
@search="handleSearch"
placeholder="请选择鱼种类"
placeholder="鱼种类支持俗名查询"
:mode="multiple ? 'multiple' : undefined"
show-search
:filter-option="false"
class="custom-fish-select"
:dropdownMatchSelectWidth="false"
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
@dropdownVisibleChange="handleDropdownVisibleChange"
:max-tag-count="multiple ? 1 : undefined"
:open="open"
@ -29,6 +28,23 @@
<template #dropdownRender>
<div class="custom-dropdown-container">
<div class="w-[340px] h-[30px] flex items-center pl-[10px]" @click.stop @mousedown.prevent>
<div>查询方式</div>
<div
class="text-[12px] font-bold mr-2 cursor-pointer"
:class="{ 'text-[#005293]': !isIntelligentQuery }"
@click="IntelligentQueryCLick(false)"
>
相似度
</div>
<div
class="text-[12px] font-bold cursor-pointer"
:class="{ 'text-[#005293]': isIntelligentQuery }"
@click="IntelligentQueryCLick(true)"
>
智能查询
</div>
</div>
<!-- 左侧可滚动的选项列表 -->
<div class="dropdown-left-list">
<div
@ -42,7 +58,7 @@
@click.stop="handleSelectOption(opt)"
@mouseenter="hoveredId = opt.id"
>
<span class="item-name">{{ opt.name }}</span>
<span class="item-name" v-html="highlightText(opt.name)"></span>
<!-- 选中对勾 -->
<span v-if="isSelected(opt.id)" class="check-icon"></span>
</div>
@ -53,11 +69,14 @@
<div class="dropdown-divider"></div>
<!-- 右侧固定显示的别名/详情 -->
<div class="dropdown-right-detail">
<div class="dropdown-right-detail" @click.stop @mousedown.prevent>
<div v-if="currentDetailData" class="detail-content">
<div class="detail-title">{{ currentDetailData.name }}</div>
<div
class="detail-title"
v-html="highlightText(currentDetailData.name)"
></div>
<div class="detail-alias" :title="currentDetailData.alias">
{{ currentDetailData.alias || "暂无别名" }}
<div v-html="highlightText(currentDetailData.alias)"></div>
</div>
</div>
<div v-else class="detail-placeholder">请选择或悬停查看</div>
@ -71,6 +90,7 @@
import { ref, onMounted, computed, watch } from "vue";
import { getFishDictoryDropdown } from "@/api/select";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
import { message } from "ant-design-vue";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- Props & Emits ---
@ -94,6 +114,7 @@ const options = ref<any[]>([]);
const searchKeyword = ref<string>("");
const hoveredId = ref<string | null>(null);
const open = ref(false); //
const isIntelligentQuery = ref(true); //
// --- Computed ---
const filteredOptions = computed(() => {
@ -128,6 +149,7 @@ const isSelected = (id: string) => {
const handleSearch = (value: string) => {
searchKeyword.value = value;
hoveredId.value = null;
};
const handleDropdownVisibleChange = (val: boolean) => {
@ -139,6 +161,8 @@ const handleDropdownVisibleChange = (val: boolean) => {
};
const handleSelectOption = (opt: any) => {
console.log(props.modelValue)
console.log(props.modelValue)
if (props.multiple) {
// --- ---
let newValues: string[] = Array.isArray(props.modelValue)
@ -165,11 +189,19 @@ const handleSelectOption = (opt: any) => {
}
}
};
const IntelligentQueryCLick = (val: boolean) => {
isIntelligentQuery.value = val;
if (val) {
message.success("智能查询已开启");
} else {
message.success("智能查询已关闭");
}
};
const handleChange = (val: any) => {
// a-select change Tag
//
emit("update:modelValue", val, '');
emit("update:modelValue", val, "");
};
const getFishNameById = (id: string) => {
@ -210,6 +242,14 @@ const init = () => {
}
};
//
const highlightText = (text) => {
if (text == null) {
return "暂无别名";
}
const reg = new RegExp(searchKeyword.value, "g");
return text.replace(reg, `<span style="color: red;">${searchKeyword.value}</span>`);
};
onMounted(() => {
init();
});
@ -226,7 +266,9 @@ onMounted(() => {
}
.custom-dropdown-container {
width: 401px;
display: flex;
flex-wrap: wrap;
background: #fff;
border-radius: 4px;
overflow: hidden;
@ -323,6 +365,9 @@ onMounted(() => {
color: #666;
line-height: 1.5;
word-break: break-all;
span {
color: red;
}
}
.detail-placeholder {

View File

@ -2,7 +2,7 @@
import { computed } from "vue";
import { useTagsViewStore } from "@/store/modules/tagsView";
import { useRoute } from "vue-router";
import GisView from "@/components/gis/GisView.vue";
// import GisView from "@/components/gis/GisView.vue";
const tagsViewStore = useTagsViewStore();
@ -12,7 +12,7 @@ const routeKey = computed(() => router.path + Math.random());
<template>
<section class="app-main">
<GisView />
<!-- <GisView /> -->
<div class="gi-panels">
<router-view v-slot="{ Component, route }" :key="routeKey">
<transition name="router-fade" mode="out-in">

View File

@ -11,11 +11,9 @@ import Sidebar from "./Sidebar/index.vue";
import { useTagsViewStore } from "@/store/modules/tagsView";
import { useUserStore } from "@/store/modules/user";
import Cookies from "js-cookie";
import {getPath,removePath } from '@/utils/auth';
// const url = import.meta.env.VITE_APP_BASE_API;
const username = Cookies.get("username");
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
@ -67,7 +65,7 @@ onBeforeUnmount(() => {
<span class="icon">
<UserOutlined />
</span>
<span class="text">{{ username }}</span>
<span class="text">{{ userStore.username }}</span>
</div>
</a-space>
<template #overlay>

View File

@ -12,6 +12,7 @@ import { UserInfo } from '@/api/user/types';
export const useUserStore = defineStore('user', () => {
// state
const Token = ref<string>(getToken() || '');
const username = ref<string>('');
const nickname = ref<string>('');
const avatar = ref<string>('');
const roles = ref<Array<string>>([]); // 用户角色编码集合 → 判断路由权限
@ -47,6 +48,7 @@ export const useUserStore = defineStore('user', () => {
if (!data.roles || data.roles.length <= 0) {
reject('getUserInfo: roles must be a non-null array!');
}
username.value = data.userInfo.username;
nickname.value = data.userInfo.nickname;
avatar.value = data.userInfo.avatar;
roles.value = data.roles;
@ -85,6 +87,7 @@ export const useUserStore = defineStore('user', () => {
}
return {
Token,
username,
nickname,
avatar,
roles,

View File

@ -46,7 +46,7 @@
v-model:value="formData.rstcd"
:loading="engLoading"
placeholder="请选择电站名称"
:disabled="isView"
:disabled="isView || !formData.baseId"
show-search
allowClear
:filter-option="filterOption"
@ -72,7 +72,7 @@
v-model:value="formData.stcd"
:loading="fpssLoading"
placeholder="请选择过鱼设施"
:disabled="isView"
:disabled="isView || !formData.rstcd"
show-search
allowClear
:filter-option="filterOption"
@ -218,16 +218,47 @@
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-col :span="12" class="imgupload" :class="{'imgupload_hidden': isView}">
<a-form-item label="图片" name="picpth">
<!-- <a-input v-model:value="formData.picpth" placeholder="图片路径" /> -->
<a-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:multiple="true"
accept=".png,.jpg,.jpeg"
:before-upload="beforeImageUpload"
@preview="handleImagePreview"
:disabled="isView"
:maxCount="5"
@remove="handleImageRemove"
>
<div v-if="!isView && imageFileList.length < 5">
<plus-outlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="视频" name="vdpth">
<!-- <a-input v-model:value="formData.vdpth" placeholder="视频路径" /> -->
<a-upload
v-if="!isView"
v-model:file-list="videoFileList"
list-type="text"
:multiple="false"
accept=".mp4"
:before-upload="beforeVideoUpload"
:disabled="isView"
:maxCount="5"
@remove="handleVideoRemove"
>
<a-button :disabled="isView && videoFileList.length < 5">
<upload-outlined />
上传视频 (MP4)
</a-button>
</a-upload>
<a-button v-else @click="handleVideoPreview"> 点击预览视频 </a-button>
</a-form-item>
</a-col>
</a-row>
@ -238,10 +269,17 @@
<script lang="ts" setup>
import { ref, reactive, watch, computed } from "vue";
import dayjs from "dayjs";
import { message } from "ant-design-vue";
import { Upload, message } from "ant-design-vue";
import { UploadOutlined, PlusOutlined } from "@ant-design/icons-vue";
import type { Rule } from "ant-design-vue/es/form";
import type { UploadProps } from "ant-design-vue";
import fishSearch from "@/components/fishSearch/index.vue";
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
import {
getBaseDropdown,
getEngInfoDropdown,
getFpssDropdown,
uploadFile,
} from "@/api/select";
// Props
interface Props {
@ -257,6 +295,8 @@ const fpssLoading = ref(false);
const baseOption = ref<any[]>([]);
const engOption = ref<any[]>([]);
const fpssOption = ref<any[]>([]);
const imageFileList = ref<any[]>([]);
const videoFileList = ref<any[]>([]);
const props = withDefaults(defineProps<Props>(), {
visible: false,
@ -315,6 +355,7 @@ const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "cancel"): void;
(e: "ok", values: any): void;
(e: "preview-click", record: any, type: string, index: number): void;
}>();
//
@ -420,7 +461,55 @@ const validateWeight = () => {
return true;
};
//
const beforeImageUpload: UploadProps["beforeUpload"] = (file) => {
const isJpgOrPng =
file.type === "image/jpeg" ||
file.type === "image/png" ||
file.name.toLowerCase().endsWith(".jpg") ||
file.name.toLowerCase().endsWith(".jpeg") ||
file.name.toLowerCase().endsWith(".png");
if (!isJpgOrPng) {
message.error("只能上传 JPG/PNG/JPEG 格式的图片!");
return Upload.LIST_IGNORE;
}
const isLt2M = file.size / 1024 / 1024 < 5; // 5M
if (!isLt2M) {
message.error("图片大小不能超过 5MB!");
return Upload.LIST_IGNORE;
}
// false
return false;
};
//
const beforeVideoUpload: UploadProps["beforeUpload"] = (file) => {
const isMp4 = file.type === "video/mp4" || file.name.toLowerCase().endsWith(".mp4");
if (!isMp4) {
message.error("只能上传 MP4 格式的视频!");
return false;
}
const isLt50M = file.size / 1024 / 1024 < 10; // 50M
if (!isLt50M) {
message.error("视频大小不能超过 10MB!");
return false;
}
// false
return false;
};
//
const handleImageRemove = (file: any) => {
const index = imageFileList.value.indexOf(file);
const newFileList = imageFileList.value.slice();
newFileList.splice(index, 1);
imageFileList.value = newFileList;
};
//
const handleVideoRemove = (file: any) => {
videoFileList.value = [];
};
// 1.
const initForm = () => {
if (props.initialValues) {
@ -453,7 +542,30 @@ const initForm = () => {
formData.bodyLengthMin = undefined;
formData.bodyLengthMax = undefined;
}
if (values.picpth) {
const paths = Array.isArray(values.picpth)
? values.picpth
: values.picpth.split(",");
imageFileList.value = paths.map((path: string, index: number) => ({
uid: `-${index}`,
name: path.split("/").pop() || `image-${index}`,
status: "done",
path: path,
url: import.meta.env.VITE_APP_PREVIEW_URL + "/?" + path, // 访URL
}));
}
//
if (values.vdpth) {
const paths = Array.isArray(values.vdpth) ? values.vdpth : values.vdpth.split(",");
videoFileList.value = paths.map((path: string, index: number) => ({
uid: `-${index}`,
name: path.split("/").pop() || `image-${index}`,
status: "done",
path: path,
url: import.meta.env.VITE_APP_PREVIEW_URL + "/?" + path, // 访URL
}));
}
//
Object.keys(formData).forEach((key) => {
//
@ -498,6 +610,9 @@ const resetForm = () => {
//
bodyLengthError.value = "";
weightError.value = "";
//
imageFileList.value = [];
videoFileList.value = [];
};
//
@ -506,7 +621,21 @@ const handleCancel = () => {
emit("cancel");
resetForm();
};
const handleVideoPreview = () => {
emit("preview-click", { vdpthList: videoFileList.value }, "formVideo", 0);
};
// ... ...
//
const handleImagePreview = async (file: any) => {
if (!props.isView) {
return "";
}
emit("preview-click", { picpthList: imageFileList.value }, "formImage", 0);
return "";
};
// ... ...
//
const handleOk = async () => {
try {
@ -520,6 +649,70 @@ const handleOk = async () => {
}
//
await formRef.value.validate();
// 1.
let uploadedPicPaths: any[] = [];
// url
const newImageFiles = imageFileList.value.filter(
(file) => !file.url && file.originFileObj
);
if (newImageFiles.length > 0) {
// 使 Promise.all
// uploadFile { data: { url: '...' } }
const uploadPromises = newImageFiles.map((file) => {
const formDataUpload = new FormData();
formDataUpload.append("file", file.originFileObj);
return uploadFile(formDataUpload); //
});
const results = await Promise.all(uploadPromises);
uploadedPicPaths = results.map((item: any, index: number) => ({
path: item.message,
uid: `-${index}`,
name: item.fileMD5,
status: "done",
url: item.fullFilePath, // 访URL
}));
}
//
const existingPics = imageFileList.value
.filter((file) => file.path)
.map((file) => file.path);
const uploadedPicPaths1 = uploadedPicPaths
.filter((file) => file.path)
.map((file) => file.path);
const finalPicPaths = [...existingPics, ...uploadedPicPaths1];
// 2.
let uploadedVideoPath = [];
const newVideoFiles = videoFileList.value.filter(
(file) => !file.url && file.originFileObj
);
if (newVideoFiles.length > 0) {
const uploadPromises = newVideoFiles.map((file) => {
const formDataUpload = new FormData();
formDataUpload.append("file", file.originFileObj);
return uploadFile(formDataUpload); //
});
const results = await Promise.all(uploadPromises);
uploadedVideoPath = results.map((item: any, index: number) => ({
path: item.message,
uid: `-${index}`,
name: item.fileMD5,
status: "done",
url: item.fullFilePath, // 访URL
}));
}
const existingVideos = videoFileList.value
.filter((file) => file.path)
.map((file) => file.path);
const uploadedVideoPaths1 = uploadedVideoPath
.filter((file) => file.path)
.map((file) => file.path);
const finalVideoPaths = [...existingVideos, ...uploadedVideoPaths1];
let fwet = "";
if (
formData.weightMin == formData.weightMax &&
@ -528,7 +721,7 @@ const handleOk = async () => {
) {
fwet = formData.weightMin;
} else if (formData.weightMin == undefined && formData.weightMax == undefined) {
fwet = "-";
fwet = "";
} else {
fwet = formData.weightMin + "~" + formData.weightMax;
}
@ -543,7 +736,7 @@ const handleOk = async () => {
formData.bodyLengthMin == undefined &&
formData.bodyLengthMax == undefined
) {
fsz = "-";
fsz = "";
} else {
fsz = formData.bodyLengthMin + "~" + formData.bodyLengthMax;
}
@ -552,9 +745,12 @@ const handleOk = async () => {
...formData,
fwet: fwet,
fsz: fsz,
picpth: finalPicPaths.join(","), //
vdpth: finalVideoPaths.join(","),
};
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(submitValues);
// return;
emit("ok", submitValues);
} catch (error) {
console.error("Validate Failed:", error);
@ -563,5 +759,52 @@ const handleOk = async () => {
};
</script>
<style scoped>
<style scoped lang="scss">
.imgupload {
/* 1. 控制上传触发按钮(那个加号框)的大小 */
:deep(.ant-upload-select-picture-card) {
width: 78px !important;
height: 78px !important;
margin-right: 8px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
}
/* 2. 控制已上传列表项容器的大小 */
:deep(.ant-upload-list-item-container) {
width: 78px !important;
height: 78px !important;
float: left; /* 确保浮动排列,避免换行问题 */
margin-right: 8px;
margin-bottom: 8px;
}
/* 3. 控制列表项内部的具体内容(图片/缩略图) */
:deep(.ant-upload-list-item) {
width: 100% !important;
height: 100% !important;
padding: 0;
border: 1px solid #d9d9d9;
border-radius: 2px;
/* 删除预览按钮 */
.ant-upload-list-item-actions {
a {
display: none;
}
}
}
}
.imgupload_hidden{
:deep(.ant-upload-list-item) {
/* 删除预览按钮 */
.ant-upload-list-item-actions {
a {
display: block;
}
}
}
}
</style>

View File

@ -78,7 +78,6 @@ import {
import dayjs from "dayjs";
import BasicSearch from "@/components/BasicSearch/index.vue"; //
import { DateSetting } from "@/utils/enumeration";
import { checkPerm } from "@/directive/permission";
import fishSearch from "@/components/fishSearch/index.vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";

View File

@ -6,7 +6,7 @@
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
:pagination="false"
:row-key="(record, index) => index"
:row-key="(_record, index) => index"
>
<template #bodyCell="{ column, record, index }">
<!-- 1. 操作列 -->
@ -29,28 +29,33 @@
<template
v-else-if="
!isEditing(index) &&
column.dataIndexKey &&
record._warnings &&
record._warnings.includes(column.dataIndexKey)
"
>
<div style="color: red; display: flex; align-items: center">
<span>{{ record[column.dataIndex] }}</span>
<span v-if="record[column.dataIndex]">{{ record[column.dataIndex] }}</span>
<span v-else> 请添加{{ column.title }}</span>
<exclamation-circle-outlined style="margin-left: 4px" />
</div>
</template>
<!-- 3. 编辑状态下的单元格 (绑定到 editingData) -->
<template v-else-if="isEditing(index) && column.dataIndex != 'picpth' && column.dataIndex != 'vdpth'">
<template
v-else-if="
isEditing(index) && column.dataIndex != 'picpth' && column.dataIndex != 'vdpth'
"
>
<template v-if="column.dataIndex === 'baseName'">
<a-select
v-model:value="editingData.baseId"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.baseLoading"
style="width: 100%"
@change="(val) => handleBaseChange(val, index)"
@change="(val) => handleBaseChange(val, index, 'input')"
>
<a-select-option
v-for="opt in baseOptions"
@ -69,11 +74,12 @@
v-model:value="editingData.rstcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.engLoading"
style="width: 100%"
:disabled="!editingData.baseId"
@change="(val) => handleEngChange(val, index)"
@change="(val) => handleEngChange(val, index, 'input')"
>
<a-select-option
v-for="opt in rowStates[index]?.engOptions || []"
@ -92,6 +98,7 @@
v-model:value="editingData.stcd"
placeholder="请选择"
show-search
allowClear
:filter-option="filterOption"
:loading="rowStates[index]?.fpssLoading"
style="width: 100%"
@ -117,6 +124,7 @@
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="(val) => delWarning(val,'strdt')"
/>
</template>
@ -135,6 +143,7 @@
v-model:value="editingData.direction"
placeholder="请选择"
style="width: 100%"
@change="(val) => delWarning(val,'direction')"
>
<a-select-option
v-for="item in direction"
@ -147,7 +156,15 @@
</template>
<!-- 数字输入框 -->
<template v-else-if="['fcnt', 'wt'].includes(column.dataIndex)">
<template v-else-if="['fcnt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
@change="(val) => delWarning(val, column.dataIndex)"
:min="0"
/>
</template>
<template v-else-if="['wt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
@ -157,7 +174,8 @@
<!-- 是否鱼苗 -->
<template v-else-if="column.dataIndex === 'isfs'">
<a-radio-group v-model:value="editingData.isfs">
<a-radio-group v-model:value="editingData.isfs"
@change="(val) => delWarning(val,'isfs')">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
@ -198,21 +216,25 @@
</template>
</template>
<template
v-else-if="column.dataIndex === 'picpth'"
>
<template v-else-if="column.dataIndex === 'picpth'">
<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)">
<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'"
>
<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)">
<div
class="text"
:class="{ text_warning: record.vdpthsWarnings.includes(item.name) }"
@click="emit('preview-click', record, 'video', index)"
>
{{ item.name }}
</div>
</div>
@ -228,8 +250,6 @@ import { message, Tag } from "ant-design-vue";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import fishSearch from "@/components/fishSearch/index.vue";
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
import { CloseCircleOutlined } from "@ant-design/icons-vue";
import { es } from "element-plus/es/locale/index.mjs";
const props: any = defineProps({
fileTableData: { type: Array, default: () => [] },
@ -269,12 +289,25 @@ const modalColumns = ref([
title: "过鱼设施名称",
width: 150,
},
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 190 },
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
{
dataIndex: "strdt",
key: "strdt",
dataIndexKey: "strdt",
title: "过鱼时间",
width: 190,
},
{
dataIndex: "ftpName",
key: "ftpName",
dataIndexKey: "ftp",
title: "鱼种类",
width: 120,
},
{
dataIndex: "isfs",
key: "isfs",
title: "是否鱼苗",
dataIndexKey: "isfs",
width: 130,
customRender: ({ text }: any) => {
const isYes = text === 1 || text === "1";
@ -285,13 +318,20 @@ const modalColumns = ref([
},
{
dataIndex: "direction",
dataIndexKey: "direction",
key: "direction",
title: "游向",
width: 120,
customRender: ({ text }: any) => props.direction.find((item: any) => item.itemCode === text)?.dictName || "-"
,
customRender: ({ text }: any) =>
props.direction.find((item: any) => item.itemCode === text)?.dictName || "-",
},
{
dataIndex: "fcnt",
key: "fcnt",
dataIndexKey: "fcnt",
title: "过鱼数量(尾)",
width: 120,
},
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 160 },
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 160 },
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
@ -335,25 +375,23 @@ const ensureRowState = (index: number) => {
};
// --- ( editingData) ---
const handleBaseChange = async (baseId: string, index: number) => {
console.log(baseId);
const handleBaseChange = async (
baseId: string,
index: number,
type: string = "start"
) => {
const state = ensureRowState(index);
editingData.value.baseName = baseOptions.value.find(
(item: any) => item.baseid == baseId
)?.basename;
if (baseId && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "baseName"
);
}
const state = ensureRowState(index);
delWarning(baseId, "baseId");
//
if (type != "start") {
editingData.value.rstcd = undefined;
editingData.value.stcd = undefined;
state.engOptions = [];
state.fpssOptions = [];
if (!baseId) return;
}
state.engLoading = true;
try {
const res = await getEngInfoDropdown({ baseId });
@ -365,20 +403,16 @@ const handleBaseChange = async (baseId: string, index: number) => {
}
};
const handleEngChange = async (rstcd: string, index: number) => {
const handleEngChange = async (rstcd: string, index: number, type: string = "start") => {
const state = ensureRowState(index);
if (rstcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "ennm"
);
}
editingData.value.ennm = state.engOptions.find(
(item: any) => item.stcd === rstcd
)?.ennm;
delWarning(rstcd, "rstcd");
if (type != "start") {
editingData.value.stcd = undefined;
state.fpssOptions = [];
if (!rstcd || !editingData.value.baseId) return;
}
state.fpssLoading = true;
try {
const res = await getFpssDropdown({ rstcd, baseId: editingData.value.baseId });
@ -391,18 +425,35 @@ const handleEngChange = async (rstcd: string, index: number) => {
};
const handleFpssChange = (stcd: string, index: number) => {
const state = ensureRowState(index);
if (stcd && editingData.value._warnings) {
editingData.value._warnings = editingData.value._warnings.filter(
(w: string) => w !== "stnm"
);
}
delWarning(stcd, "stcd");
editingData.value.stnm = state.fpssOptions.find(
(item: any) => item.stcd === stcd
)?.stnm;
};
// --- ---
// /
const delWarning = (val: any, key: string) => {
// _warnings
if (!editingData.value._warnings) {
editingData.value._warnings = [];
}
const warnings = editingData.value._warnings;
const hasWarning = warnings.includes(key);
if (val !== null && val !== undefined && val !== '') {
// 1.
if (hasWarning) {
editingData.value._warnings = warnings.filter((w: string) => w !== key);
}
} else {
// 2. (null/undefined/'')
if (!hasWarning) {
editingData.value._warnings.push(key);
}
}
};
// --- ---
const isEditing = (index: number) => editingRowIndex.value === index;
const startEdit = (index: number) => {
const originalRecord = props.fileTableData[index];
@ -416,11 +467,20 @@ const startEdit = (index: number) => {
editingRowIndex.value = index;
// 3. ( editingData )
if (editingData.value.baseId && !editingData.value.rstcd) {
handleBaseChange(editingData.value.baseId, index);
} else if (editingData.value.baseId && editingData.value.rstcd) {
handleBaseChange(editingData.value.baseId, index).then(() => {
handleEngChange(editingData.value.rstcd, index);
console.log(editingData.value.baseId);
if (editingData.value.baseId == "" || editingData.value.baseId == undefined) {
console.log(editingData.value.rstcd);
if (editingData.value.rstcd) {
handleBaseChange("", index, "start").then(() => {
handleEngChange(editingData.value.rstcd, index, "start");
});
} else {
handleEngChange("", index, "start");
}
} else if (editingData.value.baseId != "" && editingData.value.baseId != undefined) {
console.log(2);
handleBaseChange(editingData.value.baseId, index, "start").then(() => {
handleEngChange(editingData.value.rstcd, index, "start");
});
}
};
@ -515,9 +575,9 @@ const handlePreviewDelete = (index: number) => {
//
const handleFtpChange = (val: any, opt: any) => {
editingData.value.ftpName = opt.name;
delWarning(val, "ftp");
};
// --- ---
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;

View File

@ -128,6 +128,7 @@
:loading="submitLoading"
@cancel="editModalCancel"
@ok="handleEditSubmit"
@preview-click="handlePreviewClick"
/>
<!-- 媒体预览 Modal -->
@ -137,6 +138,7 @@
:footer="null"
width="900px"
@cancel="closeMediaPreview"
z-index="2000"
>
<div class="flex h-[60vh] gap-4">
<!-- 左侧混合列表 (图片+视频) -->
@ -149,9 +151,12 @@
@click="currentMediaIndex = index"
>
<span class="file-name">{{ item.name }}</span>
<!-- 删除按钮 -->
<div class="list-item-delete" @click.stop="handleDeleteMedia(item, index)">
<div
class="list-item-delete"
v-if="item.type != 'formVideo' && item.type != 'formImage'"
@click.stop="handleDeleteMedia(item, index)"
>
<CloseCircleOutlined />
</div>
</div>
@ -166,17 +171,16 @@
v-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'image'
(currentMediaItem.type === 'image' || currentMediaItem.type === 'formImage')
"
: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'
(currentMediaItem.type === 'image' || currentMediaItem.type === 'formImage')
"
class="text-gray-400"
>
@ -188,7 +192,7 @@
v-else-if="
currentMediaItem &&
currentMediaItem.url != '' &&
currentMediaItem.type === 'video'
(currentMediaItem.type === 'video' || currentMediaItem.type === 'formVideo')
"
:src="currentMediaItem.url"
controls
@ -201,7 +205,7 @@
v-else-if="
currentMediaItem &&
currentMediaItem.url == '' &&
currentMediaItem.type === 'video'
(currentMediaItem.type === 'video' || currentMediaItem.type === 'formVideo')
"
class="text-gray-400"
>
@ -240,6 +244,7 @@ import BasicTable from "@/components/BasicTable/index.vue";
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue";
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
import { checkPerm } from "@/directive/permission";
import {
getFishDraftPage,
@ -353,7 +358,7 @@ const submitLoading = ref(false);
const mediaPreviewVisible = ref(false);
const videoPreviewTitle = ref("视频预览");
interface MediaItem {
type: "image" | "video";
type: "image" | "video" | "formVideo" | "formImage";
name: string;
url: string;
}
@ -412,6 +417,7 @@ const columns = computed(() => {
// --- ---
const handleAdd = () => {
isView.value = false;
currentRecord.value = null;
editModalVisible.value = true;
};
@ -552,13 +558,14 @@ const handleReject = (id: any) => {
};
//
const getCheckboxProps = (record: any) => {
console.log(checkPerm(["sjtb:edit-review"]));
return {
disabled: ['SUBMITTED', 'APPROVED'].includes(record.status),
disabled: ["SUBMITTED", "APPROVED"].includes(record.status),
};
};
const handleDataLoaded = (params: any, data: any) => {
console.log(params, data);
return
return;
};
//
const handleSelectionChange = (keys: any) => {
@ -652,10 +659,40 @@ const handleModalOk = () => {
return;
}
for (let i = 0; i < fileTableData.value.length; i++) {
// if (fileTableData.value[i]._warnings?.length > 0) {
// message.warning("");
// return;
// }
if (fileTableData.value[i]._warnings?.length > 0) {
if (fileTableData.value[i]._warnings.includes("baseId")) {
message.warning("请检查流域,流域填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("rstcd")) {
message.warning("请检查电站,电站填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("stcd")) {
message.warning("请检查过鱼设施,过鱼设施填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("strdt")) {
message.warning("请检查过鱼时间,过鱼时间填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("ftp")) {
message.warning("请检查鱼种类,鱼种类填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("direction")) {
message.warning("请检查游向,游向填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("isfs")) {
message.warning("请检查是否鱼苗,是否鱼苗填写有误!");
return;
}
if (fileTableData.value[i]._warnings.includes("fcnt")) {
message.warning("请检查过鱼数量,过鱼数量填写有误!");
return;
}
}
if (fileTableData.value[i].picpthsWarnings?.length > 0) {
message.warning("请检查图片,图片路径有误!");
return;
@ -800,10 +837,16 @@ const fileTableaAnalysis = (res: any, type: string) => {
let data = [];
let list = [];
if (type == "file") {
list = res.data.failedRows;
list = res.data.failedRows.concat(res.data.successRows);
} else {
list = res.data.result.failedRowDetails;
list = res.data.result.failedRowDetails.concat(res.data.result.successRowDetails);
}
list.sort((a: any, b: any) => {
const keyA = a.rowIndex !== undefined && a.rowIndex !== null ? Number(a.rowIndex) : -1;
const keyB = b.rowIndex !== undefined && b.rowIndex !== null ? Number(b.rowIndex) : -1;
return keyA - keyB;
});
list.forEach((item) => {
data.push({
...item.data,
@ -827,7 +870,9 @@ const customTransform = (res: any) => {
const modifiedRecords = rawRecords.map((item: any) => {
return {
...item,
picpthList: item.picpth ? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || [] : [],
picpthList: item.picpth
? item.picpth.split(",").map((item: string) => baseUrl + "/?" + item) || []
: [],
};
});
return {
@ -896,9 +941,10 @@ const handleSearchFinish = (values: any) => {
};
// ()
const handlePreviewClick = (record: any, type: string, index: number) => {
console.log(record, type);
const mixedList: MediaItem[] = [];
tablePreviewRecord.value = record;
if (type === "image") {
tablePreviewRecord.value = record;
videoPreviewTitle.value = "图片预览";
const nameList = record.picpthList;
nameList.forEach((item: any) => {
@ -908,7 +954,8 @@ const handlePreviewClick = (record: any, type: string, index: number) => {
url: item.value ? `${baseUrl}/?${item.value}` : "",
});
});
} else {
} else if (type === "video") {
tablePreviewRecord.value = record;
videoPreviewTitle.value = "视频预览";
const nameList = record.vdpthList;
nameList.forEach((item: any) => {
@ -918,10 +965,31 @@ const handlePreviewClick = (record: any, type: string, index: number) => {
url: item.value ? `${baseUrl}/?${item.value}` : "",
});
});
} else if (type === "formImage") {
videoPreviewTitle.value = "图片预览";
const nameList = JSON.parse(JSON.stringify(record)).picpthList;
nameList.forEach((item: any) => {
mixedList.push({
type: "formImage",
name: item.name, //
url: item.url,
});
});
} else if (type === "formVideo") {
videoPreviewTitle.value = "视频预览";
const nameList = JSON.parse(JSON.stringify(record)).vdpthList;
nameList.forEach((item: any) => {
mixedList.push({
type: "formVideo",
name: item.name, //
url: item.url,
});
});
}
mediaPreviewVisible.value = true;
currentMediaIndex.value = index;
console.log(mixedList);
nextTick(() => {
previewList.value = mixedList;
});
@ -945,6 +1013,7 @@ const handleDeleteMedia = (item: any, index: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要从预览列表中移除该项吗?",
zIndex: 2002,
onOk: () => {
previewList.value.splice(index, 1);
console.log(previewList.value);

View File

@ -25,9 +25,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
proxy: {
[env.VITE_APP_BASE_API]: {
// 线上API地址
// target: 'http://localhost:8093/',
target: env.VITE_APP_BASE_URL,
// 本地API地址
target: 'http://10.84.121.21:8093',
// target: 'http://10.84.121.21:8093',
changeOrigin: true,
rewrite: path =>
path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')