Spaces:
Sleeping
Sleeping
Trae Assistant
Initial commit: Enhanced Queue Strategy Lab with Import/Export and Error Handling
6c4d394
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>智能排队策略实验室 | Queue Strategy Lab</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Glassmorphism */ | |
| .glass { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| body { | |
| background-color: #0f172a; /* Slate 900 */ | |
| color: #e2e8f0; | |
| } | |
| .input-group label { | |
| display: block; | |
| font-size: 0.875rem; | |
| color: #94a3b8; | |
| margin-bottom: 0.25rem; | |
| } | |
| .input-group input { | |
| width: 100%; | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| color: white; | |
| padding: 0.5rem; | |
| border-radius: 0.375rem; | |
| } | |
| .btn-primary { | |
| background: #4f46e5; | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.375rem; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| } | |
| .btn-primary:hover { | |
| background: #4338ca; | |
| } | |
| .btn-secondary { | |
| background: #334155; | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 0.375rem; | |
| font-weight: 600; | |
| } | |
| .btn-secondary:hover { | |
| background: #475569; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 300px; | |
| background: #1e293b; | |
| border-radius: 0.5rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app" class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="glass p-4 border-b border-gray-700 flex justify-between items-center z-10"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-xl">🚦</div> | |
| <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-cyan-400"> | |
| 智能排队策略实验室 | |
| </h1> | |
| </div> | |
| <div class="flex gap-2"> | |
| <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".json"> | |
| <button @click="triggerUpload" class="btn-secondary text-sm" title="导入配置"> | |
| <i class="fas fa-file-upload mr-1"></i> 导入 | |
| </button> | |
| <button @click="exportConfig" class="btn-secondary text-sm" title="导出配置"> | |
| <i class="fas fa-file-download mr-1"></i> 导出 | |
| </button> | |
| <button @click="showSaveModal = true" class="btn-secondary text-sm"> | |
| <i class="fas fa-save mr-1"></i> 保存 | |
| </button> | |
| <button @click="showLoadModal = true" class="btn-secondary text-sm"> | |
| <i class="fas fa-folder-open mr-1"></i> 加载 | |
| </button> | |
| </div> | |
| </header> | |
| <main class="flex-1 flex overflow-hidden"> | |
| <!-- Sidebar Controls --> | |
| <aside class="w-80 glass border-r border-gray-700 flex flex-col overflow-y-auto p-4 gap-6"> | |
| <!-- Section: Simulation Params --> | |
| <div> | |
| <h3 class="text-lg font-semibold text-indigo-400 mb-3"><i class="fas fa-sliders-h mr-2"></i>仿真参数</h3> | |
| <div class="space-y-3"> | |
| <div class="input-group"> | |
| <label>客户到达率 (人/小时)</label> | |
| <input type="number" v-model.number="params.arrival_rate" min="1" max="1000"> | |
| </div> | |
| <div class="input-group"> | |
| <label>平均服务时间 (分钟)</label> | |
| <input type="number" v-model.number="params.service_time" min="0.1" step="0.1"> | |
| </div> | |
| <div class="input-group"> | |
| <label>服务时间波动 (标准差)</label> | |
| <input type="number" v-model.number="params.service_std" min="0" step="0.1"> | |
| </div> | |
| <div class="input-group"> | |
| <label>服务窗口数量</label> | |
| <input type="number" v-model.number="params.num_servers" min="1" max="50"> | |
| </div> | |
| <div class="input-group"> | |
| <label>仿真时长 (分钟)</label> | |
| <input type="number" v-model.number="params.duration" min="10" max="1440"> | |
| </div> | |
| </div> | |
| <button @click="runSimulation" class="btn-primary w-full mt-4"> | |
| <i class="fas fa-play mr-2"></i> 开始仿真演示 | |
| </button> | |
| </div> | |
| <!-- Section: Cost Params --> | |
| <div class="border-t border-gray-700 pt-4"> | |
| <h3 class="text-lg font-semibold text-green-400 mb-3"><i class="fas fa-chart-line mr-2"></i>成本优化</h3> | |
| <div class="space-y-3"> | |
| <div class="input-group"> | |
| <label>服务员时薪 ($/小时)</label> | |
| <input type="number" v-model.number="params.cost_server" min="1"> | |
| </div> | |
| <div class="input-group"> | |
| <label>客户等待成本 ($/小时)</label> | |
| <input type="number" v-model.number="params.cost_wait" min="1"> | |
| <p class="text-xs text-gray-500 mt-1">客户因等待产生的隐性损失或不满折现</p> | |
| </div> | |
| </div> | |
| <button @click="runOptimization" class="btn-primary w-full mt-4 bg-green-600 hover:bg-green-700"> | |
| <i class="fas fa-calculator mr-2"></i> 运行优化分析 | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col overflow-y-auto bg-slate-900 p-6 gap-6"> | |
| <!-- Top: Animation & Live Stats --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[350px]"> | |
| <!-- Visualizer --> | |
| <div class="lg:col-span-2 glass rounded-xl p-4 flex flex-col relative"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h3 class="font-semibold text-gray-300">实时队列视图</h3> | |
| <div class="text-sm text-indigo-400"> | |
| 时间: <span class="font-mono">${ currentTime.toFixed(1) }</span> min | |
| </div> | |
| </div> | |
| <canvas ref="simCanvas" class="flex-1 w-full rounded bg-slate-800 border border-slate-700"></canvas> | |
| <!-- Legend --> | |
| <div class="absolute bottom-6 left-6 flex gap-4 text-xs text-gray-400"> | |
| <div class="flex items-center gap-1"><div class="w-3 h-3 rounded-full bg-blue-500"></div> 排队中</div> | |
| <div class="flex items-center gap-1"><div class="w-3 h-3 rounded-full bg-green-500"></div> 服务中</div> | |
| <div class="flex items-center gap-1"><div class="w-3 h-3 rounded-full bg-gray-500"></div> 空闲窗口</div> | |
| </div> | |
| </div> | |
| <!-- Live Stats --> | |
| <div class="glass rounded-xl p-4 flex flex-col justify-center gap-4"> | |
| <h3 class="font-semibold text-gray-300 border-b border-gray-700 pb-2">仿真统计结果</h3> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="bg-slate-800 p-3 rounded-lg text-center"> | |
| <div class="text-xs text-gray-500">平均等待</div> | |
| <div class="text-2xl font-bold text-indigo-400">${ metrics.avg_wait } <span class="text-xs">min</span></div> | |
| </div> | |
| <div class="bg-slate-800 p-3 rounded-lg text-center"> | |
| <div class="text-xs text-gray-500">最大等待</div> | |
| <div class="text-2xl font-bold text-red-400">${ metrics.max_wait } <span class="text-xs">min</span></div> | |
| </div> | |
| <div class="bg-slate-800 p-3 rounded-lg text-center"> | |
| <div class="text-xs text-gray-500">已服务人数</div> | |
| <div class="text-2xl font-bold text-green-400">${ metrics.served }</div> | |
| </div> | |
| <div class="bg-slate-800 p-3 rounded-lg text-center"> | |
| <div class="text-xs text-gray-500">窗口利用率</div> | |
| <div class="text-2xl font-bold text-yellow-400">${ metrics.utilization }<span class="text-sm">%</span></div> | |
| </div> | |
| </div> | |
| <div v-if="loading" class="text-center text-indigo-400 animate-pulse"> | |
| <i class="fas fa-circle-notch fa-spin mr-2"></i> 计算中... | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bottom: Charts --> | |
| <div class="glass rounded-xl p-4 flex-1 min-h-[400px]"> | |
| <h3 class="font-semibold text-gray-300 mb-4">成本优化分析 (服务窗口数 vs 总成本)</h3> | |
| <div ref="chartContainer" class="w-full h-[350px]"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Save Modal --> | |
| <div v-if="showSaveModal" class="fixed inset-0 bg-black/50 backdrop-blur flex items-center justify-center z-50"> | |
| <div class="glass p-6 rounded-xl w-96"> | |
| <h3 class="text-xl font-bold mb-4">保存配置</h3> | |
| <input v-model="saveName" placeholder="输入配置名称 (如: 早高峰)" class="w-full bg-slate-800 p-2 rounded mb-4 text-white"> | |
| <div class="flex justify-end gap-2"> | |
| <button @click="showSaveModal = false" class="btn-secondary">取消</button> | |
| <button @click="saveConfig" class="btn-primary">保存</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Load Modal --> | |
| <div v-if="showLoadModal" class="fixed inset-0 bg-black/50 backdrop-blur flex items-center justify-center z-50"> | |
| <div class="glass p-6 rounded-xl w-96"> | |
| <h3 class="text-xl font-bold mb-4">加载配置</h3> | |
| <ul class="space-y-2 max-h-60 overflow-y-auto mb-4"> | |
| <li v-for="(cfg, name) in savedConfigs" :key="name" class="flex justify-between items-center bg-slate-800 p-2 rounded hover:bg-slate-700 cursor-pointer" @click="loadConfig(name)"> | |
| <span>${ name }</span> | |
| <button @click.stop="deleteConfig(name)" class="text-red-400 hover:text-red-300"><i class="fas fa-trash"></i></button> | |
| </li> | |
| <li v-if="Object.keys(savedConfigs).length === 0" class="text-gray-500 text-center">暂无保存的配置</li> | |
| </ul> | |
| <div class="flex justify-end gap-2"> | |
| <button @click="showLoadModal = false" class="btn-secondary">关闭</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp, ref, onMounted, reactive, watch, nextTick } = Vue; | |
| createApp({ | |
| delimiters: ['${', '}'], | |
| setup() { | |
| // State | |
| const params = reactive({ | |
| arrival_rate: 120, | |
| service_time: 2.0, | |
| service_std: 0.5, | |
| num_servers: 5, | |
| duration: 60, | |
| cost_server: 20, | |
| cost_wait: 60 | |
| }); | |
| const metrics = reactive({ avg_wait: 0, max_wait: 0, served: 0, utilization: 0 }); | |
| const loading = ref(false); | |
| const currentTime = ref(0); | |
| // Modals | |
| const showSaveModal = ref(false); | |
| const showLoadModal = ref(false); | |
| const saveName = ref(''); | |
| const savedConfigs = ref({}); | |
| const fileInput = ref(null); | |
| // Canvas & Chart Refs | |
| const simCanvas = ref(null); | |
| const chartContainer = ref(null); | |
| let chartInstance = null; | |
| let animationFrame = null; | |
| // Simulation State | |
| let traceData = []; | |
| let playbackSpeed = 1; // Multiplier | |
| let customers = []; // { id, state: 'queue'|'service', serverIdx, x, y, targetX, targetY } | |
| let servers = []; // { id, busy: false, customerId: null } | |
| // --- Methods --- | |
| const runSimulation = async () => { | |
| loading.value = true; | |
| try { | |
| const res = await fetch('/api/simulate', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(params) | |
| }); | |
| const data = await res.json(); | |
| // Update metrics | |
| Object.assign(metrics, data.metrics); | |
| // Start Visualization | |
| traceData = data.trace; | |
| startAnimation(); | |
| } catch (e) { | |
| console.error(e); | |
| alert('仿真请求失败'); | |
| } finally { | |
| loading.value = false; | |
| } | |
| }; | |
| const runOptimization = async () => { | |
| loading.value = true; | |
| try { | |
| const res = await fetch('/api/optimize', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(params) | |
| }); | |
| const data = await res.json(); | |
| renderChart(data.results); | |
| } catch (e) { | |
| console.error(e); | |
| alert('优化请求失败'); | |
| } finally { | |
| loading.value = false; | |
| } | |
| }; | |
| // --- Visualization Logic --- | |
| const startAnimation = () => { | |
| if (animationFrame) cancelAnimationFrame(animationFrame); | |
| // Reset | |
| currentTime.value = 0; | |
| customers = []; | |
| servers = Array.from({length: params.num_servers}, (_, i) => ({ id: i, busy: false, customerId: null })); | |
| // Canvas Setup | |
| const canvas = simCanvas.value; | |
| const ctx = canvas.getContext('2d'); | |
| const dpr = window.devicePixelRatio || 1; | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = rect.width * dpr; | |
| canvas.height = rect.height * dpr; | |
| ctx.scale(dpr, dpr); | |
| const width = rect.width; | |
| const height = rect.height; | |
| let lastFrameTime = performance.now(); | |
| let traceIndex = 0; | |
| const animate = (now) => { | |
| const dt = (now - lastFrameTime) / 1000; // seconds | |
| lastFrameTime = now; | |
| // Advance simulation time (1 real sec = 1 sim min * speed) | |
| // Let's make it fast: 1 real sec = 10 sim mins | |
| const simSpeed = 5.0; | |
| currentTime.value += dt * simSpeed; | |
| // Process events up to currentTime | |
| while (traceIndex < traceData.length && traceData[traceIndex].time <= currentTime.value) { | |
| const event = traceData[traceIndex]; | |
| processEvent(event); | |
| traceIndex++; | |
| } | |
| // Draw | |
| drawScene(ctx, width, height); | |
| if (currentTime.value < params.duration || customers.length > 0) { | |
| animationFrame = requestAnimationFrame(animate); | |
| } | |
| }; | |
| requestAnimationFrame(animate); | |
| }; | |
| const processEvent = (event) => { | |
| if (event.type === 'arrival') { | |
| customers.push({ | |
| id: event.id, | |
| state: 'queue', | |
| x: 50, // Start left | |
| y: 150, | |
| color: '#3b82f6' // Blue | |
| }); | |
| } else if (event.type === 'start') { | |
| // Find customer | |
| const cust = customers.find(c => c.id === event.id); | |
| if (cust) { | |
| cust.state = 'service'; | |
| cust.color = '#22c55e'; // Green | |
| // Find free server | |
| const server = servers.find(s => !s.busy); | |
| if (server) { | |
| server.busy = true; | |
| server.customerId = cust.id; | |
| cust.serverIdx = server.id; | |
| } | |
| } | |
| } else if (event.type === 'finish') { | |
| const cust = customers.find(c => c.id === event.id); | |
| if (cust) { | |
| // Mark for removal or move to exit | |
| cust.state = 'exit'; | |
| // Free server | |
| if (cust.serverIdx !== undefined) { | |
| const server = servers[cust.serverIdx]; | |
| if (server) { | |
| server.busy = false; | |
| server.customerId = null; | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| const drawScene = (ctx, w, h) => { | |
| ctx.clearRect(0, 0, w, h); | |
| // Draw Server Booths (Right side) | |
| const serverX = w - 100; | |
| const serverGap = h / (params.num_servers + 1); | |
| servers.forEach((s, i) => { | |
| const y = (i + 1) * serverGap; | |
| ctx.fillStyle = s.busy ? '#1e293b' : '#334155'; // Darker if busy (occupied) | |
| ctx.strokeStyle = s.busy ? '#22c55e' : '#64748b'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.roundRect(serverX, y - 15, 60, 30, 5); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.font = '12px sans-serif'; | |
| ctx.fillText(`窗口 ${i+1}`, serverX + 10, y + 5); | |
| }); | |
| // Draw Queue Area (Left) | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'; | |
| ctx.fillRect(20, 20, w - 150, h - 40); | |
| // Draw Customers | |
| // Queue positions logic: organize them in lines | |
| let queueIdx = 0; | |
| // Remove exited customers smoothly | |
| customers = customers.filter(c => !(c.state === 'exit' && c.x > w)); | |
| customers.forEach(c => { | |
| let targetX, targetY; | |
| if (c.state === 'queue') { | |
| // Simple wrapping queue | |
| const col = Math.floor(queueIdx / 10); | |
| const row = queueIdx % 10; | |
| targetX = 50 + col * 20; | |
| targetY = 50 + row * 20; | |
| queueIdx++; | |
| } else if (c.state === 'service') { | |
| // Move to assigned server | |
| const sY = (c.serverIdx + 1) * serverGap; | |
| targetX = serverX + 30; | |
| targetY = sY; | |
| } else if (c.state === 'exit') { | |
| targetX = w + 50; | |
| targetY = c.y; // Keep current Y | |
| } | |
| // Lerp position (smooth movement) | |
| c.x += (targetX - c.x) * 0.1; | |
| c.y += (targetY - c.y) * 0.1; | |
| // Draw | |
| ctx.beginPath(); | |
| ctx.arc(c.x, c.y, 6, 0, Math.PI * 2); | |
| ctx.fillStyle = c.color; | |
| ctx.fill(); | |
| }); | |
| }; | |
| const renderChart = (results) => { | |
| if (!chartInstance) { | |
| chartInstance = echarts.init(chartContainer.value); | |
| } | |
| const xData = results.map(r => r.servers); | |
| const costData = results.map(r => r.total_cost); | |
| const waitData = results.map(r => r.avg_wait); | |
| // Find min cost | |
| const minCost = Math.min(...costData); | |
| const optimalServers = results.find(r => r.total_cost === minCost).servers; | |
| const option = { | |
| backgroundColor: 'transparent', | |
| tooltip: { | |
| trigger: 'axis', | |
| axisPointer: { type: 'cross' } | |
| }, | |
| legend: { | |
| data: ['总成本 ($)', '平均等待 (min)'], | |
| textStyle: { color: '#94a3b8' } | |
| }, | |
| xAxis: { | |
| type: 'category', | |
| data: xData, | |
| name: '服务窗口数', | |
| axisLine: { lineStyle: { color: '#475569' } }, | |
| axisLabel: { color: '#94a3b8' } | |
| }, | |
| yAxis: [ | |
| { | |
| type: 'value', | |
| name: '总成本 ($)', | |
| position: 'left', | |
| axisLine: { lineStyle: { color: '#22c55e' } }, | |
| axisLabel: { color: '#22c55e' }, | |
| splitLine: { lineStyle: { color: '#334155' } } | |
| }, | |
| { | |
| type: 'value', | |
| name: '平均等待 (min)', | |
| position: 'right', | |
| axisLine: { lineStyle: { color: '#3b82f6' } }, | |
| axisLabel: { color: '#3b82f6' }, | |
| splitLine: { show: false } | |
| } | |
| ], | |
| series: [ | |
| { | |
| name: '总成本 ($)', | |
| type: 'line', | |
| data: costData, | |
| smooth: true, | |
| lineStyle: { color: '#22c55e', width: 3 }, | |
| itemStyle: { color: '#22c55e' }, | |
| markPoint: { | |
| data: [ | |
| { type: 'min', name: '最佳配置', itemStyle: { color: '#f59e0b' } } | |
| ] | |
| } | |
| }, | |
| { | |
| name: '平均等待 (min)', | |
| type: 'line', | |
| yAxisIndex: 1, | |
| data: waitData, | |
| smooth: true, | |
| lineStyle: { color: '#3b82f6', width: 2, type: 'dashed' }, | |
| itemStyle: { color: '#3b82f6' } | |
| } | |
| ] | |
| }; | |
| chartInstance.setOption(option); | |
| window.addEventListener('resize', () => chartInstance.resize()); | |
| }; | |
| // --- Storage Logic --- | |
| const loadSaved = () => { | |
| const saved = localStorage.getItem('queue_sim_configs'); | |
| if (saved) savedConfigs.value = JSON.parse(saved); | |
| }; | |
| const saveConfig = () => { | |
| if (!saveName.value) return; | |
| savedConfigs.value[saveName.value] = { ...params }; | |
| localStorage.setItem('queue_sim_configs', JSON.stringify(savedConfigs.value)); | |
| showSaveModal.value = false; | |
| saveName.value = ''; | |
| alert('配置已保存'); | |
| }; | |
| const loadConfig = (name) => { | |
| Object.assign(params, savedConfigs.value[name]); | |
| showLoadModal.value = false; | |
| }; | |
| const deleteConfig = (name) => { | |
| delete savedConfigs.value[name]; | |
| localStorage.setItem('queue_sim_configs', JSON.stringify(savedConfigs.value)); | |
| }; | |
| // --- Import/Export Logic --- | |
| const triggerUpload = () => { | |
| fileInput.value.click(); | |
| }; | |
| const handleFileUpload = async (event) => { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| // Client-side read for immediate update | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const config = JSON.parse(e.target.result); | |
| Object.assign(params, config); | |
| alert('配置导入成功'); | |
| } catch (err) { | |
| alert('无效的配置文件'); | |
| console.error(err); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| // Optional: Send to server to verify server-side logic (as requested) | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| await fetch('/api/upload_config', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| } catch (e) { | |
| console.error("Server upload check failed, but client side worked:", e); | |
| } | |
| // Reset input | |
| event.target.value = ''; | |
| }; | |
| const exportConfig = () => { | |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(params, null, 2)); | |
| const downloadAnchorNode = document.createElement('a'); | |
| downloadAnchorNode.setAttribute("href", dataStr); | |
| downloadAnchorNode.setAttribute("download", "queue_config.json"); | |
| document.body.appendChild(downloadAnchorNode); | |
| downloadAnchorNode.click(); | |
| downloadAnchorNode.remove(); | |
| }; | |
| onMounted(() => { | |
| loadSaved(); | |
| }); | |
| return { | |
| params, | |
| metrics, | |
| loading, | |
| currentTime, | |
| simCanvas, | |
| chartContainer, | |
| runSimulation, | |
| runOptimization, | |
| showSaveModal, | |
| showLoadModal, | |
| saveName, | |
| savedConfigs, | |
| saveConfig, | |
| loadConfig, | |
| deleteConfig, | |
| fileInput, | |
| triggerUpload, | |
| handleFileUpload, | |
| exportConfig | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |