diff --git a/.env.example b/.env.example index 87cbb57..1c1ca53 100644 --- a/.env.example +++ b/.env.example @@ -2,45 +2,3 @@ # 免费申请: https://account.mapbox.com/access-tokens/ # 复制本文件为 .env 并填入你的 token VITE_MAPBOX_ACCESS_TOKEN=your_mapbox_public_token_here -27 个基地完整 JSON 数据 -[ - { "id": 1, "name": "Al Udeid Air Base", "country": "Qatar", "lat": 25.117, "lng": 51.314 }, - { "id": 2, "name": "Camp As Sayliyah", "country": "Qatar", "lat": 25.275, "lng": 51.520 }, - - { "id": 3, "name": "Naval Support Activity Bahrain", "country": "Bahrain", "lat": 26.236, "lng": 50.608 }, - - { "id": 4, "name": "Camp Arifjan", "country": "Kuwait", "lat": 28.832, "lng": 47.799 }, - { "id": 5, "name": "Ali Al Salem Air Base", "country": "Kuwait", "lat": 29.346, "lng": 47.520 }, - { "id": 6, "name": "Camp Buehring", "country": "Kuwait", "lat": 29.603, "lng": 47.456 }, - - { "id": 7, "name": "Al Dhafra Air Base", "country": "UAE", "lat": 24.248, "lng": 54.547 }, - - { "id": 8, "name": "Prince Sultan Air Base", "country": "Saudi Arabia", "lat": 24.062, "lng": 47.580 }, - { "id": 9, "name": "Eskan Village", "country": "Saudi Arabia", "lat": 24.774, "lng": 46.738 }, - - { "id": 10, "name": "Al Asad Airbase", "country": "Iraq", "lat": 33.785, "lng": 42.441 }, - { "id": 11, "name": "Erbil Air Base", "country": "Iraq", "lat": 36.237, "lng": 43.963 }, - { "id": 12, "name": "Baghdad Diplomatic Support Center", "country": "Iraq", "lat": 33.315, "lng": 44.366 }, - { "id": 13, "name": "Camp Taji", "country": "Iraq", "lat": 33.556, "lng": 44.256 }, - { "id": 14, "name": "Ain al-Asad", "country": "Iraq", "lat": 33.800, "lng": 42.450 }, - - { "id": 15, "name": "Al-Tanf Garrison", "country": "Syria", "lat": 33.490, "lng": 38.618 }, - { "id": 16, "name": "Rmelan Landing Zone", "country": "Syria", "lat": 37.015, "lng": 41.885 }, - { "id": 17, "name": "Shaddadi Base", "country": "Syria", "lat": 36.058, "lng": 40.730 }, - { "id": 18, "name": "Conoco Gas Field Base", "country": "Syria", "lat": 35.336, "lng": 40.295 }, - - { "id": 19, "name": "Muwaffaq Salti Air Base", "country": "Jordan", "lat": 32.356, "lng": 36.259 }, - - { "id": 20, "name": "Incirlik Air Base", "country": "Turkey", "lat": 37.002, "lng": 35.425 }, - { "id": 21, "name": "Kurecik Radar Station", "country": "Turkey", "lat": 38.354, "lng": 37.794 }, - - { "id": 22, "name": "Nevatim Air Base", "country": "Israel", "lat": 31.208, "lng": 35.012 }, - { "id": 23, "name": "Ramon Air Base", "country": "Israel", "lat": 30.776, "lng": 34.666 }, - - { "id": 24, "name": "Thumrait Air Base", "country": "Oman", "lat": 17.666, "lng": 54.024 }, - { "id": 25, "name": "Masirah Air Base", "country": "Oman", "lat": 20.675, "lng": 58.890 }, - - { "id": 26, "name": "West Cairo Air Base", "country": "Egypt", "lat": 30.915, "lng": 30.298 }, - - { "id": 27, "name": "Camp Lemonnier", "country": "Djibouti", "lat": 11.547, "lng": 43.159 } -] \ No newline at end of file diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..380053b --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,111 @@ +# Docker 部署到服务器 + +将 US-Iran 态势面板打包成 Docker 镜像,便于移植到任意服务器。 + +## 架构 + +| 服务 | 端口 | 说明 | +|--------|------|--------------------------| +| api | 3001 | 前端静态 + REST API + WebSocket | +| crawler| 8000 | RSS 爬虫 + GDELT,内部服务 | + +- 数据库:SQLite,挂载到 `app-data` volume(`/data/data.db`) +- 前端与 API 合并到同一镜像,访问 `http://主机:3001` 即可 + +## 快速部署 + +```bash +# 1. 克隆项目 +git clone usa-dashboard && cd usa-dashboard + +# 2. 构建并启动(需先配置 Mapbox Token,见下方) +docker compose up -d --build + +# 3. 访问 +# 前端 + API: http://localhost:3001 +# 爬虫状态: http://localhost:8000/crawler/status +``` + +## Mapbox Token(地图展示) + +构建时需将 Token 传入前端,否则地图为占位模式: + +```bash +# 方式 1:.env 文件 +echo "VITE_MAPBOX_ACCESS_TOKEN=pk.xxx" > .env +docker compose up -d --build + +# 方式 2:环境变量 +VITE_MAPBOX_ACCESS_TOKEN=pk.xxx docker compose up -d --build +``` + +## 推送到私有仓库并移植 + +```bash +# 1. 打标签(替换为你的仓库地址) +docker compose build +docker tag usa-dashboard-api your-registry/usa-dashboard-api:latest +docker tag usa-dashboard-crawler your-registry/usa-dashboard-crawler:latest + +# 2. 推送 +docker push your-registry/usa-dashboard-api:latest +docker push your-registry/usa-dashboard-crawler:latest + +# 3. 在目标服务器拉取并启动 +docker pull your-registry/usa-dashboard-api:latest +docker pull your-registry/usa-dashboard-crawler:latest +# 需准备 docker-compose.yml 或等效编排,见下方 +``` + +## 仅用镜像启动(无 compose) + +```bash +# 1. 创建网络与数据卷 +docker network create usa-net +docker volume create usa-data + +# 2. 启动 API(前端+接口) +docker run -d --name api --network usa-net \ + -p 3001:3001 \ + -v usa-data:/data \ + -e DB_PATH=/data/data.db \ + usa-dashboard-api + +# 3. 启动爬虫(通过 usa-net 访问 api) +docker run -d --name crawler --network usa-net \ + -v usa-data:/data \ + -e DB_PATH=/data/data.db \ + -e API_BASE=http://api:3001 \ + -e CLEANER_AI_DISABLED=1 \ + -e GDELT_DISABLED=1 \ + usa-dashboard-crawler +``` + +爬虫通过 `API_BASE` 调用 Node 的 `/api/crawler/notify`,两容器需在同一网络内。 + +## 国内服务器 / 镜像加速 + +拉取 `node`、`python` 等基础镜像慢时: + +1. **Docker 镜像加速**:见 [docs/DOCKER_MIRROR.md](docs/DOCKER_MIRROR.md) +2. **构建时使用国内镜像源**: + ```bash + docker compose build --build-arg REGISTRY=docker.m.daocloud.io/library/ + docker compose up -d + ``` + +## 常用操作 + +```bash +# 查看日志 +docker compose logs -f + +# 重启 +docker compose restart + +# 停止并删除容器(数据卷保留) +docker compose down + +# 回填战损数据(从 situation_update 重新提取) +curl -X POST http://localhost:8000/crawler/backfill +``` diff --git a/Dockerfile b/Dockerfile index 4fa2dc0..d612feb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ -# 前端 + API 一体化镜像(使用 DaoCloud 国内镜像源) -FROM docker.m.daocloud.io/library/node:20-alpine AS frontend-builder +# 前端 + API 一体化镜像 +# 国内服务器拉取慢时,可加 --build-arg REGISTRY=docker.m.daocloud.io/library +ARG REGISTRY= +FROM ${REGISTRY}node:20-alpine AS frontend-builder WORKDIR /app ARG VITE_MAPBOX_ACCESS_TOKEN ENV VITE_MAPBOX_ACCESS_TOKEN=${VITE_MAPBOX_ACCESS_TOKEN} @@ -8,7 +10,7 @@ RUN npm ci COPY . . RUN npm run build -FROM docker.m.daocloud.io/library/node:20-alpine +FROM ${REGISTRY}node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev diff --git a/Dockerfile.crawler b/Dockerfile.crawler index c975465..c1907fc 100644 --- a/Dockerfile.crawler +++ b/Dockerfile.crawler @@ -1,9 +1,11 @@ -# Python 爬虫服务(使用 DaoCloud 国内镜像源 + 清华 PyPI 源) -FROM docker.m.daocloud.io/library/python:3.11-slim +# Python 爬虫服务 +# 国内服务器可加 --build-arg REGISTRY=docker.m.daocloud.io/library +ARG REGISTRY= +FROM ${REGISTRY}python:3.11-slim WORKDIR /app COPY crawler/requirements.txt ./ -RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt COPY crawler ./ ENV DB_PATH=/data/data.db diff --git a/README.md b/README.md index 45e5703..438ca22 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ docker compose up -d # 数据库与爬虫共享 volume,首次启动自动 seed ``` +**迁移到服务器**:见 [DEPLOY.md](DEPLOY.md)(构建、推送、单机/多机部署说明) + **拉取镜像超时?** 在 Docker Desktop 配置镜像加速,见 [docs/DOCKER_MIRROR.md](docs/DOCKER_MIRROR.md) **开发时无需每次 rebuild**:使用开发模式挂载源码 + 热重载: diff --git a/docker-compose.yml b/docker-compose.yml index bb3677c..9d0dbc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: api: + image: usa-dashboard-api:latest build: context: . args: @@ -14,6 +15,7 @@ services: restart: unless-stopped crawler: + image: usa-dashboard-crawler:latest build: context: . dockerfile: Dockerfile.crawler diff --git a/server/data.db-shm b/server/data.db-shm index e9d50fa..4829ca3 100644 Binary files a/server/data.db-shm and b/server/data.db-shm differ diff --git a/server/data.db-wal b/server/data.db-wal index 3c34608..dc7ce8c 100644 Binary files a/server/data.db-wal and b/server/data.db-wal differ diff --git a/src/components/TimelinePanel.tsx b/src/components/TimelinePanel.tsx index 41e8634..cdd4f4e 100644 --- a/src/components/TimelinePanel.tsx +++ b/src/components/TimelinePanel.tsx @@ -3,6 +3,7 @@ import { Play, Pause, SkipBack, SkipForward, History } from 'lucide-react' import { usePlaybackStore, REPLAY_TICKS, REPLAY_START, REPLAY_END } from '@/store/playbackStore' import { useSituationStore } from '@/store/situationStore' import { NewsTicker } from './NewsTicker' +import { config } from '@/config' function formatTick(iso: string): string { const d = new Date(iso) @@ -78,7 +79,7 @@ export function TimelinePanel() { 数据回放 - {!isReplayMode && ( + {!isReplayMode && config.showNewsTicker && (