Files
huihuiSquare/digital-avatar-app/src/views/AvatarEdit.vue
2026-07-23 17:29:29 +08:00

534 lines
12 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>
<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>