568 lines
16 KiB
Vue
568 lines
16 KiB
Vue
<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>
|