Files
happy-life-star/mini-program/src/pages/main/PathView.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

302 lines
7.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="path-view">
<text class="page-title">实现路径</text>
<view v-if="!selectedScript" class="empty-state glass-card">
<text class="empty-icon">🗺</text>
<text class="empty-text">先生成剧本方能洞察路径</text>
<button class="btn-secondary empty-btn" @click="goToScript">去生成剧本</button>
</view>
<view v-else-if="!currentPath" class="empty-state glass-card">
<text class="empty-icon">🎯</text>
<text class="empty-text">等待开启人生导航...</text>
</view>
<view v-else class="path-content">
<view class="target-card glass-card-gold">
<text class="target-label">目标{{ currentPath.title }}</text>
<text class="target-summary">{{ (currentPath.description || currentPath.summary || currentPath.content || '').slice(0, 80) }}...</text>
</view>
<view class="timeline">
<view class="timeline-line"></view>
<view
v-for="(step, index) in currentPath.steps"
:key="index"
class="timeline-item"
>
<view class="timeline-node" :class="step.done ? 'completed' : 'pending'"></view>
<view class="step-card glass-card" :class="{ done: step.done }">
<text class="step-phase">节点 {{ index + 1 }}</text>
<text class="step-task">{{ step.task }}</text>
<text class="step-desc">{{ step.desc }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref, onMounted, watch } from 'vue'
import { useAppStore } from '../../stores/app.js'
import * as lifePathService from '../../services/lifePath.js'
const store = useAppStore()
const pathData = ref(null)
const selectedScript = computed(() => {
return store.scripts.find(s => s.isSelected)
})
const currentPath = computed(() => {
if (pathData.value) return pathData.value
if (store.currentPath) return store.currentPath
return null
})
const loadPath = async (scriptId) => {
if (!scriptId) return
try {
const res = await lifePathService.getPathByScriptId(scriptId)
pathData.value = res.data || await createPlaceholderPath(scriptId)
store.setCurrentPath(pathData.value)
} catch (error) {
pathData.value = await createPlaceholderPath(scriptId)
store.setCurrentPath(pathData.value)
}
}
const createPlaceholderPath = async (scriptId) => {
const script = selectedScript.value || {}
const title = script.title ? `${script.title} · 实现路径` : '我的实现路径'
const steps = [
{ phase: '阶段1', task: '整理目标', desc: '把剧本中的关键目标拆成可以执行的小目标。', content: '把剧本中的关键目标拆成可以执行的小目标。', done: true },
{ phase: '阶段2', task: '建立习惯', desc: '选择一个最小行动,每天稳定推进。', content: '选择一个最小行动,每天稳定推进。', done: false },
{ phase: '阶段3', task: '复盘迭代', desc: '每周回看进展,根据现实反馈调整路径。', content: '每周回看进展,根据现实反馈调整路径。', done: false }
]
try {
const res = await lifePathService.createPath({
scriptId,
title,
description: script.summary || '根据选中的人生剧本生成的占位实现路径,后续可接入AI生成更细的行动计划。',
steps,
progress: 8,
status: 'active'
})
return lifePathService.transformToFrontendFormat(res.data)
} catch (error) {
return {
id: `local-${scriptId}`,
scriptId,
title,
description: script.summary || '占位实现路径',
steps,
progress: 8,
status: 'active'
}
}
}
const goToScript = () => {
uni.$emit('switchTab', 'script')
}
watch(selectedScript, (val) => {
if (val?.id) {
loadPath(val.id)
} else {
pathData.value = null
}
})
onMounted(() => {
if (selectedScript.value?.id) {
loadPath(selectedScript.value.id)
}
})
</script>
<style scoped>
.path-view {
display: flex;
flex-direction: column;
gap: 32rpx;
min-height: 100%;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.page-title {
font-size: 36rpx;
font-weight: 400;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
letter-spacing: 4rpx;
font-family: 'Cinzel', 'Inter', serif;
}
.empty-state {
padding: 120rpx 64rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 32rpx;
}
.empty-icon {
font-size: 80rpx;
opacity: 0.3;
}
.empty-text {
font-size: 24rpx;
color: rgba(192, 132, 252, 0.5);
font-style: italic;
text-align: center;
line-height: 1.6;
}
.empty-btn {
font-size: 22rpx;
padding: 18rpx 36rpx;
}
.path-content {
display: flex;
flex-direction: column;
gap: 48rpx;
}
.target-card {
padding: 40rpx;
margin-bottom: 16rpx;
/* 金色玻璃态 + 左侧紫色边框 */
border-left: 4rpx solid #C084FC;
box-shadow: inset 0 0 20rpx rgba(168, 85, 247, 0.05),
0 4rpx 16rpx rgba(168, 85, 247, 0.1);
}
.target-label {
display: block;
font-size: 18rpx;
color: #C084FC;
font-weight: 600;
letter-spacing: 4rpx;
text-transform: uppercase;
margin-bottom: 16rpx;
}
.target-summary {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
}
.timeline {
position: relative;
padding-left: 60rpx;
display: flex;
flex-direction: column;
gap: 48rpx;
}
.timeline-line {
position: absolute;
left: 30rpx;
top: 30rpx;
bottom: 30rpx;
width: 4rpx;
background: linear-gradient(180deg,
rgba(168, 85, 247, 0.4) 0%,
rgba(168, 85, 247, 0.2) 50%,
rgba(168, 85, 247, 0.05) 100%
);
border-radius: 4rpx;
box-shadow: 0 0 15px rgba(168, 85, 247, 0.2);
}
.timeline-item {
position: relative;
}
.timeline-node {
position: absolute;
left: -46rpx;
top: 24rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(135deg, #A855F7 0%, #9333EA 100%);
border: 4rpx solid #C084FC;
/* 原型标准:节点发光 */
box-shadow: 0 0 15px rgba(168, 85, 247, 0.4),
inset 0 0 10rpx rgba(255, 255, 255, 0.2);
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.timeline-node.completed {
animation: node-pulse 2s ease-in-out infinite;
}
@keyframes node-pulse {
0%, 100% {
box-shadow: 0 0 15px rgba(168, 85, 247, 0.4),
inset 0 0 10rpx rgba(255, 255, 255, 0.2);
transform: scale(1);
}
50% {
box-shadow: 0 0 25px rgba(168, 85, 247, 0.6),
inset 0 0 15rpx rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
}
.timeline-node.pending {
background: #0F071A;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 10px rgba(168, 85, 247, 0.1);
}
.step-card {
padding: 32rpx;
opacity: 0.6;
}
.step-card.done {
opacity: 1;
border-color: rgba(192, 132, 252, 0.3);
}
.step-phase {
display: block;
font-size: 18rpx;
color: #C084FC;
font-weight: 600;
letter-spacing: 4rpx;
margin-bottom: 12rpx;
}
.step-task {
display: block;
font-size: 30rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 12rpx;
}
.step-desc {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
line-height: 1.5;
}
</style>