多个样式调整
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 21s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
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 21s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -22,6 +22,11 @@ public class BudgetArchive : BaseEntity
|
||||
/// </summary>
|
||||
public decimal RealizedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 详细描述
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 归档目标年份
|
||||
/// </summary>
|
||||
|
||||
@@ -96,7 +96,11 @@ public class BudgetService(
|
||||
if (archive != null) // 存在归档 直接读取归档数据
|
||||
{
|
||||
budget.Limit = archive.BudgetedAmount;
|
||||
return BudgetResult.FromEntity(budget, archive.RealizedAmount, referenceDate);
|
||||
return BudgetResult.FromEntity(
|
||||
budget,
|
||||
archive.RealizedAmount,
|
||||
referenceDate,
|
||||
archive.Description ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +275,7 @@ public class BudgetService(
|
||||
{
|
||||
archive.RealizedAmount = budget.Current;
|
||||
archive.ArchiveDate = DateTime.Now;
|
||||
archive.Description = budget.Description;
|
||||
updateArchives.Add(archive);
|
||||
}
|
||||
else
|
||||
@@ -283,6 +288,7 @@ public class BudgetService(
|
||||
Month = month,
|
||||
BudgetedAmount = budget.Limit,
|
||||
RealizedAmount = budget.Current,
|
||||
Description = budget.Description,
|
||||
ArchiveDate = DateTime.Now
|
||||
};
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ const timePercentage = computed(() => {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -7,39 +7,24 @@
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<!-- 月份选择器 固定区域 -->
|
||||
<div class="sticky-header">
|
||||
<van-button
|
||||
icon="arrow-left"
|
||||
plain
|
||||
size="small"
|
||||
@click="changeMonth(-1)"
|
||||
/>
|
||||
<div class="sticky-header-text" @click="showMonthPicker = true">
|
||||
{{ currentYear }}年{{ currentMonth }}月
|
||||
<van-icon name="arrow-down" />
|
||||
</div>
|
||||
<van-button
|
||||
icon="arrow"
|
||||
plain
|
||||
size="small"
|
||||
:disabled="isCurrentMonth"
|
||||
@click="changeMonth(1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 下拉刷新 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<!-- 加载中 -->
|
||||
<van-loading v-if="loading" vertical style="padding: 100px 0">
|
||||
<!-- 初始加载中 -->
|
||||
<van-loading v-if="loading && firstLoading" vertical style="padding: 100px 0">
|
||||
加载统计数据中...
|
||||
</van-loading>
|
||||
|
||||
<!-- 统计内容 -->
|
||||
<div v-else class="statistics-content">
|
||||
|
||||
<!-- 固定概览部分(置顶不滚动) -->
|
||||
<div v-if="!firstLoading" class="overview-fixed-wrapper">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<div :key="dateKey">
|
||||
<!-- 月度概览卡片 -->
|
||||
<div class="overview-card">
|
||||
<!-- 左切换按钮 -->
|
||||
<div class="nav-arrow left" @click.stop="changeMonth(-1)">
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
|
||||
<div class="overview-item clickable" @click="goToTypeOverviewBills(0)">
|
||||
<div class="label">总支出</div>
|
||||
<div class="value expense">¥{{ formatMoney(monthlyData.totalExpense) }}</div>
|
||||
@@ -59,8 +44,30 @@
|
||||
</div>
|
||||
<div class="sub-text">{{ monthlyData.totalCount }}笔交易</div>
|
||||
</div>
|
||||
|
||||
<!-- 右切换按钮 -->
|
||||
<div
|
||||
class="nav-arrow right"
|
||||
:class="{ disabled: isCurrentMonth }"
|
||||
@click.stop="!isCurrentMonth && changeMonth(1)"
|
||||
>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
|
||||
<!-- 月份日期标识 -->
|
||||
<div class="date-tag" @click="showMonthPicker = true">
|
||||
{{ dateTagLabel }}
|
||||
<van-icon name="arrow-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 统计内容(可滚动部分) -->
|
||||
<div v-if="!firstLoading" class="statistics-content">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<div :key="dateKey">
|
||||
<!-- 分类统计 -->
|
||||
<div class="common-card">
|
||||
<div class="card-header">
|
||||
@@ -98,9 +105,9 @@
|
||||
<div v-if="expenseCategoriesView.length > 0" class="category-list">
|
||||
<div
|
||||
v-for="(category) in expenseCategoriesView"
|
||||
:key="category.classify"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="goToCategoryBills(category.classify, 0)"
|
||||
@click="category.isOther ? (showAllExpense = true) : goToCategoryBills(category.classify, 0)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color" :style="{ backgroundColor: category.color }"></div>
|
||||
@@ -113,7 +120,7 @@
|
||||
<div class="category-amount">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon name="arrow" class="category-arrow" />
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,9 +141,9 @@
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in incomeCategoriesView"
|
||||
:key="category.classify"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="goToCategoryBills(category.classify, 1)"
|
||||
@click="category.isOther ? (showAllIncome = true) : goToCategoryBills(category.classify, 1)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color income-color"></div>
|
||||
@@ -149,7 +156,7 @@
|
||||
<div class="category-amount income-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon name="arrow" class="category-arrow" />
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,9 +171,9 @@
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="category in noneCategoriesView"
|
||||
:key="category.classify"
|
||||
:key="category.isOther ? 'other' : category.classify"
|
||||
class="category-item clickable"
|
||||
@click="goToCategoryBills(category.classify, 2)"
|
||||
@click="category.isOther ? (showAllNone = true) : goToCategoryBills(category.classify, 2)"
|
||||
>
|
||||
<div class="category-info">
|
||||
<div class="category-color none-color"></div>
|
||||
@@ -179,7 +186,7 @@
|
||||
<div class="category-amount none-text">¥{{ formatMoney(category.amount) }}</div>
|
||||
<div class="category-percent">{{ category.percent }}%</div>
|
||||
</div>
|
||||
<van-icon name="arrow" class="category-arrow" />
|
||||
<van-icon :name="category.isOther ? 'arrow-down' : 'arrow'" class="category-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,6 +268,8 @@
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 月份选择器 -->
|
||||
@@ -331,12 +340,19 @@ const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const firstLoading = ref(true)
|
||||
const refreshing = ref(false)
|
||||
const showMonthPicker = ref(false)
|
||||
const showAllExpense = ref(false)
|
||||
const showAllIncome = ref(false)
|
||||
const showAllNone = ref(false)
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const currentMonth = ref(new Date().getMonth() + 1)
|
||||
const selectedDate = ref([new Date().getFullYear().toString(), (new Date().getMonth() + 1).toString().padStart(2, '0')])
|
||||
|
||||
const transitionName = ref('slide-right')
|
||||
const dateKey = computed(() => `${currentYear.value}-${currentMonth.value}`)
|
||||
|
||||
// 账单列表相关
|
||||
const billListVisible = ref(false)
|
||||
const billListLoading = ref(false)
|
||||
@@ -371,33 +387,70 @@ const incomeCategories = ref([])
|
||||
const noneCategories = ref([])
|
||||
|
||||
const expenseCategoriesView = computed(() => {
|
||||
const sorted = [...expenseCategories.value]
|
||||
const unclassifiedIndex = sorted.findIndex(c => !c.classify)
|
||||
const list = [...expenseCategories.value]
|
||||
const unclassifiedIndex = list.findIndex(c => !c.classify)
|
||||
if (unclassifiedIndex !== -1) {
|
||||
const [unclassified] = sorted.splice(unclassifiedIndex, 1)
|
||||
sorted.unshift(unclassified)
|
||||
const [unclassified] = list.splice(unclassifiedIndex, 1)
|
||||
list.unshift(unclassified)
|
||||
}
|
||||
return sorted
|
||||
|
||||
if (showAllExpense.value || list.length <= 7) return list
|
||||
|
||||
const top = list.slice(0, 6)
|
||||
const rest = list.slice(6)
|
||||
top.push({
|
||||
classify: '其他',
|
||||
amount: rest.reduce((s, c) => s + c.amount, 0),
|
||||
count: rest.reduce((s, c) => s + c.count, 0),
|
||||
percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)),
|
||||
color: '#AAB7B8',
|
||||
isOther: true
|
||||
})
|
||||
return top
|
||||
})
|
||||
|
||||
const incomeCategoriesView = computed(() => {
|
||||
const sorted = [...incomeCategories.value]
|
||||
const unclassifiedIndex = sorted.findIndex(c => !c.classify)
|
||||
const list = [...incomeCategories.value]
|
||||
const unclassifiedIndex = list.findIndex(c => !c.classify)
|
||||
if (unclassifiedIndex !== -1) {
|
||||
const [unclassified] = sorted.splice(unclassifiedIndex, 1)
|
||||
sorted.unshift(unclassified)
|
||||
const [unclassified] = list.splice(unclassifiedIndex, 1)
|
||||
list.unshift(unclassified)
|
||||
}
|
||||
return sorted
|
||||
|
||||
if (showAllIncome.value || list.length <= 7) return list
|
||||
|
||||
const top = list.slice(0, 6)
|
||||
const rest = list.slice(6)
|
||||
top.push({
|
||||
classify: '其他',
|
||||
amount: rest.reduce((s, c) => s + c.amount, 0),
|
||||
count: rest.reduce((s, c) => s + c.count, 0),
|
||||
percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)),
|
||||
isOther: true
|
||||
})
|
||||
return top
|
||||
})
|
||||
|
||||
const noneCategoriesView = computed(() => {
|
||||
const sorted = [...noneCategories.value]
|
||||
const unclassifiedIndex = sorted.findIndex(c => !c.classify)
|
||||
const list = [...noneCategories.value]
|
||||
const unclassifiedIndex = list.findIndex(c => !c.classify)
|
||||
if (unclassifiedIndex !== -1) {
|
||||
const [unclassified] = sorted.splice(unclassifiedIndex, 1)
|
||||
sorted.unshift(unclassified)
|
||||
const [unclassified] = list.splice(unclassifiedIndex, 1)
|
||||
list.unshift(unclassified)
|
||||
}
|
||||
return sorted
|
||||
|
||||
if (showAllNone.value || list.length <= 7) return list
|
||||
|
||||
const top = list.slice(0, 6)
|
||||
const rest = list.slice(6)
|
||||
top.push({
|
||||
classify: '其他',
|
||||
amount: rest.reduce((s, c) => s + c.amount, 0),
|
||||
count: rest.reduce((s, c) => s + c.count, 0),
|
||||
percent: parseFloat(rest.reduce((s, c) => s + c.percent, 0).toFixed(1)),
|
||||
isOther: true
|
||||
})
|
||||
return top
|
||||
})
|
||||
|
||||
// 趋势数据
|
||||
@@ -418,7 +471,7 @@ const colors = [
|
||||
const circumference = computed(() => 2 * Math.PI * 70)
|
||||
const chartSegments = computed(() => {
|
||||
let offset = 0
|
||||
return expenseCategories.value.map((category) => {
|
||||
return expenseCategoriesView.value.map((category) => {
|
||||
const percent = category.percent / 100
|
||||
const length = circumference.value * percent
|
||||
const segment = {
|
||||
@@ -452,6 +505,31 @@ const isCurrentMonth = computed(() => {
|
||||
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
|
||||
})
|
||||
|
||||
// 日期标签展示文字
|
||||
const dateTagLabel = computed(() => {
|
||||
const now = new Date()
|
||||
const todayYear = now.getFullYear()
|
||||
const todayMonth = now.getMonth() + 1
|
||||
|
||||
if (currentYear.value === todayYear && currentMonth.value === todayMonth) {
|
||||
return '本月'
|
||||
}
|
||||
|
||||
// 计算上个月
|
||||
let lastYear = todayYear
|
||||
let lastMonth = todayMonth - 1
|
||||
if (lastMonth === 0) {
|
||||
lastMonth = 12
|
||||
lastYear--
|
||||
}
|
||||
|
||||
if (currentYear.value === lastYear && currentMonth.value === lastMonth) {
|
||||
return `上月`
|
||||
}
|
||||
|
||||
return `${currentYear.value}年${currentMonth.value}月`
|
||||
})
|
||||
|
||||
// 是否为未分类账单
|
||||
const isUnclassified = computed(() => {
|
||||
return selectedClassify.value === '未分类' || selectedClassify.value === ''
|
||||
@@ -484,6 +562,7 @@ const getBarHeight = (value, maxValue) => {
|
||||
|
||||
// 切换月份
|
||||
const changeMonth = (offset) => {
|
||||
transitionName.value = offset > 0 ? 'slide-left' : 'slide-right'
|
||||
let newMonth = currentMonth.value + offset
|
||||
let newYear = currentYear.value
|
||||
|
||||
@@ -504,26 +583,51 @@ const changeMonth = (offset) => {
|
||||
|
||||
currentYear.value = newYear
|
||||
currentMonth.value = newMonth
|
||||
|
||||
// 重置展开状态
|
||||
showAllExpense.value = false
|
||||
showAllIncome.value = false
|
||||
showAllNone.value = false
|
||||
|
||||
fetchStatistics()
|
||||
}
|
||||
|
||||
// 确认月份选择
|
||||
const onMonthConfirm = ({ selectedValues }) => {
|
||||
currentYear.value = parseInt(selectedValues[0])
|
||||
currentMonth.value = parseInt(selectedValues[1])
|
||||
const newYear = parseInt(selectedValues[0])
|
||||
const newMonth = parseInt(selectedValues[1])
|
||||
|
||||
// 判断方向以应用动画
|
||||
if (newYear > currentYear.value || (newYear === currentYear.value && newMonth > currentMonth.value)) {
|
||||
transitionName.value = 'slide-left'
|
||||
} else {
|
||||
transitionName.value = 'slide-right'
|
||||
}
|
||||
|
||||
currentYear.value = newYear
|
||||
currentMonth.value = newMonth
|
||||
showMonthPicker.value = false
|
||||
|
||||
// 重置展开状态
|
||||
showAllExpense.value = false
|
||||
showAllIncome.value = false
|
||||
showAllNone.value = false
|
||||
|
||||
fetchStatistics()
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
await fetchStatistics()
|
||||
await fetchStatistics(false)
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatistics = async () => {
|
||||
const fetchStatistics = async (showLoading = true) => {
|
||||
if (showLoading && firstLoading.value) {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchMonthlyData(),
|
||||
@@ -535,6 +639,7 @@ const fetchStatistics = async () => {
|
||||
showToast('获取统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
firstLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,8 +968,24 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overview-fixed-wrapper {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f7f8fa;
|
||||
padding: 16px 0 1px 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.overview-fixed-wrapper {
|
||||
background: #121212;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: 16px 0 0 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden; /* 防止滑动动画出现横向滚动条 */
|
||||
}
|
||||
|
||||
:deep(.van-pull-refresh) {
|
||||
@@ -875,6 +996,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
/* 月度概览卡片 */
|
||||
.overview-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
@@ -893,6 +1015,93 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: #c8c9cc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-arrow:active {
|
||||
color: var(--van-primary-color);
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.nav-arrow.left {
|
||||
left: 0;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
.nav-arrow.right {
|
||||
right: 0;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
|
||||
.nav-arrow.disabled {
|
||||
color: #f2f3f5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.date-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
color: var(--van-primary-color);
|
||||
background-color: var(--van-primary-color-light);
|
||||
padding: 2px 10px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.date-tag {
|
||||
background-color: rgba(var(--van-primary-color-rgb), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active,
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user