Files
EmailBill/Web/src/views/ClassificationBatch.vue
孙诚 e11603caec
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 12s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
样式调整
2025-12-27 21:15:26 +08:00

523 lines
12 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="批量分类"
left-text="返回"
left-arrow
@click-left="handleBack"
placeholder
/>
<div class="scroll-content">
<!-- 未分类账单统计 -->
<div class="unclassified-stat">
<span>未分类账单数: {{ unclassifiedCount }}</span>
</div>
<!-- 分组列表 -->
<div>
<van-empty v-if="reasonGroups.length === 0 && !listLoading && finished" description="暂无数据" />
<van-list
v-model:loading="listLoading"
v-model:error="error"
:finished="finished"
finished-text="没有更多了"
error-text="请求失败点击重新加载"
@load="onLoad"
>
<van-cell-group inset>
<van-cell
v-for="group in reasonGroups"
:key="group.reason"
clickable
@click="handleSelectGroup(group)"
>
<template #title>
<div class="group-title">
{{ group.reason }}
</div>
</template>
<template #label>
<div class="group-info">
<van-tag
:type="getTypeColor(group.sampleType)"
size="medium"
style="margin-right: 8px;"
>
{{ getTypeName(group.sampleType) }}
</van-tag>
<van-tag
v-if="group.sampleClassify"
type="primary"
size="medium"
style="margin-right: 8px;"
>
{{ group.sampleClassify }}
</van-tag>
<span class="count-text">{{ group.count }} 条记录</span>
</div>
</template>
<template #right-icon>
<van-icon name="arrow" />
</template>
</van-cell>
</van-cell-group>
</van-list>
</div>
<!-- 批量设置对话框 -->
<van-dialog
v-model:show="showSettingDialog"
title="批量设置分类"
:show-cancel-button="true"
@confirm="handleConfirmUpdate"
@cancel="resetForm"
>
<van-form ref="formRef" class="setting-form">
<van-cell-group inset>
<!-- 显示选中的摘要 -->
<van-field
:model-value="selectedGroup?.reason"
label="交易摘要"
readonly
input-align="left"
/>
<!-- 显示记录数量 -->
<van-field
:model-value="`${selectedGroup?.count || 0} `"
label="记录数量"
readonly
input-align="left"
/>
<!-- 交易类型选择 -->
<van-field
v-model="form.typeName"
is-link
readonly
name="type"
label="交易类型"
placeholder="请选择交易类型"
@click="showTypePicker = true"
:rules="[{ required: true, message: '请选择交易类型' }]"
/>
<!-- 分类选择 -->
<van-field name="classify" label="分类">
<template #input>
<span v-if="!form.classify" style="opacity: 0.4;">请选择分类</span>
<span v-else>{{ form.classify }}</span>
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
v-for="item in classifyOptions"
:key="item.id"
:type="form.classify === item.text ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="selectClassify(item.text)"
>
{{ item.text }}
</van-button>
<van-button
type="success"
size="small"
class="classify-btn"
@click="showAddClassify = true"
>
+ 新增
</van-button>
<van-button
v-if="form.classify"
type="warning"
size="small"
class="classify-btn"
@click="clearClassify"
>
清空
</van-button>
</div>
</van-cell-group>
</van-form>
</van-dialog>
<!-- 交易类型选择器 -->
<van-popup v-model:show="showTypePicker" position="bottom" round>
<van-picker
show-toolbar
:columns="typeOptions"
@confirm="handleConfirmType"
@cancel="showTypePicker = false"
/>
</van-popup>
<!-- 新增分类对话框 -->
<van-dialog
v-model:show="showAddClassify"
title="新增交易分类"
show-cancel-button
@confirm="addNewClassify"
>
<van-field v-model="newClassify" placeholder="请输入新的交易分类" />
</van-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
showSuccessToast,
showToast,
showLoadingToast,
closeToast,
showConfirmDialog
} from 'vant'
import { getReasonGroups, batchUpdateByReason, getUnclassifiedCount } from '@/api/transactionRecord'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
const router = useRouter()
// 交易类型选项
const typeOptions = [
{ text: '支出', value: 0 },
{ text: '收入', value: 1 },
{ text: '不计收支', value: 2 }
]
// 数据状态
const listLoading = ref(false)
const error = ref(false)
const finished = ref(false)
const reasonGroups = ref([])
const categories = ref([])
const pageIndex = ref(1)
const pageSize = 20
const unclassifiedCount = ref(0)
const total = ref(0)
// 对话框状态
const showSettingDialog = ref(false)
const showTypePicker = ref(false)
const showAddClassify = ref(false)
// 表单状态
const formRef = ref(null)
const selectedGroup = ref(null)
const newClassify = ref('')
const form = ref({
type: null,
typeName: '',
classify: ''
})
// 根据选中的类型过滤分类选项
const classifyOptions = computed(() => {
if (form.value.type === null) return []
return categories.value
.filter(c => c.type === form.value.type)
.map(c => ({ text: c.name, value: c.name, id: c.id }))
})
// 监听交易类型变化,重新加载分类
watch(() => form.value.type, (newVal) => {
// 清空已选的分类
form.value.classify = ''
// 重新加载对应类型的分类列表
loadCategories(newVal)
})
// 获取类型名称
const getTypeName = (type) => {
const typeMap = {
0: '支出',
1: '收入',
2: '不计收支'
}
return typeMap[type] || '未知'
}
// 获取类型对应的标签颜色
const getTypeColor = (type) => {
const colorMap = {
0: 'danger', // 支出 - 红色
1: 'success', // 收入 - 绿色
2: 'default' // 不计收支 - 灰色
}
return colorMap[type] || 'default'
}
// 获取分组数据
const loadReasonGroups = async () => {
try {
const res = await getReasonGroups(pageIndex.value, pageSize)
if (res.success) {
const newData = res.data || []
reasonGroups.value = [...reasonGroups.value, ...newData]
total.value = res.total || 0
// 判断是否还有更多数据
if (reasonGroups.value.length >= total.value) {
finished.value = true
}
// 加载成功后,页码+1为下次加载做准备
pageIndex.value++
error.value = false
} else {
error.value = true
showToast(res.message || '获取分组数据失败')
}
} catch (err) {
console.error('获取分组数据失败:', err)
error.value = true
showToast('获取分组数据失败')
}
}
// 获取未分类账单统计
const loadUnclassifiedCount = async () => {
try {
const res = await getUnclassifiedCount()
if (res.success) {
unclassifiedCount.value = res.data || 0
}
} catch (error) {
console.error('获取未分类账单数量失败:', error)
}
}
// 加载更多
const onLoad = async () => {
await loadReasonGroups()
// 首次加载时获取统计信息
if (pageIndex.value === 2) {
await loadUnclassifiedCount()
}
listLoading.value = false
}
// 获取所有分类
const loadCategories = async (type = null) => {
try {
const res = await getCategoryList(type)
if (res.success) {
categories.value = res.data || []
}
} catch (error) {
console.error('获取分类列表失败:', error)
}
}
// 选择分组
const handleSelectGroup = (group) => {
selectedGroup.value = group
form.value = {
type: group.sampleType,
typeName: getTypeName(group.sampleType),
classify: group.sampleClassify
}
// 加载对应类型的分类列表
loadCategories(group.sampleType)
showSettingDialog.value = true
}
// 选择分类
const selectClassify = (classify) => {
form.value.classify = classify
}
// 确认选择交易类型
const handleConfirmType = ({ selectedOptions }) => {
form.value.type = selectedOptions[0].value
form.value.typeName = selectedOptions[0].text
showTypePicker.value = false
}
// 新增分类
const addNewClassify = async () => {
if (!newClassify.value.trim()) {
showToast('请输入分类名称')
return
}
if (form.value.type === null) {
showToast('请先选择交易类型')
return
}
try {
const categoryName = newClassify.value.trim()
// 调用API创建分类
const response = await createCategory({
name: categoryName,
type: form.value.type
})
if (response.success) {
showToast('分类创建成功')
// 重新加载分类列表
await loadCategories(form.value.type)
form.value.classify = categoryName
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
} finally {
newClassify.value = ''
showAddClassify.value = false
}
}
// 清空分类
const clearClassify = () => {
form.value.classify = ''
showToast('已清空分类')
}
// 确认批量更新
const handleConfirmUpdate = async () => {
try {
// 表单验证
await formRef.value?.validate()
// 二次确认
await showConfirmDialog({
title: '确认批量设置',
message: `确定要将「${selectedGroup.value.reason}」的 ${selectedGroup.value.count} 条记录设置为「${form.value.typeName} - ${form.value.classify}」吗?`
})
showLoadingToast({
message: '更新中...',
forbidClick: true,
duration: 0
})
const res = await batchUpdateByReason({
reason: selectedGroup.value.reason,
type: form.value.type,
classify: form.value.classify
})
closeToast()
if (res.success) {
showSuccessToast(res.message || `成功更新 ${res.data} 条记录`)
showSettingDialog.value = false
resetForm()
// 重新加载数据
reasonGroups.value = []
pageIndex.value = 1
finished.value = false
listLoading.value = true
await loadReasonGroups()
await loadUnclassifiedCount()
listLoading.value = false
} else {
showToast(res.message || '批量更新失败')
}
} catch (error) {
closeToast()
if (error !== 'cancel') {
console.error('批量更新失败:', error)
showToast(error.message || '批量更新失败')
}
}
}
// 重置表单
const resetForm = () => {
selectedGroup.value = null
form.value = {
type: null,
typeName: '',
classify: ''
}
formRef.value?.resetValidation()
}
// 返回上一页
const handleBack = () => {
router.back()
}
// 页面加载
onMounted(() => {
// onLoad 会自动触发首次加载
})
</script>
<style scoped>
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.group-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 4px;
}
.group-info {
align-items: center;
margin-top: 4px;
}
.count-text {
font-size: 13px;
opacity: 0.6;
}
.unclassified-stat {
padding-left: 16px;
padding-top: 12px;
font-size: 14px;
font-weight: 500;
opacity: 0.83px;
opacity: 0.6;
}
.setting-form {
padding: 16px 0;
}
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
max-height: 300px;
overflow-y: auto;
}
.classify-btn {
flex: 0 0 auto;
min-width: 70px;
border-radius: 16px;
}
:deep(.van-cell-group--inset) {
margin: 16px;
}
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>