feat(web): 新增后台线路拓扑图布局编辑器,允许手动拖拽调整所有站点坐标
This commit is contained in:
@@ -16,8 +16,8 @@
|
||||
"mtimeMs": 1783602619602.658
|
||||
},
|
||||
"ticket-order.js": {
|
||||
"size": 37721,
|
||||
"mtimeMs": 1783947073044.0798
|
||||
"size": 38975,
|
||||
"mtimeMs": 1783947975169.5613
|
||||
},
|
||||
"ticket-search.js": {
|
||||
"size": 17269,
|
||||
@@ -28,8 +28,8 @@
|
||||
"mtimeMs": 1783666241687.1482
|
||||
},
|
||||
"index.js": {
|
||||
"size": 82754,
|
||||
"mtimeMs": 1783779774480.386
|
||||
"size": 93210,
|
||||
"mtimeMs": 1783947920227.9448
|
||||
},
|
||||
"ic-card-admin.js": {
|
||||
"size": 19095,
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"blog.css": 2,
|
||||
"custom-dialog.js": 11,
|
||||
"public-status.js": 14,
|
||||
"ticket-order.js": 30,
|
||||
"ticket-order.js": 31,
|
||||
"ticket-search.js": 12,
|
||||
"ticket-route.js": 3,
|
||||
"index.js": 5,
|
||||
"index.js": 6,
|
||||
"ic-card-admin.js": 3,
|
||||
"ic-card-order.js": 3,
|
||||
"ic-card-detail.js": 2,
|
||||
|
||||
+63
-18
@@ -74,7 +74,7 @@
|
||||
<span class="nav-icon"><i class="fas fa-credit-card"></i></span> IC卡管理
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'assets'}" @click="currentView = 'assets'">
|
||||
<span class="nav-icon"><i class="fas fa-route"></i></span> 线路图
|
||||
<span class="nav-icon"><i class="fas fa-route"></i></span> 拓扑图布局
|
||||
</div>
|
||||
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="currentView = 'settings'">
|
||||
<span class="nav-icon"><i class="fas fa-cog"></i></span> 系统设置
|
||||
@@ -794,31 +794,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentView === 'assets'">
|
||||
<div class="card mb-4">
|
||||
<div v-if="currentView === 'assets'" class="assets-editor-container" style="display: flex; flex-direction: column; height: 80vh;">
|
||||
<div class="card mb-4" style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div class="flex between mb-4" style="flex-wrap: wrap; gap: 10px;">
|
||||
<div class="flex" style="align-items: center; gap: 10px;">
|
||||
<h4>线路图</h4>
|
||||
<span class="badge" v-if="assetsManifest.routeMap" style="font-family: monospace;">{{ assetsManifest.routeMap }}</span>
|
||||
<h4>线路拓扑图布局编辑器</h4>
|
||||
<span class="badge" style="background: #2563eb; color: #fff;">拖拽站点以自定义位置</span>
|
||||
</div>
|
||||
<div class="flex" style="gap: 8px;">
|
||||
<label class="btn" style="cursor: pointer;">
|
||||
<i class="fas fa-upload"></i> 上传线路图
|
||||
<input type="file" hidden accept=".png,.jpg,.jpeg,.webp,.svg" @change="uploadRouteMap">
|
||||
</label>
|
||||
<a v-if="assetsRouteMapUrl" :href="assetsRouteMapUrl" target="_blank" class="btn">
|
||||
<i class="fas fa-external-link-alt"></i> 打开
|
||||
</a>
|
||||
<button v-if="assetsManifest.routeMap" class="danger" @click="deleteRouteMap">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
<button class="btn" @click="resetTopologyLayout">
|
||||
<i class="fas fa-undo"></i> 重置布局
|
||||
</button>
|
||||
<button class="btn primary" @click="saveTopologyLayout">
|
||||
<i class="fas fa-save"></i> 保存坐标
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="assetsRouteMapUrl" style="border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0b0b0f; padding: 16px;">
|
||||
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;" ref="mapImageRef">
|
||||
<div style="flex: 1; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0f172a; position: relative; user-select: none;"
|
||||
ref="topologyCanvas"
|
||||
@mousedown="onTopologyCanvasMouseDown"
|
||||
@mousemove="onTopologyCanvasMouseMove"
|
||||
@mouseup="onTopologyCanvasMouseUp"
|
||||
@mouseleave="onTopologyCanvasMouseUp"
|
||||
@wheel="onTopologyCanvasWheel">
|
||||
<div :style="{
|
||||
position: 'absolute',
|
||||
left: '0', top: '0',
|
||||
width: '100%', height: '100%',
|
||||
transformOrigin: '0 0',
|
||||
transform: `translate(${topologyPan.x}px, ${topologyPan.y}px) scale(${topologyPan.scale})`
|
||||
}">
|
||||
<!-- Grid Background (Optional) -->
|
||||
<svg width="10000" height="10000" style="position: absolute; left: -5000px; top: -5000px; pointer-events: none;">
|
||||
<defs>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<rect width="40" height="40" fill="none" stroke="rgba(255,255,255,0.05)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="10000" height="10000" fill="url(#grid)" />
|
||||
<!-- Lines -->
|
||||
<path v-for="l in topologyLines" :key="'path-'+l.id" :d="l.d" fill="none" :stroke="l.color" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.6" />
|
||||
</svg>
|
||||
|
||||
<!-- Stations -->
|
||||
<div v-for="node in topologyNodes" :key="node.canonical"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: node.x + 'px',
|
||||
top: node.y + 'px',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
cursor: 'grab',
|
||||
zIndex: node.canonical === topologyDraggingNode ? 100 : 10
|
||||
}"
|
||||
@mousedown.stop="onTopologyNodeMouseDown($event, node)">
|
||||
|
||||
<div v-if="node.isTransfer" style="width: 24px; height: 24px; border-radius: 50%; background: #fff; border: 3px solid #1f2937; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 6px rgba(0,0,0,0.3);">
|
||||
<div style="width: 10px; height: 10px; border-radius: 50%; background: #cbd5e1;"></div>
|
||||
</div>
|
||||
<div v-else :style="{
|
||||
width: '20px', height: '20px', borderRadius: '50%', background: '#fff',
|
||||
border: `4px solid ${node.color || '#94a3b8'}`,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
||||
}"></div>
|
||||
|
||||
<div style="position: absolute; top: 26px; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 12px; font-weight: 600; color: #f8fafc; text-shadow: 0 1px 3px rgba(0,0,0,0.8); pointer-events: none;">
|
||||
{{ node.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading">未上传线路图</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -920,7 +965,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="/custom-dialog.js?v=11"></script>
|
||||
<script src="index.js?v=5"></script>
|
||||
<script src="index.js?v=6"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isDomain = location.hostname.includes('fse-media.group');
|
||||
|
||||
+239
@@ -121,6 +121,13 @@ createApp({
|
||||
const lineFormOriginalId = ref('');
|
||||
const lineForm = reactive({ id: '', name: '', en_name: '', color: '#3366cc', stations: [] });
|
||||
|
||||
// Topology Editor State
|
||||
const topologyPan = reactive({ x: 0, y: 0, scale: 1, active: false, startX: 0, startY: 0, initialX: 0, initialY: 0 });
|
||||
const topologyNodes = ref([]);
|
||||
const topologyLines = ref([]);
|
||||
const topologyDraggingNode = ref(null);
|
||||
let topologyNodeDragStart = { x: 0, y: 0, nodeX: 0, nodeY: 0 };
|
||||
|
||||
// Legacy/Other State
|
||||
const fareMapSvg = ref('');
|
||||
const fareMapScale = ref(1);
|
||||
@@ -1073,6 +1080,9 @@ createApp({
|
||||
const found = lines.value.find((line) => line.id === selectedLine.value.id);
|
||||
selectedLine.value = found || null;
|
||||
}
|
||||
if (currentView.value === 'assets') {
|
||||
initTopologyLayout();
|
||||
}
|
||||
coreLoaded = true;
|
||||
markSynced();
|
||||
} catch (e) {
|
||||
@@ -1605,6 +1615,227 @@ createApp({
|
||||
syncSelectedIcCard().catch(console.error);
|
||||
});
|
||||
|
||||
// Topology Editor Logic
|
||||
const initTopologyLayout = () => {
|
||||
const nodesMap = new Map();
|
||||
const linesArr = lines.value || [];
|
||||
const stationsArr = stations.value || [];
|
||||
|
||||
// Helper to get transfer canonicals
|
||||
const canonicals = {};
|
||||
const isTransfer = {};
|
||||
stationsArr.forEach(s => {
|
||||
const code = String(s.code || '').trim();
|
||||
canonicals[code] = code;
|
||||
isTransfer[code] = false;
|
||||
if (Array.isArray(s.transfer_to) && s.transfer_to.length > 0) {
|
||||
const group = [code, ...s.transfer_to].map(x => String(x).trim()).sort();
|
||||
const canonical = group[0];
|
||||
group.forEach(g => canonicals[g] = canonical);
|
||||
}
|
||||
});
|
||||
|
||||
// Count lines per canonical to identify transfer hubs
|
||||
const lineCountByCanonical = {};
|
||||
linesArr.forEach(line => {
|
||||
const stops = Array.isArray(line.stations) ? line.stations : [];
|
||||
stops.forEach(stop => {
|
||||
if (stop === '-') return;
|
||||
const canonical = canonicals[stop] || stop;
|
||||
if (!lineCountByCanonical[canonical]) lineCountByCanonical[canonical] = new Set();
|
||||
lineCountByCanonical[canonical].add(line.id);
|
||||
});
|
||||
});
|
||||
Object.keys(lineCountByCanonical).forEach(c => {
|
||||
if (lineCountByCanonical[c].size > 1) {
|
||||
isTransfer[c] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Nodes
|
||||
stationsArr.forEach((s, idx) => {
|
||||
const code = String(s.code || '').trim();
|
||||
const canonical = canonicals[code] || code;
|
||||
if (nodesMap.has(canonical)) return;
|
||||
|
||||
nodesMap.set(canonical, {
|
||||
canonical,
|
||||
code: code,
|
||||
name: s.name || code,
|
||||
color: null,
|
||||
isTransfer: isTransfer[canonical],
|
||||
x: Number(s.svg_x) || (100 + (idx % 10) * 80),
|
||||
y: Number(s.svg_y) || (100 + Math.floor(idx / 10) * 80),
|
||||
originalX: Number(s.svg_x) || (100 + (idx % 10) * 80),
|
||||
originalY: Number(s.svg_y) || (100 + Math.floor(idx / 10) * 80)
|
||||
});
|
||||
});
|
||||
|
||||
// Assign colors to nodes based on lines
|
||||
linesArr.forEach(line => {
|
||||
const stops = Array.isArray(line.stations) ? line.stations : [];
|
||||
stops.forEach(stop => {
|
||||
if (stop === '-') return;
|
||||
const canonical = canonicals[stop] || stop;
|
||||
const node = nodesMap.get(canonical);
|
||||
if (node && !node.color) {
|
||||
node.color = line.color || '#93a2b7';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
topologyNodes.value = Array.from(nodesMap.values());
|
||||
updateTopologyLines();
|
||||
};
|
||||
|
||||
const updateTopologyLines = () => {
|
||||
const canonicals = {};
|
||||
stations.value.forEach(s => {
|
||||
const code = String(s.code || '').trim();
|
||||
canonicals[code] = code;
|
||||
if (Array.isArray(s.transfer_to) && s.transfer_to.length > 0) {
|
||||
const group = [code, ...s.transfer_to].map(x => String(x).trim()).sort();
|
||||
const canonical = group[0];
|
||||
group.forEach(g => canonicals[g] = canonical);
|
||||
}
|
||||
});
|
||||
|
||||
const newLines = [];
|
||||
(lines.value || []).forEach(line => {
|
||||
const stops = Array.isArray(line.stations) ? line.stations : [];
|
||||
let d = '';
|
||||
let isFirst = true;
|
||||
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const stop = stops[i];
|
||||
if (stop === '-') {
|
||||
isFirst = true;
|
||||
continue;
|
||||
}
|
||||
const canonical = canonicals[stop] || stop;
|
||||
const node = topologyNodes.value.find(n => n.canonical === canonical);
|
||||
if (!node) continue;
|
||||
|
||||
if (isFirst) {
|
||||
d += `M ${node.x} ${node.y} `;
|
||||
isFirst = false;
|
||||
} else {
|
||||
d += `L ${node.x} ${node.y} `;
|
||||
}
|
||||
}
|
||||
if (d) {
|
||||
newLines.push({ id: line.id, color: line.color || '#93a2b7', d });
|
||||
}
|
||||
});
|
||||
topologyLines.value = newLines;
|
||||
};
|
||||
|
||||
const onTopologyCanvasMouseDown = (e) => {
|
||||
topologyPan.active = true;
|
||||
topologyPan.startX = e.clientX;
|
||||
topologyPan.startY = e.clientY;
|
||||
topologyPan.initialX = topologyPan.x;
|
||||
topologyPan.initialY = topologyPan.y;
|
||||
};
|
||||
|
||||
const onTopologyCanvasMouseMove = (e) => {
|
||||
if (topologyPan.active) {
|
||||
topologyPan.x = topologyPan.initialX + (e.clientX - topologyPan.startX);
|
||||
topologyPan.y = topologyPan.initialY + (e.clientY - topologyPan.startY);
|
||||
} else if (topologyDraggingNode.value) {
|
||||
const node = topologyNodes.value.find(n => n.canonical === topologyDraggingNode.value);
|
||||
if (node) {
|
||||
const dx = (e.clientX - topologyNodeDragStart.x) / topologyPan.scale;
|
||||
const dy = (e.clientY - topologyNodeDragStart.y) / topologyPan.scale;
|
||||
// Snap to grid of 20
|
||||
node.x = Math.round((topologyNodeDragStart.nodeX + dx) / 20) * 20;
|
||||
node.y = Math.round((topologyNodeDragStart.nodeY + dy) / 20) * 20;
|
||||
updateTopologyLines();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onTopologyCanvasMouseUp = () => {
|
||||
topologyPan.active = false;
|
||||
topologyDraggingNode.value = null;
|
||||
};
|
||||
|
||||
const onTopologyCanvasWheel = (e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
topologyPan.scale = Math.max(0.1, Math.min(3, topologyPan.scale * delta));
|
||||
};
|
||||
|
||||
const onTopologyNodeMouseDown = (e, node) => {
|
||||
topologyDraggingNode.value = node.canonical;
|
||||
topologyNodeDragStart.x = e.clientX;
|
||||
topologyNodeDragStart.y = e.clientY;
|
||||
topologyNodeDragStart.nodeX = node.x;
|
||||
topologyNodeDragStart.nodeY = node.y;
|
||||
};
|
||||
|
||||
const resetTopologyLayout = () => {
|
||||
if (!confirm('确定要放弃当前修改并重置吗?')) return;
|
||||
topologyNodes.value.forEach(node => {
|
||||
node.x = node.originalX;
|
||||
node.y = node.originalY;
|
||||
});
|
||||
topologyPan.x = 0;
|
||||
topologyPan.y = 0;
|
||||
topologyPan.scale = 1;
|
||||
updateTopologyLines();
|
||||
};
|
||||
|
||||
const saveTopologyLayout = async () => {
|
||||
if (!confirm('确定要保存当前站点坐标吗?这会覆盖所有站点的 svg_x 和 svg_y。')) return;
|
||||
|
||||
mutationBusy.value = true;
|
||||
try {
|
||||
// We need to update svg_x and svg_y for all stations that belong to each canonical group
|
||||
const canonicals = {};
|
||||
stations.value.forEach(s => {
|
||||
const code = String(s.code || '').trim();
|
||||
canonicals[code] = code;
|
||||
if (Array.isArray(s.transfer_to) && s.transfer_to.length > 0) {
|
||||
const group = [code, ...s.transfer_to].map(x => String(x).trim()).sort();
|
||||
const canonical = group[0];
|
||||
group.forEach(g => canonicals[g] = canonical);
|
||||
}
|
||||
});
|
||||
|
||||
const updates = [];
|
||||
for (const s of stations.value) {
|
||||
const code = String(s.code || '').trim();
|
||||
const canonical = canonicals[code] || code;
|
||||
const node = topologyNodes.value.find(n => n.canonical === canonical);
|
||||
if (node) {
|
||||
updates.push({
|
||||
...s,
|
||||
svg_x: node.x,
|
||||
svg_y: node.y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch update one by one
|
||||
for (const update of updates) {
|
||||
await fetch(`/api/admin/stations/${encodeURIComponent(update.code)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(update)
|
||||
});
|
||||
}
|
||||
|
||||
alert('坐标保存成功!');
|
||||
await fetchData();
|
||||
initTopologyLayout(); // Re-initialize to update originalX/Y
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message);
|
||||
} finally {
|
||||
mutationBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(currentView, (v) => {
|
||||
sidebarOpen.value = false;
|
||||
if (v === 'iccards') {
|
||||
@@ -1612,6 +1843,9 @@ createApp({
|
||||
} else {
|
||||
stopIcCardSync();
|
||||
}
|
||||
if (v === 'assets') {
|
||||
setTimeout(initTopologyLayout, 300);
|
||||
}
|
||||
ensureViewData(v).catch(console.error);
|
||||
const sp = new URLSearchParams(location.search);
|
||||
if (v === 'dashboard') sp.delete('view');
|
||||
@@ -1787,6 +2021,11 @@ createApp({
|
||||
|
||||
saveCurrentFare, deleteCurrentFare, closeFareModal,
|
||||
|
||||
// Topology Editor
|
||||
topologyPan, topologyNodes, topologyLines, topologyDraggingNode,
|
||||
onTopologyCanvasMouseDown, onTopologyCanvasMouseMove, onTopologyCanvasMouseUp, onTopologyCanvasWheel, onTopologyNodeMouseDown,
|
||||
resetTopologyLayout, saveTopologyLayout,
|
||||
|
||||
saveConfig, isPushingUpdate, pushLuaUpdate, exportData, exportFareMap,
|
||||
formatTime, formatLogDetail, getStationName, getStationInfo, loadFareMap, fetchData, refreshData: fetchData,
|
||||
fareMapScale, fareMapLoading, fareMapError, zoomFareMapIn, zoomFareMapOut, zoomFareMapReset,
|
||||
|
||||
+29
-3
@@ -209,11 +209,29 @@
|
||||
}
|
||||
|
||||
stationXByCanonical = {};
|
||||
stationYByCanonical = {};
|
||||
const customXByCanonical = {};
|
||||
const customYByCanonical = {};
|
||||
|
||||
for (const s of stationsData) {
|
||||
const code = String(s.code || s.编号 || '').trim();
|
||||
if (!code) continue;
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
if (s.svg_x != null && s.svg_y != null) {
|
||||
customXByCanonical[canonical] = Number(s.svg_x);
|
||||
customYByCanonical[canonical] = Number(s.svg_y);
|
||||
}
|
||||
}
|
||||
|
||||
for (const canonical of Object.keys(occurrences)) {
|
||||
if (customXByCanonical[canonical] != null) {
|
||||
stationXByCanonical[canonical] = customXByCanonical[canonical];
|
||||
} else {
|
||||
const arr = occurrences[canonical];
|
||||
const avg = arr.reduce((a,b)=>a+b,0) / Math.max(1, arr.length);
|
||||
stationXByCanonical[canonical] = startX + Math.round(avg);
|
||||
}
|
||||
}
|
||||
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
for (const li of lineStops) {
|
||||
@@ -221,6 +239,10 @@
|
||||
for (const code of li.stops) {
|
||||
if (code === '-') continue;
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
if (customXByCanonical[canonical] != null) {
|
||||
prevX = customXByCanonical[canonical];
|
||||
continue;
|
||||
}
|
||||
const x = stationXByCanonical[canonical] ?? (prevX + minGapX);
|
||||
const nx = Math.max(x, prevX + minGapX);
|
||||
if (stationXByCanonical[canonical] == null || nx > stationXByCanonical[canonical]) {
|
||||
@@ -234,18 +256,21 @@
|
||||
// 3. Assign Grid Y to canonical stations (Average of their lines)
|
||||
const hubYByCanonical = {};
|
||||
for (const canonical of Object.keys(lineIdxsByCanonical)) {
|
||||
if (customYByCanonical[canonical] != null) {
|
||||
hubYByCanonical[canonical] = customYByCanonical[canonical];
|
||||
continue;
|
||||
}
|
||||
const idxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
|
||||
if (!idxs.length) continue;
|
||||
const ys = idxs.map(idx => baseY + idx * lineGapY);
|
||||
hubYByCanonical[canonical] = ys.reduce((sum, y) => sum + y, 0) / ys.length;
|
||||
}
|
||||
|
||||
stationYByCanonical = {};
|
||||
for (const li of lineStops) {
|
||||
const yNode = baseY + li.idx * lineGapY;
|
||||
for (const c of li.stops) {
|
||||
const canonical = stationCanonicalByCode[c] || c;
|
||||
stationYByCanonical[c] = hubYByCanonical[canonical] ?? yNode;
|
||||
stationYByCanonical[c] = customYByCanonical[canonical] ?? hubYByCanonical[canonical] ?? yNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,8 +421,9 @@
|
||||
const allCanonicals = Object.keys(stationXByCanonical);
|
||||
const minX = Math.min(...allCanonicals.map(c => stationXByCanonical[c] || 0));
|
||||
const maxX = Math.max(...allCanonicals.map(c => stationXByCanonical[c] || 0));
|
||||
const maxY = Math.max(...allCanonicals.map(c => stationYByCanonical[c] || 0));
|
||||
const width = Math.max(560, maxX + 200);
|
||||
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 120);
|
||||
const height = Math.max(260, maxY + 120, baseY + lineStops.length * lineGapY + 120);
|
||||
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
||||
|
||||
} catch(e) {
|
||||
|
||||
Reference in New Issue
Block a user