182 lines
4.7 KiB
Vue
182 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, inject, onMounted, computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { fetchWorldSetting, saveWorldSetting } from '@/api/worldSetting'
|
|
import { useToast } from '@/composables/useToast'
|
|
import { useToolRefresh } from '@/composables/useToolRefresh'
|
|
import type { Novel } from '@/types/novel'
|
|
import BaseButton from '@/components/ui/BaseButton.vue'
|
|
import BaseTextarea from '@/components/ui/BaseTextarea.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
|
|
const novelId = inject<number>('workspace-novel-id', Number(route.params.id))
|
|
const novel = inject<import('vue').Ref<Novel | null>>('workspace-novel', ref(null))
|
|
const subtitle = inject<import('vue').Ref<string>>('workspace-subtitle', ref(''))
|
|
const content = ref('')
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const hasChanges = ref(false)
|
|
|
|
const contentLength = computed(() => content.value.length)
|
|
const contentHint = computed(() => {
|
|
if (contentLength.value === 0) return '建议至少 500 字'
|
|
if (contentLength.value < 500) return `${contentLength.value} 字,建议至少 500 字`
|
|
return `${contentLength.value} 字`
|
|
})
|
|
|
|
// 世界观页不需要副标题
|
|
subtitle.value = ''
|
|
|
|
// AI 工具执行后自动刷新世界观数据
|
|
useToolRefresh(['get_world_setting', 'save_world_setting'], load)
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const ws = await fetchWorldSetting(novelId)
|
|
content.value = ws.content
|
|
} catch {
|
|
toast.error('加载失败')
|
|
router.push({ name: 'home' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onInput() {
|
|
hasChanges.value = true
|
|
}
|
|
|
|
async function handleSave() {
|
|
saving.value = true
|
|
try {
|
|
await saveWorldSetting(novelId, { content: content.value })
|
|
hasChanges.value = false
|
|
toast.success('世界观已保存')
|
|
} catch {
|
|
toast.error('保存失败')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="!loading" class="world-view">
|
|
<Teleport to="#workspace-actions">
|
|
<BaseButton
|
|
variant="primary"
|
|
:loading="saving"
|
|
:disabled="!hasChanges"
|
|
@click="handleSave"
|
|
>
|
|
保存
|
|
</BaseButton>
|
|
</Teleport>
|
|
|
|
<div class="editor-container">
|
|
<div class="editor-hint">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2" />
|
|
<path d="M8 4v5M8 11v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
|
</svg>
|
|
<span>描述故事发生的世界背景、历史、规则、地理环境等设定</span>
|
|
</div>
|
|
|
|
<BaseTextarea
|
|
v-model="content"
|
|
placeholder="在此描述你的故事世界..."
|
|
:rows="16"
|
|
:maxlength="5000"
|
|
:max-height="600"
|
|
@input="onInput"
|
|
/>
|
|
|
|
<div class="editor-footer">
|
|
<span class="word-count" :class="{ 'word-count--low': contentLength < 500 && contentLength > 0 }">
|
|
{{ contentHint }}
|
|
</span>
|
|
<span v-if="hasChanges" class="unsaved-hint">未保存的更改</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="loading" class="world-loading">
|
|
<div class="loading-pulse" style="height: 2rem; width: 40%; margin: 0 auto var(--space-lg)" />
|
|
<div class="loading-pulse" style="height: 1rem; width: 20%; margin: 0 auto var(--space-2xl)" />
|
|
<div class="loading-pulse" style="height: 300px; width: 100%" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.world-view {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
animation: fadeInUp var(--duration-normal) var(--ease-out-smooth) both;
|
|
}
|
|
|
|
.editor-container {
|
|
background: var(--paper-50);
|
|
border: 1px solid var(--paper-200);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
.editor-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
font-size: 0.85rem;
|
|
color: var(--ink-400);
|
|
margin-bottom: var(--space-lg);
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--paper-100);
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.editor-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: var(--space-sm);
|
|
}
|
|
|
|
.word-count {
|
|
font-size: 0.75rem;
|
|
color: var(--ink-300);
|
|
}
|
|
|
|
.word-count--low {
|
|
color: var(--vermilion);
|
|
}
|
|
|
|
.unsaved-hint {
|
|
font-size: 0.75rem;
|
|
color: var(--vermilion);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Loading */
|
|
.world-loading {
|
|
max-width: 800px;
|
|
margin: var(--space-3xl) auto;
|
|
}
|
|
|
|
.loading-pulse {
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--paper-100) 25%,
|
|
var(--paper-200) 50%,
|
|
var(--paper-100) 75%
|
|
);
|
|
background-size: 200% 100%;
|
|
border-radius: var(--radius-sm);
|
|
animation: shimmer 1.5s ease infinite;
|
|
}
|
|
</style>
|