大量的代码格式化
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 1m10s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 1m10s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<van-dialog
|
||||
v-model:show="show"
|
||||
title="新增交易分类"
|
||||
<van-dialog
|
||||
v-model:show="show"
|
||||
title="新增交易分类"
|
||||
show-cancel-button
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<van-field v-model="classifyName" placeholder="请输入新的交易分类" />
|
||||
<van-field
|
||||
v-model="classifyName"
|
||||
placeholder="请输入新的交易分类"
|
||||
/>
|
||||
</van-dialog>
|
||||
</template>
|
||||
|
||||
@@ -30,7 +33,7 @@ const handleConfirm = () => {
|
||||
showToast('请输入分类名称')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
emit('confirm', classifyName.value.trim())
|
||||
show.value = false
|
||||
classifyName.value = ''
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
<van-field label="时间">
|
||||
<template #input>
|
||||
<div style="display: flex; gap: 16px">
|
||||
<div @click="showDatePicker = true">{{ form.date }}</div>
|
||||
<div @click="showTimePicker = true">{{ form.time }}</div>
|
||||
<div @click="showDatePicker = true">
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div @click="showTimePicker = true">
|
||||
{{ form.time }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -37,9 +41,9 @@
|
||||
<van-field name="type" label="类型">
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.type" direction="horizontal" @change="handleTypeChange">
|
||||
<van-radio :name="0">支出</van-radio>
|
||||
<van-radio :name="1">收入</van-radio>
|
||||
<van-radio :name="2">不计</van-radio>
|
||||
<van-radio :name="0"> 支出 </van-radio>
|
||||
<van-radio :name="1"> 收入 </van-radio>
|
||||
<van-radio :name="2"> 不计 </van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -47,23 +51,20 @@
|
||||
<!-- 分类 -->
|
||||
<van-field name="category" label="分类">
|
||||
<template #input>
|
||||
<span v-if="!categoryName" style="color: var(--van-text-color-3);">请选择分类</span>
|
||||
<span v-if="!categoryName" style="color: var(--van-text-color-3)">请选择分类</span>
|
||||
<span v-else>{{ categoryName }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
|
||||
<!-- 分类选择组件 -->
|
||||
<ClassifySelector
|
||||
v-model="categoryName"
|
||||
:type="form.type"
|
||||
/>
|
||||
<ClassifySelector v-model="categoryName" :type="form.type" />
|
||||
</van-cell-group>
|
||||
|
||||
<div class="actions">
|
||||
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||
{{ submitText }}
|
||||
</van-button>
|
||||
<slot name="actions"></slot>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
@@ -137,7 +138,7 @@ const initForm = async () => {
|
||||
if (props.initialData) {
|
||||
isSyncing.value = true
|
||||
const { occurredAt, amount, reason, type, classify } = props.initialData
|
||||
|
||||
|
||||
if (occurredAt) {
|
||||
const dt = dayjs(occurredAt)
|
||||
form.value.date = dt.format('YYYY-MM-DD')
|
||||
@@ -145,11 +146,17 @@ const initForm = async () => {
|
||||
currentDate.value = form.value.date.split('-')
|
||||
currentTime.value = form.value.time.split(':')
|
||||
}
|
||||
|
||||
if (amount !== undefined) form.value.amount = amount
|
||||
if (reason !== undefined) form.value.note = reason
|
||||
if (type !== undefined) form.value.type = type
|
||||
|
||||
|
||||
if (amount !== undefined) {
|
||||
form.value.amount = amount
|
||||
}
|
||||
if (reason !== undefined) {
|
||||
form.value.note = reason
|
||||
}
|
||||
if (type !== undefined) {
|
||||
form.value.type = type
|
||||
}
|
||||
|
||||
// 如果有传入分类名称,尝试设置
|
||||
if (classify) {
|
||||
categoryName.value = classify
|
||||
@@ -166,9 +173,13 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 监听 initialData 变化 (例如重新解析后)
|
||||
watch(() => props.initialData, () => {
|
||||
initForm()
|
||||
}, { deep: true })
|
||||
watch(
|
||||
() => props.initialData,
|
||||
() => {
|
||||
initForm()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleTypeChange = (newType) => {
|
||||
if (!isSyncing.value) {
|
||||
@@ -197,7 +208,7 @@ const handleSubmit = () => {
|
||||
}
|
||||
|
||||
const fullDateTime = `${form.value.date}T${form.value.time}:00`
|
||||
|
||||
|
||||
const payload = {
|
||||
occurredAt: fullDateTime,
|
||||
classify: categoryName.value,
|
||||
@@ -205,7 +216,7 @@ const handleSubmit = () => {
|
||||
reason: form.value.note || '',
|
||||
type: form.value.type
|
||||
}
|
||||
|
||||
|
||||
emit('submit', payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<template>
|
||||
<div class="manual-bill-add">
|
||||
<BillForm
|
||||
ref="billFormRef"
|
||||
:loading="saving"
|
||||
@submit="handleSave"
|
||||
/>
|
||||
<BillForm ref="billFormRef" :loading="saving" @submit="handleSave" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!parseResult" class="input-section" style="margin: 12px 12px 0 16px;">
|
||||
<div v-if="!parseResult" class="input-section" style="margin: 12px 12px 0 16px">
|
||||
<van-field
|
||||
v-model="text"
|
||||
type="textarea"
|
||||
@@ -10,11 +10,11 @@
|
||||
:disabled="parsing || saving"
|
||||
/>
|
||||
<div class="actions">
|
||||
<van-button
|
||||
type="primary"
|
||||
round
|
||||
block
|
||||
:loading="parsing"
|
||||
<van-button
|
||||
type="primary"
|
||||
round
|
||||
block
|
||||
:loading="parsing"
|
||||
:disabled="!text.trim()"
|
||||
@click="handleParse"
|
||||
>
|
||||
@@ -31,13 +31,7 @@
|
||||
@submit="handleSave"
|
||||
>
|
||||
<template #actions>
|
||||
<van-button
|
||||
plain
|
||||
round
|
||||
block
|
||||
class="mt-2"
|
||||
@click="parseResult = null"
|
||||
>
|
||||
<van-button plain round block class="mt-2" @click="parseResult = null">
|
||||
重新输入
|
||||
</van-button>
|
||||
</template>
|
||||
@@ -60,17 +54,19 @@ const saving = ref(false)
|
||||
const parseResult = ref(null)
|
||||
|
||||
const handleParse = async () => {
|
||||
if (!text.value.trim()) return
|
||||
|
||||
if (!text.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
parsing.value = true
|
||||
parseResult.value = null
|
||||
|
||||
|
||||
try {
|
||||
const res = await parseOneLine(text.value)
|
||||
if(!res.success){
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || '解析失败')
|
||||
}
|
||||
|
||||
|
||||
parseResult.value = res.data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
@@ -84,11 +80,11 @@ const handleSave = async (payload) => {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await createTransaction(payload)
|
||||
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || '保存失败')
|
||||
}
|
||||
|
||||
|
||||
showToast('保存成功')
|
||||
text.value = ''
|
||||
parseResult.value = null
|
||||
|
||||
@@ -8,34 +8,38 @@
|
||||
<div class="collapsed-header">
|
||||
<div class="budget-info">
|
||||
<slot name="tag">
|
||||
<van-tag
|
||||
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
||||
<van-tag
|
||||
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
||||
plain
|
||||
class="status-tag"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3 class="card-title">{{ budget.name }}</h3>
|
||||
<h3 class="card-title">
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
|
||||
({{ budget.selectedCategories.join('、') }})
|
||||
</span>
|
||||
</div>
|
||||
<van-icon name="arrow-down" class="expand-icon" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="collapsed-footer">
|
||||
<div class="collapsed-item">
|
||||
<span class="compact-label">实际/目标</span>
|
||||
<span class="compact-value">
|
||||
<slot name="collapsed-amount">
|
||||
{{ budget.current !== undefined && budget.limit !== undefined
|
||||
? `¥${budget.current?.toFixed(0) || 0} / ¥${budget.limit?.toFixed(0) || 0}`
|
||||
: '--' }}
|
||||
{{
|
||||
budget.current !== undefined && budget.limit !== undefined
|
||||
? `¥${budget.current?.toFixed(0) || 0} / ¥${budget.limit?.toFixed(0) || 0}`
|
||||
: '--'
|
||||
}}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="collapsed-item">
|
||||
<span class="compact-label">达成率</span>
|
||||
<span class="compact-value" :class="percentClass">{{ percentage }}%</span>
|
||||
@@ -45,52 +49,49 @@
|
||||
|
||||
<!-- 展开状态 -->
|
||||
<div v-else class="budget-inner-card">
|
||||
<div class="card-header" style="margin-bottom: 0;">
|
||||
<div class="card-header" style="margin-bottom: 0">
|
||||
<div class="budget-info">
|
||||
<slot name="tag">
|
||||
<van-tag
|
||||
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
||||
<van-tag
|
||||
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
||||
plain
|
||||
class="status-tag"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3 class="card-title" style="max-width: 120px;">{{ budget.name }}</h3>
|
||||
<h3 class="card-title" style="max-width: 120px">
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<slot name="actions">
|
||||
<van-button
|
||||
<van-button
|
||||
v-if="budget.description"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
size="small"
|
||||
:type="showDescription ? 'primary' : 'default'"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
/>
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<van-button
|
||||
icon="edit"
|
||||
size="small"
|
||||
plain
|
||||
@click.stop="$emit('click', budget)"
|
||||
/>
|
||||
<van-button icon="edit" size="small" plain @click.stop="$emit('click', budget)" />
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="budget-body">
|
||||
<div v-if="budget.selectedCategories?.length" class="category-tags">
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
:key="cat"
|
||||
size="mini"
|
||||
class="category-tag"
|
||||
@@ -101,14 +102,16 @@
|
||||
</van-tag>
|
||||
</div>
|
||||
<div class="amount-info">
|
||||
<slot name="amount-info"></slot>
|
||||
<slot name="amount-info" />
|
||||
</div>
|
||||
|
||||
<div class="progress-section">
|
||||
<slot name="progress-info">
|
||||
<span class="period-type">{{ periodLabel }}{{ budget.category === 0 ? '使用' : '达成' }}</span>
|
||||
<van-progress
|
||||
:percentage="Math.min(percentage, 100)"
|
||||
<span class="period-type"
|
||||
>{{ periodLabel }}{{ budget.category === 0 ? '使用' : '达成' }}</span
|
||||
>
|
||||
<van-progress
|
||||
:percentage="Math.min(percentage, 100)"
|
||||
stroke-width="8"
|
||||
:color="progressColor"
|
||||
:show-pivot="false"
|
||||
@@ -118,8 +121,8 @@
|
||||
</div>
|
||||
<div class="progress-section time-progress">
|
||||
<span class="period-type">时间进度</span>
|
||||
<van-progress
|
||||
:percentage="timePercentage"
|
||||
<van-progress
|
||||
:percentage="timePercentage"
|
||||
stroke-width="4"
|
||||
color="var(--van-gray-6)"
|
||||
:show-pivot="false"
|
||||
@@ -129,24 +132,20 @@
|
||||
|
||||
<van-collapse-transition>
|
||||
<div v-if="budget.description && showDescription" class="budget-description">
|
||||
<div class="description-content rich-html-content" v-html="budget.description"></div>
|
||||
<div class="description-content rich-html-content" v-html="budget.description" />
|
||||
</div>
|
||||
</van-collapse-transition>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联账单列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showBillListModal"
|
||||
title="关联账单列表"
|
||||
height="75%"
|
||||
>
|
||||
<TransactionList
|
||||
<PopupContainer v-model="showBillListModal" title="关联账单列表" height="75%">
|
||||
<TransactionList
|
||||
:transactions="billList"
|
||||
:loading="billLoading"
|
||||
:finished="true"
|
||||
@@ -166,30 +165,26 @@
|
||||
<div class="collapsed-header">
|
||||
<div class="budget-info">
|
||||
<slot name="tag">
|
||||
<van-tag
|
||||
type="success"
|
||||
plain
|
||||
class="status-tag"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
<van-tag type="success" plain class="status-tag">
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3 class="card-title">{{ budget.name }}</h3>
|
||||
<h3 class="card-title">
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
|
||||
({{ budget.selectedCategories.join('、') }})
|
||||
</span>
|
||||
</div>
|
||||
<van-icon name="arrow-down" class="expand-icon" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="collapsed-footer no-limit-footer">
|
||||
<div class="collapsed-item">
|
||||
<span class="compact-label">实际</span>
|
||||
<span class="compact-value">
|
||||
<slot name="collapsed-amount">
|
||||
{{ budget.current !== undefined
|
||||
? `¥${budget.current?.toFixed(0) || 0}`
|
||||
: '--' }}
|
||||
{{ budget.current !== undefined ? `¥${budget.current?.toFixed(0) || 0}` : '--' }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
@@ -198,52 +193,45 @@
|
||||
|
||||
<!-- 展开状态 -->
|
||||
<div v-else class="budget-inner-card">
|
||||
<div class="card-header" style="margin-bottom: 0;">
|
||||
<div class="card-header" style="margin-bottom: 0">
|
||||
<div class="budget-info">
|
||||
<slot name="tag">
|
||||
<van-tag
|
||||
type="success"
|
||||
plain
|
||||
class="status-tag"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
<van-tag type="success" plain class="status-tag">
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3 class="card-title" style="max-width: 120px;">{{ budget.name }}</h3>
|
||||
<h3 class="card-title" style="max-width: 120px">
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<slot name="actions">
|
||||
<van-button
|
||||
<van-button
|
||||
v-if="budget.description"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
size="small"
|
||||
:type="showDescription ? 'primary' : 'default'"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
/>
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<van-button
|
||||
icon="edit"
|
||||
size="small"
|
||||
plain
|
||||
@click.stop="$emit('click', budget)"
|
||||
/>
|
||||
<van-button icon="edit" size="small" plain @click.stop="$emit('click', budget)" />
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="budget-body">
|
||||
<div v-if="budget.selectedCategories?.length" class="category-tags">
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
:key="cat"
|
||||
size="mini"
|
||||
class="category-tag"
|
||||
@@ -253,26 +241,28 @@
|
||||
{{ cat }}
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="no-limit-amount-info">
|
||||
<div class="amount-item">
|
||||
<span>
|
||||
<span class="label">实际</span>
|
||||
<span class="value" style="margin-left: 12px;">¥{{ budget.current?.toFixed(0) || 0 }}</span>
|
||||
<span class="label">实际</span>
|
||||
<span class="value" style="margin-left: 12px"
|
||||
>¥{{ budget.current?.toFixed(0) || 0 }}</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-limit-notice">
|
||||
<span>
|
||||
<van-icon name="info-o" style="margin-right: 4px;" />
|
||||
<van-icon name="info-o" style="margin-right: 4px" />
|
||||
不记额预算 - 直接计入存款明细
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<van-collapse-transition>
|
||||
<div v-if="budget.description && showDescription" class="budget-description">
|
||||
<div class="description-content rich-html-content" v-html="budget.description"></div>
|
||||
<div class="description-content rich-html-content" v-html="budget.description" />
|
||||
</div>
|
||||
</van-collapse-transition>
|
||||
</div>
|
||||
@@ -280,12 +270,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 关联账单列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showBillListModal"
|
||||
title="关联账单列表"
|
||||
height="75%"
|
||||
>
|
||||
<TransactionList
|
||||
<PopupContainer v-model="showBillListModal" title="关联账单列表" height="75%">
|
||||
<TransactionList
|
||||
:transactions="billList"
|
||||
:loading="billLoading"
|
||||
:finished="true"
|
||||
@@ -339,10 +325,10 @@ const toggleExpand = () => {
|
||||
const handleQueryBills = async () => {
|
||||
showBillListModal.value = true
|
||||
billLoading.value = true
|
||||
|
||||
|
||||
try {
|
||||
const classify = props.budget.selectedCategories
|
||||
? props.budget.selectedCategories.join(',')
|
||||
const classify = props.budget.selectedCategories
|
||||
? props.budget.selectedCategories.join(',')
|
||||
: ''
|
||||
|
||||
if (classify === '') {
|
||||
@@ -362,12 +348,11 @@ const handleQueryBills = async () => {
|
||||
sortByAmount: true
|
||||
})
|
||||
|
||||
if(response.success) {
|
||||
if (response.success) {
|
||||
billList.value = response.data || []
|
||||
} else {
|
||||
billList.value = []
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询账单列表失败:', error)
|
||||
billList.value = []
|
||||
@@ -377,19 +362,27 @@ const handleQueryBills = async () => {
|
||||
}
|
||||
|
||||
const percentage = computed(() => {
|
||||
if (!props.budget.limit) return 0
|
||||
if (!props.budget.limit) {
|
||||
return 0
|
||||
}
|
||||
return Math.round((props.budget.current / props.budget.limit) * 100)
|
||||
})
|
||||
|
||||
const timePercentage = computed(() => {
|
||||
if (!props.budget.periodStart || !props.budget.periodEnd) 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()
|
||||
|
||||
if (now <= start) return 0
|
||||
if (now >= end) return 100
|
||||
|
||||
|
||||
if (now <= start) {
|
||||
return 0
|
||||
}
|
||||
if (now >= end) {
|
||||
return 100
|
||||
}
|
||||
|
||||
return Math.round(((now - start) / (end - start)) * 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
:title="isEdit ? `编辑${getCategoryName(form.category)}预算` : `新增${getCategoryName(form.category)}预算`"
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
:title="
|
||||
isEdit
|
||||
? `编辑${getCategoryName(form.category)}预算`
|
||||
: `新增${getCategoryName(form.category)}预算`
|
||||
"
|
||||
height="75%"
|
||||
>
|
||||
<div class="add-budget-form">
|
||||
@@ -17,18 +21,20 @@
|
||||
<!-- 新增:不记额预算复选框 -->
|
||||
<van-field label="不记额预算">
|
||||
<template #input>
|
||||
<van-checkbox v-model="form.noLimit" @update:model-value="onNoLimitChange">不记额预算(仅限年度)</van-checkbox>
|
||||
<van-checkbox v-model="form.noLimit" @update:model-value="onNoLimitChange">
|
||||
不记额预算(仅限年度)
|
||||
</van-checkbox>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field name="type" label="统计周期">
|
||||
<template #input>
|
||||
<van-radio-group
|
||||
v-model="form.type"
|
||||
<van-radio-group
|
||||
v-model="form.type"
|
||||
direction="horizontal"
|
||||
:disabled="isEdit || form.noLimit"
|
||||
>
|
||||
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Month"> 月 </van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Year"> 年 </van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -48,7 +54,12 @@
|
||||
</van-field>
|
||||
<van-field label="相关分类">
|
||||
<template #input>
|
||||
<div v-if="form.selectedCategories.length === 0" style="color: var(--van-text-color-3);">可多选分类</div>
|
||||
<div
|
||||
v-if="form.selectedCategories.length === 0"
|
||||
style="color: var(--van-text-color-3)"
|
||||
>
|
||||
可多选分类
|
||||
</div>
|
||||
<div v-else class="selected-categories">
|
||||
<span class="ellipsis-text">
|
||||
{{ form.selectedCategories.join('、') }}
|
||||
@@ -67,7 +78,7 @@
|
||||
</van-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<van-button block round type="primary" @click="onSubmit">保存预算</van-button>
|
||||
<van-button block round type="primary" @click="onSubmit"> 保存预算 </van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</template>
|
||||
@@ -92,15 +103,11 @@ const form = reactive({
|
||||
category: BudgetCategory.Expense,
|
||||
limit: '',
|
||||
selectedCategories: [],
|
||||
noLimit: false // 新增字段
|
||||
noLimit: false // 新增字段
|
||||
})
|
||||
|
||||
const open = ({
|
||||
data,
|
||||
isEditFlag,
|
||||
category
|
||||
}) => {
|
||||
if(category === undefined) {
|
||||
const open = ({ data, isEditFlag, category }) => {
|
||||
if (category === undefined) {
|
||||
showToast('缺少必要参数:category')
|
||||
return
|
||||
}
|
||||
@@ -114,7 +121,7 @@ const open = ({
|
||||
category: category,
|
||||
limit: data.limit,
|
||||
selectedCategories: data.selectedCategories ? [...data.selectedCategories] : [],
|
||||
noLimit: data.noLimit || false // 新增
|
||||
noLimit: data.noLimit || false // 新增
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
@@ -124,7 +131,7 @@ const open = ({
|
||||
category: category,
|
||||
limit: '',
|
||||
selectedCategories: [],
|
||||
noLimit: false // 新增
|
||||
noLimit: false // 新增
|
||||
})
|
||||
}
|
||||
visible.value = true
|
||||
@@ -135,18 +142,22 @@ defineExpose({
|
||||
})
|
||||
|
||||
const budgetType = computed(() => {
|
||||
return form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
|
||||
return form.category === BudgetCategory.Expense
|
||||
? 0
|
||||
: form.category === BudgetCategory.Income
|
||||
? 1
|
||||
: 2
|
||||
})
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const data = {
|
||||
...form,
|
||||
limit: form.noLimit ? 0 : parseFloat(form.limit), // 不记额时金额为0
|
||||
limit: form.noLimit ? 0 : parseFloat(form.limit), // 不记额时金额为0
|
||||
selectedCategories: form.selectedCategories,
|
||||
noLimit: form.noLimit // 新增
|
||||
noLimit: form.noLimit // 新增
|
||||
}
|
||||
|
||||
|
||||
const res = form.id ? await updateBudget(data) : await createBudget(data)
|
||||
if (res.success) {
|
||||
showToast('保存成功')
|
||||
@@ -160,7 +171,7 @@ const onSubmit = async () => {
|
||||
}
|
||||
|
||||
const getCategoryName = (category) => {
|
||||
switch(category) {
|
||||
switch (category) {
|
||||
case BudgetCategory.Expense:
|
||||
return '支出'
|
||||
case BudgetCategory.Income:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="summary-container">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<div v-if="stats && (stats.month || stats.year)" :key="dateKey" class="summary-card common-card">
|
||||
<div
|
||||
v-if="stats && (stats.month || stats.year)"
|
||||
:key="dateKey"
|
||||
class="summary-card common-card"
|
||||
>
|
||||
<!-- 左切换按钮 -->
|
||||
<div class="nav-arrow left" @click.stop="changeMonth(-1)">
|
||||
<van-icon name="arrow-left" />
|
||||
@@ -20,13 +24,13 @@
|
||||
<span class="amount">¥{{ formatMoney(stats[key]?.limit || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.showDivider" class="divider"></div>
|
||||
<div v-if="config.showDivider" class="divider" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 右切换按钮 -->
|
||||
<div
|
||||
class="nav-arrow right"
|
||||
<div
|
||||
class="nav-arrow right"
|
||||
:class="{ disabled: isCurrentMonth }"
|
||||
@click.stop="!isCurrentMonth && changeMonth(1)"
|
||||
>
|
||||
@@ -71,18 +75,17 @@ const dateKey = computed(() => props.date.getFullYear() + '-' + props.date.getMo
|
||||
|
||||
const isCurrentMonth = computed(() => {
|
||||
const now = new Date()
|
||||
return props.date.getFullYear() === now.getFullYear() &&
|
||||
props.date.getMonth() === now.getMonth()
|
||||
return props.date.getFullYear() === now.getFullYear() && props.date.getMonth() === now.getMonth()
|
||||
})
|
||||
|
||||
const periodConfigs = computed(() => ({
|
||||
month: {
|
||||
label: isCurrentMonth.value ? '本月' : `${props.date.getMonth() + 1}月`,
|
||||
showDivider: true
|
||||
month: {
|
||||
label: isCurrentMonth.value ? '本月' : `${props.date.getMonth() + 1}月`,
|
||||
showDivider: true
|
||||
},
|
||||
year: {
|
||||
label: isCurrentMonth.value ? '年度' : `${props.date.getFullYear()}年`,
|
||||
showDivider: false
|
||||
year: {
|
||||
label: isCurrentMonth.value ? '年度' : `${props.date.getFullYear()}年`,
|
||||
showDivider: false
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -94,7 +97,10 @@ const changeMonth = (delta) => {
|
||||
}
|
||||
|
||||
const formatMoney = (val) => {
|
||||
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })
|
||||
return parseFloat(val || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
title="设置存款分类"
|
||||
height="60%"
|
||||
>
|
||||
<PopupContainer v-model="visible" title="设置存款分类" height="60%">
|
||||
<div class="savings-config-content">
|
||||
<div class="config-header">
|
||||
<p class="subtitle">这些分类的统计值将计入“存款”中</p>
|
||||
@@ -20,9 +16,9 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<van-button block round type="primary" @click="onSubmit">保存配置</van-button>
|
||||
<van-button block round type="primary" @click="onSubmit"> 保存配置 </van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</template>
|
||||
@@ -52,7 +48,7 @@ const fetchConfig = async () => {
|
||||
try {
|
||||
const res = await getConfig('SavingsCategories')
|
||||
if (res.success && res.data) {
|
||||
selectedCategories.value = res.data.split(',').filter(x => x)
|
||||
selectedCategories.value = res.data.split(',').filter((x) => x)
|
||||
} else {
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
@@ -98,17 +98,21 @@ const innerOptions = ref([])
|
||||
const addClassifyDialogRef = ref()
|
||||
|
||||
const displayOptions = computed(() => {
|
||||
if (props.options) return props.options
|
||||
if (props.options) {
|
||||
return props.options
|
||||
}
|
||||
return innerOptions.value
|
||||
})
|
||||
|
||||
const fetchOptions = async () => {
|
||||
if (props.options) return
|
||||
|
||||
if (props.options) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getCategoryList(props.type)
|
||||
if (response.success) {
|
||||
innerOptions.value = (response.data || []).map(item => ({
|
||||
innerOptions.value = (response.data || []).map((item) => ({
|
||||
text: item.name,
|
||||
value: item.name,
|
||||
id: item.id
|
||||
@@ -132,12 +136,12 @@ const handleAddConfirm = async (categoryName) => {
|
||||
name: categoryName,
|
||||
type: props.type
|
||||
})
|
||||
|
||||
|
||||
if (response.success) {
|
||||
showToast('分类创建成功')
|
||||
// 刷新列表
|
||||
await fetchOptions()
|
||||
|
||||
|
||||
// 如果是单选模式,且当前没有选值或就是为了新增,则自动选中
|
||||
if (!props.multiple) {
|
||||
emit('update:modelValue', categoryName)
|
||||
@@ -152,9 +156,12 @@ const handleAddConfirm = async (categoryName) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.type, () => {
|
||||
fetchOptions()
|
||||
})
|
||||
watch(
|
||||
() => props.type,
|
||||
() => {
|
||||
fetchOptions()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchOptions()
|
||||
@@ -175,8 +182,10 @@ const isSelected = (item) => {
|
||||
|
||||
// 是否全部选中
|
||||
const isAllSelected = computed(() => {
|
||||
if (!props.multiple || displayOptions.value.length === 0) return false
|
||||
return displayOptions.value.every(item => props.modelValue.includes(item.text))
|
||||
if (!props.multiple || displayOptions.value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return displayOptions.value.every((item) => props.modelValue.includes(item.text))
|
||||
})
|
||||
|
||||
// 是否有任何选中
|
||||
@@ -208,13 +217,15 @@ const toggleItem = (item) => {
|
||||
|
||||
// 切换全选
|
||||
const toggleAll = () => {
|
||||
if (!props.multiple) return
|
||||
|
||||
if (!props.multiple) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isAllSelected.value) {
|
||||
emit('update:modelValue', [])
|
||||
emit('change', [])
|
||||
} else {
|
||||
const allValues = displayOptions.value.map(item => item.text)
|
||||
const allValues = displayOptions.value.map((item) => item.text)
|
||||
emit('update:modelValue', allValues)
|
||||
emit('change', allValues)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
<div class="weekday-label">四</div>
|
||||
<div class="weekday-label">六</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Scrollable Heatmap Area -->
|
||||
<div ref="scrollContainer" class="heatmap-scroll-container">
|
||||
<div class="heatmap-content">
|
||||
<!-- Month Labels -->
|
||||
<div class="month-row">
|
||||
<div
|
||||
v-for="(month, index) in monthLabels"
|
||||
:key="index"
|
||||
<div
|
||||
v-for="(month, index) in monthLabels"
|
||||
:key="index"
|
||||
class="month-label"
|
||||
:style="{ left: month.left + 'px' }"
|
||||
>
|
||||
@@ -40,18 +40,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="heatmap-footer">
|
||||
<div v-if="totalCount > 0" class="summary-text">
|
||||
过去一年共 {{ totalCount }} 笔交易
|
||||
</div>
|
||||
<div v-if="totalCount > 0" class="summary-text">过去一年共 {{ totalCount }} 笔交易</div>
|
||||
<div class="legend">
|
||||
<span>少</span>
|
||||
<div class="legend-item level-0"></div>
|
||||
<div class="legend-item level-1"></div>
|
||||
<div class="legend-item level-2"></div>
|
||||
<div class="legend-item level-3"></div>
|
||||
<div class="legend-item level-4"></div>
|
||||
<div class="legend-item level-0" />
|
||||
<div class="legend-item level-1" />
|
||||
<div class="legend-item level-2" />
|
||||
<div class="legend-item level-3" />
|
||||
<div class="legend-item level-4" />
|
||||
<span>多</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,84 +57,84 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import { getDailyStatisticsRange } from '@/api/statistics';
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { getDailyStatisticsRange } from '@/api/statistics'
|
||||
|
||||
const stats = ref({});
|
||||
const weeks = ref([]);
|
||||
const monthLabels = ref([]);
|
||||
const totalCount = ref(0);
|
||||
const scrollContainer = ref(null);
|
||||
const thresholds = ref([2, 4, 7]); // Default thresholds
|
||||
const stats = ref({})
|
||||
const weeks = ref([])
|
||||
const monthLabels = ref([])
|
||||
const totalCount = ref(0)
|
||||
const scrollContainer = ref(null)
|
||||
const thresholds = ref([2, 4, 7]) // Default thresholds
|
||||
|
||||
const CELL_SIZE = 15;
|
||||
const CELL_GAP = 3;
|
||||
const WEEK_WIDTH = CELL_SIZE + CELL_GAP;
|
||||
const CELL_SIZE = 15
|
||||
const CELL_GAP = 3
|
||||
const WEEK_WIDTH = CELL_SIZE + CELL_GAP
|
||||
|
||||
const formatDate = (d) => {
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setFullYear(endDate.getFullYear() - 1);
|
||||
|
||||
const endDate = new Date()
|
||||
const startDate = new Date()
|
||||
startDate.setFullYear(endDate.getFullYear() - 1)
|
||||
|
||||
try {
|
||||
const res = await getDailyStatisticsRange({
|
||||
startDate: formatDate(startDate),
|
||||
endDate: formatDate(endDate)
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
const map = {};
|
||||
let count = 0;
|
||||
res.data.forEach(item => {
|
||||
map[item.date] = item;
|
||||
count += item.count;
|
||||
});
|
||||
stats.value = map;
|
||||
totalCount.value = count;
|
||||
|
||||
const map = {}
|
||||
let count = 0
|
||||
res.data.forEach((item) => {
|
||||
map[item.date] = item
|
||||
count += item.count
|
||||
})
|
||||
stats.value = map
|
||||
totalCount.value = count
|
||||
|
||||
// Calculate thresholds based on last 15 days average
|
||||
const today = new Date();
|
||||
let last15DaysSum = 0;
|
||||
for(let i = 0; i < 15; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = formatDate(d);
|
||||
last15DaysSum += (map[dateStr]?.count || 0);
|
||||
const today = new Date()
|
||||
let last15DaysSum = 0
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
const dateStr = formatDate(d)
|
||||
last15DaysSum += map[dateStr]?.count || 0
|
||||
}
|
||||
|
||||
const avg = last15DaysSum / 15;
|
||||
console.log("avg", avg)
|
||||
|
||||
const avg = last15DaysSum / 15
|
||||
console.log('avg', avg)
|
||||
// Step size calculation: ensure at least 1, roughly avg/2 to create spread
|
||||
// Level 1: 1 ~ step
|
||||
// Level 2: step+1 ~ step*2
|
||||
// Level 3: step*2+1 ~ step*3
|
||||
// Level 4: > step*3
|
||||
const step = Math.max(Math.ceil(avg / 2), 1);
|
||||
thresholds.value = [step, step * 2, step * 3];
|
||||
const step = Math.max(Math.ceil(avg / 2), 1)
|
||||
thresholds.value = [step, step * 2, step * 3]
|
||||
|
||||
generateHeatmapData(startDate, endDate);
|
||||
generateHeatmapData(startDate, endDate)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch heatmap data", e);
|
||||
console.error('Failed to fetch heatmap data', e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const generateHeatmapData = (startDate, endDate) => {
|
||||
const data = [];
|
||||
const current = new Date(startDate);
|
||||
|
||||
const allDays = [];
|
||||
|
||||
const data = []
|
||||
const current = new Date(startDate)
|
||||
|
||||
const allDays = []
|
||||
|
||||
// Adjust start date to be Monday to align weeks
|
||||
// 0 = Sunday, 1 = Monday
|
||||
const startDay = current.getDay();
|
||||
const startDay = current.getDay()
|
||||
// If startDay is 0 (Sunday), we need to go back 6 days to Monday
|
||||
// If startDay is 1 (Monday), we are good
|
||||
// If startDay is 2 (Tuesday), we need to go back 1 day
|
||||
@@ -144,114 +142,124 @@ const generateHeatmapData = (startDate, endDate) => {
|
||||
// Monday (1) -> 0 days back
|
||||
// Sunday (0) -> 6 days back
|
||||
// Tuesday (2) -> 1 day back
|
||||
|
||||
const daysToSubtract = (startDay + 6) % 7;
|
||||
|
||||
const daysToSubtract = (startDay + 6) % 7
|
||||
// We don't necessarily need to subtract from startDate for data fetching,
|
||||
// but for grid alignment we want the first column to start on Monday.
|
||||
|
||||
const alignStart = new Date(startDate);
|
||||
|
||||
const alignStart = new Date(startDate)
|
||||
// alignStart.setDate(alignStart.getDate() - daysToSubtract);
|
||||
|
||||
const tempDate = new Date(alignStart);
|
||||
|
||||
const tempDate = new Date(alignStart)
|
||||
while (tempDate <= endDate) {
|
||||
const dateStr = formatDate(tempDate);
|
||||
const dateStr = formatDate(tempDate)
|
||||
allDays.push({
|
||||
date: dateStr,
|
||||
count: stats.value[dateStr]?.count || 0,
|
||||
obj: new Date(tempDate)
|
||||
});
|
||||
tempDate.setDate(tempDate.getDate() + 1);
|
||||
})
|
||||
tempDate.setDate(tempDate.getDate() + 1)
|
||||
}
|
||||
|
||||
|
||||
// Now group into weeks
|
||||
const resultWeeks = [];
|
||||
let currentWeek = [];
|
||||
|
||||
const resultWeeks = []
|
||||
let currentWeek = []
|
||||
|
||||
// Pad first week if start date is not Monday
|
||||
// allDays[0] is startDate
|
||||
const firstDayObj = new Date(allDays[0].date);
|
||||
const firstDay = firstDayObj.getDay(); // 0-6 (Sun-Sat)
|
||||
|
||||
const firstDayObj = new Date(allDays[0].date)
|
||||
const firstDay = firstDayObj.getDay() // 0-6 (Sun-Sat)
|
||||
|
||||
// We want Monday (1) to be index 0
|
||||
// Mon(1)->0, Tue(2)->1, ..., Sun(0)->6
|
||||
const padCount = (firstDay + 6) % 7;
|
||||
|
||||
const padCount = (firstDay + 6) % 7
|
||||
|
||||
for (let i = 0; i < padCount; i++) {
|
||||
currentWeek.push(null);
|
||||
currentWeek.push(null)
|
||||
}
|
||||
|
||||
allDays.forEach(day => {
|
||||
currentWeek.push(day);
|
||||
|
||||
allDays.forEach((day) => {
|
||||
currentWeek.push(day)
|
||||
if (currentWeek.length === 7) {
|
||||
resultWeeks.push(currentWeek);
|
||||
currentWeek = [];
|
||||
resultWeeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
// Push last partial week
|
||||
if (currentWeek.length > 0) {
|
||||
while (currentWeek.length < 7) {
|
||||
currentWeek.push(null);
|
||||
currentWeek.push(null)
|
||||
}
|
||||
resultWeeks.push(currentWeek);
|
||||
resultWeeks.push(currentWeek)
|
||||
}
|
||||
|
||||
weeks.value = resultWeeks;
|
||||
|
||||
|
||||
weeks.value = resultWeeks
|
||||
|
||||
// Generate Month Labels
|
||||
const labels = [];
|
||||
let lastMonth = -1;
|
||||
|
||||
const labels = []
|
||||
let lastMonth = -1
|
||||
|
||||
resultWeeks.forEach((week, index) => {
|
||||
// Check the first valid day in the week
|
||||
const day = week.find(d => d !== null);
|
||||
const day = week.find((d) => d !== null)
|
||||
if (day) {
|
||||
const d = new Date(day.date);
|
||||
const month = d.getMonth();
|
||||
const d = new Date(day.date)
|
||||
const month = d.getMonth()
|
||||
if (month !== lastMonth) {
|
||||
labels.push({
|
||||
text: d.toLocaleString('zh-CN', { month: 'short' }),
|
||||
left: index * WEEK_WIDTH
|
||||
});
|
||||
lastMonth = month;
|
||||
})
|
||||
lastMonth = month
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
monthLabels.value = labels;
|
||||
|
||||
})
|
||||
|
||||
monthLabels.value = labels
|
||||
|
||||
// Scroll to end
|
||||
nextTick(() => {
|
||||
if (scrollContainer.value) {
|
||||
scrollContainer.value.scrollLeft = scrollContainer.value.scrollWidth;
|
||||
scrollContainer.value.scrollLeft = scrollContainer.value.scrollWidth
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const getLevelClass = (day) => {
|
||||
if (!day) return 'invisible';
|
||||
const count = day.count;
|
||||
if (count === 0) return 'level-0';
|
||||
if (count <= thresholds.value[0]) return 'level-1';
|
||||
if (count <= thresholds.value[1]) return 'level-2';
|
||||
if (count <= thresholds.value[2]) return 'level-3';
|
||||
return 'level-4';
|
||||
};
|
||||
if (!day) {
|
||||
return 'invisible'
|
||||
}
|
||||
const count = day.count
|
||||
if (count === 0) {
|
||||
return 'level-0'
|
||||
}
|
||||
if (count <= thresholds.value[0]) {
|
||||
return 'level-1'
|
||||
}
|
||||
if (count <= thresholds.value[1]) {
|
||||
return 'level-2'
|
||||
}
|
||||
if (count <= thresholds.value[2]) {
|
||||
return 'level-3'
|
||||
}
|
||||
return 'level-4'
|
||||
}
|
||||
|
||||
const onCellClick = (day) => {
|
||||
if (day) {
|
||||
// Emit event or show toast
|
||||
// console.log(day);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh: fetchData
|
||||
});
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -260,7 +268,7 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
color: var(--van-text-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
margin: 0 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--van-border-color);
|
||||
@@ -328,7 +336,7 @@ onMounted(() => {
|
||||
Row 0: 0px top
|
||||
Row 1: 18px top (15+3) - Label "二" aligns here? No, "二" is usually row 1 (index 1, 2nd row)
|
||||
If we want to align with 2nd, 4th, 6th rows (indices 1, 3, 5):
|
||||
|
||||
|
||||
Row 0: y=0
|
||||
Row 1: y=18
|
||||
Row 2: y=36
|
||||
@@ -336,28 +344,30 @@ onMounted(() => {
|
||||
Row 4: y=72
|
||||
Row 5: y=90
|
||||
Row 6: y=108
|
||||
|
||||
|
||||
Label 1 ("二") at Row 1 (y=18)
|
||||
Label 2 ("四") at Row 3 (y=54)
|
||||
Label 3 ("六") at Row 5 (y=90)
|
||||
|
||||
|
||||
Padding-top of container is 19px.
|
||||
First label margin-top: 18px
|
||||
Second label margin-top: (54 - (18+15)) = 21px
|
||||
Third label margin-top: (90 - (54+15)) = 21px
|
||||
|
||||
Let's try standard spacing.
|
||||
|
||||
Let's try standard spacing.
|
||||
Gap between tops is 36px (2 rows).
|
||||
Height of label is 15px.
|
||||
Margin needed is 36 - 15 = 21px.
|
||||
|
||||
|
||||
First label top needs to be at 18px relative to grid start.
|
||||
Container padding-top aligns with grid start (row 0 top).
|
||||
So first label margin-top should be 18px.
|
||||
*/
|
||||
margin-top: 21px;
|
||||
}
|
||||
.weekday-label:first-child { margin-top: 18px; }
|
||||
.weekday-label:first-child {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.heatmap-grid {
|
||||
display: flex;
|
||||
@@ -382,45 +392,63 @@ onMounted(() => {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.level-0 { background-color: var(--van-gray-2); }
|
||||
.level-0 {
|
||||
background-color: var(--van-gray-2);
|
||||
}
|
||||
/* Default (Light Mode) - Light to Deep Green */
|
||||
.level-1 { background-color: #9be9a8; }
|
||||
.level-2 { background-color: #40c463; }
|
||||
.level-3 { background-color: #30a14e; }
|
||||
.level-4 { background-color: #216e39; }
|
||||
.level-1 {
|
||||
background-color: #9be9a8;
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #40c463;
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #30a14e;
|
||||
}
|
||||
.level-4 {
|
||||
background-color: #216e39;
|
||||
}
|
||||
|
||||
/* Dark Mode - Dark to Light/Bright Green (GitHub Dark Mode Style) */
|
||||
/* The user requested "From Light to Deep" (浅至深) which usually means standard heatmap logic (darker = more).
|
||||
HOWEVER, in dark interfaces, usually "Brighter = More".
|
||||
If the user explicitly says "colors are wrong, should be from light to deep", and they are referring to the visual gradient:
|
||||
|
||||
|
||||
If they mean visual brightness:
|
||||
Light (Dim) -> Deep (Bright)
|
||||
|
||||
|
||||
Let's stick to the GitHub Dark Mode palette which is scientifically designed for dark backgrounds:
|
||||
L1 (Less): Dark Green (#0e4429)
|
||||
L4 (More): Neon Green (#39d353)
|
||||
This is visually "Dim to Bright".
|
||||
|
||||
|
||||
If the user meant "Light color to Dark color" literally (like white -> black green), that would look bad on dark mode.
|
||||
"浅至深" in color context usually implies saturation/intensity.
|
||||
|
||||
|
||||
Let's restore the GitHub Dark Mode colors for dark mode, as my previous change might have inverted them incorrectly or caused confusion.
|
||||
|
||||
|
||||
GitHub Dark Mode:
|
||||
L0: #161b22
|
||||
L1: #0e4429
|
||||
L2: #006d32
|
||||
L3: #26a641
|
||||
L4: #39d353
|
||||
|
||||
|
||||
This goes from Dark Green -> Bright Green.
|
||||
*/
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.level-1 { background-color: #9be9a8; }
|
||||
.level-2 { background-color: #40c463; }
|
||||
.level-3 { background-color: #30a14e; }
|
||||
.level-4 { background-color: #216e39; }
|
||||
.level-1 {
|
||||
background-color: #9be9a8;
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #40c463;
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #30a14e;
|
||||
}
|
||||
.level-4 {
|
||||
background-color: #216e39;
|
||||
}
|
||||
}
|
||||
|
||||
.heatmap-footer {
|
||||
@@ -443,4 +471,4 @@ onMounted(() => {
|
||||
height: 15px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Add Bill Modal -->
|
||||
<PopupContainer
|
||||
v-model="showAddBill"
|
||||
title="记一笔"
|
||||
height="75%"
|
||||
>
|
||||
<PopupContainer v-model="showAddBill" title="记一笔" height="75%">
|
||||
<van-tabs v-model:active="activeTab" shrink>
|
||||
<van-tab title="一句话录账" name="one">
|
||||
<OneLineBillAdd :key="componentKey" @success="handleSuccess" />
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
<div class="popup-header-fixed">
|
||||
<!-- 标题行 (无子标题且有操作时使用 Grid 布局) -->
|
||||
<div class="header-title-row" :class="{ 'has-actions': !subtitle && hasActions }">
|
||||
<h3 class="popup-title">{{ title }}</h3>
|
||||
<h3 class="popup-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<!-- 无子标题时,操作按钮与标题同行 -->
|
||||
<div v-if="!subtitle && hasActions" class="header-actions-inline">
|
||||
<slot name="header-actions"></slot>
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,18 +26,18 @@
|
||||
<div v-if="subtitle" class="header-stats">
|
||||
<span class="stats-text" v-html="subtitle" />
|
||||
<!-- 额外操作插槽 -->
|
||||
<slot v-if="hasActions" name="header-actions"></slot>
|
||||
<slot v-if="hasActions" name="header-actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域(可滚动) -->
|
||||
<div class="popup-scroll-content">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 底部页脚,固定不可滚动 -->
|
||||
<div v-if="slots.footer" class="popup-footer-fixed">
|
||||
<slot name="footer"></slot>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
@@ -47,24 +49,24 @@ import { computed, useSlots } from 'vue'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '80%',
|
||||
default: '80%'
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@@ -74,7 +76,7 @@ const slots = useSlots()
|
||||
// 双向绑定
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 判断是否有操作按钮
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="reason-group-list-v2">
|
||||
<van-empty v-if="groups.length === 0 && !loading" description="暂无数据" />
|
||||
|
||||
<van-cell-group v-else inset>
|
||||
<van-cell
|
||||
v-for="group in groups"
|
||||
<van-empty
|
||||
v-if="groups.length === 0 && !loading"
|
||||
description="暂无数据"
|
||||
/>
|
||||
|
||||
<van-cell-group
|
||||
v-else
|
||||
inset
|
||||
>
|
||||
<van-cell
|
||||
v-for="group in groups"
|
||||
:key="group.reason"
|
||||
clickable
|
||||
@click="handleGroupClick(group)"
|
||||
@@ -12,7 +18,7 @@
|
||||
>
|
||||
<template #title>
|
||||
<div class="group-header">
|
||||
<van-checkbox
|
||||
<van-checkbox
|
||||
v-if="selectable"
|
||||
:model-value="isSelected(group.reason)"
|
||||
@click.stop="handleToggleSelection(group.reason)"
|
||||
@@ -24,23 +30,26 @@
|
||||
</template>
|
||||
<template #label>
|
||||
<div class="group-info">
|
||||
<van-tag
|
||||
:type="getTypeColor(group.sampleType)"
|
||||
<van-tag
|
||||
:type="getTypeColor(group.sampleType)"
|
||||
size="medium"
|
||||
style="margin-right: 8px;"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
{{ getTypeName(group.sampleType) }}
|
||||
</van-tag>
|
||||
<van-tag
|
||||
v-if="group.sampleClassify"
|
||||
type="primary"
|
||||
<van-tag
|
||||
v-if="group.sampleClassify"
|
||||
type="primary"
|
||||
size="medium"
|
||||
style="margin-right: 8px;"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
{{ group.sampleClassify }}
|
||||
</van-tag>
|
||||
<span class="count-text">{{ group.count }} 条</span>
|
||||
<span v-if="group.totalAmount" class="amount-text">
|
||||
<span
|
||||
v-if="group.totalAmount"
|
||||
class="amount-text"
|
||||
>
|
||||
¥{{ Math.abs(group.totalAmount).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -59,8 +68,8 @@
|
||||
height="75%"
|
||||
>
|
||||
<template #header-actions>
|
||||
<van-button
|
||||
type="primary"
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="batch-classify-btn"
|
||||
@click.stop="handleBatchClassify(selectedGroup)"
|
||||
@@ -68,7 +77,7 @@
|
||||
批量分类
|
||||
</van-button>
|
||||
</template>
|
||||
|
||||
|
||||
<TransactionList
|
||||
:transactions="groupTransactions"
|
||||
:loading="transactionLoading"
|
||||
@@ -92,7 +101,10 @@
|
||||
title="批量设置分类"
|
||||
height="60%"
|
||||
>
|
||||
<van-form ref="batchFormRef" class="setting-form">
|
||||
<van-form
|
||||
ref="batchFormRef"
|
||||
class="setting-form"
|
||||
>
|
||||
<van-cell-group inset>
|
||||
<!-- 显示选中的摘要 -->
|
||||
<van-field
|
||||
@@ -101,7 +113,7 @@
|
||||
readonly
|
||||
input-align="left"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 显示记录数量 -->
|
||||
<van-field
|
||||
:model-value="`${batchGroup?.count || 0} 条`"
|
||||
@@ -111,24 +123,42 @@
|
||||
/>
|
||||
|
||||
<!-- 交易类型 -->
|
||||
<van-field name="type" label="交易类型">
|
||||
<van-field
|
||||
name="type"
|
||||
label="交易类型"
|
||||
>
|
||||
<template #input>
|
||||
<van-radio-group v-model="batchForm.type" direction="horizontal">
|
||||
<van-radio :name="0">支出</van-radio>
|
||||
<van-radio :name="1">收入</van-radio>
|
||||
<van-radio :name="2">不计</van-radio>
|
||||
<van-radio-group
|
||||
v-model="batchForm.type"
|
||||
direction="horizontal"
|
||||
>
|
||||
<van-radio :name="0">
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio :name="1">
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio :name="2">
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 分类选择 -->
|
||||
<van-field name="classify" label="分类">
|
||||
<van-field
|
||||
name="classify"
|
||||
label="分类"
|
||||
>
|
||||
<template #input>
|
||||
<span v-if="!batchForm.classify" style="opacity: 0.4;">请选择分类</span>
|
||||
<span
|
||||
v-if="!batchForm.classify"
|
||||
style="opacity: 0.4"
|
||||
>请选择分类</span>
|
||||
<span v-else>{{ batchForm.classify }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
|
||||
<!-- 分类选择组件 -->
|
||||
<ClassifySelector
|
||||
v-model="batchForm.classify"
|
||||
@@ -138,9 +168,9 @@
|
||||
</van-form>
|
||||
<template #footer>
|
||||
<van-button
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
@click="handleConfirmBatchUpdate"
|
||||
>
|
||||
确定
|
||||
@@ -152,13 +182,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import {
|
||||
showToast,
|
||||
showSuccessToast,
|
||||
showLoadingToast,
|
||||
closeToast,
|
||||
showConfirmDialog
|
||||
} from 'vant'
|
||||
import { showToast, showSuccessToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||
import { getReasonGroups, batchUpdateByReason, getTransactionList } from '@/api/transactionRecord'
|
||||
import ClassifySelector from './ClassifySelector.vue'
|
||||
import TransactionList from './TransactionList.vue'
|
||||
@@ -212,9 +236,12 @@ const batchForm = ref({
|
||||
})
|
||||
|
||||
// 监听交易类型变化,重新加载分类
|
||||
watch(() => batchForm.value.type, (newVal) => {
|
||||
batchForm.value.classify = ''
|
||||
})
|
||||
watch(
|
||||
() => batchForm.value.type,
|
||||
(newVal) => {
|
||||
batchForm.value.classify = ''
|
||||
}
|
||||
)
|
||||
|
||||
// 获取类型名称
|
||||
const getTypeName = (type) => {
|
||||
@@ -256,8 +283,10 @@ const handleGroupClick = async (group) => {
|
||||
|
||||
// 加载分组的交易记录
|
||||
const loadGroupTransactions = async () => {
|
||||
if (transactionFinished.value || !selectedGroup.value) return
|
||||
|
||||
if (transactionFinished.value || !selectedGroup.value) {
|
||||
return
|
||||
}
|
||||
|
||||
transactionLoading.value = true
|
||||
try {
|
||||
const res = await getTransactionList({
|
||||
@@ -265,17 +294,17 @@ const loadGroupTransactions = async () => {
|
||||
pageIndex: transactionPageIndex.value,
|
||||
pageSize: transactionPageSize.value
|
||||
})
|
||||
|
||||
|
||||
if (res.success) {
|
||||
const newData = res.data || []
|
||||
groupTransactions.value = [...groupTransactions.value, ...newData]
|
||||
groupTransactionsTotal.value = res.total || 0
|
||||
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (newData.length < transactionPageSize.value) {
|
||||
transactionFinished.value = true
|
||||
}
|
||||
|
||||
|
||||
transactionPageIndex.value++
|
||||
} else {
|
||||
showToast(res.message || '获取交易记录失败')
|
||||
@@ -323,7 +352,7 @@ const handleConfirmBatchUpdate = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
await batchFormRef.value?.validate()
|
||||
|
||||
|
||||
// 二次确认
|
||||
await showConfirmDialog({
|
||||
title: '确认批量设置',
|
||||
@@ -352,21 +381,19 @@ const handleConfirmBatchUpdate = async () => {
|
||||
await refresh()
|
||||
// 通知父组件数据已更改
|
||||
emit('data-changed')
|
||||
try {
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(
|
||||
'transactions-changed',
|
||||
{
|
||||
detail: {
|
||||
reason: batchGroup.value.reason
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch(e) {
|
||||
new CustomEvent('transactions-changed', {
|
||||
detail: {
|
||||
reason: batchGroup.value.reason
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('触发全局 transactions-changed 事件失败:', e)
|
||||
}
|
||||
// 关闭弹窗
|
||||
showTransactionList.value = false
|
||||
}
|
||||
// 关闭弹窗
|
||||
showTransactionList.value = false
|
||||
} else {
|
||||
showToast(res.message || '批量更新失败')
|
||||
}
|
||||
@@ -398,18 +425,18 @@ const handleTransactionClick = (transaction) => {
|
||||
|
||||
// 处理分组中的删除事件
|
||||
const handleGroupTransactionDelete = async (transactionId) => {
|
||||
groupTransactions.value = groupTransactions.value.filter(t => t.id !== transactionId)
|
||||
groupTransactions.value = groupTransactions.value.filter((t) => t.id !== transactionId)
|
||||
groupTransactionsTotal.value = Math.max(0, (groupTransactionsTotal.value || 0) - 1)
|
||||
|
||||
if(groupTransactions.value.length === 0 && !transactionFinished.value) {
|
||||
if (groupTransactions.value.length === 0 && !transactionFinished.value) {
|
||||
// 如果当前页数据为空且未加载完,则尝试加载下一页
|
||||
await loadGroupTransactions()
|
||||
}
|
||||
|
||||
if(groupTransactions.value.length === 0){
|
||||
if (groupTransactions.value.length === 0) {
|
||||
// 如果删除后当前分组没有交易了,关闭弹窗
|
||||
showTransactionList.value = false
|
||||
groups.value = groups.value.filter(g => g.reason !== selectedGroup.value.reason)
|
||||
groups.value = groups.value.filter((g) => g.reason !== selectedGroup.value.reason)
|
||||
selectedGroup.value = null
|
||||
total.value--
|
||||
}
|
||||
@@ -433,10 +460,12 @@ const onGlobalTransactionDeleted = () => {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener && window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
window.addEventListener &&
|
||||
window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener && window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
window.removeEventListener &&
|
||||
window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
})
|
||||
|
||||
// 当有交易新增/修改/批量更新时的刷新监听
|
||||
@@ -451,10 +480,12 @@ const onGlobalTransactionsChanged = () => {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener && window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||
window.addEventListener &&
|
||||
window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener && window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||
window.removeEventListener &&
|
||||
window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
|
||||
})
|
||||
|
||||
// 处理账单保存后的回调
|
||||
@@ -471,8 +502,10 @@ const handleTransactionSaved = async () => {
|
||||
* 加载数据(支持分页)
|
||||
*/
|
||||
const loadData = async () => {
|
||||
if (finished.value) return
|
||||
|
||||
if (finished.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getReasonGroups(pageIndex.value, props.pageSize)
|
||||
@@ -480,14 +513,14 @@ const loadData = async () => {
|
||||
const newData = res.data || []
|
||||
groups.value = [...groups.value, ...newData]
|
||||
total.value = res.total || 0
|
||||
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (groups.value.length >= total.value) {
|
||||
finished.value = true
|
||||
}
|
||||
|
||||
|
||||
pageIndex.value++
|
||||
|
||||
|
||||
emit('data-loaded', {
|
||||
groups: groups.value,
|
||||
total: total.value,
|
||||
@@ -522,7 +555,7 @@ const refresh = async () => {
|
||||
*/
|
||||
const getList = (onlySelected = false) => {
|
||||
if (onlySelected && props.selectable) {
|
||||
return groups.value.filter(g => selectedReasons.value.has(g.reason))
|
||||
return groups.value.filter((g) => selectedReasons.value.has(g.reason))
|
||||
}
|
||||
return [...groups.value]
|
||||
}
|
||||
@@ -564,7 +597,7 @@ const clearSelection = () => {
|
||||
* 全选
|
||||
*/
|
||||
const selectAll = () => {
|
||||
selectedReasons.value = new Set(groups.value.map(g => g.reason))
|
||||
selectedReasons.value = new Set(groups.value.map((g) => g.reason))
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
<template v-if="!loading && !saving">
|
||||
<van-icon :name="buttonIcon" />
|
||||
<span style="margin-left: 4px;">{{ buttonText }}</span>
|
||||
<span style="margin-left: 4px">{{ buttonText }}</span>
|
||||
</template>
|
||||
</van-button>
|
||||
</template>
|
||||
@@ -52,28 +52,42 @@ const hasClassifiedResults = computed(() => {
|
||||
|
||||
// 按钮类型
|
||||
const buttonType = computed(() => {
|
||||
if (saving.value) return 'warning'
|
||||
if (loading.value) return 'primary'
|
||||
if (hasClassifiedResults.value) return 'success'
|
||||
if (saving.value) {
|
||||
return 'warning'
|
||||
}
|
||||
if (loading.value) {
|
||||
return 'primary'
|
||||
}
|
||||
if (hasClassifiedResults.value) {
|
||||
return 'success'
|
||||
}
|
||||
return 'primary'
|
||||
})
|
||||
|
||||
// 按钮图标
|
||||
const buttonIcon = computed(() => {
|
||||
if (hasClassifiedResults.value) return 'success'
|
||||
if (hasClassifiedResults.value) {
|
||||
return 'success'
|
||||
}
|
||||
return 'fire'
|
||||
})
|
||||
|
||||
// 按钮文字(非加载状态)
|
||||
const buttonText = computed(() => {
|
||||
if (hasClassifiedResults.value) return '保存分类'
|
||||
if (hasClassifiedResults.value) {
|
||||
return '保存分类'
|
||||
}
|
||||
return '智能分类'
|
||||
})
|
||||
|
||||
// 加载中文字
|
||||
const loadingText = computed(() => {
|
||||
if (saving.value) return '保存中...'
|
||||
if (loading.value) return '分类中...'
|
||||
if (saving.value) {
|
||||
return '保存中...'
|
||||
}
|
||||
if (loading.value) {
|
||||
return '分类中...'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
@@ -92,8 +106,10 @@ const handleClick = () => {
|
||||
* 保存分类结果
|
||||
*/
|
||||
const handleSaveClassify = async () => {
|
||||
if (saving.value || loading.value) return
|
||||
|
||||
if (saving.value || loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
saving.value = true
|
||||
showToast({
|
||||
@@ -104,27 +120,27 @@ const handleSaveClassify = async () => {
|
||||
})
|
||||
|
||||
// 准备批量更新数据
|
||||
const items = classifiedResults.value.map(item => ({
|
||||
const items = classifiedResults.value.map((item) => ({
|
||||
id: item.id,
|
||||
classify: item.classify,
|
||||
type: item.type
|
||||
}))
|
||||
|
||||
const response = await batchUpdateClassify(items)
|
||||
|
||||
|
||||
closeToast()
|
||||
|
||||
|
||||
if (response.success) {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: `保存成功,已更新 ${items.length} 条记录`,
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
|
||||
// 清空已分类结果
|
||||
classifiedResults.value = []
|
||||
isAllCompleted.value = false
|
||||
|
||||
|
||||
// 通知父组件刷新数据
|
||||
emit('save')
|
||||
} else {
|
||||
@@ -161,7 +177,7 @@ const handleSmartClassify = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if(lockClassifiedResults.value) {
|
||||
if (lockClassifiedResults.value) {
|
||||
showToast('当前有分类任务正在进行,请稍后再试')
|
||||
loading.value = false
|
||||
return
|
||||
@@ -170,10 +186,10 @@ const handleSmartClassify = async () => {
|
||||
// 清空之前的分类结果
|
||||
isAllCompleted.value = false
|
||||
classifiedResults.value = []
|
||||
|
||||
|
||||
const batchSize = 3
|
||||
let processedCount = 0
|
||||
|
||||
|
||||
try {
|
||||
lockClassifiedResults.value = true
|
||||
// 等待父组件的 beforeClassify 事件处理完成(如果有返回 Promise)
|
||||
@@ -200,10 +216,10 @@ const handleSmartClassify = async () => {
|
||||
// 分批处理
|
||||
for (let i = 0; i < allTransactions.length; i += batchSize) {
|
||||
const batch = allTransactions.slice(i, i + batchSize)
|
||||
const transactionIds = batch.map(t => t.id)
|
||||
const transactionIds = batch.map((t) => t.id)
|
||||
const currentBatch = Math.floor(i / batchSize) + 1
|
||||
const totalBatches = Math.ceil(allTransactions.length / batchSize)
|
||||
|
||||
|
||||
// 更新批次进度
|
||||
closeToast()
|
||||
toastInstance = showToast({
|
||||
@@ -214,7 +230,7 @@ const handleSmartClassify = async () => {
|
||||
})
|
||||
|
||||
const response = await smartClassify(transactionIds)
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('智能分类请求失败')
|
||||
}
|
||||
@@ -228,23 +244,27 @@ const handleSmartClassify = async () => {
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) break
|
||||
|
||||
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
|
||||
// 处理完整的事件(SSE格式:event: type\ndata: data\n\n)
|
||||
const events = buffer.split('\n\n')
|
||||
buffer = events.pop() || '' // 保留最后一个不完整的部分
|
||||
|
||||
|
||||
for (const eventBlock of events) {
|
||||
if (!eventBlock.trim()) continue
|
||||
|
||||
if (!eventBlock.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const lines = eventBlock.split('\n')
|
||||
let eventType = ''
|
||||
let eventData = ''
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.slice(7).trim()
|
||||
@@ -252,7 +272,7 @@ const handleSmartClassify = async () => {
|
||||
eventData = line.slice(6).trim()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (eventType === 'start') {
|
||||
// 开始分类
|
||||
closeToast()
|
||||
@@ -267,23 +287,23 @@ const handleSmartClassify = async () => {
|
||||
// 收到分类结果
|
||||
const data = JSON.parse(eventData)
|
||||
processedCount++
|
||||
|
||||
|
||||
// 记录分类结果
|
||||
classifiedResults.value.push({
|
||||
id: data.id,
|
||||
classify: data.Classify,
|
||||
type: data.Type
|
||||
})
|
||||
|
||||
|
||||
// 实时更新交易记录的分类信息
|
||||
const index = props.transactions.findIndex(t => t.id === data.id)
|
||||
const index = props.transactions.findIndex((t) => t.id === data.id)
|
||||
if (index !== -1) {
|
||||
const transaction = props.transactions[index]
|
||||
transaction.upsetedClassify = data.Classify
|
||||
transaction.upsetedType = data.Type
|
||||
emit('notifyDonedTransactionId', data.id)
|
||||
}
|
||||
|
||||
|
||||
// 限制Toast更新频率,避免频繁的DOM操作
|
||||
const now = Date.now()
|
||||
if (now - lastUpdateTime > updateInterval) {
|
||||
@@ -310,7 +330,7 @@ const handleSmartClassify = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 所有批次完成
|
||||
closeToast()
|
||||
toastInstance = null
|
||||
@@ -344,18 +364,18 @@ const handleSmartClassify = async () => {
|
||||
|
||||
const removeClassifiedTransaction = (transactionId) => {
|
||||
// 从已分类结果中移除指定ID的项
|
||||
classifiedResults.value = classifiedResults.value.filter(item => item.id !== transactionId)
|
||||
classifiedResults.value = classifiedResults.value.filter((item) => item.id !== transactionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件状态
|
||||
*/
|
||||
const reset = () => {
|
||||
if(lockClassifiedResults.value) {
|
||||
if (lockClassifiedResults.value) {
|
||||
showToast('当前有分类任务正在进行,无法重置')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isAllCompleted.value = false
|
||||
classifiedResults.value = []
|
||||
loading.value = false
|
||||
@@ -365,8 +385,7 @@ const reset = () => {
|
||||
defineExpose({
|
||||
reset,
|
||||
removeClassifiedTransaction
|
||||
});
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,123 +1,124 @@
|
||||
<template>
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
title="交易详情"
|
||||
height="75%"
|
||||
:closeable="false"
|
||||
>
|
||||
<PopupContainer v-model="visible" title="交易详情" height="75%" :closeable="false">
|
||||
<template #header-actions>
|
||||
<van-button size="small" type="primary" plain @click="handleOffsetClick">抵账</van-button>
|
||||
<van-button size="small" type="primary" plain @click="handleOffsetClick"> 抵账 </van-button>
|
||||
</template>
|
||||
|
||||
<van-form style="margin-top: 12px;">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="记录时间" :value="formatDate(transaction.createTime)" />
|
||||
</van-cell-group>
|
||||
<van-form style="margin-top: 12px">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="记录时间" :value="formatDate(transaction.createTime)" />
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset title="交易明细">
|
||||
<van-field
|
||||
v-model="occurredAtLabel"
|
||||
name="occurredAt"
|
||||
label="交易时间"
|
||||
readonly
|
||||
is-link
|
||||
placeholder="请选择交易时间"
|
||||
:rules="[{ required: true, message: '请选择交易时间' }]"
|
||||
@click="showDatePicker = true"
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.reason"
|
||||
name="reason"
|
||||
label="交易摘要"
|
||||
placeholder="请输入交易摘要"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.amount"
|
||||
name="amount"
|
||||
label="交易金额"
|
||||
placeholder="请输入交易金额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入交易金额' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.balance"
|
||||
name="balance"
|
||||
label="交易后余额"
|
||||
placeholder="请输入交易后余额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入交易后余额' }]"
|
||||
/>
|
||||
<van-cell-group inset title="交易明细">
|
||||
<van-field
|
||||
v-model="occurredAtLabel"
|
||||
name="occurredAt"
|
||||
label="交易时间"
|
||||
readonly
|
||||
is-link
|
||||
placeholder="请选择交易时间"
|
||||
:rules="[{ required: true, message: '请选择交易时间' }]"
|
||||
@click="showDatePicker = true"
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.reason"
|
||||
name="reason"
|
||||
label="交易摘要"
|
||||
placeholder="请输入交易摘要"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.amount"
|
||||
name="amount"
|
||||
label="交易金额"
|
||||
placeholder="请输入交易金额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入交易金额' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="editForm.balance"
|
||||
name="balance"
|
||||
label="交易后余额"
|
||||
placeholder="请输入交易后余额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入交易后余额' }]"
|
||||
/>
|
||||
|
||||
<van-field name="type" label="交易类型">
|
||||
<template #input>
|
||||
<van-radio-group v-model="editForm.type" direction="horizontal" @change="handleTypeChange">
|
||||
<van-radio :name="0">支出</van-radio>
|
||||
<van-radio :name="1">收入</van-radio>
|
||||
<van-radio :name="2">不计</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field name="type" label="交易类型">
|
||||
<template #input>
|
||||
<van-radio-group
|
||||
v-model="editForm.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio :name="0"> 支出 </van-radio>
|
||||
<van-radio :name="1"> 收入 </van-radio>
|
||||
<van-radio :name="2"> 不计 </van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field name="classify" label="交易分类">
|
||||
<template #input>
|
||||
<div style="flex: 1;">
|
||||
<div
|
||||
v-if="transaction && transaction.unconfirmedClassify && transaction.unconfirmedClassify !== editForm.classify"
|
||||
class="suggestion-tip"
|
||||
@click="applySuggestion"
|
||||
>
|
||||
<van-icon name="bulb-o" class="suggestion-icon" />
|
||||
<span class="suggestion-text">
|
||||
建议: {{ transaction.unconfirmedClassify }}
|
||||
<span v-if="transaction.unconfirmedType !== null && transaction.unconfirmedType !== undefined && transaction.unconfirmedType !== editForm.type">
|
||||
({{ getTypeName(transaction.unconfirmedType) }})
|
||||
</span>
|
||||
<van-field name="classify" label="交易分类">
|
||||
<template #input>
|
||||
<div style="flex: 1">
|
||||
<div
|
||||
v-if="
|
||||
transaction &&
|
||||
transaction.unconfirmedClassify &&
|
||||
transaction.unconfirmedClassify !== editForm.classify
|
||||
"
|
||||
class="suggestion-tip"
|
||||
@click="applySuggestion"
|
||||
>
|
||||
<van-icon name="bulb-o" class="suggestion-icon" />
|
||||
<span class="suggestion-text">
|
||||
建议: {{ transaction.unconfirmedClassify }}
|
||||
<span
|
||||
v-if="
|
||||
transaction.unconfirmedType !== null &&
|
||||
transaction.unconfirmedType !== undefined &&
|
||||
transaction.unconfirmedType !== editForm.type
|
||||
"
|
||||
>
|
||||
({{ getTypeName(transaction.unconfirmedType) }})
|
||||
</span>
|
||||
<div class="suggestion-apply">应用</div>
|
||||
</div>
|
||||
<span v-else-if="!editForm.classify" style="color: var(--van-gray-5);">请选择交易分类</span>
|
||||
<span v-else>{{ editForm.classify }}</span>
|
||||
</span>
|
||||
<div class="suggestion-apply">应用</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<ClassifySelector
|
||||
v-model="editForm.classify"
|
||||
:type="editForm.type"
|
||||
@change="handleClassifyChange"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<span v-else-if="!editForm.classify" style="color: var(--van-gray-5)"
|
||||
>请选择交易分类</span
|
||||
>
|
||||
<span v-else>{{ editForm.classify }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<ClassifySelector
|
||||
v-model="editForm.classify"
|
||||
:type="editForm.type"
|
||||
@change="handleClassifyChange"
|
||||
/>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<template #footer>
|
||||
<van-button
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<van-button round block type="primary" :loading="submitting" @click="onSubmit">
|
||||
保存修改
|
||||
</van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 抵账候选列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showOffsetPopup"
|
||||
title="选择抵账交易"
|
||||
height="75%"
|
||||
>
|
||||
<PopupContainer v-model="showOffsetPopup" title="选择抵账交易" height="75%">
|
||||
<van-list>
|
||||
<van-cell
|
||||
v-for="item in offsetCandidates"
|
||||
:key="item.id"
|
||||
:title="item.reason"
|
||||
<van-cell
|
||||
v-for="item in offsetCandidates"
|
||||
:key="item.id"
|
||||
:title="item.reason"
|
||||
:label="formatDate(item.occurredAt)"
|
||||
:value="item.amount"
|
||||
is-link
|
||||
@@ -154,7 +155,11 @@ import { showToast, showConfirmDialog } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||
import { updateTransaction, getCandidatesForOffset, offsetTransactions } from '@/api/transactionRecord'
|
||||
import {
|
||||
updateTransaction,
|
||||
getCandidatesForOffset,
|
||||
offsetTransactions
|
||||
} from '@/api/transactionRecord'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -196,35 +201,41 @@ const occurredAtLabel = computed(() => {
|
||||
})
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.show, (newVal) => {
|
||||
visible.value = newVal
|
||||
})
|
||||
|
||||
watch(() => props.transaction, (newVal) => {
|
||||
if (newVal) {
|
||||
isSyncing.value = true
|
||||
// 填充编辑表单
|
||||
editForm.id = newVal.id
|
||||
editForm.reason = newVal.reason || ''
|
||||
editForm.amount = String(newVal.amount)
|
||||
editForm.balance = String(newVal.balance)
|
||||
editForm.type = newVal.type
|
||||
editForm.classify = newVal.classify || ''
|
||||
|
||||
// 初始化日期时间
|
||||
if (newVal.occurredAt) {
|
||||
editForm.occurredAt = newVal.occurredAt
|
||||
const dt = dayjs(newVal.occurredAt)
|
||||
currentDate.value = dt.format('YYYY-MM-DD').split('-')
|
||||
currentTime.value = dt.format('HH:mm').split(':')
|
||||
}
|
||||
|
||||
// 在下一个 tick 结束同步状态,确保 van-radio-group 的 @change 已触发完毕
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
visible.value = newVal
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.transaction,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
isSyncing.value = true
|
||||
// 填充编辑表单
|
||||
editForm.id = newVal.id
|
||||
editForm.reason = newVal.reason || ''
|
||||
editForm.amount = String(newVal.amount)
|
||||
editForm.balance = String(newVal.balance)
|
||||
editForm.type = newVal.type
|
||||
editForm.classify = newVal.classify || ''
|
||||
|
||||
// 初始化日期时间
|
||||
if (newVal.occurredAt) {
|
||||
editForm.occurredAt = newVal.occurredAt
|
||||
const dt = dayjs(newVal.occurredAt)
|
||||
currentDate.value = dt.format('YYYY-MM-DD').split('-')
|
||||
currentTime.value = dt.format('HH:mm').split(':')
|
||||
}
|
||||
|
||||
// 在下一个 tick 结束同步状态,确保 van-radio-group 的 @change 已触发完毕
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:show', newVal)
|
||||
@@ -258,7 +269,10 @@ const onConfirmTime = ({ selectedValues }) => {
|
||||
const applySuggestion = () => {
|
||||
if (props.transaction.unconfirmedClassify) {
|
||||
editForm.classify = props.transaction.unconfirmedClassify
|
||||
if (props.transaction.unconfirmedType !== null && props.transaction.unconfirmedType !== undefined) {
|
||||
if (
|
||||
props.transaction.unconfirmedType !== null &&
|
||||
props.transaction.unconfirmedType !== undefined
|
||||
) {
|
||||
editForm.type = props.transaction.unconfirmedType
|
||||
}
|
||||
}
|
||||
@@ -277,7 +291,7 @@ const getTypeName = (type) => {
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
|
||||
const data = {
|
||||
id: editForm.id,
|
||||
reason: editForm.reason,
|
||||
@@ -287,7 +301,7 @@ const onSubmit = async () => {
|
||||
classify: editForm.classify,
|
||||
occurredAt: editForm.occurredAt
|
||||
}
|
||||
|
||||
|
||||
const response = await updateTransaction(data)
|
||||
if (response.success) {
|
||||
showToast('保存成功')
|
||||
@@ -314,11 +328,13 @@ const handleClassifyChange = () => {
|
||||
|
||||
// 清空分类
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
if (!dateString) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
@@ -347,7 +363,7 @@ const handleOffsetClick = async () => {
|
||||
const handleCandidateSelect = (candidate) => {
|
||||
showConfirmDialog({
|
||||
title: '确认抵账',
|
||||
message: `确认将当前交易与 "${candidate.reason}" (${candidate.amount}) 互相抵消吗?\n抵消后两笔交易将被删除。`,
|
||||
message: `确认将当前交易与 "${candidate.reason}" (${candidate.amount}) 互相抵消吗?\n抵消后两笔交易将被删除。`
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
@@ -367,7 +383,7 @@ const handleCandidateSelect = (candidate) => {
|
||||
})
|
||||
.catch(() => {
|
||||
// on cancel
|
||||
});
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div class="transaction-list-container transaction-list">
|
||||
<van-list
|
||||
:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-list :loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||
<van-cell-group v-if="transactions && transactions.length" inset style="margin-top: 10px">
|
||||
<van-swipe-cell
|
||||
v-for="transaction in transactions"
|
||||
@@ -13,16 +8,13 @@
|
||||
class="transaction-item"
|
||||
>
|
||||
<div class="transaction-row">
|
||||
<van-checkbox
|
||||
<van-checkbox
|
||||
v-if="showCheckbox"
|
||||
:model-value="isSelected(transaction.id)"
|
||||
class="checkbox-col"
|
||||
@update:model-value="toggleSelection(transaction)"
|
||||
/>
|
||||
<div
|
||||
class="transaction-card"
|
||||
@click="handleClick(transaction)"
|
||||
>
|
||||
<div class="transaction-card" @click="handleClick(transaction)">
|
||||
<div class="card-left">
|
||||
<div class="transaction-title">
|
||||
<span class="reason">{{ transaction.reason || '(无摘要)' }}</span>
|
||||
@@ -30,34 +22,32 @@
|
||||
<div class="transaction-info">
|
||||
<div>交易时间: {{ formatDate(transaction.occurredAt) }}</div>
|
||||
<div>
|
||||
<span v-if="transaction.classify">
|
||||
分类: {{ transaction.classify }}
|
||||
</span>
|
||||
<span v-if="transaction.upsetedClassify && transaction.upsetedClassify !== transaction.classify" style="color: var(--van-warning-color)">
|
||||
<span v-if="transaction.classify"> 分类: {{ transaction.classify }} </span>
|
||||
<span
|
||||
v-if="
|
||||
transaction.upsetedClassify &&
|
||||
transaction.upsetedClassify !== transaction.classify
|
||||
"
|
||||
style="color: var(--van-warning-color)"
|
||||
>
|
||||
→ {{ transaction.upsetedClassify }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div v-if="transaction.importFrom">
|
||||
来源: {{ transaction.importFrom }}
|
||||
</div>
|
||||
<div v-if="transaction.importFrom">来源: {{ transaction.importFrom }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-middle">
|
||||
<van-tag
|
||||
:type="getTypeTagType(transaction.type)"
|
||||
size="medium"
|
||||
>
|
||||
<van-tag :type="getTypeTagType(transaction.type)" size="medium">
|
||||
{{ getTypeName(transaction.type) }}
|
||||
</van-tag>
|
||||
<template
|
||||
v-if="Number.isFinite(transaction.upsetedType) && transaction.upsetedType !== transaction.type"
|
||||
<template
|
||||
v-if="
|
||||
Number.isFinite(transaction.upsetedType) &&
|
||||
transaction.upsetedType !== transaction.type
|
||||
"
|
||||
>
|
||||
→
|
||||
<van-tag
|
||||
:type="getTypeTagType(transaction.upsetedType)"
|
||||
size="medium"
|
||||
>
|
||||
<van-tag :type="getTypeTagType(transaction.upsetedType)" size="medium">
|
||||
{{ getTypeName(transaction.upsetedType) }}
|
||||
</van-tag>
|
||||
</template>
|
||||
@@ -70,7 +60,10 @@
|
||||
<div v-if="transaction.balance && transaction.balance > 0" class="balance">
|
||||
余额: {{ formatMoney(transaction.balance) }}
|
||||
</div>
|
||||
<div v-if="transaction.refundAmount && transaction.refundAmount > 0" class="balance">
|
||||
<div
|
||||
v-if="transaction.refundAmount && transaction.refundAmount > 0"
|
||||
class="balance"
|
||||
>
|
||||
退款: {{ formatMoney(transaction.refundAmount) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,9 +72,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="showDelete" #right>
|
||||
<van-button
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
type="danger"
|
||||
text="删除"
|
||||
class="delete-button"
|
||||
@click="handleDeleteClick(transaction)"
|
||||
@@ -90,9 +83,9 @@
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty
|
||||
v-if="!loading && !(transactions && transactions.length)"
|
||||
description="暂无交易记录"
|
||||
<van-empty
|
||||
v-if="!loading && !(transactions && transactions.length)"
|
||||
description="暂无交易记录"
|
||||
/>
|
||||
</van-list>
|
||||
</div>
|
||||
@@ -212,16 +205,24 @@ const getTypeTagType = (type) => {
|
||||
|
||||
// 获取金额样式类
|
||||
const getAmountClass = (type) => {
|
||||
if (type === 0) return 'expense'
|
||||
if (type === 1) return 'income'
|
||||
if (type === 0) {
|
||||
return 'expense'
|
||||
}
|
||||
if (type === 1) {
|
||||
return 'income'
|
||||
}
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
// 格式化金额(带符号)
|
||||
const formatAmount = (amount, type) => {
|
||||
const formatted = formatMoney(amount)
|
||||
if (type === 0) return `- ${formatted}`
|
||||
if (type === 1) return `+ ${formatted}`
|
||||
if (type === 0) {
|
||||
return `- ${formatted}`
|
||||
}
|
||||
if (type === 1) {
|
||||
return `+ ${formatted}`
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
@@ -232,11 +233,13 @@ const formatMoney = (amount) => {
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
if (!dateString) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
|
||||
Reference in New Issue
Block a user