diff --git a/package-lock.json b/package-lock.json index 22edd82..651d773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-dom": "^18.3.1", "react-map-gl": "^7.1.7", "react-router-dom": "^7.13.1", + "swagger-ui-express": "^5.0.1", "ws": "^8.19.0", "zustand": "^5.0.0" }, @@ -1344,6 +1345,12 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5023,6 +5030,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.0", + "resolved": "https://registry.npmmirror.com/swagger-ui-dist/-/swagger-ui-dist-5.32.0.tgz", + "integrity": "sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", diff --git a/package.json b/package.json index c977cb4..bf8d36a 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,11 @@ "express": "^4.21.1", "lucide-react": "^0.460.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", "react-router-dom": "^7.13.1", + "swagger-ui-express": "^5.0.1", "ws": "^8.19.0", "zustand": "^5.0.0" }, diff --git a/server/data.db-wal b/server/data.db-wal index dc7ce8c..2eb1238 100644 Binary files a/server/data.db-wal and b/server/data.db-wal differ diff --git a/server/index.js b/server/index.js index 395d3d5..33535b7 100644 --- a/server/index.js +++ b/server/index.js @@ -7,12 +7,19 @@ const { WebSocketServer } = require('ws') const routes = require('./routes') const { getSituation } = require('./situationData') +const swaggerUi = require('swagger-ui-express') +const openApiSpec = require('./openapi') + const app = express() const PORT = process.env.API_PORT || 3001 app.set('trust proxy', 1) app.use(cors()) app.use(express.json()) + +// Swagger 文档 +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpec)) + app.use('/api', routes) app.get('/api/health', (_, res) => res.json({ ok: true })) app.post('/api/crawler/notify', (_, res) => { @@ -58,4 +65,5 @@ function notifyCrawlerUpdate() { server.listen(PORT, () => { console.log(`API + WebSocket running at http://localhost:${PORT}`) + console.log(`Swagger docs at http://localhost:${PORT}/api-docs`) }) diff --git a/server/openapi.js b/server/openapi.js new file mode 100644 index 0000000..7f61366 --- /dev/null +++ b/server/openapi.js @@ -0,0 +1,134 @@ +/** + * OpenAPI 3.0 规范,供 Swagger UI 展示 + */ +const PORT = process.env.API_PORT || 3001 + +module.exports = { + openapi: '3.0.3', + info: { + title: 'US-Iran Military Dashboard API', + version: '1.0.0', + description: '美伊军事态势面板后端接口', + }, + servers: [{ url: `http://localhost:${PORT}`, description: '本地' }], + paths: { + '/api/situation': { + get: { + summary: '获取态势数据', + description: '战损、基地、新闻、冲突事件等完整态势', + tags: ['态势'], + responses: { + 200: { + description: '态势 JSON', + content: { 'application/json': { schema: { type: 'object' } } }, + }, + }, + }, + }, + '/api/db/dashboard': { + get: { + summary: '数据库面板', + description: '各表原始数据,供 /db 调试页', + tags: ['调试'], + responses: { + 200: { + description: '各表数据', + content: { 'application/json': { schema: { type: 'object' } } }, + }, + }, + }, + }, + '/api/visit': { + post: { + summary: '来访统计', + description: '记录 IP,返回当前在看人数和累积访问', + tags: ['统计'], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + viewers: { type: 'number' }, + cumulative: { type: 'number' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/api/feedback': { + post: { + summary: '提交反馈', + description: '留言 1–2000 字', + tags: ['反馈'], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['content'], + properties: { content: { type: 'string', maxLength: 2000 } }, + }, + }, + }, + }, + responses: { + 200: { description: 'ok: true' }, + 400: { description: '内容超长或为空' }, + }, + }, + }, + '/api/health': { + get: { + summary: '健康检查', + tags: ['系统'], + responses: { 200: { description: 'ok: true' } }, + }, + }, + '/api/crawler/notify': { + post: { + summary: '爬虫通知', + description: '爬虫更新后调用,触发前端推送', + tags: ['系统'], + responses: { 200: { description: 'ok' } }, + }, + }, + '/api/stats': { + get: { + summary: '统计快照', + description: 'viewers / cumulative,不写入', + tags: ['统计'], + responses: { + 200: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + viewers: { type: 'number' }, + cumulative: { type: 'number' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/api/events': { + get: { + summary: '冲突事件', + description: '冲突事件列表及统计', + tags: ['态势'], + responses: { 200: { description: 'events + conflict_stats' } }, + }, + }, + }, + tags: [{ name: '态势' }, { name: '统计' }, { name: '反馈' }, { name: '调试' }, { name: '系统' }], +} diff --git a/src/utils/tickerText.ts b/src/utils/tickerText.ts index f1b3ac8..87071c8 100644 --- a/src/utils/tickerText.ts +++ b/src/utils/tickerText.ts @@ -1,10 +1,6 @@ /** - * 滚动情报文本处理:转为简体中文,过滤非中文内容 + * 滚动情报文本处理:过滤非中文为主的内容 */ -import { Converter } from 'opencc-js/t2cn' - -const t2s = Converter({ from: 'twp', to: 'cn' }) - /** 简体中文字符范围 */ const ZH_REGEX = /[\u4e00-\u9fff]/g @@ -16,20 +12,10 @@ export function isMostlyChinese(text: string): boolean { return zhCount / text.length >= 0.3 } -/** 繁体转简体 */ -export function toSimplifiedChinese(text: string): string { - if (!text?.trim()) return text - try { - return t2s(text) - } catch { - return text - } -} - -/** 处理滚动情报项:转为简体,非中文为主则过滤 */ +/** 处理滚动情报项:非中文为主则过滤 */ export function processTickerText(text: string): string | null { const t = (text || '').trim() if (!t) return null if (!isMostlyChinese(t)) return null - return toSimplifiedChinese(t) + return t }