Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>[[ project.title_cn ]] - [[ project.name ]]</title> | |
| <script src="/static/js/tailwindcss.js"></script> | |
| <script src="/static/js/vue.global.js"></script> | |
| <script src="/static/js/html2canvas.min.js"></script> | |
| <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: #f1f1f1; } | |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } | |
| .marker-pulse { | |
| animation: pulse-animation 2s infinite; | |
| } | |
| @keyframes pulse-animation { | |
| 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } | |
| } | |
| .map-container { | |
| position: relative; | |
| overflow: hidden; | |
| user-select: none; | |
| cursor: crosshair; | |
| } | |
| .map-container.preview-mode { | |
| cursor: default; | |
| } | |
| .marker { | |
| position: absolute; | |
| transform: translate(-50%, -50%); | |
| transition: all 0.2s ease; | |
| z-index: 10; | |
| } | |
| .marker:hover { | |
| z-index: 20; | |
| transform: translate(-50%, -50%) scale(1.2); | |
| } | |
| .tooltip-card { | |
| position: absolute; | |
| z-index: 30; | |
| background: white; | |
| border-radius: 0.5rem; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| padding: 1rem; | |
| width: 250px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| transform: translate(-50%, -110%); /* Default above */ | |
| } | |
| .tooltip-card.active { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| /* Arrow */ | |
| .tooltip-card::after { | |
| content: ""; | |
| position: absolute; | |
| top: 100%; | |
| left: 50%; | |
| margin-left: -8px; | |
| border-width: 8px; | |
| border-style: solid; | |
| border-color: white transparent transparent transparent; | |
| } | |
| [v-cloak] { display: none; } | |
| </style> | |
| </head> | |
| <body class="bg-slate-50 text-slate-800 h-screen flex flex-col overflow-hidden"> | |
| <div id="app" v-cloak class="flex flex-col h-full"> | |
| <!-- Header --> | |
| <header class="bg-white border-b border-slate-200 px-6 py-3 flex justify-between items-center shadow-sm z-50"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xl"> | |
| <i class="fa-solid fa-map-location-dot"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg text-slate-800">[[ project.title_cn ]]</h1> | |
| <p class="text-xs text-slate-500">v[[ project.version ]]</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <button @click="isPreview = !isPreview" | |
| :class="isPreview ? 'bg-indigo-100 text-indigo-700 border-indigo-200' : 'bg-white border-slate-300 hover:bg-slate-50'" | |
| class="px-4 py-2 rounded-lg border text-sm font-medium flex items-center gap-2 transition-colors"> | |
| <i :class="isPreview ? 'fa-solid fa-eye' : 'fa-regular fa-eye'"></i> | |
| {{ isPreview ? '退出预览' : '预览交互' }} | |
| </button> | |
| <button @click="exportProject" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 shadow-sm transition-colors"> | |
| <i class="fa-solid fa-download"></i> | |
| 导出 HTML | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex overflow-hidden"> | |
| <!-- Left Sidebar (Settings) --> | |
| <aside class="w-80 bg-white border-r border-slate-200 flex flex-col z-40"> | |
| <div class="p-4 border-b border-slate-100"> | |
| <h2 class="font-semibold text-slate-800 mb-4">地图设置</h2> | |
| <!-- Image Upload --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-slate-700 mb-2">底图上传</label> | |
| <div class="relative border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-blue-500 transition-colors bg-slate-50 cursor-pointer" | |
| @dragover.prevent @drop.prevent="handleDrop"> | |
| <input type="file" ref="fileInput" @change="handleFileChange" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> | |
| <div v-if="!bgImage"> | |
| <i class="fa-regular fa-image text-2xl text-slate-400 mb-2"></i> | |
| <p class="text-xs text-slate-500">点击或拖拽上传图片</p> | |
| <button @click.stop="loadDemoImage" class="mt-2 text-xs text-blue-600 hover:underline">使用演示地图</button> | |
| </div> | |
| <div v-else class="relative h-20 w-full"> | |
| <img :src="bgImage" class="h-full w-full object-cover rounded"> | |
| <button @click.stop="bgImage = null; markers = []" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs hover:bg-red-600">×</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Marker Editor (Only visible when a marker is selected) --> | |
| <div v-if="selectedMarkerIndex !== -1" class="animate-fade-in"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h3 class="font-medium text-slate-700 text-sm">编辑热点 #{{ selectedMarkerIndex + 1 }}</h3> | |
| <button @click="deleteMarker(selectedMarkerIndex)" class="text-red-500 text-xs hover:text-red-700"> | |
| <i class="fa-solid fa-trash"></i> 删除 | |
| </button> | |
| </div> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1">标题</label> | |
| <input v-model="markers[selectedMarkerIndex].title" type="text" class="w-full px-3 py-2 border border-slate-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" placeholder="输入标题..."> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1">描述内容</label> | |
| <textarea v-model="markers[selectedMarkerIndex].desc" rows="3" class="w-full px-3 py-2 border border-slate-300 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" placeholder="输入描述..."></textarea> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1">图标</label> | |
| <select v-model="markers[selectedMarkerIndex].icon" class="w-full px-2 py-2 border border-slate-300 rounded text-sm bg-white"> | |
| <option value="fa-location-dot">📍 定位</option> | |
| <option value="fa-circle-info">ℹ️ 信息</option> | |
| <option value="fa-star">⭐ 星标</option> | |
| <option value="fa-camera">📷 相机</option> | |
| <option value="fa-shop">🏪 商店</option> | |
| <option value="fa-restroom">🚻 洗手间</option> | |
| <option value="fa-utensils">🍴 餐饮</option> | |
| <option value="fa-plus">➕ 加号</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1">颜色</label> | |
| <input type="color" v-model="markers[selectedMarkerIndex].color" class="w-full h-9 border border-slate-300 rounded cursor-pointer"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-else class="text-center py-8 text-slate-400 bg-slate-50 rounded-lg border border-dashed border-slate-200"> | |
| <p class="text-sm">点击地图任意位置<br>添加新热点</p> | |
| </div> | |
| </div> | |
| <!-- Markers List --> | |
| <div class="flex-1 overflow-y-auto p-4"> | |
| <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3">热点列表 ({{ markers.length }})</h3> | |
| <div class="space-y-2"> | |
| <div v-for="(marker, index) in markers" :key="index" | |
| @click="selectMarker(index)" | |
| :class="{'border-blue-500 ring-1 ring-blue-500 bg-blue-50': selectedMarkerIndex === index, 'border-slate-200 hover:border-blue-300': selectedMarkerIndex !== index}" | |
| class="p-3 border rounded-lg cursor-pointer transition-all flex items-start gap-3 bg-white"> | |
| <div class="w-6 h-6 rounded-full flex items-center justify-center text-white text-xs shrink-0" :style="{backgroundColor: marker.color}"> | |
| <i class="fa-solid" :class="marker.icon"></i> | |
| </div> | |
| <div class="overflow-hidden"> | |
| <div class="font-medium text-sm truncate text-slate-700">{{ marker.title || '未命名热点' }}</div> | |
| <div class="text-xs text-slate-500 truncate">{{ marker.desc || '无描述' }}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Canvas Area --> | |
| <main class="flex-1 bg-slate-100 relative overflow-auto flex items-center justify-center p-8"> | |
| <div v-if="!bgImage" class="text-center text-slate-400"> | |
| <i class="fa-regular fa-image text-6xl mb-4 opacity-30"></i> | |
| <p>请先上传底图</p> | |
| </div> | |
| <div v-else | |
| ref="mapContainer" | |
| class="map-container shadow-2xl rounded-lg bg-white relative inline-block" | |
| :class="{'preview-mode': isPreview}" | |
| @click="handleMapClick"> | |
| <img :src="bgImage" class="max-w-none" style="max-height: 80vh; display: block;"> | |
| <!-- Markers --> | |
| <div v-for="(marker, index) in markers" :key="index" | |
| class="marker w-8 h-8 rounded-full flex items-center justify-center text-white shadow-lg cursor-pointer" | |
| :class="{'marker-pulse': selectedMarkerIndex === index && !isPreview}" | |
| :style="{ | |
| left: marker.x + '%', | |
| top: marker.y + '%', | |
| backgroundColor: marker.color, | |
| fontSize: '14px' | |
| }" | |
| @click.stop="handleMarkerClick(index)"> | |
| <i class="fa-solid" :class="marker.icon"></i> | |
| <!-- Tooltip (Only visible in preview or if active) --> | |
| <div v-if="isPreview && activeTooltipIndex === index" | |
| class="tooltip-card active" | |
| @click.stop> | |
| <h4 class="font-bold text-slate-800 mb-1">{{ marker.title }}</h4> | |
| <p class="text-sm text-slate-600 leading-relaxed">{{ marker.desc }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <script> | |
| </script> | |
| </body> | |
| </html> | |