Files
happy-life-star/mini-program/src/pages/main/RecordView.vue
T
peanut 60c63850ee feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面
- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s
- 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突
- 新增人生事件(life-event)模块:表单和详情页面
- 新增 EpicScript 灵感接口(Controller/Service/DTO)
- 优化登录、引导、主页、记录、剧本详情等多个页面
- 优化服务管理脚本和 Nginx 配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:38:35 +08:00

754 lines
16 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="record-view">
<view class="profile-card kos-card">
<view class="profile-top">
<view class="avatar-wrap">
<image class="avatar" :src="avatar" mode="aspectFill" />
<view class="avatar-edit" @click="editProfile">
<view class="edit-pen"></view>
</view>
</view>
<view class="profile-info">
<view class="name-row">
<text class="profile-name">{{ profile.nickname || 'Zoey' }}</text>
<text class="star"></text>
</view>
<text class="profile-subtitle">正在成为更清晰的自己</text>
</view>
<view class="edit-profile kos-pill" @click="editProfile">
<view class="tiny-pen"></view>
<text>编辑资料</text>
</view>
</view>
<view class="profile-divider"></view>
<view class="meta-grid">
<view class="meta-item">
<text class="meta-icon"></text>
<view>
<text class="meta-label">星座</text>
<text class="meta-value">{{ profile.zodiac || '巨蟹座' }}</text>
</view>
</view>
<view class="meta-item">
<view class="person-icon"></view>
<view>
<text class="meta-label">MBTI</text>
<text class="meta-value">{{ profile.mbti || 'ENTJ' }}</text>
</view>
</view>
<view class="meta-item no-border">
<view class="job-icon"></view>
<view>
<text class="meta-label">职业</text>
<text class="meta-value">{{ profile.profession || '产品经理' }}</text>
</view>
</view>
</view>
<view class="profile-divider small"></view>
<view class="hobby-row">
<text class="heart"></text>
<view>
<text class="meta-label">爱好</text>
<text class="hobby-text">{{ heroTags.join(' · ') }}</text>
</view>
</view>
</view>
<view class="section-head">
<view>
<view class="title-line">
<text class="section-title">人生轨迹</text>
<text class="star gold"></text>
</view>
<text class="section-subtitle">你的成长之路正在展开</text>
</view>
<view class="map-btn kos-pill">
<view class="map-icon"></view>
<text>轨迹地图</text>
</view>
</view>
<scroll-view class="filters" scroll-x :show-scrollbar="false">
<view class="filter-row">
<text
v-for="filter in filters"
:key="filter.value"
class="filter-chip kos-pill"
:class="{ active: activeFilter === filter.value }"
@click="activeFilter = filter.value"
>{{ filter.label }}</text>
<text class="add-filter"></text>
</view>
</scroll-view>
<view v-if="displayEvents.length" class="timeline">
<view v-for="(event, index) in displayEvents" :key="event.id || index" class="timeline-item" @click="openDetail(event)">
<view class="rail">
<view class="node" :class="'node-' + (index % 4)">
<view></view>
</view>
<view class="line" :class="'line-' + (index % 4)"></view>
</view>
<view class="event-card kos-card">
<view class="date-box">
<text class="date-month">{{ getMonth(event.time) }}</text>
<text class="date-age">{{ getAgeText(event.time) }}</text>
</view>
<view class="event-divider"></view>
<view class="event-body">
<text class="event-title">{{ event.title }}</text>
<text class="event-tag" :class="'tag-' + (index % 4)">{{ getEventTag(event) }}</text>
<text class="event-summary">{{ event.content }}</text>
</view>
<view class="thumb" :class="'thumb-' + (index % 4)">
<text>{{ getEventInitial(event) }}</text>
</view>
<text class="chevron"></text>
</view>
</view>
</view>
<view v-else class="empty-card kos-card">
<text class="empty-title">还没有人生轨迹</text>
<text class="empty-text">记录第一个重要时刻让数字生命档案开始发光</text>
</view>
<view class="create-fab" @click="createEvent">
<view class="plus-core"></view>
<text>记录人生经历</text>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const activeFilter = ref('all')
const filters = [
{ label: '全部', value: 'all' },
{ label: '童年', value: 'childhood' },
{ label: '高光', value: 'highlight' },
{ label: '低谷', value: 'valley' },
{ label: '美好的瞬间', value: 'daily_log' }
]
const sampleEvents = [
{
id: 'sample-1',
title: '决定全职做自己热爱的AI产品',
time: '2025-05-20',
content: '辞去稳定的工作,孤注一掷地投入到AI产品的创业中,每天都充满挑战...',
eventType: 'highlight',
tags: ['高光']
},
{
id: 'sample-2',
title: '一段关系的结束',
time: '2024-11-12',
content: '这段关系让我成长了很多,虽然很痛,但也让我更清楚自己想要什么...',
eventType: 'valley',
tags: ['低谷']
},
{
id: 'sample-3',
title: '第一次独自旅行',
time: '2024-06-08',
content: '去了大理,遇见了很多有趣的人和事。那段时间真正感受到了自由和快乐...',
eventType: 'daily_log',
tags: ['美好的瞬间']
},
{
id: 'sample-4',
title: '考上理想的高中',
time: '2016-09-01',
content: '那天拿到录取通知书,爸妈为我开心地庆祝,我暗下决心要更加努力...',
eventType: 'childhood',
tags: ['童年']
}
]
const profile = computed(() => store.userProfile || store.registrationData || {})
const events = computed(() => {
const list = store.events || []
return list.length ? list : sampleEvents
})
const heroTags = computed(() => {
const hobbies = profile.value.hobbies
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4)
return ['阅读', '旅行', '音乐', '创作']
})
const avatar = computed(() => {
const nickname = profile.value.nickname || 'Zoey'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
})
const displayEvents = computed(() => {
if (activeFilter.value === 'all') return events.value
return events.value.filter(event => event.eventType === activeFilter.value || event.emotionType === activeFilter.value)
})
const getMonth = (date) => {
if (!date) return '未知'
return String(date).slice(0, 7).replace('-', '.')
}
const getAgeText = (date) => {
if (!date) return ''
const year = Number(String(date).slice(0, 4))
if (!year || !profile.value.birthYear) return ''
return `${Math.max(0, year - Number(profile.value.birthYear))}`
}
const getEventTag = (event) => {
if (Array.isArray(event.tags) && event.tags.length) return event.tags[0]
const map = { childhood: '童年', highlight: '高光', valley: '低谷', daily_log: '美好的瞬间' }
return map[event.eventType] || '人生'
}
const getEventInitial = (event) => {
return (event.title || '记').slice(0, 1)
}
const createEvent = () => {
uni.navigateTo({ url: '/pages/life-event/form' })
}
const openDetail = (event) => {
if (String(event.id || '').startsWith('sample-')) return
uni.navigateTo({ url: `/pages/life-event/detail?id=${event.id}` })
}
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
</script>
<style scoped>
.record-view {
display: flex;
flex-direction: column;
gap: 28rpx;
padding-bottom: 34rpx;
}
.profile-card {
position: relative;
overflow: hidden;
border-radius: 34rpx;
padding: 40rpx 34rpx 32rpx;
}
.profile-card::after {
content: '';
position: absolute;
right: -28rpx;
bottom: -20rpx;
width: 250rpx;
height: 180rpx;
background: radial-gradient(circle, rgba(122, 58, 255, 0.35), transparent 62%);
border: 1rpx solid rgba(158, 88, 255, 0.26);
border-radius: 50%;
transform: rotate(-18deg);
}
.profile-top,
.meta-grid,
.hobby-row {
position: relative;
z-index: 1;
}
.profile-top {
display: flex;
align-items: center;
gap: 24rpx;
}
.avatar-wrap {
position: relative;
flex-shrink: 0;
width: 134rpx;
height: 134rpx;
padding: 6rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fff, #9b54ff 38%, #4a67ff);
box-shadow: 0 0 42rpx rgba(149, 89, 255, 0.56);
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
background: #20123d;
}
.avatar-edit {
position: absolute;
right: -6rpx;
bottom: -6rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(135deg, #8b4dff, #4a2cff);
box-shadow: 0 0 20rpx rgba(158, 91, 255, 0.6);
}
.edit-pen,
.tiny-pen {
width: 18rpx;
height: 6rpx;
border-radius: 6rpx;
background: #fff;
transform: rotate(-45deg);
margin: 21rpx auto;
}
.profile-info {
flex: 1;
min-width: 0;
}
.name-row {
display: flex;
align-items: center;
gap: 14rpx;
}
.profile-name {
color: #fff;
font-size: 44rpx;
line-height: 1.1;
font-weight: 800;
}
.star {
color: #ffd589;
font-size: 30rpx;
}
.profile-subtitle {
display: block;
margin-top: 14rpx;
color: rgba(239, 232, 255, 0.78);
font-size: 27rpx;
}
.edit-profile {
flex-shrink: 0;
height: 64rpx;
padding: 0 22rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 10rpx;
color: #dccbff;
font-size: 24rpx;
}
.tiny-pen {
width: 16rpx;
margin: 0;
background: currentColor;
}
.profile-divider {
position: relative;
z-index: 1;
height: 1rpx;
margin: 32rpx 0 24rpx;
background: rgba(180, 139, 255, 0.22);
}
.profile-divider.small {
margin: 24rpx 0;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.meta-item {
min-width: 0;
display: flex;
align-items: center;
gap: 14rpx;
padding-right: 18rpx;
border-right: 1rpx solid rgba(180, 139, 255, 0.22);
}
.meta-item + .meta-item {
padding-left: 22rpx;
}
.meta-item.no-border {
border-right: 0;
}
.meta-icon,
.heart {
color: #a855ff;
font-size: 42rpx;
line-height: 1;
text-shadow: 0 0 24rpx rgba(168, 85, 255, 0.7);
}
.person-icon,
.job-icon {
position: relative;
width: 38rpx;
height: 38rpx;
flex-shrink: 0;
}
.person-icon::before {
content: '';
position: absolute;
left: 10rpx;
top: 0;
width: 16rpx;
height: 16rpx;
border: 4rpx solid #a855ff;
border-radius: 50%;
}
.person-icon::after {
content: '';
position: absolute;
left: 2rpx;
bottom: 0;
width: 30rpx;
height: 18rpx;
border: 4rpx solid #a855ff;
border-radius: 18rpx 18rpx 4rpx 4rpx;
}
.job-icon {
border: 5rpx solid #a855ff;
border-radius: 8rpx;
box-sizing: border-box;
}
.job-icon::before {
content: '';
position: absolute;
left: 10rpx;
top: -10rpx;
width: 12rpx;
height: 8rpx;
border: 4rpx solid #a855ff;
border-bottom: 0;
border-radius: 8rpx 8rpx 0 0;
}
.meta-label,
.meta-value,
.hobby-text {
display: block;
}
.meta-label {
color: rgba(219, 204, 247, 0.54);
font-size: 22rpx;
}
.meta-value,
.hobby-text {
margin-top: 4rpx;
color: #fff;
font-size: 27rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hobby-row {
display: flex;
align-items: center;
gap: 28rpx;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.title-line {
display: flex;
align-items: center;
gap: 14rpx;
}
.section-title {
color: #fff;
font-size: 44rpx;
line-height: 1.1;
font-weight: 800;
}
.gold {
color: #ffd184;
}
.section-subtitle {
display: block;
margin-top: 14rpx;
color: rgba(210, 194, 242, 0.68);
font-size: 26rpx;
}
.map-btn {
height: 64rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 12rpx;
color: #caa9ff;
font-size: 24rpx;
}
.map-icon {
width: 28rpx;
height: 22rpx;
border: 4rpx solid currentColor;
border-radius: 4rpx;
transform: skewY(-12deg);
}
.filters {
width: 100%;
white-space: nowrap;
}
.filter-row {
display: inline-flex;
gap: 18rpx;
align-items: center;
}
.filter-chip,
.add-filter {
height: 62rpx;
min-width: 104rpx;
padding: 0 30rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
font-size: 25rpx;
font-weight: 600;
}
.filter-chip.active {
color: #fff;
border-color: rgba(198, 111, 255, 0.8);
background: linear-gradient(135deg, #b449ff, #7331ff);
box-shadow: 0 0 28rpx rgba(171, 68, 255, 0.52);
}
.add-filter {
min-width: 62rpx;
padding: 0;
border: 2rpx dashed rgba(155, 112, 255, 0.45);
color: #c49cff;
font-size: 36rpx;
}
.timeline {
display: flex;
flex-direction: column;
}
.timeline-item {
display: grid;
grid-template-columns: 88rpx 1fr;
min-height: 188rpx;
}
.rail {
position: relative;
display: flex;
justify-content: center;
}
.node {
position: relative;
z-index: 2;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid currentColor;
box-shadow: 0 0 28rpx currentColor;
}
.node view {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: currentColor;
}
.node-0 { color: #b14fff; }
.node-1 { color: #5ea1ff; }
.node-2 { color: #55e0c6; }
.node-3 { color: #ff8f4b; }
.line {
position: absolute;
top: 56rpx;
bottom: -6rpx;
width: 4rpx;
background: linear-gradient(180deg, currentColor, rgba(255,255,255,0.14));
color: #8d63ff;
}
.event-card {
min-height: 160rpx;
margin-bottom: 22rpx;
border-radius: 28rpx;
display: grid;
grid-template-columns: 116rpx 1rpx 1fr 108rpx 24rpx;
align-items: center;
gap: 22rpx;
padding: 22rpx;
}
.date-box {
color: rgba(217, 203, 244, 0.68);
}
.date-month,
.date-age {
display: block;
font-size: 25rpx;
}
.date-age {
margin-top: 8rpx;
}
.event-divider {
width: 1rpx;
height: 92rpx;
background: rgba(180, 139, 255, 0.18);
}
.event-body {
min-width: 0;
}
.event-title {
display: block;
color: #fff;
font-size: 27rpx;
line-height: 1.25;
font-weight: 800;
}
.event-tag {
display: inline-flex;
margin-top: 14rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 20rpx;
}
.tag-0 { color: #8effc7; background: rgba(50, 196, 128, 0.18); }
.tag-1 { color: #80b9ff; background: rgba(74, 128, 255, 0.2); }
.tag-2 { color: #d49cff; background: rgba(157, 67, 255, 0.22); }
.tag-3 { color: #ffc26f; background: rgba(240, 145, 45, 0.18); }
.event-summary {
display: -webkit-box;
margin-top: 12rpx;
color: rgba(226, 215, 246, 0.66);
font-size: 24rpx;
line-height: 1.45;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.thumb {
width: 96rpx;
height: 96rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 44rpx;
font-weight: 900;
overflow: hidden;
}
.thumb-0 { background: linear-gradient(135deg, #8d3bff, #d76cff); }
.thumb-1 { background: linear-gradient(135deg, #1b5fff, #39c6ff); }
.thumb-2 { background: linear-gradient(135deg, #0ea98a, #67e8d3); }
.thumb-3 { background: linear-gradient(135deg, #b45309, #f59e0b); }
.chevron {
color: rgba(223, 213, 245, 0.68);
font-size: 54rpx;
}
.empty-card {
border-radius: 30rpx;
padding: 36rpx;
}
.empty-title,
.empty-text {
display: block;
}
.empty-title {
color: #fff;
font-size: 30rpx;
font-weight: 800;
}
.empty-text {
margin-top: 10rpx;
color: rgba(220, 207, 244, 0.66);
}
.create-fab {
align-self: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
color: #caa6ff;
font-size: 24rpx;
}
.plus-core {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 56rpx;
background: linear-gradient(135deg, #b348ff, #582cff);
box-shadow: 0 0 38rpx rgba(171, 72, 255, 0.62);
}
</style>