feat(web/route): 适配移动端响应式布局 + 统一站点拖拽逻辑
1. 线路规划页面(/route)移动端适配:
- 新增 980px / 768px / 480px 三档响应式CSS
- 侧边栏改为抽屉式叠加层,主容器垂直流式布局
- 顶部导航、工具栏、编辑器堆叠式排列
- SVG线路容器开启横向惯性滚动,最小宽度720px
- 票价地图、表格、模态框均针对小屏优化
- 按钮/输入框统一最小高度44px,满足可访问性
2. 重构站点拖拽逻辑(修复电脑端拖拽失效):
- 原实现:mousemove绑定单站点<g>,鼠标离开即丢失拖拽
- 新实现:mousedown/touchstart仅记录起点,
window级别监听 mousemove/mouseup/touchmove/touchend
- 通过 SVG createSVGPoint + getScreenCTM().inverse()
统一将屏幕坐标转换为SVG逻辑坐标再换算站点索引
- 共用 swapStationByIndex() / saveDragResult() 核心函数
3. 静态资源版本号升级: style.css v16, ticket-route.js v5
This commit is contained in:
+90
-34
@@ -400,24 +400,38 @@ createApp({
|
||||
selectedTicket.value = null;
|
||||
};
|
||||
|
||||
/* 拖动调整站序 */
|
||||
const onStationDragStart = (index) => {
|
||||
if (fareMode.value) return;
|
||||
draggingStationIndex.value = index;
|
||||
/* 拖动调整站序 - 统一实现(鼠标+触摸) */
|
||||
const dragActiveSvg = ref(null);
|
||||
|
||||
const computeStationIndexFromClientXY = (clientX, clientY) => {
|
||||
const svgEl = dragActiveSvg.value;
|
||||
if (!svgEl || !selectedLine.value) return -1;
|
||||
const pt = svgEl.createSVGPoint();
|
||||
pt.x = clientX;
|
||||
pt.y = clientY;
|
||||
const ctm = svgEl.getScreenCTM();
|
||||
if (!ctm) return -1;
|
||||
const loc = pt.matrixTransform(ctm.inverse());
|
||||
return Math.max(0, Math.min(
|
||||
(selectedLine.value.stations?.length || 1) - 1,
|
||||
Math.round((loc.x - 50) / 120)
|
||||
));
|
||||
};
|
||||
|
||||
const onStationDragOver = (index) => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
if (draggingStationIndex.value === index) return;
|
||||
|
||||
const list = selectedLine.value.stations;
|
||||
const temp = list[draggingStationIndex.value];
|
||||
list.splice(draggingStationIndex.value, 1);
|
||||
list.splice(index, 0, temp);
|
||||
draggingStationIndex.value = index;
|
||||
const swapStationByIndex = (targetIndex) => {
|
||||
if (draggingStationIndex.value === null) return false;
|
||||
if (targetIndex < 0) return false;
|
||||
if (draggingStationIndex.value === targetIndex) return false;
|
||||
const list = selectedLine.value.stations;
|
||||
if (!list) return false;
|
||||
const temp = list[draggingStationIndex.value];
|
||||
list.splice(draggingStationIndex.value, 1);
|
||||
list.splice(targetIndex, 0, temp);
|
||||
draggingStationIndex.value = targetIndex;
|
||||
return true;
|
||||
};
|
||||
|
||||
const onStationDrop = async () => {
|
||||
const saveDragResult = async () => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
try {
|
||||
await updateLineStations(selectedLine.value.id, selectedLine.value.stations);
|
||||
@@ -426,6 +440,61 @@ createApp({
|
||||
await fetchData();
|
||||
}
|
||||
draggingStationIndex.value = null;
|
||||
dragActiveSvg.value = null;
|
||||
};
|
||||
|
||||
/* 鼠标事件 */
|
||||
const onStationDragStart = (index, event) => {
|
||||
if (fareMode.value) return;
|
||||
draggingStationIndex.value = index;
|
||||
const svgEl = (event.currentTarget && event.currentTarget.closest)
|
||||
? event.currentTarget.closest('svg')
|
||||
: null;
|
||||
dragActiveSvg.value = svgEl || document.querySelector('.visual-line-container svg');
|
||||
};
|
||||
|
||||
const onStationDragOver = (index) => {
|
||||
swapStationByIndex(index);
|
||||
};
|
||||
|
||||
const onStationDrop = async () => {
|
||||
await saveDragResult();
|
||||
};
|
||||
|
||||
const globalMouseMoveHandler = (e) => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
const targetIndex = computeStationIndexFromClientXY(e.clientX, e.clientY);
|
||||
swapStationByIndex(targetIndex);
|
||||
};
|
||||
|
||||
const globalMouseUpHandler = async () => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
await saveDragResult();
|
||||
};
|
||||
|
||||
/* 触摸事件 - 移动端拖拽支持 */
|
||||
const onStationTouchStart = (index, event) => {
|
||||
if (fareMode.value) return;
|
||||
if (!event.touches || event.touches.length !== 1) return;
|
||||
draggingStationIndex.value = index;
|
||||
const svgEl = (event.currentTarget && event.currentTarget.closest)
|
||||
? event.currentTarget.closest('svg')
|
||||
: null;
|
||||
dragActiveSvg.value = svgEl || document.querySelector('.visual-line-container svg');
|
||||
};
|
||||
|
||||
const globalTouchMoveHandler = (e) => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
if (!e.touches || e.touches.length !== 1) return;
|
||||
if (e.cancelable) e.preventDefault();
|
||||
const t = e.touches[0];
|
||||
const targetIndex = computeStationIndexFromClientXY(t.clientX, t.clientY);
|
||||
swapStationByIndex(targetIndex);
|
||||
};
|
||||
|
||||
const globalTouchEndHandler = async () => {
|
||||
if (draggingStationIndex.value === null) return;
|
||||
await saveDragResult();
|
||||
};
|
||||
|
||||
/* 订单管理 */
|
||||
@@ -889,29 +958,15 @@ createApp({
|
||||
});
|
||||
|
||||
watch(currentView, () => { sidebarOpen.value = false; });
|
||||
|
||||
/* 失败 */
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
loadFareMap();
|
||||
window.addEventListener('mouseup', async () => {
|
||||
if (draggingStationIndex.value !== null) {
|
||||
if (selectedLine.value) {
|
||||
try {
|
||||
await requestJson(`/api/lines/${encodeURIComponent(selectedLine.value.id)}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ ...(lines.value.find(l => l.id === selectedLine.value.id) || selectedLine.value), stations: selectedLine.value.stations })
|
||||
}, { expectOk: true });
|
||||
await fetchData();
|
||||
} catch (e) {
|
||||
alert(`保存站序失败:${e?.message || String(e)}`);
|
||||
await fetchData();
|
||||
}
|
||||
}
|
||||
draggingStationIndex.value = null;
|
||||
}
|
||||
});
|
||||
window.addEventListener('mousemove', globalMouseMoveHandler);
|
||||
window.addEventListener('mouseup', globalMouseUpHandler);
|
||||
window.addEventListener('touchmove', globalTouchMoveHandler, { passive: false });
|
||||
window.addEventListener('touchend', globalTouchEndHandler);
|
||||
window.addEventListener('touchcancel', globalTouchEndHandler);
|
||||
});
|
||||
|
||||
/* 计算 */
|
||||
@@ -976,6 +1031,7 @@ createApp({
|
||||
isStationInLine, addStationToLine, removeStationFromLine,
|
||||
handleStationClick, isStationSelected,
|
||||
onStationDragStart, onStationDragOver, onStationDrop, draggingStationIndex,
|
||||
onStationTouchStart,
|
||||
showStationModal, stationForm, stationFormOriginalCode, transferTargets, saveStationSettings, closeStationModal,
|
||||
showLineModal, lineForm, openLineModal, saveLineSettings, closeLineModal,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user