Files
intc-workbenches/src/views/ManagePage.vue
2025-12-13 19:52:29 +08:00

1898 lines
51 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<page class="manage-page">
<div class="manage-header">
<h1>书签管理</h1>
<button class="back-btn" @click="goBack">返回工作台</button>
</div>
<div class="manage-container">
<!-- 大分类 Tab 标签页固定在顶部 -->
<div class="manage-section fixed-section">
<div class="section-header">
<h2>书签分类</h2>
<div class="header-actions">
<button class="add-btn" @click="showAddSuperCategoryDialog">+ 添加分类</button>
</div>
</div>
<!-- Tab 标签 -->
<div class="tab-container">
<div class="tab-list">
<div
v-for="superCategory in superCategories"
:key="superCategory.id"
:class="['tab-item', { active: activeSuperCategoryId === superCategory.id }]"
@click="switchTab(superCategory.id)"
>
<span class="tab-sort">{{ superCategory.sortOrder }}</span>
<span class="tab-title">{{ superCategory.title }}</span>
<span class="tab-count">{{ getCategoryCountInSuper(superCategory.id) }} 个分组</span>
<div class="tab-actions" @click.stop>
<button class="tab-edit-btn" @click="editSuperCategory(superCategory)" title="编辑分类"></button>
<button class="tab-delete-btn" @click="deleteSuperCategory(superCategory)" title="删除分类">×</button>
</div>
</div>
</div>
</div>
</div>
<!-- 当前分类下的书签分组管理 -->
<div v-if="activeSuperCategoryId" class="manage-section">
<div class="section-header">
<h2>书签分组</h2>
<button class="add-btn" @click="showAddCategoryDialog">+ 添加书签分组</button>
</div>
<div class="category-list">
<div v-for="category in currentCategories" :key="category.id" class="category-item">
<div class="category-info">
<span class="category-key">{{ category.key }}</span>
<span class="category-name">{{ category.title }}</span>
<span class="category-sort">排序: {{ category.sortOrder }}</span>
</div>
<div class="category-footer">
<span class="category-count">{{ category.tools ? category.tools.length : 0 }} 个网址</span>
<div class="category-actions">
<button class="edit-btn" @click="editCategory(category)">编辑</button>
<button class="delete-btn" @click="deleteCategory(category)">删除</button>
</div>
</div>
</div>
</div>
</div>
<!-- 网址管理 -->
<div v-if="activeSuperCategoryId" class="manage-section">
<div class="section-header">
<h2>网址管理</h2>
</div>
<div v-for="category in currentCategories" :key="category.id" class="tool-group">
<div class="tool-group-header">
<h3>{{ category.title }}</h3>
<button class="add-btn small" @click="showAddToolDialog(category)">+ 添加网址</button>
</div>
<div class="tool-list">
<div v-for="tool in category.tools" :key="tool.id" class="tool-item">
<div class="tool-main">
<div class="tool-icon" :style="{background: tool.color}">
<span>{{ tool.icon }}</span>
</div>
<div class="tool-info">
<div class="tool-name">{{ tool.name }}</div>
<div class="tool-desc">{{ tool.desc }}</div>
</div>
<span class="tool-sort">排序: {{ tool.sortOrder || 0 }}</span>
</div>
<div class="tool-footer">
<div class="tool-url">{{ tool.url }}</div>
<div class="tool-actions">
<button class="edit-btn" @click="editTool(category, tool)">编辑</button>
<button class="delete-btn" @click="deleteTool(category, tool)">删除</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分类编辑对话框 -->
<div v-if="showSuperCategoryDialog" class="dialog-overlay" @click="closeSuperCategoryDialog">
<div class="dialog" @click.stop>
<h3>{{ superCategoryForm.id ? '编辑分类' : '添加分类' }}</h3>
<div class="form-group">
<label><span class="required">*</span>分类标识</label>
<input v-model="superCategoryForm.key" placeholder="例如work" :disabled="!!superCategoryForm.id" />
</div>
<div class="form-group">
<label><span class="required">*</span>分类名称</label>
<input v-model="superCategoryForm.title" placeholder="例如:工作" />
</div>
<div class="form-group">
<label>排序序号</label>
<input v-model.number="superCategoryForm.sortOrder" type="number" placeholder="数字越小越靠前例如0" />
</div>
<div class="dialog-actions">
<button class="cancel-btn" @click="closeSuperCategoryDialog">取消</button>
<button class="confirm-btn" @click="saveSuperCategory">保存</button>
</div>
</div>
</div>
<!-- 分类编辑对话框 -->
<div v-if="showCategoryDialog" class="dialog-overlay" @click="closeCategoryDialog">
<div class="dialog" @click.stop>
<h3>{{ categoryForm.id ? '编辑书签分组' : '添加书签分组' }}</h3>
<div class="form-group">
<label><span class="required">*</span>书签分组标识</label>
<input v-model="categoryForm.key" placeholder="例如doc" :disabled="!!categoryForm.id" />
</div>
<div class="form-group">
<label><span class="required">*</span>书签分组名称</label>
<input v-model="categoryForm.title" placeholder="例如书签1" />
</div>
<div class="form-group">
<label>排序序号</label>
<input v-model.number="categoryForm.sortOrder" type="number" placeholder="数字越小越靠前例如0" />
</div>
<div class="dialog-actions">
<button class="cancel-btn" @click="closeCategoryDialog">取消</button>
<button class="confirm-btn" @click="saveCategory">保存</button>
</div>
</div>
</div>
<!-- 工具编辑对话框 -->
<div v-if="showToolDialog" class="dialog-overlay" @click="closeToolDialog">
<div class="dialog" @click.stop>
<h3>{{ toolForm.id ? '编辑网址' : '添加网址' }}</h3>
<div class="form-group">
<label><span class="required">*</span>访问地址</label>
<input v-model="toolForm.url" placeholder="例如https://www.baidu.com" />
</div>
<div class="form-group">
<label><span class="required">*</span>网址名称</label>
<input v-model="toolForm.name" placeholder="例如网址1" />
</div>
<div class="form-group">
<label>网址描述</label>
<input v-model="toolForm.desc" placeholder="例如:网址描述" />
</div>
<div class="form-group">
<label>显示地址</label>
<input v-model="toolForm.displayUrl" placeholder="例如www.baidu.com" />
</div>
<div class="form-group">
<label>图标文字</label>
<input v-model="toolForm.icon" placeholder="例如:官" maxlength="4" />
</div>
<div class="form-group">
<label>图标颜色</label>
<div class="color-picker">
<div
v-for="(colorOption, index) in colorOptions"
:key="index"
class="color-option"
:class="{ active: toolForm.color === colorOption.value }"
:style="{ background: colorOption.value }"
@click="toolForm.color = colorOption.value"
:title="colorOption.name"
>
<span v-if="toolForm.color === colorOption.value" class="check-icon"></span>
</div>
</div>
</div>
<div class="form-group">
<label>排序序号</label>
<input v-model.number="toolForm.sortOrder" type="number" placeholder="数字越小越靠前例如0" />
</div>
<div class="dialog-actions">
<button class="cancel-btn" @click="closeToolDialog">取消</button>
<button class="confirm-btn" @click="saveTool">保存</button>
</div>
</div>
</div>
<!-- 确认对话框 -->
<div v-if="showConfirmDialog" class="dialog-overlay" @click="cancelConfirm">
<div class="confirm-dialog" @click.stop>
<div class="confirm-icon warning"></div>
<div class="confirm-message">{{ confirmMessage }}</div>
<div class="confirm-actions">
<button class="cancel-btn" @click="cancelConfirm">取消</button>
<button class="confirm-btn danger" @click="confirmAction">确定</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<transition name="message-fade">
<div v-if="message.show" :class="['message-toast', message.type]">
<span class="message-icon">{{ messageIcon }}</span>
<span class="message-text">{{ message.text }}</span>
</div>
</transition>
</page>
</template>
<script>
import Page from '../components/Page'
import ToolService from '../services/ToolService'
import Sortable from 'sortablejs'
export default {
name: 'ManagePage',
components: { Page },
data() {
return {
loading: false,
superCategories: [],
categories: [],
activeSuperCategoryId: null, // 当前Tab的分类ID
showSuperCategoryDialog: false,
showCategoryDialog: false,
showToolDialog: false,
showConfirmDialog: false,
confirmMessage: '',
confirmCallback: null,
message: {
show: false,
text: '',
type: 'success' // success, error, warning
},
colorOptions: [
{ name: '粉色', value: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' },
{ name: '蓝色', value: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)' },
{ name: '紫色', value: 'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)' },
{ name: '绿色', value: 'linear-gradient(135deg, #96fbc4 0%, #f9f586 100%)' },
{ name: '橙色', value: 'linear-gradient(135deg, #ff9966 0%, #ff5e62 100%)' },
{ name: '红黄', value: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
{ name: '紫蓝', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ name: '青色', value: 'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)' },
{ name: '蓝绿', value: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
{ name: '深紫', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
{ name: '粉蓝', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
{ name: '深绿', value: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
{ name: '淡紫', value: 'linear-gradient(135deg, #fbc2eb 0%, #a6c1ee 100%)' },
{ name: '天蓝', value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
{ name: '淡青', value: 'linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)' },
{ name: '金黄', value: 'linear-gradient(135deg, #f8b500 0%, #fceabb 100%)' },
{ name: '橙红', value: 'linear-gradient(135deg, #e43a15 0%, #e65245 100%)' },
{ name: '绿青', value: 'linear-gradient(135deg, #0ba360 0%, #3cba92 100%)' },
{ name: '翠绿', value: 'linear-gradient(135deg, #38ef7d 0%, #11998e 100%)' },
{ name: '深蓝', value: 'linear-gradient(135deg, #2af598 0%, #009efd 100%)' },
{ name: '粉红', value: 'linear-gradient(135deg, #f77062 0%, #fe5196 100%)' },
{ name: '橙粉', value: 'linear-gradient(135deg, #ee0979 0%, #ff6a00 100%)' },
{ name: '淡粉', value: 'linear-gradient(135deg, #fdcbf1 0%, #e6dee9 100%)' },
{ name: '深青', value: 'linear-gradient(135deg, #a8c0ff 0%, #3f2b96 100%)' },
{ name: '淡黄', value: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' },
{ name: '淡蓝', value: 'linear-gradient(135deg, #accbee 0%, #e7f0fd 100%)' }
],
superCategoryForm: {
id: null,
key: '',
title: '',
sortOrder: 0
},
categoryForm: {
id: null,
key: '',
title: '',
superCategoryId: null,
sortOrder: 0
},
toolForm: {
id: null,
categoryKey: '',
name: '',
desc: '',
url: '',
displayUrl: '',
icon: '',
color: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)',
sortOrder: 0
},
currentCategory: null,
categorySortable: null, // 分组拖拽实例
toolSortables: [], // 网址拖拽实例数组
superCategorySortable: null // 大分类拖拽实例
}
},
computed: {
messageIcon() {
const icons = {
success: '✓',
error: '✕',
warning: '⚠'
}
return icons[this.message.type] || '✓'
},
// 当前激活的分类下的分类列表
currentCategories() {
if (!this.activeSuperCategoryId) return []
return this.categories.filter(c => c.superCategoryId === this.activeSuperCategoryId)
}
},
created() {
this.loadData()
},
mounted() {
// 初始化拖拽功能
this.initSortable()
},
updated() {
// 数据更新后重新初始化拖拽
this.$nextTick(() => {
this.initSortable()
})
},
methods: {
// 消息提示
showMessage(text, type = 'success') {
this.message = { show: true, text, type }
setTimeout(() => {
this.message.show = false
}, 3000)
},
// 确认对话框
showConfirm(message, callback) {
this.confirmMessage = message
this.confirmCallback = callback
this.showConfirmDialog = true
},
confirmAction() {
if (this.confirmCallback) {
this.confirmCallback()
}
this.showConfirmDialog = false
this.confirmCallback = null
},
cancelConfirm() {
this.showConfirmDialog = false
this.confirmCallback = null
},
goBack() {
this.$router.push('/')
},
// 辅助方法
getSuperCategoryName(superCategoryId) {
const superCat = this.superCategories.find(s => s.id === superCategoryId)
return superCat ? superCat.title : '未分类'
},
getCategoryCountInSuper(superCategoryId) {
return this.categories.filter(c => c.superCategoryId === superCategoryId).length
},
// 切换Tab
switchTab(superCategoryId) {
this.activeSuperCategoryId = superCategoryId
},
async loadData() {
try {
this.loading = true
const [superCategoriesRes, categoriesRes, toolsRes] = await Promise.all([
ToolService.getSuperCategories(),
ToolService.getCategories(),
ToolService.getTools()
])
if (superCategoriesRes && superCategoriesRes.code === 200 && categoriesRes && categoriesRes.code === 200 && toolsRes && toolsRes.code === 200) {
// 处理分类数据
const superCategories = (superCategoriesRes.rows || []).map(superCat => ({
id: superCat.id,
key: superCat.superCategoryKey,
title: superCat.superCategoryTitle,
sortOrder: superCat.sortOrder || 0
}))
const categories = (categoriesRes.rows || []).map(cat => ({
id: cat.id,
key: cat.categoryKey,
title: cat.categoryTitle,
superCategoryId: cat.superCategoryId,
sortOrder: cat.sortOrder || 0,
tools: []
}))
const tools = (toolsRes.rows || []).map(tool => ({
id: tool.id,
categoryId: tool.categoryId,
name: tool.name,
desc: tool.description,
url: tool.url,
displayUrl: tool.displayUrl,
icon: tool.icon,
color: tool.color,
sortOrder: tool.sortOrder || 0
}))
tools.forEach(tool => {
const category = categories.find(cat => cat.id === tool.categoryId)
if (category) {
category.tools.push(tool)
}
})
categories.sort((a, b) => a.sortOrder - b.sortOrder)
superCategories.sort((a, b) => a.sortOrder - b.sortOrder)
this.superCategories = superCategories
this.categories = categories
// 默认激活第一个分类
if (superCategories.length > 0 && !this.activeSuperCategoryId) {
this.activeSuperCategoryId = superCategories[0].id
}
}
} catch (error) {
console.error('加载数据异常:', error)
this.showMessage('加载数据失败', 'error')
} finally {
this.loading = false
}
},
// 分类管理
showAddSuperCategoryDialog() {
this.superCategoryForm = {
id: null,
key: '',
title: '',
sortOrder: this.superCategories.length
}
this.showSuperCategoryDialog = true
},
editSuperCategory(superCategory) {
this.superCategoryForm = {
id: superCategory.id,
key: superCategory.key,
title: superCategory.title,
sortOrder: superCategory.sortOrder
}
this.showSuperCategoryDialog = true
},
closeSuperCategoryDialog() {
this.showSuperCategoryDialog = false
},
saveSuperCategory() {
if (!this.superCategoryForm.key) {
this.showMessage('请填写分类标识', 'warning')
return
}
if (!this.superCategoryForm.title) {
this.showMessage('请填写分类名称', 'warning')
return
}
if (this.superCategoryForm.id) {
this.updateSuperCategoryToServer()
} else {
if (this.superCategories.find(c => c.key === this.superCategoryForm.key)) {
this.showMessage('分类标识已存在,请使用其他标识', 'warning')
return
}
this.addSuperCategoryToServer()
}
},
async addSuperCategoryToServer() {
try {
const data = {
superCategoryKey: this.superCategoryForm.key,
superCategoryTitle: this.superCategoryForm.title,
sortOrder: this.superCategoryForm.sortOrder
}
const response = await ToolService.createSuperCategory(data)
if (response && response.code === 200) {
this.showMessage('添加成功')
this.closeSuperCategoryDialog()
this.loadData()
} else {
this.showMessage('添加失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('添加分类失败:', error)
this.showMessage('添加失败,请检查网络连接', 'error')
}
},
async updateSuperCategoryToServer() {
try {
const response = await ToolService.updateSuperCategory({
id: this.superCategoryForm.id,
superCategoryKey: this.superCategoryForm.key,
superCategoryTitle: this.superCategoryForm.title,
sortOrder: this.superCategoryForm.sortOrder
})
if (response && response.code === 200) {
this.showMessage('修改成功')
this.closeSuperCategoryDialog()
this.loadData()
} else {
this.showMessage('修改失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('修改分类失败:', error)
this.showMessage('修改失败,请检查网络连接', 'error')
}
},
deleteSuperCategory(superCategory) {
const categoryCount = this.getCategoryCountInSuper(superCategory.id)
const message = categoryCount > 0
? `该分类下还有 ${categoryCount} 个分组,确定要删除“${superCategory.title}”吗?`
: `确定要删除分类“${superCategory.title}”吗?`
this.showConfirm(message, () => {
this.deleteSuperCategoryFromServer(superCategory.id)
})
},
async deleteSuperCategoryFromServer(id) {
try {
const response = await ToolService.deleteSuperCategory([id])
if (response && response.code === 200) {
this.showMessage('删除成功')
this.loadData()
} else {
this.showMessage('删除失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('删除分类失败:', error)
this.showMessage('删除失败,请检查网络连接', 'error')
}
},
// 分类管理
showAddCategoryDialog() {
if (!this.activeSuperCategoryId) {
this.showMessage('请先选择一个分类', 'warning')
return
}
this.categoryForm = {
id: null,
key: '',
title: '',
superCategoryId: this.activeSuperCategoryId,
sortOrder: this.currentCategories.length
}
this.showCategoryDialog = true
},
editCategory(category) {
this.categoryForm = {
id: category.id,
key: category.key,
title: category.title,
superCategoryId: category.superCategoryId,
sortOrder: category.sortOrder
}
this.showCategoryDialog = true
},
closeCategoryDialog() {
this.showCategoryDialog = false
},
saveCategory() {
if (!this.categoryForm.key) {
this.showMessage('请填写分类标识', 'warning')
return
}
if (!this.categoryForm.title) {
this.showMessage('请填写分类名称', 'warning')
return
}
if (this.categoryForm.id) {
this.updateCategoryToServer()
} else {
if (this.categories.find(c => c.key === this.categoryForm.key)) {
this.showMessage('分类标识已存在,请使用其他标识', 'warning')
return
}
this.addCategoryToServer()
}
},
async addCategoryToServer() {
try {
const data = {
categoryKey: this.categoryForm.key,
categoryTitle: this.categoryForm.title,
superCategoryId: this.categoryForm.superCategoryId,
sortOrder: this.categoryForm.sortOrder
}
const response = await ToolService.createCategory(data)
if (response && response.code === 200) {
this.showMessage('添加成功')
this.closeCategoryDialog()
this.loadData()
} else {
this.showMessage('添加失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('添加分类失败:', error)
this.showMessage('添加失败,请检查网络连接', 'error')
}
},
async updateCategoryToServer() {
try {
const response = await ToolService.updateCategory({
id: this.categoryForm.id,
categoryKey: this.categoryForm.key,
categoryTitle: this.categoryForm.title,
superCategoryId: this.categoryForm.superCategoryId,
sortOrder: this.categoryForm.sortOrder
})
if (response && response.code === 200) {
this.showMessage('修改成功')
this.closeCategoryDialog()
this.loadData()
} else {
this.showMessage('修改失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('修改分类失败:', error)
this.showMessage('修改失败,请检查网络连接', 'error')
}
},
deleteCategory(category) {
const message = category.tools && category.tools.length > 0
? `该分类下还有 ${category.tools.length} 个网址,确定要删除"${category.title}"吗?`
: `确定要删除分类"${category.title}"吗?`
this.showConfirm(message, () => {
this.deleteCategoryFromServer(category.id)
})
},
async deleteCategoryFromServer(id) {
try {
const response = await ToolService.deleteCategory([id])
if (response && response.code === 200) {
this.showMessage('删除成功')
this.loadData()
} else {
this.showMessage('删除失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('删除分类失败:', error)
this.showMessage('删除失败,请检查网络连接', 'error')
}
},
// 工具管理
showAddToolDialog(category) {
this.currentCategory = category
this.toolForm = {
id: null,
categoryKey: category.key,
name: '',
desc: '',
url: '',
displayUrl: '',
icon: '',
color: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)',
sortOrder: category.tools.length
}
this.showToolDialog = true
},
editTool(category, tool) {
this.currentCategory = category
this.toolForm = {
id: tool.id,
categoryKey: category.key,
name: tool.name,
desc: tool.desc,
url: tool.url,
displayUrl: tool.displayUrl,
icon: tool.icon,
color: tool.color,
sortOrder: tool.sortOrder || 0
}
this.showToolDialog = true
},
closeToolDialog() {
this.showToolDialog = false
this.currentCategory = null
},
saveTool() {
if (!this.toolForm.name) {
this.showMessage('请填写网址名称', 'warning')
return
}
if (!this.toolForm.url) {
this.showMessage('请填写访问地址', 'warning')
return
}
// 验证URL格式
if (!this.isValidUrl(this.toolForm.url)) {
this.showMessage('请输入完整的URL地址必须以http://或https://开头', 'warning')
return
}
if (this.toolForm.id) {
this.updateToolToServer()
} else {
if (this.currentCategory.tools.find(t => t.name === this.toolForm.name)) {
this.showMessage('该分类下已存在同名网址,请使用其他名称', 'warning')
return
}
this.addToolToServer()
}
},
// 验证URL格式
isValidUrl(url) {
if (!url) return false
return url.startsWith('http://') || url.startsWith('https://')
},
async addToolToServer() {
try {
const response = await ToolService.createTool({
categoryId: this.currentCategory.id,
name: this.toolForm.name,
description: this.toolForm.desc,
url: this.toolForm.url,
displayUrl: this.toolForm.displayUrl,
icon: this.toolForm.icon,
color: this.toolForm.color,
sortOrder: this.toolForm.sortOrder
})
if (response && response.code === 200) {
this.showMessage('添加成功')
this.closeToolDialog()
this.loadData()
} else {
this.showMessage('添加失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('添加工具失败:', error)
this.showMessage('添加失败,请检查网络连接', 'error')
}
},
async updateToolToServer() {
try {
const response = await ToolService.updateTool({
id: this.toolForm.id,
categoryId: this.currentCategory.id,
name: this.toolForm.name,
description: this.toolForm.desc,
url: this.toolForm.url,
displayUrl: this.toolForm.displayUrl,
icon: this.toolForm.icon,
color: this.toolForm.color,
sortOrder: this.toolForm.sortOrder
})
if (response && response.code === 200) {
this.showMessage('修改成功')
this.closeToolDialog()
this.loadData()
} else {
this.showMessage('修改失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('修改工具失败:', error)
this.showMessage('修改失败,请检查网络连接', 'error')
}
},
deleteTool(category, tool) {
this.showConfirm(`确定要删除网址"${tool.name}"吗?`, () => {
this.deleteToolFromServer(tool.id)
})
},
async deleteToolFromServer(id) {
try {
const response = await ToolService.deleteTool([id])
if (response && response.code === 200) {
this.showMessage('删除成功')
this.loadData()
} else {
this.showMessage('删除失败:' + (response.msg || '未知错误'), 'error')
}
} catch (error) {
console.error('删除工具失败:', error)
this.showMessage('删除失败,请检查网络连接', 'error')
}
},
// 初始化拖拽排序
initSortable() {
// 销毁旧的 Sortable 实例
if (this.superCategorySortable) {
this.superCategorySortable.destroy()
}
if (this.categorySortable) {
this.categorySortable.destroy()
}
if (this.toolSortables) {
this.toolSortables.forEach(s => s.destroy())
}
this.toolSortables = []
// 大分类 Tab 拖拽
const tabListEl = document.querySelector('.tab-list')
if (tabListEl) {
this.superCategorySortable = Sortable.create(tabListEl, {
animation: 150,
handle: '.tab-item',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: (evt) => {
this.handleSuperCategorySort(evt)
}
})
}
// 书签分组拖拽
const categoryListEl = document.querySelector('.category-list')
if (categoryListEl) {
this.categorySortable = Sortable.create(categoryListEl, {
animation: 150,
handle: '.category-item',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: (evt) => {
this.handleCategorySort(evt)
}
})
}
// 网址列表拖拽(每个分组都需要初始化)
const toolLists = document.querySelectorAll('.tool-list')
toolLists.forEach((toolListEl, index) => {
const sortable = Sortable.create(toolListEl, {
animation: 150,
handle: '.tool-item',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd: (evt) => {
this.handleToolSort(evt, index)
}
})
this.toolSortables.push(sortable)
})
},
// 处理大分类排序
async handleSuperCategorySort(evt) {
const { oldIndex, newIndex } = evt
if (oldIndex === newIndex) return
const superCategories = [...this.superCategories]
const movedItem = superCategories[oldIndex]
superCategories.splice(oldIndex, 1)
superCategories.splice(newIndex, 0, movedItem)
// 更新排序号
const updates = superCategories.map((cat, index) => ({
id: cat.id,
sortOrder: index
}))
try {
// 批量更新排序
for (const update of updates) {
await ToolService.updateSuperCategory({
id: update.id,
superCategoryKey: superCategories.find(c => c.id === update.id).key,
superCategoryTitle: superCategories.find(c => c.id === update.id).title,
sortOrder: update.sortOrder
})
}
this.showMessage('排序保存成功')
await this.loadData()
} catch (error) {
console.error('排序失败:', error)
this.showMessage('排序保存失败', 'error')
await this.loadData()
}
},
// 处理书签分组排序
async handleCategorySort(evt) {
const { oldIndex, newIndex } = evt
if (oldIndex === newIndex) return
const categories = [...this.currentCategories]
const movedItem = categories[oldIndex]
categories.splice(oldIndex, 1)
categories.splice(newIndex, 0, movedItem)
// 更新排序号
const updates = categories.map((cat, index) => ({
id: cat.id,
sortOrder: index
}))
try {
// 批量更新排序
for (const update of updates) {
await ToolService.updateCategory({
id: update.id,
categoryKey: categories.find(c => c.id === update.id).key,
categoryTitle: categories.find(c => c.id === update.id).title,
superCategoryId: categories.find(c => c.id === update.id).superCategoryId,
sortOrder: update.sortOrder
})
}
this.showMessage('排序保存成功')
await this.loadData()
} catch (error) {
console.error('排序失败:', error)
this.showMessage('排序保存失败', 'error')
await this.loadData()
}
},
// 处理网址排序
async handleToolSort(evt, categoryIndex) {
const { oldIndex, newIndex } = evt
if (oldIndex === newIndex) return
const category = this.currentCategories[categoryIndex]
const tools = [...category.tools]
const movedItem = tools[oldIndex]
tools.splice(oldIndex, 1)
tools.splice(newIndex, 0, movedItem)
// 更新排序号
const updates = tools.map((tool, index) => ({
...tool,
sortOrder: index
}))
try {
// 批量更新排序
for (const tool of updates) {
await ToolService.updateTool({
id: tool.id,
categoryId: category.id,
name: tool.name,
description: tool.desc,
url: tool.url,
displayUrl: tool.displayUrl,
icon: tool.icon,
color: tool.color,
sortOrder: tool.sortOrder
})
}
this.showMessage('排序保存成功')
await this.loadData()
} catch (error) {
console.error('排序失败:', error)
this.showMessage('排序保存失败', 'error')
await this.loadData()
}
}
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@text-primary: #333;
@text-secondary: #666;
@text-tertiary: #999;
@border-color: #dcdfe6;
@white-color: #fff;
.manage-page {
min-height: 100vh;
background: @primary-gradient;
padding-top: 64px; // 为固定头部预留空间
}
.manage-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: @primary-gradient;
padding: 20px 40px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: center; // PC端居中
align-items: center;
z-index: 1000;
h1 {
margin: 0;
font-size: 36px; // 与HomePage保持一致
color: @white-color;
font-weight: 800; // 与HomePage保持一致
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
letter-spacing: 2px; // 添加字母间距
}
.back-btn {
position: absolute; // 绝对定位避免遮挡标题
right: 40px;
padding: 12px 28px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 12px;
color: white;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
// 返回图标
&::before {
content: '◄';
margin-right: 8px;
font-size: 14px;
display: inline-block;
transition: transform 0.3s ease;
}
&:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.5);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
&::before {
transform: translateX(-4px);
}
}
&:active {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
}
}
.manage-container {
padding: 10px 8px;
padding-top: 180px; // 减小与固定区域的间距
}
// 固定的书签分类区域
.fixed-section {
position: fixed;
top:70px;
left: 0;
right: 0;
z-index: 999;
margin: 0;
border-radius: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 28px 20px 20px 20px !important; // 顶部增加更多内边距
}
.manage-section {
background: white;
border-radius: 12px;
padding: 10px 12px;
margin-bottom: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
h2 {
margin: 0;
font-size: 20px;
color: @text-primary;
flex: 1; // 让标题占据剩余空间
min-width: 0; // 允许文本截断
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
flex-shrink: 0; // 防止按钮被压缩
}
}
}
// Tab 标签样式
.tab-container {
margin-top: 10px;
.tab-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 0;
.tab-item {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 10px;
background: #f5f7fa;
border: 2px solid transparent;
border-radius: 8px;
cursor: move; // 显示可拖拽
transition: all 0.3s ease;
user-select: none;
.tab-title {
font-size: 15px;
font-weight: 600;
color: @text-secondary;
transition: color 0.3s ease;
}
.tab-sort {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 8px;
background: rgba(102, 126, 234, 0.1);
color: @primary-color;
font-size: 13px;
font-weight: 700;
border-radius: 6px;
transition: all 0.3s ease;
}
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
background: #e5e7eb;
color: @text-secondary;
font-size: 12px;
font-weight: 600;
border-radius: 12px;
transition: background 0.3s ease;
}
.tab-actions {
display: none;
gap: 4px;
margin-left: 8px;
button {
width: 24px;
height: 24px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
color: @text-tertiary;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&.tab-edit-btn:hover {
background: #409eff;
color: white;
}
&.tab-delete-btn:hover {
background: #f56c6c;
color: white;
}
}
}
&:hover {
background: #e5e7eb;
.tab-actions {
display: flex;
}
}
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
.tab-title {
color: white;
}
.tab-sort {
background: rgba(255, 255, 255, 0.3);
color: white;
}
.tab-count {
background: rgba(255, 255, 255, 0.25);
color: white;
}
.tab-actions {
display: flex;
button {
color: white;
&.tab-edit-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
&.tab-delete-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
}
}
}
}
// 统一的添加按钮样式
.add-btn {
padding: 10px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
&:active {
transform: translateY(0);
}
&.small {
padding: 6px 16px;
font-size: 13px;
}
}
.category-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 10px;
.category-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: #f9fafb;
border-radius: 6px;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: move; // 显示可拖拽
&:hover {
background: #f3f4f6;
border-color: @primary-color;
}
.category-info {
display: flex;
align-items: center;
gap: 8px;
.category-key {
padding: 3px 10px;
background: @primary-color;
color: white;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.category-name {
font-size: 14px;
color: @text-primary;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.category-super {
font-size: 12px;
color: #67c23a;
background: rgba(103, 194, 58, 0.1);
padding: 2px 8px;
border-radius: 4px;
white-space: nowrap;
font-weight: 500;
}
.category-sort {
font-size: 12px;
color: @primary-color;
background: rgba(102, 126, 234, 0.1);
padding: 2px 8px;
border-radius: 4px;
white-space: nowrap;
font-weight: 500;
}
}
.category-footer {
display: flex;
justify-content: space-between;
align-items: center;
.category-count {
font-size: 12px;
color: @text-tertiary;
white-space: nowrap;
}
.category-actions {
display: flex;
gap: 6px;
}
}
}
}
.tool-group {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.tool-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 12px 16px;
background: #f9fafb;
border-radius: 6px;
h3 {
margin: 0;
font-size: 16px;
color: @text-primary;
}
}
.tool-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
.tool-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: move; // 显示可拖拽
&:hover {
background: #f3f4f6;
border-color: @primary-color;
}
.tool-main {
display: flex;
align-items: center;
gap: 10px;
.tool-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: white;
font-weight: bold;
flex-shrink: 0;
}
.tool-info {
flex: 1;
min-width: 0;
.tool-name {
font-size: 13px;
font-weight: 500;
color: @text-primary;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-desc {
font-size: 11px;
color: @text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.tool-sort {
font-size: 11px;
color: @primary-color;
background: rgba(102, 126, 234, 0.1);
padding: 2px 8px;
border-radius: 4px;
white-space: nowrap;
font-weight: 500;
flex-shrink: 0;
}
}
.tool-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
.tool-url {
font-size: 10px;
color: @text-tertiary;
font-family: 'Consolas', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.tool-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
}
}
}
}
button {
&.edit-btn {
padding: 5px 10px;
background: #409eff;
border: none;
border-radius: 4px;
color: white;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #66b1ff;
}
}
&.delete-btn {
padding: 5px 10px;
background: #f56c6c;
border: none;
border-radius: 4px;
color: white;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #f78989;
}
}
}
// 对话框样式
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
.dialog {
background: white;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 500px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
h3 {
margin: 0 0 20px;
font-size: 20px;
color: @text-primary;
}
.form-group {
margin-bottom: 16px;
label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: @text-secondary;
font-weight: 500;
.required {
color: #f56c6c;
margin-right: 4px;
}
}
input, select {
width: 100%;
padding: 10px 12px;
border: 1px solid @border-color;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s ease;
&:focus {
outline: none;
border-color: @primary-color;
}
&:disabled {
background: #f5f7fa;
color: #999;
cursor: not-allowed;
}
}
select {
cursor: pointer;
}
}
// 颜色选择器样式
.color-picker {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
padding: 0 2px;
.color-option {
width: 100%;
aspect-ratio: 1;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 3px solid transparent;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
&.active {
border-color: @primary-color;
box-shadow: 0 0 0 2px white, 0 0 0 4px @primary-color;
}
.check-icon {
color: white;
font-size: 20px;
font-weight: bold;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
button {
padding: 10px 24px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.cancel-btn {
background: white;
border: 1px solid @border-color;
color: @text-primary;
&:hover {
color: @primary-color;
border-color: @primary-color;
}
}
.confirm-btn {
background: @primary-color;
border: none;
color: white;
&:hover {
background: #5568d3;
}
}
}
}
}
// 确认对话框样式
.confirm-dialog {
background: white;
border-radius: 12px;
padding: 32px 24px 24px;
width: 90%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
.confirm-icon {
font-size: 48px;
margin-bottom: 16px;
&.warning {
color: #e6a23c;
}
}
.confirm-message {
font-size: 16px;
color: @text-primary;
margin-bottom: 24px;
line-height: 1.6;
}
.confirm-actions {
display: flex;
gap: 12px;
justify-content: center;
button {
padding: 10px 32px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
}
.cancel-btn {
background: white;
border: 1px solid @border-color;
color: @text-primary;
&:hover {
color: @primary-color;
border-color: @primary-color;
}
}
.confirm-btn {
background: @primary-color;
border: none;
color: white;
&:hover {
background: #5568d3;
}
&.danger {
background: #f56c6c;
&:hover {
background: #f78989;
}
}
}
}
}
// 消息提示样式
.message-toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
padding: 14px 24px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
z-index: 3000;
min-width: 300px;
justify-content: center;
.message-icon {
font-size: 18px;
font-weight: bold;
}
&.success {
background: #f0f9ff;
border: 1px solid #67c23a;
color: #67c23a;
.message-icon {
color: #67c23a;
}
}
&.error {
background: #fef0f0;
border: 1px solid #f56c6c;
color: #f56c6c;
.message-icon {
color: #f56c6c;
}
}
&.warning {
background: #fdf6ec;
border: 1px solid #e6a23c;
color: #e6a23c;
.message-icon {
color: #e6a23c;
}
}
}
// 消息提示动画
.message-fade-enter-active,
.message-fade-leave-active {
transition: all 0.3s ease;
}
.message-fade-enter {
opacity: 0;
transform: translate(-50%, -20px);
}
.message-fade-leave-to {
opacity: 0;
transform: translate(-50%, -20px);
}
// 拖拽样式
.sortable-ghost {
opacity: 0.4;
background: #e0e7ff !important;
border: 2px dashed @primary-color !important;
}
.sortable-chosen {
opacity: 0.8;
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sortable-drag {
opacity: 0.8;
transform: rotate(2deg);
}
// 响应式布局 - 手机端
@media (max-width: 600px) {
.manage-header {
justify-content: flex-start; // 手机端左对齐
padding: 20px;
h1 {
font-size: 24px; // 手机端与HomePage保持一致
}
.back-btn {
right: 20px;
padding: 10px 20px;
font-size: 14px;
&::before {
font-size: 12px;
}
}
}
.manage-container {
padding: 16px 8px;
padding-top: 200px; // 手机端为固定区域预留更多空间
}
.fixed-section {
padding: 12px 8px;
}
.category-list {
grid-template-columns: 1fr !important;
}
.tool-list {
grid-template-columns: 1fr !important;
}
}
</style>