feat: 完善人生轨迹详情页功能及微信小程序登录优化
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user