feat: 更新未读消息计数的刷新频率,优化消息视图;添加分类标签显示功能,增强预算卡片的可读性
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 19s
Docker Build & Deploy / Deploy to Production (push) Successful in 13s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 19s
Docker Build & Deploy / Deploy to Production (push) Successful in 13s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -113,11 +113,13 @@ onMounted(() => {
|
||||
|
||||
setInterval(() => {
|
||||
messageStore.updateUnreadCount()
|
||||
}, 30 * 1000) // 每30秒更新一次未读消息数
|
||||
}, 60 * 1000) // 每60秒更新一次未读消息数
|
||||
|
||||
// 监听路由变化调整
|
||||
watch(() => route.path, (newPath) => {
|
||||
setActive(newPath)
|
||||
|
||||
messageStore.updateUnreadCount()
|
||||
})
|
||||
|
||||
const setActive = (path) => {
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||
@@ -122,6 +122,7 @@ const form = ref({
|
||||
})
|
||||
|
||||
const categoryName = ref('')
|
||||
const isSyncing = ref(false)
|
||||
|
||||
// 弹窗控制
|
||||
const showDatePicker = ref(false)
|
||||
@@ -134,6 +135,7 @@ const currentTime = ref(dayjs().format('HH:mm').split(':'))
|
||||
// 初始化数据
|
||||
const initForm = async () => {
|
||||
if (props.initialData) {
|
||||
isSyncing.value = true
|
||||
const { occurredAt, amount, reason, type, classify } = props.initialData
|
||||
|
||||
if (occurredAt) {
|
||||
@@ -152,6 +154,10 @@ const initForm = async () => {
|
||||
if (classify) {
|
||||
categoryName.value = classify
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +171,9 @@ watch(() => props.initialData, () => {
|
||||
}, { deep: true })
|
||||
|
||||
const handleTypeChange = (newType) => {
|
||||
categoryName.value = ''
|
||||
if (!isSyncing.value) {
|
||||
categoryName.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmDate = ({ selectedValues }) => {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3 class="card-title">{{ budget.name }}</h3>
|
||||
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
|
||||
({{ budget.selectedCategories.join('、') }})
|
||||
</span>
|
||||
</div>
|
||||
<van-icon name="arrow-down" class="expand-icon" />
|
||||
</div>
|
||||
@@ -85,6 +88,18 @@
|
||||
</div>
|
||||
|
||||
<div class="budget-body">
|
||||
<div v-if="budget.selectedCategories?.length" class="category-tags">
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
:key="cat"
|
||||
size="mini"
|
||||
class="category-tag"
|
||||
plain
|
||||
round
|
||||
>
|
||||
{{ cat }}
|
||||
</van-tag>
|
||||
</div>
|
||||
<div class="amount-info">
|
||||
<slot name="amount-info"></slot>
|
||||
</div>
|
||||
@@ -425,12 +440,29 @@ const timePercentage = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -438,6 +470,18 @@ const timePercentage = computed(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
opacity: 0.7;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.amount-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<van-field name="type" label="交易类型">
|
||||
<template #input>
|
||||
<van-radio-group v-model="editForm.type" direction="horizontal">
|
||||
<van-radio-group v-model="editForm.type" direction="horizontal" @change="handleTypeChange">
|
||||
<van-radio :name="0">支出</van-radio>
|
||||
<van-radio :name="1">收入</van-radio>
|
||||
<van-radio :name="2">不计</van-radio>
|
||||
@@ -149,7 +149,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, defineProps, defineEmits, computed } from 'vue'
|
||||
import { ref, reactive, watch, defineProps, defineEmits, computed, nextTick } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
@@ -171,6 +171,7 @@ const emit = defineEmits(['update:show', 'save'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const isSyncing = ref(false)
|
||||
|
||||
// 日期选择相关
|
||||
const showDatePicker = ref(false)
|
||||
@@ -201,6 +202,7 @@ watch(() => props.show, (newVal) => {
|
||||
|
||||
watch(() => props.transaction, (newVal) => {
|
||||
if (newVal) {
|
||||
isSyncing.value = true
|
||||
// 填充编辑表单
|
||||
editForm.id = newVal.id
|
||||
editForm.reason = newVal.reason || ''
|
||||
@@ -216,6 +218,11 @@ watch(() => props.transaction, (newVal) => {
|
||||
currentDate.value = dt.format('YYYY-MM-DD').split('-')
|
||||
currentTime.value = dt.format('HH:mm').split(':')
|
||||
}
|
||||
|
||||
// 在下一个 tick 结束同步状态,确保 van-radio-group 的 @change 已触发完毕
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -223,6 +230,13 @@ watch(visible, (newVal) => {
|
||||
emit('update:show', newVal)
|
||||
})
|
||||
|
||||
// 处理类型切换
|
||||
const handleTypeChange = () => {
|
||||
if (!isSyncing.value) {
|
||||
editForm.classify = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理日期确认
|
||||
const onConfirmDate = ({ selectedValues }) => {
|
||||
const dateStr = selectedValues.join('-')
|
||||
@@ -241,12 +255,6 @@ const onConfirmTime = ({ selectedValues }) => {
|
||||
showTimePicker.value = false
|
||||
}
|
||||
|
||||
// 监听交易类型变化,重新加载分类
|
||||
watch(() => editForm.type, (newVal) => {
|
||||
// 清空已选的分类
|
||||
editForm.classify = ''
|
||||
})
|
||||
|
||||
const applySuggestion = () => {
|
||||
if (props.transaction.unconfirmedClassify) {
|
||||
editForm.classify = props.transaction.unconfirmedClassify
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<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">
|
||||
@@ -66,7 +66,7 @@
|
||||
<van-empty v-else description="暂无支出预算" />
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||
@@ -76,7 +76,7 @@
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<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">
|
||||
@@ -117,11 +117,11 @@
|
||||
<van-empty v-else description="暂无收入预算" />
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||
<div class="scroll-content" style="padding-top:4px">
|
||||
<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
|
||||
@@ -155,7 +155,7 @@
|
||||
<van-empty v-else description="暂无存款计划" />
|
||||
</div>
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
@@ -183,6 +183,7 @@ import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue'
|
||||
const activeTab = ref(BudgetCategory.Expense)
|
||||
const budgetEditRef = ref(null)
|
||||
const savingsConfigRef = ref(null)
|
||||
const isRefreshing = ref(false)
|
||||
|
||||
const expenseBudgets = ref([])
|
||||
const incomeBudgets = ref([])
|
||||
@@ -229,6 +230,16 @@ const fetchBudgetList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
try {
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats()])
|
||||
} catch (err) {
|
||||
console.error('刷新失败', err)
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCategoryStats = async () => {
|
||||
try {
|
||||
const res = await getCategoryStats(activeTab.value)
|
||||
@@ -364,6 +375,11 @@ const handleDelete = (budget) => {
|
||||
margin: 0 12px 12px;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@
|
||||
<div v-else class="detail-content">
|
||||
{{ currentMessage.content }}
|
||||
</div>
|
||||
<div v-if="currentMessage.url" class="detail-footer" style="padding: 16px;">
|
||||
<van-button type="primary" block @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>
|
||||
</div>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -144,11 +144,6 @@ const viewDetail = async (item) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (item.messageType === 1) {
|
||||
handleUrlJump(item.url || item.content);
|
||||
return;
|
||||
}
|
||||
|
||||
currentMessage.value = item;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user