2661 lines
70 KiB
Vue
2661 lines
70 KiB
Vue
<template>
|
||
<view class="bill-page page-enter theme-transition" :style="{ background: themeConfig.bgColor }">
|
||
<PageHeader
|
||
title="账单管理"
|
||
subtitle="理性消费,合理规划"
|
||
:showRight="true"
|
||
>
|
||
<template #right>
|
||
<view class="header-actions">
|
||
<view
|
||
class="header-btn"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
@click="toggleBatchMode"
|
||
>
|
||
<text
|
||
class="batch-icon"
|
||
:style="{ color: isBatchMode ? themeConfig.primaryColor : themeConfig.textSecondary }"
|
||
>{{ isBatchMode ? '✓' : '☰' }}</text>
|
||
</view>
|
||
<view
|
||
class="header-btn"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
@click="handleBudgetSetting"
|
||
>
|
||
<FaIcon icon="gear" :size="22" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</PageHeader>
|
||
|
||
<view class="sticky-header card-enter" :style="{ background: themeConfig.bgColor }">
|
||
<view class="search-bar">
|
||
<view
|
||
class="search-input"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor, boxShadow: `0 2px 8px ${themeConfig.shadowColor}` }"
|
||
>
|
||
<FaIcon icon="search" :size="22" :color="themeConfig.textSecondary" />
|
||
<input
|
||
v-model="searchWord"
|
||
@input="handleSearch"
|
||
placeholder="搜索备注/分类"
|
||
:style="{ color: themeConfig.textPrimary }"
|
||
/>
|
||
<FaIcon
|
||
v-if="searchWord"
|
||
class="clear-icon"
|
||
icon="xmark"
|
||
:size="14"
|
||
:color="themeConfig.textSecondary"
|
||
@click="clearSearch"
|
||
/>
|
||
</view>
|
||
|
||
<view class="filter-section">
|
||
<view class="filter-row-label" :style="{ color: themeConfig.textSecondary }">类型</view>
|
||
<CapsuleFilter
|
||
:options="BILL_TYPE_FILTER_OPTIONS"
|
||
:value="billType"
|
||
@change="handleTypeFilterChange"
|
||
size="small"
|
||
/>
|
||
</view>
|
||
|
||
<view class="filter-section">
|
||
<view class="filter-row-label" :style="{ color: themeConfig.textSecondary }">时间</view>
|
||
<CapsuleFilter
|
||
:options="BILL_TIME_FILTER_OPTIONS"
|
||
:value="activeTimeTag"
|
||
@change="handleTimeFilterChange"
|
||
size="small"
|
||
:scrollable="true"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal" v-if="showCustomDateModal" @click="showCustomDateModal = false">
|
||
<view
|
||
class="modal-box date-picker-box theme-transition"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
@click.stop
|
||
>
|
||
<view class="modal-title" :style="{ color: themeConfig.textPrimary }">选择时间范围</view>
|
||
<view class="form-field">
|
||
<text class="field-label" :style="{ color: themeConfig.textSecondary }">开始日期:</text>
|
||
<picker mode="date" :value="customStartDate" @change="e => customStartDate = e.detail.value">
|
||
<view
|
||
class="date-picker field-value"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
>{{ customStartDate }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="form-field">
|
||
<text class="field-label" :style="{ color: themeConfig.textSecondary }">结束日期:</text>
|
||
<picker mode="date" :value="customEndDate" @change="e => customEndDate = e.detail.value">
|
||
<view
|
||
class="date-picker field-value"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
>{{ customEndDate }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="btns">
|
||
<button
|
||
class="cancel-btn theme-transition"
|
||
:style="{ backgroundColor: themeConfig.borderColor, color: themeConfig.textSecondary }"
|
||
@click="showCustomDateModal = false"
|
||
>取消</button>
|
||
<button
|
||
class="confirm-btn theme-transition btn btn--primary"
|
||
@click="confirmCustomDate"
|
||
>确认</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="main-scroll" scroll-y :show-scrollbar="false">
|
||
<AppCard
|
||
class="budget-card"
|
||
:padding="24"
|
||
:radius="20"
|
||
:marginBottom="20"
|
||
>
|
||
<view class="budget-title">
|
||
<text :style="{ color: themeConfig.textPrimary }">本月预算</text>
|
||
<text class="budget-value" :style="{ color: themeConfig.primaryColor }" v-if="monthlyBudget">¥{{ monthlyBudget }}</text>
|
||
<text class="budget-set-btn" :style="{ color: themeConfig.primaryColor }" v-else @click="handleBudgetSetting">去设置</text>
|
||
</view>
|
||
<view v-if="monthlyBudget">
|
||
<view class="progress-bar" :style="{ backgroundColor: themeConfig.bgColor }">
|
||
<view class="progress-fill" :style="{ width: budgetProgress + '%', backgroundColor: getProgressColor() }"></view>
|
||
</view>
|
||
<view class="budget-info">
|
||
<text :class="{ over: budgetProgress > 100 }" :style="{ color: budgetProgress > 100 ? themeConfig.dangerColor : themeConfig.textSecondary }">已支出 ¥{{ monthExpend }} / ¥{{ monthlyBudget }}</text>
|
||
<text :style="{ color: themeConfig.textSecondary }">剩余预算:¥{{ remainingBudget }}</text>
|
||
<text :style="{ color: themeConfig.textSecondary }">{{ budgetProgress.toFixed(1) }}%</text>
|
||
</view>
|
||
</view>
|
||
<view class="budget-empty" v-else>
|
||
<text :style="{ color: themeConfig.textSecondary }">尚未设置本月预算,点击右上角「去设置」添加</text>
|
||
</view>
|
||
</AppCard>
|
||
|
||
<AppCard
|
||
class="budget-card"
|
||
v-if="activeTimeTag === 'lastMonth'"
|
||
:padding="24"
|
||
:radius="20"
|
||
:marginBottom="20"
|
||
>
|
||
<view class="budget-title">
|
||
<text :style="{ color: themeConfig.textPrimary }">上月预算</text>
|
||
<text class="budget-value" :style="{ color: themeConfig.primaryColor }" v-if="lastMonthBudget">¥{{ lastMonthBudget }}</text>
|
||
<text class="budget-set-btn" :style="{ color: themeConfig.primaryColor }" v-else @click="handleLastMonthBudgetSetting">去设置</text>
|
||
</view>
|
||
<view v-if="lastMonthBudget">
|
||
<view class="progress-bar" :style="{ backgroundColor: themeConfig.bgColor }">
|
||
<view class="progress-fill" :style="{ width: lastMonthBudgetProgress + '%', backgroundColor: getProgressColor() }"></view>
|
||
</view>
|
||
<view class="budget-info">
|
||
<text :class="{ over: lastMonthBudgetProgress > 100 }" :style="{ color: lastMonthBudgetProgress > 100 ? themeConfig.dangerColor : themeConfig.textSecondary }">已支出 ¥{{ lastMonthExpend }} / ¥{{ lastMonthBudget }}</text>
|
||
<text :style="{ color: themeConfig.textSecondary }">剩余预算:¥{{ lastMonthRemainingBudget }}</text>
|
||
<text :style="{ color: themeConfig.textSecondary }">{{ lastMonthBudgetProgress.toFixed(1) }}%</text>
|
||
</view>
|
||
</view>
|
||
<view class="budget-empty" v-else>
|
||
<text :style="{ color: themeConfig.textSecondary }">尚未设置上月预算,点击右上角「去设置」添加</text>
|
||
</view>
|
||
</AppCard>
|
||
|
||
<AppCard class="stat-card card-enter" :padding="24" :radius="12" :marginBottom="20">
|
||
<scroll-view class="stat-scroll" scroll-x :show-scrollbar="false">
|
||
<view class="stat-row">
|
||
<view class="stat-item">
|
||
<view class="stat-icon expend-icon">💸</view>
|
||
<view class="stat-info">
|
||
<text class="label" :style="{ color: themeConfig.textSecondary }">总支出</text>
|
||
<text class="value expend" :style="{ color: themeConfig.dangerColor }">¥{{ totalExpend.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-item">
|
||
<view class="stat-icon income-icon">💰</view>
|
||
<view class="stat-info">
|
||
<text class="label" :style="{ color: themeConfig.textSecondary }">总收入</text>
|
||
<text class="value income" :style="{ color: themeConfig.secondaryColor }">¥{{ totalIncome.toFixed(2) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-item">
|
||
<view class="stat-icon balance-icon">📊</view>
|
||
<view class="stat-info">
|
||
<text class="label" :style="{ color: themeConfig.textSecondary }">当前结余</text>
|
||
<text class="value balance" :style="{ color: currentPeriodBalance >= 0 ? themeConfig.secondaryColor : themeConfig.dangerColor }">
|
||
{{ currentPeriodBalance >= 0 ? '+' : '-' }}¥{{ Math.abs(currentPeriodBalance).toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-item">
|
||
<view class="stat-icon month-icon">📅</view>
|
||
<view class="stat-info">
|
||
<text class="label" :style="{ color: themeConfig.textSecondary }">累计结余</text>
|
||
<text class="value balance" :style="{ color: monthlyTotalBalance >= 0 ? themeConfig.secondaryColor : themeConfig.dangerColor }">
|
||
{{ monthlyTotalBalance >= 0 ? '+' : '-' }}¥{{ Math.abs(monthlyTotalBalance).toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</AppCard>
|
||
|
||
<view class="cate-stat-section card-enter">
|
||
<view class="cate-stat-single" v-if="cateStatList.length <= 2">
|
||
<view
|
||
class="cate-stat-item-wide theme-transition"
|
||
v-for="item in cateStatList"
|
||
:key="item.cate"
|
||
@click="filterByCate(item.cate)"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
>
|
||
<view class="cate-left-wide">
|
||
<view class="cate-icon-wide" :style="{ backgroundColor: item.color }">{{ item.icon }}</view>
|
||
<text class="cate-name-wide" :style="{ color: themeConfig.textPrimary }">{{ getCateLabel(item.cate) }}</text>
|
||
</view>
|
||
<view class="cate-right-wide">
|
||
<view class="cate-money-row-wide">
|
||
<text class="cate-money expend" :style="{ color: themeConfig.dangerColor }">支出:¥{{ item.expend.toFixed(2) }}</text>
|
||
<text class="cate-money income" :style="{ color: themeConfig.secondaryColor }">收入:¥{{ item.income.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cate-rate-row-wide">
|
||
<text class="cate-rate" :style="{ color: themeConfig.textSecondary }">占比:支出{{ item.expendRate }}% / 收入{{ item.incomeRate }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="cate-stat-scroll-wrapper" v-else>
|
||
<scroll-view class="cate-stat-scroll" scroll-x="true" :show-scrollbar="false" @scroll="onCateScroll">
|
||
<view class="cate-stat-container-scroll">
|
||
<view
|
||
class="cate-stat-item-scroll theme-transition"
|
||
v-for="item in cateStatList"
|
||
:key="item.cate"
|
||
@click="filterByCate(item.cate)"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
>
|
||
<view class="cate-icon-scroll" :style="{ backgroundColor: item.color }">{{ item.icon }}</view>
|
||
<text class="cate-name-scroll" :style="{ color: themeConfig.textPrimary }">{{ getCateLabel(item.cate) }}</text>
|
||
<view class="cate-stat-row-scroll">
|
||
<text class="cate-stat-text" :style="{ color: themeConfig.dangerColor }">支出 ¥{{ item.expend.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cate-stat-row-scroll">
|
||
<text class="cate-stat-text" :style="{ color: themeConfig.secondaryColor }">收入 ¥{{ item.income.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="cate-stat-row-scroll">
|
||
<text class="cate-rate-scroll" :style="{ color: themeConfig.textSecondary }">占比 {{ item.expendRate }}%</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="scroll-indicator">
|
||
<view
|
||
class="indicator-dot"
|
||
v-for="(item, index) in cateStatList"
|
||
:key="index"
|
||
:class="{ active: currentCateIndex === index }"
|
||
:style="{ backgroundColor: currentCateIndex === index ? themeConfig.primaryColor : themeConfig.borderColor }"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bill-list-container content-fade">
|
||
<SkeletonScreen v-if="!billListLoaded" type="list" :count="5" />
|
||
|
||
<AppCard
|
||
v-else
|
||
class="bill-list-card card-enter"
|
||
:padding="16"
|
||
:radius="20"
|
||
:marginBottom="20"
|
||
>
|
||
<view v-if="Object.keys(groupedBillList).length === 0" class="bill-empty-wrap">
|
||
<EmptyState
|
||
type="bill"
|
||
title="还没有账单记录"
|
||
desc="点击按钮记录你的第一笔收支"
|
||
actionText="记一笔"
|
||
@action="openAddModal"
|
||
/>
|
||
</view>
|
||
|
||
<view v-for="(group, dateKey) in groupedBillList" :key="dateKey" class="bill-group">
|
||
<view class="bill-group-header">
|
||
<text class="bill-group-date" :style="{ color: themeConfig.textSecondary }">{{ formatBillDate(dateKey) }}</text>
|
||
<text class="bill-group-summary" :style="{ color: themeConfig.textSecondary }">
|
||
收 +¥{{ group.income.toFixed(2) }} 支 -¥{{ group.expend.toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
|
||
<SwipeableItem
|
||
v-for="item in group.items"
|
||
:key="item.id"
|
||
:actions="swipeActions"
|
||
:index="item.id"
|
||
@action="(action) => handleSwipeAction(action, item)"
|
||
@closeOthers="closeOtherSwipe"
|
||
>
|
||
<view
|
||
class="bill-item-content theme-transition"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
>
|
||
<view class="bill-left">
|
||
<text class="cate-icon" :style="{ backgroundColor: getCateColor(item.cate) }">{{ getCateIcon(item.cate) }}</text>
|
||
<view class="bill-text">
|
||
<text class="cate-name" :style="{ color: themeConfig.textPrimary }">{{ getCateLabel(item.cate) }}</text>
|
||
<view class="bill-meta">
|
||
<text class="channel" v-if="item.channel" :style="{ color: themeConfig.textSecondary }">{{ getChannelLabel(item.channel) }}</text>
|
||
<text class="meta-divider" v-if="item.channel && item.note">·</text>
|
||
<text class="note" v-if="item.note" :style="{ color: themeConfig.textSecondary }">{{ item.note }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<text
|
||
class="money"
|
||
:class="item.type === BILL_TYPE.EXPEND ? 'expend' : 'income'"
|
||
:style="{ color: item.type === BILL_TYPE.EXPEND ? themeConfig.dangerColor : themeConfig.secondaryColor }"
|
||
>
|
||
{{ item.type === BILL_TYPE.EXPEND ? '-' : '+' }}{{ Number(item.money).toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
</SwipeableItem>
|
||
</view>
|
||
|
||
<view class="batch-bar" v-if="isBatchMode">
|
||
<text :style="{ color: themeConfig.textSecondary }">已选 {{ selectedBillIds.length }} 项</text>
|
||
<view class="batch-actions">
|
||
<view class="batch-btn" :style="{ backgroundColor: themeConfig.dangerColor }" @click="batchDelete">批量删除</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="scroll-tip" v-if="showScrollTip" :style="{ color: themeConfig.textSecondary }">上滑查看更多</view>
|
||
<view class="pagination-tip" v-if="hasMore" @click="loadMore">
|
||
<text :style="{ color: themeConfig.primaryColor }">加载更多</text>
|
||
</view>
|
||
<view class="pagination-tip" v-else-if="billList.length > 0 && !hasMore" :style="{ color: themeConfig.textSecondary }">
|
||
<text>已经到底了</text>
|
||
</view>
|
||
</AppCard>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view class="modal" v-if="showBudgetModal" @click="showBudgetModal = false">
|
||
<view
|
||
class="modal-box budget-modal-box theme-transition"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
@click.stop
|
||
>
|
||
<view class="modal-title" :style="{ color: themeConfig.textPrimary }">设置预算</view>
|
||
<view class="form-field">
|
||
<text class="field-label" :style="{ color: themeConfig.textSecondary }">预算月份:</text>
|
||
<picker mode="month" :value="selectedBudgetMonth" @change="onBudgetMonthChange">
|
||
<view
|
||
class="date-picker field-value"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
>{{ selectedBudgetMonth }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="form-field">
|
||
<text class="field-label" :style="{ color: themeConfig.textSecondary }">预算金额(元)</text>
|
||
<input
|
||
v-model="tempBudget"
|
||
type="digit"
|
||
placeholder="请输入预算金额"
|
||
class="input money-input field-value"
|
||
placeholder-class="input-placeholder"
|
||
@input="validateMoneyInput(tempBudget, 'tempBudget')"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
/>
|
||
</view>
|
||
<view class="btns">
|
||
<button
|
||
class="cancel-btn theme-transition"
|
||
:style="{ backgroundColor: themeConfig.borderColor, color: themeConfig.textSecondary }"
|
||
@click="showBudgetModal = false"
|
||
>取消</button>
|
||
<button
|
||
class="confirm-btn theme-transition btn btn--primary"
|
||
@click="saveBudget"
|
||
>确认设置</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="float-buttons" @click.stop v-if="!showAdd && !isBatchMode">
|
||
<view
|
||
class="add-btn theme-transition"
|
||
@click="handleAddBtnClick"
|
||
:style="{ background: themeConfig.gradientSoft, transform: addBtnScale }"
|
||
>
|
||
<text class="add-btn-icon">+</text>
|
||
</view>
|
||
</view>
|
||
|
||
<BottomSheet
|
||
:visible="showAdd"
|
||
:title="isEdit ? '编辑账单' : '记一笔'"
|
||
height="85vh"
|
||
@update:visible="val => showAdd = val"
|
||
@close="handleSheetClose"
|
||
>
|
||
<view class="type-section">
|
||
<view
|
||
class="type-btn"
|
||
:class="{ active: form.type === BILL_TYPE.EXPEND }"
|
||
:style="{
|
||
background: form.type === BILL_TYPE.EXPEND ? themeConfig.gradientSoft : themeConfig.borderColor,
|
||
color: form.type === BILL_TYPE.EXPEND ? '#fff' : themeConfig.textSecondary
|
||
}"
|
||
@click="form.type = BILL_TYPE.EXPEND"
|
||
>
|
||
<text class="type-icon">💰</text>
|
||
<text class="type-text">支出</text>
|
||
</view>
|
||
<view
|
||
class="type-btn"
|
||
:class="{ active: form.type === BILL_TYPE.INCOME }"
|
||
:style="{
|
||
background: form.type === BILL_TYPE.INCOME ? themeConfig.gradientSoft : themeConfig.borderColor,
|
||
color: form.type === BILL_TYPE.INCOME ? '#fff' : themeConfig.textSecondary
|
||
}"
|
||
@click="form.type = BILL_TYPE.INCOME"
|
||
>
|
||
<text class="type-icon">📈</text>
|
||
<text class="type-text">收入</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="amount-section">
|
||
<text class="amount-label" :style="{ color: themeConfig.textSecondary }">金额</text>
|
||
<view class="amount-input-wrap">
|
||
<text class="amount-symbol" :style="{ color: form.type === BILL_TYPE.EXPEND ? themeConfig.dangerColor : themeConfig.secondaryColor }">¥</text>
|
||
<input
|
||
v-model="formattedMoney"
|
||
type="digit"
|
||
placeholder="0.00"
|
||
class="amount-input"
|
||
@input="handleMoneyInput"
|
||
@blur="formatMoneyOnBlur"
|
||
:style="{ color: form.type === BILL_TYPE.EXPEND ? themeConfig.dangerColor : themeConfig.secondaryColor }"
|
||
/>
|
||
</view>
|
||
<view class="quick-amounts">
|
||
<text
|
||
class="quick-amount-btn"
|
||
v-for="amount in QUICK_MONEY_AMOUNTS"
|
||
:key="amount"
|
||
@click="fillQuickMoney(amount)"
|
||
:style="{ backgroundColor: themeConfig.borderColor, color: themeConfig.textSecondary }"
|
||
>¥{{ amount }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">分类</text>
|
||
<view class="recent-cate-row" v-if="recentCateList.length">
|
||
<text class="recent-label" :style="{ color: themeConfig.textSecondary }">最近</text>
|
||
<view class="recent-cate-items">
|
||
<view
|
||
class="recent-cate-item"
|
||
v-for="cate in recentCateList"
|
||
:key="cate.value"
|
||
:class="{ active: form.cate === cate.value }"
|
||
@click="form.cate = cate.value"
|
||
>
|
||
<text class="recent-cate-icon" :style="{ backgroundColor: cate.color }">{{ cate.icon }}</text>
|
||
<text class="recent-cate-name" :style="{ color: themeConfig.textPrimary }">{{ cate.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="cate-grid-new">
|
||
<view
|
||
class="cate-item-new"
|
||
v-for="cate in cateList"
|
||
:key="cate.value"
|
||
:class="{ active: form.cate === cate.value }"
|
||
@click="selectCate(cate)"
|
||
>
|
||
<view class="cate-icon-wrap" :style="{ backgroundColor: form.cate === cate.value ? cate.color : themeConfig.borderColor }">
|
||
<text class="cate-icon">{{ cate.icon }}</text>
|
||
</view>
|
||
<text class="cate-label" :style="{ color: form.cate === cate.value ? cate.color : themeConfig.textSecondary }">{{ cate.label }}</text>
|
||
</view>
|
||
<view class="cate-item-new add-cate" @click="showCustomCate = true">
|
||
<view class="cate-icon-wrap add-icon-wrap">
|
||
<text class="cate-icon">+</text>
|
||
</view>
|
||
<text class="cate-label" :style="{ color: themeConfig.textSecondary }">自定义</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">支付渠道</text>
|
||
<view class="channel-grid">
|
||
<text
|
||
class="channel-btn"
|
||
v-for="channel in currentChannels"
|
||
:key="channel.value"
|
||
:class="{ active: form.channel === channel.value }"
|
||
@click="form.channel = channel.value"
|
||
:style="{
|
||
backgroundColor: form.channel === channel.value ? themeConfig.primaryColor : 'transparent',
|
||
color: form.channel === channel.value ? '#fff' : themeConfig.textSecondary,
|
||
borderColor: form.channel === channel.value ? themeConfig.primaryColor : themeConfig.borderColor
|
||
}"
|
||
>{{ channel.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">备注</text>
|
||
<view class="note-input-wrap">
|
||
<input
|
||
v-model="form.note"
|
||
type="text"
|
||
placeholder="添加备注(选填)"
|
||
class="note-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary }"
|
||
/>
|
||
</view>
|
||
<view class="quick-tags">
|
||
<text
|
||
class="quick-tag"
|
||
v-for="tag in QUICK_NOTE_TAGS"
|
||
:key="tag"
|
||
@click="form.note = tag"
|
||
:style="{ backgroundColor: themeConfig.borderColor, color: themeConfig.textSecondary }"
|
||
>#{{ tag }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">日期</text>
|
||
<picker mode="date" :value="form.date" @change="e => form.date = e.detail.value">
|
||
<view class="date-picker-wrap" :style="{ backgroundColor: themeConfig.bgColor }">
|
||
<text class="date-text" :style="{ color: themeConfig.textPrimary }">
|
||
{{ form.date === formatCurrentDate() ? '今天' : formatDate(form.date) }}
|
||
</text>
|
||
<FaIcon icon="chevron-right" :size="14" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="section" v-if="showAiSuggestion">
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">AI 智能建议</text>
|
||
<view
|
||
class="ai-suggestion"
|
||
:style="{ borderColor: themeConfig.primaryColor }"
|
||
@click="applyAiSuggestion"
|
||
>
|
||
<text class="ai-suggestion-text" :style="{ color: themeConfig.primaryColor }">{{ aiSuggestionText }}</text>
|
||
<text class="ai-suggestion-hint" :style="{ color: themeConfig.textSecondary }">点击应用</text>
|
||
</view>
|
||
</view>
|
||
|
||
<template #footer>
|
||
<view class="modal-footer">
|
||
<button class="btn-cancel" :style="{ color: themeConfig.textSecondary }" @click="closeModal">取消</button>
|
||
<button
|
||
class="btn-confirm"
|
||
:style="{ background: themeConfig.gradientSoft }"
|
||
@click="submitBill"
|
||
:loading="submitting"
|
||
>{{ isEdit ? '保存修改' : '确认账单' }}</button>
|
||
</view>
|
||
</template>
|
||
</BottomSheet>
|
||
|
||
<view class="modal modal-center" v-if="showCustomCate" @click="showCustomCate = false">
|
||
<view
|
||
class="custom-cate-box theme-transition"
|
||
:style="{ backgroundColor: themeConfig.cardBgColor }"
|
||
@click.stop
|
||
>
|
||
<text class="modal-title" :style="{ color: themeConfig.textPrimary }">自定义分类</text>
|
||
<input
|
||
v-model="customCateName"
|
||
placeholder="分类名称"
|
||
class="input"
|
||
maxlength="6"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
/>
|
||
<view class="color-select">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">选择颜色</text>
|
||
<view class="color-list">
|
||
<view
|
||
class="color-item"
|
||
v-for="color in colorOptions"
|
||
:key="color"
|
||
:style="{ backgroundColor: color }"
|
||
:class="{ active: selectedColor === color }"
|
||
@click="selectedColor = color"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
<input
|
||
v-model="customCateIcon"
|
||
placeholder="输入图标(如🍚)"
|
||
class="input"
|
||
maxlength="2"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
/>
|
||
<view class="btns">
|
||
<button
|
||
class="cancel-btn theme-transition"
|
||
:style="{ backgroundColor: themeConfig.borderColor, color: themeConfig.textSecondary }"
|
||
@click="showCustomCate = false"
|
||
>取消</button>
|
||
<button
|
||
class="confirm-btn theme-transition btn btn--primary"
|
||
@click="addCustomCate"
|
||
>确认添加</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<CenterModal
|
||
:visible="showDeleteConfirm"
|
||
title="确认删除"
|
||
:showCancel="true"
|
||
cancelText="取消"
|
||
confirmText="删除"
|
||
:confirmColor="themeConfig.dangerColor"
|
||
@confirm="confirmDeleteBill"
|
||
@cancel="showDeleteConfirm = false"
|
||
@update:visible="val => showDeleteConfirm = val"
|
||
>
|
||
<text class="delete-confirm-text" :style="{ color: themeConfig.textPrimary }">
|
||
{{ deleteConfirmMessage }}
|
||
</text>
|
||
</CenterModal>
|
||
|
||
<CenterModal
|
||
:visible="showBatchDeleteConfirm"
|
||
title="批量删除确认"
|
||
:showCancel="true"
|
||
cancelText="取消"
|
||
confirmText="删除"
|
||
:confirmColor="themeConfig.dangerColor"
|
||
@confirm="confirmBatchDelete"
|
||
@cancel="showBatchDeleteConfirm = false"
|
||
>
|
||
<text class="delete-confirm-text" :style="{ color: themeConfig.textPrimary }">
|
||
确定要删除选中的 {{ selectedBillIds.length }} 条账单吗?
|
||
</text>
|
||
</CenterModal>
|
||
|
||
<GrowthModal
|
||
:visible="growthModalVisible"
|
||
:title="growthModalTitle"
|
||
:message="growthModalMessage"
|
||
:icon="growthModalIcon"
|
||
:confirm-text="growthModalConfirmText"
|
||
@close="growthModalVisible = false"
|
||
/>
|
||
|
||
</template>
|
||
|
||
<script>
|
||
import { getBillList, addBill, editBill, deleteBill, getMonthBudget, setMonthBudget } from '@/api'
|
||
import { aiClassify as classifyBillApi } from '@/api/index.js'
|
||
import { checkLogin } from '@/utils/auth'
|
||
import { refreshNavBarTheme, forceApplyTabBarTheme } from '@/utils/themeGlobal'
|
||
import { getThemeConfigFromStorage, THEME_PRESETS } from '@/utils/theme'
|
||
|
||
import PageHeader from '@/components/PageHeader.vue'
|
||
import CapsuleFilter from '@/components/CapsuleFilter.vue'
|
||
import AppCard from '@/components/AppCard.vue'
|
||
import BottomSheet from '@/components/BottomSheet.vue'
|
||
import CenterModal from '@/components/CenterModal.vue'
|
||
import EmptyState from '@/components/EmptyState.vue'
|
||
import FaIcon from '@/components/FaIcon.vue'
|
||
import GrowthModal from '@/components/GrowthModal.vue'
|
||
import SwipeableItem from '@/components/SwipeableItem.vue'
|
||
import SkeletonScreen from '@/components/SkeletonScreen.vue'
|
||
|
||
import themeMixin from '@/mixins/themeMixin.js'
|
||
import growthModalMixin from '@/mixins/growthModalMixin.js'
|
||
|
||
import {
|
||
DEFAULT_CATE_LIST,
|
||
BILL_TYPE,
|
||
COLOR_OPTIONS,
|
||
SLIDE_BTN_WIDTH,
|
||
BILL_FILTER_TYPE,
|
||
TIME_TAG,
|
||
QUICK_MONEY_AMOUNTS,
|
||
QUICK_NOTE_TAGS,
|
||
BUDGET_PROGRESS_COLOR,
|
||
PROGRESS_COLORS,
|
||
RECENT_CATE_MAX_COUNT,
|
||
SLIDE_THRESHOLD,
|
||
BILL_TYPE_FILTER_OPTIONS,
|
||
BILL_TIME_FILTER_OPTIONS,
|
||
PAY_CHANNELS,
|
||
getChannelsByType,
|
||
getChannelLabel,
|
||
getCateLabel
|
||
} from '@/utils/constants'
|
||
|
||
const expenseCategories = DEFAULT_CATE_LIST.filter(c => c.type === BILL_TYPE.EXPEND)
|
||
const incomeCategories = DEFAULT_CATE_LIST.filter(c => c.type === BILL_TYPE.INCOME)
|
||
|
||
const TYPE_TO_CATEGORY = {
|
||
[BILL_TYPE.EXPEND]: expenseCategories,
|
||
[BILL_TYPE.INCOME]: incomeCategories
|
||
}
|
||
|
||
const CATEGORY_TO_TYPES = {}
|
||
DEFAULT_CATE_LIST.forEach(c => {
|
||
CATEGORY_TO_TYPES[c.value] = c.type
|
||
})
|
||
|
||
export default {
|
||
components: {
|
||
PageHeader,
|
||
CapsuleFilter,
|
||
AppCard,
|
||
BottomSheet,
|
||
CenterModal,
|
||
EmptyState,
|
||
FaIcon,
|
||
GrowthModal,
|
||
SwipeableItem,
|
||
SkeletonScreen
|
||
},
|
||
mixins: [themeMixin, growthModalMixin],
|
||
data() {
|
||
return {
|
||
BILL_TYPE_FILTER_OPTIONS,
|
||
BILL_TIME_FILTER_OPTIONS,
|
||
QUICK_MONEY_AMOUNTS,
|
||
QUICK_NOTE_TAGS,
|
||
BILL_FILTER_TYPE,
|
||
TIME_TAG,
|
||
BILL_TYPE,
|
||
expenseCategories,
|
||
incomeCategories,
|
||
TYPE_TO_CATEGORY,
|
||
CATEGORY_TO_TYPES,
|
||
|
||
searchWord: '',
|
||
billType: BILL_FILTER_TYPE.ALL,
|
||
activeTimeTag: TIME_TAG.TODAY,
|
||
billList: [],
|
||
billListLoaded: false,
|
||
displayBillList: [],
|
||
|
||
showAdd: false,
|
||
isEdit: false,
|
||
currentEditId: '',
|
||
form: { type: BILL_TYPE.EXPEND, money: '', cate: '', channel: 'wechat', note: '', date: '' },
|
||
formattedMoney: '',
|
||
moneyInputError: '',
|
||
|
||
allCateList: DEFAULT_CATE_LIST,
|
||
showCustomCate: false,
|
||
customCateName: '',
|
||
customCateIcon: '',
|
||
selectedColor: COLOR_OPTIONS[0],
|
||
colorOptions: COLOR_OPTIONS,
|
||
|
||
monthlyBudget: '',
|
||
monthExpend: 0,
|
||
budgetProgress: 0,
|
||
remainingBudget: 0,
|
||
totalExpend: 0,
|
||
totalIncome: 0,
|
||
totalBalance: 0,
|
||
currentPeriodBalance: 0,
|
||
monthlyTotalBalance: 0,
|
||
cateStatList: [],
|
||
currentCateIndex: 0,
|
||
|
||
searchTimer: null,
|
||
showBudgetModal: false,
|
||
tempBudget: '',
|
||
selectedBudgetMonth: '',
|
||
refreshing: false,
|
||
submitting: false,
|
||
recentCateList: [],
|
||
filterCate: '',
|
||
showScrollTip: false,
|
||
addBtnScale: 'scale(1)',
|
||
|
||
showCustomDateModal: false,
|
||
customStartDate: '',
|
||
customEndDate: '',
|
||
customDateRange: { start: '', end: '' },
|
||
|
||
lastMonthBudget: '',
|
||
lastMonthExpend: 0,
|
||
lastMonthBudgetProgress: 0,
|
||
lastMonthRemainingBudget: 0,
|
||
|
||
isBatchMode: false,
|
||
selectedBillIds: [],
|
||
|
||
showDeleteConfirm: false,
|
||
deleteConfirmMessage: '',
|
||
pendingDeleteId: null,
|
||
|
||
showBatchDeleteConfirm: false,
|
||
|
||
currentMonthFirst: '',
|
||
currentMonthLast: '',
|
||
|
||
page: 1,
|
||
pageSize: 20,
|
||
hasMore: true,
|
||
|
||
showAiSuggestion: false,
|
||
aiSuggestionText: '',
|
||
aiSuggestionData: null,
|
||
|
||
swipeActions: [
|
||
{ key: 'edit', label: '编辑' },
|
||
{ key: 'delete', label: '删除' }
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
currentChannels() {
|
||
return getChannelsByType(this.form.type)
|
||
},
|
||
cateList() {
|
||
return this.allCateList.filter(item => item.type === this.form.type)
|
||
},
|
||
filteredBillList() {
|
||
let list = this.getBillByTimeRange()
|
||
if (this.billType === BILL_FILTER_TYPE.EXPEND) {
|
||
list = list.filter(item => item.type === BILL_TYPE.EXPEND)
|
||
} else if (this.billType === BILL_FILTER_TYPE.INCOME) {
|
||
list = list.filter(item => item.type === BILL_TYPE.INCOME)
|
||
}
|
||
if (this.filterCate) list = list.filter(item => item.cate === this.filterCate)
|
||
if (this.searchWord) {
|
||
const keyword = this.searchWord.trim()
|
||
list = list.filter(item =>
|
||
(item.cate && item.cate.includes(keyword)) ||
|
||
(item.note && item.note.includes(keyword))
|
||
)
|
||
}
|
||
return list
|
||
},
|
||
groupedBillList() {
|
||
const groups = {}
|
||
this.displayBillList.forEach(item => {
|
||
const dateKey = item.date
|
||
if (!groups[dateKey]) {
|
||
groups[dateKey] = { date: dateKey, items: [], income: 0, expend: 0 }
|
||
}
|
||
groups[dateKey].items.push(item)
|
||
if (item.type === BILL_TYPE.INCOME) {
|
||
groups[dateKey].income += Number(item.money) || 0
|
||
} else if (item.type === BILL_TYPE.EXPEND) {
|
||
groups[dateKey].expend += Number(item.money) || 0
|
||
}
|
||
})
|
||
return groups
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.themeChangeHandler = (themeData) => {
|
||
try {
|
||
let newTheme = null
|
||
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
||
newTheme = THEME_PRESETS[themeData.key]
|
||
} else if (themeData?.config) {
|
||
newTheme = themeData.config
|
||
}
|
||
if (newTheme) {
|
||
this.themeConfig = newTheme
|
||
this.$forceUpdate()
|
||
}
|
||
} catch (e) {
|
||
console.error('主题更新失败:', e)
|
||
}
|
||
}
|
||
uni.$on('themeChanged', this.themeChangeHandler)
|
||
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
const paramsDate = options?.date || today
|
||
this.selectedDate = paramsDate
|
||
this.form.date = paramsDate
|
||
this.initCurrentMonthDate()
|
||
this.initBudgetMonth()
|
||
this.recentCateList = uni.getStorageSync('recentCateList') || []
|
||
checkLogin()
|
||
this.getList()
|
||
this.customStartDate = paramsDate
|
||
this.customEndDate = paramsDate
|
||
this.customDateRange = { start: paramsDate, end: paramsDate }
|
||
this.getMonthBudgetData()
|
||
},
|
||
onUnload() {
|
||
uni.$off('themeChanged', this.themeChangeHandler)
|
||
},
|
||
onShow() {
|
||
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
||
this.themeConfig = latestTheme
|
||
this.$forceUpdate()
|
||
}
|
||
refreshNavBarTheme()
|
||
setTimeout(() => {
|
||
forceApplyTabBarTheme(latestTheme)
|
||
}, 100)
|
||
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
this.form.date = today
|
||
this.selectedDate = today
|
||
this._onShowId = Date.now()
|
||
const currentId = this._onShowId
|
||
setTimeout(() => {
|
||
if (this._onShowId !== currentId) return
|
||
this.getMonthBudgetData()
|
||
}, 200)
|
||
},
|
||
onPullDownRefresh() {
|
||
this.refreshing = true
|
||
this.getList().finally(() => {
|
||
this.refreshing = false
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
watch: {
|
||
activeTimeTag(newVal) {
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.getList()
|
||
if (newVal === TIME_TAG.MONTH) this.getMonthBudgetData()
|
||
if (newVal === TIME_TAG.LAST_MONTH) this.getLastMonthBudgetData()
|
||
this.$nextTick(() => {
|
||
this.calcStatistics()
|
||
})
|
||
},
|
||
billList() {
|
||
this.calcStatistics()
|
||
}
|
||
},
|
||
methods: {
|
||
initCurrentMonthDate() {
|
||
const now = new Date()
|
||
const year = now.getFullYear()
|
||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||
const firstDay = '01'
|
||
const lastDay = String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2, '0')
|
||
this.currentMonthFirst = `${year}-${month}-${firstDay}`
|
||
this.currentMonthLast = `${year}-${month}-${lastDay}`
|
||
},
|
||
initBudgetMonth() {
|
||
const now = new Date()
|
||
const year = now.getFullYear()
|
||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||
this.selectedBudgetMonth = `${year}-${month}`
|
||
},
|
||
onBudgetMonthChange(e) {
|
||
this.selectedBudgetMonth = e.detail.value
|
||
},
|
||
formatDate(dateStr) {
|
||
return dateStr || ''
|
||
},
|
||
getChannelLabel(value) {
|
||
return getChannelLabel(value)
|
||
},
|
||
getCateLabel(value) {
|
||
return getCateLabel(value)
|
||
},
|
||
formatCurrentDate() {
|
||
const now = new Date()
|
||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
},
|
||
formatBillDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||
},
|
||
handleSearch() {
|
||
clearTimeout(this.searchTimer)
|
||
this.searchTimer = setTimeout(() => {}, 300)
|
||
},
|
||
clearSearch() {
|
||
this.searchWord = ''
|
||
this.$forceUpdate()
|
||
},
|
||
handleTypeFilterChange(val) {
|
||
this.billType = val
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.$nextTick(() => {
|
||
this.loadFirstPage()
|
||
})
|
||
},
|
||
handleTimeFilterChange(val) {
|
||
if (val === TIME_TAG.CUSTOM) {
|
||
this.openCustomDatePicker()
|
||
} else {
|
||
this.switchTimeTag(val)
|
||
}
|
||
},
|
||
toggleBatchMode() {
|
||
this.isBatchMode = !this.isBatchMode
|
||
if (!this.isBatchMode) {
|
||
this.selectedBillIds = []
|
||
}
|
||
},
|
||
handleSwipeAction(action, item) {
|
||
if (action === 'edit') {
|
||
this.handleEditBill(item)
|
||
} else if (action === 'delete') {
|
||
this.handleDeleteBill(item.id)
|
||
}
|
||
},
|
||
closeOtherSwipe(id) {
|
||
},
|
||
async getList() {
|
||
if (this._listLoading) return
|
||
this._listLoading = true
|
||
this.billListLoaded = false
|
||
try {
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
let start = '', end = ''
|
||
|
||
if (this.activeTimeTag === TIME_TAG.TODAY) {
|
||
start = end = today
|
||
} else if (this.activeTimeTag === TIME_TAG.WEEK) {
|
||
const dayOfWeek = now.getDay() || 7
|
||
const diffDays = dayOfWeek - 1
|
||
const weekStart = new Date(now)
|
||
weekStart.setDate(now.getDate() - diffDays)
|
||
start = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`
|
||
end = today
|
||
} else if (this.activeTimeTag === TIME_TAG.MONTH) {
|
||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
||
start = `${monthStart.getFullYear()}-${String(monthStart.getMonth() + 1).padStart(2, '0')}-${String(monthStart.getDate()).padStart(2, '0')}`
|
||
end = `${monthEnd.getFullYear()}-${String(monthEnd.getMonth() + 1).padStart(2, '0')}-${String(monthEnd.getDate()).padStart(2, '0')}`
|
||
} else if (this.activeTimeTag === TIME_TAG.LAST_MONTH) {
|
||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0)
|
||
start = `${lastMonth.getFullYear()}-${String(lastMonth.getMonth() + 1).padStart(2, '0')}-01`
|
||
end = `${lastMonthEnd.getFullYear()}-${String(lastMonthEnd.getMonth() + 1).padStart(2, '0')}-${String(lastMonthEnd.getDate()).padStart(2, '0')}`
|
||
} else if (this.activeTimeTag === TIME_TAG.THIS_YEAR) {
|
||
start = `${now.getFullYear()}-01-01`
|
||
end = today
|
||
} else if (this.activeTimeTag === TIME_TAG.CUSTOM) {
|
||
start = this.customDateRange.start || today
|
||
end = this.customDateRange.end || today
|
||
}
|
||
|
||
const res = await getBillList({ start_time: start, end_time: end })
|
||
this.billList = res.map(item => ({
|
||
id: item.ID,
|
||
type: item.type,
|
||
money: item.money,
|
||
cate: item.cate,
|
||
channel: item.channel,
|
||
note: item.note,
|
||
date: item.date,
|
||
from_account: item.from_account,
|
||
to_account: item.to_account
|
||
}))
|
||
this.page = 1
|
||
this.hasMore = this.billList.length > this.pageSize
|
||
this.loadFirstPage()
|
||
} catch (e) {
|
||
console.error('获取账单失败:', e)
|
||
this.billList = []
|
||
} finally {
|
||
this.billListLoaded = true
|
||
this.refreshing = false
|
||
this._listLoading = false
|
||
}
|
||
},
|
||
loadFirstPage() {
|
||
const firstPageData = this.filteredBillList.slice(0, this.pageSize)
|
||
this.displayBillList = firstPageData
|
||
},
|
||
loadMore() {
|
||
if (!this.hasMore) return
|
||
this.page++
|
||
const start = (this.page - 1) * this.pageSize
|
||
const end = this.page * this.pageSize
|
||
const nextPageData = this.filteredBillList.slice(start, end)
|
||
this.displayBillList = [...this.displayBillList, ...nextPageData]
|
||
const totalFiltered = this.filteredBillList.length
|
||
this.hasMore = this.displayBillList.length < totalFiltered
|
||
},
|
||
async getMonthBudgetData() {
|
||
try {
|
||
const res = await getMonthBudget()
|
||
this.monthlyBudget = res && res.amount ? Number(res.amount) : ''
|
||
this.calcStatistics()
|
||
} catch (e) {
|
||
console.error('获取本月预算失败:', e)
|
||
this.monthlyBudget = ''
|
||
uni.showToast({ title: '获取预算失败', icon: 'none' })
|
||
}
|
||
},
|
||
getProgressColor() {
|
||
if (this.budgetProgress >= BUDGET_PROGRESS_COLOR.OVER) {
|
||
return PROGRESS_COLORS.OVER
|
||
} else if (this.budgetProgress >= BUDGET_PROGRESS_COLOR.WARNING) {
|
||
return PROGRESS_COLORS.WARNING
|
||
} else {
|
||
return PROGRESS_COLORS.NORMAL
|
||
}
|
||
},
|
||
handleBudgetSetting() {
|
||
this.tempBudget = this.monthlyBudget ? this.monthlyBudget.toString() : ''
|
||
this.initBudgetMonth()
|
||
this.showBudgetModal = true
|
||
},
|
||
async saveBudget() {
|
||
if (!checkLogin()) return
|
||
const num = Number(this.tempBudget)
|
||
if (!this.tempBudget || isNaN(num) || num <= 0) {
|
||
uni.showToast({ title: '请输入有效的预算金额', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
await setMonthBudget({ amount: num, month: this.selectedBudgetMonth })
|
||
const currentMonth = new Date().getMonth() + 1
|
||
const budgetMonth = Number(this.selectedBudgetMonth.split('-')[1])
|
||
if (budgetMonth === currentMonth) {
|
||
this.monthlyBudget = num
|
||
} else {
|
||
this.lastMonthBudget = num
|
||
}
|
||
this.showBudgetModal = false
|
||
this.tempBudget = ''
|
||
this.calcStatistics()
|
||
uni.showToast({ title: '预算设置成功', icon: 'success' })
|
||
} catch (err) {
|
||
console.error('设置预算失败:', err)
|
||
uni.showToast({ title: '设置失败', icon: 'none' })
|
||
}
|
||
},
|
||
calcStatistics() {
|
||
this.totalExpend = 0
|
||
this.totalIncome = 0
|
||
this.monthExpend = 0
|
||
this.monthlyTotalBalance = 0
|
||
this.lastMonthExpend = 0
|
||
const filteredByTime = this.getBillByTimeRange()
|
||
const allMonthlyBills = this.getMonthlyBills()
|
||
filteredByTime.forEach(item => {
|
||
const money = Number(item.money) || 0
|
||
if (item.type === BILL_TYPE.EXPEND) this.totalExpend += money
|
||
else if (item.type === BILL_TYPE.INCOME) this.totalIncome += money
|
||
if (item.type === BILL_TYPE.EXPEND && this.isCurrentMonth(item.date)) this.monthExpend += money
|
||
if (item.type === BILL_TYPE.EXPEND) {
|
||
if (this.isCurrentMonth(item.date)) {
|
||
this.monthExpend += money
|
||
}
|
||
if (this.isLastMonth(item.date)) {
|
||
this.lastMonthExpend += money
|
||
}
|
||
}
|
||
})
|
||
allMonthlyBills.forEach(item => {
|
||
const money = Number(item.money) || 0
|
||
if (item.type === BILL_TYPE.EXPEND) this.monthlyTotalBalance -= money
|
||
else if (item.type === BILL_TYPE.INCOME) this.monthlyTotalBalance += money
|
||
})
|
||
this.currentPeriodBalance = Number((this.totalIncome - this.totalExpend).toFixed(2))
|
||
this.totalBalance = this.currentPeriodBalance
|
||
this.monthlyTotalBalance = Number(this.monthlyTotalBalance.toFixed(2))
|
||
this.budgetProgress = this.monthlyBudget ? (this.monthExpend / this.monthlyBudget) * 100 : 0
|
||
this.remainingBudget = this.monthlyBudget ? (this.monthlyBudget - this.monthExpend).toFixed(2) : 0
|
||
this.lastMonthBudgetProgress = this.lastMonthBudget ? (this.lastMonthExpend / this.lastMonthBudget) * 100 : 0
|
||
this.lastMonthRemainingBudget = this.lastMonthBudget ? (this.lastMonthBudget - this.lastMonthExpend).toFixed(2) : 0
|
||
this.calcCateStatistics(filteredByTime)
|
||
},
|
||
getMonthlyBills() {
|
||
const now = new Date()
|
||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0)
|
||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)
|
||
return this.billList.filter(item => {
|
||
if (!item.date) return false
|
||
const billDateStr = item.date.replace(/-/g, '/')
|
||
const billTime = new Date(billDateStr).getTime()
|
||
return billTime >= monthStart.getTime() && billTime <= monthEnd.getTime()
|
||
})
|
||
},
|
||
getBillByTimeRange() {
|
||
const now = new Date()
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
|
||
let startDate, endDate
|
||
|
||
if (this.activeTimeTag === TIME_TAG.TODAY) {
|
||
startDate = new Date(today)
|
||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
||
} else if (this.activeTimeTag === TIME_TAG.WEEK) {
|
||
const dayOfWeek = today.getDay() || 7
|
||
const diffDays = dayOfWeek - 1
|
||
const weekStart = new Date(today)
|
||
weekStart.setDate(today.getDate() - diffDays)
|
||
startDate = weekStart
|
||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
||
} else if (this.activeTimeTag === TIME_TAG.MONTH) {
|
||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0)
|
||
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)
|
||
startDate = monthStart
|
||
endDate = monthEnd
|
||
} else if (this.activeTimeTag === TIME_TAG.LAST_MONTH) {
|
||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0)
|
||
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59)
|
||
startDate = lastMonth
|
||
endDate = lastMonthEnd
|
||
} else if (this.activeTimeTag === TIME_TAG.THIS_YEAR) {
|
||
const yearStart = new Date(now.getFullYear(), 0, 1, 0, 0, 0)
|
||
startDate = yearStart
|
||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
||
} else if (this.activeTimeTag === TIME_TAG.CUSTOM) {
|
||
if (!this.customDateRange.start || !this.customDateRange.end) {
|
||
startDate = new Date(today)
|
||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
||
} else {
|
||
startDate = new Date(this.customDateRange.start)
|
||
endDate = new Date(this.customDateRange.end)
|
||
endDate.setHours(23, 59, 59)
|
||
}
|
||
} else {
|
||
startDate = new Date(today)
|
||
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
||
}
|
||
|
||
return this.billList.filter(item => {
|
||
if (!item.date) return false
|
||
const billTime = new Date(item.date).getTime()
|
||
return billTime >= startDate.getTime() && billTime <= endDate.getTime()
|
||
})
|
||
},
|
||
switchTimeTag(tag) {
|
||
this.activeTimeTag = tag
|
||
this.filterCate = ''
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.$nextTick(() => {
|
||
this.getList()
|
||
this.calcStatistics()
|
||
})
|
||
},
|
||
openCustomDatePicker() {
|
||
this.showCustomDateModal = true
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
this.customStartDate = today
|
||
this.customEndDate = today
|
||
},
|
||
confirmCustomDate() {
|
||
if (!this.customStartDate || !this.customEndDate) {
|
||
uni.showToast({ title: '请选择完整的时间范围', icon: 'none' })
|
||
return
|
||
}
|
||
if (new Date(this.customStartDate) > new Date(this.customEndDate)) {
|
||
uni.showToast({ title: '开始日期不能晚于结束日期', icon: 'none' })
|
||
return
|
||
}
|
||
this.customDateRange = {
|
||
start: this.customStartDate,
|
||
end: this.customEndDate
|
||
}
|
||
this.showCustomDateModal = false
|
||
this.activeTimeTag = TIME_TAG.CUSTOM
|
||
this.filterCate = ''
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.$nextTick(() => {
|
||
this.getList()
|
||
this.calcStatistics()
|
||
})
|
||
uni.showToast({ title: '时间范围已选择', icon: 'success' })
|
||
},
|
||
isCurrentMonth(dateStr) {
|
||
if (!dateStr) return false
|
||
const date = new Date(dateStr.replace(/-/g, '/'))
|
||
const now = new Date()
|
||
return date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear()
|
||
},
|
||
isLastMonth(dateStr) {
|
||
if (!dateStr) return false
|
||
const date = new Date(dateStr.replace(/-/g, '/'))
|
||
const now = new Date()
|
||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||
return date.getMonth() === lastMonth.getMonth() && date.getFullYear() === lastMonth.getFullYear()
|
||
},
|
||
calcCateStatistics(list) {
|
||
const cateMap = {}
|
||
const totalExpend = list.filter(item => item.type === BILL_TYPE.EXPEND).reduce((sum, item) => sum + Number(item.money), 0)
|
||
const totalIncome = list.filter(item => item.type === BILL_TYPE.INCOME).reduce((sum, item) => sum + Number(item.money), 0)
|
||
list.forEach(item => {
|
||
if (item.type === BILL_TYPE.EXPEND || item.type === BILL_TYPE.INCOME) {
|
||
const amount = Number(item.money)
|
||
const cate = item.cate
|
||
if (!cateMap[cate]) {
|
||
cateMap[cate] = {
|
||
cate: cate,
|
||
expend: 0,
|
||
income: 0,
|
||
expendRate: 0,
|
||
incomeRate: 0,
|
||
icon: this.getCateIcon(cate),
|
||
color: this.getCateColor(cate)
|
||
}
|
||
}
|
||
if (item.type === BILL_TYPE.EXPEND) cateMap[cate].expend += amount
|
||
else cateMap[cate].income += amount
|
||
}
|
||
})
|
||
Object.values(cateMap).forEach(item => {
|
||
item.expendRate = totalExpend > 0 ? ((item.expend / totalExpend) * 100).toFixed(1) : 0
|
||
item.incomeRate = totalIncome > 0 ? ((item.income / totalIncome) * 100).toFixed(1) : 0
|
||
})
|
||
this.cateStatList = Object.values(cateMap).sort((a, b) => {
|
||
const totalA = a.expend + a.income
|
||
const totalB = b.expend + b.income
|
||
return totalB - totalA
|
||
})
|
||
},
|
||
getCateIcon(cate) {
|
||
const findItem = this.allCateList.find(item => item.value === cate)
|
||
return findItem ? findItem.icon : '📦'
|
||
},
|
||
getCateColor(cate) {
|
||
const findItem = this.allCateList.find(item => item.value === cate)
|
||
return findItem ? findItem.color : '#84b3d9'
|
||
},
|
||
openAddModal() {
|
||
this.isEdit = false
|
||
this.currentEditId = ''
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
this.form = { type: BILL_TYPE.EXPEND, money: '', cate: '', channel: 'wechat', note: '', date: today }
|
||
this.formattedMoney = ''
|
||
this.moneyInputError = ''
|
||
this.showAdd = true
|
||
this.showAiSuggestion = false
|
||
this.aiSuggestionText = ''
|
||
this.aiSuggestionData = null
|
||
},
|
||
handleEditBill(item) {
|
||
this.isEdit = true
|
||
this.currentEditId = item.id
|
||
this.form = {
|
||
type: item.type,
|
||
money: item.money.toString(),
|
||
cate: item.cate,
|
||
channel: item.channel || 'wechat',
|
||
note: item.note || '',
|
||
date: item.date
|
||
}
|
||
this.formatMoneyOnBlur()
|
||
this.showAdd = true
|
||
this.showAiSuggestion = false
|
||
this.aiSuggestionText = ''
|
||
this.aiSuggestionData = null
|
||
},
|
||
handleDeleteBill(id) {
|
||
this.pendingDeleteId = id
|
||
this.deleteConfirmMessage = '确定要删除这条账单吗?删除后不可恢复。'
|
||
this.showDeleteConfirm = true
|
||
},
|
||
async confirmDeleteBill() {
|
||
try {
|
||
await deleteBill({ id: this.pendingDeleteId })
|
||
uni.showToast({ title: '删除成功' })
|
||
this.showDeleteConfirm = false
|
||
this.pendingDeleteId = null
|
||
this.getList()
|
||
} catch (e) {
|
||
console.error('删除账单失败:', e)
|
||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||
}
|
||
},
|
||
batchEdit() {
|
||
uni.showToast({ title: '批量编辑功能开发中', icon: 'none' })
|
||
},
|
||
batchDelete() {
|
||
if (this.selectedBillIds.length === 0) {
|
||
uni.showToast({ title: '请先选择账单', icon: 'none' })
|
||
return
|
||
}
|
||
this.showBatchDeleteConfirm = true
|
||
},
|
||
async confirmBatchDelete() {
|
||
try {
|
||
const promises = this.selectedBillIds.map(id => deleteBill({ id }))
|
||
await Promise.all(promises)
|
||
uni.showToast({ title: '批量删除成功' })
|
||
this.showBatchDeleteConfirm = false
|
||
this.selectedBillIds = []
|
||
this.isBatchMode = false
|
||
this.getList()
|
||
} catch (e) {
|
||
console.error('批量删除失败:', e)
|
||
uni.showToast({ title: '批量删除失败', icon: 'none' })
|
||
}
|
||
},
|
||
handleSheetClose() {
|
||
this.showAdd = false
|
||
},
|
||
closeModal() {
|
||
this.showAdd = false
|
||
const now = new Date()
|
||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||
this.form = { type: BILL_TYPE.EXPEND, money: '', cate: '', channel: 'wechat', note: '', date: today }
|
||
this.formattedMoney = ''
|
||
this.moneyInputError = ''
|
||
this.showAiSuggestion = false
|
||
this.aiSuggestionText = ''
|
||
this.aiSuggestionData = null
|
||
},
|
||
async submitBill() {
|
||
if (!checkLogin()) return
|
||
this.formatMoneyOnBlur()
|
||
if ((!this.form.money || Number(this.form.money) <= 0) || !this.form.cate) {
|
||
return uni.showToast({ title: '请填写有效的金额和分类', icon: 'none' })
|
||
}
|
||
this.submitting = true
|
||
try {
|
||
const billData = {
|
||
...this.form,
|
||
money: parseFloat(this.form.money) || 0,
|
||
from_account: '',
|
||
to_account: ''
|
||
}
|
||
if (this.isEdit) {
|
||
await editBill({ id: this.currentEditId, ...billData })
|
||
uni.showToast({ title: '修改成功' })
|
||
} else {
|
||
const res = await addBill(billData)
|
||
uni.showToast({ title: '账单成功' })
|
||
if (res && res.growth) {
|
||
this.showGrowthModal(res.growth)
|
||
}
|
||
}
|
||
this.closeModal()
|
||
this.getList()
|
||
} catch (e) {
|
||
console.error('提交账单失败:', e)
|
||
uni.showToast({ title: this.isEdit ? '修改失败' : '账单失败', icon: 'none' })
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
},
|
||
async classifyBill(content) {
|
||
try {
|
||
const res = await classifyBillApi({ content })
|
||
if (res && res.type === 'bill' && res.data) {
|
||
this.aiSuggestionData = res.data
|
||
const parts = []
|
||
if (res.data.cate) parts.push(`分类:${getCateLabel(res.data.cate)}`)
|
||
if (res.data.type) parts.push(`类型:${res.data.type === 1 ? '支出' : '收入'}`)
|
||
if (res.data.channel) parts.push(`渠道:${getChannelLabel(res.data.channel)}`)
|
||
this.aiSuggestionText = parts.join(' · ')
|
||
this.showAiSuggestion = true
|
||
}
|
||
} catch (e) {
|
||
console.log('AI分类未返回有效结果,跳过建议')
|
||
}
|
||
},
|
||
applyAiSuggestion() {
|
||
if (!this.aiSuggestionData) return
|
||
const data = this.aiSuggestionData
|
||
if (data.cate) this.form.cate = data.cate
|
||
if (data.type) this.form.type = data.type
|
||
if (data.channel) this.form.channel = data.channel
|
||
this.showAiSuggestion = false
|
||
this.aiSuggestionData = null
|
||
uni.showToast({ title: '已应用AI建议', icon: 'success' })
|
||
},
|
||
addCustomCate() {
|
||
if (!this.customCateName.trim()) {
|
||
return uni.showToast({ title: '请输入分类名称', icon: 'none' })
|
||
}
|
||
const isExist = this.allCateList.some(item => item.value === this.customCateName)
|
||
if (isExist) {
|
||
return uni.showToast({ title: '分类已存在', icon: 'none' })
|
||
}
|
||
const newCate = {
|
||
value: this.customCateName,
|
||
label: this.customCateName,
|
||
icon: this.customCateIcon || '📦',
|
||
color: this.selectedColor,
|
||
type: this.form.type
|
||
}
|
||
this.allCateList.push(newCate)
|
||
this.form.cate = this.customCateName
|
||
this.customCateName = ''
|
||
this.customCateIcon = ''
|
||
this.showCustomCate = false
|
||
uni.showToast({ title: '分类添加成功' })
|
||
},
|
||
selectCate(cate) {
|
||
this.form.cate = cate.value
|
||
let recent = [...this.recentCateList]
|
||
recent = recent.filter(item => item.value !== cate.value)
|
||
recent.unshift(cate)
|
||
this.recentCateList = recent.slice(0, RECENT_CATE_MAX_COUNT)
|
||
uni.setStorageSync('recentCateList', this.recentCateList)
|
||
},
|
||
handleMoneyInput(e) {
|
||
const value = e.detail.value
|
||
this.validateMoneyInput(value, 'form.money')
|
||
},
|
||
validateMoneyInput(value, target) {
|
||
this.moneyInputError = ''
|
||
let cleanValue = value === null || value === undefined ? '' : String(value)
|
||
cleanValue = cleanValue.replace(/[^\d.]/g, '')
|
||
if (cleanValue.indexOf('.') !== -1) {
|
||
const parts = cleanValue.split('.')
|
||
if (parts.length > 2) {
|
||
this.moneyInputError = '请输入有效金额,最多两位小数'
|
||
cleanValue = parts[0] + '.' + parts[1].substring(0, 2)
|
||
} else if (parts[1] && parts[1].length > 2) {
|
||
this.moneyInputError = '请输入有效金额,最多两位小数'
|
||
cleanValue = parts[0] + '.' + parts[1].substring(0, 2)
|
||
}
|
||
}
|
||
if (target === 'form.money') {
|
||
this.form.money = cleanValue
|
||
this.formattedMoney = cleanValue
|
||
} else if (target === 'tempBudget') {
|
||
this.tempBudget = cleanValue
|
||
}
|
||
},
|
||
formatMoneyOnBlur() {
|
||
if (!this.form.money) {
|
||
this.formattedMoney = ''
|
||
return
|
||
}
|
||
let num = String(this.form.money).replace(/[^\d.]/g, '')
|
||
if (num === '.') num = '0.00'
|
||
else if (num.indexOf('.') === -1) num = `${num}.00`
|
||
else {
|
||
const parts = num.split('.')
|
||
parts[1] = parts[1].padEnd(2, '0').substring(0, 2)
|
||
num = `${parts[0]}.${parts[1]}`
|
||
}
|
||
this.formattedMoney = num
|
||
this.form.money = num
|
||
},
|
||
fillQuickMoney(amount) {
|
||
this.form.money = amount.toString()
|
||
this.formattedMoney = `${amount}.00`
|
||
this.moneyInputError = ''
|
||
},
|
||
filterByCate(cate) {
|
||
this.filterCate = this.filterCate === cate ? '' : cate
|
||
uni.showToast({
|
||
title: this.filterCate ? `已筛选${cate}账单` : '已取消分类筛选',
|
||
icon: 'none'
|
||
})
|
||
const index = this.cateStatList.findIndex(item => item.cate === cate)
|
||
if (index !== -1) {
|
||
this.currentCateIndex = index
|
||
}
|
||
this.page = 1
|
||
this.hasMore = true
|
||
this.$nextTick(() => {
|
||
this.loadFirstPage()
|
||
})
|
||
},
|
||
onCateScroll(e) {
|
||
const scrollLeft = e.detail.scrollLeft
|
||
const itemWidth = 216
|
||
this.currentCateIndex = Math.round(scrollLeft / itemWidth)
|
||
},
|
||
handleScroll(e) {
|
||
const { scrollTop, scrollHeight, windowHeight } = e.detail
|
||
this.showScrollTip = scrollTop + windowHeight < scrollHeight - 100
|
||
},
|
||
handleAddBtnClick() {
|
||
this.addBtnScale = 'scale(1.1)'
|
||
setTimeout(() => {
|
||
this.addBtnScale = 'scale(1)'
|
||
this.openAddModal()
|
||
}, 150)
|
||
},
|
||
getLastMonthFormat() {
|
||
const now = new Date()
|
||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||
const year = lastMonth.getFullYear()
|
||
const month = String(lastMonth.getMonth() + 1).padStart(2, '0')
|
||
return `${year}-${month}`
|
||
},
|
||
async getLastMonthBudgetData() {
|
||
try {
|
||
const lastMonth = this.getLastMonthFormat()
|
||
const res = await getMonthBudget({ month: lastMonth })
|
||
this.lastMonthBudget = res && res.amount ? Number(res.amount) : ''
|
||
this.calcStatistics()
|
||
} catch (e) {
|
||
console.error('获取上月预算失败:', e)
|
||
this.lastMonthBudget = ''
|
||
uni.showToast({ title: '获取上月预算失败', icon: 'none' })
|
||
}
|
||
},
|
||
handleLastMonthBudgetSetting() {
|
||
this.tempBudget = this.lastMonthBudget ? this.lastMonthBudget.toString() : ''
|
||
this.selectedBudgetMonth = this.getLastMonthFormat()
|
||
this.showBudgetModal = true
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.bill-page {
|
||
height: 100vh;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sticky-header {
|
||
flex-shrink: 0;
|
||
z-index: 10;
|
||
padding: 0 32rpx;
|
||
padding-top: 12rpx;
|
||
padding-bottom: 16rpx;
|
||
}
|
||
|
||
.main-scroll {
|
||
flex: 1;
|
||
height: 0;
|
||
}
|
||
|
||
.search-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.search-input {
|
||
display: flex;
|
||
align-items: center;
|
||
border-radius: 12rpx;
|
||
padding: 0 20rpx;
|
||
height: 70rpx;
|
||
}
|
||
|
||
.search-input input {
|
||
flex: 1;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.search-input .clear-icon {
|
||
font-size: 24rpx;
|
||
padding: 8rpx;
|
||
border-radius: 50%;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.filter-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.filter-row-label {
|
||
font-size: 24rpx;
|
||
flex-shrink: 0;
|
||
min-width: 60rpx;
|
||
}
|
||
|
||
.budget-card {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.budget-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.budget-title text {
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.budget-value {
|
||
font-weight: 600;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.budget-set-btn {
|
||
font-size: 26rpx;
|
||
padding: 8rpx 20rpx;
|
||
border: 1rpx solid;
|
||
border-radius: 10rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.budget-empty {
|
||
text-align: center;
|
||
padding: 30rpx 0;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 12rpx;
|
||
border-radius: 6rpx;
|
||
overflow: hidden;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
border-radius: 6rpx;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.budget-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.budget-info text {
|
||
flex: 1;
|
||
}
|
||
|
||
.budget-info text:nth-child(2) {
|
||
text-align: center;
|
||
}
|
||
|
||
.budget-info text:last-child {
|
||
text-align: right;
|
||
}
|
||
|
||
.stat-scroll {
|
||
width: 100%;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.stat-item {
|
||
flex-shrink: 0;
|
||
width: 200rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
padding: 28rpx 12rpx;
|
||
border-radius: 18rpx;
|
||
background: rgba(0, 0, 0, 0.03);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
border-radius: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.expend-icon {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
}
|
||
|
||
.income-icon {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
}
|
||
|
||
.balance-icon {
|
||
background: rgba(59, 130, 246, 0.15);
|
||
}
|
||
|
||
.month-icon {
|
||
background: rgba(139, 92, 246, 0.15);
|
||
}
|
||
|
||
.stat-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.stat-info .label {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.stat-info .value {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.cate-stat-section {
|
||
padding: 0 32rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.cate-stat-single {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.cate-stat-item-wide {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 140rpx;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cate-stat-item-wide:active {
|
||
transform: scale(0.99);
|
||
}
|
||
|
||
.cate-left-wide {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-right: 20rpx;
|
||
min-width: 80rpx;
|
||
}
|
||
|
||
.cate-icon-wide {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
line-height: 64rpx;
|
||
border-radius: 20rpx;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.cate-name-wide {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
margin-top: 8rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.cate-right-wide {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.cate-money-row-wide {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.cate-money {
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.cate-rate-row-wide {
|
||
padding-top: 10rpx;
|
||
border-top: 1rpx solid #f0f5fa;
|
||
margin-top: 6rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.cate-rate {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.cate-stat-scroll-wrapper {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.cate-stat-scroll {
|
||
white-space: nowrap;
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.cate-stat-container-scroll {
|
||
display: inline-flex;
|
||
gap: 16rpx;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.cate-stat-item-scroll {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 200rpx;
|
||
min-height: 160rpx;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 16rpx;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
transition: all 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cate-icon-scroll {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
border-radius: 18rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.cate-name-scroll {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.cate-stat-row-scroll {
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.cate-stat-text {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.cate-rate-scroll {
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.scroll-indicator {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.indicator-dot {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-radius: 50%;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.indicator-dot.active {
|
||
width: 24rpx;
|
||
border-radius: 10rpx;
|
||
}
|
||
|
||
.bill-list-container {
|
||
width: 100%;
|
||
margin-bottom: 160rpx;
|
||
}
|
||
|
||
.skeleton-container {
|
||
padding: 16rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.skeleton-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 140rpx;
|
||
margin: 12rpx 0;
|
||
padding: 0 24rpx;
|
||
border-radius: 20rpx;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.skeleton-icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 20rpx;
|
||
background: #e8e8e8;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.skeleton-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.skeleton-line {
|
||
height: 30rpx;
|
||
background: #e8e8e8;
|
||
border-radius: 6rpx;
|
||
margin-bottom: 8rpx;
|
||
width: 60%;
|
||
}
|
||
|
||
.skeleton-line.short {
|
||
width: 30%;
|
||
height: 24rpx;
|
||
}
|
||
|
||
.skeleton-money {
|
||
width: 120rpx;
|
||
height: 36rpx;
|
||
background: #e8e8e8;
|
||
border-radius: 6rpx;
|
||
}
|
||
|
||
.bill-list-card {
|
||
min-height: 500rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.bill-group {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.bill-group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12rpx 8rpx;
|
||
border-bottom: 1rpx solid #f0f5fa;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.bill-group-date {
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.bill-group-summary {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.bill-item-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16rpx 24rpx;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.bill-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.bill-left .cate-icon {
|
||
margin-right: 20rpx;
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
border-radius: 16rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.bill-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.bill-text .cate-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.bill-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.bill-text .note {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.money {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
margin-left: 20rpx;
|
||
padding: 4rpx 8rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.scroll-tip {
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.pagination-tip {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
padding: 20rpx 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.batch-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16rpx;
|
||
margin-top: 16rpx;
|
||
border-radius: 12rpx;
|
||
background: rgba(0, 0, 0, 0.03);
|
||
}
|
||
|
||
.batch-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.batch-btn {
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 12rpx;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.header-btn {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.25s ease;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.header-btn:active {
|
||
transform: scale(0.93);
|
||
}
|
||
|
||
.batch-icon {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.float-buttons {
|
||
position: fixed;
|
||
bottom: calc(40rpx + constant(safe-area-inset-bottom));
|
||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
right: 30rpx;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.add-btn {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 6px 24rpx rgba(0, 0, 0, 0.15);
|
||
position: relative;
|
||
}
|
||
|
||
.add-btn-icon {
|
||
font-size: 48rpx;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
}
|
||
|
||
.modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.modal-center {
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-box {
|
||
width: 100%;
|
||
padding: 30rpx;
|
||
padding-bottom: calc(30rpx + constant(safe-area-inset-bottom));
|
||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||
border-radius: 20rpx 20rpx 0 0;
|
||
}
|
||
|
||
.date-picker-box {
|
||
}
|
||
|
||
.modal-title {
|
||
text-align: center;
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 28rpx;
|
||
}
|
||
|
||
.form-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 28rpx;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.field-value {
|
||
flex: 1;
|
||
}
|
||
|
||
.date-picker {
|
||
padding: 20rpx;
|
||
border: 1rpx solid;
|
||
border-radius: 12rpx;
|
||
font-size: 26rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.input {
|
||
border: 1rpx solid #f0f5fa;
|
||
padding: 20rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.money-input {
|
||
font-size: 34rpx;
|
||
text-align: left;
|
||
}
|
||
|
||
.input-placeholder {
|
||
color: #9AA4B2;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.btns {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 20rpx;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.btns button {
|
||
flex: 1;
|
||
padding: 20rpx 30rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cancel-btn {
|
||
border: none;
|
||
}
|
||
|
||
.confirm-btn {
|
||
border: none;
|
||
color: #fff;
|
||
}
|
||
|
||
.confirm-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.budget-modal-box {
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
}
|
||
|
||
.custom-cate-box {
|
||
width: 85%;
|
||
max-width: 650rpx;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.color-select {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.color-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.color-item {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.color-item.active {
|
||
border: 2rpx solid #2A3A4D;
|
||
}
|
||
|
||
.delete-confirm-text {
|
||
font-size: 28rpx;
|
||
line-height: 1.6;
|
||
text-align: center;
|
||
}
|
||
|
||
.type-section {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.type-btn {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
padding: 14rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
transition: all 0.25s ease;
|
||
min-height: 64rpx;
|
||
}
|
||
|
||
.type-btn.active {
|
||
transform: scale(1.02);
|
||
box-shadow: 0 4rpx 12rpx rgba(132, 179, 217, 0.3);
|
||
}
|
||
|
||
.type-icon {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.type-text {
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.amount-section {
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.amount-label {
|
||
font-size: 26rpx;
|
||
margin-bottom: 16rpx;
|
||
display: block;
|
||
}
|
||
|
||
.amount-input-wrap {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 24rpx;
|
||
background: rgba(132, 179, 217, 0.08);
|
||
border-radius: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.amount-symbol {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
line-height: 1.3;
|
||
margin-right: 6rpx;
|
||
}
|
||
|
||
.amount-input {
|
||
font-size: 44rpx;
|
||
font-weight: 700;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
flex: 1;
|
||
}
|
||
|
||
.quick-amounts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.quick-amount-btn {
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 24rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.quick-amount-btn:active {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.section {
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 20rpx;
|
||
display: block;
|
||
}
|
||
|
||
.recent-cate-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.recent-label {
|
||
font-size: 24rpx;
|
||
padding: 6rpx 16rpx;
|
||
background: rgba(132, 179, 217, 0.1);
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.recent-cate-items {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
flex-wrap: wrap;
|
||
flex: 1;
|
||
}
|
||
|
||
.recent-cate-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 8rpx 16rpx;
|
||
background: rgba(132, 179, 217, 0.1);
|
||
border-radius: 12rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.recent-cate-item.active {
|
||
background: #84b3d9;
|
||
}
|
||
|
||
.recent-cate-icon {
|
||
font-size: 24rpx;
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.recent-cate-name {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.cate-grid-new {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8rpx;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.cate-item-new {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
width: 80rpx;
|
||
padding: 8rpx 4rpx;
|
||
border-radius: 10rpx;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cate-item-new:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.cate-item-new.active {
|
||
background: rgba(132, 179, 217, 0.12);
|
||
}
|
||
|
||
.cate-icon-wrap {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.cate-icon {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.add-icon-wrap {
|
||
border: 2rpx dashed #84b3d9;
|
||
background: transparent;
|
||
}
|
||
|
||
.add-icon-wrap .cate-icon {
|
||
color: #84b3d9;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.cate-label {
|
||
font-size: 18rpx;
|
||
}
|
||
|
||
.channel-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.channel-btn {
|
||
padding: 10rpx 22rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 24rpx;
|
||
border: 2rpx solid;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.channel-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.note-input-wrap {
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.note-input {
|
||
width: 100%;
|
||
padding: 12rpx 16rpx;
|
||
border-radius: 10rpx;
|
||
font-size: 24rpx;
|
||
box-sizing: border-box;
|
||
min-height: 56rpx;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.quick-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.quick-tag {
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 18rpx;
|
||
}
|
||
|
||
.date-picker-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.date-text {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.btn-cancel {
|
||
flex: 1;
|
||
height: 84rpx;
|
||
border-radius: 14rpx;
|
||
border: 2rpx solid transparent;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.btn-confirm {
|
||
flex: 2;
|
||
height: 84rpx;
|
||
border-radius: 14rpx;
|
||
border: none;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.btn-confirm:active {
|
||
transform: scale(0.98);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.btn-confirm[disabled] {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.ai-suggestion {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
border: 2rpx dashed;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.ai-suggestion-text {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.ai-suggestion-hint {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.theme-transition {
|
||
transition: background-color 0.3s ease, color 0.3s ease,
|
||
border-color 0.3s ease, box-shadow 0.3s ease,
|
||
opacity 0.25s ease, transform 0.25s ease;
|
||
}
|
||
|
||
@keyframes pageFadeIn {
|
||
0% { opacity: 0; transform: translateY(20rpx); }
|
||
100% { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
@keyframes cardEnter {
|
||
0% { opacity: 0; transform: translateY(16rpx); }
|
||
100% { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.content-fade {
|
||
padding: 0 32rpx;
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
</style> |