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(() => {
|
setInterval(() => {
|
||||||
messageStore.updateUnreadCount()
|
messageStore.updateUnreadCount()
|
||||||
}, 30 * 1000) // 每30秒更新一次未读消息数
|
}, 60 * 1000) // 每60秒更新一次未读消息数
|
||||||
|
|
||||||
// 监听路由变化调整
|
// 监听路由变化调整
|
||||||
watch(() => route.path, (newPath) => {
|
watch(() => route.path, (newPath) => {
|
||||||
setActive(newPath)
|
setActive(newPath)
|
||||||
|
|
||||||
|
messageStore.updateUnreadCount()
|
||||||
})
|
})
|
||||||
|
|
||||||
const setActive = (path) => {
|
const setActive = (path) => {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||||
@@ -122,6 +122,7 @@ const form = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const categoryName = ref('')
|
const categoryName = ref('')
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
|
||||||
// 弹窗控制
|
// 弹窗控制
|
||||||
const showDatePicker = ref(false)
|
const showDatePicker = ref(false)
|
||||||
@@ -134,6 +135,7 @@ const currentTime = ref(dayjs().format('HH:mm').split(':'))
|
|||||||
// 初始化数据
|
// 初始化数据
|
||||||
const initForm = async () => {
|
const initForm = async () => {
|
||||||
if (props.initialData) {
|
if (props.initialData) {
|
||||||
|
isSyncing.value = true
|
||||||
const { occurredAt, amount, reason, type, classify } = props.initialData
|
const { occurredAt, amount, reason, type, classify } = props.initialData
|
||||||
|
|
||||||
if (occurredAt) {
|
if (occurredAt) {
|
||||||
@@ -152,6 +154,10 @@ const initForm = async () => {
|
|||||||
if (classify) {
|
if (classify) {
|
||||||
categoryName.value = classify
|
categoryName.value = classify
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
isSyncing.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +171,9 @@ watch(() => props.initialData, () => {
|
|||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
const handleTypeChange = (newType) => {
|
const handleTypeChange = (newType) => {
|
||||||
|
if (!isSyncing.value) {
|
||||||
categoryName.value = ''
|
categoryName.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onConfirmDate = ({ selectedValues }) => {
|
const onConfirmDate = ({ selectedValues }) => {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
</van-tag>
|
</van-tag>
|
||||||
</slot>
|
</slot>
|
||||||
<h3 class="card-title">{{ budget.name }}</h3>
|
<h3 class="card-title">{{ budget.name }}</h3>
|
||||||
|
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
|
||||||
|
({{ budget.selectedCategories.join('、') }})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<van-icon name="arrow-down" class="expand-icon" />
|
<van-icon name="arrow-down" class="expand-icon" />
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +88,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="budget-body">
|
<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">
|
<div class="amount-info">
|
||||||
<slot name="amount-info"></slot>
|
<slot name="amount-info"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -425,12 +440,29 @@ const timePercentage = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
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 {
|
.header-actions {
|
||||||
@@ -438,6 +470,18 @@ const timePercentage = computed(() => {
|
|||||||
gap: 8px;
|
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 {
|
.amount-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
<van-field name="type" label="交易类型">
|
<van-field name="type" label="交易类型">
|
||||||
<template #input>
|
<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="0">支出</van-radio>
|
||||||
<van-radio :name="1">收入</van-radio>
|
<van-radio :name="1">收入</van-radio>
|
||||||
<van-radio :name="2">不计</van-radio>
|
<van-radio :name="2">不计</van-radio>
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { showToast, showConfirmDialog } from 'vant'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
@@ -171,6 +171,7 @@ const emit = defineEmits(['update:show', 'save'])
|
|||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
|
||||||
// 日期选择相关
|
// 日期选择相关
|
||||||
const showDatePicker = ref(false)
|
const showDatePicker = ref(false)
|
||||||
@@ -201,6 +202,7 @@ watch(() => props.show, (newVal) => {
|
|||||||
|
|
||||||
watch(() => props.transaction, (newVal) => {
|
watch(() => props.transaction, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
|
isSyncing.value = true
|
||||||
// 填充编辑表单
|
// 填充编辑表单
|
||||||
editForm.id = newVal.id
|
editForm.id = newVal.id
|
||||||
editForm.reason = newVal.reason || ''
|
editForm.reason = newVal.reason || ''
|
||||||
@@ -216,6 +218,11 @@ watch(() => props.transaction, (newVal) => {
|
|||||||
currentDate.value = dt.format('YYYY-MM-DD').split('-')
|
currentDate.value = dt.format('YYYY-MM-DD').split('-')
|
||||||
currentTime.value = dt.format('HH:mm').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)
|
emit('update:show', newVal)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 处理类型切换
|
||||||
|
const handleTypeChange = () => {
|
||||||
|
if (!isSyncing.value) {
|
||||||
|
editForm.classify = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理日期确认
|
// 处理日期确认
|
||||||
const onConfirmDate = ({ selectedValues }) => {
|
const onConfirmDate = ({ selectedValues }) => {
|
||||||
const dateStr = selectedValues.join('-')
|
const dateStr = selectedValues.join('-')
|
||||||
@@ -241,12 +255,6 @@ const onConfirmTime = ({ selectedValues }) => {
|
|||||||
showTimePicker.value = false
|
showTimePicker.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听交易类型变化,重新加载分类
|
|
||||||
watch(() => editForm.type, (newVal) => {
|
|
||||||
// 清空已选的分类
|
|
||||||
editForm.classify = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const applySuggestion = () => {
|
const applySuggestion = () => {
|
||||||
if (props.transaction.unconfirmedClassify) {
|
if (props.transaction.unconfirmedClassify) {
|
||||||
editForm.classify = props.transaction.unconfirmedClassify
|
editForm.classify = props.transaction.unconfirmedClassify
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
:title="activeTabTitle"
|
:title="activeTabTitle"
|
||||||
:get-value-class="getValueClass"
|
:get-value-class="getValueClass"
|
||||||
/>
|
/>
|
||||||
<div class="scroll-content">
|
<van-pull-refresh v-model="isRefreshing" class="scroll-content" @refresh="onRefresh">
|
||||||
<div class="budget-list">
|
<div class="budget-list">
|
||||||
<template v-if="expenseBudgets?.length > 0">
|
<template v-if="expenseBudgets?.length > 0">
|
||||||
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<van-empty v-else description="暂无支出预算" />
|
<van-empty v-else description="暂无支出预算" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</van-pull-refresh>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
:title="activeTabTitle"
|
:title="activeTabTitle"
|
||||||
:get-value-class="getValueClass"
|
:get-value-class="getValueClass"
|
||||||
/>
|
/>
|
||||||
<div class="scroll-content">
|
<van-pull-refresh v-model="isRefreshing" class="scroll-content" @refresh="onRefresh">
|
||||||
<div class="budget-list">
|
<div class="budget-list">
|
||||||
<template v-if="incomeBudgets?.length > 0">
|
<template v-if="incomeBudgets?.length > 0">
|
||||||
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
||||||
@@ -117,11 +117,11 @@
|
|||||||
<van-empty v-else description="暂无收入预算" />
|
<van-empty v-else description="暂无收入预算" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</van-pull-refresh>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
<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">
|
<div class="budget-list">
|
||||||
<template v-if="savingsBudgets?.length > 0">
|
<template v-if="savingsBudgets?.length > 0">
|
||||||
<BudgetCard
|
<BudgetCard
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
<van-empty v-else description="暂无存款计划" />
|
<van-empty v-else description="暂无存款计划" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</van-pull-refresh>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue'
|
|||||||
const activeTab = ref(BudgetCategory.Expense)
|
const activeTab = ref(BudgetCategory.Expense)
|
||||||
const budgetEditRef = ref(null)
|
const budgetEditRef = ref(null)
|
||||||
const savingsConfigRef = ref(null)
|
const savingsConfigRef = ref(null)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
const expenseBudgets = ref([])
|
const expenseBudgets = ref([])
|
||||||
const incomeBudgets = 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 () => {
|
const fetchCategoryStats = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getCategoryStats(activeTab.value)
|
const res = await getCategoryStats(activeTab.value)
|
||||||
@@ -364,6 +375,11 @@ const handleDelete = (budget) => {
|
|||||||
margin: 0 12px 12px;
|
margin: 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.delete-button {
|
.delete-button {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,11 @@
|
|||||||
<div v-else class="detail-content">
|
<div v-else class="detail-content">
|
||||||
{{ currentMessage.content }}
|
{{ currentMessage.content }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentMessage.url" class="detail-footer" style="padding: 16px;">
|
<template v-if="currentMessage.url && currentMessage.messageType === 1" #footer>
|
||||||
<van-button type="primary" block @click="handleUrlJump(currentMessage.url)">
|
<van-button type="primary" block round @click="handleUrlJump(currentMessage.url)">
|
||||||
查看详情
|
查看详情
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</template>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -144,11 +144,6 @@ const viewDetail = async (item) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.messageType === 1) {
|
|
||||||
handleUrlJump(item.url || item.content);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMessage.value = item;
|
currentMessage.value = item;
|
||||||
detailVisible.value = true;
|
detailVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user