WholeProcessPlatform/frontend/src/views/system/disposeManage/disposeManageForm.vue
2026-07-16 17:33:08 +08:00

568 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-modal
:title="isEdit ? '编辑飞行路径' : '新增飞行路径'"
v-model:open="modalVisible"
:confirm-loading="localLoading"
width="1536px"
:destroy-on-close="true"
ok-text="保存"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
name="edit_form"
:labelCol="{ span: 5 }"
>
<a-row>
<a-col :span="12">
<a-form-item label="飞行路径名称" name="name">
<a-input
v-model:value="formData.name"
style="width: 100%"
placeholder="请输入飞行路径名称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="飞行路径Code" name="code">
<a-input
v-model:value="formData.code"
style="width: 100%"
placeholder="请输入飞行路径Code"
/>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="备注" name="remark">
<a-textarea
v-model:value="formData.remark"
:rows="4"
style="width: 100%"
placeholder="请输入备注"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="水电基地" name="baseId">
<a-select
v-model:value="formData.baseId"
show-search
style="width: 100%"
placeholder="请选择水电基地"
:options="ennmOptions"
:filter-option="filterOption"
:disabled="isEdit"
@change="getdianzha"
/>
</a-form-item>
</a-col>
</a-row>
<a-divider />
<div class="flex justify-between items-center mb-2">
<a-button type="primary" @click="handleAddRow"> 新增行</a-button>
</div>
<div ref="tableWrapperRef">
<a-table
:data-source="currentPageData"
:columns="columns"
:pagination="false"
row-key="id"
size="small"
:scroll="{ x: 950 }"
:custom-row="customRow"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'drag'">
<UnorderedListOutlined
v-if="editingRowId !== record.id"
class="drag-handle cursor-move text-gray-400"
/>
</template>
<template v-else-if="column.key === 'index'">
{{ (currentPage - 1) * pageSize + index + 1 }}
</template>
<template v-else-if="column.key === 'time'">
<template v-if="editingRowId === record.id">
<a-input-number
v-model:value="record.time"
style="width: 100%"
placeholder="时间(s)"
/>
</template>
<template v-else>
{{ record.time ?? '-' }}
</template>
</template>
<template v-else-if="column.key === 'lgtd'">
<template v-if="editingRowId === record.id">
<a-input-number
v-model:value="record.lgtd"
style="width: 100%"
placeholder="经度"
/>
</template>
<template v-else>
{{ record.lgtd ?? '-' }}
</template>
</template>
<template v-else-if="column.key === 'lttd'">
<template v-if="editingRowId === record.id">
<a-input-number
v-model:value="record.lttd"
style="width: 100%"
placeholder="纬度"
/>
</template>
<template v-else>
{{ record.lttd ?? '-' }}
</template>
</template>
<template v-else-if="column.key === 'dtmel'">
<template v-if="editingRowId === record.id">
<a-input-number
v-model:value="record.dtmel"
style="width: 100%"
placeholder="高程(m)"
/>
</template>
<template v-else>
{{ record.dtmel ?? '-' }}
</template>
</template>
<template v-else-if="column.key === 'stnm'">
<template v-if="editingRowId === record.id">
<a-select
v-model:value="record.stcd"
style="width: 100%"
placeholder="请选择电站"
:options="stationOptions"
allow-clear
@change="(val) => handleStnmChange(record, val)"
/>
</template>
<template v-else>
{{ getStationLabel(record.stcd) || '-' }}
</template>
</template>
<template v-else-if="column.key === 'action'">
<template v-if="editingRowId === record.id">
<a-button
type="link"
size="small"
@click="handleSaveRow(record)"
>保存</a-button
>
</template>
<template v-else>
<a-button
type="link"
size="small"
@click="handleStartEdit(record)"
>编辑</a-button
>
</template>
<a-popconfirm
title="确定删除该航点?"
@confirm="handleDeleteRow(record)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</div>
<div v-if="formData.list.length > pageSize" class="flex justify-end mt-2">
<a-pagination
v-model:current="currentPage"
:total="formData.list.length"
:page-size="pageSize"
size="small"
show-size-changer
:page-size-options="['5', '10', '20', '50']"
show-total
@change="handlePageChange"
/>
</div>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, nextTick, onBeforeUnmount } from 'vue';
import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { UnorderedListOutlined } from '@ant-design/icons-vue';
import Sortable from 'sortablejs';
import dayjs from 'dayjs';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { getEngInfoDropdown } from '@/api/select';
import { useDraggable } from '@/utils/drag';
const JidiSelectEventStore = useJidiSelectEventStore();
// 电站名称下拉(假数据)
const stationOptions = ref([]);
// 表格列配置
const columns = [
{ title: '排序', key: 'drag', width: 60, align: 'center' as const },
{ title: '序号', key: 'index', width: 60, align: 'center' as const },
{ title: '时间(s)', key: 'time', width: 130 },
{ title: '经度°', key: 'lgtd', width: 150 },
{ title: '纬度°', key: 'lttd', width: 150 },
{ title: '高程(m)', key: 'dtmel', width: 120 },
{ title: '电站名称', key: 'stnm', width: 160 },
{ title: '操作', key: 'action', width: 120, align: 'center' as const }
];
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
}
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false
});
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const formRef = ref();
const tableWrapperRef = ref<HTMLElement | null>(null);
let sortableInstance: Sortable | null = null;
// 当前正在编辑的行 ID同一时间只允许编辑一行
const editingRowId = ref<string | null>(null);
// 给每行添加 data-row-key 属性,供拖拽时识别行身份
const customRow = (record: any) => {
return {
attrs: {
'data-row-key': record?.id
}
};
};
// 分页
const currentPage = ref(1);
const pageSize = ref(5);
const currentPageData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return formData.list.slice(start, start + pageSize.value);
});
const defaultFormData = reactive({
id: undefined,
name: undefined,
baseId: undefined,
code: undefined,
remark: undefined,
list: [] as any[]
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入飞行路径名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入飞行路径Code', trigger: 'blur' }]
};
const isEdit = computed(() => !!props.initialValues);
// 生成唯一ID
const generateId = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16).toUpperCase();
});
};
// 开始编辑行
const handleStartEdit = (record: any) => {
editingRowId.value = record.id;
};
// 保存行(退出编辑模式,保留当前输入值)
const handleSaveRow = (record: any) => {
editingRowId.value = null;
};
// 新增航点
const handleAddRow = () => {
if(!formData.baseId){
message.warning('请先选择水电基地');
return;
}
// 有行正在编辑时禁止新增
if (editingRowId.value) {
message.warning('请先保存当前编辑的行');
return;
}
const newRow = {
id: generateId(),
recordUser: null,
recordTime: null,
modifyTime: null,
displayRecordUser: null,
roamId: null,
code: formData.code || null,
name: formData.name || null,
lgtd: undefined,
lttd: undefined,
dtmel: undefined,
time: undefined,
orderIndex: formData.list.length,
stcd: null,
stnm: undefined,
departmentId: null,
displayDepartment: null
};
formData.list.push(newRow);
// 新增行自动进入编辑模式
editingRowId.value = newRow.id;
// 翻到最后一页
const lastPage = Math.ceil(formData.list.length / pageSize.value);
currentPage.value = lastPage;
nextTick(() => initSortable());
};
// 删除航点
const handleDeleteRow = (record: any) => {
// 如果正编辑的行被删了,退出编辑
if (editingRowId.value === record.id) {
editingRowId.value = null;
}
const idx = formData.list.findIndex((item: any) => item.id === record.id);
if (idx !== -1) {
formData.list.splice(idx, 1);
// 如果当前页没数据了,往前翻一页
if (currentPageData.value.length === 0 && currentPage.value > 1) {
currentPage.value--;
}
nextTick(() => initSortable());
}
};
// 页码变化
const handlePageChange = () => {
editingRowId.value = null; // 切换页时退出编辑
nextTick(() => initSortable());
};
// Sortable 拖拽 — 基于 DOM data-row-key 排序,不依赖 oldIndex/newIndex
const initSortable = () => {
destroySortable();
if (!tableWrapperRef.value) return;
const tbody = tableWrapperRef.value.querySelector('.ant-table-tbody');
if (!tbody) return;
sortableInstance = new Sortable(tbody as HTMLElement, {
handle: '.drag-handle',
animation: 150,
onEnd: evt => {
if (!tbody) return;
// 读取拖拽后 DOM 中的行顺序(只读当前页)
const rows = tbody.querySelectorAll<HTMLElement>('tr.ant-table-row');
const domIds: (string | null)[] = [];
rows.forEach(row => {
const id = row.getAttribute('data-row-key');
if (id) domIds.push(id);
});
if (domIds.length === 0) return;
const page = currentPage.value;
const size = pageSize.value;
const start = (page - 1) * size;
const end = start + size;
// 按 DOM 顺序重排当前页数据
const pageItems = domIds
.map(id => formData.list.find((item: any) => item.id === id))
.filter(Boolean);
if (pageItems.length === 0) return;
// 重建完整数组:前面 + 当前页(新顺序)+ 后面
const before = formData.list.slice(0, start);
const after = formData.list.slice(end);
formData.list.splice(
0,
formData.list.length,
...before,
...pageItems,
...after
);
}
});
};
const destroySortable = () => {
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
};
// 表单提交
const handleOk = async () => {
// 有行正在编辑时禁止保存
if (editingRowId.value) {
message.warning('请先保存当前编辑的行');
return;
}
try {
await formRef.value.validate();
// 转换数据baseId → rvcd更新 orderIndex
const submitValues = {
name: formData.name,
code: formData.code,
remark: formData.remark || null,
baseId: formData.baseId,
id: formData.id || undefined,
list: formData.list.map((item: any, index: number) => ({
...item,
orderIndex: index,
time: item.time ?? null,
lgtd: item.lgtd ?? null,
lttd: item.lttd ?? null,
dtmel: item.dtmel ?? null
}))
};
emit('ok', submitValues);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
// 初始化表单
const initForm = () => {
currentPage.value = 1;
editingRowId.value = null;
if (props.initialValues) {
const values = props.initialValues;
// 先将匹配的字段复制到 formData
Object.keys(formData).forEach(key => {
if (key === 'list') return; // list 单独处理
if (values.hasOwnProperty(key)) {
if (key === 'captureDate' && values[key]) {
formData[key] = dayjs(values[key]);
} else {
formData[key] = values[key];
}
}
});
// rvcd → baseId 映射
if (values.baseId) {
formData.baseId = values.baseId;
getdianzha(values.baseId);
}
// list 数据
formData.list = values.list
? values.list.map((item: any) => ({ ...item }))
: [];
} else {
resetForm();
}
nextTick(() => initSortable());
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
formData.list = [];
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
resetForm();
};
// 搜索过滤
const filterOption = (inputValue: string, option: any) => {
const label = option.label || option.name || '';
const reachcdName = option.reachcdName || '';
const keyword = inputValue || '';
return label.indexOf(keyword) !== -1 || reachcdName.indexOf(keyword) !== -1;
};
const ennmOptions = computed(() => {
return JidiSelectEventStore.jidiData.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
});
const getdianzha = async (data: any) => {
// 切换水电基地时,清空已新增的航点(仅在新增模式下)
if (!isEdit.value && formData.list.length > 0) {
formData.list = [];
currentPage.value = 1;
editingRowId.value = null;
nextTick(() => initSortable());
}
const res = await getEngInfoDropdown({ baseId: data });
stationOptions.value = res.data.map((item: any) => ({
label: item.ennm,
value: item.stcd
}));
};
// select 选中 stcd 后同步填充 stnm供展示时使用
const handleStnmChange = (record: any, stcd: string | undefined) => {
const option = stationOptions.value.find((opt: any) => opt.value === stcd);
record.stnm = option ? option.label : null;
};
// 根据 stcd 查找电站名称 label 用于展示
const getStationLabel = (stcd: string | undefined) => {
const option = stationOptions.value.find((opt: any) => opt.value === stcd);
return option ? option.label : stcd;
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
initForm();
} else {
destroySortable();
}
},
{ immediate: false }
);
onBeforeUnmount(() => {
destroySortable();
});
defineExpose({
localLoading
});
</script>
<style scoped lang="scss">
.drag-handle {
cursor: grab;
&:active {
cursor: grabbing;
}
}
</style>