917 lines
26 KiB
Vue
917 lines
26 KiB
Vue
<template>
|
||
<view class="plans-screen">
|
||
<view class="plans-bg"></view>
|
||
<view class="status-bar-placeholder"></view>
|
||
|
||
<!-- 固定导航栏 -->
|
||
<view class="plans-header">
|
||
<view class="plans-back-btn" @click="handleBack">
|
||
<text class="plans-back-icon">‹</text>
|
||
</view>
|
||
<text class="plans-title">我的方案</text>
|
||
<view class="plans-refresh-btn" @click="handleRefresh">
|
||
<text class="plans-refresh-icon" :class="{ rotating: loading }">↻</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 固定Tab切换 -->
|
||
<view class="plans-tabs">
|
||
<view v-for="tab in tabs" :key="tab.value"
|
||
:class="['plans-tab', { 'plans-tab-active': currentTab === tab.value }]"
|
||
@click="switchTab(tab.value as 'naming' | 'affinity' | 'zeji')">
|
||
<text class="plans-tab-text">{{ tab.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 滚动列表内容 -->
|
||
<scroll-view class="plans-scroll" scroll-y="true">
|
||
<view class="plans-content">
|
||
<!-- 初始加载状态 -->
|
||
<view v-if="loading && list.length === 0" class="initial-loading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 列表 -->
|
||
<template v-else-if="list.length > 0">
|
||
<!-- 名字方案列表 -->
|
||
<template v-if="currentTab === 'naming'">
|
||
<view v-for="item in list" :key="item.id" class="list-item" @click="viewNamingDetail(item)">
|
||
<view :class="['item-icon', item.category === 'company' ? 'icon-gold' : 'icon-red']">
|
||
<text class="icon-text">{{ item.category === 'company' ? '企' : '名' }}</text>
|
||
</view>
|
||
<view class="item-content">
|
||
<view class="item-header">
|
||
<text class="item-title">{{ item.title || '未命名方案' }}</text>
|
||
<text class="item-date">{{ item.relative_time }}</text>
|
||
</view>
|
||
<text class="item-subtitle">{{ item.subtitle }}</text>
|
||
<view class="item-tags">
|
||
<text :class="['item-tag', item.category === 'company' ? 'tag-gold' : 'tag-red']">
|
||
{{ item.category === 'company' ? '公司' : '个人' }}
|
||
</text>
|
||
<text class="item-status">
|
||
<text class="status-icon">{{ item.has_solutions ? '✓' : '⏱' }}</text>
|
||
{{ item.has_solutions ? '已生成方案' : '生成中' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="item-arrow">
|
||
<text class="arrow-icon">›</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 缘分合盘列表 -->
|
||
<template v-else-if="currentTab === 'affinity'">
|
||
<view v-for="item in list" :key="item.id" class="list-item" @click="viewAffinityDetail(item)">
|
||
<view class="item-icon icon-pink">
|
||
<text class="icon-text">缘</text>
|
||
</view>
|
||
<view class="item-content">
|
||
<view class="item-header">
|
||
<text class="item-title">{{ item.person1_name }} & {{ item.person2_name }}</text>
|
||
<text class="item-date">{{ formatDate(item.created_time) }}</text>
|
||
</view>
|
||
<text class="item-subtitle">
|
||
{{ getRelationshipLabel(item.relationship) }}
|
||
<template v-if="item.score">· 缘分指数 {{ item.score }}分</template>
|
||
</text>
|
||
<view class="item-tags">
|
||
<text v-if="item.score_badge" class="item-tag tag-pink">{{ item.score_badge }}</text>
|
||
<text class="item-status">
|
||
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
|
||
{{ getTaskStatusText(item.task_status) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="item-arrow">
|
||
<text class="arrow-icon">›</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 八字择吉列表 -->
|
||
<template v-else-if="currentTab === 'zeji'">
|
||
<view v-for="item in list" :key="item.id" class="list-item" @click="viewZejiDetail(item)">
|
||
<view class="item-icon icon-purple">
|
||
<text class="icon-text">吉</text>
|
||
</view>
|
||
<view class="item-content">
|
||
<view class="item-header">
|
||
<text class="item-title">{{ item.event_name || '择吉事项' }}</text>
|
||
<text class="item-date">{{ formatDate(item.created_time) }}</text>
|
||
</view>
|
||
<text class="item-subtitle">
|
||
{{ item.event_type_label }}
|
||
<template v-if="item.selected_date">· {{ item.selected_date }}</template>
|
||
</text>
|
||
<view class="item-tags">
|
||
<text v-if="item.score_badge" class="item-tag tag-purple">{{ item.score_badge }}</text>
|
||
<text class="item-status">
|
||
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
|
||
{{ getTaskStatusText(item.task_status) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="item-arrow">
|
||
<text class="arrow-icon">›</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 加载更多按钮 -->
|
||
<view class="load-more-wrapper">
|
||
<view v-if="loading" class="loading-more">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
<view v-else-if="hasMore" class="load-more-btn" @click="loadNextPage">
|
||
<text class="load-more-text">点击加载更多</text>
|
||
</view>
|
||
<view v-else class="no-more">
|
||
<text class="no-more-text">— 已加载全部 —</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 空状态 -->
|
||
<template v-else>
|
||
<view class="empty-state">
|
||
<text class="empty-icon">📋</text>
|
||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||
<view class="empty-btn" @click="handleEmptyAction">
|
||
<text class="empty-btn-text">{{ getEmptyButtonText() }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, watch } from "vue";
|
||
import { userApi, namingApi, affinityApi, baziZejiApi } from "@/api";
|
||
import type { MyReportItem } from "@/api/types";
|
||
import type { AffinityListItem } from "@/api";
|
||
import type { BaziZejiListItem } from "@/api";
|
||
|
||
declare const uni: any;
|
||
|
||
const props = defineProps<{
|
||
/** 从详情页返回时父级传入,用于选中「名字 / 缘分合盘 / 八字择吉」子 tab */
|
||
focusListTab?: 'naming' | 'affinity' | 'zeji' | null;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
back: [];
|
||
navigate: [screen: string];
|
||
showDetail: [data: any, category?: string, serviceType?: string];
|
||
showNamingSolutionsList: [payload: { reportId: number; solutions: any[]; category?: string; serviceType?: string }];
|
||
showAffinityResult: [data: any, fromMyPlans?: boolean];
|
||
showAuspiciousResult: [data: any, fromMyPlans?: boolean];
|
||
focusListTabConsumed: [];
|
||
}>();
|
||
|
||
// Tab配置
|
||
const tabs = [
|
||
{ label: '名字', value: 'naming' },
|
||
{ label: '缘分合盘', value: 'affinity' },
|
||
{ label: '八字择吉', value: 'zeji' },
|
||
];
|
||
|
||
// 响应式数据
|
||
const currentTab = ref<'naming' | 'affinity' | 'zeji'>('naming');
|
||
const loading = ref(false);
|
||
const list = ref<any[]>([]);
|
||
const pageNo = ref(1);
|
||
const pageSize = 10;
|
||
const total = ref(0);
|
||
const hasMore = ref(true);
|
||
|
||
// 重置分页状态(须在 switchTab / watch 之前声明,避免暂时性死区)
|
||
const resetPagination = () => {
|
||
pageNo.value = 1;
|
||
list.value = [];
|
||
hasMore.value = true;
|
||
total.value = 0;
|
||
};
|
||
|
||
// 加载列表数据
|
||
const loadList = async (refresh = false) => {
|
||
if (loading.value) return;
|
||
if (!refresh && !hasMore.value) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
let res: any;
|
||
|
||
if (currentTab.value === 'naming') {
|
||
res = await userApi.getMyReports({
|
||
page_no: pageNo.value,
|
||
page_size: pageSize,
|
||
});
|
||
const items = res?.items || (Array.isArray(res) ? res : []);
|
||
|
||
if (refresh) {
|
||
list.value = items;
|
||
} else {
|
||
list.value = [...list.value, ...items];
|
||
}
|
||
|
||
total.value = res?.total || items.length;
|
||
hasMore.value = list.value.length < total.value;
|
||
|
||
} else if (currentTab.value === 'affinity') {
|
||
res = await affinityApi.getAffinityList({
|
||
page_no: pageNo.value,
|
||
page_size: pageSize,
|
||
});
|
||
|
||
if (res && res.data) {
|
||
const items = Array.isArray(res.data.data) ? res.data.data : [];
|
||
|
||
if (refresh) {
|
||
list.value = items;
|
||
} else {
|
||
list.value = [...list.value, ...items];
|
||
}
|
||
|
||
total.value = res.data.total || 0;
|
||
hasMore.value = res.data.has_next || false;
|
||
}
|
||
|
||
} else if (currentTab.value === 'zeji') {
|
||
res = await baziZejiApi.getBaziZejiList({
|
||
page_no: pageNo.value,
|
||
page_size: pageSize,
|
||
});
|
||
|
||
if (res && res.data) {
|
||
const items = Array.isArray(res.data.data) ? res.data.data : [];
|
||
|
||
if (refresh) {
|
||
list.value = items;
|
||
} else {
|
||
list.value = [...list.value, ...items];
|
||
}
|
||
|
||
total.value = res.data.total || 0;
|
||
hasMore.value = res.data.has_next || false;
|
||
}
|
||
}
|
||
|
||
pageNo.value++;
|
||
|
||
} catch (error) {
|
||
console.error('加载失败:', error);
|
||
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 点击加载下一页
|
||
const loadNextPage = () => {
|
||
if (!loading.value && hasMore.value) {
|
||
loadList();
|
||
}
|
||
};
|
||
|
||
// 刷新
|
||
const handleRefresh = async () => {
|
||
if (loading.value) return;
|
||
resetPagination();
|
||
await loadList(true);
|
||
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 });
|
||
};
|
||
|
||
// 切换Tab(依赖 resetPagination、loadList,须放在二者之后)
|
||
const switchTab = (tab: 'naming' | 'affinity' | 'zeji') => {
|
||
if (currentTab.value === tab) return;
|
||
currentTab.value = tab;
|
||
resetPagination();
|
||
loadList(true);
|
||
};
|
||
|
||
watch(
|
||
() => props.focusListTab,
|
||
(v) => {
|
||
if (!v) return;
|
||
if (v === currentTab.value) {
|
||
emit('focusListTabConsumed');
|
||
return;
|
||
}
|
||
switchTab(v);
|
||
emit('focusListTabConsumed');
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// 查看名字方案详情
|
||
const viewNamingDetail = async (item: MyReportItem) => {
|
||
|
||
|
||
if (!item.has_solutions) {
|
||
uni.showToast({ title: "方案生成中,请稍后查看", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
const parseMaybeJson = (value: any) => {
|
||
if (value == null) return value;
|
||
if (typeof value === "object") return value;
|
||
if (typeof value !== "string") return value;
|
||
const raw = value.trim();
|
||
if (!raw) return value;
|
||
try {
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
return value;
|
||
}
|
||
};
|
||
|
||
try {
|
||
uni.showLoading({ title: "加载中..." });
|
||
|
||
const solutionsResult = await namingApi.getSolutionsByReportId(item.id);
|
||
|
||
const solutions = solutionsResult?.solutions || solutionsResult?.items || solutionsResult;
|
||
|
||
// 新规则:接口返回 solutions 数组且有数据 -> 跳转到新的方案列表页
|
||
if (Array.isArray(solutions) && solutions.length > 0) {
|
||
uni.hideLoading();
|
||
emit("showNamingSolutionsList", {
|
||
reportId: item.id,
|
||
solutions,
|
||
category: item.category,
|
||
serviceType: String(item.service_type || ''),
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 否则:回退到旧版详情页(接口可能直接返回详情对象)
|
||
const parsedLegacy = parseMaybeJson(solutionsResult);
|
||
uni.hideLoading();
|
||
if (parsedLegacy && typeof parsedLegacy === "object") {
|
||
emit("showDetail", parsedLegacy as any, item.category, String(item.service_type || ''));
|
||
return;
|
||
}
|
||
|
||
uni.showToast({ title: "暂无方案数据", icon: "none" });
|
||
return;
|
||
|
||
} catch (e: any) {
|
||
uni.hideLoading();
|
||
console.error('Error in viewNamingDetail:', e);
|
||
uni.showToast({ title: e.msg || e.message || "获取详情失败", icon: "none" });
|
||
}
|
||
};
|
||
|
||
// 查看缘分合盘详情
|
||
const viewAffinityDetail = async (item: AffinityListItem) => {
|
||
if (item.task_status !== 5) {
|
||
uni.showToast({ title: "生成中,请稍后查看", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
uni.showLoading({ title: "加载中..." });
|
||
const detailData = await affinityApi.getAffinityDetail(item.id);
|
||
uni.hideLoading();
|
||
|
||
if (!detailData) {
|
||
uni.showToast({ title: "获取详情失败", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
const transformedData = {
|
||
relationship: detailData.relationship,
|
||
relationshipLabel: getRelationshipLabel(detailData.relationship),
|
||
person1: {
|
||
name: detailData.person1_name,
|
||
gender: detailData.person1_gender,
|
||
birthDate: detailData.person1_birth_date,
|
||
},
|
||
person2: {
|
||
name: detailData.person2_name,
|
||
gender: detailData.person2_gender,
|
||
birthDate: detailData.person2_birth_date,
|
||
},
|
||
score: detailData.score,
|
||
scoreBadge: detailData.score_badge,
|
||
sixDimension: detailData.six_dimension ? JSON.parse(detailData.six_dimension) : null,
|
||
radarDesc: detailData.radar_desc,
|
||
analysisCards: detailData.analysis_cards ? JSON.parse(detailData.analysis_cards).map((card: any, index: number) => ({
|
||
id: `card-${index}`,
|
||
icon: ['💗', '💬', '🛡️', '⚡', '🎯', '🏠'][index] || '✨',
|
||
iconBg: ['rgba(236, 72, 153, 0.2)', 'rgba(59, 130, 246, 0.2)', 'rgba(34, 197, 94, 0.2)', 'rgba(234, 179, 8, 0.2)', 'rgba(168, 85, 247, 0.2)', 'rgba(249, 115, 22, 0.2)'][index] || 'rgba(156, 163, 175, 0.2)',
|
||
title: card.title,
|
||
score: String(card.score),
|
||
summary: card.content.substring(0, 50) + '...',
|
||
content: card.content,
|
||
})) : [],
|
||
unlocked: detailData.unlocked_content ? JSON.parse(detailData.unlocked_content) : null,
|
||
isUnlocked: detailData.is_unlocked === 1,
|
||
unlockPrice: detailData.unlock_price,
|
||
unlockStats: {
|
||
unlockCount: 12392,
|
||
accuracy: '98%',
|
||
},
|
||
};
|
||
|
||
emit('showAffinityResult', transformedData, true);
|
||
};
|
||
|
||
// 查看八字择吉详情
|
||
const viewZejiDetail = async (item: BaziZejiListItem) => {
|
||
uni.showLoading({ title: "加载中..." });
|
||
const detailData = await baziZejiApi.getBaziZejiDetail(item.id);
|
||
uni.hideLoading();
|
||
|
||
if (!detailData) {
|
||
uni.showToast({ title: "获取详情失败", icon: "none" });
|
||
return;
|
||
}
|
||
|
||
emit('showAuspiciousResult', detailData, true);
|
||
};
|
||
|
||
// 工具函数:相对时间(刚刚 / N分钟前 / N小时前 / …)
|
||
const formatDate = (dateStr: string) => {
|
||
if (!dateStr) return '';
|
||
const date = new Date(dateStr);
|
||
if (Number.isNaN(date.getTime())) return String(dateStr).trim();
|
||
|
||
const now = Date.now();
|
||
const diff = now - date.getTime();
|
||
if (diff < 0) {
|
||
const y = date.getFullYear();
|
||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
const h = String(date.getHours()).padStart(2, '0');
|
||
const min = String(date.getMinutes()).padStart(2, '0');
|
||
return `${y}-${m}-${d} ${h}:${min}`;
|
||
}
|
||
|
||
const minutes = Math.floor(diff / (1000 * 60));
|
||
if (minutes < 1) return '刚刚';
|
||
if (minutes < 60) return `${minutes}分钟前`;
|
||
|
||
const hours = Math.floor(minutes / 60);
|
||
if (hours < 24) return `${hours}小时前`;
|
||
|
||
const days = Math.floor(hours / 24);
|
||
if (days === 1) return '昨天';
|
||
if (days < 7) return `${days}天前`;
|
||
if (days < 30) return `${Math.floor(days / 7)}周前`;
|
||
if (days < 365) return `${Math.floor(days / 30)}月前`;
|
||
return `${Math.floor(days / 365)}年前`;
|
||
};
|
||
|
||
const getRelationshipLabel = (relationship: string) => {
|
||
const labels: Record<string, string> = {
|
||
'couple': '恋人',
|
||
'married': '夫妻',
|
||
'crush': '暗恋',
|
||
'partner': '合作伙伴',
|
||
'friend': '朋友',
|
||
'family': '家人',
|
||
};
|
||
return labels[relationship] || relationship;
|
||
};
|
||
|
||
const getTaskStatusIcon = (status: number) => {
|
||
if (status === 5) return '✓';
|
||
if (status === 0) return '⏱';
|
||
return '⏱';
|
||
};
|
||
|
||
const getTaskStatusText = (status: number) => {
|
||
if (status === 5) return '已生成';
|
||
if (status === 0) return '生成中';
|
||
return '生成中';
|
||
};
|
||
|
||
const getEmptyText = () => {
|
||
if (currentTab.value === 'naming') return '暂无起名方案';
|
||
if (currentTab.value === 'affinity') return '暂无缘分合盘记录';
|
||
if (currentTab.value === 'zeji') return '暂无八字择吉记录';
|
||
return '暂无数据';
|
||
};
|
||
|
||
const getEmptyButtonText = () => {
|
||
if (currentTab.value === 'naming') return '去起名';
|
||
if (currentTab.value === 'affinity') return '去测算';
|
||
if (currentTab.value === 'zeji') return '去择吉';
|
||
return '去测算';
|
||
};
|
||
|
||
const handleEmptyAction = () => {
|
||
if (currentTab.value === 'naming') {
|
||
emit('navigate', 'naming');
|
||
} else if (currentTab.value === 'affinity') {
|
||
emit('navigate', 'affinity');
|
||
} else if (currentTab.value === 'zeji') {
|
||
emit('navigate', 'calendar');
|
||
}
|
||
};
|
||
|
||
const handleBack = () => {
|
||
emit('back');
|
||
};
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
loadList(true);
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.plans-screen {
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f0efe9;
|
||
position: relative;
|
||
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||
}
|
||
|
||
.status-bar-placeholder {
|
||
height: var(--status-bar-height, 0);
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.plans-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
pointer-events: none;
|
||
opacity: 0.3;
|
||
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
}
|
||
|
||
.plans-header {
|
||
position: sticky;
|
||
top: var(--status-bar-height, 0);
|
||
z-index: 100;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 32rpx;
|
||
border-bottom: 1rpx solid #dcd3c9;
|
||
background-color: rgba(253, 251, 247, 0.95);
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.plans-back-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
margin-left: -16rpx;
|
||
}
|
||
|
||
.plans-back-icon {
|
||
font-size: 48rpx;
|
||
color: #5a5a5a;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.plans-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #2c2c2c;
|
||
letter-spacing: 0.2em;
|
||
}
|
||
|
||
.plans-refresh-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
margin-right: -16rpx;
|
||
}
|
||
|
||
.plans-refresh-icon {
|
||
font-size: 40rpx;
|
||
color: #5a5a5a;
|
||
font-weight: 300;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.plans-refresh-icon.rotating {
|
||
animation: rotate 1s linear infinite;
|
||
}
|
||
|
||
.plans-tabs {
|
||
position: sticky;
|
||
top: calc(var(--status-bar-height, 0) + 88rpx);
|
||
z-index: 99;
|
||
display: flex;
|
||
background-color: rgba(253, 251, 247, 0.95);
|
||
border-bottom: 1rpx solid #dcd3c9;
|
||
padding: 0 32rpx;
|
||
backdrop-filter: blur(10rpx);
|
||
}
|
||
|
||
.plans-tab {
|
||
flex: 1;
|
||
padding: 24rpx 0;
|
||
text-align: center;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.plans-tab-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.plans-tab-active .plans-tab-text {
|
||
color: #8b2323;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.plans-tab-active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 48rpx;
|
||
height: 4rpx;
|
||
background-color: #8b2323;
|
||
border-radius: 2rpx;
|
||
}
|
||
|
||
.plans-scroll {
|
||
flex: 1;
|
||
height: calc(100vh - var(--status-bar-height, 0) - 88rpx - 76rpx);
|
||
}
|
||
|
||
.plans-content {
|
||
padding: 32rpx;
|
||
}
|
||
|
||
.initial-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 400rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.list-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
background-color: #fffdf9;
|
||
padding: 32rpx;
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid #e5e5e5;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||
margin-bottom: 32rpx;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.list-item:active {
|
||
transform: scale(0.98);
|
||
background-color: #f8f6f2;
|
||
}
|
||
|
||
.item-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-right: 32rpx;
|
||
}
|
||
|
||
.icon-red {
|
||
background-color: rgba(139, 35, 35, 0.1);
|
||
}
|
||
|
||
.icon-gold {
|
||
background-color: rgba(212, 175, 55, 0.1);
|
||
}
|
||
|
||
.icon-pink {
|
||
background-color: rgba(236, 72, 153, 0.1);
|
||
}
|
||
|
||
.icon-purple {
|
||
background-color: rgba(147, 51, 234, 0.1);
|
||
}
|
||
|
||
.icon-text {
|
||
font-size: 36rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.icon-red .icon-text {
|
||
color: #8b2323;
|
||
}
|
||
|
||
.icon-gold .icon-text {
|
||
color: #d4af37;
|
||
}
|
||
|
||
.icon-pink .icon-text {
|
||
color: #ec4899;
|
||
}
|
||
|
||
.icon-purple .icon-text {
|
||
color: #9333ea;
|
||
}
|
||
|
||
.item-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.item-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #2c2c2c;
|
||
flex: 1;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.item-date {
|
||
font-size: 20rpx;
|
||
color: #999;
|
||
background-color: #f5f5f5;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.item-subtitle {
|
||
font-size: 24rpx;
|
||
color: #5a5a5a;
|
||
margin-bottom: 24rpx;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.item-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.item-tag {
|
||
font-size: 20rpx;
|
||
padding: 2rpx 12rpx;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.tag-red {
|
||
border-color: rgba(139, 35, 35, 0.3);
|
||
color: #8b2323;
|
||
}
|
||
|
||
.tag-gold {
|
||
border-color: rgba(212, 175, 55, 0.3);
|
||
color: #d4af37;
|
||
}
|
||
|
||
.tag-pink {
|
||
border-color: rgba(236, 72, 153, 0.3);
|
||
color: #ec4899;
|
||
}
|
||
|
||
.tag-purple {
|
||
border-color: rgba(147, 51, 234, 0.3);
|
||
color: #9333ea;
|
||
}
|
||
|
||
.item-status {
|
||
font-size: 20rpx;
|
||
color: #ccc;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 20rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.item-arrow {
|
||
align-self: center;
|
||
margin-left: 16rpx;
|
||
}
|
||
|
||
.arrow-icon {
|
||
font-size: 32rpx;
|
||
color: #ccc;
|
||
}
|
||
|
||
.load-more-wrapper {
|
||
padding: 32rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.loading-more {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.load-more-btn {
|
||
background: #8b2323;
|
||
color: white;
|
||
padding: 20rpx 40rpx;
|
||
border-radius: 12rpx;
|
||
display: inline-block;
|
||
}
|
||
|
||
.load-more-text {
|
||
font-size: 28rpx;
|
||
color: white;
|
||
}
|
||
|
||
.no-more {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.no-more-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 500rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 96rpx;
|
||
opacity: 0.3;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.empty-btn {
|
||
background-color: #8b2323;
|
||
padding: 16rpx 64rpx;
|
||
border-radius: 8rpx;
|
||
border: none;
|
||
}
|
||
|
||
.empty-btn-text {
|
||
font-size: 28rpx;
|
||
color: white;
|
||
}
|
||
|
||
@keyframes rotate {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style>
|
||
|