feat: complete grounded digital avatar chat experience
This commit is contained in:
@@ -44,22 +44,21 @@
|
||||
|
||||
<div v-if="avatars.length" class="avatar-list">
|
||||
<div class="avatar-card" v-for="a in avatars" :key="a.id">
|
||||
<div class="avatar-photo">
|
||||
<img v-if="a.photoUrl" :src="a.photoUrl" alt="" referrerpolicy="no-referrer" class="avatar-img" />
|
||||
<div v-else class="avatar-placeholder">{{ a.emoji || '🤖' }}</div>
|
||||
</div>
|
||||
<div class="avatar-details">
|
||||
<h2 class="avatar-name">{{ a.displayName || a.name }}</h2>
|
||||
<p class="avatar-desc">{{ a.description || '暂无描述' }}</p>
|
||||
<div class="avatar-status">
|
||||
<span class="status-dot" :class="a.status"></span>
|
||||
<span class="status-text">{{ statusText(a.status) }}</span>
|
||||
<div class="avatar-card-main">
|
||||
<div class="avatar-photo">
|
||||
<img v-if="a.photoUrl" :src="a.photoUrl" alt="" referrerpolicy="no-referrer" class="avatar-img" />
|
||||
<div v-else class="avatar-placeholder">{{ a.emoji || '🤖' }}</div>
|
||||
</div>
|
||||
<div class="avatar-details">
|
||||
<div class="avatar-name-row"><h2 class="avatar-name">{{ a.displayName || a.name }}</h2><span class="avatar-status"><i class="status-dot" :class="a.status"></i>{{ statusText(a.status) }}</span></div>
|
||||
<p class="avatar-desc">{{ a.description || '暂无描述' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar-actions">
|
||||
<button class="chat-link" @click="goToChat(a.id)">对话</button>
|
||||
<button class="chat-link" @click="goToChat(a.id)"><span>💬</span> 对话</button>
|
||||
<button class="share-link" @click="shareAvatar(a)"><span>↗</span> 分享</button>
|
||||
<button class="edit-link" @click="goToEdit(a.id)">编辑</button>
|
||||
<button class="del-link" @click="askDelete(a)">删除</button>
|
||||
<button class="del-link" @click="askDelete(a)" aria-label="删除分身">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,6 +70,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="shareToast" class="share-toast">{{ shareToast }}</p>
|
||||
|
||||
<!-- 分身工具入口 -->
|
||||
<section class="tools-section">
|
||||
<h3 class="section-title">分身工具</h3>
|
||||
@@ -161,6 +162,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAvatarStore } from '@/store/avatar'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { createAvatarShareLink } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const avatarStore = useAvatarStore()
|
||||
@@ -177,6 +179,7 @@ const avatars = computed(() => avatarStore.avatars)
|
||||
const showDelete = ref(false)
|
||||
const pendingDelete = ref<any>(null)
|
||||
const deleting = ref(false)
|
||||
const shareToast = ref('')
|
||||
|
||||
const activities = ref<Array<{ id: string; type: string; text: string; createdAt: string }>>([
|
||||
{ id: '1', type: 'create', text: '数字分身创建成功', createdAt: new Date(Date.now() - 86400000).toISOString() },
|
||||
@@ -268,6 +271,38 @@ const goToChat = (id: string) => {
|
||||
router.push(`/avatar/chat/${id}`)
|
||||
}
|
||||
|
||||
const showShareToast = (message: string) => {
|
||||
shareToast.value = message
|
||||
window.setTimeout(() => { shareToast.value = '' }, 2400)
|
||||
}
|
||||
|
||||
const copyShareLink = async (link: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(link)
|
||||
showShareToast('公开对话链接已复制')
|
||||
return
|
||||
}
|
||||
window.prompt('复制公开对话链接', link)
|
||||
}
|
||||
|
||||
const shareAvatar = async (avatar: any) => {
|
||||
try {
|
||||
const result: any = await createAvatarShareLink(avatar.id)
|
||||
const token = result?.shareToken
|
||||
if (!token) throw new Error('未能生成分享链接')
|
||||
const link = `${window.location.origin}${window.location.pathname}#/share/${token}`
|
||||
const title = `${avatar.displayName || avatar.name},和我聊聊`
|
||||
if (navigator.share) {
|
||||
await navigator.share({ title, text: avatar.description || '点击和我聊聊', url: link })
|
||||
showShareToast('已唤起分享')
|
||||
return
|
||||
}
|
||||
await copyShareLink(link)
|
||||
} catch (error: any) {
|
||||
if (error?.name !== 'AbortError') showShareToast(error?.message || '分享链接生成失败')
|
||||
}
|
||||
}
|
||||
|
||||
const goToAvatarCard = () => {
|
||||
router.push('/avatar/card')
|
||||
}
|
||||
@@ -481,22 +516,21 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.avatar-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #F4E5D9;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 22px rgba(112, 62, 22, .07);
|
||||
}
|
||||
.avatar-card-main { display: flex; align-items: center; gap: 14px; }
|
||||
|
||||
.avatar-photo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 66px;
|
||||
height: 66px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #F3F4F6;
|
||||
background: #FFF0E6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -516,9 +550,12 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.avatar-name-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
|
||||
.avatar-name {
|
||||
font-size: 16px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px;
|
||||
color: #18191C;
|
||||
@@ -527,16 +564,19 @@ onMounted(() => {
|
||||
.avatar-desc {
|
||||
font-size: 13px;
|
||||
color: #9398AE;
|
||||
margin: 0 0 8px;
|
||||
margin: 5px 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.avatar-status {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
gap: 4px;
|
||||
color: #75809A;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -557,30 +597,28 @@ onMounted(() => {
|
||||
background: #F59E0B;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #9398AE;
|
||||
}
|
||||
|
||||
.avatar-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.chat-link {
|
||||
padding: 7px 14px;
|
||||
background: #FFF0E6;
|
||||
color: #F97316;
|
||||
flex: 1;
|
||||
padding: 10px 8px;
|
||||
background: linear-gradient(135deg, #F97316, #FB923C);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-link span, .share-link span { margin-right: 3px; }
|
||||
.share-link { flex: 1; padding: 10px 8px; border: 1px solid #FFD5AF; border-radius: 10px; color: #C15F18; background: #FFF8F1; font-size: 13px; cursor: pointer; }
|
||||
|
||||
.edit-link {
|
||||
padding: 7px 14px;
|
||||
padding: 10px 10px;
|
||||
background: #F3F4F6;
|
||||
color: #6B7280;
|
||||
border: none;
|
||||
@@ -595,9 +633,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.del-link {
|
||||
padding: 7px 14px;
|
||||
background: #FEF2F2;
|
||||
color: #EF4444;
|
||||
padding: 10px 2px;
|
||||
background: transparent;
|
||||
color: #B6BCC8;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
@@ -605,6 +643,8 @@ onMounted(() => {
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.share-toast { position: fixed; left: 50%; bottom: 92px; z-index: 300; max-width: calc(100vw - 48px); transform: translateX(-50%); padding: 10px 14px; border-radius: 10px; color: white; background: rgba(39, 32, 28, .88); font-size: 13px; text-align: center; }
|
||||
|
||||
.del-link:hover {
|
||||
background: #FEE2E2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user