1
This commit is contained in:
185
src/views/AvatarView.vue
Normal file
185
src/views/AvatarView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user