Files
EmailBill/Web/src/views/ClassificationEdit.vue
SunCheng 6abc5f8b6d
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 6m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
tmp2
2026-02-02 11:07:49 +08:00

700 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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>
<div class="page-container-flex">
<van-nav-bar
:title="navTitle"
left-text="返回"
left-arrow
placeholder
@click-left="handleBack"
/>
<div class="scroll-content">
<!-- 第一层选择交易类型 -->
<div
v-if="currentLevel === 0"
class="level-container"
>
<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>
<!-- 第二层分类列表 -->
<div
v-else
class="level-container"
>
<!-- 面包屑导航 -->
<div class="breadcrumb">
<van-tag
type="primary"
closeable
style="margin-left: 16px"
@close="handleBackToRoot"
>
{{ currentTypeName }}
</van-tag>
</div>
<!-- 分类列表 -->
<van-empty
v-if="categories.length === 0"
description="暂无分类"
/>
<van-cell-group
v-else
inset
>
<van-swipe-cell
v-for="category in categories"
:key="category.id"
>
<van-cell :title="category.name">
<template #icon>
<div
v-if="category.icon"
class="category-icon"
v-html="parseIcon(category.icon)"
/>
</template>
<template #default>
<div class="category-actions">
<van-button
size="small"
type="primary"
plain
@click="handleIconSelect(category)"
>
选择图标
</van-button>
<van-button
size="small"
@click="handleEditOld(category)"
>
编辑
</van-button>
</div>
</template>
</van-cell>
<template #right>
<van-button
square
type="danger"
text="删除"
@click="handleDelete(category)"
/>
</template>
</van-swipe-cell>
</van-cell-group>
</div>
<!-- 底部安全距离 -->
<div style="height: calc(55px + env(safe-area-inset-bottom, 0px))" />
<div class="bottom-button">
<!-- 新增分类按钮 -->
<van-button
type="primary"
size="large"
icon="plus"
@click="handleAddCategory"
>
新增分类
</van-button>
</div>
<!-- 新增分类对话框 -->
<van-dialog
v-model:show="showAddDialog"
title="新增分类"
@confirm="handleConfirmAdd"
@cancel="resetAddForm"
>
<van-form ref="addFormRef">
<van-field
v-model="addForm.name"
name="name"
label="分类名称"
placeholder="请输入分类名称"
:rules="[{ required: true, message: '请输入分类名称' }]"
/>
</van-form>
</van-dialog>
<!-- 编辑分类对话框 -->
<van-dialog
v-model:show="showEditDialog"
title="编辑分类"
show-cancel-button
@confirm="handleConfirmEdit"
>
<van-form ref="editFormRef">
<van-field
v-model="editForm.name"
name="name"
label="分类名称"
placeholder="请输入分类名称"
:rules="[{ required: true, message: '请输入分类名称' }]"
/>
</van-form>
</van-dialog>
<!-- 删除确认对话框 -->
<van-dialog
v-model:show="showDeleteConfirm"
title="删除分类"
message="删除后无法恢复,确定要删除吗?"
@confirm="handleConfirmDelete"
/>
<!-- 图标选择对话框 -->
<van-dialog
v-model:show="showIconDialog"
title="选择图标"
show-cancel-button
@confirm="handleConfirmIconSelect"
>
<div class="icon-selector">
<div
v-if="currentCategory && currentCategory.icon"
class="icon-list"
>
<div
v-for="(icon, index) in parseIconArray(currentCategory.icon)"
:key="index"
class="icon-item"
:class="{ active: selectedIconIndex === index }"
@click="selectedIconIndex = index"
>
<div
class="icon-preview"
v-html="icon"
/>
</div>
</div>
<div
v-else
class="empty-icons"
>
<van-empty description="暂无图标" />
</div>
<div class="icon-actions">
<van-button
type="primary"
size="small"
:loading="isGeneratingIcon"
:disabled="isGeneratingIcon"
@click="handleGenerateIcon"
>
{{ isGeneratingIcon ? 'AI生成中...' : '生成新图标' }}
</van-button>
</div>
</div>
</van-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant'
import {
getCategoryList,
createCategory,
deleteCategory,
updateCategory,
generateIcon,
updateSelectedIcon
} from '@/api/transactionCategory'
const router = useRouter()
// 交易类型选项
const typeOptions = [
{ value: 0, label: '支出' },
{ value: 1, label: '收入' },
{ value: 2, label: '不计收支' }
]
// 层级状态
const currentLevel = ref(0) // 0=类型选择, 1=分类管理
const currentType = ref(null) // 当前选中的交易类型
const currentTypeName = computed(() => {
const type = typeOptions.find((t) => t.value === currentType.value)
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: ''
})
// 图标选择对话框
const showIconDialog = ref(false)
const currentCategory = ref(null) // 当前正在编辑图标的分类
const selectedIconIndex = ref(0)
const isGeneratingIcon = ref(false)
// 计算导航栏标题
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 {
if (window.history.length > 1) {
router.back()
} else {
router.replace('/')
}
}
}
/**
* 返回到根目录(类型选择)
*/
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 handleEdit = (category) => {
editForm.value = {
id: category.id,
name: category.name
}
showEditDialog.value = true
}
/**
* 打开图标选择器
*/
const handleIconSelect = (category) => {
currentCategory.value = category
selectedIconIndex.value = 0
showIconDialog.value = true
}
/**
* 生成新图标
*/
const handleGenerateIcon = async () => {
if (!currentCategory.value) {
return
}
try {
isGeneratingIcon.value = true
showLoadingToast({
message: 'AI正在生成图标...',
forbidClick: true,
duration: 0
})
const { success, data, message } = await generateIcon(currentCategory.value.id)
if (success) {
showSuccessToast('图标生成成功')
// 重新加载分类列表以获取最新的图标
await loadCategories()
// 更新当前分类引用
const updated = categories.value.find((c) => c.id === currentCategory.value.id)
if (updated) {
currentCategory.value = updated
}
} else {
showToast(message || '生成图标失败')
}
} catch (error) {
console.error('生成图标失败:', error)
showToast('生成图标失败: ' + (error.message || '未知错误'))
} finally {
isGeneratingIcon.value = false
closeToast()
}
}
/**
* 确认选择图标
*/
const handleConfirmIconSelect = async () => {
if (!currentCategory.value) {return}
try {
showLoadingToast({
message: '保存中...',
forbidClick: true,
duration: 0
})
const { success, message } = await updateSelectedIcon(
currentCategory.value.id,
selectedIconIndex.value
)
if (success) {
showSuccessToast('图标保存成功')
showIconDialog.value = false
await loadCategories()
} else {
showToast(message || '保存失败')
}
} catch (error) {
console.error('保存图标失败:', error)
showToast('保存图标失败: ' + (error.message || '未知错误'))
} finally {
closeToast()
}
}
/**
* 编辑分类
*/
const handleEditOld = (category) => {
editForm.value = {
id: category.id,
name: category.name
}
showEditDialog.value = true
}
/**
* 确认编辑
*/
const handleConfirmEdit = async () => {
try {
await editFormRef.value?.validate()
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()
}
}
/**
* 删除分类
*/
const handleDelete = async (category) => {
deleteTarget.value = category
showDeleteConfirm.value = true
}
/**
* 确认删除
*/
const handleConfirmDelete = async () => {
if (!deleteTarget.value) {
return
}
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()
}
}
/**
* 重置新增表单
*/
const resetAddForm = () => {
addForm.value = {
name: ''
}
}
/**
* 解析图标数组(第一个图标为当前选中的)
*/
const parseIcon = (iconJson) => {
if (!iconJson) {return ''}
try {
const icons = JSON.parse(iconJson)
return Array.isArray(icons) && icons.length > 0 ? icons[0] : ''
} catch {
return ''
}
}
/**
* 解析图标数组为完整数组
*/
const parseIconArray = (iconJson) => {
if (!iconJson) {return []}
try {
const icons = JSON.parse(iconJson)
return Array.isArray(icons) ? icons : []
} catch {
return []
}
}
onMounted(() => {
// 初始化时显示类型选择
currentLevel.value = 0
})
</script>
<style scoped>
.level-container {
min-height: calc(100vh - 50px);
margin-top: 16px;
}
.breadcrumb {
padding: 8px 0;
}
.breadcrumb .van-tag {
cursor: pointer;
}
.category-icon {
width: 24px;
height: 24px;
margin-right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.category-icon :deep(svg) {
width: 100%;
height: 100%;
fill: currentColor;
}
.category-actions {
display: flex;
gap: 8px;
}
.icon-selector {
padding: 16px;
}
.icon-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.icon-item {
width: 60px;
height: 60px;
border: 2px solid var(--van-border-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.icon-item:hover {
border-color: var(--van-primary-color);
background-color: var(--van-primary-color-light);
}
.icon-item.active {
border-color: var(--van-primary-color);
background-color: var(--van-primary-color-light);
box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
}
.icon-preview {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-preview :deep(svg) {
width: 100%;
height: 100%;
fill: currentColor;
}
.empty-icons {
padding: 20px 0;
}
.icon-actions {
padding-top: 16px;
border-top: 1px solid var(--van-border-color);
display: flex;
justify-content: center;
}
/* 深色模式 */
/* @media (prefers-color-scheme: dark) {
.level-container {
background: var(--van-background);
}
} */
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>