# AI生成的导航页 **Repository Path**: hertzhe/AI-Nav ## Basic Information - **Project Name**: AI生成的导航页 - **Description**: 用Deepseek生成的导航页,可通过Docker部署 具有以下功能: 1、可修改网页标题 2、可分类,分类可拖拽排序 3、添加网址时,自动获取网址标题及图标 4、网址可拖拽排序 5、可切换主题风格 6、支持数据导出与导入 # 主打一个自己不写一行代码 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-07-19 - **Last Updated**: 2025-07-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 页面标题前面显示一个图标(图标也要能设置,上传本地图片,保存到服务器),还要能设置整个页面的背景(上传本地图片,保存到服务器) 为了实现页面标题前的图标和页面背景的自定义设置,我们需要对前端和后端进行一些修改。以下是具体的实现步骤: --- ### **1. 后端修改:支持图标和背景图片的上传** #### **1.1 修改 `server.js` 文件** 我们需要添加两个新的 API 接口,用于处理图标和背景图片的上传。 ```javascript const multer = require('multer'); const upload = multer({ dest: 'data/uploads/' }); // 上传文件存储目录 // 确保上传目录存在 if (!fs.existsSync('data/uploads')) { fs.mkdirSync('data/uploads', { recursive: true }); } // 上传页面标题图标 app.post('/api/upload-icon', upload.single('icon'), (req, res) => { if (!req.file) { return res.status(400).json({ success: false, message: '未上传文件' }); } const iconPath = `/data/uploads/${req.file.filename}`; res.json({ success: true, iconPath }); }); // 上传页面背景图片 app.post('/api/upload-background', upload.single('background'), (req, res) => { if (!req.file) { return res.status(400).json({ success: false, message: '未上传文件' }); } const backgroundPath = `/data/uploads/${req.file.filename}`; res.json({ success: true, backgroundPath }); }); ``` #### **1.2 修改 `package.json` 文件** 添加 `multer` 依赖,用于处理文件上传: ```json "dependencies": { "express": "^4.18.2", "axios": "^1.6.2", "cheerio": "^1.0.0-rc.12", "crypto": "^1.0.1", "https": "^1.0.0", "multer": "^1.4.5-lts.1" } ``` 运行 `npm install` 安装依赖。 --- ### **2. 前端修改:支持图标和背景图片的设置** #### **2.1 修改 `app.js` 文件** 在 Vue 的 `data` 中添加两个新字段: ```javascript data() { return { // 其他字段... pageIcon: '', // 页面标题图标 pageBackground: '', // 页面背景图片 showIconModal: false, // 是否显示图标设置模态框 showBackgroundModal: false, // 是否显示背景设置模态框 }; }, ``` 添加两个新方法,用于上传图标和背景图片: ```javascript methods: { // 上传页面标题图标 async uploadIcon(event) { const file = event.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('icon', file); try { const response = await fetch('/api/upload-icon', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageIcon = data.iconPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传图标失败:', error); } }, // 上传页面背景图片 async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('background', file); try { const response = await fetch('/api/upload-background', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageBackground = data.backgroundPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传背景图片失败:', error); } }, } ``` 在 `saveData` 方法中,将 `pageIcon` 和 `pageBackground` 保存到后端: ```javascript async saveData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, }; try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (error) { console.error('保存数据失败:', error); alert('保存数据失败,请稍后重试!'); } } ``` 在 `loadData` 方法中,加载 `pageIcon` 和 `pageBackground`: ```javascript async loadData() { try { const response = await fetch('/api/data'); const data = await response.json(); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; } catch (error) { console.error('加载数据失败:', error); alert('加载数据失败,请稍后重试!'); } } ``` --- #### **2.2 修改 `index.html` 文件** 在页面标题部分添加图标显示和设置按钮: ```html
图标

{{ pageTitle }}

``` 在页面底部添加图标和背景图片的设置模态框: ```html ``` 在页面样式中动态设置背景图片: ```html ``` --- ### **3. 测试功能** 1. 启动后端服务: ```bash node server.js ``` 2. 打开前端页面,进入管理模式后: - 点击设置图标按钮,上传本地图片作为页面标题图标。 - 点击设置背景按钮,上传本地图片作为页面背景。 3. 刷新页面,图标和背景图片应能正确加载并显示。 --- ### **4. 其他注意事项** - **文件存储**:上传的图片会保存在 `data/uploads` 目录下,确保该目录有写入权限。 - **图片格式**:建议限制上传的图片格式(如 `accept="image/*"`),避免用户上传非图片文件。 - **图片大小**:可以在后端添加文件大小限制,避免上传过大的图片。 通过这些修改,你的书签管理应用将支持自定义页面标题图标和背景图片,提升用户体验! # 路径由 /app/backend/data/ 改成 /data/ 要将路径从 `/app/backend/data/` 改为 `/data/`,需要对相关文件进行修改,确保所有引用路径的地方都更新为新的路径。以下是具体的修改步骤: --- ### **1. 修改 `server.js` 文件** 将 `DATA_FILE_PATH` 和 `ICON_DIR` 的路径改为 `/data/`: ```javascript // 书签数据文件路径 const DATA_FILE_PATH = path.join('/data', 'bookmarks.json'); // 图标存储目录 const ICON_DIR = path.join('/data', 'icons'); if (!fs.existsSync(ICON_DIR)) { fs.mkdirSync(ICON_DIR, { recursive: true }); } // 确保数据目录存在 if (!fs.existsSync(path.dirname(DATA_FILE_PATH))) { fs.mkdirSync(path.dirname(DATA_FILE_PATH), { recursive: true }); } ``` 将上传文件的存储路径改为 `/data/uploads/`: ```javascript const upload = multer({ dest: '/data/uploads/' }); // 上传文件存储目录 // 确保上传目录存在 if (!fs.existsSync('/data/uploads')) { fs.mkdirSync('/data/uploads', { recursive: true }); } ``` --- ### **2. 修改 `nginx.conf` 文件** 将 `/data/` 路径的静态文件服务配置更新为新的路径: ```nginx # 后端静态文件服务(如图标和上传的文件) location /data/ { alias /data/; } ``` --- ### **3. 修改 `Dockerfile` 文件** 在 Dockerfile 中,确保 `/data` 目录被正确创建,并且文件被复制到正确的位置: ```dockerfile # 使用 Node.js 作为基础镜像 FROM node:18-alpine as build # 设置工作目录 WORKDIR /app # 复制前端文件 COPY frontend /app/frontend # 安装前端依赖 RUN cd frontend && npm install # 复制后端文件 COPY backend /app/backend # 安装后端依赖 RUN cd backend && npm install --no-fund --no-audit # 使用 Nginx 作为生产服务器 FROM nginx:alpine # 安装 Node.js(用于运行后端服务) RUN apk add --no-cache nodejs # 创建 /data 目录 RUN mkdir -p /data/uploads /data/icons # 复制前端构建文件到 Nginx 的静态文件目录 COPY --from=build /app/frontend /usr/share/nginx/html # 复制后端文件到容器 COPY --from=build /app/backend /app/backend # 复制 Nginx 配置文件 COPY nginx.conf /etc/nginx/conf.d/default.conf # 复制启动脚本 COPY entrypoint.sh /entrypoint.sh # 设置启动脚本为可执行 RUN chmod +x /entrypoint.sh # 暴露端口 EXPOSE 80 # 设置环境变量 ENV MANAGEMENT_PASSWORD=yourpassword # 启动应用 ENTRYPOINT ["/entrypoint.sh"] ``` --- ### **4. 修改 `entrypoint.sh` 文件** 确保 `/data` 目录的权限正确,并启动后端服务和 Nginx: ```bash #!/bin/sh # 确保 /data 目录的权限 chmod -R 777 /data # 启动后端服务 node /app/backend/server.js & # 启动 Nginx nginx -g "daemon off;" ``` --- ### **5. 测试路径修改** 1. **构建 Docker 镜像**: ```bash docker build -t bookmark-app . ``` 2. **运行 Docker 容器**: ```bash docker run -p 80:80 -v /path/to/local/data:/data bookmark-app ``` 其中 `/path/to/local/data` 是你本地用于存储数据的目录。 3. **验证路径**: - 确保 `/data/bookmarks.json`、`/data/icons/` 和 `/data/uploads/` 目录在容器中正确创建。 - 上传图标和背景图片,检查文件是否存储在 `/data/uploads/` 目录下。 - 访问页面,确保图标和背景图片能够正确加载。 --- ### **6. 其他注意事项** - **数据持久化**:通过 Docker 的 `-v` 参数将 `/data` 目录挂载到本地,确保数据在容器重启后不会丢失。 - **权限问题**:如果容器内无法写入 `/data` 目录,检查本地目录的权限,确保容器有读写权限。 - **路径一致性**:确保所有代码中引用的路径都更新为 `/data/`,避免路径不一致导致的问题。 通过这些修改,路径已经从 `/app/backend/data/` 更新为 `/data/`,并且所有功能都能正常运行。 # 浏览器页面源文件显示: ,背景没起作用 如果浏览器页面源文件中显示 ``,但背景图片没有生效,可能是以下原因之一: --- ### **1. `pageBackground` 的值不正确** 确保 `pageBackground` 的值是一个有效的图片 URL。例如: - 如果图片存储在 `/data/uploads/` 目录下,`pageBackground` 的值应该是类似 `/data/uploads/filename.jpg` 的路径。 - 如果图片存储在外部服务器,`pageBackground` 的值应该是完整的 URL,例如 `https://example.com/path/to/image.jpg`。 #### **检查 `pageBackground` 的值** 在 Vue.js 的 `mounted` 或 `updated` 钩子中打印 `pageBackground` 的值,确保它是正确的: ```javascript mounted() { console.log('pageBackground:', this.pageBackground); } ``` 如果 `pageBackground` 是空字符串或无效路径,背景图片将不会生效。 --- ### **2. 图片路径问题** 如果 `pageBackground` 的值是相对路径(如 `/data/uploads/filename.jpg`),确保路径是正确的,并且图片可以通过该路径访问。 #### **检查图片是否可以访问** 在浏览器中直接访问 `pageBackground` 的 URL,例如: ``` http://127.0.0.1:8080/data/uploads/filename.jpg ``` 如果无法访问,可能是以下原因: - 图片文件不存在。 - Nginx 配置未正确提供静态文件服务。 #### **检查 Nginx 配置** 确保 Nginx 配置中正确设置了静态文件服务: ```nginx location /data/ { alias /data/; # 静态文件目录 } ``` 如果路径不正确,修改为实际存储图片的目录。 --- ### **3. CSS 样式问题** 确保 `backgroundImage` 和 `backgroundSize` 的 CSS 样式正确应用到了 `` 元素。 #### **检查浏览器开发者工具** 打开浏览器的开发者工具(按 `F12`),检查 `` 元素的样式: - 确保 `background-image` 和 `background-size` 属性已正确应用。 - 如果样式被覆盖或未生效,可能是其他 CSS 规则干扰了背景图片的显示。 #### **手动测试 CSS** 在开发者工具中手动添加以下样式,检查背景图片是否显示: ```css body { background-image: url('/data/uploads/filename.jpg'); background-size: cover; } ``` 如果手动添加样式后背景图片显示,说明问题出在 Vue.js 动态绑定的逻辑上。 --- ### **4. Vue.js 动态绑定问题** 确保 Vue.js 正确绑定了 `pageBackground` 的值。以下是可能的问题和解决方法: #### **问题 1:`pageBackground` 未正确更新** 如果 `pageBackground` 的值未正确更新,背景图片不会生效。确保在上传图片后,`pageBackground` 的值被正确更新。 例如,在上传成功后更新 `pageBackground`: ```javascript methods: { async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('background', file); try { const response = await fetch('/api/upload-background', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageBackground = data.backgroundPath; // 更新 pageBackground this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传背景图片失败:', error); } }, } ``` #### **问题 2:样式绑定语法错误** 确保 Vue.js 的样式绑定语法正确。例如: ```html ``` 如果 `pageBackground` 是空字符串,`backgroundImage` 的值将是空字符串,背景图片不会生效。 --- ### **5. 图片文件格式问题** 确保上传的图片文件格式是浏览器支持的格式(如 `.jpg`、`.png`、`.gif` 等)。如果图片格式不正确,浏览器可能无法加载。 --- ### **6. 缓存问题** 如果图片路径正确但未显示,可能是浏览器缓存问题。尝试以下方法: - 清除浏览器缓存。 - 使用无痕模式访问页面。 - 在图片 URL 中添加随机参数,强制刷新缓存: ```javascript this.pageBackground = `/data/uploads/filename.jpg?${Date.now()}`; ``` --- ### **总结** 1. 检查 `pageBackground` 的值是否正确。 2. 确保图片路径可以通过浏览器访问。 3. 检查 CSS 样式是否正确应用。 4. 确保 Vue.js 正确绑定了 `pageBackground` 的值。 5. 检查图片文件格式和浏览器缓存。 如果按照以上步骤操作后问题仍未解决,请提供以下信息: - `pageBackground` 的值。 - 浏览器开发者工具中 `` 元素的样式截图。 - 上传图片的相关代码(前端和后端)。 我可以进一步帮助你分析问题! # 我觉得是Vue的作用域是
,是在外面一层了 你说得对!Vue.js 的作用域仅限于 `
` 内部,而 `` 标签是在 Vue 实例的作用域之外的。因此,直接在 `` 上使用 Vue 的动态绑定(如 `:style`)是无效的。 --- ### **解决方案** 为了让背景图片生效,我们需要通过 JavaScript 直接操作 `` 的样式,而不是依赖 Vue 的动态绑定。以下是具体的实现方法: --- ### **1. 使用 JavaScript 直接操作 `` 样式** 在 Vue 组件中,通过 JavaScript 动态修改 `` 的样式。 #### **修改 `app.js`** 在 Vue 组件中,添加一个监听器,当 `pageBackground` 变化时,直接修改 `` 的样式: ```javascript const { createApp } = Vue; const app = createApp({ data() { return { pageBackground: '', // 页面背景图片路径 // 其他数据... }; }, watch: { // 监听 pageBackground 的变化 pageBackground(newBackground) { this.updateBodyBackground(newBackground); } }, methods: { // 更新 的背景图片 updateBodyBackground(background) { if (background) { document.body.style.backgroundImage = `url(${background})`; document.body.style.backgroundSize = 'cover'; } else { document.body.style.backgroundImage = ''; document.body.style.backgroundSize = ''; } }, // 其他方法... }, mounted() { // 初始化时设置背景图片 this.updateBodyBackground(this.pageBackground); } }); app.mount('#app'); ``` #### **说明** - 使用 `watch` 监听 `pageBackground` 的变化。 - 当 `pageBackground` 变化时,调用 `updateBodyBackground` 方法,直接修改 `` 的样式。 - 在 `mounted` 钩子中初始化背景图片。 --- ### **2. 修改 `index.html`** 在 `index.html` 中,移除 `` 上的 Vue 动态绑定,改为静态样式: ```html Deepseek生成的收藏夹
``` --- ### **3. 测试功能** 1. 启动应用后,检查 `` 的背景图片是否正确显示。 2. 上传新的背景图片,检查是否动态更新。 --- ### **4. 完整代码示例** 以下是完整的 `app.js` 和 `index.html` 示例: #### **`app.js`** ```javascript const { createApp } = Vue; const app = createApp({ data() { return { pageBackground: '', // 页面背景图片路径 // 其他数据... }; }, watch: { // 监听 pageBackground 的变化 pageBackground(newBackground) { this.updateBodyBackground(newBackground); } }, methods: { // 更新 的背景图片 updateBodyBackground(background) { if (background) { document.body.style.backgroundImage = `url(${background})`; document.body.style.backgroundSize = 'cover'; } else { document.body.style.backgroundImage = ''; document.body.style.backgroundSize = ''; } }, // 上传背景图片 async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('background', file); try { const response = await fetch('/api/upload-background', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageBackground = data.backgroundPath; // 更新 pageBackground this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传背景图片失败:', error); } }, // 其他方法... }, mounted() { // 初始化时设置背景图片 this.updateBodyBackground(this.pageBackground); } }); app.mount('#app'); ``` #### **`index.html`** ```html Deepseek生成的收藏夹
``` --- ### **总结** 1. Vue 的作用域仅限于 `
`,无法直接绑定到 ``。 2. 通过 JavaScript 直接操作 `` 的样式,实现背景图片的动态更新。 3. 使用 `watch` 监听 `pageBackground` 的变化,并在变化时更新 `` 的背景图片。 如果还有其他问题,请随时告诉我! # 为了让背景图更好的显示,把书签卡片的背景颜色改成半透明 为了让背景图片更好地显示,可以将书签卡片的背景颜色改为半透明。这样背景图片可以透过卡片显示出来,提升视觉效果。 以下是具体的修改步骤: --- ### **1. 修改 `styles.css` 文件** 在 `styles.css` 文件中,找到书签卡片的样式,并将其背景颜色改为半透明。 #### **修改前** ```css .card { background-color: #fff; /* 白色背景 */ border: none; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 10px; } ``` #### **修改后** ```css .card { background-color: rgba(255, 255, 255, 0.8); /* 半透明白色背景 */ border: none; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 10px; } ``` - `rgba(255, 255, 255, 0.8)`:表示白色背景,透明度为 0.8(80% 不透明)。 - 你可以根据需要调整透明度值(`0.8`),值越小越透明。 --- ### **2. 深色主题下的半透明背景** 如果你有深色主题的支持,也需要为深色主题下的卡片设置半透明背景。 #### **修改前** ```css .dark-theme .card { background-color: #2d2d2d; /* 深色背景 */ border: none; color: #f8f9fa; } ``` #### **修改后** ```css .dark-theme .card { background-color: rgba(45, 45, 45, 0.8); /* 半透明深色背景 */ border: none; color: #f8f9fa; } ``` - `rgba(45, 45, 45, 0.8)`:表示深色背景,透明度为 0.8(80% 不透明)。 --- ### **3. 其他相关样式** 如果书签卡片内部有其他元素(如卡片标题、链接等),也可以根据需要调整它们的背景颜色为半透明。 例如,修改卡片标题的背景颜色: #### **修改前** ```css .card-header { background-color: #e9ecef; border-bottom: none; padding: 0.5rem 1rem; border-radius: 8px 8px 0 0; margin-bottom: 8px; } ``` #### **修改后** ```css .card-header { background-color: rgba(233, 236, 239, 0.8); /* 半透明白色背景 */ border-bottom: none; padding: 0.5rem 1rem; border-radius: 8px 8px 0 0; margin-bottom: 8px; } ``` --- ### **4. 完整的 `styles.css` 文件** 以下是修改后的完整 `styles.css` 文件: ```css /* 浅色主题 */ body { background-color: #f8f9fa; color: #333; font-family: Arial, sans-serif; } /* 卡片样式 */ .card { background-color: rgba(255, 255, 255, 0.8); /* 半透明白色背景 */ border: none; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 10px; } .card-header { background-color: rgba(233, 236, 239, 0.8); /* 半透明白色背景 */ border-bottom: none; padding: 0.5rem 1rem; border-radius: 8px 8px 0 0; margin-bottom: 8px; } .card-body { padding: 0.5rem 1rem; } .btn-link { color: #0d6efd; } .btn-link:hover { color: #0a58ca; } /* 深色主题 */ body.dark-theme { background-color: #1e1e1e; color: #f8f9fa; } .dark-theme .card { background-color: rgba(45, 45, 45, 0.8); /* 半透明深色背景 */ border: none; color: #f8f9fa; } .dark-theme .card-header { background-color: rgba(51, 51, 51, 0.8); /* 半透明深色背景 */ border-bottom: none; padding: 0.5rem 1rem; border-radius: 8px 8px 0 0; margin-bottom: 8px; } .dark-theme .card-body { color: #f8f9fa; } .dark-theme .btn-link { color: #4dabf7; } .dark-theme .btn-link:hover { color: #1c7ed6; } .dark-theme hr { background-color: #444; } /* 时间日期样式 */ #current-time { color: inherit !important; font-size: 0.9rem !important; margin-top: 4px !important; } /* 链接样式 */ a { color: inherit; text-decoration: none; } a:hover { text-decoration: underline; } /* 减少行间距 */ .card-body p, .card-body a { margin: 0; line-height: 1.4; } /* 分类标题样式 */ .card-header h5 { margin: 0; font-size: 1.2rem; font-weight: bold; color: inherit; } /* 链接图标样式 */ .card-body img { width: 20px; height: 20px; margin-right: 8px; } /* 编辑和删除按钮样式 */ .card-body .btn-link { padding: 0; margin-left: 8px; } /* 书签卡片样式 */ .card-body .card { background-color: transparent; box-shadow: none; margin-bottom: 0; } .card-body .card-body { padding: 0; } /* 确保拖拽功能正常 */ #categories .card-body .row > div { cursor: grab; user-select: none; } #categories .card-body .row > div.sortable-chosen { cursor: grabbing; opacity: 0.8; } /* 模态框样式(固定为浅色主题) */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.5); } .modal-dialog { background: white; padding: 20px; border-radius: 8px; max-width: 400px; width: 100%; } .modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ddd; padding-bottom: 10px; margin-bottom: 10px; } .modal-title { color: #333; } .modal-body { color: #333; } .modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } ``` --- ### **5. 测试效果** 1. 启动应用后,检查书签卡片的背景是否为半透明。 2. 切换浅色和深色主题,确保卡片背景颜色正确显示。 3. 上传背景图片,检查卡片是否透出背景图片。 --- ### **总结** - 将书签卡片的背景颜色改为半透明(`rgba`)。 - 确保浅色和深色主题下的卡片背景颜色都正确设置。 - 测试效果,确保背景图片可以透过卡片显示。 如果还有其他问题,请随时告诉我! # 这句话超过VUE的作用范围了吧,没有起作用 你说得对!`` 标签位于 Vue 实例的作用域之外,因此 Vue 的动态绑定(如 `:href`)不会生效。为了让 Favicon 动态更新,我们需要通过 JavaScript 直接操作 `` 标签。 --- ### **解决方案** 我们可以通过 JavaScript 动态修改 `` 标签的 `href` 属性,而不是依赖 Vue 的动态绑定。以下是具体实现方法: --- ### **1. 修改 `index.html`** 在 `index.html` 中,将 `` 标签改为静态的,并为其添加一个 `id`,以便后续通过 JavaScript 操作: ```html ``` --- ### **2. 修改 `app.js`** 在 Vue 组件中,监听 `pageIcon` 的变化,并通过 JavaScript 动态更新 Favicon。 #### **修改后的 `app.js`** ```javascript const { createApp } = Vue; const app = createApp({ data() { return { pageIcon: '', // 页面标题图标 // 其他数据... }; }, watch: { // 监听 pageIcon 的变化 pageIcon(newIcon) { this.updateFavicon(newIcon); } }, methods: { // 更新 Favicon updateFavicon(iconPath) { const favicon = document.getElementById('favicon'); if (favicon) { favicon.href = iconPath || '/data/icons/default.png'; } }, // 其他方法... }, mounted() { // 初始化时设置 Favicon this.updateFavicon(this.pageIcon); } }); app.mount('#app'); ``` --- ### **3. 测试功能** 1. 启动应用后,检查浏览器标签页的 Favicon 是否正确显示。 2. 上传新的图标,检查 Favicon 是否动态更新。 --- ### **4. 完整代码** 以下是修改后的完整 `index.html` 和 `app.js` 文件: #### **`index.html`** ```html Deepseek生成的收藏夹
图标

{{ pageTitle }}

{{ currentTime }}

{{ category.name }}
``` #### **`app.js`** ```javascript const { createApp } = Vue; const app = createApp({ data() { return { pageIcon: '', // 页面标题图标 // 其他数据... }; }, watch: { // 监听 pageIcon 的变化 pageIcon(newIcon) { this.updateFavicon(newIcon); } }, methods: { // 更新 Favicon updateFavicon(iconPath) { const favicon = document.getElementById('favicon'); if (favicon) { favicon.href = iconPath || '/data/icons/default.png'; } }, // 其他方法... }, mounted() { // 初始化时设置 Favicon this.updateFavicon(this.pageIcon); } }); app.mount('#app'); ``` --- ### **总结** 1. 将 `` 标签改为静态的,并为其添加 `id`。 2. 在 Vue 组件中,通过 JavaScript 动态更新 Favicon 的 `href` 属性。 3. 确保 `pageIcon` 变化时,Favicon 能够动态更新。 如果还有其他问题,请随时告诉我! # 现在有个问题,背景图片太大的话,会上传失败 如果背景图片太大导致上传失败,可能是由于以下几个原因导致的: ### 1. **文件大小限制** - **问题**:`multer` 默认没有限制上传文件的大小,但如果文件过大,可能会导致上传失败或服务器内存不足。 - **解决方案**:可以在 `multer` 配置中设置文件大小限制。修改 `server.js` 中的 `multer` 配置,添加 `limits` 选项来限制文件大小。例如: ```javascript const upload = multer({ dest: UPLOAD_DIR, limits: { fileSize: 5 * 1024 * 1024 } // 限制文件大小为 5MB }); ``` 这样,超过 5MB 的文件会被拒绝上传。 ### 2. **Nginx 上传限制** - **问题**:如果使用 Nginx 作为反向代理,Nginx 默认也有上传文件大小的限制(通常是 1MB)。 - **解决方案**:修改 `nginx.conf` 文件,增加 `client_max_body_size` 配置项来允许更大的文件上传。例如: ```nginx server { listen 80; server_name _; # 设置上传文件大小限制为 10MB client_max_body_size 10M; # 前端静态文件服务 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端静态文件服务(如图标) location /data/ { alias /data/; } # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 这样,Nginx 将允许上传最大 10MB 的文件。 ### 3. **前端文件上传限制** - **问题**:前端没有对文件大小进行校验,导致用户上传过大的文件。 - **解决方案**:在前端代码中添加文件大小校验逻辑。可以在 `app.js` 中的 `uploadBackground` 方法中添加文件大小检查。例如: ```javascript async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } const formData = new FormData(); formData.append('background', file); try { const response = await fetch('/api/upload-background', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageBackground = data.backgroundPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传背景图片失败:', error); } } ``` 这样,用户上传超过 5MB 的文件时,前端会直接提示错误,避免上传失败。 ### 4. **服务器内存限制** - **问题**:如果上传的文件非常大,可能会导致服务器内存不足,尤其是 Node.js 在处理大文件时可能会占用大量内存。 - **解决方案**:可以使用 `multer` 的 `storage` 选项,将文件直接写入磁盘,而不是存储在内存中。例如: ```javascript const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOAD_DIR); }, filename: (req, file, cb) => { cb(null, file.originalname); } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } }); // 限制文件大小为 5MB ``` 这样可以减少内存占用,避免因文件过大导致服务器崩溃。 ### 5. **用户体验优化** - **问题**:用户上传大文件时,可能没有明确的提示,导致上传失败后用户不清楚原因。 - **解决方案**:在前端上传文件时,增加文件大小提示和上传进度显示。例如: ```html 请选择小于 5MB 的图片文件。 ``` 同时,可以在上传过程中显示进度条或加载动画,提升用户体验。 ### 总结 通过以上几种方式,可以有效解决背景图片太大导致上传失败的问题: 1. 在 `multer` 中设置文件大小限制。 2. 在 Nginx 中调整 `client_max_body_size`。 3. 在前端添加文件大小校验。 4. 使用 `multer` 的 `storage` 选项,避免大文件占用内存。 5. 优化用户体验,提供明确的文件大小提示和上传进度。 通过这些改进,可以确保用户上传的背景图片不会因为文件过大而导致上传失败。 # 设置成深色主题后,只要刷新浏览器,就会恢复成浅色主题,请修复 要修复深色主题在刷新浏览器后恢复为浅色主题的问题,我们需要将用户的主题偏好保存到 **本地存储(LocalStorage)** 中。这样,即使用户刷新页面,主题设置也会被保留。 以下是修复步骤: --- ### **1. 修改 `app.js` 代码** 在 `app.js` 中,我们需要做以下修改: #### **(1) 在 `data` 中初始化主题时,从 LocalStorage 中读取主题设置** ```javascript data() { return { // 其他数据... isDarkTheme: localStorage.getItem('isDarkTheme') === 'true', // 从 LocalStorage 读取主题设置 }; }, ``` #### **(2) 在 `toggleTheme` 方法中,保存主题设置到 LocalStorage** ```javascript methods: { // 切换主题 toggleTheme() { this.isDarkTheme = !this.isDarkTheme; localStorage.setItem('isDarkTheme', this.isDarkTheme); // 保存主题设置到 LocalStorage this.updateBodyTheme(); }, // 更新页面主题 updateBodyTheme() { if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } }, // 其他方法保持不变... }, ``` #### **(3) 在 `mounted` 中初始化主题** 确保在页面加载时,根据 LocalStorage 中的主题设置初始化页面主题: ```javascript mounted() { this.loadData(); // 加载数据 this.updateBodyTheme(); // 初始化主题 this.updateTime(); // 初始化时间 setInterval(() => this.updateTime(), 1000); // 每秒更新时间 this.initSortable(); // 初始化拖拽排序 } ``` --- ### **2. 修改后的完整 `app.js` 代码** 以下是修改后的完整 `app.js` 代码: ```javascript const { createApp } = Vue; const app = createApp({ data() { return { categories: [], // 书签分类 newCategoryName: '', // 新分类名称 newLink: { name: '', url: '', icon: '', category: '' }, // 新书签 editingCategory: null, // 正在编辑的分类 editingLink: null, // 正在编辑的书签 currentTime: '', // 当前时间 isAuthenticated: false, // 是否进入管理模式 isLoading: false, // 是否正在加载 showCategoryModal: false, // 是否显示分类模态框 showLinkModal: false, // 是否显示书签模态框 isEditingCategory: false, // 是否正在编辑分类 isEditingLink: false, // 是否正在编辑书签 pageTitle: 'Deepseek生成的收藏夹', // 默认标题 pageIcon: '', // 页面标题图标 pageBackground: '', // 页面背景图片 isEditingTitle: false, // 是否正在编辑标题 showTitleModal: false, // 是否显示标题模态框 isDarkTheme: localStorage.getItem('isDarkTheme') === 'true', // 从 LocalStorage 读取主题设置 sortableInstances: [], // 拖拽排序实例 passwordInput: '', // 用户输入的密码 showPasswordModal: false, // 是否显示密码输入模态框 showIconModal: false, // 是否显示图标设置模态框 showBackgroundModal: false, // 是否显示背景设置模态框 uploadProgress: 0, // 上传进度 }; }, watch: { // 监听 pageBackground 的变化 pageBackground(newBackground) { this.updateBodyBackground(newBackground); }, // 监听 pageIcon 的变化 pageIcon(newIcon) { this.updateFavicon(newIcon); }, // 监听页面标题变化 pageTitle(newTitle) { document.title = newTitle; }, // 监听链接 URL 变化,自动获取网站信息 'newLink.url': function (newUrl) { if (newUrl) { this.fetchWebsiteInfo(newUrl); } } }, methods: { // 更新时间 updateTime() { const now = new Date(); const options = { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', // 显示完整的星期名称 hour: '2-digit', // 显示小时 minute: '2-digit', // 显示分钟 second: '2-digit', // 显示秒 hour12: false, // 使用 24 小时制 }; this.currentTime = now.toLocaleString('zh-CN', options); // 使用中文格式 }, // 更新 的背景图片 updateBodyBackground(background) { if (background) { document.body.style.backgroundImage = `url(${background})`; document.body.style.backgroundSize = 'cover'; } else { document.body.style.backgroundImage = ''; document.body.style.backgroundSize = ''; } }, // 更新 Favicon updateFavicon(iconPath) { const favicon = document.getElementById('favicon'); if (favicon) { favicon.href = iconPath || '/data/icons/default.png'; } }, // 获取网站信息 async fetchWebsiteInfo(url) { this.isLoading = true; try { const response = await fetch('/api/fetch-website-info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await response.json(); if (data.success) { this.newLink.name = data.title; this.newLink.icon = data.icon; } else { this.newLink.name = ''; this.newLink.icon = ''; } } catch (error) { console.error('无法获取网站信息:', error); this.newLink.name = ''; this.newLink.icon = ''; } finally { this.isLoading = false; } }, // 上传页面标题图标 async uploadIcon(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } const formData = new FormData(); formData.append('icon', file); try { const response = await fetch('/api/upload-icon', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageIcon = data.iconPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传图标失败:', error); } }, // 上传页面背景图片 async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } // 创建 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('background', file); // 监听上传进度 xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); this.uploadProgress = percent; // 更新进度条 } }); // 监听上传完成 xhr.addEventListener('load', () => { if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); if (data.success) { this.pageBackground = data.backgroundPath; this.saveData(); // 保存数据到后端 } } else { console.error('上传失败:', xhr.statusText); } this.uploadProgress = 0; // 重置进度条 }); // 监听上传错误 xhr.addEventListener('error', () => { console.error('上传失败'); this.uploadProgress = 0; // 重置进度条 }); // 发送请求 xhr.open('POST', '/api/upload-background', true); xhr.send(formData); }, // 打开密码输入模态框 openPasswordModal() { this.showPasswordModal = true; }, // 关闭密码输入模态框 closePasswordModal() { this.showPasswordModal = false; this.passwordInput = ''; // 清空输入框 }, // 验证密码 async validatePassword() { try { const response = await fetch('/api/validate-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: this.passwordInput }), }); const result = await response.json(); if (result.success) { this.isAuthenticated = true; this.closePasswordModal(); this.initSortable(); // 进入管理模式后重新初始化拖拽排序 } else { alert(result.message || '密码错误!'); this.passwordInput = ''; // 清空输入框 } } catch (error) { console.error('验证密码失败:', error); alert('无法连接到服务器,请稍后重试!'); } }, // 切换管理模式 toggleManagementMode() { if (this.isAuthenticated) { // 退出管理模式时不需要密码 this.isAuthenticated = false; } else { // 进入管理模式时打开密码输入模态框 this.openPasswordModal(); } this.updateSortableInstances(); // 更新拖拽排序状态 }, // 更新拖拽排序实例 updateSortableInstances() { this.sortableInstances.forEach(instance => { instance.option('disabled', !this.isAuthenticated); // 根据管理模式状态更新拖拽配置 }); }, // 初始化拖拽排序 initSortable() { // 清空现有的拖拽排序实例 this.sortableInstances = []; // 初始化分类拖拽排序 const categoryContainer = document.querySelector('#categories'); if (categoryContainer) { const categorySortable = Sortable.create(categoryContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 handle: '.card-header', // 只有分类的头部可以拖拽 onEnd: (evt) => { const movedCategory = this.categories[evt.oldIndex]; this.categories.splice(evt.oldIndex, 1); this.categories.splice(evt.newIndex, 0, movedCategory); this.saveData(); } }); this.sortableInstances.push(categorySortable); } // 初始化每个分类的书签拖拽排序 this.categories.forEach((category, index) => { const linkContainer = document.querySelector(`#links-${index}`); if (linkContainer) { const linkSortable = Sortable.create(linkContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 onEnd: (evt) => { const movedLink = category.links[evt.oldIndex]; category.links.splice(evt.oldIndex, 1); category.links.splice(evt.newIndex, 0, movedLink); this.saveData(); } }); this.sortableInstances.push(linkSortable); } }); }, // 打开分类模态框 openCategoryModal(category) { if (category) { this.editingCategory = category; this.newCategoryName = category.name; this.isEditingCategory = true; } else { this.newCategoryName = ''; this.isEditingCategory = false; } this.showCategoryModal = true; }, // 关闭分类模态框 closeCategoryModal() { this.showCategoryModal = false; this.editingCategory = null; this.newCategoryName = ''; }, // 保存分类 async saveCategory(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newCategoryName.trim()) { if (this.isEditingCategory) { this.editingCategory.name = this.newCategoryName; } else { this.categories.push({ name: this.newCategoryName, links: [] }); } await this.saveData(); this.closeCategoryModal(); this.initSortable(); // 重新初始化拖拽排序 } }, // 打开书签模态框 openLinkModal(link) { if (link) { this.editingLink = link; this.newLink = { ...link, category: this.getCategoryName(link) }; this.isEditingLink = true; } else { const defaultCategory = this.categories.length > 0 ? this.categories[0].name : ''; this.newLink = { name: '', url: '', icon: '', category: defaultCategory }; this.isEditingLink = false; } this.showLinkModal = true; }, // 关闭书签模态框 closeLinkModal() { this.showLinkModal = false; this.editingLink = null; this.newLink = { name: '', url: '', icon: '', category: '' }; }, // 保存书签 async saveLink(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newLink.name.trim() && this.newLink.url.trim() && this.newLink.category) { const category = this.categories.find(cat => cat.name === this.newLink.category); if (category) { if (this.isEditingLink) { const oldCategory = this.categories.find(cat => cat.links.includes(this.editingLink)); if (oldCategory !== category) { oldCategory.links = oldCategory.links.filter(l => l !== this.editingLink); category.links.push(this.newLink); } else { Object.assign(this.editingLink, this.newLink); } } else { category.links.push(this.newLink); } await this.saveData(); this.closeLinkModal(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 删除分类 async deleteCategory(category) { const confirmDelete = confirm('确定要删除此分类及其所有链接吗?'); if (confirmDelete) { this.categories = this.categories.filter(cat => cat !== category); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } }, // 删除书签 async deleteLink(link) { const confirmDelete = confirm('确定要删除此链接吗?'); if (confirmDelete) { const category = this.categories.find(cat => cat.links.includes(link)); if (category) { category.links = category.links.filter(l => l !== link); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 获取书签所属分类名称 getCategoryName(link) { const category = this.categories.find(cat => cat.links.includes(link)); return category ? category.name : ''; }, // 保存数据到后端 async saveData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, }; try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (error) { console.error('保存数据失败:', error); alert('保存数据失败,请稍后重试!'); } }, // 加载数据 async loadData() { try { const response = await fetch('/api/data'); const data = await response.json(); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; } catch (error) { console.error('加载数据失败:', error); alert('加载数据失败,请稍后重试!'); } }, // 导出数据 async exportData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); try { if (window.showSaveFilePicker) { // 使用现代浏览器的文件保存 API const handle = await window.showSaveFilePicker({ suggestedName: 'bookmarks.json', types: [ { description: 'JSON Files', accept: { 'application/json': ['.json'] }, }, ], }); const writable = await handle.createWritable(); await writable.write(dataBlob); await writable.close(); alert('数据导出成功!'); } else { // 兼容旧版浏览器的文件保存方式 const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'bookmarks.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); alert('数据已下载到默认下载目录!'); } } catch (error) { console.error('导出数据失败:', error); alert('导出数据失败,请重试!'); } }, // 导入数据 importData() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.saveData(); alert('数据导入成功!'); } catch (error) { alert('文件格式错误,请导入正确的 JSON 文件!'); } }; reader.readAsText(file); } }; input.click(); }, // 切换主题 toggleTheme() { this.isDarkTheme = !this.isDarkTheme; localStorage.setItem('isDarkTheme', this.isDarkTheme); // 保存主题设置到 LocalStorage this.updateBodyTheme(); }, // 更新页面主题 updateBodyTheme() { if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } }, // 切换编辑标题状态 toggleEditTitle() { this.showTitleModal = true; // 显示标题模态框 }, // 保存标题 saveTitle() { this.showTitleModal = false; // 隐藏标题模态框 this.saveData(); // 保存标题到后端 }, }, mounted() { this.loadData(); // 加载数据 this.updateBodyTheme(); // 初始化主题 this.updateTime(); // 初始化时间 setInterval(() => this.updateTime(), 1000); // 每秒更新时间 this.initSortable(); // 初始化拖拽排序 } }); app.mount('#app'); ``` --- ### **3. 修复后的效果** 1. 用户切换主题后,主题设置会被保存到 **LocalStorage** 中。 2. 刷新页面后,主题设置会从 LocalStorage 中读取并应用到页面。 3. 深色主题和浅色主题的设置会在页面刷新后保持不变。 --- ### **4. 测试方法** 1. 切换到深色主题。 2. 刷新页面,确认主题仍然是深色。 3. 切换到浅色主题。 4. 刷新页面,确认主题仍然是浅色。 通过以上修改,主题设置会在页面刷新后保持不变。 # 主题设置也改成保存在服务器,不要保存在本地 为了将主题设置保存到服务器而不是本地存储(LocalStorage),我们需要对代码进行以下修改: 1. **后端 API 支持**:在 `server.js` 中添加一个字段来保存主题设置。 2. **前端逻辑调整**:在 `app.js` 中修改主题切换逻辑,将主题设置保存到服务器。 --- ### **1. 修改 `server.js`** 在 `server.js` 中,修改书签数据的结构,增加一个字段来保存主题设置(`isDarkTheme`)。 #### **(1) 修改默认数据** 在 `loadData` 函数中,增加 `isDarkTheme` 字段: ```javascript function loadData() { try { if (fs.existsSync(DATA_FILE_PATH)) { const data = fs.readFileSync(DATA_FILE_PATH, 'utf8'); return JSON.parse(data); } } catch (error) { console.error('读取数据失败:', error); } return { categories: [], pageTitle: '我的书签', pageIcon: '', pageBackground: '', isDarkTheme: false, // 默认主题为浅色 }; } ``` #### **(2) 修改保存数据逻辑** 在 `saveData` 函数中,确保 `isDarkTheme` 字段被正确保存: ```javascript function saveData(data) { try { fs.writeFileSync(DATA_FILE_PATH, JSON.stringify(data, null, 2), 'utf8'); } catch (error) { console.error('保存数据失败:', error); } } ``` --- ### **2. 修改 `app.js`** 在 `app.js` 中,修改主题切换逻辑,将主题设置保存到服务器。 #### **(1) 修改 `data` 初始化** 从服务器加载数据时,初始化 `isDarkTheme` 字段: ```javascript data() { return { categories: [], // 书签分类 newCategoryName: '', // 新分类名称 newLink: { name: '', url: '', icon: '', category: '' }, // 新书签 editingCategory: null, // 正在编辑的分类 editingLink: null, // 正在编辑的书签 currentTime: '', // 当前时间 isAuthenticated: false, // 是否进入管理模式 isLoading: false, // 是否正在加载 showCategoryModal: false, // 是否显示分类模态框 showLinkModal: false, // 是否显示书签模态框 isEditingCategory: false, // 是否正在编辑分类 isEditingLink: false, // 是否正在编辑书签 pageTitle: 'Deepseek生成的收藏夹', // 默认标题 pageIcon: '', // 页面标题图标 pageBackground: '', // 页面背景图片 isEditingTitle: false, // 是否正在编辑标题 showTitleModal: false, // 是否显示标题模态框 isDarkTheme: false, // 是否启用深色主题(从服务器加载) sortableInstances: [], // 拖拽排序实例 passwordInput: '', // 用户输入的密码 showPasswordModal: false, // 是否显示密码输入模态框 showIconModal: false, // 是否显示图标设置模态框 showBackgroundModal: false, // 是否显示背景设置模态框 uploadProgress: 0, // 上传进度 }; }, ``` #### **(2) 修改 `loadData` 方法** 从服务器加载数据时,初始化 `isDarkTheme` 字段: ```javascript async loadData() { try { const response = await fetch('/api/data'); const data = await response.json(); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.isDarkTheme = data.isDarkTheme || false; // 从服务器加载主题设置 this.updateBodyTheme(); // 初始化主题 } catch (error) { console.error('加载数据失败:', error); alert('加载数据失败,请稍后重试!'); } }, ``` #### **(3) 修改 `toggleTheme` 方法** 切换主题时,将主题设置保存到服务器: ```javascript // 切换主题 async toggleTheme() { this.isDarkTheme = !this.isDarkTheme; this.updateBodyTheme(); await this.saveData(); // 保存主题设置到服务器 }, // 更新页面主题 updateBodyTheme() { if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } }, ``` #### **(4) 修改 `saveData` 方法** 保存数据时,将 `isDarkTheme` 字段发送到服务器: ```javascript // 保存数据到后端 async saveData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, isDarkTheme: this.isDarkTheme, // 保存主题设置 }; try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (error) { console.error('保存数据失败:', error); alert('保存数据失败,请稍后重试!'); } }, ``` --- ### **3. 修改后的完整 `app.js` 代码** 以下是修改后的完整 `app.js` 代码: ```javascript const { createApp } = Vue; const app = createApp({ data() { return { categories: [], // 书签分类 newCategoryName: '', // 新分类名称 newLink: { name: '', url: '', icon: '', category: '' }, // 新书签 editingCategory: null, // 正在编辑的分类 editingLink: null, // 正在编辑的书签 currentTime: '', // 当前时间 isAuthenticated: false, // 是否进入管理模式 isLoading: false, // 是否正在加载 showCategoryModal: false, // 是否显示分类模态框 showLinkModal: false, // 是否显示书签模态框 isEditingCategory: false, // 是否正在编辑分类 isEditingLink: false, // 是否正在编辑书签 pageTitle: 'Deepseek生成的收藏夹', // 默认标题 pageIcon: '', // 页面标题图标 pageBackground: '', // 页面背景图片 isEditingTitle: false, // 是否正在编辑标题 showTitleModal: false, // 是否显示标题模态框 isDarkTheme: false, // 是否启用深色主题(从服务器加载) sortableInstances: [], // 拖拽排序实例 passwordInput: '', // 用户输入的密码 showPasswordModal: false, // 是否显示密码输入模态框 showIconModal: false, // 是否显示图标设置模态框 showBackgroundModal: false, // 是否显示背景设置模态框 uploadProgress: 0, // 上传进度 }; }, watch: { // 监听 pageBackground 的变化 pageBackground(newBackground) { this.updateBodyBackground(newBackground); }, // 监听 pageIcon 的变化 pageIcon(newIcon) { this.updateFavicon(newIcon); }, // 监听页面标题变化 pageTitle(newTitle) { document.title = newTitle; }, // 监听链接 URL 变化,自动获取网站信息 'newLink.url': function (newUrl) { if (newUrl) { this.fetchWebsiteInfo(newUrl); } } }, methods: { // 更新时间 updateTime() { const now = new Date(); const options = { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', // 显示完整的星期名称 hour: '2-digit', // 显示小时 minute: '2-digit', // 显示分钟 second: '2-digit', // 显示秒 hour12: false, // 使用 24 小时制 }; this.currentTime = now.toLocaleString('zh-CN', options); // 使用中文格式 }, // 更新 的背景图片 updateBodyBackground(background) { if (background) { document.body.style.backgroundImage = `url(${background})`; document.body.style.backgroundSize = 'cover'; } else { document.body.style.backgroundImage = ''; document.body.style.backgroundSize = ''; } }, // 更新 Favicon updateFavicon(iconPath) { const favicon = document.getElementById('favicon'); if (favicon) { favicon.href = iconPath || '/data/icons/default.png'; } }, // 获取网站信息 async fetchWebsiteInfo(url) { this.isLoading = true; try { const response = await fetch('/api/fetch-website-info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await response.json(); if (data.success) { this.newLink.name = data.title; this.newLink.icon = data.icon; } else { this.newLink.name = ''; this.newLink.icon = ''; } } catch (error) { console.error('无法获取网站信息:', error); this.newLink.name = ''; this.newLink.icon = ''; } finally { this.isLoading = false; } }, // 上传页面标题图标 async uploadIcon(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } const formData = new FormData(); formData.append('icon', file); try { const response = await fetch('/api/upload-icon', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageIcon = data.iconPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传图标失败:', error); } }, // 上传页面背景图片 async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } // 创建 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('background', file); // 监听上传进度 xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); this.uploadProgress = percent; // 更新进度条 } }); // 监听上传完成 xhr.addEventListener('load', () => { if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); if (data.success) { this.pageBackground = data.backgroundPath; this.saveData(); // 保存数据到后端 } } else { console.error('上传失败:', xhr.statusText); } this.uploadProgress = 0; // 重置进度条 }); // 监听上传错误 xhr.addEventListener('error', () => { console.error('上传失败'); this.uploadProgress = 0; // 重置进度条 }); // 发送请求 xhr.open('POST', '/api/upload-background', true); xhr.send(formData); }, // 打开密码输入模态框 openPasswordModal() { this.showPasswordModal = true; }, // 关闭密码输入模态框 closePasswordModal() { this.showPasswordModal = false; this.passwordInput = ''; // 清空输入框 }, // 验证密码 async validatePassword() { try { const response = await fetch('/api/validate-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: this.passwordInput }), }); const result = await response.json(); if (result.success) { this.isAuthenticated = true; this.closePasswordModal(); this.initSortable(); // 进入管理模式后重新初始化拖拽排序 } else { alert(result.message || '密码错误!'); this.passwordInput = ''; // 清空输入框 } } catch (error) { console.error('验证密码失败:', error); alert('无法连接到服务器,请稍后重试!'); } }, // 切换管理模式 toggleManagementMode() { if (this.isAuthenticated) { // 退出管理模式时不需要密码 this.isAuthenticated = false; } else { // 进入管理模式时打开密码输入模态框 this.openPasswordModal(); } this.updateSortableInstances(); // 更新拖拽排序状态 }, // 更新拖拽排序实例 updateSortableInstances() { this.sortableInstances.forEach(instance => { instance.option('disabled', !this.isAuthenticated); // 根据管理模式状态更新拖拽配置 }); }, // 初始化拖拽排序 initSortable() { // 清空现有的拖拽排序实例 this.sortableInstances = []; // 初始化分类拖拽排序 const categoryContainer = document.querySelector('#categories'); if (categoryContainer) { const categorySortable = Sortable.create(categoryContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 handle: '.card-header', // 只有分类的头部可以拖拽 onEnd: (evt) => { const movedCategory = this.categories[evt.oldIndex]; this.categories.splice(evt.oldIndex, 1); this.categories.splice(evt.newIndex, 0, movedCategory); this.saveData(); } }); this.sortableInstances.push(categorySortable); } // 初始化每个分类的书签拖拽排序 this.categories.forEach((category, index) => { const linkContainer = document.querySelector(`#links-${index}`); if (linkContainer) { const linkSortable = Sortable.create(linkContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 onEnd: (evt) => { const movedLink = category.links[evt.oldIndex]; category.links.splice(evt.oldIndex, 1); category.links.splice(evt.newIndex, 0, movedLink); this.saveData(); } }); this.sortableInstances.push(linkSortable); } }); }, // 打开分类模态框 openCategoryModal(category) { if (category) { this.editingCategory = category; this.newCategoryName = category.name; this.isEditingCategory = true; } else { this.newCategoryName = ''; this.isEditingCategory = false; } this.showCategoryModal = true; }, // 关闭分类模态框 closeCategoryModal() { this.showCategoryModal = false; this.editingCategory = null; this.newCategoryName = ''; }, // 保存分类 async saveCategory(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newCategoryName.trim()) { if (this.isEditingCategory) { this.editingCategory.name = this.newCategoryName; } else { this.categories.push({ name: this.newCategoryName, links: [] }); } await this.saveData(); this.closeCategoryModal(); this.initSortable(); // 重新初始化拖拽排序 } }, // 打开书签模态框 openLinkModal(link) { if (link) { this.editingLink = link; this.newLink = { ...link, category: this.getCategoryName(link) }; this.isEditingLink = true; } else { const defaultCategory = this.categories.length > 0 ? this.categories[0].name : ''; this.newLink = { name: '', url: '', icon: '', category: defaultCategory }; this.isEditingLink = false; } this.showLinkModal = true; }, // 关闭书签模态框 closeLinkModal() { this.showLinkModal = false; this.editingLink = null; this.newLink = { name: '', url: '', icon: '', category: '' }; }, // 保存书签 async saveLink(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newLink.name.trim() && this.newLink.url.trim() && this.newLink.category) { const category = this.categories.find(cat => cat.name === this.newLink.category); if (category) { if (this.isEditingLink) { const oldCategory = this.categories.find(cat => cat.links.includes(this.editingLink)); if (oldCategory !== category) { oldCategory.links = oldCategory.links.filter(l => l !== this.editingLink); category.links.push(this.newLink); } else { Object.assign(this.editingLink, this.newLink); } } else { category.links.push(this.newLink); } await this.saveData(); this.closeLinkModal(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 删除分类 async deleteCategory(category) { const confirmDelete = confirm('确定要删除此分类及其所有链接吗?'); if (confirmDelete) { this.categories = this.categories.filter(cat => cat !== category); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } }, // 删除书签 async deleteLink(link) { const confirmDelete = confirm('确定要删除此链接吗?'); if (confirmDelete) { const category = this.categories.find(cat => cat.links.includes(link)); if (category) { category.links = category.links.filter(l => l !== link); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 获取书签所属分类名称 getCategoryName(link) { const category = this.categories.find(cat => cat.links.includes(link)); return category ? category.name : ''; }, // 保存数据到后端 async saveData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, isDarkTheme: this.isDarkTheme, // 保存主题设置 }; try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (error) { console.error('保存数据失败:', error); alert('保存数据失败,请稍后重试!'); } }, // 加载数据 async loadData() { try { const response = await fetch('/api/data'); const data = await response.json(); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.isDarkTheme = data.isDarkTheme || false; // 从服务器加载主题设置 this.updateBodyTheme(); // 初始化主题 } catch (error) { console.error('加载数据失败:', error); alert('加载数据失败,请稍后重试!'); } }, // 导出数据 async exportData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, isDarkTheme: this.isDarkTheme, // 导出主题设置 }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); try { if (window.showSaveFilePicker) { // 使用现代浏览器的文件保存 API const handle = await window.showSaveFilePicker({ suggestedName: 'bookmarks.json', types: [ { description: 'JSON Files', accept: { 'application/json': ['.json'] }, }, ], }); const writable = await handle.createWritable(); await writable.write(dataBlob); await writable.close(); alert('数据导出成功!'); } else { // 兼容旧版浏览器的文件保存方式 const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'bookmarks.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); alert('数据已下载到默认下载目录!'); } } catch (error) { console.error('导出数据失败:', error); alert('导出数据失败,请重试!'); } }, // 导入数据 importData() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.isDarkTheme = data.isDarkTheme || false; // 导入主题设置 this.saveData(); alert('数据导入成功!'); } catch (error) { alert('文件格式错误,请导入正确的 JSON 文件!'); } }; reader.readAsText(file); } }; input.click(); }, // 切换主题 async toggleTheme() { this.isDarkTheme = !this.isDarkTheme; this.updateBodyTheme(); await this.saveData(); // 保存主题设置到服务器 }, // 更新页面主题 updateBodyTheme() { if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } }, // 切换编辑标题状态 toggleEditTitle() { this.showTitleModal = true; // 显示标题模态框 }, // 保存标题 saveTitle() { this.showTitleModal = false; // 隐藏标题模态框 this.saveData(); // 保存标题到后端 }, }, mounted() { this.loadData(); // 加载数据 this.updateTime(); // 初始化时间 setInterval(() => this.updateTime(), 1000); // 每秒更新时间 this.initSortable(); // 初始化拖拽排序 } }); app.mount('#app'); ``` --- ### **4. 测试方法** 1. 切换到深色主题,刷新页面,确认主题仍然是深色。 2. 切换到浅色主题,刷新页面,确认主题仍然是浅色。 3. 导出数据,确认主题设置被正确保存到 JSON 文件中。 4. 导入数据,确认主题设置被正确加载。 通过以上修改,主题设置会被保存到服务器,并在页面刷新后保持不变。 # 在设置图标及设置背景对话框上,增加一个选项:不需要图标或背景 为了在设置图标和设置背景的对话框中增加一个选项,允许用户选择**不需要图标或背景**,我们需要对前端代码进行修改。以下是具体的实现步骤: --- ### **1. 修改 `index.html`** 在设置图标和设置背景的模态框中,增加一个按钮或选项,允许用户选择“不需要图标”或“不需要背景”。 #### **(1) 设置图标模态框** ```html ``` #### **(2) 设置背景模态框** ```html ``` --- ### **2. 修改 `app.js`** 在 `app.js` 中,增加两个方法 `removeIcon` 和 `removeBackground`,用于处理用户选择“不需要图标”或“不需要背景”的逻辑。 #### **(1) 增加 `removeIcon` 方法** ```javascript methods: { // 移除图标 async removeIcon() { this.pageIcon = ''; // 清空图标路径 await this.saveData(); // 保存数据到后端 this.showIconModal = false; // 关闭模态框 }, // 其他方法保持不变... }, ``` #### **(2) 增加 `removeBackground` 方法** ```javascript methods: { // 移除背景 async removeBackground() { this.pageBackground = ''; // 清空背景路径 await this.saveData(); // 保存数据到后端 this.showBackgroundModal = false; // 关闭模态框 }, // 其他方法保持不变... }, ``` --- ### **3. 修改后的完整 `app.js` 代码** 以下是修改后的完整 `app.js` 代码,包含新增的 `removeIcon` 和 `removeBackground` 方法: ```javascript const { createApp } = Vue; const app = createApp({ data() { return { categories: [], // 书签分类 newCategoryName: '', // 新分类名称 newLink: { name: '', url: '', icon: '', category: '' }, // 新书签 editingCategory: null, // 正在编辑的分类 editingLink: null, // 正在编辑的书签 currentTime: '', // 当前时间 isAuthenticated: false, // 是否进入管理模式 isLoading: false, // 是否正在加载 showCategoryModal: false, // 是否显示分类模态框 showLinkModal: false, // 是否显示书签模态框 isEditingCategory: false, // 是否正在编辑分类 isEditingLink: false, // 是否正在编辑书签 pageTitle: 'Deepseek生成的收藏夹', // 默认标题 pageIcon: '', // 页面标题图标 pageBackground: '', // 页面背景图片 isEditingTitle: false, // 是否正在编辑标题 showTitleModal: false, // 是否显示标题模态框 isDarkTheme: localStorage.getItem('isDarkTheme') === 'true', // 从 LocalStorage 读取主题设置 sortableInstances: [], // 拖拽排序实例 passwordInput: '', // 用户输入的密码 showPasswordModal: false, // 是否显示密码输入模态框 showIconModal: false, // 是否显示图标设置模态框 showBackgroundModal: false, // 是否显示背景设置模态框 uploadProgress: 0, // 上传进度 }; }, watch: { // 监听 pageBackground 的变化 pageBackground(newBackground) { this.updateBodyBackground(newBackground); }, // 监听 pageIcon 的变化 pageIcon(newIcon) { this.updateFavicon(newIcon); }, // 监听页面标题变化 pageTitle(newTitle) { document.title = newTitle; }, // 监听链接 URL 变化,自动获取网站信息 'newLink.url': function (newUrl) { if (newUrl) { this.fetchWebsiteInfo(newUrl); } } }, methods: { // 更新时间 updateTime() { const now = new Date(); const options = { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', // 显示完整的星期名称 hour: '2-digit', // 显示小时 minute: '2-digit', // 显示分钟 second: '2-digit', // 显示秒 hour12: false, // 使用 24 小时制 }; this.currentTime = now.toLocaleString('zh-CN', options); // 使用中文格式 }, // 更新 的背景图片 updateBodyBackground(background) { if (background) { document.body.style.backgroundImage = `url(${background})`; document.body.style.backgroundSize = 'cover'; } else { document.body.style.backgroundImage = ''; document.body.style.backgroundSize = ''; } }, // 更新 Favicon updateFavicon(iconPath) { const favicon = document.getElementById('favicon'); if (favicon) { favicon.href = iconPath || '/data/icons/default.png'; } }, // 获取网站信息 async fetchWebsiteInfo(url) { this.isLoading = true; try { const response = await fetch('/api/fetch-website-info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await response.json(); if (data.success) { this.newLink.name = data.title; this.newLink.icon = data.icon; } else { this.newLink.name = ''; this.newLink.icon = ''; } } catch (error) { console.error('无法获取网站信息:', error); this.newLink.name = ''; this.newLink.icon = ''; } finally { this.isLoading = false; } }, // 上传页面标题图标 async uploadIcon(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } const formData = new FormData(); formData.append('icon', file); try { const response = await fetch('/api/upload-icon', { method: 'POST', body: formData, }); const data = await response.json(); if (data.success) { this.pageIcon = data.iconPath; this.saveData(); // 保存数据到后端 } } catch (error) { console.error('上传图标失败:', error); } }, // 移除图标 async removeIcon() { this.pageIcon = ''; // 清空图标路径 await this.saveData(); // 保存数据到后端 this.showIconModal = false; // 关闭模态框 }, // 上传页面背景图片 async uploadBackground(event) { const file = event.target.files[0]; if (!file) return; // 检查文件大小(例如限制为 5MB) const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) { alert('文件大小不能超过 5MB'); return; } // 创建 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('background', file); // 监听上传进度 xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); this.uploadProgress = percent; // 更新进度条 } }); // 监听上传完成 xhr.addEventListener('load', () => { if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); if (data.success) { this.pageBackground = data.backgroundPath; this.saveData(); // 保存数据到后端 } } else { console.error('上传失败:', xhr.statusText); } this.uploadProgress = 0; // 重置进度条 }); // 监听上传错误 xhr.addEventListener('error', () => { console.error('上传失败'); this.uploadProgress = 0; // 重置进度条 }); // 发送请求 xhr.open('POST', '/api/upload-background', true); xhr.send(formData); }, // 移除背景 async removeBackground() { this.pageBackground = ''; // 清空背景路径 await this.saveData(); // 保存数据到后端 this.showBackgroundModal = false; // 关闭模态框 }, // 打开密码输入模态框 openPasswordModal() { this.showPasswordModal = true; }, // 关闭密码输入模态框 closePasswordModal() { this.showPasswordModal = false; this.passwordInput = ''; // 清空输入框 }, // 验证密码 async validatePassword() { try { const response = await fetch('/api/validate-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: this.passwordInput }), }); const result = await response.json(); if (result.success) { this.isAuthenticated = true; this.closePasswordModal(); this.initSortable(); // 进入管理模式后重新初始化拖拽排序 } else { alert(result.message || '密码错误!'); this.passwordInput = ''; // 清空输入框 } } catch (error) { console.error('验证密码失败:', error); alert('无法连接到服务器,请稍后重试!'); } }, // 切换管理模式 toggleManagementMode() { if (this.isAuthenticated) { // 退出管理模式时不需要密码 this.isAuthenticated = false; } else { // 进入管理模式时打开密码输入模态框 this.openPasswordModal(); } this.updateSortableInstances(); // 更新拖拽排序状态 }, // 更新拖拽排序实例 updateSortableInstances() { this.sortableInstances.forEach(instance => { instance.option('disabled', !this.isAuthenticated); // 根据管理模式状态更新拖拽配置 }); }, // 初始化拖拽排序 initSortable() { // 清空现有的拖拽排序实例 this.sortableInstances = []; // 初始化分类拖拽排序 const categoryContainer = document.querySelector('#categories'); if (categoryContainer) { const categorySortable = Sortable.create(categoryContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 handle: '.card-header', // 只有分类的头部可以拖拽 onEnd: (evt) => { const movedCategory = this.categories[evt.oldIndex]; this.categories.splice(evt.oldIndex, 1); this.categories.splice(evt.newIndex, 0, movedCategory); this.saveData(); } }); this.sortableInstances.push(categorySortable); } // 初始化每个分类的书签拖拽排序 this.categories.forEach((category, index) => { const linkContainer = document.querySelector(`#links-${index}`); if (linkContainer) { const linkSortable = Sortable.create(linkContainer, { animation: 150, disabled: !this.isAuthenticated, // 根据管理模式状态启用或禁用拖拽 onEnd: (evt) => { const movedLink = category.links[evt.oldIndex]; category.links.splice(evt.oldIndex, 1); category.links.splice(evt.newIndex, 0, movedLink); this.saveData(); } }); this.sortableInstances.push(linkSortable); } }); }, // 打开分类模态框 openCategoryModal(category) { if (category) { this.editingCategory = category; this.newCategoryName = category.name; this.isEditingCategory = true; } else { this.newCategoryName = ''; this.isEditingCategory = false; } this.showCategoryModal = true; }, // 关闭分类模态框 closeCategoryModal() { this.showCategoryModal = false; this.editingCategory = null; this.newCategoryName = ''; }, // 保存分类 async saveCategory(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newCategoryName.trim()) { if (this.isEditingCategory) { this.editingCategory.name = this.newCategoryName; } else { this.categories.push({ name: this.newCategoryName, links: [] }); } await this.saveData(); this.closeCategoryModal(); this.initSortable(); // 重新初始化拖拽排序 } }, // 打开书签模态框 openLinkModal(link) { if (link) { this.editingLink = link; this.newLink = { ...link, category: this.getCategoryName(link) }; this.isEditingLink = true; } else { const defaultCategory = this.categories.length > 0 ? this.categories[0].name : ''; this.newLink = { name: '', url: '', icon: '', category: defaultCategory }; this.isEditingLink = false; } this.showLinkModal = true; }, // 关闭书签模态框 closeLinkModal() { this.showLinkModal = false; this.editingLink = null; this.newLink = { name: '', url: '', icon: '', category: '' }; }, // 保存书签 async saveLink(event) { event.preventDefault(); // 阻止默认提交行为 if (this.newLink.name.trim() && this.newLink.url.trim() && this.newLink.category) { const category = this.categories.find(cat => cat.name === this.newLink.category); if (category) { if (this.isEditingLink) { const oldCategory = this.categories.find(cat => cat.links.includes(this.editingLink)); if (oldCategory !== category) { oldCategory.links = oldCategory.links.filter(l => l !== this.editingLink); category.links.push(this.newLink); } else { Object.assign(this.editingLink, this.newLink); } } else { category.links.push(this.newLink); } await this.saveData(); this.closeLinkModal(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 删除分类 async deleteCategory(category) { const confirmDelete = confirm('确定要删除此分类及其所有链接吗?'); if (confirmDelete) { this.categories = this.categories.filter(cat => cat !== category); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } }, // 删除书签 async deleteLink(link) { const confirmDelete = confirm('确定要删除此链接吗?'); if (confirmDelete) { const category = this.categories.find(cat => cat.links.includes(link)); if (category) { category.links = category.links.filter(l => l !== link); await this.saveData(); this.initSortable(); // 重新初始化拖拽排序 } } }, // 获取书签所属分类名称 getCategoryName(link) { const category = this.categories.find(cat => cat.links.includes(link)); return category ? category.name : ''; }, // 保存数据到后端 async saveData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, isDarkTheme: this.isDarkTheme, // 保存主题设置 }; try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch (error) { console.error('保存数据失败:', error); alert('保存数据失败,请稍后重试!'); } }, // 加载数据 async loadData() { try { const response = await fetch('/api/data'); const data = await response.json(); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.isDarkTheme = data.isDarkTheme || false; // 从服务器加载主题设置 this.updateBodyTheme(); // 初始化主题 } catch (error) { console.error('加载数据失败:', error); alert('加载数据失败,请稍后重试!'); } }, // 导出数据 async exportData() { const data = { categories: this.categories, pageTitle: this.pageTitle, pageIcon: this.pageIcon, pageBackground: this.pageBackground, isDarkTheme: this.isDarkTheme, // 导出主题设置 }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); try { if (window.showSaveFilePicker) { // 使用现代浏览器的文件保存 API const handle = await window.showSaveFilePicker({ suggestedName: 'bookmarks.json', types: [ { description: 'JSON Files', accept: { 'application/json': ['.json'] }, }, ], }); const writable = await handle.createWritable(); await writable.write(dataBlob); await writable.close(); alert('数据导出成功!'); } else { // 兼容旧版浏览器的文件保存方式 const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = 'bookmarks.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); alert('数据已下载到默认下载目录!'); } } catch (error) { console.error('导出数据失败:', error); alert('导出数据失败,请重试!'); } }, // 导入数据 importData() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); this.categories = data.categories || []; this.pageTitle = data.pageTitle || 'Deepseek生成的收藏夹'; this.pageIcon = data.pageIcon || ''; this.pageBackground = data.pageBackground || ''; this.isDarkTheme = data.isDarkTheme || false; // 导入主题设置 this.saveData(); alert('数据导入成功!'); } catch (error) { alert('文件格式错误,请导入正确的 JSON 文件!'); } }; reader.readAsText(file); } }; input.click(); }, // 切换主题 async toggleTheme() { this.isDarkTheme = !this.isDarkTheme; this.updateBodyTheme(); await this.saveData(); // 保存主题设置到服务器 }, // 更新页面主题 updateBodyTheme() { if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); } }, // 切换编辑标题状态 toggleEditTitle() { this.showTitleModal = true; // 显示标题模态框 }, // 保存标题 saveTitle() { this.showTitleModal = false; // 隐藏标题模态框 this.saveData(); // 保存标题到后端 }, }, mounted() { this.loadData(); // 加载数据 this.updateTime(); // 初始化时间 setInterval(() => this.updateTime(), 1000); // 每秒更新时间 this.initSortable(); // 初始化拖拽排序 } }); app.mount('#app'); ``` --- ### **4. 测试方法** 1. 打开设置图标模态框,点击“不需要图标”按钮,确认图标被移除。 2. 打开设置背景模态框,点击“不需要背景”按钮,确认背景被移除。 3. 刷新页面,确认图标和背景的设置被正确保存和加载。 通过以上修改,用户可以选择不需要图标或背景,并且这些设置会被保存到服务器。 # 页面底部也加一条横线,横线下居中显示两个链接:第一个链接显示为:飘鸿出品,链接url为https://gitee.com/piaohong1978/AI-Nav ;第二个显示为 Power by Deepseek,链接url为:https://chat.deepseek.com/ 以下是完整的 `index.html` 文件内容,包含您要求的底部横线和链接: ```html Deepseek生成的收藏夹
图标

{{ pageTitle }}

{{ currentTime }}

{{ uploadProgress }}%
{{ category.name }}

飘鸿出品 Power by Deepseek
``` ### 修改说明: 1. 在 `
` 标签(即 `id="app"` 的 `div` 的结束标签)之后,添加了一条横线 `
` 和两个链接。 2. 使用 Bootstrap 的 `text-center` 类将链接居中显示。 3. 链接分别指向 `https://gitee.com/piaohong1978/AI-Nav` 和 `https://chat.deepseek.com/`,并设置了 `target="_blank"` 以在新标签页中打开。 这样,页面底部会显示一条横线,并在横线下居中显示两个链接。