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:
+231
-119
@@ -53,33 +53,44 @@
|
||||
<p class="jr-admin-sidebar-copy">集中处理票务、线路、资源和运营日志。</p>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<a href="https://ticket.fse-media.group" id="homeLink" class="nav-item" style="text-decoration: none;">
|
||||
<a href="https://ticket.fse-media.group" id="homeLink" class="nav-item"
|
||||
style="text-decoration: none;">
|
||||
<span class="nav-icon"><i class="fas fa-home"></i></span> 返回首页
|
||||
</a>
|
||||
<div class="nav-item" :class="{active: currentView === 'dashboard'}" @click="currentView = 'dashboard'">
|
||||
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span> 仪表盘 </div>
|
||||
<div class="nav-item" :class="{active: currentView === 'dashboard'}"
|
||||
@click="currentView = 'dashboard'">
|
||||
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span> 仪表盘
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'management'}"
|
||||
@click="currentView = 'management'">
|
||||
<span class="nav-icon"><i class="fas fa-network-wired"></i></span> 线路与票价 </div>
|
||||
<div class="nav-item" :class="{active: currentView === 'faremap'}" @click="currentView = 'faremap'">
|
||||
<span class="nav-icon"><i class="fas fa-network-wired"></i></span> 线路与票价
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'faremap'}"
|
||||
@click="currentView = 'faremap'">
|
||||
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'tickets'}" @click="currentView = 'tickets'">
|
||||
<div class="nav-item" :class="{active: currentView === 'tickets'}"
|
||||
@click="currentView = 'tickets'">
|
||||
<span class="nav-icon"><i class="fas fa-ticket-alt"></i></span> 车票记录
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'vouchers'}" @click="currentView = 'vouchers'">
|
||||
<div class="nav-item" :class="{active: currentView === 'vouchers'}"
|
||||
@click="currentView = 'vouchers'">
|
||||
<span class="nav-icon"><i class="fas fa-receipt"></i></span> 凭证记录
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'iccards'}" @click="currentView = 'iccards'">
|
||||
<div class="nav-item" :class="{active: currentView === 'iccards'}"
|
||||
@click="currentView = 'iccards'">
|
||||
<span class="nav-icon"><i class="fas fa-credit-card"></i></span> IC卡管理
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'issues'}" @click="currentView = 'issues'">
|
||||
<div class="nav-item" :class="{active: currentView === 'issues'}"
|
||||
@click="currentView = 'issues'">
|
||||
<span class="nav-icon"><i class="fas fa-exclamation-triangle"></i></span> 问题汇总
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'assets'}" @click="currentView = 'assets'">
|
||||
<div class="nav-item" :class="{active: currentView === 'assets'}"
|
||||
@click="currentView = 'assets'">
|
||||
<span class="nav-icon"><i class="fas fa-route"></i></span> 线路图上传
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="currentView = 'settings'">
|
||||
<div class="nav-item" :class="{active: currentView === 'settings'}"
|
||||
@click="currentView = 'settings'">
|
||||
<span class="nav-icon"><i class="fas fa-cog"></i></span> 系统设置
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'logs'}" @click="currentView = 'logs'">
|
||||
@@ -93,7 +104,8 @@
|
||||
<div class="header">
|
||||
<div class="jr-admin-header-copy">
|
||||
<div class="flex" style="gap: 12px;">
|
||||
<button class="icon-btn mobile-only" @click="sidebarOpen = !sidebarOpen" title="菜单"><i class="fas fa-bars"></i></button>
|
||||
<button class="icon-btn mobile-only" @click="sidebarOpen = !sidebarOpen" title="菜单"><i
|
||||
class="fas fa-bars"></i></button>
|
||||
<div>
|
||||
<span class="jr-kicker">OPERATIONS CONSOLE</span>
|
||||
<h3 style="margin: 0;">{{ viewTitle }}</h3>
|
||||
@@ -146,7 +158,8 @@
|
||||
style="height: 100%; display: flex; flex-direction: column; margin-bottom: 0;">
|
||||
<div class="flex between mb-4">
|
||||
<h4>线路列表</h4>
|
||||
<button @click="showAddLine = true" title="新建线路"><i class="fas fa-plus"></i></button>
|
||||
<button @click="showAddLine = true" title="新建线路"><i
|
||||
class="fas fa-plus"></i></button>
|
||||
</div>
|
||||
|
||||
<!--添加车站-->
|
||||
@@ -159,7 +172,8 @@
|
||||
<input v-model="newLine.en_name" placeholder="英文名称"
|
||||
style="margin-bottom: 8px; width: 100%;">
|
||||
<div class="flex">
|
||||
<input type="text" v-model="newLine.color" placeholder="#HEX颜色" style="flex: 1;">
|
||||
<input type="text" v-model="newLine.color" placeholder="#HEX颜色"
|
||||
style="flex: 1;">
|
||||
<input type="color" v-model="newLine.color" title="选择颜色"
|
||||
style="width: 40px; padding: 0; border: none; height: 32px;">
|
||||
<button @click="createLine" style="padding: 0 12px;" title="确认创建线路"><i
|
||||
@@ -170,14 +184,16 @@
|
||||
</div>
|
||||
<div class="list-lines" style="flex: 1; overflow-y: auto;">
|
||||
<div v-for="l in lines" :key="l.id" class="line-item"
|
||||
:class="{active: selectedLine && selectedLine.id === l.id}" @click="selectLine(l)">
|
||||
:class="{active: selectedLine && selectedLine.id === l.id}"
|
||||
@click="selectLine(l)">
|
||||
<div class="line-color-dot" :style="{background: l.color}"></div>
|
||||
<div class="line-info">
|
||||
<div class="line-name">{{ l.name || l.id }}</div>
|
||||
<div class="line-meta">{{ (l.stations || []).length }} 站</div>
|
||||
</div>
|
||||
<div class="line-actions" v-if="selectedLine && selectedLine.id === l.id">
|
||||
<button class="danger sm" @click.stop="deleteLine(l.id)"><i class="fas fa-trash"></i></button>
|
||||
<button class="danger sm" @click.stop="deleteLine(l.id)"><i
|
||||
class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,13 +205,15 @@
|
||||
<div class="flex between">
|
||||
<div v-if="selectedLine">
|
||||
<div class="flex">
|
||||
<h3 :style="{color: selectedLine.color}">{{ selectedLine.name || selectedLine.id
|
||||
<h3 :style="{color: selectedLine.color}">{{ selectedLine.name ||
|
||||
selectedLine.id
|
||||
}}</h3>
|
||||
<span class="badge">{{ selectedLine.id }}</span>
|
||||
</div>
|
||||
<div class="flex mt-2" style="align-items:center; gap:8px;">
|
||||
<label style="font-size:0.8em; color:var(--muted);">EN:</label>
|
||||
<span style="font-size:0.9em;">{{ selectedLine.en_name || 'N/A' }}</span>
|
||||
<span style="font-size:0.9em;">{{ selectedLine.en_name || 'N/A'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -203,8 +221,10 @@
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button v-if="selectedLine" @click="openLineModal" title="编辑线路"><i class="fas fa-pen"></i></button>
|
||||
<button @click="refreshData" title="刷新"><i class="fas fa-sync-alt"></i></button>
|
||||
<button v-if="selectedLine" @click="openLineModal" title="编辑线路"><i
|
||||
class="fas fa-pen"></i></button>
|
||||
<button @click="refreshData" title="刷新"><i
|
||||
class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,17 +236,20 @@
|
||||
<label class="switch-label">
|
||||
<input type="checkbox" v-model="fareMode">
|
||||
<span class="slider"></span>
|
||||
<span class="label-text"><i class="fas fa-coins"></i> 票价设置/车站编辑模式</span>
|
||||
<span class="label-text"><i class="fas fa-coins"></i> 票价设置</span>
|
||||
</label>
|
||||
<label class="switch-label" style="margin-left: 10px;">
|
||||
<input type="checkbox" v-model="stationEditMode">
|
||||
<span class="slider"></span>
|
||||
<span class="label-text"><i class="fas fa-exchange-alt"></i> 换乘设置模式</span>
|
||||
<span class="label-text"><i class="fas fa-exchange-alt"></i>
|
||||
换乘设置模式/车站编辑模式</span>
|
||||
</label>
|
||||
<div v-if="fareMode" class="hint-text text-warning">
|
||||
<i class="fas fa-info-circle"></i> 点击两个站点以设置票价 </div>
|
||||
<i class="fas fa-info-circle"></i> 点击两个站点以设置票价
|
||||
</div>
|
||||
<div v-else-if="stationEditMode" class="hint-text text-info">
|
||||
<i class="fas fa-info-circle"></i> 点击站点以设置换乘 </div>
|
||||
<i class="fas fa-info-circle"></i> 点击站点以设置换乘/编辑车站
|
||||
</div>
|
||||
<div v-else class="hint-text text-muted">
|
||||
<i class="fas fa-info-circle"></i> 点击站点删除
|
||||
</div>
|
||||
@@ -235,29 +258,31 @@
|
||||
<div class="flex"
|
||||
style="background: rgba(255,255,255,0.05); padding: 8px; border-radius: 6px;">
|
||||
<div style="font-weight: bold; margin-right: 8px;">添加站点:</div>
|
||||
<input v-model="newStation.code" placeholder="编号 (01-01)" style="width: 100px;">
|
||||
<input v-model="newStation.code" placeholder="编号 (01-01)"
|
||||
style="width: 100px;">
|
||||
<input v-model="newStation.name" placeholder="中文名" style="width: 120px;">
|
||||
<input v-model="newStation.en_name" placeholder="英文名" style="width: 120px;">
|
||||
<button @click="addStationToLine"
|
||||
:disabled="!newStation.code || !newStation.name"><i class="fas fa-plus"></i>
|
||||
:disabled="!newStation.code || !newStation.name"><i
|
||||
class="fas fa-plus"></i>
|
||||
添加</button>
|
||||
<button @click="addBreakpointToLine" class="btn" style="margin-left: 8px;" title="在末尾插入一个线路断点">
|
||||
<button @click="addBreakpointToLine" class="btn" style="margin-left: 8px;"
|
||||
title="在末尾插入一个线路断点">
|
||||
<i class="fas fa-cut"></i> 插入断点
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可视化线路编辑-->
|
||||
<div class="visual-line-container"
|
||||
ref="visualLineViewport"
|
||||
<div class="visual-line-container" ref="visualLineViewport"
|
||||
:class="{ 'is-panning': lineViewportPan.active }"
|
||||
@mousedown="startLineViewportPan"
|
||||
@mousemove="moveLineViewportPan">
|
||||
@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"
|
||||
:stroke="selectedLine.color" stroke-width="4" stroke-linecap="round" />
|
||||
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120"
|
||||
y2="100" :stroke="selectedLine.color" stroke-width="4"
|
||||
stroke-linecap="round" />
|
||||
|
||||
<!--票价显示-->
|
||||
<g v-for="(s, i) in selectedLine.stations.slice(0, selectedLine.stations.length-1)"
|
||||
@@ -269,42 +294,57 @@
|
||||
<!--车站节点-->
|
||||
<g v-for="(sCode, index) in selectedLine.stations" :key="index"
|
||||
@mousedown="onStationDragStart(index)" @mouseup="onStationDrop"
|
||||
@mousemove="onStationDragOver(index)" @click="handleStationClick(sCode, index)"
|
||||
class="station-node" :class="{
|
||||
@mousemove="onStationDragOver(index)"
|
||||
@click="handleStationClick(sCode, index)" class="station-node" :class="{
|
||||
'selected': isStationSelected(sCode),
|
||||
'fare-source': fareSelection[0] === sCode,
|
||||
'fare-target': fareSelection[1] === sCode
|
||||
}">
|
||||
|
||||
<template v-if="sCode === '-'">
|
||||
<rect :x="50 + index * 120 - 10" :y="90" width="20" height="20" fill="var(--bg)" stroke="var(--danger)" stroke-dasharray="2,2" />
|
||||
<text :x="50 + index * 120" y="105" text-anchor="middle" fill="var(--danger)" font-size="12" style="pointer-events: none;">✂</text>
|
||||
<text :x="50 + index * 120" y="135" text-anchor="middle" fill="var(--danger)" font-size="10" style="pointer-events: none;">断点</text>
|
||||
<rect :x="50 + index * 120 - 10" :y="90" width="20" height="20"
|
||||
fill="var(--bg)" stroke="var(--danger)"
|
||||
stroke-dasharray="2,2" />
|
||||
<text :x="50 + index * 120" y="105" text-anchor="middle"
|
||||
fill="var(--danger)" font-size="12"
|
||||
style="pointer-events: none;">✂</text>
|
||||
<text :x="50 + index * 120" y="135" text-anchor="middle"
|
||||
fill="var(--danger)" font-size="10"
|
||||
style="pointer-events: none;">断点</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!--车站节点图形-->
|
||||
<circle :cx="50 + index * 120" cy="100" r="14" fill="var(--bg)"
|
||||
:stroke="selectedLine.color" stroke-width="3" />
|
||||
<circle v-if="isStationSelected(sCode)" :cx="50 + index * 120" cy="100" r="8"
|
||||
:fill="selectedLine.color" />
|
||||
<circle v-if="isStationSelected(sCode)" :cx="50 + index * 120"
|
||||
cy="100" r="8" :fill="selectedLine.color" />
|
||||
|
||||
<!--节点标签-->
|
||||
<text :x="50 + index * 120" y="70" text-anchor="middle" fill="var(--text)"
|
||||
font-weight="bold" font-size="12" style="pointer-events: none;">{{
|
||||
<text :x="50 + index * 120" y="70" text-anchor="middle"
|
||||
fill="var(--text)" font-weight="bold" font-size="12"
|
||||
style="pointer-events: none;">{{
|
||||
getStationName(sCode) }}</text>
|
||||
<text :x="50 + index * 120" y="135" text-anchor="middle" fill="var(--muted)"
|
||||
font-size="10" style="pointer-events: none;">{{ sCode }}</text>
|
||||
<text :x="50 + index * 120" y="135" text-anchor="middle"
|
||||
fill="var(--muted)" font-size="10"
|
||||
style="pointer-events: none;">{{ sCode }}</text>
|
||||
<g v-if="getTransferLineBadges(sCode).length > 0">
|
||||
<g v-for="(li, liIdx) in getTransferLineBadges(sCode)" :key="`${sCode}-xfer-${li.id}`">
|
||||
<circle :cx="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" cy="150" r="5"
|
||||
:fill="li.color" stroke="#ffffff" stroke-width="1" />
|
||||
<text :x="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" y="165" text-anchor="middle"
|
||||
fill="var(--muted)" font-size="7" style="pointer-events: none;">{{ li.id }}</text>
|
||||
<g v-for="(li, liIdx) in getTransferLineBadges(sCode)"
|
||||
:key="`${sCode}-xfer-${li.id}`">
|
||||
<circle
|
||||
:cx="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14"
|
||||
cy="150" r="5" :fill="li.color" stroke="#ffffff"
|
||||
stroke-width="1" />
|
||||
<text
|
||||
:x="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14"
|
||||
y="165" text-anchor="middle" fill="var(--muted)"
|
||||
font-size="7" style="pointer-events: none;">{{ li.id
|
||||
}}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!--删除-->
|
||||
<title>{{ getStationName(sCode) }} ({{ sCode }}){{ getTransferTitleSuffix(sCode) }}</title>
|
||||
<title>{{ getStationName(sCode) }} ({{ sCode }}){{
|
||||
getTransferTitleSuffix(sCode) }}</title>
|
||||
</template>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -330,11 +370,13 @@
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label>常规票价</label>
|
||||
<input v-model.number="currentFare.cost_regular" type="number" class="w-100">
|
||||
<input v-model.number="currentFare.cost_regular" type="number"
|
||||
class="w-100">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label>特急票价</label>
|
||||
<input v-model.number="currentFare.cost_express" type="number" class="w-100">
|
||||
<input v-model.number="currentFare.cost_express" type="number"
|
||||
class="w-100">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="danger" @click="deleteCurrentFare"
|
||||
@@ -367,27 +409,34 @@
|
||||
<span class="label-text">设置跨名/强制换乘</span>
|
||||
</label>
|
||||
<p class="text-muted" style="font-size: 0.85em; margin-top: 6px;">
|
||||
<i class="fas fa-magic text-primary"></i> 提示:系统会自动将中文名或英文名相同的站点连接为换乘站,无需手动设置。
|
||||
<i class="fas fa-magic text-primary"></i>
|
||||
提示:系统会自动将中文名或英文名相同的站点连接为换乘站,无需手动设置。
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4" v-if="stationForm.transfer_enabled">
|
||||
<label>可换乘到的站点</label>
|
||||
<select v-model="stationForm.transfer_to" multiple class="w-100" style="height: 180px;">
|
||||
<optgroup label="同名站点 (已自动换乘)" v-if="transferTargets.sameName.length > 0">
|
||||
<option v-for="t in transferTargets.sameName" :key="t.code" :value="t.code">
|
||||
<select v-model="stationForm.transfer_to" multiple class="w-100"
|
||||
style="height: 180px;">
|
||||
<optgroup label="同名站点 (已自动换乘)"
|
||||
v-if="transferTargets.sameName.length > 0">
|
||||
<option v-for="t in transferTargets.sameName" :key="t.code"
|
||||
:value="t.code">
|
||||
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="其他站点">
|
||||
<option v-for="t in transferTargets.others" :key="t.code" :value="t.code">
|
||||
<option v-for="t in transferTargets.others" :key="t.code"
|
||||
:value="t.code">
|
||||
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<p class="text-muted" style="font-size: 0.8em; margin-top: 4px;">按住 Ctrl / Cmd 键多选</p>
|
||||
<p class="text-muted" style="font-size: 0.8em; margin-top: 4px;">按住 Ctrl /
|
||||
Cmd 键多选</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="danger" @click="deleteStation(stationFormOriginalCode)">删除</button>
|
||||
<button class="danger"
|
||||
@click="deleteStation(stationFormOriginalCode)">删除</button>
|
||||
<button @click="saveStationSettings">保存</button>
|
||||
<button class="danger" @click="closeStationModal">取消</button>
|
||||
</div>
|
||||
@@ -412,7 +461,8 @@
|
||||
<div class="mb-4">
|
||||
<label>颜色</label>
|
||||
<div class="flex" style="gap:8px;">
|
||||
<input type="text" v-model="lineForm.color" class="w-100" placeholder="#3366cc">
|
||||
<input type="text" v-model="lineForm.color" class="w-100"
|
||||
placeholder="#3366cc">
|
||||
<input type="color" v-model="lineForm.color" title="选择颜色"
|
||||
style="width: 48px; padding: 0; border: none; height: 32px;">
|
||||
</div>
|
||||
@@ -435,14 +485,17 @@
|
||||
<button @click="loadFareMap" title="刷新"><i class="fas fa-sync-alt"></i></button>
|
||||
<button @click="zoomFareMapOut" title="缩小"><i class="fas fa-minus"></i></button>
|
||||
<button @click="zoomFareMapIn" title="放大"><i class="fas fa-plus"></i></button>
|
||||
<button @click="zoomFareMapReset" title="重置"><i class="fas fa-crosshairs"></i></button>
|
||||
<button @click="exportFareMap" title="导出图像"><i class="fas fa-download"></i></button>
|
||||
<button @click="zoomFareMapReset" title="重置"><i
|
||||
class="fas fa-crosshairs"></i></button>
|
||||
<button @click="exportFareMap" title="导出图像"><i
|
||||
class="fas fa-download"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fareMapLoading" class="loading">加载中...</div>
|
||||
<div v-else-if="fareMapError" class="loading">{{ fareMapError }}</div>
|
||||
<div v-else class="faremap-viewport">
|
||||
<div class="faremap-canvas" :style="{ transform: `scale(${fareMapScale})` }" v-html="fareMapSvg"></div>
|
||||
<div class="faremap-canvas" :style="{ transform: `scale(${fareMapScale})` }"
|
||||
v-html="fareMapSvg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,12 +541,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="o in orderList" :key="o.code">
|
||||
<td class="mono" style="font-weight:bold; font-size:1.1em;">{{ o.code }}</td>
|
||||
<td class="mono" style="font-weight:bold; font-size:1.1em;">{{ o.code }}
|
||||
</td>
|
||||
<td>{{ o.start_name }} <i class="fas fa-arrow-right text-muted"></i> {{
|
||||
o.terminal_name }}</td>
|
||||
<td>{{ formatTrainType(o.train_type) }}</td>
|
||||
<td>{{ o.price }}</td>
|
||||
<td><span class="badge" :class="formatTicketStatus(o.status).class">{{ formatTicketStatus(o.status).text }}</span></td>
|
||||
<td><span class="badge" :class="formatTicketStatus(o.status).class">{{
|
||||
formatTicketStatus(o.status).text }}</span></td>
|
||||
<td>{{ formatTime(o.created_ts) }}</td>
|
||||
<td>
|
||||
<div class="flex" style="gap:4px;">
|
||||
@@ -536,7 +591,8 @@
|
||||
<div class="management-container ic-admin-layout">
|
||||
<div class="management-sidebar">
|
||||
|
||||
<div class="card jr-admin-list-card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
||||
<div class="card jr-admin-list-card"
|
||||
style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
||||
<div class="flex between mb-4">
|
||||
<div>
|
||||
<h4>卡片列表</h4>
|
||||
@@ -545,27 +601,29 @@
|
||||
<span class="badge">{{ icCards.length }}</span>
|
||||
</div>
|
||||
<div class="flex mb-4" style="flex-wrap:wrap;">
|
||||
<input v-model="icCardSearch" placeholder="搜索卡号 / 订单号 / 凭证码 / 姓名" style="flex:1;">
|
||||
<input v-model="icCardSearch" placeholder="搜索卡号 / 订单号 / 凭证码 / 姓名"
|
||||
style="flex:1;">
|
||||
</div>
|
||||
<div class="list-lines jr-scroll-box" style="flex:1; min-height:320px;">
|
||||
<div v-if="!icCards.length" class="empty-state" style="padding:24px 0;">
|
||||
<p>暂无 IC 卡记录。</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="card in icCards"
|
||||
:key="card.card_id"
|
||||
<div v-for="card in icCards" :key="card.card_id"
|
||||
class="line-item ic-card-item"
|
||||
:class="{ active: icSelectedId === card.card_id }"
|
||||
@click="loadIcCard(card.card_id)"
|
||||
>
|
||||
<div class="line-color-dot" :style="{ background: icStatusColor(card.status) }"></div>
|
||||
@click="loadIcCard(card.card_id)">
|
||||
<div class="line-color-dot"
|
||||
:style="{ background: icStatusColor(card.status) }"></div>
|
||||
<div class="line-info">
|
||||
<div class="line-name">{{ displayIcCardId(card) }}</div>
|
||||
<div class="line-meta">{{ card.holder_name || '未登记持卡人' }} · IC 储值卡</div>
|
||||
<div class="line-meta">订单 {{ cardOrderCode(card) }} · 余额 {{ formatMoney(card.balance) }}</div>
|
||||
<div class="line-meta">{{ card.holder_name || '未登记持卡人' }} · IC 储值卡
|
||||
</div>
|
||||
<div class="line-meta">订单 {{ cardOrderCode(card) }} · 余额 {{
|
||||
formatMoney(card.balance) }}</div>
|
||||
</div>
|
||||
<div class="line-actions" style="opacity:1;">
|
||||
<span class="badge" :class="icStatusInfo(card.status).className">{{ icStatusInfo(card.status).text }}</span>
|
||||
<span class="badge" :class="icStatusInfo(card.status).className">{{
|
||||
icStatusInfo(card.status).text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,10 +637,15 @@
|
||||
<h4>卡片详情</h4>
|
||||
</div>
|
||||
<div class="flex" style="gap:8px;">
|
||||
<button class="btn" @click="loadIcCard(icSelectedId)" :disabled="!icSelectedCard || isViewBusy"><i class="fas fa-rotate-right"></i> 刷新</button>
|
||||
<button class="btn" @click="loadIcCard(icSelectedId)"
|
||||
:disabled="!icSelectedCard || isViewBusy"><i
|
||||
class="fas fa-rotate-right"></i> 刷新</button>
|
||||
|
||||
<button class="btn danger" @click="deleteIcCard" :disabled="!icSelectedCard"><i class="fas fa-trash"></i> 删除卡</button>
|
||||
<button class="btn primary" @click="saveIcCard" :disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
||||
<button class="btn danger" @click="deleteIcCard"
|
||||
:disabled="!icSelectedCard"><i class="fas fa-trash"></i>
|
||||
删除卡</button>
|
||||
<button class="btn primary" @click="saveIcCard"
|
||||
:disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!icSelectedCard" class="empty-state">
|
||||
@@ -592,10 +655,15 @@
|
||||
<div v-else>
|
||||
<div class="flex between mb-4" style="align-items:flex-start;">
|
||||
<div>
|
||||
<div class="mono" style="font-size:1.4rem; font-weight:700;">{{ displayIcCardId(icSelectedCard) }}</div>
|
||||
<div class="text-muted" style="margin-top:6px;">订单号 {{ cardOrderCode(icSelectedCard) }} · 来源 {{ icSelectedCard.source || '---' }}</div>
|
||||
<div class="mono" style="font-size:1.4rem; font-weight:700;">{{
|
||||
displayIcCardId(icSelectedCard) }}</div>
|
||||
<div class="text-muted" style="margin-top:6px;">订单号 {{
|
||||
cardOrderCode(icSelectedCard) }} · 来源 {{ icSelectedCard.source
|
||||
|| '---' }}</div>
|
||||
</div>
|
||||
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
|
||||
<span class="badge"
|
||||
:class="icStatusInfo(icSelectedCard.status).className">{{
|
||||
icStatusInfo(icSelectedCard.status).text }}</span>
|
||||
</div>
|
||||
<div class="jr-admin-summary-grid">
|
||||
<div class="jr-admin-summary-item">
|
||||
@@ -635,10 +703,15 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="ic-inline-meta">
|
||||
<div class="list-item"><span class="k">创建时间</span><span class="v">{{ formatTime(icSelectedCard.created_ts) }}</span></div>
|
||||
<div class="list-item"><span class="k">最后更新</span><span class="v">{{ formatTime(icSelectedCard.last_update_ts) }}</span></div>
|
||||
<div class="list-item"><span class="k">首次充值</span><span class="v">{{ formatMoney(icSelectedCard.purchase_amount ?? icSelectedCard.balance) }}</span></div>
|
||||
<div class="list-item"><span class="k">购卡金额</span><span class="v">{{ formatMoney(icSelectedCard.purchase_amount) }}</span></div>
|
||||
<div class="list-item"><span class="k">创建时间</span><span class="v">{{
|
||||
formatTime(icSelectedCard.created_ts) }}</span></div>
|
||||
<div class="list-item"><span class="k">最后更新</span><span class="v">{{
|
||||
formatTime(icSelectedCard.last_update_ts) }}</span></div>
|
||||
<div class="list-item"><span class="k">首次充值</span><span class="v">{{
|
||||
formatMoney(icSelectedCard.purchase_amount ??
|
||||
icSelectedCard.balance) }}</span></div>
|
||||
<div class="list-item"><span class="k">购卡金额</span><span class="v">{{
|
||||
formatMoney(icSelectedCard.purchase_amount) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -650,14 +723,18 @@
|
||||
<div class="timeline">
|
||||
<div v-if="!icSelectedCard" class="loading">选择卡片后显示事件流。</div>
|
||||
<div v-else-if="!icSelectedEvents.length" class="loading">暂无事件记录。</div>
|
||||
<div v-for="(event, idx) in icSelectedEvents" :key="`${event.ts || 0}-${event.type || 'event'}-${idx}`" class="timeline-item">
|
||||
<div v-for="(event, idx) in icSelectedEvents"
|
||||
:key="`${event.ts || 0}-${event.type || 'event'}-${idx}`"
|
||||
class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="flex between">
|
||||
<span style="font-weight:600;">{{ icEventTitle(event) }}</span>
|
||||
<span class="text-muted" style="font-size:0.8rem;">{{ formatTime(event.ts) }}</span>
|
||||
<span class="text-muted" style="font-size:0.8rem;">{{
|
||||
formatTime(event.ts) }}</span>
|
||||
</div>
|
||||
<div class="log-detail" style="margin-top:8px;">{{ formatIcEventDetail(event) }}</div>
|
||||
<div class="log-detail" style="margin-top:8px;">{{
|
||||
formatIcEventDetail(event) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -686,7 +763,8 @@
|
||||
<div class="flex between mb-4">
|
||||
<h4>车票记录</h4>
|
||||
<div class="flex">
|
||||
<input v-model="ticketSearch" placeholder="搜索 Ticket ID / 站点" style="width: 200px;">
|
||||
<input v-model="ticketSearch" placeholder="搜索 Ticket ID / 站点"
|
||||
style="width: 200px;">
|
||||
<button @click="refreshData"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,7 +795,8 @@
|
||||
<td>
|
||||
<div class="st-container">
|
||||
<div class="st-main-row">
|
||||
<span class="st-name">{{ getStationInfo(t.terminal).name }}</span>
|
||||
<span class="st-name">{{ getStationInfo(t.terminal).name
|
||||
}}</span>
|
||||
<span class="st-code">{{ t.terminal }}</span>
|
||||
</div>
|
||||
<div class="st-en">{{ getStationInfo(t.terminal).en_name }}</div>
|
||||
@@ -767,23 +846,27 @@
|
||||
selectedTicket.index.terminal }}</span>
|
||||
<span class="st-code">{{ selectedTicket.index.terminal }}</span>
|
||||
</div>
|
||||
<div class="st-en">{{ selectedTicket.index.terminal_en || '' }}</div>
|
||||
<div class="st-en">{{ selectedTicket.index.terminal_en || '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted mt-2" style="font-size: 0.9rem;">
|
||||
类型: {{ formatTrainType(selectedTicket.index.type ||
|
||||
selectedTicket.index.train_type) }} | 票价: {{ selectedTicket.index.price ||
|
||||
selectedTicket.index.train_type) }} | 票价: {{ selectedTicket.index.price
|
||||
||
|
||||
selectedTicket.index.cost }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>行程记录</h5>
|
||||
<div class="timeline">
|
||||
<div v-for="ev in selectedTicket.events" :key="ev.ts || ev['时间戳']" class="timeline-item">
|
||||
<div v-for="ev in selectedTicket.events" :key="ev.ts || ev['时间戳']"
|
||||
class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="flex between">
|
||||
<span style="font-weight: 600;">{{ formatTicketEvent(ev) }}</span>
|
||||
<span style="font-weight: 600;">{{ formatTicketEvent(ev)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.9rem; margin-top: 4px;">
|
||||
{{ formatTicketEventUnified(ev) }}
|
||||
@@ -807,50 +890,70 @@
|
||||
<div class="flex" style="gap: 8px; flex-wrap: wrap;">
|
||||
<label class="btn primary" style="cursor: pointer;">
|
||||
<i class="fas fa-upload"></i> 上传线路图
|
||||
<input type="file" accept=".png,.jpg,.jpeg,.webp,.svg" style="display: none;" @change="uploadRouteMap">
|
||||
<input type="file" accept=".png,.jpg,.jpeg,.webp,.svg"
|
||||
style="display: none;" @change="uploadRouteMap">
|
||||
</label>
|
||||
<button class="btn" @click="deleteRouteMap" :disabled="!assetsManifest.routeMap || mutationBusy">
|
||||
<button class="btn" @click="deleteRouteMap"
|
||||
:disabled="!assetsManifest.routeMap || mutationBusy">
|
||||
<i class="fas fa-trash"></i> 删除当前线路图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid" style="display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 16px; align-items: start;">
|
||||
<div style="border: 1px solid var(--border); padding: 16px; background: var(--surface);">
|
||||
<div class="grid"
|
||||
style="display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 16px; align-items: start;">
|
||||
<div
|
||||
style="border: 1px solid var(--border); padding: 16px; background: var(--surface);">
|
||||
<div style="font-weight: 700; margin-bottom: 12px;">上传说明</div>
|
||||
<div class="text-muted" style="display: grid; gap: 8px; line-height: 1.7;">
|
||||
<div>支持格式:PNG、JPG、WEBP、SVG</div>
|
||||
<div>建议上传横向长图,便于首页和购票页展示。</div>
|
||||
<div>再次上传会自动替换旧文件。</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); display: grid; gap: 10px;">
|
||||
<div
|
||||
style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); display: grid; gap: 10px;">
|
||||
<label style="display: grid; gap: 6px;">
|
||||
<span style="font-weight: 700;">线路图署名</span>
|
||||
<input v-model="assetsForm.routeMapCredit" class="input" type="text" maxlength="200" placeholder="例如:OpenStreetMap contributors / 作者名 / 来源名">
|
||||
<input v-model="assetsForm.routeMapCredit" class="input" type="text"
|
||||
maxlength="200"
|
||||
placeholder="例如:OpenStreetMap contributors / 作者名 / 来源名">
|
||||
</label>
|
||||
<div class="text-muted">许可协议固定为 <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC BY 4.0</a></div>
|
||||
<div class="text-muted">许可协议固定为 <a
|
||||
href="https://creativecommons.org/licenses/by/4.0/" target="_blank"
|
||||
rel="noopener noreferrer">CC BY 4.0</a></div>
|
||||
<div class="flex" style="gap: 8px; flex-wrap: wrap;">
|
||||
<button class="btn" @click="saveRouteMapCredit" :disabled="mutationBusy">
|
||||
<button class="btn" @click="saveRouteMapCredit"
|
||||
:disabled="mutationBusy">
|
||||
<i class="fas fa-pen"></i> 保存署名
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); display: grid; gap: 8px;">
|
||||
<div
|
||||
style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); display: grid; gap: 8px;">
|
||||
<div><strong>当前文件:</strong> {{ assetsManifest.routeMap || '未上传' }}</div>
|
||||
<div><strong>当前署名:</strong> {{ assetsManifest.routeMapCredit || '未填写' }}</div>
|
||||
<div><strong>许可协议:</strong> <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">{{ assetsManifest.routeMapLicense }}</a></div>
|
||||
<div><strong>更新时间:</strong> {{ assetsManifest.updatedAt ? formatTime(assetsManifest.updatedAt) : '暂无' }}</div>
|
||||
<div><strong>当前署名:</strong> {{ assetsManifest.routeMapCredit || '未填写' }}
|
||||
</div>
|
||||
<div><strong>许可协议:</strong> <a
|
||||
href="https://creativecommons.org/licenses/by/4.0/" target="_blank"
|
||||
rel="noopener noreferrer">{{ assetsManifest.routeMapLicense }}</a>
|
||||
</div>
|
||||
<div><strong>更新时间:</strong> {{ assetsManifest.updatedAt ?
|
||||
formatTime(assetsManifest.updatedAt) : '暂无' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); background: #0f172a; min-height: 420px; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 16px; box-sizing: border-box;">
|
||||
<div
|
||||
style="border: 1px solid var(--border); background: #0f172a; min-height: 420px; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 16px; box-sizing: border-box;">
|
||||
<div style="width: 100%;">
|
||||
<template v-if="assetsRouteMapUrl">
|
||||
<img :src="assetsRouteMapUrl" alt="线路图预览" style="display: block; max-width: 100%; height: auto; background: #ffffff; margin: 0 auto;">
|
||||
<img :src="assetsRouteMapUrl" alt="线路图预览"
|
||||
style="display: block; max-width: 100%; height: auto; background: #ffffff; margin: 0 auto;">
|
||||
<div class="jr-asset-credit" style="color: #cbd5e1;">
|
||||
<span>线路图署名:{{ assetsManifest.routeMapCredit || '未填写' }}</span>
|
||||
<span>许可协议:</span>
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer" style="color: #93c5fd;">{{ assetsManifest.routeMapLicense }}</a>
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
style="color: #93c5fd;">{{ assetsManifest.routeMapLicense }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-muted" style="padding: 24px; text-align: center;">
|
||||
@@ -879,7 +982,8 @@
|
||||
<div class="flex" style="flex-direction: column; gap: 10px;">
|
||||
<div>
|
||||
<button @click="pushLuaUpdate" :disabled="isPushingUpdate">
|
||||
<i class="fas fa-satellite-dish"></i> {{ isPushingUpdate ? '推送中...' : '手动推送更新' }}
|
||||
<i class="fas fa-satellite-dish"></i> {{ isPushingUpdate ? '推送中...' :
|
||||
'手动推送更新' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.9rem;">
|
||||
@@ -916,7 +1020,8 @@
|
||||
</select>
|
||||
<input v-model="logTypeFilter" placeholder="type (可逗号分隔)" style="width: 260px;">
|
||||
<input v-model="logQuery" placeholder="关键字" style="width: 220px;">
|
||||
<input v-model.number="logMax" type="number" min="10" max="5000" step="10" style="width: 120px;">
|
||||
<input v-model.number="logMax" type="number" min="10" max="5000" step="10"
|
||||
style="width: 120px;">
|
||||
<button @click="fetchLogs" :disabled="logLoading">
|
||||
<i class="fas" :class="logLoading ? 'fa-spinner fa-spin' : 'fa-filter'"></i> 筛选
|
||||
</button>
|
||||
@@ -942,7 +1047,8 @@
|
||||
<td>
|
||||
<details>
|
||||
<summary class="text-muted" style="cursor: pointer;">查看</summary>
|
||||
<pre style="white-space: pre-wrap; margin: 8px 0 0; font-size: 0.85rem; line-height: 1.35;">{{ formatLogDetail(l) }}</pre>
|
||||
<pre
|
||||
style="white-space: pre-wrap; margin: 8px 0 0; font-size: 0.85rem; line-height: 1.35;">{{ formatLogDetail(l) }}</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -976,19 +1082,23 @@
|
||||
<td style="white-space: nowrap;">{{ formatTime(issue.ts) }}</td>
|
||||
<td>
|
||||
<div class="jr-issue-primary mono">{{ issue.displayDevice }}</div>
|
||||
<div v-if="issue.machine_id && issue.machine_id !== issue.displayDevice" class="jr-issue-sub mono">{{ issue.machine_id }}</div>
|
||||
<div v-if="issue.machine_id && issue.machine_id !== issue.displayDevice"
|
||||
class="jr-issue-sub mono">{{ issue.machine_id }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="jr-issue-primary">{{ issue.displayStation }}</div>
|
||||
<div v-if="issue.station_code" class="jr-issue-sub mono">{{ issue.station_code }}</div>
|
||||
<div v-if="issue.station_code" class="jr-issue-sub mono">{{
|
||||
issue.station_code }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="jr-issue-error">{{ issue.displayError }}</div>
|
||||
</td>
|
||||
<td class="jr-issue-detail-cell">
|
||||
<div v-if="issue.detailEntries && issue.detailEntries.length" class="jr-issue-detail-card">
|
||||
<div v-if="issue.detailEntries && issue.detailEntries.length"
|
||||
class="jr-issue-detail-card">
|
||||
<div class="jr-issue-detail-grid">
|
||||
<div v-for="entry in issue.detailEntries" :key="`${idx}-${entry.key}`" class="jr-issue-detail-item">
|
||||
<div v-for="entry in issue.detailEntries"
|
||||
:key="`${idx}-${entry.key}`" class="jr-issue-detail-item">
|
||||
<span>{{ entry.label }}</span>
|
||||
<strong>{{ entry.value }}</strong>
|
||||
</div>
|
||||
@@ -1007,14 +1117,16 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="empty-state">
|
||||
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 16px; opacity: 0.3; color: var(--success);"></i>
|
||||
<i class="fas fa-check-circle"
|
||||
style="font-size: 48px; margin-bottom: 16px; opacity: 0.3; color: var(--success);"></i>
|
||||
<p>暂无问题上报记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank"
|
||||
rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||
<span class="version">v1.0.12</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+292
@@ -4647,6 +4647,298 @@ body.jr-admin-login-page {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Route 页面移动端专项适配 ========== */
|
||||
@media (max-width: 980px) {
|
||||
body.jr-admin-route-page {
|
||||
overflow: auto;
|
||||
}
|
||||
body.jr-admin-route-page #app {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
}
|
||||
.jr-admin-route-page .jr-admin-app {
|
||||
min-height: auto;
|
||||
border-radius: 0;
|
||||
}
|
||||
.jr-admin-route-page .jr-admin-main-shell {
|
||||
width: min(100%, calc(100% - 24px));
|
||||
padding: 12px 0 24px;
|
||||
}
|
||||
.jr-admin-route-page .jr-admin-app {
|
||||
flex-direction: column;
|
||||
}
|
||||
.jr-admin-route-page .sidebar {
|
||||
width: 86%;
|
||||
max-width: 340px;
|
||||
}
|
||||
.jr-admin-route-page .jr-admin-header-side {
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body.jr-admin-route-page .jr-topbar-inner {
|
||||
min-height: 52px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
gap: 6px;
|
||||
}
|
||||
body.jr-admin-route-page .jr-top-status {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
body.jr-admin-route-page .jr-brandbar-inner {
|
||||
min-height: auto;
|
||||
padding: 14px 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
body.jr-admin-route-page .jr-nav {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
body.jr-admin-route-page .jr-nav a {
|
||||
min-width: 88px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .jr-admin-page .header,
|
||||
.jr-admin-route-page .jr-admin-page .content {
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
.jr-admin-route-page .content {
|
||||
padding: 14px;
|
||||
}
|
||||
.jr-admin-route-page .header {
|
||||
padding: 12px 14px;
|
||||
min-height: 60px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .jr-page-intro {
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.jr-admin-route-page .jr-page-intro h1 {
|
||||
font-size: clamp(1.4rem, 6vw, 2rem);
|
||||
}
|
||||
.jr-admin-route-page .jr-page-intro p {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .card {
|
||||
padding: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .management-container {
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.jr-admin-route-page .management-sidebar {
|
||||
width: 100%;
|
||||
max-height: 380px;
|
||||
}
|
||||
.jr-admin-route-page .management-main {
|
||||
overflow: visible;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .editor-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
.jr-admin-route-page .editor-toolbar > .flex:first-child {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.jr-admin-route-page .switch-label {
|
||||
width: 100%;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.jr-admin-route-page .hint-text {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
.jr-admin-route-page .editor-toolbar > .flex:last-child {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.jr-admin-route-page .editor-toolbar > .flex:last-child input,
|
||||
.jr-admin-route-page .editor-toolbar > .flex:last-child button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .visual-line-container {
|
||||
padding: 16px 10px;
|
||||
min-height: 280px;
|
||||
cursor: default;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.jr-admin-route-page .visual-line-container svg {
|
||||
min-width: 720px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .faremap-card .flex.between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.jr-admin-route-page .faremap-card .flex.between > .flex:last-child {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.jr-admin-route-page .faremap-card .flex.between > .flex:last-child button {
|
||||
flex: 1;
|
||||
min-width: 52px;
|
||||
}
|
||||
.jr-admin-route-page .faremap-viewport {
|
||||
max-height: 60vh;
|
||||
}
|
||||
.jr-admin-route-page .faremap-canvas {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .ticket-table,
|
||||
.jr-admin-route-page table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.jr-admin-route-page .ticket-table th,
|
||||
.jr-admin-route-page .ticket-table td,
|
||||
.jr-admin-route-page table th,
|
||||
.jr-admin-route-page table td {
|
||||
padding: 10px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .modal-card {
|
||||
width: 96%;
|
||||
max-width: 96%;
|
||||
padding: 18px 16px;
|
||||
max-height: 92vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
.jr-admin-route-page .modal-title {
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.jr-admin-route-page .modal-actions {
|
||||
flex-direction: column-reverse;
|
||||
margin-top: 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
.jr-admin-route-page .modal-actions button {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
}
|
||||
.jr-admin-route-page select[multiple] {
|
||||
height: 160px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.jr-admin-route-page button,
|
||||
.jr-admin-route-page .btn {
|
||||
min-height: 44px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
.jr-admin-route-page button.sm,
|
||||
.jr-admin-route-page .btn.sm {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
.jr-admin-route-page .icon-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.jr-admin-route-page input,
|
||||
.jr-admin-route-page select {
|
||||
min-height: 44px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .line-item {
|
||||
padding: 14px 12px;
|
||||
min-height: 56px;
|
||||
}
|
||||
.jr-admin-route-page .line-item .line-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.jr-admin-route-page .line-item .line-actions button {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.jr-admin-route-page .site-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.jr-admin-route-page .header h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.jr-admin-route-page .jr-admin-header-pill {
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.jr-admin-route-page .badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.jr-admin-route-page h4 {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.jr-admin-route-page h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.jr-admin-route-page .jr-brand-copy strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.jr-admin-route-page .jr-brand-copy span {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.jr-admin-route-page .jr-brand-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.jr-admin-route-page .faremap-viewport {
|
||||
max-height: 50vh;
|
||||
}
|
||||
.jr-admin-route-page .visual-line-container svg {
|
||||
min-width: 640px;
|
||||
}
|
||||
.jr-admin-route-page #app .ticket-header .flex.between:last-of-type {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
| ||||