Files
EmailBill/Web/src/views/ClassificationEdit.vue

570 lines
12 KiB
Vue
Raw Normal View History

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
@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>
<Icon
2026-02-02 11:07:34 +08:00
v-if="category.icon"
: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"
@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))" />
</div>
2025-12-26 15:21:31 +08:00
<!-- 新增分类按钮 -->
<div class="bottom-button">
<van-button
type="primary"
size="large"
icon="plus"
@click="handleAddCategory"
2026-02-15 10:10:28 +08:00
>
新增分类
</van-button>
2025-12-27 21:15:26 +08:00
</div>
<!-- 新增分类对话框 -->
<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>
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'
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,
deleteCategory,
updateCategory
2025-12-26 15:21:31 +08:00
} from '@/api/transactionCategory'
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: '不计收支' }
]
// 层级状态
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)
// 编辑对话框
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)
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()
}
}
/**
* 重置新增表单
*/
const resetAddForm = () => {
addForm.value = {
name: ''
}
}
2026-02-02 11:07:34 +08:00
/**
* 打开图标选择器
*/
const handleIconSelect = async (category) => {
2026-02-02 11:07:34 +08:00
currentCategory.value = category
showIconDialog.value = true
try {
isLoadingIcons.value = true
2026-02-02 11:07:34 +08:00
const { success: keywordsSuccess, data: keywordsResponse } = await generateSearchKeywords(category.name)
2026-02-02 11:07:34 +08:00
if (!keywordsSuccess || !keywordsResponse || !keywordsResponse.keywords || keywordsResponse.keywords.length === 0) {
showToast('生成搜索关键字失败')
return
2026-02-02 11:07:34 +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) {
console.error('搜索图标错误:', error)
showToast('搜索图标失败')
isLoadingIcons.value = false
2026-02-02 11:07:34 +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
})
const { success, message } = await updateCategoryIconApi(
2026-02-02 11:07:34 +08:00
currentCategory.value.id,
iconIdentifier
2026-02-02 11:07:34 +08:00
)
if (success) {
showSuccessToast('图标保存成功')
showIconDialog.value = false
currentCategory.value = null
iconCandidates.value = []
2026-02-02 11:07:34 +08:00
await loadCategories()
} else {
showToast(message || '保存失败')
}
} catch (error) {
console.error('保存图标失败:', error)
showToast('保存图标失败')
2026-02-02 11:07:34 +08:00
} finally {
closeToast()
}
}
2026-02-15 10:10:28 +08:00
/**
* 取消图标选择
2026-02-15 10:10:28 +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
/**
* 编辑分类
*/
const handleEdit = (category) => {
2026-02-02 11:07:34 +08:00
editForm.value = {
id: category.id,
name: category.name
}
showEditDialog.value = true
}
/**
* 确认编辑
*/
const handleConfirmEdit = async () => {
try {
await editFormRef.value?.validate()
2026-01-16 11:15:44 +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>
<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;
}
.scroll-content {
flex: 1;
overflow-y: auto;
2026-02-02 11:07:34 +08:00
}
.bottom-button {
2026-02-02 11:07:34 +08:00
padding: 16px;
}
.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>