feat(管理后台): 新增线路编辑器拖拽平移并修复代理下Socket连接问题

调整socket.io传输顺序优先使用轮询以适配代理服务器,新增可视化线路编辑器拖拽平移功能,修复多处CSS布局问题并更新静态资源缓存版本。
This commit is contained in:
2026-06-21 11:21:09 +08:00
parent 7fea8807b8
commit b1cb84f736
5 changed files with 86 additions and 10 deletions
+9 -5
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<!-- 充满未知和不稳定的票务系统! -->
@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FSE铁路票务系统控制台</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="style.css?v=13">
<link rel="stylesheet" href="style.css?v=14">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
@@ -293,8 +293,12 @@
</div>
<!-- 可视化线路编辑-->
<div class="visual-line-container">
<svg width="100%" height="200"
<div class="visual-line-container"
ref="visualLineViewport"
:class="{ 'is-panning': lineViewportPan.active }"
@mousedown="startLineViewportPan"
@mousemove="moveLineViewportPan">
<svg :width="lineEditorSvgWidth" height="200"
v-if="selectedLine.stations && selectedLine.stations.length > 0">
<!--站点连接线-->
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100"
@@ -929,7 +933,7 @@
</div>
<script src="/custom-dialog.js?v=12"></script>
<script src="/public-status.js?v=13"></script>
<script src="index.js?v=3"></script>
<script src="index.js?v=5"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const isDomain = location.hostname.includes('fse-media.group');
+52 -1
View File
@@ -35,7 +35,9 @@ createApp({
});
const connected = ref(false);
const socket = io({ transports: ['websocket', 'polling'], upgrade: false });
// Prefer polling first so admin remains connected even when the proxy
// does not support WebSocket upgrades reliably.
const socket = io({ transports: ['polling', 'websocket'] });
// Data State
const stations = ref([]);
@@ -100,6 +102,15 @@ createApp({
const showFareModal = ref(false);
const currentFare = reactive({ exists: false, cost_regular: 0, cost_express: 0 });
const draggingStationIndex = ref(null);
const visualLineViewport = ref(null);
const lineViewportPan = reactive({
active: false,
startX: 0,
startY: 0,
scrollLeft: 0,
scrollTop: 0,
moved: false
});
const showStationModal = ref(false);
const stationForm = reactive({ code: '', name: '', en_name: '', transfer_enabled: false, transfer_to: [] });
const stationFormOriginalCode = ref('');
@@ -654,6 +665,36 @@ createApp({
draggingStationIndex.value = null;
};
const startLineViewportPan = (event) => {
const viewport = visualLineViewport.value;
if (!viewport) return;
if (event.button !== 0) return;
if (event.target && event.target.closest('.station-node')) return;
lineViewportPan.active = true;
lineViewportPan.moved = false;
lineViewportPan.startX = event.clientX;
lineViewportPan.startY = event.clientY;
lineViewportPan.scrollLeft = viewport.scrollLeft;
lineViewportPan.scrollTop = viewport.scrollTop;
};
const moveLineViewportPan = (event) => {
if (!lineViewportPan.active) return;
const viewport = visualLineViewport.value;
if (!viewport) return;
const deltaX = event.clientX - lineViewportPan.startX;
const deltaY = event.clientY - lineViewportPan.startY;
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
lineViewportPan.moved = true;
}
viewport.scrollLeft = lineViewportPan.scrollLeft - deltaX;
viewport.scrollTop = lineViewportPan.scrollTop - deltaY;
};
const endLineViewportPan = () => {
lineViewportPan.active = false;
};
// --- Order Management ---
const fetchOrders = async () => {
if (loadingState.orders) return;
@@ -1171,6 +1212,10 @@ createApp({
};
const handleStationClick = async (code) => {
if (lineViewportPan.moved) {
lineViewportPan.moved = false;
return;
}
if (stationEditMode.value) {
openStationModal(code);
return;
@@ -1398,6 +1443,7 @@ createApp({
startIcCardSync();
}
appMouseupHandler = async () => {
endLineViewportPan();
if (draggingStationIndex.value !== null) {
if (selectedLine.value) {
try {
@@ -1428,6 +1474,10 @@ createApp({
// Computed
const recentLogs = computed(() => logs.value);
const orderList = computed(() => orders.value);
const lineEditorSvgWidth = computed(() => {
const count = Array.isArray(selectedLine.value?.stations) ? selectedLine.value.stations.length : 0;
return Math.max(960, 100 + Math.max(0, count - 1) * 120 + 120);
});
const lastSyncText = computed(() => lastSyncAt.value ? formatTime(lastSyncAt.value) : '尚未同步');
const isViewBusy = computed(() => {
if (loadingState.core) return true;
@@ -1520,6 +1570,7 @@ createApp({
// Management
selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations,
visualLineViewport, lineViewportPan, lineEditorSvgWidth, startLineViewportPan, moveLineViewportPan,
selectLine, createLine, deleteLine, deleteStation, updateLineInfo, getFareText,
isStationInLine, addStationToLine, removeStationFromLine,
handleStationClick, isStationSelected,
+22 -2
View File
@@ -686,6 +686,7 @@ main {
display: flex;
flex-direction: column;
flex-shrink: 0;
min-height: 0;
}
.management-main {
@@ -702,6 +703,7 @@ main {
flex-direction: column;
gap: 8px;
padding-right: 4px;
min-height: 0;
}
.line-item {
@@ -771,18 +773,26 @@ main {
.visual-line-container {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
overflow: auto;
background-color: #00000022;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 40px;
min-height: 0;
cursor: grab;
user-select: none;
scrollbar-width: thin;
}
.visual-line-container svg {
min-width: 100%;
flex-shrink: 0;
}
.visual-line-container.is-panning {
cursor: grabbing;
}
.station-node {
@@ -3383,6 +3393,12 @@ body.jr-ticket-board-page .jr-board-card:last-child {
align-items: start;
}
.ic-admin-layout .management-sidebar,
.ic-admin-layout .management-main,
.jr-admin-list-card {
min-height: 0;
}
.ic-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -3886,6 +3902,10 @@ body.jr-admin-login-page {
.jr-admin-list-card .jr-scroll-box {
padding-right: 4px;
min-height: 320px;
max-height: 560px;
overflow-y: auto;
overscroll-behavior: contain;
}
.jr-admin-summary-grid {
+1 -1
View File
@@ -609,7 +609,7 @@
</div>
<script src="/custom-dialog.js?v=12"></script>
<script src="/public-status.js?v=13"></script>
<script src="ticket-route.js?v=2"></script>
<script src="ticket-route.js?v=3"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const isDomain = location.hostname.includes('fse-media.group');
+2 -1
View File
@@ -24,7 +24,8 @@ createApp({
});
const connected = ref(false);
const socket = io({ transports: ['websocket'], upgrade: false, timeout: 20000 });
// Keep the legacy route console usable behind proxies that only allow polling.
const socket = io({ transports: ['polling', 'websocket'], timeout: 20000 });
const stations = ref([]);