Files
happy-life-star/mini-program/src/pages/main/ScriptView.vue
T
peanut 97a74d5969 style: 优化剧本生成器移动端布局(方案 A)
主要变更:
- 参数区域改为并排两列 grid 布局,匹配原型图
- 全面调小字体尺寸(input: 22rpx→18rpx, label: 16rpx→17rpx)
- 优化间距(主容器 padding: 32rpx→24rpx, NPC 容器:24rpx→16rpx)
- 添加超小屏幕(≤320px)适配媒体查询
- 圆角调整接近原型图(主容器:56rpx)
- 输入框高度降低(72rpx→64rpx),视觉更紧凑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:31:12 +08:00

638 lines
15 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-main">
<view class="input-group">
<text class="label">1. 剧本主题</text>
<input
class="glass-input theme-input"
placeholder="如:巅峰重现、治愈之旅、赛博觉醒..."
v-model="scriptConfig.theme"
/>
</view>
<view class="input-group">
<view class="npc-header">
<text class="label">2. 关键配角/新的人设</text>
<button class="add-btn" @click="addNpc">+ 添加</button>
</view>
<view class="npc-container">
<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>
<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-section">
<text class="param-section-label">3. 核心参数</text>
<view class="params-container">
<view class="param-group">
<text class="param-sublabel">叙事风格</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-sublabel">故事篇幅</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>
</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: 20rpx;
min-height: 100%;
}
.page-title {
font-size: 32rpx;
font-weight: 300;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 4rpx;
}
.section-card {
padding: 20rpx;
border-radius: 28rpx;
}
.glass-card-main {
padding: 24rpx;
border-radius: 56rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.section-title {
font-size: 17rpx;
color: rgba(192, 132, 252, 0.6);
font-weight: 600;
letter-spacing: 3rpx;
text-transform: uppercase;
}
.section-hint {
font-size: 13rpx;
color: rgba(255, 255, 255, 0.35);
font-style: italic;
}
.profile-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10rpx;
}
.glass-input, .glass-picker {
width: 100%;
height: 64rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 0 16rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 18rpx;
box-sizing: border-box;
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.theme-input {
height: 72rpx;
font-size: 19rpx;
}
.picker-value {
line-height: 64rpx;
color: rgba(255, 255, 255, 0.9);
}
.input-group {
margin-bottom: 24rpx;
}
.label {
display: block;
font-size: 17rpx;
color: rgba(192, 132, 252, 0.6);
font-weight: 600;
letter-spacing: 3rpx;
text-transform: uppercase;
margin-bottom: 10rpx;
}
.npc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.add-btn {
font-size: 16rpx;
color: #C084FC;
border: 1px solid rgba(192, 132, 252, 0.3);
padding: 5rpx 10rpx;
border-radius: 10rpx;
background: transparent;
}
.npc-container {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
padding: 16rpx;
margin-bottom: 14rpx;
}
.npc-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10rpx;
margin-bottom: 14rpx;
}
.npc-input, .npc-picker {
height: 60rpx;
padding: 0 14rpx;
font-size: 17rpx;
}
.npc-picker .picker-value {
line-height: 60rpx;
font-size: 17rpx;
}
.glass-textarea {
width: 100%;
height: 88rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 14rpx;
color: rgba(255, 255, 255, 0.9);
font-size: 17rpx;
box-sizing: border-box;
}
.npc-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.npc-tag {
background: rgba(168, 85, 247, 0.1);
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 20rpx;
padding: 6rpx 16rpx;
font-size: 18rpx;
color: rgba(243, 232, 255, 0.8);
display: flex;
align-items: center;
gap: 6rpx;
}
.delete-btn {
color: rgba(255, 255, 255, 0.4);
font-size: 20rpx;
padding: 0 2rpx;
}
.params-section {
margin-bottom: 24rpx;
}
.param-section-label {
display: block;
font-size: 17rpx;
color: rgba(192, 132, 252, 0.6);
font-weight: 600;
letter-spacing: 3rpx;
text-transform: uppercase;
margin-bottom: 12rpx;
}
.params-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
.param-group {
display: flex;
flex-direction: column;
gap: 8rpx;
min-width: 0;
}
.param-sublabel {
font-size: 15rpx;
color: rgba(255, 255, 255, 0.4);
margin-left: 4rpx;
}
.param-options {
display: flex;
flex-wrap: wrap;
gap: 6rpx;
}
.param-option {
padding: 6rpx 12rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 18rpx;
font-size: 17rpx;
color: rgba(255, 255, 255, 0.4);
white-space: nowrap;
}
.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: 80rpx;
border-radius: 28rpx;
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.3);
margin-top: 8rpx;
}
.scripts-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.script-card {
padding: 20rpx;
border-left: 3rpx solid transparent;
border-radius: 28rpx;
}
.script-card.selected {
border-left-color: #C084FC;
}
.script-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.script-title {
font-size: 26rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.script-persona {
font-size: 15rpx;
color: rgba(168, 85, 247, 0.6);
background: rgba(168, 85, 247, 0.1);
padding: 5rpx 10rpx;
border-radius: 10rpx;
border: 1px solid rgba(168, 85, 247, 0.2);
}
.script-summary {
display: block;
font-size: 20rpx;
color: rgba(255, 255, 255, 0.5);
line-height: 1.5;
margin-bottom: 14rpx;
}
.script-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.script-style {
font-size: 16rpx;
color: #C084FC;
}
.select-btn {
font-size: 18rpx;
color: #C084FC;
font-weight: 600;
background: transparent;
border: none;
padding: 0;
}
.empty-state {
padding: 56rpx 36rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
opacity: 0.5;
border-radius: 48rpx;
}
.empty-icon {
font-size: 48rpx;
}
.empty-text {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.5);
text-align: center;
}
.generating-state {
padding: 56rpx 36rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
border-radius: 48rpx;
}
.spinner {
width: 48rpx;
height: 48rpx;
border: 3rpx 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: 20rpx;
color: rgba(192, 132, 252, 0.6);
font-style: italic;
letter-spacing: 2rpx;
}
/* 超小屏幕适配 */
@media (max-width: 320px) {
.params-container {
grid-template-columns: 1fr;
}
.npc-form {
grid-template-columns: 1fr 1fr;
}
.profile-grid {
grid-template-columns: 1fr;
}
}
</style>