1
This commit is contained in:
151
src/components/avatar/AvatarEditDialog.vue
Normal file
151
src/components/avatar/AvatarEditDialog.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useAvatarStore } from "../../stores/avatar.js";
|
||||
import { pickVideoFile, importAvatarVideo } from "../../services/avatarVideo.js";
|
||||
import { localVideoToPlayableUrl } from "../../services/localVideo.js";
|
||||
import { avatarNameExists } from "../../services/avatarDb.js";
|
||||
|
||||
const emit = defineEmits(["saved"]);
|
||||
|
||||
const avatarStore = useAvatarStore();
|
||||
|
||||
const visible = ref(false);
|
||||
const name = ref("");
|
||||
const localSourcePath = ref("");
|
||||
const previewSrc = ref("");
|
||||
const saving = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
function resetForm() {
|
||||
name.value = "";
|
||||
localSourcePath.value = "";
|
||||
previewSrc.value = "";
|
||||
errorMessage.value = "";
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
resetForm();
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
async function onPickVideo() {
|
||||
errorMessage.value = "";
|
||||
try {
|
||||
const path = await pickVideoFile();
|
||||
if (!path) return;
|
||||
localSourcePath.value = path;
|
||||
previewSrc.value = localVideoToPlayableUrl(path);
|
||||
} catch (err) {
|
||||
errorMessage.value =
|
||||
err instanceof Error ? err.message : String(err || "选择文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
errorMessage.value = "";
|
||||
const title = name.value.trim();
|
||||
if (!title) {
|
||||
errorMessage.value = "请输入名称";
|
||||
return;
|
||||
}
|
||||
if (await avatarNameExists(title)) {
|
||||
errorMessage.value = "名称重复";
|
||||
return;
|
||||
}
|
||||
if (!localSourcePath.value) {
|
||||
errorMessage.value = "请选择视频";
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const savedPath = await importAvatarVideo(localSourcePath.value);
|
||||
await avatarStore.addTemplate({ name: title, filePath: savedPath });
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
} catch (err) {
|
||||
errorMessage.value =
|
||||
err instanceof Error ? err.message : String(err || "保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openAdd });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
modal
|
||||
header="添加视频形象"
|
||||
:style="{ width: '720px' }"
|
||||
:draggable="false"
|
||||
class="avatar-edit-dialog"
|
||||
>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="dashboard-field-label mb-1">
|
||||
名称 <span class="text-red-400">*</span>
|
||||
</div>
|
||||
<InputText
|
||||
v-model="name"
|
||||
class="w-full"
|
||||
size="small"
|
||||
placeholder="例如:主播小美"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dashboard-field-label mb-1">视频 <span class="text-red-400">*</span></div>
|
||||
<div
|
||||
v-if="!previewSrc"
|
||||
class="flex h-48 cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-white/20 bg-white/5 text-center text-sm text-slate-400 transition-colors hover:border-blue-400/40 hover:bg-white/10"
|
||||
@click="onPickVideo"
|
||||
>
|
||||
<i class="pi pi-video mb-2 text-2xl text-slate-500" />
|
||||
<span>点击选择视频文件</span>
|
||||
<span class="mt-1 text-xs">支持 mp4 / mov / webm 等</span>
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div class="overflow-hidden rounded-lg bg-black p-1">
|
||||
<video :src="previewSrc" controls class="max-h-48 w-full" />
|
||||
</div>
|
||||
<Button
|
||||
label="重新选择"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="w-full"
|
||||
@click="onPickVideo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="errorMessage" class="text-sm text-red-400">{{ errorMessage }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
<div class="mb-2 text-base font-medium text-slate-200">形象示例</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div class="rounded-lg bg-emerald-500/10 p-2 text-emerald-400">正脸自拍</div>
|
||||
<div class="rounded-lg bg-emerald-500/10 p-2 text-emerald-400">可张口闭口</div>
|
||||
<div class="rounded-lg bg-red-500/10 p-2 text-red-400">面部有干扰</div>
|
||||
</div>
|
||||
<div class="dashboard-field-label mb-2">形象视频要求</div>
|
||||
<ul class="space-y-1.5 text-xs leading-relaxed">
|
||||
<li>视频时长建议 10~30 秒,格式 MP4,分辨率 1080p~4K</li>
|
||||
<li>每一帧需正面露脸、无遮挡,且仅出现同一人脸</li>
|
||||
<li>建议闭口或微微张口,张口幅度不宜过大</li>
|
||||
<li>可正常语气循环说「一二三四五六七八九」等文字</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" severity="secondary" @click="onCancel" />
|
||||
<Button label="保存" :loading="saving" @click="onSave" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user