邃芒慧语、照片说话(PC) https://photo.metavatar.cc/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1458 lines
40 KiB

  1. <template>
  2. <div class="page flex justify-around">
  3. <!-- 左侧栏 -->
  4. <div class="left">
  5. <!-- 顶部 -->
  6. <!-- 选中AiFace -->
  7. <div class="left-top" v-show="showUploadBtn == 1">
  8. <el-row class="justify-between">
  9. <!-- title框 -->
  10. <div
  11. v-show="!submitImgBtn"
  12. class="flex justify-between"
  13. style="width: 300px"
  14. >
  15. <el-input
  16. maxlength="15"
  17. v-model="title"
  18. :placeholder="$t('createVideo.typeTitle')"
  19. />
  20. <el-icon class="icon-edit"><i-ep-edit /></el-icon>
  21. </div>
  22. </el-row>
  23. <!-- 中间展示大图 -->
  24. <div class="bigImg inmiddle">
  25. <img :src="imgUrl" alt="" class="inmiddle" />
  26. </div>
  27. </div>
  28. <!-- 选中需要上传的图片前 -->
  29. <div class="left-top" v-show="showUploadBtn == 2">
  30. <!-- 中间展示大图 -->
  31. <div class="noImg">
  32. <my-loading
  33. style="margin-top: 150px"
  34. :isLoading="compressionImgLoad"
  35. ></my-loading>
  36. <div v-if="!compressionImgLoad" class="trueImg inmiddle">
  37. <div class="imgBox">
  38. <img src="../../assets/img/trueImg.png" alt="" />
  39. </div>
  40. <div class="icon">✔</div>
  41. </div>
  42. <div v-if="!compressionImgLoad" class="upload inmiddle">
  43. +
  44. <input accept="image/*" type="file" @change="lookImg" />
  45. </div>
  46. <div v-if="!compressionImgLoad" class="falseImg inmiddle">
  47. <div>
  48. <div class="imgBox">
  49. <img src="../../assets/img/false1.png" alt="" />
  50. </div>
  51. <div class="icon">×</div>
  52. </div>
  53. <div>
  54. <div class="imgBox">
  55. <img src="../../assets/img/false2.png" alt="" />
  56. </div>
  57. <div class="icon">×</div>
  58. </div>
  59. <div>
  60. <div class="imgBox">
  61. <img src="../../assets/img/false3.png" alt="" />
  62. </div>
  63. <div class="icon">×</div>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. <!-- loading -->
  69. <div class="left-top" v-show="showUploadBtn == 3">
  70. <!-- 中间展示大图 -->
  71. <div class="noImg">
  72. <my-loading
  73. style="margin-top: 150px"
  74. :isLoading="compressionImgLoad"
  75. ></my-loading>
  76. <div v-if="!compressionImgLoad" class="trueImg inmiddle">
  77. <div class="imgBox">
  78. <img src="../../assets/img/trueImg.png" alt="" />
  79. </div>
  80. <div class="icon">✔</div>
  81. </div>
  82. <div v-if="!compressionImgLoad" class="upload inmiddle">
  83. +
  84. <input accept="image/*" type="file" @change="lookImg" />
  85. </div>
  86. <div v-if="!compressionImgLoad" class="falseImg inmiddle">
  87. <div>
  88. <div class="imgBox">
  89. <img src="../../assets/img/false1.png" alt="" />
  90. </div>
  91. <div class="icon">×</div>
  92. </div>
  93. <div>
  94. <div class="imgBox">
  95. <img src="../../assets/img/false2.png" alt="" />
  96. </div>
  97. <div class="icon">×</div>
  98. </div>
  99. <div>
  100. <div class="imgBox">
  101. <img src="../../assets/img/false3.png" alt="" />
  102. </div>
  103. <div class="icon">×</div>
  104. </div>
  105. </div>
  106. </div>
  107. </div>
  108. <!-- 选中需要上传的图片前后 -->
  109. <div class="left-top" v-show="showUploadBtn == 4">
  110. <el-row class="justify-between">
  111. <!-- 上传按钮 -->
  112. <div class="btnBox" v-show="submitImgBtn">
  113. <button class="uploadBtn">
  114. {{ $t("createVideo.reselect") }}
  115. <input accept="image/*" type="file" @change="lookImg" />
  116. </button>
  117. <button class="uploadBtn" @click="changeFile">
  118. {{ $t("createVideo.sureUpload") }}
  119. </button>
  120. </div>
  121. </el-row>
  122. <!-- 中间展示大图 -->
  123. <div class="bigImg inmiddle">
  124. <img :src="imgUrl" alt="" class="inmiddle" />
  125. </div>
  126. </div>
  127. <!-- 底部 -->
  128. <div class="left-bottom">
  129. <div class="tab flex">
  130. <div
  131. class="pointer"
  132. :class="showFace == 0 ? 'tabAvtive' : ''"
  133. @click="showFace = 0"
  134. >
  135. {{ $t("createVideo.availableMould") }}
  136. </div>
  137. <div
  138. v-if="AccessToken"
  139. class="pointer"
  140. :class="showFace == 1 ? 'tabAvtive' : ''"
  141. @click="showFace = 1"
  142. >
  143. {{ $t("createVideo.personalWarehouse") }}
  144. </div>
  145. </div>
  146. <!-- 系统相册 -->
  147. <div
  148. class="faceList"
  149. v-if="showFace == 0"
  150. v-infinite-scroll="loadMoreAiFaceSystem"
  151. :infinite-scroll-disabled="overLoadSystem"
  152. >
  153. <!-- 上传图片组件 -->
  154. <div v-if="showFace == 1" class="faceItem" @click="clickAddBtn">
  155. <span class="uploadImg">+ </span>
  156. </div>
  157. <my-loading :isLoading="feceItemLoading"></my-loading>
  158. <!-- face列表 -->
  159. <div
  160. :class="faceItemActive == item.id ? 'faceItemActive' : ''"
  161. class="faceItem pointer"
  162. @click="setImgUrl(item.material, item.id)"
  163. v-for="item in AiFaceList"
  164. :key="item.id"
  165. >
  166. <div class="imgBox">
  167. <img :src="item.material" alt="" />
  168. </div>
  169. s
  170. </div>
  171. </div>
  172. <!-- 个人相册 -->
  173. <div
  174. class="faceList"
  175. v-if="showFace == 1"
  176. v-infinite-scroll="loadMoreAiFaceUser"
  177. :infinite-scroll-disabled="overLoadUser"
  178. >
  179. <!-- 上传图片组件 -->
  180. <div v-if="showFace == 1" class="faceItem" @click="clickAddBtn">
  181. <span class="uploadImg">+ </span>
  182. </div>
  183. <my-loading :isLoading="feceItemLoading2"></my-loading>
  184. <!-- face列表 -->
  185. <div
  186. :class="faceItemActive == item.id ? 'faceItemActive' : ''"
  187. class="faceItem pointer"
  188. @click="setImgUrl(item.material, item.id)"
  189. v-for="item in userAiFaceList"
  190. :key="item.id"
  191. >
  192. <div class="imgBox">
  193. <img :src="item.material" alt="" />
  194. </div>
  195. <div
  196. v-if="faceItemActive == item.id"
  197. @click="deleteFaceImg(item.id)"
  198. class="deleteIcon"
  199. >
  200. ✖︎
  201. </div>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. <!-- 右侧栏 -->
  207. <div class="right">
  208. <el-tabs type="border-card" class="tabs">
  209. <!-- ----------------------------- 1 -->
  210. <el-tab-pane :label="$t('createVideo.typeScript')">
  211. <div class="card1">
  212. <div class="text">
  213. <!-- 文本域 -->
  214. <el-input
  215. id="textInputId"
  216. v-model="textareaContent"
  217. :rows="15"
  218. type="textarea"
  219. :placeholder="$t('createVideo.aiText')"
  220. />
  221. <span
  222. class="pointer"
  223. @click="getPreviewAudio"
  224. style="margin-right: 10px; font-size: 20px"
  225. >🔊</span
  226. >
  227. <span class="pointer" @click="insertIcon">🕗</span>
  228. </div>
  229. <p>{{ $t("createVideo.languages") }}</p>
  230. <!-- 语言 -->
  231. <el-select
  232. @change="chooseType"
  233. v-model="language"
  234. :placeholder="$t('createVideo.select')"
  235. style="width: 100%"
  236. >
  237. <el-option
  238. v-for="item in languageOptions"
  239. :key="item.value"
  240. :label="item.name"
  241. :value="item.id"
  242. >
  243. <img
  244. :src="item.img"
  245. alt=""
  246. style="
  247. width: 20px;
  248. margin-right: 10px;
  249. vertical-align: text-top;
  250. "
  251. />
  252. <span style="line-height: 20px">{{ item.name }}</span>
  253. </el-option>
  254. </el-select>
  255. <p>{{ $t("createVideo.voices") }}</p>
  256. <!-- 声音 -->
  257. <el-select
  258. @change="chooseStyle"
  259. :disabled="language ? false : true"
  260. v-model="sound"
  261. :placeholder="$t('createVideo.select')"
  262. style="width: 100%"
  263. >
  264. <el-option
  265. v-for="item in soundOptions"
  266. :key="item.value"
  267. :label="item.displayName"
  268. :value="item.id"
  269. />
  270. </el-select>
  271. <p>{{ $t("createVideo.styles") }}</p>
  272. <!-- 风格 -->
  273. <el-select
  274. :disabled="soundStyleOptions ? false : true"
  275. v-model="soundStyle"
  276. :placeholder="$t('createVideo.select')"
  277. style="width: 100%"
  278. >
  279. <el-option
  280. v-for="item in soundStyleOptions"
  281. :key="item"
  282. :label="item"
  283. :value="item"
  284. />
  285. </el-select>
  286. <!-- 生成视频 -->
  287. <el-button @click="saveOrUpdate(1)" color="#000" type="primary">
  288. <my-loading :isLoading="createLoading"></my-loading>
  289. <span v-if="!createLoading">{{
  290. $t("createVideo.generateVideo")
  291. }}</span></el-button
  292. >
  293. <!-- 保存视频 -->
  294. <el-button
  295. @click="saveOrUpdate(1, 'saveVideo')"
  296. color="#000"
  297. type="primary"
  298. style="margin-left: 0"
  299. >
  300. <my-loading :isLoading="createLoading"></my-loading>
  301. <span v-if="!createLoading">{{
  302. $t("createVideo.saveVideo")
  303. }}</span></el-button
  304. >
  305. </div>
  306. </el-tab-pane>
  307. <!-- ------------------------ 2 -->
  308. <el-tab-pane :label="$t('createVideo.uploadAudio')">
  309. <div class="card2">
  310. <!-- 上传视频 -->
  311. <p>
  312. <el-icon class="icon-VideoCamera"><i-ep-VideoCamera /></el-icon>
  313. {{ $t("createVideo.uploadAudio") }}
  314. </p>
  315. <div class="uploadVideo">
  316. +
  317. <input
  318. accept="audio/*"
  319. type="file"
  320. ref="fileInput"
  321. @click="e.target.value = null"
  322. @change="handleFileChange"
  323. />
  324. <!-- <video
  325. style="position: absolute; top: 0"
  326. v-if="videoSrc"
  327. :src="videoSrc"
  328. ref="videoPlayer"
  329. controls
  330. ></video> -->
  331. </div>
  332. <!-- 录制MP3上传语音 -->
  333. <p>
  334. <el-icon class="icon-Mic"><i-ep-Mic /></el-icon>
  335. {{ $t("createVideo.recordedAudio") }}
  336. </p>
  337. <div
  338. class="uploadAudio"
  339. :class="audioUrl ? 'uploadAudioAvtion' : ''"
  340. >
  341. <div class="btnBox">
  342. <button
  343. class="pointer"
  344. :disabled="!isRecording"
  345. @click="startRecording"
  346. >
  347. {{ $t("createVideo.startRecorded") }}
  348. </button>
  349. <button
  350. class="pointer"
  351. @click="stopRecording"
  352. :disabled="isRecording"
  353. >
  354. {{ $t("createVideo.stopRecorded") }}
  355. </button>
  356. </div>
  357. <!-- <button @click="playRecording">播放录音</button> -->
  358. <audio v-if="audioUrl" controls :src="audioUrl"></audio>
  359. <!-- <button v-if="audioUrl" @click="uploadAudio" class="pointer">
  360. 上传音频
  361. </button> -->
  362. </div>
  363. <!-- 生成视频 -->
  364. <el-button @click="saveOrUpdate(2)" color="#000" type="primary">
  365. <my-loading :isLoading="createLoading"></my-loading>
  366. <span v-if="!createLoading">{{
  367. $t("createVideo.generateVideo")
  368. }}</span>
  369. </el-button>
  370. <!-- 保存视频 -->
  371. <el-button
  372. @click="saveOrUpdate(2, 'saveVideo')"
  373. color="#000"
  374. type="primary"
  375. style="margin-left: 0"
  376. >
  377. <my-loading :isLoading="createLoading"></my-loading>
  378. <span v-if="!createLoading">{{
  379. $t("createVideo.saveVideo")
  380. }}</span></el-button
  381. >
  382. </div>
  383. </el-tab-pane>
  384. <!-- ----------------------- 3 -->
  385. <el-tab-pane v-if="false" :label="$t('createVideo.aiScript')">
  386. <div class="card3">
  387. <div class="text">
  388. <el-input
  389. v-model="title"
  390. :placeholder="$t('createVideo.typeTitle')"
  391. />
  392. <el-button type="primary">
  393. {{ $t("createVideo.confirm") }}
  394. </el-button>
  395. <el-input
  396. v-model="title"
  397. :rows="15"
  398. type="textarea"
  399. :placeholder="$t('createVideo.aiText')"
  400. />
  401. <el-icon :size="20">
  402. <Edit />
  403. </el-icon>
  404. </div>
  405. <p>{{ $t("createVideo.languages") }}</p>
  406. <el-select
  407. v-model="language"
  408. placeholder="Select"
  409. style="width: 100%"
  410. >
  411. <el-option
  412. v-for="item in languageOptions"
  413. :key="item.value"
  414. :label="item.label"
  415. :value="item.value"
  416. />
  417. </el-select>
  418. <p>{{ $t("createVideo.voices") }}</p>
  419. <el-select
  420. v-model="language"
  421. placeholder="Select"
  422. style="width: 100%"
  423. >
  424. <el-option
  425. v-for="item in languageOptions"
  426. :key="item.value"
  427. :label="item.label"
  428. :value="item.value"
  429. />
  430. </el-select>
  431. <p>{{ $t("createVideo.styles") }}</p>
  432. <el-select
  433. v-model="language"
  434. placeholder="Select"
  435. style="width: 100%"
  436. >
  437. <el-option
  438. v-for="item in languageOptions"
  439. :key="item.value"
  440. :label="item.label"
  441. :value="item.value"
  442. />
  443. </el-select>
  444. <el-button type="primary">{{
  445. $t("createVideo.generateVideo")
  446. }}</el-button>
  447. </div>
  448. </el-tab-pane>
  449. </el-tabs>
  450. </div>
  451. <!-- 试听的mp3 -->
  452. <div style="opacity: 0; width: 0">
  453. <audio autoplay :src="previewAudioUrl"></audio>
  454. </div>
  455. </div>
  456. </template>
  457. <script setup>
  458. import { getCurrentInstance } from "vue";
  459. import {
  460. personPhotoList,
  461. personPhotoUserListList,
  462. awsImgUpload,
  463. delImgApi,
  464. videoUpload,
  465. saveImgApi,
  466. uploadProgressApi,
  467. saveAudioApi,
  468. voiceTotalApi,
  469. chooseTypeApi,
  470. saveOrUpdateApi,
  471. createVideoApi,
  472. previewAudioApi,
  473. findByIdApi,
  474. } from "@/apis/createVideo";
  475. import { useI18n } from "vue-i18n";
  476. import Recorder from "js-audio-recorder"; //mp3插件
  477. import imageCompression from "browser-image-compression"; //压缩图片插件
  478. import { useRoute, useRouter } from "vue-router";
  479. const route = useRoute();
  480. const router = useRouter();
  481. const { locale } = useI18n();
  482. const lanChange = ref(locale);
  483. const AccessToken = localStorage.getItem("AccessToken"); // 判断有没有登录
  484. //#region 头像区的逻辑
  485. // 获取AI头像
  486. const AiFaceList = ref([]); //系统相册列表
  487. const userAiFaceList = ref([]); //个人相册列表
  488. const feceItemLoading = ref(false);
  489. const feceItemLoading2 = ref(false);
  490. function getAiFaceList(pageNum, pageSize, sendType) {
  491. feceItemLoading.value = true;
  492. personPhotoList(pageNum, pageSize, sendType)
  493. .then((res) => {
  494. AiFaceList.value = res.data.list;
  495. feceItemLoading.value = false;
  496. })
  497. .catch((err) => {
  498. feceItemLoading.value = false;
  499. console.log(err, "err");
  500. });
  501. }
  502. function getAiFaceUserList(pageNum, pageSize, sendType) {
  503. feceItemLoading2.value = true;
  504. personPhotoUserListList(pageNum, pageSize, sendType)
  505. .then((res) => {
  506. userAiFaceList.value = res.data.list;
  507. feceItemLoading2.value = false;
  508. })
  509. .catch((err) => {
  510. feceItemLoading2.value = false;
  511. console.log(err, "err");
  512. });
  513. }
  514. const pageNumSystem = ref(1);
  515. const overLoadSystem = ref(false); //禁用系统相册加载更多
  516. const pageNumUser = ref(1);
  517. const overLoadUser = ref(false); //禁用个人相册加载更多
  518. //系统相册触底加载更多
  519. function loadMoreAiFaceSystem() {
  520. pageNumSystem.value += 1;
  521. personPhotoList(pageNumSystem.value, 10, 1)
  522. .then((res) => {
  523. AiFaceList.value.push(...res.data.list);
  524. if (res.data.list.length == 0) {
  525. overLoadSystem.value = true;
  526. pageNumSystem.value = pageNumSystem.value - 1;
  527. }
  528. })
  529. .catch((err) => {
  530. console.log(err, "err");
  531. });
  532. }
  533. //个人相册触底加载更多
  534. function loadMoreAiFaceUser() {
  535. pageNumUser.value += 1;
  536. personPhotoUserListList(pageNumUser.value, 10, 3)
  537. .then((res) => {
  538. if (res.data.list.length == 0) {
  539. overLoadUser.value = true;
  540. pageNumUser.value = pageNumUser.value - 1;
  541. }
  542. userAiFaceList.value.push(...res.data.list);
  543. })
  544. .catch((err) => {
  545. console.log(err, "err");
  546. });
  547. }
  548. // 选择显示的face列表
  549. const showFace = ref("0");
  550. function ChangeShowFaceList(index) {
  551. showFace.value = index;
  552. }
  553. const faceItemActive = ref(); //高亮处理和当前选择图片的ID
  554. // 点击aifece
  555. function setImgUrl(url, id) {
  556. faceItemActive.value = id;
  557. showUploadBtn.value = 1;
  558. submitImgBtn.value = false;
  559. imgUrl.value = url;
  560. }
  561. // 删除个人相册照片
  562. async function deleteFaceImg(id) {
  563. const msg =
  564. lanChange.value == "zh-cn"
  565. ? `确定要删除这张图片吗? `
  566. : `Are you sure to delete this image ? `;
  567. const notice = lanChange.value == "zh-cn" ? "提示" : "Notice";
  568. const confirm = lanChange.value == "zh-cn" ? "确定" : "Confirm";
  569. const cancel = lanChange.value == "zh-cn" ? "取消" : "Cancel";
  570. ElMessageBox.confirm(msg, notice, {
  571. confirmButtonText: confirm,
  572. cancelButtonText: cancel,
  573. }).then(async () => {
  574. try {
  575. const res = await delImgApi(id);
  576. imgUrl.value = null;
  577. faceItemActive.value = null;
  578. userAiFaceList.value = [];
  579. getAiFaceUserList(1, 10, 3);
  580. pageNumUser.value = 1;
  581. overLoadUser.value = false;
  582. } catch (error) {
  583. ElMessage.error(errpr);
  584. }
  585. });
  586. }
  587. //#endregion ------------------------------
  588. //#region 预览图片
  589. const compressionImgLoad = ref(false);
  590. async function lookImg(e) {
  591. if (e.target.files[0]) {
  592. faceItemActive.value = null;
  593. imgUrl.value = null;
  594. imgData.value = null;
  595. showUploadBtn.value = 3;
  596. submitImgBtn.value = false;
  597. compressionImgLoad.value = true;
  598. // 压缩配置
  599. const options = {
  600. maxSizeMB: 2, // 最大压缩大小为 1MB
  601. useWebWorker: true, // 使用 Web Worker 进行压缩,提高性能
  602. };
  603. const compressedFile = await imageCompression(e.target.files[0], options);
  604. imgUrl.value = URL.createObjectURL(compressedFile);
  605. imgData.value = compressedFile;
  606. showUploadBtn.value = 4;
  607. submitImgBtn.value = true;
  608. compressionImgLoad.value = false;
  609. } else {
  610. return;
  611. }
  612. }
  613. //#endregion ------------------
  614. // 标题
  615. const title = ref("");
  616. //#region 上传图片
  617. const imgUrl = ref(""); //图片回显路径
  618. const imgData = ref(""); // 上传图片文件
  619. const showUploadBtn = ref(1); //上传图片+号的显示隐藏
  620. const submitImgBtn = ref(false); //右上角俩按钮显示与隐藏
  621. // 点击AI头像区的+号
  622. function clickAddBtn() {
  623. faceItemActive.value = null;
  624. showUploadBtn.value = 2;
  625. }
  626. // 上传图片
  627. async function changeFile() {
  628. compressionImgLoad.value = true;
  629. showUploadBtn.value = 3;
  630. imgUrl.value = URL.createObjectURL(imgData.value);
  631. let formData = new FormData();
  632. const timestamp = Date.now();
  633. formData.append("file", imgData.value, timestamp + ".png");
  634. try {
  635. // 上传图片
  636. const res = await awsImgUpload(formData);
  637. // 保存图片
  638. const res2 = await saveImgApi({ material: res.data.url });
  639. // 重新获取列表
  640. compressionImgLoad.value = false;
  641. showUploadBtn.value = 4;
  642. userAiFaceList.value = [];
  643. getAiFaceUserList(1, 10, 3);
  644. pageNumUser.value = 1;
  645. overLoadUser.value = false;
  646. if (res2.code == 200) {
  647. ElMessage.success(
  648. lanChange.value == "zh-cn" ? `上传成功` : `Upload successfully`
  649. );
  650. } else {
  651. ElMessage.error(
  652. lanChange.value == "zh-cn" ? `上传失败` : `Upload failure`
  653. );
  654. }
  655. } catch (error) {
  656. ElMessage.error(error);
  657. }
  658. }
  659. //#endregion ---------------------------
  660. //#region card1的逻辑
  661. const textareaContent = ref("");
  662. function insertIcon() {
  663. const textarea = document.getElementById("textInputId");
  664. const icon = "🕗"; // 这里使用了一个星星作为示例图标,你可以替换为你自己的图标
  665. // 获取光标位置
  666. const startPos = textarea.selectionStart;
  667. const endPos = textarea.selectionEnd;
  668. console.log(startPos);
  669. console.log(endPos);
  670. // 在光标位置插入图标
  671. textareaContent.value =
  672. textareaContent.value.substring(0, startPos) +
  673. icon +
  674. textareaContent.value.substring(endPos, textareaContent.value.length);
  675. // 设置光标位置为插入图标后的位置
  676. const newCursorPos = startPos + icon.length;
  677. textarea.setSelectionRange(newCursorPos, newCursorPos);
  678. textarea.focus();
  679. }
  680. const language = ref(""); //选择的语言
  681. const sound = ref(""); // 选择的声音
  682. const soundStyle = ref(""); //选择的风格
  683. // 语言下拉框内容
  684. const languageOptions = ref([
  685. {
  686. id: "0",
  687. name: "中文",
  688. },
  689. {
  690. id: "1",
  691. name: "English",
  692. },
  693. ]);
  694. async function voiceTotal() {
  695. const res = await voiceTotalApi();
  696. languageOptions.value = res.data;
  697. }
  698. // 声音下拉框内容
  699. const soundOptions = ref([]);
  700. async function chooseType() {
  701. const res = await chooseTypeApi(language.value);
  702. soundOptions.value = res.data;
  703. sound.value = null;
  704. }
  705. // 风格下拉框内容
  706. const soundStyleOptions = ref(null);
  707. async function chooseStyle() {
  708. const item = soundOptions.value.find((element) => element.id == sound.value);
  709. soundStyle.value = null;
  710. soundStyleOptions.value = item.style;
  711. }
  712. // 试听
  713. const previewAudioUrl = ref(null);
  714. async function getPreviewAudio() {
  715. if (!textareaContent.value) {
  716. ElMessage.error(
  717. lanChange.value == "zh-cn"
  718. ? `您还没有输入文案`
  719. : `You have not entered the copy yet`
  720. );
  721. return;
  722. }
  723. if (!sound.value) {
  724. ElMessage.error(
  725. lanChange.value == "zh-cn"
  726. ? `您还没有选择声音`
  727. : `You have not selected the sound`
  728. );
  729. return;
  730. }
  731. try {
  732. const res = await previewAudioApi(
  733. textareaContent.value,
  734. sound.value,
  735. soundStyle.value
  736. );
  737. if (!res.data.data) {
  738. ElMessage.error(
  739. lanChange.value == "zh-cn"
  740. ? `暂时无法试听,请稍后重试`
  741. : `Unable to listen, please try again later`
  742. );
  743. }
  744. previewAudioUrl.value = res.data.data;
  745. } catch (error) {
  746. ElMessage.error(error);
  747. }
  748. }
  749. //#endregion
  750. //#region 录制MP3上传语音
  751. const parameter = {
  752. sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
  753. sampleRate: 8000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
  754. numChannels: 1, // 声道,支持 1 或 2, 默认是1
  755. };
  756. const recorder = new Recorder(parameter);
  757. const isRecording = ref(true);
  758. const audioUrl = ref(null); // audio控件的url
  759. const audioData = ref("");
  760. const videoId = ref(null); //上传mp3的videoId,创建视频会用到
  761. const startRecording = async () => {
  762. try {
  763. await recorder.start();
  764. console.log("Recording started");
  765. isRecording.value = false;
  766. } catch (error) {
  767. console.error(error);
  768. }
  769. };
  770. const stopRecording = async () => {
  771. try {
  772. const recording = await recorder.stop();
  773. audioUrl.value = URL.createObjectURL(recorder.getWAVBlob());
  774. audioData.value = recorder.getWAVBlob();
  775. isRecording.value = true;
  776. } catch (error) {
  777. console.error(error);
  778. }
  779. };
  780. const playRecording = () => {
  781. if (audioUrl.value) {
  782. const audio = new Audio(audioUrl.value);
  783. audio.play();
  784. }
  785. };
  786. // 上传音频
  787. async function uploadAudio(isSaveVideo) {
  788. createLoading.value = true;
  789. let formData = new FormData();
  790. const timestamp = Date.now();
  791. formData.append("file", audioData.value, timestamp + ".mp3");
  792. try {
  793. const res = await videoUpload(formData);
  794. videoId.value = res.data.videoId;
  795. if (res) {
  796. // ElMessage.success("上传成功!");
  797. } else {
  798. ElMessage.error(
  799. lanChange.value == "zh-cn" ? `上传音频失败!` : `Failed to upload audio!`
  800. );
  801. return;
  802. }
  803. // 查询进度
  804. intervalId = setInterval(() => {
  805. uploadProgress(res.data.videoId, isSaveVideo);
  806. }, 1000);
  807. // ElMessage.success("上传成功!");
  808. } catch (error) {
  809. ElMessage.error(error);
  810. }
  811. }
  812. //#endregion ------------------------------------
  813. //#region 上传音频功能
  814. const fileInput = ref(null);
  815. let intervalId = null; // 用于存储 setInterval 的返回值
  816. const handleFileChange = async () => {
  817. const file = fileInput.value.files[0];
  818. if (file) {
  819. const videoUrl = URL.createObjectURL(file);
  820. audioUrl.value = videoUrl;
  821. audioData.value = file;
  822. }
  823. };
  824. // 获取视频上传进度
  825. const afterSaveAudioId = ref(null);
  826. async function uploadProgress(videoId, isSaveVideo) {
  827. const res = await uploadProgressApi(videoId);
  828. if (res.data == "complete") {
  829. clearInterval(intervalId);
  830. try {
  831. const res2 = await saveAudioApi({ videoId: videoId });
  832. afterSaveAudioId.value = res2.data.id; // 得到id
  833. } catch (error) {
  834. ElMessage.error(error);
  835. }
  836. // 定义参数
  837. const saveOrUpdateData = {
  838. id: id.value, //从草稿或者重新编辑回到创建视频时有id
  839. title: title.value,
  840. personPhotoId: faceItemActive.value, //任务照片id,
  841. voiceFrom: tabFrom.value, //:声音来源 1:来自选择模板,2:来自用户上传
  842. paperwork: textareaContent.value, //文案
  843. speakTypeStr: soundStyle.value, //声音风格
  844. voiceMouldId: sound.value, //选择声音id voiceFrom=1 时必传
  845. voiceMaterialId: afterSaveAudioId.value, //上传音频且保存音频后得到的id voiceFrom=2 时必传
  846. };
  847. try {
  848. const res3 = await saveOrUpdateApi(saveOrUpdateData); // 保存数据
  849. // 这个if判断是不是仅仅保存视频
  850. if (isSaveVideo == "saveVideo") {
  851. ElMessage.success(
  852. lanChange.value == "zh-cn" ? `已保存为草稿` : `Saved as draft`
  853. );
  854. createLoading.value = false;
  855. router.push("/myCreating");
  856. return;
  857. }
  858. const res4 = await createVideoApi({ id: res3.data.id }); // 生成视频
  859. if (res4.message == "success") {
  860. ElMessage.success(
  861. lanChange.value == "zh-cn"
  862. ? `视频正在生成中,请耐心等待`
  863. : `The video is being generated, please wait patiently`
  864. );
  865. }
  866. createLoading.value = false;
  867. router.push("/myCreating");
  868. } catch (error) {
  869. ElMessage.error(error);
  870. createLoading.value = false;
  871. }
  872. }
  873. }
  874. //#endregion --------------------------------------
  875. //#region 生成视频
  876. // tab栏切换
  877. const tabFrom = ref(1);
  878. const createLoading = ref(false);
  879. // 上传音频后,得到videoId
  880. // 保存数据
  881. async function saveOrUpdate(index, isSaveVideo) {
  882. if (createLoading.value == true) {
  883. return;
  884. }
  885. tabFrom.value = index;
  886. const saveOrUpdateData = {
  887. id: id.value, //从草稿或者重新编辑回到创建视频时有id
  888. title: title.value,
  889. personPhotoId: faceItemActive.value, //任务照片id,
  890. voiceFrom: tabFrom.value, //:声音来源 1:来自选择模板,2:来自用户上传
  891. paperwork: textareaContent.value, //文案
  892. speakTypeStr: soundStyle.value, //声音风格
  893. voiceMouldId: sound.value, //选择声音id voiceFrom=1 时必传
  894. voiceMaterialId: afterSaveAudioId.value, //上传音频且保存音频后得到的id voiceFrom=2 时必传
  895. };
  896. if (index == 1) {
  897. createLoading.value = true;
  898. try {
  899. const res2 = await saveOrUpdateApi(saveOrUpdateData); // 保存数据
  900. // 这个if判断是不是仅仅保存视频
  901. if (isSaveVideo == "saveVideo") {
  902. ElMessage.success(
  903. lanChange.value == "zh-cn" ? `已保存为草稿` : `Saved as draft`
  904. );
  905. createLoading.value = false;
  906. router.push("/myCreating");
  907. return;
  908. }
  909. const res3 = await createVideoApi({ id: res2.data.id }); // 生成视频
  910. if (res3.message == "success") {
  911. ElMessage.success(
  912. lanChange.value == "zh-cn"
  913. ? `视频正在生成中,请耐心等待`
  914. : `The video is being generated, please wait patiently`
  915. );
  916. }
  917. createLoading.value = false;
  918. router.push("/myCreating");
  919. } catch (error) {
  920. ElMessage.error(error);
  921. createLoading.value = false;
  922. }
  923. }
  924. if (index == 2) {
  925. try {
  926. const res1 = await uploadAudio(isSaveVideo); // 上传音频
  927. } catch (error) {
  928. ElMessage.error(error);
  929. createLoading.value = false;
  930. }
  931. }
  932. }
  933. //#endregion ----------------------------
  934. //#region 根据作品id查询作品信息并回显
  935. const id = ref(null); //作品的id
  936. async function findById(findId) {
  937. const res = await findByIdApi(findId);
  938. id.value = res.data.id;
  939. title.value = res.data.title; //标题回显
  940. faceItemActive.value = res.data.personPhotoId; //aiface高亮和图片id
  941. tabFrom.value = res.data.voiceFrom; //输入文案还是上传mp3模式
  942. textareaContent.value = res.data.paperwork; //文案内容
  943. afterSaveAudioId.value = res.data.voiceMaterialId; //保存音频后得到的id
  944. imgUrl.value = res.data.personPhotoUrl; //图片回显
  945. audioUrl.value = res.data.voiceMaterialUrl; // mp3回显
  946. language.value = res.data.voiceInfo.languageId;
  947. await chooseType(language.value);
  948. sound.value = res.data.voiceMouldId; //声音id
  949. chooseStyle();
  950. soundStyle.value = res.data.speakTypeStr; //选择风格
  951. }
  952. //#endregion ----------------------------
  953. //#region
  954. //#endregion ----------------------------
  955. // $t在script中的使用
  956. const {
  957. appContext: {
  958. config: { globalProperties },
  959. },
  960. } = getCurrentInstance();
  961. const string = "createVideo.typeScript";
  962. const t = globalProperties.$t(string);
  963. onMounted(async () => {
  964. getAiFaceList(1, 10, 1);
  965. if (AccessToken) {
  966. getAiFaceUserList(1, 10, 3);
  967. }
  968. await voiceTotal();
  969. if (route.query.id) {
  970. findById(route.query.id);
  971. }
  972. });
  973. </script>
  974. <style lang="scss" scoped>
  975. .page {
  976. // height: 1200px;
  977. height: calc(100vh - 80px);
  978. padding: 20px 0 0 40px;
  979. background-color: #fff;
  980. border-radius: 25px 0 0;
  981. }
  982. .left {
  983. width: 70%;
  984. height: 80%;
  985. :deep(.el-input__wrapper) {
  986. border: none;
  987. border-bottom: 2px solid #000;
  988. border-radius: 0;
  989. box-shadow: none;
  990. }
  991. .el-icon.icon-edit {
  992. width: 35px;
  993. height: 35px;
  994. font-size: 35px;
  995. color: #8c939d;
  996. text-align: center;
  997. }
  998. &-top {
  999. position: relative;
  1000. width: 100%;
  1001. // height: 65%;
  1002. height: 580px;
  1003. .uploadBtn {
  1004. position: relative;
  1005. padding: 5px 10px;
  1006. margin-left: 10px;
  1007. border-radius: 5px;
  1008. transition: all 0.3s;
  1009. &:hover {
  1010. background-color: #000;
  1011. color: #fff;
  1012. }
  1013. }
  1014. .bigImg {
  1015. display: flex;
  1016. align-items: center;
  1017. justify-content: space-between;
  1018. width: 80%;
  1019. height: 88%;
  1020. margin-top: 20px;
  1021. img {
  1022. max-width: 100%;
  1023. max-height: 100%;
  1024. }
  1025. }
  1026. .noImg {
  1027. position: relative;
  1028. width: 100%;
  1029. height: 100%;
  1030. border: 1px solid #000;
  1031. .imgBox {
  1032. position: relative;
  1033. width: 100%;
  1034. height: 100%;
  1035. border-radius: 50%;
  1036. overflow: hidden;
  1037. border: 1px solid #ccc;
  1038. img {
  1039. position: absolute;
  1040. top: 50%;
  1041. left: 50%;
  1042. transform: translate(-50%, -50%);
  1043. max-width: 120%;
  1044. }
  1045. }
  1046. .trueImg {
  1047. position: relative;
  1048. top: 60px;
  1049. width: 100px;
  1050. height: 100px;
  1051. border-radius: 50%;
  1052. .icon {
  1053. position: absolute;
  1054. top: 0%;
  1055. right: -5%;
  1056. border: 2px solid #00ff95;
  1057. color: #00ff95;
  1058. }
  1059. }
  1060. .el-upload {
  1061. position: absolute;
  1062. top: 0;
  1063. left: 0;
  1064. width: 100%;
  1065. height: 100%;
  1066. opacity: 0;
  1067. }
  1068. .upload {
  1069. top: 200px;
  1070. width: 100px;
  1071. height: 100px;
  1072. font-size: 50px;
  1073. line-height: 100px;
  1074. text-align: center;
  1075. background-color: #ccc;
  1076. border-radius: 50%;
  1077. }
  1078. .falseImg {
  1079. top: 350px;
  1080. // background-color: #795562;
  1081. display: flex;
  1082. justify-content: space-evenly;
  1083. width: 700px;
  1084. height: 100px;
  1085. > div {
  1086. position: relative;
  1087. width: 100px;
  1088. height: 100px;
  1089. background-color: #ccc;
  1090. border-radius: 50%;
  1091. .icon {
  1092. position: absolute;
  1093. top: 0%;
  1094. right: -5%;
  1095. border: 2px solid red;
  1096. color: red;
  1097. }
  1098. }
  1099. }
  1100. }
  1101. // 公共类名
  1102. input {
  1103. position: absolute;
  1104. top: 0;
  1105. left: 0;
  1106. width: 100%;
  1107. height: 100%;
  1108. opacity: 0;
  1109. cursor: pointer;
  1110. }
  1111. .icon {
  1112. position: absolute;
  1113. right: 2px;
  1114. width: 30px;
  1115. height: 30px;
  1116. font-size: 20px;
  1117. font-weight: 700;
  1118. line-height: 26px;
  1119. text-align: center;
  1120. border: 2px solid #000;
  1121. border-radius: 50%;
  1122. }
  1123. }
  1124. &-bottom {
  1125. width: 100%;
  1126. height: 32%;
  1127. margin-top: 10px;
  1128. .tab {
  1129. padding: 10px;
  1130. > div {
  1131. margin-right: 20px;
  1132. padding: 5px 10px;
  1133. transition: all 0.3s;
  1134. border-radius: 5px;
  1135. &:hover {
  1136. background-color: #000;
  1137. color: #fff;
  1138. }
  1139. }
  1140. .tabAvtive {
  1141. background-color: #000;
  1142. color: #fff;
  1143. }
  1144. }
  1145. .ai {
  1146. width: 100%;
  1147. }
  1148. .faceList {
  1149. padding: 8px;
  1150. display: flex;
  1151. flex-wrap: wrap;
  1152. justify-content: flex-start;
  1153. width: 100%;
  1154. height: 180px;
  1155. overflow-y: scroll;
  1156. .faceItem {
  1157. position: relative;
  1158. margin-right: 35px;
  1159. margin-bottom: 35px;
  1160. width: 150px;
  1161. height: 150px;
  1162. // padding: 0 25px;
  1163. line-height: 150px;
  1164. text-align: center;
  1165. border-radius: 50%;
  1166. background-color: #fbf8f8;
  1167. // border: 2px solid #fff;
  1168. box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px 5px;
  1169. // overflow: hidden;
  1170. transition: all 0.3s;
  1171. .imgBox {
  1172. cursor: pointer;
  1173. position: absolute;
  1174. top: 0;
  1175. left: 0;
  1176. right: 0;
  1177. bottom: 0;
  1178. z-index: 3;
  1179. width: 150px;
  1180. height: 150px;
  1181. border-radius: 50%;
  1182. background-color: #fbf8f8;
  1183. overflow: hidden;
  1184. img {
  1185. position: absolute;
  1186. top: 50%;
  1187. left: 50%;
  1188. transform: translate(-50%, -50%);
  1189. width: 100%;
  1190. }
  1191. }
  1192. .deleteIcon {
  1193. position: absolute;
  1194. top: 0;
  1195. right: 15px;
  1196. z-index: 999;
  1197. width: 25px;
  1198. height: 25px;
  1199. border: 2px solid #6d76ef;
  1200. border-radius: 50%;
  1201. color: #6d76ef;
  1202. font-size: 20px;
  1203. line-height: 23px;
  1204. cursor: pointer;
  1205. }
  1206. .uploadImg {
  1207. line-height: 145px;
  1208. font-size: 50px;
  1209. font-weight: 700;
  1210. text-align: center;
  1211. }
  1212. }
  1213. .faceItemActive {
  1214. position: relative;
  1215. transition: all 0.3s;
  1216. &::before {
  1217. content: "";
  1218. position: absolute;
  1219. // top: -3px;
  1220. // right: -7px;
  1221. // left: -3px;
  1222. // bottom: -7px;
  1223. top: -3px;
  1224. right: -3px;
  1225. left: -3px;
  1226. bottom: -3px;
  1227. z-index: 2;
  1228. border-radius: 50%;
  1229. background: linear-gradient(
  1230. 225deg,
  1231. rgb(81 217 202 / 100%),
  1232. rgb(114 100 245 / 100%)
  1233. );
  1234. }
  1235. }
  1236. }
  1237. }
  1238. }
  1239. .right {
  1240. width: 25%;
  1241. height: 100%;
  1242. background-color: green;
  1243. :deep(.el-tabs--border-card) {
  1244. font-size: 20px;
  1245. border: none;
  1246. }
  1247. .tabs {
  1248. height: 100%;
  1249. .card1 {
  1250. .text {
  1251. padding: 10px;
  1252. border: 1px solid #000;
  1253. .el-button {
  1254. float: right;
  1255. margin-top: 10px;
  1256. }
  1257. :deep(.el-textarea__inner) {
  1258. resize: none;
  1259. box-shadow: none;
  1260. }
  1261. }
  1262. p {
  1263. margin: 10px 0;
  1264. }
  1265. > .el-button {
  1266. width: 100%;
  1267. height: 50px;
  1268. margin-top: 10px;
  1269. }
  1270. :deep(.el-input .is-focus) {
  1271. // box-shadow: none !important;
  1272. // box-shadow: 0 0 0 1px #000 !important;
  1273. }
  1274. }
  1275. .card2 {
  1276. padding: 10px;
  1277. .el-icon {
  1278. // color: red;
  1279. vertical-align: text-top;
  1280. }
  1281. > div {
  1282. position: relative;
  1283. margin-top: 10px;
  1284. width: 100%;
  1285. border: #000 1px solid;
  1286. border-radius: 10px;
  1287. overflow: hidden;
  1288. transition: all 0.3s;
  1289. box-shadow: #00000026 2px 2px 2px 2px;
  1290. &:hover {
  1291. transform: translateY(-10px);
  1292. }
  1293. }
  1294. .uploadAudio {
  1295. padding: 20px;
  1296. // margin-top: 30px;
  1297. margin-bottom: 20px;
  1298. height: 70px;
  1299. .btnBox {
  1300. display: flex;
  1301. justify-content: space-between;
  1302. margin-bottom: 10px;
  1303. }
  1304. audio {
  1305. height: 50px;
  1306. width: 100%;
  1307. }
  1308. button {
  1309. border-radius: 10px;
  1310. height: 30px;
  1311. line-height: 22px;
  1312. padding: 5px 10px;
  1313. background-color: #fff;
  1314. transition: all 0.3s;
  1315. &:hover {
  1316. background-color: #000;
  1317. color: #fff;
  1318. }
  1319. }
  1320. }
  1321. .uploadAudioAvtion {
  1322. height: 170px;
  1323. }
  1324. .uploadVideo {
  1325. text-align: center;
  1326. margin-bottom: 20px;
  1327. height: 100px;
  1328. font-size: 50px;
  1329. font-weight: 700;
  1330. line-height: 100px;
  1331. input {
  1332. position: absolute;
  1333. top: 0;
  1334. left: 0;
  1335. width: 100%;
  1336. height: 100%;
  1337. opacity: 0;
  1338. }
  1339. video {
  1340. position: absolute;
  1341. top: 0;
  1342. left: 0;
  1343. z-index: -1;
  1344. width: 100%;
  1345. height: 100%;
  1346. }
  1347. }
  1348. > .el-button {
  1349. width: 100%;
  1350. height: 50px;
  1351. margin-top: 10px;
  1352. // background-color: #000;
  1353. }
  1354. }
  1355. .card3 {
  1356. .text {
  1357. padding: 10px;
  1358. border: 1px solid #000;
  1359. .el-button {
  1360. float: right;
  1361. margin-top: 10px;
  1362. }
  1363. :deep(.el-textarea__inner) {
  1364. resize: none;
  1365. box-shadow: none;
  1366. }
  1367. }
  1368. p {
  1369. margin: 10px 0;
  1370. }
  1371. > .el-button {
  1372. width: 100%;
  1373. height: 50px;
  1374. margin-top: 10px;
  1375. }
  1376. }
  1377. :deep(.el-tabs__item.is-active) {
  1378. color: #909393;
  1379. background-color: #000;
  1380. }
  1381. :deep(.el-tabs__nav) {
  1382. width: 100%;
  1383. }
  1384. :deep(.el-tabs__item) {
  1385. width: 34%;
  1386. }
  1387. :deep(.el-tabs__item:hover) {
  1388. color: #909393;
  1389. }
  1390. :deep(.el-tabs__content) {
  1391. height: calc(100% - 60px);
  1392. padding: 0;
  1393. }
  1394. }
  1395. }
  1396. </style>