fix:优化整个项目
This commit is contained in:
@@ -69,13 +69,27 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========== 3. 测试经 Nginx 访问 /api/config(本机)==========="
|
echo "========== 3. 测试经 Nginx 访问 /api/config(本机 HTTP 80)==========="
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
code_nginx=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1/api/config" -H "Host: view.airtep.com" 2>/dev/null)
|
code_80=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1/api/config" -H "Host: view.airtep.com" 2>/dev/null)
|
||||||
echo "curl -H Host:view.airtep.com http://127.0.0.1/api/config => HTTP $code_nginx"
|
echo "curl http://127.0.0.1/api/config (Host: view.airtep.com) => HTTP $code_80"
|
||||||
if [ "$code_nginx" = "200" ]; then
|
[ "$code_80" = "200" ] && echo "[OK] 80 反代正常。" || echo "[失败] 80 返回 $code_80"
|
||||||
echo "[OK] 经 Nginx 反代访问 /api/config 正常。"
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========== 4. 测试 HTTPS https://view.airtep.com/api/config ==========="
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
code_443=$(curl -sk -o /dev/null -w "%{http_code}" "https://view.airtep.com/api/config" 2>/dev/null)
|
||||||
|
echo "curl -sk https://view.airtep.com/api/config => HTTP $code_443"
|
||||||
|
if [ "$code_443" = "200" ]; then
|
||||||
|
echo "[OK] HTTPS 反代正常。"
|
||||||
else
|
else
|
||||||
echo "[注意] 经 Nginx 返回 $code_nginx,请检查 Nginx 配置并重载: nginx -t && nginx -s reload"
|
echo "[失败] HTTPS 返回 $code_443。若 80 正常而 443 为 404,说明 443 的 server 块内缺少 location /api 或顺序有误。"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========== 5. Nginx 中 location 顺序(前 20 个)==========="
|
||||||
|
if [ -n "$NGINX_CONF" ] && [ -f "$NGINX_CONF" ]; then
|
||||||
|
grep -n "location \|server {" "$NGINX_CONF" | head -25
|
||||||
|
fi
|
||||||
|
|||||||
102
docs/nginx-view.airtep.com-完整示例.conf
Normal file
102
docs/nginx-view.airtep.com-完整示例.conf
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# view.airtep.com 完整 server 配置示例(80+443 合一,/api 反代 5599)
|
||||||
|
# 在宝塔 → 网站 → view.airtep.com → 设置 → 配置文件 中,用本文件内容替换整个 server { } 块后保存,再 nginx -t && nginx -s reload
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name view.airtep.com;
|
||||||
|
index index.php index.html index.htm default.php default.htm default.html;
|
||||||
|
root /www/wwwroot/view.airtep.com/dist/;
|
||||||
|
|
||||||
|
#CERT-APPLY-CHECK--START
|
||||||
|
include /www/server/panel/vhost/nginx/well-known/view.airtep.com.conf;
|
||||||
|
#CERT-APPLY-CHECK--END
|
||||||
|
include /www/server/panel/vhost/nginx/extension/view.airtep.com/*.conf;
|
||||||
|
|
||||||
|
#SSL-START
|
||||||
|
ssl_certificate /www/server/panel/vhost/cert/view.airtep.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /www/server/panel/vhost/cert/view.airtep.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_tickets on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000";
|
||||||
|
error_page 497 https://$host$request_uri;
|
||||||
|
#SSL-END
|
||||||
|
|
||||||
|
#ERROR-PAGE-START
|
||||||
|
error_page 404 /404.html;
|
||||||
|
#ERROR-PAGE-END
|
||||||
|
|
||||||
|
#PHP-INFO-START
|
||||||
|
include enable-php-00.conf;
|
||||||
|
#PHP-INFO-END
|
||||||
|
|
||||||
|
#REWRITE-START
|
||||||
|
include /www/server/panel/vhost/rewrite/view.airtep.com.conf;
|
||||||
|
#REWRITE-END
|
||||||
|
|
||||||
|
# ---------- 先配路由与反代,再配禁止规则 ----------
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:5599/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /edit {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:3003;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止访问的敏感文件(不含 api.config.json、config.json)
|
||||||
|
location ~* (\.user\.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$
|
||||||
|
{
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.well-known {
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
|
||||||
|
expires 30d;
|
||||||
|
error_log /dev/null;
|
||||||
|
access_log /dev/null;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ .*\.(js|css)?$ {
|
||||||
|
expires 12h;
|
||||||
|
error_log /dev/null;
|
||||||
|
access_log /dev/null;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log /www/wwwlogs/view.airtep.com.log;
|
||||||
|
error_log /www/wwwlogs/view.airtep.com.error.log;
|
||||||
|
}
|
||||||
19
docs/nginx-view.airtep.com-必加.conf
Normal file
19
docs/nginx-view.airtep.com-必加.conf
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# view.airtep.com 必须有的两段配置(避免 404)
|
||||||
|
# 在宝塔 → 网站 → view.airtep.com → 设置 → 配置文件 中,
|
||||||
|
# 在 server { } 内、且不要有重复或冲突的 location /api 或 location /api/ 。
|
||||||
|
# 只保留下面这一种 /api 反代,删掉任何 proxy_pass 到 3003 或别的端口的 location /api/ 。
|
||||||
|
|
||||||
|
# 1) 把 /api 开头的请求转到本机 5599(本项目 API)
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:5599/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2) 若 api.config.json 被「禁止访问」规则误拦,可加下面这段(二选一,一般 root 正确即可)
|
||||||
|
# location = /api.config.json {
|
||||||
|
# alias /www/wwwroot/view.airtep.com/dist/api.config.json;
|
||||||
|
# }
|
||||||
86
docs/前端与接口关系说明.md
Normal file
86
docs/前端与接口关系说明.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 前端与接口关系说明
|
||||||
|
|
||||||
|
## 一、架构关系
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器访问 https://view.airtep.com
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Nginx (80/443)
|
||||||
|
│
|
||||||
|
├─ / → root dist/ → index.html、config.json、api.config.json、lib/、image/
|
||||||
|
├─ /api/* → 反代到 127.0.0.1:5599/ → 后端 Node(无 /api 前缀)
|
||||||
|
└─ 其他静态 → dist/ 下文件
|
||||||
|
```
|
||||||
|
|
||||||
|
- **前端**:纯静态,来自 `dist/`(index.html、config.json、api.config.json、lib/、image/)。由 Nginx 的 `root` + `location /` 提供。
|
||||||
|
- **接口**:Node 进程监听 **5599**,路由为 **/config、/stats、/view、/join、/comments** 等(**没有 /api 前缀**)。
|
||||||
|
- **Nginx 反代**:必须用 `location /api` 且 `proxy_pass http://127.0.0.1:5599/;`(末尾有 `/`),这样请求 `/api/config` 会被转成后端的 `/config`。
|
||||||
|
|
||||||
|
## 二、为何出现 404
|
||||||
|
|
||||||
|
`nginx -T` 里会看到**多个** `location /api` 和 **location /api/**,来自不同站点或 include:
|
||||||
|
|
||||||
|
- **location /api**(4 个字符)→ 反代到 5599 ✅
|
||||||
|
- **location /api/**(5 个字符)→ 可能反代到 3000、3001、9091 等 ❌
|
||||||
|
|
||||||
|
Nginx 对**前缀 location** 选**最长匹配**。请求 `/api/config` 时,若当前 server 里**同时存在** `location /api` 和 `location /api/`,会匹配到 **location /api/**。若这个 `/api/` 指向的是 3001 等其它服务,就会返回 404。
|
||||||
|
|
||||||
|
因此:**view.airtep.com 的 server 里只能保留「反代到 5599」的那一个 location,且不要有「location /api/」指向其它端口。**
|
||||||
|
|
||||||
|
## 三、正确配置(view.airtep.com 仅此一段 /api)
|
||||||
|
|
||||||
|
在 view.airtep.com 的 server 块内:
|
||||||
|
|
||||||
|
- **只保留**下面这一段,**不要**再写 `location /api/` 或其它端口的 `/api`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:5599/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 若存在 **include**(例如 `extension/view.airtep.com/*.conf`),检查这些 conf 里是否带有 **location /api/** 或 **location /api**。若有且指向 3000/3001/9091 等,要在 view.airtep.com 的站点下**删掉或注释**这些 include 中的 `/api` 配置,或在该 include 里**只保留**上面这段 5599 反代。
|
||||||
|
|
||||||
|
## 四、检查与验证
|
||||||
|
|
||||||
|
在服务器上:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 本机 API 正常
|
||||||
|
curl -s http://127.0.0.1:5599/config
|
||||||
|
# 应返回:{"danmakuEnabled":false,"danmakuPosition":"top"}
|
||||||
|
|
||||||
|
# 2. 查看 view.airtep.com 所在 server 块中的 /api 配置(行号供对照)
|
||||||
|
grep -n "location /api\|proxy_pass" /www/server/panel/vhost/nginx/view.airtep.com.conf
|
||||||
|
|
||||||
|
# 3. 查看是否有 extension 覆盖 /api
|
||||||
|
ls /www/server/panel/vhost/nginx/extension/view.airtep.com/
|
||||||
|
cat /www/server/panel/vhost/nginx/extension/view.airtep.com/*.conf
|
||||||
|
|
||||||
|
# 4. 重载后测 80 和 443
|
||||||
|
nginx -t && nginx -s reload
|
||||||
|
curl -s -o /dev/null -w "80: %{http_code}\n" "http://127.0.0.1/api/config" -H "Host: view.airtep.com"
|
||||||
|
curl -sk -o /dev/null -w "443: %{http_code}\n" "https://view.airtep.com/api/config"
|
||||||
|
# 两者都应为 200
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、后端数据落库与前端显示
|
||||||
|
|
||||||
|
- **存储**:后端使用 `server/data/store.json`(JSON 文件),路径由 `server/db.js` 的 `__dirname` 决定,与进程当前工作目录无关;每次修改(浏览/点赞/分享/加入离开/评论)都会调用 `save()` 落盘。
|
||||||
|
- **验证落库**:在服务器上查看 `server/data/store.json` 或请求 `curl -s http://127.0.0.1:5599/stats` 即可确认数据是否写入。
|
||||||
|
- **前端显示异常**:若接口正常但页面上的「播放次数、点赞、分享、评论、在看」出现被置 0 或错乱,多为前端用「部分接口返回值」直接刷新整块统计导致(例如 `/join` 只返回 `watchingNow`,却用 `updateStatsUI({ watchingNow })` 把其他项也刷成 0)。处理方式:前端维护一份 `lastStats`,每次只合并本次接口返回的字段再刷新 UI,避免部分更新覆盖其它统计。
|
||||||
|
|
||||||
|
## 六、小结
|
||||||
|
|
||||||
|
| 角色 | 说明 |
|
||||||
|
|------------|------|
|
||||||
|
| 前端 | Nginx 提供 dist/ 下静态资源,同域访问。 |
|
||||||
|
| 接口 | Node 监听 5599,路由为 /config、/stats 等(无 /api 前缀)。 |
|
||||||
|
| Nginx 反代 | 仅用 **location /api** 且 **proxy_pass http://127.0.0.1:5599/**,且本站点内不要有 **location /api/** 指向其它端口。 |
|
||||||
|
| 404 原因 | 存在 **location /api/** 且指向 3001 等,优先于 **location /api**,请求被转到错误后端。 |
|
||||||
79
index.html
79
index.html
@@ -85,6 +85,7 @@
|
|||||||
}
|
}
|
||||||
.bottom_right .CustomButton:hover { background: rgba(0,0,0,.5); }
|
.bottom_right .CustomButton:hover { background: rgba(0,0,0,.5); }
|
||||||
.bottom_right .CustomButton.selected { border-color: #fa6400; }
|
.bottom_right .CustomButton.selected { border-color: #fa6400; }
|
||||||
|
.CustomButton .btn-icon { width: 22px; height: 22px; margin-bottom: 2px; flex-shrink: 0; }
|
||||||
.CustomButton .btn-label { display: block; }
|
.CustomButton .btn-label { display: block; }
|
||||||
.safeHeight { height: env(safe-area-inset-bottom, 0); }
|
.safeHeight { height: env(safe-area-inset-bottom, 0); }
|
||||||
|
|
||||||
@@ -113,9 +114,16 @@
|
|||||||
.modal-box .btns .btn-ok { background: #fa6400; color: #fff; }
|
.modal-box .btns .btn-ok { background: #fa6400; color: #fff; }
|
||||||
.modal-box .btns .btn-cancel { background: #555; color: #fff; }
|
.modal-box .btns .btn-cancel { background: #555; color: #fff; }
|
||||||
|
|
||||||
/* 隐藏 Pannellum 左侧控件及左下角标志 */
|
/* 拖拽顺滑:渲染层走 GPU 合成,减少卡顿 */
|
||||||
|
#panorama .pnlm-render-container { transform: translateZ(0); backface-visibility: hidden; }
|
||||||
|
#panorama .pnlm-container { transform: translateZ(0); }
|
||||||
|
/* 隐藏 Pannellum 左侧控件及左下角标志;左下角标题块保留原样式,仅做排版对齐(与顶部作者左侧对齐、不贴边) */
|
||||||
#panorama .pnlm-controls-container { display: none !important; }
|
#panorama .pnlm-controls-container { display: none !important; }
|
||||||
#panorama .pnlm-about-msg { display: none !important; }
|
#panorama .pnlm-about-msg { display: none !important; }
|
||||||
|
#panorama .pnlm-panorama-info {
|
||||||
|
left: max(12px, env(safe-area-inset-left)) !important;
|
||||||
|
padding-left: 12px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -153,10 +161,22 @@
|
|||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="bottomWp">
|
<div class="bottomWp">
|
||||||
<div class="bottom_right">
|
<div class="bottom_right">
|
||||||
<div class="CustomButton" id="btnIntro" title="简介"><span class="btn-label">简介</span></div>
|
<div class="CustomButton" id="btnIntro" title="简介">
|
||||||
<div class="CustomButton" id="btnShare" title="分享"><span class="btn-label">分享</span></div>
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="8" y1="7" x2="16" y2="7"/><line x1="8" y1="11" x2="16" y2="11"/></svg>
|
||||||
<div class="CustomButton" id="btnLike" title="赞"><span class="btn-label">赞</span></div>
|
<span class="btn-label">简介</span>
|
||||||
<div class="CustomButton" id="btnComment" title="留言"><span class="btn-label">留言</span></div>
|
</div>
|
||||||
|
<div class="CustomButton" id="btnShare" title="分享">
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||||
|
<span class="btn-label">分享</span>
|
||||||
|
</div>
|
||||||
|
<div class="CustomButton" id="btnLike" title="赞">
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||||
|
<span class="btn-label">赞</span>
|
||||||
|
</div>
|
||||||
|
<div class="CustomButton" id="btnComment" title="留言">
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
<span class="btn-label">留言</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="safeHeight"></div>
|
<div class="safeHeight"></div>
|
||||||
@@ -198,6 +218,7 @@
|
|||||||
var viewerId = null;
|
var viewerId = null;
|
||||||
var statsInterval = null;
|
var statsInterval = null;
|
||||||
var danmakuLastId = 0;
|
var danmakuLastId = 0;
|
||||||
|
var lastStats = { viewCount: 0, likeCount: 0, shareCount: 0, commentCount: 0, watchingNow: 0 };
|
||||||
|
|
||||||
function uuid() {
|
function uuid() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
@@ -229,22 +250,49 @@
|
|||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMobileView() {
|
||||||
|
return (typeof window.matchMedia !== 'undefined' && window.matchMedia('(max-width: 768px)').matches) ||
|
||||||
|
(typeof window.orientation !== 'undefined') || ('ontouchstart' in window && window.innerWidth <= 1024);
|
||||||
|
}
|
||||||
function buildViewer(config) {
|
function buildViewer(config) {
|
||||||
destroyViewer();
|
destroyViewer();
|
||||||
hideError();
|
hideError();
|
||||||
if (container) container.style.display = 'block';
|
if (container) container.style.display = 'block';
|
||||||
config.autoLoad = config.autoLoad !== false;
|
config.autoLoad = config.autoLoad !== false;
|
||||||
config.hfov = config.hfov || 100;
|
var mobile = isMobileView();
|
||||||
config.minHfov = config.minHfov != null ? config.minHfov : 50;
|
var targetHfov;
|
||||||
config.maxHfov = config.maxHfov != null ? config.maxHfov : 120;
|
if (mobile) {
|
||||||
|
targetHfov = config.hfov != null ? Math.min(config.hfov, 82) : 78;
|
||||||
|
config.minHfov = config.minHfov != null ? config.minHfov : 55;
|
||||||
|
config.maxHfov = config.maxHfov != null ? Math.min(config.maxHfov, 108) : 105;
|
||||||
|
config.hfov = config.maxHfov;
|
||||||
|
} else {
|
||||||
|
targetHfov = config.hfov || 100;
|
||||||
|
config.minHfov = config.minHfov != null ? config.minHfov : 50;
|
||||||
|
config.maxHfov = config.maxHfov != null ? config.maxHfov : 120;
|
||||||
|
config.hfov = config.maxHfov;
|
||||||
|
}
|
||||||
config.showZoomCtrl = false;
|
config.showZoomCtrl = false;
|
||||||
config.compass = false;
|
config.compass = false;
|
||||||
config.showFullscreenCtrl = false;
|
config.showFullscreenCtrl = false;
|
||||||
|
config.draggable = true;
|
||||||
|
config.friction = 0.06;
|
||||||
|
config.touchPanSpeedCoeffFactor = 1.2;
|
||||||
try {
|
try {
|
||||||
currentViewer = pannellum.viewer('panorama', config);
|
currentViewer = pannellum.viewer('panorama', config);
|
||||||
|
var fisheyeDone = false;
|
||||||
|
function runFisheyeEnter() {
|
||||||
|
if (fisheyeDone || !currentViewer) return;
|
||||||
|
fisheyeDone = true;
|
||||||
|
if (currentViewer.getHfov && currentViewer.getHfov() === config.maxHfov) {
|
||||||
|
currentViewer.setHfov(targetHfov, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
currentViewer.on('error', function(err) {
|
currentViewer.on('error', function(err) {
|
||||||
showError('全景图加载失败:' + (err && err.message ? err.message : '未知错误'));
|
showError('全景图加载失败:' + (err && err.message ? err.message : '未知错误'));
|
||||||
});
|
});
|
||||||
|
currentViewer.on('load', runFisheyeEnter);
|
||||||
|
setTimeout(runFisheyeEnter, 500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError('创建查看器失败:' + (e.message || e));
|
showError('创建查看器失败:' + (e.message || e));
|
||||||
}
|
}
|
||||||
@@ -252,16 +300,19 @@
|
|||||||
|
|
||||||
function updateStatsUI(stats) {
|
function updateStatsUI(stats) {
|
||||||
if (!stats) return;
|
if (!stats) return;
|
||||||
|
var keys = ['viewCount', 'likeCount', 'shareCount', 'commentCount', 'watchingNow'];
|
||||||
|
keys.forEach(function(k) { if (stats[k] !== undefined) lastStats[k] = stats[k]; });
|
||||||
|
var s = lastStats;
|
||||||
var w = document.getElementById('watchingNow');
|
var w = document.getElementById('watchingNow');
|
||||||
if (w) w.textContent = (stats.watchingNow || 0) + ' 人在看';
|
if (w) w.textContent = (s.watchingNow || 0) + ' 人在看';
|
||||||
var v = document.getElementById('viewCount');
|
var v = document.getElementById('viewCount');
|
||||||
if (v) v.textContent = '共 ' + formatNum(stats.viewCount || 0) + ' 次播放';
|
if (v) v.textContent = '共 ' + formatNum(s.viewCount || 0) + ' 次播放';
|
||||||
var l = document.getElementById('likeCount');
|
var l = document.getElementById('likeCount');
|
||||||
if (l) l.textContent = formatNum(stats.likeCount || 0);
|
if (l) l.textContent = formatNum(s.likeCount || 0);
|
||||||
var s = document.getElementById('shareCount');
|
var shareEl = document.getElementById('shareCount');
|
||||||
if (s) s.textContent = formatNum(stats.shareCount || 0);
|
if (shareEl) shareEl.textContent = formatNum(s.shareCount || 0);
|
||||||
var c = document.getElementById('commentCount');
|
var c = document.getElementById('commentCount');
|
||||||
if (c) c.textContent = formatNum(stats.commentCount || 0);
|
if (c) c.textContent = formatNum(s.commentCount || 0);
|
||||||
}
|
}
|
||||||
function formatNum(n) {
|
function formatNum(n) {
|
||||||
n = Number(n);
|
n = Number(n);
|
||||||
|
|||||||
117
nginx.config
Normal file
117
nginx.config
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
server
|
||||||
|
{
|
||||||
|
listen 80;
|
||||||
|
listen 443 ssl http2 ;
|
||||||
|
server_name view.airtep.com;
|
||||||
|
index index.php index.html index.htm default.php default.htm default.html;
|
||||||
|
root /www/wwwroot/view.airtep.com/dist/;
|
||||||
|
#CERT-APPLY-CHECK--START
|
||||||
|
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
|
||||||
|
include /www/server/panel/vhost/nginx/well-known/view.airtep.com.conf;
|
||||||
|
#CERT-APPLY-CHECK--END
|
||||||
|
include /www/server/panel/vhost/nginx/extension/view.airtep.com/*.conf;
|
||||||
|
|
||||||
|
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
|
||||||
|
#error_page 404/404.html;
|
||||||
|
ssl_certificate /www/server/panel/vhost/cert/view.airtep.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /www/server/panel/vhost/cert/view.airtep.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_tickets on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000";
|
||||||
|
error_page 497 https://$host$request_uri;
|
||||||
|
|
||||||
|
#SSL-END
|
||||||
|
|
||||||
|
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
|
||||||
|
error_page 404 /404.html;
|
||||||
|
#error_page 502 /502.html;
|
||||||
|
#ERROR-PAGE-END
|
||||||
|
|
||||||
|
#PHP-INFO-START PHP引用配置,可以注释或修改
|
||||||
|
include enable-php-00.conf;
|
||||||
|
#PHP-INFO-END
|
||||||
|
|
||||||
|
#REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
|
||||||
|
include /www/server/panel/vhost/rewrite/view.airtep.com.conf;
|
||||||
|
#REWRITE-END
|
||||||
|
|
||||||
|
# 禁止访问的敏感文件
|
||||||
|
location ~* (\.user.ini|\.htaccess|\.htpasswd|\.env.*|\.project|\.bashrc|\.bash_profile|\.bash_logout|\.DS_Store|\.gitignore|\.gitattributes|LICENSE|README\.md|CLAUDE\.md|CHANGELOG\.md|CHANGELOG|CONTRIBUTING\.md|TODO\.md|FAQ\.md|composer\.json|composer\.lock|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|\.\w+~|\.swp|\.swo|\.bak(up)?|\.old|\.tmp|\.temp|\.log|\.sql(\.gz)?|docker-compose\.yml|docker\.env|Dockerfile|\.csproj|\.sln|Cargo\.toml|Cargo\.lock|go\.mod|go\.sum|phpunit\.xml|phpunit\.xml|pom\.xml|build\.gradl|pyproject\.toml|requirements\.txt|application(-\w+)?\.(ya?ml|properties))$
|
||||||
|
{
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止访问的敏感目录
|
||||||
|
location ~* /(\.git|\.svn|\.bzr|\.vscode|\.claude|\.idea|\.ssh|\.github|\.npm|\.yarn|\.pnpm|\.cache|\.husky|\.turbo|\.next|\.nuxt|node_modules|runtime)/ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
#一键申请SSL证书验证目录相关设置
|
||||||
|
location ~ \.well-known{
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将 /api 转发到本机 Node 后端(端口与 start-api.sh 一致)
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:5599/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#禁止在证书验证目录放入敏感文件
|
||||||
|
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
|
||||||
|
{
|
||||||
|
expires 30d;
|
||||||
|
error_log /dev/null;
|
||||||
|
access_log /dev/null;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ .*\.(js|css)?$
|
||||||
|
{
|
||||||
|
expires 12h;
|
||||||
|
error_log /dev/null;
|
||||||
|
access_log /dev/null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# location /api/ {
|
||||||
|
# proxy_pass http://localhost:3003/api/;
|
||||||
|
# }
|
||||||
|
# 前端 SPA:/edit、/db 等路由都返回 index.html
|
||||||
|
location = /edit {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:3003;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log /www/wwwlogs/view.airtep.com.log;
|
||||||
|
error_log /www/wwwlogs/view.airtep.com.error.log;
|
||||||
|
}
|
||||||
55
src/main.js
55
src/main.js
@@ -10,6 +10,7 @@
|
|||||||
var viewerId = null;
|
var viewerId = null;
|
||||||
var statsInterval = null;
|
var statsInterval = null;
|
||||||
var danmakuLastId = 0;
|
var danmakuLastId = 0;
|
||||||
|
var lastStats = { viewCount: 0, likeCount: 0, shareCount: 0, commentCount: 0, watchingNow: 0 };
|
||||||
|
|
||||||
function uuid() {
|
function uuid() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
@@ -48,22 +49,53 @@
|
|||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMobileView() {
|
||||||
|
return (
|
||||||
|
(typeof window.matchMedia !== 'undefined' &&
|
||||||
|
window.matchMedia('(max-width: 768px)').matches) ||
|
||||||
|
typeof window.orientation !== 'undefined' ||
|
||||||
|
('ontouchstart' in window && window.innerWidth <= 1024)
|
||||||
|
);
|
||||||
|
}
|
||||||
function buildViewer(config) {
|
function buildViewer(config) {
|
||||||
destroyViewer();
|
destroyViewer();
|
||||||
hideError();
|
hideError();
|
||||||
if (container) container.style.display = 'block';
|
if (container) container.style.display = 'block';
|
||||||
config.autoLoad = config.autoLoad !== false;
|
config.autoLoad = config.autoLoad !== false;
|
||||||
config.hfov = config.hfov || 100;
|
var mobile = isMobileView();
|
||||||
config.minHfov = config.minHfov != null ? config.minHfov : 50;
|
var targetHfov;
|
||||||
config.maxHfov = config.maxHfov != null ? config.maxHfov : 120;
|
if (mobile) {
|
||||||
|
targetHfov = config.hfov != null ? Math.min(config.hfov, 82) : 78;
|
||||||
|
config.minHfov = config.minHfov != null ? config.minHfov : 55;
|
||||||
|
config.maxHfov = config.maxHfov != null ? Math.min(config.maxHfov, 108) : 105;
|
||||||
|
config.hfov = config.maxHfov;
|
||||||
|
} else {
|
||||||
|
targetHfov = config.hfov || 100;
|
||||||
|
config.minHfov = config.minHfov != null ? config.minHfov : 50;
|
||||||
|
config.maxHfov = config.maxHfov != null ? config.maxHfov : 120;
|
||||||
|
config.hfov = config.maxHfov;
|
||||||
|
}
|
||||||
config.showZoomCtrl = false;
|
config.showZoomCtrl = false;
|
||||||
config.compass = false;
|
config.compass = false;
|
||||||
config.showFullscreenCtrl = false;
|
config.showFullscreenCtrl = false;
|
||||||
|
config.draggable = true;
|
||||||
|
config.friction = 0.06;
|
||||||
|
config.touchPanSpeedCoeffFactor = 1.2;
|
||||||
try {
|
try {
|
||||||
currentViewer = pannellum.viewer('panorama', config);
|
currentViewer = pannellum.viewer('panorama', config);
|
||||||
|
var fisheyeDone = false;
|
||||||
|
function runFisheyeEnter() {
|
||||||
|
if (fisheyeDone || !currentViewer) return;
|
||||||
|
fisheyeDone = true;
|
||||||
|
if (currentViewer.getHfov && currentViewer.getHfov() === config.maxHfov) {
|
||||||
|
currentViewer.setHfov(targetHfov, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
currentViewer.on('error', function (err) {
|
currentViewer.on('error', function (err) {
|
||||||
showError('全景图加载失败:' + (err && err.message ? err.message : '未知错误'));
|
showError('全景图加载失败:' + (err && err.message ? err.message : '未知错误'));
|
||||||
});
|
});
|
||||||
|
currentViewer.on('load', runFisheyeEnter);
|
||||||
|
setTimeout(runFisheyeEnter, 500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError('创建查看器失败:' + (e.message || e));
|
showError('创建查看器失败:' + (e.message || e));
|
||||||
}
|
}
|
||||||
@@ -71,16 +103,21 @@
|
|||||||
|
|
||||||
function updateStatsUI(stats) {
|
function updateStatsUI(stats) {
|
||||||
if (!stats) return;
|
if (!stats) return;
|
||||||
|
var keys = ['viewCount', 'likeCount', 'shareCount', 'commentCount', 'watchingNow'];
|
||||||
|
keys.forEach(function (k) {
|
||||||
|
if (stats[k] !== undefined) lastStats[k] = stats[k];
|
||||||
|
});
|
||||||
|
var cur = lastStats;
|
||||||
var w = document.getElementById('watchingNow');
|
var w = document.getElementById('watchingNow');
|
||||||
if (w) w.textContent = (stats.watchingNow || 0) + ' 人在看';
|
if (w) w.textContent = (cur.watchingNow || 0) + ' 人在看';
|
||||||
var v = document.getElementById('viewCount');
|
var v = document.getElementById('viewCount');
|
||||||
if (v) v.textContent = '共 ' + formatNum(stats.viewCount || 0) + ' 次播放';
|
if (v) v.textContent = '共 ' + formatNum(cur.viewCount || 0) + ' 次播放';
|
||||||
var l = document.getElementById('likeCount');
|
var l = document.getElementById('likeCount');
|
||||||
if (l) l.textContent = formatNum(stats.likeCount || 0);
|
if (l) l.textContent = formatNum(cur.likeCount || 0);
|
||||||
var s = document.getElementById('shareCount');
|
var shareEl = document.getElementById('shareCount');
|
||||||
if (s) s.textContent = formatNum(stats.shareCount || 0);
|
if (shareEl) shareEl.textContent = formatNum(cur.shareCount || 0);
|
||||||
var c = document.getElementById('commentCount');
|
var c = document.getElementById('commentCount');
|
||||||
if (c) c.textContent = formatNum(stats.commentCount || 0);
|
if (c) c.textContent = formatNum(cur.commentCount || 0);
|
||||||
}
|
}
|
||||||
function formatNum(n) {
|
function formatNum(n) {
|
||||||
n = Number(n);
|
n = Number(n);
|
||||||
|
|||||||
Reference in New Issue
Block a user