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

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

View File

@ -83,6 +83,7 @@
:loading="shuJuTianBaoStore.baseLoading" :loading="shuJuTianBaoStore.baseLoading"
:filter-option="filterOption" :filter-option="filterOption"
style="width: 135px" style="width: 135px"
> >
<a-select-option <a-select-option
v-for="opt in shuJuTianBaoStore.baseOption" v-for="opt in shuJuTianBaoStore.baseOption"
@ -116,6 +117,9 @@
</div> </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 <a-select
v-else-if="item.type === 'Select'" v-else-if="item.type === 'Select'"
v-model:value="formData[item.name]" v-model:value="formData[item.name]"

View File

@ -1,128 +1,214 @@
<template> <template>
<a-select <div
ref="selectRef" class="custom-fish-search-container"
:style="{ width: width }" ref="containerRef"
:value="modelValue" @mouseenter="isHovered = true"
:options="options" @mouseleave="isHovered = false"
: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 显示名称 (仅在多选时生效) --> <!-- 1. 输入框区域 -->
<template #tagRender="{ value: tagId, onClose }" v-if="multiple"> <div
<a-tag closable @close="onClose" style="margin-right: 3px; max-width: 120px"> class="fish-input-wrapper"
{{ getFishNameById(tagId) }} :class="{ 'is-focused': isOpen }"
</a-tag> @click="handleWrapperClick"
</template> >
<!--
核心逻辑
- isOpen true 强制显示 input并聚焦
- isOpen false 显示 selectedItem placeholder
-->
<template #dropdownRender> <!-- 情况 A: 显示输入框 (当下拉框打开时) -->
<div class="custom-dropdown-container"> <input
<div class="w-[340px] h-[30px] flex items-center pl-[10px]" @click.stop @mousedown.prevent> v-if="isOpen"
<div>查询方式</div> ref="inputRef"
<div v-model="searchKeyword"
class="text-[12px] font-bold mr-2 cursor-pointer" type="text"
:class="{ 'text-[#005293]': !isIntelligentQuery }" class="visible-input"
@click="IntelligentQueryCLick(false)" @focus="handleInputFocus"
> @input="handleSearchInput"
相似度 @blur="handleInputBlur"
</div> @keydown.stop
<div @keydown.enter="handleEnterKey"
class="text-[12px] font-bold cursor-pointer" :disabled="disabled"
:class="{ 'text-[#005293]': isIntelligentQuery }" placeholder="请输入搜索..."
@click="IntelligentQueryCLick(true)" />
>
智能查询
</div>
</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,
}"
@click.stop="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>
<!-- 中间分割线 --> <!-- 情况 B: 显示选中项或占位符 (当下拉框关闭时) -->
<div class="dropdown-divider"></div> <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="dropdown-right-detail" @click.stop @mousedown.prevent> <div class="suffix-icon">
<div v-if="currentDetailData" class="detail-content"> <!-- Loading 状态 -->
<div <span v-if="loading" class="loading-icon"></span>
class="detail-title"
v-html="highlightText(currentDetailData.name)" <!-- 清除图标当有值选中或正在搜索且鼠标悬停时显示 -->
></div> <span
<div class="detail-alias" :title="currentDetailData.alias"> v-else-if="(selectedItem || searchKeyword) && isHovered"
<div v-html="highlightText(currentDetailData.alias)"></div> 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)"
>
相似度
</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
v-for="opt in filteredOptions"
:key="opt.id"
class="dropdown-item"
:class="{
'is-active': isSelected(opt.id),
'is-hovered': hoveredId === opt.id,
}"
@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="divider"></div>
<!-- 右侧详情预览 -->
<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>
</div>
<div v-else class="detail-placeholder">请选择或悬停查看</div>
</div> </div>
</div> </div>
<div v-else class="detail-placeholder">请选择或悬停查看</div>
</div> </div>
</div> </transition>
</template> </teleport>
</a-select> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed, watch } from "vue"; import { ref, onMounted, computed, nextTick, onBeforeUnmount } from "vue";
import { getFishDictoryDropdown } from "@/api/select"; import { getFishDictoryDropdown, getSimilarFishDictoryDropdown } from "@/api/select";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao"; import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import { CloseCircleOutlined, DownOutlined } from "@ant-design/icons-vue";
const shuJuTianBaoStore = useShuJuTianBaoStore(); const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- Props & Emits --- // --- Props & Emits ---
interface Props { interface Props {
modelValue: string | string[]; // ()() modelValue: any; //
width?: string; width?: string;
multiple?: boolean; // placeholder?: string;
disabled?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
multiple: false, // placeholder: "鱼种类支持俗名查询",
width: "100%",
disabled: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: string | string[], opt: any): void; (e: "update:modelValue", value: string, opt: any): void;
}>(); }>();
// --- State --- // --- State ---
const loading = ref(false); const loading = ref(false);
const options = ref<any[]>([]); const options = ref<any[]>([]);
const allOptions = ref<any[]>([]);
const searchKeyword = ref<string>(""); const searchKeyword = ref<string>("");
const hoveredId = ref<string | null>(null); const hoveredId = ref<string | null>(null);
const open = ref(false); // const isOpen = ref(false);
const isIntelligentQuery = ref(true); // 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 --- // --- Computed ---
const selectedItem = computed(() => {
if (!props.modelValue) return null;
return options.value.find((opt) => opt.id === props.modelValue);
});
const filteredOptions = computed(() => { const filteredOptions = computed(() => {
if (!searchKeyword.value) { if (isIntelligentQuery.value) {
return options.value; return options.value;
} }
//
if (!searchKeyword.value) {
return allOptions.value; //
}
const lowerKeyword = searchKeyword.value.toLowerCase(); 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 nameMatch = item.name?.toLowerCase().includes(lowerKeyword);
const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword); const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword);
return nameMatch || aliasMatch; return nameMatch || aliasMatch;
@ -131,153 +217,370 @@ const filteredOptions = computed(() => {
const currentDetailData = computed(() => { const currentDetailData = computed(() => {
if (hoveredId.value) { 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; return null;
}); });
// --- Methods --- // --- Methods ---
//
const isSelected = (id: string) => { const isSelected = (id: string) => {
if (props.multiple) { return props.modelValue === id;
return Array.isArray(props.modelValue) && props.modelValue.includes(id); };
const toggleDropdown = () => {
if (props.disabled) return;
if (isOpen.value) {
closeDropdown();
} else { } else {
return props.modelValue === id; openDropdown();
} }
}; };
const handleSearch = (value: string) => { const openDropdown = async () => {
searchKeyword.value = value; isOpen.value = true;
await nextTick();
inputRef.value?.focus();
};
const closeDropdown = () => {
isOpen.value = false;
hoveredId.value = null; hoveredId.value = null;
}; };
const handleDropdownVisibleChange = (val: boolean) => { //
open.value = val; const handleClear = () => {
if (!val) { // 1.
hoveredId.value = null; emit("update:modelValue", "", null);
searchKeyword.value = ""; // 2.
searchKeyword.value = "";
options.value = allOptions.value; //
// 3.
closeDropdown();
};
const handleWrapperClick = () => {
toggleDropdown();
};
const handleInputFocus = () => {
if (!isOpen.value) {
isOpen.value = true;
} }
}; };
const handleSelectOption = (opt: any) => { const handleInputBlur = () => {
console.log(props.modelValue) setTimeout(() => {
console.log(props.modelValue) // handleClickOutside
if (props.multiple) { }, 100);
// --- ---
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 IntelligentQueryCLick = (val: boolean) => {
isIntelligentQuery.value = val; const handleSearchInput = () => {
if (val) { hoveredId.value = null;
message.success("智能查询已开启"); if (!isOpen.value) {
} else { isOpen.value = true;
message.success("智能查询已关闭");
} }
}; };
const handleChange = (val: any) => { //
// a-select change Tag const executeSearch = () => {
// const keyword = searchKeyword.value;
emit("update:modelValue", val, "");
};
const getFishNameById = (id: string) => { //
if (!id) return ""; if (!isOpen.value) {
const fish = options.value.find((item: any) => item.id === id); isOpen.value = true;
return fish ? fish.name : id;
};
// multiple
watch(
() => props.multiple,
(newVal) => {
// modelValue
if (!newVal && Array.isArray(props.modelValue)) {
emit("update:modelValue", props.modelValue || null, options.value);
}
// modelValue
if (newVal && typeof props.modelValue === "string") {
emit("update:modelValue", props.modelValue ? [props.modelValue] : [], {});
}
} }
);
const init = () => { // 1.
if (!isIntelligentQuery.value) {
if (!keyword) {
options.value = allOptions.value;
}
// computed searchKeyword allOptions
return;
}
// 2.
if (!keyword) {
options.value = allOptions.value;
return;
}
// Loading
loading.value = true;
// ()
getSimilarFishDictoryDropdown({ name: keyword })
.then((res) => {
options.value = res.data || [];
loading.value = false;
})
.catch(() => {
loading.value = false;
});
};
//
const loadDefaultOptions = () => {
let data = shuJuTianBaoStore.getFishOption(); let data = shuJuTianBaoStore.getFishOption();
if (data.length === 0) { if (data && data.length > 0) {
allOptions.value = data;
options.value = data; //
} else {
loading.value = true; loading.value = true;
getFishDictoryDropdown() getFishDictoryDropdown()
.then((res) => { .then((res) => {
options.value = res.data || []; const list = res.data || [];
allOptions.value = list;
options.value = list;
loading.value = false; loading.value = false;
shuJuTianBaoStore.setFishOption(options.value); shuJuTianBaoStore.setFishOption(list);
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
} else {
options.value = data;
} }
}; };
// //
const highlightText = (text) => { const handleEnterKey = (event: KeyboardEvent) => {
if (text == null) { //
return "暂无别名"; event.preventDefault();
}
const reg = new RegExp(searchKeyword.value, "g"); //
return text.replace(reg, `<span style="color: red;">${searchKeyword.value}</span>`); 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(() => { onMounted(() => {
init(); init();
document.addEventListener("click", handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener("click", handleClickOutside);
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 样式保持不变 */ .custom-fish-search-container {
.custom-fish-select { position: relative;
:deep(.ant-select-dropdown) { width: v-bind(width);
padding: 0 !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
min-width: 400px !important; Arial, sans-serif;
width: auto !important; }
.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; .fish-dropdown-panel {
display: flex; position: absolute;
flex-wrap: wrap;
background: #fff; background: #fff;
border-radius: 4px; 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; 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; height: 300px;
} }
.dropdown-left-list { .list-container {
width: 150px; width: 150px;
height: 100%;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #f0f0f0; border-right: 1px solid #f0f0f0;
@ -288,6 +591,20 @@ onMounted(() => {
background: #ccc; background: #ccc;
border-radius: 3px; 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 { .dropdown-item {
@ -295,7 +612,7 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: #333; color: #333;
transition: background-color 0.3s; transition: background-color 0.2s;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -315,11 +632,10 @@ onMounted(() => {
} }
.item-name { .item-name {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1; flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.check-icon { .check-icon {
@ -329,22 +645,19 @@ onMounted(() => {
} }
} }
.dropdown-divider { .divider {
width: 1px; width: 1px;
background-color: #e8e8e8; background-color: #e8e8e8;
height: 100%; height: 100%;
} }
.dropdown-right-detail { .detail-container {
width: 250px; width: 250px;
height: 100%;
padding: 16px; padding: 16px;
box-sizing: border-box;
background-color: #fff;
overflow-y: auto; overflow-y: auto;
background: #fff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
} }
.detail-content { .detail-content {
@ -357,7 +670,6 @@ onMounted(() => {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-bottom: 4px;
} }
.detail-alias { .detail-alias {
@ -365,8 +677,10 @@ onMounted(() => {
color: #666; color: #666;
line-height: 1.5; line-height: 1.5;
word-break: break-all; word-break: break-all;
span {
color: red; :deep(span) {
color: #ff4d4f;
font-weight: bold;
} }
} }
@ -374,16 +688,25 @@ onMounted(() => {
color: #999; color: #999;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
position: absolute; margin-top: 50%;
left: 50%; transform: translateY(-50%);
top: 50%;
transform: translate(-50%, -50%);
} }
.empty-tip { .empty-tip {
padding: 10px; padding: 20px 0;
color: #999; color: #999;
text-align: center; text-align: center;
font-size: 12px; 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> </style>

View File

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

View File

@ -84,18 +84,87 @@ export function parseTime(time :any, cFormat :any) {
}) })
return time_str return time_str
} }
export function downloadFile(obj :any, name :any, suffix :any) { export function downloadFile(obj: any, name: any, suffix: any) {
const url = window.URL.createObjectURL(new Blob([obj])) try {
const link = document.createElement('a') const url = window.URL.createObjectURL(new Blob([obj]));
link.style.display = 'none' const link = document.createElement('a');
link.href = url link.style.display = 'none';
const fileName = parseTime(new Date(),'') + '-' + name + '.' + suffix link.href = url;
link.setAttribute('download', fileName)
document.body.appendChild(link) // 优化文件名生成逻辑,避免多余的中横线
link.click() const timeStamp = parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') || '';
document.body.removeChild(link) 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 }); const modules = import.meta.glob('@/assets/legend/*.svg', { eager: true });
// 图例图标映射 // 图例图标映射
export const iconMap: Record<string, string> = {}; export const iconMap: Record<string, string> = {};

View File

@ -258,7 +258,7 @@
上传视频 (MP4) 上传视频 (MP4)
</a-button> </a-button>
</a-upload> </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-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -307,7 +307,9 @@ const getBaseDropdownSelect = async () => {
try { try {
baseLoading.value = true; baseLoading.value = true;
const res = await getBaseDropdown({}); const res = await getBaseDropdown({});
baseOption.value = res.data; let list = res.data || [];
if (list.length > 0) list.shift();
baseOption.value = list;
} catch (error) { } catch (error) {
console.error("获取流域列表失败:", error); console.error("获取流域列表失败:", error);
} finally { } finally {
@ -642,7 +644,6 @@ const handleOk = async () => {
// //
const isBodyLenValid = validateBodyLength(); const isBodyLenValid = validateBodyLength();
const isWeightValid = validateWeight(); const isWeightValid = validateWeight();
console.log(isBodyLenValid, isWeightValid);
if (!isBodyLenValid || !isWeightValid) { if (!isBodyLenValid || !isWeightValid) {
message.error("请检查体长或体重填写是否正确"); message.error("请检查体长或体重填写是否正确");
return; return;
@ -749,7 +750,6 @@ const handleOk = async () => {
vdpth: finalVideoPaths.join(","), vdpth: finalVideoPaths.join(","),
}; };
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss"); if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(submitValues);
// return; // return;
emit("ok", submitValues); emit("ok", submitValues);
} catch (error) { } catch (error) {

View File

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

View File

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

View File

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