diff --git a/package-lock.json b/package-lock.json index 651d773..6fdedbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,8 @@ "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", "express": "^4.21.1", - "lucide-react": "^0.460.0", + "lucide-react": "^0.576.0", "mapbox-gl": "^3.6.0", - "opencc-js": "^1.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-map-gl": "^7.1.7", @@ -3576,11 +3575,11 @@ } }, "node_modules/lucide-react": { - "version": "0.460.0", - "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.460.0.tgz", - "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "version": "0.576.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.576.0.tgz", + "integrity": "sha512-koNxU14BXrxUfZQ9cUaP0ES1uyPZKYDjk31FQZB6dQ/x+tXk979sVAn9ppZ/pVeJJyOxVM8j1E+8QEuSc02Vug==", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/mapbox-gl": { @@ -3893,11 +3892,6 @@ "wrappy": "1" } }, - "node_modules/opencc-js": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/opencc-js/-/opencc-js-1.0.5.tgz", - "integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index 6548247..3345eb0 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", "express": "^4.21.1", - "lucide-react": "^0.460.0", + "lucide-react": "^0.576.0", "mapbox-gl": "^3.6.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/server/data.db b/server/data.db index 0754b25..dd93bc5 100644 Binary files a/server/data.db and b/server/data.db differ diff --git a/server/data.db-shm b/server/data.db-shm index 39aaafb..beb6dd2 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 4acc4df..be4e1a0 100644 Binary files a/server/data.db-wal and b/server/data.db-wal differ diff --git a/server/db.js b/server/db.js index 10febfe..9324192 100644 --- a/server/db.js +++ b/server/db.js @@ -193,4 +193,15 @@ try { `) } catch (_) {} +// 分享次数:累计分享次数 +try { + db.exec(` + CREATE TABLE IF NOT EXISTS share_count ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total INTEGER NOT NULL DEFAULT 0 + ); + INSERT OR IGNORE INTO share_count (id, total) VALUES (1, 0); + `) +} catch (_) {} + module.exports = db diff --git a/server/routes.js b/server/routes.js index 1b65869..d0e6438 100644 --- a/server/routes.js +++ b/server/routes.js @@ -84,6 +84,16 @@ function getClientIp(req) { return req.ip || req.socket?.remoteAddress || 'unknown' } +function getStats() { + const viewers = db.prepare( + "SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')" + ).get().n + const cumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0 + const feedbackCount = db.prepare('SELECT COUNT(*) as n FROM feedback').get().n ?? 0 + const shareCount = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0 + return { viewers, cumulative, feedbackCount, shareCount } +} + router.post('/visit', (req, res) => { try { const ip = getClientIp(req) @@ -93,14 +103,10 @@ router.post('/visit', (req, res) => { db.prepare( 'INSERT INTO visitor_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1' ).run() - const viewers = db.prepare( - "SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')" - ).get().n - const cumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0 - res.json({ viewers, cumulative }) + res.json(getStats()) } catch (err) { console.error(err) - res.status(500).json({ viewers: 0, cumulative: 0 }) + res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 }) } }) @@ -121,16 +127,25 @@ router.post('/feedback', (req, res) => { } }) -router.get('/stats', (req, res) => { +router.post('/share', (req, res) => { try { - const viewers = db.prepare( - "SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')" - ).get().n - const cumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0 - res.json({ viewers, cumulative }) + db.prepare( + 'INSERT INTO share_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1' + ).run() + const shareCount = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0 + res.json({ ok: true, shareCount }) } catch (err) { console.error(err) - res.status(500).json({ viewers: 0, cumulative: 0 }) + res.status(500).json({ ok: false, shareCount: 0 }) + } +}) + +router.get('/stats', (req, res) => { + try { + res.json(getStats()) + } catch (err) { + console.error(err) + res.status(500).json({ viewers: 0, cumulative: 0, feedbackCount: 0, shareCount: 0 }) } }) diff --git a/src/components/CombatLossesPanel.tsx b/src/components/CombatLossesPanel.tsx index a01387a..359e4fb 100644 --- a/src/components/CombatLossesPanel.tsx +++ b/src/components/CombatLossesPanel.tsx @@ -6,10 +6,10 @@ import { Ship, Shield, Car, - Scan, + Drone, Rocket, - Wind, - Anchor, + Asterisk, + Amphora, TrendingDown, UserCircle, Activity, @@ -35,10 +35,10 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia { label: '战舰', icon: Ship, iconColor: 'text-blue-500', us: usLosses.warships, ir: iranLosses.warships }, { label: '装甲', icon: Shield, iconColor: 'text-emerald-500', us: usLosses.armor, ir: iranLosses.armor }, { label: '车辆', icon: Car, iconColor: 'text-slate-400', us: usLosses.vehicles, ir: iranLosses.vehicles }, - { label: '无人机', icon: Scan, iconColor: 'text-violet-400', us: usLosses.drones ?? 0, ir: iranLosses.drones ?? 0 }, + { label: '无人机', icon: Drone, iconColor: 'text-violet-400', us: usLosses.drones ?? 0, ir: iranLosses.drones ?? 0 }, { label: '导弹', icon: Rocket, iconColor: 'text-orange-500', us: usLosses.missiles ?? 0, ir: iranLosses.missiles ?? 0 }, - { label: '直升机', icon: Wind, iconColor: 'text-teal-400', us: usLosses.helicopters ?? 0, ir: iranLosses.helicopters ?? 0 }, - { label: '潜艇', icon: Anchor, iconColor: 'text-indigo-400', us: usLosses.submarines ?? 0, ir: iranLosses.submarines ?? 0 }, + { label: '直升机', icon: Asterisk, iconColor: 'text-teal-400', us: usLosses.helicopters ?? 0, ir: iranLosses.helicopters ?? 0 }, + { label: '潜艇', icon: Amphora, iconColor: 'text-indigo-400', us: usLosses.submarines ?? 0, ir: iranLosses.submarines ?? 0 }, ] return ( diff --git a/src/components/HeaderPanel.tsx b/src/components/HeaderPanel.tsx index b7f1d89..a351c7f 100644 --- a/src/components/HeaderPanel.tsx +++ b/src/components/HeaderPanel.tsx @@ -25,6 +25,8 @@ export function HeaderPanel() { const [liked, setLiked] = useState(false) const [viewers, setViewers] = useState(0) const [cumulative, setCumulative] = useState(0) + const [feedbackCount, setFeedbackCount] = useState(0) + const [shareCount, setShareCount] = useState(0) const [feedbackOpen, setFeedbackOpen] = useState(false) const [feedbackText, setFeedbackText] = useState('') const [feedbackSending, setFeedbackSending] = useState(false) @@ -41,6 +43,8 @@ export function HeaderPanel() { const data = await res.json() if (data.viewers != null) setViewers(data.viewers) if (data.cumulative != null) setCumulative(data.cumulative) + if (data.feedbackCount != null) setFeedbackCount(data.feedbackCount) + if (data.shareCount != null) setShareCount(data.shareCount) } catch { setViewers((v) => (v > 0 ? v : 0)) setCumulative((c) => (c > 0 ? c : 0)) @@ -56,16 +60,27 @@ export function HeaderPanel() { const handleShare = async () => { const url = window.location.href const title = '美伊军事态势显示' + let shared = false if (typeof navigator.share === 'function') { try { await navigator.share({ title, url }) + shared = true } catch (e) { if ((e as Error).name !== 'AbortError') { await copyToClipboard(url) + shared = true } } } else { await copyToClipboard(url) + shared = true + } + if (shared) { + try { + const res = await fetch('/api/share', { method: 'POST' }) + const data = await res.json() + if (data.shareCount != null) setShareCount(data.shareCount) + } catch {} } } @@ -87,6 +102,7 @@ export function HeaderPanel() { if (data.ok) { setFeedbackText('') setFeedbackDone(true) + setFeedbackCount((c) => c + 1) setTimeout(() => { setFeedbackOpen(false) setFeedbackDone(false) @@ -163,7 +179,7 @@ export function HeaderPanel() { className="flex shrink-0 items-center gap-1 rounded border border-military-border px-1.5 py-0.5 text-[9px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400 sm:px-2 sm:py-1 sm:text-[10px]" > - 留言 + 留言 {feedbackCount > 0 && {feedbackCount}}