大量的代码格式化
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,58 +1,60 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar title="账单" placeholder>
|
||||
<template #right>
|
||||
<van-button
|
||||
<van-button
|
||||
v-if="tabActive === 'email'"
|
||||
size="small"
|
||||
type="primary"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="emailRecordRef.handleSync()"
|
||||
>
|
||||
立即同步
|
||||
</van-button>
|
||||
<van-icon
|
||||
v-if="tabActive === 'message'"
|
||||
name="passed"
|
||||
size="20"
|
||||
@click="messageViewRef?.handleMarkAllRead()"
|
||||
<van-icon
|
||||
v-if="tabActive === 'message'"
|
||||
name="passed"
|
||||
size="20"
|
||||
@click="messageViewRef?.handleMarkAllRead()"
|
||||
/>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
<van-tabs v-model:active="tabActive" type="card" style="margin: 12px 0 2px 0;">
|
||||
<van-tabs v-model:active="tabActive" type="card" style="margin: 12px 0 2px 0">
|
||||
<van-tab title="账单" name="balance" />
|
||||
<van-tab title="邮件" name="email" />
|
||||
<van-tab title="消息" name="message" />
|
||||
</van-tabs>
|
||||
|
||||
<TransactionsRecord v-if="tabActive === 'balance'" ref="transactionsRecordRef"/>
|
||||
<TransactionsRecord v-if="tabActive === 'balance'" ref="transactionsRecordRef" />
|
||||
<EmailRecord v-else-if="tabActive === 'email'" ref="emailRecordRef" />
|
||||
<MessageView v-else-if="tabActive === 'message'" ref="messageViewRef" :is-component="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import TransactionsRecord from './TransactionsRecord.vue';
|
||||
import EmailRecord from './EmailRecord.vue';
|
||||
import MessageView from './MessageView.vue';
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import TransactionsRecord from './TransactionsRecord.vue'
|
||||
import EmailRecord from './EmailRecord.vue'
|
||||
import MessageView from './MessageView.vue'
|
||||
|
||||
const route = useRoute();
|
||||
const tabActive = ref(route.query.tab || 'balance');
|
||||
const route = useRoute()
|
||||
const tabActive = ref(route.query.tab || 'balance')
|
||||
|
||||
// 监听路由参数变化,用于从 tabbar 点击时切换 tab
|
||||
watch(() => route.query.tab, (newTab) => {
|
||||
if (newTab) {
|
||||
tabActive.value = newTab;
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(newTab) => {
|
||||
if (newTab) {
|
||||
tabActive.value = newTab
|
||||
}
|
||||
}
|
||||
});
|
||||
)
|
||||
|
||||
const transactionsRecordRef = ref(null);
|
||||
const emailRecordRef = ref(null);
|
||||
const messageViewRef = ref(null);
|
||||
const transactionsRecordRef = ref(null)
|
||||
const emailRecordRef = ref(null)
|
||||
const messageViewRef = ref(null)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -66,4 +68,4 @@ const messageViewRef = ref(null);
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<div class="page-container-flex">
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar
|
||||
title="智能分析"
|
||||
left-arrow
|
||||
placeholder
|
||||
<van-nav-bar
|
||||
title="智能分析"
|
||||
left-arrow
|
||||
placeholder
|
||||
@click-left="onClickLeft"
|
||||
>
|
||||
<template #right>
|
||||
<van-icon
|
||||
name="setting-o"
|
||||
size="20"
|
||||
style="cursor: pointer; padding-right: 12px;"
|
||||
<van-icon
|
||||
name="setting-o"
|
||||
size="20"
|
||||
style="cursor: pointer; padding-right: 12px"
|
||||
@click="onClickPrompt"
|
||||
/>
|
||||
</template>
|
||||
@@ -31,11 +31,13 @@
|
||||
show-word-limit
|
||||
:disabled="analyzing"
|
||||
/>
|
||||
|
||||
|
||||
<div class="quick-questions">
|
||||
<div class="quick-title">快捷问题</div>
|
||||
<van-tag
|
||||
v-for="(q, index) in quickQuestions"
|
||||
<div class="quick-title">
|
||||
快捷问题
|
||||
</div>
|
||||
<van-tag
|
||||
v-for="(q, index) in quickQuestions"
|
||||
:key="index"
|
||||
type="primary"
|
||||
plain
|
||||
@@ -47,9 +49,9 @@
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
:loading="analyzing"
|
||||
loading-text="分析中..."
|
||||
@@ -61,23 +63,32 @@
|
||||
</div>
|
||||
|
||||
<!-- 结果区域 -->
|
||||
<div v-if="showResult" class="result-section">
|
||||
<div
|
||||
v-if="showResult"
|
||||
class="result-section"
|
||||
>
|
||||
<div class="result-header">
|
||||
<h3>分析结果</h3>
|
||||
<van-icon
|
||||
v-if="!analyzing"
|
||||
name="delete-o"
|
||||
<van-icon
|
||||
v-if="!analyzing"
|
||||
name="delete-o"
|
||||
size="18"
|
||||
@click="clearResult"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref="resultContainer" class="result-content rich-html-content">
|
||||
<div v-html="resultHtml"></div>
|
||||
<van-loading v-if="analyzing" class="result-loading">
|
||||
|
||||
<div
|
||||
ref="resultContainer"
|
||||
class="result-content rich-html-content"
|
||||
>
|
||||
<div v-html="resultHtml" />
|
||||
<van-loading
|
||||
v-if="analyzing"
|
||||
class="result-loading"
|
||||
>
|
||||
AI正在分析中...
|
||||
</van-loading>
|
||||
<div ref="scrollAnchor"></div>
|
||||
<div ref="scrollAnchor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +221,7 @@ const startAnalysis = async () => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userInput: userInput.value
|
||||
@@ -226,8 +237,10 @@ const startAnalysis = async () => {
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) break
|
||||
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n')
|
||||
@@ -235,7 +248,7 @@ const startAnalysis = async () => {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6).trim()
|
||||
|
||||
|
||||
if (data === '[DONE]') {
|
||||
continue
|
||||
}
|
||||
@@ -254,7 +267,6 @@ const startAnalysis = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('分析失败:', error)
|
||||
showToast('分析失败,请重试')
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar title="预算管理" placeholder>
|
||||
<van-nav-bar
|
||||
title="预算管理"
|
||||
placeholder
|
||||
>
|
||||
<template #right>
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings
|
||||
&& uncoveredCategories.length > 0
|
||||
&& !isArchive"
|
||||
name="warning-o"
|
||||
size="20"
|
||||
<van-icon
|
||||
v-if="
|
||||
activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0 && !isArchive
|
||||
"
|
||||
name="warning-o"
|
||||
size="20"
|
||||
color="var(--van-danger-color)"
|
||||
style="margin-right: 12px"
|
||||
title="查看未覆盖预算的分类"
|
||||
@click="showUncoveredDetails = true"
|
||||
@click="showUncoveredDetails = true"
|
||||
/>
|
||||
<van-icon
|
||||
v-if="isArchive"
|
||||
@@ -22,193 +25,289 @@
|
||||
style="margin-right: 12px"
|
||||
@click="showArchiveSummary()"
|
||||
/>
|
||||
<van-icon
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
name="plus"
|
||||
size="20"
|
||||
name="plus"
|
||||
size="20"
|
||||
title="添加预算"
|
||||
@click="budgetEditRef.open({ category: activeTab })"
|
||||
@click="budgetEditRef.open({ category: activeTab })"
|
||||
/>
|
||||
<van-icon
|
||||
v-else
|
||||
name="setting-o"
|
||||
size="20"
|
||||
name="setting-o"
|
||||
size="20"
|
||||
title="储蓄分类配置"
|
||||
@click="savingsConfigRef.open()"
|
||||
/>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<van-tabs v-model:active="activeTab" type="card" class="budget-tabs" style="margin: 12px 4px;">
|
||||
<van-tab title="支出" :name="BudgetCategory.Expense">
|
||||
<BudgetSummary
|
||||
<van-tabs
|
||||
v-model:active="activeTab"
|
||||
type="card"
|
||||
class="budget-tabs"
|
||||
style="margin: 12px 4px"
|
||||
>
|
||||
<van-tab
|
||||
title="支出"
|
||||
:name="BudgetCategory.Expense"
|
||||
>
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
v-model:date="selectedDate"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<van-pull-refresh v-model="isRefreshing" class="scroll-content" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
class="scroll-content"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<template v-if="expenseBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
||||
<BudgetCard
|
||||
<van-swipe-cell
|
||||
v-for="budget in expenseBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{ 'warning': (budget.current / budget.limit) > 0.8 }"
|
||||
:percent-class="{
|
||||
warning: budget.current / budget.limit > 0.8
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">已支出</div>
|
||||
<div class="value expense">¥{{ formatMoney(budget.current) }}</div>
|
||||
<div class="label">
|
||||
已支出
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">预算</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
<div class="label">
|
||||
预算
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">余额</div>
|
||||
<div class="value" :class="budget.limit - budget.current >= 0 ? 'income' : 'expense'">
|
||||
<div class="label">
|
||||
余额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(budget.limit - budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty v-else description="暂无支出预算" />
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无支出预算"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||
<BudgetSummary
|
||||
<van-tab
|
||||
title="收入"
|
||||
:name="BudgetCategory.Income"
|
||||
>
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
v-model:date="selectedDate"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<van-pull-refresh v-model="isRefreshing" class="scroll-content" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
class="scroll-content"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<template v-if="incomeBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
||||
<BudgetCard
|
||||
<van-swipe-cell
|
||||
v-for="budget in incomeBudgets"
|
||||
:key="budget.id"
|
||||
>
|
||||
<BudgetCard
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||
:percent-class="{
|
||||
income: budget.current / budget.limit >= 1
|
||||
}"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
@click="budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})"
|
||||
@click="
|
||||
budgetEditRef.open({
|
||||
data: budget,
|
||||
isEditFlag: true,
|
||||
category: budget.category
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">已收入</div>
|
||||
<div class="value income">¥{{ formatMoney(budget.current) }}</div>
|
||||
<div class="label">
|
||||
已收入
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">目标</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
<div class="label">
|
||||
目标
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">差额</div>
|
||||
<div class="value" :class="budget.current >= budget.limit ? 'income' : 'expense'">
|
||||
<div class="label">
|
||||
差额
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="budget.current >= budget.limit ? 'income' : 'expense'"
|
||||
>
|
||||
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
<template #right>
|
||||
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(budget)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</template>
|
||||
<van-empty v-else description="暂无收入预算" />
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无收入预算"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||
<van-pull-refresh v-model="isRefreshing" class="scroll-content" style="padding-top:4px" @refresh="onRefresh">
|
||||
<van-tab
|
||||
title="存款"
|
||||
:name="BudgetCategory.Savings"
|
||||
>
|
||||
<van-pull-refresh
|
||||
v-model="isRefreshing"
|
||||
class="scroll-content"
|
||||
style="padding-top: 4px"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div class="budget-list">
|
||||
<template v-if="savingsBudgets?.length > 0">
|
||||
<BudgetCard
|
||||
v-for="budget in savingsBudgets"
|
||||
<BudgetCard
|
||||
v-for="budget in savingsBudgets"
|
||||
:key="budget.id"
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||
:percent-class="{ income: budget.current / budget.limit >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px;"
|
||||
style="margin: 0 12px 12px"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">已存</div>
|
||||
<div class="value income">¥{{ formatMoney(budget.current) }}</div>
|
||||
<div class="label">
|
||||
已存
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">目标</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
<div class="label">
|
||||
目标
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">还差</div>
|
||||
<div class="label">
|
||||
还差
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(Math.max(0, budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<div class="card-footer-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow-left"
|
||||
plain
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow-left"
|
||||
plain
|
||||
type="primary"
|
||||
@click.stop="handleSavingsNav(budget, -1)"
|
||||
>
|
||||
</van-button>
|
||||
/>
|
||||
<span class="current-date-label">
|
||||
{{ getSavingsDateLabel(budget) }}
|
||||
</span>
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow"
|
||||
plain
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow"
|
||||
plain
|
||||
type="primary"
|
||||
icon-position="right"
|
||||
:disabled="disabledSavingsNextNav(budget)"
|
||||
@click.stop="handleSavingsNav(budget, 1)"
|
||||
>
|
||||
</van-button>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
</template>
|
||||
<van-empty v-else description="暂无存款计划" />
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无存款计划"
|
||||
/>
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<BudgetEditPopup
|
||||
<BudgetEditPopup
|
||||
ref="budgetEditRef"
|
||||
@success="fetchBudgetList"
|
||||
@success="fetchBudgetList"
|
||||
/>
|
||||
<SavingsConfigPopup
|
||||
ref="savingsConfigRef"
|
||||
@@ -222,21 +321,37 @@
|
||||
height="60%"
|
||||
>
|
||||
<div class="uncovered-list">
|
||||
<div v-for="item in uncoveredCategories" :key="item.category" class="uncovered-item">
|
||||
<div
|
||||
v-for="item in uncoveredCategories"
|
||||
:key="item.category"
|
||||
class="uncovered-item"
|
||||
>
|
||||
<div class="item-left">
|
||||
<div class="category-name">{{ item.category }}</div>
|
||||
<div class="transaction-count">{{ item.transactionCount }} 笔记录</div>
|
||||
<div class="category-name">
|
||||
{{ item.category }}
|
||||
</div>
|
||||
<div class="transaction-count">
|
||||
{{ item.transactionCount }} 笔记录
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="item-amount" :class="activeTab === BudgetCategory.Expense ? 'expense' : 'income'">
|
||||
<div
|
||||
class="item-amount"
|
||||
:class="activeTab === BudgetCategory.Expense ? 'expense' : 'income'"
|
||||
>
|
||||
¥{{ formatMoney(item.totalAmount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<van-button block round type="primary" @click="showUncoveredDetails = false">
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
type="primary"
|
||||
@click="showUncoveredDetails = false"
|
||||
>
|
||||
我知道了
|
||||
</van-button>
|
||||
</template>
|
||||
@@ -248,10 +363,13 @@
|
||||
:subtitle="`${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月`"
|
||||
height="70%"
|
||||
>
|
||||
<div style="padding: 16px;">
|
||||
<div style="padding: 16px">
|
||||
<div
|
||||
class="rich-html-content"
|
||||
v-html="archiveSummary || '<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'"
|
||||
class="rich-html-content"
|
||||
v-html="
|
||||
archiveSummary ||
|
||||
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</PopupContainer>
|
||||
@@ -261,7 +379,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary, getSavingsBudget } from '@/api/budget'
|
||||
import {
|
||||
getBudgetList,
|
||||
deleteBudget,
|
||||
getCategoryStats,
|
||||
getUncoveredCategories,
|
||||
getArchiveSummary,
|
||||
getSavingsBudget
|
||||
} from '@/api/budget'
|
||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
||||
@@ -279,7 +404,6 @@ const uncoveredCategories = ref([])
|
||||
|
||||
const showSummaryPopup = ref(false)
|
||||
const archiveSummary = ref('')
|
||||
const isSavingSummary = ref(false)
|
||||
|
||||
const expenseBudgets = ref([])
|
||||
const incomeBudgets = ref([])
|
||||
@@ -290,14 +414,19 @@ const overallStats = ref({
|
||||
})
|
||||
|
||||
const activeTabTitle = computed(() => {
|
||||
if (activeTab.value === BudgetCategory.Expense) return '使用'
|
||||
if (activeTab.value === BudgetCategory.Expense) {
|
||||
return '使用'
|
||||
}
|
||||
return '达成'
|
||||
})
|
||||
|
||||
const isArchive = computed(() => {
|
||||
const now = new Date()
|
||||
return selectedDate.value.getFullYear() < now.getFullYear() ||
|
||||
(selectedDate.value.getFullYear() === now.getFullYear() && selectedDate.value.getMonth() < now.getMonth())
|
||||
return (
|
||||
selectedDate.value.getFullYear() < now.getFullYear() ||
|
||||
(selectedDate.value.getFullYear() === now.getFullYear() &&
|
||||
selectedDate.value.getMonth() < now.getMonth())
|
||||
)
|
||||
})
|
||||
|
||||
watch(activeTab, async () => {
|
||||
@@ -305,23 +434,29 @@ watch(activeTab, async () => {
|
||||
})
|
||||
|
||||
watch(selectedDate, async () => {
|
||||
await Promise.all([
|
||||
fetchBudgetList(),
|
||||
fetchCategoryStats(),
|
||||
fetchUncoveredCategories()
|
||||
])
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats(), fetchUncoveredCategories()])
|
||||
})
|
||||
|
||||
const getValueClass = (rate) => {
|
||||
const numRate = parseFloat(rate)
|
||||
if (numRate === 0) return ''
|
||||
if (numRate === 0) {
|
||||
return ''
|
||||
}
|
||||
if (activeTab.value === BudgetCategory.Expense) {
|
||||
if (numRate >= 100) return 'expense'
|
||||
if (numRate >= 80) return 'warning'
|
||||
if (numRate >= 100) {
|
||||
return 'expense'
|
||||
}
|
||||
if (numRate >= 80) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'income'
|
||||
} else {
|
||||
if (numRate >= 100) return 'income'
|
||||
if (numRate >= 80) return 'warning'
|
||||
if (numRate >= 100) {
|
||||
return 'income'
|
||||
}
|
||||
if (numRate >= 80) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'expense'
|
||||
}
|
||||
}
|
||||
@@ -331,9 +466,9 @@ const fetchBudgetList = async () => {
|
||||
const res = await getBudgetList(selectedDate.value.toISOString())
|
||||
if (res.success) {
|
||||
const data = res.data || []
|
||||
expenseBudgets.value = data.filter(b => b.category === BudgetCategory.Expense)
|
||||
incomeBudgets.value = data.filter(b => b.category === BudgetCategory.Income)
|
||||
savingsBudgets.value = data.filter(b => b.category === BudgetCategory.Savings)
|
||||
expenseBudgets.value = data.filter((b) => b.category === BudgetCategory.Expense)
|
||||
incomeBudgets.value = data.filter((b) => b.category === BudgetCategory.Income)
|
||||
savingsBudgets.value = data.filter((b) => b.category === BudgetCategory.Savings)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载预算列表失败', err)
|
||||
@@ -393,18 +528,17 @@ const fetchUncoveredCategories = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchBudgetList(),
|
||||
fetchCategoryStats(),
|
||||
fetchUncoveredCategories()
|
||||
])
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats(), fetchUncoveredCategories()])
|
||||
} catch (err) {
|
||||
console.error('获取初始化数据失败', err)
|
||||
}
|
||||
})
|
||||
|
||||
const formatMoney = (val) => {
|
||||
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })
|
||||
return parseFloat(val || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
})
|
||||
}
|
||||
|
||||
const getPeriodLabel = (type) => {
|
||||
@@ -427,10 +561,12 @@ const getPeriodLabel = (type) => {
|
||||
}
|
||||
|
||||
const getProgressColor = (budget) => {
|
||||
if (!budget.limit || budget.limit === 0) return 'var(--van-primary-color)'
|
||||
|
||||
if (!budget.limit || budget.limit === 0) {
|
||||
return 'var(--van-primary-color)'
|
||||
}
|
||||
|
||||
const ratio = Math.min(Math.max(budget.current / budget.limit, 0), 1)
|
||||
|
||||
|
||||
// 颜色插值辅助函数
|
||||
const interpolate = (start, end, t) => {
|
||||
return Math.round(start + (end - start) * t)
|
||||
@@ -441,7 +577,7 @@ const getProgressColor = (budget) => {
|
||||
// 找到当前值所在的区间
|
||||
let startStop = stops[0]
|
||||
let endStop = stops[stops.length - 1]
|
||||
|
||||
|
||||
for (let i = 0; i < stops.length - 1; i++) {
|
||||
if (value >= stops[i].p && value <= stops[i + 1].p) {
|
||||
startStop = stops[i]
|
||||
@@ -449,28 +585,28 @@ const getProgressColor = (budget) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 计算区间内的相对比例
|
||||
const range = endStop.p - startStop.p
|
||||
const t = (value - startStop.p) / range
|
||||
|
||||
|
||||
const r = interpolate(startStop.c.r, endStop.c.r, t)
|
||||
const g = interpolate(startStop.c.g, endStop.c.g, t)
|
||||
const b = interpolate(startStop.c.b, endStop.c.b, t)
|
||||
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
}
|
||||
|
||||
let stops
|
||||
|
||||
|
||||
if (budget.category === BudgetCategory.Expense) {
|
||||
// 支出: 这是一个"安全 -> 警示 -> 危险"的过程
|
||||
// 使用 蓝绿色 -> 黄色 -> 橙红色的渐变,更加自然且具有高级感
|
||||
stops = [
|
||||
{ p: 0, c: { r: 64, g: 169, b: 255 } }, // 0% 清新的蓝色 (Safe/Fresh)
|
||||
{ p: 0.4, c: { r: 54, g: 207, b: 201 } }, // 40% 青色过渡
|
||||
{ p: 0.7, c: { r: 250, g: 173, b: 20 } }, // 70% 温暖的黄色 (Warning)
|
||||
{ p: 1, c: { r: 255, g: 77, b: 79 } } // 100% 柔和的红色 (Danger)
|
||||
{ p: 0, c: { r: 64, g: 169, b: 255 } }, // 0% 清新的蓝色 (Safe/Fresh)
|
||||
{ p: 0.4, c: { r: 54, g: 207, b: 201 } }, // 40% 青色过渡
|
||||
{ p: 0.7, c: { r: 250, g: 173, b: 20 } }, // 70% 温暖的黄色 (Warning)
|
||||
{ p: 1, c: { r: 255, g: 77, b: 79 } } // 100% 柔和的红色 (Danger)
|
||||
]
|
||||
} else {
|
||||
// 收入/存款: 这是一个"开始 -> 积累 -> 达成"的过程
|
||||
@@ -481,17 +617,17 @@ const getProgressColor = (budget) => {
|
||||
// { p: 0.7, c: { r: 115, g: 209, b: 61 } }, // 70% 草绿 (Good)
|
||||
// { p: 1, c: { r: 35, g: 120, b: 4 } } // 100% 深绿 (Excellent)
|
||||
// ]
|
||||
|
||||
|
||||
// 如果用户喜欢"红->蓝"的逻辑,可以尝试这种"红->白->蓝"的冷暖过渡:
|
||||
stops = [
|
||||
{ p: 0, c: { r: 245, g: 34, b: 45 } }, // 深红
|
||||
{ p: 0, c: { r: 245, g: 34, b: 45 } }, // 深红
|
||||
{ p: 0.45, c: { r: 255, g: 204, b: 204 } }, // 浅红
|
||||
{ p: 0.5, c: { r: 240, g: 242, b: 245 } }, // 中性灰白
|
||||
{ p: 0.5, c: { r: 240, g: 242, b: 245 } }, // 中性灰白
|
||||
{ p: 0.55, c: { r: 186, g: 231, b: 255 } }, // 浅蓝
|
||||
{ p: 1, c: { r: 24, g: 144, b: 255 } } // 深蓝
|
||||
{ p: 1, c: { r: 24, g: 144, b: 255 } } // 深蓝
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
return getGradientColor(ratio, stops)
|
||||
}
|
||||
|
||||
@@ -514,7 +650,7 @@ const handleDelete = async (budget) => {
|
||||
title: '删除预算',
|
||||
message: `确定要删除预算 "${budget.name}" 吗?`
|
||||
})
|
||||
|
||||
|
||||
const res = await deleteBudget(budget.id)
|
||||
if (res.success) {
|
||||
showToast('删除成功')
|
||||
@@ -531,7 +667,9 @@ const handleDelete = async (budget) => {
|
||||
}
|
||||
|
||||
const getSavingsDateLabel = (budget) => {
|
||||
if (!budget.periodStart) return ''
|
||||
if (!budget.periodStart) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(budget.periodStart)
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
return `${date.getFullYear()}年`
|
||||
@@ -541,12 +679,14 @@ const getSavingsDateLabel = (budget) => {
|
||||
}
|
||||
|
||||
const handleSavingsNav = async (budget, offset) => {
|
||||
if (!budget.periodStart) return
|
||||
|
||||
if (!budget.periodStart) {
|
||||
return
|
||||
}
|
||||
|
||||
const date = new Date(budget.periodStart)
|
||||
let year = date.getFullYear()
|
||||
let month = date.getMonth() + 1
|
||||
|
||||
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
year += offset
|
||||
} else {
|
||||
@@ -559,12 +699,12 @@ const handleSavingsNav = async (budget, offset) => {
|
||||
year--
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const res = await getSavingsBudget(year, month, budget.type)
|
||||
if (res.success && res.data) {
|
||||
// 找到并更新对应的 budget 对象
|
||||
const index = savingsBudgets.value.findIndex(b => b.id === budget.id)
|
||||
const index = savingsBudgets.value.findIndex((b) => b.id === budget.id)
|
||||
if (index !== -1) {
|
||||
savingsBudgets.value[index] = res.data
|
||||
}
|
||||
@@ -578,7 +718,9 @@ const handleSavingsNav = async (budget, offset) => {
|
||||
}
|
||||
|
||||
const disabledSavingsNextNav = (budget) => {
|
||||
if (!budget.periodStart) return true
|
||||
if (!budget.periodStart) {
|
||||
return true
|
||||
}
|
||||
const date = new Date(budget.periodStart)
|
||||
const now = new Date()
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
@@ -693,7 +835,9 @@ const disabledSavingsNextNav = (budget) => {
|
||||
.item-amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, system-ui;
|
||||
font-family:
|
||||
DIN Alternate,
|
||||
system-ui;
|
||||
}
|
||||
|
||||
/* 设置页面容器背景色 */
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
@month-show="onMonthShow"
|
||||
@select="onDateSelect"
|
||||
/>
|
||||
|
||||
|
||||
<ContributionHeatmap ref="heatmapRef" />
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(60px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(60px + env(safe-area-inset-bottom, 0px))" />
|
||||
|
||||
<!-- 日期交易列表弹出层 -->
|
||||
<PopupContainer
|
||||
@@ -24,7 +24,7 @@
|
||||
height="75%"
|
||||
>
|
||||
<template #header-actions>
|
||||
<SmartClassifyButton
|
||||
<SmartClassifyButton
|
||||
ref="smartClassifyButtonRef"
|
||||
:transactions="dateTransactions"
|
||||
@save="onSmartClassifySave"
|
||||
@@ -50,221 +50,227 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, onBeforeUnmount } from "vue";
|
||||
import { showToast } from "vant";
|
||||
import request from "@/api/request";
|
||||
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
||||
import TransactionList from "@/components/TransactionList.vue";
|
||||
import TransactionDetail from "@/components/TransactionDetail.vue";
|
||||
import SmartClassifyButton from "@/components/SmartClassifyButton.vue";
|
||||
import PopupContainer from "@/components/PopupContainer.vue";
|
||||
import ContributionHeatmap from "@/components/ContributionHeatmap.vue";
|
||||
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import request from '@/api/request'
|
||||
import { getTransactionDetail, getTransactionsByDate } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
import SmartClassifyButton from '@/components/SmartClassifyButton.vue'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import ContributionHeatmap from '@/components/ContributionHeatmap.vue'
|
||||
|
||||
const dailyStatistics = ref({});
|
||||
const listVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const dateTransactions = ref([]);
|
||||
const currentTransaction = ref(null);
|
||||
const listLoading = ref(false);
|
||||
const selectedDate = ref(null);
|
||||
const selectedDateText = ref("");
|
||||
const heatmapRef = ref(null);
|
||||
const dailyStatistics = ref({})
|
||||
const listVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const dateTransactions = ref([])
|
||||
const currentTransaction = ref(null)
|
||||
const listLoading = ref(false)
|
||||
const selectedDate = ref(null)
|
||||
const selectedDateText = ref('')
|
||||
const heatmapRef = ref(null)
|
||||
|
||||
// 设置日历可选范围(例如:过去2年到未来1年)
|
||||
const minDate = new Date(new Date().getFullYear() - 2, 0, 1); // 2年前的1月1日
|
||||
const maxDate = new Date(new Date().getFullYear() + 1, 11, 31); // 明年12月31日
|
||||
const minDate = new Date(new Date().getFullYear() - 2, 0, 1) // 2年前的1月1日
|
||||
const maxDate = new Date(new Date().getFullYear() + 1, 11, 31) // 明年12月31日
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
// 计算页面高度,滚动3/4高度以显示更多日期
|
||||
const height = document.querySelector(".calendar-container").clientHeight * 0.43;
|
||||
document.querySelector(".van-calendar__body").scrollBy({
|
||||
const height = document.querySelector('.calendar-container').clientHeight * 0.43
|
||||
document.querySelector('.van-calendar__body').scrollBy({
|
||||
top: -height,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 获取日历统计数据
|
||||
const fetchDailyStatistics = async (year, month) => {
|
||||
try {
|
||||
const response = await request.get("/TransactionRecord/GetDailyStatistics", {
|
||||
params: { year, month },
|
||||
});
|
||||
const response = await request.get('/TransactionRecord/GetDailyStatistics', {
|
||||
params: { year, month }
|
||||
})
|
||||
if (response.success && response.data) {
|
||||
// 将数组转换为对象,key为日期
|
||||
const statsMap = {};
|
||||
const statsMap = {}
|
||||
response.data.forEach((item) => {
|
||||
statsMap[item.date] = {
|
||||
count: item.count,
|
||||
amount: item.amount,
|
||||
};
|
||||
});
|
||||
amount: item.amount
|
||||
}
|
||||
})
|
||||
dailyStatistics.value = {
|
||||
...dailyStatistics.value,
|
||||
...statsMap,
|
||||
};
|
||||
...statsMap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取日历统计数据失败:", error);
|
||||
console.error('获取日历统计数据失败:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const smartClassifyButtonRef = ref(null);
|
||||
const smartClassifyButtonRef = ref(null)
|
||||
// 获取指定日期的交易列表
|
||||
const fetchDateTransactions = async (date) => {
|
||||
try {
|
||||
listLoading.value = true;
|
||||
listLoading.value = true
|
||||
const dateStr = date
|
||||
.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
.replace(/\//g, "-");
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
|
||||
const response = await getTransactionsByDate(dateStr);
|
||||
const response = await getTransactionsByDate(dateStr)
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 根据金额从大到小排序
|
||||
dateTransactions.value = response
|
||||
.data
|
||||
.sort((a, b) => b.amount - a.amount);
|
||||
dateTransactions.value = response.data.sort((a, b) => b.amount - a.amount)
|
||||
// 重置智能分类按钮
|
||||
smartClassifyButtonRef.value?.reset()
|
||||
} else {
|
||||
dateTransactions.value = [];
|
||||
showToast(response.message || "获取交易列表失败");
|
||||
dateTransactions.value = []
|
||||
showToast(response.message || '获取交易列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取日期交易列表失败:", error);
|
||||
dateTransactions.value = [];
|
||||
showToast("获取交易列表失败");
|
||||
console.error('获取日期交易列表失败:', error)
|
||||
dateTransactions.value = []
|
||||
showToast('获取交易列表失败')
|
||||
} finally {
|
||||
listLoading.value = false;
|
||||
listLoading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getBalance = (transactions) => {
|
||||
let balance = 0;
|
||||
transactions.forEach(tx => {
|
||||
if(tx.type === 1) {
|
||||
balance += tx.amount;
|
||||
} else if(tx.type === 0) {
|
||||
balance -= tx.amount;
|
||||
let balance = 0
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.type === 1) {
|
||||
balance += tx.amount
|
||||
} else if (tx.type === 0) {
|
||||
balance -= tx.amount
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if(balance >= 0) {
|
||||
return `结余收入 ${balance.toFixed(1)} 元`;
|
||||
if (balance >= 0) {
|
||||
return `结余收入 ${balance.toFixed(1)} 元`
|
||||
} else {
|
||||
return `结余支出 ${(-balance).toFixed(1)} 元`;
|
||||
return `结余支出 ${(-balance).toFixed(1)} 元`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 当月份显示时触发
|
||||
const onMonthShow = ({ date }) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
fetchDailyStatistics(year, month);
|
||||
};
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
fetchDailyStatistics(year, month)
|
||||
}
|
||||
|
||||
// 日期选择事件
|
||||
const onDateSelect = (date) => {
|
||||
selectedDate.value = date;
|
||||
selectedDateText.value = formatSelectedDate(date);
|
||||
fetchDateTransactions(date);
|
||||
listVisible.value = true;
|
||||
};
|
||||
selectedDate.value = date
|
||||
selectedDateText.value = formatSelectedDate(date)
|
||||
fetchDateTransactions(date)
|
||||
listVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化选中的日期
|
||||
const formatSelectedDate = (date) => {
|
||||
return date.toLocaleDateString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
weekday: "long",
|
||||
});
|
||||
};
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = async (transaction) => {
|
||||
try {
|
||||
const response = await getTransactionDetail(transaction.id);
|
||||
const response = await getTransactionDetail(transaction.id)
|
||||
if (response.success) {
|
||||
currentTransaction.value = response.data;
|
||||
detailVisible.value = true;
|
||||
currentTransaction.value = response.data
|
||||
detailVisible.value = true
|
||||
} else {
|
||||
showToast(response.message || "获取详情失败");
|
||||
showToast(response.message || '获取详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取详情出错:", error);
|
||||
showToast("获取详情失败");
|
||||
console.error('获取详情出错:', error)
|
||||
showToast('获取详情失败')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 详情保存后的回调
|
||||
const onDetailSave = async (saveData) => {
|
||||
var item = dateTransactions.value.find(tx => tx.id === saveData.id);
|
||||
if(!item) return
|
||||
const item = dateTransactions.value.find((tx) => tx.id === saveData.id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果分类发生了变化 移除智能分类的内容,防止被智能分类覆盖
|
||||
if(item.classify !== saveData.classify) {
|
||||
if (item.classify !== saveData.classify) {
|
||||
// 通知智能分类按钮组件移除指定项
|
||||
smartClassifyButtonRef.value?.removeClassifiedTransaction(saveData.id)
|
||||
item.upsetedClassify = ''
|
||||
}
|
||||
|
||||
// 更新当前日期交易列表中的数据
|
||||
Object.assign(item, saveData);
|
||||
|
||||
|
||||
Object.assign(item, saveData)
|
||||
|
||||
// 重新加载当前月份的统计数据
|
||||
const now = selectedDate.value || new Date();
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||
};
|
||||
const now = selectedDate.value || new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
}
|
||||
|
||||
// 处理删除事件:从当前日期交易列表中移除,并刷新当日和当月统计
|
||||
const handleDateTransactionDelete = async (transactionId) => {
|
||||
dateTransactions.value = dateTransactions.value.filter(t => t.id !== transactionId)
|
||||
dateTransactions.value = dateTransactions.value.filter((t) => t.id !== transactionId)
|
||||
|
||||
// 刷新当前日期以及当月的统计数据
|
||||
const now = selectedDate.value || new Date();
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||
};
|
||||
const now = selectedDate.value || new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
}
|
||||
|
||||
// 智能分类保存回调
|
||||
const onSmartClassifySave = async () => {
|
||||
// 保存完成后重新加载数据
|
||||
if (selectedDate.value) {
|
||||
await fetchDateTransactions(selectedDate.value);
|
||||
await fetchDateTransactions(selectedDate.value)
|
||||
}
|
||||
// 重新加载统计数据
|
||||
const now = selectedDate.value || new Date();
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||
};
|
||||
const now = selectedDate.value || new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
}
|
||||
|
||||
const formatterCalendar = (day) => {
|
||||
const dayCopy = { ...day };
|
||||
const dayCopy = { ...day }
|
||||
if (dayCopy.date.toDateString() === new Date().toDateString()) {
|
||||
dayCopy.text = "今天";
|
||||
dayCopy.text = '今天'
|
||||
}
|
||||
|
||||
// 格式化日期为 yyyy-MM-dd
|
||||
const dateKey = dayCopy.date
|
||||
.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
.replace(/\//g, "-");
|
||||
const stats = dailyStatistics.value[dateKey];
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
const stats = dailyStatistics.value[dateKey]
|
||||
|
||||
if (stats) {
|
||||
dayCopy.topInfo = `${stats.count}笔`; // 展示消费笔数
|
||||
dayCopy.bottomInfo = `${stats.amount.toFixed(1)}元`; // 展示消费金额
|
||||
dayCopy.topInfo = `${stats.count}笔` // 展示消费笔数
|
||||
dayCopy.bottomInfo = `${(stats.amount || 0).toFixed(1)}元` // 展示消费金额
|
||||
}
|
||||
|
||||
return dayCopy;
|
||||
};
|
||||
return dayCopy
|
||||
}
|
||||
|
||||
// 初始加载当前月份数据
|
||||
const now = new Date();
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||
const now = new Date()
|
||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||||
|
||||
// 全局删除事件监听,确保日历页面数据一致
|
||||
const onGlobalTransactionDeleted = () => {
|
||||
@@ -276,10 +282,12 @@ const onGlobalTransactionDeleted = () => {
|
||||
heatmapRef.value?.refresh()
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 当有交易被新增/修改/批量更新时刷新
|
||||
@@ -292,15 +300,17 @@ const onGlobalTransactionsChanged = () => {
|
||||
heatmapRef.value?.refresh()
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.van-calendar{
|
||||
.van-calendar {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@@ -340,5 +350,4 @@ onBeforeUnmount(() => {
|
||||
:deep(.heatmap-card) {
|
||||
flex-shrink: 0; /* Prevent heatmap from shrinking */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar
|
||||
title="批量分类"
|
||||
<van-nav-bar
|
||||
title="批量分类"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
placeholder
|
||||
placeholder
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 分组列表 -->
|
||||
<van-empty v-if="!hasData && finished" description="暂无数据" />
|
||||
|
||||
|
||||
<van-list
|
||||
v-model:loading="listLoading"
|
||||
v-model:error="error"
|
||||
@@ -85,7 +85,7 @@ const onLoad = async () => {
|
||||
listLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await groupListRef.value.loadData()
|
||||
|
||||
@@ -94,7 +94,7 @@ const onLoad = async () => {
|
||||
await loadUnclassifiedCount()
|
||||
_loadedUnclassifiedInitially.value = true
|
||||
}
|
||||
|
||||
|
||||
error.value = false
|
||||
} catch (err) {
|
||||
console.error('加载分组数据失败:', err)
|
||||
|
||||
@@ -1,121 +1,102 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar
|
||||
:title="navTitle"
|
||||
<van-nav-bar
|
||||
:title="navTitle"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
placeholder
|
||||
placeholder
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<div class="scroll-content">
|
||||
<!-- 第一层:选择交易类型 -->
|
||||
<div v-if="currentLevel === 0" class="level-container">
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="type in typeOptions"
|
||||
:key="type.value"
|
||||
:title="type.label"
|
||||
is-link
|
||||
@click="handleSelectType(type.value)"
|
||||
/>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<!-- 第二层:分类列表 -->
|
||||
<div v-else class="level-container">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<van-tag
|
||||
type="primary"
|
||||
closeable
|
||||
style="margin-left: 16px;"
|
||||
@close="handleBackToRoot"
|
||||
>
|
||||
{{ currentTypeName }}
|
||||
</van-tag>
|
||||
<!-- 第一层:选择交易类型 -->
|
||||
<div v-if="currentLevel === 0" class="level-container">
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="type in typeOptions"
|
||||
:key="type.value"
|
||||
:title="type.label"
|
||||
is-link
|
||||
@click="handleSelectType(type.value)"
|
||||
/>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<van-empty v-if="categories.length === 0" description="暂无分类" />
|
||||
|
||||
<van-cell-group v-else inset>
|
||||
<van-swipe-cell v-for="category in categories" :key="category.id">
|
||||
<van-cell
|
||||
:title="category.name"
|
||||
is-link
|
||||
@click="handleEdit(category)"
|
||||
/>
|
||||
<template #right>
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
text="删除"
|
||||
@click="handleDelete(category)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
<!-- 第二层:分类列表 -->
|
||||
<div v-else class="level-container">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<van-tag type="primary" closeable style="margin-left: 16px" @close="handleBackToRoot">
|
||||
{{ currentTypeName }}
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(55px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<!-- 分类列表 -->
|
||||
<van-empty v-if="categories.length === 0" description="暂无分类" />
|
||||
|
||||
<div class="bottom-button">
|
||||
<!-- 新增分类按钮 -->
|
||||
<van-button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon="plus"
|
||||
@click="handleAddCategory"
|
||||
<van-cell-group v-else inset>
|
||||
<van-swipe-cell v-for="category in categories" :key="category.id">
|
||||
<van-cell :title="category.name" is-link @click="handleEdit(category)" />
|
||||
<template #right>
|
||||
<van-button square type="danger" text="删除" @click="handleDelete(category)" />
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(55px + env(safe-area-inset-bottom, 0px))" />
|
||||
|
||||
<div class="bottom-button">
|
||||
<!-- 新增分类按钮 -->
|
||||
<van-button type="primary" size="large" icon="plus" @click="handleAddCategory">
|
||||
新增分类
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 新增分类对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showAddDialog"
|
||||
title="新增分类"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="resetAddForm"
|
||||
>
|
||||
新增分类
|
||||
</van-button>
|
||||
</div>
|
||||
<van-form ref="addFormRef">
|
||||
<van-field
|
||||
v-model="addForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</van-dialog>
|
||||
|
||||
<!-- 新增分类对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showAddDialog"
|
||||
title="新增分类"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="resetAddForm"
|
||||
>
|
||||
<van-form ref="addFormRef">
|
||||
<van-field
|
||||
v-model="addForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</van-dialog>
|
||||
<!-- 编辑分类对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showEditDialog"
|
||||
title="编辑分类"
|
||||
show-cancel-button
|
||||
@confirm="handleConfirmEdit"
|
||||
>
|
||||
<van-form ref="editFormRef">
|
||||
<van-field
|
||||
v-model="editForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</van-dialog>
|
||||
|
||||
<!-- 编辑分类对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showEditDialog"
|
||||
title="编辑分类"
|
||||
show-cancel-button
|
||||
@confirm="handleConfirmEdit"
|
||||
>
|
||||
<van-form ref="editFormRef">
|
||||
<van-field
|
||||
v-model="editForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</van-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showDeleteConfirm"
|
||||
title="删除分类"
|
||||
message="删除后无法恢复,确定要删除吗?"
|
||||
@confirm="handleConfirmDelete"
|
||||
/>
|
||||
<!-- 删除确认对话框 -->
|
||||
<van-dialog
|
||||
v-model:show="showDeleteConfirm"
|
||||
title="删除分类"
|
||||
message="删除后无法恢复,确定要删除吗?"
|
||||
@confirm="handleConfirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -123,12 +104,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
showSuccessToast,
|
||||
showToast,
|
||||
showLoadingToast,
|
||||
closeToast
|
||||
} from 'vant'
|
||||
import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant'
|
||||
import {
|
||||
getCategoryList,
|
||||
createCategory,
|
||||
@@ -149,7 +125,7 @@ const typeOptions = [
|
||||
const currentLevel = ref(0) // 0=类型选择, 1=分类管理
|
||||
const currentType = ref(null) // 当前选中的交易类型
|
||||
const currentTypeName = computed(() => {
|
||||
const type = typeOptions.find(t => t.value === currentType.value)
|
||||
const type = typeOptions.find((t) => t.value === currentType.value)
|
||||
return type ? type.label : ''
|
||||
})
|
||||
|
||||
@@ -301,7 +277,7 @@ const handleEdit = (category) => {
|
||||
const handleConfirmEdit = async () => {
|
||||
try {
|
||||
await editFormRef.value?.validate()
|
||||
|
||||
|
||||
showLoadingToast({
|
||||
message: '保存中...',
|
||||
forbidClick: true,
|
||||
@@ -340,7 +316,9 @@ const handleDelete = async (category) => {
|
||||
* 确认删除
|
||||
*/
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!deleteTarget.value) return
|
||||
if (!deleteTarget.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
showLoadingToast({
|
||||
@@ -382,7 +360,6 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.level-container {
|
||||
min-height: calc(100vh - 50px);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div class="page-container-flex classification-nlp">
|
||||
<van-nav-bar
|
||||
title="自然语言分类"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
<van-nav-bar title="自然语言分类" left-text="返回" left-arrow @click-left="onClickLeft" />
|
||||
|
||||
<div class="scroll-content">
|
||||
<!-- 输入区域 -->
|
||||
@@ -21,15 +16,9 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
|
||||
<div class="action-buttons">
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
:loading="analyzing"
|
||||
@click="handleAnalyze"
|
||||
>
|
||||
<van-button type="primary" block round :loading="analyzing" @click="handleAnalyze">
|
||||
分析查询
|
||||
</van-button>
|
||||
</div>
|
||||
@@ -41,9 +30,9 @@
|
||||
<van-cell title="查询关键词" :value="analysisResult.searchKeyword" />
|
||||
<van-cell title="AI建议类型" :value="getTypeName(analysisResult.targetType)" />
|
||||
<van-cell title="AI建议分类" :value="analysisResult.targetClassify" />
|
||||
<van-cell
|
||||
title="找到记录"
|
||||
:value="`${analysisResult.records.length} 条`"
|
||||
<van-cell
|
||||
title="找到记录"
|
||||
:value="`${analysisResult.records.length} 条`"
|
||||
is-link
|
||||
@click="showRecordsList = true"
|
||||
/>
|
||||
@@ -59,32 +48,14 @@
|
||||
/>
|
||||
|
||||
<!-- 记录列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showRecordsList"
|
||||
title="交易记录列表"
|
||||
height="75%"
|
||||
>
|
||||
<div style="background: var(--van-background);">
|
||||
<PopupContainer v-model="showRecordsList" title="交易记录列表" height="75%">
|
||||
<div style="background: var(--van-background)">
|
||||
<!-- 批量操作按钮 -->
|
||||
<div class="batch-actions">
|
||||
<van-button
|
||||
plain
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="selectAll"
|
||||
>
|
||||
全选
|
||||
</van-button>
|
||||
<van-button
|
||||
plain
|
||||
type="default"
|
||||
size="small"
|
||||
@click="selectNone"
|
||||
>
|
||||
全不选
|
||||
</van-button>
|
||||
<van-button
|
||||
type="success"
|
||||
<van-button plain type="primary" size="small" @click="selectAll"> 全选 </van-button>
|
||||
<van-button plain type="default" size="small" @click="selectNone"> 全不选 </van-button>
|
||||
<van-button
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="submitting"
|
||||
:disabled="selectedIds.size === 0"
|
||||
@@ -138,9 +109,11 @@ const onClickLeft = () => {
|
||||
|
||||
// 将带目标分类的记录转换为普通交易记录格式供列表显示
|
||||
const displayRecords = computed(() => {
|
||||
if (!analysisResult.value) return []
|
||||
|
||||
return analysisResult.value.records.map(r => ({
|
||||
if (!analysisResult.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return analysisResult.value.records.map((r) => ({
|
||||
id: r.id,
|
||||
reason: r.reason,
|
||||
amount: r.amount,
|
||||
@@ -178,14 +151,14 @@ const handleAnalyze = async () => {
|
||||
try {
|
||||
analyzing.value = true
|
||||
const response = await nlpAnalysis(userInput.value)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
analysisResult.value = response.data
|
||||
|
||||
|
||||
// 默认全选
|
||||
const allIds = new Set(response.data.records.map(r => r.id))
|
||||
const allIds = new Set(response.data.records.map((r) => r.id))
|
||||
selectedIds.value = allIds
|
||||
|
||||
|
||||
showToast(`找到 ${response.data.records.length} 条记录`)
|
||||
} else {
|
||||
showToast(response.message || '分析失败')
|
||||
@@ -200,8 +173,10 @@ const handleAnalyze = async () => {
|
||||
|
||||
// 全选
|
||||
const selectAll = () => {
|
||||
if (!analysisResult.value) return
|
||||
const allIds = new Set(analysisResult.value.records.map(r => r.id))
|
||||
if (!analysisResult.value) {
|
||||
return
|
||||
}
|
||||
const allIds = new Set(analysisResult.value.records.map((r) => r.id))
|
||||
selectedIds.value = allIds
|
||||
}
|
||||
|
||||
@@ -218,7 +193,7 @@ const updateSelectedIds = (newSelectedIds) => {
|
||||
// 点击记录查看详情
|
||||
const handleRecordClick = (transaction) => {
|
||||
// 从原始记录中获取完整信息
|
||||
const record = analysisResult.value?.records.find(r => r.id === transaction.id)
|
||||
const record = analysisResult.value?.records.find((r) => r.id === transaction.id)
|
||||
if (record) {
|
||||
currentTransaction.value = {
|
||||
id: record.id,
|
||||
@@ -263,18 +238,18 @@ const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
|
||||
// 构建批量更新数据(使用AI修改后的结果)
|
||||
const items = analysisResult.value.records
|
||||
.filter(r => selectedIds.value.has(r.id))
|
||||
.map(r => ({
|
||||
.filter((r) => selectedIds.value.has(r.id))
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
classify: r.upsetedClassify,
|
||||
type: r.upsetedType
|
||||
}))
|
||||
|
||||
|
||||
const response = await batchUpdateClassify(items)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
showToast('分类设置成功')
|
||||
// 清空结果,让用户进行新的查询
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<template>
|
||||
<div class="page-container-flex smart-classification">
|
||||
<van-nav-bar
|
||||
title="智能分类"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
<div class="scroll-content" style="padding-top: 5px;">
|
||||
<van-nav-bar title="智能分类" left-text="返回" left-arrow @click-left="onClickLeft" />
|
||||
|
||||
<div class="scroll-content" style="padding-top: 5px">
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-info">
|
||||
<span class="stats-label">未分类账单 </span>
|
||||
@@ -23,13 +18,13 @@
|
||||
/>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="bottom-button">
|
||||
<van-button
|
||||
type="primary"
|
||||
<van-button
|
||||
type="primary"
|
||||
:loading="classifying"
|
||||
:disabled="selectedCount === 0"
|
||||
round
|
||||
@@ -38,9 +33,9 @@
|
||||
>
|
||||
{{ classifying ? '分类中...' : `开始分类 (${selectedCount}组)` }}
|
||||
</van-button>
|
||||
|
||||
<van-button
|
||||
type="success"
|
||||
|
||||
<van-button
|
||||
type="success"
|
||||
:disabled="!hasChanges || classifying"
|
||||
round
|
||||
class="action-btn"
|
||||
@@ -56,11 +51,7 @@
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||
import {
|
||||
getUnclassifiedCount,
|
||||
smartClassify,
|
||||
batchUpdateClassify
|
||||
} from '@/api/transactionRecord'
|
||||
import { getUnclassifiedCount, smartClassify, batchUpdateClassify } from '@/api/transactionRecord'
|
||||
import ReasonGroupList from '@/components/ReasonGroupList.vue'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -74,7 +65,9 @@ const suppressDataChanged = ref(false)
|
||||
|
||||
// 计算已选中的数量
|
||||
const selectedCount = computed(() => {
|
||||
if (!groupListRef.value) return 0
|
||||
if (!groupListRef.value) {
|
||||
return 0
|
||||
}
|
||||
return groupListRef.value.getSelectedReasons().size
|
||||
})
|
||||
|
||||
@@ -114,10 +107,12 @@ const onClickLeft = () => {
|
||||
if (hasChanges.value) {
|
||||
showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '有未保存的分类结果,确定要离开吗?',
|
||||
}).then(() => {
|
||||
router.back()
|
||||
}).catch(() => {})
|
||||
message: '有未保存的分类结果,确定要离开吗?'
|
||||
})
|
||||
.then(() => {
|
||||
router.back()
|
||||
})
|
||||
.catch(() => {})
|
||||
} else {
|
||||
router.back()
|
||||
}
|
||||
@@ -125,17 +120,19 @@ const onClickLeft = () => {
|
||||
|
||||
// 开始智能分类
|
||||
const startClassify = async () => {
|
||||
if (!groupListRef.value) return
|
||||
|
||||
if (!groupListRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有选中分组
|
||||
const selectedGroups = groupListRef.value.getList(true)
|
||||
|
||||
|
||||
// 获取所有选中分组的账单ID
|
||||
const idsToClassify = []
|
||||
for (const group of selectedGroups) {
|
||||
idsToClassify.push(...group.transactionIds)
|
||||
}
|
||||
|
||||
|
||||
if (idsToClassify.length === 0) {
|
||||
showToast('请先选择要分类的账单组')
|
||||
return
|
||||
@@ -149,13 +146,13 @@ const startClassify = async () => {
|
||||
|
||||
classifying.value = true
|
||||
classifyBuffer.value = ''
|
||||
|
||||
|
||||
// 用于存储分类结果的临时对象
|
||||
const classifyResults = new Map()
|
||||
|
||||
|
||||
try {
|
||||
const response = await smartClassify(idsToClassify)
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
@@ -166,23 +163,27 @@ const startClassify = async () => {
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) break
|
||||
|
||||
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
|
||||
if (!line.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const eventMatch = line.match(/^event: (.+)$/m)
|
||||
const dataMatch = line.match(/^data: (.+)$/m)
|
||||
|
||||
|
||||
if (eventMatch && dataMatch) {
|
||||
const eventType = eventMatch[1]
|
||||
const data = dataMatch[1]
|
||||
|
||||
|
||||
handleSSEEvent(eventType, data, classifyResults)
|
||||
}
|
||||
}
|
||||
@@ -215,8 +216,9 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
let braceCount = 0
|
||||
let closeBrace = -1
|
||||
for (let i = openBrace; i < classifyBuffer.value.length; i++) {
|
||||
if (classifyBuffer.value[i] === '{') braceCount++
|
||||
else if (classifyBuffer.value[i] === '}') {
|
||||
if (classifyBuffer.value[i] === '{') {
|
||||
braceCount++
|
||||
} else if (classifyBuffer.value[i] === '}') {
|
||||
braceCount--
|
||||
if (braceCount === 0) {
|
||||
closeBrace = i
|
||||
@@ -227,7 +229,7 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
|
||||
if (closeBrace !== -1) {
|
||||
const jsonStr = classifyBuffer.value.substring(openBrace, closeBrace + 1)
|
||||
|
||||
|
||||
try {
|
||||
const result = JSON.parse(jsonStr)
|
||||
if (result.id && groupListRef.value) {
|
||||
@@ -235,7 +237,7 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
classify: result.Classify || '',
|
||||
type: result.Type !== undefined ? result.Type : null
|
||||
})
|
||||
|
||||
|
||||
// 更新组件内的分组显示状态
|
||||
const groups = groupListRef.value.getList()
|
||||
for (const group of groups) {
|
||||
@@ -260,7 +262,7 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
} catch (e) {
|
||||
console.error('JSON解析失败:', e)
|
||||
}
|
||||
|
||||
|
||||
classifyBuffer.value = classifyBuffer.value.substring(closeBrace + 1)
|
||||
startIndex = 0
|
||||
} else {
|
||||
@@ -283,12 +285,14 @@ const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
|
||||
// 保存分类
|
||||
const saveClassifications = async () => {
|
||||
if (!groupListRef.value) return
|
||||
|
||||
if (!groupListRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有已分类的账单
|
||||
const groups = groupListRef.value.getList()
|
||||
const itemsToUpdate = []
|
||||
|
||||
|
||||
for (const group of groups) {
|
||||
if (group.sampleClassify) {
|
||||
// 为该分组的所有账单添加分类
|
||||
@@ -362,4 +366,4 @@ onMounted(async () => {
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="refreshing"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<!-- 加载提示 -->
|
||||
<van-loading v-if="loading && !(emailList && emailList.length)" vertical style="padding: 50px 0">
|
||||
<van-loading
|
||||
v-if="loading && !(emailList && emailList.length)"
|
||||
vertical
|
||||
style="padding: 50px 0"
|
||||
>
|
||||
加载中...
|
||||
</van-loading>
|
||||
<!-- 邮件列表 -->
|
||||
@@ -14,7 +21,11 @@
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell-group v-if="emailList && emailList.length" inset style="margin-top: 10px">
|
||||
<van-cell-group
|
||||
v-if="emailList && emailList.length"
|
||||
inset
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
<van-swipe-cell
|
||||
v-for="email in emailList"
|
||||
:key="email.id"
|
||||
@@ -27,18 +38,22 @@
|
||||
>
|
||||
<template #value>
|
||||
<div class="email-info">
|
||||
<div class="email-date">{{ formatDate(email.receivedDate) }}</div>
|
||||
<div v-if="email.transactionCount > 0" class="bill-count">
|
||||
<span style="font-size: 12px;">已解析{{ email.transactionCount }}条账单</span>
|
||||
<div class="email-date">
|
||||
{{ formatDate(email.receivedDate) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="email.transactionCount > 0"
|
||||
class="bill-count"
|
||||
>
|
||||
<span style="font-size: 12px">已解析{{ email.transactionCount }}条账单</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<template #right>
|
||||
<van-button
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
type="danger"
|
||||
text="删除"
|
||||
class="delete-button"
|
||||
@click="handleDelete(email)"
|
||||
@@ -47,26 +62,26 @@
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-empty
|
||||
v-if="!loading && !(emailList && emailList.length)"
|
||||
description="暂无邮件记录"
|
||||
<van-empty
|
||||
v-if="!loading && !(emailList && emailList.length)"
|
||||
description="暂无邮件记录"
|
||||
/>
|
||||
</van-list>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 详情弹出层 -->
|
||||
<PopupContainer
|
||||
v-model="detailVisible"
|
||||
:title="currentEmail ? (currentEmail.Subject || currentEmail.subject || '(无主题)') : ''"
|
||||
:title="currentEmail ? currentEmail.Subject || currentEmail.subject || '(无主题)' : ''"
|
||||
height="75%"
|
||||
>
|
||||
<template #header-actions>
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="refreshingAnalysis"
|
||||
@click="handleRefreshAnalysis"
|
||||
>
|
||||
@@ -75,34 +90,51 @@
|
||||
</template>
|
||||
|
||||
<div v-if="currentEmail">
|
||||
<van-cell-group inset style="margin-top: 12px;">
|
||||
<van-cell title="发件人" :value="currentEmail.From || currentEmail.from || '未知'" />
|
||||
<van-cell title="接收时间" :value="formatDate(currentEmail.ReceivedDate || currentEmail.receivedDate)" />
|
||||
<van-cell title="记录时间" :value="formatDate(currentEmail.CreateTime || currentEmail.createTime)" />
|
||||
<van-cell
|
||||
v-if="(currentEmail.TransactionCount || currentEmail.transactionCount || 0) > 0"
|
||||
title="已解析账单数"
|
||||
<van-cell-group
|
||||
inset
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<van-cell
|
||||
title="发件人"
|
||||
:value="currentEmail.From || currentEmail.from || '未知'"
|
||||
/>
|
||||
<van-cell
|
||||
title="接收时间"
|
||||
:value="formatDate(currentEmail.ReceivedDate || currentEmail.receivedDate)"
|
||||
/>
|
||||
<van-cell
|
||||
title="记录时间"
|
||||
:value="formatDate(currentEmail.CreateTime || currentEmail.createTime)"
|
||||
/>
|
||||
<van-cell
|
||||
v-if="(currentEmail.TransactionCount || currentEmail.transactionCount || 0) > 0"
|
||||
title="已解析账单数"
|
||||
:value="`${currentEmail.TransactionCount || currentEmail.transactionCount || 0}条`"
|
||||
is-link
|
||||
@click="viewTransactions"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<div class="email-content">
|
||||
<h4 style="margin-left: 10px;">邮件内容</h4>
|
||||
<div
|
||||
v-if="currentEmail.htmlBody"
|
||||
class="content-body html-content"
|
||||
<h4 style="margin-left: 10px">
|
||||
邮件内容
|
||||
</h4>
|
||||
<div
|
||||
v-if="currentEmail.htmlBody"
|
||||
class="content-body html-content"
|
||||
v-html="currentEmail.htmlBody"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="currentEmail.body"
|
||||
/>
|
||||
<div
|
||||
v-else-if="currentEmail.body"
|
||||
class="content-body"
|
||||
>
|
||||
{{ currentEmail.body }}
|
||||
</div>
|
||||
<div v-else class="content-body empty-content">
|
||||
<div
|
||||
v-else
|
||||
class="content-body empty-content"
|
||||
>
|
||||
暂无邮件内容
|
||||
<div style="font-size: 12px; margin-top: 8px; color: var(--van-gray-6);">
|
||||
<div style="font-size: 12px; margin-top: 8px; color: var(--van-gray-6)">
|
||||
Debug: {{ Object.keys(currentEmail).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +171,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getEmailList, getEmailDetail, deleteEmail, refreshTransactionRecords, syncEmails, getEmailTransactions } from '@/api/emailRecord'
|
||||
import {
|
||||
getEmailList,
|
||||
getEmailDetail,
|
||||
deleteEmail,
|
||||
refreshTransactionRecords,
|
||||
syncEmails,
|
||||
getEmailTransactions
|
||||
} from '@/api/emailRecord'
|
||||
import { getTransactionDetail } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
@@ -163,8 +202,10 @@ const currentTransaction = ref(null)
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (loading.value) return // 防止重复加载
|
||||
|
||||
if (loading.value) {
|
||||
return
|
||||
} // 防止重复加载
|
||||
|
||||
if (isRefresh) {
|
||||
pageIndex.value = 1
|
||||
emailList.value = []
|
||||
@@ -177,19 +218,19 @@ const loadData = async (isRefresh = false) => {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize
|
||||
}
|
||||
|
||||
|
||||
const response = await getEmailList(params)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data || []
|
||||
total.value = response.total || 0
|
||||
|
||||
|
||||
if (isRefresh) {
|
||||
emailList.value = newList
|
||||
} else {
|
||||
emailList.value = [...(emailList.value || []), ...newList]
|
||||
}
|
||||
|
||||
|
||||
// 判断是否还有更多数据(返回数据少于pageSize条或为空,说明没有更多了)
|
||||
if (newList.length === 0 || newList.length < pageSize) {
|
||||
finished.value = true
|
||||
@@ -246,7 +287,7 @@ const handleDelete = async (email) => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要删除这封邮件吗?',
|
||||
message: '确定要删除这封邮件吗?'
|
||||
})
|
||||
|
||||
const response = await deleteEmail(email.id)
|
||||
@@ -266,17 +307,19 @@ const handleDelete = async (email) => {
|
||||
|
||||
// 重新分析
|
||||
const handleRefreshAnalysis = async () => {
|
||||
if (!currentEmail.value) return
|
||||
|
||||
if (!currentEmail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要重新分析该邮件并刷新交易记录吗?',
|
||||
message: '确定要重新分析该邮件并刷新交易记录吗?'
|
||||
})
|
||||
|
||||
refreshingAnalysis.value = true
|
||||
const response = await refreshTransactionRecords(currentEmail.value.id)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
showToast('重新分析成功')
|
||||
detailVisible.value = false
|
||||
@@ -298,7 +341,7 @@ const handleSync = async () => {
|
||||
try {
|
||||
syncing.value = true
|
||||
const response = await syncEmails()
|
||||
|
||||
|
||||
if (response.success) {
|
||||
showToast(response.message || '同步成功')
|
||||
// 同步成功后刷新列表
|
||||
@@ -316,12 +359,14 @@ const handleSync = async () => {
|
||||
|
||||
// 查看关联的账单列表
|
||||
const viewTransactions = async () => {
|
||||
if (!currentEmail.value) return
|
||||
|
||||
if (!currentEmail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const emailId = currentEmail.value.id
|
||||
const response = await getEmailTransactions(emailId)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
transactionListVisible.value = true
|
||||
@@ -340,18 +385,22 @@ const onGlobalTransactionDeleted = (e) => {
|
||||
// 如果交易列表弹窗打开,尝试重新加载邮箱的交易列表
|
||||
if (transactionListVisible.value && currentEmail.value) {
|
||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||
getEmailTransactions(emailId).then(response => {
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
}).catch(() => {})
|
||||
getEmailTransactions(emailId)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 监听新增/修改/批量更新事件,刷新弹窗内交易或邮件列表
|
||||
@@ -359,21 +408,25 @@ const onGlobalTransactionsChanged = (e) => {
|
||||
console.log('收到全局交易变更事件:', e)
|
||||
if (transactionListVisible.value && currentEmail.value) {
|
||||
const emailId = currentEmail.value.id || currentEmail.value.Id
|
||||
getEmailTransactions(emailId).then(response => {
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
}).catch(() => {})
|
||||
getEmailTransactions(emailId)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
} else {
|
||||
// 也刷新邮件列表以保持统计一致
|
||||
loadData(true)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 处理点击账单
|
||||
@@ -394,7 +447,7 @@ const handleTransactionClick = async (transaction) => {
|
||||
|
||||
const handleTransactionDelete = (transactionId) => {
|
||||
// 从当前的交易列表中移除该交易
|
||||
transactionList.value = transactionList.value.filter(t => t.id !== transactionId)
|
||||
transactionList.value = transactionList.value.filter((t) => t.id !== transactionId)
|
||||
|
||||
// 刷新邮件列表
|
||||
loadData(true)
|
||||
@@ -402,16 +455,15 @@ const handleTransactionDelete = (transactionId) => {
|
||||
// 刷新当前邮件详情
|
||||
if (currentEmail.value) {
|
||||
const emailId = currentEmail.value.id
|
||||
getEmailDetail(emailId).then(response => {
|
||||
getEmailDetail(emailId).then((response) => {
|
||||
if (response.success) {
|
||||
currentEmail.value = response.data
|
||||
}
|
||||
})
|
||||
}
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('transaction-deleted', { detail: transactionId }))
|
||||
} catch(e) {
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transactionId }))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
@@ -426,27 +478,28 @@ const handleTransactionSave = async () => {
|
||||
transactionList.value = response.data || []
|
||||
}
|
||||
}
|
||||
try {
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(
|
||||
'transactions-changed',
|
||||
{
|
||||
detail: {
|
||||
emailId: currentEmail.value?.id
|
||||
}
|
||||
}))
|
||||
} catch(e) {
|
||||
new CustomEvent('transactions-changed', {
|
||||
detail: {
|
||||
emailId: currentEmail.value?.id
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
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',
|
||||
@@ -454,8 +507,6 @@ const formatDate = (dateString) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
loadData(true)
|
||||
})
|
||||
@@ -467,7 +518,6 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.van-pull-refresh) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -528,4 +578,4 @@ defineExpose({
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page-container-flex log-view">
|
||||
<van-nav-bar
|
||||
title="查看日志"
|
||||
<van-nav-bar
|
||||
title="查看日志"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
placeholder
|
||||
placeholder
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
@@ -17,19 +17,34 @@
|
||||
@search="handleSearch"
|
||||
@clear="handleClear"
|
||||
/>
|
||||
|
||||
|
||||
<div class="filter-row">
|
||||
<van-dropdown-menu>
|
||||
<van-dropdown-item v-model="selectedLevel" :options="levelOptions" @change="handleSearch" />
|
||||
<van-dropdown-item v-model="selectedDate" :options="dateOptions" @change="handleSearch" />
|
||||
<van-dropdown-item
|
||||
v-model="selectedLevel"
|
||||
:options="levelOptions"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<van-dropdown-item
|
||||
v-model="selectedDate"
|
||||
:options="dateOptions"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="refreshing"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<!-- 加载提示 -->
|
||||
<van-loading v-if="loading && !logList.length" vertical style="padding: 50px 0">
|
||||
<van-loading
|
||||
v-if="loading && !logList.length"
|
||||
vertical
|
||||
style="padding: 50px 0"
|
||||
>
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
@@ -41,8 +56,8 @@
|
||||
class="log-list"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div
|
||||
v-for="(log, index) in logList"
|
||||
<div
|
||||
v-for="(log, index) in logList"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
:class="getLevelClass(log.level)"
|
||||
@@ -51,19 +66,21 @@
|
||||
<span class="log-level">{{ log.level }}</span>
|
||||
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="log-message">{{ log.message }}</div>
|
||||
<div class="log-message">
|
||||
{{ log.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty
|
||||
v-if="!loading && !logList.length"
|
||||
<van-empty
|
||||
v-if="!loading && !logList.length"
|
||||
description="暂无日志"
|
||||
image="search"
|
||||
/>
|
||||
</van-list>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: 20px"></div>
|
||||
<div style="height: 20px" />
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,9 +123,7 @@ const levelOptions = ref([
|
||||
])
|
||||
|
||||
// 日期选项
|
||||
const dateOptions = ref([
|
||||
{ text: '全部日期', value: '' }
|
||||
])
|
||||
const dateOptions = ref([{ text: '全部日期', value: '' }])
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
@@ -122,12 +137,12 @@ const handleBack = () => {
|
||||
*/
|
||||
const getLevelClass = (level) => {
|
||||
const levelMap = {
|
||||
'ERR': 'level-error',
|
||||
'FTL': 'level-fatal',
|
||||
'WRN': 'level-warning',
|
||||
'INF': 'level-info',
|
||||
'DBG': 'level-debug',
|
||||
'VRB': 'level-verbose'
|
||||
ERR: 'level-error',
|
||||
FTL: 'level-fatal',
|
||||
WRN: 'level-warning',
|
||||
INF: 'level-info',
|
||||
DBG: 'level-debug',
|
||||
VRB: 'level-verbose'
|
||||
}
|
||||
return levelMap[level] || 'level-default'
|
||||
}
|
||||
@@ -145,7 +160,9 @@ const formatTime = (timestamp) => {
|
||||
* 加载日志数据
|
||||
*/
|
||||
const loadLogs = async (reset = false) => {
|
||||
if (fetching.value) return
|
||||
if (fetching.value) {
|
||||
return
|
||||
}
|
||||
|
||||
fetching.value = true
|
||||
|
||||
@@ -175,7 +192,7 @@ const loadLogs = async (reset = false) => {
|
||||
|
||||
if (response.success) {
|
||||
const newLogs = response.data || []
|
||||
|
||||
|
||||
if (reset) {
|
||||
logList.value = newLogs
|
||||
} else {
|
||||
@@ -223,8 +240,10 @@ const onRefresh = async () => {
|
||||
* 加载更多
|
||||
*/
|
||||
const onLoad = async () => {
|
||||
if (finished.value || fetching.value) return
|
||||
|
||||
if (finished.value || fetching.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是第一次加载
|
||||
if (pageIndex.value === 1 && logList.value.length === 0) {
|
||||
await loadLogs(false)
|
||||
@@ -257,14 +276,11 @@ const loadAvailableDates = async () => {
|
||||
try {
|
||||
const response = await getAvailableDates()
|
||||
if (response.success && response.data) {
|
||||
const dates = response.data.map(date => ({
|
||||
const dates = response.data.map((date) => ({
|
||||
text: formatDate(date),
|
||||
value: date
|
||||
}))
|
||||
dateOptions.value = [
|
||||
{ text: '全部日期', value: '' },
|
||||
...dates
|
||||
]
|
||||
dateOptions.value = [{ text: '全部日期', value: '' }, ...dates]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日期列表失败:', error)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<van-icon name="lock" />
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="refreshing"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell-group v-if="list.length" inset style="margin-top: 10px">
|
||||
<van-swipe-cell v-for="item in list" :key="item.id">
|
||||
<div class="message-card" @click="viewDetail(item)">
|
||||
<van-cell-group
|
||||
v-if="list.length"
|
||||
inset
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
<van-swipe-cell
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
>
|
||||
<div
|
||||
class="message-card"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<div class="card-left">
|
||||
<div class="message-title" :class="{ 'unread': !item.isRead }">
|
||||
<div
|
||||
class="message-title"
|
||||
:class="{ unread: !item.isRead }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="message-content">
|
||||
@@ -23,16 +39,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-right">
|
||||
<van-tag v-if="!item.isRead" type="danger">未读</van-tag>
|
||||
<van-icon name="arrow" size="16" class="arrow-icon" />
|
||||
<van-tag
|
||||
v-if="!item.isRead"
|
||||
type="danger"
|
||||
>
|
||||
未读
|
||||
</van-tag>
|
||||
<van-icon
|
||||
name="arrow"
|
||||
size="16"
|
||||
class="arrow-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #right>
|
||||
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(item)" />
|
||||
<van-button
|
||||
square
|
||||
text="删除"
|
||||
type="danger"
|
||||
class="delete-button"
|
||||
@click="handleDelete(item)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
<van-empty v-else-if="!loading" description="暂无消息" />
|
||||
<van-empty
|
||||
v-else-if="!loading"
|
||||
description="暂无消息"
|
||||
/>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
@@ -44,16 +78,26 @@
|
||||
height="75%"
|
||||
>
|
||||
<div
|
||||
v-if="currentMessage.messageType === 2"
|
||||
class="detail-content rich-html-content"
|
||||
v-if="currentMessage.messageType === 2"
|
||||
class="detail-content rich-html-content"
|
||||
v-html="currentMessage.content"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="detail-content"
|
||||
>
|
||||
</div>
|
||||
<div v-else class="detail-content">
|
||||
{{ currentMessage.content }}
|
||||
</div>
|
||||
<template v-if="currentMessage.url && currentMessage.messageType === 1" #footer>
|
||||
<van-button type="primary" block round @click="handleUrlJump(currentMessage.url)">
|
||||
<template
|
||||
v-if="currentMessage.url && currentMessage.messageType === 1"
|
||||
#footer
|
||||
>
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
@click="handleUrlJump(currentMessage.url)"
|
||||
>
|
||||
查看详情
|
||||
</van-button>
|
||||
</template>
|
||||
@@ -62,164 +106,166 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showToast, showDialog } from 'vant';
|
||||
import { getMessageList, markAsRead, deleteMessage, markAllAsRead } from '@/api/message';
|
||||
import { useMessageStore } from '@/stores/message';
|
||||
import PopupContainer from '@/components/PopupContainer.vue';
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showDialog } from 'vant'
|
||||
import { getMessageList, markAsRead, deleteMessage, markAllAsRead } from '@/api/message'
|
||||
import { useMessageStore } from '@/stores/message'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
|
||||
const messageStore = useMessageStore();
|
||||
const router = useRouter();
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const pageIndex = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const messageStore = useMessageStore()
|
||||
const router = useRouter()
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const detailVisible = ref(false);
|
||||
const currentMessage = ref({});
|
||||
const detailVisible = ref(false)
|
||||
const currentMessage = ref({})
|
||||
|
||||
const onLoad = async () => {
|
||||
if (refreshing.value) {
|
||||
list.value = [];
|
||||
pageIndex.value = 1;
|
||||
refreshing.value = false;
|
||||
list.value = []
|
||||
pageIndex.value = 1
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMessageList({
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
// 格式化时间
|
||||
const data = res.data.map(item => ({
|
||||
const data = res.data.map((item) => ({
|
||||
...item,
|
||||
createTime: new Date(item.createTime).toLocaleString()
|
||||
}));
|
||||
|
||||
}))
|
||||
|
||||
if (pageIndex.value === 1) {
|
||||
list.value = data;
|
||||
list.value = data
|
||||
} else {
|
||||
list.value = [...list.value, ...data];
|
||||
list.value = [...list.value, ...data]
|
||||
}
|
||||
|
||||
|
||||
// 判断是否加载完成
|
||||
if (list.value.length >= res.total || data.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
finished.value = true
|
||||
} else {
|
||||
pageIndex.value++;
|
||||
pageIndex.value++
|
||||
}
|
||||
} else {
|
||||
showToast(res.message || '加载失败');
|
||||
finished.value = true;
|
||||
showToast(res.message || '加载失败')
|
||||
finished.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('加载失败');
|
||||
finished.value = true;
|
||||
console.error(error)
|
||||
showToast('加载失败')
|
||||
finished.value = true
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onRefresh = () => {
|
||||
finished.value = false;
|
||||
loading.value = true;
|
||||
onLoad();
|
||||
};
|
||||
finished.value = false
|
||||
loading.value = true
|
||||
onLoad()
|
||||
}
|
||||
|
||||
const viewDetail = async (item) => {
|
||||
if (!item.isRead) {
|
||||
try {
|
||||
await markAsRead(item.id);
|
||||
item.isRead = true;
|
||||
messageStore.updateUnreadCount();
|
||||
await markAsRead(item.id)
|
||||
item.isRead = true
|
||||
messageStore.updateUnreadCount()
|
||||
} catch (error) {
|
||||
console.error('标记已读失败', error);
|
||||
console.error('标记已读失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
currentMessage.value = item;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
currentMessage.value = item
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleUrlJump = (targetUrl) => {
|
||||
if (!targetUrl) return;
|
||||
|
||||
if (targetUrl.startsWith('http')) {
|
||||
window.open(targetUrl, '_blank');
|
||||
} else if (targetUrl.startsWith('/')) {
|
||||
router.push(targetUrl);
|
||||
detailVisible.value = false;
|
||||
} else {
|
||||
showToast('无效的URL');
|
||||
if (!targetUrl) {
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
if (targetUrl.startsWith('http')) {
|
||||
window.open(targetUrl, '_blank')
|
||||
} else if (targetUrl.startsWith('/')) {
|
||||
router.push(targetUrl)
|
||||
detailVisible.value = false
|
||||
} else {
|
||||
showToast('无效的URL')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (item) => {
|
||||
showDialog({
|
||||
title: '提示',
|
||||
message: '确定要删除这条消息吗?',
|
||||
showCancelButton: true,
|
||||
showCancelButton: true
|
||||
}).then(async (action) => {
|
||||
if (action === 'confirm') {
|
||||
try {
|
||||
const res = await deleteMessage(item.id);
|
||||
const res = await deleteMessage(item.id)
|
||||
if (res.success) {
|
||||
showToast('删除成功');
|
||||
const wasUnread = !item.isRead;
|
||||
list.value = list.value.filter(i => i.id !== item.id);
|
||||
showToast('删除成功')
|
||||
const wasUnread = !item.isRead
|
||||
list.value = list.value.filter((i) => i.id !== item.id)
|
||||
if (wasUnread) {
|
||||
messageStore.updateUnreadCount();
|
||||
messageStore.updateUnreadCount()
|
||||
}
|
||||
} else {
|
||||
showToast(res.message || '删除失败');
|
||||
showToast(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除消息失败', error);
|
||||
showToast('删除失败');
|
||||
console.error('删除消息失败', error)
|
||||
showToast('删除失败')
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
showDialog({
|
||||
title: '提示',
|
||||
message: '确定要将所有消息标记为已读吗?',
|
||||
showCancelButton: true,
|
||||
showCancelButton: true
|
||||
}).then(async (action) => {
|
||||
if (action === 'confirm') {
|
||||
try {
|
||||
const res = await markAllAsRead();
|
||||
const res = await markAllAsRead()
|
||||
if (res.success) {
|
||||
showToast('操作成功');
|
||||
showToast('操作成功')
|
||||
// 刷新列表
|
||||
onRefresh();
|
||||
onRefresh()
|
||||
// 更新未读计数
|
||||
messageStore.updateUnreadCount();
|
||||
messageStore.updateUnreadCount()
|
||||
} else {
|
||||
showToast(res.message || '操作失败');
|
||||
showToast(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记所有已读失败', error);
|
||||
showToast('操作失败');
|
||||
console.error('标记所有已读失败', error)
|
||||
showToast('操作失败')
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// onLoad 会由 van-list 自动触发
|
||||
});
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
handleMarkAllRead
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -257,7 +303,7 @@ defineExpose({
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-content{
|
||||
.message-content {
|
||||
font-size: 14px;
|
||||
color: var(--van-text-color-2);
|
||||
margin-bottom: 6px;
|
||||
@@ -305,4 +351,4 @@ defineExpose({
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page-container-flex periodic-record">
|
||||
<van-nav-bar
|
||||
:title="navTitle"
|
||||
<van-nav-bar
|
||||
:title="navTitle"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
placeholder
|
||||
placeholder
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
@@ -36,11 +36,14 @@
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="分类" :value="item.classify || '未分类'" />
|
||||
<van-cell title="下次执行时间" :value="formatDateTime(item.nextExecuteTime) || '未设置'" />
|
||||
<van-cell
|
||||
title="下次执行时间"
|
||||
:value="formatDateTime(item.nextExecuteTime) || '未设置'"
|
||||
/>
|
||||
<van-cell title="状态">
|
||||
<template #value>
|
||||
<van-switch
|
||||
:model-value="item.isEnabled"
|
||||
<van-switch
|
||||
:model-value="item.isEnabled"
|
||||
size="20px"
|
||||
@update:model-value="(val) => toggleEnabled(item.id, val)"
|
||||
@click.stop
|
||||
@@ -49,9 +52,9 @@
|
||||
</van-cell>
|
||||
</div>
|
||||
<template #right>
|
||||
<van-button
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
type="danger"
|
||||
text="删除"
|
||||
class="delete-button"
|
||||
@click="deletePeriodic(item)"
|
||||
@@ -61,26 +64,20 @@
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty
|
||||
v-if="!loading && !periodicList.length"
|
||||
<van-empty
|
||||
v-if="!loading && !periodicList.length"
|
||||
description="暂无周期性账单"
|
||||
image="search"
|
||||
/>
|
||||
</van-list>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 底部新增按钮 -->
|
||||
<div class="bottom-button">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="large"
|
||||
round
|
||||
icon="plus"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
<van-button type="primary" size="large" round icon="plus" @click="openAddDialog">
|
||||
新增周期账单
|
||||
</van-button>
|
||||
</div>
|
||||
@@ -93,111 +90,104 @@
|
||||
>
|
||||
<van-form>
|
||||
<van-cell-group inset title="周期设置">
|
||||
<van-field
|
||||
v-model="form.periodicTypeText"
|
||||
is-link
|
||||
readonly
|
||||
name="periodicType"
|
||||
label="周期"
|
||||
placeholder="请选择周期类型"
|
||||
:rules="[{ required: true, message: '请选择周期类型' }]"
|
||||
@click="showPeriodicTypePicker = true"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.periodicTypeText"
|
||||
is-link
|
||||
readonly
|
||||
name="periodicType"
|
||||
label="周期"
|
||||
placeholder="请选择周期类型"
|
||||
:rules="[{ required: true, message: '请选择周期类型' }]"
|
||||
@click="showPeriodicTypePicker = true"
|
||||
/>
|
||||
|
||||
<!-- 每周配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 1"
|
||||
v-model="form.weekdaysText"
|
||||
is-link
|
||||
readonly
|
||||
name="weekdays"
|
||||
label="星期"
|
||||
placeholder="请选择星期几"
|
||||
:rules="[{ required: true, message: '请选择星期几' }]"
|
||||
@click="showWeekdaysPicker = true"
|
||||
/>
|
||||
<!-- 每周配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 1"
|
||||
v-model="form.weekdaysText"
|
||||
is-link
|
||||
readonly
|
||||
name="weekdays"
|
||||
label="星期"
|
||||
placeholder="请选择星期几"
|
||||
:rules="[{ required: true, message: '请选择星期几' }]"
|
||||
@click="showWeekdaysPicker = true"
|
||||
/>
|
||||
|
||||
<!-- 每月配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 2"
|
||||
v-model="form.monthDaysText"
|
||||
is-link
|
||||
readonly
|
||||
name="monthDays"
|
||||
label="日期"
|
||||
placeholder="请选择每月的日期"
|
||||
:rules="[{ required: true, message: '请选择日期' }]"
|
||||
@click="showMonthDaysPicker = true"
|
||||
/>
|
||||
<!-- 每月配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 2"
|
||||
v-model="form.monthDaysText"
|
||||
is-link
|
||||
readonly
|
||||
name="monthDays"
|
||||
label="日期"
|
||||
placeholder="请选择每月的日期"
|
||||
:rules="[{ required: true, message: '请选择日期' }]"
|
||||
@click="showMonthDaysPicker = true"
|
||||
/>
|
||||
|
||||
<!-- 每季度配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 3"
|
||||
v-model="form.quarterDay"
|
||||
name="quarterDay"
|
||||
label="季度第几天"
|
||||
placeholder="请输入季度开始后第几天"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入季度开始后第几天' }]"
|
||||
/>
|
||||
<!-- 每季度配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 3"
|
||||
v-model="form.quarterDay"
|
||||
name="quarterDay"
|
||||
label="季度第几天"
|
||||
placeholder="请输入季度开始后第几天"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入季度开始后第几天' }]"
|
||||
/>
|
||||
|
||||
<!-- 每年配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 4"
|
||||
v-model="form.yearDay"
|
||||
name="yearDay"
|
||||
label="年第几天"
|
||||
placeholder="请输入年开始后第几天"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入年开始后第几天' }]"
|
||||
/>
|
||||
<!-- 每年配置 -->
|
||||
<van-field
|
||||
v-if="form.periodicType === 4"
|
||||
v-model="form.yearDay"
|
||||
name="yearDay"
|
||||
label="年第几天"
|
||||
placeholder="请输入年开始后第几天"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入年开始后第几天' }]"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset title="基本信息">
|
||||
<van-field
|
||||
v-model="form.reason"
|
||||
name="reason"
|
||||
label="摘要"
|
||||
placeholder="请输入交易摘要"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.amount"
|
||||
name="amount"
|
||||
label="金额"
|
||||
placeholder="请输入金额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入金额' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.type"
|
||||
name="type"
|
||||
label="类型"
|
||||
>
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.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="分类">
|
||||
<template #input>
|
||||
<span v-if="!form.classify" style="color: var(--van-gray-5);">请选择交易分类</span>
|
||||
<span v-else>{{ form.classify }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 分类选择组件 -->
|
||||
<ClassifySelector
|
||||
v-model="form.classify"
|
||||
:type="form.type"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.reason"
|
||||
name="reason"
|
||||
label="摘要"
|
||||
placeholder="请输入交易摘要"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.amount"
|
||||
name="amount"
|
||||
label="金额"
|
||||
placeholder="请输入金额"
|
||||
type="number"
|
||||
:rules="[{ required: true, message: '请输入金额' }]"
|
||||
/>
|
||||
<van-field v-model="form.type" name="type" label="类型">
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.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="分类">
|
||||
<template #input>
|
||||
<span v-if="!form.classify" style="color: var(--van-gray-5)">请选择交易分类</span>
|
||||
<span v-else>{{ form.classify }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 分类选择组件 -->
|
||||
<ClassifySelector v-model="form.classify" :type="form.type" />
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
<template #footer>
|
||||
@@ -240,8 +230,8 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import {
|
||||
getPeriodicList,
|
||||
import {
|
||||
getPeriodicList,
|
||||
deletePeriodic as deletePeriodicApi,
|
||||
togglePeriodicEnabled
|
||||
} from '@/api/transactionPeriodic'
|
||||
@@ -330,19 +320,19 @@ const loadData = async (isRefresh = false) => {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize
|
||||
}
|
||||
|
||||
|
||||
const response = await getPeriodicList(params)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data || []
|
||||
total.value = response.total || 0
|
||||
|
||||
|
||||
if (isRefresh) {
|
||||
periodicList.value = newList
|
||||
} else {
|
||||
periodicList.value = [...periodicList.value, ...newList]
|
||||
}
|
||||
|
||||
|
||||
if (newList.length === 0 || newList.length < pageSize) {
|
||||
finished.value = true
|
||||
} else {
|
||||
@@ -387,27 +377,29 @@ const getPeriodicTypeText = (item) => {
|
||||
3: '每季度',
|
||||
4: '每年'
|
||||
}
|
||||
|
||||
|
||||
let text = typeMap[item.periodicType] || '未知'
|
||||
|
||||
|
||||
if (item.periodicConfig) {
|
||||
switch (item.periodicType) {
|
||||
case 1: // 每周
|
||||
{
|
||||
const weekdays = item.periodicConfig.split(',').map(
|
||||
d => {
|
||||
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return dayMap[parseInt(d)] || ''
|
||||
}).join('、')
|
||||
text += ` (${weekdays})`
|
||||
break
|
||||
}
|
||||
case 2: // 每月
|
||||
{
|
||||
const days = item.periodicConfig.split(',').join('、')
|
||||
text += ` (${days}日)`
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
// 每周
|
||||
const weekdays = item.periodicConfig
|
||||
.split(',')
|
||||
.map((d) => {
|
||||
const dayMap = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return dayMap[parseInt(d)] || ''
|
||||
})
|
||||
.join('、')
|
||||
text += ` (${weekdays})`
|
||||
break
|
||||
}
|
||||
case 2: {
|
||||
// 每月
|
||||
const days = item.periodicConfig.split(',').join('、')
|
||||
text += ` (${days}日)`
|
||||
break
|
||||
}
|
||||
case 3: // 每季度
|
||||
text += ` (第${item.periodicConfig}天)`
|
||||
break
|
||||
@@ -416,7 +408,7 @@ const getPeriodicTypeText = (item) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -436,20 +428,22 @@ const editPeriodic = (item) => {
|
||||
form.type = item.type
|
||||
form.classify = item.classify
|
||||
form.periodicType = item.periodicType
|
||||
form.periodicTypeText = periodicTypeColumns.find(t => t.value === item.periodicType)?.text || ''
|
||||
|
||||
form.periodicTypeText = periodicTypeColumns.find((t) => t.value === item.periodicType)?.text || ''
|
||||
|
||||
// 解析周期配置
|
||||
if (item.periodicConfig) {
|
||||
switch (item.periodicType) {
|
||||
case 1: // 每周
|
||||
form.weekdays = item.periodicConfig.split(',').map(d => parseInt(d))
|
||||
form.weekdaysText = form.weekdays.map(d => {
|
||||
return weekdaysColumns.find(w => w.value === d)?.text || ''
|
||||
}).join('、')
|
||||
form.weekdays = item.periodicConfig.split(',').map((d) => parseInt(d))
|
||||
form.weekdaysText = form.weekdays
|
||||
.map((d) => {
|
||||
return weekdaysColumns.find((w) => w.value === d)?.text || ''
|
||||
})
|
||||
.join('、')
|
||||
break
|
||||
case 2: // 每月
|
||||
form.monthDays = item.periodicConfig.split(',').map(d => parseInt(d))
|
||||
form.monthDaysText = form.monthDays.map(d => `${d}日`).join('、')
|
||||
form.monthDays = item.periodicConfig.split(',').map((d) => parseInt(d))
|
||||
form.monthDaysText = form.monthDays.map((d) => `${d}日`).join('、')
|
||||
break
|
||||
case 3: // 每季度
|
||||
form.quarterDay = item.periodicConfig
|
||||
@@ -459,7 +453,7 @@ const editPeriodic = (item) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -468,7 +462,7 @@ const deletePeriodic = async (item) => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要删除这条周期性账单吗?',
|
||||
message: '确定要删除这条周期性账单吗?'
|
||||
})
|
||||
|
||||
const response = await deletePeriodicApi(item.id)
|
||||
@@ -493,7 +487,7 @@ const toggleEnabled = async (id, enabled) => {
|
||||
if (response.success) {
|
||||
showToast(enabled ? '已启用' : '已禁用')
|
||||
// 更新本地数据
|
||||
const item = periodicList.value.find(p => p.id === id)
|
||||
const item = periodicList.value.find((p) => p.id === id)
|
||||
if (item) {
|
||||
item.isEnabled = enabled
|
||||
}
|
||||
@@ -510,7 +504,9 @@ const toggleEnabled = async (id, enabled) => {
|
||||
}
|
||||
|
||||
const formatDateTime = (date) => {
|
||||
if (!date) return ''
|
||||
if (!date) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
@@ -557,7 +553,6 @@ const onMonthDaysConfirm = ({ selectedValues, selectedOptions }) => {
|
||||
form.monthDaysText = selectedOptions[0].text
|
||||
showMonthDaysPicker.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -599,5 +594,4 @@ const onMonthDaysConfirm = ({ selectedValues, selectedOptions }) => {
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar title="定时任务" left-arrow placeholder @click-left="onClickLeft" />
|
||||
<van-nav-bar
|
||||
title="定时任务"
|
||||
left-arrow
|
||||
placeholder
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<van-pull-refresh v-model="loading" @refresh="fetchTasks">
|
||||
<div v-for="task in tasks" :key="task.name" class="task-card">
|
||||
<van-pull-refresh
|
||||
v-model="loading"
|
||||
@refresh="fetchTasks"
|
||||
>
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.name"
|
||||
class="task-card"
|
||||
>
|
||||
<van-cell-group inset>
|
||||
<van-cell :title="task.jobDescription" :label="task.triggerDescription || task.name">
|
||||
<van-cell
|
||||
:title="task.jobDescription"
|
||||
:label="task.triggerDescription || task.name"
|
||||
>
|
||||
<template #value>
|
||||
<van-tag :type="task.status === 'Paused' ? 'warning' : 'success'">
|
||||
{{ task.status === 'Paused' ? '已暂停' : '已启动' }}
|
||||
</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="任务标识" :value="task.name" />
|
||||
<van-cell title="下次执行" :value="task.nextRunTime || '无'" />
|
||||
<van-cell
|
||||
title="任务标识"
|
||||
:value="task.name"
|
||||
/>
|
||||
<van-cell
|
||||
title="下次执行"
|
||||
:value="task.nextRunTime || '无'"
|
||||
/>
|
||||
<div class="card-footer">
|
||||
<van-row gutter="10">
|
||||
<van-col span="12">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
block
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
block
|
||||
icon="play"
|
||||
@click="handleExecute(task)"
|
||||
>
|
||||
@@ -28,21 +49,21 @@
|
||||
</van-button>
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<van-button
|
||||
<van-button
|
||||
v-if="task.status !== 'Paused'"
|
||||
type="warning"
|
||||
size="small"
|
||||
block
|
||||
type="warning"
|
||||
size="small"
|
||||
block
|
||||
icon="pause"
|
||||
@click="handlePause(task)"
|
||||
>
|
||||
暂停任务
|
||||
</van-button>
|
||||
<van-button
|
||||
<van-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
block
|
||||
type="success"
|
||||
size="small"
|
||||
block
|
||||
icon="play-circle-o"
|
||||
@click="handleResume(task)"
|
||||
>
|
||||
@@ -55,10 +76,13 @@
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-empty v-if="tasks.length === 0 && !loading" description="无定时任务" />
|
||||
<van-empty
|
||||
v-if="tasks.length === 0 && !loading"
|
||||
description="无定时任务"
|
||||
/>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(20px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(20px + env(safe-area-inset-bottom, 0px))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,7 +126,7 @@ const handleExecute = async (task) => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '确认执行',
|
||||
message: `确定要立即执行"${task.jobDescription}"吗?`,
|
||||
message: `确定要立即执行"${task.jobDescription}"吗?`
|
||||
})
|
||||
|
||||
showLoadingToast({
|
||||
@@ -132,7 +156,7 @@ const handlePause = async (task) => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '确认暂停',
|
||||
message: `确定要暂停"${task.jobDescription}"吗?`,
|
||||
message: `确定要暂停"${task.jobDescription}"吗?`
|
||||
})
|
||||
|
||||
const { success, message } = await pauseJob(task.name)
|
||||
|
||||
@@ -1,59 +1,137 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar title="设置" placeholder/>
|
||||
<van-nav-bar
|
||||
title="设置"
|
||||
placeholder
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<div
|
||||
class="detail-header"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<p>账单</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="从支付宝导入" is-link @click="handleImportClick('Alipay')" />
|
||||
<van-cell title="从微信导入" is-link @click="handleImportClick('WeChat')" />
|
||||
<van-cell title="周期记录" is-link @click="handlePeriodicRecord" />
|
||||
<van-cell
|
||||
title="从支付宝导入"
|
||||
is-link
|
||||
@click="handleImportClick('Alipay')"
|
||||
/>
|
||||
<van-cell
|
||||
title="从微信导入"
|
||||
is-link
|
||||
@click="handleImportClick('WeChat')"
|
||||
/>
|
||||
<van-cell
|
||||
title="周期记录"
|
||||
is-link
|
||||
@click="handlePeriodicRecord"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 隐藏的文件选择器 -->
|
||||
<input ref="fileInputRef" type="file" accept=".csv,.xlsx,.xls" style="display: none" @change="handleFileChange" />
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<div
|
||||
class="detail-header"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<p>分类</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="待确认分类" is-link @click="handleUnconfirmedClassification" />
|
||||
<van-cell title="编辑分类" is-link @click="handleEditClassification" />
|
||||
<van-cell title="批量分类" is-link @click="handleBatchClassification" />
|
||||
<van-cell title="智能分类" is-link @click="handleSmartClassification" />
|
||||
<van-cell
|
||||
title="待确认分类"
|
||||
is-link
|
||||
@click="handleUnconfirmedClassification"
|
||||
/>
|
||||
<van-cell
|
||||
title="编辑分类"
|
||||
is-link
|
||||
@click="handleEditClassification"
|
||||
/>
|
||||
<van-cell
|
||||
title="批量分类"
|
||||
is-link
|
||||
@click="handleBatchClassification"
|
||||
/>
|
||||
<van-cell
|
||||
title="智能分类"
|
||||
is-link
|
||||
@click="handleSmartClassification"
|
||||
/>
|
||||
<!-- <van-cell title="自然语言分类" is-link @click="handleNaturalLanguageClassification" /> -->
|
||||
</van-cell-group>
|
||||
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<div
|
||||
class="detail-header"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<p>通知</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="开启消息通知">
|
||||
<template #right-icon>
|
||||
<van-switch v-model="notificationEnabled" size="24" :loading="notificationLoading" @change="handleNotificationToggle" />
|
||||
<van-switch
|
||||
v-model="notificationEnabled"
|
||||
size="24"
|
||||
:loading="notificationLoading"
|
||||
@change="handleNotificationToggle"
|
||||
/>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell v-if="notificationEnabled" title="测试通知" is-link @click="handleTestNotification" />
|
||||
<van-cell
|
||||
v-if="notificationEnabled"
|
||||
title="测试通知"
|
||||
is-link
|
||||
@click="handleTestNotification"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<div
|
||||
class="detail-header"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<p>开发者</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="查看日志" is-link @click="handleLogView" />
|
||||
<van-cell title="清除缓存" is-link @click="handleReloadFromNetwork" />
|
||||
<van-cell title="定时任务" is-link @click="handleScheduledTasks" />
|
||||
<van-cell
|
||||
title="查看日志"
|
||||
is-link
|
||||
@click="handleLogView"
|
||||
/>
|
||||
<van-cell
|
||||
title="清除缓存"
|
||||
is-link
|
||||
@click="handleReloadFromNetwork"
|
||||
/>
|
||||
<van-cell
|
||||
title="定时任务"
|
||||
is-link
|
||||
@click="handleScheduledTasks"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<div
|
||||
class="detail-header"
|
||||
style="padding-bottom: 5px"
|
||||
>
|
||||
<p>账户</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="退出登录" is-link @click="handleLogout" />
|
||||
<van-cell
|
||||
title="退出登录"
|
||||
is-link
|
||||
@click="handleLogout"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(80px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(80px + env(safe-area-inset-bottom, 0px))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,15 +160,15 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
function urlBase64ToUint8Array (base64String) {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray;
|
||||
return outputArray
|
||||
}
|
||||
|
||||
const handleNotificationToggle = async (checked) => {
|
||||
@@ -113,12 +191,12 @@ const handleNotificationToggle = async (checked) => {
|
||||
return
|
||||
}
|
||||
|
||||
let { success, data, message } = await getVapidPublicKey()
|
||||
const { success, data, message } = await getVapidPublicKey()
|
||||
|
||||
if (!success) {
|
||||
throw new Error(message || '获取 VAPID 公钥失败')
|
||||
}
|
||||
|
||||
|
||||
const convertedVapidKey = urlBase64ToUint8Array(data)
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
@@ -132,7 +210,7 @@ const handleNotificationToggle = async (checked) => {
|
||||
p256DH: subJson.keys.p256dh,
|
||||
auth: subJson.keys.auth
|
||||
})
|
||||
|
||||
|
||||
showSuccessToast('开启成功')
|
||||
} else {
|
||||
// 关闭通知
|
||||
@@ -184,7 +262,11 @@ const handleFileChange = async (event) => {
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
const validTypes = ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
const validTypes = [
|
||||
'text/csv',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
]
|
||||
if (!validTypes.includes(file.type)) {
|
||||
showToast('请选择 CSV 或 Excel 文件')
|
||||
return
|
||||
@@ -218,8 +300,7 @@ const handleFileChange = async (event) => {
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
showToast('上传失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
closeToast()
|
||||
// 清空文件输入,允许重复选择同一文件
|
||||
event.target.value = ''
|
||||
@@ -249,9 +330,9 @@ const handleLogout = async () => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要退出登录吗?',
|
||||
message: '确定要退出登录吗?'
|
||||
})
|
||||
|
||||
|
||||
authStore.logout()
|
||||
showSuccessToast('已退出登录')
|
||||
router.push({ name: 'login' })
|
||||
@@ -276,7 +357,7 @@ const handleReloadFromNetwork = async () => {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要刷新网络吗?此操作不可撤销。',
|
||||
message: '确定要刷新网络吗?此操作不可撤销。'
|
||||
})
|
||||
|
||||
// PWA程序强制页面更新到最新版本
|
||||
@@ -300,7 +381,6 @@ const handleReloadFromNetwork = async () => {
|
||||
const handleScheduledTasks = () => {
|
||||
router.push({ name: 'scheduled-tasks' })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -331,4 +411,4 @@ const handleScheduledTasks = () => {
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,11 @@
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<!-- 加载提示 -->
|
||||
<van-loading v-if="loading && !(transactionList && transactionList.length)" vertical style="padding: 50px 0">
|
||||
<van-loading
|
||||
v-if="loading && !(transactionList && transactionList.length)"
|
||||
vertical
|
||||
style="padding: 50px 0"
|
||||
>
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
@@ -26,14 +30,16 @@
|
||||
:show-delete="true"
|
||||
@load="onLoad"
|
||||
@click="viewDetail"
|
||||
@delete="(id) => {
|
||||
// 从当前的交易列表中移除该交易
|
||||
transactionList.value = transactionList.value.filter(t => t.id !== id)
|
||||
}"
|
||||
@delete="
|
||||
(id) => {
|
||||
// 从当前的交易列表中移除该交易
|
||||
transactionList.value = transactionList.value.filter((t) => t.id !== id)
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 详情/编辑弹出层 -->
|
||||
@@ -48,10 +54,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import {
|
||||
getTransactionList,
|
||||
getTransactionDetail
|
||||
} from '@/api/transactionRecord'
|
||||
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
|
||||
@@ -69,8 +72,6 @@ const currentTransaction = ref(null)
|
||||
const searchKeyword = ref('')
|
||||
let searchTimer = null
|
||||
|
||||
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
@@ -85,24 +86,24 @@ const loadData = async (isRefresh = false) => {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize
|
||||
}
|
||||
|
||||
|
||||
// 添加搜索关键词
|
||||
if (searchKeyword.value) {
|
||||
params.searchKeyword = searchKeyword.value
|
||||
}
|
||||
|
||||
|
||||
const response = await getTransactionList(params)
|
||||
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data || []
|
||||
total.value = response.total || 0
|
||||
|
||||
|
||||
if (isRefresh) {
|
||||
transactionList.value = newList
|
||||
} else {
|
||||
transactionList.value = [...(transactionList.value || []), ...newList]
|
||||
}
|
||||
|
||||
|
||||
if (newList.length === 0 || newList.length < pageSize) {
|
||||
finished.value = true
|
||||
} else {
|
||||
@@ -194,10 +195,12 @@ const onGlobalTransactionDeleted = () => {
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 外部新增/修改/批量更新时的刷新监听
|
||||
@@ -208,18 +211,16 @@ const onGlobalTransactionsChanged = () => {
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
:deep(.van-pull-refresh) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -231,7 +232,6 @@ onBeforeUnmount(() => {
|
||||
padding: 4px 12px;
|
||||
z-index: 100;
|
||||
margin-top: 10px;
|
||||
|
||||
}
|
||||
|
||||
.top-search-bar :deep(.van-search) {
|
||||
@@ -239,10 +239,8 @@ onBeforeUnmount(() => {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 设置页面容器背景色 */
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
@click-left="onClickLeft"
|
||||
>
|
||||
<template #right>
|
||||
<van-button
|
||||
<van-button
|
||||
v-if="selectedIds.size > 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="confirming"
|
||||
@click="handleConfirmSelected"
|
||||
>
|
||||
@@ -20,8 +20,13 @@
|
||||
</van-nav-bar>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div v-if="loading && transactions.length === 0" class="loading-container">
|
||||
<van-loading vertical>加载中...</van-loading>
|
||||
<div
|
||||
v-if="loading && transactions.length === 0"
|
||||
class="loading-container"
|
||||
>
|
||||
<van-loading vertical>
|
||||
加载中...
|
||||
</van-loading>
|
||||
</div>
|
||||
|
||||
<TransactionList
|
||||
@@ -92,7 +97,7 @@ const handleConfirmSelected = async () => {
|
||||
|
||||
// 转换数据格式以适配 TransactionList 组件
|
||||
const displayTransactions = computed(() => {
|
||||
return transactions.value.map(t => ({
|
||||
return transactions.value.map((t) => ({
|
||||
...t,
|
||||
upsetedClassify: t.unconfirmedClassify,
|
||||
upsetedType: t.unconfirmedType
|
||||
@@ -104,13 +109,12 @@ const loadData = async () => {
|
||||
try {
|
||||
const response = await getUnconfirmedTransactionList()
|
||||
if (response && response.success) {
|
||||
transactions.value = (response.data || [])
|
||||
.map(t => ({
|
||||
...t,
|
||||
upsetedClassify: t.unconfirmedClassify,
|
||||
upsetedType: t.unconfirmedType
|
||||
}))
|
||||
selectedIds.value = new Set(response.data.map(t => t.id))
|
||||
transactions.value = (response.data || []).map((t) => ({
|
||||
...t,
|
||||
upsetedClassify: t.unconfirmedClassify,
|
||||
upsetedType: t.unconfirmedType
|
||||
}))
|
||||
selectedIds.value = new Set(response.data.map((t) => t.id))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取待确认列表失败:', error)
|
||||
@@ -125,7 +129,7 @@ const handleTransactionClick = (transaction) => {
|
||||
}
|
||||
|
||||
const handleTransactionDeleted = (id) => {
|
||||
transactions.value = transactions.value.filter(t => t.id !== id)
|
||||
transactions.value = transactions.value.filter((t) => t.id !== id)
|
||||
}
|
||||
|
||||
const updateSelectedIds = (ids) => {
|
||||
|
||||
Reference in New Issue
Block a user