Files
----/前端源码/uni-app/components/screens/ProfileFavorites.vue

557 lines
12 KiB
Vue
Raw Permalink 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.
<template>
<view class="favorites-screen">
<view class="favorites-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="favorites-header">
<view class="favorites-back-btn" @click="handleBack">
<text class="favorites-back-icon"></text>
</view>
<text class="favorites-title">我的收藏</text>
<view class="favorites-header-placeholder"></view>
</view>
<!-- Tabs -->
<view class="favorites-tabs">
<view class="favorites-tabs-container">
<view v-for="tab in tabs" :key="tab.value" class="favorites-tab-btn"
:class="{ 'favorites-tab-btn-active': activeTab === tab.value }" @click="switchTab(tab.value)">
<text class="favorites-tab-text" :class="{ 'favorites-tab-text-active': activeTab === tab.value }">
{{ tab.label }}
</text>
</view>
</view>
</view>
<!-- List -->
<scroll-view scroll-y class="favorites-list" @scrolltolower="loadMore">
<view class="favorites-list-inner">
<view v-if="loading && filteredList.length === 0" class="favorites-loading">
<text class="favorites-loading-text">加载中...</text>
</view>
<template v-else-if="filteredList.length > 0">
<view v-for="(item, index) in filteredList" :key="item.id" class="favorites-item"
:style="{ animationDelay: (index * 0.05) + 's' }" @click="viewDetail(item)">
<!-- Left Border Accent -->
<view class="favorites-item-border"
:class="item.category === 'company' ? 'favorites-item-border-gold' : 'favorites-item-border-red'"></view>
<view class="favorites-item-main">
<view class="favorites-item-header">
<text class="favorites-item-name">{{ item.name || '未命名' }}</text>
<view class="favorites-item-type-icon">
<text class="favorites-item-type-text">{{ item.category === 'company' ? '企' : '名' }}</text>
</view>
</view>
<text v-if="item.pinyin" class="favorites-item-pinyin">{{ item.pinyin }}</text>
<view class="favorites-item-bottom">
<view class="favorites-item-tags">
<text v-for="(tag, idx) in item.tags" :key="idx" class="favorites-item-tag"
:class="item.category === 'company' ? 'favorites-item-tag-gold' : 'favorites-item-tag-red'">
{{ tag }}
</text>
</view>
<text class="favorites-item-date-inline">{{ formatDate(item.created_time) }}</text>
</view>
</view>
<view class="favorites-item-right">
<view class="favorites-item-heart">
<text class="favorites-item-heart-icon"></text>
</view>
</view>
</view>
<view v-if="hasMore" class="favorites-load-more">
<text class="favorites-load-more-text">{{ loading ? '加载中...' : '上拉加载更多' }}</text>
</view>
<view v-else class="favorites-no-more">
<text class="favorites-no-more-text"> 已加载全部 </text>
</view>
</template>
<template v-else>
<view class="favorites-empty">
<text class="favorites-empty-icon"></text>
<text class="favorites-empty-text">暂无收藏</text>
</view>
</template>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { userApi } from "@/api";
import type { MyFavoriteItem } from "@/api/types";
declare const uni: any;
const emit = defineEmits<{
back: [];
navigate: [screen: string];
showDetail: [data: any, category?: string, serviceType?: string];
}>();
const loading = ref(false);
const list = ref<MyFavoriteItem[]>([]);
const pageNo = ref(1);
const pageSize = 10;
const total = ref(0);
const hasMore = ref(true);
const activeTab = ref<'all' | 'personal' | 'company'>('all');
const tabs = [
{ value: 'all' as const, label: '全部' },
{ value: 'personal' as const, label: '个人名' },
{ value: 'company' as const, label: '商号' },
];
// 根据tab过滤列表
const filteredList = computed(() => {
if (activeTab.value === 'all') return list.value;
return list.value.filter(item => item.category === activeTab.value);
});
const loadList = async (refresh = false) => {
if (loading.value) return;
if (!refresh && !hasMore.value) return;
loading.value = true;
try {
if (refresh) {
pageNo.value = 1;
list.value = [];
}
const res = await userApi.getMyFavorites({
page_no: pageNo.value,
page_size: pageSize,
category: activeTab.value === 'all' ? undefined : activeTab.value,
});
console.log('getMyFavorites response:', res);
const items = res?.items || (Array.isArray(res) ? res : []);
list.value = refresh ? items : [...list.value, ...items];
total.value = res?.total || items.length;
hasMore.value = list.value.length < total.value;
pageNo.value++;
} catch (e: any) {
console.error('loadList error:', e);
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
} finally {
loading.value = false;
}
};
const switchTab = (tab: 'all' | 'personal' | 'company') => {
activeTab.value = tab;
loadList(true);
};
const loadMore = () => {
if (!loading.value && hasMore.value) loadList();
};
const parseMaybeJson = (value: any): 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;
}
};
const viewDetail = async (item: MyFavoriteItem) => {
if (!item.solution_id) {
uni.showToast({ title: "方案ID不存在", icon: "none" });
return;
}
try {
const detailData = await userApi.getSolutionDetail(item.solution_id);
const parsedData = parseMaybeJson(detailData);
const serviceType = String((item as any)?.service_type || '');
emit('showDetail', parsedData, item.category, serviceType);
} catch (e: any) {
uni.showToast({ title: e.msg || "加载详情失败", icon: "none" });
}
};
const handleBack = () => {
emit('back');
};
const formatDate = (dateStr: string) => {
if (!dateStr) return "";
// 处理 created_time 格式 "2026-01-13"
return dateStr.replace(/-/g, ".");
};
onMounted(() => loadList(true));
</script>
<style scoped>
.favorites-screen {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
}
/* 强制所有容器全宽 */
:deep(*) {
box-sizing: border-box;
}
:deep(.shell) {
width: 100% !important;
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.favorites-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");
}
/* Header */
.favorites-header {
position: relative;
z-index: 10;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(5px);
}
.favorites-back-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -8px;
}
.favorites-back-icon {
font-size: 24px;
color: #5a5a5a;
font-weight: 300;
}
.favorites-title {
font-size: 16px;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.favorites-header-placeholder {
width: 32px;
}
/* Tabs */
.favorites-tabs {
position: relative;
z-index: 10;
padding: 12px 16px;
background-color: #fdfbf7;
}
.favorites-tabs-container {
display: flex;
background-color: #f0efe9;
padding: 4px;
border-radius: 8px;
}
.favorites-tab-btn {
flex: 1;
padding: 8px 0;
text-align: center;
border-radius: 6px;
transition: all 0.3s;
}
.favorites-tab-btn-active {
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.favorites-tab-text {
font-size: 12px;
font-weight: 700;
color: #999;
}
.favorites-tab-text-active {
color: #8b2323;
}
/* List */
.favorites-list {
flex: 1;
height: 0;
position: relative;
z-index: 10;
margin-top: 8px;
}
.favorites-list-inner {
padding: 16px;
padding-top: 0;
}
.favorites-item {
position: relative;
display: flex;
align-items: center;
background-color: #fffdf9;
padding: 16px;
border-radius: 12px;
border: 1px solid #e5e5e5;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
animation: fadeInUp 0.3s ease-out forwards;
opacity: 0;
overflow: hidden;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.favorites-item-border {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
}
.favorites-item-border-red {
background-color: #8b2323;
}
.favorites-item-border-gold {
background-color: #d4af37;
}
.favorites-item-main {
flex: 1;
min-width: 0;
padding-left: 8px;
}
.favorites-item-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.favorites-item-name {
font-size: 18px;
font-weight: 700;
color: #2c2c2c;
font-family: "SimSun", "Songti SC", serif;
}
.favorites-item-type-icon {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.favorites-item-type-text {
font-size: 9px;
color: #999;
}
.favorites-item-pinyin {
font-size: 11px;
color: #999;
margin-bottom: 8px;
display: block;
font-style: italic;
}
.favorites-item-bottom {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.favorites-item-category {
font-size: 11px;
color: #999;
margin-bottom: 8px;
display: block;
}
.favorites-item-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
flex: 1;
}
.favorites-item-tag {
font-size: 9px;
padding: 3px 8px;
border-radius: 4px;
background-color: #f5f5f5;
color: #5a5a5a;
white-space: nowrap;
}
.favorites-item-tag-red {
background-color: rgba(139, 35, 35, 0.05);
color: #8b2323;
}
.favorites-item-tag-gold {
background-color: rgba(212, 175, 55, 0.05);
color: #d4af37;
}
.favorites-item-date-inline {
font-size: 9px;
color: #ccc;
display: flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
margin-left: 8px;
}
.favorites-item-right {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
margin-left: 12px;
}
.favorites-item-date {
font-size: 9px;
color: #ccc;
display: flex;
align-items: center;
}
.favorites-item-heart {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.favorites-item-heart-icon {
font-size: 16px;
color: #8b2323;
}
.favorites-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.favorites-loading-text {
font-size: 14px;
color: #999;
}
.favorites-load-more,
.favorites-no-more {
text-align: center;
padding: 16px 0;
}
.favorites-load-more-text,
.favorites-no-more-text {
font-size: 12px;
color: #999;
}
.favorites-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 250px;
}
.favorites-empty-icon {
font-size: 48px;
opacity: 0.3;
margin-bottom: 12px;
}
.favorites-empty-text {
font-size: 14px;
color: #999;
margin-bottom: 16px;
}
.favorites-empty-btn {
background-color: #8b2323;
padding: 10px 32px;
border-radius: 4px;
border: none;
}
.favorites-empty-btn-text {
font-size: 14px;
color: #d4af37;
font-weight: 600;
}
</style>