修复网址数据服务器持久化存储
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 忽略不需要的文件
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
README.md
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 使用 Node.js 作为基础镜像
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制 package.json 并安装依赖
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 复制服务器文件
|
||||||
|
COPY server.js .
|
||||||
|
|
||||||
|
# 创建 public 目录并复制静态文件
|
||||||
|
RUN mkdir -p public
|
||||||
|
COPY index.html public/
|
||||||
|
COPY script.js public/
|
||||||
|
COPY style.css public/
|
||||||
|
|
||||||
|
# 创建数据目录
|
||||||
|
RUN mkdir -p data
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# 启动服务器
|
||||||
|
CMD ["node", "server.js"]
|
||||||
71
README.md
Normal file
71
README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Android开发网址管理器
|
||||||
|
|
||||||
|
一个简洁美观的网址管理工具,专为Android开发者设计。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📚 网址分类管理
|
||||||
|
- 🔍 搜索和过滤
|
||||||
|
- ⭐ 收藏功能
|
||||||
|
- 🏷️ 标签系统
|
||||||
|
- 📊 统计信息
|
||||||
|
- 🌓 深色/浅色主题切换
|
||||||
|
- 💾 数据导入/导出
|
||||||
|
|
||||||
|
## Docker部署
|
||||||
|
|
||||||
|
### 方式一:使用docker-compose(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建并启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
服务将在 `http://localhost:3006` 启动
|
||||||
|
|
||||||
|
### 方式二:使用Docker命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t workdizhi:latest .
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
docker run -d -p 3006:80 --name workdizhi-web workdizhi:latest
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker logs -f workdizhi-web
|
||||||
|
|
||||||
|
# 停止容器
|
||||||
|
docker stop workdizhi-web
|
||||||
|
|
||||||
|
# 删除容器
|
||||||
|
docker rm workdizhi-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改端口
|
||||||
|
|
||||||
|
如果需要使用其他端口(例如80端口),可以修改 `docker-compose.yml` 中的端口映射:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "80:80" # 改为80端口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 纯前端实现(HTML + CSS + JavaScript)
|
||||||
|
- 使用localStorage进行数据持久化
|
||||||
|
- 响应式设计,支持移动端
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 数据存储在浏览器的localStorage中,清除浏览器数据会丢失
|
||||||
|
- 建议定期使用导出功能备份数据
|
||||||
|
- 如需多用户或云端同步,需要后端支持
|
||||||
|
|
||||||
184
TROUBLESHOOTING.md
Normal file
184
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# 故障排查指南
|
||||||
|
|
||||||
|
## 连接超时问题排查
|
||||||
|
|
||||||
|
如果访问 `http://101.43.95.130:3006` 时出现连接超时(ERR_CONNECTION_TIMED_OUT),请按以下步骤排查:
|
||||||
|
|
||||||
|
### 1. 检查容器是否运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看容器状态
|
||||||
|
docker ps -a | grep workdizhi-web
|
||||||
|
|
||||||
|
# 或使用docker-compose
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**如果容器没有运行:**
|
||||||
|
```bash
|
||||||
|
# 启动容器
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 或使用docker命令
|
||||||
|
docker start workdizhi-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查容器日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看容器日志,检查是否有错误
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 或
|
||||||
|
docker logs -f workdizhi-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查端口监听状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查3006端口是否在监听
|
||||||
|
netstat -tlnp | grep 3006
|
||||||
|
# 或
|
||||||
|
ss -tlnp | grep 3006
|
||||||
|
# 或
|
||||||
|
lsof -i :3006
|
||||||
|
```
|
||||||
|
|
||||||
|
**如果端口没有监听,检查:**
|
||||||
|
- 容器是否正常启动
|
||||||
|
- docker-compose.yml 中的端口映射是否正确
|
||||||
|
|
||||||
|
### 4. 检查防火墙配置
|
||||||
|
|
||||||
|
#### CentOS/RHEL 7+ (firewalld)
|
||||||
|
```bash
|
||||||
|
# 查看防火墙状态
|
||||||
|
systemctl status firewalld
|
||||||
|
|
||||||
|
# 如果防火墙开启,需要开放3006端口
|
||||||
|
firewall-cmd --permanent --add-port=3006/tcp
|
||||||
|
firewall-cmd --reload
|
||||||
|
|
||||||
|
# 验证端口是否开放
|
||||||
|
firewall-cmd --list-ports
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ubuntu/Debian (ufw)
|
||||||
|
```bash
|
||||||
|
# 查看防火墙状态
|
||||||
|
ufw status
|
||||||
|
|
||||||
|
# 开放3006端口
|
||||||
|
ufw allow 3006/tcp
|
||||||
|
|
||||||
|
# 重新加载
|
||||||
|
ufw reload
|
||||||
|
```
|
||||||
|
|
||||||
|
#### iptables
|
||||||
|
```bash
|
||||||
|
# 开放3006端口
|
||||||
|
iptables -A INPUT -p tcp --dport 3006 -j ACCEPT
|
||||||
|
iptables-save
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 检查云服务器安全组(重要!)
|
||||||
|
|
||||||
|
如果使用的是云服务器(阿里云、腾讯云、AWS等),需要在控制台配置安全组规则:
|
||||||
|
|
||||||
|
1. 登录云服务器控制台
|
||||||
|
2. 找到对应的服务器实例
|
||||||
|
3. 进入"安全组"配置
|
||||||
|
4. 添加入站规则:
|
||||||
|
- 协议:TCP
|
||||||
|
- 端口:3006
|
||||||
|
- 源:0.0.0.0/0(允许所有IP访问,或指定特定IP)
|
||||||
|
- 动作:允许
|
||||||
|
|
||||||
|
### 6. 本地测试
|
||||||
|
|
||||||
|
在服务器上测试本地访问:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试本地3006端口
|
||||||
|
curl http://localhost:3006
|
||||||
|
|
||||||
|
# 或
|
||||||
|
curl http://127.0.0.1:3006
|
||||||
|
|
||||||
|
# 如果本地可以访问,说明容器正常,问题在防火墙或安全组
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 检查Docker服务状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查Docker服务是否运行
|
||||||
|
systemctl status docker
|
||||||
|
|
||||||
|
# 如果未运行,启动Docker
|
||||||
|
systemctl start docker
|
||||||
|
systemctl enable docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 重新构建和启动
|
||||||
|
|
||||||
|
如果以上都正常,尝试重新构建和启动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止并删除旧容器
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 重新构建镜像
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 验证部署
|
||||||
|
|
||||||
|
部署成功后,应该能看到:
|
||||||
|
- 容器状态为 `Up`
|
||||||
|
- 端口映射正确:`0.0.0.0:3006->80/tcp`
|
||||||
|
- 日志无错误信息
|
||||||
|
|
||||||
|
## 快速检查脚本
|
||||||
|
|
||||||
|
可以运行以下命令快速检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "=== 检查容器状态 ==="
|
||||||
|
docker ps -a | grep workdizhi-web
|
||||||
|
|
||||||
|
echo -e "\n=== 检查端口监听 ==="
|
||||||
|
netstat -tlnp | grep 3006 || ss -tlnp | grep 3006
|
||||||
|
|
||||||
|
echo -e "\n=== 检查防火墙 ==="
|
||||||
|
if command -v firewall-cmd &> /dev/null; then
|
||||||
|
firewall-cmd --list-ports | grep 3006
|
||||||
|
elif command -v ufw &> /dev/null; then
|
||||||
|
ufw status | grep 3006
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n=== 测试本地访问 ==="
|
||||||
|
curl -I http://localhost:3006 2>&1 | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 容器启动后立即退出
|
||||||
|
**A:** 检查日志:`docker logs workdizhi-web`,可能是镜像构建失败或文件缺失
|
||||||
|
|
||||||
|
### Q: 本地可以访问,外网无法访问
|
||||||
|
**A:** 检查防火墙和安全组配置,确保3006端口已开放
|
||||||
|
|
||||||
|
### Q: 端口被占用
|
||||||
|
**A:** 检查是否有其他服务占用3006端口,或修改docker-compose.yml使用其他端口
|
||||||
|
|
||||||
|
### Q: 容器运行但无法访问
|
||||||
|
**A:** 检查Nginx配置,确保容器内Nginx正常启动
|
||||||
|
|
||||||
287
data/urls.json
Normal file
287
data/urls.json
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"title": "Android开发者文档",
|
||||||
|
"url": "https://developer.android.com",
|
||||||
|
"description": "官方Android开发文档和API参考",
|
||||||
|
"category": "dev-docs",
|
||||||
|
"icon": "fas fa-book",
|
||||||
|
"tags": [
|
||||||
|
"文档",
|
||||||
|
"官方",
|
||||||
|
"Android"
|
||||||
|
],
|
||||||
|
"favorite": true,
|
||||||
|
"createdAt": "2025-12-20T11:23:23.411Z",
|
||||||
|
"lastAccessed": "2025-12-20T11:34:32.393Z",
|
||||||
|
"accessCount": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"title": "GitHub",
|
||||||
|
"url": "https://github.com",
|
||||||
|
"description": "代码托管和版本控制平台",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-code",
|
||||||
|
"tags": [
|
||||||
|
"工具",
|
||||||
|
"代码",
|
||||||
|
"版本控制"
|
||||||
|
],
|
||||||
|
"favorite": true,
|
||||||
|
"createdAt": "2025-12-20T11:23:23.411Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:19:54.525Z",
|
||||||
|
"accessCount": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"title": "Stack Overflow",
|
||||||
|
"url": "https://stackoverflow.com",
|
||||||
|
"description": "开发者问答社区",
|
||||||
|
"category": "community",
|
||||||
|
"icon": "fas fa-comments",
|
||||||
|
"tags": [
|
||||||
|
"社区",
|
||||||
|
"问答",
|
||||||
|
"帮助"
|
||||||
|
],
|
||||||
|
"favorite": false,
|
||||||
|
"createdAt": "2025-12-20T11:23:23.411Z",
|
||||||
|
"lastAccessed": "2025-12-20T11:23:31.944Z",
|
||||||
|
"accessCount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"title": "Material Design",
|
||||||
|
"url": "https://material.io",
|
||||||
|
"description": "Google的设计系统",
|
||||||
|
"category": "design",
|
||||||
|
"icon": "fas fa-palette",
|
||||||
|
"tags": [
|
||||||
|
"设计",
|
||||||
|
"UI",
|
||||||
|
"Material"
|
||||||
|
],
|
||||||
|
"favorite": false,
|
||||||
|
"createdAt": "2025-12-20T11:23:23.411Z",
|
||||||
|
"lastAccessed": "2025-12-20T12:31:00.584Z",
|
||||||
|
"accessCount": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766232957678",
|
||||||
|
"createdAt": "2025-12-20T12:15:57.678Z",
|
||||||
|
"lastAccessed": "2025-12-21T15:13:28.465Z",
|
||||||
|
"accessCount": 6,
|
||||||
|
"title": "gitea",
|
||||||
|
"url": "http://101.43.95.130:3001/",
|
||||||
|
"description": "公司内部代码托管和版本管理平台",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [
|
||||||
|
"代码版本管理"
|
||||||
|
],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766233366850",
|
||||||
|
"createdAt": "2025-12-20T12:22:46.850Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:55:51.738Z",
|
||||||
|
"accessCount": 3,
|
||||||
|
"title": "gerrit(3.13.1)",
|
||||||
|
"url": "http://101.43.95.130:8082/",
|
||||||
|
"description": "gerrit版本,使用docker容器部署的,一款代码review平台。",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [
|
||||||
|
"代码审核"
|
||||||
|
],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766233826313",
|
||||||
|
"createdAt": "2025-12-20T12:30:26.313Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:58:25.837Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "Drone",
|
||||||
|
"url": "http://101.43.95.130:3002/",
|
||||||
|
"description": "流水线CI工具,不仅能构建APK,还能做:自动备份\n自动部署后端服务\n自动生成文档\n自动通知飞书/钉钉\n自动执行脚本",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766233993733",
|
||||||
|
"createdAt": "2025-12-20T12:33:13.733Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:58:46.376Z",
|
||||||
|
"accessCount": 2,
|
||||||
|
"title": "AI提示词优化",
|
||||||
|
"url": "http://101.43.95.130:5002/",
|
||||||
|
"description": "优化提示词",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766234237652",
|
||||||
|
"createdAt": "2025-12-20T12:37:17.652Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:55:27.836Z",
|
||||||
|
"accessCount": 2,
|
||||||
|
"title": "gerrit(3.9.0)",
|
||||||
|
"url": "http://101.43.95.130:8080/",
|
||||||
|
"description": "原生方式部署的gerrit",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [
|
||||||
|
"代码审核工具"
|
||||||
|
],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766234344830",
|
||||||
|
"createdAt": "2025-12-20T12:39:04.830Z",
|
||||||
|
"lastAccessed": null,
|
||||||
|
"accessCount": 0,
|
||||||
|
"title": "蒲公英",
|
||||||
|
"url": "https://www.pgyer.com/",
|
||||||
|
"description": "",
|
||||||
|
"category": "tools",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235165350",
|
||||||
|
"createdAt": "2025-12-20T12:52:45.350Z",
|
||||||
|
"lastAccessed": "2025-12-20T12:52:50.097Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "知你后台网址",
|
||||||
|
"url": "https://xingyao.xunpaisoft.com/LMPTECaZKX.php/dashboard?ref=addtabs",
|
||||||
|
"description": "知你后台管理系统",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235263277",
|
||||||
|
"createdAt": "2025-12-20T12:54:23.277Z",
|
||||||
|
"lastAccessed": "2025-12-20T12:54:29.783Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "友盟统计(知你)",
|
||||||
|
"url": "https://mobile.umeng.com/platform/apps/list",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235418609",
|
||||||
|
"createdAt": "2025-12-20T12:56:58.609Z",
|
||||||
|
"lastAccessed": "2025-12-20T13:52:05.611Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "腾讯云",
|
||||||
|
"url": "https://console.cloud.tencent.com/",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235454814",
|
||||||
|
"createdAt": "2025-12-20T12:57:34.814Z",
|
||||||
|
"lastAccessed": "2025-12-21T14:58:36.671Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "智能饭菜推荐系统",
|
||||||
|
"url": "http://101.43.95.130:5003/",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235507121",
|
||||||
|
"createdAt": "2025-12-20T12:58:27.121Z",
|
||||||
|
"lastAccessed": "2025-12-20T14:23:52.126Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "使用 PromptForge 构建更智能的AI应用",
|
||||||
|
"url": "http://101.43.95.130:3000/",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235612635",
|
||||||
|
"createdAt": "2025-12-20T13:00:12.635Z",
|
||||||
|
"lastAccessed": "2025-12-20T13:00:17.586Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "知你蓝胡",
|
||||||
|
"url": "https://lanhuapp.com/web/#/item/project/stage?pid=8562bbb3-7986-4478-ad9d-725d1d22a2f4&image_id=ca8a08af-16b7-48f7-8ec2-5334a2ab70c6&tid=0be10d44-29d1-429f-9a42-53814cda6ad7",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766235853976",
|
||||||
|
"createdAt": "2025-12-20T13:04:13.976Z",
|
||||||
|
"lastAccessed": null,
|
||||||
|
"accessCount": 0,
|
||||||
|
"title": "腾讯bugly",
|
||||||
|
"url": "https://bugly.tds.qq.com/v2/index/main?from=buglyqq",
|
||||||
|
"description": "",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1766244183937",
|
||||||
|
"createdAt": "2025-12-20T15:23:03.937Z",
|
||||||
|
"lastAccessed": "2025-12-20T15:23:07.021Z",
|
||||||
|
"accessCount": 1,
|
||||||
|
"title": "码云",
|
||||||
|
"url": "https://gitee.com/",
|
||||||
|
"description": "代码托管",
|
||||||
|
"category": "",
|
||||||
|
"icon": "fas fa-globe",
|
||||||
|
"tags": [],
|
||||||
|
"favorite": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "dev-docs",
|
||||||
|
"name": "开发文档",
|
||||||
|
"color": "#3498db",
|
||||||
|
"icon": "fas fa-book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tools",
|
||||||
|
"name": "工具",
|
||||||
|
"color": "#2ecc71",
|
||||||
|
"icon": "fas fa-tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "community",
|
||||||
|
"name": "社区",
|
||||||
|
"color": "#9b59b6",
|
||||||
|
"icon": "fas fa-comments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "design",
|
||||||
|
"name": "设计",
|
||||||
|
"color": "#e74c3c",
|
||||||
|
"icon": "fas fa-palette"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastUpdated": "2025-12-22T14:53:21.453Z"
|
||||||
|
}
|
||||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: workdizhi-web
|
||||||
|
ports:
|
||||||
|
- "3006:3000" # 将容器的3000端口映射到主机的3006端口
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# 挂载数据目录,实现数据持久化
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- PORT=3000
|
||||||
|
networks:
|
||||||
|
- workdizhi-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
workdizhi-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
21
docker-compose.yml.fix
Normal file
21
docker-compose.yml.fix
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: workdizhi-web
|
||||||
|
ports:
|
||||||
|
- "3006:80" # 将容器的80端口映射到主机的3006端口
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# 挂载数据目录,用于持久化存储
|
||||||
|
- ./data:/usr/share/nginx/html/data:ro
|
||||||
|
# 如果需要,也可以挂载整个 html 目录以便更新
|
||||||
|
# - ./:/usr/share/nginx/html:ro
|
||||||
|
networks:
|
||||||
|
- workdizhi-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
workdizhi-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "workdizhi",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Android开发网址管理器 - 带服务器端数据存储",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"body-parser": "^1.20.2"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"url-manager",
|
||||||
|
"android-dev"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
|
||||||
117
script.js
117
script.js
@@ -9,20 +9,35 @@ class UrlManager {
|
|||||||
this.currentTag = '';
|
this.currentTag = '';
|
||||||
this.viewMode = 'grid';
|
this.viewMode = 'grid';
|
||||||
this.editingUrlId = null;
|
this.editingUrlId = null;
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
this.loadData();
|
await this.loadData();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.render();
|
this.render();
|
||||||
this.setupKeyboardShortcuts();
|
this.setupKeyboardShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据存储
|
// 数据存储
|
||||||
loadData() {
|
async loadData() {
|
||||||
// 加载默认数据或从localStorage加载
|
try {
|
||||||
|
// 优先从服务器加载数据
|
||||||
|
const response = await fetch('/api/urls');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
this.urls = data.urls && data.urls.length > 0 ? data.urls : this.getDefaultUrls();
|
||||||
|
this.categories = data.categories && data.categories.length > 0 ? data.categories : this.getDefaultCategories();
|
||||||
|
|
||||||
|
// 同时保存到 localStorage 作为备份
|
||||||
|
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||||||
|
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('从服务器加载数据失败,尝试从 localStorage 加载:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降级到 localStorage
|
||||||
const savedUrls = localStorage.getItem('urlManager_urls');
|
const savedUrls = localStorage.getItem('urlManager_urls');
|
||||||
const savedCategories = localStorage.getItem('urlManager_categories');
|
const savedCategories = localStorage.getItem('urlManager_categories');
|
||||||
|
|
||||||
@@ -40,15 +55,49 @@ class UrlManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData() {
|
async saveData() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
const dataToSave = {
|
||||||
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
urls: this.urls || [],
|
||||||
|
categories: this.categories || []
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('保存数据:', {
|
||||||
|
urlsCount: dataToSave.urls.length,
|
||||||
|
categoriesCount: dataToSave.categories.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// 优先保存到服务器
|
||||||
|
const response = await fetch('/api/urls', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(dataToSave)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('服务器保存成功:', result);
|
||||||
|
|
||||||
|
// 服务器保存成功,同时保存到 localStorage 作为备份
|
||||||
|
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||||||
|
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('服务器返回错误:', response.status, errorText);
|
||||||
|
throw new Error(`服务器返回错误: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存数据到localStorage失败:', error);
|
console.error('保存到服务器失败:', error);
|
||||||
// 如果localStorage已满或其他错误,只记录错误,不抛出异常
|
// 降级到 localStorage
|
||||||
// 因为数据已经在内存中了,只是无法持久化
|
try {
|
||||||
// 抛出异常会导致误报"保存失败"
|
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||||||
|
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||||||
|
console.warn('已降级保存到 localStorage');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存数据到localStorage也失败:', e);
|
||||||
|
}
|
||||||
|
throw error; // 重新抛出错误,让调用者知道保存失败
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,20 +858,47 @@ class UrlManager {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.target.result);
|
const data = JSON.parse(e.target.result);
|
||||||
|
console.log('导入数据:', data);
|
||||||
|
|
||||||
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
|
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
|
||||||
if (data.urls) this.urls = data.urls;
|
// 更新数据
|
||||||
if (data.categories) this.categories = data.categories;
|
if (data.urls) {
|
||||||
this.saveData();
|
this.urls = data.urls;
|
||||||
|
console.log('导入网址数量:', this.urls.length);
|
||||||
|
} else {
|
||||||
|
this.urls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.categories) {
|
||||||
|
this.categories = data.categories;
|
||||||
|
console.log('导入分类数量:', this.categories.length);
|
||||||
|
} else {
|
||||||
|
this.categories = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待数据保存到服务器
|
||||||
|
console.log('开始保存数据到服务器...');
|
||||||
|
await this.saveData();
|
||||||
|
console.log('数据保存完成');
|
||||||
|
|
||||||
|
// 验证保存是否成功
|
||||||
|
const verifyResponse = await fetch('/api/urls');
|
||||||
|
if (verifyResponse.ok) {
|
||||||
|
const savedData = await verifyResponse.json();
|
||||||
|
console.log('服务器上的数据:', savedData);
|
||||||
|
console.log('服务器网址数量:', savedData.urls ? savedData.urls.length : 0);
|
||||||
|
}
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.closeAllModals();
|
this.closeAllModals();
|
||||||
alert('数据导入成功!');
|
alert(`数据导入成功!已导入 ${this.urls.length} 个网址,${this.categories.length} 个分类,并已保存到服务器!`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('文件格式错误,请选择有效的JSON文件');
|
console.error('导入数据失败:', error);
|
||||||
|
alert('文件格式错误或保存失败: ' + error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -1019,6 +1095,7 @@ class UrlManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
window.urlManager = new UrlManager();
|
window.urlManager = new UrlManager();
|
||||||
|
await window.urlManager.init();
|
||||||
});
|
});
|
||||||
|
|||||||
86
server.js
Normal file
86
server.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const cors = require('cors');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
|
const DATA_FILE = path.join(DATA_DIR, 'urls.json');
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
// 确保数据目录存在
|
||||||
|
async function ensureDataDir() {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建数据目录失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
app.get('/api/urls', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureDataDir();
|
||||||
|
const data = await fs.readFile(DATA_FILE, 'utf8');
|
||||||
|
res.json(JSON.parse(data));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
// 文件不存在,返回空数据
|
||||||
|
res.json({ urls: [], categories: [] });
|
||||||
|
} else {
|
||||||
|
console.error('读取数据失败:', error);
|
||||||
|
res.status(500).json({ error: '读取数据失败', message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存数据
|
||||||
|
app.post('/api/urls', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureDataDir();
|
||||||
|
const data = {
|
||||||
|
urls: req.body.urls || [],
|
||||||
|
categories: req.body.categories || [],
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('收到保存请求:', {
|
||||||
|
urlsCount: data.urls.length,
|
||||||
|
categoriesCount: data.categories.length,
|
||||||
|
timestamp: data.lastUpdated
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
|
||||||
|
// 验证文件是否写入成功
|
||||||
|
const verifyData = await fs.readFile(DATA_FILE, 'utf8');
|
||||||
|
const verifyJson = JSON.parse(verifyData);
|
||||||
|
console.log('数据已保存,验证:', {
|
||||||
|
urlsCount: verifyJson.urls.length,
|
||||||
|
categoriesCount: verifyJson.categories.length
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: '数据保存成功', savedCount: { urls: data.urls.length, categories: data.categories.length } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存数据失败:', error);
|
||||||
|
res.status(500).json({ error: '保存数据失败', message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Workdizhi 服务器运行在端口 ${PORT}`);
|
||||||
|
console.log(`数据文件位置: ${DATA_FILE}`);
|
||||||
|
});
|
||||||
|
|
||||||
297
workdizhi数据持久化修复方案.md
Normal file
297
workdizhi数据持久化修复方案.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Workdizhi 数据持久化修复方案
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
**当前问题**:
|
||||||
|
- 数据存储在浏览器的 `localStorage` 中
|
||||||
|
- `docker-compose.yml` 中没有数据卷挂载
|
||||||
|
- 在新电脑打开时,浏览器的 localStorage 是空的,导致数据丢失
|
||||||
|
|
||||||
|
**根本原因**:
|
||||||
|
- `localStorage` 是浏览器本地存储,不是服务器端存储
|
||||||
|
- 容器中没有数据持久化配置
|
||||||
|
- 数据没有保存到服务器文件系统
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案 1: 添加后端 API + 文件存储(推荐)
|
||||||
|
|
||||||
|
这是最彻底的解决方案,需要修改代码添加后端 API。
|
||||||
|
|
||||||
|
#### 步骤 1: 创建简单的 Node.js 后端
|
||||||
|
|
||||||
|
创建 `server.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const DATA_FILE = path.join(__dirname, 'data', 'urls.json');
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
// 确保数据目录存在
|
||||||
|
async function ensureDataDir() {
|
||||||
|
const dataDir = path.dirname(DATA_FILE);
|
||||||
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
app.get('/api/urls', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureDataDir();
|
||||||
|
const data = await fs.readFile(DATA_FILE, 'utf8');
|
||||||
|
res.json(JSON.parse(data));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
res.json({ urls: [], categories: [] });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存数据
|
||||||
|
app.post('/api/urls', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureDataDir();
|
||||||
|
await fs.writeFile(DATA_FILE, JSON.stringify(req.body, null, 2));
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2: 修改前端代码
|
||||||
|
|
||||||
|
修改 `script.js` 中的数据加载和保存方法:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 修改 loadData 方法
|
||||||
|
async loadData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/urls');
|
||||||
|
const data = await response.json();
|
||||||
|
this.urls = data.urls || this.getDefaultUrls();
|
||||||
|
this.categories = data.categories || this.getDefaultCategories();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从服务器加载数据失败,使用默认数据:', error);
|
||||||
|
// 降级到 localStorage
|
||||||
|
const savedUrls = localStorage.getItem('urlManager_urls');
|
||||||
|
const savedCategories = localStorage.getItem('urlManager_categories');
|
||||||
|
if (savedUrls) {
|
||||||
|
this.urls = JSON.parse(savedUrls);
|
||||||
|
} else {
|
||||||
|
this.urls = this.getDefaultUrls();
|
||||||
|
}
|
||||||
|
if (savedCategories) {
|
||||||
|
this.categories = JSON.parse(savedCategories);
|
||||||
|
} else {
|
||||||
|
this.categories = this.getDefaultCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改 saveData 方法
|
||||||
|
async saveData() {
|
||||||
|
try {
|
||||||
|
// 先保存到服务器
|
||||||
|
const response = await fetch('/api/urls', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
urls: this.urls,
|
||||||
|
categories: this.categories
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('服务器保存失败');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存到服务器失败,降级到 localStorage:', error);
|
||||||
|
// 降级到 localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||||||
|
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存到 localStorage 也失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3: 更新 Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 复制文件
|
||||||
|
COPY server.js .
|
||||||
|
COPY index.html public/
|
||||||
|
COPY script.js public/
|
||||||
|
COPY style.css public/
|
||||||
|
|
||||||
|
# 创建数据目录
|
||||||
|
RUN mkdir -p data
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4: 更新 docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: workdizhi-web
|
||||||
|
ports:
|
||||||
|
- "3006:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# 挂载数据目录,持久化存储
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- workdizhi-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
workdizhi-network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 2: 使用 Nginx + 文件存储(简单方案)
|
||||||
|
|
||||||
|
如果不想添加后端,可以使用 Nginx 的静态文件服务 + JavaScript 文件操作。
|
||||||
|
|
||||||
|
#### 步骤 1: 修改 script.js 使用 fetch API 保存到文件
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 添加文件保存方法
|
||||||
|
async saveToFile() {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
urls: this.urls,
|
||||||
|
categories: this.categories,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 Blob 和下载方式保存(需要用户手动操作)
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'workdizhi_backup.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件加载方法
|
||||||
|
async loadFromFile(file) {
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
this.urls = data.urls || this.getDefaultUrls();
|
||||||
|
this.categories = data.categories || this.getDefaultCategories();
|
||||||
|
this.saveData();
|
||||||
|
this.render();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载文件失败:', error);
|
||||||
|
alert('文件格式错误或损坏');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 3: 导出/导入功能(临时方案)
|
||||||
|
|
||||||
|
添加导出和导入功能,让用户可以手动备份和恢复数据。
|
||||||
|
|
||||||
|
#### 修改 script.js 添加导出/导入功能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 导出数据
|
||||||
|
exportData() {
|
||||||
|
const data = {
|
||||||
|
urls: this.urls,
|
||||||
|
categories: this.categories,
|
||||||
|
exportDate: new Date().toISOString()
|
||||||
|
};
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `workdizhi_backup_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入数据
|
||||||
|
importData(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
this.urls = data.urls || [];
|
||||||
|
this.categories = data.categories || [];
|
||||||
|
this.saveData();
|
||||||
|
this.render();
|
||||||
|
alert('数据导入成功!');
|
||||||
|
} catch (error) {
|
||||||
|
alert('文件格式错误,导入失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐实施步骤
|
||||||
|
|
||||||
|
### 快速修复(方案 3 - 导出/导入)
|
||||||
|
|
||||||
|
1. 添加导出/导入按钮到界面
|
||||||
|
2. 用户可以定期导出数据备份
|
||||||
|
3. 在新电脑上导入备份文件
|
||||||
|
|
||||||
|
### 长期方案(方案 1 - 后端 API)
|
||||||
|
|
||||||
|
1. 创建 Node.js 后端服务
|
||||||
|
2. 修改前端代码使用 API
|
||||||
|
3. 添加数据卷挂载
|
||||||
|
4. 重新构建和部署
|
||||||
|
|
||||||
|
## 当前数据位置
|
||||||
|
|
||||||
|
- **浏览器 localStorage**: `urlManager_urls`, `urlManager_categories`
|
||||||
|
- **服务器**: 无持久化存储
|
||||||
|
|
||||||
|
## 修复后的数据位置
|
||||||
|
|
||||||
|
- **服务器文件**: `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
|
||||||
|
- **数据卷**: 挂载到容器内的 `/app/data` 或 `/usr/share/nginx/html/data`
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **备份现有数据**: 在修改前,先导出当前 localStorage 中的数据
|
||||||
|
2. **测试**: 修改后充分测试数据保存和加载功能
|
||||||
|
3. **迁移**: 如果已有数据,需要提供迁移脚本
|
||||||
|
|
||||||
106
导入功能说明.md
Normal file
106
导入功能说明.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 导入功能持久化说明
|
||||||
|
|
||||||
|
## ✅ 导入数据会持久化
|
||||||
|
|
||||||
|
**是的,导入数据后会持久化到服务器!**
|
||||||
|
|
||||||
|
## 🔄 导入流程
|
||||||
|
|
||||||
|
1. **选择文件**: 用户选择 JSON 文件
|
||||||
|
2. **解析数据**: 读取并解析 JSON 文件
|
||||||
|
3. **确认导入**: 用户确认覆盖当前数据
|
||||||
|
4. **更新内存**: 将导入的数据加载到内存
|
||||||
|
5. **保存到服务器**: 调用 `saveData()` 方法,数据会:
|
||||||
|
- ✅ 优先保存到服务器 (`/api/urls` POST 请求)
|
||||||
|
- ✅ 保存到服务器文件 (`/app/data/urls.json`)
|
||||||
|
- ✅ 同时保存到 localStorage 作为备份
|
||||||
|
6. **渲染界面**: 更新页面显示
|
||||||
|
7. **提示成功**: 显示"数据导入成功并已保存到服务器!"
|
||||||
|
|
||||||
|
## 📋 代码实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleFileImport(file) {
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
|
||||||
|
// 更新数据
|
||||||
|
if (data.urls) this.urls = data.urls;
|
||||||
|
if (data.categories) this.categories = data.categories;
|
||||||
|
|
||||||
|
// 等待保存到服务器(关键步骤)
|
||||||
|
await this.saveData();
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
alert('数据导入成功并已保存到服务器!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 验证方法
|
||||||
|
|
||||||
|
### 方法 1: 检查服务器文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 导入数据后,检查服务器文件
|
||||||
|
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2: 检查 API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 导入数据后,通过 API 获取数据
|
||||||
|
curl http://localhost:3006/api/urls
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 3: 重启容器测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 导入数据后,重启容器
|
||||||
|
docker restart workdizhi-web
|
||||||
|
|
||||||
|
# 访问网站,数据应该还在
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 数据流向
|
||||||
|
|
||||||
|
```
|
||||||
|
导入 JSON 文件
|
||||||
|
↓
|
||||||
|
解析数据
|
||||||
|
↓
|
||||||
|
更新内存 (this.urls, this.categories)
|
||||||
|
↓
|
||||||
|
调用 saveData()
|
||||||
|
↓
|
||||||
|
POST /api/urls (保存到服务器)
|
||||||
|
↓
|
||||||
|
写入 /app/data/urls.json (持久化)
|
||||||
|
↓
|
||||||
|
同时保存到 localStorage (备份)
|
||||||
|
↓
|
||||||
|
✅ 数据已持久化
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **异步操作**: `saveData()` 是异步方法,导入时会等待保存完成
|
||||||
|
2. **数据覆盖**: 导入会覆盖当前所有数据,请先备份
|
||||||
|
3. **文件格式**: 必须是有效的 JSON 格式
|
||||||
|
4. **网络问题**: 如果服务器保存失败,会降级到 localStorage
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
- ✅ **导入数据会持久化**: 数据会保存到服务器文件
|
||||||
|
- ✅ **自动保存**: 导入后自动调用 `saveData()`
|
||||||
|
- ✅ **多级备份**: 服务器文件 + localStorage
|
||||||
|
- ✅ **跨设备同步**: 所有设备访问同一服务器,数据同步
|
||||||
|
|
||||||
|
## 📝 使用建议
|
||||||
|
|
||||||
|
1. **定期导出**: 建议定期导出数据作为备份
|
||||||
|
2. **导入前备份**: 导入前先导出当前数据
|
||||||
|
3. **验证导入**: 导入后检查数据是否正确
|
||||||
|
4. **重启测试**: 重启容器验证数据是否持久化
|
||||||
|
|
||||||
201
导入问题排查指南.md
Normal file
201
导入问题排查指南.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# 导入数据问题排查指南
|
||||||
|
|
||||||
|
## 🔍 问题现象
|
||||||
|
|
||||||
|
导入数据后,在另一台电脑只能看到一条测试网址,说明导入的数据没有正确保存到服务器。
|
||||||
|
|
||||||
|
## 🔧 已实施的修复
|
||||||
|
|
||||||
|
### 1. 增强错误处理
|
||||||
|
- 添加了详细的控制台日志
|
||||||
|
- 保存失败时会显示具体错误信息
|
||||||
|
- 导入成功后会显示导入的数据数量
|
||||||
|
|
||||||
|
### 2. 添加数据验证
|
||||||
|
- 导入后会自动验证服务器上的数据
|
||||||
|
- 显示导入的网址和分类数量
|
||||||
|
- 保存失败时会抛出错误
|
||||||
|
|
||||||
|
### 3. 增强服务器日志
|
||||||
|
- 服务器会记录每次保存请求的详细信息
|
||||||
|
- 记录保存的网址和分类数量
|
||||||
|
- 保存后会验证文件内容
|
||||||
|
|
||||||
|
## 📋 排查步骤
|
||||||
|
|
||||||
|
### 步骤 1: 检查浏览器控制台
|
||||||
|
|
||||||
|
1. 打开网站:http://101.43.95.130:3006
|
||||||
|
2. 按 F12 打开开发者工具
|
||||||
|
3. 切换到 Console 标签
|
||||||
|
4. 执行导入操作
|
||||||
|
5. 查看控制台输出,应该看到:
|
||||||
|
- `导入数据: {...}`
|
||||||
|
- `导入网址数量: X`
|
||||||
|
- `开始保存数据到服务器...`
|
||||||
|
- `服务器保存成功: {...}`
|
||||||
|
- `服务器上的数据: {...}`
|
||||||
|
|
||||||
|
### 步骤 2: 检查网络请求
|
||||||
|
|
||||||
|
1. 在开发者工具中切换到 Network 标签
|
||||||
|
2. 执行导入操作
|
||||||
|
3. 查找 POST 请求到 `/api/urls`
|
||||||
|
4. 检查:
|
||||||
|
- 请求状态码(应该是 200)
|
||||||
|
- 请求体(应该包含所有导入的数据)
|
||||||
|
- 响应内容(应该显示成功)
|
||||||
|
|
||||||
|
### 步骤 3: 检查服务器日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看容器日志
|
||||||
|
docker logs -f workdizhi-web
|
||||||
|
|
||||||
|
# 执行导入操作,应该看到:
|
||||||
|
# - "收到保存请求: { urlsCount: X, categoriesCount: Y }"
|
||||||
|
# - "数据已保存,验证: { urlsCount: X, categoriesCount: Y }"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4: 检查数据文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查服务器上的数据文件
|
||||||
|
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
|
||||||
|
|
||||||
|
# 检查容器内的数据文件
|
||||||
|
docker exec workdizhi-web cat /app/data/urls.json
|
||||||
|
|
||||||
|
# 通过 API 获取数据
|
||||||
|
curl http://localhost:3006/api/urls
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 常见问题
|
||||||
|
|
||||||
|
### 问题 1: 导入的数据格式不正确
|
||||||
|
|
||||||
|
**症状**: 控制台显示 "文件格式错误"
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
- 确保 JSON 文件格式正确
|
||||||
|
- 文件应包含 `urls` 和 `categories` 字段
|
||||||
|
- 示例格式:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
{"id": "1", "title": "网址1", "url": "https://example.com"}
|
||||||
|
],
|
||||||
|
"categories": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 2: 保存请求失败
|
||||||
|
|
||||||
|
**症状**: 控制台显示 "保存到服务器失败"
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 网络连接是否正常
|
||||||
|
2. 服务器是否运行:`docker ps | grep workdizhi`
|
||||||
|
3. API 是否可访问:`curl http://localhost:3006/api/health`
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
# 重启服务
|
||||||
|
cd /home/renjianbo/devops/workdizhi/workdizhi
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 3: 数据保存但读取时为空
|
||||||
|
|
||||||
|
**症状**: 保存成功,但读取时只有默认数据
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 数据文件是否存在
|
||||||
|
2. 文件内容是否正确
|
||||||
|
3. 文件权限是否正确
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
# 检查文件
|
||||||
|
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
|
||||||
|
|
||||||
|
# 修复权限(如果需要)
|
||||||
|
sudo chown -R renjianbo:renjianbo /home/renjianbo/devops/workdizhi/workdizhi/data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 4: 导入后数据被覆盖
|
||||||
|
|
||||||
|
**症状**: 导入成功,但之后添加的数据覆盖了导入的数据
|
||||||
|
|
||||||
|
**原因**: 可能是 localStorage 中的旧数据覆盖了服务器数据
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 清除浏览器 localStorage
|
||||||
|
2. 重新加载页面
|
||||||
|
3. 数据会从服务器重新加载
|
||||||
|
|
||||||
|
## 🔍 调试命令
|
||||||
|
|
||||||
|
### 查看当前数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 服务器文件
|
||||||
|
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
|
||||||
|
|
||||||
|
# API 获取
|
||||||
|
curl http://localhost:3006/api/urls | python -m json.tool
|
||||||
|
|
||||||
|
# 容器内文件
|
||||||
|
docker exec workdizhi-web cat /app/data/urls.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试保存功能
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试保存
|
||||||
|
curl -X POST http://localhost:3006/api/urls \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"urls": [
|
||||||
|
{"id": "1", "title": "测试1", "url": "https://test1.com"},
|
||||||
|
{"id": "2", "title": "测试2", "url": "https://test2.com"}
|
||||||
|
],
|
||||||
|
"categories": []
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 验证保存
|
||||||
|
curl http://localhost:3006/api/urls
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 实时查看日志
|
||||||
|
docker logs -f workdizhi-web
|
||||||
|
|
||||||
|
# 查看最近 50 行
|
||||||
|
docker logs --tail 50 workdizhi-web
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 验证导入成功
|
||||||
|
|
||||||
|
导入数据后,执行以下验证:
|
||||||
|
|
||||||
|
1. **浏览器控制台**: 应该看到 "数据导入成功!已导入 X 个网址..."
|
||||||
|
2. **服务器文件**: `cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` 应该包含所有导入的数据
|
||||||
|
3. **API 获取**: `curl http://localhost:3006/api/urls` 应该返回所有数据
|
||||||
|
4. **重启测试**: 重启容器后,数据应该还在
|
||||||
|
|
||||||
|
## 🎯 下一步
|
||||||
|
|
||||||
|
如果问题仍然存在,请:
|
||||||
|
|
||||||
|
1. 打开浏览器开发者工具(F12)
|
||||||
|
2. 切换到 Console 标签
|
||||||
|
3. 执行导入操作
|
||||||
|
4. 复制所有控制台输出
|
||||||
|
5. 检查 Network 标签中的 POST 请求
|
||||||
|
6. 查看请求和响应的详细信息
|
||||||
|
|
||||||
|
这些信息可以帮助进一步诊断问题。
|
||||||
|
|
||||||
115
数据持久化问题分析.md
Normal file
115
数据持久化问题分析.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Workdizhi 数据持久化问题分析
|
||||||
|
|
||||||
|
## 🔍 问题诊断
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
1. **数据存储方式**: 浏览器的 `localStorage`
|
||||||
|
2. **Docker 配置**: 没有数据卷挂载
|
||||||
|
3. **问题表现**: 在新电脑打开后,添加的网址都消失了
|
||||||
|
|
||||||
|
### 根本原因
|
||||||
|
|
||||||
|
- `localStorage` 是浏览器本地存储,存储在用户的浏览器中
|
||||||
|
- 数据没有保存到服务器文件系统
|
||||||
|
- 容器中没有数据持久化配置
|
||||||
|
- 换电脑或清除浏览器数据时,数据会丢失
|
||||||
|
|
||||||
|
## ✅ 当前已有的功能
|
||||||
|
|
||||||
|
项目已经实现了导出/导入功能:
|
||||||
|
- ✅ 导出按钮:可以将数据导出为 JSON 文件
|
||||||
|
- ✅ 导入按钮:可以从 JSON 文件导入数据
|
||||||
|
- ✅ 快捷键:Ctrl+S 快速导出
|
||||||
|
|
||||||
|
## 🔧 解决方案
|
||||||
|
|
||||||
|
### 方案 1: 使用导出/导入功能(临时方案)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 无需修改代码
|
||||||
|
- 立即可用
|
||||||
|
- 用户可以手动备份
|
||||||
|
|
||||||
|
**使用方法**:
|
||||||
|
1. 定期点击"导出"按钮,保存 JSON 文件
|
||||||
|
2. 在新电脑上,点击"导入"按钮,选择之前导出的 JSON 文件
|
||||||
|
3. 数据即可恢复
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 需要手动操作
|
||||||
|
- 容易忘记备份
|
||||||
|
|
||||||
|
### 方案 2: 添加服务器端存储(推荐)
|
||||||
|
|
||||||
|
需要修改代码,添加后端 API 来保存数据到服务器文件系统。
|
||||||
|
|
||||||
|
#### 实施步骤
|
||||||
|
|
||||||
|
1. **创建简单的后端服务**(Node.js + Express)
|
||||||
|
2. **修改前端代码**,使用 API 保存和加载数据
|
||||||
|
3. **添加数据卷挂载**到 docker-compose.yml
|
||||||
|
4. **重新构建和部署**
|
||||||
|
|
||||||
|
详细方案请参考:`workdizhi数据持久化修复方案.md`
|
||||||
|
|
||||||
|
### 方案 3: 使用浏览器同步(如果支持)
|
||||||
|
|
||||||
|
如果浏览器支持同步功能(如 Chrome 同步),可以:
|
||||||
|
- 启用浏览器同步
|
||||||
|
- localStorage 数据会同步到云端
|
||||||
|
- 在新电脑登录同一账号即可恢复
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 依赖浏览器服务
|
||||||
|
- 可能不适用于所有浏览器
|
||||||
|
|
||||||
|
## 📋 当前 Docker 配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: workdizhi-web
|
||||||
|
ports:
|
||||||
|
- "3006:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- workdizhi-network
|
||||||
|
# ❌ 缺少 volumes 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔨 快速修复建议
|
||||||
|
|
||||||
|
### 立即行动
|
||||||
|
|
||||||
|
1. **使用导出功能备份当前数据**
|
||||||
|
- 打开网站:http://101.43.95.130:3006
|
||||||
|
- 点击"导出"按钮
|
||||||
|
- 保存 JSON 文件到安全位置
|
||||||
|
|
||||||
|
2. **在新电脑上导入数据**
|
||||||
|
- 打开网站
|
||||||
|
- 点击"导入"按钮
|
||||||
|
- 选择之前导出的 JSON 文件
|
||||||
|
|
||||||
|
### 长期方案
|
||||||
|
|
||||||
|
实施方案 2,添加服务器端存储,实现真正的数据持久化。
|
||||||
|
|
||||||
|
## 📊 数据位置对比
|
||||||
|
|
||||||
|
| 存储方式 | 当前 | 修复后 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| **浏览器 localStorage** | ✅ 使用中 | ⚠️ 降级使用(API 失败时) |
|
||||||
|
| **服务器文件** | ❌ 无 | ✅ `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` |
|
||||||
|
| **Docker 卷** | ❌ 无 | ✅ 挂载到容器 |
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **立即备份**: 在修改前,先导出当前所有数据
|
||||||
|
2. **测试**: 修改后充分测试数据保存和加载
|
||||||
|
3. **迁移**: 如果实施服务器端存储,需要提供数据迁移方案
|
||||||
|
|
||||||
214
部署说明.md
Normal file
214
部署说明.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Workdizhi 服务器端存储部署说明
|
||||||
|
|
||||||
|
## ✅ 部署完成
|
||||||
|
|
||||||
|
服务器端数据持久化已成功实施!
|
||||||
|
|
||||||
|
## 📋 部署内容
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
|
||||||
|
1. **server.js** - Node.js Express 后端服务器
|
||||||
|
2. **package.json** - Node.js 依赖配置
|
||||||
|
3. **Dockerfile** - 更新为 Node.js 镜像
|
||||||
|
4. **docker-compose.yml** - 添加数据卷挂载
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
1. **script.js** - 修改数据加载和保存方法,支持服务器 API
|
||||||
|
|
||||||
|
## 🔧 技术架构
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **框架**: Express.js
|
||||||
|
- **端口**: 3000(容器内)
|
||||||
|
- **数据存储**: `/app/data/urls.json`
|
||||||
|
- **API 端点**:
|
||||||
|
- `GET /api/urls` - 获取数据
|
||||||
|
- `POST /api/urls` - 保存数据
|
||||||
|
- `GET /api/health` - 健康检查
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **数据加载**: 优先从服务器加载,失败时降级到 localStorage
|
||||||
|
- **数据保存**: 优先保存到服务器,失败时降级到 localStorage
|
||||||
|
- **兼容性**: 完全向后兼容,支持旧数据迁移
|
||||||
|
|
||||||
|
### 数据持久化
|
||||||
|
|
||||||
|
- **服务器端**: `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
|
||||||
|
- **Docker 卷**: 挂载到容器的 `/app/data`
|
||||||
|
- **备份**: localStorage 作为备用存储
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 访问应用
|
||||||
|
|
||||||
|
- **Web 界面**: http://101.43.95.130:3006
|
||||||
|
- **API 端点**: http://101.43.95.130:3006/api/urls
|
||||||
|
|
||||||
|
### 数据管理
|
||||||
|
|
||||||
|
1. **自动保存**: 添加、编辑、删除网址时自动保存到服务器
|
||||||
|
2. **自动加载**: 打开页面时自动从服务器加载数据
|
||||||
|
3. **数据迁移**: 如果 localStorage 中有旧数据,会自动迁移到服务器
|
||||||
|
|
||||||
|
### 备份和恢复
|
||||||
|
|
||||||
|
数据文件位置:`/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份数据
|
||||||
|
cp /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json /path/to/backup/
|
||||||
|
|
||||||
|
# 恢复数据
|
||||||
|
cp /path/to/backup/urls.json /home/renjianbo/devops/workdizhi/workdizhi/data/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据存储位置
|
||||||
|
|
||||||
|
| 位置 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **服务器文件** | `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` | 主数据存储 |
|
||||||
|
| **容器内** | `/app/data/urls.json` | 容器内的数据文件 |
|
||||||
|
| **浏览器** | localStorage | 备用存储(降级使用) |
|
||||||
|
|
||||||
|
## 🔄 服务管理
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/renjianbo/devops/workdizhi/workdizhi
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 停止服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/renjianbo/devops/workdizhi/workdizhi
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重启服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/renjianbo/devops/workdizhi/workdizhi
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f workdizhi-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重新构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/renjianbo/devops/workdizhi/workdizhi
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 验证部署
|
||||||
|
|
||||||
|
### 1. 检查服务状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps | grep workdizhi
|
||||||
|
# 应该显示容器运行中
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试 API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 健康检查
|
||||||
|
curl http://localhost:3006/api/health
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
curl http://localhost:3006/api/urls
|
||||||
|
|
||||||
|
# 保存数据
|
||||||
|
curl -X POST http://localhost:3006/api/urls \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"urls":[],"categories":[]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查数据文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /home/renjianbo/devops/workdizhi/workdizhi/data/
|
||||||
|
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 故障排查
|
||||||
|
|
||||||
|
### 问题 1: 数据未保存
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 查看容器日志:`docker logs workdizhi-web`
|
||||||
|
2. 检查数据目录权限:`ls -la /home/renjianbo/devops/workdizhi/workdizhi/data/`
|
||||||
|
3. 检查 API 响应:浏览器开发者工具 Network 标签
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
# 修复权限
|
||||||
|
chmod 755 /home/renjianbo/devops/workdizhi/workdizhi/data
|
||||||
|
chown -R renjianbo:renjianbo /home/renjianbo/devops/workdizhi/workdizhi/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 2: 无法访问
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 端口是否监听:`netstat -tlnp | grep 3006`
|
||||||
|
2. 防火墙是否开放:`sudo firewall-cmd --list-ports`
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
# 开放端口
|
||||||
|
sudo firewall-cmd --permanent --add-port=3006/tcp
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题 3: 数据丢失
|
||||||
|
|
||||||
|
**恢复**:
|
||||||
|
1. 检查数据文件是否存在
|
||||||
|
2. 如果有备份,恢复备份文件
|
||||||
|
3. 如果没有备份,检查 localStorage(浏览器开发者工具)
|
||||||
|
|
||||||
|
## 📝 数据迁移
|
||||||
|
|
||||||
|
### 从 localStorage 迁移到服务器
|
||||||
|
|
||||||
|
1. 打开网站:http://101.43.95.130:3006
|
||||||
|
2. 如果 localStorage 中有数据,会自动加载
|
||||||
|
3. 进行任何操作(添加、编辑、删除)会自动保存到服务器
|
||||||
|
4. 数据迁移完成
|
||||||
|
|
||||||
|
### 从导出文件迁移
|
||||||
|
|
||||||
|
1. 使用导入功能导入 JSON 文件
|
||||||
|
2. 数据会自动保存到服务器
|
||||||
|
|
||||||
|
## 🎯 优势
|
||||||
|
|
||||||
|
✅ **数据持久化**: 数据保存在服务器,不会因换电脑而丢失
|
||||||
|
✅ **多设备同步**: 所有设备访问同一服务器,数据同步
|
||||||
|
✅ **自动备份**: 数据文件可以定期备份
|
||||||
|
✅ **向后兼容**: 支持 localStorage 降级,确保兼容性
|
||||||
|
✅ **易于维护**: 数据文件位置明确,易于备份和恢复
|
||||||
|
|
||||||
|
## 📌 注意事项
|
||||||
|
|
||||||
|
1. **定期备份**: 建议定期备份 `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
|
||||||
|
2. **权限管理**: 确保数据目录有正确的读写权限
|
||||||
|
3. **磁盘空间**: 监控数据文件大小,避免占用过多空间
|
||||||
|
4. **安全**: 如果暴露在公网,考虑添加认证机制
|
||||||
|
|
||||||
|
## 🔗 相关文件
|
||||||
|
|
||||||
|
- **后端服务**: `server.js`
|
||||||
|
- **前端代码**: `script.js`
|
||||||
|
- **Docker 配置**: `docker-compose.yml`
|
||||||
|
- **数据文件**: `data/urls.json`
|
||||||
|
|
||||||
Reference in New Issue
Block a user