封装调整
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 25s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
孙诚
2026-01-08 14:41:50 +08:00
parent 500a6495bd
commit 58ee44987b
15 changed files with 353 additions and 672 deletions

View File

@@ -43,22 +43,14 @@ public class BudgetRecord : BaseEntity
public enum BudgetPeriodType
{
/// <summary>
/// 周
/// </summary>
Week,
/// <summary>
/// 月
/// </summary>
Month,
Month = 1,
/// <summary>
/// 年
/// </summary>
Year,
/// <summary>
/// 长期
/// </summary>
Longterm
Year = 2
}
public enum BudgetCategory

View File

@@ -61,19 +61,10 @@ public class BudgetService(
public static (DateTime start, DateTime end) GetPeriodRange(DateTime startDate, BudgetPeriodType type, DateTime referenceDate)
{
if (type == BudgetPeriodType.Longterm) return (startDate, DateTime.MaxValue);
DateTime start;
DateTime end;
if (type == BudgetPeriodType.Week)
{
var daysFromStart = (referenceDate.Date - startDate.Date).Days;
var weeksFromStart = daysFromStart / 7;
start = startDate.Date.AddDays(weeksFromStart * 7);
end = start.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
}
else if (type == BudgetPeriodType.Month)
if (type == BudgetPeriodType.Month)
{
start = new DateTime(referenceDate.Year, referenceDate.Month, 1);
end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);

View File

@@ -52,27 +52,11 @@
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
type="success"
size="small"
class="classify-btn"
@click="addClassifyDialogRef.open()"
>
+ 新增
</van-button>
<van-button
v-for="item in categoryList"
:key="item.id"
:type="categoryName === item.name ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="selectClassify(item)"
>
{{ item.name }}
</van-button>
</div>
<!-- 分类选择组件 -->
<ClassifySelector
v-model="categoryName"
:type="form.type"
/>
</van-cell-group>
<div class="actions">
@@ -83,12 +67,6 @@
</div>
</van-form>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddClassify"
/>
<!-- 日期选择弹窗 -->
<van-popup v-model:show="showDatePicker" position="bottom" round teleport="body">
<van-date-picker
@@ -115,8 +93,7 @@
import { ref, onMounted, watch } from 'vue'
import { showToast } from 'vant'
import dayjs from 'dayjs'
import AddClassifyDialog from '@/components/AddClassifyDialog.vue'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
import ClassifySelector from '@/components/ClassifySelector.vue'
const props = defineProps({
initialData: {
@@ -135,13 +112,10 @@ const props = defineProps({
const emit = defineEmits(['submit'])
const addClassifyDialogRef = ref()
// 表单数据
const form = ref({
type: 0, // 0: 支出, 1: 收入, 2: 不计
amount: '',
categoryId: null,
date: dayjs().format('YYYY-MM-DD'),
time: dayjs().format('HH:mm'),
note: ''
@@ -153,10 +127,7 @@ const categoryName = ref('')
const showDatePicker = ref(false)
const showTimePicker = ref(false)
// 选择器数据
const categoryList = ref([])
// 日期时间临时变量 (Vant DatePicker 需要数组或特定格式)
// 日期时间临时变量 (Vant DatePicker 需要数组 or 特定格式)
const currentDate = ref(dayjs().format('YYYY-MM-DD').split('-'))
const currentTime = ref(dayjs().format('HH:mm').split(':'))
@@ -177,29 +148,11 @@ const initForm = async () => {
if (reason !== undefined) form.value.note = reason
if (type !== undefined) form.value.type = type
// 加载分类列表
await loadClassifyList(form.value.type)
// 如果有传入分类名称,尝试匹配
// 如果有传入分类名称,尝试设置
if (classify) {
const found = categoryList.value.find(c => c.name === classify)
if (found) {
selectClassify(found)
} else {
// 如果没找到对应分类但有分类名称可能需要特殊处理或者就显示名称但不关联ID
// 这里暂时只显示名称ID为空或者需要自动创建
// 按照原有逻辑,后端需要分类名称,所以这里只要设置 categoryName 即可
// 但是 ManualBillAdd 原逻辑是需要 categoryId 的。
// 不过 createTransaction 接口传的是 classify (name)。
// 让我们看 ManualBillAdd 的 handleSave:
// classify: categoryName.value
// 所以只要 categoryName 有值就行。
categoryName.value = classify
}
}
} else {
await loadClassifyList(form.value.type)
}
}
onMounted(() => {
@@ -213,50 +166,6 @@ watch(() => props.initialData, () => {
const handleTypeChange = (newType) => {
categoryName.value = ''
form.value.categoryId = null
loadClassifyList(newType)
}
const loadClassifyList = async (type = null) => {
try {
const response = await getCategoryList(type)
if (response.success) {
categoryList.value = response.data || []
}
} catch (error) {
console.error('加载分类列表出错:', error)
}
}
const selectClassify = (item) => {
categoryName.value = item.name
form.value.categoryId = item.id
}
const handleAddClassify = async (name) => {
try {
// 调用API创建分类
const response = await createCategory({
name: name,
type: form.value.type
})
if (response.success) {
showToast('分类创建成功')
const newId = response.data
// 重新加载分类列表
await loadClassifyList(form.value.type)
// 选中新创建的分类
categoryName.value = name
form.value.categoryId = newId
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
}
}
const onConfirmDate = ({ selectedValues }) => {

View File

@@ -41,7 +41,7 @@
<span class="percent" :class="percentClass">{{ percentage }}%</span>
</slot>
</div>
<div v-if="budget.type !== BudgetPeriodType.Longterm" class="progress-section time-progress">
<div class="progress-section time-progress">
<span class="period-type">时间进度</span>
<van-progress
:percentage="timePercentage"
@@ -126,7 +126,7 @@ const percentage = computed(() => {
})
const timePercentage = computed(() => {
if (!props.budget.periodStart || !props.budget.periodEnd || props.budget.type === BudgetPeriodType.Longterm) return 0
if (!props.budget.periodStart || !props.budget.periodEnd) return 0
const start = new Date(props.budget.periodStart).getTime()
const end = new Date(props.budget.periodEnd).getTime()
const now = new Date().getTime()

View File

@@ -17,7 +17,6 @@
<van-field name="type" label="统计周期">
<template #input>
<van-radio-group v-model="form.type" direction="horizontal">
<van-radio :name="BudgetPeriodType.Week"></van-radio>
<van-radio :name="BudgetPeriodType.Month"></van-radio>
<van-radio :name="BudgetPeriodType.Year"></van-radio>
</van-radio-group>
@@ -45,29 +44,13 @@
</div>
</template>
</van-field>
<div class="classify-buttons">
<van-button
v-if="filteredCategories.length > 0"
:type="isAllSelected ? 'primary' : 'default'"
size="small"
class="classify-btn all-btn"
@click="toggleAll"
>
{{ isAllSelected ? '取消全选' : '全选' }}
</van-button>
<van-button
v-for="item in filteredCategories"
:key="item.id"
:type="form.selectedCategories.includes(item.name) ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="toggleCategory(item.name)"
>
{{ item.name }}
</van-button>
<div v-if="filteredCategories.length === 0" class="no-data">暂无分类</div>
</div>
<ClassifySelector
v-model="form.selectedCategories"
:type="budgetType"
multiple
:show-add="false"
:show-clear="false"
/>
</van-cell-group>
</van-form>
</div>
@@ -78,19 +61,18 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed } from 'vue'
import { showToast } from 'vant'
import { getCategoryList } from '@/api/transactionCategory'
import { createBudget, updateBudget } from '@/api/budget'
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const categories = ref([])
const form = reactive({
id: undefined,
name: '',
@@ -137,44 +119,10 @@ defineExpose({
open
})
const filteredCategories = computed(() => {
const targetType = form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
return categories.value.filter(c => c.type === targetType)
const budgetType = computed(() => {
return form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
})
const isAllSelected = computed(() => {
return filteredCategories.value.length > 0 &&
filteredCategories.value.every(c => form.selectedCategories.includes(c.name))
})
const toggleCategory = (name) => {
const index = form.selectedCategories.indexOf(name)
if (index > -1) {
form.selectedCategories.splice(index, 1)
} else {
form.selectedCategories.push(name)
}
}
const toggleAll = () => {
if (isAllSelected.value) {
form.selectedCategories = []
} else {
form.selectedCategories = filteredCategories.value.map(c => c.name)
}
}
const fetchCategories = async () => {
try {
const res = await getCategoryList()
if (res.success) {
categories.value = res.data || []
}
} catch (err) {
console.error('获取分类列表失败', err)
}
}
const onSubmit = async () => {
try {
const data = {
@@ -207,10 +155,6 @@ const getCategoryName = (category) => {
return ''
}
}
onMounted(() => {
fetchCategories()
})
</script>
<style scoped>
@@ -235,29 +179,9 @@ onMounted(() => {
width: 100%;
}
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
overflow-y: auto;
}
.classify-btn {
flex: 0 0 auto;
min-width: 60px;
border-radius: 16px;
padding: 0 12px;
}
.all-btn {
font-weight: bold;
border-style: dashed;
}
.no-data {
font-size: 13px;
color: #969799;
padding: 8px 0;
padding: 8px 16px;
}
</style>

View File

@@ -30,7 +30,6 @@ const props = defineProps({
})
const periodConfigs = {
week: { label: '本周', showDivider: true },
month: { label: '本月', showDivider: true },
year: { label: '年度', showDivider: false }
}

View File

@@ -11,29 +11,13 @@
<div class="category-section">
<div class="section-title">可多选分类</div>
<div class="classify-buttons">
<van-button
v-if="incomeCategories.length > 0"
:type="isAllSelected ? 'primary' : 'default'"
size="small"
class="classify-btn all-btn"
@click="toggleAll"
>
{{ isAllSelected ? '取消全选' : '全选' }}
</van-button>
<van-button
v-for="item in incomeCategories"
:key="item.id"
:type="selectedCategories.includes(item.name) ? 'primary' : 'default'"
size="small"
class="classify-btn"
style="margin-bottom: 8px; margin-right: 8px;"
@click="toggleCategory(item.name)"
>
{{ item.name }}
</van-button>
<div v-if="incomeCategories.length === 0" class="no-data">暂无收入分类</div>
</div>
<ClassifySelector
v-model="selectedCategories"
:type="2"
multiple
:show-add="false"
:show-clear="false"
/>
</div>
</div>
@@ -44,21 +28,19 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import { getCategoryList } from '@/api/transactionCategory'
import { getConfig, setConfig } from '@/api/config'
import PopupContainer from '@/components/PopupContainer.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
const emit = defineEmits(['success'])
const visible = ref(false)
const categories = ref([])
const selectedCategories = ref([])
const open = async () => {
visible.value = true
await fetchCategories()
await fetchConfig()
}
@@ -66,43 +48,6 @@ defineExpose({
open
})
const incomeCategories = computed(() => {
return categories.value.filter(c => c.type === 1) // Income = 1
})
const isAllSelected = computed(() => {
return incomeCategories.value.length > 0 &&
incomeCategories.value.every(c => selectedCategories.value.includes(c.name))
})
const toggleCategory = (name) => {
const index = selectedCategories.value.indexOf(name)
if (index > -1) {
selectedCategories.value.splice(index, 1)
} else {
selectedCategories.value.push(name)
}
}
const toggleAll = () => {
if (isAllSelected.value) {
selectedCategories.value = []
} else {
selectedCategories.value = incomeCategories.value.map(c => c.name)
}
}
const fetchCategories = async () => {
try {
const res = await getCategoryList()
if (res.success) {
categories.value = res.data || []
}
} catch (err) {
console.error('获取分类列表失败', err)
}
}
const fetchConfig = async () => {
try {
const res = await getConfig('SavingsCategories')
@@ -156,22 +101,6 @@ const onSubmit = async () => {
margin-bottom: 12px;
}
.classify-buttons {
display: flex;
flex-wrap: wrap;
}
.classify-btn {
margin-bottom: 8px;
margin-right: 8px;
border-radius: 20px;
min-width: 60px;
}
.all-btn {
border-style: dashed;
}
.no-data {
text-align: center;
color: #969799;

View File

@@ -0,0 +1,249 @@
<template>
<div class="classify-selector">
<div class="classify-buttons">
<!-- 全选按钮 (仅多选模式) -->
<van-button
v-if="multiple && showAll"
:type="isAllSelected ? 'primary' : 'default'"
size="small"
class="classify-btn all-btn"
@click="toggleAll"
>
{{ isAllSelected ? '取消全选' : '全选' }}
</van-button>
<!-- 新增按钮 -->
<van-button
v-if="showAdd"
type="success"
size="small"
class="classify-btn"
@click="openAddDialog"
>
+ 新增
</van-button>
<!-- 分类项按钮 -->
<van-button
v-for="item in displayOptions"
:key="item.id || item.text"
:type="isSelected(item) ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="toggleItem(item)"
>
{{ item.text }}
</van-button>
<!-- 清空按钮 -->
<van-button
v-if="showClear && hasSelection"
type="warning"
size="small"
class="classify-btn"
@click="clear"
>
清空
</van-button>
</div>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddConfirm"
/>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { showToast } from 'vant'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
import AddClassifyDialog from './AddClassifyDialog.vue'
const props = defineProps({
modelValue: {
type: [String, Array],
default: ''
},
options: {
type: Array,
default: null
},
type: {
type: [Number, String],
default: null
},
multiple: {
type: Boolean,
default: false
},
showAdd: {
type: Boolean,
default: true
},
showClear: {
type: Boolean,
default: true
},
showAll: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:modelValue', 'add', 'change'])
const innerOptions = ref([])
const addClassifyDialogRef = ref()
const displayOptions = computed(() => {
if (props.options) return props.options
return innerOptions.value
})
const fetchOptions = async () => {
if (props.options) return
try {
const response = await getCategoryList(props.type)
if (response.success) {
innerOptions.value = (response.data || []).map(item => ({
text: item.name,
value: item.name,
id: item.id
}))
}
} catch (error) {
console.error('ClassifySelector 加载分类失败:', error)
}
}
// 打开新增对话框
const openAddDialog = () => {
addClassifyDialogRef.value?.open()
}
// 处理新增确认
const handleAddConfirm = async (categoryName) => {
try {
// 调用API创建分类
const response = await createCategory({
name: categoryName,
type: props.type
})
if (response.success) {
showToast('分类创建成功')
// 刷新列表
await fetchOptions()
// 如果是单选模式,且当前没有选值或就是为了新增,则自动选中
if (!props.multiple) {
emit('update:modelValue', categoryName)
emit('change', categoryName)
}
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('ClassifySelector 创建分类出错:', error)
showToast('创建分类失败')
}
}
watch(() => props.type, () => {
fetchOptions()
})
onMounted(() => {
fetchOptions()
})
// 公开刷新方法
defineExpose({
refresh: fetchOptions
})
// 是否选中
const isSelected = (item) => {
if (props.multiple) {
return Array.isArray(props.modelValue) && props.modelValue.includes(item.text)
}
return props.modelValue === item.text
}
// 是否全部选中
const isAllSelected = computed(() => {
if (!props.multiple || displayOptions.value.length === 0) return false
return displayOptions.value.every(item => props.modelValue.includes(item.text))
})
// 是否有任何选中
const hasSelection = computed(() => {
if (props.multiple) {
return Array.isArray(props.modelValue) && props.modelValue.length > 0
}
return !!props.modelValue
})
// 切换选中状态
const toggleItem = (item) => {
if (props.multiple) {
const newValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
const index = newValue.indexOf(item.text)
if (index > -1) {
newValue.splice(index, 1)
} else {
newValue.push(item.text)
}
emit('update:modelValue', newValue)
emit('change', newValue)
} else {
const newValue = props.modelValue === item.text ? '' : item.text
emit('update:modelValue', newValue)
emit('change', newValue)
}
}
// 切换全选
const toggleAll = () => {
if (!props.multiple) return
if (isAllSelected.value) {
emit('update:modelValue', [])
emit('change', [])
} else {
const allValues = displayOptions.value.map(item => item.text)
emit('update:modelValue', allValues)
emit('change', allValues)
}
}
// 清空
const clear = () => {
const newValue = props.multiple ? [] : ''
emit('update:modelValue', newValue)
emit('change', newValue)
}
</script>
<style scoped>
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
}
.classify-btn {
flex: 0 0 auto;
min-width: 70px;
border-radius: 16px;
}
.all-btn {
font-weight: bold;
border-style: dashed;
}
</style>

View File

@@ -129,36 +129,11 @@
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
type="success"
size="small"
class="classify-btn"
@click="addClassifyDialogRef.open()"
>
+ 新增
</van-button>
<van-button
v-for="item in classifyOptions"
:key="item.id"
:type="batchForm.classify === item.text ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="selectClassify(item.text)"
>
{{ item.text }}
</van-button>
<van-button
v-if="batchForm.classify"
type="warning"
size="small"
class="classify-btn"
@click="clearClassify"
>
清空
</van-button>
</div>
<!-- 分类选择组件 -->
<ClassifySelector
v-model="batchForm.classify"
:type="batchForm.type"
/>
</van-cell-group>
</van-form>
<template #footer>
@@ -172,12 +147,6 @@
</van-button>
</template>
</PopupContainer>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddClassify"
/>
</div>
</template>
@@ -191,8 +160,7 @@ import {
showConfirmDialog
} from 'vant'
import { getReasonGroups, batchUpdateByReason, getTransactionList } from '@/api/transactionRecord'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
import AddClassifyDialog from './AddClassifyDialog.vue'
import ClassifySelector from './ClassifySelector.vue'
import TransactionList from './TransactionList.vue'
import TransactionDetail from './TransactionDetail.vue'
import PopupContainer from './PopupContainer.vue'
@@ -219,8 +187,6 @@ const selectedReasons = ref(new Set())
const pageIndex = ref(1)
const finished = ref(false)
const total = ref(0)
const categories = ref([])
// 弹窗状态
const showTransactionList = ref(false)
const showTransactionDetail = ref(false)
@@ -239,27 +205,15 @@ const transactionPageSize = ref(20)
const showBatchDialog = ref(false)
const batchFormRef = ref(null)
const batchGroup = ref(null)
const addClassifyDialogRef = ref()
const batchForm = ref({
type: null,
typeName: '',
classify: ''
})
// 根据选中的类型过滤分类选项
const classifyOptions = computed(() => {
if (batchForm.value.type === null) return []
return categories.value
.filter(c => c.type === batchForm.value.type)
.map(c => ({ text: c.name, value: c.name, id: c.id }))
})
// 监听交易类型变化,重新加载分类
watch(() => batchForm.value.type, (newVal) => {
batchForm.value.classify = ''
if (newVal !== null) {
loadCategories(newVal)
}
})
// 获取类型名称
@@ -361,62 +315,9 @@ const handleBatchClassify = (group) => {
typeName: getTypeName(group.sampleType),
classify: group.sampleClassify || ''
}
// 加载对应类型的分类列表
loadCategories(group.sampleType)
showBatchDialog.value = true
}
// 获取所有分类
const loadCategories = async (type = null) => {
try {
const res = await getCategoryList(type)
if (res.success) {
categories.value = res.data || []
}
} catch (error) {
console.error('获取分类列表失败:', error)
}
}
// 选择分类
const selectClassify = (classify) => {
batchForm.value.classify = classify
}
// 新增分类
const handleAddClassify = async (categoryName) => {
if (batchForm.value.type === null) {
showToast('请先选择交易类型')
return
}
try {
// 调用API创建分类
const response = await createCategory({
name: categoryName,
type: batchForm.value.type
})
if (response.success) {
showToast('分类创建成功')
// 重新加载分类列表
await loadCategories(batchForm.value.type)
batchForm.value.classify = categoryName
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
}
}
// 清空分类
const clearClassify = () => {
batchForm.value.classify = ''
showToast('已清空分类')
}
// 确认批量更新
const handleConfirmBatchUpdate = async () => {
try {
@@ -744,20 +645,5 @@ defineExpose({
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;
}
/* 交易列表弹窗 - 自定义样式 */
</style>

View File

@@ -61,36 +61,12 @@
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
type="success"
size="small"
class="classify-btn"
@click="addClassifyDialogRef.open()"
>
+ 新增
</van-button>
<van-button
v-for="item in classifyColumns"
:key="item.id"
:type="editForm.classify === item.text ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="selectClassify(item.text)"
>
{{ item.text }}
</van-button>
<van-button
v-if="editForm.classify"
type="warning"
size="small"
class="classify-btn"
@click="clearClassify"
>
清空
</van-button>
</div>
<!-- 分类选择组件 -->
<ClassifySelector
v-model="editForm.classify"
:type="editForm.type"
@change="handleClassifyChange"
/>
</van-cell-group>
</van-form>
@@ -107,12 +83,6 @@
</template>
</PopupContainer>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddClassify"
/>
<!-- 抵账候选列表弹窗 -->
<PopupContainer
v-model="showOffsetPopup"
@@ -138,9 +108,8 @@
import { ref, reactive, watch, defineProps, defineEmits } from 'vue'
import { showToast, showConfirmDialog } from 'vant'
import PopupContainer from '@/components/PopupContainer.vue'
import AddClassifyDialog from '@/components/AddClassifyDialog.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
import { updateTransaction, getCandidatesForOffset, offsetTransactions } from '@/api/transactionRecord'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
const props = defineProps({
show: {
@@ -157,10 +126,6 @@ const emit = defineEmits(['update:show', 'save'])
const visible = ref(false)
const submitting = ref(false)
const addClassifyDialogRef = ref()
// 分类相关
const classifyColumns = ref([])
// 编辑表单
const editForm = reactive({
@@ -186,9 +151,6 @@ watch(() => props.transaction, (newVal) => {
editForm.balance = String(newVal.balance)
editForm.type = newVal.type
editForm.classify = newVal.classify || ''
// 根据交易类型加载分类
loadClassifyList(newVal.type)
}
})
@@ -200,26 +162,8 @@ watch(visible, (newVal) => {
watch(() => editForm.type, (newVal) => {
// 清空已选的分类
editForm.classify = ''
// 重新加载对应类型的分类列表
loadClassifyList(newVal)
})
// 加载分类列表
const loadClassifyList = async (type = null) => {
try {
const response = await getCategoryList(type)
if (response.success) {
classifyColumns.value = (response.data || []).map(item => ({
text: item.name,
value: item.name,
id: item.id
}))
}
} catch (error) {
console.error('加载分类列表出错:', error)
}
}
// 提交编辑
const onSubmit = async () => {
try {
@@ -239,8 +183,6 @@ const onSubmit = async () => {
showToast('保存成功')
visible.value = false
emit('save', data)
// 重新加载分类列表
await loadClassifyList(editForm.type)
} else {
showToast(response.message || '保存失败')
}
@@ -252,47 +194,15 @@ const onSubmit = async () => {
}
}
// 选择分类
const selectClassify = (classify) => {
editForm.classify = classify
// 分类选择变化
const handleClassifyChange = () => {
if (editForm.id > 0 && editForm.type >= 0) {
// 直接保存
onSubmit()
}
}
// 新增分类
const handleAddClassify = async (categoryName) => {
try {
// 调用API创建分类
const response = await createCategory({
name: categoryName,
type: editForm.type
})
if (response.success) {
showToast('分类创建成功')
// 重新加载分类列表
await loadClassifyList(editForm.type)
editForm.classify = categoryName
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
}
}
// 清空分类
const clearClassify = () => {
editForm.classify = ''
showToast('已清空分类')
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
@@ -352,16 +262,4 @@ const handleCandidateSelect = (candidate) => {
</script>
<style scoped>
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
}
.classify-btn {
flex: 0 0 auto;
min-width: 70px;
border-radius: 16px;
}
</style>

View File

@@ -2,10 +2,8 @@
* 预算周期类型
*/
export const BudgetPeriodType = {
Week: 0,
Month: 1,
Year: 2,
Longterm: 3
Year: 2
}
/**

View File

@@ -17,15 +17,16 @@
</template>
</van-nav-bar>
<div class="scroll-content">
<div class="page-content">
<van-tabs v-model:active="activeTab" sticky offset-top="0" type="card">
<van-tab title="支出" :name="BudgetCategory.Expense">
<BudgetSummary
v-if="activeTab !== BudgetCategory.Savings"
:stats="overallStats"
:title="activeTabTitle"
:get-value-class="getValueClass"
/>
<van-tabs v-model:active="activeTab" type="card" class="budget-tabs">
<van-tab title="支出" :name="BudgetCategory.Expense">
<div class="scroll-content">
<div class="budget-list">
<template v-if="expenseBudgets?.length > 0">
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
@@ -66,14 +67,13 @@
</template>
<van-empty v-else description="暂无支出预算" />
</div>
<!-- 底部安全距离 -->
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
</div>
</van-tab>
<van-tab title="收入" :name="BudgetCategory.Income">
<BudgetSummary
:stats="overallStats"
:title="activeTabTitle"
:get-value-class="getValueClass"
/>
<div class="scroll-content">
<div class="budget-list">
<template v-if="incomeBudgets?.length > 0">
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
@@ -115,9 +115,13 @@
</template>
<van-empty v-else description="暂无收入预算" />
</div>
<!-- 底部安全距离 -->
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
</div>
</van-tab>
<van-tab title="存款" :name="BudgetCategory.Savings">
<div class="scroll-content">
<div class="budget-list">
<template v-if="savingsBudgets?.length > 0">
<van-swipe-cell v-for="budget in savingsBudgets" :key="budget.id">
@@ -162,11 +166,11 @@
</template>
<van-empty v-else description="暂无存款计划" />
</div>
</van-tab>
</van-tabs>
<!-- 底部安全距离 -->
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
</div>
</van-tab>
</van-tabs>
<!-- 添加/编辑预算弹窗 -->
<BudgetEditPopup
@@ -178,7 +182,6 @@
@success="fetchBudgetList"
/>
</div>
</div>
</template>
<script setup>
@@ -226,7 +229,6 @@ const overallStats = computed(() => {
}
return {
week: getStatsForType(BudgetPeriodType.Week),
month: getStatsForType(BudgetPeriodType.Month),
year: getStatsForType(BudgetPeriodType.Year)
}
@@ -274,10 +276,8 @@ const formatMoney = (val) => {
const getPeriodLabel = (type) => {
const map = {
[BudgetPeriodType.Week]: '本周',
[BudgetPeriodType.Month]: '本月',
[BudgetPeriodType.Year]: '本年',
[BudgetPeriodType.Longterm]: '长期'
[BudgetPeriodType.Year]: '本年'
}
return map[type] || '周期'
}
@@ -298,18 +298,11 @@ const getIncomeProgressColor = (budget) => {
const refDateMap = {}
const handleSwitchPeriod = async (budget, direction) => {
if (budget.type === BudgetPeriodType.Longterm) {
showToast('长期预算不支持切换周期')
return
}
// 获取或初始化该预算的参考日期
let currentRefDate = refDateMap[budget.id] || new Date()
const date = new Date(currentRefDate)
if (budget.type === BudgetPeriodType.Week) {
date.setDate(date.getDate() + direction * 7)
} else if (budget.type === BudgetPeriodType.Month) {
if (budget.type === BudgetPeriodType.Month) {
date.setMonth(date.getMonth() + direction)
} else if (budget.type === BudgetPeriodType.Year) {
date.setFullYear(date.getFullYear() + direction)
@@ -362,6 +355,27 @@ const handleToggleStop = async (budget) => {
</script>
<style scoped>
.budget-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
:deep(.van-tabs__content) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
:deep(.van-tab__panel) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.budget-list {
padding-top: 8px;
padding-bottom: 20px;

View File

@@ -192,36 +192,11 @@
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
type="success"
size="small"
class="classify-btn"
@click="addClassifyDialogRef.open()"
>
+ 新增
</van-button>
<van-button
v-for="item in classifyColumns"
: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
v-if="form.classify"
type="warning"
size="small"
class="classify-btn"
@click="clearClassify"
>
清空
</van-button>
</div>
<!-- 分类选择组件 -->
<ClassifySelector
v-model="form.classify"
:type="form.type"
/>
</van-cell-group>
</van-form>
<template #footer>
@@ -257,12 +232,6 @@
@cancel="showMonthDaysPicker = false"
/>
</van-popup>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddClassify"
/>
</div>
</template>
@@ -275,13 +244,11 @@ import {
deletePeriodic as deletePeriodicApi,
togglePeriodicEnabled
} from '@/api/transactionPeriodic'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
import PopupContainer from '@/components/PopupContainer.vue'
import AddClassifyDialog from '@/components/AddClassifyDialog.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
const router = useRouter()
const navTitle = ref('周期账单')
const addClassifyDialogRef = ref()
const periodicList = ref([])
const loading = ref(false)
@@ -299,9 +266,6 @@ const showPeriodicTypePicker = ref(false)
const showWeekdaysPicker = ref(false)
const showMonthDaysPicker = ref(false)
// 分类列表
const classifyColumns = ref([])
// 周期类型
const periodicTypeColumns = [
{ text: '每天', value: 0 },
@@ -459,24 +423,6 @@ const openAddDialog = () => {
isEdit.value = false
resetForm()
dialogVisible.value = true
// 加载分类列表
loadClassifyList(form.type)
}
// 加载分类列表
const loadClassifyList = async (type = null) => {
try {
const response = await getCategoryList(type)
if (response.success) {
classifyColumns.value = (response.data || []).map(item => ({
text: item.name,
value: item.name,
id: item.id
}))
}
} catch (error) {
console.error('加载分类列表出错:', error)
}
}
// 编辑
@@ -490,9 +436,6 @@ const editPeriodic = (item) => {
form.periodicType = item.periodicType
form.periodicTypeText = periodicTypeColumns.find(t => t.value === item.periodicType)?.text || ''
// 加载对应类型的分类列表
loadClassifyList(item.type)
// 解析周期配置
if (item.periodicConfig) {
switch (item.periodicType) {
@@ -607,40 +550,6 @@ const onMonthDaysConfirm = ({ selectedValues, selectedOptions }) => {
showMonthDaysPicker.value = false
}
// 选择分类
const selectClassify = (classify) => {
form.classify = classify
}
// 清空分类
const clearClassify = () => {
form.classify = ''
showToast('已清空分类')
}
// 新增分类
const handleAddClassify = async (categoryName) => {
try {
// 调用API创建分类
const response = await createCategory({
name: categoryName,
type: form.type
})
if (response.success) {
showToast('分类创建成功')
// 重新加载分类列表
await loadClassifyList(form.type)
form.classify = categoryName
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
}
}
</script>
<style scoped>
@@ -679,17 +588,4 @@ const handleAddClassify = async (categoryName) => {
height: 100%;
}
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
}
.classify-btn {
flex: 0 0 auto;
min-width: 70px;
border-radius: 16px;
}
</style>

View File

@@ -181,7 +181,6 @@ public class BudgetController(
factor = b.Type switch
{
BudgetPeriodType.Month => 12,
BudgetPeriodType.Week => 52,
BudgetPeriodType.Year => 1,
_ => 0
};
@@ -191,7 +190,6 @@ public class BudgetController(
factor = b.Type switch
{
BudgetPeriodType.Month => 1,
BudgetPeriodType.Week => 52m / 12m,
BudgetPeriodType.Year => 1m / 12m,
_ => 0
};

View File

@@ -35,10 +35,8 @@ public class BudgetDto
StartDate = entity.StartDate.ToString("yyyy-MM-dd"),
Period = entity.Type switch
{
BudgetPeriodType.Longterm => "长期",
BudgetPeriodType.Year => $"{start:yy}年",
BudgetPeriodType.Month => $"{start:yy}年第{start.Month}月",
BudgetPeriodType.Week => $"{start:yy}年第{System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(start, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday)}周",
_ => $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}"
},
PeriodStart = start,