diff --git a/README.md b/README.md index 6f49cbd..d070f40 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,16 @@ docker compose up -d **拉取镜像超时?** 在 Docker Desktop 配置镜像加速,见 [docs/DOCKER_MIRROR.md](docs/DOCKER_MIRROR.md) +**开发时无需每次 rebuild**:使用开发模式挂载源码 + 热重载: + +```bash +docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d +``` + +- API:`node --watch` 监听 `server/` 变更并自动重启 +- 爬虫:`uvicorn --reload` 监听 `crawler/` 变更并自动重启 +- 修改 `server/` 或 `crawler/` 后,服务会自动重载,无需重新 build + 环境变量(可选,在 .env 或 docker-compose.yml 中配置): - `VITE_MAPBOX_ACCESS_TOKEN`:Mapbox 令牌,构建时注入 diff --git a/crawler/__pycache__/cleaner_ai.cpython-311.pyc b/crawler/__pycache__/cleaner_ai.cpython-311.pyc new file mode 100644 index 0000000..df451e6 Binary files /dev/null and b/crawler/__pycache__/cleaner_ai.cpython-311.pyc differ diff --git a/crawler/__pycache__/config.cpython-311.pyc b/crawler/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..a9256d8 Binary files /dev/null and b/crawler/__pycache__/config.cpython-311.pyc differ diff --git a/crawler/__pycache__/db_writer.cpython-311.pyc b/crawler/__pycache__/db_writer.cpython-311.pyc new file mode 100644 index 0000000..db00cd2 Binary files /dev/null and b/crawler/__pycache__/db_writer.cpython-311.pyc differ diff --git a/crawler/__pycache__/parser.cpython-311.pyc b/crawler/__pycache__/parser.cpython-311.pyc new file mode 100644 index 0000000..6fb50be Binary files /dev/null and b/crawler/__pycache__/parser.cpython-311.pyc differ diff --git a/crawler/__pycache__/parser_ai.cpython-311.pyc b/crawler/__pycache__/parser_ai.cpython-311.pyc new file mode 100644 index 0000000..cb9b721 Binary files /dev/null and b/crawler/__pycache__/parser_ai.cpython-311.pyc differ diff --git a/crawler/__pycache__/realtime_conflict_service.cpython-311.pyc b/crawler/__pycache__/realtime_conflict_service.cpython-311.pyc new file mode 100644 index 0000000..ef0d3a0 Binary files /dev/null and b/crawler/__pycache__/realtime_conflict_service.cpython-311.pyc differ diff --git a/crawler/__pycache__/translate_utils.cpython-311.pyc b/crawler/__pycache__/translate_utils.cpython-311.pyc new file mode 100644 index 0000000..58d7e04 Binary files /dev/null and b/crawler/__pycache__/translate_utils.cpython-311.pyc differ diff --git a/crawler/scrapers/__pycache__/rss_scraper.cpython-311.pyc b/crawler/scrapers/__pycache__/rss_scraper.cpython-311.pyc index d6837b9..5882ceb 100644 Binary files a/crawler/scrapers/__pycache__/rss_scraper.cpython-311.pyc and b/crawler/scrapers/__pycache__/rss_scraper.cpython-311.pyc differ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..d291f0d --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,13 @@ +# 开发模式:挂载源码 + 热重载,代码更新后无需重新 build +# 使用: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d +# 或: docker compose --profile dev up -d (需在 dev 服务加 profiles) +services: + api: + volumes: + - ./server:/app/server:ro + command: ["node", "--watch", "server/index.js"] + + crawler: + volumes: + - ./crawler:/app + command: ["uvicorn", "realtime_conflict_service:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/server/db.js b/server/db.js index f7d77b5..b55a414 100644 --- a/server/db.js +++ b/server/db.js @@ -162,4 +162,16 @@ try { `) } catch (_) {} +// 后台留言:供开发者收集用户反馈 +try { + db.exec(` + CREATE TABLE IF NOT EXISTS feedback ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + ip TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `) +} catch (_) {} + module.exports = db diff --git a/server/routes.js b/server/routes.js index 8119323..d8427e3 100644 --- a/server/routes.js +++ b/server/routes.js @@ -8,6 +8,7 @@ const router = express.Router() router.get('/db/dashboard', (req, res) => { try { const tables = [ + 'feedback', 'situation', 'force_summary', 'power_index', @@ -23,6 +24,7 @@ router.get('/db/dashboard', (req, res) => { ] const data = {} const timeSort = { + feedback: 'created_at DESC', situation: 'updated_at DESC', situation_update: 'timestamp DESC', gdelt_events: 'event_time DESC', @@ -89,6 +91,23 @@ router.post('/visit', (req, res) => { } }) +router.post('/feedback', (req, res) => { + try { + const content = (req.body?.content ?? '').toString().trim() + if (!content || content.length > 2000) { + return res.status(400).json({ ok: false, error: '留言内容 1–2000 字' }) + } + const ip = getClientIp(req) + db.prepare( + 'INSERT INTO feedback (content, ip) VALUES (?, ?)' + ).run(content.slice(0, 2000), ip) + res.json({ ok: true }) + } catch (err) { + console.error(err) + res.status(500).json({ ok: false, error: err.message }) + } +}) + router.get('/stats', (req, res) => { try { const viewers = db.prepare( diff --git a/src/components/HeaderPanel.tsx b/src/components/HeaderPanel.tsx index 6cdfda6..5bb96f5 100644 --- a/src/components/HeaderPanel.tsx +++ b/src/components/HeaderPanel.tsx @@ -3,7 +3,7 @@ import { StatCard } from './StatCard' import { useSituationStore } from '@/store/situationStore' import { useReplaySituation } from '@/hooks/useReplaySituation' import { usePlaybackStore } from '@/store/playbackStore' -import { Wifi, WifiOff, Clock, Share2, Heart, Eye } from 'lucide-react' +import { Wifi, WifiOff, Clock, Share2, Heart, Eye, MessageSquare } from 'lucide-react' const STORAGE_LIKES = 'us-iran-dashboard-likes' @@ -25,6 +25,10 @@ export function HeaderPanel() { const [liked, setLiked] = useState(false) const [viewers, setViewers] = useState(0) const [cumulative, setCumulative] = useState(0) + const [feedbackOpen, setFeedbackOpen] = useState(false) + const [feedbackText, setFeedbackText] = useState('') + const [feedbackSending, setFeedbackSending] = useState(false) + const [feedbackDone, setFeedbackDone] = useState(false) useEffect(() => { const timer = setInterval(() => setNow(new Date()), 1000) @@ -69,6 +73,32 @@ export function HeaderPanel() { return navigator.clipboard?.writeText(text) ?? Promise.resolve() } + const handleFeedback = async () => { + const text = feedbackText.trim() + if (!text || feedbackSending) return + setFeedbackSending(true) + try { + const res = await fetch('/api/feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: text }), + }) + const data = await res.json() + if (data.ok) { + setFeedbackText('') + setFeedbackDone(true) + setTimeout(() => { + setFeedbackOpen(false) + setFeedbackDone(false) + }, 800) + } + } catch { + setFeedbackDone(false) + } finally { + setFeedbackSending(false) + } + } + const handleLike = () => { if (liked) return setLiked(true) @@ -127,6 +157,14 @@ export function HeaderPanel() { | 累积 {cumulative} +