|
@@ -0,0 +1,211 @@
|
|
|
+<template>
|
|
|
+ <ui-group-option class="sign-guide" :class="{ search }">
|
|
|
+ <div class="info">
|
|
|
+ <div class="guide-cover">
|
|
|
+ <img :src="getResource(getFileUrl(guide.cover))" />
|
|
|
+ <ui-icon
|
|
|
+ type="preview"
|
|
|
+ class="icon"
|
|
|
+ ctrl
|
|
|
+ @click="flyPlayGuide(guide)"
|
|
|
+ v-if="paths.length"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <p>{{ guide.title }}</p>
|
|
|
+ <!-- <ui-input
|
|
|
+ class="view-title-input"
|
|
|
+ type="text"
|
|
|
+ :modelValue="guide.title"
|
|
|
+ :maxlength="15"
|
|
|
+ @update:modelValue="(title: string) => $emit('updateTitle', title.trim())"
|
|
|
+ v-show="isEditTitle"
|
|
|
+ ref="inputRef"
|
|
|
+ height="28px"
|
|
|
+ /> -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="actions" v-if="edit">
|
|
|
+ <ui-more
|
|
|
+ :options="menus"
|
|
|
+ style="margin-left: 20px"
|
|
|
+ @click="(action: keyof typeof actions) => actions[action]()"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </ui-group-option>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script setup lang="ts">
|
|
|
+ import { Guide, getGuidePaths } from "@/store";
|
|
|
+ import { getFileUrl, saveAs } from "@/utils";
|
|
|
+ import { getResource } from "@/env";
|
|
|
+ import { computed, watchEffect, nextTick, ref } from "vue";
|
|
|
+ import { playSceneGuide, isScenePlayIng, pauseSceneGuide } from "@/sdk";
|
|
|
+ import { VideoRecorder } from "simaqcore";
|
|
|
+ import useFocus from "bill/hook/useFocus";
|
|
|
+ import { Message } from "bill/expose-common";
|
|
|
+ import { flyPlayGuide } from "@/hook/use-fly";
|
|
|
+
|
|
|
+ const props = withDefaults(
|
|
|
+ defineProps<{ guide: Guide; edit?: boolean; search?: boolean }>(),
|
|
|
+ {
|
|
|
+ edit: true,
|
|
|
+ search: false,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const inputRef = ref();
|
|
|
+ const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
|
|
|
+ watchEffect(() => {
|
|
|
+ if (!isEditTitle.value && !props.guide.title.length) {
|
|
|
+ isEditTitle.value = true;
|
|
|
+ Message.warning("导览名称不可为空");
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const emit = defineEmits<{
|
|
|
+ (e: "updateTitle", t: string): void;
|
|
|
+ (e: "delete"): void;
|
|
|
+ (e: "play"): void;
|
|
|
+ (e: "edit"): void;
|
|
|
+ (e: "record"): void;
|
|
|
+ }>();
|
|
|
+
|
|
|
+// const menus = [
|
|
|
+// { label: "重命名", value: "editTitle" },
|
|
|
+// { label: "编辑", value: "edit" },
|
|
|
+// { label: "删除", value: "delete" },
|
|
|
+// ];
|
|
|
+ const menus = [
|
|
|
+ { label: "编辑", value: "edit" },
|
|
|
+ { label: "录制", value: "record" },
|
|
|
+ ];
|
|
|
+ const actions = {
|
|
|
+ edit: () => emit("edit"),
|
|
|
+ record: () => emit("record"),
|
|
|
+ delete: () => emit("delete"),
|
|
|
+ download: () => {
|
|
|
+ const config: any = {
|
|
|
+ // uploadUrl: '',
|
|
|
+ // resolution: '4k',
|
|
|
+ // autoDownload: false,
|
|
|
+ // systemAudio: true,
|
|
|
+ // debug: true,
|
|
|
+ resolution: "4k",
|
|
|
+ autoDownload: false,
|
|
|
+ platform: "canvas",
|
|
|
+
|
|
|
+ config: {
|
|
|
+ frameRate: 60,
|
|
|
+ canvasId: ".scene-canvas > canvas",
|
|
|
+ },
|
|
|
+ disbaledAudio: false,
|
|
|
+ systemAudio: false,
|
|
|
+ debug: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ const videoRecorder = new VideoRecorder(config);
|
|
|
+ videoRecorder.startRecord();
|
|
|
+
|
|
|
+ let stopWatch: () => void;
|
|
|
+ const stopRecord = () => {
|
|
|
+ stopWatch && stopWatch();
|
|
|
+ pauseSceneGuide();
|
|
|
+ };
|
|
|
+
|
|
|
+ videoRecorder.on("record", (blob) => {
|
|
|
+ saveAs(
|
|
|
+ new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }),
|
|
|
+ props.guide.title + ".mp4"
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ videoRecorder.off("*");
|
|
|
+ videoRecorder.on("startRecord", () => {
|
|
|
+ flyPlayGuide(props.guide);
|
|
|
+ stopWatch = watchEffect(() => {
|
|
|
+ if (!isScenePlayIng.value) {
|
|
|
+ videoRecorder.endRecord();
|
|
|
+ nextTick(stopWatch);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ videoRecorder.on("cancelRecord", stopRecord);
|
|
|
+ videoRecorder.on("endRecord", stopRecord);
|
|
|
+ },
|
|
|
+ };
|
|
|
+ const paths = computed(() => getGuidePaths(props.guide));
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <style lang="scss" scoped>
|
|
|
+ .sign-guide {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 0;
|
|
|
+ border-bottom: 1px solid var(--colors-border-color);
|
|
|
+
|
|
|
+ &.search {
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .guide-cover {
|
|
|
+ position: relative;
|
|
|
+ &::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 1;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: rgba(255, 255, 255, 0.6);
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ div:not(.view-title-input) {
|
|
|
+ margin-left: 10px;
|
|
|
+
|
|
|
+ p {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .actions {
|
|
|
+ flex: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|
|
|
+ <style>
|
|
|
+ .view-title-input.ui-input .text.suffix input {
|
|
|
+ padding-right: 50px;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|