index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <template>
  2. <view class="content">
  3. <!-- 步骤引导轮播 -->
  4. <view class="uni-margin-wrap">
  5. <swiper class="swiper" circular :indicator-dots="true" indicator-color="rgba(255,255,255,0.3)"
  6. indicator-active-color="#fff" :autoplay="true" :interval="5000" :duration="800">
  7. <swiper-item>
  8. <view class="swiper-item">
  9. <view class="step-number">1</view>
  10. <text class="step-text">填写视频名称</text>
  11. </view>
  12. </swiper-item>
  13. <swiper-item>
  14. <view class="swiper-item">
  15. <view class="step-number">2</view>
  16. <text class="step-text">选择个人形象照片,最好是正面照</text>
  17. </view>
  18. </swiper-item>
  19. <swiper-item>
  20. <view class="swiper-item">
  21. <view class="step-number">3</view>
  22. <text
  23. class="step-text">选择生成视频内容的方式:\n若选择文字输入,则输入口播的文字内容及使用音色;\n若选择语音录入则点击开始录音;\n若选择音频选择则选择本机音频文件</text>
  24. </view>
  25. </swiper-item>
  26. </swiper>
  27. </view>
  28. <!-- 表单区域 -->
  29. <u--form class="form-area" labelPosition="left" :model="formData" ref="uForm" labelWidth="70">
  30. <view class="form-card">
  31. <u-form-item lableWidth="100rpx" label="视频名称" prop="formData.videoName" borderBottom ref="videoName">
  32. <u--input type="text" v-model="formData.videoName" placeholder="请输入视频名称" border="none"></u--input>
  33. </u-form-item>
  34. <u-form-item lableWidth="100rpx" label="上传形象" prop="formData.videoName" borderBottom ref="videoName">
  35. <upload-component accept="image" :max-count="1" @file-updated="handleFileUpdated" />
  36. </u-form-item>
  37. <u-form-item @click="openPicker" :label="currentType" labelPosition="top" borderBottom>
  38. <u-picker :show="show" :columns="columns" keyName="label" closeOnClickOverlay="true"
  39. immediateChange="true" @confirm="confirmPicker"></u-picker>
  40. </u-form-item>
  41. <u-form-item >
  42. <u--textarea v-if="formData.current == 'text'" v-model="formData.txtCount" placeholder="请输入视频中的文字内容" class="text-area" />
  43. <view class="form-area-item input-panel" v-if="formData.current == 'record'">
  44. <button @click="toggleRecording" class="record-btn" :class="{ recording: isRecording }">
  45. {{ isRecording ? '停止录音' : '开始录音' }}
  46. </button>
  47. <view class="record-status" v-if="isRecording">
  48. <view class="audio-wave">
  49. <view class="wave-bar" :style="{ height: '30%' }"></view>
  50. <view class="wave-bar" :style="{ height: '70%' }"></view>
  51. <view class="wave-bar" :style="{ height: '50%' }"></view>
  52. </view>
  53. <text class="status-text">录音中...</text>
  54. </view>
  55. </view>
  56. <!-- 音频选择区域 -->
  57. <view class="form-area-item input-panel" v-if="formData.current == 'audio'" :key="timeStamp">
  58. <upload-component accept="file" :max-count="1" @file-updated="handleFileUpdatedWav" />
  59. </view>
  60. <!-- 音频预览 -->
  61. <audio v-if="audioPath" class="audio-bar" :src="audioPath" name="语音试听"
  62. poster="../static/images/xijiao/avator.png" :action="audioAction" controls>
  63. </audio>
  64. </u-form-item>
  65. <u-form-item @click="openTimbrePicker" :label="timbreType" labelPosition="top" borderBottom>
  66. <u-picker :show="timbreShow" :columns="timbreContent" keyName="text" closeOnClickOverlay="true"
  67. immediateChange="true" @confirm="confirmTimbrePicker"></u-picker>
  68. </u-form-item>
  69. </view>
  70. </u--form>
  71. <!-- 生成按钮 -->
  72. <view class="btn-area">
  73. <u-button @click="jumpToList" class="create-project" :hover-class="button - hover">
  74. 生成数字人视频
  75. </u-button>
  76. </view>
  77. <!-- 提示弹窗 -->
  78. <uni-popup ref="alertDialog" type="dialog">
  79. <uni-popup-dialog type="warning" cancelText="确定" title="提示" :content="missingContent" @close="dialogClose"
  80. :style="{
  81. '--uni-dialog-border-radius': '12rpx',
  82. '--uni-dialog-title-color': '#BD3124'
  83. }"></uni-popup-dialog>
  84. </uni-popup>
  85. </view>
  86. </template>
  87. <script>
  88. import UploadComponent from './common/fileupload/index.vue';
  89. import { uploadPerson, audioCreateVideo, textCreateVideo } from "@/api/system/user"
  90. import store from '@/store'
  91. const recorderManager = uni.getRecorderManager();
  92. const innerAudioContext = uni.createInnerAudioContext();
  93. innerAudioContext.autoplay = true;
  94. export default {
  95. components: {
  96. UploadComponent
  97. },
  98. data() {
  99. return {
  100. timbreType: '选择音色',
  101. currentType: '选择内容',
  102. timbreShow: false,
  103. show: false,
  104. columns: [[
  105. { id: 'text', label: '文字输入' },
  106. { id: 'record', label: '语音录入' },
  107. { id: 'audio', label: '音频选择' }]
  108. ],
  109. // 表单数据
  110. formData: {
  111. current: '', // 生成任务类型(text/record/audio)
  112. videoName: '', // 视频名称
  113. imagefile: '', // 形象文件路径
  114. audioFile: '', // 音频文件路径
  115. txtCount: '', // 文字内容
  116. timbre: '' // 音色选择
  117. },
  118. // 音色选项(修复原数据value重复问题)
  119. timbreContent: [[
  120. { value: '1', text: '青年男' },
  121. { value: '2', text: '青年女' },
  122. { value: '3', text: '中年男' },
  123. { value: '4', text: '中年女' }
  124. ]],
  125. imagePath: '', // 形象图片本地路径
  126. isRecording: false, // 录音状态
  127. audioPath: null, // 录音/音频本地路径
  128. recorderManager: null,
  129. innerAudioContext: null,
  130. showPerson: false, // 是否显示音频文件
  131. timeStamp: null, // 音频文件时间戳(用于刷新)
  132. audioAction: { method: 'pause' },
  133. fileInfo: {}, // 音频文件信息
  134. missingContent: '', // 缺失项提示文本
  135. personPath: '', // 人物形象路径
  136. createVideoPath: '' // 生成的视频路径
  137. }
  138. },
  139. onLoad() {
  140. // 初始化录音管理器
  141. this.recorderManager = uni.getRecorderManager();
  142. this.innerAudioContext = uni.createInnerAudioContext();
  143. // 监听录音停止
  144. this.recorderManager.onStop((res) => {
  145. console.log('录音结束,临时路径:', res.tempFilePath);
  146. this.audioPath = res.tempFilePath;
  147. this.isRecording = false;
  148. });
  149. // 监听录音错误
  150. this.recorderManager.onError((err) => {
  151. uni.showToast({ title: '录音出错', icon: 'none' });
  152. });
  153. },
  154. onShow() {
  155. // 页面显示时加载本地存储的音频文件
  156. this.timeStamp = new Date().getTime();
  157. this.fileInfo.path = uni.getStorageSync('filePath');
  158. this.fileInfo.name = uni.getStorageSync('fileName');
  159. if (this.fileInfo.name) {
  160. this.showPerson = true;
  161. this.uploadAudioPath(this.fileInfo.path);
  162. }
  163. },
  164. methods: {
  165. handleFileUpdatedWav(data){
  166. this.formData.audioFile = data.url
  167. },
  168. openTimbrePicker() {
  169. this.timbreShow = true;
  170. },
  171. // 音色选择
  172. confirmTimbrePicker(data) {
  173. console.log('选择结果:', data.value[0]);
  174. this.formData.timbre = data.value[0].value
  175. this.timbreType = data.value[0].text
  176. this.timbreShow = false;
  177. },
  178. openPicker() {
  179. this.show = true;
  180. },
  181. confirmPicker(data) {
  182. console.log('选择结果:', data.value[0]);
  183. this.formData.current = data.value[0].id
  184. this.currentType = data.value[0].label
  185. this.show = false;
  186. },
  187. handleFileUpdated(data) {
  188. this.formData.imagefile = data.url
  189. },
  190. // 选择音频文件(跳转文件列表)
  191. chooseFile() {
  192. if (!this.imagePath) {
  193. uni.showToast({ title: '请先选择形象照片', icon: 'none' });
  194. return;
  195. }
  196. uni.navigateTo({ url: "/pages/root-filelist/root-filelist" });
  197. },
  198. // 录音控制(开始/停止)
  199. toggleRecording() {
  200. if (this.isRecording) {
  201. // 停止录音并上传
  202. this.recorderManager.stop();
  203. this.uploadAudioPath(this.audioPath);
  204. } else {
  205. // 开始录音(清空已有录音)
  206. if (this.audioPath) this.audioPath = null;
  207. this.recorderManager.start({
  208. duration: 600000, // 最大录音时间(10分钟)
  209. sampleRate: 44100,
  210. numberOfChannels: 1,
  211. encodeBitRate: 192000,
  212. format: 'wav'
  213. });
  214. this.isRecording = true;
  215. }
  216. },
  217. // 验证表单并提交生成视频
  218. jumpToList() {
  219. // 表单验证
  220. const missingFields = [];
  221. // if (!this.formData.videoName.trim()) missingFields.push('视频名称');
  222. // if (!this.formData.imagefile) missingFields.push('人物形象');
  223. // // 内容验证(文字/语音至少一项)
  224. // const hasTextContent = this.formData.txtCount.trim() && this.formData.timbre;
  225. // const hasVoiceContent = this.audioPath;
  226. // if (!hasTextContent && !hasVoiceContent) missingFields.push('文字内容或语音输入');
  227. // // 显示缺失提示
  228. // if (missingFields.length > 0) {
  229. // this.missingContent = `请完善以下信息:${missingFields.join('、')}`;
  230. // this.$refs.alertDialog.open();
  231. // return;
  232. // }
  233. // 跳转生成页面
  234. uni.navigateTo({ url: 'list/creatingVideo/creatingVideo' });
  235. // 调用生成接口
  236. if (this.formData.current === 'record' || this.formData.current === 'audio') {
  237. // 音频生成视频
  238. audioCreateVideo(this.formData).then(response => {
  239. if (response.status === 200) this.createVideoPath = response.data;
  240. this.openWebsocket();
  241. }).catch(() => {
  242. uni.showToast({ title: '视频生成请求已提交', icon: 'none' });
  243. });
  244. } else if (this.formData.current === 'text') {
  245. // 文字生成视频
  246. textCreateVideo(this.formData).then(response => {
  247. if (response.status === 200) this.createVideoPath = response.data;
  248. this.openWebsocket();
  249. });
  250. }
  251. },
  252. openWebsocket() {
  253. uni.connectSocket({
  254. url: 'ws://192.168.0.2:8080/ws',
  255. header: {
  256. 'Content-Type': 'application/json'
  257. },
  258. method: 'POST',
  259. data: {
  260. 'videoId': this.createVideoPath
  261. },
  262. success: (res) => {
  263. console.log('连接成功', res);
  264. },
  265. fail: (err) => {
  266. console.error('连接失败', err);
  267. }
  268. });
  269. },
  270. // 关闭提示弹窗
  271. dialogClose() {
  272. console.log('弹窗关闭');
  273. }
  274. }
  275. }
  276. </script>
  277. <style scoped lang="scss">
  278. /* 基础变量定义 */
  279. $primary: #BD3124; // 主色调(红色)
  280. $primary-light: #F8E1E3; // 主色浅色
  281. $text-main: #333; // 主要文本色
  282. $text-secondary: #666; // 次要文本色
  283. $border-radius: 12rpx; // 统一圆角
  284. $shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); // 基础阴影
  285. /* 页面容器 */
  286. .content {
  287. display: flex;
  288. flex-direction: column;
  289. padding: 20rpx;
  290. background-color: #f9f9f9;
  291. min-height: 100vh;
  292. }
  293. /* 轮播步骤区 */
  294. .uni-margin-wrap {
  295. width: 100%;
  296. margin-bottom: 30rpx;
  297. border-radius: $border-radius;
  298. overflow: hidden;
  299. box-shadow: $shadow;
  300. }
  301. .swiper {
  302. height: 320rpx;
  303. }
  304. .swiper-item {
  305. position: relative;
  306. display: flex;
  307. flex-direction: column;
  308. align-items: center;
  309. justify-content: center;
  310. height: 100%;
  311. background: linear-gradient(135deg, $primary, #E65C68); // 渐变背景
  312. color: #fff;
  313. padding: 40rpx;
  314. text-align: center;
  315. }
  316. .step-number {
  317. width: 60rpx;
  318. height: 60rpx;
  319. background-color: rgba(255, 255, 255, 0.2);
  320. border-radius: 50%;
  321. display: flex;
  322. align-items: center;
  323. justify-content: center;
  324. font-size: 30rpx;
  325. font-weight: bold;
  326. margin-bottom: 20rpx;
  327. }
  328. .step-text {
  329. font-size: 28rpx;
  330. line-height: 1.6;
  331. white-space: pre-line; // 支持换行
  332. }
  333. /* 表单区域 */
  334. .form-area {
  335. width: 100%;
  336. }
  337. .form-card {
  338. background-color: #fff;
  339. border-radius: $border-radius;
  340. padding: 30rpx 20rpx;
  341. box-shadow: $shadow;
  342. }
  343. .form-area-item {
  344. display: flex;
  345. flex-direction: row;
  346. align-items: center;
  347. margin-bottom: 35rpx;
  348. padding: 0 10rpx;
  349. }
  350. .form-area-item-text {
  351. width: 160rpx;
  352. font-size: 28rpx;
  353. color: $text-main;
  354. font-weight: 500;
  355. }
  356. /* 输入控件样式 */
  357. .form-input {
  358. flex: 1;
  359. height: 80rpx;
  360. line-height: 80rpx;
  361. padding: 0 20rpx;
  362. border: 1px solid #eee;
  363. border-radius: 8rpx;
  364. font-size: 26rpx;
  365. transition: all 0.3s;
  366. &:focus {
  367. border-color: $primary;
  368. box-shadow: 0 0 0 2rpx rgba(189, 49, 36, 0.2);
  369. }
  370. }
  371. .text-area {
  372. flex: 1;
  373. min-height: 160rpx;
  374. padding: 20rpx;
  375. border: 1px solid #eee;
  376. border-radius: 8rpx;
  377. font-size: 26rpx;
  378. line-height: 1.6;
  379. resize: none;
  380. transition: all 0.3s;
  381. &:focus {
  382. border-color: $primary;
  383. box-shadow: 0 0 0 2rpx rgba(189, 49, 36, 0.2);
  384. }
  385. }
  386. /* 按钮样式 */
  387. .btn-primary {
  388. height: 70rpx;
  389. line-height: 70rpx;
  390. padding: 0 25rpx;
  391. border-radius: 8rpx;
  392. font-size: 26rpx;
  393. background-color: $primary;
  394. color: #fff;
  395. display: flex;
  396. align-items: center;
  397. justify-content: center;
  398. transition: all 0.2s;
  399. &:hover {
  400. background-color: #C94030;
  401. transform: scale(0.98);
  402. }
  403. }
  404. .record-btn {
  405. height: 70rpx;
  406. line-height: 70rpx;
  407. padding: 0 30rpx;
  408. border-radius: 8rpx;
  409. font-size: 26rpx;
  410. background-color: #f5f5f5;
  411. color: $text-main;
  412. transition: all 0.2s;
  413. &.recording {
  414. background-color: $primary;
  415. color: #fff;
  416. }
  417. &:hover {
  418. transform: scale(0.98);
  419. }
  420. }
  421. /* 图片预览 */
  422. .image-preview {
  423. width: 80rpx;
  424. height: 80rpx;
  425. margin-left: 20rpx;
  426. border-radius: 8rpx;
  427. overflow: hidden;
  428. border: 1px solid #eee;
  429. }
  430. .preview-img {
  431. width: 100%;
  432. height: 100%;
  433. object-fit: cover;
  434. }
  435. /* 录音动画 */
  436. .record-status {
  437. display: flex;
  438. align-items: center;
  439. margin-left: 20rpx;
  440. }
  441. .audio-wave {
  442. display: flex;
  443. align-items: center;
  444. justify-content: center;
  445. gap: 6rpx;
  446. height: 40rpx;
  447. }
  448. .wave-bar {
  449. width: 6rpx;
  450. background-color: $primary;
  451. border-radius: 3rpx;
  452. animation: wave 1s infinite ease-in-out;
  453. }
  454. .wave-bar:nth-child(2) {
  455. animation-delay: 0.2s;
  456. }
  457. .wave-bar:nth-child(3) {
  458. animation-delay: 0.4s;
  459. }
  460. @keyframes wave {
  461. 0%,
  462. 100% {
  463. transform: scaleY(0.5);
  464. }
  465. 50% {
  466. transform: scaleY(1);
  467. }
  468. }
  469. .status-text {
  470. font-size: 24rpx;
  471. color: $text-secondary;
  472. margin-left: 10rpx;
  473. }
  474. /* 音频文件信息 */
  475. .file-info {
  476. display: flex;
  477. flex-direction: column;
  478. margin-left: 20rpx;
  479. }
  480. .file-name {
  481. font-size: 24rpx;
  482. color: $text-main;
  483. width: 200rpx;
  484. overflow: hidden;
  485. white-space: nowrap;
  486. text-overflow: ellipsis;
  487. }
  488. .file-type {
  489. font-size: 22rpx;
  490. color: #999;
  491. }
  492. /* 音频播放器 */
  493. .audio-bar {
  494. width: 100%;
  495. margin: 20rpx auto;
  496. }
  497. /* 生成按钮 */
  498. .btn-area {
  499. display: flex;
  500. justify-content: center;
  501. margin: 40rpx 0;
  502. padding: 0 20rpx;
  503. }
  504. .create-project {
  505. width: 100%;
  506. height: 90rpx;
  507. line-height: 90rpx;
  508. background-color: $primary;
  509. color: #fff;
  510. border-radius: 45rpx;
  511. font-size: 30rpx;
  512. font-weight: 500;
  513. box-shadow: 0 6rpx 12rpx rgba(189, 49, 36, 0.2);
  514. transition: all 0.3s;
  515. }
  516. .button-hover {
  517. transform: translateY(2rpx);
  518. box-shadow: 0 3rpx 6rpx rgba(189, 49, 36, 0.2);
  519. }
  520. /* 提示文本 */
  521. .none-file {
  522. margin-left: 20rpx;
  523. font-size: 24rpx;
  524. color: #999;
  525. }
  526. /* 选项容器 */
  527. .content-type-wrap,
  528. .timbre-wrap {
  529. flex: 1;
  530. /deep/ .uni-data-checkbox {
  531. display: flex;
  532. flex-wrap: wrap;
  533. gap: 15rpx 20rpx;
  534. }
  535. /deep/ .uni-checkbox {
  536. margin-right: 5rpx;
  537. }
  538. /deep/ .uni-checkbox-input.checked {
  539. background-color: $primary !important;
  540. border-color: $primary !important;
  541. }
  542. }
  543. /* 功能区块面板 */
  544. .input-panel {
  545. padding: 20rpx;
  546. background-color: #fcfcfc;
  547. border-radius: 8rpx;
  548. margin-top: 10rpx;
  549. }
  550. </style>