fix:优化docker p配置
This commit is contained in:
10
README.md
10
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 令牌,构建时注入
|
||||
|
||||
BIN
crawler/__pycache__/cleaner_ai.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/cleaner_ai.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/config.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/db_writer.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/db_writer.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/parser.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/parser.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/parser_ai.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/parser_ai.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/realtime_conflict_service.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/realtime_conflict_service.cpython-311.pyc
Normal file
Binary file not shown.
BIN
crawler/__pycache__/translate_utils.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/translate_utils.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
13
docker-compose.dev.yml
Normal file
13
docker-compose.dev.yml
Normal file
@@ -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"]
|
||||
12
server/db.js
12
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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
<span className="text-[10px] opacity-70">|</span>
|
||||
<span className="text-[10px]">累积 <b className="tabular-nums">{cumulative}</b></span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackOpen(true)}
|
||||
className="flex items-center gap-1 rounded border border-military-border px-2 py-1 text-[10px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400"
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
留言
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShare}
|
||||
@@ -198,6 +236,48 @@ export function HeaderPanel() {
|
||||
className="border-military-accent/50"
|
||||
/>
|
||||
</div>
|
||||
{/* 留言弹窗 */}
|
||||
{feedbackOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
|
||||
onClick={() => !feedbackSending && setFeedbackOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="w-[90%] max-w-md rounded-lg border border-military-border bg-military-panel p-4 shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="mb-2 text-sm font-medium text-military-accent">后台留言</h3>
|
||||
<p className="mb-2 text-[10px] text-military-text-secondary">
|
||||
您的反馈将提交给开发者,用于后续优化
|
||||
</p>
|
||||
<textarea
|
||||
value={feedbackText}
|
||||
onChange={(e) => setFeedbackText(e.target.value)}
|
||||
placeholder="请输入留言内容(1–2000 字)..."
|
||||
rows={4}
|
||||
maxLength={2000}
|
||||
className="mb-3 w-full resize-none rounded border border-military-border bg-military-dark px-2 py-2 text-xs text-military-text-primary placeholder:text-military-text-secondary focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !feedbackSending && setFeedbackOpen(false)}
|
||||
className="rounded border border-military-border px-3 py-1 text-[10px] text-military-text-secondary hover:bg-military-border/30"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFeedback}
|
||||
disabled={!feedbackText.trim() || feedbackSending}
|
||||
className="rounded bg-cyan-600 px-3 py-1 text-[10px] text-white hover:bg-cyan-500 disabled:opacity-50"
|
||||
>
|
||||
{feedbackSending ? '提交中...' : feedbackDone ? '已提交' : '提交'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user