Files
happy-life-star/mini-program/src/pages/main/RecordView.vue
T
peanut 755059807a feat: 优化管理后台页面UI、修复TS编译错误、新增人生事件模块
- 优化 AI 配置列表页面:重构统计卡片、搜索表单、表格列展示
- 修复 3 处 TypeScript TS6133 编译错误,恢复构建
- 新增管理员修改密码和重置密码功能
- 优化小程序多个页面样式和交互
- 人生事件模块完善

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

787 lines
17 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" @click="openMap">
<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" @click="addFilter"></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 customFilters = ref([])
const baseFilters = [
{ label: '全部', value: 'all' },
{ label: '童年', value: 'childhood' },
{ label: '高光', value: 'highlight' },
{ label: '低谷', value: 'valley' },
{ label: '美好的瞬间', value: 'daily_log' }
]
const filters = computed(() => [...baseFilters, ...customFilters.value])
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
if (activeFilter.value.startsWith('custom_')) {
const label = activeFilter.value.replace('custom_', '')
return events.value.filter(event => Array.isArray(event.tags) && event.tags.includes(label))
}
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) => {
uni.setStorageSync('current_life_event', JSON.parse(JSON.stringify(event || {})))
if (!event?.id) return
uni.navigateTo({ url: `/pages/life-event/detail?id=${event.id}` })
}
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
const openMap = () => {
uni.navigateTo({ url: '/pages/main/PathView' })
}
const addFilter = () => {
uni.showModal({
title: '新增筛选',
editable: true,
placeholderText: '输入标签名',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm || !value) return
const filter = { label: value, value: `custom_${value}` }
if (!customFilters.value.some(item => item.label === value)) {
customFilters.value.push(filter)
}
activeFilter.value = filter.value
}
})
}
</script>
<style scoped>
.record-view {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 22rpx;
}
.profile-card {
position: relative;
overflow: hidden;
border-radius: 34rpx;
padding: 34rpx 30rpx 28rpx;
}
.profile-card::after {
content: '';
position: absolute;
right: -28rpx;
bottom: -20rpx;
width: 220rpx;
height: 156rpx;
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: 22rpx;
}
.avatar-wrap {
position: relative;
flex-shrink: 0;
width: 112rpx;
height: 112rpx;
padding: 5rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fff, #9b54ff 38%, #4a67ff);
box-shadow: 0 0 34rpx rgba(149, 89, 255, 0.52);
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
background: #20123d;
}
.avatar-edit {
position: absolute;
right: -5rpx;
bottom: -5rpx;
width: 40rpx;
height: 40rpx;
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: 17rpx auto;
}
.profile-info {
flex: 1;
min-width: 0;
}
.name-row {
display: flex;
align-items: center;
gap: 14rpx;
}
.profile-name {
color: #fff;
font-size: 36rpx;
line-height: 1.1;
font-weight: 800;
}
.star {
color: #ffd589;
font-size: 26rpx;
}
.profile-subtitle {
display: block;
margin-top: 10rpx;
color: rgba(239, 232, 255, 0.78);
font-size: 24rpx;
}
.edit-profile {
flex-shrink: 0;
height: 56rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 8rpx;
color: #dccbff;
font-size: 22rpx;
}
.tiny-pen {
width: 16rpx;
margin: 0;
background: currentColor;
}
.profile-divider {
position: relative;
z-index: 1;
height: 1rpx;
margin: 28rpx 0 20rpx;
background: rgba(180, 139, 255, 0.22);
}
.profile-divider.small {
margin: 20rpx 0;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.meta-item {
min-width: 0;
display: flex;
align-items: center;
gap: 10rpx;
padding-right: 12rpx;
border-right: 1rpx solid rgba(180, 139, 255, 0.22);
}
.meta-item + .meta-item {
padding-left: 16rpx;
}
.meta-item.no-border {
border-right: 0;
}
.meta-item > view,
.hobby-row > view {
min-width: 0;
}
.meta-icon,
.heart {
color: #a855ff;
font-size: 34rpx;
line-height: 1;
text-shadow: 0 0 24rpx rgba(168, 85, 255, 0.7);
}
.person-icon,
.job-icon {
position: relative;
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
.person-icon::before {
content: '';
position: absolute;
left: 8rpx;
top: 0;
width: 14rpx;
height: 14rpx;
border: 3rpx solid #a855ff;
border-radius: 50%;
}
.person-icon::after {
content: '';
position: absolute;
left: 1rpx;
bottom: 0;
width: 28rpx;
height: 16rpx;
border: 3rpx solid #a855ff;
border-radius: 18rpx 18rpx 4rpx 4rpx;
}
.job-icon {
border: 4rpx solid #a855ff;
border-radius: 8rpx;
box-sizing: border-box;
}
.job-icon::before {
content: '';
position: absolute;
left: 8rpx;
top: -8rpx;
width: 12rpx;
height: 7rpx;
border: 3rpx 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: 20rpx;
}
.meta-value,
.hobby-text {
margin-top: 3rpx;
color: #fff;
font-size: 23rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hobby-row {
display: flex;
align-items: center;
gap: 22rpx;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.title-line {
display: flex;
align-items: center;
gap: 12rpx;
}
.section-title {
color: #fff;
font-size: 40rpx;
line-height: 1.1;
font-weight: 800;
}
.gold {
color: #ffd184;
}
.section-subtitle {
display: block;
margin-top: 10rpx;
color: rgba(210, 194, 242, 0.68);
font-size: 24rpx;
}
.map-btn {
height: 56rpx;
padding: 0 20rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 10rpx;
color: #caa9ff;
font-size: 22rpx;
}
.map-icon {
width: 24rpx;
height: 20rpx;
border: 3rpx solid currentColor;
border-radius: 4rpx;
transform: skewY(-12deg);
}
.filters {
width: 100%;
white-space: nowrap;
}
.filter-row {
display: inline-flex;
gap: 16rpx;
align-items: center;
}
.filter-chip,
.add-filter {
height: 54rpx;
min-width: 92rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
font-size: 22rpx;
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: 56rpx;
padding: 0;
border: 2rpx dashed rgba(155, 112, 255, 0.45);
color: #c49cff;
font-size: 32rpx;
}
.timeline {
display: flex;
flex-direction: column;
}
.timeline-item {
display: grid;
grid-template-columns: 76rpx 1fr;
min-height: 170rpx;
}
.rail {
position: relative;
display: flex;
justify-content: center;
}
.node {
position: relative;
z-index: 2;
width: 46rpx;
height: 46rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid currentColor;
box-shadow: 0 0 24rpx currentColor;
}
.node view {
width: 20rpx;
height: 20rpx;
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: 50rpx;
bottom: -6rpx;
width: 4rpx;
background: linear-gradient(180deg, currentColor, rgba(255,255,255,0.14));
color: #8d63ff;
}
.event-card {
min-height: 146rpx;
margin-bottom: 18rpx;
border-radius: 28rpx;
display: grid;
grid-template-columns: 96rpx 1rpx 1fr 88rpx 20rpx;
align-items: center;
gap: 18rpx;
padding: 20rpx;
}
.date-box {
color: rgba(217, 203, 244, 0.68);
}
.date-month,
.date-age {
display: block;
font-size: 23rpx;
}
.date-age {
margin-top: 6rpx;
}
.event-divider {
width: 1rpx;
height: 82rpx;
background: rgba(180, 139, 255, 0.18);
}
.event-body {
min-width: 0;
}
.event-title {
display: block;
color: #fff;
font-size: 24rpx;
line-height: 1.25;
font-weight: 800;
}
.event-tag {
display: inline-flex;
margin-top: 10rpx;
padding: 5rpx 12rpx;
border-radius: 999rpx;
font-size: 19rpx;
}
.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: 9rpx;
color: rgba(226, 215, 246, 0.66);
font-size: 22rpx;
line-height: 1.45;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.thumb {
width: 78rpx;
height: 78rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 36rpx;
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: 46rpx;
}
.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: 8rpx;
color: #caa6ff;
font-size: 22rpx;
}
.plus-core {
width: 78rpx;
height: 78rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 46rpx;
background: linear-gradient(135deg, #b348ff, #582cff);
box-shadow: 0 0 38rpx rgba(171, 72, 255, 0.62);
}
</style>