新增前后端AI聊天模块,包括流式对话、配置管理和UI组件优化。

This commit is contained in:
2026-03-19 01:35:13 +08:00
commit ba6ceb94c8
66 changed files with 8631 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { createNovel, fetchNovel, updateNovel } from '@/api/novels'
import { useToast } from '@/composables/useToast'
import BaseInput from '@/components/ui/BaseInput.vue'
import BaseTextarea from '@/components/ui/BaseTextarea.vue'
import BaseButton from '@/components/ui/BaseButton.vue'
const route = useRoute()
const router = useRouter()
const toast = useToast()
const isEdit = computed(() => route.name === 'novel-edit')
const novelId = computed(() => Number(route.params.id))
const title = ref('')
const description = ref('')
const saving = ref(false)
const errors = ref<{ title?: string }>({})
async function load() {
if (!isEdit.value) return
try {
const novel = await fetchNovel(novelId.value)
title.value = novel.title
description.value = novel.description
} catch {
toast.error('作品不存在')
router.push({ name: 'home' })
}
}
function validate(): boolean {
errors.value = {}
if (!title.value.trim()) {
errors.value.title = '请输入作品名称'
return false
}
if (title.value.length > 200) {
errors.value.title = '名称不能超过200字'
return false
}
return true
}
async function handleSubmit() {
if (!validate()) return
saving.value = true
try {
if (isEdit.value) {
await updateNovel(novelId.value, {
title: title.value.trim(),
description: description.value.trim(),
})
toast.success('保存成功')
router.push({ name: 'novel-detail', params: { id: novelId.value } })
} else {
const novel = await createNovel({
title: title.value.trim(),
description: description.value.trim(),
})
toast.success('创建成功')
router.push({ name: 'novel-detail', params: { id: novel.id } })
}
} catch {
toast.error(isEdit.value ? '保存失败' : '创建失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<div class="form-view">
<div class="form-header">
<BaseButton variant="ghost" @click="router.back()">
&larr; 返回
</BaseButton>
</div>
<div class="form-container">
<h1 class="form-title">{{ isEdit ? '编辑作品' : '新建作品' }}</h1>
<form @submit.prevent="handleSubmit" class="novel-form">
<BaseInput
v-model="title"
label="作品名称"
:error="errors.title"
/>
<BaseTextarea
v-model="description"
label="作品简介"
/>
<div class="form-actions">
<BaseButton variant="ghost" type="button" @click="router.back()">
取消
</BaseButton>
<BaseButton variant="primary" type="submit" :loading="saving">
{{ isEdit ? '保存修改' : '开始创作' }}
</BaseButton>
</div>
</form>
</div>
</div>
</template>
<style scoped>
.form-view {
max-width: 600px;
margin: 0 auto;
animation: fadeInUp var(--duration-normal) var(--ease-out-smooth) both;
}
.form-header {
margin-bottom: var(--space-lg);
}
.form-container {
background: var(--paper-50);
border: 1px solid var(--paper-200);
border-radius: var(--radius-lg);
padding: var(--space-2xl);
box-shadow: var(--shadow-sm);
}
.form-title {
font-family: var(--font-display);
font-size: 1.6rem;
color: var(--ink-900);
margin-bottom: var(--space-2xl);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-md);
margin-top: var(--space-xl);
}
</style>