Files
yaoayanui/src/views/AvatarView.vue
fengchuanhn@gmail.com cdbf605205 1
2026-05-18 15:11:59 +08:00

186 lines
5.4 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.
<script setup>
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useAvatarStore } from "../stores/avatar.js";
import AvatarEditDialog from "../components/avatar/AvatarEditDialog.vue";
import { localVideoToPlayableUrl } from "../services/localVideo.js";
const avatarStore = useAvatarStore();
const { templates, loading } = storeToRefs(avatarStore);
const editDialogRef = ref(null);
const feedback = ref({ severity: "", message: "" });
const editingId = ref(null);
const editingName = ref("");
onMounted(() => {
avatarStore.refresh();
});
function showFeedback(severity, message) {
feedback.value = { severity, message };
}
function videoSrc(item) {
if (!item?.filePath) return "";
try {
return localVideoToPlayableUrl(item.filePath);
} catch {
return "";
}
}
function onAdd() {
feedback.value = { severity: "", message: "" };
editDialogRef.value?.openAdd();
}
function onCloud() {
showFeedback("warn", "云端形象功能即将上线");
}
function startRename(item) {
editingId.value = item.id;
editingName.value = item.name;
}
function cancelRename() {
editingId.value = null;
editingName.value = "";
}
async function commitRename(item) {
const result = await avatarStore.renameTemplate(item.id, editingName.value);
if (!result.ok) {
showFeedback("error", result.message || "重命名失败");
return;
}
cancelRename();
}
async function onDelete(item) {
if (!window.confirm(`确认删除形象「${item.name}」?`)) return;
await avatarStore.removeTemplate(item);
showFeedback("success", "已删除");
}
function onSaved() {
showFeedback("success", "形象已添加");
}
</script>
<template>
<div class="avatar-page custom-scrollbar h-full overflow-y-auto p-5">
<div class="mb-4 flex flex-wrap items-end justify-between gap-3">
<div>
<h1 class="text-2xl font-bold text-slate-100">数字人形象</h1>
<p class="mt-1 text-sm text-slate-500">管理多个数字人形象</p>
</div>
<div class="flex flex-wrap gap-2">
<Button
label="云端形象"
size="small"
severity="secondary"
outlined
icon="pi pi-cloud"
@click="onCloud"
/>
<Button label="添加" size="small" icon="pi pi-plus" @click="onAdd" />
</div>
</div>
<p
v-if="feedback.message"
class="mb-3 text-sm"
:class="{
'text-red-400': feedback.severity === 'error',
'text-amber-400': feedback.severity === 'warn',
'text-emerald-400': feedback.severity === 'success',
}"
>
{{ feedback.message }}
</p>
<p v-if="loading" class="py-12 text-center text-sm text-slate-500">加载中...</p>
<p
v-else-if="templates.length === 0"
class="rounded-xl border border-white/10 bg-white/5 py-16 text-center text-sm text-slate-500"
>
暂无形象点击添加上传视频
</p>
<div v-else class="-mx-2 flex flex-wrap">
<div
v-for="item in templates"
:key="item.id"
class="w-full p-2 md:w-1/2 xl:w-1/3"
>
<div
class="rounded-xl border border-white/10 bg-white/5 p-4 shadow-sm transition-shadow hover:border-purple-500/40 hover:shadow-md"
>
<div class="mb-3 flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<div
v-if="editingId !== item.id"
class="inline-flex max-w-full items-center rounded-full bg-blue-500/15 px-2 py-1"
>
<span
class="cursor-pointer truncate text-sm font-medium text-slate-100"
:title="item.name"
@click="startRename(item)"
>
{{ item.name }}
</span>
<button
type="button"
class="ml-2 shrink-0 text-xs text-slate-500 hover:text-blue-400"
aria-label="重命名"
@click="startRename(item)"
>
<i class="pi pi-pencil" />
</button>
</div>
<div v-else class="flex gap-1">
<InputText v-model="editingName" size="small" class="flex-1" />
<Button
icon="pi pi-check"
size="small"
severity="success"
aria-label="确认"
@click="commitRename(item)"
/>
<Button
icon="pi pi-times"
size="small"
severity="secondary"
aria-label="取消"
@click="cancelRename"
/>
</div>
</div>
<Button
icon="pi pi-trash"
size="small"
severity="danger"
text
aria-label="删除"
@click="onDelete(item)"
/>
</div>
<div class="overflow-hidden rounded-lg bg-black p-1">
<video
v-if="videoSrc(item)"
:src="videoSrc(item)"
controls
class="aspect-video w-full bg-black object-contain"
/>
<p v-else class="py-12 text-center text-xs text-slate-500">无法预览视频</p>
</div>
</div>
</div>
</div>
<AvatarEditDialog ref="editDialogRef" @saved="onSaved" />
</div>
</template>