WholeProcessPlatform/frontend/src/views/system/disposeManage/disposeManageForm.vue

568 lines
16 KiB
Vue
Raw Normal View History

2026-07-15 18:24:05 +08:00
<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;
2026-07-16 17:33:08 +08:00
getdianzha(values.baseId);
2026-07-15 18:24:05 +08:00
}
// 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>