fix: 优化留言和分享数据
This commit is contained in:
16
package-lock.json
generated
16
package-lock.json
generated
@@ -13,9 +13,8 @@
|
|||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.576.0",
|
||||||
"mapbox-gl": "^3.6.0",
|
"mapbox-gl": "^3.6.0",
|
||||||
"opencc-js": "^1.0.5",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-map-gl": "^7.1.7",
|
"react-map-gl": "^7.1.7",
|
||||||
@@ -3576,11 +3575,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.460.0",
|
"version": "0.576.0",
|
||||||
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.460.0.tgz",
|
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.576.0.tgz",
|
||||||
"integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==",
|
"integrity": "sha512-koNxU14BXrxUfZQ9cUaP0ES1uyPZKYDjk31FQZB6dQ/x+tXk979sVAn9ppZ/pVeJJyOxVM8j1E+8QEuSc02Vug==",
|
||||||
"peerDependencies": {
|
"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": {
|
"node_modules/mapbox-gl": {
|
||||||
@@ -3893,11 +3892,6 @@
|
|||||||
"wrappy": "1"
|
"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": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.576.0",
|
||||||
"mapbox-gl": "^3.6.0",
|
"mapbox-gl": "^3.6.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
11
server/db.js
11
server/db.js
@@ -193,4 +193,15 @@ try {
|
|||||||
`)
|
`)
|
||||||
} catch (_) {}
|
} 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
|
module.exports = db
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ function getClientIp(req) {
|
|||||||
return req.ip || req.socket?.remoteAddress || 'unknown'
|
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) => {
|
router.post('/visit', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const ip = getClientIp(req)
|
const ip = getClientIp(req)
|
||||||
@@ -93,14 +103,10 @@ router.post('/visit', (req, res) => {
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO visitor_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1'
|
'INSERT INTO visitor_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1'
|
||||||
).run()
|
).run()
|
||||||
const viewers = db.prepare(
|
res.json(getStats())
|
||||||
"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 })
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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 {
|
try {
|
||||||
const viewers = db.prepare(
|
db.prepare(
|
||||||
"SELECT COUNT(*) as n FROM visits WHERE last_seen > datetime('now', '-2 minutes')"
|
'INSERT INTO share_count (id, total) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET total = total + 1'
|
||||||
).get().n
|
).run()
|
||||||
const cumulative = db.prepare('SELECT total FROM visitor_count WHERE id = 1').get()?.total ?? 0
|
const shareCount = db.prepare('SELECT total FROM share_count WHERE id = 1').get()?.total ?? 0
|
||||||
res.json({ viewers, cumulative })
|
res.json({ ok: true, shareCount })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
Ship,
|
Ship,
|
||||||
Shield,
|
Shield,
|
||||||
Car,
|
Car,
|
||||||
Scan,
|
Drone,
|
||||||
Rocket,
|
Rocket,
|
||||||
Wind,
|
Asterisk,
|
||||||
Anchor,
|
Amphora,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
Activity,
|
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: 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: 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: 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: 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: Asterisk, 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: Amphora, iconColor: 'text-indigo-400', us: usLosses.submarines ?? 0, ir: iranLosses.submarines ?? 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export function HeaderPanel() {
|
|||||||
const [liked, setLiked] = useState(false)
|
const [liked, setLiked] = useState(false)
|
||||||
const [viewers, setViewers] = useState(0)
|
const [viewers, setViewers] = useState(0)
|
||||||
const [cumulative, setCumulative] = useState(0)
|
const [cumulative, setCumulative] = useState(0)
|
||||||
|
const [feedbackCount, setFeedbackCount] = useState(0)
|
||||||
|
const [shareCount, setShareCount] = useState(0)
|
||||||
const [feedbackOpen, setFeedbackOpen] = useState(false)
|
const [feedbackOpen, setFeedbackOpen] = useState(false)
|
||||||
const [feedbackText, setFeedbackText] = useState('')
|
const [feedbackText, setFeedbackText] = useState('')
|
||||||
const [feedbackSending, setFeedbackSending] = useState(false)
|
const [feedbackSending, setFeedbackSending] = useState(false)
|
||||||
@@ -41,6 +43,8 @@ export function HeaderPanel() {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.viewers != null) setViewers(data.viewers)
|
if (data.viewers != null) setViewers(data.viewers)
|
||||||
if (data.cumulative != null) setCumulative(data.cumulative)
|
if (data.cumulative != null) setCumulative(data.cumulative)
|
||||||
|
if (data.feedbackCount != null) setFeedbackCount(data.feedbackCount)
|
||||||
|
if (data.shareCount != null) setShareCount(data.shareCount)
|
||||||
} catch {
|
} catch {
|
||||||
setViewers((v) => (v > 0 ? v : 0))
|
setViewers((v) => (v > 0 ? v : 0))
|
||||||
setCumulative((c) => (c > 0 ? c : 0))
|
setCumulative((c) => (c > 0 ? c : 0))
|
||||||
@@ -56,16 +60,27 @@ export function HeaderPanel() {
|
|||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
const url = window.location.href
|
const url = window.location.href
|
||||||
const title = '美伊军事态势显示'
|
const title = '美伊军事态势显示'
|
||||||
|
let shared = false
|
||||||
if (typeof navigator.share === 'function') {
|
if (typeof navigator.share === 'function') {
|
||||||
try {
|
try {
|
||||||
await navigator.share({ title, url })
|
await navigator.share({ title, url })
|
||||||
|
shared = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((e as Error).name !== 'AbortError') {
|
if ((e as Error).name !== 'AbortError') {
|
||||||
await copyToClipboard(url)
|
await copyToClipboard(url)
|
||||||
|
shared = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await copyToClipboard(url)
|
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) {
|
if (data.ok) {
|
||||||
setFeedbackText('')
|
setFeedbackText('')
|
||||||
setFeedbackDone(true)
|
setFeedbackDone(true)
|
||||||
|
setFeedbackCount((c) => c + 1)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setFeedbackOpen(false)
|
setFeedbackOpen(false)
|
||||||
setFeedbackDone(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]"
|
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]"
|
||||||
>
|
>
|
||||||
<MessageSquare className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
<MessageSquare className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||||
留言
|
留言 {feedbackCount > 0 && <span className="tabular-nums">{feedbackCount}</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -171,7 +187,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]"
|
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]"
|
||||||
>
|
>
|
||||||
<Share2 className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
<Share2 className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||||
分享
|
分享 {shareCount > 0 && <span className="tabular-nums">{shareCount}</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -39,18 +39,19 @@ export function Dashboard() {
|
|||||||
<div className="h-[45vmin] min-h-[180px] w-full shrink-0 xl:min-h-0 xl:flex-1">
|
<div className="h-[45vmin] min-h-[180px] w-full shrink-0 xl:min-h-0 xl:flex-1">
|
||||||
<WarMap />
|
<WarMap />
|
||||||
</div>
|
</div>
|
||||||
|
{/* 竖屏:战损→美国基地→伊朗基地。xl:美国基地|战损(中)|伊朗基地 */}
|
||||||
<div className="flex shrink-0 flex-col gap-2 overflow-x-auto border-t border-military-border bg-military-panel/95 px-3 py-2 xl:flex-row xl:items-stretch xl:overflow-visible xl:px-4">
|
<div className="flex shrink-0 flex-col gap-2 overflow-x-auto border-t border-military-border bg-military-panel/95 px-3 py-2 xl:flex-row xl:items-stretch xl:overflow-visible xl:px-4">
|
||||||
|
<BaseStatusPanel keyLocations={situation.usForces.keyLocations} className="order-2 shrink-0 xl:order-1 xl:min-w-[200px] xl:border-r xl:border-military-border xl:pr-4" />
|
||||||
<CombatLossesPanel
|
<CombatLossesPanel
|
||||||
usLosses={situation.usForces.combatLosses}
|
usLosses={situation.usForces.combatLosses}
|
||||||
iranLosses={situation.iranForces.combatLosses}
|
iranLosses={situation.iranForces.combatLosses}
|
||||||
conflictStats={situation.conflictStats}
|
conflictStats={situation.conflictStats}
|
||||||
civilianTotal={situation.civilianCasualtiesTotal}
|
civilianTotal={situation.civilianCasualtiesTotal}
|
||||||
className="min-w-0 flex-1 shrink-0 py-1"
|
className="order-1 min-w-0 flex-1 shrink-0 py-1 xl:order-2"
|
||||||
/>
|
/>
|
||||||
<BaseStatusPanel keyLocations={situation.usForces.keyLocations} className="shrink-0 xl:min-w-[200px] xl:border-r xl:border-military-border xl:pr-4" />
|
|
||||||
<IranBaseStatusPanel
|
<IranBaseStatusPanel
|
||||||
keyLocations={situation.iranForces.keyLocations}
|
keyLocations={situation.iranForces.keyLocations}
|
||||||
className="min-w-0 shrink-0 xl:min-w-[200px]"
|
className="order-3 min-w-0 shrink-0 xl:min-w-[200px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import path from 'path'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['lucide-react'],
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|||||||
Reference in New Issue
Block a user