Files
EmailBill/Web/src/views/TransactionsRecord.vue
孙诚 cbbb0c10cb
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 15s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s
页面样式统一
2025-12-29 16:07:43 +08:00

567 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-pull-refresh v-model="refreshing" @refresh="onRefresh">
<!-- 加载提示 -->
<van-loading v-if="loading && !(transactionList && transactionList.length)" vertical style="padding: 50px 0">
加载中...
</van-loading>
<!-- 交易记录列表 -->
<TransactionList
:transactions="transactionList"
:loading="loading"
:finished="finished"
@load="onLoad"
@click="viewDetail"
@delete="handleDelete"
/>
<!-- 底部安全距离 -->
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
</van-pull-refresh>
<!-- 详情/编辑弹出层 -->
<TransactionDetail
v-model:show="detailVisible"
:transaction="currentTransaction"
@save="onDetailSave"
/>
<!-- 新增交易记录弹出层 -->
<van-popup
v-model:show="addDialogVisible"
position="bottom"
:style="{ height: '85%' }"
round
closeable
>
<div class="popup-container">
<div class="popup-header-fixed">
<h3>手动录账单</h3>
</div>
<div class="popup-scroll-content">
<van-form @submit="onAddSubmit">
<van-cell-group inset title="基本信息">
<van-field
v-model="addForm.occurredAt"
is-link
readonly
name="occurredAt"
label="交易时间"
placeholder="请选择交易时间"
@click="showDateTimePicker = true"
:rules="[{ required: true, message: '请选择交易时间' }]"
/>
</van-cell-group>
<van-cell-group inset title="交易明细">
<van-field
v-model="addForm.reason"
name="reason"
label="交易摘要"
placeholder="请输入交易摘要"
type="textarea"
rows="2"
autosize
maxlength="200"
show-word-limit
/>
<van-field
v-model="addForm.amount"
name="amount"
label="交易金额"
placeholder="请输入交易金额"
type="number"
:rules="[{ required: true, message: '请输入交易金额' }]"
/>
<van-field
v-model="addForm.typeText"
is-link
readonly
name="type"
label="交易类型"
placeholder="请选择交易类型"
@click="showAddTypePicker = true"
:rules="[{ required: true, message: '请选择交易类型' }]"
/>
<van-field
v-model="addForm.classify"
is-link
readonly
name="classify"
label="交易分类"
placeholder="请选择或输入交易分类"
@click="showAddClassifyPicker = true"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit" :loading="addSubmitting">
确认添加
</van-button>
</div>
</van-form>
</div>
</div>
</van-popup>
<!-- 新增交易 - 日期时间选择器 -->
<van-popup v-model:show="showDateTimePicker" position="bottom" round>
<van-date-picker
v-model="dateTimeValue"
title="选择日期时间"
:min-date="new Date(2020, 0, 1)"
:max-date="new Date()"
@confirm="onDateTimeConfirm"
@cancel="showDateTimePicker = false"
/>
</van-popup>
<!-- 新增交易 - 交易类型选择器 -->
<van-popup v-model:show="showAddTypePicker" position="bottom" round>
<van-picker
show-toolbar
:columns="typeColumns"
@confirm="onAddTypeConfirm"
@cancel="showAddTypePicker = false"
/>
</van-popup>
<!-- 新增交易 - 交易分类选择器 -->
<van-popup v-model:show="showAddClassifyPicker" position="bottom" round>
<van-picker
ref="addClassifyPickerRef"
:columns="classifyColumns"
@confirm="onAddClassifyConfirm"
@cancel="showAddClassifyPicker = false"
>
<template #toolbar>
<div class="picker-toolbar">
<van-button class="toolbar-cancel" size="small" @click="clearAddClassify">清空</van-button>
<van-button class="toolbar-add" size="small" type="primary" @click="showAddClassify = true">新增</van-button>
<van-button class="toolbar-confirm" size="small" type="primary" @click="confirmAddClassify">确认</van-button>
</div>
</template>
</van-picker>
</van-popup>
<!-- 新增分类对话框 -->
<van-dialog
v-model:show="showAddClassify"
title="新增交易分类"
show-cancel-button
@confirm="addNewClassify"
>
<van-field v-model="newClassify" placeholder="请输入新的交易分类" />
</van-dialog>
<!-- 底部浮动搜索框 -->
<div class="floating-search">
<van-search
v-model="searchKeyword"
placeholder="搜索交易摘要、来源、卡号、分类"
@update:model-value="onSearchChange"
@clear="onSearchClear"
shape="round"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { showToast, showConfirmDialog } from 'vant'
import {
getTransactionList,
getTransactionDetail,
createTransaction,
deleteTransaction
} from '@/api/transactionRecord'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
const transactionList = ref([])
const loading = ref(false)
const refreshing = ref(false)
const finished = ref(false)
const pageIndex = ref(1)
const pageSize = 20
const total = ref(0)
const detailVisible = ref(false)
const currentTransaction = ref(null)
// 搜索相关
const searchKeyword = ref('')
let searchTimer = null
// 新增交易弹窗相关
const addDialogVisible = ref(false)
const addSubmitting = ref(false)
const showDateTimePicker = ref(false)
const dateTimeValue = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()])
const showAddTypePicker = ref(false)
const showAddClassifyPicker = ref(false)
const addClassifyPickerRef = ref(null)
const showAddClassify = ref(false)
const newClassify = ref('')
// 交易类型
const typeColumns = [
{ text: '支出', value: 0 },
{ text: '收入', value: 1 },
{ text: '不计入收支', value: 2 }
]
// 分类相关
const classifyColumns = ref([])
// 新增表单
const addForm = reactive({
occurredAt: '',
reason: '',
amount: '',
type: 0,
typeText: '',
classify: ''
})
// 加载分类列表
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 loadData = async (isRefresh = false) => {
if (isRefresh) {
pageIndex.value = 1
transactionList.value = []
finished.value = false
}
loading.value = true
try {
const params = {
pageIndex: pageIndex.value,
pageSize: pageSize
}
// 添加搜索关键词
if (searchKeyword.value) {
params.searchKeyword = searchKeyword.value
}
const response = await getTransactionList(params)
if (response.success) {
const newList = response.data || []
total.value = response.total || 0
if (isRefresh) {
transactionList.value = newList
} else {
transactionList.value = [...(transactionList.value || []), ...newList]
}
if (newList.length === 0 || newList.length < pageSize) {
finished.value = true
} else {
finished.value = false
pageIndex.value++
}
} else {
showToast(response.message || '加载数据失败')
finished.value = true
}
} catch (error) {
console.error('加载数据出错:', error)
showToast('加载数据出错: ' + (error.message || '未知错误'))
finished.value = true
} finally {
loading.value = false
refreshing.value = false
}
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
transactionList.value = []
loadData(false)
}
// 搜索相关方法
const onSearchChange = () => {
// 防抖处理用户停止输入500ms后自动搜索
if (searchTimer) {
clearTimeout(searchTimer)
}
searchTimer = setTimeout(() => {
onSearch()
}, 500)
}
const onSearch = () => {
// 重置分页状态并刷新数据
transactionList.value = []
finished.value = false
loadData(true)
}
const onSearchClear = () => {
searchKeyword.value = ''
onSearch()
}
// 加载更多
const onLoad = () => {
loadData(false)
}
// 查看详情
const viewDetail = async (transaction) => {
try {
const response = await getTransactionDetail(transaction.id)
if (response.success) {
currentTransaction.value = response.data
detailVisible.value = true
} else {
showToast(response.message || '获取详情失败')
}
} catch (error) {
console.error('获取详情出错:', error)
showToast('获取详情失败')
}
}
// 详情保存后的回调
const onDetailSave = async () => {
loadData(true)
// 重新加载分类列表
await loadClassifyList()
}
// 删除
const handleDelete = async (transaction) => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要删除这条交易记录吗?',
})
const response = await deleteTransaction(transaction.id)
if (response.success) {
showToast('删除成功')
loadData(true)
} else {
showToast(response.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除出错:', error)
showToast('删除失败')
}
}
}
// 打开新增弹窗
const openAddDialog = () => {
// 重置表单
addForm.occurredAt = ''
addForm.reason = ''
addForm.amount = ''
addForm.type = 0
addForm.typeText = ''
addForm.classify = ''
// 设置默认日期时间为当前时间
const now = new Date()
dateTimeValue.value = [now.getFullYear(), now.getMonth() + 1, now.getDate()]
addForm.occurredAt = formatDateForSubmit(now)
addDialogVisible.value = true
}
// 格式化日期用于提交
const formatDateForSubmit = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 日期时间选择确认
const onDateTimeConfirm = ({ selectedValues }) => {
const date = new Date(selectedValues[0], selectedValues[1] - 1, selectedValues[2])
addForm.occurredAt = formatDateForSubmit(date)
showDateTimePicker.value = false
}
// 新增交易 - 交易类型选择确认
const onAddTypeConfirm = ({ selectedValues, selectedOptions }) => {
addForm.type = selectedValues[0]
addForm.typeText = selectedOptions[0].text
showAddTypePicker.value = false
}
// 新增交易 - 交易分类选择确认
const onAddClassifyConfirm = ({ selectedOptions }) => {
if (selectedOptions && selectedOptions[0]) {
addForm.classify = selectedOptions[0].text
}
showAddClassifyPicker.value = false
}
// 新增交易 - 清空分类
const clearAddClassify = () => {
addForm.classify = ''
showAddClassifyPicker.value = false
showToast('已清空分类')
}
// 新增交易 - 确认分类(从 picker 中获取选中值)
const confirmAddClassify = () => {
if (addClassifyPickerRef.value) {
const selectedValues = addClassifyPickerRef.value.getSelectedOptions()
if (selectedValues && selectedValues[0]) {
addForm.classify = selectedValues[0].text
}
}
showAddClassifyPicker.value = false
}
// 新增分类
const addNewClassify = async () => {
if (!newClassify.value.trim()) {
showToast('请输入分类名称')
return
}
try {
const response = await createCategory({
name: newClassify.value.trim(),
type: addForm.type
})
if (response.success) {
showToast('新增分类成功')
newClassify.value = ''
// 重新加载分类列表
await loadClassifyList(addForm.type)
// 设置为新增的分类
addForm.classify = response.data.name
} else {
showToast(response.message || '新增分类失败')
}
} catch (error) {
console.error('新增分类失败:', error)
showToast('新增分类失败')
}
}
// 提交新增交易
const onAddSubmit = async () => {
try {
addSubmitting.value = true
const data = {
occurredAt: addForm.occurredAt,
reason: addForm.reason,
amount: parseFloat(addForm.amount),
type: addForm.type,
classify: addForm.classify || null
}
const response = await createTransaction(data)
if (response.success) {
showToast('添加成功')
addDialogVisible.value = false
loadData(true)
// 重新加载分类列表
await loadClassifyList()
} else {
showToast(response.message || '添加失败')
}
} catch (error) {
console.error('添加出错:', error)
showToast('添加失败: ' + (error.message || '未知错误'))
} finally {
addSubmitting.value = false
}
}
onMounted(async () => {
await loadClassifyList()
// 不需要手动调用 loadDatavan-list 会自动触发 onLoad
})
// 暴露给父级方法调用
defineExpose({
openAddDialog
})
</script>
<style scoped>
:deep(.van-pull-refresh) {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.floating-search {
position: fixed;
bottom: 90px;
left: 0;
right: 0;
z-index: 999;
padding: 8px 16px;
background: transparent;
pointer-events: none;
}
.floating-search :deep(.van-search) {
pointer-events: auto;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
border-radius: 20px;
border: none;
}
.picker-toolbar {
display: flex;
width: 100%;
align-items: center;
padding: 5px 10px;
border-bottom: 1px solid #ebedf0;
}
.toolbar-cancel {
margin-right: auto;
}
.toolbar-confirm {
margin-left: auto;
}
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>