1135 lines
28 KiB
Vue
1135 lines
28 KiB
Vue
<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">¥ {{ 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">
|
||
¥ {{ 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">
|
||
-¥ {{ 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>¥{{ inst.total_amount.toFixed(2) }}</span>
|
||
<span>|</span>
|
||
<span>{{ inst.total_periods }}期</span>
|
||
<span>|</span>
|
||
<span>每期 ¥{{ 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>
|