2026-02-15 10:10:28 +08:00
|
|
|
|
<template>
|
2025-12-27 21:15:26 +08:00
|
|
|
|
<div class="page-container-flex">
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<van-nav-bar
|
|
|
|
|
|
:title="navTitle"
|
2025-12-26 15:21:31 +08:00
|
|
|
|
left-text="返回"
|
|
|
|
|
|
left-arrow
|
2026-01-16 11:15:44 +08:00
|
|
|
|
placeholder
|
2026-01-07 14:33:30 +08:00
|
|
|
|
@click-left="handleBack"
|
2025-12-26 15:21:31 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
2025-12-27 21:15:26 +08:00
|
|
|
|
<div class="scroll-content">
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<!-- 第一层:选择交易类型 -->
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentLevel === 0"
|
|
|
|
|
|
class="level-container"
|
|
|
|
|
|
>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<van-cell-group inset>
|
|
|
|
|
|
<van-cell
|
|
|
|
|
|
v-for="type in typeOptions"
|
|
|
|
|
|
:key="type.value"
|
|
|
|
|
|
:title="type.label"
|
|
|
|
|
|
is-link
|
|
|
|
|
|
@click="handleSelectType(type.value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</van-cell-group>
|
|
|
|
|
|
</div>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<!-- 第二层:分类列表 -->
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="level-container"
|
|
|
|
|
|
>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<!-- 面包屑导航 -->
|
|
|
|
|
|
<div class="breadcrumb">
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<van-tag
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
closeable
|
|
|
|
|
|
style="margin-left: 16px"
|
|
|
|
|
|
@close="handleBackToRoot"
|
|
|
|
|
|
>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
{{ currentTypeName }}
|
|
|
|
|
|
</van-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分类列表 -->
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<van-empty
|
|
|
|
|
|
v-if="categories.length === 0"
|
|
|
|
|
|
description="暂无分类"
|
|
|
|
|
|
/>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<van-cell-group
|
|
|
|
|
|
v-else
|
|
|
|
|
|
inset
|
|
|
|
|
|
>
|
|
|
|
|
|
<van-swipe-cell
|
|
|
|
|
|
v-for="category in categories"
|
|
|
|
|
|
:key="category.id"
|
|
|
|
|
|
>
|
2026-02-02 11:07:34 +08:00
|
|
|
|
<van-cell :title="category.name">
|
|
|
|
|
|
<template #icon>
|
2026-02-16 21:55:38 +08:00
|
|
|
|
<Icon
|
2026-02-02 11:07:34 +08:00
|
|
|
|
v-if="category.icon"
|
2026-02-16 21:55:38 +08:00
|
|
|
|
:icon-identifier="category.icon"
|
|
|
|
|
|
:size="20"
|
2026-02-02 11:07:34 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #default>
|
|
|
|
|
|
<div class="category-actions">
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
plain
|
|
|
|
|
|
@click="handleIconSelect(category)"
|
|
|
|
|
|
>
|
|
|
|
|
|
选择图标
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
size="small"
|
2026-02-16 21:55:38 +08:00
|
|
|
|
@click="handleEdit(category)"
|
2026-02-02 11:07:34 +08:00
|
|
|
|
>
|
|
|
|
|
|
编辑
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</van-cell>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<template #right>
|
2026-01-27 15:29:25 +08:00
|
|
|
|
<van-button
|
|
|
|
|
|
square
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
text="删除"
|
|
|
|
|
|
@click="handleDelete(category)"
|
|
|
|
|
|
/>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</van-swipe-cell>
|
|
|
|
|
|
</van-cell-group>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<!-- 底部安全距离 -->
|
|
|
|
|
|
<div style="height: calc(55px + env(safe-area-inset-bottom, 0px))" />
|
2026-02-16 21:55:38 +08:00
|
|
|
|
</div>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
<!-- 新增分类按钮 -->
|
|
|
|
|
|
<div class="bottom-button">
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon="plus"
|
|
|
|
|
|
@click="handleAddCategory"
|
2026-02-15 10:10:28 +08:00
|
|
|
|
>
|
2026-02-16 21:55:38 +08:00
|
|
|
|
新增分类
|
|
|
|
|
|
</van-button>
|
2025-12-27 21:15:26 +08:00
|
|
|
|
</div>
|
2026-02-16 21:55:38 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 新增分类对话框 -->
|
|
|
|
|
|
<PopupContainer
|
|
|
|
|
|
v-model:show="showAddDialog"
|
|
|
|
|
|
title="新增分类"
|
|
|
|
|
|
show-cancel-button
|
|
|
|
|
|
show-confirm-button
|
|
|
|
|
|
confirm-text="确认"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
@confirm="handleConfirmAdd"
|
|
|
|
|
|
@cancel="resetAddForm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<van-form ref="addFormRef">
|
|
|
|
|
|
<van-field
|
|
|
|
|
|
v-model="addForm.name"
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
label="分类名称"
|
|
|
|
|
|
placeholder="请输入分类名称"
|
|
|
|
|
|
:rules="[{ required: true, message: '请输入分类名称' }]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</van-form>
|
|
|
|
|
|
</PopupContainer>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 编辑分类对话框 -->
|
|
|
|
|
|
<PopupContainer
|
|
|
|
|
|
v-model:show="showEditDialog"
|
|
|
|
|
|
title="编辑分类"
|
|
|
|
|
|
show-cancel-button
|
|
|
|
|
|
show-confirm-button
|
|
|
|
|
|
confirm-text="保存"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
@confirm="handleConfirmEdit"
|
|
|
|
|
|
@cancel="showEditDialog = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<van-form ref="editFormRef">
|
|
|
|
|
|
<van-field
|
|
|
|
|
|
v-model="editForm.name"
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
label="分类名称"
|
|
|
|
|
|
placeholder="请输入分类名称"
|
|
|
|
|
|
:rules="[{ required: true, message: '请输入分类名称' }]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</van-form>
|
|
|
|
|
|
</PopupContainer>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 删除确认对话框 -->
|
|
|
|
|
|
<PopupContainer
|
|
|
|
|
|
v-model:show="showDeleteConfirm"
|
|
|
|
|
|
title="删除分类"
|
|
|
|
|
|
show-cancel-button
|
|
|
|
|
|
show-confirm-button
|
|
|
|
|
|
confirm-text="确定"
|
|
|
|
|
|
cancel-text="取消"
|
|
|
|
|
|
@confirm="handleConfirmDelete"
|
|
|
|
|
|
@cancel="showDeleteConfirm = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
|
|
|
|
|
|
删除后无法恢复,确定要删除吗?
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</PopupContainer>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 图标选择对话框 -->
|
|
|
|
|
|
<IconSelector
|
|
|
|
|
|
v-model:show="showIconDialog"
|
|
|
|
|
|
:icons="iconCandidates"
|
|
|
|
|
|
:title="`为「${currentCategory?.name || ''}」选择图标`"
|
|
|
|
|
|
:default-icon-identifier="currentCategory?.icon || ''"
|
|
|
|
|
|
@confirm="handleConfirmIconSelect"
|
|
|
|
|
|
@cancel="handleCancelIconSelect"
|
|
|
|
|
|
/>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-02-16 21:55:38 +08:00
|
|
|
|
import { ref, computed } from 'vue'
|
2025-12-26 15:21:31 +08:00
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-01-16 11:15:44 +08:00
|
|
|
|
import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant'
|
2026-02-16 21:55:38 +08:00
|
|
|
|
import Icon from '@/components/Icon.vue'
|
|
|
|
|
|
import IconSelector from '@/components/IconSelector.vue'
|
|
|
|
|
|
import PopupContainer from '@/components/PopupContainer.vue'
|
2025-12-26 15:21:31 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getCategoryList,
|
|
|
|
|
|
createCategory,
|
2026-01-01 15:20:59 +08:00
|
|
|
|
deleteCategory,
|
2026-02-16 21:55:38 +08:00
|
|
|
|
updateCategory
|
2025-12-26 15:21:31 +08:00
|
|
|
|
} from '@/api/transactionCategory'
|
2026-02-16 21:55:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
generateSearchKeywords,
|
|
|
|
|
|
searchIcons,
|
|
|
|
|
|
updateCategoryIcon as updateCategoryIconApi
|
|
|
|
|
|
} from '@/api/icons'
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
|
|
// 交易类型选项
|
|
|
|
|
|
const typeOptions = [
|
|
|
|
|
|
{ value: 0, label: '支出' },
|
|
|
|
|
|
{ value: 1, label: '收入' },
|
|
|
|
|
|
{ value: 2, label: '不计收支' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// 层级状态
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const currentLevel = ref(0) // 0=类型选择, 1=分类管理
|
2025-12-26 15:21:31 +08:00
|
|
|
|
const currentType = ref(null) // 当前选中的交易类型
|
|
|
|
|
|
const currentTypeName = computed(() => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
const type = typeOptions.find((t) => t.value === currentType.value)
|
2025-12-26 15:21:31 +08:00
|
|
|
|
return type ? type.label : ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 分类数据
|
|
|
|
|
|
const categories = ref([])
|
|
|
|
|
|
// 编辑对话框
|
|
|
|
|
|
const showAddDialog = ref(false)
|
|
|
|
|
|
const addFormRef = ref(null)
|
|
|
|
|
|
const addForm = ref({
|
|
|
|
|
|
name: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 删除确认
|
|
|
|
|
|
const showDeleteConfirm = ref(false)
|
|
|
|
|
|
const deleteTarget = ref(null)
|
|
|
|
|
|
|
2026-01-01 15:20:59 +08:00
|
|
|
|
// 编辑对话框
|
|
|
|
|
|
const showEditDialog = ref(false)
|
|
|
|
|
|
const editFormRef = ref(null)
|
|
|
|
|
|
const editForm = ref({
|
|
|
|
|
|
id: 0,
|
|
|
|
|
|
name: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-02 11:07:34 +08:00
|
|
|
|
// 图标选择对话框
|
|
|
|
|
|
const showIconDialog = ref(false)
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const currentCategory = ref(null)
|
|
|
|
|
|
const iconCandidates = ref([])
|
|
|
|
|
|
const isLoadingIcons = ref(false)
|
2026-02-15 10:10:28 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
// 计算导航栏标题
|
|
|
|
|
|
const navTitle = computed(() => {
|
|
|
|
|
|
if (currentLevel.value === 0) {
|
|
|
|
|
|
return '编辑分类'
|
|
|
|
|
|
}
|
|
|
|
|
|
return currentTypeName.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选择交易类型,进入分类管理
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleSelectType = async (type) => {
|
|
|
|
|
|
currentType.value = type
|
|
|
|
|
|
currentLevel.value = 1
|
|
|
|
|
|
await loadCategories()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载分类列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
const loadCategories = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
showLoadingToast({
|
|
|
|
|
|
message: '加载中...',
|
|
|
|
|
|
forbidClick: true,
|
|
|
|
|
|
duration: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { success, data } = await getCategoryList(currentType.value)
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
categories.value = data || []
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast('加载分类失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载分类错误:', error)
|
|
|
|
|
|
showToast('加载分类失败: ' + (error.message || '未知错误'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
closeToast()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 返回上一级
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
|
if (currentLevel.value === 1) {
|
|
|
|
|
|
currentLevel.value = 0
|
|
|
|
|
|
currentType.value = null
|
|
|
|
|
|
categories.value = []
|
|
|
|
|
|
} else {
|
2026-01-20 19:56:29 +08:00
|
|
|
|
if (window.history.length > 1) {
|
|
|
|
|
|
router.back()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.replace('/')
|
|
|
|
|
|
}
|
2025-12-26 15:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 返回到根目录(类型选择)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleBackToRoot = () => {
|
|
|
|
|
|
currentLevel.value = 0
|
|
|
|
|
|
currentType.value = null
|
|
|
|
|
|
categories.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新增分类
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleAddCategory = () => {
|
|
|
|
|
|
addForm.value = {
|
|
|
|
|
|
name: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
showAddDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 确认新增
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleConfirmAdd = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await addFormRef.value?.validate()
|
|
|
|
|
|
|
|
|
|
|
|
showLoadingToast({
|
|
|
|
|
|
message: '创建中...',
|
|
|
|
|
|
forbidClick: true,
|
|
|
|
|
|
duration: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { success, message } = await createCategory({
|
|
|
|
|
|
name: addForm.value.name,
|
|
|
|
|
|
type: currentType.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccessToast('创建成功')
|
|
|
|
|
|
showAddDialog.value = false
|
|
|
|
|
|
resetAddForm()
|
|
|
|
|
|
await loadCategories()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(message || '创建失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建失败:', error)
|
|
|
|
|
|
showToast('创建失败: ' + (error.message || '未知错误'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
closeToast()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 15:20:59 +08:00
|
|
|
|
/**
|
2026-02-16 21:55:38 +08:00
|
|
|
|
* 重置新增表单
|
2026-01-01 15:20:59 +08:00
|
|
|
|
*/
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const resetAddForm = () => {
|
|
|
|
|
|
addForm.value = {
|
|
|
|
|
|
name: ''
|
2026-01-01 15:20:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 11:07:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 打开图标选择器
|
|
|
|
|
|
*/
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const handleIconSelect = async (category) => {
|
2026-02-02 11:07:34 +08:00
|
|
|
|
currentCategory.value = category
|
|
|
|
|
|
showIconDialog.value = true
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-16 21:55:38 +08:00
|
|
|
|
isLoadingIcons.value = true
|
2026-02-02 11:07:34 +08:00
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const { success: keywordsSuccess, data: keywordsResponse } = await generateSearchKeywords(category.name)
|
2026-02-02 11:07:34 +08:00
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
if (!keywordsSuccess || !keywordsResponse || !keywordsResponse.keywords || keywordsResponse.keywords.length === 0) {
|
|
|
|
|
|
showToast('生成搜索关键字失败')
|
|
|
|
|
|
return
|
2026-02-02 11:07:34 +08:00
|
|
|
|
}
|
2026-02-16 21:55:38 +08:00
|
|
|
|
|
|
|
|
|
|
const { success: iconsSuccess, data: icons } = await searchIcons(keywordsResponse.keywords)
|
|
|
|
|
|
|
|
|
|
|
|
console.log('图标搜索响应:', { iconsSuccess, icons, iconsType: typeof icons, iconsIsArray: Array.isArray(icons) })
|
|
|
|
|
|
|
|
|
|
|
|
if (!iconsSuccess) {
|
|
|
|
|
|
showToast('搜索图标失败')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!icons || icons.length === 0) {
|
|
|
|
|
|
console.warn('图标数据为空')
|
|
|
|
|
|
showToast('未找到匹配的图标')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
iconCandidates.value = icons
|
2026-02-02 11:07:34 +08:00
|
|
|
|
} catch (error) {
|
2026-02-16 21:55:38 +08:00
|
|
|
|
console.error('搜索图标错误:', error)
|
|
|
|
|
|
showToast('搜索图标失败')
|
|
|
|
|
|
isLoadingIcons.value = false
|
2026-02-02 11:07:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 确认选择图标
|
|
|
|
|
|
*/
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const handleConfirmIconSelect = async (iconIdentifier) => {
|
2026-02-15 10:10:28 +08:00
|
|
|
|
if (!currentCategory.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-02 11:07:34 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
showLoadingToast({
|
|
|
|
|
|
message: '保存中...',
|
|
|
|
|
|
forbidClick: true,
|
|
|
|
|
|
duration: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const { success, message } = await updateCategoryIconApi(
|
2026-02-02 11:07:34 +08:00
|
|
|
|
currentCategory.value.id,
|
2026-02-16 21:55:38 +08:00
|
|
|
|
iconIdentifier
|
2026-02-02 11:07:34 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccessToast('图标保存成功')
|
|
|
|
|
|
showIconDialog.value = false
|
2026-02-16 21:55:38 +08:00
|
|
|
|
currentCategory.value = null
|
|
|
|
|
|
iconCandidates.value = []
|
2026-02-02 11:07:34 +08:00
|
|
|
|
await loadCategories()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(message || '保存失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存图标失败:', error)
|
2026-02-16 21:55:38 +08:00
|
|
|
|
showToast('保存图标失败')
|
2026-02-02 11:07:34 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
closeToast()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 10:10:28 +08:00
|
|
|
|
/**
|
2026-02-16 21:55:38 +08:00
|
|
|
|
* 取消图标选择
|
2026-02-15 10:10:28 +08:00
|
|
|
|
*/
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const handleCancelIconSelect = () => {
|
|
|
|
|
|
showIconDialog.value = false
|
|
|
|
|
|
currentCategory.value = null
|
|
|
|
|
|
iconCandidates.value = []
|
2026-02-15 10:10:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 11:07:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 编辑分类
|
|
|
|
|
|
*/
|
2026-02-16 21:55:38 +08:00
|
|
|
|
const handleEdit = (category) => {
|
2026-02-02 11:07:34 +08:00
|
|
|
|
editForm.value = {
|
|
|
|
|
|
id: category.id,
|
|
|
|
|
|
name: category.name
|
|
|
|
|
|
}
|
|
|
|
|
|
showEditDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 15:20:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 确认编辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleConfirmEdit = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await editFormRef.value?.validate()
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2026-01-01 15:20:59 +08:00
|
|
|
|
showLoadingToast({
|
|
|
|
|
|
message: '保存中...',
|
|
|
|
|
|
forbidClick: true,
|
|
|
|
|
|
duration: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { success, message } = await updateCategory({
|
|
|
|
|
|
id: editForm.value.id,
|
|
|
|
|
|
name: editForm.value.name
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccessToast('保存成功')
|
|
|
|
|
|
showEditDialog.value = false
|
|
|
|
|
|
await loadCategories()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(message || '保存失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存失败:', error)
|
|
|
|
|
|
showToast('保存失败: ' + (error.message || '未知错误'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
closeToast()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 删除分类
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleDelete = async (category) => {
|
|
|
|
|
|
deleteTarget.value = category
|
|
|
|
|
|
showDeleteConfirm.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 确认删除
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleConfirmDelete = async () => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (!deleteTarget.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
showLoadingToast({
|
|
|
|
|
|
message: '删除中...',
|
|
|
|
|
|
forbidClick: true,
|
|
|
|
|
|
duration: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const { success, message } = await deleteCategory(deleteTarget.value.id)
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccessToast('删除成功')
|
|
|
|
|
|
showDeleteConfirm.value = false
|
|
|
|
|
|
deleteTarget.value = null
|
|
|
|
|
|
await loadCategories()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(message || '删除失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除失败:', error)
|
|
|
|
|
|
showToast('删除失败: ' + (error.message || '未知错误'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
closeToast()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
<style scoped lang="scss">
|
2025-12-26 15:21:31 +08:00
|
|
|
|
.level-container {
|
|
|
|
|
|
min-height: calc(100vh - 50px);
|
2025-12-28 10:23:57 +08:00
|
|
|
|
margin-top: 16px;
|
2025-12-26 15:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.breadcrumb .van-tag {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
.scroll-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
2026-02-02 11:07:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
.bottom-button {
|
2026-02-02 11:07:34 +08:00
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 21:55:38 +08:00
|
|
|
|
.category-actions {
|
2026-02-02 11:07:34 +08:00
|
|
|
|
display: flex;
|
2026-02-15 10:10:28 +08:00
|
|
|
|
gap: 8px;
|
2025-12-26 18:03:52 +08:00
|
|
|
|
}
|
2025-12-26 15:21:31 +08:00
|
|
|
|
</style>
|