feat: add avatar chat and knowledge workflow

This commit is contained in:
stefanfeng
2026-07-23 17:21:49 +08:00
parent 501f548bcc
commit 2d9f26a6f0
15 changed files with 3635 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
<template>
<div class="edit-avatar-page">
<!-- 顶部导航 -->
<header class="page-header">
<button class="back-btn" @click="goBack"></button>
<h1 class="page-title">形象微调编辑</h1>
<button class="save-btn" :disabled="loading || saving" @click="saveChanges">
{{ saving ? '保存中...' : '保存' }}
</button>
</header>
<div v-if="loading" class="status-banner">加载中...</div>
<div v-else-if="errorMsg" class="status-banner error">{{ errorMsg }}</div>
<!-- 头像预览 -->
<section class="photo-section">
<div class="photo-container">
<div class="photo-preview">
<img v-if="formData.photoUrl" :src="formData.photoUrl" alt="" class="photo-image" referrerpolicy="no-referrer" />
<div v-else class="photo-placeholder">🤖</div>
</div>
<span class="photo-hint">可直接修改下方头像链接</span>
</div>
</section>
<!-- 基本信息表单 -->
<section class="form-section">
<h3 class="section-title">基本信息</h3>
<div class="form-item">
<label class="form-label">分身名称</label>
<input
v-model="formData.name"
class="form-input"
placeholder="请输入分身名称"
/>
</div>
<div class="form-item">
<label class="form-label">显示名称</label>
<input
v-model="formData.displayName"
class="form-input"
placeholder="请输入显示名称"
/>
</div>
<div class="form-item">
<label class="form-label">分身描述</label>
<textarea
v-model="formData.description"
class="form-textarea"
placeholder="描述你的数字分身的功能和特点"
rows="3"
></textarea>
</div>
<div class="form-item">
<label class="form-label">头像链接</label>
<input
v-model="formData.photoUrl"
class="form-input"
placeholder="请输入头像图片 URL"
/>
</div>
<div class="form-item">
<label class="form-label">状态</label>
<div class="status-selector">
<button
class="status-option"
:class="{ active: formData.status === 'active' }"
@click="formData.status = 'active'"
>
活跃中
</button>
<button
class="status-option"
:class="{ active: formData.status === 'inactive' }"
@click="formData.status = 'inactive'"
>
未激活
</button>
</div>
</div>
</section>
<!-- 高级设置 -->
<section class="form-section">
<h3 class="section-title">高级设置</h3>
<div class="form-item">
<label class="form-label">回复风格</label>
<select v-model="formData.replyStyle" class="form-select">
<option value="professional">专业正式</option>
<option value="casual">轻松随意</option>
<option value="friendly">友好亲切</option>
</select>
</div>
<div class="form-item">
<div class="slider-head"><label class="form-label">创造力</label><b>{{ formData.creativity }}</b></div>
<input v-model.number="formData.creativity" type="range" min="0" max="100" class="range" />
</div>
<div class="form-item">
<div class="slider-head"><label class="form-label">严谨度</label><b>{{ formData.rigor }}</b></div>
<input v-model.number="formData.rigor" type="range" min="0" max="100" class="range" />
</div>
<div class="form-item">
<div class="slider-head"><label class="form-label">幽默感</label><b>{{ formData.humor }}</b></div>
<input v-model.number="formData.humor" type="range" min="0" max="100" class="range" />
</div>
<div class="form-item">
<label class="form-label">回复长度</label>
<div class="choice-row">
<button v-for="item in responseLengths" :key="item.value" type="button" class="choice-btn" :class="{ active: formData.responseLength === item.value }" @click="formData.responseLength = item.value">{{ item.label }}</button>
</div>
</div>
<div class="form-item">
<label class="form-label">系统提示词</label>
<textarea v-model="formData.systemPrompt" class="form-textarea" rows="4" placeholder="给分身的额外指令,例如语气、禁用内容或回答边界"></textarea>
</div>
<div class="form-item">
<label class="form-label">自动回复</label>
<div class="toggle-container">
<span class="toggle-label">{{ formData.autoReply ? '开启' : '关闭' }}</span>
<button
class="toggle-btn"
:class="{ active: formData.autoReply }"
@click="formData.autoReply = !formData.autoReply"
>
<span class="toggle-dot"></span>
</button>
</div>
</div>
</section>
<!-- 危险操作 -->
<section class="danger-section">
<button class="delete-btn" :disabled="loading || deleting" @click="deleteAvatar">
{{ deleting ? '删除中...' : '删除数字分身' }}
</button>
</section>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { deleteAvatar as apiDeleteAvatar, getAvatarDetail, updateAvatar } from '@/api'
import { useAvatarStore } from '@/store/avatar'
import { buildAvatarUpdatePayload, normalizeAvatarEditForm } from '@/utils/avatar-page-data.js'
const router = useRouter()
const route = useRoute()
const avatarStore = useAvatarStore()
const avatarId = route.params.id as string
// 表单数据
const formData = reactive({
name: '',
displayName: '',
description: '',
status: 'active' as 'active' | 'inactive',
photoUrl: '',
replyStyle: 'professional',
creativity: 50,
rigor: 50,
humor: 30,
responseLength: 'medium',
systemPrompt: '',
autoReply: true
})
const responseLengths = [
{ value: 'short', label: '简短' },
{ value: 'medium', label: '适中' },
{ value: 'long', label: '详细' }
]
const loading = ref(true)
const saving = ref(false)
const deleting = ref(false)
const errorMsg = ref('')
const loadAvatar = async () => {
loading.value = true
errorMsg.value = ''
try {
const avatar: any = await getAvatarDetail(avatarId)
Object.assign(formData, normalizeAvatarEditForm(avatar))
} catch (e: any) {
errorMsg.value = e?.message || '分身加载失败'
} finally {
loading.value = false
}
}
// 保存修改
const saveChanges = async () => {
if (loading.value || saving.value) return
if (!formData.name.trim()) {
errorMsg.value = '请输入分身名称'
return
}
saving.value = true
errorMsg.value = ''
try {
await updateAvatar(avatarId, buildAvatarUpdatePayload(formData))
await avatarStore.loadAvatars()
router.replace('/avatar/manage')
} catch (e: any) {
errorMsg.value = e?.message || '保存失败,请重试'
} finally {
saving.value = false
}
}
// 删除分身
const deleteAvatar = async () => {
if (loading.value || deleting.value) return
if (confirm('确定要删除这个数字分身吗?此操作不可恢复。')) {
deleting.value = true
errorMsg.value = ''
try {
await apiDeleteAvatar(avatarId)
await avatarStore.loadAvatars()
router.replace('/avatar/manage')
} catch (e: any) {
errorMsg.value = e?.message || '删除失败,请重试'
} finally {
deleting.value = false
}
}
}
// 返回
const goBack = () => {
router.back()
}
onMounted(async () => {
await loadAvatar()
})
</script>
<style scoped>
.edit-avatar-page {
min-height: 100vh;
background: #F8F9FA;
padding-bottom: 40px;
}
/* 顶部导航 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: white;
border-bottom: 1px solid #EDEEF1;
}
.back-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
color: #18191C;
}
.page-title {
font-size: 17px;
font-weight: 600;
margin: 0;
color: #18191C;
}
.status-banner {
margin: 16px 20px 0;
padding: 12px 14px;
border-radius: 10px;
background: #FFF7ED;
color: #9A3412;
font-size: 14px;
}
.status-banner.error {
background: #FEF2F2;
color: #B91C1C;
}
.save-btn {
background: #F97316;
color: white;
border: none;
padding: 8px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
/* 头像上传 */
.photo-section {
padding: 30px 20px;
display: flex;
justify-content: center;
}
.photo-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
cursor: pointer;
}
.photo-preview {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3);
}
.photo-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-placeholder {
font-size: 48px;
}
.photo-hint {
font-size: 13px;
color: #F97316;
font-weight: 500;
}
/* 表单区域 */
.form-section {
padding: 20px;
margin-bottom: 12px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px;
color: #18191C;
}
.form-item {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #18191C;
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #EDEEF1;
border-radius: 10px;
font-size: 15px;
color: #18191C;
background: white;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: #F97316;
}
.form-textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid #EDEEF1;
border-radius: 10px;
font-size: 15px;
color: #18191C;
background: white;
resize: vertical;
font-family: inherit;
transition: border-color 0.2s;
}
.form-textarea:focus {
outline: none;
border-color: #F97316;
}
.form-select {
width: 100%;
padding: 12px 16px;
border: 1px solid #EDEEF1;
border-radius: 10px;
font-size: 15px;
color: #18191C;
background: white;
cursor: pointer;
}
.slider-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.slider-head .form-label { margin-bottom: 0; }
.slider-head b { color: #F97316; font-size: 14px; }
.range { width: 100%; accent-color: #F97316; }
.choice-row { display: flex; gap: 8px; }
.choice-btn {
flex: 1; padding: 10px 8px; border: 1px solid #EDEEF1; border-radius: 10px;
background: white; color: #6B7280; cursor: pointer;
}
.choice-btn.active { border-color: #F97316; color: #F97316; background: #FFF0E6; }
/* 状态选择器 */
.status-selector {
display: flex;
gap: 12px;
}
.status-option {
flex: 1;
padding: 12px;
border: 2px solid #EDEEF1;
border-radius: 10px;
background: white;
font-size: 14px;
color: #9398AE;
cursor: pointer;
transition: all 0.2s;
}
.status-option.active {
border-color: #F97316;
color: #F97316;
background: #FFF0E6;
}
/* 开关切换 */
.toggle-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: white;
border-radius: 10px;
border: 1px solid #EDEEF1;
}
.toggle-label {
font-size: 15px;
color: #18191C;
}
.toggle-btn {
width: 48px;
height: 28px;
border-radius: 14px;
border: none;
background: #D1D5DB;
cursor: pointer;
position: relative;
transition: background 0.3s;
padding: 0;
}
.toggle-btn.active {
background: #F97316;
}
.toggle-dot {
position: absolute;
top: 2px;
left: 2px;
width: 24px;
height: 24px;
border-radius: 50%;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.toggle-btn.active .toggle-dot {
transform: translateX(20px);
}
/* 危险操作 */
.danger-section {
padding: 20px;
}
.delete-btn {
width: 100%;
padding: 14px;
background: white;
color: #EF4444;
border: 2px solid #EF4444;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.delete-btn:hover {
background: #EF4444;
color: white;
}
</style>