修改鱼种类,修改导入鱼种类报错

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
扈兆增 2026-04-29 14:40:16 +08:00
parent 0f0199fd41
commit 4f34ac0d65
9 changed files with 676 additions and 254 deletions

View File

@ -16,6 +16,14 @@ export function getEngInfoDropdown(data:any) {
data
});
}
// 更新导入任务
export function updateImportTask(data:any) {
return request({
url: '/data/importTask/update',
method: 'post',
data
});
}
//过鱼设施下拉列表
export function getFpssDropdown(params:any) {
return request({
@ -31,6 +39,14 @@ export function getFishDictoryDropdown() {
method: 'get'
});
}
// 类似鱼类名称下拉列表
export function getSimilarFishDictoryDropdown(params:any) {
return request({
url: '/env/fishDictory/similar',
method: 'get',
params
});
}
// 上传文件
export function uploadFile(data:any) {
return request({

View File

@ -83,6 +83,7 @@
:loading="shuJuTianBaoStore.baseLoading"
:filter-option="filterOption"
style="width: 135px"
>
<a-select-option
v-for="opt in shuJuTianBaoStore.baseOption"
@ -116,6 +117,9 @@
</div>
<!-- 下拉选择 -->
<!-- <div v-else-if="item.type === 'Select'">
<div v-for="i in item.options"> {{ i[item.values?.name] }} {{ i[item.values?.value] }}</div>
</div> -->
<a-select
v-else-if="item.type === 'Select'"
v-model:value="formData[item.name]"

View File

@ -1,128 +1,214 @@
<template>
<a-select
ref="selectRef"
:style="{ width: width }"
:value="modelValue"
:options="options"
:loading="loading"
@change="handleChange"
@search="handleSearch"
placeholder="鱼种类支持俗名查询"
:mode="multiple ? 'multiple' : undefined"
show-search
:filter-option="false"
class="custom-fish-select"
:dropdownMatchSelectWidth="false"
@dropdownVisibleChange="handleDropdownVisibleChange"
:max-tag-count="multiple ? 1 : undefined"
:open="open"
@update:open="open = $event"
:field-names="{ label: 'name', value: 'id' }"
>
<!-- 自定义 Tag 显示名称 (仅在多选时生效) -->
<template #tagRender="{ value: tagId, onClose }" v-if="multiple">
<a-tag closable @close="onClose" style="margin-right: 3px; max-width: 120px">
{{ getFishNameById(tagId) }}
</a-tag>
</template>
<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)"
class="custom-fish-search-container"
ref="containerRef"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<!-- 1. 输入框区域 -->
<div
class="fish-input-wrapper"
:class="{ 'is-focused': isOpen }"
@click="handleWrapperClick"
>
<!--
核心逻辑
- isOpen true 强制显示 input并聚焦
- isOpen false 显示 selectedItem placeholder
-->
<!-- 情况 A: 显示输入框 (当下拉框打开时) -->
<input
v-if="isOpen"
ref="inputRef"
v-model="searchKeyword"
type="text"
class="visible-input"
@focus="handleInputFocus"
@input="handleSearchInput"
@blur="handleInputBlur"
@keydown.stop
@keydown.enter="handleEnterKey"
:disabled="disabled"
placeholder="请输入搜索..."
/>
<!-- 情况 B: 显示选中项或占位符 (当下拉框关闭时) -->
<div v-else class="display-value">
<span v-if="selectedItem" class="single-value">{{ selectedItem.name }}</span>
<span v-else class="placeholder">{{ placeholder }}</span>
</div>
<!-- 右侧图标 -->
<div class="suffix-icon">
<!-- Loading 状态 -->
<span v-if="loading" class="loading-icon"></span>
<!-- 清除图标当有值选中或正在搜索且鼠标悬停时显示 -->
<span
v-else-if="(selectedItem || searchKeyword) && isHovered"
class="clear-icon"
@click.stop="handleClear"
title="清空"
>
<CloseCircleOutlined />
</span>
<!-- 下拉箭头其他情况显示 -->
<span v-else class="arrow-icon" :class="{ rotate: isOpen }"
><DownOutlined
/></span>
</div>
</div>
<!-- 2. 下拉面板 (绝对定位) -->
<teleport to="body">
<transition name="fade">
<div
v-show="isOpen"
class="fish-dropdown-panel"
:style="dropdownStyle"
@mousedown.prevent
>
<!-- 顶部查询方式切换 -->
<div class="dropdown-header">
<span class="header-label">查询方式</span>
<span
class="mode-btn"
:class="{ active: !isIntelligentQuery }"
@click="switchQueryMode(false)"
>
相似度
</div>
<div
class="text-[12px] font-bold cursor-pointer"
:class="{ 'text-[#005293]': isIntelligentQuery }"
@click="IntelligentQueryCLick(true)"
</span>
<span
class="mode-btn"
:class="{ active: isIntelligentQuery }"
@click="switchQueryMode(true)"
>
智能查询
</span>
</div>
<div class="dropdown-body">
<!-- 左侧列表 -->
<div class="list-container" ref="listContainerRef">
<div v-if="loading" class="loading-wrapper">
<a-spin size="small" />
<span class="loading-text">搜索中...</span>
</div>
<!-- 左侧可滚动的选项列表 -->
<div class="dropdown-left-list">
<div
v-for="opt in filteredOptions"
:key="opt.id"
class="dropdown-item"
:class="{
'is-active': isSelected(opt.id),
'is-hovered': opt.id === hoveredId,
'is-hovered': hoveredId === opt.id,
}"
@click.stop="handleSelectOption(opt)"
@click="handleSelectOption(opt)"
@mouseenter="hoveredId = opt.id"
>
<span class="item-name" v-html="highlightText(opt.name)"></span>
<!-- 选中对勾 -->
<span v-if="isSelected(opt.id)" class="check-icon"></span>
</div>
<div v-if="filteredOptions.length === 0" class="empty-tip">无匹配数据</div>
</div>
<!-- 中间分割线 -->
<div class="dropdown-divider"></div>
<div class="divider"></div>
<!-- 右侧固定显示的别名/详情 -->
<div class="dropdown-right-detail" @click.stop @mousedown.prevent>
<!-- 右侧详情预览 -->
<div class="detail-container">
<div v-if="currentDetailData" class="detail-content">
<div
class="detail-title"
v-html="highlightText(currentDetailData.name)"
></div>
<div class="detail-alias" :title="currentDetailData.alias">
<div v-html="highlightText(currentDetailData.alias)"></div>
<div
v-html="highlightText(currentDetailData.alias || '暂无别名')"
></div>
</div>
</div>
<div v-else class="detail-placeholder">请选择或悬停查看</div>
</div>
</div>
</template>
</a-select>
</div>
</transition>
</teleport>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, computed, watch } from "vue";
import { getFishDictoryDropdown } from "@/api/select";
import { ref, onMounted, computed, nextTick, onBeforeUnmount } from "vue";
import { getFishDictoryDropdown, getSimilarFishDictoryDropdown } from "@/api/select";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
import { message } from "ant-design-vue";
import { CloseCircleOutlined, DownOutlined } from "@ant-design/icons-vue";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- Props & Emits ---
interface Props {
modelValue: string | string[]; // ()()
modelValue: any; //
width?: string;
multiple?: boolean; //
placeholder?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
multiple: false, //
placeholder: "鱼种类支持俗名查询",
width: "100%",
disabled: false,
});
const emit = defineEmits<{
(e: "update:modelValue", value: string | string[], opt: any): void;
(e: "update:modelValue", value: string, opt: any): void;
}>();
// --- State ---
const loading = ref(false);
const options = ref<any[]>([]);
const allOptions = ref<any[]>([]);
const searchKeyword = ref<string>("");
const hoveredId = ref<string | null>(null);
const open = ref(false); //
const isIntelligentQuery = ref(true); //
const isOpen = ref(false);
const isIntelligentQuery = ref(true);
const isHovered = ref(false); //
//
let searchTimer: any = null;
// DOM Refs
const containerRef = ref<HTMLElement | null>(null);
const inputRef = ref<HTMLInputElement | null>(null);
//
const dropdownStyle = computed(() => {
if (!containerRef.value) return {};
const rect = containerRef.value.getBoundingClientRect();
return {
top: `${rect.bottom + window.scrollY + 4}px`,
left: `${rect.left + window.scrollX}px`,
width: `${Math.max(rect.width, 400)}px`,
};
});
// --- Computed ---
const selectedItem = computed(() => {
if (!props.modelValue) return null;
return options.value.find((opt) => opt.id === props.modelValue);
});
const filteredOptions = computed(() => {
if (!searchKeyword.value) {
if (isIntelligentQuery.value) {
return options.value;
}
//
if (!searchKeyword.value) {
return allOptions.value; //
}
const lowerKeyword = searchKeyword.value.toLowerCase();
return options.value.filter((item: any) => {
return allOptions.value.filter((item: any) => {
const nameMatch = item.name?.toLowerCase().includes(lowerKeyword);
const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword);
return nameMatch || aliasMatch;
@ -131,153 +217,370 @@ const filteredOptions = computed(() => {
const currentDetailData = computed(() => {
if (hoveredId.value) {
return options.value.find((item: any) => item.id === hoveredId.value);
// options allOptions
return (
options.value.find((item: any) => item.id === hoveredId.value) ||
allOptions.value.find((item: any) => item.id === hoveredId.value)
);
}
if (props.modelValue) {
return allOptions.value.find((item: any) => item.id === props.modelValue);
}
return null;
});
// --- Methods ---
//
const isSelected = (id: string) => {
if (props.multiple) {
return Array.isArray(props.modelValue) && props.modelValue.includes(id);
} else {
return props.modelValue === id;
};
const toggleDropdown = () => {
if (props.disabled) return;
if (isOpen.value) {
closeDropdown();
} else {
openDropdown();
}
};
const handleSearch = (value: string) => {
searchKeyword.value = value;
const openDropdown = async () => {
isOpen.value = true;
await nextTick();
inputRef.value?.focus();
};
const closeDropdown = () => {
isOpen.value = false;
hoveredId.value = null;
};
const handleDropdownVisibleChange = (val: boolean) => {
open.value = val;
if (!val) {
hoveredId.value = null;
//
const handleClear = () => {
// 1.
emit("update:modelValue", "", null);
// 2.
searchKeyword.value = "";
options.value = allOptions.value; //
// 3.
closeDropdown();
};
const handleWrapperClick = () => {
toggleDropdown();
};
const handleInputFocus = () => {
if (!isOpen.value) {
isOpen.value = true;
}
};
const handleSelectOption = (opt: any) => {
console.log(props.modelValue)
console.log(props.modelValue)
if (props.multiple) {
// --- ---
let newValues: string[] = Array.isArray(props.modelValue)
? [...props.modelValue]
: [];
const index = newValues.indexOf(opt.id);
if (index > -1) {
newValues.splice(index, 1); //
} else {
newValues.push(opt.id); //
}
emit("update:modelValue", newValues, opt);
} else {
// --- ---
// ID
//
if (props.modelValue === opt.id) {
emit("update:modelValue", "", opt); //
} else {
emit("update:modelValue", opt.id, opt); //
//
open.value = false;
}
}
const handleInputBlur = () => {
setTimeout(() => {
// handleClickOutside
}, 100);
};
const IntelligentQueryCLick = (val: boolean) => {
isIntelligentQuery.value = val;
if (val) {
message.success("智能查询已开启");
} else {
message.success("智能查询已关闭");
const handleSearchInput = () => {
hoveredId.value = null;
if (!isOpen.value) {
isOpen.value = true;
}
};
const handleChange = (val: any) => {
// a-select change Tag
//
emit("update:modelValue", val, "");
};
//
const executeSearch = () => {
const keyword = searchKeyword.value;
const getFishNameById = (id: string) => {
if (!id) return "";
const fish = options.value.find((item: any) => item.id === id);
return fish ? fish.name : id;
};
//
if (!isOpen.value) {
isOpen.value = true;
}
// multiple
watch(
() => props.multiple,
(newVal) => {
// modelValue
if (!newVal && Array.isArray(props.modelValue)) {
emit("update:modelValue", props.modelValue || null, options.value);
// 1.
if (!isIntelligentQuery.value) {
if (!keyword) {
options.value = allOptions.value;
}
// modelValue
if (newVal && typeof props.modelValue === "string") {
emit("update:modelValue", props.modelValue ? [props.modelValue] : [], {});
// computed searchKeyword allOptions
return;
}
// 2.
if (!keyword) {
options.value = allOptions.value;
return;
}
);
const init = () => {
let data = shuJuTianBaoStore.getFishOption();
if (data.length === 0) {
// Loading
loading.value = true;
getFishDictoryDropdown()
// ()
getSimilarFishDictoryDropdown({ name: keyword })
.then((res) => {
options.value = res.data || [];
loading.value = false;
shuJuTianBaoStore.setFishOption(options.value);
})
.catch(() => {
loading.value = false;
});
};
//
const loadDefaultOptions = () => {
let data = shuJuTianBaoStore.getFishOption();
if (data && data.length > 0) {
allOptions.value = data;
options.value = data; //
} else {
options.value = data;
loading.value = true;
getFishDictoryDropdown()
.then((res) => {
const list = res.data || [];
allOptions.value = list;
options.value = list;
loading.value = false;
shuJuTianBaoStore.setFishOption(list);
})
.catch(() => {
loading.value = false;
});
}
};
//
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>`);
//
const handleEnterKey = (event: KeyboardEvent) => {
//
event.preventDefault();
//
executeSearch();
};
const handleSelectOption = (opt: any) => {
// 1.
if (props.modelValue === opt.id) {
emit("update:modelValue", "", null);
} else {
emit("update:modelValue", opt.id, opt);
}
// 2.
searchKeyword.value = "";
options.value = allOptions.value; // 便
// 3.
closeDropdown();
// 4.
inputRef.value?.blur();
};
const switchQueryMode = (val: boolean) => {
isIntelligentQuery.value = val;
message.success(val ? "智能查询已开启" : "相似度查询已开启");
if (searchKeyword.value) {
executeSearch(); //
} else {
options.value = allOptions.value;
}
};
const highlightText = (text: string) => {
if (!text) return "暂无别名";
if (!searchKeyword.value) return text;
const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const reg = new RegExp(`(${escapeRegExp(searchKeyword.value)})`, "gi");
return text.replace(reg, `<span style="color: #ff4d4f; font-weight: bold;">$1</span>`);
};
//
const init = () => {
loadDefaultOptions();
};
//
const handleClickOutside = (event: MouseEvent) => {
if (!isOpen.value) return;
const target = event.target as HTMLElement;
const isInsideContainer = containerRef.value?.contains(target);
const isInsideDropdown = target.closest(".fish-dropdown-panel");
if (!isInsideContainer && !isInsideDropdown) {
closeDropdown();
}
};
onMounted(() => {
init();
document.addEventListener("click", handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener("click", handleClickOutside);
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.custom-fish-select {
:deep(.ant-select-dropdown) {
padding: 0 !important;
min-width: 400px !important;
width: auto !important;
.custom-fish-search-container {
position: relative;
width: v-bind(width);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
}
.fish-input-wrapper {
position: relative;
min-height: 32px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
background-color: #fff;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
border-color: #40a9ff;
}
&.is-focused {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.placeholder {
color: #bfbfbf;
}
.single-value {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #333;
}
.display-value {
flex: 1;
overflow: hidden;
margin-right: 20px;
pointer-events: none; /* 让点击事件穿透到 wrapper */
text-overflow: ellipsis;
white-space: nowrap;
}
/* 输入框样式 */
.visible-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #333;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
&::placeholder {
color: #bfbfbf;
}
}
.suffix-icon {
color: #bfbfbf;
font-size: 12px;
pointer-events: none;
margin-left: 8px;
display: flex;
align-items: center;
.rotate {
transform: rotate(180deg);
transition: transform 0.3s;
}
/* 新增:清除图标样式 */
.clear-icon {
pointer-events: auto; /* 允许点击 */
cursor: pointer;
font-size: 14px;
line-height: 1;
transition: color 0.3s;
&:hover {
color: #333;
}
}
}
}
.custom-dropdown-container {
width: 401px;
display: flex;
flex-wrap: wrap;
/* 下拉面板样式 */
.fish-dropdown-panel {
position: absolute;
background: #fff;
border-radius: 4px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 9999;
overflow: hidden;
margin-top: 4px;
}
.dropdown-header {
height: 36px;
padding: 0 12px;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
user-select: none;
.header-label {
font-size: 12px;
color: #666;
margin-right: 8px;
}
.mode-btn {
font-size: 12px;
font-weight: bold;
cursor: pointer;
padding: 2px 6px;
border-radius: 2px;
color: #666;
margin-right: 8px;
&:last-child {
margin-right: 0;
}
&.active {
color: #005293;
background: rgba(0, 82, 147, 0.1);
}
&:hover:not(.active) {
color: #333;
}
}
}
.dropdown-body {
display: flex;
height: 300px;
}
.dropdown-left-list {
.list-container {
width: 150px;
height: 100%;
overflow-y: auto;
border-right: 1px solid #f0f0f0;
@ -288,6 +591,20 @@ onMounted(() => {
background: #ccc;
border-radius: 3px;
}
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 100px;
color: #999;
.loading-text {
margin-top: 8px;
font-size: 12px;
}
}
}
.dropdown-item {
@ -295,7 +612,7 @@ onMounted(() => {
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.3s;
transition: background-color 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
@ -315,11 +632,10 @@ onMounted(() => {
}
.item-name {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.check-icon {
@ -329,22 +645,19 @@ onMounted(() => {
}
}
.dropdown-divider {
.divider {
width: 1px;
background-color: #e8e8e8;
height: 100%;
}
.dropdown-right-detail {
.detail-container {
width: 250px;
height: 100%;
padding: 16px;
box-sizing: border-box;
background-color: #fff;
overflow-y: auto;
background: #fff;
display: flex;
flex-direction: column;
position: relative;
}
.detail-content {
@ -357,7 +670,6 @@ onMounted(() => {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.detail-alias {
@ -365,8 +677,10 @@ onMounted(() => {
color: #666;
line-height: 1.5;
word-break: break-all;
span {
color: red;
:deep(span) {
color: #ff4d4f;
font-weight: bold;
}
}
@ -374,16 +688,25 @@ onMounted(() => {
color: #999;
font-size: 14px;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin-top: 50%;
transform: translateY(-50%);
}
.empty-tip {
padding: 10px;
padding: 20px 0;
color: #999;
text-align: center;
font-size: 12px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>

View File

@ -1,7 +1,6 @@
import { defineStore } from 'pinia';
import { ref } from 'vue'; // 使用 ref 更简单直观
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from '@/api/select';
import { set } from 'lodash';
export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
// 1. 直接使用 ref 定义状态,确保响应式
@ -19,10 +18,6 @@ export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
const res = await getBaseDropdown({});
if (res.data && Array.isArray(res.data)) {
const list = [...res.data];
list.unshift({
baseid: 'all',
basename: '当前全部'
});
// 直接赋值给 ref触发响应式更新
baseOption.value = list;
}
@ -53,6 +48,7 @@ export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
try {
fpssLoading.value = true;
const res = await getFpssDropdown({ baseId, rstcd });
console.log(res.data)
fpssOption.value = res.data;
} catch (error) {
console.log(error);

View File

@ -84,18 +84,87 @@ export function parseTime(time :any, cFormat :any) {
})
return time_str
}
export function downloadFile(obj :any, name :any, suffix :any) {
const url = window.URL.createObjectURL(new Blob([obj]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
const fileName = parseTime(new Date(),'') + '-' + name + '.' + suffix
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
export function downloadFile(obj: any, name: any, suffix: any) {
try {
const url = window.URL.createObjectURL(new Blob([obj]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
// 优化文件名生成逻辑,避免多余的中横线
const timeStamp = parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') || '';
const separator = timeStamp ? '-' : '';
const fileName = `${timeStamp}${separator}${name}.${suffix}`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// 延迟移除,确保下载触发
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error('Download failed:', error);
}
}
/**
* URL
* @param url
* @param fileName URL 使
*/
export function downloadFileByUrl(url: string, fileName?: string) {
if (!url) return;
// 如果没有提供文件名,尝试从 URL 中提取
let name = fileName;
if (!name) {
const urlParts = url.split('/');
name = urlParts[urlParts.length - 1] || 'download_file';
// 去除查询参数
name = name.split('?')[0];
}
// 方法 1: 使用 fetch 获取 Blob (推荐,可重命名且强制下载)
// 注意:如果 URL 跨域且服务器未配置 CORSfetch 会失败
fetch(url)
.then((response) => {
if (!response.ok) throw new Error('Network response was not ok');
return response.blob();
})
.then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();
// 清理
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}, 100);
})
.catch((error) => {
console.warn('Fetch download failed (possibly CORS), falling back to direct link:', error);
// 方法 2: 回退方案 - 直接使用 a 标签跳转
// 这种方式对于 PDF/图片等浏览器支持预览的文件,可能会直接在新标签页打开而不是下载
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
// 如果同源download 属性生效;如果跨域,大多数浏览器会忽略 download 属性并直接打开
if (fileName) {
link.setAttribute('download', fileName);
}
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
const modules = import.meta.glob('@/assets/legend/*.svg', { eager: true });
// 图例图标映射
export const iconMap: Record<string, string> = {};

View File

@ -258,7 +258,7 @@
上传视频 (MP4)
</a-button>
</a-upload>
<a-button v-else @click="handleVideoPreview"> 点击预览视频 </a-button>
<a-button v-else-if="videoFileList.length > 0" @click="handleVideoPreview"> 点击预览视频 </a-button>
</a-form-item>
</a-col>
</a-row>
@ -307,7 +307,9 @@ const getBaseDropdownSelect = async () => {
try {
baseLoading.value = true;
const res = await getBaseDropdown({});
baseOption.value = res.data;
let list = res.data || [];
if (list.length > 0) list.shift();
baseOption.value = list;
} catch (error) {
console.error("获取流域列表失败:", error);
} finally {
@ -642,7 +644,6 @@ const handleOk = async () => {
//
const isBodyLenValid = validateBodyLength();
const isWeightValid = validateWeight();
console.log(isBodyLenValid, isWeightValid);
if (!isBodyLenValid || !isWeightValid) {
message.error("请检查体长或体重填写是否正确");
return;
@ -749,7 +750,6 @@ const handleOk = async () => {
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) {

View File

@ -9,7 +9,7 @@
@values-change="onValuesChange"
>
<template #ftp="{ onChange }">
<fishSearch v-model="localTypeDate" width="280px" @update:modelValue="onChange" />
<fishSearch v-model="localTypeDate" width="200px" @update:modelValue="onChange" />
</template>
<template #actions>
<a-tooltip title="新增">
@ -51,13 +51,13 @@
</a-button>
</a-tooltip>
<a-tooltip title="下载模板">
<a-button v-hasPerm="['sjtb:import-add']">
<a-button v-hasPerm="['sjtb:import-add']" @click="downloadTemplate">
下载模板
</a-button>
</a-tooltip>
<a-tooltip placement="leftBottom">
<a-tooltip placement="left">
<template #title>
<div>1.</div>
<div>导入须知仅支持 ZIP 压缩包上传压缩包内需包含 imagesvideo 文件夹根目录需放置 xlsx 格式表格单包大小请勿超过 10MBExcel 单次最大支持 10000 行数据超出请分批导入具体请参考下载模版格式</div>
</template>
<a-button>
<template #icon><QuestionOutlined /></template>
@ -80,6 +80,7 @@ import BasicSearch from "@/components/BasicSearch/index.vue"; // 确保路径正
import { DateSetting } from "@/utils/enumeration";
import fishSearch from "@/components/fishSearch/index.vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
import { downloadFileByUrl } from "@/utils/index";
interface Props {
direction: any[];
@ -130,7 +131,7 @@ const searchList: any = computed(() => [
type: "Select",
name: "stcd",
label: "过鱼设施",
values: { name: "stnm", value: "rstcd" },
values: { name: "stnm", value: "stcd" },
fieldProps: {
allowClear: true,
},
@ -201,6 +202,12 @@ const onValuesChange = (changedValues: any, allValues: any) => {
}
};
//
const downloadTemplate = () => {
// 20260429090252740092641634938251
downloadFileByUrl(import.meta.env.VITE_APP_PREVIEW_URL + "/?20260429090252740092641634938251", "过鱼设施数据填报模板", "zip");
};
const handleReset = () => {
localTypeDate.value = null;
emit("reset", initSearchData);

View File

@ -6,7 +6,7 @@
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
:pagination="false"
:row-key="(_record, index) => index"
row-key="id"
>
<template #bodyCell="{ column, record, index }">
<!-- 1. 操作列 -->
@ -249,9 +249,10 @@ import { ref, reactive, onMounted, h } from "vue";
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 { getBaseDropdown, getEngInfoDropdown, getFpssDropdown,updateImportTask } from "@/api/select";
const props: any = defineProps({
taskId: { type: String, default: '' },
fileTableData: { type: Array, default: () => [] },
fileLoading: { type: Boolean, default: false },
direction: { type: Array, default: () => [] },
@ -355,7 +356,9 @@ onMounted(() => {
const loadBaseOptions = async () => {
try {
const res = await getBaseDropdown({});
baseOptions.value = res.data || [];
let list = res.data || [];
if (list.length > 0) list.shift();
baseOptions.value = list;
} catch (e) {
console.error("Load base options failed", e);
}
@ -467,9 +470,7 @@ const startEdit = (index: number) => {
editingRowIndex.value = index;
// 3. ( editingData )
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");
@ -478,7 +479,6 @@ const startEdit = (index: number) => {
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");
});
@ -550,6 +550,9 @@ const saveEdit = (index: number) => {
// 4.
editingRowIndex.value = null;
editingData.value = null;
console.log(newData)
console.log(props.taskId)
// updateImportTask
message.success("保存成功");
};

View File

@ -35,14 +35,27 @@
v-if="record.status === 'DRAFT' || record.status === 'REJECTED'"
>提交</a-button
>
<a-button
v-hasPerm="['sjtb:import-add']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="
record.status === 'DRAFT' ||
record.status === 'REJECTED' ||
record.status === 'SUBMITTED'
record.status === 'REJECTED'
"
>编辑</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleEdit(record, 'edit')"
v-if="
record.status === 'PENDING' ||
record.status === 'REJECTED'||
record.status === 'PENDING'
"
>编辑</a-button
>
@ -58,22 +71,24 @@
type="link"
size="small"
@click="handleEdit(record, 'view')"
v-if="record.status === 'SUBMITTED' || record.status === 'APPROVED'"
v-if="record.status === 'PENDING' || record.status === 'APPROVED'"
>查看</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
size="small"
@click="handleSuccess([record.id])"
v-if="record.status === 'SUBMITTED'"
v-if="record.status === 'PENDING'"
>审批</a-button
>
<a-button
v-hasPerm="['sjtb:edit-review']"
type="link"
danger
size="small"
@click="handleReject(record.id)"
v-if="record.status === 'SUBMITTED'"
v-if="record.status === 'PENDING'"
>驳回</a-button
>
</div>
@ -97,9 +112,11 @@
v-model:open="visible"
:maskClosable="false"
:confirm-loading="fileLoading"
@cancel="taskId = ''"
>
<GuoYuSheShiShuJuTianBaoTable
ref="modalTableRef"
:taskId="taskId"
:fileLoading="fileLoading"
:fileTableData="fileTableData"
:direction="direction"
@ -138,7 +155,7 @@
:footer="null"
width="900px"
@cancel="closeMediaPreview"
z-index="2000"
:zIndex="2000"
>
<div class="flex h-[60vh] gap-4">
<!-- 左侧混合列表 (图片+视频) -->
@ -245,7 +262,6 @@ import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
import GuoYuSheShiShuJuTianBaoTable from "./guoYuSheShiShuJuTianBaoTable.vue";
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
import { checkPerm } from "@/directive/permission";
import {
getFishDraftPage,
addFishDraft,
@ -264,7 +280,6 @@ import {
import { Tag } from "ant-design-vue"; // Tag
import { getDictItemsByCode } from "@/api/dict";
import dayjs from "dayjs";
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
const baseUrl = import.meta.env.VITE_APP_PREVIEW_URL;
// --- ---
@ -558,15 +573,10 @@ const handleReject = (id: any) => {
};
//
const getCheckboxProps = (record: any) => {
console.log(checkPerm(["sjtb:edit-review"]));
return {
disabled: ["SUBMITTED", "APPROVED"].includes(record.status),
disabled: [!checkPerm(['sjtb:edit-review'])&&"PENDING", "APPROVED"].includes(record.status),
};
};
const handleDataLoaded = (params: any, data: any) => {
console.log(params, data);
return;
};
//
const handleSelectionChange = (keys: any) => {
batchData.value = keys;
@ -651,9 +661,6 @@ const checkTableDataChanges = () => {
//
const handleModalOk = () => {
console.log(orgFileTableData.value);
console.log(fileTableData.value);
console.log(modalTableRef.value.editingData);
if (modalTableRef.value.editingData != undefined) {
message.warning("请点击保存后提交数据!");
return;
@ -941,7 +948,6 @@ const handleSearchFinish = (values: any) => {
};
// ()
const handlePreviewClick = (record: any, type: string, index: number) => {
console.log(record, type);
const mixedList: MediaItem[] = [];
if (type === "image") {
tablePreviewRecord.value = record;
@ -989,7 +995,6 @@ const handlePreviewClick = (record: any, type: string, index: number) => {
mediaPreviewVisible.value = true;
currentMediaIndex.value = index;
console.log(mixedList);
nextTick(() => {
previewList.value = mixedList;
});
@ -1016,7 +1021,6 @@ const handleDeleteMedia = (item: any, index: number) => {
zIndex: 2002,
onOk: () => {
previewList.value.splice(index, 1);
console.log(previewList.value);
if (videoPreviewTitle.value == "图片预览") {
if (previewList.value.length == 0) {
tablePreviewRecord.value.picpthList = [];
@ -1064,7 +1068,7 @@ onMounted(() => {
getDictItemsByCode({ dictCode: "direction" }).then((res) => {
direction.value = res.data;
});
getDictItemsByCode({ dictCode: "guoyuStatus" }).then((res) => {
getDictItemsByCode({ dictCode: "approvalStatus" }).then((res) => {
guoyuStatus.value = res.data;
});
});