新增角色管理、世界观设定、章节管理和AI工具调用功能

- 角色管理:CRUD API + 卡片网格UI,每个角色约200字描述
- 世界观设定:单篇长文编辑器,建议500字以上
- 章节管理:CRUD + 排序 API,左右分栏编辑器,单章5000字上限
- AI工具调用:支持工具审批流程和结构化工具调用
- 小说详情页新增章节、角色、世界观入口按钮

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:58:17 +08:00
parent 580dc8be52
commit 20c759150a
34 changed files with 3495 additions and 127 deletions

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { Chapter } from '@/types/chapter'
import BaseModal from '@/components/ui/BaseModal.vue'
import BaseInput from '@/components/ui/BaseInput.vue'
import BaseButton from '@/components/ui/BaseButton.vue'
const props = defineProps<{
show: boolean
chapter: Chapter | null
saving: boolean
}>()
const emit = defineEmits<{
submit: [data: { title: string }]
close: []
}>()
const title = ref('')
const isEdit = computed(() => !!props.chapter)
const modalTitle = computed(() => isEdit.value ? '重命名章节' : '新建章节')
const canSubmit = computed(() => title.value.trim().length > 0 && !props.saving)
watch(() => props.show, (val) => {
if (val) {
title.value = props.chapter?.title ?? ''
}
})
function handleSubmit() {
if (!canSubmit.value) return
emit('submit', { title: title.value.trim() })
}
</script>
<template>
<BaseModal :show="show" @close="emit('close')">
<form class="chapter-form" @submit.prevent="handleSubmit">
<h2 class="form-title">{{ modalTitle }}</h2>
<BaseInput
v-model="title"
label="章节标题"
:maxlength="200"
required
/>
<div class="form-actions">
<BaseButton variant="secondary" type="button" @click="emit('close')">取消</BaseButton>
<BaseButton variant="primary" type="submit" :loading="saving" :disabled="!canSubmit">
{{ isEdit ? '保存' : '创建' }}
</BaseButton>
</div>
</form>
</BaseModal>
</template>
<style scoped>
.chapter-form {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.form-title {
font-family: var(--font-display);
font-size: 1.4rem;
font-weight: 700;
color: var(--ink-900);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
padding-top: var(--space-sm);
}
</style>