效果图
🌟【定制化开发服务,让您的项目领先一步】🌟
如有需求,直接私信留下您的联系方式。谢谢。
我的邮箱:2351598671@qq.com
完整代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>智能收藏夹管理系统</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<el-container class="bookmark-manager">
<!-- 顶部导航 -->
<el-header>
<div class="header-content">
<div class="logo">
<i class="el-icon-collection"></i>
智能收藏夹管理系统
</div>
<div class="header-actions">
<el-button type="primary" size="small" @click="importBookmarks">
<i class="el-icon-upload2"></i> 导入收藏夹
</el-button>
<el-button type="success" size="small" @click="exportBookmarks">
<i class="el-icon-download"></i> 导出收藏夹
</el-button>
</div>
</div>
</el-header>
<el-container>
<!-- 左侧分类栏 -->
<el-aside width="250px">
<div class="category-header">
<span>分类管理</span>
<el-button type="text" @click="addCategory">
<i class="el-icon-plus"></i>
</el-button>
</div>
<el-menu
:default-active="activeCategory"
class="category-menu"
@select="handleCategorySelect">
<el-menu-item index="all">
<i class="el-icon-files"></i>
<span>全部收藏</span>
<span class="item-count">{{ totalBookmarks }}</span>
</el-menu-item>
<el-menu-item index="uncategorized">
<i class="el-icon-folder"></i>
<span>未分类</span>
<span class="item-count">{{ uncategorizedCount }}</span>
</el-menu-item>
<el-menu-item
v-for="category in categories"
:key="category.id"
:index="category.id">
<i class="el-icon-folder"></i>
<span>{{ category.name }}</span>
<span class="item-count">{{ category.count }}</span>
</el-menu-item>
</el-menu>
<div class="tag-section">
<div class="section-header">
<span>常用标签</span>
</div>
<div class="tag-cloud">
<el-tag
v-for="tag in popularTags"
:key="tag.name"
:type="tag.type"
size="small"
@click="filterByTag(tag.name)">
{{ tag.name }} ({{ tag.count }})
</el-tag>
</div>
</div>
</el-aside>
<!-- 主要内容区 -->
<el-main>
<!-- 搜索和工具栏 -->
<div class="toolbar">
<el-input
placeholder="搜索收藏..."
v-model="searchQuery"
class="search-input"
prefix-icon="el-icon-search"
clearable>
</el-input>
<div class="tool-actions">
<el-select v-model="sortBy" size="small" placeholder="排序方式">
<el-option label="添加时间" value="date"></el-option>
<el-option label="名称" value="name"></el-option>
<el-option label="访问次数" value="visits"></el-option>
</el-select>
<el-button-group>
<el-button
size="small"
:type="viewMode === 'list' ? 'primary' : ''"
@click="viewMode = 'list'">
<i class="el-icon-menu"></i>
</el-button>
<el-button
size="small"
:type="viewMode === 'grid' ? 'primary' : ''"
@click="viewMode = 'grid'">
<i class="el-icon-s-grid"></i>
</el-button>
</el-button-group>
</div>
</div>
<!-- 批量操作工具栏 -->
<div class="batch-toolbar" v-show="selected.length > 0">
<span class="selected-count">已选择 {{ selected.length }} 项</span>
<el-button-group>
<el-button size="small" @click="batchMove">
<i class="el-icon-folder"></i> 移动到
</el-button>
<el-button size="small" @click="batchAddTags">
<i class="el-icon-price-tag"></i> 添加标签
</el-button>
<el-button size="small" type="danger" @click="batchDelete">
<i class="el-icon-delete"></i> 删除
</el-button>
</el-button-group>
</div>
<!-- 书签列表/网格视图 -->
<div :class="['bookmark-container', viewMode]">
<template v-if="viewMode === 'list'">
<el-table
:data="filteredBookmarks"
@selection-change="handleSelectionChange"
style="width: 100%">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column width="40">
<template slot-scope="scope">
<img :src="getFavicon(scope.row.url)" class="favicon"/>
</template>
</el-table-column>
<el-table-column prop="title" label="标题">
<template slot-scope="scope">
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
<div class="bookmark-tags">
<el-tag
v-for="tag in scope.row.tags"
:key="tag"
size="mini">
{{ tag }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" width="120">
</el-table-column>
<el-table-column prop="addDate" label="添加时间" width="180">
</el-table-column>
<el-table-column width="100">
<template slot-scope="scope">
<el-dropdown trigger="click">
<el-button type="text">
<i class="el-icon-more"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="editBookmark(scope.row)">
编辑
</el-dropdown-item>
<el-dropdown-item @click.native="moveBookmark(scope.row)">
移动到
</el-dropdown-item>
<el-dropdown-item @click.native="deleteBookmark(scope.row)">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</template>
<template v-else>
<el-checkbox-group v-model="selected" class="grid-view">
<div
v-for="item in filteredBookmarks"
:key="item.id"
class="grid-item">
<el-checkbox :label="item.id" class="grid-checkbox"></el-checkbox>
<div class="grid-content">
<img :src="getFavicon(item.url)" class="grid-favicon"/>
<div class="grid-title">{{ item.title }}</div>
<div class="grid-tags">
<el-tag
v-for="tag in item.tags"
:key="tag"
size="mini">
{{ tag }}
</el-tag>
</div>
</div>
</div>
</el-checkbox-group>
</template>
</div>
</el-main>
</el-container>
</el-container>
<!-- 添加/编辑书签对话框 -->
<el-dialog
:title="dialogType === 'add' ? '添加书签' : '编辑书签'"
:visible.sync="bookmarkDialogVisible"
width="500px">
<el-form :model="bookmarkForm" label-width="80px">
<el-form-item label="标题">
<el-input v-model="bookmarkForm.title"></el-input>
</el-form-item>
<el-form-item label="URL">
<el-input v-model="bookmarkForm.url"></el-input>
</el-form-item>
<el-form-item label="分类">
<el-select v-model="bookmarkForm.category" style="width: 100%">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标签">
<el-select
v-model="bookmarkForm.tags"
multiple
filterable
allow-create
default-first-option
style="width: 100%">
<el-option
v-for="tag in allTags"
:key="tag"
:label="tag"
:value="tag">
</el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="bookmarkDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveBookmark">确定</el-button>
</span>
</el-dialog>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
activeCategory: 'all',
searchQuery: '',
sortBy: 'date',
viewMode: 'list',
selected: [],
bookmarkDialogVisible: false,
dialogType: 'add',
bookmarkForm: {
title: '',
url: '',
category: '',
tags: []
},
categories: [
{ id: '1', name: '工作', count: 15 },
{ id: '2', name: '学习', count: 23 },
{ id: '3', name: '娱乐', count: 8 },
{ id: '4', name: '生活', count: 12 }
],
bookmarks: [
{
id: '1',
title: 'Google',
url: 'https://www.google.com',
category: '1',
tags: ['搜索', '工具'],
addDate: '2024-02-25 10:30'
},
{
id: '2',
title: 'GitHub',
url: 'https://github.com',
category: '2',
tags: ['开发', '代码'],
addDate: '2024-02-24 15:20'
}
// 更多书签数据...
],
popularTags: [
{ name: '工具', count: 15, type: '' },
{ name: '教程', count: 12, type: 'success' },
{ name: '参考', count: 8, type: 'info' },
{ name: '娱乐', count: 6, type: 'warning' }
]
}
},
computed: {
totalBookmarks() {
return this.bookmarks.length
},
uncategorizedCount() {
return this.bookmarks.filter(b => !b.category).length
},
filteredBookmarks() {
let result = [...this.bookmarks]
// 分类过滤
if (this.activeCategory !== 'all') {
if (this.activeCategory === 'uncategorized') {
result = result.filter(b => !b.category)
} else {
result = result.filter(b => b.category === this.activeCategory)
}
}
// 搜索过滤
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase()
result = result.filter(b =>
b.title.toLowerCase().includes(query) ||
b.url.toLowerCase().includes(query) ||
b.tags.some(tag => tag.toLowerCase().includes(query))
)
}
// 排序
result.sort((a, b) => {
switch (this.sortBy) {
case 'name':
return a.title.localeCompare(b.title)
case 'date':
return new Date(b.addDate) - new Date(a.addDate)
case 'visits':
return (b.visits || 0) - (a.visits || 0)
default:
return 0
}
})
return result
},
allTags() {
const tags = new Set()
this.bookmarks.forEach(bookmark => {
bookmark.tags.forEach(tag => tags.add(tag))
})
return Array.from(tags)
}
},
methods: {
handleCategorySelect(index) {
this.activeCategory = index
},
handleSelectionChange(val) {
this.selected = val.map(item => item.id)
},
getFavicon(url) {
try {
const domain = new URL(url).hostname
return `https://www.google.com/s2/favicons?domain=${domain}`
} catch {
return 'https://via.placeholder.com/16'
}
},
importBookmarks() {
// 实现导入功能
this.$message({
message: '导入功能开发中...',
type: 'info'
})
},
exportBookmarks() {
// 实现导出功能
this.$message({
message: '导出功能开发中...',
type: 'info'
})
},
addCategory() {
this.$prompt('请输入分类名称', '新建分类', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(({ value }) => {
if (value) {
this.categories.push({
id: Date.now().toString(),
name: value,
count: 0
})
}
})
},
filterByTag(tag) {
this.searchQuery = tag
},
editBookmark(bookmark) {
this.dialogType = 'edit'
this.bookmarkForm = { ...bookmark }
this.bookmarkDialogVisible = true
},
moveBookmark(bookmark) {
// 实现移动功能
},
deleteBookmark(bookmark) {
this.$confirm('确定要删除这个书签吗?', '提示', {
type: 'warning'
}).then(() => {
const index = this.bookmarks.findIndex(b => b.id === bookmark.id)
if (index > -1) {
this.bookmarks.splice(index, 1)
this.$message({
type: 'success',
message: '删除成功'
})
}
})
},
batchMove() {
// 实现批量移动功能
},
batchAddTags() {
// 实现批量添加标签功能
},
batchDelete() {
this.$confirm(`确定要删除选中的 ${this.selected.length} 个书签吗?`, '提示', {
type: 'warning'
}).then(() => {
this.bookmarks = this.bookmarks.filter(b => !this.selected.includes(b.id))
this.selected = []
this.$message({
type: 'success',
message: '批量删除成功'
})
})
},
saveBookmark() {
if (this.dialogType === 'add') {
this.bookmarks.push({
id: Date.now().toString(),
...this.bookmarkForm,
addDate: new Date().toLocaleString()
})
} else {
const index = this.bookmarks.findIndex(b => b.id === this.bookmarkForm.id)
if (index > -1) {
this.bookmarks[index] = { ...this.bookmarkForm }
}
}
this.bookmarkDialogVisible = false
this.$message({
type: 'success',
message: this.dialogType === 'add' ? '添加成功' : '更新成功'
})
}
}
})
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background-color: #f5f7fa;
}
.bookmark-manager {
height: 100vh;
}
.el-header {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
position: relative;
z-index: 1;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.logo {
font-size: 18px;
font-weight: bold;
color: #409EFF;
}
.logo i {
margin-right: 8px;
}
.el-aside {
background-color: #fff;
border-right: 1px solid #e6e6e6;
}
.category-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e6e6e6;
}
.category-menu {
border-right: none;
}
.item-count {
float: right;
color: #909399;
font-size: 12px;
}
.tag-section {
padding: 15px;
}
.section-header {
margin-bottom: 10px;
color: #606266;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.toolbar {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-input {
width: 300px;
}
.tool-actions {
display: flex;
gap: 10px;
}
.batch-toolbar {
background-color: #f0f9ff;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.selected-count {
color: #409EFF;
}
.bookmark-container {
background-color: #fff;
border-radius: 4px;
padding: 20px;
}
.bookmark-container.grid {
padding: 10px;
}
.favicon {
width: 16px;
height: 16px;
vertical-align: middle;
}
.bookmark-tags {
margin-top: 5px;
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.grid-item {
position: relative;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 15px;
transition: all 0.3s;
}
.grid-item:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.grid-checkbox {
position: absolute;
top: 10px;
right: 10px;
}
.grid-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.grid-favicon {
width: 32px;
height: 32px;
}
.grid-title {
text-align: center;
font-size: 14px;
color: #303133;
}
.grid-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.el-aside {
width: 100% !important;
position: fixed;
top: 60px;
bottom: 0;
z-index: 2;
transform: translateX(-100%);
transition: transform 0.3s;
}
.el-aside.show {
transform: translateX(0);
}
.search-input {
width: 100%;
}
.tool-actions {
margin-top: 10px;
}
.toolbar {
flex-direction: column;
}
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>
</body>
</html>