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"> <html lang="zh-CN">
<!-- 充满未知和不稳定的票务系统! --> <!-- 充满未知和不稳定的票务系统! -->
@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FSE铁路票务系统控制台</title> <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="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="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
</head> </head>
@@ -293,8 +293,12 @@
</div> </div>
<!-- 可视化线路编辑--> <!-- 可视化线路编辑-->
<div class="visual-line-container"> <div class="visual-line-container"
<svg width="100%" height="200" ref="visualLineViewport"
:class="{ 'is-panning': lineViewportPan.active }"
@mousedown="startLineViewportPan"
@mousemove="moveLineViewportPan">
<svg :width="lineEditorSvgWidth" height="200"
v-if="selectedLine.stations && selectedLine.stations.length > 0"> v-if="selectedLine.stations && selectedLine.stations.length > 0">
<!--站点连接线--> <!--站点连接线-->
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100" <line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100"
@@ -929,7 +933,7 @@
</div> </div>
<script src="/custom-dialog.js?v=12"></script> <script src="/custom-dialog.js?v=12"></script>
<script src="/public-status.js?v=13"></script> <script src="/public-status.js?v=13"></script>
<script src="index.js?v=3"></script> <script src="index.js?v=5"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const isDomain = location.hostname.includes('fse-media.group'); const isDomain = location.hostname.includes('fse-media.group');
+52 -1
View File
@@ -35,7 +35,9 @@ createApp({
}); });
const connected = ref(false); 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 // Data State
const stations = ref([]); const stations = ref([]);
@@ -100,6 +102,15 @@ createApp({
const showFareModal = ref(false); const showFareModal = ref(false);
const currentFare = reactive({ exists: false, cost_regular: 0, cost_express: 0 }); const currentFare = reactive({ exists: false, cost_regular: 0, cost_express: 0 });
const draggingStationIndex = ref(null); 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 showStationModal = ref(false);
const stationForm = reactive({ code: '', name: '', en_name: '', transfer_enabled: false, transfer_to: [] }); const stationForm = reactive({ code: '', name: '', en_name: '', transfer_enabled: false, transfer_to: [] });
const stationFormOriginalCode = ref(''); const stationFormOriginalCode = ref('');
@@ -654,6 +665,36 @@ createApp({
draggingStationIndex.value = null; 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 --- // --- Order Management ---
const fetchOrders = async () => { const fetchOrders = async () => {
if (loadingState.orders) return; if (loadingState.orders) return;
@@ -1171,6 +1212,10 @@ createApp({
}; };
const handleStationClick = async (code) => { const handleStationClick = async (code) => {
if (lineViewportPan.moved) {
lineViewportPan.moved = false;
return;
}
if (stationEditMode.value) { if (stationEditMode.value) {
openStationModal(code); openStationModal(code);
return; return;
@@ -1398,6 +1443,7 @@ createApp({
startIcCardSync(); startIcCardSync();
} }
appMouseupHandler = async () => { appMouseupHandler = async () => {
endLineViewportPan();
if (draggingStationIndex.value !== null) { if (draggingStationIndex.value !== null) {
if (selectedLine.value) { if (selectedLine.value) {
try { try {
@@ -1428,6 +1474,10 @@ createApp({
// Computed // Computed
const recentLogs = computed(() => logs.value); const recentLogs = computed(() => logs.value);
const orderList = computed(() => orders.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 lastSyncText = computed(() => lastSyncAt.value ? formatTime(lastSyncAt.value) : '尚未同步');
const isViewBusy = computed(() => { const isViewBusy = computed(() => {
if (loadingState.core) return true; if (loadingState.core) return true;
@@ -1520,6 +1570,7 @@ createApp({
// Management // Management
selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations, selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations,
visualLineViewport, lineViewportPan, lineEditorSvgWidth, startLineViewportPan, moveLineViewportPan,
selectLine, createLine, deleteLine, deleteStation, updateLineInfo, getFareText, selectLine, createLine, deleteLine, deleteStation, updateLineInfo, getFareText,
isStationInLine, addStationToLine, removeStationFromLine, isStationInLine, addStationToLine, removeStationFromLine,
handleStationClick, isStationSelected, handleStationClick, isStationSelected,
+22 -2
View File
@@ -686,6 +686,7 @@ main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 0;
min-height: 0;
} }
.management-main { .management-main {
@@ -702,6 +703,7 @@ main {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding-right: 4px; padding-right: 4px;
min-height: 0;
} }
.line-item { .line-item {
@@ -771,18 +773,26 @@ main {
.visual-line-container { .visual-line-container {
flex: 1; flex: 1;
overflow-x: auto; overflow: auto;
overflow-y: hidden;
background-color: #00000022; background-color: #00000022;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
padding: 40px; padding: 40px;
min-height: 0;
cursor: grab;
user-select: none;
scrollbar-width: thin;
} }
.visual-line-container svg { .visual-line-container svg {
min-width: 100%; min-width: 100%;
flex-shrink: 0;
}
.visual-line-container.is-panning {
cursor: grabbing;
} }
.station-node { .station-node {
@@ -3383,6 +3393,12 @@ body.jr-ticket-board-page .jr-board-card:last-child {
align-items: start; align-items: start;
} }
.ic-admin-layout .management-sidebar,
.ic-admin-layout .management-main,
.jr-admin-list-card {
min-height: 0;
}
.ic-form-grid { .ic-form-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -3886,6 +3902,10 @@ body.jr-admin-login-page {
.jr-admin-list-card .jr-scroll-box { .jr-admin-list-card .jr-scroll-box {
padding-right: 4px; padding-right: 4px;
min-height: 320px;
max-height: 560px;
overflow-y: auto;
overscroll-behavior: contain;
} }
.jr-admin-summary-grid { .jr-admin-summary-grid {
+1 -1
View File
@@ -609,7 +609,7 @@
</div> </div>
<script src="/custom-dialog.js?v=12"></script> <script src="/custom-dialog.js?v=12"></script>
<script src="/public-status.js?v=13"></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> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const isDomain = location.hostname.includes('fse-media.group'); const isDomain = location.hostname.includes('fse-media.group');
+2 -1
View File
@@ -24,7 +24,8 @@ createApp({
}); });
const connected = ref(false); 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([]); const stations = ref([]);