fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 33s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 33s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
681
Web/src/components/Transaction/TransactionDetailSheet.vue
Normal file
681
Web/src/components/Transaction/TransactionDetailSheet.vue
Normal file
@@ -0,0 +1,681 @@
|
||||
<template>
|
||||
<van-popup
|
||||
v-model:show="visible"
|
||||
position="bottom"
|
||||
:style="{ height: 'auto', maxHeight: '85%', borderTopLeftRadius: '16px', borderTopRightRadius: '16px' }"
|
||||
teleport="body"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="transaction-detail-sheet">
|
||||
<!-- 头部 -->
|
||||
<div class="sheet-header">
|
||||
<div class="header-title">
|
||||
交易详情
|
||||
</div>
|
||||
<van-icon
|
||||
name="cross"
|
||||
class="header-close"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 金额区域 -->
|
||||
<div class="amount-section">
|
||||
<div class="amount-label">
|
||||
金额
|
||||
</div>
|
||||
<!-- 只读显示模式 -->
|
||||
<div
|
||||
v-if="!isEditingAmount"
|
||||
class="amount-value"
|
||||
@click="startEditAmount"
|
||||
>
|
||||
¥ {{ formatAmount(parseFloat(editForm.amount) || 0) }}
|
||||
</div>
|
||||
<!-- 编辑模式 -->
|
||||
<div v-else class="amount-input-wrapper">
|
||||
<span class="currency-symbol">¥</span>
|
||||
<input
|
||||
ref="amountInputRef"
|
||||
v-model="editForm.amount"
|
||||
type="number"
|
||||
inputmode="decimal"
|
||||
class="amount-input"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@blur="finishEditAmount"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单字段 -->
|
||||
<div class="form-section">
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
时间
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showDatePicker = true"
|
||||
>
|
||||
{{ formatDateTime(editForm.occurredAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row no-border">
|
||||
<div class="form-label">
|
||||
备注
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<input
|
||||
v-model="editForm.reason"
|
||||
type="text"
|
||||
class="reason-input"
|
||||
placeholder="请输入备注"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
类型
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<van-radio-group
|
||||
v-model="editForm.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio
|
||||
:name="0"
|
||||
class="type-radio"
|
||||
>
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="1"
|
||||
class="type-radio"
|
||||
>
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="2"
|
||||
class="type-radio"
|
||||
>
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
分类
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showClassifySelector = !showClassifySelector"
|
||||
>
|
||||
<span v-if="editForm.classify">{{ editForm.classify }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="placeholder"
|
||||
>请选择分类</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分类选择器(展开/收起) -->
|
||||
<div
|
||||
v-if="showClassifySelector"
|
||||
class="classify-section"
|
||||
>
|
||||
<ClassifySelector
|
||||
v-model="editForm.classify"
|
||||
:type="editForm.type"
|
||||
:show-add="false"
|
||||
:show-clear="false"
|
||||
:show-all="false"
|
||||
@change="handleClassifyChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-section">
|
||||
<van-button
|
||||
class="delete-btn"
|
||||
:loading="deleting"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</van-button>
|
||||
<van-button
|
||||
class="save-btn"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
保存
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期时间选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-datetime-picker
|
||||
v-model="currentDateTime"
|
||||
type="datetime"
|
||||
title="选择日期时间"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="handleDateTimeConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { showToast, showDialog } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||
import {
|
||||
updateTransaction,
|
||||
deleteTransaction
|
||||
} from '@/api/transactionRecord'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
transaction: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'save', 'delete'])
|
||||
|
||||
const visible = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
const showClassifySelector = ref(false)
|
||||
const isEditingAmount = ref(false)
|
||||
|
||||
// 金额输入框引用
|
||||
const amountInputRef = ref(null)
|
||||
|
||||
// 日期时间选择器配置
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date(2030, 11, 31)
|
||||
const currentDateTime = ref(new Date())
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
id: 0,
|
||||
amount: 0,
|
||||
type: 0,
|
||||
classify: '',
|
||||
occurredAt: '',
|
||||
reason: ''
|
||||
})
|
||||
|
||||
// 监听 props 变化
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
visible.value = newVal
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.transaction,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 填充表单数据
|
||||
editForm.id = newVal.id
|
||||
editForm.amount = newVal.amount
|
||||
editForm.type = newVal.type
|
||||
editForm.classify = newVal.classify || ''
|
||||
editForm.occurredAt = newVal.occurredAt
|
||||
editForm.reason = newVal.reason || ''
|
||||
|
||||
// 初始化日期时间
|
||||
if (newVal.occurredAt) {
|
||||
currentDateTime.value = new Date(newVal.occurredAt)
|
||||
}
|
||||
|
||||
// 收起分类选择器
|
||||
showClassifySelector.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:show', newVal)
|
||||
if (!newVal) {
|
||||
// 关闭时收起分类选择器
|
||||
showClassifySelector.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化金额显示
|
||||
const formatAmount = (amount) => {
|
||||
return Number(amount).toFixed(2)
|
||||
}
|
||||
|
||||
// 开始编辑金额
|
||||
const startEditAmount = () => {
|
||||
isEditingAmount.value = true
|
||||
// 自动聚焦输入框
|
||||
setTimeout(() => {
|
||||
amountInputRef.value?.focus()
|
||||
// 选中所有文本,方便用户直接输入新值
|
||||
amountInputRef.value?.select()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 完成编辑金额
|
||||
const finishEditAmount = () => {
|
||||
// 验证并格式化金额
|
||||
const parsed = parseFloat(editForm.amount)
|
||||
editForm.amount = isNaN(parsed) || parsed < 0 ? 0 : parsed
|
||||
isEditingAmount.value = false
|
||||
}
|
||||
|
||||
// 格式化日期时间显示
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) {return ''}
|
||||
return dayjs(dateTime).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
// 类型切换
|
||||
const handleTypeChange = () => {
|
||||
// 切换类型时清空分类,让用户重新选择
|
||||
editForm.classify = ''
|
||||
showClassifySelector.value = false
|
||||
}
|
||||
|
||||
// 分类选择变化 - 自动保存
|
||||
const handleClassifyChange = async () => {
|
||||
if (editForm.id > 0 && editForm.classify) {
|
||||
await handleSave()
|
||||
}
|
||||
}
|
||||
|
||||
// 日期时间确认
|
||||
const handleDateTimeConfirm = (value) => {
|
||||
editForm.occurredAt = dayjs(value).format('YYYY-MM-DDTHH:mm:ss')
|
||||
currentDateTime.value = value
|
||||
showDatePicker.value = false
|
||||
}
|
||||
|
||||
// 保存修改
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!editForm.amount || editForm.amount <= 0) {
|
||||
showToast('请输入有效金额')
|
||||
return
|
||||
}
|
||||
|
||||
if (!editForm.classify) {
|
||||
showToast('请选择分类')
|
||||
return
|
||||
}
|
||||
|
||||
if (!editForm.occurredAt) {
|
||||
showToast('请选择交易时间')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
|
||||
const data = {
|
||||
id: editForm.id,
|
||||
amount: editForm.amount,
|
||||
type: editForm.type,
|
||||
classify: editForm.classify,
|
||||
occurredAt: editForm.occurredAt,
|
||||
reason: editForm.reason
|
||||
}
|
||||
|
||||
const response = await updateTransaction(data)
|
||||
if (response.success) {
|
||||
showToast('保存成功')
|
||||
emit('save', data)
|
||||
visible.value = false
|
||||
} else {
|
||||
showToast(response.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存出错:', error)
|
||||
showToast('保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除交易
|
||||
const handleDelete = async () => {
|
||||
showDialog({
|
||||
title: '确认删除',
|
||||
message: '确定要删除这条交易记录吗?删除后无法恢复。',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonColor: '#EF4444'
|
||||
}).then(async () => {
|
||||
try {
|
||||
deleting.value = true
|
||||
const response = await deleteTransaction(editForm.id)
|
||||
if (response.success) {
|
||||
showToast('删除成功')
|
||||
emit('delete', editForm.id)
|
||||
visible.value = false
|
||||
} else {
|
||||
showToast(response.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除出错:', error)
|
||||
showToast('删除失败')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.transaction-detail-sheet {
|
||||
background: #FFFFFF;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #09090B;
|
||||
}
|
||||
|
||||
.header-close {
|
||||
font-size: 24px;
|
||||
color: #71717A;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
|
||||
.amount-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #71717A;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090B;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090B;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
max-width: 200px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090B;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 2px solid #E4E4E7;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366F1;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
|
||||
// 移除 number 类型的上下箭头
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Firefox
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid #E4E4E7;
|
||||
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #71717A;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #09090B;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
|
||||
.reason-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
color: #09090B;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-radio-group) {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.van-radio) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.van-radio__label) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classify-section {
|
||||
padding: 16px;
|
||||
background: #F4F4F5;
|
||||
border-radius: 8px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
.delete-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #EF4444;
|
||||
background: transparent;
|
||||
color: #EF4444;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: #6366F1;
|
||||
color: #FAFAFA;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.transaction-detail-sheet {
|
||||
background: #18181B;
|
||||
|
||||
.sheet-header {
|
||||
.header-title {
|
||||
color: #FAFAFA;
|
||||
}
|
||||
|
||||
.header-close {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
.amount-label {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #FAFAFA;
|
||||
}
|
||||
|
||||
.amount-input-wrapper {
|
||||
.currency-symbol {
|
||||
color: #FAFAFA;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
color: #FAFAFA;
|
||||
border-bottom-color: #27272A;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366F1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
.form-row {
|
||||
border-bottom-color: #27272A;
|
||||
|
||||
.form-label {
|
||||
color: #A1A1AA;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
color: #FAFAFA;
|
||||
|
||||
.reason-input {
|
||||
color: #FAFAFA;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classify-section {
|
||||
background: #27272A;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user