Files
spring-festival-greetings/pages/mine/avatar.vue
2026-01-28 23:48:18 +08:00

564 lines
12 KiB
Vue
Raw 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="avatar-page" :style="{ paddingTop: navBarTop + 'px' }">
<!-- Navbar -->
<view
class="nav-bar"
:style="{ height: navBarHeight + 'px', paddingTop: navBarTop + 'px' }"
>
<view class="nav-content">
<view class="back-btn" @tap="goBack">
<text class="back-arrow"></text>
</view>
<view class="title-wrap">
<text class="title">我的制作记录</text>
<view class="more-btn"></view>
</view>
</view>
</view>
<!-- Current Avatar Card -->
<view class="current-card" v-if="currentAvatar">
<view class="avatar-preview">
<view class="ring-bg"></view>
<image
:src="currentAvatar.imageUrl"
mode="aspectFill"
class="avatar-img"
/>
<image
v-if="currentAvatar.decorUrl"
:src="currentAvatar.decorUrl"
mode="widthFix"
class="decor-img"
/>
</view>
<view class="status-info">
<view class="status-title">当前正在使用</view>
<view class="decor-name">
<text class="star-icon"></text>
<text>{{ currentAvatar.decorName || "金马贺岁挂饰" }}</text>
</view>
</view>
<view class="change-btn" @tap="changeUserAvatar(currentAvatar.imageUrl)">
<text>替换头像</text>
</view>
</view>
<!-- History List Header -->
<view class="list-header">
<view class="title-block">
<view class="red-line"></view>
<text class="section-title">历史制作记录</text>
</view>
<text class="count-text"> {{ totalCount }} 件作品</text>
</view>
<!-- Grid List -->
<view class="list-container">
<view
v-for="item in list"
:key="item.id"
class="grid-item"
:class="{ active: item.id === currentAvatar?.id }"
@tap="selectAvatar(item)"
>
<view class="img-box">
<image :src="item.imageUrl" mode="aspectFill" class="item-img" />
<view class="active-badge" v-if="item.id === currentAvatar?.id">
<text class="check-icon"></text>
<text>当前使用</text>
</view>
</view>
<view class="item-info">
<text class="item-name">{{
item.decorName || getDefaultName(item)
}}</text>
<text class="item-date">{{ formatDate(item.createdAt) }} 制作</text>
</view>
</view>
</view>
<!-- Loading State -->
<view class="loading-state" v-if="loading">
<text>加载中...</text>
</view>
<view class="empty-state" v-if="!loading && list.length === 0">
<text>暂无头像记录</text>
</view>
<view class="no-more" v-if="!loading && !hasMore && list.length > 0">
<text>没有更多了</text>
</view>
<!-- Bottom Action -->
<view class="bottom-action">
<text class="explore-text">EXPLORE MORE STYLES</text>
<view class="action-btn" @tap="goToMake">
<view class="btn-icon">🧭</view>
<text>去发现更多精美挂饰</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
import { getMyAvatar, userAvatarChange } from "@/api/mine.js";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const navBarTop = ref(0);
const navBarHeight = ref(44);
const list = ref([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const isRefreshing = ref(false);
const totalCount = ref(0);
const currentAvatar = ref(null);
// Default names for fallback
const names = [
"金马贺岁",
"同心如意",
"爆竹一声",
"红火灯笼",
"财源滚滚",
"锦鲤附体",
];
onMounted(() => {
const sysInfo = uni.getSystemInfoSync();
navBarTop.value = sysInfo.statusBarHeight;
fetchList(true);
});
onPullDownRefresh(() => {
onRefresh();
});
onReachBottom(() => {
loadMore();
});
const fetchList = async (reset = false) => {
if (loading.value) return;
if (reset) {
page.value = 1;
hasMore.value = true;
}
if (!hasMore.value) return;
loading.value = true;
try {
const res = await getMyAvatar(page.value);
const dataList = res?.list || [];
totalCount.value = res?.totalCount || 0;
if (reset) {
list.value = dataList;
// Assume the first one is current for demo if not specified
if (dataList.length > 0) {
currentAvatar.value = dataList[0];
}
} else {
list.value = [...list.value, ...dataList];
}
hasMore.value = res.hasNext;
if (hasMore.value) {
page.value++;
}
} catch (e) {
console.error("Failed to fetch avatar list", e);
uni.showToast({ title: "加载失败", icon: "none" });
} finally {
loading.value = false;
isRefreshing.value = false;
uni.stopPullDownRefresh();
}
};
const changeUserAvatar = async (imageUrl) => {
const res = await userAvatarChange(imageUrl);
if (res.success) {
userStore.setUserInfo({
...userStore.userInfo,
avatarUrl: imageUrl,
});
uni.showToast({
title: "头像更换成功",
icon: "success",
});
}
};
const loadMore = () => {
fetchList();
};
const onRefresh = () => {
isRefreshing.value = true;
fetchList(true);
};
const goBack = () => {
uni.navigateBack();
};
const goToMake = () => {
// Replace with actual route to avatar maker
uni.navigateTo({
url: "/pages/avatar/index",
});
};
const selectAvatar = async (item) => {
currentAvatar.value = item;
};
const formatDate = (dateStr) => {
if (!dateStr) return "";
const date = new Date(dateStr);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
};
const getDefaultName = (item) => {
// Simple deterministic name generation based on ID char code sum
if (!item.id) return "新春头像";
let sum = 0;
for (let i = 0; i < item.id.length; i++) {
sum += item.id.charCodeAt(i);
}
return names[sum % names.length];
};
</script>
<style lang="scss" scoped>
.avatar-page {
min-height: 100vh;
background: #f9f9f9;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding-bottom: 40px;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background-color: #f9f9f9;
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between; // Changed to space-between
padding: 0 16px; // Added padding
.back-btn {
height: 100%;
display: flex;
align-items: center;
padding-right: 20px;
.back-arrow {
font-size: 32px;
color: #333;
font-weight: 300;
}
}
.title-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-right: 32px; // Balance the back button space
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.more-btn {
position: absolute;
right: -20px;
font-size: 20px;
color: #333;
display: none; // Hide for now as per design mockup clean look
}
}
}
}
.current-card {
margin: 60px 20px 20px;
background: #fff;
border-radius: 24px;
padding: 24px;
display: flex;
align-items: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
// Background decoration (faint glow)
&::before {
content: "";
position: absolute;
top: -50%;
right: -20%;
width: 200px;
height: 200px;
background: radial-gradient(
circle,
rgba(255, 59, 48, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
border-radius: 50%;
pointer-events: none;
}
.avatar-preview {
position: relative;
width: 80px;
height: 80px;
margin-right: 16px;
.ring-bg {
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid #ffd700;
border-radius: 50%;
opacity: 0.5;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.decor-img {
position: absolute;
bottom: 0;
right: 0;
width: 40px;
height: 40px;
}
}
.status-info {
flex: 1;
.status-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.decor-name {
display: flex;
align-items: center;
font-size: 12px;
color: #ff3b30;
.star-icon {
margin-right: 4px;
font-size: 10px;
}
}
}
.change-btn {
background: #ff3b30;
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(255, 59, 48, 0.3);
&:active {
transform: scale(0.96);
}
}
}
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
margin-bottom: 16px;
.title-block {
display: flex;
align-items: center;
.red-line {
width: 4px;
height: 16px;
background: #ff3b30;
border-radius: 2px;
margin-right: 8px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.count-text {
font-size: 12px;
color: #999;
}
}
.list-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 0 20px;
}
.grid-item {
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: column;
transition: all 0.2s;
&.active {
box-shadow: 0 0 0 2px #ff3b30;
.img-box {
.active-badge {
display: flex;
}
}
}
.img-box {
position: relative;
width: 100%;
aspect-ratio: 1;
background: #f5f5f5;
/* padding: 20px; Removed padding to make it full square */
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
.item-img {
width: 100%;
height: 100%;
/* border-radius: 8px; Removed border-radius */
}
.active-badge {
position: absolute;
bottom: 8px;
right: 8px;
background: #ffb300;
color: #fff;
font-size: 10px;
padding: 2px 8px;
border-radius: 10px;
display: none;
align-items: center;
gap: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.check-icon {
font-size: 8px;
}
}
}
.item-info {
padding: 12px;
text-align: center;
.item-name {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.item-date {
font-size: 10px;
color: #999;
}
}
}
.loading-state,
.empty-state,
.no-more {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
.bottom-action {
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding-bottom: 40px;
.explore-text {
font-size: 10px;
letter-spacing: 2px;
color: #999;
text-transform: uppercase;
}
.action-btn {
background: linear-gradient(135deg, #ff3b30, #ff2d55);
color: #fff;
padding: 14px 32px;
border-radius: 30px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 8px 20px rgba(255, 59, 48, 0.3);
font-size: 16px;
font-weight: 600;
.btn-icon {
font-size: 18px;
}
&:active {
transform: scale(0.98);
box-shadow: 0 4px 10px rgba(255, 59, 48, 0.2);
}
}
}
</style>