feat(管理后台): 新增线路编辑器拖拽平移并修复代理下Socket连接问题
调整socket.io传输顺序优先使用轮询以适配代理服务器,新增可视化线路编辑器拖拽平移功能,修复多处CSS布局问题并更新静态资源缓存版本。
This commit is contained in:
+9
-5
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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([]);
|
||||
|
||||
Reference in New Issue
Block a user