Refactor Docker setup and enhance development environment
- Updated docker-compose.dev.yml to use Dockerfile.dev for backend builds and added HOST environment variable for frontend. - Introduced Dockerfile.dev for streamlined backend development with Go. - Enhanced development documentation to reflect changes in local setup and API proxying. - Removed outdated frontend Dockerfile and adjusted frontend configuration for improved development experience.
This commit is contained in:
13
Dockerfile.dev
Normal file
13
Dockerfile.dev
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM golang:1.21-alpine
|
||||||
|
|
||||||
|
WORKDIR /hnh-map
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o hnh-map ./cmd/hnh-map
|
||||||
|
|
||||||
|
EXPOSE 3080
|
||||||
|
|
||||||
|
CMD ["/hnh-map/hnh-map", "-grids=/map"]
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# Development: backend (Go) on 8080, frontend (Nuxt dev) on 3000 with proxy to backend.
|
# Development: backend API on 3080 + frontend Nuxt dev server on 3000.
|
||||||
# Open http://localhost:3000/map/ — /map/api, /map/updates, /map/grids are proxied to backend.
|
# Open http://localhost:3000/ for app development with live-reload.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.dev
|
||||||
ports:
|
ports:
|
||||||
- "3080:3080"
|
- "3080:3080"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -26,5 +26,6 @@ services:
|
|||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- NUXT_PUBLIC_API_BASE=/map/api
|
- NUXT_PUBLIC_API_BASE=/map/api
|
||||||
|
- HOST=0.0.0.0
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
@@ -35,10 +35,13 @@ npm run dev
|
|||||||
docker compose -f docker-compose.dev.yml up
|
docker compose -f docker-compose.dev.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
- Фронт: порт **3000** (Nuxt dev-сервер).
|
Dev-композ поднимает два сервиса:
|
||||||
- Бэкенд: порт **3080** (чтобы не конфликтовать с другими сервисами на 8080).
|
|
||||||
|
|
||||||
Откройте http://localhost:3000/. Запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд (host `backend`, порт 3080).
|
- `backend` — Go API на порту `3080` (без сборки/раздачи фронтенд-статики в dev-режиме).
|
||||||
|
- `frontend` — Nuxt dev-сервер на порту `3000` с live-reload; запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд.
|
||||||
|
|
||||||
|
Используйте [http://localhost:3000/](http://localhost:3000/) как основной URL для разработки интерфейса.
|
||||||
|
Порт `3080` предназначен для API и backend-эндпоинтов; корень `/` может возвращать `404` в dev-режиме — это ожидаемо.
|
||||||
|
|
||||||
### Сборка образа и prod-композ
|
### Сборка образа и prod-композ
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Use Node 20+ for build (required by Tailwind/PostCSS toolchain)
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run generate
|
|
||||||
|
|
||||||
# Output: .output/public is the static site root (for Go http.Dir("frontend"))
|
|
||||||
FROM alpine:3.19
|
|
||||||
RUN apk add --no-cache bash
|
|
||||||
COPY --from=builder /app/.output/public /frontend
|
|
||||||
# Optional: when integrating with main Dockerfile, copy /frontend into the image
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full w-full" @click="mapLogic.closeContextMenus()">
|
<div class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
|
||||||
<div
|
<div
|
||||||
v-if="mapsLoaded && maps.length === 0"
|
v-if="mapsLoaded && maps.length === 0"
|
||||||
class="absolute inset-0 z-[500] flex flex-col items-center justify-center gap-4 bg-base-200/90 p-6"
|
class="absolute inset-0 z-[500] flex flex-col items-center justify-center gap-4 bg-base-200/90 p-6"
|
||||||
@@ -95,6 +95,8 @@ let markers: UniqueList<InstanceType<typeof Marker>> | null = null
|
|||||||
let characters: UniqueList<InstanceType<typeof Character>> | null = null
|
let characters: UniqueList<InstanceType<typeof Character>> | null = null
|
||||||
let markersHidden = false
|
let markersHidden = false
|
||||||
let autoMode = false
|
let autoMode = false
|
||||||
|
let mapContainer: HTMLElement | null = null
|
||||||
|
let contextMenuHandler: ((ev: MouseEvent) => void) | null = null
|
||||||
|
|
||||||
function toLatLng(x: number, y: number) {
|
function toLatLng(x: number, y: number) {
|
||||||
return map!.unproject([x, y], HnHMaxZoom)
|
return map!.unproject([x, y], HnHMaxZoom)
|
||||||
@@ -187,7 +189,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
|
const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
|
||||||
if (config?.title) document.title = config.title
|
if (config?.title) document.title = config.title
|
||||||
if (config?.auths) auths.value = config.auths
|
const user = await api.me().catch(() => null)
|
||||||
|
auths.value = (user as { auths?: string[] } | null)?.auths ?? config?.auths ?? []
|
||||||
|
|
||||||
map = L.map(mapRef.value, {
|
map = L.map(mapRef.value, {
|
||||||
minZoom: HnHMinZoom,
|
minZoom: HnHMinZoom,
|
||||||
@@ -257,13 +260,25 @@ onMounted(async () => {
|
|||||||
const markerIconPath = baseURL.endsWith('/') ? baseURL : baseURL + '/'
|
const markerIconPath = baseURL.endsWith('/') ? baseURL : baseURL + '/'
|
||||||
L.Icon.Default.imagePath = markerIconPath
|
L.Icon.Default.imagePath = markerIconPath
|
||||||
|
|
||||||
map.on('contextmenu', (mev: L.LeafletMouseEvent) => {
|
// Document-level capture so we get contextmenu before any map layer or iframe can swallow it
|
||||||
if (auths.value.includes('admin')) {
|
mapContainer = map.getContainer()
|
||||||
const point = map!.project(mev.latlng, 6)
|
contextMenuHandler = (ev: MouseEvent) => {
|
||||||
|
const target = ev.target as Node
|
||||||
|
if (!mapContainer?.contains(target)) return
|
||||||
|
const isAdmin = auths.value.includes('admin')
|
||||||
|
if (import.meta.dev) console.log('[MapView contextmenu]', { isAdmin, auths: auths.value })
|
||||||
|
if (isAdmin) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
const rect = mapContainer.getBoundingClientRect()
|
||||||
|
const containerPoint = L.point(ev.clientX - rect.left, ev.clientY - rect.top)
|
||||||
|
const latlng = map!.containerPointToLatLng(containerPoint)
|
||||||
|
const point = map!.project(latlng, 6)
|
||||||
const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
|
const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
|
||||||
mapLogic.openTileContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, coords)
|
mapLogic.openTileContextMenu(ev.clientX, ev.clientY, coords)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
document.addEventListener('contextmenu', contextMenuHandler, true)
|
||||||
|
|
||||||
const updatesPath = `${backendBase}/updates`
|
const updatesPath = `${backendBase}/updates`
|
||||||
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
||||||
@@ -353,6 +368,8 @@ onMounted(async () => {
|
|||||||
marker.setClickCallback(() => map!.setView(marker.marker!.getLatLng(), HnHMaxZoom))
|
marker.setClickCallback(() => map!.setView(marker.marker!.getLatLng(), HnHMaxZoom))
|
||||||
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
||||||
if (auths.value.includes('admin')) {
|
if (auths.value.includes('admin')) {
|
||||||
|
mev.originalEvent.preventDefault()
|
||||||
|
mev.originalEvent.stopPropagation()
|
||||||
mapLogic.openMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
mapLogic.openMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -473,6 +490,9 @@ onBeforeUnmount(() => {
|
|||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
window.removeEventListener('keydown', onKeydown)
|
window.removeEventListener('keydown', onKeydown)
|
||||||
}
|
}
|
||||||
|
if (contextMenuHandler) {
|
||||||
|
document.removeEventListener('contextmenu', contextMenuHandler, true)
|
||||||
|
}
|
||||||
if (intervalId) clearInterval(intervalId)
|
if (intervalId) clearInterval(intervalId)
|
||||||
if (source) source.close()
|
if (source) source.close()
|
||||||
if (map) map.remove()
|
if (map) map.remove()
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Context menu (tile) -->
|
<!-- Context menu (tile) — Teleport so it is not clipped by map/overflow -->
|
||||||
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
v-show="contextMenu.tile.show"
|
v-show="contextMenu.tile.show"
|
||||||
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
class="fixed z-[9999] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
<!-- Context menu (marker) -->
|
<!-- Context menu (marker) -->
|
||||||
<div
|
<div
|
||||||
v-show="contextMenu.marker.show"
|
v-show="contextMenu.marker.show"
|
||||||
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
class="fixed z-[9999] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
Hide marker {{ contextMenu.marker.data?.name }}
|
Hide marker {{ contextMenu.marker.data?.name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function useMapLogic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTileContextMenu(clientX: number, clientY: number, coords: { x: number; y: number }) {
|
function openTileContextMenu(clientX: number, clientY: number, coords: { x: number; y: number }) {
|
||||||
|
closeContextMenus()
|
||||||
contextMenu.tile.show = true
|
contextMenu.tile.show = true
|
||||||
contextMenu.tile.x = clientX
|
contextMenu.tile.x = clientX
|
||||||
contextMenu.tile.y = clientY
|
contextMenu.tile.y = clientY
|
||||||
@@ -108,6 +109,7 @@ export function useMapLogic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openMarkerContextMenu(clientX: number, clientY: number, id: number, name: string) {
|
function openMarkerContextMenu(clientX: number, clientY: number, id: number, name: string) {
|
||||||
|
closeContextMenus()
|
||||||
contextMenu.marker.show = true
|
contextMenu.marker.show = true
|
||||||
contextMenu.marker.x = clientX
|
contextMenu.marker.x = clientX
|
||||||
contextMenu.marker.y = clientY
|
contextMenu.marker.y = clientY
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export default defineNuxtConfig({
|
|||||||
// Dev: proxy /map API, SSE and grids to Go backend (e.g. docker compose -f docker-compose.dev.yml)
|
// Dev: proxy /map API, SSE and grids to Go backend (e.g. docker compose -f docker-compose.dev.yml)
|
||||||
nitro: {
|
nitro: {
|
||||||
devProxy: {
|
devProxy: {
|
||||||
'/map/api': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/api': { target: 'http://backend:3080/map/api', changeOrigin: true },
|
||||||
'/map/updates': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/updates': { target: 'http://backend:3080/map/updates', changeOrigin: true },
|
||||||
'/map/grids': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/grids': { target: 'http://backend:3080/map/grids', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
|
|
||||||
integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
|
|
||||||
crossorigin=""/>
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but map-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user