This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 15:11:59 +08:00
parent 4771397301
commit cdbf605205
25 changed files with 1497 additions and 112 deletions

185
src/views/AvatarView.vue Normal file
View File

@@ -0,0 +1,185 @@
<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>