邃芒慧语、照片说话(PC) https://photo.metavatar.cc/
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

1232 рядки
32 KiB

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