186 lines
5.4 KiB
Vue
186 lines
5.4 KiB
Vue
<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>
|