1898 lines
51 KiB
Vue
1898 lines
51 KiB
Vue
<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>
|