878 lines
21 KiB
Vue
878 lines
21 KiB
Vue
<template>
|
|
<view class="records-page">
|
|
<scroll-view class="records-scroll" scroll-y :scroll-with-animation="true"
|
|
:style="{ paddingBottom: scrollPaddingBottom + 'px' }">
|
|
<view class="hero-panel">
|
|
<view class="hero-main">
|
|
<view class="hero-copy">
|
|
<text class="hero-kicker">健康档案</text>
|
|
<text class="hero-title">{{ heroTitle }}</text>
|
|
<text class="hero-desc">{{ heroDesc }}</text>
|
|
</view>
|
|
<view class="hero-action" @click="handleAddRecord">
|
|
<TnIcon name="add-lack" :size="34" color="#FFFFFF" />
|
|
</view>
|
|
</view>
|
|
|
|
<view class="summary-row">
|
|
<view class="summary-item">
|
|
<text class="summary-value">{{ records.length }}</text>
|
|
<text class="summary-label">当前记录</text>
|
|
</view>
|
|
<view class="summary-divider" />
|
|
<view class="summary-item">
|
|
<text class="summary-value">{{ monthCount }}</text>
|
|
<text class="summary-label">覆盖月份</text>
|
|
</view>
|
|
<view class="summary-divider" />
|
|
<view class="summary-item">
|
|
<text class="summary-value">{{ totalCostText }}</text>
|
|
<text class="summary-label">累计花费</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="category-strip">
|
|
<TnTabs v-model="currentFilterIndex" height="78rpx" bar-width="36rpx" bar-color="#F97316"
|
|
active-color="#F97316" color="#64748B" bg-color="#FFFFFF" font-size="25rpx" active-font-size="27rpx"
|
|
:bottom-shadow="false" @change="onFilterChange">
|
|
<TnTabsItem v-for="tab in filterTabs" :key="tab" :title="tab" />
|
|
</TnTabs>
|
|
</view>
|
|
|
|
<view class="records-stage">
|
|
<view v-if="loading" class="loading-state">
|
|
<TnLoading :show="true" mode="circle" color="#F97316" :size="54" />
|
|
<text class="loading-text">正在整理健康记录...</text>
|
|
<view class="loading-card" v-for="i in 3" :key="i">
|
|
<view class="skeleton-icon" />
|
|
<view class="skeleton-content">
|
|
<view class="skeleton-line skeleton-line-title" />
|
|
<view class="skeleton-line skeleton-line-meta" />
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else-if="groupedRecords.length === 0" class="empty-state">
|
|
<TnEmpty mode="data" color="#F97316" size="xl" />
|
|
<text class="empty-title">{{ emptyTitle }}</text>
|
|
<text class="empty-desc">{{ emptyDesc }}</text>
|
|
<TnButton text-color="#FFFFFF" shape="round" :bold="true" :shadow="true"
|
|
:custom-style="primaryButtonStyle" width="320" height="88" @click="handleAddRecord">
|
|
<view class="button-inner">
|
|
<TnIcon name="add-lack" :size="28" />
|
|
<text>添加记录</text>
|
|
</view>
|
|
</TnButton>
|
|
</view>
|
|
|
|
<view v-else class="records-content">
|
|
<view class="section-head">
|
|
<view>
|
|
<text class="section-title">记录时间线</text>
|
|
<text class="section-subtitle">{{ activeFilterLabel }} · {{ records.length }} 条</text>
|
|
</view>
|
|
<TnTag bg-color="#FFF7ED" text-color="#F97316" shape="round" size="sm">
|
|
{{ latestDateText }}
|
|
</TnTag>
|
|
</view>
|
|
|
|
<view class="month-group" v-for="(group, groupIndex) in groupedRecords" :key="group.monthKey">
|
|
<view class="month-head">
|
|
<view class="month-dot" :class="{ 'month-dot-active': groupIndex === 0 }" />
|
|
<text class="month-label">{{ group.monthLabel }}</text>
|
|
<TnTag bg-color="#F1F5F9" text-color="#475569" shape="round" size="sm">
|
|
{{ group.records.length }} 条
|
|
</TnTag>
|
|
</view>
|
|
|
|
<view v-for="record in group.records" :key="record._id" class="record-card"
|
|
hover-class="record-card-active" @click="handleRecordTap(record)">
|
|
<view class="record-accent" :style="{ background: getTypeStyle(record.type).accent }" />
|
|
<view class="record-icon" :style="{ backgroundColor: getTypeStyle(record.type).bg }">
|
|
<TnIcon :name="getTypeStyle(record.type).icon" :size="38"
|
|
:color="getTypeStyle(record.type).color" />
|
|
</view>
|
|
|
|
<view class="record-main">
|
|
<view class="record-title-row">
|
|
<text class="record-title">{{ getRecordTitle(record) }}</text>
|
|
<TnTag :bg-color="getTypeStyle(record.type).soft"
|
|
:text-color="getTypeStyle(record.type).color" shape="round" size="sm">
|
|
{{ getTypeLabel(record.type) }}
|
|
</TnTag>
|
|
</view>
|
|
<text class="record-desc">{{ getRecordDesc(record) }}</text>
|
|
<view class="record-meta">
|
|
<view class="meta-pill">
|
|
<TnIcon name="calendar" :size="22" color="#64748B" />
|
|
<text>{{ formatRecordDate(record.date) }}</text>
|
|
</view>
|
|
<view v-if="record.petName" class="meta-pill">
|
|
<TnIcon name="floral" :size="22" color="#64748B" />
|
|
<text>{{ record.petName }}</text>
|
|
</view>
|
|
<view v-if="record.hospital" class="meta-pill meta-pill-wide">
|
|
<TnIcon name="location" :size="22" color="#64748B" />
|
|
<text>{{ record.hospital }}</text>
|
|
</view>
|
|
<view v-if="record.cost" class="meta-pill cost-pill">
|
|
<text>¥{{ formatCost(record.cost) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<TnIcon name="right" :size="28" color="#CBD5E1" />
|
|
</view>
|
|
</view>
|
|
|
|
<view class="bottom-tip">
|
|
<TnIcon name="success-circle-fill" :size="24" color="#14B8A6" />
|
|
<text>已展示当前分类的全部记录</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<view v-if="!loading && groupedRecords.length > 0" class="fab-wrap" :style="{ bottom: fabBottom + 'px' }">
|
|
<TnButton text-color="#FFFFFF" shape="round" :bold="true" :shadow="true"
|
|
:custom-style="primaryButtonStyle" width="280" height="88" :font-size="28" @click="handleAddRecord">
|
|
<view class="button-inner">
|
|
<TnIcon name="add-lack" :size="28" />
|
|
<text>添加记录</text>
|
|
</view>
|
|
</TnButton>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
import TnButton from '@/uni_modules/tuniaoui-vue3/components/button/src/button.vue'
|
|
import TnEmpty from '@/uni_modules/tuniaoui-vue3/components/empty/src/empty.vue'
|
|
import TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
|
|
import TnLoading from '@/uni_modules/tuniaoui-vue3/components/loading/src/loading.vue'
|
|
import TnTabs from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue'
|
|
import TnTabsItem from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue'
|
|
import TnTag from '@/uni_modules/tuniaoui-vue3/components/tag/src/tag.vue'
|
|
|
|
interface HealthRecord {
|
|
_id : string
|
|
pet_id : string
|
|
user_id : string
|
|
type : string
|
|
title : string
|
|
date : number | string
|
|
nextDate ?: number | string
|
|
hospital ?: string
|
|
doctor ?: string
|
|
description ?: string
|
|
vaccineName ?: string
|
|
batchNumber ?: string
|
|
dewormType ?: string
|
|
brand ?: string
|
|
diagnosis ?: string
|
|
medications ?: Array<{ name : string; dosage : string; frequency : string }>
|
|
images ?: string[]
|
|
cost ?: number
|
|
tags ?: string[]
|
|
createdAt : number
|
|
updatedAt : number
|
|
petName ?: string
|
|
}
|
|
|
|
interface RecordGroup {
|
|
monthKey : string
|
|
monthLabel : string
|
|
records : HealthRecord[]
|
|
}
|
|
|
|
interface TypeStyle {
|
|
icon : string
|
|
color : string
|
|
bg : string
|
|
soft : string
|
|
accent : string
|
|
}
|
|
|
|
const loading = ref(true)
|
|
const hasLoaded = ref(false)
|
|
const records = ref<HealthRecord[]>([])
|
|
const groupedRecords = ref<RecordGroup[]>([])
|
|
const scrollPaddingBottom = ref(190)
|
|
const fabBottom = ref(78)
|
|
const currentFilterIndex = ref(0)
|
|
let recordsRequestId = 0
|
|
const primaryButtonStyle = {
|
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
|
}
|
|
|
|
const filterTabs = ['全部', '疫苗', '驱虫', '病历', '体检', '其他']
|
|
const filterValues = ['all', 'vaccine', 'deworm', 'medical', 'checkup', 'other']
|
|
|
|
const typeStyleMap : Record<string, TypeStyle> = {
|
|
vaccine: {
|
|
icon: 'safe-fill',
|
|
color: '#2563EB',
|
|
bg: '#EFF6FF',
|
|
soft: '#DBEAFE',
|
|
accent: 'linear-gradient(180deg, #2563EB, #60A5FA)',
|
|
},
|
|
deworm: {
|
|
icon: 'notice-fill',
|
|
color: '#7C3AED',
|
|
bg: '#F5F3FF',
|
|
soft: '#EDE9FE',
|
|
accent: 'linear-gradient(180deg, #7C3AED, #A78BFA)',
|
|
},
|
|
medical: {
|
|
icon: 'write-feather',
|
|
color: '#F97316',
|
|
bg: '#FFF7ED',
|
|
soft: '#FFEDD5',
|
|
accent: 'linear-gradient(180deg, #F97316, #FDBA74)',
|
|
},
|
|
checkup: {
|
|
icon: 'data-fill',
|
|
color: '#0D9488',
|
|
bg: '#F0FDFA',
|
|
soft: '#CCFBF1',
|
|
accent: 'linear-gradient(180deg, #0D9488, #5EEAD4)',
|
|
},
|
|
other: {
|
|
icon: 'inventory-fill',
|
|
color: '#475569',
|
|
bg: '#F8FAFC',
|
|
soft: '#E2E8F0',
|
|
accent: 'linear-gradient(180deg, #64748B, #CBD5E1)',
|
|
},
|
|
}
|
|
|
|
const typeLabelMap : Record<string, string> = {
|
|
vaccine: '疫苗',
|
|
deworm: '驱虫',
|
|
medical: '病历',
|
|
checkup: '体检',
|
|
other: '其他',
|
|
}
|
|
|
|
const activeFilterLabel = computed(() => filterTabs[currentFilterIndex.value] || '全部')
|
|
|
|
const monthCount = computed(() => groupedRecords.value.length)
|
|
|
|
const latestDateText = computed(() => {
|
|
const latest = records.value[0]
|
|
return latest ? formatRecordDate(latest.date) : '暂无'
|
|
})
|
|
|
|
const totalCostText = computed(() => {
|
|
const total = records.value.reduce((sum, item) => sum + (Number(item.cost) || 0), 0)
|
|
if (total >= 10000) return `${(total / 10000).toFixed(1)}w`
|
|
if (total > 0) return `¥${Math.round(total)}`
|
|
return '¥0'
|
|
})
|
|
|
|
const heroTitle = computed(() => {
|
|
if (loading.value) return '正在同步记录'
|
|
if (!records.value.length) return currentFilterIndex.value === 0 ? '从第一条记录开始' : `${activeFilterLabel.value}暂无记录`
|
|
return currentFilterIndex.value === 0 ? '毛球健康一眼掌握' : `${activeFilterLabel.value}记录`
|
|
})
|
|
|
|
const heroDesc = computed(() => {
|
|
if (loading.value) return '正在读取疫苗、驱虫、病历和体检信息'
|
|
if (!records.value.length) return '添加记录后,这里会自动生成按月整理的健康时间线'
|
|
return `最近更新 ${latestDateText.value},共整理 ${records.value.length} 条健康事件`
|
|
})
|
|
|
|
const emptyTitle = computed(() => (currentFilterIndex.value === 0 ? '还没有健康记录' : `暂无${activeFilterLabel.value}记录`))
|
|
|
|
const emptyDesc = computed(() => (
|
|
currentFilterIndex.value === 0
|
|
? '记录疫苗、驱虫、体检和就诊情况,让每一次照护都有迹可循。'
|
|
: '切换其他分类看看,或为毛球补上一条新的健康记录。'
|
|
))
|
|
|
|
onMounted(() => {
|
|
calcLayout()
|
|
loadRecords()
|
|
})
|
|
|
|
onShow(() => {
|
|
loadRecords(!hasLoaded.value)
|
|
})
|
|
|
|
function calcLayout() {
|
|
const systemInfo = uni.getSystemInfoSync()
|
|
const bottom = systemInfo.safeAreaInsets?.bottom || 0
|
|
fabBottom.value = bottom + 58
|
|
scrollPaddingBottom.value = bottom + 190
|
|
}
|
|
|
|
async function loadRecords(showLoading = !hasLoaded.value) {
|
|
const requestId = ++recordsRequestId
|
|
if (showLoading) loading.value = true
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
if (!uid) {
|
|
if (requestId !== recordsRequestId) return
|
|
records.value = []
|
|
groupedRecords.value = []
|
|
return
|
|
}
|
|
|
|
const res = await uniCloud.callFunction({
|
|
name: 'recordManager',
|
|
data: {
|
|
action: 'list',
|
|
uid,
|
|
type: currentFilterIndex.value === 0 ? undefined : filterValues[currentFilterIndex.value],
|
|
},
|
|
})
|
|
|
|
if (requestId !== recordsRequestId) return
|
|
|
|
if (res.result?.code === 0 && res.result.data) {
|
|
const list = (res.result.data.list || []) as HealthRecord[]
|
|
records.value = sortRecords(list)
|
|
groupedRecords.value = groupByMonth(records.value)
|
|
} else {
|
|
records.value = []
|
|
groupedRecords.value = []
|
|
}
|
|
} catch (e) {
|
|
if (requestId !== recordsRequestId) return
|
|
console.warn('加载健康记录失败', e)
|
|
records.value = []
|
|
groupedRecords.value = []
|
|
} finally {
|
|
if (requestId === recordsRequestId) {
|
|
loading.value = false
|
|
hasLoaded.value = true
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFilterChange(index : number | string) {
|
|
currentFilterIndex.value = Number(index)
|
|
loadRecords(false)
|
|
}
|
|
|
|
function sortRecords(list : HealthRecord[]) {
|
|
return [...list].sort((a, b) => getTimeValue(b.date) - getTimeValue(a.date))
|
|
}
|
|
|
|
function groupByMonth(list : HealthRecord[]) {
|
|
const map = new Map<string, HealthRecord[]>()
|
|
list.forEach((record) => {
|
|
const date = getDate(record.date)
|
|
if (!date) return
|
|
const month = date.getMonth() + 1
|
|
const key = `${date.getFullYear()}-${String(month).padStart(2, '0')}`
|
|
if (!map.has(key)) map.set(key, [])
|
|
map.get(key)!.push(record)
|
|
})
|
|
|
|
return Array.from(map.entries()).map(([monthKey, groupRecords]) => ({
|
|
monthKey,
|
|
monthLabel: getMonthLabel(groupRecords[0].date),
|
|
records: groupRecords,
|
|
}))
|
|
}
|
|
|
|
function getTypeStyle(type : string) {
|
|
return typeStyleMap[type] || typeStyleMap.other
|
|
}
|
|
|
|
function getTypeLabel(type : string) {
|
|
return typeLabelMap[type] || '其他'
|
|
}
|
|
|
|
function getRecordTitle(record : HealthRecord) {
|
|
if (record.title) return record.title
|
|
if (record.vaccineName) return record.vaccineName
|
|
if (record.diagnosis) return record.diagnosis
|
|
return getTypeLabel(record.type)
|
|
}
|
|
|
|
function getRecordDesc(record : HealthRecord) {
|
|
if (record.description) return record.description
|
|
if (record.hospital && record.doctor) return `${record.hospital} · ${record.doctor}`
|
|
if (record.hospital) return record.hospital
|
|
if (record.brand) return `品牌:${record.brand}`
|
|
if (record.batchNumber) return `批号:${record.batchNumber}`
|
|
return '点击查看记录详情'
|
|
}
|
|
|
|
function formatRecordDate(value : number | string | undefined) {
|
|
const date = getDate(value)
|
|
if (!date) return ''
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${month}月${day}日`
|
|
}
|
|
|
|
function getMonthLabel(value : number | string | undefined) {
|
|
const date = getDate(value)
|
|
if (!date) return '未知月份'
|
|
const year = date.getFullYear()
|
|
const month = date.getMonth() + 1
|
|
const now = new Date()
|
|
if (year === now.getFullYear() && month === now.getMonth() + 1) return `${month}月`
|
|
return `${year}年${month}月`
|
|
}
|
|
|
|
function getDate(value : number | string | undefined) {
|
|
if (!value) return null
|
|
const date = new Date(value)
|
|
return Number.isNaN(date.getTime()) ? null : date
|
|
}
|
|
|
|
function getTimeValue(value : number | string | undefined) {
|
|
return getDate(value)?.getTime() || 0
|
|
}
|
|
|
|
function formatCost(value : number) {
|
|
const num = Number(value) || 0
|
|
return Number.isInteger(num) ? String(num) : num.toFixed(2)
|
|
}
|
|
|
|
function handleAddRecord() {
|
|
uni.navigateTo({ url: '/pages/record/type-select' })
|
|
}
|
|
|
|
function handleRecordTap(record : HealthRecord) {
|
|
uni.navigateTo({ url: `/pages/record/detail?id=${record._id}` })
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
$primary: #F97316;
|
|
$primary-light: #FB923C;
|
|
$primary-bg: #FFF7ED;
|
|
$text-dark: #0F172A;
|
|
$text-body: #475569;
|
|
$text-muted: #64748B;
|
|
$border: #E2E8F0;
|
|
$card-bg: #FFFFFF;
|
|
$shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
|
|
$shadow-active: 0 12rpx 34rpx rgba(15, 23, 42, 0.1);
|
|
|
|
.records-page {
|
|
position: relative;
|
|
height: 100%;
|
|
background: #F8FAFC;
|
|
}
|
|
|
|
.records-scroll {
|
|
height: 100vh;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.hero-panel {
|
|
margin: 24rpx 24rpx 0;
|
|
padding: 30rpx 28rpx 26rpx;
|
|
border-radius: 28rpx;
|
|
background: linear-gradient(135deg, #F97316 0%, #FB923C 54%, #2DD4BF 145%);
|
|
box-shadow: 0 14rpx 36rpx rgba(249, 115, 22, 0.22);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hero-main {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.hero-copy {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.hero-kicker {
|
|
font-size: 23rpx;
|
|
font-weight: 700;
|
|
color: rgba(255, 255, 255, 0.78);
|
|
}
|
|
|
|
.hero-title {
|
|
margin-top: 8rpx;
|
|
font-size: 40rpx;
|
|
font-weight: 900;
|
|
color: #FFFFFF;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.hero-desc {
|
|
margin-top: 12rpx;
|
|
font-size: 25rpx;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.hero-action {
|
|
width: 76rpx;
|
|
height: 76rpx;
|
|
border-radius: 24rpx;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1rpx solid rgba(255, 255, 255, 0.28);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: opacity 0.2s ease;
|
|
|
|
&:active {
|
|
opacity: 0.78;
|
|
}
|
|
}
|
|
|
|
.summary-row {
|
|
margin-top: 28rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 18rpx 0;
|
|
border-radius: 18rpx;
|
|
background: rgba(255, 255, 255, 0.16);
|
|
}
|
|
|
|
.summary-item {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4rpx;
|
|
min-width: 0;
|
|
}
|
|
|
|
.summary-value {
|
|
font-size: 32rpx;
|
|
font-weight: 900;
|
|
color: #FFFFFF;
|
|
max-width: 180rpx;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.summary-label {
|
|
font-size: 21rpx;
|
|
color: rgba(255, 255, 255, 0.78);
|
|
}
|
|
|
|
.summary-divider {
|
|
width: 1rpx;
|
|
height: 42rpx;
|
|
background: rgba(255, 255, 255, 0.28);
|
|
}
|
|
|
|
.category-strip {
|
|
display: grid;
|
|
place-items: center;
|
|
height: 80rpx;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 20;
|
|
margin: 20rpx 24rpx 0;
|
|
padding: 4rpx;
|
|
border-radius: 22rpx;
|
|
background: $card-bg;
|
|
box-shadow: $shadow;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.category-strip :deep(.tn-tabs) {
|
|
width: 100%;
|
|
align-self: center;
|
|
}
|
|
|
|
.category-strip :deep(.tn-tabs__container) {
|
|
align-items: center;
|
|
}
|
|
|
|
.category-strip :deep(.tn-tabs__bar-container) {
|
|
bottom: 4rpx;
|
|
}
|
|
|
|
.records-stage {
|
|
min-height: 720rpx;
|
|
}
|
|
|
|
.loading-state {
|
|
padding: 48rpx 24rpx 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
}
|
|
|
|
.loading-text {
|
|
font-size: 26rpx;
|
|
color: $text-muted;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.loading-card {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
padding: 24rpx;
|
|
border-radius: 24rpx;
|
|
background: $card-bg;
|
|
box-shadow: $shadow;
|
|
}
|
|
|
|
.skeleton-icon {
|
|
width: 78rpx;
|
|
height: 78rpx;
|
|
border-radius: 22rpx;
|
|
background: #E2E8F0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.skeleton-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14rpx;
|
|
}
|
|
|
|
.skeleton-line {
|
|
height: 24rpx;
|
|
border-radius: 12rpx;
|
|
background: #E2E8F0;
|
|
}
|
|
|
|
.skeleton-line-title {
|
|
width: 64%;
|
|
}
|
|
|
|
.skeleton-line-meta {
|
|
width: 42%;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
padding: 98rpx 48rpx 0;
|
|
}
|
|
|
|
.empty-title {
|
|
font-size: 32rpx;
|
|
font-weight: 800;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.empty-desc {
|
|
max-width: 520rpx;
|
|
font-size: 26rpx;
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
color: $text-muted;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.records-content {
|
|
padding: 26rpx 24rpx 0;
|
|
}
|
|
|
|
.section-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
margin-bottom: 18rpx;
|
|
}
|
|
|
|
.section-title {
|
|
display: block;
|
|
font-size: 31rpx;
|
|
font-weight: 900;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.section-subtitle {
|
|
display: block;
|
|
margin-top: 6rpx;
|
|
font-size: 23rpx;
|
|
color: $text-muted;
|
|
}
|
|
|
|
.month-group {
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.month-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
margin-bottom: 14rpx;
|
|
padding: 0 4rpx;
|
|
}
|
|
|
|
.month-dot {
|
|
width: 18rpx;
|
|
height: 18rpx;
|
|
border-radius: 50%;
|
|
background: #CBD5E1;
|
|
box-shadow: 0 0 0 8rpx #F1F5F9;
|
|
flex-shrink: 0;
|
|
|
|
&.month-dot-active {
|
|
background: $primary;
|
|
box-shadow: 0 0 0 8rpx #FFEDD5;
|
|
}
|
|
}
|
|
|
|
.month-label {
|
|
flex: 1;
|
|
font-size: 27rpx;
|
|
font-weight: 800;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.record-card {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
min-height: 150rpx;
|
|
margin-bottom: 16rpx;
|
|
padding: 22rpx 20rpx 22rpx 24rpx;
|
|
border-radius: 24rpx;
|
|
background: $card-bg;
|
|
box-shadow: $shadow;
|
|
overflow: hidden;
|
|
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
|
|
|
&.record-card-active {
|
|
transform: scale(0.985);
|
|
box-shadow: $shadow-active;
|
|
}
|
|
}
|
|
|
|
.record-accent {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 7rpx;
|
|
}
|
|
|
|
.record-icon {
|
|
width: 82rpx;
|
|
height: 82rpx;
|
|
border-radius: 22rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.record-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.record-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
min-width: 0;
|
|
}
|
|
|
|
.record-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 29rpx;
|
|
font-weight: 800;
|
|
color: $text-dark;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.record-desc {
|
|
font-size: 24rpx;
|
|
color: $text-muted;
|
|
line-height: 1.45;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.record-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 10rpx;
|
|
margin-top: 2rpx;
|
|
}
|
|
|
|
.meta-pill {
|
|
max-width: 220rpx;
|
|
height: 42rpx;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5rpx;
|
|
padding: 0 12rpx;
|
|
border-radius: 999rpx;
|
|
background: #F8FAFC;
|
|
border: 1rpx solid $border;
|
|
font-size: 21rpx;
|
|
color: $text-body;
|
|
|
|
text {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
.meta-pill-wide {
|
|
max-width: 280rpx;
|
|
}
|
|
|
|
.cost-pill {
|
|
background: #FEF2F2;
|
|
border-color: #FEE2E2;
|
|
color: #DC2626;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.bottom-tip {
|
|
height: 86rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8rpx;
|
|
font-size: 23rpx;
|
|
color: $text-muted;
|
|
}
|
|
|
|
.button-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.fab-wrap {
|
|
position: fixed;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 60;
|
|
}
|
|
</style>
|