feat: 优化管理后台页面UI、修复TS编译错误、新增人生事件模块

- 优化 AI 配置列表页面:重构统计卡片、搜索表单、表格列展示
- 修复 3 处 TypeScript TS6133 编译错误,恢复构建
- 新增管理员修改密码和重置密码功能
- 优化小程序多个页面样式和交互
- 人生事件模块完善

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 23:23:09 +08:00
parent 60c63850ee
commit 755059807a
62 changed files with 4661 additions and 3019 deletions
+5 -2
View File
@@ -1,8 +1,11 @@
# 开发环境配置(本地开发调试)
VITE_APP_ENV=dev
# 本地环境
VITE_API_BASE_URL=http://localhost:19089/api
VITE_WS_URL=ws://localhost:19089/ws
# VITE_API_BASE_URL=http://localhost:19089/api
# VITE_WS_URL=ws://localhost:19089/ws
# 直连后端服务(不经过 Nginx
VITE_API_BASE_URL=http://101.200.208.45:19089/api
VITE_WS_URL=ws://101.200.208.45:19089/ws
# 测试环境
# VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
# VITE_WS_URL=wss://lifescript.happylifeos.com/ws
+3 -2
View File
@@ -1,5 +1,6 @@
# 测试环境配置(小程序体验版)
VITE_APP_ENV=test
VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
VITE_WS_URL=wss://lifescript.happylifeos.com/ws
# 直连后端服务(不经过 Nginx
VITE_API_BASE_URL=http://101.200.208.45:19089/api
VITE_WS_URL=ws://101.200.208.45:19089/ws
VITE_DEBUG=true
+67 -12
View File
@@ -4,15 +4,23 @@
<!-- 分割线 -->
<view v-if="block.type === 'hr'" class="markdown-hr"></view>
<text v-else-if="block.type === 'h1'" class="markdown-h1">{{ block.content }}</text>
<text v-else-if="block.type === 'h2'" class="markdown-h2">{{ block.content }}</text>
<!-- 三级标题 -->
<text v-else-if="block.type === 'h3'" class="markdown-h3">{{ block.content }}</text>
<!-- 四级标题 -->
<text v-else-if="block.type === 'h4'" class="markdown-h4">{{ block.content }}</text>
<view v-else-if="block.type === 'blockquote'" class="markdown-quote">
<text>{{ block.content }}</text>
</view>
<!-- 列表项 -->
<view v-else-if="block.type === 'li'" class="markdown-li">
<text class="li-bullet"> </text>
<text class="li-bullet">{{ block.marker }}</text>
<text class="li-content">{{ block.content }}</text>
</view>
@@ -53,6 +61,18 @@ const parsedBlocks = computed(() => {
continue
}
const h1Match = trimmed.match(/^#\s+(.+)/)
if (h1Match) {
blocks.push({ type: 'h1', content: h1Match[1] })
continue
}
const h2Match = trimmed.match(/^##\s+(.+)/)
if (h2Match) {
blocks.push({ type: 'h2', content: h2Match[1] })
continue
}
// 三级标题 ###
const h3Match = trimmed.match(/^###\s+(.+)/)
if (h3Match) {
@@ -67,10 +87,16 @@ const parsedBlocks = computed(() => {
continue
}
// 列表项 * 或 -
const liMatch = trimmed.match(/^[*\-]\s+(.+)/)
const quoteMatch = trimmed.match(/^>\s+(.+)/)
if (quoteMatch) {
blocks.push({ type: 'blockquote', content: quoteMatch[1] })
continue
}
// 列表项 *、-、+ 或 1.
const liMatch = trimmed.match(/^([*\-+]|\d+\.)\s+(.+)/)
if (liMatch) {
blocks.push({ type: 'li', content: liMatch[1] })
blocks.push({ type: 'li', marker: /^\d+\./.test(liMatch[1]) ? liMatch[1] : '•', content: liMatch[2] })
continue
}
@@ -117,7 +143,7 @@ const parseBoldText = (text) => {
.markdown-container {
display: flex;
flex-direction: column;
gap: 16rpx;
gap: 14rpx;
}
/* 分割线 */
@@ -133,24 +159,52 @@ const parseBoldText = (text) => {
}
/* 标题 */
.markdown-h1,
.markdown-h2,
.markdown-h3 {
display: block;
font-size: 30rpx;
font-weight: 600;
color: rgba(243, 232, 255, 0.95);
margin: 20rpx 0 12rpx 0;
color: rgba(248, 244, 255, 0.96);
line-height: 1.4;
font-weight: 800;
}
.markdown-h1 {
font-size: 32rpx;
margin: 4rpx 0 10rpx 0;
}
.markdown-h2 {
font-size: 30rpx;
margin: 14rpx 0 8rpx 0;
}
.markdown-h3 {
font-size: 28rpx;
margin: 12rpx 0 6rpx 0;
}
.markdown-h4 {
display: block;
font-size: 28rpx;
font-weight: 600;
font-size: 26rpx;
font-weight: 800;
color: rgba(243, 232, 255, 0.95);
margin: 16rpx 0 8rpx 0;
margin: 10rpx 0 4rpx 0;
line-height: 1.4;
}
.markdown-quote {
padding: 16rpx 18rpx;
border-left: 5rpx solid rgba(180, 108, 255, 0.8);
border-radius: 14rpx;
color: rgba(218, 202, 246, 0.82);
background: rgba(168, 85, 247, 0.1);
}
.markdown-quote text {
font-size: 24rpx;
line-height: 1.65;
}
/* 列表项 */
.markdown-li {
display: flex;
@@ -164,6 +218,7 @@ const parseBoldText = (text) => {
color: #C084FC;
font-size: 24rpx;
line-height: 1.5;
min-width: 28rpx;
flex-shrink: 0;
}
+10 -3
View File
@@ -16,7 +16,6 @@ import { ref, onMounted, onUnmounted } from 'vue'
const isPlaying = ref(false)
const bottomPosition = ref('180rpx')
const audioContext = null
let audioInstance = null
// 背景音乐 URL - 使用原型中的音乐
@@ -29,9 +28,18 @@ const initAudio = () => {
audioInstance.loop = true
audioInstance.autoplay = false
audioInstance.onPlay(() => {
isPlaying.value = true
})
audioInstance.onPause(() => {
isPlaying.value = false
})
audioInstance.onError((err) => {
console.error('音乐播放失败:', err)
isPlaying.value = false
uni.showToast({ title: '音乐资源暂不可用', icon: 'none' })
})
audioInstance.onEnded(() => {
@@ -47,8 +55,7 @@ const toggleMusic = async () => {
audioInstance.pause()
} else {
try {
await audioInstance.play()
isPlaying.value = true
audioInstance.play()
} catch (err) {
console.error('音乐播放失败:', err)
isPlaying.value = false
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+734 -161
View File
@@ -1,248 +1,821 @@
<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 class="script-library">
<view class="page-head">
<view class="title-row">
<text class="spark"></text>
<text class="page-title">我的剧本</text>
</view>
<view class="head-actions">
<view class="circle-btn" @click="openSearch">
<view class="search-icon"></view>
</view>
<text class="signature">{{ signature }}</text>
<view class="chips">
<text v-for="chip in chips" :key="chip" class="chip kos-pill">{{ chip }}</text>
<view class="circle-btn" @click="openMoreMenu">
<view class="more-icon">
<view></view>
<view></view>
<view></view>
</view>
</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 class="type-tabs">
<text
v-for="tab in typeTabs"
:key="tab.value"
class="type-tab"
:class="{ active: activeType === tab.value }"
@click="activeType = tab.value"
>{{ tab.label }}</text>
<view class="new-script" @click="createScript">
<text class="plus"></text>
<text>新建剧本</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 class="filter-bar">
<scroll-view class="status-scroll" scroll-x :show-scrollbar="false">
<view class="status-row">
<text
v-for="filter in statusFilters"
:key="filter.value"
class="status-chip"
:class="{ active: activeStatus === filter.value }"
@click="activeStatus = filter.value"
>{{ filter.label }}</text>
</view>
</scroll-view>
<view class="sort-tools">
<text class="sort-text" @click="toggleSort">{{ sortLabel }}</text>
<view class="grid-icon" :class="{ active: viewMode === 'grid' }" @click="toggleViewMode">
<view v-for="i in 4" :key="i"></view>
</view>
</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 v-if="visibleScripts.length" class="script-list" :class="{ grid: viewMode === 'grid' }">
<view
v-for="(script, index) in visibleScripts"
:key="script.id || index"
class="script-card"
@click="viewScript(script)"
>
<view class="cover" :class="'cover-' + (index % 6)">
<text>{{ getInitial(script) }}</text>
</view>
<view class="card-main">
<view class="card-top">
<view class="title-wrap">
<text class="script-title">{{ script.title }}</text>
<text class="length-badge">{{ getLengthLabel(script.length) }}</text>
</view>
<view class="right-state">
<text class="state-pill" :class="'state-' + getStatus(script)">{{ getStatusLabel(script) }}</text>
<text class="ellipsis" @click.stop="openScriptMenu(script)"></text>
</view>
</view>
<view class="tag-row">
<text v-for="tag in getTags(script)" :key="tag" class="tag">{{ tag }}</text>
</view>
<text class="summary">{{ script.summary || script.content || '一段正在生成中的平行人生剧本。' }}</text>
<view class="meta-row">
<text>{{ getChapterCount(script) }}</text>
<text>|</text>
<text>{{ getWordCount(script) }}</text>
<text>|</text>
<text>{{ getDateText(script) }}</text>
</view>
<view v-if="getStatus(script) === 'progress'" class="progress-row">
<view class="progress-track">
<view class="progress-fill" :style="{ width: getProgress(script) + '%' }"></view>
</view>
<text>{{ getProgress(script) }}%</text>
</view>
<view v-else-if="isFavorite(script)" class="favorite-row">
<text class="favorite-star"></text>
<text>已收藏</text>
</view>
</view>
</view>
</view>
<button class="logout kos-pill" @click="handleLogout">退出登录</button>
<view v-else class="empty-card">
<view class="empty-book">
<view></view>
<view></view>
</view>
<text class="empty-title">还没有人生剧本</text>
<text class="empty-text">去爽文生成页写下一句灵感生成你的第一段平行人生</text>
<view class="empty-action" @click="createScript">新建剧本</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref } 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 activeType = ref('long')
const activeStatus = ref('all')
const keyword = ref('')
const sortMode = ref('updated')
const viewMode = ref('list')
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
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 typeTabs = [
{ label: '长篇', value: 'long' },
{ label: '短篇', value: 'short' },
{ label: '风格', value: 'style' }
]
const statusFilters = [
{ label: '全部', value: 'all' },
{ label: '进行中', value: 'progress' },
{ label: '已完成', value: 'done' },
{ label: '草稿箱', value: 'draft' },
{ label: '收藏夹', value: 'favorite' }
]
const fallbackScripts = [
{
id: 'demo-1',
title: '逆袭人生:从低谷到巅峰',
length: 'long',
status: 'progress',
tags: ['逆袭成长', '都市', '事业', '热血'],
summary: '从被分手、被否定的低谷开始,凭借天赋、努力与智慧,一步步逆袭成为行业巅峰,收获事业、财富...',
chapterCount: 28,
wordCount: 128000,
updatedAt: '今天 21:30',
progress: 28
},
{
id: 'demo-2',
title: '如果那年我没有放弃',
length: 'long',
status: 'done',
tags: ['成长治愈', '校园', '爱情', '温暖'],
summary: '重回十八岁,弥补遗憾,勇敢追梦,守护那些曾经错过的人和事。',
chapterCount: 36,
wordCount: 156000,
completedAt: '2025.05.10',
isFavorite: true
},
{
id: 'demo-3',
title: '重生之我在未来等你',
length: 'long',
status: 'progress',
tags: ['重生', '科幻', '爱情', '未来'],
summary: '一觉醒来,回到十年前的那一天。这一次,我不仅要改变自己的人生,还要找到你。',
chapterCount: 18,
wordCount: 83000,
updatedAt: '昨天 18:47',
progress: 46
},
{
id: 'demo-4',
title: '天才作曲家的璀璨之路',
length: 'long',
status: 'draft',
tags: ['音乐', '励志', '天赋', '梦想'],
summary: '从默默无闻到享誉全球,用音符征服世界,写下属于自己的传奇乐章。',
chapterCount: 9,
wordCount: 31000,
createdAt: '2025.05.08'
},
{
id: 'demo-5',
title: '咖啡馆里的奇遇',
length: 'short',
status: 'done',
tags: ['生活', '治愈', '奇幻', '温暖'],
summary: '一杯咖啡,一次奇遇,改变了我平凡的生活,也让我遇见了最特别的你。',
chapterCount: 1,
wordCount: 23000,
completedAt: '2025.05.01',
isFavorite: true
},
{
id: 'demo-6',
title: '赛博时代的追光者',
length: 'long',
status: 'draft',
tags: ['科幻', '未来', '冒险', '热血'],
summary: '在数据与代码构建的世界里,我追寻光明,也在黑暗中寻找真正的自由。',
chapterCount: 3,
wordCount: 12000,
createdAt: '2025.05.12'
}
]
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
const signature = computed(() => profile.value.future?.ideal || '正在把人生整理成一份会发光的档案。')
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
const visibleScripts = computed(() => {
const filtered = scripts.value.filter(script => {
const status = getStatus(script)
if (keyword.value) {
const haystack = [script.title, script.summary, script.content, script.style, ...(script.tags || [])].join(' ')
if (!haystack.includes(keyword.value)) return false
}
if (activeStatus.value === 'favorite') return isFavorite(script)
if (activeStatus.value !== 'all' && status !== activeStatus.value) return false
if (activeType.value === 'short') return script.length === 'short'
if (activeType.value === 'long') return script.length !== 'short'
return true
})
return [...filtered].sort((a, b) => {
if (sortMode.value === 'words') return Number(b.wordCount || 0) - Number(a.wordCount || 0)
if (sortMode.value === 'progress') return getProgress(b) - getProgress(a)
return String(b.updateTime || b.updatedAt || b.createTime || b.date || '').localeCompare(String(a.updateTime || a.updatedAt || a.createTime || a.date || ''))
})
})
const sortLabel = computed(() => {
const map = { updated: '最近更新⌄', words: '字数最多⌄', progress: '进度最高⌄' }
return map[sortMode.value] || '最近更新⌄'
})
const getStatus = (script) => {
if (script.status) return script.status
if (script.isDraft) return 'draft'
if (script.isCompleted || script.completedAt) return 'done'
return script.progress ? 'progress' : 'done'
}
const handleLogout = () => {
const getStatusLabel = (script) => {
const map = { progress: '进行中', done: '已完成', draft: '草稿' }
return map[getStatus(script)] || '已完成'
}
const getLengthLabel = (length) => {
return length === 'short' ? '短篇' : '长篇'
}
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return [script.style || '逆袭成长', '都市', '事业', '热血']
}
const getChapterCount = (script) => script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (!count) return '3.1万字'
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || '2025.05.10'}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || '2025.05.08'}`
return `最近更新:${script.updatedAt || script.date || '今天 21:30'}`
}
const getProgress = (script) => Math.max(1, Math.min(99, Number(script.progress || 28)))
const getInitial = (script) => (script.title || '剧').slice(0, 1)
const isFavorite = (script) => {
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
}
const viewScript = (script) => {
if (!script?.id || String(script.id).startsWith('demo-')) return
uni.navigateTo({ url: `/pages/main/ScriptDetailView?id=${script.id}` })
}
const createScript = () => {
uni.$emit('switchTab', 'script')
}
const openSearch = () => {
uni.showModal({
title: '退出登录',
content: '确定要离开当前数字生命档案吗?',
success: async (res) => {
if (res.confirm) {
await store.logout()
uni.reLaunch({ url: '/pages/login/index' })
title: '搜索剧本',
editable: true,
placeholderText: '输入标题、标签或关键词',
success: (res) => {
if (res.confirm) keyword.value = String(res.content || '').trim()
}
})
}
const openMoreMenu = () => {
uni.showActionSheet({
itemList: ['清空搜索', '只看收藏', '查看全部'],
success: ({ tapIndex }) => {
if (tapIndex === 0) keyword.value = ''
if (tapIndex === 1) activeStatus.value = 'favorite'
if (tapIndex === 2) {
keyword.value = ''
activeStatus.value = 'all'
}
}
})
}
const toggleSort = () => {
const order = ['updated', 'words', 'progress']
sortMode.value = order[(order.indexOf(sortMode.value) + 1) % order.length]
}
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
}
const openScriptMenu = (script) => {
const favorite = isFavorite(script)
uni.showActionSheet({
itemList: [favorite ? '取消收藏' : '收藏剧本', '查看详情', '映射路径'],
success: ({ tapIndex }) => {
if (tapIndex === 0) {
const next = { ...localFavorites.value }
if (favorite) delete next[String(script.id)]
else next[String(script.id)] = true
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
uni.showToast({ title: favorite ? '已取消收藏' : '已收藏', icon: 'success' })
}
if (tapIndex === 1) viewScript(script)
if (tapIndex === 2) mapScript(script)
}
})
}
const mapScript = async (script) => {
if (!script?.id || String(script.id).startsWith('demo-')) {
uni.showToast({ title: '示例剧本暂不可映射', icon: 'none' })
return
}
const res = await store.selectScript(script.id)
if (!res.success) {
uni.showToast({ title: res.error || '映射失败', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/main/PathView' })
}
</script>
<style scoped>
.mine-view {
.script-library {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 32rpx;
padding-bottom: 26rpx;
}
.profile-card {
border-radius: 34rpx;
padding: 32rpx;
.page-head,
.title-row,
.head-actions,
.type-tabs,
.filter-bar,
.sort-tools,
.card-top,
.title-wrap,
.right-state,
.meta-row,
.progress-row,
.favorite-row {
display: flex;
align-items: center;
gap: 22rpx;
}
.avatar {
width: 112rpx;
height: 112rpx;
flex-shrink: 0;
.page-head {
justify-content: space-between;
}
.title-row {
gap: 14rpx;
}
.spark {
color: #ffd58c;
font-size: 34rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.5);
}
.page-title {
color: #fff;
font-size: 42rpx;
font-weight: 900;
}
.head-actions {
gap: 20rpx;
}
.circle-btn {
width: 58rpx;
height: 58rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 50rpx;
border: 1rpx solid rgba(142, 105, 255, 0.36);
background: rgba(10, 11, 38, 0.72);
}
.search-icon {
width: 25rpx;
height: 25rpx;
border: 4rpx solid #fff;
border-radius: 50%;
position: relative;
}
.search-icon::after {
content: '';
position: absolute;
right: -9rpx;
bottom: -8rpx;
width: 14rpx;
height: 4rpx;
border-radius: 999rpx;
background: #fff;
transform: rotate(45deg);
}
.more-icon {
display: flex;
gap: 5rpx;
}
.more-icon view {
width: 6rpx;
height: 6rpx;
border-radius: 50%;
background: #fff;
}
.type-tabs {
justify-content: space-between;
border-bottom: 1rpx solid rgba(126, 87, 255, 0.18);
padding-bottom: 16rpx;
}
.type-tab {
position: relative;
color: rgba(224, 214, 243, 0.7);
font-size: 31rpx;
font-weight: 900;
background: linear-gradient(135deg, #b245ff, #2a7dff);
box-shadow: 0 0 36rpx rgba(168, 85, 255, 0.55);
padding: 0 20rpx 14rpx;
}
.profile-main {
flex: 1;
min-width: 0;
.type-tab.active {
color: #fff;
}
.name-row {
.type-tab.active::after {
content: '';
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: -17rpx;
height: 5rpx;
border-radius: 999rpx;
background: #b246ff;
box-shadow: 0 0 18rpx rgba(178, 70, 255, 0.8);
}
.new-script {
margin-left: auto;
height: 64rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.name {
gap: 8rpx;
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;
font-weight: 800;
background: linear-gradient(135deg, #b346ff, #7330ff);
box-shadow: 0 0 26rpx rgba(168, 85, 247, 0.54);
}
.chips,
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
.plus {
font-size: 32rpx;
line-height: 1;
}
.chip,
.tag {
height: 44rpx;
padding: 0 16rpx;
.filter-bar {
gap: 14rpx;
}
.status-scroll {
flex: 1;
min-width: 0;
white-space: nowrap;
}
.status-row {
display: inline-flex;
gap: 16rpx;
}
.status-chip {
height: 52rpx;
min-width: 88rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
color: rgba(244, 235, 255, 0.86);
font-size: 21rpx;
justify-content: center;
color: rgba(224, 214, 243, 0.78);
font-size: 23rpx;
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.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;
.status-chip.active {
color: #fff;
font-size: 48rpx;
font-weight: 900;
border-color: rgba(206, 82, 255, 0.92);
background: rgba(130, 48, 220, 0.42);
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.46);
}
.stat-label {
display: block;
margin-top: 6rpx;
color: rgba(224, 211, 246, 0.62);
.sort-tools {
gap: 14rpx;
flex-shrink: 0;
}
.sort-text {
color: #c99fff;
font-size: 23rpx;
}
.section-card {
border-radius: 30rpx;
padding: 30rpx;
}
.section-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.memory-line {
.grid-icon {
width: 48rpx;
height: 48rpx;
border-radius: 18rpx;
display: grid;
grid-template-columns: 86rpx 1fr;
grid-template-columns: repeat(2, 1fr);
gap: 6rpx;
padding: 11rpx;
box-sizing: border-box;
border: 1rpx solid rgba(151, 111, 255, 0.32);
}
.grid-icon view {
border: 2rpx solid rgba(230, 222, 250, 0.78);
border-radius: 3rpx;
}
.script-list {
display: flex;
flex-direction: column;
gap: 18rpx;
padding: 22rpx 0;
border-bottom: 1rpx solid rgba(180, 139, 255, 0.14);
}
.memory-line:last-child {
border-bottom: 0;
.script-list.grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
.memory-label {
color: #b56cff;
.script-list.grid .script-card {
grid-template-columns: 1fr;
}
.script-list.grid .cover {
width: 100%;
}
.grid-icon.active {
border-color: rgba(206, 82, 255, 0.9);
background: rgba(130, 48, 220, 0.28);
}
.script-card {
display: grid;
grid-template-columns: 150rpx 1fr;
gap: 22rpx;
min-height: 196rpx;
padding: 20rpx;
border-radius: 24rpx;
border: 1rpx solid rgba(105, 79, 210, 0.34);
background:
radial-gradient(circle at 100% 0%, rgba(112, 72, 255, 0.14), transparent 38%),
rgba(9, 12, 42, 0.72);
box-shadow: inset 0 0 28rpx rgba(92, 57, 197, 0.08);
}
.cover {
width: 150rpx;
height: 150rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 54rpx;
font-weight: 900;
overflow: hidden;
background: linear-gradient(135deg, #3b1a90, #d65cff);
}
.cover-0 { background: linear-gradient(135deg, #29135f, #9037ff 48%, #191b5e); }
.cover-1 { background: linear-gradient(135deg, #3c1c4a, #f2b3cc 48%, #16143b); }
.cover-2 { background: linear-gradient(135deg, #1a225f, #7d4cff 48%, #0a0f2c); }
.cover-3 { background: linear-gradient(135deg, #2f240b, #f7b44a 48%, #0d0a16); }
.cover-4 { background: linear-gradient(135deg, #3f2417, #d8b58a 48%, #17101d); }
.cover-5 { background: linear-gradient(135deg, #141451, #cc46ff 48%, #0c0b28); }
.card-main {
min-width: 0;
}
.card-top {
justify-content: space-between;
gap: 14rpx;
}
.title-wrap {
min-width: 0;
gap: 10rpx;
}
.script-title {
color: #fff;
font-size: 27rpx;
line-height: 1.25;
font-weight: 900;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.length-badge {
flex-shrink: 0;
padding: 4rpx 9rpx;
border-radius: 8rpx;
color: #c985ff;
font-size: 18rpx;
border: 1rpx solid rgba(182, 92, 255, 0.5);
background: rgba(128, 55, 204, 0.22);
}
.right-state {
flex-shrink: 0;
gap: 14rpx;
}
.state-pill {
height: 34rpx;
padding: 0 14rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
font-size: 19rpx;
}
.state-progress {
color: #ffbf4c;
background: rgba(170, 103, 20, 0.22);
}
.state-done {
color: #79e6a9;
background: rgba(44, 146, 88, 0.2);
}
.state-draft {
color: rgba(224, 214, 243, 0.76);
background: rgba(255, 255, 255, 0.06);
}
.ellipsis {
color: rgba(224, 214, 243, 0.66);
font-size: 24rpx;
letter-spacing: 3rpx;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 16rpx;
}
.tag {
height: 34rpx;
padding: 0 14rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
color: #d49cff;
font-size: 19rpx;
background: rgba(149, 55, 255, 0.2);
}
.summary {
display: -webkit-box;
margin-top: 14rpx;
color: rgba(226, 215, 246, 0.72);
font-size: 22rpx;
line-height: 1.55;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.meta-row {
gap: 14rpx;
margin-top: 14rpx;
color: rgba(218, 204, 243, 0.66);
font-size: 21rpx;
}
.progress-row {
justify-content: flex-end;
gap: 14rpx;
margin-top: 14rpx;
color: #bd72ff;
font-size: 22rpx;
font-weight: 800;
}
.memory-text {
color: rgba(224, 211, 246, 0.72);
font-size: 24rpx;
line-height: 1.55;
.progress-track {
width: 118rpx;
height: 6rpx;
border-radius: 999rpx;
background: rgba(173, 160, 210, 0.18);
overflow: hidden;
}
.logout {
height: 72rpx;
.progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #b246ff, #d878ff);
}
.favorite-row {
justify-content: flex-end;
gap: 8rpx;
margin-top: 14rpx;
color: #b768ff;
font-size: 23rpx;
font-weight: 800;
}
.favorite-star {
font-size: 28rpx;
}
.empty-card {
margin-top: 30rpx;
border-radius: 26rpx;
padding: 44rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border: 1rpx solid rgba(105, 79, 210, 0.34);
background: rgba(9, 12, 42, 0.72);
}
.empty-book {
display: flex;
gap: 6rpx;
margin-bottom: 18rpx;
}
.empty-book view {
width: 32rpx;
height: 46rpx;
border: 4rpx solid #b768ff;
border-radius: 8rpx 4rpx 4rpx 8rpx;
}
.empty-title {
color: #fff;
font-size: 28rpx;
font-weight: 900;
}
.empty-text {
margin-top: 12rpx;
color: rgba(226, 215, 246, 0.68);
font-size: 22rpx;
line-height: 1.5;
}
.empty-action {
margin-top: 22rpx;
height: 56rpx;
padding: 0 30rpx;
border-radius: 999rpx;
color: rgba(224, 211, 246, 0.74);
font-size: 25rpx;
display: flex;
align-items: center;
color: #fff;
font-size: 23rpx;
font-weight: 800;
background: linear-gradient(135deg, #b346ff, #7330ff);
}
</style>
+34 -2
View File
@@ -63,10 +63,42 @@ const loadPath = async (scriptId) => {
if (!scriptId) return
try {
const res = await lifePathService.getPathByScriptId(scriptId)
pathData.value = res.data || null
pathData.value = res.data || await createPlaceholderPath(scriptId)
store.setCurrentPath(pathData.value)
} catch (error) {
pathData.value = null
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'
}
}
}
+141 -108
View File
@@ -66,7 +66,7 @@
</view>
<text class="section-subtitle">你的成长之路正在展开</text>
</view>
<view class="map-btn kos-pill">
<view class="map-btn kos-pill" @click="openMap">
<view class="map-icon"></view>
<text>轨迹地图</text>
</view>
@@ -81,7 +81,7 @@
:class="{ active: activeFilter === filter.value }"
@click="activeFilter = filter.value"
>{{ filter.label }}</text>
<text class="add-filter"></text>
<text class="add-filter" @click="addFilter"></text>
</view>
</scroll-view>
@@ -130,14 +130,16 @@ import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const activeFilter = ref('all')
const customFilters = ref([])
const filters = [
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 = [
{
@@ -192,6 +194,10 @@ const avatar = computed(() => {
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)
})
@@ -222,28 +228,50 @@ const createEvent = () => {
}
const openDetail = (event) => {
if (String(event.id || '').startsWith('sample-')) return
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: 28rpx;
padding-bottom: 34rpx;
gap: 24rpx;
padding-bottom: 22rpx;
}
.profile-card {
position: relative;
overflow: hidden;
border-radius: 34rpx;
padding: 40rpx 34rpx 32rpx;
padding: 34rpx 30rpx 28rpx;
}
.profile-card::after {
@@ -251,8 +279,8 @@ const editProfile = () => {
position: absolute;
right: -28rpx;
bottom: -20rpx;
width: 250rpx;
height: 180rpx;
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%;
@@ -269,18 +297,18 @@ const editProfile = () => {
.profile-top {
display: flex;
align-items: center;
gap: 24rpx;
gap: 22rpx;
}
.avatar-wrap {
position: relative;
flex-shrink: 0;
width: 134rpx;
height: 134rpx;
padding: 6rpx;
width: 112rpx;
height: 112rpx;
padding: 5rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fff, #9b54ff 38%, #4a67ff);
box-shadow: 0 0 42rpx rgba(149, 89, 255, 0.56);
box-shadow: 0 0 34rpx rgba(149, 89, 255, 0.52);
}
.avatar {
@@ -292,10 +320,10 @@ const editProfile = () => {
.avatar-edit {
position: absolute;
right: -6rpx;
bottom: -6rpx;
width: 48rpx;
height: 48rpx;
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);
@@ -308,7 +336,7 @@ const editProfile = () => {
border-radius: 6rpx;
background: #fff;
transform: rotate(-45deg);
margin: 21rpx auto;
margin: 17rpx auto;
}
.profile-info {
@@ -324,33 +352,33 @@ const editProfile = () => {
.profile-name {
color: #fff;
font-size: 44rpx;
font-size: 36rpx;
line-height: 1.1;
font-weight: 800;
}
.star {
color: #ffd589;
font-size: 30rpx;
font-size: 26rpx;
}
.profile-subtitle {
display: block;
margin-top: 14rpx;
margin-top: 10rpx;
color: rgba(239, 232, 255, 0.78);
font-size: 27rpx;
font-size: 24rpx;
}
.edit-profile {
flex-shrink: 0;
height: 64rpx;
padding: 0 22rpx;
height: 56rpx;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 10rpx;
gap: 8rpx;
color: #dccbff;
font-size: 24rpx;
font-size: 22rpx;
}
.tiny-pen {
@@ -363,12 +391,12 @@ const editProfile = () => {
position: relative;
z-index: 1;
height: 1rpx;
margin: 32rpx 0 24rpx;
margin: 28rpx 0 20rpx;
background: rgba(180, 139, 255, 0.22);
}
.profile-divider.small {
margin: 24rpx 0;
margin: 20rpx 0;
}
.meta-grid {
@@ -380,23 +408,28 @@ const editProfile = () => {
min-width: 0;
display: flex;
align-items: center;
gap: 14rpx;
padding-right: 18rpx;
gap: 10rpx;
padding-right: 12rpx;
border-right: 1rpx solid rgba(180, 139, 255, 0.22);
}
.meta-item + .meta-item {
padding-left: 22rpx;
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: 42rpx;
font-size: 34rpx;
line-height: 1;
text-shadow: 0 0 24rpx rgba(168, 85, 255, 0.7);
}
@@ -404,35 +437,35 @@ const editProfile = () => {
.person-icon,
.job-icon {
position: relative;
width: 38rpx;
height: 38rpx;
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
}
.person-icon::before {
content: '';
position: absolute;
left: 10rpx;
left: 8rpx;
top: 0;
width: 16rpx;
height: 16rpx;
border: 4rpx solid #a855ff;
width: 14rpx;
height: 14rpx;
border: 3rpx solid #a855ff;
border-radius: 50%;
}
.person-icon::after {
content: '';
position: absolute;
left: 2rpx;
left: 1rpx;
bottom: 0;
width: 30rpx;
height: 18rpx;
border: 4rpx solid #a855ff;
width: 28rpx;
height: 16rpx;
border: 3rpx solid #a855ff;
border-radius: 18rpx 18rpx 4rpx 4rpx;
}
.job-icon {
border: 5rpx solid #a855ff;
border: 4rpx solid #a855ff;
border-radius: 8rpx;
box-sizing: border-box;
}
@@ -440,11 +473,11 @@ const editProfile = () => {
.job-icon::before {
content: '';
position: absolute;
left: 10rpx;
top: -10rpx;
left: 8rpx;
top: -8rpx;
width: 12rpx;
height: 8rpx;
border: 4rpx solid #a855ff;
height: 7rpx;
border: 3rpx solid #a855ff;
border-bottom: 0;
border-radius: 8rpx 8rpx 0 0;
}
@@ -457,14 +490,14 @@ const editProfile = () => {
.meta-label {
color: rgba(219, 204, 247, 0.54);
font-size: 22rpx;
font-size: 20rpx;
}
.meta-value,
.hobby-text {
margin-top: 4rpx;
margin-top: 3rpx;
color: #fff;
font-size: 27rpx;
font-size: 23rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
@@ -474,7 +507,7 @@ const editProfile = () => {
.hobby-row {
display: flex;
align-items: center;
gap: 28rpx;
gap: 22rpx;
}
.section-head {
@@ -486,12 +519,12 @@ const editProfile = () => {
.title-line {
display: flex;
align-items: center;
gap: 14rpx;
gap: 12rpx;
}
.section-title {
color: #fff;
font-size: 44rpx;
font-size: 40rpx;
line-height: 1.1;
font-weight: 800;
}
@@ -502,26 +535,26 @@ const editProfile = () => {
.section-subtitle {
display: block;
margin-top: 14rpx;
margin-top: 10rpx;
color: rgba(210, 194, 242, 0.68);
font-size: 26rpx;
}
.map-btn {
height: 64rpx;
padding: 0 24rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 12rpx;
color: #caa9ff;
font-size: 24rpx;
}
.map-btn {
height: 56rpx;
padding: 0 20rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
gap: 10rpx;
color: #caa9ff;
font-size: 22rpx;
}
.map-icon {
width: 28rpx;
height: 22rpx;
border: 4rpx solid currentColor;
width: 24rpx;
height: 20rpx;
border: 3rpx solid currentColor;
border-radius: 4rpx;
transform: skewY(-12deg);
}
@@ -533,21 +566,21 @@ const editProfile = () => {
.filter-row {
display: inline-flex;
gap: 18rpx;
gap: 16rpx;
align-items: center;
}
.filter-chip,
.add-filter {
height: 62rpx;
min-width: 104rpx;
padding: 0 30rpx;
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: 25rpx;
font-size: 22rpx;
font-weight: 600;
}
@@ -559,11 +592,11 @@ const editProfile = () => {
}
.add-filter {
min-width: 62rpx;
min-width: 56rpx;
padding: 0;
border: 2rpx dashed rgba(155, 112, 255, 0.45);
color: #c49cff;
font-size: 36rpx;
font-size: 32rpx;
}
.timeline {
@@ -573,8 +606,8 @@ const editProfile = () => {
.timeline-item {
display: grid;
grid-template-columns: 88rpx 1fr;
min-height: 188rpx;
grid-template-columns: 76rpx 1fr;
min-height: 170rpx;
}
.rail {
@@ -586,19 +619,19 @@ const editProfile = () => {
.node {
position: relative;
z-index: 2;
width: 52rpx;
height: 52rpx;
width: 46rpx;
height: 46rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid currentColor;
box-shadow: 0 0 28rpx currentColor;
box-shadow: 0 0 24rpx currentColor;
}
.node view {
width: 24rpx;
height: 24rpx;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: currentColor;
}
@@ -610,7 +643,7 @@ const editProfile = () => {
.line {
position: absolute;
top: 56rpx;
top: 50rpx;
bottom: -6rpx;
width: 4rpx;
background: linear-gradient(180deg, currentColor, rgba(255,255,255,0.14));
@@ -618,14 +651,14 @@ const editProfile = () => {
}
.event-card {
min-height: 160rpx;
margin-bottom: 22rpx;
min-height: 146rpx;
margin-bottom: 18rpx;
border-radius: 28rpx;
display: grid;
grid-template-columns: 116rpx 1rpx 1fr 108rpx 24rpx;
grid-template-columns: 96rpx 1rpx 1fr 88rpx 20rpx;
align-items: center;
gap: 22rpx;
padding: 22rpx;
gap: 18rpx;
padding: 20rpx;
}
.date-box {
@@ -635,16 +668,16 @@ const editProfile = () => {
.date-month,
.date-age {
display: block;
font-size: 25rpx;
font-size: 23rpx;
}
.date-age {
margin-top: 8rpx;
margin-top: 6rpx;
}
.event-divider {
width: 1rpx;
height: 92rpx;
height: 82rpx;
background: rgba(180, 139, 255, 0.18);
}
@@ -655,17 +688,17 @@ const editProfile = () => {
.event-title {
display: block;
color: #fff;
font-size: 27rpx;
font-size: 24rpx;
line-height: 1.25;
font-weight: 800;
}
.event-tag {
display: inline-flex;
margin-top: 14rpx;
padding: 6rpx 14rpx;
margin-top: 10rpx;
padding: 5rpx 12rpx;
border-radius: 999rpx;
font-size: 20rpx;
font-size: 19rpx;
}
.tag-0 { color: #8effc7; background: rgba(50, 196, 128, 0.18); }
@@ -675,9 +708,9 @@ const editProfile = () => {
.event-summary {
display: -webkit-box;
margin-top: 12rpx;
margin-top: 9rpx;
color: rgba(226, 215, 246, 0.66);
font-size: 24rpx;
font-size: 22rpx;
line-height: 1.45;
overflow: hidden;
-webkit-line-clamp: 2;
@@ -685,14 +718,14 @@ const editProfile = () => {
}
.thumb {
width: 96rpx;
height: 96rpx;
border-radius: 22rpx;
width: 78rpx;
height: 78rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 44rpx;
font-size: 36rpx;
font-weight: 900;
overflow: hidden;
}
@@ -704,7 +737,7 @@ const editProfile = () => {
.chevron {
color: rgba(223, 213, 245, 0.68);
font-size: 54rpx;
font-size: 46rpx;
}
.empty-card {
@@ -733,20 +766,20 @@ const editProfile = () => {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
gap: 8rpx;
color: #caa6ff;
font-size: 24rpx;
font-size: 22rpx;
}
.plus-core {
width: 92rpx;
height: 92rpx;
width: 78rpx;
height: 78rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 56rpx;
font-size: 46rpx;
background: linear-gradient(135deg, #b348ff, #582cff);
box-shadow: 0 0 38rpx rgba(171, 72, 255, 0.62);
}
+335 -103
View File
@@ -16,7 +16,10 @@
<view class="mode-tabs kos-card">
<view class="mode-tab" :class="{ active: mode === 'inspiration' }" @click="mode = 'inspiration'">
<text class="tab-icon"></text>
<view class="tab-icon sparkles-mini">
<view class="spark main"></view>
<view class="spark dot"></view>
</view>
<view>
<text class="tab-title">灵感模式</text>
<text class="tab-sub">一句话生成爽文</text>
@@ -34,37 +37,37 @@
<view v-if="mode === 'inspiration'" class="prompt-panel kos-card">
<view class="panel-head">
<view class="panel-title-row">
<text class="spark"></text>
<view class="panel-sparkles">
<view class="panel-spark large"></view>
<view class="panel-spark small"></view>
</view>
<text class="panel-title">写下你想要的故事设定或人生想法</text>
</view>
<view class="help-chip" @click="shuffleInspirations">不知道写什么?</view>
<view class="help-chip" @click="shuffleInspirations">
<view class="bulb-icon"></view>
<text>不知道写什么?</text>
</view>
</view>
<textarea
class="prompt-box"
maxlength="500"
v-model="prompt"
placeholder="例如:&#10;“如果我没有分手,现在会怎样?”&#10;“我成为顶级作曲家的人生”&#10;“重生回18岁改变一切”&#10;“从小镇做题家到世界首富”"
:placeholder="promptPlaceholder"
placeholder-class="placeholder"
/>
<view class="prompt-tools">
<view class="tool-pill kos-pill">语音输入</view>
<text class="counter">{{ prompt.length }}/500</text>
<view class="tool-pill kos-pill" @click="shuffleInspirations">随机灵感</view>
</view>
<scroll-view class="style-scroll" scroll-x :show-scrollbar="false">
<view class="style-row">
<text
v-for="item in styleOptions"
:key="item.value"
class="style-chip kos-pill"
:class="{ active: style === item.value }"
@click="style = item.value"
>{{ item.label }}</text>
<view class="tool-pill kos-pill" @click="handleVoiceInput">
<view class="mic-icon"></view>
<text>语音输入</text>
</view>
</scroll-view>
<text class="counter">{{ prompt.length }}/500</text>
<view class="tool-pill kos-pill" @click="shuffleInspirations">
<view class="refresh-icon"></view>
<text>随机灵感</text>
</view>
</view>
<button class="generate-btn kos-primary" :disabled="generating || !prompt.trim()" :loading="generating" @click="generateByPrompt">
<view>
@@ -72,7 +75,11 @@
<text class="generate-sub">今日剩余生成次数{{ remainingCount }}</text>
</view>
<view class="planet-badge">
<view></view>
<view class="planet-face">
<view class="planet-eye left"></view>
<view class="planet-eye right"></view>
<view class="planet-mouth"></view>
</view>
</view>
</button>
</view>
@@ -171,6 +178,13 @@ const generating = ref(false)
const remainingCount = ref(3)
const style = ref('career')
const randomRecommendations = ref([])
const promptPlaceholder = [
'例如:',
'“如果我没有分手,现在会怎样?”',
'“我成为顶级作曲家的人生”',
'“重生回18岁改变一切”',
'“从小镇做题家到世界首富”'
].join('\n')
const custom = reactive({
theme: '',
style: 'career',
@@ -249,6 +263,7 @@ const generateByPrompt = async () => {
}
prompt.value = ''
if (typeof res.data?.remainingCount === 'number') remainingCount.value = res.data.remainingCount
mode.value = 'list'
uni.showToast({ title: '剧本已生成', icon: 'success' })
}
@@ -286,14 +301,28 @@ const selectScript = async (id) => {
}
uni.navigateTo({ url: '/pages/main/PathView' })
}
const handleVoiceInput = () => {
uni.showModal({
title: '语音输入',
content: '语音识别入口已保留。后续接入微信录音和AI语音转文字后,会把识别结果自动填入灵感输入框。',
confirmText: '填入示例',
cancelText: '知道了',
success: (res) => {
if (res.confirm && !prompt.value.trim()) {
prompt.value = '如果我能重新选择一次,我想把人生过成更勇敢的版本'
}
}
})
}
</script>
<style scoped>
.script-view {
display: flex;
flex-direction: column;
gap: 28rpx;
padding-bottom: 32rpx;
gap: 24rpx;
padding-bottom: 24rpx;
}
.page-head,
@@ -313,7 +342,7 @@ const selectScript = async (id) => {
.title {
color: #fff;
font-size: 46rpx;
font-size: 40rpx;
line-height: 1.08;
font-weight: 900;
}
@@ -326,23 +355,24 @@ const selectScript = async (id) => {
.subtitle {
display: block;
margin-top: 16rpx;
margin-top: 14rpx;
color: rgba(219, 203, 247, 0.74);
font-size: 27rpx;
font-size: 24rpx;
}
.script-list-btn,
.help-chip,
.tool-pill,
.rewrite-btn {
height: 60rpx;
padding: 0 22rpx;
height: 56rpx;
padding: 0 20rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: #caa0ff;
font-size: 24rpx;
font-size: 22rpx;
gap: 9rpx;
}
.list-icon {
@@ -354,7 +384,7 @@ const selectScript = async (id) => {
}
.mode-tabs {
height: 94rpx;
height: 88rpx;
border-radius: 28rpx;
padding: 6rpx;
display: grid;
@@ -366,27 +396,209 @@ const selectScript = async (id) => {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
gap: 14rpx;
color: rgba(216, 207, 238, 0.62);
}
.mode-tab.active {
color: #fff;
border: 1rpx solid rgba(191, 91, 255, 0.85);
background: rgba(115, 45, 255, 0.2);
box-shadow: 0 0 30rpx rgba(167, 60, 255, 0.5);
background:
radial-gradient(circle at 18% 12%, rgba(208, 118, 255, 0.22), transparent 42%),
rgba(115, 45, 255, 0.22);
box-shadow: 0 0 30rpx rgba(167, 60, 255, 0.48), inset 0 0 22rpx rgba(190, 92, 255, 0.18);
}
.tab-icon {
position: relative;
width: 34rpx;
height: 34rpx;
color: #d875ff;
font-size: 34rpx;
flex-shrink: 0;
}
.gear-icon {
position: relative;
width: 30rpx;
height: 30rpx;
border: 3rpx solid currentColor;
border-radius: 50%;
box-sizing: border-box;
color: currentColor;
}
.gear-icon::before {
content: '';
position: absolute;
left: 8rpx;
top: 8rpx;
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: currentColor;
}
.spark,
.panel-spark,
.book-sparkle {
position: absolute;
transform: rotate(45deg);
}
.spark::before,
.spark::after,
.panel-spark::before,
.panel-spark::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
border-radius: 999rpx;
background: currentColor;
transform: translate(-50%, -50%);
}
.spark::before,
.panel-spark::before {
width: 4rpx;
height: 100%;
}
.spark::after,
.panel-spark::after {
width: 100%;
height: 4rpx;
}
.spark.main {
left: 8rpx;
top: 7rpx;
width: 19rpx;
height: 19rpx;
}
.spark.dot {
right: 3rpx;
top: 2rpx;
width: 10rpx;
height: 10rpx;
opacity: 0.8;
}
.panel-sparkles {
position: relative;
width: 38rpx;
height: 38rpx;
flex-shrink: 0;
color: #cf78ff;
}
.panel-spark.large {
left: 9rpx;
top: 8rpx;
width: 24rpx;
height: 24rpx;
}
.panel-spark.small {
left: 1rpx;
bottom: 3rpx;
width: 12rpx;
height: 12rpx;
opacity: 0.78;
}
.bulb-icon {
position: relative;
width: 26rpx;
height: 30rpx;
flex-shrink: 0;
}
.bulb-icon::before {
content: '';
position: absolute;
left: 5rpx;
top: 2rpx;
width: 16rpx;
height: 18rpx;
border: 3rpx solid currentColor;
border-radius: 50% 50% 45% 45%;
box-sizing: border-box;
}
.bulb-icon::after {
content: '';
position: absolute;
left: 9rpx;
bottom: 2rpx;
width: 10rpx;
height: 8rpx;
border-top: 3rpx solid currentColor;
border-bottom: 3rpx solid currentColor;
box-sizing: border-box;
}
.mic-icon {
position: relative;
width: 24rpx;
height: 30rpx;
flex-shrink: 0;
}
.mic-icon::before {
content: '';
position: absolute;
left: 7rpx;
top: 1rpx;
width: 10rpx;
height: 19rpx;
border: 3rpx solid currentColor;
border-radius: 999rpx;
box-sizing: border-box;
}
.mic-icon::after {
content: '';
position: absolute;
left: 4rpx;
bottom: 1rpx;
width: 16rpx;
height: 11rpx;
border: 3rpx solid currentColor;
border-top: 0;
border-radius: 0 0 999rpx 999rpx;
box-sizing: border-box;
}
.refresh-icon {
position: relative;
width: 28rpx;
height: 28rpx;
border: 4rpx solid currentColor;
flex-shrink: 0;
}
.refresh-icon::before {
content: '';
position: absolute;
inset: 5rpx;
border: 3rpx solid currentColor;
border-left-color: transparent;
border-radius: 50%;
transform: rotate(-28deg);
}
.refresh-icon::after {
content: '';
position: absolute;
right: 3rpx;
top: 3rpx;
width: 0;
height: 0;
border-left: 8rpx solid currentColor;
border-top: 5rpx solid transparent;
border-bottom: 5rpx solid transparent;
transform: rotate(25deg);
}
.tab-title,
@@ -395,45 +607,51 @@ const selectScript = async (id) => {
}
.tab-title {
font-size: 27rpx;
font-size: 24rpx;
font-weight: 800;
}
.tab-sub {
margin-top: 4rpx;
font-size: 20rpx;
font-size: 19rpx;
color: rgba(224, 214, 243, 0.52);
}
.prompt-panel,
.custom-panel {
border-radius: 32rpx;
padding: 30rpx;
padding: 28rpx;
border-color: rgba(173, 84, 255, 0.36);
background:
radial-gradient(circle at 16% 0%, rgba(143, 64, 255, 0.18), transparent 42%),
rgba(12, 12, 42, 0.62);
box-shadow: inset 0 0 34rpx rgba(132, 56, 255, 0.14), 0 0 26rpx rgba(140, 55, 255, 0.12);
}
.panel-title-row {
display: flex;
align-items: center;
gap: 14rpx;
gap: 12rpx;
min-width: 0;
}
.panel-title {
color: #c783ff;
font-size: 29rpx;
font-size: 26rpx;
font-weight: 900;
}
.prompt-box {
width: 100%;
height: 216rpx;
height: 214rpx;
box-sizing: border-box;
margin-top: 28rpx;
padding: 30rpx;
margin-top: 26rpx;
padding: 26rpx 28rpx;
border-radius: 24rpx;
border: 1rpx solid rgba(151, 111, 255, 0.26);
background: rgba(13, 15, 43, 0.72);
background: rgba(12, 14, 42, 0.86);
color: #f7f1ff;
font-size: 27rpx;
font-size: 24rpx;
line-height: 1.55;
}
@@ -442,50 +660,28 @@ const selectScript = async (id) => {
grid-template-columns: 1fr auto 1fr;
gap: 16rpx;
align-items: center;
margin-top: 18rpx;
margin-top: 16rpx;
}
.counter {
color: rgba(222, 211, 240, 0.62);
font-size: 23rpx;
}
.style-scroll {
width: 100%;
margin-top: 20rpx;
white-space: nowrap;
}
.style-row {
display: inline-flex;
gap: 14rpx;
}
.style-chip {
display: inline-flex;
height: 52rpx;
padding: 0 20rpx;
border-radius: 999rpx;
align-items: center;
color: rgba(224, 214, 243, 0.66);
font-size: 22rpx;
}
.style-chip.active {
color: #fff;
background: rgba(152, 62, 255, 0.34);
border-color: rgba(191, 91, 255, 0.82);
}
.generate-btn {
width: 100%;
height: 104rpx;
margin-top: 28rpx;
margin-top: 24rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 28rpx;
overflow: visible;
background:
radial-gradient(circle at 92% 44%, rgba(244, 187, 255, 0.42), transparent 22%),
linear-gradient(135deg, #b64cff 0%, #762fff 48%, #4a20ff 100%);
box-shadow: 0 0 34rpx rgba(168, 85, 247, 0.58), inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
}
.generate-title,
@@ -495,38 +691,72 @@ const selectScript = async (id) => {
}
.generate-title {
font-size: 30rpx;
font-size: 28rpx;
font-weight: 900;
}
.generate-sub {
margin-top: 8rpx;
font-size: 22rpx;
font-size: 21rpx;
color: rgba(255, 255, 255, 0.74);
}
.planet-badge {
position: relative;
width: 68rpx;
height: 68rpx;
width: 78rpx;
height: 78rpx;
border-radius: 50%;
background: radial-gradient(circle, #e8b7ff, #8b37ff 62%, #4c1d95);
box-shadow: 0 0 24rpx rgba(219, 143, 255, 0.7);
background: radial-gradient(circle at 36% 28%, #f0c7ff, #a64cff 62%, #5a22d6);
box-shadow: 0 0 30rpx rgba(219, 143, 255, 0.82);
flex-shrink: 0;
}
.planet-badge::after {
content: '';
position: absolute;
left: -12rpx;
top: 27rpx;
width: 90rpx;
height: 20rpx;
left: -20rpx;
top: 34rpx;
width: 124rpx;
height: 24rpx;
border: 4rpx solid rgba(231, 201, 255, 0.72);
border-top-color: transparent;
border-radius: 50%;
transform: rotate(-18deg);
}
.planet-face {
position: absolute;
inset: 0;
z-index: 1;
}
.planet-eye {
position: absolute;
top: 31rpx;
width: 7rpx;
height: 12rpx;
border-radius: 999rpx;
background: #16062f;
}
.planet-eye.left {
left: 27rpx;
}
.planet-eye.right {
right: 27rpx;
}
.planet-mouth {
position: absolute;
left: 34rpx;
bottom: 23rpx;
width: 14rpx;
height: 9rpx;
border-bottom: 3rpx solid #16062f;
border-radius: 0 0 999rpx 999rpx;
}
.field {
margin-top: 24rpx;
}
@@ -577,43 +807,44 @@ const selectScript = async (id) => {
.section-title {
color: #c684ff;
font-size: 31rpx;
font-size: 28rpx;
font-weight: 900;
}
.refresh {
color: #c99fff;
font-size: 24rpx;
font-size: 22rpx;
}
.recommend-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
gap: 16rpx;
margin-top: 18rpx;
}
.recommend-card {
min-height: 118rpx;
min-height: 110rpx;
border-radius: 22rpx;
padding: 24rpx;
padding: 22rpx;
background: rgba(12, 12, 42, 0.54);
}
.recommend-text {
display: block;
color: rgba(248, 244, 255, 0.9);
font-size: 25rpx;
font-size: 22rpx;
line-height: 1.5;
}
.recommend-tag {
display: inline-flex;
margin-top: 18rpx;
padding: 8rpx 16rpx;
margin-top: 16rpx;
padding: 7rpx 14rpx;
border-radius: 999rpx;
color: #d985ff;
background: rgba(151, 66, 255, 0.18);
font-size: 21rpx;
font-size: 20rpx;
}
.recent-section,
@@ -625,8 +856,9 @@ const selectScript = async (id) => {
.script-card {
border-radius: 24rpx;
padding: 22rpx;
gap: 20rpx;
padding: 20rpx;
gap: 18rpx;
background: rgba(12, 12, 42, 0.6);
}
.script-card.selected {
@@ -634,15 +866,15 @@ const selectScript = async (id) => {
}
.script-cover {
width: 92rpx;
height: 92rpx;
width: 86rpx;
height: 86rpx;
flex-shrink: 0;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 42rpx;
font-size: 38rpx;
font-weight: 900;
background: linear-gradient(135deg, #7d35ff, #1bb7ff);
}
@@ -660,20 +892,20 @@ const selectScript = async (id) => {
.script-title {
color: #fff;
font-size: 27rpx;
font-size: 24rpx;
font-weight: 900;
}
.script-date {
margin-top: 8rpx;
margin-top: 7rpx;
color: rgba(218, 204, 243, 0.62);
font-size: 22rpx;
font-size: 21rpx;
}
.script-summary {
margin-top: 8rpx;
margin-top: 7rpx;
color: rgba(218, 204, 243, 0.68);
font-size: 23rpx;
font-size: 22rpx;
line-height: 1.45;
overflow: hidden;
text-overflow: ellipsis;
@@ -682,7 +914,7 @@ const selectScript = async (id) => {
.empty-panel {
border-radius: 24rpx;
padding: 28rpx;
padding: 26rpx;
color: rgba(230, 218, 250, 0.66);
}
</style>
+148 -72
View File
@@ -17,24 +17,27 @@
<MusicPlayer ref="musicPlayer" />
<view class="bottom-nav">
<view class="nav-inner" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view class="nav-inner">
<view class="nav-item" :class="{ active: activeTab === 'record' }" @click="switchTab('record')">
<view class="planet-icon">
<view class="tab-icon planet-ring-icon">
<view class="planet-core"></view>
<view class="planet-ring"></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 class="tab-icon book-star-icon">
<view class="book-page left"></view>
<view class="book-page right"></view>
<view class="book-sparkle"></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 class="tab-icon smile-face-icon">
<view class="smile-eye left"></view>
<view class="smile-eye right"></view>
<view class="smile-mouth"></view>
</view>
<text>我的</text>
</view>
@@ -156,7 +159,7 @@ onUnmounted(() => {
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 28rpx 156rpx;
padding: 0 28rpx 132rpx;
}
.bottom-nav {
@@ -171,16 +174,21 @@ onUnmounted(() => {
}
.nav-inner {
height: 108rpx;
height: 104rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
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);
border: 1rpx solid rgba(150, 95, 255, 0.26);
background:
radial-gradient(circle at 18% 14%, rgba(137, 78, 255, 0.18), transparent 36%),
linear-gradient(180deg, rgba(20, 13, 52, 0.9), rgba(12, 7, 34, 0.94));
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.08),
inset 0 0 34rpx rgba(130, 72, 255, 0.12),
0 14rpx 42rpx rgba(0, 0, 0, 0.36);
backdrop-filter: blur(28rpx);
-webkit-backdrop-filter: blur(28rpx);
}
.nav-item {
@@ -189,93 +197,161 @@ onUnmounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(216, 208, 235, 0.58);
font-size: 23rpx;
font-weight: 600;
transition: transform 0.22s ease, color 0.22s ease;
gap: 5rpx;
color: rgba(174, 165, 199, 0.72);
font-size: 22rpx;
font-weight: 800;
letter-spacing: 0;
transition: transform 0.32s cubic-bezier(0.23, 1, 0.32, 1), color 0.32s ease, text-shadow 0.32s ease;
}
.nav-item.active {
color: #b86cff;
transform: translateY(-4rpx);
text-shadow: 0 0 22rpx rgba(178, 91, 255, 0.8);
color: #a855f7;
transform: translateY(-6rpx);
text-shadow: 0 0 24rpx rgba(168, 85, 247, 0.86);
}
.planet-icon,
.book-icon,
.smile-icon {
.tab-icon {
position: relative;
width: 46rpx;
height: 42rpx;
width: 48rpx;
height: 48rpx;
color: currentColor;
}
.planet-core {
position: absolute;
left: 10rpx;
top: 8rpx;
width: 26rpx;
height: 26rpx;
top: 11rpx;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 24rpx currentColor;
box-shadow: 0 0 26rpx rgba(168, 85, 247, 0.74);
}
.planet-icon::before {
.planet-core::after {
content: '';
position: absolute;
left: 2rpx;
top: 14rpx;
width: 42rpx;
height: 14rpx;
left: 9rpx;
top: 7rpx;
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.58);
}
.planet-ring {
position: absolute;
left: 1rpx;
top: 16rpx;
width: 48rpx;
height: 16rpx;
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;
transform: rotate(-22deg);
opacity: 0.96;
}
.eye {
.book-page {
position: absolute;
top: 12rpx;
width: 6rpx;
height: 6rpx;
border-radius: 50%;
background: currentColor;
top: 13rpx;
width: 20rpx;
height: 26rpx;
border: 4rpx solid currentColor;
box-sizing: border-box;
opacity: 0.95;
}
.eye.left { left: 12rpx; }
.eye.right { right: 12rpx; }
.book-page.left {
left: 5rpx;
border-radius: 8rpx 3rpx 3rpx 8rpx;
transform: skewY(5deg);
}
.smile-icon::after {
.book-page.right {
right: 5rpx;
border-radius: 3rpx 8rpx 8rpx 3rpx;
transform: skewY(-5deg);
}
.book-star-icon::after {
content: '';
position: absolute;
left: 12rpx;
right: 12rpx;
bottom: 10rpx;
left: 23rpx;
top: 15rpx;
width: 3rpx;
height: 23rpx;
border-radius: 999rpx;
background: currentColor;
opacity: 0.55;
}
.book-sparkle {
position: absolute;
right: 4rpx;
top: 4rpx;
width: 14rpx;
height: 14rpx;
transform: rotate(45deg);
}
.book-sparkle::before,
.book-sparkle::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
border-radius: 999rpx;
background: currentColor;
transform: translate(-50%, -50%);
}
.book-sparkle::before {
width: 4rpx;
height: 100%;
}
.book-sparkle::after {
width: 100%;
height: 4rpx;
}
.smile-face-icon {
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 18rpx rgba(174, 165, 199, 0.22);
}
.nav-item.active .smile-face-icon {
box-shadow: 0 0 24rpx rgba(168, 85, 247, 0.62);
}
.smile-eye {
position: absolute;
top: 16rpx;
width: 7rpx;
height: 7rpx;
border-radius: 50%;
background: #100719;
}
.smile-eye.left {
left: 13rpx;
}
.smile-eye.right {
right: 13rpx;
}
.smile-mouth {
position: absolute;
left: 16rpx;
right: 16rpx;
bottom: 14rpx;
height: 8rpx;
border-bottom: 4rpx solid currentColor;
border-radius: 0 0 20rpx 20rpx;
border-bottom: 4rpx solid #100719;
border-radius: 0 0 18rpx 18rpx;
}
</style>
+568 -206
View File
@@ -5,92 +5,133 @@
<view class="topbar">
<text class="back" @click="goBack"></text>
<text class="title">{{ isEdit ? '编辑资料' : '初始化档案' }} </text>
<text class="title">编辑资料 <text class="gold"></text></text>
<text class="save" @click="saveProfile">保存</text>
</view>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
<view class="hero-card kos-card">
<view class="avatar">{{ avatarText }}</view>
<view class="hero-card glass-card">
<view class="avatar-wrap">
<image class="avatar-img" :src="avatarUrl" mode="aspectFill" />
<view class="avatar-edit" @click="chooseAvatar">
<view class="pen-icon"></view>
</view>
</view>
<view class="hero-info">
<text class="hero-name">{{ form.nickname || 'Zoey' }} </text>
<view class="hero-name-row">
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
<text class="gold hero-star"></text>
</view>
<text class="hero-sub">正在成为更清晰的自己</text>
<text class="quote">生活是自己的选择也是</text>
<view class="hero-line"></view>
<text class="hero-quote">生活是自己的选择也是</text>
</view>
<view class="hero-planet">
<view class="planet-core"></view>
<view class="planet-ring"></view>
</view>
</view>
<view class="panel kos-card">
<view class="panel-title">基础信息</view>
<view class="line-field">
<text class="field-label">昵称</text>
<input class="line-input" v-model="form.nickname" placeholder="输入你的昵称" placeholder-class="placeholder" />
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon basic-icon"></view>
<text class="section-title">基础信息</text>
</view>
<view class="line-field">
<text class="field-label">性别</text>
<view class="profile-row">
<text class="row-label">昵称</text>
<input class="row-input" v-model="form.nickname" placeholder="输入昵称" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">性别</text>
<view class="gender-row">
<text v-for="item in genderOptions" :key="item" class="choice" :class="{ active: form.gender === item }" @click="form.gender = item">{{ item }}</text>
</view>
</view>
<view class="line-field">
<text class="field-label">生日</text>
<picker mode="date" :value="birthday" @change="onBirthday">
<view class="line-value">{{ birthday || '选择生日' }} </view>
<view class="profile-row">
<text class="row-label">生日</text>
<picker class="row-picker" mode="date" :value="birthday" @change="onBirthday">
<view class="row-value">{{ birthdayDisplay }}</view>
</picker>
<text class="chevron"></text>
</view>
<view class="line-field">
<text class="field-label">所在城市</text>
<input class="line-input" v-model="form.city" placeholder="填写城市" placeholder-class="placeholder" />
<view class="profile-row">
<text class="row-label">所在城市</text>
<input class="row-input" v-model="form.city" placeholder="填写城市" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
</view>
<view class="dual-panel kos-card">
<view class="select-block">
<view class="block-title">星座</view>
<view class="zodiac-grid">
<view
v-for="item in zodiacOptions"
:key="item.name"
class="zodiac-item"
:class="{ active: form.zodiac === item.name }"
@click="form.zodiac = item.name"
>
<text class="zodiac-symbol">{{ item.symbol }}</text>
<text>{{ item.name }}</text>
<view class="astro-panel">
<view class="astro-col">
<view class="astro-title-row">
<view class="zodiac-head-icon"></view>
<text class="astro-title">星座</text>
<text class="astro-current">{{ form.zodiac || '巨蟹座' }}</text>
<text class="chevron"></text>
</view>
<text class="select-title">选择星座</text>
<view class="zodiac-grid">
<view
v-for="item in zodiacOptions"
:key="item.name"
class="zodiac-item"
:class="{ active: form.zodiac === item.name }"
@click="form.zodiac = item.name"
>
<view class="zodiac-bubble">{{ item.symbol }}</view>
<text>{{ item.name }}</text>
</view>
</view>
</view>
<view class="astro-col mbti-col">
<view class="astro-title-row">
<view class="mbti-head-icon"></view>
<text class="astro-title">MBTI</text>
<text class="astro-current">{{ form.mbti || 'ENTJ' }}</text>
<text class="chevron"></text>
</view>
<text class="select-title">选择MBTI</text>
<view class="mbti-grid">
<text
v-for="item in mbtiOptions"
:key="item"
class="mbti-chip"
:class="{ active: form.mbti === item }"
@click="form.mbti = item"
>{{ item }}</text>
</view>
</view>
</view>
<view class="select-block mbti-block">
<view class="block-title">MBTI</view>
<view class="mbti-grid">
<text
v-for="item in mbtiOptions"
:key="item"
class="mbti-chip"
:class="{ active: form.mbti === item }"
@click="form.mbti = item"
>{{ item }}</text>
</view>
</view>
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon job-title-icon"></view>
<text class="section-title">职业信息</text>
</view>
<view class="profile-row">
<text class="row-label">职业</text>
<input class="row-input" v-model="form.profession" placeholder="产品经理" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">行业</text>
<input class="row-input" v-model="form.industry" placeholder="互联网" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">公司可选</text>
<input class="row-input muted" v-model="form.company" placeholder="填写公司名称" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
</view>
<view class="panel kos-card">
<view class="panel-title">职业信息</view>
<view class="line-field">
<text class="field-label">职业</text>
<input class="line-input" v-model="form.profession" placeholder="产品经理" placeholder-class="placeholder" />
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon smile-title-icon"></view>
<text class="section-title">性格标签</text>
<text class="section-hint">最多选择5个</text>
</view>
<view class="line-field">
<text class="field-label">行业</text>
<input class="line-input" v-model="form.industry" placeholder="互联网" placeholder-class="placeholder" />
</view>
<view class="line-field">
<text class="field-label">公司可选</text>
<input class="line-input" v-model="form.company" placeholder="填写公司名称" placeholder-class="placeholder" />
</view>
</view>
<view class="panel kos-card">
<view class="panel-title">性格标签最多选择5个</view>
<view class="tag-grid">
<text
v-for="tag in personalityTags"
@@ -103,10 +144,14 @@
</view>
</view>
<view class="panel kos-card">
<view class="panel-head">
<text class="panel-title">兴趣爱好最多选择5个</text>
<text class="custom"> 自定义兴趣</text>
<view class="panel glass-card">
<view class="section-head with-action">
<view class="section-title-wrap">
<view class="section-icon heart-title-icon"></view>
<text class="section-title">兴趣爱好</text>
<text class="section-hint">最多选择5个</text>
</view>
<text class="custom" @click="addCustomHobby"> 自定义兴趣</text>
</view>
<view class="tag-grid">
<text
@@ -119,18 +164,21 @@
</view>
</view>
<view class="panel kos-card">
<view class="panel-title">个人简介</view>
<textarea class="bio" v-model="form.future.ideal" maxlength="200" placeholder="热爱阅读和旅行,喜欢用文字和镜头记录生活。相信真诚和努力能让世界变得更美好。" placeholder-class="placeholder" />
<view class="panel glass-card bio-panel">
<view class="section-head">
<view class="section-icon bio-title-icon"></view>
<text class="section-title">个人简介</text>
</view>
<textarea
class="bio"
v-model="form.future.ideal"
maxlength="200"
:placeholder="bioPlaceholder"
placeholder-class="placeholder"
/>
<text class="bio-count">{{ (form.future.ideal || '').length }}/200</text>
</view>
</scroll-view>
<view class="bottom-bar">
<button class="submit kos-primary" :loading="saving" :disabled="saving || !form.nickname.trim()" @click="saveProfile">
{{ saving ? '正在保存' : '保存生命档案' }}
</button>
</view>
</view>
</template>
@@ -143,12 +191,23 @@ const statusBarHeight = ref(20)
const isEdit = ref(false)
const saving = ref(false)
const birthday = ref('')
const avatarLocal = ref('')
const bioPlaceholder = '热爱阅读和旅行,喜欢用文字和镜头记录生活。\n相信真诚和努力能让世界变得更美好。'
const genderOptions = ['女', '男', '不透露']
const zodiacOptions = [
{ name: '白羊座', symbol: '♈' }, { name: '金牛座', symbol: '♉' }, { name: '双子座', symbol: '♊' }, { name: '巨蟹座', symbol: '♋' },
{ name: '狮子座', symbol: '♌' }, { name: '处女座', symbol: '♍' }, { name: '天秤座', symbol: '♎' }, { name: '天蝎座', symbol: '' },
{ name: '射手座', symbol: '' }, { name: '摩羯座', symbol: '♑' }, { name: '水瓶座', symbol: '♒' }, { name: '双鱼座', symbol: '♓' }
{ name: '白羊座', symbol: '♈' },
{ name: '金牛座', symbol: '' },
{ name: '双子座', symbol: '' },
{ name: '巨蟹座', symbol: '♋' },
{ name: '狮子座', symbol: '♌' },
{ name: '处女座', symbol: '♍' },
{ name: '天秤座', symbol: '♎' },
{ name: '天蝎座', symbol: '♏' },
{ name: '射手座', symbol: '♐' },
{ name: '摩羯座', symbol: '♑' },
{ name: '水瓶座', symbol: '♒' },
{ name: '双鱼座', symbol: '♓' }
]
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
const personalityTags = ['理性', '感性', '乐观', '独立', '有创造力', '坚韧', '细腻', '好奇', '内敛', '冒险', '自由']
@@ -171,27 +230,37 @@ const form = reactive({
future: { vision: '', ideal: '' }
})
const avatarText = computed(() => (form.nickname || 'Z').slice(0, 1))
const avatarUrl = computed(() => {
if (avatarLocal.value) return avatarLocal.value
const nickname = form.nickname || 'Zoey'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
})
const birthdayDisplay = computed(() => {
if (!birthday.value) return '1998年06月18日'
const [year, month, day] = birthday.value.split('-')
return `${year}${month}${day}`
})
const syncFromStore = () => {
const source = store.userProfile || store.registrationData || {}
Object.assign(form, {
nickname: source.nickname || '',
gender: source.gender || '',
zodiac: source.zodiac || '',
mbti: source.mbti || '',
profession: source.profession || '',
city: source.city || '',
industry: source.industry || '',
gender: source.gender || '',
zodiac: source.zodiac || '巨蟹座',
mbti: source.mbti || 'ENTJ',
profession: source.profession || '产品经理',
city: source.city || '上海市',
industry: source.industry || '互联网',
company: source.company || '',
personalityTags: Array.isArray(source.personalityTags) ? [...source.personalityTags] : [],
hobbies: Array.isArray(source.hobbies) ? [...source.hobbies] : [],
personalityTags: Array.isArray(source.personalityTags) && source.personalityTags.length ? [...source.personalityTags] : ['理性', '乐观', '独立', '有创造力', '坚韧'],
hobbies: Array.isArray(source.hobbies) && source.hobbies.length ? [...source.hobbies] : ['阅读', '旅行', '音乐'],
childhood: { date: source.childhood?.date || '', text: source.childhood?.text || '' },
joy: { date: source.joy?.date || '', text: source.joy?.text || '' },
low: { date: source.low?.date || '', text: source.low?.text || '' },
future: { vision: source.future?.vision || '', ideal: source.future?.ideal || '' }
future: { vision: source.future?.vision || '', ideal: source.future?.ideal || '热爱阅读和旅行,喜欢用文字和镜头记录生活。\n相信真诚和努力能让世界变得更美好。' }
})
birthday.value = source.birthday || ''
birthday.value = source.birthday || '1998-06-18'
}
const onBirthday = (event) => {
@@ -211,6 +280,32 @@ const toggleList = (list, tag, max) => {
list.push(tag)
}
const chooseAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
avatarLocal.value = res.tempFilePaths?.[0] || ''
uni.showToast({ title: '头像已更新到本地预览', icon: 'none' })
}
})
}
const addCustomHobby = () => {
uni.showModal({
title: '自定义兴趣',
editable: true,
placeholderText: '输入兴趣名称',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm || !value) return
if (!hobbyOptions.includes(value)) hobbyOptions.push(value)
toggleList(form.hobbies, value, 5)
}
})
}
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
saving.value = true
@@ -259,48 +354,52 @@ onMounted(() => {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 86% 10%, rgba(124, 58, 237, 0.3), transparent 30%),
radial-gradient(circle at 10% 32%, rgba(48, 112, 255, 0.16), transparent 28%),
linear-gradient(180deg, #07091d 0%, #07031a 52%, #04020e 100%);
radial-gradient(circle at 84% 3%, rgba(87, 122, 255, 0.12), transparent 28%),
radial-gradient(circle at 14% 26%, rgba(130, 71, 255, 0.16), transparent 30%),
linear-gradient(180deg, #05081b 0%, #07031a 52%, #03020d 100%);
}
.status-space,
.topbar,
.scroll,
.bottom-bar {
.scroll {
position: relative;
z-index: 1;
}
.status-space,
.topbar,
.bottom-bar {
.topbar {
flex-shrink: 0;
}
.topbar {
height: 92rpx;
display: grid;
grid-template-columns: 80rpx 1fr 80rpx;
grid-template-columns: 90rpx 1fr 90rpx;
align-items: center;
padding: 0 28rpx;
padding: 0 32rpx;
}
.back {
font-size: 66rpx;
color: #fff;
font-size: 68rpx;
line-height: 1;
}
.title {
text-align: center;
color: #fff;
font-size: 36rpx;
font-weight: 900;
}
.save,
.custom {
color: #c06dff;
font-size: 27rpx;
.gold {
color: #ffd58c;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.save {
color: #b94cff;
font-size: 28rpx;
text-align: right;
}
@@ -312,177 +411,443 @@ onMounted(() => {
padding: 0 28rpx 28rpx;
}
.hero-card,
.panel,
.dual-panel {
border-radius: 28rpx;
margin-bottom: 22rpx;
padding: 28rpx;
.glass-card {
position: relative;
overflow: hidden;
border: 1rpx solid rgba(124, 75, 255, 0.34);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.14), transparent 34%),
rgba(10, 13, 43, 0.74);
box-shadow: inset 0 0 34rpx rgba(123, 60, 255, 0.08), 0 14rpx 48rpx rgba(0, 0, 0, 0.22);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.hero-card {
min-height: 190rpx;
border-radius: 24rpx;
margin-bottom: 22rpx;
padding: 22rpx 28rpx;
display: flex;
align-items: center;
gap: 28rpx;
min-height: 150rpx;
}
.avatar {
width: 132rpx;
height: 132rpx;
.avatar-wrap {
position: relative;
width: 136rpx;
height: 136rpx;
flex-shrink: 0;
padding: 5rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 58rpx;
font-weight: 900;
background: linear-gradient(135deg, #b245ff, #2a7dff);
box-shadow: 0 0 38rpx rgba(168, 85, 255, 0.52);
background: linear-gradient(135deg, #fff, #9b54ff 46%, #4a67ff);
box-shadow: 0 0 34rpx rgba(162, 91, 255, 0.52);
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
background: #27124a;
}
.avatar-edit {
position: absolute;
right: -4rpx;
bottom: -2rpx;
width: 42rpx;
height: 42rpx;
border-radius: 50%;
background: linear-gradient(135deg, #8f4dff, #582cff);
box-shadow: 0 0 18rpx rgba(158, 91, 255, 0.62);
}
.pen-icon {
width: 17rpx;
height: 6rpx;
margin: 18rpx auto;
border-radius: 999rpx;
background: #fff;
transform: rotate(-45deg);
}
.hero-info {
position: relative;
z-index: 2;
flex: 1;
min-width: 0;
}
.hero-name,
.hero-sub,
.quote,
.panel-title,
.block-title {
display: block;
.hero-name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.hero-name {
color: #fff;
font-size: 42rpx;
font-weight: 900;
line-height: 1.1;
}
.hero-star {
font-size: 26rpx;
}
.hero-sub {
margin-top: 8rpx;
color: rgba(239, 232, 255, 0.78);
display: block;
margin-top: 12rpx;
color: rgba(239, 232, 255, 0.84);
font-size: 25rpx;
}
.quote {
.hero-line {
width: 280rpx;
height: 1rpx;
margin-top: 14rpx;
color: #c06dff;
font-size: 24rpx;
background: rgba(180, 139, 255, 0.22);
}
.panel-title,
.block-title {
margin-bottom: 20rpx;
color: #eadcff;
font-size: 28rpx;
font-weight: 900;
.hero-quote {
display: block;
margin-top: 14rpx;
color: #b94cff;
font-size: 25rpx;
font-weight: 700;
}
.panel-head {
.hero-planet {
position: absolute;
right: 34rpx;
top: 28rpx;
width: 210rpx;
height: 150rpx;
opacity: 0.42;
}
.planet-core {
position: absolute;
right: 30rpx;
top: 24rpx;
width: 86rpx;
height: 86rpx;
border-radius: 50%;
background: radial-gradient(circle at 34% 26%, #7d63ff, #4b1da8 62%, #19083b);
box-shadow: 0 0 48rpx rgba(141, 78, 255, 0.58);
}
.planet-ring {
position: absolute;
right: 4rpx;
top: 58rpx;
width: 158rpx;
height: 34rpx;
border: 5rpx solid rgba(161, 92, 255, 0.55);
border-top-color: transparent;
border-radius: 50%;
transform: rotate(-18deg);
}
.panel {
border-radius: 24rpx;
margin-bottom: 18rpx;
padding: 24rpx;
}
.section-head,
.section-title-wrap {
display: flex;
align-items: center;
}
.section-head {
min-height: 44rpx;
gap: 10rpx;
}
.section-head.with-action {
justify-content: space-between;
}
.line-field {
min-height: 76rpx;
display: grid;
grid-template-columns: 150rpx 1fr;
align-items: center;
border-top: 1rpx solid rgba(180, 139, 255, 0.14);
}
.field-label {
color: rgba(221, 207, 246, 0.72);
.section-title {
color: rgba(239, 232, 255, 0.9);
font-size: 25rpx;
font-weight: 800;
}
.line-input,
.line-value {
color: #fff;
font-size: 26rpx;
.section-hint {
color: rgba(222, 211, 240, 0.54);
font-size: 22rpx;
}
.section-icon {
position: relative;
width: 28rpx;
height: 28rpx;
color: #a855ff;
flex-shrink: 0;
}
.basic-icon::before {
content: '♜';
color: currentColor;
font-size: 28rpx;
line-height: 1;
}
.job-title-icon {
border: 4rpx solid currentColor;
border-radius: 6rpx;
box-sizing: border-box;
}
.job-title-icon::before {
content: '';
position: absolute;
left: 7rpx;
top: -8rpx;
width: 10rpx;
height: 7rpx;
border: 3rpx solid currentColor;
border-bottom: 0;
border-radius: 6rpx 6rpx 0 0;
}
.smile-title-icon {
border-radius: 50%;
border: 3rpx solid currentColor;
}
.smile-title-icon::before,
.smile-title-icon::after {
content: '';
position: absolute;
top: 8rpx;
width: 4rpx;
height: 4rpx;
border-radius: 50%;
background: currentColor;
}
.smile-title-icon::before { left: 7rpx; }
.smile-title-icon::after { right: 7rpx; }
.heart-title-icon::before {
content: '♡';
font-size: 30rpx;
line-height: 1;
}
.bio-title-icon {
border: 3rpx solid currentColor;
border-radius: 5rpx;
box-sizing: border-box;
}
.bio-title-icon::before,
.bio-title-icon::after {
content: '';
position: absolute;
left: 6rpx;
right: 6rpx;
height: 3rpx;
background: currentColor;
}
.bio-title-icon::before { top: 8rpx; }
.bio-title-icon::after { top: 15rpx; }
.profile-row {
min-height: 64rpx;
display: grid;
grid-template-columns: 154rpx 1fr 24rpx;
align-items: center;
border-top: 1rpx solid rgba(180, 139, 255, 0.16);
}
.section-head + .profile-row {
margin-top: 8rpx;
}
.row-label {
color: rgba(205, 191, 238, 0.82);
font-size: 24rpx;
}
.row-input,
.row-value {
min-width: 0;
color: rgba(255, 255, 255, 0.9);
font-size: 24rpx;
text-align: left;
}
.row-picker {
min-width: 0;
}
.muted {
color: rgba(217, 205, 238, 0.42);
}
.chevron {
color: rgba(218, 204, 243, 0.7);
font-size: 44rpx;
line-height: 1;
text-align: right;
}
.gender-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
gap: 16rpx;
}
.choice,
.mbti-chip,
.tag-choice {
height: 52rpx;
height: 40rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
border: 1rpx solid rgba(151, 111, 255, 0.28);
background: rgba(255, 255, 255, 0.035);
font-size: 23rpx;
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
font-size: 22rpx;
box-sizing: border-box;
}
.choice.active,
.mbti-chip.active,
.tag-choice.active {
color: #fff;
border-color: rgba(202, 97, 255, 0.9);
background: rgba(149, 55, 255, 0.38);
box-shadow: 0 0 24rpx rgba(168, 67, 255, 0.38);
border-color: rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
.dual-panel {
.astro-panel {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
margin-top: 8rpx;
border-top: 1rpx solid rgba(180, 139, 255, 0.16);
}
.mbti-block {
border-left: 1rpx solid rgba(180, 139, 255, 0.14);
padding-left: 24rpx;
.astro-col {
min-width: 0;
padding: 18rpx 18rpx 0 0;
}
.mbti-col {
border-left: 1rpx solid rgba(180, 139, 255, 0.18);
padding-left: 18rpx;
padding-right: 0;
}
.astro-title-row {
display: grid;
grid-template-columns: 34rpx 74rpx 1fr 18rpx;
align-items: center;
gap: 8rpx;
}
.zodiac-head-icon,
.mbti-head-icon {
width: 28rpx;
height: 28rpx;
color: #a855ff;
}
.zodiac-head-icon {
font-size: 28rpx;
line-height: 1;
}
.mbti-head-icon {
border: 4rpx solid currentColor;
border-radius: 4rpx;
box-sizing: border-box;
}
.astro-title {
color: rgba(222, 211, 240, 0.76);
font-size: 24rpx;
}
.astro-current {
color: rgba(255, 255, 255, 0.9);
font-size: 23rpx;
text-align: right;
}
.select-title {
display: block;
margin-top: 22rpx;
color: rgba(222, 211, 240, 0.72);
font-size: 21rpx;
}
.zodiac-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
gap: 14rpx 8rpx;
margin-top: 14rpx;
}
.zodiac-item {
min-height: 88rpx;
border-radius: 18rpx;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
color: rgba(226, 217, 246, 0.84);
font-size: 19rpx;
}
.zodiac-bubble {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.76);
}
.zodiac-item.active {
color: #fff;
background: rgba(149, 55, 255, 0.26);
box-shadow: inset 0 0 18rpx rgba(168, 67, 255, 0.28);
}
.zodiac-symbol {
color: #a855ff;
font-size: 32rpx;
font-size: 28rpx;
background: rgba(124, 58, 237, 0.28);
border: 1rpx solid rgba(173, 84, 255, 0.36);
}
.zodiac-item.active .zodiac-bubble {
color: #fff;
border-color: rgba(215, 128, 255, 0.95);
background: linear-gradient(135deg, rgba(168, 85, 247, 0.8), rgba(98, 47, 190, 0.66));
box-shadow: 0 0 22rpx rgba(190, 92, 255, 0.72);
}
.mbti-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(4, 1fr);
gap: 12rpx;
margin-top: 14rpx;
}
.mbti-chip {
height: 42rpx;
font-size: 20rpx;
}
.tag-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14rpx;
grid-template-columns: repeat(5, 1fr);
gap: 14rpx 24rpx;
margin-top: 20rpx;
}
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
.tag-choice.dashed {
@@ -490,41 +855,38 @@ onMounted(() => {
color: #c06dff;
}
.custom {
color: #c06dff;
font-size: 23rpx;
}
.bio-panel {
margin-bottom: 0;
}
.bio {
width: 100%;
height: 140rpx;
height: 116rpx;
box-sizing: border-box;
padding: 22rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(151, 111, 255, 0.24);
background: rgba(12, 15, 46, 0.72);
margin-top: 16rpx;
padding: 18rpx 20rpx;
border-radius: 18rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 25rpx;
line-height: 1.55;
font-size: 23rpx;
line-height: 1.5;
}
.bio-count {
display: block;
margin-top: 10rpx;
margin-top: 8rpx;
text-align: right;
color: rgba(224, 214, 243, 0.56);
font-size: 22rpx;
font-size: 20rpx;
}
.bottom-bar {
height: 116rpx;
box-sizing: border-box;
padding: 14rpx 28rpx 22rpx;
background: rgba(5, 6, 21, 0.72);
backdrop-filter: blur(24rpx);
}
.submit {
width: 100%;
height: 80rpx;
border-radius: 999rpx;
color: #fff;
font-size: 28rpx;
font-weight: 900;
.placeholder {
color: rgba(214, 204, 235, 0.42);
}
</style>
+32
View File
@@ -37,11 +37,30 @@ export const deleteEvent = async (id) => {
return response
}
export const assistEventWriting = async (eventData = {}) => {
return post('/lifeEvent/ai-assist', eventData)
}
export const chatAboutEvent = async (eventData = {}) => {
return post('/lifeEvent/chat-placeholder', eventData)
}
export const shareEvent = async (eventData = {}) => {
return post('/lifeEvent/share-placeholder', eventData)
}
export const favoriteEvent = async ({ id, favorite }) => {
return post('/lifeEvent/favorite-placeholder', { id, favorite })
}
const transformToBackendFormat = (frontendData) => {
const {
id,
title,
time,
timeMode = 'date',
eventDateText,
endTime,
content,
aiFeedback,
eventType = 'daily_log',
@@ -54,6 +73,9 @@ const transformToBackendFormat = (frontendData) => {
id,
title,
eventDate: time,
timeMode,
eventDateText: eventDateText || time,
eventEndDate: timeMode === 'range' ? endTime : null,
content,
aiReply: aiFeedback,
eventType,
@@ -71,6 +93,9 @@ export const transformToFrontendFormat = (backendData) => {
userId,
title,
eventDate,
timeMode,
eventDateText,
eventEndDate,
content,
aiReply,
eventType,
@@ -85,6 +110,9 @@ export const transformToFrontendFormat = (backendData) => {
userId,
title: title || '',
time: eventDate ? eventDate.split('T')[0] : '',
timeMode: timeMode || 'date',
eventDateText: eventDateText || (eventDate ? eventDate.split('T')[0] : ''),
endTime: eventEndDate ? eventEndDate.split('T')[0] : '',
content: content || '',
aiFeedback: aiReply || '',
eventType: eventType || 'daily_log',
@@ -107,6 +135,10 @@ export default {
createEvent,
updateEvent,
deleteEvent,
assistEventWriting,
chatAboutEvent,
shareEvent,
favoriteEvent,
transformToFrontendFormat,
transformListToFrontend
}
+9 -2
View File
@@ -49,11 +49,12 @@ const transformToBackendFormat = (frontendData) => {
title,
description,
content,
steps: inputSteps,
status = 'active',
progress = 0
} = frontendData
let steps = []
let steps = Array.isArray(inputSteps) ? inputSteps : []
if (content) {
const stepMatches = content.match(/(\d+)\.\s*([^:]+)[:]\s*([^\n]+)/g)
if (stepMatches) {
@@ -123,7 +124,13 @@ export const transformToFrontendFormat = (backendData) => {
title: title || '实现路径',
description: description || '',
content,
steps: steps || [],
steps: Array.isArray(steps)
? steps.map((step, index) => ({
...step,
task: step.task || step.phase || `阶段${index + 1}`,
desc: step.desc || step.content || step.action || ''
}))
: [],
status: status || 'active',
progress: progress || 0,
createTime
+62
View File
@@ -151,6 +151,62 @@ const createEvent = async (eventData) => {
}
}
const updateEvent = async (eventData) => {
try {
await lifeEventService.updateEvent(eventData)
await fetchEvents()
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
const deleteEvent = async (id) => {
try {
await lifeEventService.deleteEvent(id)
await fetchEvents()
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}
const assistEventWriting = async (eventData) => {
try {
const res = await lifeEventService.assistEventWriting(eventData)
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const chatAboutEvent = async (eventData) => {
try {
const res = await lifeEventService.chatAboutEvent(eventData)
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const shareEvent = async (eventData) => {
try {
const res = await lifeEventService.shareEvent(eventData)
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const favoriteEvent = async ({ id, favorite }) => {
try {
const res = await lifeEventService.favoriteEvent({ id, favorite })
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const getEventById = (id) => {
return state.events.find(event => String(event.id) === String(id)) || null
}
@@ -350,6 +406,12 @@ export const useAppStore = () => {
setCurrentStep,
fetchEvents,
createEvent,
updateEvent,
deleteEvent,
assistEventWriting,
chatAboutEvent,
shareEvent,
favoriteEvent,
getEventById,
fetchScripts,
createScript,