upload project source code
This commit is contained in:
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal file
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal file
@@ -0,0 +1,556 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user