Files
ToDoList/WebUI/src/views/AssetPage.vue
祀梦 2979197b1c release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
2026-03-14 22:21:26 +08:00

1135 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAccountStore } from '@/stores/useAccountStore'
import type { FinancialAccount, DebtInstallment } from '@/api/types'
import { ElMessage, ElMessageBox } from 'element-plus'
import AccountDialog from '@/components/AccountDialog.vue'
import BalanceDialog from '@/components/BalanceDialog.vue'
import AccountHistoryDialog from '@/components/AccountHistoryDialog.vue'
import InstallmentDialog from '@/components/InstallmentDialog.vue'
const store = useAccountStore()
// ============ 弹窗控制 ============
const showAccountDialog = ref(false)
const showBalanceDialog = ref(false)
const showHistoryDialog = ref(false)
const showInstallmentDialog = ref(false)
const editingAccount = ref<FinancialAccount | null>(null)
const balanceAccount = ref<FinancialAccount | null>(null)
const historyAccount = ref<FinancialAccount | null>(null)
const editingInstallment = ref<DebtInstallment | null>(null)
const installmentAccountId = ref<number | null>(null)
onMounted(async () => {
await store.init()
})
// ============ 格式化工具 ============
function formatMoney(amount: number): string {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function formatDate(dateStr: string | null): string {
if (!dateStr) return ''
const d = new Date(dateStr)
return `${d.getMonth() + 1}${d.getDate()}`
}
function getPaymentCountdownText(days: number): string {
if (days === 0) return '今天还款'
if (days > 0) return `还有 ${days}`
return `已逾期 ${Math.abs(days)}`
}
function getPaymentCountdownType(days: number): 'today' | 'urgent' | 'soon' | 'safe' | 'overdue' {
if (days === 0) return 'today'
if (days < 0) return 'overdue'
if (days <= 3) return 'urgent'
if (days <= 7) return 'soon'
return 'safe'
}
// ============ 账户操作 ============
function openCreateAccount() {
editingAccount.value = null
showAccountDialog.value = true
}
function openEditAccount(account: FinancialAccount) {
editingAccount.value = account
showAccountDialog.value = true
}
function openUpdateBalance(account: FinancialAccount) {
balanceAccount.value = account
showBalanceDialog.value = true
}
function openHistory(account: FinancialAccount) {
historyAccount.value = account
showHistoryDialog.value = true
}
async function handleDeleteAccount(account: FinancialAccount) {
try {
await ElMessageBox.confirm(
`确定要删除「${account.name}」吗?相关的变更历史和分期计划也会被删除。`,
'确认删除',
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
)
const success = await store.deleteAccount(account.id)
if (success) ElMessage.success('账户删除成功~')
} catch {
// 用户取消
}
}
// ============ 分期计划操作 ============
function openCreateInstallment(accountId?: number) {
editingInstallment.value = null
installmentAccountId.value = accountId || null
showInstallmentDialog.value = true
}
function openEditInstallment(inst: DebtInstallment) {
editingInstallment.value = inst
installmentAccountId.value = null
showInstallmentDialog.value = true
}
async function handlePayInstallment(inst: DebtInstallment) {
try {
const accountName = inst.account_name || '该账户'
await ElMessageBox.confirm(
`确认标记「${accountName}」第 ${inst.current_period} 期已还款 (¥${inst.payment_amount.toFixed(2)})`,
'确认还款',
{ confirmButtonText: '确认已还', cancelButtonText: '取消', type: 'info' }
)
const result = await store.payInstallment(inst.id)
if (result) {
if (result.is_completed) {
ElMessage.success('恭喜!分期已全部还清~')
} else {
ElMessage.success(`${inst.current_period} 期已标记为已还~`)
}
}
} catch {
// 用户取消
}
}
async function handleDeleteInstallment(inst: DebtInstallment) {
try {
await ElMessageBox.confirm(
`确定要删除这个分期计划吗?`,
'确认删除',
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
)
const success = await store.deleteInstallment(inst.id)
if (success) ElMessage.success('分期计划删除成功~')
} catch {
// 用户取消
}
}
</script>
<template>
<div class="asset-page">
<div class="asset-container">
<!-- 概览统计 -->
<div class="overview-card">
<div class="overview-item">
<div class="overview-icon overview-icon--savings">
<el-icon :size="22"><Wallet /></el-icon>
</div>
<div class="overview-info">
<span class="overview-value">{{ formatMoney(store.totalSavings) }}</span>
<span class="overview-label">总资产</span>
</div>
</div>
<div class="overview-divider"></div>
<div class="overview-item">
<div class="overview-icon overview-icon--debt">
<el-icon :size="22"><CreditCard /></el-icon>
</div>
<div class="overview-info">
<span class="overview-value">{{ formatMoney(store.totalDebt) }}</span>
<span class="overview-label">总欠款</span>
</div>
</div>
<div class="overview-divider"></div>
<div class="overview-item">
<div class="overview-icon overview-icon--net">
<el-icon :size="22"><TrendCharts /></el-icon>
</div>
<div class="overview-info">
<span class="overview-value" :class="{ 'negative': store.netAssets < 0 }">
{{ formatMoney(store.netAssets) }}
</span>
<span class="overview-label">净资产</span>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="toolbar">
<div class="toolbar-top">
<div class="toolbar-left">
<span class="page-title">资产总览</span>
</div>
<div class="toolbar-right">
<button class="action-btn action-btn--primary" @click="openCreateAccount">
<el-icon :size="16"><Plus /></el-icon>
<span>新建账户</span>
</button>
</div>
</div>
</div>
<!-- 还款提醒区 -->
<div v-if="store.upcomingPayments.length > 0" class="remind-section">
<div class="remind-header">
<el-icon :size="16" color="#FF6B6B"><Bell /></el-icon>
<span>还款提醒</span>
</div>
<div class="remind-list">
<div
v-for="inst in store.upcomingPayments"
:key="inst.id"
class="remind-item"
:class="getPaymentCountdownType(inst.days_until_payment!)"
>
<div class="remind-left">
<span class="remind-name">{{ inst.account_name || '未知账户' }}</span>
<span class="remind-detail">
{{ inst.current_period }}/{{ inst.total_periods }}
</span>
</div>
<div class="remind-center">
<span class="remind-date">{{ formatDate(inst.next_payment_date) }}</span>
<span class="remind-amount">&yen; {{ inst.payment_amount.toFixed(2) }}</span>
</div>
<div class="remind-right">
<span class="remind-countdown" :class="`countdown--${getPaymentCountdownType(inst.days_until_payment!)}`">
{{ getPaymentCountdownText(inst.days_until_payment!) }}
</span>
<el-button
v-if="inst.days_until_payment! >= 0"
type="primary"
size="small"
round
@click="handlePayInstallment(inst)"
>
标记已还
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="store.loading" class="loading-state">
<el-icon class="loading-icon is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<template v-else>
<!-- 存款账户 -->
<div class="account-section">
<div class="section-header">
<el-icon :size="16" color="#67C23A"><Wallet /></el-icon>
<span>存款账户</span>
<span class="section-count">{{ store.savingsAccounts.length }}</span>
</div>
<div v-if="store.savingsAccounts.length === 0" class="section-empty">
还没有存款账户点击新建账户添加吧~
</div>
<div v-else class="cards-grid">
<div
v-for="acc in store.savingsAccounts"
:key="acc.id"
class="account-card account-card--savings"
>
<div class="card-color-bar" :style="{ background: acc.color }"></div>
<div class="card-body">
<div class="card-top">
<div class="card-icon" :style="{ background: acc.color + '20', color: acc.color }">
<el-icon :size="20"><Wallet /></el-icon>
</div>
<div class="card-title-area">
<h4 class="card-title">{{ acc.name }}</h4>
<span v-if="acc.description" class="card-desc">{{ acc.description }}</span>
</div>
</div>
<div class="card-balance positive">
&yen; {{ formatMoney(acc.balance) }}
</div>
<div class="card-actions">
<el-button text size="small" @click="openUpdateBalance(acc)">
<el-icon><Edit /></el-icon>
<span>更新余额</span>
</el-button>
<el-button text size="small" @click="openHistory(acc)">
<el-icon><Clock /></el-icon>
<span>历史</span>
</el-button>
<el-button text size="small" @click="openEditAccount(acc)">
<el-icon><Setting /></el-icon>
</el-button>
<el-button text size="small" @click="handleDeleteAccount(acc)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 欠款账户 -->
<div class="account-section">
<div class="section-header">
<el-icon :size="16" color="#FFB347"><CreditCard /></el-icon>
<span>欠款账户</span>
<span class="section-count">{{ store.debtAccounts.length }}</span>
</div>
<div v-if="store.debtAccounts.length === 0" class="section-empty">
还没有欠款账户点击新建账户添加吧~
</div>
<div v-else class="cards-grid cards-grid--debt">
<div
v-for="acc in store.debtAccounts"
:key="acc.id"
class="account-card account-card--debt"
>
<div class="card-color-bar" :style="{ background: acc.color }"></div>
<div class="card-body">
<div class="card-top">
<div class="card-icon" :style="{ background: acc.color + '20', color: acc.color }">
<el-icon :size="20"><CreditCard /></el-icon>
</div>
<div class="card-title-area">
<h4 class="card-title">{{ acc.name }}</h4>
<span v-if="acc.description" class="card-desc">{{ acc.description }}</span>
</div>
</div>
<div class="card-balance negative">
-&yen; {{ formatMoney(acc.balance) }}
</div>
<!-- 分期信息 -->
<div v-if="acc.installments && acc.installments.length > 0" class="installment-info">
<div
v-for="inst in acc.installments"
:key="inst.next_payment_date"
class="installment-item"
>
<div class="installment-progress">
<span> {{ inst.remaining_periods }} 期未还</span>
<span class="installment-date">{{ formatDate(inst.next_payment_date) }}</span>
</div>
<div class="installment-detail">
<span v-if="inst.days_until_payment !== null"
class="installment-countdown"
:class="`countdown--${getPaymentCountdownType(inst.days_until_payment)}`"
>
{{ getPaymentCountdownText(inst.days_until_payment) }}
</span>
</div>
</div>
</div>
<div class="card-actions">
<el-button text size="small" @click="openUpdateBalance(acc)">
<el-icon><Edit /></el-icon>
<span>更新余额</span>
</el-button>
<el-button text size="small" @click="openHistory(acc)">
<el-icon><Clock /></el-icon>
<span>历史</span>
</el-button>
<el-button text size="small" @click="openCreateInstallment(acc.id)">
<el-icon><Plus /></el-icon>
<span>分期</span>
</el-button>
<el-button text size="small" @click="openEditAccount(acc)">
<el-icon><Setting /></el-icon>
</el-button>
<el-button text size="small" @click="handleDeleteAccount(acc)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- 分期计划管理 -->
<div v-if="store.installments.length > 0" class="account-section">
<div class="section-header">
<el-icon :size="16" color="#C8A2C8"><List /></el-icon>
<span>分期计划管理</span>
<span class="section-count">{{ store.installments.length }}</span>
</div>
<div class="installment-table">
<div
v-for="inst in store.installments"
:key="inst.id"
class="installment-row"
:class="{ completed: inst.is_completed }"
>
<div class="inst-info">
<div class="inst-name">
<span class="inst-dot" :style="{ background: inst.account_color || '#FFB7C5' }"></span>
{{ inst.account_name || '未知账户' }}
<el-tag v-if="inst.is_completed" size="small" type="success">已还清</el-tag>
</div>
<div class="inst-meta">
<span>&yen;{{ inst.total_amount.toFixed(2) }}</span>
<span>|</span>
<span>{{ inst.total_periods }}</span>
<span>|</span>
<span>每期 &yen;{{ inst.payment_amount.toFixed(2) }}</span>
<span>|</span>
<span>每月{{ inst.payment_day }}号还</span>
</div>
<div v-if="!inst.is_completed && inst.next_payment_date" class="inst-next">
下次还款: {{ formatDate(inst.next_payment_date) }}
<span class="inst-countdown" :class="`countdown--${getPaymentCountdownType(inst.days_until_payment!)}`">
{{ getPaymentCountdownText(inst.days_until_payment!) }}
</span>
</div>
</div>
<div class="inst-progress-bar">
<div class="progress-track">
<div
class="progress-fill"
:style="{ width: `${((inst.current_period - 1) / inst.total_periods) * 100}%` }"
:class="{ complete: inst.is_completed }"
></div>
</div>
<span class="progress-text">{{ inst.is_completed ? inst.total_periods : inst.current_period - 1 }}/{{ inst.total_periods }}</span>
</div>
<div class="inst-actions">
<el-button
v-if="!inst.is_completed && inst.days_until_payment !== null && inst.days_until_payment >= 0"
type="primary"
size="small"
round
@click="handlePayInstallment(inst)"
>
还款
</el-button>
<el-button text size="small" @click="openEditInstallment(inst)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button text size="small" @click="handleDeleteInstallment(inst)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- 弹窗 -->
<AccountDialog
:visible="showAccountDialog"
:edit-account="editingAccount"
@update:visible="showAccountDialog = $event"
/>
<BalanceDialog
:visible="showBalanceDialog"
:account="balanceAccount"
@update:visible="showBalanceDialog = $event"
/>
<AccountHistoryDialog
:visible="showHistoryDialog"
:account="historyAccount"
@update:visible="showHistoryDialog = $event"
/>
<InstallmentDialog
:visible="showInstallmentDialog"
:edit-installment="editingInstallment"
:account-id="installmentAccountId"
@update:visible="showInstallmentDialog = $event"
/>
</div>
</template>
<style scoped lang="scss">
.asset-page {
min-height: calc(100vh - 60px);
padding: 24px;
display: flex;
justify-content: center;
}
.asset-container {
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
gap: 20px;
}
// ============ 概览统计 ============
.overview-card {
display: flex;
align-items: center;
justify-content: space-around;
padding: 24px 32px;
background: white;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-md);
animation: fadeInUp 0.4s ease;
}
.overview-item {
display: flex;
align-items: center;
gap: 14px;
.overview-icon {
width: 46px;
height: 46px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
&--savings {
background: rgba(103, 194, 58, 0.15);
color: #67C23A;
}
&--debt {
background: rgba(255, 179, 71, 0.15);
color: #FFB347;
}
&--net {
background: rgba(200, 162, 200, 0.15);
color: #C8A2C8;
}
}
.overview-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.overview-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
&.negative {
color: #F56C6C;
}
}
.overview-label {
font-size: 12px;
color: var(--text-secondary);
}
}
.overview-divider {
width: 1px;
height: 40px;
background: rgba(255, 183, 197, 0.2);
}
// ============ 工具栏 ============
.toolbar {
display: flex;
flex-direction: column;
gap: 12px;
.toolbar-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.toolbar-left {
.page-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
outline: none;
&--primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
color: white;
box-shadow: 0 2px 8px rgba(255, 183, 197, 0.4);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 183, 197, 0.5);
}
}
}
// ============ 还款提醒 ============
.remind-section {
background: linear-gradient(135deg, rgba(255, 107, 107, 0.06) 0%, rgba(255, 179, 71, 0.06) 100%);
border: 1px solid rgba(255, 107, 107, 0.15);
border-radius: var(--radius-lg);
padding: 20px;
animation: fadeInUp 0.4s ease;
.remind-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 14px;
}
}
.remind-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.remind-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: white;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
gap: 16px;
&.overdue {
border-left: 3px solid #F56C6C;
}
&.today {
border-left: 3px solid #FF6B6B;
}
&.urgent {
border-left: 3px solid #FFB347;
}
&.soon {
border-left: 3px solid #E6A23C;
}
}
.remind-left {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 100px;
.remind-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.remind-detail {
font-size: 12px;
color: var(--text-secondary);
}
}
.remind-center {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
.remind-date {
font-size: 14px;
color: var(--text-primary);
}
.remind-amount {
font-size: 16px;
font-weight: 700;
color: #FF6B6B;
}
}
.remind-right {
display: flex;
align-items: center;
gap: 10px;
.remind-countdown {
font-size: 13px;
font-weight: 600;
padding: 2px 10px;
border-radius: 12px;
white-space: nowrap;
}
}
// ============ 倒计时样式 ============
.countdown--today {
color: #FF6B6B;
background: rgba(255, 107, 107, 0.1);
font-weight: 700 !important;
}
.countdown--overdue {
color: #F56C6C;
background: rgba(245, 108, 108, 0.1);
}
.countdown--urgent {
color: #FFB347;
background: rgba(255, 179, 71, 0.1);
}
.countdown--soon {
color: #E6A23C;
background: rgba(230, 162, 60, 0.1);
}
.countdown--safe {
color: var(--primary);
background: rgba(255, 183, 197, 0.12);
}
// ============ 加载 & 空状态 ============
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 60px 0;
color: var(--text-secondary);
.loading-icon {
font-size: 32px;
color: var(--primary);
}
}
// ============ 账户区域 ============
.account-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
padding: 4px 0;
.section-count {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
background: rgba(255, 183, 197, 0.15);
padding: 2px 8px;
border-radius: 10px;
}
}
.section-empty {
text-align: center;
padding: 32px;
color: var(--text-secondary);
font-size: 14px;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
// ============ 账户卡片 ============
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.account-card {
position: relative;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
display: flex;
transition: all 0.2s ease;
&:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
}
.card-color-bar {
width: 4px;
flex-shrink: 0;
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
}
.card-body {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.card-top {
display: flex;
align-items: flex-start;
gap: 12px;
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.card-title-area {
flex: 1;
min-width: 0;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.card-desc {
font-size: 12px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
margin-top: 2px;
}
.card-balance {
font-size: 22px;
font-weight: 700;
padding-left: 2px;
&.positive {
color: #67C23A;
}
&.negative {
color: #FFB347;
}
}
// ============ 分期信息 ============
.installment-info {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 12px;
background: rgba(255, 179, 71, 0.04);
border-radius: var(--radius-sm);
overflow: hidden;
}
.installment-item {
display: flex;
flex-direction: column;
gap: 6px;
.installment-progress {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
font-size: 12px;
color: var(--text-secondary);
min-width: 0;
> span {
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.installment-date {
color: var(--text-primary);
font-weight: 500;
}
}
.installment-detail {
display: flex;
justify-content: flex-end;
.installment-countdown {
font-size: 12px;
font-weight: 600;
padding: 2px 10px;
border-radius: 10px;
white-space: nowrap;
flex-shrink: 0;
}
}
}
// 欠款账户卡片内容更多,需要更大的最小宽度
.cards-grid--debt {
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
// ============ 卡片操作按钮 ============
.card-actions {
display: flex;
justify-content: flex-end;
gap: 0;
padding-top: 10px;
margin-top: auto;
border-top: 1px dashed rgba(255, 183, 197, 0.15);
opacity: 0.6;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
}
// ============ 分期计划管理表 ============
.installment-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.installment-row {
display: flex;
align-items: center;
gap: 20px;
padding: 18px 20px;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
&:hover {
box-shadow: var(--shadow-md);
.inst-actions {
opacity: 1;
}
}
&.completed {
opacity: 0.6;
&:hover {
opacity: 0.8;
}
}
}
.inst-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
.inst-name {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
.inst-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
}
.inst-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-secondary);
}
.inst-next {
font-size: 13px;
color: var(--text-secondary);
.inst-countdown {
margin-left: 8px;
font-size: 12px;
font-weight: 600;
padding: 1px 8px;
border-radius: 10px;
}
}
}
.inst-progress-bar {
display: flex;
align-items: center;
gap: 10px;
min-width: 140px;
.progress-track {
flex: 1;
height: 6px;
background: rgba(255, 183, 197, 0.15);
border-radius: 3px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(135deg, var(--primary), var(--accent));
border-radius: 3px;
transition: width 0.3s ease;
&.complete {
background: #67C23A;
}
}
}
.progress-text {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
min-width: 36px;
text-align: right;
}
}
.inst-actions {
display: flex;
align-items: center;
gap: 4px;
opacity: 0.6;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
}
// ============ 动画 ============
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// ============ 响应式 ============
@media (max-width: 768px) {
.overview-card {
flex-direction: column;
gap: 16px;
padding: 20px;
}
.overview-divider {
width: 100%;
height: 1px;
}
.cards-grid {
grid-template-columns: 1fr;
}
.remind-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.remind-right {
width: 100%;
justify-content: space-between;
}
.installment-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
.inst-actions {
opacity: 1;
}
}
}
</style>