Merge branch 'main' into dev-tw

This commit is contained in:
tangwei 2026-04-28 18:13:25 +08:00
commit 9a90569ec5
37 changed files with 4124 additions and 1342 deletions

View File

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

View File

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

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="qgc-buji-web" />
<meta name="keywords" content="qgc-buji-web" />
<title>水电水利建设项目全过程环境管理信息平台</title>
<title>水电水利建设项目全过程环境管理信息平台数据填报子系统</title>
</head>
<body>
<div id="app"></div>

View File

@ -8,27 +8,101 @@ export function getFishDraftPage(data:any) {
data
});
}
//新增目录
//新增过鱼数据
export function addFishDraft(queryParams:any) {
return request({
url: '/data/fishDraft/add',
url: '/data/fishDraft/saveDraft',
method: 'post',
data: queryParams
});
}
//修改目录
//修改过鱼数据
export function editFishDraft(queryParams:any) {
return request({
url: '/data/fishDraft/update',
url: '/data/fishDraft/updateDraft',
method: 'post',
data: queryParams
});
}
//删除
//删除 过鱼数据
export function delFishDraft(data:any) {
return request({
url: '/data/fishDraft/batchDelete',
url: '/data/fishDraft/batchRemoveDraft',
method: 'post',
data
});
}
}
//提交过鱼数据
export function submitFishDraft(data:any) {
return request({
url: '/data/fishDraft/submitDrafts',
method: 'post',
data
});
}
//审批过鱼数据
export function successFishDraft(data:any) {
return request({
url: '/data/fishDraft/batchApprove',
method: 'post',
data
});
}
//驳回过鱼数据
export function rejectFishDraft(data:any) {
return request({
url: '/data/fishDraft/reject',
method: 'post',
data
});
}
// 导入zip
export function importFishZip(data:FormData) {
return request({
url: '/data/fishDraft/importZip',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data' }
});
}
// 取消导入任务
export function cancelImportTask(data:any) {
return request({
url: '/data/fishDraft/cancelImport',
method: 'post',
data
});
}
//检测用户导入状态
export function checkImportStatus() {
return request({
url: '/data/fishDraft/checkImportStatus',
method: 'get'
});
}
// 查询用户导入导入结果
export function getLastImportResult() {
return request({
url: '/data/fishDraft/getLastImportResult',
method: 'get'
});
}
// 批量保存草稿
export function batchSaveDraft(data:any) {
return request({
url: '/data/fishDraft/batchSaveDraft',
method: 'post',
data
});
}
// 标记导入任务为成功
export function markImportTaskSuccess(data:any) {
return request({
url: '/data/importTask/markSuccess',
method: 'post',
data
});
}

View File

@ -0,0 +1,33 @@
import request from '@/utils/request';
// 基地下拉列表
export function getBaseDropdown(data:any) {
return request({
url: '/env/hydrobase/dropdown',
method: 'get',
data
});
}
//电站下拉列表
export function getEngInfoDropdown(params:any) {
return request({
url: '/env/engInfo/dropdown',
method: 'get',
params
});
}
//过鱼设施下拉列表
export function getFpssDropdown(params:any) {
return request({
url: '/env/fpss/dropdown',
method: 'get',
params
});
}
//鱼类名称下拉列表
export function getFishDictoryDropdown() {
return request({
url: '/env/fishDictory/listByName',
method: 'get'
});
}

View File

@ -0,0 +1,27 @@
import request from '@/utils/request';
export function queryPageList(queryParams:any){
return request({
url: '/data/approvalMain/queryPageList' ,
method: 'post',
data:queryParams
});
}
// 查询审批操作日志列表
export function getApprovalLogList(params: any) {
return request({
url: '/data/approvalLog/queryPageList',
method: 'post',
data: params
});
}
// 查询审批变更记录列表
export function getApprovalChangeLogList(params: any) {
return request({
url: '/data/approvalChangeLog/queryPageList',
method: 'post',
data: params
});
}

View File

@ -6,6 +6,7 @@
:rules="rules"
layout="inline"
class="basic-search-form"
@reset="handleReset"
@finish="handleFinish"
@values-change="handleValuesChange"
>
@ -18,14 +19,18 @@
:name="item.name"
style="width: 100%; margin-bottom: 0"
>
<!-- 1. 优先检查是否有具名插槽或者 type custom -->
<slot
v-if="$slots[item.name] || item.type === 'custom'"
:name="item.name"
:value="formData[item.name]"
:onChange="(val:any) => { formData[item.name] = val }"
:formModel="formData"
/>
<!-- 1. 优先检查是否有具名插槽或者 type custom -->
<slot
v-if="$slots[item.name] || item.type === 'custom'"
:name="item.name"
:value="formData[item.name]"
:onChange="(val:any) => {
formData[item.name] = val;
triggerManualValuesChange(item.name, val);
}"
:formModel="formData"
/>
<!-- 普通日期选择器 -->
<a-date-picker
v-else-if="item.type === 'DataPicker'"
@ -38,7 +43,9 @@
:allow-clear="item.fieldProps?.allowClear"
:presets="item.presets"
style="width: 100%"
@change="(val) => triggerManualValuesChange(item.name, val)"
/>
<!-- 日期范围选择器 -->
<a-range-picker
v-else-if="item.type === 'RangePicker'"
@ -51,6 +58,7 @@
:allow-clear="item.fieldProps?.allowClear"
:presets="item.presets"
style="width: 100%"
@change="(val) => triggerManualValuesChange(item.name, val)"
/>
<!-- 普通输入框 -->
@ -60,38 +68,50 @@
:placeholder="item.placeholder || '请输入'"
:allow-clear="item.fieldProps?.allowClear"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
/>
<!-- 电站下拉框 -->
<div class="flex gap-[10px]" v-else-if="item.type === 'waterStation'">
<a-form-item-rest>
<a-select
:value="formData.stcd?.dataDimensionData"
placeholder="请选择"
@change="dataDimensionDataChange"
style="width: 135px"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
<a-select
:value="formData.baseId"
placeholder="请选择"
@change="dataDimensionDataChange"
show-search
allow-clear
:loading="shuJuTianBaoStore.baseLoading"
:filter-option="filterOption"
style="width: 135px"
>
{{ opt.label }}
</a-select-option>
</a-select>
<a-select
:value="formData.stcd?.stcdId"
placeholder="请选择电站"
@change="stcdIdChange"
style="width: 135px"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
<a-select-option
v-for="opt in shuJuTianBaoStore.baseOption"
:key="opt.baseid"
:value="opt.baseid"
:label="opt.basename"
>
{{ opt.basename }}
</a-select-option>
</a-select>
<a-select
:value="formData.rstcd"
placeholder="请选择电站"
@change="stcdIdChange"
show-search
allow-clear
:loading="shuJuTianBaoStore.engLoading"
:filter-option="filterOption"
style="width: 135px"
>
{{ opt.label }}
</a-select-option>
</a-select>
<a-select-option
v-for="opt in shuJuTianBaoStore.engOption"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.ennm"
>
{{ opt.ennm }}
</a-select-option>
</a-select>
</a-form-item-rest>
</div>
@ -102,27 +122,28 @@
:placeholder="item.placeholder || '请选择'"
:allow-clear="item.fieldProps?.allowClear"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(val) => triggerManualValuesChange(item.name, val)"
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
:key="opt[item.values?.value] || opt.value || opt.itemCode"
:value="opt[item.values?.value] || opt.value || opt.itemCode"
:label="opt[item.values?.name] || opt.label || opt.dictName"
>
{{ opt.label }}
{{ opt[item.values?.name] || opt.label || opt.dictName }}
</a-select-option>
</a-select>
<!-- 单选 -->
<!-- 单选-->
<a-radio-group
v-else-if="item.type === 'Radio'"
v-model:value="formData[item.name]"
:style="{ width: item.width ? item.width + 'px' : '200px' }"
@change="(e) => triggerManualValuesChange(item.name, e.target.value)"
>
<a-radio
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
>
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-radio>
</a-radio-group>
@ -149,7 +170,9 @@
</template>
<script lang="ts" setup>
import { ref, computed, reactive, watch, onMounted } from "vue";
import { ref, computed, reactive, watch, onMounted, nextTick } from "vue";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- ---
export interface SearchItem {
@ -163,7 +186,13 @@ export interface SearchItem {
xlSpan?: number;
width?: number;
presets?: any[];
options?: { label: string; value: any }[];
values?: any;
options?: {
itemCode?: string;
dictName?: string;
label: string;
value: any;
}[];
component?: any;
}
@ -190,23 +219,32 @@ const formRef = ref<any>();
const formData = reactive<any>({});
const rules = reactive<Record<string, any>>({});
const filterOption = (inputValue: string, option: any) => {
if (!option.label) return false;
return option.label.indexOf(inputValue) !== -1;
};
// 2. false/null/undefined
const validSearchList = computed(() => {
return props.searchList.filter(item => item);
return props.searchList.filter((item) => item);
});
// --- ---
const initForm = () => {
const initial = JSON.parse(JSON.stringify(props.initialValues || {}));
// 1. formData
Object.keys(formData).forEach((key) => delete formData[key]);
// 2.
Object.assign(formData, initial);
// 3. searchList false/null/undefined
validSearchList.value.forEach((item) => {
if (item.type == "waterStation") {
//
shuJuTianBaoStore.getBaseOption();
shuJuTianBaoStore.getEngOption(formData.baseId);
}
if (item.fieldProps?.required) {
rules[item.name] = [
{ required: true, message: `${item.label}不能为空`, trigger: "blur" },
@ -214,15 +252,35 @@ const initForm = () => {
}
});
};
/**
* 手动触发 valuesChange 事件
* 用于处理那些没有被 a-form 直接管理的字段 waterStation 内部逻辑
* 或者作为标准控件的备份触发机制
*/
const triggerManualValuesChange = (changedKey: string, newValue: any) => {
//
const changedValues = { [changedKey]: newValue };
//
// 使 {...formData}
emit("valuesChange", changedValues, { ...formData });
};
const dataDimensionDataChange = (value: any) => {
formData.stcd.dataDimensionData = value;
formData.baseId = value;
formData.rstcd = "";
shuJuTianBaoStore.getEngOption(formData.baseId);
// valuesChange a-form-item-rest
triggerManualValuesChange("baseId", formData.baseId);
};
// const hbrvcdChange = (value: any) => {
// formData.stcd.hbrvcd = value;
// };
const stcdIdChange = (value: any) => {
formData.stcd.stcdId = value;
formData.rstcd = value;
// valuesChange
triggerManualValuesChange("rstcd", formData.rstcd);
};
onMounted(() => {
initForm();
});
@ -240,7 +298,8 @@ watch(
// --- ---
const handleFinish = (values: any) => {
emit("finish", values);
const finalValues = { ...formData, ...values };
emit("finish", finalValues);
};
const handleValuesChange = (changedValues: any, allValues: any) => {
@ -250,12 +309,16 @@ const handleValuesChange = (changedValues: any, allValues: any) => {
const handleReset = () => {
if (formRef.value) {
formRef.value.resetFields();
nextTick(() => {
initForm();
});
emit("reset");
}
};
defineExpose({
form: formRef,
formData,
reset: handleReset,
submit: () => formRef.value?.submit(),
});

View File

@ -33,6 +33,8 @@ interface Props {
searchParams?: Record<string, any>;
//
defaultPageSize?: number;
getCheckboxProps?: (record: any) => any;
transformData?: (res: any) => { records: any[]; total: number };
}
const props = withDefaults(defineProps<Props>(), {
@ -40,6 +42,8 @@ const props = withDefaults(defineProps<Props>(), {
rowKey: "id",
searchParams: () => ({}),
defaultPageSize: 20,
getCheckboxProps: undefined,
transformData: undefined,
});
const emit = defineEmits<{
@ -66,9 +70,7 @@ const rowSelection = computed(() => ({
selectedRows.value = rows;
emit("selection-change", keys, rows);
},
getCheckboxProps: (record: any) => ({
disabled: record.disabled, //
}),
getCheckboxProps: props.getCheckboxProps ? props.getCheckboxProps : (record: any) => ({})
}));
// --- Pagination Config ---
@ -88,33 +90,42 @@ const paginationConfig = computed(() => ({
* 获取列表数据
* @param extraParams 额外的临时参数可选
*/
const getList = async (extraParams?: Record<string, any>) => {
const getList = async (filter?: Record<string, any>) => {
loading.value = true;
try {
//
const params = {
...props.searchParams,
...extraParams,
// ...props.searchParams,
skip: page.value,
take: size.value,
filter: {}
filter: filter,
// skip/take
// skip: (page.value - 1) * size.value,
// take: size.value,
};
const res = await props.listUrl(params);
// { data: { records: [], total: 0 } }
//
const records = res?.data?.records || res?.data?.list || res?.data || [];
const totalCount = res?.data?.total || res?.total || 0;
let records: any[] = [];
let totalCount: number = 0;
// [!code ++] transformData使
if (props.transformData) {
const result = props.transformData(res);
records = result.records || [];
totalCount = result.total || 0;
} else {
// [!code ++] 使
records = res?.data?.records || res?.data || [];
totalCount = res?.data?.total || res?.total || 0;
}
tableData.value = records;
total.value = totalCount;
//
emit("data-loaded", params, { records, total: totalCount });
} catch (error) {
console.error("Fetch table data error:", error);
tableData.value = [];
@ -177,10 +188,9 @@ watch(
// --- Lifecycle ---
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 如有必要,添加少量样式 */
</style>
</style>

View File

@ -1,24 +1,28 @@
<template>
<a-select
ref="selectRef"
:style="{ width: width }"
:value="modelValue"
:options="options"
:loading="loading"
@change="handleChange"
placeholder="请选择鱼名称"
mode="multiple"
@search="handleSearch"
placeholder="请选择鱼种类"
:mode="multiple ? 'multiple' : undefined"
show-search
:filter-option="filterOption"
:filter-option="false"
class="custom-fish-select"
:dropdownMatchSelectWidth="false"
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
@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 }">
<a-tag
closable
@close="onClose"
style="margin-right: 3px; max-width: 120px"
>
<!-- 自定义 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>
@ -32,7 +36,7 @@
:key="opt.id"
class="dropdown-item"
:class="{
'is-active': Array.isArray(modelValue) && modelValue.includes(opt.id),
'is-active': isSelected(opt.id),
'is-hovered': opt.id === hoveredId,
}"
@click.stop="handleSelectOption(opt)"
@ -40,11 +44,9 @@
>
<span class="item-name">{{ opt.name }}</span>
<!-- 选中对勾 -->
<span v-if="Array.isArray(modelValue) && modelValue.includes(opt.id)" class="check-icon"></span>
</div>
<div v-if="filteredOptions.length === 0" class="empty-tip">
无匹配数据
<span v-if="isSelected(opt.id)" class="check-icon"></span>
</div>
<div v-if="filteredOptions.length === 0" class="empty-tip">无匹配数据</div>
</div>
<!-- 中间分割线 -->
@ -66,30 +68,44 @@
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { ref, onMounted, computed, watch } from "vue";
import { getFishDictoryDropdown } from "@/api/select";
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
const shuJuTianBaoStore = useShuJuTianBaoStore();
// --- Props & Emits ---
interface Props {
modelValue: string[]; // ID
options: any[];
width: string;
modelValue: string | string[]; // ()()
width?: string;
multiple?: boolean; //
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
multiple: false, //
});
const emit = defineEmits<{
(e: "update:modelValue", value: string[]): void;
(e: "update:modelValue", value: string | string[], opt: any): void;
}>();
// --- State ---
// API
const options = ref<any>(props.options || []);
const loading = ref(false);
const options = ref<any[]>([]);
const searchKeyword = ref<string>("");
const hoveredId = ref<string | null>(null);
const open = ref(false); //
// --- Computed ---
const filteredOptions = computed(() => {
return options.value;
if (!searchKeyword.value) {
return options.value;
}
const lowerKeyword = searchKeyword.value.toLowerCase();
return options.value.filter((item: any) => {
const nameMatch = item.name?.toLowerCase().includes(lowerKeyword);
const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword);
return nameMatch || aliasMatch;
});
});
const currentDetailData = computed(() => {
@ -100,40 +116,60 @@ const currentDetailData = computed(() => {
});
// --- Methods ---
const handleDropdownVisibleChange = (open: boolean) => {
if (!open) {
hoveredId.value = null;
//
const isSelected = (id: string) => {
if (props.multiple) {
return Array.isArray(props.modelValue) && props.modelValue.includes(id);
} else {
return props.modelValue === id;
}
};
const filterOption = (input: string, option: any) => {
if (!input) return true;
const targetOpt = options.value.find((item: any) => item.id === option.value);
if (!targetOpt) return false;
const handleSearch = (value: string) => {
searchKeyword.value = value;
};
const lowerInput = input.toLowerCase();
const nameMatch = targetOpt.name?.toLowerCase().includes(lowerInput);
const aliasMatch = targetOpt.alias?.toLowerCase().includes(lowerInput);
return nameMatch || aliasMatch;
const handleDropdownVisibleChange = (val: boolean) => {
open.value = val;
if (!val) {
hoveredId.value = null;
searchKeyword.value = "";
}
};
const handleSelectOption = (opt: any) => {
let newValues: string[] = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
const index = newValues.indexOf(opt.id);
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);
if (index > -1) {
newValues.splice(index, 1); //
} else {
newValues.push(opt.id); //
}
emit("update:modelValue", newValues, opt);
} else {
newValues.push(opt.id);
// --- ---
// ID
//
if (props.modelValue === opt.id) {
emit("update:modelValue", "", opt); //
} else {
emit("update:modelValue", opt.id, opt); //
//
open.value = false;
}
}
//
emit("update:modelValue", newValues);
};
const handleChange = (val: any) => {
// a-select tag
emit("update:modelValue", val);
// a-select change Tag
//
emit("update:modelValue", val, '');
};
const getFishNameById = (id: string) => {
@ -141,9 +177,46 @@ const getFishNameById = (id: string) => {
const fish = options.value.find((item: any) => item.id === id);
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 = () => {
let data = shuJuTianBaoStore.getFishOption();
if (data.length === 0) {
loading.value = true;
getFishDictoryDropdown()
.then((res) => {
options.value = res.data || [];
loading.value = false;
shuJuTianBaoStore.setFishOption(options.value);
})
.catch(() => {
loading.value = false;
});
} else {
options.value = data;
}
};
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.custom-fish-select {
:deep(.ant-select-dropdown) {
padding: 0 !important;
@ -157,7 +230,7 @@ const getFishNameById = (id: string) => {
background: #fff;
border-radius: 4px;
overflow: hidden;
height: 300px; /* 固定高度 */
height: 300px;
}
.dropdown-left-list {
@ -165,7 +238,7 @@ const getFishNameById = (id: string) => {
height: 100%;
overflow-y: auto;
border-right: 1px solid #f0f0f0;
&::-webkit-scrollbar {
width: 6px;
}
@ -206,7 +279,7 @@ const getFishNameById = (id: string) => {
text-overflow: ellipsis;
flex: 1;
}
.check-icon {
color: #1890ff;
font-weight: bold;
@ -268,4 +341,4 @@ const getFishNameById = (id: string) => {
text-align: center;
font-size: 12px;
}
</style>
</style>

View File

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

View File

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

View File

@ -7,6 +7,7 @@ export default {
// 登录页面国际化
login: {
title: '水电水利建设项目全过程环境管理信息平台',
titleSjtb: '水电水利建设项目全过程环境管理信息平台数据填报子系统',
username: '用户名',
rulesUsername: '用户账号/身份证号/手机号 不能为空',
password: '密码',

View File

@ -54,7 +54,9 @@ onBeforeUnmount(() => {
href="/"
class="h-[50px] min-w-[350px] flex items-center justify-center text-white"
>
<h1 class="text-blank font-bold text-[16px]">{{ t("login.title") }}</h1></a
<!-- <h1 class="text-blank font-bold text-[16px]">{{ t("login.title") }}</h1> -->
<h1 class="text-blank font-bold text-[16px]">{{ t("login.titleSjtb") }}</h1>
</a
>
</transition>
<Sidebar />

View File

@ -0,0 +1,343 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="地表水水质达标率">
<div class="body_item">
<div class="tabs_all">
<div :class="tabs == 1 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(1)">自建水质站</div>
<div :class="tabs == 2 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(2)">国家水质站</div>
</div>
<div v-show="tabs == 1 || tabs == 2" class="tabs_body">
<div ref="chartRef" class="chart-container"></div>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 便
defineOptions({
name: 'EnvironmentalQuality'
});
const tabs = ref(1);
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// - 1
const selfBuiltData = [
{ name: '雅砻江\n干流', current: 99, lastYear: 100 },
{ name: '大渡河\n干流', current: 99, lastYear: 100 },
{ name: '黄河上游\n干流', current: 99, lastYear: 100 }
];
// - 2
const nationalData = [
{ name: '长江\n干流', current: 98, lastYear: 99 },
{ name: '珠江\n干流', current: 97, lastYear: 98 },
{ name: '淮河\n干流', current: 96, lastYear: 97 }
];
// tab
const getCurrentData = () => {
return tabs.value === 1 ? selfBuiltData : nationalData;
};
// tab
const handleTabChange = (tab: number) => {
tabs.value = tab;
};
// tabs
watch(tabs, (newVal) => {
if (newVal === 1 || newVal === 2) {
nextTick(() => {
setTimeout(() => {
if (!chartInstance) {
initChart();
} else {
//
updateChartData();
}
}, 50);
});
}
});
//
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
const currentData = getCurrentData();
const option = {
//
legend: {
top: 10,
itemWidth: 18,
itemHeight: 12,
itemGap: 20,
icon: 'roundRect',
data: [
{ name: '2026-04 月度', itemStyle: { color: '#4A8BC2' } },
{ name: '去年同期', itemStyle: { color: '#9B59B6' } }
],
textStyle: {
fontSize: 12,
color: '#333'
}
},
//
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(200, 200, 200, 0.2)'
},
label: {
show: false
}
},
backgroundColor: '#fff',
borderColor: '#e8e8e8',
borderWidth: 1,
padding: 12,
textStyle: {
color: '#333',
fontSize: 12
},
formatter: (params: any) => {
const name = params[0].name.replace('\n', '');
let result = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`;
params.forEach((item: any) => {
result += `<div style="display: flex; align-items: center; margin-bottom: 4px;">
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ${item.color}; margin-right: 8px;"></span>
<span>${item.seriesName}:</span>
<span style="margin-left: auto; font-weight: bold;">${item.value} %</span>
</div>`;
});
return result;
}
},
//
grid: {
left: 60,
right: 10,
top: 60,
bottom: 50
},
// X
xAxis: {
type: 'value',
min: 0,
max: 100,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
lineStyle: {
color: '#e8e8e8',
type: 'solid'
}
},
axisLabel: {
fontSize: 12,
color: '#666'
}
},
// Y
yAxis: {
type: 'category',
data: currentData.map(item => item.name),
inverse: true,
axisLine: {
show: true,
lineStyle: {
color: '#333',
width: 1
}
},
axisTick: {
show: true,
length: 4,
lineStyle: {
color: '#333',
width: 1
}
},
axisLabel: {
fontSize: 12,
color: '#333',
margin: 12
},
// Y"(%)"
name: '达标率(%)',
nameLocation: 'end',
nameGap: 20,
nameTextStyle: {
fontSize: 12,
color: '#666',
align: 'left'
}
},
//
series: [
{
name: '2026-04 月度',
type: 'bar',
data: currentData.map(item => item.current),
barWidth: 10,
barGap: '20%',
itemStyle: {
color: '#4A8BC2',
borderRadius: [0, 3, 3, 0]
},
markArea: {
silent: true,
itemStyle: {
color: 'rgba(240, 245, 250, 0.5)'
},
data: [
[{ yAxis: 1 }, { yAxis: 2 }]
]
}
},
{
name: '去年同期',
type: 'bar',
data: currentData.map(item => item.lastYear),
barWidth: 10,
barGap: '20%',
itemStyle: {
color: '#9B59B6',
borderRadius: [0, 3, 3, 0]
}
}
]
};
chartInstance.setOption(option);
};
//
const updateChartData = () => {
if (!chartInstance) return;
const currentData = getCurrentData();
chartInstance.setOption({
yAxis: {
data: currentData.map(item => item.name)
},
series: [
{
data: currentData.map(item => item.current)
},
{
data: currentData.map(item => item.lastYear)
}
]
});
//
chartInstance.resize();
};
//
onMounted(() => {
//
setTimeout(() => {
initChart();
}, 100);
// resize
window.addEventListener('resize', () => {
chartInstance?.resize();
});
});
//
onUnmounted(() => {
chartInstance?.dispose();
window.removeEventListener('resize', () => {
chartInstance?.resize();
});
});
</script>
<style lang="scss" scoped>
.body_item {
width: 100%;
height: 600px;
display: flex;
align-items: center;
justify-content: space-between;
.tabs_all {
width: 28px;
height: 600px;
box-sizing: border-box;
border: 2px solid #2f6b98;
border-radius: 5px;
.zhong_tabs {
width: 100%;
height: 50%;
background: #2f6b98;
color: #fff;
font-size: 14px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
text-shadow: 0 0 .25px currentcolor;
cursor: pointer;
}
.no_tabs {
width: 100%;
height: 50%;
font-size: 14px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
}
.no_tabs:hover {
color: #40a9ff;
}
}
.tabs_body {
width: 368px;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
.chart-container {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,124 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="栖息地保护工作开展情况">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div style="width: 60px;height: 62px;display: flex;align-items: center;justify-content: center;">
<div class="facility-icon">
<i style="color: #fff;" :class="facility.icon" type="icon-shengtailiuliang2"></i>
</div>
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 16px;"> <span class="facility-count">{{ facility.count
}}</span><span></span></div>
</div>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 便
defineOptions({
name: 'qixidibaohugongzuokaizhanQK'
});
//
const facilities = ref([
{
name: '栖息地',
count: 56,
icon: 'icon iconfont icon-qixidi'
},
{
name: '水温监测',
count: 1722,
icon: 'icon iconfont icon-diwenshuijianhuan'
},
{
name: '水文监测',
count: 135,
icon: 'icon iconfont icon-shuiwen-line'
},
{
name: '视频监控',
count: 135,
icon: 'icon iconfont icon-shipinjiankongshebei'
},
]);
//
onMounted(() => {
//
});
//
onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.facility-grid {
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
.facility-card {
width: 200px;
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
// margin-right: 8px;
background: #5389b5;
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
// margin-bottom: 4px;
// font-weight: 500;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
// font-weight: 600;
}
</style>

View File

@ -1,6 +1,6 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="环保设施情况">
<SidePanelItem title="水质监测工作开展情况">
<div class="facility-grid" >
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div style="width: 60px;height: 62px;display: flex;align-items: center;justify-content: center;">

View File

@ -9,7 +9,7 @@ NProgress.configure({ showSpinner: false });
const permissionStore = usePermissionStoreHook();
// 白名单路由
const whiteList = ['/login', '/login-sjtb']; //login
const whiteList = ['/login']; //login
// 查找第一个可用路由
function findFirstAvailableRoute(routes: any[]): string | undefined {
@ -35,7 +35,7 @@ router.beforeEach(async (to, from, next) => {
if (userStore.Token) {
// 登录成功,跳转到首页
if (to.path === '/login-sjtb' || to.path === '/login') {//login
if (to.path === '/login') {//login
next({ path: '/' });
NProgress.done();
} else {
@ -80,7 +80,7 @@ router.beforeEach(async (to, from, next) => {
} catch (error) {
console.log(error);
await userStore.resetToken();
to.path === '/login-sjtb' ? next(`/login-sjtb?redirect=${to.path}`) : next(`/login?redirect=${to.path}`);
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
@ -91,7 +91,7 @@ router.beforeEach(async (to, from, next) => {
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
to.path === '/login-sjtb' ? next(`/login-sjtb?redirect=${to.path}`) : next(`/login?redirect=${to.path}`);
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}

View File

@ -16,13 +16,13 @@ export const constantRoutes: RouteRecordRaw[] = [
}
]
},
// {
// path: '/login',
// component: () => import('@/views/login/index.vue'),
// meta: { hidden: true }
// },
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
},
{
path: '/login-sjtb',
component: () => import('@/views/login-sjtb/index.vue'),
meta: { hidden: true }
},
@ -31,11 +31,11 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/error-page/404.vue'),
meta: { hidden: true }
},
{
path: '/401',
component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true }
},
{
path: '/401',
component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true }
}
];
// 创建路由

View File

@ -0,0 +1,85 @@
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 定义状态,确保响应式
const fpssOption = ref<any[]>([]);
const fpssLoading = ref(false);
const baseOption = ref<any[]>([]);
const baseLoading = ref(false);
const engOption = ref<any[]>([]);
const engLoading = ref(false);
const fishOption = ref([]);
// 获取水电基地列表
const getBaseOption = async () => {
try {
baseLoading.value = true;
const res = await getBaseDropdown({});
if (res.data && Array.isArray(res.data)) {
const list = [...res.data];
list.unshift({
baseid: 'all',
basename: '当前全部'
});
// 直接赋值给 ref触发响应式更新
baseOption.value = list;
}
} catch (error) {
console.error('获取水电基地列表失败:', error);
} finally {
baseLoading.value = false;
}
};
// 获取电站列表
const getEngOption = async (baseId: string) => {
try {
engLoading.value = true;
const param = baseId === 'all' ? {} : { baseId };
const res = await getEngInfoDropdown(param);
if (res.data && Array.isArray(res.data)) {
// 直接赋值给 ref
engOption.value = res.data;
}
} catch (error) {
console.error('获取电站列表失败:', error);
} finally {
engLoading.value = false;
}
};
// 获取过鱼设施列表
const getFpssOption = async (baseId: string, rstcd: string) => {
try {
fpssLoading.value = true;
const res = await getFpssDropdown({ baseId, rstcd });
fpssOption.value = res.data;
} catch (error) {
console.log(error);
} finally {
fpssLoading.value = false;
}
};
const getFishOption = () => {
return fishOption.value;
};
const setFishOption = (data: any[]) => {
fishOption.value = data;
};
// 3. 直接返回 ref 和方法
// 在组件中使用时store.baseOption 会自动解包为数组
return {
fpssOption,
baseOption,
engOption,
fishOption,
fpssLoading,
baseLoading,
engLoading,
getBaseOption,
getEngOption,
getFpssOption,
getFishOption,
setFishOption
};
});

View File

@ -10,7 +10,7 @@
<!-- 左侧背景图区域 -->
<div class="left-section">
<div class="slogan">
<p>采集网站及数据管理子系统</p>
<p>{{ $t("login.titleSjtb") }}</p>
</div>
</div>
@ -168,6 +168,14 @@
>
忘记密码
</a-button> -->
<a-button
type="link"
size="mini"
block
:style="{ marginTop: '10px', border: 'none' }"
>
注册
</a-button>
<!-- 忘记密码 -->
</a-form>
@ -245,11 +253,10 @@ import loginImg from "@/assets/images/logo.png";
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
import { getCaptcha } from "@/api/auth";
import { message } from "ant-design-vue";
import { setPath } from '@/utils/auth';
import { setPath } from "@/utils/auth";
//
import router from "@/router";
import Cookies from "js-cookie";
// API
import { useRoute } from "vue-router";
import { LoginData } from "@/api/auth/types";
@ -288,8 +295,8 @@ const state = reactive({
redirect: "",
loginData: {
uuid: "",
username: "admin",
password: "123456",
username: "",
password: "",
code: "",
} as LoginData,
loginRules: {
@ -322,11 +329,12 @@ const forgotPasswordRules = ref({
],
});
const { loginData, loginRules, loading,
// passwordType, capslockTooltipDisabled
} = toRefs(
state
);
const {
loginData,
loginRules,
loading,
// passwordType, capslockTooltipDisabled
} = toRefs(state);
// function checkCapslock(e: any) {
// const { key } = e;
@ -363,10 +371,10 @@ function onFinish() {
userStore
.login(user)
.then(() => {
Cookies.set("username", user.username);
setPath('/login-sjtb')
setPath("/login-sjtb");
router.push({ path: "/" });
state.loading = false;
message.success("登录成功");
})
.catch(() => {
getCode();
@ -396,22 +404,6 @@ function getOtherQuery(query: any) {
return acc;
}, {});
}
function getCookie() {
const username = Cookies.get("username");
let password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
rememberMe == "true" ? (remember.value = Boolean(rememberMe)) : false;
// cookie
state.cookiePass = password === undefined ? "" : password;
password = password === undefined ? state.loginData.password : password;
state.loginData = {
username: username === undefined ? state.loginData.username : username,
password: decrypt(password),
code: "",
uuid: "",
};
remember.value = rememberMe === undefined ? false : Boolean(rememberMe);
}
function getCode() {
getCaptcha().then((result: any) => {
codeUrl.value = result.data.img;
@ -437,7 +429,10 @@ const startCountdown = () => {
// const showForgotPasswordPage = () => {
// showForgotPassword.value = true;
// };
//
const goRegister = () => {
router.push({ path: "/register" });
};
//
const backToLogin = () => {
showForgotPassword.value = false;
@ -554,7 +549,6 @@ const handleResetPassword = async () => {
};
onMounted(() => {
getCookie();
getCode();
});
</script>
@ -624,8 +618,8 @@ onMounted(() => {
.slogan {
position: absolute;
top: 20%;
left: 18%;
width: 490px;
left: 16%;
width: 780px;
height: 112px;
color: #040504;
font-size: 40px;

View File

@ -288,8 +288,8 @@ const state = reactive({
redirect: "",
loginData: {
uuid: "",
username: "admin",
password: "123456",
username: "",
password: "",
code: "",
} as LoginData,
loginRules: {
@ -359,11 +359,9 @@ function onFinish() {
if (user.password !== state.cookiePass) {
user.password = encrypt(user.password);
}
console.log(user);
userStore
.login(user)
.then(() => {
Cookies.set("username", user.username);
router.push({ path: "/" });
setPath('/login')
state.loading = false;
@ -396,22 +394,6 @@ function getOtherQuery(query: any) {
return acc;
}, {});
}
function getCookie() {
const username = Cookies.get("username");
let password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
rememberMe == "true" ? (remember.value = Boolean(rememberMe)) : false;
// cookie
state.cookiePass = password === undefined ? "" : password;
password = password === undefined ? state.loginData.password : password;
state.loginData = {
username: username === undefined ? state.loginData.username : username,
password: decrypt(password),
code: "",
uuid: "",
};
remember.value = rememberMe === undefined ? false : Boolean(rememberMe);
}
function getCode() {
getCaptcha().then((result: any) => {
codeUrl.value = result.data.img;
@ -554,7 +536,6 @@ const handleResetPassword = async () => {
};
onMounted(() => {
getCookie();
getCode();
});
</script>

View File

@ -1,5 +1,25 @@
<script setup lang="ts">
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
import RightDrawer from "@/components/RightDrawer/index.vue";
import QiXiDiBaoHuGongZuoKaiZhan from "@/modules/qixidibaohugongzuokaizhanQK/index.vue"
// import QixidijchuXx from "@/modules/qixidijchuXx"
// import QiXiDiShuiWenBianHua from "@/modules/qixidishuiwenbianhua"
// import QiXiDiLiuLiangBianHua from "@/modules/qixidiliuliangbianhua"
</script>
<template>
<div>
<h2>栖息地</h2>
<div class="moduleContent">
<div class="leftContent">
<JidiSelectorMod />
</div>
<div class="rightContent">
<RightDrawer>
<QiXiDiBaoHuGongZuoKaiZhan />
<!-- <QiXiDiShuiWenBianHua /> -->
<!-- <QiXiDiLiuLiangBianHua /> -->
<!-- <QixidijchuXx /> -->
</RightDrawer>
</div>
</div>
</template>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,326 @@
<template>
<div class="register-container">
<div class="register-wrapper">
<!-- 左侧背景图区域 -->
<div class="left-section">
<div class="slogan">
<p>{{ $t("login.titleSjtb") }}</p>
</div>
</div>
<!-- 右侧注册表单区域 -->
<div class="right-section">
<a-tabs v-model:activeKey="activeTab" class="register-tabs">
<a-tab-pane key="register" tab="用户注册">
<a-form
:model="registerData"
:rules="registerRules"
layout="vertical"
class="form-container"
@finish="onRegister"
>
<!-- 登录账号 -->
<a-form-item name="username" label="登录账号">
<a-input
v-model:value="registerData.username"
placeholder="请输入登录账号4-20个字符"
:prefix="h(UserOutlined)"
/>
</a-form-item>
<!-- 真实姓名 -->
<a-form-item name="realName" label="真实姓名">
<a-input
v-model:value="registerData.realName"
placeholder="请输入真实姓名"
/>
</a-form-item>
<!-- 手机号 -->
<a-form-item name="phone" label="手机号">
<a-input
v-model:value="registerData.phone"
placeholder="请输入11位手机号"
:prefix="h(MobileOutlined)"
/>
</a-form-item>
<!-- 密码 -->
<a-form-item name="password" label="密码">
<a-input-password
v-model:value="registerData.password"
placeholder="请设置密码6-20个字符"
:prefix="h(LockOutlined)"
/>
</a-form-item>
<!-- 确认密码 -->
<a-form-item name="confirmPassword" label="确认密码">
<a-input-password
v-model:value="registerData.confirmPassword"
placeholder="请再次输入密码"
:prefix="h(LockOutlined)"
/>
</a-form-item>
<!-- 验证码 -->
<a-form-item name="code" label="验证码">
<a-row :gutter="8">
<a-col :span="16">
<a-input
v-model:value="registerData.code"
placeholder="请输入验证码"
/>
</a-col>
<a-col :span="8">
<img
v-if="captchaImg"
:src="captchaImg"
@click="refreshCaptcha"
style="cursor: pointer; width: 100%; height: 36px;"
/>
</a-col>
</a-row>
</a-form-item>
<!-- 注册按钮 -->
<a-button
type="primary"
size="large"
block
htmlType="submit"
:loading="loading"
>
<span>立即注册</span>
</a-button>
<!-- 返回登录 -->
<a-button
type="link"
size="small"
block
@click="backToLogin"
:style="{ marginTop: '10px' }"
>
已有账号返回登录
</a-button>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, h } from "vue";
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
import { getCaptcha,
// registerUser
} from "@/api/auth";
import { message } from "ant-design-vue";
import router from "@/router";
import { encrypt } from "@/utils/rsaEncrypt";
//
const registerData = reactive({
//
username: "",
realName: "",
phone: "",
password: "",
confirmPassword: "",
code: "",
uuid: "",
//
userType: 1,
status: 1,
regStatus: 0
});
//
const registerRules = {
//
username: [
{ required: true, message: "请输入登录账号", trigger: "blur" },
{ min: 4, max: 20, message: "账号长度4-20个字符", trigger: "blur" },
{ pattern: /^[a-zA-Z0-9_]+$/, message: "只能包含字母、数字和下划线", trigger: "blur" }
],
//
realName: [
{ required: true, message: "请输入真实姓名", trigger: "blur" },
{ min: 2, max: 20, message: "姓名长度2-20个字符", trigger: "blur" }
],
//
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: "blur" }
],
//
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, max: 20, message: "密码长度6-20个字符", trigger: "blur" }
],
//
confirmPassword: [
{ required: true, message: "请再次输入密码", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (value && value !== registerData.password) {
return Promise.reject("两次输入的密码不一致");
}
return Promise.resolve();
},
trigger: "blur"
}
],
//
code: [
{ required: true, message: "请输入验证码", trigger: "blur" }
]
};
const loading = ref(false);
const captchaImg = ref("");
const activeTab = ref("register");
//
const refreshCaptcha = async () => {
try {
const res = await getCaptcha();
registerData.uuid = res.data.verifyCodeKey;
captchaImg.value = res.data.verifyCodeImg; // base64
} catch (error) {
message.error("获取验证码失败");
}
};
//
const onRegister = async () => {
loading.value = true;
try {
//
const encryptedPassword = encrypt(registerData.password);
//
const registerParams = {
username: registerData.username,
realName: registerData.realName,
phone: registerData.phone,
password: encryptedPassword,
//
userType: 1,
status: 1,
regStatus: 0,
//
code: registerData.code,
uuid: registerData.uuid
};
//
// await registerUser(registerParams);
message.success("注册成功,等待管理员审核");
//
setTimeout(() => {
router.push({ path: "/login" });
}, 1500);
} catch (error: any) {
message.error(error.message || "注册失败,请重试");
//
refreshCaptcha();
} finally {
loading.value = false;
}
};
//
const backToLogin = () => {
router.push({ path: "/login" });
};
//
onMounted(() => {
refreshCaptcha();
});
</script>
<style scoped lang="scss">
.register-container {
margin: 0 auto;
position: relative;
width: 100%;
height: 100%;
min-width: 1500px;
background-color: #fff;
.register-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 600px;
background: url("@/assets/images/bg_sjtb.png");
background-repeat: no-repeat;
background-size: 100% 100%;
}
//
.left-section {
.slogan {
position: absolute;
top: 20%;
left: 18%;
width: 700px;
height: 112px;
color: #040504;
font-size: 40px;
}
}
//
.right-section {
position: absolute;
left: 70%;
top: 15%;
width: 25%;
// max-height: 650px;
// max-width: 400px;
min-height: 650px;
border-radius: 3px;
padding: 20px 24px 24px;
background-color: #fff;
overflow-y: auto;
:deep(.ant-form-item) {
margin-bottom: 14px;
}
:deep(.ant-form-item-label > label) {
font-size: 13px;
}
:deep(.ant-input-prefix) {
display: flex;
width: 26px;
svg {
width: 18px;
height: 18px;
margin-right: 4px;
}
}
}
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="approval-log-search">
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData"
@finish="onSearchFinish" @values-change="onValuesChange" @reset="handleReset">
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import BasicSearch from "@/components/BasicSearch/index.vue";
// --- Props & Emits ---
interface Props {
actionTypeDict: any[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
// initSearchData
const initSearchData = {
action: '',
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Select",
name: "action",
label: "操作类型",
fieldProps: {
allowClear: true,
},
options: props.actionTypeDict || [],
},
]);
// --- Methods ---
// 2.
const onSearchFinish = (values: any) => {
console.log(values);
emit("searchFinish", values);
};
const handleReset = () => {
emit("reset", initSearchData);
};
const onValuesChange = (changedValues: any, allValues: any) => {
// searchData便使
searchData.value = { ...searchData.value, ...allValues };
};
// --- Lifecycle ---
onMounted(() => {
emit("searchFinish", initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,66 @@
<template>
<div class="change-log-search">
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData"
@finish="onSearchFinish" @values-change="onValuesChange" @reset="handleReset">
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import BasicSearch from "@/components/BasicSearch/index.vue";
// --- Props & Emits ---
interface Props {
operationTypeDict: any[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
// initSearchData
const initSearchData = {
operationType: '',
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Select",
name: "operationType",
label: "操作类型",
fieldProps: {
allowClear: true,
},
options: props.operationTypeDict || [],
},
]);
// --- Methods ---
// 2.
const onSearchFinish = (values: any) => {
console.log(values);
emit("searchFinish", values);
};
const handleReset = () => {
emit("reset", initSearchData);
};
const onValuesChange = (changedValues: any, allValues: any) => {
// searchData便使
searchData.value = { ...searchData.value, ...allValues };
};
// --- Lifecycle ---
onMounted(() => {
emit("searchFinish", initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -1,546 +0,0 @@
<template>
<div class="guoYuSheShiShuJuTianBao-page">
<!-- 搜索区域组件具体 props 需根据实际子组件调整 -->
<GuoYuSheShiShuJuTianBaoSearch
:import-btn="importBtn"
:save-btn="saveBtn"
:handle-add="handleAdd"
:batchData="batchData"
:batchDel="batchDel"
@search-finish="handleSearchFinish"
/>
<!-- 主表格 -->
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="getFishDraftPage"
:search-params="{}"
:enable-row-selection="true"
@selection-change="handleSelectionChange"
>
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger size="small" @click="handleDelete([record.id])"
>删除</a-button
>
</div>
</template>
</template>
</BasicTable>
<!-- <BasicTable :columns="columns" :listUrl="getFishDraftPage" /> -->
<!-- 导入预览 Modal -->
<a-modal
title="导入数据预览"
ok-text="提交导入"
cancel-text="取消"
:width="1500"
v-model:open="visible"
:confirm-loading="fileLoading"
@cancel="handleModalCancel"
@ok="handleModalOk"
>
<a-table
size="small"
:loading="fileLoading"
:data-source="fileTableData"
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
row-key="index"
>
<!-- 如果需要复杂的行内编辑插槽可在此定义但目前逻辑主要在 column render 中处理 -->
</a-table>
</a-modal>
<!-- 新增/编辑 Modal (对应 React EditModal) -->
<!-- 假设已创建对应的 Vue 组件 GuoYuSheShiShuJuTianBaoForm -->
<EditModal
v-model:visible="editModalVisible"
:initial-values="currentRecord"
:loading="submitLoading"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/>
<!-- 视频预览 Modal -->
<a-modal
title="视频预览"
v-model:open="videoPreviewVisible"
:footer="null"
width="800px"
@cancel="closeVideoPreview"
>
<video
v-if="currentVideoUrl"
controls
autoplay
style="width: 100%"
:src="currentVideoUrl"
>
您的浏览器不支持视频播放
</video>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, h } from "vue";
import { message, Modal } from "ant-design-vue"; // 使 ant-design-vue
import JSZip from "jszip";
import * as XLSX from "xlsx";
import BasicTable from "@/components/BasicTable/index.vue";
import GuoYuSheShiShuJuTianBaoSearch from "./guoYuSheShiShuJuTianBaoSearch.vue";
import EditModal from "./guoYuSheShiShuJuTianBaoForm.vue";
import {
getFishDraftPage,
addFishDraft,
editFishDraft,
delFishDraft,
} from "@/api/guoYuSheShiShuJuTianBao";
import dayjs from "dayjs";
import { Tag } from 'ant-design-vue'; // Tag
// import { FileImageOutlined, VideoCameraOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
// --- ---
interface FormData {
[key: string]: any;
}
interface ColumnConfig {
dataIndex: string;
key: string;
title: string;
width?: number;
customRender?: (text: any, record: any) => any;
}
const tableRef = ref<any>(null);
// --- ---
const baseColumnsConfig: ColumnConfig[] = [
{ dataIndex: "engName", key: "engName", title: "水电基地", width: 100 },
{ dataIndex: "baseName", key: "baseName", title: "电站名称", width: 120 },
{ dataIndex: "fpname", key: "fpname", title: "过鱼设施名称", width: 150 },
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 150 },
{ dataIndex: "ftp", key: "ftp", title: "鱼种类", width: 120 },
{
dataIndex: "isfs",
key: "isfs",
title: "是否鱼苗",
width: 74,
customRender: ({ text }: any) => {
const isYes = text === 1 || text === '1';
return h(
Tag,
{
color: isYes ? 'success' : 'error', // Antdv Tag
style: { margin: 0 } // margin使
},
() => isYes ? '是' : '否'
);
},
},
{ dataIndex: "direction", key: "direction", title: "游向", width: 80 },
{ dataIndex: "fcnt", key: "fcnt", title: "过鱼数量(尾)", width: 120 },
{ dataIndex: "fsz", key: "fsz", title: "体长(cm)", width: 110 },
{ dataIndex: "fwet", key: "fwet", title: "平均体重(g)", width: 110 },
{ dataIndex: "wt", key: "wt", title: "水温(℃)", width: 80 },
{ dataIndex: "picpth", key: "level5", title: "图片", width: 100 },
{ dataIndex: "vdpth", key: "level6", title: "视频", width: 100 },
{ dataIndex: "tm", key: "tm", title: "填报时间", width: 150 },
{ dataIndex: "status", key: "status", title: "状态", width: 100 },
];
// --- ---
const visible = ref(false); // Modal
//
const editModalVisible = ref(false);
const currentRecord = ref<FormData | null>(null);
const submitLoading = ref(false);
//
const videoPreviewVisible = ref(false);
const currentVideoUrl = ref<string>("");
//
const tableData = ref<any[]>([]);
const fileTableData = ref<any[]>([]);
const batchData = ref<any[]>([]);
const fileLoading = ref(false);
// Key ()
const editingKey = ref<string | number>("");
// --- ---
// Zip Blob URL
// const getBlobUrlFromZip = async (zip: JSZip, fileName: string): Promise<string> => {
// try {
// const file = zip.file(fileName);
// if (!file) return "";
// const blob = await file.async("blob");
// return URL.createObjectURL(blob);
// } catch (e) {
// console.error("Extract file failed", e);
// return "";
// }
// };
// ( VNode Antdv columns render )
// Vue Antdv render (text, record, index)
// const createMediaRender = (type: "image" | "video") => {
// return (text: string) => {
// if (!text) return "-";
// // 使 h
// // click columns 使 slots h
// // UI Antdv customRender
// return type === "image" ? "" : "";
// };
// };
// --- Columns ---
// Columns
const columns = computed(() => {
return [
...baseColumnsConfig.map((col) => {
if (col.dataIndex === "level5") {
return {
...col,
customRender: ({ text }: any) => {
if (!text) return "-";
// Icon
return `<span style="color:#52c41a; cursor:pointer">查看图片</span>`;
},
};
}
if (col.dataIndex === "level6") {
return {
...col,
customRender: ({ text }: any) => {
if (!text) return "-";
return `<span style="color:#1890ff; cursor:pointer">播放视频</span>`;
},
};
}
return { ...col, visible: true };
}),
{
title: "操作",
key: "action",
dataIndex: "action",
fixed: "right",
width: 100,
align: "center",
},
];
});
// Columns ()
const modalColumns = computed(() => {
const isEditing = (_record: any, index: number) => index === editingKey.value;
// const save = async (index: number) => {
// // fileTableData input change
// editingKey.value = "";
// message.success("");
// };
// const deleteRow = (index: number) => {
// fileTableData.value = fileTableData.value.filter((_, i) => i !== index);
// message.success("");
// };
return baseColumnsConfig
.map((col) => ({
...col,
customRender: ({ text, record, index }: any) => {
const editing = isEditing(record, index);
//
if (col.dataIndex === "level5" || col.dataIndex === "level6") {
if (editing) {
// Input VNode slot h
return "Input编辑中";
}
return col.dataIndex === "level5" ? "查看图片" : "播放视频";
}
//
if (editing) {
// Input
return "Input编辑中";
}
return text;
},
// Antdv slots
slots: { customRender: `cell-${col.dataIndex}` },
}))
// .concat({
// title: "",
// dataIndex: "operation",
// fixed: "right",
// width: 140,
// align: "center",
// customRender: ({ record, index }: any) => {
// const editable = isEditing(record, index);
// return editable ? "/" : "/";
// },
// slots: { customRender: "cell-operation" },
// });
});
// --- ---
const handleAdd = () => {
currentRecord.value = null;
editModalVisible.value = true;
};
const handleEdit = (record: any) => {
currentRecord.value = { ...record };
editModalVisible.value = true;
};
//
const handleDelete = (ids: any[]) => {
console.log(ids)
Modal.confirm({
title: "是否确认删除选中数据吗?",
onOk: async () => {
let res: any = await delFishDraft(ids);
if (res && res?.code == 0) {
message.success("删除成功");
tableRef.value?.getList();
}
},
});
};
//
const batchDel = () => {
handleDelete(batchData.value);
};
//
const handleSelectionChange = (keys: any) => {
batchData.value = keys;
};
const editModalCancel = () => {
editModalVisible.value = false;
};
const handleEditSubmit = async (values: FormData) => {
submitLoading.value = true;
console.log(values);
//
// setTimeout(() => {
if (currentRecord.value) {
//
let res: any = await editFishDraft({
...values
});
if (res && res?.code == 0) {
message.success("编辑成功");
editModalVisible.value = false;
tableRef.value?.getList();
}
submitLoading.value = false;
// const newData = tableData.value.map((item) => {
// // ID
// if (JSON.stringify(item) === JSON.stringify(currentRecord.value)) {
// // ID
// return { ...item, ...values };
// }
// return item;
// });
// // key
// const targetIndex = tableData.value.findIndex(
// (item) => item.key === currentRecord.value?.key
// );
// if (targetIndex > -1) {
// tableData.value[targetIndex] = { ...tableData.value[targetIndex], ...values };
// }
} else {
//
let res: any = await addFishDraft({
...values,
tm: dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"),
stcd: 1,
});
if (res && res?.code == 0) {
message.success("新增成功");
editModalVisible.value = false;
tableRef.value?.getList();
}
submitLoading.value = false;
}
// }, 500);
};
const parseExcelFile = async (fileName: string, arrayBuffer: ArrayBuffer) => {
try {
const workbook = XLSX.read(arrayBuffer, {
type: "array",
cellDates: true, // JS Date
dateNF: "yyyy-mm-dd", //
});
const firstSheetName = workbook.SheetNames[0];
if (!firstSheetName) throw new Error("Excel文件中没有工作表");
const worksheet = workbook.Sheets[firstSheetName];
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet);
return jsonData;
} catch (error) {
console.error(`解析文件 ${fileName} 失败:`, error);
message.error(`文件 ${fileName} 解析失败`);
return [];
}
};
const handleModalOk = () => {
tableData.value = [...fileTableData.value];
visible.value = false;
message.success("数据已导入至列表");
};
const handleModalCancel = () => {
visible.value = false;
editingKey.value = "";
};
const importBtn = async (file: File) => {
fileLoading.value = true;
editingKey.value = "";
const hideMessage = message.loading("正在解析压缩包...", 0);
try {
const zip = await JSZip.loadAsync(file);
const zipPathMap: Record<string, string> = {};
//
zip.forEach((relativePath, zipEntry) => {
if (!zipEntry.dir) {
const lowerPath = relativePath.toLowerCase();
zipPathMap[lowerPath] = relativePath;
const pathParts = relativePath.split("/");
for (let i = 0; i < pathParts.length; i++) {
const subPath = pathParts.slice(i).join("/");
if (subPath) zipPathMap[subPath.toLowerCase()] = relativePath;
}
}
});
const fileNames = Object.keys(zip.files);
if (fileNames.length === 0) {
hideMessage();
message.warning("压缩包为空");
fileLoading.value = false;
return;
}
let allExcelData: any[] = [];
for (const fileName of fileNames) {
const zipEntry = zip.files[fileName];
if (zipEntry.dir) continue;
if (!fileName.match(/\.(xls|xlsx)$/i)) continue;
try {
const arrayBuffer = await zipEntry.async("arraybuffer");
const data = await parseExcelFile(fileName, arrayBuffer);
if (!data || data.length === 0) continue;
const transformedData = await Promise.all(
data.map(async (item: any) => {
const newObj: any = {};
for (const excelKey in item) {
if (!Object.prototype.hasOwnProperty.call(item, excelKey)) continue;
const value = item[excelKey];
//
const matchedCol = baseColumnsConfig.find(
(col) => excelKey.includes(col.title) || col.title.includes(excelKey)
);
if (matchedCol) {
let finalValue = value;
//
if (
(matchedCol.dataIndex === "level5" ||
matchedCol.dataIndex === "level6") &&
value &&
typeof value === "string"
) {
const trimPath = value.trim().replace(/\\/g, "/");
if (trimPath) {
const searchKey = trimPath.toLowerCase();
const realPath = zipPathMap[searchKey];
if (realPath) {
try {
const zipFile = zip.file(realPath);
if (zipFile) {
const blob = await zipFile.async("blob");
finalValue = URL.createObjectURL(blob);
}
} catch (e) {
console.error(`Failed to extract blob for: ${realPath}`, e);
}
}
}
}
newObj[matchedCol.dataIndex] = finalValue;
}
}
return newObj;
})
);
allExcelData = [...allExcelData, ...transformedData];
} catch (err) {
console.error(`读取文件 ${fileName} 失败`, err);
}
}
fileTableData.value = allExcelData;
visible.value = true;
hideMessage();
message.success(`解析完成,共获取 ${allExcelData.length} 条数据`);
} catch (error) {
hideMessage();
console.error("ZIP 解析失败:", error);
message.error("文件格式错误或解析失败");
} finally {
fileLoading.value = false;
}
};
const saveBtn = async () => {
// TODO:
console.log("Save button clicked");
};
const handleSearchFinish = (values: any) => {
console.log(values);
// const newSearchData = { ...searchData.value, ...e };
// searchData.value = newSearchData;
// getData(newSearchData, label);
};
const closeVideoPreview = () => {
videoPreviewVisible.value = false;
currentVideoUrl.value = "";
};
// --- ---
onMounted(() => {});
</script>
<style lang="scss" scoped>
.guoYuSheShiShuJuTianBao-page {
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 20px;
}
</style>

View File

@ -1,10 +1,11 @@
<template>
<a-modal
:title="isEdit ? '编辑数据' : '新增数据'"
:title="isView ? '查看数据' : isEdit ? '编辑数据' : '新增数据'"
v-model:open="modalVisible"
:confirm-loading="loading"
width="800px"
:destroy-on-close="true"
:footer="isView ? null : undefined"
@cancel="handleCancel"
@ok="handleOk"
>
@ -17,31 +18,86 @@
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="水电基地" name="engName">
<a-input v-model:value="formData.engName" placeholder="请输入水电基地" />
<a-form-item label="流域" name="baseId">
<a-select
v-model:value="formData.baseId"
:loading="baseLoading"
placeholder="请选择流域"
:disabled="isView"
show-search
allowClear
:filter-option="filterOption"
@change="baseChange"
>
<a-select-option
v-for="opt in baseOption"
:key="opt.baseid"
:value="opt.baseid"
:label="opt.basename"
>
{{ opt.basename }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="电站名称" name="baseName">
<a-input v-model:value="formData.baseName" placeholder="请输入电站名称" />
<a-form-item label="电站名称" name="rstcd">
<a-select
v-model:value="formData.rstcd"
:loading="engLoading"
placeholder="请选择电站名称"
:disabled="isView"
show-search
allowClear
:filter-option="filterOption"
@change="engChange"
>
<a-select-option
v-for="opt in engOption"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.ennm"
>
{{ opt.ennm }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="过鱼设施名称" name="fpname">
<a-input v-model:value="formData.fpname" placeholder="请输入过鱼设施名称" />
<a-form-item label="过鱼设施" name="stcd">
<a-select
v-model:value="formData.stcd"
:loading="fpssLoading"
placeholder="请选择过鱼设施"
:disabled="isView"
show-search
allowClear
:filter-option="filterOption"
>
<a-select-option
v-for="opt in fpssOption"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.stnm"
>
{{ opt.stnm }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="过鱼时间" name="strdt">
<a-date-picker
v-model:value="formData.strdt"
show-time
style="width: 100%"
format="YYYY-MM-DD"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -50,12 +106,12 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="鱼种类" name="ftp">
<a-input v-model:value="formData.ftp" placeholder="请输入鱼种类" />
<fishSearch v-model="formData.ftp" :disabled="isView" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="是否鱼苗" name="isfs">
<a-radio-group v-model:value="formData.isfs">
<a-radio-group v-model:value="formData.isfs" :disabled="isView">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
@ -70,11 +126,14 @@
v-model:value="formData.direction"
placeholder="请选择游向"
allow-clear
:disabled="isView"
>
<a-select-option value="上行">上行</a-select-option>
<a-select-option value="下行">下行</a-select-option>
<a-select-option value="上行折返">上行折返</a-select-option>
<a-select-option value="下行折返">下行折返</a-select-option>
<a-select-option
v-for="item in direction"
:key="item.itemCode"
:value="item.itemCode"
>{{ item.dictName }}</a-select-option
>
</a-select>
</a-form-item>
</a-col>
@ -85,6 +144,7 @@
style="width: 100%"
placeholder="数量"
:min="0"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -104,6 +164,7 @@
placeholder="请输入"
:min="0"
@change="validateBodyLength"
:disabled="isView"
/>
<span class="px-[10px]">~</span>
<a-input-number
@ -112,6 +173,7 @@
placeholder="请输入"
:min="0"
@change="validateBodyLength"
:disabled="isView"
/>
</div>
</a-form-item>
@ -129,6 +191,7 @@
placeholder="请输入"
:min="0"
@change="validateWeight"
:disabled="isView"
/>
<span class="px-[10px]">~</span>
<a-input-number
@ -137,6 +200,7 @@
placeholder="请输入"
:min="0"
@change="validateWeight"
:disabled="isView"
/>
</div>
</a-form-item>
@ -150,6 +214,7 @@
style="width: 100%"
placeholder="水温"
:min="0"
:disabled="isView"
/>
</a-form-item>
</a-col>
@ -172,22 +237,75 @@
<script lang="ts" setup>
import { ref, reactive, watch, computed } from "vue";
import dayjs from "dayjs";
import { message } from "ant-design-vue";
import type { Rule } from "ant-design-vue/es/form";
import fishSearch from "@/components/fishSearch/index.vue";
import { getBaseDropdown, getEngInfoDropdown, getFpssDropdown } from "@/api/select";
// Props
interface Props {
visible: boolean;
direction: any[];
initialValues?: any | null;
loading?: boolean;
isView?: boolean;
}
const baseLoading = ref(false);
const engLoading = ref(false);
const fpssLoading = ref(false);
const baseOption = ref<any[]>([]);
const engOption = ref<any[]>([]);
const fpssOption = ref<any[]>([]);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false,
});
// --- v-model ---
const getBaseDropdownSelect = async () => {
try {
baseLoading.value = true;
const res = await getBaseDropdown({});
baseOption.value = res.data;
} catch (error) {
console.error("获取流域列表失败:", error);
} finally {
baseLoading.value = false;
}
};
const baseChange = async (baseId: string) => {
formData.rstcd = undefined;
formData.stcd = undefined;
await getEngInfoDropdownSelect(baseId);
await getFpssDropdownSelect(formData.rstcd, baseId);
};
const getEngInfoDropdownSelect = async (baseId: string) => {
try {
engLoading.value = true;
const res = await getEngInfoDropdown({ baseId });
engOption.value = res.data;
} catch (error) {
console.error("获取电站列表失败", error);
} finally {
engLoading.value = false;
}
};
const engChange = async (rstcd: string) => {
formData.stcd = undefined;
await getFpssDropdownSelect(rstcd, formData.baseId);
};
const getFpssDropdownSelect = async (rstcd: string, baseId: string) => {
try {
fpssLoading.value = true;
const res = await getFpssDropdown({ rstcd, baseId });
fpssOption.value = res.data;
} catch (error) {
console.error("获取流量列表失败", error);
} finally {
fpssLoading.value = false;
}
};
const modalVisible = computed({
get: () => props.visible,
set: (val) => emit("update:visible", val),
@ -208,10 +326,9 @@ const weightError = ref<string>("");
//
const defaultFormData = reactive({
id: undefined,
engName: undefined,
baseName: undefined,
fpname: undefined,
baseId: undefined,
stcd: undefined,
rstcd: undefined,
strdt: undefined,
ftp: undefined,
isfs: 0,
@ -228,42 +345,19 @@ const defaultFormData = reactive({
weightMax: undefined,
});
const formData: any = reactive({ ...defaultFormData });
//
// const validateBodyLengthRange = (rule: any, value: any) => {
// const min = formData.bodyLengthMin;
// const max = formData.bodyLengthMax;
// //
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
// if (Number(min) >= Number(max)) {
// return Promise.reject("");
// }
// }
// return Promise.resolve();
// };
// const validateWeightRange = (rule: any, value: any) => {
// const min = formData.weightMin;
// const max = formData.weightMax;
// //
// if (min !== undefined && min !== null && max !== undefined && max !== null) {
// if (Number(min) >= Number(max)) {
// return Promise.reject("");
// }
// }
// };
const filterOption = (inputValue: string, option: any) => {
if (!option.label) return false;
return option.label.indexOf(inputValue) !== -1;
};
//
const rules: Record<string, Rule[]> = {
// engName: [{ required: true, message: "", trigger: "blur" }],
// baseName: [{ required: true, message: "", trigger: "blur" }],
// fpname: [{ required: true, message: "", trigger: "blur" }],
// strdt: [{ required: true, message: "", trigger: "change" }],
// //
// bodyLengthMin: [{ validator: validateBodyLengthRange, trigger: "change" }],
// bodyLengthMax: [{ validator: validateBodyLengthRange, trigger: "change" }],
// weightMin: [{ validator: validateWeightRange, trigger: "change" }],
// weightMax: [{ validator: validateWeightRange, trigger: "change" }],
baseId: [{ required: true, message: "请选择流域", trigger: "change" }],
rstcd: [{ required: true, message: "请选择电站", trigger: "change" }],
stcd: [{ required: true, message: "请选择过鱼设施", trigger: "change" }],
strdt: [{ required: true, message: "请选择过鱼时间", trigger: "change" }],
ftp: [{ required: true, message: "请选择鱼种类", trigger: "change" }],
direction: [{ required: true, message: "请选择游向", trigger: "change" }],
fcnt: [{ required: true, message: "请输入过鱼数量", trigger: "change" }],
};
//
@ -276,14 +370,25 @@ const validateBodyLength = () => {
//
bodyLengthError.value = "";
//
if (min !== undefined && min !== null && max !== undefined && max !== null) {
if (Number(min) >= Number(max)) {
bodyLengthError.value = "最小体长必须小于最大体长";
// handleOk
// ( undefined, null, )
const hasMin = min !== undefined && min !== null && min !== "";
const hasMax = max !== undefined && max !== null && max !== "";
// 1.
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
bodyLengthError.value = "最小体长和最大体长均需填写";
return false;
}
// 2.
if (hasMin && hasMax) {
// min > max
if (Number(min) > Number(max)) {
bodyLengthError.value = "最小体长不能大于最大体长";
return false;
}
}
return true;
};
@ -294,12 +399,25 @@ const validateWeight = () => {
//
weightError.value = "";
if (min !== undefined && min !== null && max !== undefined && max !== null) {
if (Number(min) >= Number(max)) {
weightError.value = "最小体重必须小于最大体重";
//
const hasMin = min !== undefined && min !== null && min !== "";
const hasMax = max !== undefined && max !== null && max !== "";
// 1.
if ((hasMin && !hasMax) || (!hasMin && hasMax)) {
weightError.value = "最小体重和最大体重均需填写";
return false;
}
// 2.
if (hasMin && hasMax) {
// min > max
if (Number(min) > Number(max)) {
weightError.value = "最小体重不能大于最大体重";
return false;
}
}
return true;
};
@ -308,12 +426,15 @@ const initForm = () => {
if (props.initialValues) {
// --- ---
const values = props.initialValues;
//
if (values.fwet) {
const weights = values.fwet.split("~");
formData.weightMin = weights[0];
formData.weightMax = weights[1];
if (weights.length === 2) {
formData.weightMin = weights[0];
formData.weightMax = weights[1];
} else {
formData.weightMin = weights[0];
formData.weightMax = weights[0];
}
} else {
formData.weightMin = undefined;
formData.weightMax = undefined;
@ -321,8 +442,13 @@ const initForm = () => {
if (values.fsz) {
const sizes = values.fsz.split("~");
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[1];
if (sizes.length === 2) {
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[1];
} else {
formData.bodyLengthMin = sizes[0];
formData.bodyLengthMax = sizes[0];
}
} else {
formData.bodyLengthMin = undefined;
formData.bodyLengthMax = undefined;
@ -345,32 +471,24 @@ const initForm = () => {
}
});
} else {
// --- ---
resetForm();
}
};
// 2. watch visible
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
//
getBaseDropdownSelect();
getEngInfoDropdownSelect(formData.baseId);
getFpssDropdownSelect(formData.rstcd, formData.baseId);
initForm();
} else {
//
// initialValues
}
},
{ immediate: false } // immediate false
);
// 3. watch
//
// watch(
// () => [props.visible, props.initialValues],
// ...
// );
//
const resetForm = () => {
if (formRef.value) {
@ -378,8 +496,8 @@ const resetForm = () => {
}
Object.assign(formData, defaultFormData);
//
bodyLengthError.value = '';
weightError.value = '';
bodyLengthError.value = "";
weightError.value = "";
};
//
@ -402,13 +520,41 @@ const handleOk = async () => {
}
//
await formRef.value.validate();
let fwet = "";
if (
formData.weightMin == formData.weightMax &&
formData.weightMin != undefined &&
formData.weightMax != undefined
) {
fwet = formData.weightMin;
} else if (formData.weightMin == undefined && formData.weightMax == undefined) {
fwet = "-";
} else {
fwet = formData.weightMin + "~" + formData.weightMax;
}
let fsz = "";
if (
formData.bodyLengthMin == formData.bodyLengthMax &&
formData.bodyLengthMin != undefined &&
formData.bodyLengthMax != undefined
) {
fsz = formData.bodyLengthMin;
} else if (
formData.bodyLengthMin == undefined &&
formData.bodyLengthMax == undefined
) {
fsz = "-";
} else {
fsz = formData.bodyLengthMin + "~" + formData.bodyLengthMax;
}
//
const submitValues = {
...formData,
fwet: formData.weightMin + "~" + formData.weightMax,
fsz: formData.bodyLengthMin + "~" + formData.bodyLengthMax,
fwet: fwet,
fsz: fsz,
};
if (!formData.id) submitValues.tm = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(submitValues);
emit("ok", submitValues);
} catch (error) {
console.error("Validate Failed:", error);
@ -418,5 +564,4 @@ const handleOk = async () => {
</script>
<style scoped>
/* 如有需要,添加局部样式 */
</style>

View File

@ -0,0 +1,222 @@
<template>
<div class="guoYuSheShiShuJuTianBao-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #ftp="{ onChange }">
<fishSearch v-model="localTypeDate" width="280px" @update:modelValue="onChange" />
</template>
<template #actions>
<a-tooltip title="新增">
<a-button v-hasPerm="['sjtb:import-add']" @click="props.handleAdd">
新增
</a-button>
</a-tooltip>
<a-tooltip title="导入zip">
<a-button v-hasPerm="['sjtb:import-add']" @click="props.importBtn">
导入zip
</a-button>
</a-tooltip>
<a-button
v-hasPerm="['sjtb:import-add']"
@click="props.batchDelBtn"
:disabled="batchData.length === 0"
>
批量删除
</a-button>
<a-tooltip title="提交数据">
<a-button
v-hasPerm="['sjtb:import-add']"
@click="props.submitBtn"
:disabled="batchData.length === 0"
>
<template #icon><SaveOutlined /></template>
提交数据
</a-button>
</a-tooltip>
<a-tooltip title="批量审批">
<a-button
v-hasPerm="['sjtb:edit-review']"
@click="props.successBtn"
:disabled="batchData.length === 0"
>
<template #icon><CheckSquareOutlined /></template>
批量审批
</a-button>
</a-tooltip>
<a-tooltip title="下载模板">
<a-button v-hasPerm="['sjtb:import-add']">
下载模板
</a-button>
</a-tooltip>
<a-tooltip placement="leftBottom">
<template #title>
<div>1.</div>
</template>
<a-button>
<template #icon><QuestionOutlined /></template>
</a-button>
</a-tooltip>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch } from "vue";
import {
SaveOutlined,
CheckSquareOutlined,
QuestionOutlined,
} from "@ant-design/icons-vue";
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";
interface Props {
direction: any[];
guoyuStatus: any[];
importBtn: () => void;
batchDelBtn: () => void;
submitBtn: () => void;
successBtn: () => void;
batchData: any[];
handleAdd: () => void;
}
const shuJuTianBaoStore = useShuJuTianBaoStore();
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
const localTypeDate = ref<string>(null);
const basicSearchRef = ref<any>();
const initSearchData = {
baseId: "all",
stcd: null,
rstcd: null,
ftp: null,
status: null,
direction: null,
strdt: [
dayjs().startOf("month").format("YYYY-MM-DD HH:mm:ss"),
dayjs().endOf("day").format("YYYY-MM-DD HH:mm:ss"),
],
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "waterStation",
name: "baseId",
label: "流域",
fieldProps: {
allowClear: true,
},
options: [],
},
{
type: "Select",
name: "stcd",
label: "过鱼设施",
values: { name: "stnm", value: "rstcd" },
fieldProps: {
allowClear: true,
},
options: shuJuTianBaoStore.fpssOption,
},
{
type: "Select",
name: "direction",
label: "游向",
width: 120,
options: props.direction,
fieldProps: {
allowClear: true,
},
},
{
type: "custom",
name: "ftp",
label: "鱼种类",
fieldProps: {
allowClear: true,
},
},
{
width: 120,
type: "Select",
name: "status",
label: "审批状态",
fieldProps: {
allowClear: true,
},
options: props.guoyuStatus,
},
{
span: 12,
type: "RangePicker",
name: "strdt",
label: "过鱼时间",
picker: "date",
fieldProps: {
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
allowClear: false,
},
presets: DateSetting.RangeButton.days,
},
]);
// --- Methods ---
// 2.
const onSearchFinish = (values: any) => {
emit("searchFinish", values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
// searchData便使
if (
Object.keys(changedValues)[0] == "rstcd" ||
Object.keys(changedValues)[0] == "baseId"
) {
shuJuTianBaoStore.getFpssOption(
allValues.baseId == "all" ? "" : allValues.baseId,
allValues.rstcd
);
const formInstance = basicSearchRef.value?.formData;
formInstance.stcd = null;
}
};
const handleReset = () => {
localTypeDate.value = null;
emit("reset", initSearchData);
};
watch(
() => initSearchData.ftp,
(newVal) => {
localTypeDate.value = newVal || "";
},
{ immediate: true }
);
onMounted(() => {
emit("searchFinish", initSearchData);
shuJuTianBaoStore.getFpssOption("", "");
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,552 @@
<template>
<a-table
size="small"
:loading="fileLoading"
:data-source="fileTableData"
:columns="modalColumns"
:scroll="{ y: 500, x: '100%' }"
:pagination="false"
:row-key="(record, index) => index"
>
<template #bodyCell="{ column, record, index }">
<!-- 1. 操作列 -->
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<template v-if="editingRowIndex === index">
<a-button type="link" size="small" @click="saveEdit(index)">保存</a-button>
<a-button type="link" size="small" @click="cancelEdit">取消</a-button>
</template>
<template v-else>
<a-button type="link" size="small" @click="startEdit(index)">编辑</a-button>
<a-button type="link" danger size="small" @click="handlePreviewDelete(index)"
>删除</a-button
>
</template>
</div>
</template>
<!-- 2. 警告提示列 (非编辑状态) -->
<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>
<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-if="column.dataIndex === 'baseName'">
<a-select
v-model:value="editingData.baseId"
placeholder="请选择"
show-search
:filter-option="filterOption"
:loading="rowStates[index]?.baseLoading"
style="width: 100%"
@change="(val) => handleBaseChange(val, index)"
>
<a-select-option
v-for="opt in baseOptions"
:key="opt.baseid"
:value="opt.baseid"
:label="opt.basename"
>
{{ opt.basename }}
</a-select-option>
</a-select>
</template>
<!-- 电站名称 -->
<template v-else-if="column.dataIndex === 'ennm'">
<a-select
v-model:value="editingData.rstcd"
placeholder="请选择"
show-search
:filter-option="filterOption"
:loading="rowStates[index]?.engLoading"
style="width: 100%"
:disabled="!editingData.baseId"
@change="(val) => handleEngChange(val, index)"
>
<a-select-option
v-for="opt in rowStates[index]?.engOptions || []"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.ennm"
>
{{ opt.ennm }}
</a-select-option>
</a-select>
</template>
<!-- 过鱼设施 -->
<template v-else-if="column.dataIndex === 'stnm'">
<a-select
v-model:value="editingData.stcd"
placeholder="请选择"
show-search
:filter-option="filterOption"
:loading="rowStates[index]?.fpssLoading"
style="width: 100%"
:disabled="!editingData.rstcd"
@change="(val) => handleFpssChange(val, index)"
>
<a-select-option
v-for="opt in rowStates[index]?.fpssOptions || []"
:key="opt.stcd"
:value="opt.stcd"
:label="opt.stnm"
>
{{ opt.stnm }}
</a-select-option>
</a-select>
</template>
<!-- 过鱼时间 -->
<template v-else-if="column.dataIndex === 'strdt'">
<a-date-picker
v-model:value="editingData.strdt"
show-time
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</template>
<!-- 鱼种类 -->
<template v-else-if="column.dataIndex === 'ftpName'">
<fishSearch
v-model="editingData.ftp"
style="width: 100%"
@update:modelValue="handleFtpChange"
/>
</template>
<!-- 游向 -->
<template v-else-if="column.dataIndex === 'direction'">
<a-select
v-model:value="editingData.direction"
placeholder="请选择"
style="width: 100%"
>
<a-select-option
v-for="item in direction"
:key="item.itemCode"
:value="item.itemCode"
>
{{ item.dictName }}
</a-select-option>
</a-select>
</template>
<!-- 数字输入框 -->
<template v-else-if="['fcnt', 'wt'].includes(column.dataIndex)">
<a-input-number
v-model:value="editingData[column.dataIndex]"
style="width: 100%"
:min="0"
/>
</template>
<!-- 是否鱼苗 -->
<template v-else-if="column.dataIndex === 'isfs'">
<a-radio-group v-model:value="editingData.isfs">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</template>
<!-- 体长 (编辑态使用 Min/Max) -->
<template v-else-if="column.dataIndex === 'fsz'">
<div class="flex">
<a-input-number
v-model:value="editingData.bodyLengthMin"
style="width: 50%"
:min="0"
/>
<span class="px-[2px]">~</span>
<a-input-number
v-model:value="editingData.bodyLengthMax"
style="width: 50%"
:min="0"
/>
</div>
</template>
<!-- 体重 (编辑态使用 Min/Max) -->
<template v-else-if="column.dataIndex === 'fwet'">
<div class="flex align-center">
<a-input-number
v-model:value="editingData.weightMin"
style="width: 50%"
:min="0"
/>
<span class="px-[2px]">~</span>
<a-input-number
v-model:value="editingData.weightMax"
style="width: 50%"
:min="0"
/>
</div>
</template>
</template>
<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)">
{{ item.name }}
</div>
</div>
<div v-if="record.picpthList.length==0">暂无图片</div>
</template>
<template
v-else-if="column.dataIndex === 'vdpth'"
>
<div class="preview" v-for="(item, index) in record.vdpthList">
<div class="text" :class="{'text_warning': record.vdpthsWarnings.includes(item.name)}" @click="emit('preview-click', record, 'vdpth' ,index)">
{{ item.name }}
</div>
</div>
<div v-if="record.vdpthList.length==0">暂无视频</div>
</template>
</template>
</a-table>
</template>
<script setup lang="ts">
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 { CloseCircleOutlined } from "@ant-design/icons-vue";
import { es } from "element-plus/es/locale/index.mjs";
const props: any = defineProps({
fileTableData: { type: Array, default: () => [] },
fileLoading: { type: Boolean, default: false },
direction: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:fileTableData", "preview-click"]);
// --- ---
const editingRowIndex = ref<number | null>(null);
const baseOptions = ref<any[]>([]);
const rowStates = reactive<Record<number, any>>({});
// 使
const editingData = ref<any>(null);
const modalColumns = ref([
{
dataIndex: "baseName",
key: "baseName",
dataIndexKey: "baseId",
title: "流域",
width: 140,
},
{
dataIndex: "ennm",
key: "ennm",
dataIndexKey: "rstcd",
title: "电站名称",
width: 140,
},
{
dataIndex: "stnm",
key: "stnm",
dataIndexKey: "stcd",
title: "过鱼设施名称",
width: 150,
},
{ dataIndex: "strdt", key: "strdt", title: "过鱼时间", width: 190 },
{ dataIndex: "ftpName", key: "ftpName", title: "鱼种类", width: 120 },
{
dataIndex: "isfs",
key: "isfs",
title: "是否鱼苗",
width: 130,
customRender: ({ text }: any) => {
const isYes = text === 1 || text === "1";
return h(Tag, { color: isYes ? "success" : "error", style: { margin: 0 } }, () =>
isYes ? "是" : "否"
);
},
},
{
dataIndex: "direction",
key: "direction",
title: "游向",
width: 120,
customRender: ({ text }: any) => props.direction.find((item: any) => item.itemCode === text)?.dictName || "-"
,
},
{ 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 },
{ dataIndex: "picpth", key: "picpth", title: "图片", width: 160 },
{ dataIndex: "vdpth", key: "vdpth", title: "视频", width: 160 },
{
title: "操作",
key: "action",
dataIndex: "action",
fixed: "right",
width: 100,
align: "center",
},
]);
// --- ---
onMounted(() => {
loadBaseOptions();
});
const loadBaseOptions = async () => {
try {
const res = await getBaseDropdown({});
baseOptions.value = res.data || [];
} catch (e) {
console.error("Load base options failed", e);
}
};
const ensureRowState = (index: number) => {
if (!rowStates[index]) {
rowStates[index] = {
engOptions: [],
fpssOptions: [],
baseLoading: false,
engLoading: false,
fpssLoading: false,
};
}
return rowStates[index];
};
// --- ( editingData) ---
const handleBaseChange = async (baseId: string, index: number) => {
console.log(baseId);
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);
//
editingData.value.rstcd = undefined;
editingData.value.stcd = undefined;
state.engOptions = [];
state.fpssOptions = [];
if (!baseId) return;
state.engLoading = true;
try {
const res = await getEngInfoDropdown({ baseId });
state.engOptions = res.data || [];
} catch (e) {
message.error("获取电站列表失败");
} finally {
state.engLoading = false;
}
};
const handleEngChange = async (rstcd: string, index: number) => {
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;
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 });
state.fpssOptions = res.data || [];
} catch (e) {
message.error("获取设施列表失败");
} finally {
state.fpssLoading = false;
}
};
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"
);
}
editingData.value.stnm = state.fpssOptions.find(
(item: any) => item.stcd === stcd
)?.stnm;
};
// --- ---
const isEditing = (index: number) => editingRowIndex.value === index;
const startEdit = (index: number) => {
const originalRecord = props.fileTableData[index];
// 1.
editingData.value = JSON.parse(JSON.stringify(originalRecord));
// 2. fsz/fwet Min/Max 使
processStringToMinMax(editingData.value);
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);
});
}
};
// Min/Max
const processStringToMinMax = (data: any) => {
if (data.fsz) {
const sizes = String(data.fsz).split("~");
data.bodyLengthMin = sizes[0] || "";
data.bodyLengthMax = sizes[1] || sizes[0] || "";
} else {
data.bodyLengthMin = "";
data.bodyLengthMax = "";
}
if (data.fwet) {
const weights = String(data.fwet).split("~");
data.weightMin = weights[0] || "";
data.weightMax = weights[1] || weights[0] || "";
} else {
data.weightMin = "";
data.weightMax = "";
}
};
// Min/Max
const processMinMaxToString = (data: any) => {
//
if (data.bodyLengthMin !== "" || data.bodyLengthMax !== "") {
if (data.bodyLengthMin == data.bodyLengthMax) {
data.fsz = data.bodyLengthMin;
} else {
data.fsz = `${data.bodyLengthMin}~${data.bodyLengthMax}`;
}
} else {
data.fsz = "";
}
//
if (data.weightMin !== "" || data.weightMax !== "") {
if (data.weightMin == data.weightMax) {
data.fwet = data.weightMin;
} else {
data.fwet = `${data.weightMin}~${data.weightMax}`;
}
} else {
data.fwet = "";
}
//
delete data.bodyLengthMin;
delete data.bodyLengthMax;
delete data.weightMin;
delete data.weightMax;
};
const saveEdit = (index: number) => {
// 1. Min/Max fsz/fwet
processMinMaxToString(editingData.value);
// 2.
const newData = [...props.fileTableData];
newData[index] = { ...editingData.value };
// 3.
emit("update:fileTableData", newData);
// 4.
editingRowIndex.value = null;
editingData.value = null;
message.success("保存成功");
};
const cancelEdit = () => {
editingRowIndex.value = null;
editingData.value = null;
// props.fileTableData退
};
const handlePreviewDelete = (index: number) => {
const newData = [...props.fileTableData];
newData.splice(index, 1);
emit("update:fileTableData", newData);
if (editingRowIndex.value === index) {
editingRowIndex.value = null;
editingData.value = null;
} else if (editingRowIndex.value !== null && editingRowIndex.value > index) {
editingRowIndex.value--;
}
message.success("删除成功");
};
//
const handleFtpChange = (val: any, opt: any) => {
editingData.value.ftpName = opt.name;
};
// --- ---
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
defineExpose({
editingRowIndex,
editingData,
});
</script>
<style lang="scss" scoped>
.preview {
width: 100%;
position: relative;
cursor: pointer;
color: #1890ff;
&:hover {
color: #40a9ff;
}
display: flex;
justify-content: justify-between;
align-items: center;
.text {
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text_warning {
color: red;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,472 +0,0 @@
<template>
<div class="guoYuSheShiShuJuTianBao-search">
<!-- 隐藏的文件输入框 -->
<input
ref="fileInputRef"
type="file"
accept=".zip,application/zip,application/x-zip-compressed"
style="display: none"
@change="handleFileSelect"
/>
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #typeDate="{ onChange }">
<fishSearch
v-model="localTypeDate"
width="280px"
:options="options"
@update:modelValue="onChange"
/>
</template>
<!-- 自定义重置及操作按钮区域 -->
<template #actions>
<a-tooltip title="新增">
<a-button @click="props.handleAdd"> 新增 </a-button>
</a-tooltip>
<a-tooltip title="导入zip">
<a-button v-hasPerm="['sjtb:import-zip']" @click="triggerFileInput">
导入zip
</a-button>
</a-tooltip>
<a-button @click="props.batchDel" :disabled="batchData.length === 0">
批量删除
</a-button>
<a-tooltip title="提交数据">
<a-button @click="props.saveBtn">
<template #icon><SaveOutlined /></template>
提交数据
</a-button>
</a-tooltip>
<a-tooltip title="批量审批">
<a-button @click="props.saveBtn">
<template #icon><CheckSquareOutlined /></template>
批量审批
</a-button>
</a-tooltip>
<a-tooltip placement="leftBottom">
<template #title>
<div>1.</div>
</template>
<a-button>
<template #icon><QuestionOutlined /></template>
</a-button>
</a-tooltip>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch } from "vue";
import { message } from "ant-design-vue";
import {
SaveOutlined,
CheckSquareOutlined,
QuestionOutlined,
} from "@ant-design/icons-vue";
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";
// --- Props & Emits ---
interface Props {
importBtn: (file: File) => void;
batchDel: () => void;
saveBtn: () => void;
batchData: any[];
handleAdd: () => void;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "searchFinish", values: any, label: string): void;
}>();
const localTypeDate = ref<string[]>([]);
// --- State ---
const fileInputRef = ref<HTMLInputElement>();
const options = ref<any>([
{
_tls: {},
id: "00DDF2A72147B2115384F64DDFE26A5E",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 1,
name: "异唇裂腹鱼",
code: null,
nameEn: null,
alias: null,
description: null,
logo: null,
introduce: null,
inffile: null,
genus: null,
family: null,
species: null,
fsz: null,
type: 1,
typeName: "淡水",
rare: null,
specOrigin: null,
specOriginName: null,
ptype: null,
ptypeName: null,
rvcd: "null",
rvcdName: "",
zyFishId: "00DDF2A72147B2115384F64DDFE26A5E",
habitMigrat: null,
feedingHabit: null,
spawnCharact: null,
spawnMonth: null,
food: null,
timeFeed: null,
orignDate: null,
pretemp: null,
flowRate: null,
depth: null,
botmMater: null,
wqtq: null,
habitat: null,
situation: null,
resourceType: null,
shapedesc: null,
protectlvl: null,
habitation: null,
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
{
_tls: {},
id: "0249006974f34c288d6cb4df54e3b19d",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 2,
name: "匙吻鲟",
code: null,
nameEn: "Polyodon spathula",
alias: "美国匙吻鲟、鸭嘴鲟",
description:
"匙吻鲟(Polyodonspathula)亦称匙吻猫鱼(spoonbillcat)。产于北美洲的原始鱼,为桨吻鲟(paddlefish)的一种。属鲟形目、匙吻鲟科是北美洲的一种名贵大型淡水经济鱼类。匙吻鲟的显著特点是吻呈扁平桨状特别长。鱼的体表光滑无鳞背部黑蓝灰色有一些斑点在其间体侧有点状赭色腹部白色。个体大这种大型淡水鱼可以长到220厘米重达90公斤以上。",
logo: "20240527221754634033127655455265",
introduce: null,
inffile:
"20240527221811830658320352201158,20240527221805865127213075311524,20240527221822527347221377607671,20240527221828072460253583084314,20240527221800311481326028334838,20240527221817630761245563388673",
genus: "匙吻鲟属",
family: "匙吻鲟科",
species: "匙吻鲟",
fsz: "85~220",
type: 1,
typeName: "淡水",
rare: null,
specOrigin: 2,
specOriginName: "外来鱼类",
ptype: 4,
ptypeName: "易危",
rvcd: "SJLY148",
rvcdName: "大渡河",
zyFishId: "0249006974f34c288d6cb4df54e3b19d",
habitMigrat: "繁殖洄游",
feedingHabit: "肉食性",
spawnCharact: "粘性卵类型",
spawnMonth: "4-5",
food: "主要以浮游动物,也以甲壳类和双壳类生物为食",
timeFeed: "夜间觅食",
orignDate:
"匙吻鲟在美国密西西比河流域的22个洲均有发现。包括密苏里河到蒙大拿州俄亥俄河和它的主要支流流域。雄鱼在79龄达到性成熟雌鱼晚一年相对怀卵量约为每克体重3.5粒。匙吻鲟多在45月繁殖适宜水温为1618℃繁殖期会游到江河上游产卵受精卵灰黑色直径22.5毫米有黏性往往粘在砾石上孵化孵化期67天。",
pretemp: "037℃",
flowRate: "0.3m/s",
depth: "2~2.5",
botmMater: "泥质",
wqtq: "适宜的pH范围为6.58对溶解氧要求较高应在5毫克/升以上。",
habitat: null,
situation: null,
resourceType: null,
shapedesc:
"匙吻鲟有一个形如匙柄的长吻长约为体长的三分之一。身体流线型体表光滑无鳞。眼小口较大位于吻末端的腹面不能伸缩上颌背面具有粗糙的颗粒感觉器。鳃盖骨大而向后延伸鳃盖膜长达胸鳍至腹鳍的1/2处。头部有一喷水孔和喷水腔。胸鳍较小下位腹鳍腹位背鳍起点在腹鳍之后。尾鳍分叉歪尾型上叶长于下叶尾柄披有梗栉状的甲鳞。背部黑蓝灰色常有一些斑点间于其中两侧逐渐变浅体侧有点状褐色腹部白色。",
protectlvl: null,
habitation: "缓流型;广温性;中上层水域",
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
{
_tls: {},
id: "02A23B169BF240589B2C37C5E81A8DC2",
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
departmentId: null,
displayDepartment: null,
index: 3,
name: "南方马口鱼",
code: null,
nameEn: "Chinese hooksnout carp",
alias:
"午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公午仔鱼、山涟仔、桃花鱼、山鳡、坑爬、宽口、大口扒、扯口婆、红车公",
description:
"南方马口鱼Opsariichthys uncirostris bidens Gunther1873是鲤科马口鱼属的一种生活的溪流中的小型鱼类。体长稍侧扁腹部圆。头稍尖头长大于体高。吻钝吻长远比其宽为大。口特大下颌前端突起两侧面各有一凹陷恰与上颌突出部分吻合。下咽齿3行。鳞圆形背鳍条27无硬刺。臀鳍条38-10。背部黑灰色体侧下半部及腹面银白色喉部、口唇及各鳍橙黄背鳍上有黑色的小斑点眼上部有一红色斑点体两侧具有浅蓝色的垂直条纹。生殖季节时雄鱼体色更为鲜艳。",
logo: "20240527192500111683624865306342",
introduce: null,
inffile:
"20240527192505300717052825727341,20240527192533035616525871580354,20240527192510217883087850433201,20240527192516128514164206355182,20240527192522835236402141341053,20240527192527583177528213025212",
genus: "马口鱼属",
family: "鲤科",
species: "南方马口鱼",
fsz: "720",
type: 1,
typeName: "淡水",
rare: null,
specOrigin: 1,
specOriginName: "本土",
ptype: 4,
ptypeName: "易危",
rvcd: "null",
rvcdName: "",
zyFishId: "02A23B169BF240589B2C37C5E81A8DC2",
habitMigrat: "定居型",
feedingHabit: "肉食性",
spawnCharact: "沉性卵类型",
spawnMonth: "6-8",
food: "摄食小型鱼类和水生昆虫。",
timeFeed: "白天觅食",
orignDate:
"产卵期在68月份。第一年生长较迅速可达711厘米。1龄鱼即有繁殖能力系小型鱼类。",
pretemp: "030℃",
flowRate: "0.3m/s",
depth: "1~1.5",
botmMater: "砂砾底质",
wqtq: "pH在7.27.8之内,凉爽清洁、溶氧丰富的水质",
habitat: null,
situation: null,
resourceType: null,
shapedesc:
"背鳍条37臀鳍条39侧线鳞4547下咽齿3行1·4·54·4·1。鳃耙外侧10脊椎骨35。体长为体高的3.14.3倍为头长的3.53.9倍为尾柄长的4.75.2倍为尾柄高的10.211.3倍。头长为吻长的2.73.2倍为眼径的5.06.2倍为眼间距的3.13.3倍。体延长侧扁。吻长其长略大于宽口大端位口裂向上倾斜下颌后端延长达眼前缘其前缘凸起两侧凹陷恰与上颌前端和两侧嵌合。眼中等大位于头侧上方。鳃耙短小而稀疏。下咽齿圆柱性顶端尖而长。侧线完全前端弯向体侧腹方后端向上延至尾柄正中。背鳍无硬刺其起点至吻端稍大于至尾鳍基部的距离胸鳍不达腹鳍其末端可达胸、腹鳍间距的3/5处。腹鳍外缘略钝圆起点约与背鳍不分支鳍条相对。鳔2视后室约为前室的2倍腹腔膜银白色。体背部灰黑色腹部银白色体侧有浅蓝色的垂直条纹胸鳍、腹鳍和臀鳍为橙黄色。雄鱼在生殖期出现婚装头部、吻部和臀部有显眼的珠星臀鳍的第14根分支鳍条特别延长全身具有很鲜艳的婚姻色。",
protectlvl: null,
habitation: "流水型;冷水性;中上水层",
fid: null,
enable: null,
internal: null,
orderIndex: null,
filterContent: null,
platformId: null,
isTempStorage: null,
},
]);
// initSearchData
const initSearchData = {
dmStcd: "008660306300000079", // ID
stcd: {
dataDimensionData: "all",
dataDimensionType: "hyBase",
hbrvcd: "",
stcdId: "",
},
mway: "1",
typeDate: [],
strdt: [
dayjs().startOf("month").format("YYYY-MM-DD"),
dayjs().endOf("day").format("YYYY-MM-DD"),
],
};
const searchData = ref<any>({ ...initSearchData });
// --- Search List Configuration ---
// STCDSTRDTENDDTDIRECTION012
const searchList: any = computed(() => [
{
type: "waterStation",
name: "engName",
label: "水电基地",
fieldProps: {
allowClear: true,
},
options: [],
},
{
type: "Select",
name: "fpname",
label: "过鱼设施",
fieldProps: {
allowClear: true,
},
options: [],
},
{
type: "Select",
name: "direction",
label: "游向",
width: 120,
options: [
{ label: "上行", value: "上行" },
{ label: "下行", value: "下行" },
{ label: "上行折返", value: "上行折返" },
{ label: "下行折返", value: "下行折返" },
],
fieldProps: {
allowClear: true,
},
},
{
type: "custom",
name: "ftp",
label: "鱼种类",
fieldProps: {
allowClear: true,
},
},
checkPerm(["sjtb:edit-ztcx"]) && {
width: 120,
type: "Select",
name: "status",
label: "审批状态",
fieldProps: {
allowClear: true,
},
options: [
{ label: "正常", value: "01" },
{ label: "异常", value: "02" },
],
},
{
span: 12,
type: "RangePicker",
name: "strdt",
label: "过鱼时间",
picker: "date",
fieldProps: {
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
allowClear: false,
// disabledDate: disabledDateFn, //
},
presets: DateSetting.RangeButton.days,
},
]);
// --- Methods ---
// 1.
const handleFileSelect = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
// (50MB)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
message.error("文件大小不能超过50MB");
resetFileInput();
return;
}
//
const isZip =
file.name.toLowerCase().endsWith(".zip") ||
file.type === "application/zip" ||
file.type === "application/x-zip-compressed";
if (!isZip) {
message.error("请选择.zip格式的压缩包");
resetFileInput();
return;
}
props.importBtn(file);
resetFileInput();
};
const resetFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.value = "";
}
};
const triggerFileInput = () => {
fileInputRef.value?.click();
};
// 2.
const onSearchFinish = (values: any) => {
console.log(values);
// label dmStcd
// React label options.find(...)
// ID
const label = "默认水温站"; // TODO: values.dmStcd
// const params: any = {};
// if (values.strdt) {
// params.startDate = values.strdt[0].format("YYYY-MM-DD");
// params.endDate = values.strdt[1].format("YYYY-MM-DD");
// }
emit("searchFinish", values, label);
};
const onValuesChange = (changedValues: any, allValues: any) => {
// searchData便使
searchData.value = { ...searchData.value, ...allValues };
if (changedValues.strdt) {
//
console.log("Time changed:", changedValues.strdt);
}
};
// const handleReset = (form: any) => {
// //
// if (form) {
// form.resetFields();
// }
// //
// nextTick(() => {
// if (form) {
// form.setFieldsValue(initSearchData);
// }
// //
// emit("searchFinish", initSearchData, "");
// });
// };
watch(
() => initSearchData.typeDate,
(newVal) => {
localTypeDate.value = newVal || [];
},
{ immediate: true }
);
// --- Lifecycle ---
onMounted(() => {
//
emit("searchFinish", initSearchData, "两河口出库水温站");
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,446 @@
<template>
<div class="shengPiJiLu-page">
<!-- 搜索区域组件具体 props 需根据实际子组件调整 -->
<GuoYuSheShiShuJuTianBaoSearch @search-finish="handleSearchFinish" @reset="handleReset" />
<!-- 主表格 -->
<BasicTable ref="tableRef" :columns="columns" :list-url="queryPageList" :search-params="{}">
<!-- 使用 bodyCell 插槽自定义单元格渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action' || column.dataIndex === 'action'">
<div class="flex">
<a-button type="link" size="small" @click="handleShowApprovalLog(record)">审批详情</a-button>
<a-button type="link" size="small" @click="handleShowChangeLog(record)">变更详情</a-button>
</div>
</template>
<template v-if="column.dataIndex === 'bizType'">
{{ handName(record.bizType, yeWuType) }}
</template>
<template v-if="column.dataIndex === 'status'">
{{ handName(record.status, shenStatus) }}
</template>
</template>
</BasicTable>
<!-- 审批操作日志弹框 -->
<a-modal
v-model:open="approvalLogVisible"
title="审批操作日志"
width="1600px"
:footer="null"
destroy-on-close="false"
>
<div class="approval-log-modal-content">
<ApprovalLogSearch
:action-type-dict="actionTypeDict"
@search-finish="handleApprovalLogSearch"
@reset="handleApprovalLogReset"
/>
<BasicTable
ref="approvalLogTableRef"
:columns="approvalLogColumns"
:list-url="getApprovalLogList"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
{{ handName(record.action, actionTypeDict) }}
</template>
</template>
</BasicTable>
</div>
</a-modal>
<!-- 数据变更记录弹框 -->
<a-modal
v-model:open="changeLogVisible"
title="数据变更记录"
width="1600px"
:footer="null"
destroy-on-close="false"
>
<div class="change-log-modal-content">
<ChangeLogSearch
:operation-type-dict="operationTypeDict"
@search-finish="handleChangeLogSearch"
@reset="handleChangeLogReset"
/>
<BasicTable
ref="changeLogTableRef"
:columns="changeLogColumns"
:list-url="getApprovalChangeLogList"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'operationType'">
{{ handName(record.operationType, operationTypeDict) }}
</template>
<template v-if="column.dataIndex === 'bizType'">
{{ handName(record.bizType, yeWuType) }}
</template>
<template v-if="column.dataIndex === 'changeJson'">
<pre style="max-height: 200px; overflow: auto; margin: 0;">{{ record.changeJson }}</pre>
</template>
</template>
</BasicTable>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { queryPageList, getApprovalLogList, getApprovalChangeLogList } from '@/api/shengPiJiLu';
import BasicTable from "@/components/BasicTable/index.vue";
import GuoYuSheShiShuJuTianBaoSearch from "./shengPiJiLuSearch.vue";
import ApprovalLogSearch from "./approvalLogSearch.vue";
import ChangeLogSearch from "./changeLogSearch.vue";
import { getDictItemsByCode } from '@/api/dict';
let columns = ref([
{
title: '审批批次号',
dataIndex: 'approvalNo',
key: 'approvalNo',
width: 160,
fixed: 'left'
},
{
title: '业务类型',
dataIndex: 'bizType',
key: 'bizType',
width: 100,
align: 'center'
},
{
title: '数据条数',
dataIndex: 'dataCount',
key: 'dataCount',
width: 100,
align: 'center'
},
{
title: '提交人',
dataIndex: 'applyUserName',
key: 'applyUserName',
width: 120,
align: 'center'
},
{
title: '提交时间',
dataIndex: 'applyTime',
key: 'applyTime',
width: 160,
align: 'center'
},
{
title: '审批状态',
dataIndex: 'status',
key: 'status',
width: 80,
align: 'center',
},
{
title: '审批人',
dataIndex: 'approverName',
key: 'approverName',
width: 120,
align: 'center'
},
{
title: '审批时间',
dataIndex: 'approveTime',
key: 'approveTime',
width: 160,
align: 'center'
},
{
title: '审批备注',
dataIndex: 'remark',
key: 'remark',
ellipsis: true,
},
{
title: '操作',
key: 'action',
width: 160,
fixed: 'right',
align: 'center'
}
]);
const tableRef = ref()
const handleSearchFinish = (values: any) => {
console.log(values);
const filters = [
values.approvalNo && {
field: "approvalNo",
operator: "contains",
dataType: "string",
value: values.approvalNo,
},
values.status && {
field: "status",
operator: "eq",
dataType: "string",
value: values.status,
},
].filter(Boolean);
const filter = {
logic: "and",
filters: filters,
};
tableRef.value?.getList(filter);
};
const handleReset = (values) => {
handleSearchFinish(values);
};
//
const approvalLogVisible = ref(false);
const approvalLogTableRef = ref();
const approvalLogColumns = ref([
{
title: '操作类型',
dataIndex: 'action',
key: 'action',
width: 120,
align: 'center'
},
{
title: '操作人',
dataIndex: 'operatorName',
key: 'operatorName',
width: 150,
align: 'center'
},
{
title: '操作时间',
dataIndex: 'operateTime',
key: 'operateTime',
width: 180
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180
},
{
title: '审批意见',
dataIndex: 'commentInfo',
key: 'commentInfo',
ellipsis: true,
},
]);
//
const changeLogVisible = ref(false);
const changeLogTableRef = ref();
const changeLogColumns = ref([
{
title: '业务类型',
dataIndex: 'bizType',
key: 'bizType',
width: 120,
align: 'center'
},
{
title: '操作类型',
dataIndex: 'operationType',
key: 'operationType',
width: 120,
align: 'center'
},
{
title: '操作人',
dataIndex: 'operatorName',
key: 'operatorName',
width: 150,
align: 'center'
},
{
title: '操作时间',
dataIndex: 'operateTime',
key: 'operateTime',
width: 180
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180
},
{
title: '变更内容',
dataIndex: 'changeJson',
key: 'changeJson',
ellipsis: true
},
]);
const currentApprovalId = ref('');
const actionTypeDict = ref([]);
const operationTypeDict = ref([]);
//
const handleShowApprovalLog = (record: any) => {
currentApprovalId.value = record.id;
approvalLogVisible.value = true;
// BasicTable
setTimeout(() => {
const filter = {
logic: "and",
filters: [
{
field: "approvalId",
operator: "eq",
dataType: "string",
value: record.id,
}
]
};
approvalLogTableRef.value?.getList(filter);
}, 300);
};
//
const handleApprovalLogSearch = (values: any) => {
console.log('审批日志搜索:', values);
const filters = [
{
field: "approvalId",
operator: "eq",
dataType: "string",
value: currentApprovalId.value,
},
values.action && {
field: "action",
operator: "eq",
dataType: "string",
value: values.action,
},
].filter(Boolean);
const filter = {
logic: "and",
filters: filters,
};
approvalLogTableRef.value?.getList(filter);
};
//
const handleApprovalLogReset = (values: any) => {
console.log('审批日志重置:', values);
handleApprovalLogSearch(values);
};
//
const handleShowChangeLog = (record: any) => {
currentApprovalId.value = record.id;
changeLogVisible.value = true;
// BasicTable
setTimeout(() => {
const filter = {
logic: "and",
filters: [
{
field: "approvalId",
operator: "eq",
dataType: "string",
value: record.id,
}
]
};
changeLogTableRef.value?.getList(filter);
}, 300);
};
//
const handleChangeLogSearch = (values: any) => {
console.log('变更日志搜索:', values);
const filters = [
{
field: "approvalId",
operator: "eq",
dataType: "string",
value: currentApprovalId.value,
},
values.operationType && {
field: "operationType",
operator: "eq",
dataType: "string",
value: values.operationType,
},
].filter(Boolean);
const filter = {
logic: "and",
filters: filters,
};
changeLogTableRef.value?.getList(filter);
};
//
const handleChangeLogReset = (values: any) => {
console.log('变更日志重置:', values);
handleChangeLogSearch(values);
};
//
onMounted(() => {
dictNmae()
});
const shenStatus = ref([])
const yeWuType = ref([])
const dictNmae = () => {
getDictItemsByCode({ dictCode: 'shenStatus' }).then((res) => {
shenStatus.value = res.data;
});
getDictItemsByCode({ dictCode: 'yeWuType' }).then((res) => {
yeWuType.value = res.data;
});
// TODO:
getDictItemsByCode({ dictCode: 'caoType' }).then((res) => {
actionTypeDict.value = res.data;
});
getDictItemsByCode({ dictCode: 'caoTypeTwo' }).then((res) => {
operationTypeDict.value = res.data;
});
}
const handName = (val: any, arr: any) => {
let dictName1 = ''
arr.forEach((item: any) => {
if (item.itemCode == val) {
dictName1 = item.dictName
}
})
return dictName1
}
</script>
<style scoped lang="scss">
.shengPiJiLu-page {
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 20px;
}
.approval-log-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.change-log-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<div class="guoYuSheShiShuJuTianBao-search">
<BasicSearch ref="basicSearchRef" :searchList="searchList" :initial-values="initSearchData"
@finish="onSearchFinish" @values-change="onValuesChange" @reset="handleReset">
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import BasicSearch from "@/components/BasicSearch/index.vue"; //
import { getDictItemsByCode } from '@/api/dict';
// --- Props & Emits ---
interface Props {
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
// initSearchData
const initSearchData = {
approvalNo:'',
status:'',
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Input",
name: "approvalNo",
label: "审批批次号",
fieldProps: {
allowClear: true,
},
options: [],
},
{
type: "Select",
name: "status",
label: "审批状态",
fieldProps: {
allowClear: true,
},
options: statusData.value,
},
]);
// --- Methods ---
// 2.
const onSearchFinish = (values: any) => {
console.log(values);
emit("searchFinish", values);
};
const handleReset = () => {
emit("reset", initSearchData);
};
const onValuesChange = (changedValues: any, allValues: any) => {
// searchData便使
searchData.value = { ...searchData.value, ...allValues };
};
// --- Lifecycle ---
onMounted(() => {
emit("searchFinish", initSearchData);
getstatusData()
});
const statusData = ref(false)
const getstatusData = () => {
getDictItemsByCode({ dictCode: "shenStatus" }).then((res) => {
statusData.value = res.data;
});
};
</script>
<style lang="scss"></style>

View File

@ -2,7 +2,7 @@
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
import RightDrawer from "@/components/RightDrawer/index.vue";
import ShuiZhiJianCeGongZuoQingKuang from "@/modules/shuizhijiancegongzuoQK/index.vue"
// import EnvironmentalQuality from "@/modules/environmentalQuality/index.vue" //
import EnvironmentalQuality from "@/modules/EnvironmentalQuality/index.vue" //
</script>
<template>
@ -13,7 +13,7 @@ import ShuiZhiJianCeGongZuoQingKuang from "@/modules/shuizhijiancegongzuoQK/inde
<div class="rightContent">
<RightDrawer>
<ShuiZhiJianCeGongZuoQingKuang />
<!-- <EnvironmentalQuality /> -->
<EnvironmentalQuality />
</RightDrawer>
</div>
</div>

View File

@ -68,7 +68,7 @@ const input = ref("");
const title = ref("");
const info: any = ref({
rolename: "",
level: "系统管理员",
level: "2",
description: "",
});
const faultList: any = [
@ -90,7 +90,7 @@ function addClick() {
title.value = "新增角色";
info.value = {
rolename: "",
level: "系统管理员",
level: "2",
description: "",
};
dialogVisible.value = true;

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",

View File

@ -27,7 +27,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
// 线上API地址
// target: 'http://localhost:8093/',
// 本地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), '')