first commot
This commit is contained in:
226
Web/src/views/CalendarView.vue
Normal file
226
Web/src/views/CalendarView.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="calendar-container">
|
||||
<van-calendar
|
||||
title="日历"
|
||||
:poppable="false"
|
||||
:show-confirm="false"
|
||||
:formatter="formatterCalendar"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@month-show="onMonthShow"
|
||||
@select="onDateSelect"
|
||||
/>
|
||||
|
||||
<!-- 日期交易列表弹出层 -->
|
||||
<van-popup
|
||||
v-model:show="listVisible"
|
||||
position="bottom"
|
||||
:style="{ height: '85%' }"
|
||||
round
|
||||
closeable
|
||||
>
|
||||
<div class="date-transactions">
|
||||
<div class="popup-header">
|
||||
<h3>{{ selectedDateText }}</h3>
|
||||
<p v-if="dateTransactions.length">共 {{ dateTransactions.length }} 笔交易</p>
|
||||
</div>
|
||||
|
||||
<TransactionList
|
||||
:transactions="dateTransactions"
|
||||
:loading="listLoading"
|
||||
:finished="true"
|
||||
:show-delete="false"
|
||||
@click="viewDetail"
|
||||
/>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 交易详情组件 -->
|
||||
<TransactionDetail
|
||||
v-model:show="detailVisible"
|
||||
:transaction="currentTransaction"
|
||||
@save="onDetailSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import request from '@/api/request'
|
||||
import { getTransactionDetail, getTransactionsByDate } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
|
||||
const dailyStatistics = ref({})
|
||||
const listVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const dateTransactions = ref([])
|
||||
const currentTransaction = ref(null)
|
||||
const listLoading = ref(false)
|
||||
const selectedDate = ref(null)
|
||||
const selectedDateText = ref('')
|
||||
|
||||
// 设置日历可选范围(例如:过去2年到未来1年)
|
||||
const minDate = new Date(new Date().getFullYear() - 2, 0, 1) // 2年前的1月1日
|
||||
const maxDate = new Date(new Date().getFullYear() + 1, 11, 31) // 明年12月31日
|
||||
|
||||
// 获取日历统计数据
|
||||
const fetchDailyStatistics = async (year, month) => {
|
||||
try {
|
||||
const response = await request.get('/TransactionRecord/GetDailyStatistics', {
|
||||
params: { year, month }
|
||||
})
|
||||
if (response.success && response.data) {
|
||||
// 将数组转换为对象,key为日期
|
||||
const statsMap = {}
|
||||
response.data.forEach(item => {
|
||||
statsMap[item.date] = {
|
||||
count: item.count,
|
||||
amount: item.amount
|
||||
}
|
||||
})
|
||||
dailyStatistics.value = {
|
||||
...dailyStatistics.value,
|
||||
...statsMap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日历统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取指定日期的交易列表
|
||||
const fetchDateTransactions = async (date) => {
|
||||
try {
|
||||
listLoading.value = true
|
||||
const dateStr = date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-')
|
||||
|
||||
const response = await getTransactionsByDate(dateStr)
|
||||
|
||||
if (response.success && response.data) {
|
||||
dateTransactions.value = response.data
|
||||
} else {
|
||||
dateTransactions.value = []
|
||||
showToast(response.message || '获取交易列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日期交易列表失败:', error)
|
||||
dateTransactions.value = []
|
||||
showToast('获取交易列表失败')
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 当月份显示时触发
|
||||
const onMonthShow = ({ date }) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
fetchDailyStatistics(year, month)
|
||||
}
|
||||
|
||||
// 日期选择事件
|
||||
const onDateSelect = (date) => {
|
||||
selectedDate.value = date
|
||||
selectedDateText.value = formatSelectedDate(date)
|
||||
fetchDateTransactions(date)
|
||||
listVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化选中的日期
|
||||
const formatSelectedDate = (date) => {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
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 = () => {
|
||||
// 重新加载当前日期的交易列表
|
||||
if (selectedDate.value) {
|
||||
fetchDateTransactions(selectedDate.value)
|
||||
}
|
||||
|
||||
// 重新加载当前月份的统计数据
|
||||
const now = selectedDate.value || new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
}
|
||||
|
||||
const formatterCalendar = (day) => {
|
||||
const dayCopy = { ...day };
|
||||
if (dayCopy.date.toDateString() === new Date().toDateString()) {
|
||||
dayCopy.text = '今天';
|
||||
}
|
||||
|
||||
// 格式化日期为 yyyy-MM-dd
|
||||
const dateKey = dayCopy.date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-');
|
||||
const stats = dailyStatistics.value[dateKey]
|
||||
|
||||
if (stats) {
|
||||
dayCopy.topInfo = `${stats.count}笔` // 展示消费笔数
|
||||
dayCopy.bottomInfo = `${stats.amount.toFixed(1)}元` // 展示消费金额
|
||||
}
|
||||
|
||||
return dayCopy;
|
||||
};
|
||||
|
||||
// 初始加载当前月份数据
|
||||
const now = new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-container :deep(.van-calendar) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.date-transactions {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
}
|
||||
|
||||
.popup-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popup-header p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #969799;
|
||||
}
|
||||
</style>
|
||||
474
Web/src/views/EmailRecord.vue
Normal file
474
Web/src/views/EmailRecord.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<template>
|
||||
<div class="email-record-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar title="邮件记录" fixed placeholder>
|
||||
<template #right>
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="handleSync"
|
||||
>
|
||||
立即同步
|
||||
</van-button>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="refresh-wrapper">
|
||||
<!-- 加载提示 -->
|
||||
<van-loading v-if="loading && !(emailList && emailList.length)" vertical style="padding: 50px 0">
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
<!-- 邮件列表 -->
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell-group v-if="emailList && emailList.length" inset style="margin-top: 10px">
|
||||
<van-swipe-cell
|
||||
v-for="email in emailList"
|
||||
:key="email.id"
|
||||
>
|
||||
<van-cell
|
||||
:title="email.subject"
|
||||
:label="`来自: ${email.from}`"
|
||||
is-link
|
||||
@click="viewDetail(email)"
|
||||
>
|
||||
<template #value>
|
||||
<div class="email-info">
|
||||
<div class="email-date">{{ formatDate(email.receivedDate) }}</div>
|
||||
<div class="bill-count" v-if="email.transactionCount > 0">
|
||||
<span style="font-size: 12px;">已解析{{ email.transactionCount }}条账单</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
text="删除"
|
||||
class="delete-button"
|
||||
@click="handleDelete(email)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty
|
||||
v-if="!loading && !(emailList && emailList.length)"
|
||||
description="暂无邮件记录"
|
||||
/>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 详情弹出层 -->
|
||||
<van-popup
|
||||
v-model:show="detailVisible"
|
||||
position="bottom"
|
||||
:style="{ height: '80%' }"
|
||||
round
|
||||
closeable
|
||||
>
|
||||
<div class="email-detail" v-if="currentEmail">
|
||||
<div class="detail-header" style="margin-top: 10px; margin-left: 10px;">
|
||||
<h3>{{ currentEmail.Subject || currentEmail.subject || '(无主题)' }}</h3>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="发件人" :value="currentEmail.From || currentEmail.from || '未知'" />
|
||||
<van-cell title="接收时间" :value="formatDate(currentEmail.ReceivedDate || currentEmail.receivedDate)" />
|
||||
<van-cell title="记录时间" :value="formatDate(currentEmail.CreateTime || currentEmail.createTime)" />
|
||||
<van-cell
|
||||
title="已解析账单数"
|
||||
:value="`${currentEmail.TransactionCount || currentEmail.transactionCount || 0}条`"
|
||||
is-link
|
||||
@click="viewTransactions"
|
||||
v-if="(currentEmail.TransactionCount || currentEmail.transactionCount || 0) > 0"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<div class="email-content">
|
||||
<h4 style="margin-left: 10px;">邮件内容</h4>
|
||||
<div
|
||||
v-if="currentEmail.htmlBody"
|
||||
v-html="currentEmail.htmlBody"
|
||||
class="content-body html-content"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="currentEmail.body"
|
||||
class="content-body"
|
||||
>
|
||||
{{ currentEmail.body }}
|
||||
</div>
|
||||
<div v-else class="content-body empty-content">
|
||||
暂无邮件内容
|
||||
<div style="font-size: 12px; margin-top: 8px; color: #999;">
|
||||
Debug: {{ Object.keys(currentEmail).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin: 16px;">
|
||||
<van-button
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
:loading="refreshingAnalysis"
|
||||
@click="handleRefreshAnalysis"
|
||||
>
|
||||
重新分析
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 账单列表弹出层 -->
|
||||
<van-popup
|
||||
v-model:show="transactionListVisible"
|
||||
position="bottom"
|
||||
:style="{ height: '70%' }"
|
||||
round
|
||||
closeable
|
||||
>
|
||||
<div class="transaction-list-popup">
|
||||
<div class="list-header">
|
||||
<h3 style="margin: 16px;">关联账单列表</h3>
|
||||
</div>
|
||||
<TransactionList
|
||||
:transactions="transactionList"
|
||||
:loading="false"
|
||||
:finished="true"
|
||||
:show-delete="false"
|
||||
@click="handleTransactionClick"
|
||||
/>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 账单详情编辑弹出层 -->
|
||||
<TransactionDetail
|
||||
:show="transactionDetailVisible"
|
||||
:transaction="currentTransaction"
|
||||
@update:show="transactionDetailVisible = $event"
|
||||
@save="handleTransactionSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getEmailList, getEmailDetail, deleteEmail, refreshTransactionRecords, syncEmails, getEmailTransactions } from '@/api/emailRecord'
|
||||
import { getTransactionDetail } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
|
||||
const emailList = ref([])
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const finished = ref(false)
|
||||
const lastId = ref(null) // 游标分页:记录最后一条记录的ID
|
||||
const lastTime = ref(null) // 游标分页:记录最后一条记录的时间
|
||||
const total = ref(0)
|
||||
const detailVisible = ref(false)
|
||||
const currentEmail = ref(null)
|
||||
const refreshingAnalysis = ref(false)
|
||||
const syncing = ref(false)
|
||||
const transactionListVisible = ref(false)
|
||||
const transactionList = ref([])
|
||||
const transactionDetailVisible = ref(false)
|
||||
const currentTransaction = ref(null)
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (loading.value) return // 防止重复加载
|
||||
|
||||
if (isRefresh) {
|
||||
lastId.value = null
|
||||
lastTime.value = null
|
||||
emailList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
if (lastTime.value && lastId.value) {
|
||||
params.lastReceivedDate = lastTime.value
|
||||
params.lastId = lastId.value
|
||||
}
|
||||
|
||||
const response = await getEmailList(params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data || []
|
||||
total.value = response.total || 0
|
||||
const newLastId = response.lastId || 0
|
||||
const newLastTime = response.lastTime
|
||||
|
||||
if (isRefresh) {
|
||||
emailList.value = newList
|
||||
} else {
|
||||
emailList.value = [...(emailList.value || []), ...newList]
|
||||
}
|
||||
|
||||
// 更新游标
|
||||
if (newLastId > 0 && newLastTime) {
|
||||
lastId.value = newLastId
|
||||
lastTime.value = newLastTime
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据(返回数据少于20条或为空,说明没有更多了)
|
||||
if (newList.length === 0 || newList.length < 20) {
|
||||
finished.value = true
|
||||
} else {
|
||||
finished.value = false
|
||||
}
|
||||
} 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 = () => {
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const onLoad = () => {
|
||||
if (!finished.value && !loading.value) {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = async (email) => {
|
||||
try {
|
||||
const response = await getEmailDetail(email.id)
|
||||
console.log('详情 API 返回:', response)
|
||||
if (response.success) {
|
||||
currentEmail.value = response.data
|
||||
console.log('currentEmail:', currentEmail.value)
|
||||
detailVisible.value = true
|
||||
} else {
|
||||
showToast(response.message || '获取详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情出错:', error)
|
||||
showToast('获取详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (email) => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要删除这封邮件吗?',
|
||||
})
|
||||
|
||||
const response = await deleteEmail(email.id)
|
||||
if (response.success) {
|
||||
showToast('删除成功')
|
||||
loadData(true)
|
||||
} else {
|
||||
showToast(response.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除出错:', error)
|
||||
showToast('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新分析
|
||||
const handleRefreshAnalysis = async () => {
|
||||
if (!currentEmail.value) return
|
||||
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要重新分析该邮件并刷新交易记录吗?',
|
||||
})
|
||||
|
||||
refreshingAnalysis.value = true
|
||||
const response = await refreshTransactionRecords(currentEmail.value.id || currentEmail.value.Id)
|
||||
|
||||
if (response.success) {
|
||||
showToast('重新分析成功')
|
||||
detailVisible.value = false
|
||||
} else {
|
||||
showToast(response.message || '重新分析失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('重新分析出错:', error)
|
||||
showToast('重新分析失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
} finally {
|
||||
refreshingAnalysis.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 立即同步
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
syncing.value = true
|
||||
const response = await syncEmails()
|
||||
|
||||
if (response.success) {
|
||||
showToast(response.message || '同步成功')
|
||||
// 同步成功后刷新列表
|
||||
loadData(true)
|
||||
} else {
|
||||
showToast(response.message || '同步失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('同步出错:', error)
|
||||
showToast('同步失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看关联的账单列表
|
||||
const viewTransactions = async () => {
|
||||
if (!currentEmail.value) return
|
||||
|
||||
try {
|
||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||
const response = await getEmailTransactions(emailId)
|
||||
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
transactionListVisible.value = true
|
||||
} else {
|
||||
showToast(response.message || '获取账单列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账单列表出错:', error)
|
||||
showToast('获取账单列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理点击账单
|
||||
const handleTransactionClick = async (transaction) => {
|
||||
try {
|
||||
const response = await getTransactionDetail(transaction.id)
|
||||
if (response.success) {
|
||||
currentTransaction.value = response.data
|
||||
transactionDetailVisible.value = true
|
||||
} else {
|
||||
showToast(response.message || '获取账单详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账单详情出错:', error)
|
||||
showToast('获取账单详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 账单保存后刷新列表
|
||||
const handleTransactionSave = async () => {
|
||||
// 刷新账单列表
|
||||
if (currentEmail.value) {
|
||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||
const response = await getEmailTransactions(emailId)
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.email-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bill-count {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.email-date {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.transaction-list-popup {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.email-content h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.content-body {
|
||||
background-color: #2c2c2c;
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
134
Web/src/views/SettingView.vue
Normal file
134
Web/src/views/SettingView.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-nav-bar title="设置" />
|
||||
<div class="detail-header">
|
||||
<p>账单导入</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="从支付宝导入" is-link @click="handleImportClick('Alipay')" />
|
||||
<van-cell title="从微信导入" is-link @click="handleImportClick('WeChat')" />
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 隐藏的文件选择器 -->
|
||||
<input ref="fileInputRef" type="file" accept=".csv,.xlsx,.xls" style="display: none" @change="handleFileChange" />
|
||||
|
||||
<div class="detail-header">
|
||||
<p>账单处理</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="智能分类" is-link />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { showLoadingToast, showSuccessToast, showToast, closeToast } from 'vant'
|
||||
import { uploadBillFile } from '@/api/billImport'
|
||||
|
||||
const fileInputRef = ref(null)
|
||||
const currentType = ref('')
|
||||
|
||||
/**
|
||||
* 处理导入按钮点击
|
||||
*/
|
||||
const handleImportClick = (type) => {
|
||||
currentType.value = type
|
||||
// 触发文件选择
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件选择
|
||||
*/
|
||||
const handleFileChange = async (event) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
const validTypes = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
if (!validTypes.includes(file.type)) {
|
||||
showToast('请选择 CSV 或 Excel 文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(限制为 10MB)
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
showToast('文件大小不能超过 10MB')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载提示
|
||||
showLoadingToast({
|
||||
message: '上传中...',
|
||||
forbidClick: true,
|
||||
duration: 0
|
||||
})
|
||||
|
||||
// 上传文件
|
||||
const typeName = currentType.value === 'Alipay' ? '支付宝' : '微信'
|
||||
const { success, message } = await uploadBillFile(file, currentType.value)
|
||||
|
||||
if (!success) {
|
||||
showToast(message || `${typeName}账单导入失败`)
|
||||
return
|
||||
}
|
||||
|
||||
showSuccessToast(message || `${typeName}账单导入成功`)
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
showToast('上传失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
finally {
|
||||
closeToast()
|
||||
// 清空文件输入,允许重复选择同一文件
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 设置页面容器背景色 */
|
||||
:deep(.van-nav-bar) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 页面背景色 */
|
||||
:deep(body) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(body) {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* 增加卡片对比度 */
|
||||
:deep(.van-cell-group--inset) {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.van-cell-group--inset) {
|
||||
background-color: #2c2c2c;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
padding: 16px 16px 5px 16px;
|
||||
}
|
||||
|
||||
.detail-header p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #969799;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
607
Web/src/views/TransactionsRecord.vue
Normal file
607
Web/src/views/TransactionsRecord.vue
Normal file
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div class="transaction-record-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar title="交易记录" fixed placeholder style="z-index: 9999;">
|
||||
<template #right>
|
||||
<van-button type="primary" size="small" @click="openAddDialog">
|
||||
手动录账
|
||||
</van-button>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="refresh-wrapper">
|
||||
<!-- 加载提示 -->
|
||||
<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"
|
||||
/>
|
||||
</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="transaction-detail">
|
||||
<div class="detail-header" style="padding-top: 10px; padding-left: 10px;">
|
||||
<h3>手动录账单</h3>
|
||||
</div>
|
||||
|
||||
<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-field
|
||||
v-model="addForm.subClassify"
|
||||
is-link
|
||||
readonly
|
||||
name="subClassify"
|
||||
label="交易子分类"
|
||||
placeholder="请选择或输入交易子分类"
|
||||
@click="showAddSubClassifyPicker = 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>
|
||||
</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-popup v-model:show="showAddSubClassifyPicker" position="bottom" round>
|
||||
<van-picker
|
||||
ref="addSubClassifyPickerRef"
|
||||
:columns="subClassifyColumns"
|
||||
@confirm="onAddSubClassifyConfirm"
|
||||
@cancel="showAddSubClassifyPicker = false"
|
||||
>
|
||||
<template #toolbar>
|
||||
<div class="picker-toolbar">
|
||||
<van-button class="toolbar-cancel" size="small" @click="clearAddSubClassify">清空</van-button>
|
||||
<van-button class="toolbar-add" size="small" type="primary" @click="showAddSubClassify = true">新增</van-button>
|
||||
<van-button class="toolbar-confirm" size="small" type="primary" @click="confirmAddSubClassify">确认</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-picker>
|
||||
</van-popup>
|
||||
|
||||
<!-- 底部浮动搜索框 -->
|
||||
<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 { getCategoryTree } 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 lastId = ref(null)
|
||||
const lastTime = ref(null)
|
||||
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 showAddSubClassifyPicker = ref(false)
|
||||
const addClassifyPickerRef = ref(null)
|
||||
const addSubClassifyPickerRef = ref(null)
|
||||
|
||||
// 交易类型
|
||||
const typeColumns = [
|
||||
{ text: '支出', value: 0 },
|
||||
{ text: '收入', value: 1 },
|
||||
{ text: '不计入收支', value: 2 }
|
||||
]
|
||||
|
||||
// 分类相关
|
||||
const classifyColumns = ref([])
|
||||
const subClassifyColumns = ref([])
|
||||
|
||||
// 新增表单
|
||||
const addForm = reactive({
|
||||
occurredAt: '',
|
||||
reason: '',
|
||||
amount: '',
|
||||
type: 0,
|
||||
typeText: '',
|
||||
classify: '',
|
||||
subClassify: ''
|
||||
})
|
||||
|
||||
// 加载分类列表(从分类树中提取)
|
||||
const loadClassifyList = async (type = null) => {
|
||||
try {
|
||||
const response = await getCategoryTree(type)
|
||||
if (response.success) {
|
||||
// 从树形结构中提取分类名称(Level 2)
|
||||
classifyColumns.value = (response.data || []).map(item => ({
|
||||
text: item.name,
|
||||
value: item.name,
|
||||
id: item.id,
|
||||
children: item.children || []
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分类列表出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载子分类列表(根据选中的分类)
|
||||
const loadSubClassifyList = async (classifyName) => {
|
||||
try {
|
||||
// 从已加载的分类树中查找对应的子分类
|
||||
const classifyItem = classifyColumns.value.find(item => item.value === classifyName)
|
||||
if (classifyItem && classifyItem.children) {
|
||||
subClassifyColumns.value = classifyItem.children.map(child => ({
|
||||
text: child.name,
|
||||
value: child.name,
|
||||
id: child.id
|
||||
}))
|
||||
} else {
|
||||
subClassifyColumns.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子分类列表出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
lastId.value = null
|
||||
lastTime.value = null
|
||||
transactionList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
if (lastTime.value && lastId.value) {
|
||||
params.lastOccurredAt = lastTime.value
|
||||
params.lastId = lastId.value
|
||||
}
|
||||
|
||||
// 添加搜索关键词
|
||||
if (searchKeyword.value) {
|
||||
params.searchKeyword = searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await getTransactionList(params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data || []
|
||||
total.value = response.total || 0
|
||||
const newLastId = response.lastId || 0
|
||||
const newLastTime = response.lastTime
|
||||
|
||||
if (isRefresh) {
|
||||
transactionList.value = newList
|
||||
} else {
|
||||
transactionList.value = [...(transactionList.value || []), ...newList]
|
||||
}
|
||||
|
||||
if (newLastId > 0 && newLastTime) {
|
||||
lastId.value = newLastId
|
||||
lastTime.value = newLastTime
|
||||
}
|
||||
|
||||
if (newList.length === 0 || newList.length < 20) {
|
||||
finished.value = true
|
||||
} else {
|
||||
finished.value = false
|
||||
}
|
||||
} 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
|
||||
lastId.value = null
|
||||
transactionList.value = []
|
||||
loadData(false)
|
||||
}
|
||||
|
||||
// 搜索相关方法
|
||||
const onSearchChange = () => {
|
||||
// 防抖处理,用户停止输入500ms后自动搜索
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer)
|
||||
}
|
||||
searchTimer = setTimeout(() => {
|
||||
onSearch()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
// 重置分页状态并刷新数据
|
||||
lastId.value = null
|
||||
lastTime.value = null
|
||||
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 = ''
|
||||
addForm.subClassify = ''
|
||||
|
||||
// 设置默认日期时间为当前时间
|
||||
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 = async ({ selectedOptions }) => {
|
||||
if (selectedOptions && selectedOptions[0]) {
|
||||
addForm.classify = selectedOptions[0].text
|
||||
// 加载对应的子分类
|
||||
await loadSubClassifyList(selectedOptions[0].value)
|
||||
}
|
||||
showAddClassifyPicker.value = false
|
||||
}
|
||||
|
||||
// 新增交易 - 交易子分类选择确认
|
||||
const onAddSubClassifyConfirm = ({ selectedOptions }) => {
|
||||
if (selectedOptions && selectedOptions[0]) {
|
||||
addForm.subClassify = selectedOptions[0].text
|
||||
}
|
||||
showAddSubClassifyPicker.value = false
|
||||
}
|
||||
|
||||
// 新增交易 - 清空分类
|
||||
const clearAddClassify = () => {
|
||||
addForm.classify = ''
|
||||
showAddClassifyPicker.value = false
|
||||
showToast('已清空分类')
|
||||
}
|
||||
|
||||
// 新增交易 - 清空子分类
|
||||
const clearAddSubClassify = () => {
|
||||
addForm.subClassify = ''
|
||||
showAddSubClassifyPicker.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
|
||||
}
|
||||
|
||||
// 新增交易 - 确认子分类(从 picker 中获取选中值)
|
||||
const confirmAddSubClassify = () => {
|
||||
if (addSubClassifyPickerRef.value) {
|
||||
const selectedValues = addSubClassifyPickerRef.value.getSelectedOptions()
|
||||
if (selectedValues && selectedValues[0]) {
|
||||
addForm.subClassify = selectedValues[0].text
|
||||
}
|
||||
}
|
||||
showAddSubClassifyPicker.value = false
|
||||
}
|
||||
|
||||
// 提交新增交易
|
||||
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,
|
||||
subClassify: addForm.subClassify || 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()
|
||||
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/styles/common.css';
|
||||
|
||||
.floating-search {
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user