feat: 小程序脚本首页重构 + 社交数据导入 + TTS 播放优化
- 后端:新增社交数据导入/审批/洞察生成 API(SocialContent/SocialInsight) - 后端:优化脚本上下文服务,TTS 服务增强 - 小程序:重构脚本首页布局,新增社交导入页面 - 小程序:新增 useTtsPlayer composable,移除旧 ScriptAudioPlayer 组件 - 小程序:新增社交导入服务,优化请求服务 - SQL:新增社交数据导入建表脚本 - 文档:补充设计文档和实施计划 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-entry" @click="openSocialImport">
|
||||
<view>
|
||||
<text class="profile-entry-title">社交数据导入</text>
|
||||
<text class="profile-entry-copy">生成并确认人生素材画像,让爽文更像你</text>
|
||||
</view>
|
||||
<text class="profile-entry-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="type-tabs">
|
||||
<text
|
||||
v-for="tab in typeTabs"
|
||||
@@ -297,6 +305,10 @@ const createScript = () => {
|
||||
uni.$emit('switchTab', 'script')
|
||||
}
|
||||
|
||||
const openSocialImport = () => {
|
||||
uni.navigateTo({ url: '/pages/social-import/index' })
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
uni.showModal({
|
||||
title: '搜索剧本',
|
||||
@@ -592,6 +604,41 @@ const mapScript = async (script) => {
|
||||
background: rgba(130, 48, 220, 0.28);
|
||||
}
|
||||
|
||||
.profile-entry {
|
||||
min-height: 92rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
border: 1rpx solid rgba(105, 79, 210, 0.34);
|
||||
background: rgba(9, 12, 42, 0.72);
|
||||
}
|
||||
|
||||
.profile-entry-title,
|
||||
.profile-entry-copy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-entry-title {
|
||||
color: #fff;
|
||||
font-size: 29rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.profile-entry-copy {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(226, 215, 246, 0.68);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.profile-entry-arrow {
|
||||
color: rgba(226, 215, 246, 0.7);
|
||||
font-size: 46rpx;
|
||||
}
|
||||
|
||||
.script-card {
|
||||
display: grid;
|
||||
grid-template-columns: 150rpx 1fr;
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
<text class="stat-label">字数</text>
|
||||
</view>
|
||||
</view>
|
||||
<ScriptAudioPlayer v-if="script?.id" :script-id="script.id" />
|
||||
<view class="audio-inline" @click="trackTtsClick">
|
||||
<text class="audio-inline-icon">▶</text>
|
||||
<text class="audio-inline-text">{{ detailTtsButtonText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs kos-card">
|
||||
@@ -63,7 +66,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import Markdown from '../../components/Markdown.vue'
|
||||
import analytics from '../../services/analytics.js'
|
||||
import ScriptAudioPlayer from '../../components/ScriptAudioPlayer.vue'
|
||||
import { useTtsPlayer } from '../../composables/useTtsPlayer.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const statusBarHeight = ref(20)
|
||||
@@ -71,6 +74,7 @@ const activeTab = ref('content')
|
||||
const scriptId = ref('')
|
||||
const script = ref(null)
|
||||
const pagePath = '/pages/main/ScriptDetailView'
|
||||
const ttsPlayer = useTtsPlayer({ pagePath })
|
||||
|
||||
const fullContent = computed(() => script.value?.content || '暂无正文内容。')
|
||||
const lengthText = computed(() => {
|
||||
@@ -78,6 +82,11 @@ const lengthText = computed(() => {
|
||||
return map[script.value?.length] || script.value?.length || '中篇'
|
||||
})
|
||||
|
||||
const detailTtsButtonText = computed(() => {
|
||||
if (!script.value?.id) return '生成保存后可语音播放'
|
||||
return ttsPlayer.buttonText.value
|
||||
})
|
||||
|
||||
const outline = computed(() => {
|
||||
const text = fullContent.value
|
||||
const parts = text.split(/\n{2,}/).filter(Boolean)
|
||||
@@ -102,6 +111,7 @@ const outline = computed(() => {
|
||||
|
||||
const loadScript = async () => {
|
||||
if (!scriptId.value) return
|
||||
ttsPlayer.reset()
|
||||
script.value = store.getScriptById(scriptId.value)
|
||||
if (!script.value) {
|
||||
await store.fetchScripts()
|
||||
@@ -124,6 +134,13 @@ const selectCurrent = async () => {
|
||||
uni.navigateTo({ url: '/pages/main/PathView' })
|
||||
}
|
||||
|
||||
const trackTtsClick = () => {
|
||||
analytics.track('script_detail_tts_click', {
|
||||
script_id: script.value?.id || ''
|
||||
}, { eventType: 'tts', pagePath })
|
||||
ttsPlayer.playSource(script.value?.id || '')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
@@ -143,6 +160,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
ttsPlayer.reset()
|
||||
analytics.trackPageLeave(pagePath, { script_id: scriptId.value })
|
||||
})
|
||||
</script>
|
||||
@@ -254,6 +272,41 @@ onUnmounted(() => {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.audio-inline {
|
||||
height: 76rpx;
|
||||
margin-top: 26rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 0 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
color: #fff;
|
||||
font-size: 25rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
background: linear-gradient(135deg, #24c6dc, #7f5af0);
|
||||
box-shadow: 0 12rpx 30rpx rgba(36, 198, 220, 0.22);
|
||||
}
|
||||
|
||||
.audio-inline-icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(5, 6, 21, 0.86);
|
||||
font-size: 18rpx;
|
||||
font-weight: 900;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
.audio-inline-text {
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 18rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@
|
||||
<view class="safe-top" :style="{ height: safeAreaTop + 14 + 'px' }"></view>
|
||||
|
||||
<scroll-view class="content" scroll-y :enhanced="true" :show-scrollbar="false">
|
||||
<RecordView v-if="activeTab === 'record'" />
|
||||
<ScriptView v-if="activeTab === 'script'" />
|
||||
<RecordView v-if="activeTab === 'record'" />
|
||||
<MineView v-if="activeTab === 'mine'" />
|
||||
</scroll-view>
|
||||
|
||||
@@ -18,13 +18,6 @@
|
||||
|
||||
<view class="bottom-nav">
|
||||
<view class="nav-inner">
|
||||
<view class="nav-item" :class="{ active: activeTab === 'record' }" @click="switchTab('record')">
|
||||
<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="tab-icon book-star-icon">
|
||||
<view class="book-page left"></view>
|
||||
@@ -33,6 +26,13 @@
|
||||
</view>
|
||||
<text>爽文生成</text>
|
||||
</view>
|
||||
<view class="nav-item" :class="{ active: activeTab === 'record' }" @click="switchTab('record')">
|
||||
<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 === 'mine' }" @click="switchTab('mine')">
|
||||
<view class="tab-icon smile-face-icon">
|
||||
<view class="smile-eye left"></view>
|
||||
@@ -56,7 +56,7 @@ import MusicPlayer from '../../components/MusicPlayer.vue'
|
||||
import analytics from '../../services/analytics.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const activeTab = ref('record')
|
||||
const activeTab = ref('script')
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
|
||||
const pagePath = '/pages/main/index'
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<scroll-view class="page kos-page" scroll-y :show-scrollbar="false">
|
||||
<view class="nav" :style="{ paddingTop: safeAreaTop + 'px' }">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">社交数据导入</text>
|
||||
<text class="manage" @click="openInsights">画像</text>
|
||||
</view>
|
||||
|
||||
<view class="hero kos-card">
|
||||
<text class="hero-title">把真实表达变成人生素材</text>
|
||||
<text class="hero-copy">仅分析你主动粘贴、上传并授权的内容。确认后的画像才会用于爽文生成。</text>
|
||||
</view>
|
||||
|
||||
<view class="platforms">
|
||||
<view
|
||||
v-for="item in platforms"
|
||||
:key="item.value"
|
||||
class="platform"
|
||||
:class="{ active: platform === item.value }"
|
||||
@click="platform = item.value"
|
||||
>
|
||||
<text>{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="actions">
|
||||
<view class="action kos-card" @click="startManual">
|
||||
<text class="action-title">粘贴文本</text>
|
||||
<text class="action-copy">适合小红书笔记、微博正文、朋友圈文字</text>
|
||||
</view>
|
||||
<view class="action kos-card" @click="startLink">
|
||||
<text class="action-title">保存链接</text>
|
||||
<text class="action-copy">先记录来源链接,再补充正文让 AI 分析</text>
|
||||
</view>
|
||||
<view class="action kos-card" @click="startScreenshot">
|
||||
<text class="action-title">上传截图</text>
|
||||
<text class="action-copy">服务端 OCR 未启用时会提示改用文本导入</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="consent kos-card">
|
||||
<text class="consent-title">授权边界</text>
|
||||
<text class="consent-copy">不会自动登录或抓取小红书、微博、微信等平台;不会分析未授权内容;你可以随时删除导入内容或撤销画像。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import * as socialImport from '../../services/socialImport.js'
|
||||
import analytics from '../../services/analytics.js'
|
||||
|
||||
const pagePath = '/pages/social-import/index'
|
||||
const platform = ref('xiaohongshu')
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
|
||||
const platforms = [
|
||||
{ label: '小红书', value: 'xiaohongshu' },
|
||||
{ label: '微博', value: 'weibo' },
|
||||
{ label: '微信', value: 'wechat' },
|
||||
{ label: '其他', value: 'other' }
|
||||
]
|
||||
|
||||
const goBack = () => uni.navigateBack()
|
||||
|
||||
const openInsights = () => {
|
||||
analytics.track('social_import_insights_click', {}, { eventType: 'social_import', pagePath })
|
||||
uni.navigateTo({ url: '/pages/social-import/insights' })
|
||||
}
|
||||
|
||||
const openPreview = (mode, payload = {}) => {
|
||||
uni.setStorageSync('social_import_draft', {
|
||||
mode,
|
||||
platform: platform.value,
|
||||
...payload
|
||||
})
|
||||
uni.navigateTo({ url: '/pages/social-import/preview' })
|
||||
}
|
||||
|
||||
const startManual = () => {
|
||||
analytics.track('social_import_manual_start', { platform: platform.value }, { eventType: 'social_import', pagePath })
|
||||
openPreview('manual')
|
||||
}
|
||||
|
||||
const startLink = () => {
|
||||
analytics.track('social_import_link_start', { platform: platform.value }, { eventType: 'social_import', pagePath })
|
||||
openPreview('link')
|
||||
}
|
||||
|
||||
const startScreenshot = () => {
|
||||
analytics.track('social_import_screenshot_start', { platform: platform.value }, { eventType: 'social_import', pagePath })
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const filePath = res.tempFilePaths?.[0]
|
||||
if (!filePath) return
|
||||
uni.showLoading({ title: '上传中' })
|
||||
try {
|
||||
await socialImport.screenshotImport({ platform: platform.value, filePath })
|
||||
uni.showToast({ title: '已导入', icon: 'success' })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '请先使用文本导入', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
padding: 0 28rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back,
|
||||
.manage {
|
||||
width: 96rpx;
|
||||
color: #d9c2ff;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.back {
|
||||
font-size: 58rpx;
|
||||
}
|
||||
|
||||
.manage {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-top: 42rpx;
|
||||
padding: 34rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 54rpx;
|
||||
line-height: 1.25;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.hero-copy,
|
||||
.consent-copy,
|
||||
.action-copy {
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.58;
|
||||
}
|
||||
|
||||
.platforms {
|
||||
margin-top: 36rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.platform {
|
||||
height: 62rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
border: 1rpx solid rgba(178, 128, 255, 0.28);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.platform.active {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #a855f7, #6d28d9);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 34rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.action,
|
||||
.consent {
|
||||
border-radius: 24rpx;
|
||||
padding: 26rpx;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.action-title,
|
||||
.consent-title {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.consent {
|
||||
margin-top: 34rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<scroll-view class="page kos-page" scroll-y :show-scrollbar="false">
|
||||
<view class="nav" :style="{ paddingTop: safeAreaTop + 'px' }">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">人生素材画像</text>
|
||||
<text class="add" @click="openImport">导入</text>
|
||||
</view>
|
||||
|
||||
<view class="summary kos-card">
|
||||
<text class="summary-title">{{ confirmedCount }} 个已确认画像</text>
|
||||
<text class="summary-copy">爽文生成只会使用你确认过的画像,建议项不会直接进入创作。</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<text
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ active: activeStatus === tab.value }"
|
||||
@click="activeStatus = tab.value"
|
||||
>{{ tab.label }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="empty kos-card">
|
||||
<text class="empty-title">正在加载画像</text>
|
||||
<text class="empty-copy">马上把你的素材卡片整理出来</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="visibleInsights.length" class="list">
|
||||
<view v-for="item in visibleInsights" :key="item.id" class="insight kos-card">
|
||||
<view class="insight-head">
|
||||
<text class="label">{{ item.label }}</text>
|
||||
<text class="status" :class="'status-' + item.status">{{ statusText(item.status) }}</text>
|
||||
</view>
|
||||
<text class="type">{{ typeText(item.insightType) }}</text>
|
||||
<text class="desc">{{ item.summary }}</text>
|
||||
<text v-if="item.evidenceExcerpt" class="evidence">“{{ item.evidenceExcerpt }}”</text>
|
||||
<view class="ops">
|
||||
<text v-if="item.status !== 'confirmed'" @click="confirmInsight(item)">确认</text>
|
||||
<text v-if="item.status !== 'rejected'" @click="rejectInsight(item)">不采用</text>
|
||||
<text @click="deleteInsight(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="empty kos-card">
|
||||
<text class="empty-title">还没有画像</text>
|
||||
<text class="empty-copy">{{ loadError || '先导入一段社交内容,系统会生成待确认的人生素材画像。' }}</text>
|
||||
<button @click="openImport">去导入内容</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import * as socialImport from '../../services/socialImport.js'
|
||||
import analytics from '../../services/analytics.js'
|
||||
|
||||
const pagePath = '/pages/social-import/insights'
|
||||
const insights = ref([])
|
||||
const activeStatus = ref('all')
|
||||
const loading = ref(true)
|
||||
const loadError = ref('')
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待确认', value: 'suggested' },
|
||||
{ label: '已确认', value: 'confirmed' },
|
||||
{ label: '不采用', value: 'rejected' }
|
||||
]
|
||||
|
||||
const visibleInsights = computed(() => {
|
||||
if (activeStatus.value === 'all') return insights.value
|
||||
return insights.value.filter(item => item.status === activeStatus.value)
|
||||
})
|
||||
|
||||
const confirmedCount = computed(() => insights.value.filter(item => item.status === 'confirmed').length)
|
||||
|
||||
const goBack = () => uni.navigateBack()
|
||||
const openImport = () => uni.redirectTo({ url: '/pages/social-import/index' })
|
||||
|
||||
const loadInsights = async () => {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
const res = await socialImport.listInsights()
|
||||
insights.value = res.data || []
|
||||
} catch (error) {
|
||||
loadError.value = error.message || '画像加载失败,请稍后重试'
|
||||
insights.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateStatus = async (item, status) => {
|
||||
try {
|
||||
await socialImport.updateInsight(item.id, { status })
|
||||
analytics.track('social_insight_status_update', { status }, { eventType: 'social_import', pagePath })
|
||||
await loadInsights()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const confirmInsight = (item) => updateStatus(item, 'confirmed')
|
||||
const rejectInsight = (item) => updateStatus(item, 'rejected')
|
||||
|
||||
const deleteInsight = async (item) => {
|
||||
try {
|
||||
await socialImport.deleteInsight(item.id)
|
||||
await loadInsights()
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const statusText = (status) => {
|
||||
const map = { suggested: '待确认', confirmed: '已确认', rejected: '不采用' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const typeText = (type) => {
|
||||
const map = {
|
||||
value: '价值偏好',
|
||||
interest: '兴趣倾向',
|
||||
emotion_pattern: '情绪模式',
|
||||
script_theme: '剧本主题'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
analytics.track('social_insights_view', {}, { eventType: 'social_import', pagePath })
|
||||
loadInsights()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
padding: 0 28rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back,
|
||||
.add {
|
||||
width: 96rpx;
|
||||
color: #d9c2ff;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.back {
|
||||
font-size: 58rpx;
|
||||
}
|
||||
|
||||
.add {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-top: 30rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 46rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.summary-copy,
|
||||
.desc,
|
||||
.evidence,
|
||||
.type {
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 28rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
border: 1rpx solid rgba(178, 128, 255, 0.28);
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background: rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.insight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.insight-head,
|
||||
.ops {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 5rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
color: #e9d5ff;
|
||||
background: rgba(168, 85, 247, 0.22);
|
||||
}
|
||||
|
||||
.status-confirmed {
|
||||
color: #8ff0bd;
|
||||
background: rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: rgba(235, 225, 255, 0.58);
|
||||
}
|
||||
|
||||
.evidence {
|
||||
padding-left: 16rpx;
|
||||
border-left: 4rpx solid rgba(168, 85, 247, 0.55);
|
||||
}
|
||||
|
||||
.ops {
|
||||
justify-content: flex-end;
|
||||
color: #d9c2ff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
min-height: 420rpx;
|
||||
margin-top: 28rpx;
|
||||
padding: 40rpx 28rpx;
|
||||
border-radius: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24rpx;
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.empty-copy {
|
||||
max-width: 560rpx;
|
||||
text-align: center;
|
||||
color: rgba(235, 225, 255, 0.68);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.empty button {
|
||||
height: 72rpx;
|
||||
padding: 0 34rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
background: linear-gradient(135deg, #a855f7, #6d28d9);
|
||||
}
|
||||
|
||||
.empty button::after {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<scroll-view class="page kos-page" scroll-y :show-scrollbar="false">
|
||||
<view class="nav" :style="{ paddingTop: safeAreaTop + 'px' }">
|
||||
<text class="back" @click="goBack">‹</text>
|
||||
<text class="title">导入预览</text>
|
||||
<text class="ghost"></text>
|
||||
</view>
|
||||
|
||||
<view class="form">
|
||||
<picker :range="platformLabels" :value="platformIndex" @change="onPlatformChange">
|
||||
<view class="field picker-field kos-card">
|
||||
<text class="label">来源平台</text>
|
||||
<text class="value">{{ platformLabel }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view v-if="mode === 'link'" class="field kos-card">
|
||||
<text class="label">原文链接</text>
|
||||
<input class="input" v-model="sourceUrl" placeholder="粘贴链接" placeholder-class="placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="field kos-card">
|
||||
<text class="label">标题</text>
|
||||
<input class="input" v-model="titleText" placeholder="可选" placeholder-class="placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="field kos-card">
|
||||
<text class="label">正文内容</text>
|
||||
<textarea class="textarea" v-model="contentText" maxlength="20000" placeholder="粘贴你希望用于分析的文字" placeholder-class="placeholder" />
|
||||
</view>
|
||||
|
||||
<view class="approval" @click="approvedForAi = !approvedForAi">
|
||||
<view class="checkbox" :class="{ checked: approvedForAi }"></view>
|
||||
<text>我授权本次内容用于生成可确认的人生素材画像</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit kos-primary" :disabled="submitting" @click="submitImport">
|
||||
{{ submitting ? '导入中' : '导入并生成画像' }}
|
||||
</button>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import * as socialImport from '../../services/socialImport.js'
|
||||
import analytics from '../../services/analytics.js'
|
||||
|
||||
const pagePath = '/pages/social-import/preview'
|
||||
const draft = uni.getStorageSync('social_import_draft') || {}
|
||||
const mode = ref(draft.mode || 'manual')
|
||||
const platform = ref(draft.platform || 'xiaohongshu')
|
||||
const sourceUrl = ref(draft.sourceUrl || '')
|
||||
const titleText = ref(draft.title || '')
|
||||
const contentText = ref(draft.content || '')
|
||||
const approvedForAi = ref(true)
|
||||
const submitting = ref(false)
|
||||
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
|
||||
|
||||
const platforms = [
|
||||
{ label: '小红书', value: 'xiaohongshu' },
|
||||
{ label: '微博', value: 'weibo' },
|
||||
{ label: '微信', value: 'wechat' },
|
||||
{ label: '其他', value: 'other' }
|
||||
]
|
||||
|
||||
const platformLabels = platforms.map(item => item.label)
|
||||
const platformIndex = computed(() => Math.max(0, platforms.findIndex(item => item.value === platform.value)))
|
||||
const platformLabel = computed(() => platforms[platformIndex.value]?.label || '小红书')
|
||||
|
||||
const goBack = () => uni.navigateBack()
|
||||
|
||||
const onPlatformChange = (event) => {
|
||||
platform.value = platforms[Number(event.detail.value)]?.value || 'xiaohongshu'
|
||||
}
|
||||
|
||||
const submitImport = async () => {
|
||||
const content = contentText.value.trim()
|
||||
if (!content) {
|
||||
uni.showToast({ title: '请先粘贴正文内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (mode.value === 'link' && !sourceUrl.value.trim()) {
|
||||
uni.showToast({ title: '请填写原文链接', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '分析中' })
|
||||
try {
|
||||
const payload = {
|
||||
platform: platform.value,
|
||||
title: titleText.value.trim(),
|
||||
content,
|
||||
approvedForAi: approvedForAi.value
|
||||
}
|
||||
const res = mode.value === 'link'
|
||||
? await socialImport.linkImport({ ...payload, sourceUrl: sourceUrl.value.trim() })
|
||||
: await socialImport.manualImport(payload)
|
||||
|
||||
if (approvedForAi.value && res.data?.id) {
|
||||
await socialImport.generateInsights([res.data.id])
|
||||
}
|
||||
analytics.track('social_import_submit_success', {
|
||||
mode: mode.value,
|
||||
platform: platform.value,
|
||||
approved_for_ai: approvedForAi.value
|
||||
}, { eventType: 'social_import', pagePath })
|
||||
uni.removeStorageSync('social_import_draft')
|
||||
uni.showToast({ title: '已生成画像', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/social-import/insights' })
|
||||
}, 400)
|
||||
} catch (error) {
|
||||
analytics.track('social_import_submit_fail', {
|
||||
mode: mode.value,
|
||||
platform: platform.value,
|
||||
error: error.message || 'unknown'
|
||||
}, { eventType: 'social_import', pagePath })
|
||||
uni.showToast({ title: error.message || '导入失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
padding: 0 28rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back,
|
||||
.ghost {
|
||||
width: 96rpx;
|
||||
color: #d9c2ff;
|
||||
font-size: 58rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 34rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
|
||||
.picker-field {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(235, 225, 255, 0.72);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.value,
|
||||
.input,
|
||||
.textarea {
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: 340rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: rgba(235, 225, 255, 0.42);
|
||||
}
|
||||
|
||||
.approval {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
color: rgba(235, 225, 255, 0.8);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border-radius: 10rpx;
|
||||
border: 2rpx solid rgba(216, 180, 254, 0.8);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background: #a855f7;
|
||||
border-color: #a855f7;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 38rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.submit::after {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user