# 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
,是在外面一层了
你说得对!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生成的收藏夹
```
#### **`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生成的收藏夹
```
### 修改说明:
1. 在 `
` 标签(即 `id="app"` 的 `div` 的结束标签)之后,添加了一条横线 `