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>
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<view class="event-detail-page">
|
||||
<view class="space-bg"></view>
|
||||
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
|
||||
|
||||
<view class="header">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">人生轨迹内容</text>
|
||||
<text class="more">•••</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y :show-scrollbar="false">
|
||||
<view class="hero-card kos-card">
|
||||
<view class="cover">{{ (event.title || '轨').slice(0, 1) }}</view>
|
||||
<view class="hero-body">
|
||||
<text class="date">{{ event.time || event.date || '未知日期' }}</text>
|
||||
<text class="event-title">{{ event.title || '未命名经历' }}</text>
|
||||
<text class="tag">{{ primaryTag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel kos-card">
|
||||
<text class="panel-title">经历描述</text>
|
||||
<text class="body-text">{{ event.content || event.description || '暂无内容。' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="panel ai-panel kos-card">
|
||||
<view class="ai-head">
|
||||
<text class="spark">✦</text>
|
||||
<text>AI 人生解读</text>
|
||||
</view>
|
||||
<view v-for="block in analysisBlocks" :key="block.title" class="analysis-block">
|
||||
<text class="analysis-title">{{ block.title }}</text>
|
||||
<text class="analysis-text">{{ block.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="related-tags">
|
||||
<text v-for="tag in relatedTags" :key="tag" class="tag-pill kos-pill">{{ tag }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="bottom-actions" :style="{ paddingBottom: safeAreaBottom + 14 + 'px' }">
|
||||
<view class="action kos-pill" @click="notReady('编辑')">编辑</view>
|
||||
<view class="action kos-pill" @click="notReady('收藏')">收藏</view>
|
||||
<view class="action primary kos-primary" @click="notReady('聊天')">聊聊这段经历</view>
|
||||
<view class="action kos-pill" @click="notReady('分享')">分享</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const safeAreaTop = ref(20)
|
||||
const safeAreaBottom = ref(0)
|
||||
const eventId = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const info = uni.getWindowInfo()
|
||||
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
||||
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
|
||||
const pages = getCurrentPages()
|
||||
eventId.value = pages[pages.length - 1]?.options?.id || ''
|
||||
if (!store.events?.length) await store.fetchEvents()
|
||||
})
|
||||
|
||||
const event = computed(() => store.getEventById(eventId.value) || {})
|
||||
const primaryTag = computed(() => Array.isArray(event.value.tags) && event.value.tags.length ? event.value.tags[0] : '人生片段')
|
||||
const relatedTags = computed(() => {
|
||||
if (Array.isArray(event.value.tags) && event.value.tags.length) return event.value.tags
|
||||
return ['理解', '成长', '转折']
|
||||
})
|
||||
|
||||
const analysisBlocks = computed(() => {
|
||||
const ai = event.value.aiFeedback
|
||||
if (!ai) {
|
||||
return [
|
||||
{ title: '情绪整理', text: '这段经历还没有 AI 解读,但它已经是一枚重要的生命坐标。' },
|
||||
{ title: '成长意义', text: '你可以继续补充当时的选择、关系和感受,让它成为后续剧本生成的素材。' }
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ title: '情绪整理', text: ai.slice(0, 90) },
|
||||
{ title: '能力映射', text: '从这段经历里,可以看到你对变化的感知、承受和重新组织能力。' },
|
||||
{ title: '下一步建议', text: '把这段经历与一个目标连接起来,它会更容易转化成行动路径。' }
|
||||
]
|
||||
})
|
||||
|
||||
const notReady = (label) => {
|
||||
uni.showToast({ title: `${label}能力稍后开放`, icon: 'none' })
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-detail-page {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
background: #050615;
|
||||
}
|
||||
|
||||
.space-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 18% 2%, rgba(124, 58, 237, 0.3), transparent 34%),
|
||||
radial-gradient(circle at 88% 22%, rgba(14, 165, 233, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
|
||||
}
|
||||
|
||||
.top-safe,
|
||||
.header,
|
||||
.content,
|
||||
.bottom-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 92rpx;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 80rpx 1fr 80rpx;
|
||||
align-items: center;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
.back {
|
||||
font-size: 66rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.more {
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.panel {
|
||||
border-radius: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 256rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 88rpx;
|
||||
font-weight: 900;
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, rgba(56, 189, 248, 0.3), transparent 30%),
|
||||
linear-gradient(135deg, #7d35ff, #111827);
|
||||
}
|
||||
|
||||
.hero-body,
|
||||
.panel {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.date,
|
||||
.tag,
|
||||
.body-text,
|
||||
.analysis-text {
|
||||
display: block;
|
||||
color: rgba(226, 216, 246, 0.72);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.24;
|
||||
}
|
||||
|
||||
.tag {
|
||||
width: fit-content;
|
||||
margin-top: 18rpx;
|
||||
padding: 9rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #caa0ff;
|
||||
background: rgba(124, 58, 237, 0.18);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.ai-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 18rpx;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.spark {
|
||||
color: #d982ff;
|
||||
}
|
||||
|
||||
.analysis-block {
|
||||
padding: 20rpx 0;
|
||||
border-top: 1rpx solid rgba(180, 139, 255, 0.14);
|
||||
}
|
||||
|
||||
.analysis-title {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
color: #dccbff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.analysis-text {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.related-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14rpx;
|
||||
padding-bottom: 34rpx;
|
||||
}
|
||||
|
||||
.tag-pill {
|
||||
height: 48rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(237, 233, 254, 0.78);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
min-height: 124rpx;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 0.8fr 0.8fr 1.5fr 0.8fr;
|
||||
gap: 12rpx;
|
||||
padding: 14rpx 22rpx;
|
||||
background: rgba(5, 6, 21, 0.72);
|
||||
backdrop-filter: blur(24rpx);
|
||||
}
|
||||
|
||||
.action {
|
||||
height: 72rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #caa0ff;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.action.primary {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<view class="event-form-page">
|
||||
<view class="space-bg"></view>
|
||||
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
|
||||
|
||||
<view class="header">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<view>
|
||||
<text class="title">记录人生经历 ✦</text>
|
||||
<text class="subtitle">记录每一个重要时刻,AI将帮你生成专属人生轨迹</text>
|
||||
</view>
|
||||
<text class="save" @click="submit">保存</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y :show-scrollbar="false">
|
||||
<view class="form-card kos-card">
|
||||
<view class="group">
|
||||
<view class="group-title">
|
||||
<text class="group-icon">◷</text>
|
||||
<text>时间</text>
|
||||
</view>
|
||||
<view class="segmented">
|
||||
<text v-for="item in timeModes" :key="item" class="seg active-first">{{ item }}</text>
|
||||
</view>
|
||||
<picker mode="date" :value="form.time" @change="e => form.time = e.detail.value">
|
||||
<view class="date-picker field-box">
|
||||
<text>{{ formatDate(form.time) }}</text>
|
||||
<text class="chevron">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="group">
|
||||
<view class="label-row">
|
||||
<view class="group-title">
|
||||
<text class="group-icon">✎</text>
|
||||
<text>事件标题 ✦</text>
|
||||
</view>
|
||||
<text class="count">{{ form.title.length }}/30</text>
|
||||
</view>
|
||||
<input class="field-box input" maxlength="30" v-model="form.title" placeholder="给这段经历起个标题吧..." placeholder-class="placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="group">
|
||||
<view class="label-row">
|
||||
<view class="group-title">
|
||||
<text class="group-icon">▣</text>
|
||||
<text>具体内容 ✦</text>
|
||||
</view>
|
||||
<text class="count">{{ form.content.length }}/500</text>
|
||||
</view>
|
||||
<view class="textarea-wrap">
|
||||
<textarea class="textarea" maxlength="500" v-model="form.content" placeholder="请详细记录这段经历的背景、发生的事情、你的感受和收获..." placeholder-class="placeholder" />
|
||||
<view class="ai-btn kos-pill" @click="assistWrite">✦ AI 帮我写</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="group">
|
||||
<view class="label-row">
|
||||
<view class="group-title">
|
||||
<text class="group-icon">♡</text>
|
||||
<text>相关标签</text>
|
||||
</view>
|
||||
<view class="custom-tag kos-pill">+ 自定义标签</view>
|
||||
</view>
|
||||
<view class="tag-grid">
|
||||
<text
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
class="tag kos-pill"
|
||||
:class="{ active: form.tags.includes(tag) }"
|
||||
@click="toggleTag(tag)"
|
||||
>{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit kos-primary" :loading="saving" @click="submit">
|
||||
<text>✦ 提交记录</text>
|
||||
<text class="submit-sub">记录后可在时间轴查看</text>
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const safeAreaTop = ref(20)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
time: new Date().toISOString().slice(0, 10),
|
||||
content: '',
|
||||
tags: [],
|
||||
eventType: 'daily_log'
|
||||
})
|
||||
|
||||
const timeModes = ['具体日期', '年月', '季节', '时间轴范围']
|
||||
const tags = ['成长', '学习', '工作', '旅行', '感情', '家庭', '友情', '挑战', '突破', '收获', '感动', '迷茫']
|
||||
|
||||
onMounted(() => {
|
||||
const info = uni.getWindowInfo()
|
||||
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
||||
})
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '选择具体日期'
|
||||
const [year, month, day] = value.split('-')
|
||||
return `${year}年${month}月${day}日`
|
||||
}
|
||||
|
||||
const toggleTag = (tag) => {
|
||||
const index = form.tags.indexOf(tag)
|
||||
if (index >= 0) form.tags.splice(index, 1)
|
||||
else form.tags.push(tag)
|
||||
}
|
||||
|
||||
const assistWrite = () => {
|
||||
if (!form.content) {
|
||||
form.content = '那一天,我清楚地感受到自己正在经历一次变化。事情本身也许并不宏大,但它让我重新看见了自己的选择、情绪和力量。'
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.title || !form.content || saving.value) {
|
||||
uni.showToast({ title: '请填写标题和内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
const result = await store.createEvent({ ...form, aiFeedback: buildAiFeedback() })
|
||||
saving.value = false
|
||||
if (result.success) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const buildAiFeedback = () => {
|
||||
return '这段经历体现了你的自我观察能力。它可能意味着你正在从旧的反应模式里走出来,开始更主动地理解自己的处境。建议保留当时的细节,因为这些细节会成为后续人生剧本的重要素材。'
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-form-page {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
background: #050615;
|
||||
}
|
||||
|
||||
.space-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 86% 88%, rgba(126, 57, 255, 0.34), transparent 30%),
|
||||
radial-gradient(circle at 18% 8%, rgba(63, 120, 255, 0.18), transparent 26%),
|
||||
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
|
||||
}
|
||||
|
||||
.top-safe,
|
||||
.header,
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-safe,
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
min-height: 138rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 72rpx 1fr 72rpx;
|
||||
align-items: center;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
.back {
|
||||
color: #fff;
|
||||
font-size: 70rpx;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 14rpx;
|
||||
color: rgba(218, 204, 243, 0.72);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.save {
|
||||
text-align: right;
|
||||
color: #c06dff;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 28rpx 34rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
border-radius: 34rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
|
||||
.group-title,
|
||||
.label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
gap: 14rpx;
|
||||
color: #d4b7ff;
|
||||
font-size: 31rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
color: #a855ff;
|
||||
font-size: 38rpx;
|
||||
}
|
||||
|
||||
.count,
|
||||
.custom-tag {
|
||||
color: #b58bff;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.custom-tag {
|
||||
height: 50rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
height: 66rpx;
|
||||
margin-top: 24rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 5rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
border: 1rpx solid rgba(151, 111, 255, 0.26);
|
||||
background: rgba(11, 12, 38, 0.7);
|
||||
}
|
||||
|
||||
.seg {
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(224, 214, 243, 0.72);
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.seg:first-child {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #a843ff, #7630ff);
|
||||
box-shadow: 0 0 24rpx rgba(168, 67, 255, 0.5);
|
||||
}
|
||||
|
||||
.field-box,
|
||||
.textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 22rpx;
|
||||
border: 1rpx solid rgba(151, 111, 255, 0.24);
|
||||
background: rgba(12, 15, 46, 0.72);
|
||||
color: #fff;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
height: 78rpx;
|
||||
margin-top: 22rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: rgba(224, 214, 243, 0.62);
|
||||
font-size: 52rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 82rpx;
|
||||
margin-top: 18rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.textarea-wrap {
|
||||
position: relative;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
height: 258rpx;
|
||||
padding: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ai-btn {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
bottom: 18rpx;
|
||||
height: 54rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #d5b3ff;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.tag-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
height: 58rpx;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(224, 214, 243, 0.68);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tag.active {
|
||||
color: #fff;
|
||||
border-color: rgba(192, 100, 255, 0.84);
|
||||
background: rgba(137, 51, 255, 0.35);
|
||||
box-shadow: 0 0 22rpx rgba(168, 67, 255, 0.34);
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
height: 112rpx;
|
||||
margin-top: 38rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit text {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 31rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.submit-sub {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 255, 255, 0.72) !important;
|
||||
font-size: 23rpx !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -164,7 +164,7 @@ const handleLogin = async () => {
|
||||
radial-gradient(circle at 50% -8%, rgba(180, 129, 255, 0.28), transparent 28%),
|
||||
linear-gradient(180deg, #13091f 0%, #1b0b31 46%, #100719 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<view class="mine-view">
|
||||
<view class="profile-card kos-card">
|
||||
<view class="avatar">{{ avatarText }}</view>
|
||||
<view class="profile-main">
|
||||
<view class="name-row">
|
||||
<text class="name">{{ profile.nickname || 'Zoey' }}</text>
|
||||
<text class="star">✦</text>
|
||||
</view>
|
||||
<text class="signature">{{ signature }}</text>
|
||||
<view class="chips">
|
||||
<text v-for="chip in chips" :key="chip" class="chip kos-pill">{{ chip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="edit-btn kos-pill" @click="editProfile">编辑资料</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card kos-card">
|
||||
<text class="stat-value">{{ eventsCount }}</text>
|
||||
<text class="stat-label">人生记录</text>
|
||||
</view>
|
||||
<view class="stat-card kos-card">
|
||||
<text class="stat-value">{{ scriptsCount }}</text>
|
||||
<text class="stat-label">生成剧本</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card kos-card">
|
||||
<view class="section-title">兴趣爱好</view>
|
||||
<view class="tag-cloud">
|
||||
<text v-for="tag in hobbyTags" :key="tag" class="tag kos-pill">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card kos-card">
|
||||
<view class="section-title">生命摘要</view>
|
||||
<view class="memory-line">
|
||||
<text class="memory-label">童年</text>
|
||||
<text class="memory-text">{{ profile.childhood?.text || '还没有写下最早的光。' }}</text>
|
||||
</view>
|
||||
<view class="memory-line">
|
||||
<text class="memory-label">高光</text>
|
||||
<text class="memory-text">{{ profile.joy?.text || '等待记录一次会发光的瞬间。' }}</text>
|
||||
</view>
|
||||
<view class="memory-line">
|
||||
<text class="memory-label">未来</text>
|
||||
<text class="memory-text">{{ profile.future?.vision || '未来档案还在生成中。' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="logout kos-pill" @click="handleLogout">退出登录</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const profile = computed(() => store.userProfile || store.registrationData || {})
|
||||
const eventsCount = computed(() => store.events?.length || 0)
|
||||
const scriptsCount = computed(() => store.scripts?.length || 0)
|
||||
|
||||
const avatarText = computed(() => (profile.value.nickname || 'Z').slice(0, 1))
|
||||
const chips = computed(() => [profile.value.zodiac, profile.value.mbti, profile.value.profession].filter(Boolean))
|
||||
const hobbyTags = computed(() => {
|
||||
const hobbies = profile.value.hobbies
|
||||
if (Array.isArray(hobbies) && hobbies.length) return hobbies
|
||||
return ['阅读', '旅行', '音乐', '创作']
|
||||
})
|
||||
const signature = computed(() => profile.value.future?.ideal || '正在把人生整理成一份会发光的档案。')
|
||||
|
||||
const editProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要离开当前数字生命档案吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await store.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mine-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
border-radius: 34rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 50rpx;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #b245ff, #2a7dff);
|
||||
box-shadow: 0 0 36rpx rgba(168, 85, 255, 0.55);
|
||||
}
|
||||
|
||||
.profile-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #fff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.star {
|
||||
color: #ffd184;
|
||||
}
|
||||
|
||||
.signature {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: rgba(224, 211, 246, 0.66);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chips,
|
||||
.tag-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.chip,
|
||||
.tag {
|
||||
height: 44rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: rgba(244, 235, 255, 0.86);
|
||||
font-size: 21rpx;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
align-self: flex-start;
|
||||
height: 54rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #caa0ff;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-radius: 28rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: rgba(224, 211, 246, 0.62);
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: 30rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.memory-line {
|
||||
display: grid;
|
||||
grid-template-columns: 86rpx 1fr;
|
||||
gap: 18rpx;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid rgba(180, 139, 255, 0.14);
|
||||
}
|
||||
|
||||
.memory-line:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.memory-label {
|
||||
color: #b56cff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.memory-text {
|
||||
color: rgba(224, 211, 246, 0.72);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.logout {
|
||||
height: 72rpx;
|
||||
border-radius: 999rpx;
|
||||
color: rgba(224, 211, 246, 0.74);
|
||||
font-size: 25rpx;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,265 +1,363 @@
|
||||
<template>
|
||||
<view class="detail-view">
|
||||
<!-- 状态栏占位 -->
|
||||
<view :style="{ height: statusBarHeight + 'px' }" class="status-bar-placeholder"></view>
|
||||
<view class="detail-page">
|
||||
<view class="space-bg"></view>
|
||||
<view class="status-space" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
|
||||
<view class="detail-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-text">返回</text>
|
||||
</view>
|
||||
<text class="detail-title">{{ script?.title || '剧本详情' }}</text>
|
||||
<view class="topbar">
|
||||
<button class="back-btn" @click="goBack">‹</button>
|
||||
<text class="top-title">人生剧本 ✦</text>
|
||||
<button class="save-btn kos-pill" @click="selectCurrent">映射</button>
|
||||
</view>
|
||||
|
||||
<scroll-view class="detail-content" scroll-y>
|
||||
<view class="content-container">
|
||||
<!-- 基本信息卡片 -->
|
||||
<view class="info-card glass-card">
|
||||
<view class="info-row">
|
||||
<text class="info-label">主题</text>
|
||||
<text class="info-value">{{ script?.theme || '-' }}</text>
|
||||
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
|
||||
<view class="hero-card kos-card">
|
||||
<text class="eyebrow">PARALLEL LIFE</text>
|
||||
<text class="script-title">{{ script?.title || '未命名剧本' }}</text>
|
||||
<text class="script-summary">{{ script?.summary || '命运正在整理章节。' }}</text>
|
||||
<view class="stats">
|
||||
<view class="stat">
|
||||
<text class="stat-value">{{ script?.style || '爽文' }}</text>
|
||||
<text class="stat-label">风格</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">风格</text>
|
||||
<text class="info-value">{{ script?.style || '-' }}</text>
|
||||
<view class="stat">
|
||||
<text class="stat-value">{{ lengthText }}</text>
|
||||
<text class="stat-label">篇幅</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">篇幅</text>
|
||||
<text class="info-value">{{ script?.length || '-' }}</text>
|
||||
<view class="stat">
|
||||
<text class="stat-value">{{ script?.wordCount || 0 }}</text>
|
||||
<text class="stat-label">字数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 -->
|
||||
<view class="full-content glass-card">
|
||||
<view class="content-header">
|
||||
<text class="content-icon">📖</text>
|
||||
<text class="content-label">完整剧本</text>
|
||||
<view class="tabs kos-card">
|
||||
<view class="tab" :class="{ active: activeTab === 'content' }" @click="activeTab = 'content'">正文</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'outline' }" @click="activeTab = 'outline'">大纲</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'content'" class="article-card kos-card">
|
||||
<Markdown :content="fullContent" />
|
||||
</view>
|
||||
|
||||
<view v-else class="outline-list">
|
||||
<view v-for="(item, index) in outline" :key="index" class="outline-card kos-card">
|
||||
<view class="outline-node">{{ index + 1 }}</view>
|
||||
<view class="outline-body">
|
||||
<text class="outline-title">{{ item.title }}</text>
|
||||
<text class="outline-text">{{ item.text }}</text>
|
||||
</view>
|
||||
<Markdown :content="fullContent" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="bottom-actions">
|
||||
<button class="secondary-btn kos-pill" @click="goBack">返回列表</button>
|
||||
<button class="primary-btn kos-primary" @click="selectCurrent">映射实现路径</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import Markdown from '../../components/Markdown.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const statusBarHeight = ref(20)
|
||||
const activeTab = ref('content')
|
||||
const scriptId = ref('')
|
||||
const script = ref(null)
|
||||
const fullContent = ref('')
|
||||
const statusBarHeight = ref(0)
|
||||
|
||||
const scripts = computed(() => store.scripts || [])
|
||||
|
||||
onMounted(() => {
|
||||
// 获取状态栏高度(从 App.vue 存储的全局值)
|
||||
statusBarHeight.value = uni.getStorageSync('statusBarHeight') || 20
|
||||
|
||||
// 获取剧本 ID
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const scriptId = currentPage.options.id
|
||||
|
||||
// 从 store 获取剧本详情
|
||||
if (scriptId) {
|
||||
script.value = scripts.value.find(s => s.id === scriptId)
|
||||
// 优先读取 content 字段(转换后的格式),兼容 plotJson.fullContent
|
||||
if (script.value?.content) {
|
||||
fullContent.value = script.value.content
|
||||
} else if (script.value?.plotJson?.fullContent) {
|
||||
fullContent.value = script.value.plotJson.fullContent
|
||||
} else {
|
||||
fullContent.value = '暂无完整内容'
|
||||
}
|
||||
}
|
||||
const fullContent = computed(() => script.value?.content || '暂无正文内容。')
|
||||
const lengthText = computed(() => {
|
||||
const map = { short: '短篇', medium: '中篇', long: '长篇' }
|
||||
return map[script.value?.length] || script.value?.length || '中篇'
|
||||
})
|
||||
|
||||
const outline = computed(() => {
|
||||
const text = fullContent.value
|
||||
const parts = text.split(/\n{2,}/).filter(Boolean)
|
||||
if (parts.length > 1) {
|
||||
return parts.slice(0, 8).map((part, index) => {
|
||||
const clean = part.replace(/[#>*_`]/g, '').trim()
|
||||
const lines = clean.split('\n').filter(Boolean)
|
||||
return {
|
||||
title: lines[0]?.replace(/[【】]/g, '') || `章节 ${index + 1}`,
|
||||
text: lines.slice(1).join(' ').slice(0, 120) || clean.slice(0, 120)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
{ title: '起点', text: script.value?.theme || '从真实的人生经验出发。' },
|
||||
{ title: '转折', text: '关键机会出现,旧有困境开始松动。' },
|
||||
{ title: '高光', text: '主角完成一次真正的选择,并看见新的自我。' },
|
||||
{ title: '回响', text: '剧本落回现实,转化为可执行路径。' }
|
||||
]
|
||||
})
|
||||
|
||||
const loadScript = async () => {
|
||||
if (!scriptId.value) return
|
||||
script.value = store.getScriptById(scriptId.value)
|
||||
if (!script.value) {
|
||||
await store.fetchScripts()
|
||||
script.value = store.getScriptById(scriptId.value)
|
||||
}
|
||||
}
|
||||
|
||||
const selectCurrent = async () => {
|
||||
if (!script.value?.id) return
|
||||
const res = await store.selectScript(script.value.id)
|
||||
if (!res.success) {
|
||||
uni.showToast({ title: res.error || '映射失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/main/PathView' })
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
statusBarHeight.value = uni.getStorageSync('statusBarHeight') || 20
|
||||
const pages = getCurrentPages()
|
||||
scriptId.value = pages[pages.length - 1]?.options?.id || ''
|
||||
await loadScript()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
}
|
||||
|
||||
/* 状态栏占位 */
|
||||
.status-bar-placeholder {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 顶部导航栏 ==================== */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 32rpx 24rpx;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
border-bottom: 1px solid rgba(168, 85, 247, 0.2);
|
||||
flex-shrink: 0;
|
||||
.detail-page {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
background: #050615;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
z-index: 10;
|
||||
.space-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 18% 4%, rgba(129, 67, 255, 0.28), transparent 30%),
|
||||
radial-gradient(circle at 90% 16%, rgba(54, 122, 255, 0.16), transparent 28%),
|
||||
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
|
||||
}
|
||||
|
||||
.status-space,
|
||||
.topbar,
|
||||
.scroll,
|
||||
.bottom-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-space,
|
||||
.topbar,
|
||||
.bottom-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 32rpx;
|
||||
color: #C084FC;
|
||||
font-weight: 600;
|
||||
.topbar {
|
||||
height: 92rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 86rpx 1fr 86rpx;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(243, 232, 255, 0.8);
|
||||
.back-btn {
|
||||
color: #fff;
|
||||
font-size: 64rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(243, 232, 255, 0.9);
|
||||
font-family: 'Cinzel', 'Inter', serif;
|
||||
flex: 1;
|
||||
.top-title {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 8rpx;
|
||||
margin-right: 8rpx;
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* ==================== 滚动内容区 ==================== */
|
||||
.detail-content {
|
||||
.save-btn {
|
||||
height: 54rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #caa0ff;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
.hero-card {
|
||||
border-radius: 32rpx;
|
||||
padding: 36rpx 30rpx;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: block;
|
||||
color: #c186ff;
|
||||
font-size: 18rpx;
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.script-title {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
color: #fff;
|
||||
font-size: 44rpx;
|
||||
line-height: 1.18;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.script-summary {
|
||||
display: block;
|
||||
margin-top: 18rpx;
|
||||
color: rgba(226, 216, 246, 0.7);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.62;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 18rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.stat-value,
|
||||
.stat-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 6rpx;
|
||||
color: rgba(219, 207, 243, 0.58);
|
||||
font-size: 19rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
height: 76rpx;
|
||||
margin-top: 24rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 6rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.tab {
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(224, 214, 243, 0.64);
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #a63cff, #6530ff);
|
||||
box-shadow: 0 0 26rpx rgba(162, 71, 255, 0.48);
|
||||
}
|
||||
|
||||
.article-card {
|
||||
margin: 24rpx 0 34rpx;
|
||||
border-radius: 30rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.outline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
/* scroll-view 在小程序中不适合直接承担内容留白,改由内部容器控制 */
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
gap: 18rpx;
|
||||
margin: 24rpx 0 34rpx;
|
||||
}
|
||||
|
||||
/* ==================== 信息卡片 ==================== */
|
||||
.info-card {
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, rgba(168, 85, 247, 0.12), rgba(232, 121, 249, 0.08));
|
||||
border: 1px solid rgba(168, 85, 247, 0.25);
|
||||
border-radius: 40rpx;
|
||||
box-shadow: inset 0 0 20rpx rgba(168, 85, 247, 0.05),
|
||||
0 4rpx 20rpx rgba(168, 85, 247, 0.08);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
.outline-card {
|
||||
border-radius: 26rpx;
|
||||
padding: 26rpx;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.outline-node {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1px solid rgba(168, 85, 247, 0.1);
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #a855ff, #4f46e5);
|
||||
box-shadow: 0 0 22rpx rgba(168, 85, 255, 0.48);
|
||||
}
|
||||
|
||||
.outline-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
.outline-title {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 27rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(192, 132, 252, 0.7);
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
.outline-text {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: rgba(223, 211, 245, 0.7);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
margin-left: 16rpx;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
color: rgba(243, 232, 255, 0.9);
|
||||
.bottom-actions {
|
||||
height: 132rpx;
|
||||
padding: 16rpx 30rpx 26rpx;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.45fr;
|
||||
gap: 18rpx;
|
||||
background: rgba(5, 6, 21, 0.72);
|
||||
backdrop-filter: blur(24rpx);
|
||||
}
|
||||
|
||||
.secondary-btn,
|
||||
.primary-btn {
|
||||
height: 82rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* ==================== 完整内容卡片 ==================== */
|
||||
.full-content {
|
||||
padding: 40rpx 32rpx;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 40rpx;
|
||||
min-height: 400rpx;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid rgba(168, 85, 247, 0.2);
|
||||
}
|
||||
|
||||
.content-icon {
|
||||
font-size: 36rpx;
|
||||
filter: drop-shadow(0 0 8rpx rgba(192, 132, 252, 0.6));
|
||||
}
|
||||
|
||||
.content-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(192, 132, 252, 0.8);
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Markdown 内容间距调整 */
|
||||
.full-content .markdown-container {
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.full-content .markdown-h3 {
|
||||
margin-top: 32rpx;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.full-content .markdown-h4 {
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.full-content .markdown-p {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.8;
|
||||
color: rgba(243, 232, 255, 0.85);
|
||||
}
|
||||
|
||||
.full-content .markdown-hr {
|
||||
margin: 40rpx 0;
|
||||
.secondary-btn {
|
||||
color: #caa0ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,364 +1,281 @@
|
||||
<template>
|
||||
<view class="main-page">
|
||||
<!-- 星空背景 -->
|
||||
<view class="stars-container">
|
||||
<view
|
||||
v-for="star in stars"
|
||||
:key="star.id"
|
||||
class="star"
|
||||
:style="{
|
||||
left: star.left + '%',
|
||||
top: star.top + '%',
|
||||
width: star.size + 'px',
|
||||
height: star.size + 'px',
|
||||
'--opacity': star.opacity,
|
||||
'--x': star.xMove + 'px',
|
||||
'--y': star.yMove + 'px',
|
||||
'--duration': star.duration + 's',
|
||||
'--delay': star.delay + 's'
|
||||
}"
|
||||
></view>
|
||||
<view class="app-shell">
|
||||
<view class="space-bg">
|
||||
<view class="nebula nebula-a"></view>
|
||||
<view class="nebula nebula-b"></view>
|
||||
<view class="stars"></view>
|
||||
</view>
|
||||
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
</view>
|
||||
<view class="safe-top" :style="{ height: safeAreaTop + 14 + 'px' }"></view>
|
||||
|
||||
<view class="header" :style="{ paddingTop: safeAreaTop + 20 + 'px' }">
|
||||
<view class="header-left">
|
||||
<view class="logo-box">
|
||||
<image class="logo" src="/static/logo.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="brand">
|
||||
<text class="brand-title font-serif">人生 OS</text>
|
||||
<text class="brand-subtitle">LIFE HARMONY v3.1</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right" @click="goToProfile">
|
||||
<view class="avatar-box">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="page-content" scroll-y>
|
||||
<view v-if="activeTab === 'record'" class="tab-page">
|
||||
<RecordView></RecordView>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'script'" class="tab-page">
|
||||
<ScriptView></ScriptView>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'path'" class="tab-page">
|
||||
<PathView></PathView>
|
||||
</view>
|
||||
<scroll-view class="content" scroll-y :enhanced="true" :show-scrollbar="false">
|
||||
<RecordView v-if="activeTab === 'record'" />
|
||||
<ScriptView v-if="activeTab === 'script'" />
|
||||
<MineView v-if="activeTab === 'mine'" />
|
||||
</scroll-view>
|
||||
|
||||
<!-- 全局音乐播放器 -->
|
||||
<MusicPlayer ref="musicPlayer" />
|
||||
|
||||
<view class="bottom-nav" :style="{ paddingBottom: safeAreaBottom + 'px' }">
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'record' }"
|
||||
@click="switchTab('record')"
|
||||
>
|
||||
<text class="nav-icon">📖</text>
|
||||
<text class="nav-label">回溯过去</text>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'script' }"
|
||||
@click="switchTab('script')"
|
||||
>
|
||||
<text class="nav-icon">✨</text>
|
||||
<text class="nav-label">创造未来</text>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === 'path' }"
|
||||
@click="switchTab('path')"
|
||||
>
|
||||
<text class="nav-icon">🗺️</text>
|
||||
<text class="nav-label">路径实现</text>
|
||||
<view class="bottom-nav">
|
||||
<view class="nav-inner" :style="{ paddingBottom: safeAreaBottom + 'px' }">
|
||||
<view class="nav-item" :class="{ active: activeTab === 'record' }" @click="switchTab('record')">
|
||||
<view class="planet-icon">
|
||||
<view class="planet-core"></view>
|
||||
</view>
|
||||
<text>人生轨迹</text>
|
||||
</view>
|
||||
<view class="nav-item" :class="{ active: activeTab === 'script' }" @click="switchTab('script')">
|
||||
<view class="book-icon">
|
||||
<view></view>
|
||||
<view></view>
|
||||
</view>
|
||||
<text>爽文生成</text>
|
||||
</view>
|
||||
<view class="nav-item" :class="{ active: activeTab === 'mine' }" @click="switchTab('mine')">
|
||||
<view class="smile-icon">
|
||||
<view class="eye left"></view>
|
||||
<view class="eye right"></view>
|
||||
</view>
|
||||
<text>我的</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import RecordView from './RecordView.vue'
|
||||
import ScriptView from './ScriptView.vue'
|
||||
import PathView from './PathView.vue'
|
||||
import MineView from './MineView.vue'
|
||||
import MusicPlayer from '../../components/MusicPlayer.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const activeTab = ref('record')
|
||||
const musicPlayer = ref(null)
|
||||
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
|
||||
// 星空背景数据
|
||||
const stars = ref([])
|
||||
|
||||
const initStars = () => {
|
||||
stars.value = []
|
||||
for (let i = 0; i < 60; i++) {
|
||||
stars.value.push({
|
||||
id: i,
|
||||
size: Math.random() * 3 + 1,
|
||||
left: Math.random() * 100,
|
||||
top: Math.random() * 100,
|
||||
opacity: 0.2 + Math.random() * 0.5,
|
||||
xMove: (Math.random() - 0.5) * 100,
|
||||
yMove: (Math.random() - 0.5) * 100,
|
||||
duration: 15 + Math.random() * 20,
|
||||
delay: Math.random() * -20
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
|
||||
|
||||
store.initialize()
|
||||
uni.$on('switchTab', switchTab)
|
||||
|
||||
initStars()
|
||||
|
||||
// 预加载所有 Tab 的数据,确保首次进入页面时数据已就绪
|
||||
await Promise.all([
|
||||
store.fetchEvents(),
|
||||
store.fetchScripts(),
|
||||
store.fetchPaths()
|
||||
])
|
||||
})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
const nickname = store.userProfile?.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
|
||||
})
|
||||
|
||||
const switchTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
|
||||
|
||||
const session = await store.restoreSession()
|
||||
if (session.status !== store.SESSION_STATUS.AUTHENTICATED) {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.$on('switchTab', switchTab)
|
||||
await Promise.all([
|
||||
store.fetchUserProfile(),
|
||||
store.fetchEvents(),
|
||||
store.fetchScripts(),
|
||||
store.fetchPaths(),
|
||||
store.fetchInspirationRecommendations()
|
||||
])
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('switchTab', switchTab)
|
||||
})
|
||||
|
||||
const goToProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/profile/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-page {
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
.app-shell {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #050615;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 星空背景 */
|
||||
.stars-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
opacity: var(--opacity);
|
||||
animation: float-star var(--duration) ease-in-out infinite;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
@keyframes float-star {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: var(--opacity);
|
||||
}
|
||||
50% {
|
||||
transform: translate(var(--x), var(--y)) scale(1.5);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
.space-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 82% 16%, rgba(100, 50, 255, 0.28), transparent 26%),
|
||||
radial-gradient(circle at 0% 58%, rgba(36, 134, 255, 0.2), transparent 28%),
|
||||
linear-gradient(180deg, #07091d 0%, #08031a 48%, #04020e 100%);
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
.nebula {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
filter: blur(20rpx);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
.nebula-a {
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
right: -170rpx;
|
||||
top: 160rpx;
|
||||
background: radial-gradient(circle, rgba(144, 67, 255, 0.38), transparent 66%);
|
||||
}
|
||||
|
||||
.nebula-b {
|
||||
width: 520rpx;
|
||||
height: 520rpx;
|
||||
left: -260rpx;
|
||||
bottom: 100rpx;
|
||||
background: radial-gradient(circle, rgba(30, 104, 255, 0.18), transparent 70%);
|
||||
}
|
||||
|
||||
.stars {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
inset: 0;
|
||||
opacity: 0.48;
|
||||
background-image:
|
||||
radial-gradient(circle, rgba(255,255,255,0.82) 0 1rpx, transparent 2rpx),
|
||||
radial-gradient(circle, rgba(178,128,255,0.52) 0 1rpx, transparent 2rpx),
|
||||
radial-gradient(circle, rgba(255,202,120,0.8) 0 1rpx, transparent 2rpx);
|
||||
background-size: 122rpx 138rpx, 184rpx 164rpx, 246rpx 220rpx;
|
||||
background-position: 20rpx 0, 60rpx 30rpx, 16rpx 80rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
.safe-top {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-top: calc(24rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(24rpx + env(safe-area-inset-top));
|
||||
background: rgba(15, 7, 26, 0.6);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
display: block;
|
||||
font-size: 16rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tab-page {
|
||||
padding: 32rpx;
|
||||
padding-bottom: calc(180rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 28rpx 156rpx;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 140rpx;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: rgba(15, 7, 26, 0.95);
|
||||
backdrop-filter: blur(40rpx);
|
||||
-webkit-backdrop-filter: blur(40rpx);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
box-sizing: border-box;
|
||||
padding-left: 28rpx;
|
||||
padding-right: 28rpx;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
height: 108rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
border-radius: 34rpx;
|
||||
border: 1rpx solid rgba(153, 112, 255, 0.32);
|
||||
background: rgba(11, 9, 35, 0.84);
|
||||
box-shadow: inset 0 0 38rpx rgba(129, 65, 255, 0.12), 0 18rpx 70rpx rgba(0, 0, 0, 0.36);
|
||||
backdrop-filter: blur(26rpx);
|
||||
-webkit-backdrop-filter: blur(26rpx);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
min-width: 160rpx;
|
||||
gap: 8rpx;
|
||||
color: rgba(216, 208, 235, 0.58);
|
||||
font-size: 23rpx;
|
||||
font-weight: 600;
|
||||
transition: transform 0.22s ease, color 0.22s ease;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #C084FC;
|
||||
transform: translateY(-8rpx);
|
||||
text-shadow: 0 0 15px rgba(168, 85, 247, 0.6);
|
||||
color: #b86cff;
|
||||
transform: translateY(-4rpx);
|
||||
text-shadow: 0 0 22rpx rgba(178, 91, 255, 0.8);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 40rpx;
|
||||
.planet-icon,
|
||||
.book-icon,
|
||||
.smile-icon {
|
||||
position: relative;
|
||||
width: 46rpx;
|
||||
height: 42rpx;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
.planet-core {
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 8rpx;
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 24rpx currentColor;
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
.planet-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 2rpx;
|
||||
top: 14rpx;
|
||||
width: 42rpx;
|
||||
height: 14rpx;
|
||||
border: 4rpx solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
transform: rotate(-22deg);
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
display: flex;
|
||||
gap: 4rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-icon view {
|
||||
width: 17rpx;
|
||||
height: 34rpx;
|
||||
border-radius: 6rpx 3rpx 3rpx 6rpx;
|
||||
border: 4rpx solid currentColor;
|
||||
box-shadow: 0 0 18rpx currentColor;
|
||||
}
|
||||
|
||||
.smile-icon {
|
||||
border-radius: 50%;
|
||||
border: 5rpx solid currentColor;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.eye.left { left: 12rpx; }
|
||||
.eye.right { right: 12rpx; }
|
||||
|
||||
.smile-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
right: 12rpx;
|
||||
bottom: 10rpx;
|
||||
height: 8rpx;
|
||||
border-bottom: 4rpx solid currentColor;
|
||||
border-radius: 0 0 20rpx 20rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,314 +1,61 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="bg-decoration">
|
||||
<view class="aurora-top"></view>
|
||||
<view class="aurora-bottom"></view>
|
||||
<view class="top-safe" :style="{ height: safeAreaTop + 'px' }"></view>
|
||||
<view class="header">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">个人中心</text>
|
||||
<text></text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y :style="{ paddingTop: safeAreaTop + 20 + 'px', paddingBottom: safeAreaBottom + 20 + 'px' }">
|
||||
<view class="user-card">
|
||||
<view class="avatar-box">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill" />
|
||||
<view class="verified-badge">✓</view>
|
||||
</view>
|
||||
|
||||
<view class="user-info">
|
||||
<text class="nickname font-serif">{{ userProfile.nickname || '未同步系统' }}</text>
|
||||
<text class="user-tags">{{ userTags }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-card glass-card">
|
||||
<text class="stat-label">觉醒深度</text>
|
||||
<text class="stat-value">Lv.4</text>
|
||||
</view>
|
||||
<view class="stat-card glass-card">
|
||||
<text class="stat-label">星历契合</text>
|
||||
<text class="stat-value">98%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<view class="menu-item glass-card" @click="editProfile">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">👤</text>
|
||||
<text class="menu-title">个人档案设置</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item glass-card">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">🔄</text>
|
||||
<text class="menu-title">多账号切换</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item glass-card">
|
||||
<view class="menu-left">
|
||||
<text class="menu-icon">📧</text>
|
||||
<text class="menu-title">与开发者对话</text>
|
||||
</view>
|
||||
<text class="menu-arrow">↗</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="logout-btn" @click="handleLogout">
|
||||
TERMINATE LIFE HARMONY
|
||||
</button>
|
||||
</scroll-view>
|
||||
|
||||
<MineView class="content" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import MineView from '../main/MineView.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
const safeAreaTop = ref(20)
|
||||
|
||||
onMounted(() => {
|
||||
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
|
||||
const info = uni.getWindowInfo()
|
||||
safeAreaTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
||||
})
|
||||
|
||||
const userProfile = computed(() => store.userProfile || {})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
const nickname = userProfile.value.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${nickname}&backgroundColor=A855F7`
|
||||
})
|
||||
|
||||
const userTags = computed(() => {
|
||||
const { mbti, zodiac, profession } = userProfile.value
|
||||
const tags = [mbti || 'QUESTER', zodiac || 'STAR', profession || '星民']
|
||||
return tags.join(' · ')
|
||||
})
|
||||
|
||||
const editProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await store.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
|
||||
position: relative;
|
||||
color: #fff;
|
||||
background:
|
||||
radial-gradient(circle at 18% 2%, rgba(124, 58, 237, 0.3), transparent 34%),
|
||||
linear-gradient(180deg, #090514 0%, #1a0a2f 55%, #07020d 100%);
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
.header {
|
||||
height: 92rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 80rpx 1fr 80rpx;
|
||||
align-items: center;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
.aurora-top {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
filter: blur(120rpx);
|
||||
border-radius: 50%;
|
||||
.back {
|
||||
font-size: 62rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.aurora-bottom {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
filter: blur(100rpx);
|
||||
border-radius: 50%;
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40rpx;
|
||||
padding-top: calc(60rpx + constant(safe-area-inset-top));
|
||||
padding-top: calc(60rpx + env(safe-area-inset-top));
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
/* 原型标准:强发光边框 */
|
||||
border: 4rpx solid rgba(168, 85, 247, 0.3);
|
||||
box-shadow: 0 0 40px rgba(168, 85, 247, 0.1),
|
||||
inset 0 0 20rpx rgba(168, 85, 247, 0.05);
|
||||
padding: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.verified-badge {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: #9333EA;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
color: white;
|
||||
border: 4rpx solid #0F071A;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 16rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.user-tags {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(168, 85, 247, 0.6);
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
/* 原型标准:玻璃态效果 */
|
||||
background: rgba(168, 85, 247, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(168, 85, 247, 0.15);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(168, 85, 247, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 12rpx rgba(168, 85, 247, 0.03);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: 4rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(243, 232, 255, 0.9);
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
padding: 32rpx;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 6rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
color: rgba(168, 85, 247, 0.5);
|
||||
border-color: rgba(168, 85, 247, 0.2);
|
||||
padding: 24rpx 32rpx 40rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,31 +16,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import { logRuntimeEnv } from '../../services/request.js'
|
||||
|
||||
const statusBarHeight = ref(20)
|
||||
const safeAreaBottom = ref(0)
|
||||
|
||||
onLoad(() => {
|
||||
onMounted(() => {
|
||||
logRuntimeEnv('splash:onLoad')
|
||||
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
statusBarHeight.value = windowInfo.statusBarHeight || 20
|
||||
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
|
||||
setTimeout(async () => {
|
||||
const store = useAppStore()
|
||||
const token = uni.getStorageSync('access_token')
|
||||
if (token) {
|
||||
await store.fetchUserProfile()
|
||||
if (store.hasProfile) {
|
||||
uni.reLaunch({ url: '/pages/main/index' })
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/onboarding/index' })
|
||||
}
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
const session = await store.restoreSession()
|
||||
|
||||
if (session.status === store.SESSION_STATUS.AUTHENTICATED) {
|
||||
const target = session.hasProfile ? '/pages/main/index' : '/pages/onboarding/index'
|
||||
console.log('[AUTH] route', { target, reason: session.reason, hasProfile: session.hasProfile })
|
||||
uni.reLaunch({ url: target })
|
||||
return
|
||||
}
|
||||
|
||||
if (session.status === store.SESSION_STATUS.ERROR) {
|
||||
console.log('[AUTH] route', { target: '/pages/login/index', reason: session.reason })
|
||||
uni.showToast({ title: '服务连接异常,请稍后重试', icon: 'none' })
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[AUTH] route', { target: '/pages/login/index', reason: session.reason })
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user