Files
happy-life-star/UniApp/src/pages/main/ScriptView.vue
T
2026-02-27 11:32:50 +08:00

577 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="script-view">
<text class="page-title font-serif">剧本生成器</text>
<view class="section-card glass-card">
<view class="section-header">
<text class="section-title">我的基础人设</text>
<text class="section-hint">可自由修改</text>
</view>
<view class="profile-grid">
<input
class="glass-input"
placeholder="姓名"
v-model="nickname"
/>
<picker class="glass-picker" mode="selector" :range="zodiacOptions" :value="zodiacIndex" @change="onZodiacChange">
<view class="picker-value">{{ registrationData.zodiac || '星座' }}</view>
</picker>
<picker class="glass-picker" mode="selector" :range="mbtiOptions" :value="mbtiIndex" @change="onMbtiChange">
<view class="picker-value">{{ registrationData.mbti || 'MBTI' }}</view>
</picker>
<input
class="glass-input"
placeholder="职业"
v-model="profession"
/>
</view>
</view>
<view class="section-card glass-card">
<view class="input-group">
<text class="label">剧本主题</text>
<input
class="glass-input"
placeholder="如:巅峰重现、治愈之旅、赛博觉醒..."
v-model="scriptConfig.theme"
/>
</view>
<view class="input-group">
<view class="npc-header">
<text class="label">关键配角/新的人设</text>
<button class="add-btn" @click="addNpc">+ 添加</button>
</view>
<view class="npc-form">
<input
class="glass-input npc-input"
placeholder="姓名"
v-model="npcConfig.name"
/>
<picker class="glass-picker npc-picker" mode="selector" :range="npcRoleOptions" :value="npcRoleIndex" @change="onNpcRoleChange">
<view class="picker-value">{{ npcConfig.role || '角色' }}</view>
</picker>
<picker class="glass-picker npc-picker" mode="selector" :range="npcRelationOptions" :value="npcRelationIndex" @change="onNpcRelationChange">
<view class="picker-value">{{ npcConfig.relation || '关系' }}</view>
</picker>
</view>
<textarea
class="glass-textarea"
placeholder="自由描述TA的人设特点或关键剧情点..."
v-model="npcConfig.desc"
rows="2"
/>
<view class="npc-list">
<view
v-for="(npc, index) in customNpcs"
:key="index"
class="npc-tag"
>
<text>{{ npc.name }} ({{ npc.role }})</text>
<text class="delete-btn" @click="removeNpc(index)">×</text>
</view>
</view>
</view>
<view class="params-row">
<view class="param-group">
<text class="param-label">叙事风格</text>
<view class="param-options">
<text
v-for="style in scriptStyles"
:key="style"
class="param-option"
:class="{ active: scriptConfig.style === style }"
@click="scriptConfig.style = style"
>
{{ style }}
</text>
</view>
</view>
<view class="param-group">
<text class="param-label">故事篇幅</text>
<view class="param-options">
<text
v-for="length in scriptLengths"
:key="length"
class="param-option"
:class="{ active: scriptConfig.length === length }"
@click="scriptConfig.length = length"
>
{{ length }}
</text>
</view>
</view>
</view>
<button
class="btn-primary generate-btn"
:loading="isGenerating"
:disabled="isGenerating || !scriptConfig.theme"
@click="generateScript"
>
<text v-if="isGenerating">命运编织中...</text>
<text v-else>生成平行人生剧本</text>
</button>
</view>
<view v-if="scripts.length > 0" class="scripts-list">
<view
v-for="script in scripts"
:key="script.id"
class="script-card glass-card"
:class="{ selected: script.isSelected }"
>
<view class="script-header">
<text class="script-title">{{ script.title }}</text>
<text class="script-persona">{{ script.theme || '追光者' }}</text>
</view>
<text class="script-summary" lines="3">{{ getScriptSummary(script) }}</text>
<view class="script-footer">
<text class="script-style">{{ script.style || '风格' }}</text>
<button class="select-btn" @click="selectScript(script.id)">
路径映射
</button>
</view>
</view>
</view>
<view v-else-if="!isGenerating" class="empty-state glass-card">
<text class="empty-icon">🎬</text>
<text class="empty-text">尚未生成剧本定义你的未来篇章</text>
</view>
<view v-if="isGenerating" class="generating-state glass-card">
<view class="spinner"></view>
<text class="generating-text">正在采集星海中的深紫色碎屑...</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, reactive, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
const store = useAppStore()
const zodiacOptions = ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座']
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
const npcRoleOptions = ['伙伴', '宿敌', '导师', '挚爱', '下属', '路人']
const npcRelationOptions = ['信任', '对立', '暧昧', '敬畏', '背叛', '守护']
const scriptStyles = ['爽文', '治愈', '热血', '玄幻', '职场', '赛博']
const scriptLengths = ['短篇', '中篇', '长篇', '史诗']
const registrationData = computed(() => store.registrationData || {})
const scripts = computed(() => store.scripts || [])
const nickname = computed({
get: () => registrationData.value.nickname || '',
set: (val) => store.updateRegistration({ nickname: val })
})
const profession = computed({
get: () => registrationData.value.profession || '',
set: (val) => store.updateRegistration({ profession: val })
})
const zodiacIndex = computed(() => zodiacOptions.indexOf(registrationData.value.zodiac))
const mbtiIndex = computed(() => mbtiOptions.indexOf(registrationData.value.mbti))
const scriptConfig = reactive({
theme: '',
style: '爽文',
length: '中篇'
})
const npcConfig = reactive({
name: '',
role: '伙伴',
relation: '信任',
desc: ''
})
const customNpcs = ref([])
const isGenerating = ref(false)
const npcRoleIndex = computed(() => npcRoleOptions.indexOf(npcConfig.role))
const npcRelationIndex = computed(() => npcRelationOptions.indexOf(npcConfig.relation))
const onZodiacChange = (e) => {
store.updateRegistration({ zodiac: zodiacOptions[e.detail.value] })
}
const onMbtiChange = (e) => {
store.updateRegistration({ mbti: mbtiOptions[e.detail.value] })
}
const onNpcRoleChange = (e) => {
npcConfig.role = npcRoleOptions[e.detail.value]
}
const onNpcRelationChange = (e) => {
npcConfig.relation = npcRelationOptions[e.detail.value]
}
const addNpc = () => {
if (npcConfig.name) {
customNpcs.value.push({ ...npcConfig })
npcConfig.name = ''
npcConfig.desc = ''
}
}
const removeNpc = (index) => {
customNpcs.value.splice(index, 1)
}
const generateScript = async () => {
if (!scriptConfig.theme || isGenerating.value) return
isGenerating.value = true
try {
await store.createScript({
theme: scriptConfig.theme,
style: scriptConfig.style,
length: scriptConfig.length,
character: registrationData.value,
events: store.events
})
scriptConfig.theme = ''
} finally {
isGenerating.value = false
}
}
const selectScript = async (id) => {
await store.selectScript(id)
uni.$emit('switchTab', 'path')
}
const getScriptSummary = (script) => {
const text = script.summary || script.content || ''
return text.replace(/\s+/g, ' ').trim()
}
onMounted(async () => {
await store.fetchUserProfile()
await store.fetchEvents()
await store.fetchScripts()
})
</script>
<style scoped>
.script-view {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.page-title {
font-size: 36rpx;
font-weight: 400;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
letter-spacing: 4rpx;
}
.section-card {
padding: 32rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title {
font-size: 22rpx;
color: rgba(192, 132, 252, 0.6);
font-weight: 600;
letter-spacing: 4rpx;
text-transform: uppercase;
}
.section-hint {
font-size: 16rpx;
color: rgba(255, 255, 255, 0.35);
font-style: italic;
}
.profile-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
.glass-input, .glass-picker {
width: 100%;
height: 80rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 0 24rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.picker-value {
line-height: 80rpx;
color: rgba(255, 255, 255, 0.9);
}
.input-group {
margin-bottom: 24rpx;
}
.label {
display: block;
font-size: 18rpx;
color: rgba(255, 255, 255, 0.35);
font-weight: 600;
letter-spacing: 4rpx;
text-transform: uppercase;
margin-bottom: 16rpx;
}
.npc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.add-btn {
font-size: 20rpx;
color: #C084FC;
border: 1px solid rgba(192, 132, 252, 0.3);
padding: 8rpx 16rpx;
border-radius: 12rpx;
background: transparent;
}
.npc-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12rpx;
margin-bottom: 16rpx;
}
.npc-input, .npc-picker {
height: 72rpx;
padding: 0 16rpx;
font-size: 24rpx;
}
.npc-picker .picker-value {
line-height: 72rpx;
font-size: 24rpx;
}
.glass-textarea {
width: 100%;
height: 120rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 20rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 24rpx;
margin-bottom: 16rpx;
}
.npc-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.npc-tag {
background: rgba(168, 85, 247, 0.1);
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 24rpx;
padding: 8rpx 20rpx;
font-size: 20rpx;
color: rgba(243, 232, 255, 0.8);
display: flex;
align-items: center;
gap: 8rpx;
}
.delete-btn {
color: rgba(255, 255, 255, 0.4);
font-size: 28rpx;
padding: 0 4rpx;
}
.params-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
margin-bottom: 32rpx;
}
.param-group {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.param-label {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.35);
margin-left: 8rpx;
}
.param-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.param-option {
padding: 10rpx 20rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
.param-option.active {
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.5);
color: #C084FC;
}
.generate-btn {
width: 100%;
height: 96rpx;
box-shadow: 0 8rpx 40rpx rgba(168, 85, 247, 0.3);
}
.scripts-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.script-card {
padding: 32rpx;
border-left: 4rpx solid transparent;
}
.script-card.selected {
border-left-color: #C084FC;
}
.script-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.script-title {
font-size: 32rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.script-persona {
font-size: 18rpx;
color: rgba(168, 85, 247, 0.6);
background: rgba(168, 85, 247, 0.1);
padding: 6rpx 16rpx;
border-radius: 12rpx;
border: 1px solid rgba(168, 85, 247, 0.2);
}
.script-summary {
display: block;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
line-height: 1.6;
margin-bottom: 24rpx;
}
.script-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.script-style {
font-size: 20rpx;
color: #C084FC;
}
.select-btn {
font-size: 24rpx;
color: #C084FC;
font-weight: 600;
background: transparent;
}
.empty-state {
padding: 80rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
opacity: 0.5;
}
.empty-icon {
font-size: 64rpx;
}
.empty-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
text-align: center;
}
.generating-state {
padding: 80rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 32rpx;
}
.spinner {
width: 64rpx;
height: 64rpx;
border: 4rpx solid #A855F7;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.generating-text {
font-size: 26rpx;
color: rgba(192, 132, 252, 0.6);
font-style: italic;
letter-spacing: 2rpx;
}
</style>