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}}