feat: 完善人生轨迹详情页功能及微信小程序登录优化

This commit is contained in:
2026-06-15 10:17:30 +08:00
parent cb3c5e6724
commit 74a2c15b6b
37 changed files with 1974 additions and 516 deletions
+70 -1
View File
@@ -2,7 +2,13 @@ import { computed, onUnmounted, ref } from 'vue'
import { createTtsTask, getTtsTask, getTtsTaskBySource } from '../services/tts.js'
import analytics from '../services/analytics.js'
const readResponseData = (response) => response?.data ?? response ?? null
const readResponseData = (response) => {
if (!response) return null
if (Object.prototype.hasOwnProperty.call(response, 'data')) {
return response.data ?? null
}
return response
}
export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {}) => {
const task = ref(null)
@@ -11,6 +17,8 @@ export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {})
const statusText = ref('')
let audio = null
let timer = null
let prewarmPromise = null
let prewarmSourceId = ''
const buttonText = computed(() => {
if (loading.value) return '正在生成朗读'
@@ -155,6 +163,49 @@ export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {})
}
}
const prewarmSource = async (sourceId) => {
if (!sourceId) return null
if (task.value?.status === 'success' && task.value?.sourceId === sourceId) {
return task.value
}
if (prewarmPromise && prewarmSourceId === sourceId) {
return prewarmPromise
}
prewarmSourceId = sourceId
prewarmPromise = (async () => {
try {
const existingResponse = await getTtsTaskBySource({ sourceType, sourceId })
const existingTask = readResponseData(existingResponse)
if (existingTask?.id) {
setTask(existingTask)
}
if (existingTask?.status === 'success' || existingTask?.status === 'pending' || existingTask?.status === 'processing') {
return existingTask
}
} catch (error) {
// Old scripts may not have a task yet; create one silently below.
}
try {
const createResponse = await createTtsTask({ sourceType, sourceId })
const nextTask = readResponseData(createResponse)
if (nextTask?.id) {
setTask(nextTask)
}
return nextTask
} catch (error) {
track('script_tts_error', sourceId, { error: error?.message || error?.errMsg || 'prewarm failed' })
return null
} finally {
prewarmPromise = null
prewarmSourceId = ''
}
})()
return prewarmPromise
}
const playSource = async (sourceId) => {
if (!sourceId) {
uni.showToast({ title: '生成保存后可播放', icon: 'none' })
@@ -162,6 +213,21 @@ export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {})
}
if (loading.value) return
if (prewarmPromise && prewarmSourceId === sourceId) {
const warmedTask = await prewarmPromise
if (warmedTask?.status === 'success') {
setTask(warmedTask)
play(sourceId)
return
}
if ((warmedTask?.status === 'pending' || warmedTask?.status === 'processing') && warmedTask?.id) {
setTask(warmedTask)
loading.value = true
pollTask(warmedTask.id, sourceId)
return
}
}
if (task.value?.status === 'success') {
play(sourceId)
return
@@ -195,6 +261,8 @@ export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {})
setTask(null)
statusText.value = ''
loading.value = false
prewarmPromise = null
prewarmSourceId = ''
}
onUnmounted(reset)
@@ -205,6 +273,7 @@ export const useTtsPlayer = ({ pagePath = '', sourceType = 'epic_script' } = {})
playing,
statusText,
buttonText,
prewarmSource,
playSource,
reset
}
+50 -173
View File
@@ -45,23 +45,14 @@
<view class="event-body-section">
<view class="event-body-toolbar">
<view class="content-action favorite-action" @click="toggleFavorite">
<view class="bookmark-icon" :class="{ active: isFavorite }"></view>
<text>{{ isFavorite ? '已收藏' : '收藏' }}</text>
<view class="action-btn favorite-btn" @click="toggleFavorite">
<text class="action-icon">{{ isFavorite ? '★' : '☆' }}</text>
<text class="action-label">{{ isFavorite ? '已收藏' : '收藏' }}</text>
</view>
<view class="toolbar-right">
<view class="analysis-pill" @click="scrollToAnalysis">
<text></text>
</view>
<view class="content-action share-action" @click="shareCurrent">
<view class="share-icon">
<view></view>
<view></view>
<view></view>
</view>
<text>分享</text>
</view>
<view class="action-btn share-btn" @click="shareCurrent">
<text class="action-icon"></text>
<text class="action-label"></text>
</view>
</view>
@@ -75,12 +66,9 @@
</view>
<view class="event-body-footer">
<view class="content-action edit-action" @click="editEvent">
<view class="edit-icon"></view>
<text>编辑</text>
</view>
<text class="collapse-action" @click="isDescriptionCollapsed = !isDescriptionCollapsed">
{{ isDescriptionCollapsed ? '展开' : '收起' }} ^
<text class="edit-link" @click="editEvent"> 编辑</text>
<text class="collapse-link" @click="isDescriptionCollapsed = !isDescriptionCollapsed">
{{ isDescriptionCollapsed ? '展开 ' : '收起 ' }}
</text>
</view>
</view>
@@ -238,8 +226,18 @@ const locationText = computed(() => {
return profile.value.city ? `中国 · ${profile.value.city}` : '中国 · 深圳'
})
// 将【】格式标题转换为 Markdown # 标题格式
const convertToMarkdown = (text) => {
if (!text) return text
// 将 "【数字 标题】" 或 "【标题】" 转换为 "# 标题",保留完整内容
return text.replace(/^【\s*(.+?)\s*】$/gm, (match, title) => {
return `# ${title.trim()}`
})
}
const eventDescription = computed(() => {
return displayEvent.value.content || displayEvent.value.description || '经过了长时间的纠结和准备,我终于辞去了稳定的工作,全职投入到自己热爱的AI产品创业中。这是我人生中最大的一次冒险,也是我第一次真正为自己而活。'
const raw = displayEvent.value.content || displayEvent.value.description || '经过了长时间的纠结和准备,我终于辞去了稳定的工作,全职投入到自己热爱的AI产品创业中。这是我人生中最大的一次冒险,也是我第一次真正为自己而活。'
return convertToMarkdown(raw)
})
const relatedTags = computed(() => {
@@ -300,16 +298,6 @@ const onContentScroll = (event) => {
currentScrollTop.value = event.detail?.scrollTop || 0
}
const scrollToAnalysis = () => {
uni.createSelectorQuery()
.select('#analysisPanel')
.boundingClientRect((rect) => {
if (!rect) return
scrollTop.value = Math.max(0, currentScrollTop.value + rect.top - 24)
})
.exec()
}
const editEvent = () => {
if (!eventId.value) return
uni.navigateTo({ url: `/pages/life-event/form?id=${eventId.value}` })
@@ -652,55 +640,37 @@ const goBack = () => {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
gap: 12rpx;
}
.event-body-toolbar {
min-height: 64rpx;
min-height: 48rpx;
}
.toolbar-right {
.action-btn {
display: flex;
align-items: center;
gap: 14rpx;
gap: 8rpx;
color: rgba(238, 231, 255, 0.72);
font-size: 20rpx;
font-weight: 700;
line-height: 1;
padding: 8rpx 0;
}
.content-action {
min-height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
color: rgba(238, 231, 255, 0.88);
font-size: 23rpx;
font-weight: 800;
.action-icon {
font-size: 26rpx;
line-height: 1;
color: rgba(238, 231, 255, 0.82);
}
.favorite-action,
.share-action,
.edit-action {
padding: 0 4rpx;
}
.analysis-pill {
min-width: 92rpx;
height: 54rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 0 18rpx 18rpx 18rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(231, 217, 255, 0.86);
font-size: 23rpx;
font-weight: 900;
background: rgba(157, 104, 255, 0.18);
border: 1rpx solid rgba(193, 154, 255, 0.24);
box-shadow: inset 0 0 18rpx rgba(185, 124, 255, 0.08);
.favorite-btn .action-icon {
color: #ffd36e;
}
.description-title-row {
margin-top: 16rpx;
position: relative;
}
.section-title-row {
@@ -735,25 +705,28 @@ const goBack = () => {
.description-markdown {
margin-top: 22rpx;
border-radius: 22rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.042);
border: 1rpx solid rgba(180, 139, 255, 0.18);
overflow: hidden;
}
.description-markdown.collapsed {
max-height: 168rpx;
max-height: 280rpx;
}
.event-body-footer {
margin-top: 18rpx;
margin-top: 14rpx;
}
.collapse-action {
min-height: 64rpx;
display: flex;
align-items: center;
justify-content: flex-end;
color: #a970ff;
font-size: 24rpx;
font-weight: 800;
.edit-link,
.collapse-link {
color: rgba(169, 112, 255, 0.78);
font-size: 22rpx;
font-weight: 700;
line-height: 1;
padding: 8rpx 0;
}
.analysis-panel {
@@ -930,101 +903,5 @@ const goBack = () => {
font-weight: 900;
}
.edit-icon {
position: relative;
width: 34rpx;
height: 34rpx;
border-left: 4rpx solid currentColor;
border-bottom: 4rpx solid currentColor;
transform: skew(-12deg);
}
.edit-icon::after {
content: '';
position: absolute;
left: 4rpx;
bottom: 7rpx;
width: 31rpx;
height: 5rpx;
border-radius: 999rpx;
background: currentColor;
transform: rotate(-45deg);
}
.bookmark-icon {
width: 26rpx;
height: 34rpx;
border: 4rpx solid currentColor;
border-bottom: 0;
border-radius: 5rpx 5rpx 0 0;
position: relative;
box-sizing: border-box;
}
.bookmark-icon::after {
content: '';
position: absolute;
left: 3rpx;
right: 3rpx;
bottom: -8rpx;
height: 16rpx;
background: #0d0d2b;
transform: rotate(45deg);
border-right: 4rpx solid currentColor;
border-bottom: 4rpx solid currentColor;
}
.bookmark-icon.active {
color: #ffd36e;
}
.share-icon {
position: relative;
width: 38rpx;
height: 34rpx;
}
.share-icon view {
position: absolute;
width: 9rpx;
height: 9rpx;
border: 4rpx solid currentColor;
border-radius: 50%;
}
.share-icon view:nth-child(1) {
left: 0;
top: 12rpx;
}
.share-icon view:nth-child(2) {
right: 0;
top: 0;
}
.share-icon view:nth-child(3) {
right: 0;
bottom: 0;
}
.share-icon::before,
.share-icon::after {
content: '';
position: absolute;
left: 12rpx;
width: 22rpx;
height: 3rpx;
background: currentColor;
transform-origin: left center;
}
.share-icon::before {
top: 14rpx;
transform: rotate(-28deg);
}
.share-icon::after {
bottom: 12rpx;
transform: rotate(28deg);
}
/* 操作按钮使用 Unicode 图标,无需自定义 CSS 图标 */
</style>
+2 -5
View File
@@ -146,17 +146,14 @@ const terminateLifeHarmony = () => {
box-sizing: border-box;
overflow: hidden;
color: #fff;
background:
radial-gradient(circle at 50% 18%, rgba(125, 73, 255, 0.22), transparent 36%),
radial-gradient(circle at 10% 74%, rgba(24, 88, 156, 0.2), transparent 32%),
linear-gradient(180deg, #120a36 0%, #05061c 46%, #03020d 100%);
background: transparent;
}
.profile-center::before {
content: '';
position: absolute;
left: 50%;
top: -80rpx;
top: 80rpx;
width: 720rpx;
height: 720rpx;
transform: translateX(-50%);
@@ -123,6 +123,9 @@ const loadScript = async () => {
await store.fetchScripts()
script.value = store.getScriptById(scriptId.value)
}
if (script.value?.id) {
ttsPlayer.prewarmSource(script.value.id)
}
}
const continueCurrent = () => {
+256 -18
View File
@@ -1,5 +1,5 @@
<template>
<view class="script-library">
<view class="script-library" @click="closeScriptMenu">
<view class="page-head">
<view class="back-title" @click="backToScript">
<text class="back-arrow"></text>
@@ -71,7 +71,27 @@
</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 class="menu-wrap">
<text class="ellipsis" :class="{ active: activeMenuId === String(script.id) }" @click.stop="toggleScriptMenu(script)"></text>
<view v-if="activeMenuId === String(script.id)" class="card-menu" @click.stop>
<view class="menu-action" @click="toggleFavorite(script)">
<text class="menu-dot"></text>
<text>{{ isFavorite(script) ? '取消收藏' : '收藏剧本' }}</text>
</view>
<view class="menu-action" @click="continueScript(script)">
<text class="menu-dot"></text>
<text>继续生成</text>
</view>
<view class="menu-action" @click="openScriptDetailFromMenu(script)">
<text class="menu-dot"></text>
<text>查看详情</text>
</view>
<view class="menu-action danger" @click="requestDeleteScript(script)">
<text class="menu-dot"></text>
<text>删除剧本</text>
</view>
</view>
</view>
</view>
</view>
@@ -112,6 +132,17 @@
<text class="empty-text">去爽文生成页写下一句灵感生成你的第一段平行人生</text>
<view class="empty-action" @click="createScript">新建剧本</view>
</view>
<view v-if="deleteTarget" class="confirm-mask" @click.stop="cancelDeleteScript">
<view class="confirm-panel" @click.stop>
<text class="confirm-title">删除剧本</text>
<text class="confirm-copy">确定删除{{ deleteTarget.title || '未命名剧本' }}删除后将无法在历史剧本中查看</text>
<view class="confirm-actions">
<view class="confirm-btn secondary" @click="cancelDeleteScript">取消</view>
<view class="confirm-btn danger" :class="{ disabled: deletingScript }" @click="confirmDeleteScript">{{ deletingScript ? '删除中' : '确认删除' }}</view>
</view>
</view>
</view>
</view>
</template>
@@ -126,6 +157,9 @@ const keyword = ref('')
const sortMode = ref('updated')
const viewMode = ref('list')
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
const activeMenuId = ref('')
const deleteTarget = ref(null)
const deletingScript = ref(false)
const typeTabs = [
{ label: '长篇', value: 'long' },
@@ -350,23 +384,73 @@ const toggleViewMode = () => {
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
}
const openScriptMenu = (script) => {
const closeScriptMenu = () => {
activeMenuId.value = ''
}
const toggleScriptMenu = (script) => {
const id = String(script?.id || '')
activeMenuId.value = activeMenuId.value === id ? '' : id
}
const toggleFavorite = (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) openScriptDetail(script)
}
})
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)
closeScriptMenu()
uni.showToast({ title: favorite ? '已取消收藏' : '已收藏', icon: 'success' })
}
const continueScript = (script) => {
closeScriptMenu()
viewScript(script)
}
const openScriptDetailFromMenu = (script) => {
closeScriptMenu()
openScriptDetail(script)
}
const requestDeleteScript = (script) => {
closeScriptMenu()
if (!script?.id || String(script.id).startsWith('demo-')) {
uni.showToast({ title: '示例剧本不可删除', icon: 'none' })
return
}
deleteTarget.value = script
}
const cancelDeleteScript = () => {
if (deletingScript.value) return
deleteTarget.value = null
}
const confirmDeleteScript = async () => {
if (!deleteTarget.value?.id || deletingScript.value) return
deletingScript.value = true
const id = String(deleteTarget.value.id)
const res = await store.deleteScript(id)
deletingScript.value = false
if (!res.success) {
uni.showToast({ title: res.error || '删除失败', icon: 'none' })
return
}
const next = { ...localFavorites.value }
delete next[id]
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
const conversationId = deleteTarget.value.conversationId || deleteTarget.value.plotJson?.conversationId
if (conversationId) uni.removeStorageSync(`script_conversation_history_${conversationId}`)
uni.removeStorageSync(`script_chat_history_${id}`)
const pending = uni.getStorageSync('pending_open_script_chat')
if (pending?.id === id) uni.removeStorageSync('pending_open_script_chat')
deleteTarget.value = null
uni.showToast({ title: '已删除剧本', icon: 'success' })
}
</script>
@@ -670,6 +754,7 @@ const openScriptMenu = (script) => {
}
.right-state {
position: relative;
flex-shrink: 0;
gap: 14rpx;
}
@@ -699,11 +784,89 @@ const openScriptMenu = (script) => {
}
.ellipsis {
min-width: 48rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 18rpx;
color: rgba(224, 214, 243, 0.66);
font-size: 24rpx;
letter-spacing: 3rpx;
}
.ellipsis.active {
color: #fff;
background: rgba(149, 55, 255, 0.22);
box-shadow: 0 0 20rpx rgba(168, 67, 255, 0.28);
}
.menu-wrap {
position: relative;
}
.card-menu {
position: absolute;
top: 52rpx;
right: 0;
z-index: 30;
width: 202rpx;
padding: 10rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(192, 132, 252, 0.32);
background:
radial-gradient(circle at 90% 0%, rgba(168, 85, 247, 0.22), transparent 36%),
rgba(12, 8, 34, 0.96);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.08),
0 18rpx 42rpx rgba(0, 0, 0, 0.34);
backdrop-filter: blur(22rpx);
-webkit-backdrop-filter: blur(22rpx);
}
.card-menu::before {
content: '';
position: absolute;
right: 22rpx;
top: -10rpx;
width: 18rpx;
height: 18rpx;
border-left: 1rpx solid rgba(192, 132, 252, 0.3);
border-top: 1rpx solid rgba(192, 132, 252, 0.3);
background: rgba(12, 8, 34, 0.96);
transform: rotate(45deg);
}
.menu-action {
position: relative;
z-index: 1;
height: 58rpx;
padding: 0 14rpx;
display: flex;
align-items: center;
gap: 12rpx;
border-radius: 16rpx;
color: rgba(240, 232, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
}
.menu-action:active {
background: rgba(149, 55, 255, 0.18);
}
.menu-action.danger {
color: #ff8fa3;
}
.menu-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: currentColor;
opacity: 0.72;
}
.tag-row {
display: flex;
flex-wrap: wrap;
@@ -826,4 +989,79 @@ const openScriptMenu = (script) => {
font-weight: 800;
background: linear-gradient(135deg, #b346ff, #7330ff);
}
.confirm-mask {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
padding: 0 48rpx;
background: rgba(3, 2, 13, 0.62);
backdrop-filter: blur(12rpx);
-webkit-backdrop-filter: blur(12rpx);
}
.confirm-panel {
width: 100%;
box-sizing: border-box;
padding: 34rpx 30rpx 28rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(192, 132, 252, 0.34);
background:
radial-gradient(circle at 92% 0%, rgba(168, 85, 247, 0.2), transparent 36%),
linear-gradient(145deg, rgba(22, 13, 58, 0.98), rgba(8, 7, 26, 0.98));
box-shadow: 0 24rpx 72rpx rgba(0, 0, 0, 0.42);
}
.confirm-title {
display: block;
color: #fff;
font-size: 32rpx;
font-weight: 900;
text-align: center;
}
.confirm-copy {
display: block;
margin-top: 18rpx;
color: rgba(229, 219, 247, 0.76);
font-size: 24rpx;
line-height: 1.55;
text-align: center;
}
.confirm-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18rpx;
margin-top: 30rpx;
}
.confirm-btn {
height: 70rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 25rpx;
font-weight: 900;
}
.confirm-btn.secondary {
color: #e8ccff;
border: 1rpx solid rgba(192, 132, 252, 0.3);
background: rgba(88, 28, 135, 0.2);
}
.confirm-btn.danger {
color: #fff;
background: linear-gradient(145deg, #e05276, #8d2444);
box-shadow: 0 12rpx 30rpx rgba(224, 82, 118, 0.24);
}
.confirm-btn.disabled {
opacity: 0.55;
}
</style>
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -3,10 +3,10 @@
<view class="space-bg"></view>
<view class="status-space" :style="{ height: capsuleTopReservePx + 'px' }"></view>
<view class="topbar" :style="topbarStyle">
<view class="topbar">
<text class="back" @click="goBack"></text>
<text class="title">编辑资料 <text class="gold"></text></text>
<text class="save" @click="saveProfile">保存</text>
<text class="save" :class="{ disabled: saving }" @click="saveProfile">{{ saving ? '保存中' : '保存' }}</text>
</view>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
@@ -188,7 +188,7 @@ import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
const store = useAppStore()
const { capsuleTopReservePx, topbarStyle } = useMenuButtonSafeArea({ extraTopPx: 2 })
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 2 })
const isEdit = ref(false)
const saving = ref(false)
const birthday = ref('')
@@ -319,6 +319,7 @@ const saveProfile = async () => {
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
@@ -331,9 +332,12 @@ const goBack = () => {
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}
onMounted(() => {
onMounted(async () => {
const pages = getCurrentPages()
isEdit.value = pages[pages.length - 1]?.options?.edit === '1'
if (isEdit.value) {
await store.fetchUserProfile()
}
syncFromStore()
})
</script>
@@ -374,7 +378,7 @@ onMounted(() => {
.topbar {
height: 72rpx;
display: grid;
grid-template-columns: 76rpx 1fr 76rpx;
grid-template-columns: 76rpx 1fr 112rpx;
align-items: center;
padding: 0 28rpx;
}
@@ -401,6 +405,12 @@ onMounted(() => {
color: #b94cff;
font-size: 26rpx;
text-align: right;
justify-self: end;
min-width: 96rpx;
}
.save.disabled {
opacity: 0.58;
}
.scroll {
+17
View File
@@ -40,6 +40,22 @@ export const login = async ({ phone, smsCode }) => {
return response
}
export const wechatLogin = async ({ code, nickname, avatar }) => {
const response = await post('/auth/wechat/login', { code, nickname, avatar })
if (response.data) {
const { accessToken, refreshToken } = response.data
if (accessToken) {
uni.setStorageSync('access_token', accessToken)
}
if (refreshToken) {
uni.setStorageSync('refresh_token', refreshToken)
}
}
return response
}
/**
* 用户登出
* @returns {Promise<void>}
@@ -120,6 +136,7 @@ export const checkPhone = async (phone) => {
export default {
getSmsCode,
login,
wechatLogin,
logout,
refreshToken,
validateToken,
+16 -1
View File
@@ -95,6 +95,10 @@ const transformToBackendFormat = (frontendData) => {
character,
events,
plotJson,
conversationId,
parentScriptId,
revisionIndex,
revisionOf,
useSocialInsights
} = frontendData
@@ -112,7 +116,14 @@ const transformToBackendFormat = (frontendData) => {
plotTurning: frontendData.plotTurning || '',
plotClimax: frontendData.plotClimax || '',
plotEnding: frontendData.plotEnding || '',
plotJson: plotJson || (content ? { fullContent: content } : null),
plotJson: {
...(content ? { fullContent: content } : {}),
...(plotJson || {}),
...(conversationId ? { conversationId } : {}),
...(parentScriptId ? { parentScriptId } : {}),
...(revisionIndex ? { revisionIndex } : {}),
...(revisionOf ? { revisionOf } : {})
},
isSelected,
characterInfo,
lifeEventsSummary,
@@ -171,6 +182,10 @@ export const transformToFrontendFormat = (backendData) => {
date: createTime ? String(createTime).slice(0, 10) : new Date().toLocaleDateString(),
mode: plotJson?.mode || 'custom',
prompt: plotJson?.prompt || '',
conversationId: plotJson?.conversationId || '',
parentScriptId: plotJson?.parentScriptId || '',
revisionIndex: plotJson?.currentRevisionIndex || plotJson?.revisionIndex || 0,
revisionOf: plotJson?.revisionOf || '',
wordCount: content ? content.length : 0
}
}
+27 -9
View File
@@ -6,6 +6,24 @@
import { get, post, put } from './request.js'
const normalizeDate = (value) => {
if (!value) return ''
return String(value).slice(0, 10)
}
const parseJsonArray = (value) => {
if (Array.isArray(value)) return value
if (!value) return []
if (typeof value !== 'string') return []
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch (error) {
console.warn('[userProfile] JSON array parse failed', error)
return []
}
}
/**
* 获取当前用户档案
* @returns {Promise<Object|null>} 用户档案
@@ -85,9 +103,9 @@ const transformToBackendFormat = (frontendData) => {
futureVision: future?.vision || null,
// 理想生活状态
idealLife: future?.ideal || null,
city: city || null,
industry: industry || null,
company: company || null,
city: city ?? '',
industry: industry ?? '',
company: company ?? '',
personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags,
birthday: birthday || null
}
@@ -134,20 +152,20 @@ export const transformToFrontendFormat = (backendData) => {
profession: profession || '',
mbti: mbti || '',
// 兴趣爱好从JSON字符串解析
hobbies: hobbies ? (typeof hobbies === 'string' ? JSON.parse(hobbies) : hobbies) : [],
hobbies: parseJsonArray(hobbies),
// 童年经历
childhood: {
date: childhoodDate || '',
date: normalizeDate(childhoodDate),
text: childhoodContent || ''
},
// 高光时刻
joy: {
date: peakDate || '',
date: normalizeDate(peakDate),
text: peakContent || ''
},
// 低谷时期
low: {
date: valleyDate || '',
date: normalizeDate(valleyDate),
text: valleyContent || ''
},
// 未来期许
@@ -158,8 +176,8 @@ export const transformToFrontendFormat = (backendData) => {
city: city || '',
industry: industry || '',
company: company || '',
personalityTags: personalityTags ? (typeof personalityTags === 'string' ? JSON.parse(personalityTags) : personalityTags) : [],
birthday: birthday || ''
personalityTags: parseJsonArray(personalityTags),
birthday: normalizeDate(birthday)
}
}
+55 -8
View File
@@ -55,6 +55,15 @@ const resetAuthState = () => {
state.userProfile = null
}
const applyProfileResponse = (profileData) => {
const profile = userProfileService.transformToFrontendFormat(profileData)
state.userProfile = profile
if (profile) {
Object.assign(state.registrationData, profile)
}
return profile
}
const clearStoredTokens = () => {
uni.removeStorageSync('access_token')
uni.removeStorageSync('refresh_token')
@@ -98,14 +107,14 @@ const fetchUserProfile = async (options = {}) => {
try {
const res = await userProfileService.getCurrentProfile()
if (res.data) {
state.userProfile = userProfileService.transformToFrontendFormat(res.data)
Object.assign(state.registrationData, state.userProfile)
const profile = applyProfileResponse(res.data)
logAuth('profile:success', { hasProfile: true })
return profile
} else {
state.userProfile = null
logAuth('profile:empty', { hasProfile: false })
}
return res.data
return null
} catch (error) {
state.userProfile = null
logAuth('profile:fail', {
@@ -125,13 +134,17 @@ const saveUserProfile = async () => {
state.isLoading = true
try {
const dataToSave = { ...state.registrationData }
let response
if (state.userProfile?.id) {
await userProfileService.updateProfile({
response = await userProfileService.updateProfile({
id: state.userProfile.id,
...dataToSave
})
} else {
await userProfileService.createProfile(dataToSave)
response = await userProfileService.createProfile(dataToSave)
}
if (response?.data) {
applyProfileResponse(response.data)
}
await fetchUserProfile()
return { success: true }
@@ -243,8 +256,30 @@ const fetchScripts = async () => {
const createScript = async (scriptData) => {
try {
const res = await epicScriptService.createScript(scriptData)
const script = epicScriptService.transformToFrontendFormat(res.data)
await fetchScripts()
return { success: true, data: res.data }
return { success: true, data: script || res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const updateScript = async (scriptData) => {
try {
const res = await epicScriptService.updateScript(scriptData)
const script = epicScriptService.transformToFrontendFormat(res.data)
await fetchScripts()
return { success: true, data: script || res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
const deleteScript = async (id) => {
try {
await epicScriptService.deleteScript(id)
await fetchScripts()
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
@@ -416,8 +451,18 @@ const initialize = async () => {
export const useAppStore = () => {
return readonly({
...state,
hasProfile,
get isLoggedIn() { return state.isLoggedIn },
get isLoading() { return state.isLoading },
get currentStep() { return state.currentStep },
get userInfo() { return state.userInfo },
get userProfile() { return state.userProfile },
get events() { return state.events },
get scripts() { return state.scripts },
get inspirationRecommendations() { return state.inspirationRecommendations },
get paths() { return state.paths },
get currentPath() { return state.currentPath },
get registrationData() { return state.registrationData },
get hasProfile() { return hasProfile.value },
login,
loginWithWechat,
logout,
@@ -436,6 +481,8 @@ export const useAppStore = () => {
getEventById,
fetchScripts,
createScript,
updateScript,
deleteScript,
fetchInspirationRecommendations,
fetchRandomInspirations,
generateScriptFromInspiration,