sign.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <ui-group-option class="sign-guide">
  3. <div class="info">
  4. <div class="guide-cover">
  5. <img :src="getResource(getFileUrl(guide.cover))" />
  6. <ui-icon
  7. type="preview"
  8. class="icon"
  9. ctrl
  10. @click="playSceneGuide(paths, undefined, true)"
  11. v-if="paths.length"
  12. />
  13. </div>
  14. <div>
  15. <p>{{ guide.title }}</p>
  16. </div>
  17. </div>
  18. <div class="actions" v-if="edit">
  19. <ui-more
  20. :options="menus"
  21. style="margin-left: 20px"
  22. @click="(action: keyof typeof actions) => actions[action]()"
  23. />
  24. </div>
  25. </ui-group-option>
  26. <Teleport to="body">
  27. <div class="edit-add-type" v-if="downloading">
  28. <div class="edit-hot-item">
  29. <h3 class="edit-title">设置视频参数</h3>
  30. <ui-input
  31. require
  32. class="input"
  33. :options="[
  34. { value: '1080p', label: '1080p' },
  35. { value: '2k', label: '2k' },
  36. { value: '4k', label: '4k' },
  37. ]"
  38. width="100%"
  39. placeholder="设置分辨率"
  40. type="select"
  41. v-model="videoConfig.resolution"
  42. maxlength="15"
  43. />
  44. <ui-input
  45. require
  46. class="input"
  47. :options="[
  48. { value: 30, label: '30' },
  49. { value: 60, label: '60' },
  50. { value: 90, label: '90' },
  51. ]"
  52. width="100%"
  53. placeholder="设置帧率"
  54. type="select"
  55. v-model="videoConfig.frameRate"
  56. maxlength="15"
  57. />
  58. <div>
  59. <span>预计渲染时间:</span>
  60. <span>{{ time }}s</span>
  61. </div>
  62. <div class="edit-hot">
  63. <a @click="() => (downloading = false)">
  64. <ui-icon type="nav-edit" />
  65. 确定
  66. </a>
  67. </div>
  68. </div>
  69. </div>
  70. </Teleport>
  71. </template>
  72. <script setup lang="ts">
  73. import { Guide, getGuidePaths } from "@/store";
  74. import { getFileUrl, saveAs } from "@/utils";
  75. import { getResource } from "@/env";
  76. import { computed, watchEffect, nextTick, ref } from "vue";
  77. import { playSceneGuide, isScenePlayIng, pauseSceneGuide } from "@/sdk";
  78. import { VideoRecorder } from "@simaq/core";
  79. const props = withDefaults(defineProps<{ guide: Guide; edit?: boolean }>(), {
  80. edit: true,
  81. });
  82. const emit = defineEmits<{
  83. (e: "delete"): void;
  84. (e: "play"): void;
  85. (e: "edit"): void;
  86. }>();
  87. const menus = [
  88. { label: "编辑", value: "edit" },
  89. { label: "下载", value: "download" },
  90. { label: "删除", value: "delete" },
  91. ];
  92. const downloading = ref(false);
  93. const videoConfig = ref({
  94. resolution: "1080p",
  95. frameRate: 60,
  96. });
  97. const actions = {
  98. edit: () => emit("edit"),
  99. delete: () => emit("delete"),
  100. download: async () => {
  101. downloading.value = true;
  102. await new Promise<void>((resolve) => {
  103. const stop = watchEffect(() => {
  104. if (downloading.value === false) {
  105. stop();
  106. resolve();
  107. }
  108. });
  109. });
  110. const config: any = {
  111. // uploadUrl: '',
  112. // resolution: '1080p' | '2k' | '4k';
  113. // frameRate: [30, 60, 90]
  114. // autoDownload: false,
  115. // systemAudio: true,
  116. // debug: true,
  117. resolution: videoConfig.value.resolution || "1080p",
  118. autoDownload: false,
  119. platform: "canvas",
  120. config: {
  121. frameRate: videoConfig.value.frameRate || 60,
  122. canvasId: ".scene-canvas > canvas",
  123. },
  124. disbaledAudio: false,
  125. systemAudio: false,
  126. debug: false,
  127. };
  128. const videoRecorder = new VideoRecorder(config);
  129. videoRecorder.startRecord();
  130. let stopWatch: () => void;
  131. const stopRecord = () => {
  132. stopWatch && stopWatch();
  133. pauseSceneGuide();
  134. };
  135. videoRecorder.on("record", (blob) => {
  136. saveAs(
  137. new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }),
  138. props.guide.title + ".mp4"
  139. );
  140. });
  141. videoRecorder.off("*");
  142. videoRecorder.on("startRecord", () => {
  143. playSceneGuide(paths.value, undefined, true);
  144. stopWatch = watchEffect(() => {
  145. if (!isScenePlayIng.value) {
  146. videoRecorder.endRecord();
  147. nextTick(stopWatch);
  148. }
  149. });
  150. });
  151. videoRecorder.on("cancelRecord", stopRecord);
  152. videoRecorder.on("endRecord", stopRecord);
  153. },
  154. };
  155. const paths = computed(() => getGuidePaths(props.guide));
  156. const time = computed(() => paths.value.reduceRight((t, c) => t + c.time, 0));
  157. </script>
  158. <style lang="scss" scoped>
  159. .sign-guide {
  160. display: flex;
  161. justify-content: space-between;
  162. align-items: center;
  163. padding: 20px 0;
  164. border-bottom: 1px solid var(--colors-border-color);
  165. &:first-child {
  166. border-top: 1px solid var(--colors-border-color);
  167. }
  168. .info {
  169. flex: 1;
  170. display: flex;
  171. align-items: center;
  172. .guide-cover {
  173. position: relative;
  174. &::after {
  175. content: "";
  176. position: absolute;
  177. inset: 0;
  178. background: rgba(0, 0, 0, 0.2);
  179. }
  180. .icon {
  181. position: absolute;
  182. z-index: 1;
  183. left: 50%;
  184. top: 50%;
  185. transform: translate(-50%, -50%);
  186. font-size: 16px;
  187. }
  188. img {
  189. width: 48px;
  190. height: 48px;
  191. object-fit: cover;
  192. border-radius: 4px;
  193. overflow: hidden;
  194. background-color: rgba(255, 255, 255, 0.6);
  195. display: block;
  196. }
  197. }
  198. div {
  199. margin-left: 10px;
  200. p {
  201. color: #fff;
  202. font-size: 14px;
  203. margin-bottom: 6px;
  204. }
  205. }
  206. }
  207. .actions {
  208. flex: none;
  209. }
  210. }
  211. .edit-add-type {
  212. color: #fff;
  213. position: fixed;
  214. inset: 0;
  215. background: rgba(0, 0, 0, 0.3);
  216. backdrop-filter: blur(4px);
  217. z-index: 2000;
  218. padding: 20px;
  219. overflow-y: auto;
  220. .edit-hot-item {
  221. margin: 100px auto 20px;
  222. width: 400px;
  223. padding: 20px;
  224. background: rgba(27, 27, 28, 0.8);
  225. box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
  226. border-radius: 4px;
  227. .input {
  228. margin-bottom: 10px;
  229. }
  230. }
  231. }
  232. .edit-hot {
  233. margin-top: 20px;
  234. text-align: right;
  235. span {
  236. font-size: 14px;
  237. color: rgba(255, 255, 255, 0.6);
  238. cursor: pointer;
  239. }
  240. }
  241. .edit-close {
  242. position: absolute;
  243. cursor: pointer;
  244. top: calc((100% - 18px) / 2);
  245. right: 0;
  246. transform: translateY(-50%);
  247. }
  248. .edit-title {
  249. padding-bottom: 18px;
  250. margin-bottom: 18px;
  251. position: relative;
  252. color: #fff;
  253. &::after {
  254. content: "";
  255. position: absolute;
  256. left: -20px;
  257. right: -20px;
  258. height: 1px;
  259. bottom: 0;
  260. background-color: rgba(255, 255, 255, 0.16);
  261. }
  262. }
  263. .edit-title {
  264. padding-bottom: 18px;
  265. margin-bottom: 18px;
  266. position: relative;
  267. &::after {
  268. content: "";
  269. position: absolute;
  270. left: -20px;
  271. right: -20px;
  272. height: 1px;
  273. bottom: 0;
  274. background-color: rgba(255, 255, 255, 0.16);
  275. }
  276. }
  277. </style>