123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- <template>
- <view class="content">
- <!-- 步骤引导轮播 -->
- <view class="uni-margin-wrap">
- <swiper class="swiper" circular :indicator-dots="true" indicator-color="rgba(255,255,255,0.3)"
- indicator-active-color="#fff" :autoplay="true" :interval="5000" :duration="800">
- <swiper-item>
- <view class="swiper-item">
- <view class="step-number">1</view>
- <text class="step-text">填写视频名称</text>
- </view>
- </swiper-item>
- <swiper-item>
- <view class="swiper-item">
- <view class="step-number">2</view>
- <text class="step-text">选择个人形象照片,最好是正面照</text>
- </view>
- </swiper-item>
- <swiper-item>
- <view class="swiper-item">
- <view class="step-number">3</view>
- <text
- class="step-text">选择生成视频内容的方式:\n若选择文字输入,则输入口播的文字内容及使用音色;\n若选择语音录入则点击开始录音;\n若选择音频选择则选择本机音频文件</text>
- </view>
- </swiper-item>
- </swiper>
- </view>
- <!-- 表单区域 -->
- <u--form class="form-area" labelPosition="left" :model="formData" ref="uForm" labelWidth="70">
- <view class="form-card">
- <u-form-item lableWidth="100rpx" label="视频名称" prop="formData.videoName" borderBottom ref="videoName">
- <u--input type="text" v-model="formData.videoName" placeholder="请输入视频名称" border="none"></u--input>
- </u-form-item>
- <u-form-item lableWidth="100rpx" label="上传形象" prop="formData.videoName" borderBottom ref="videoName">
- <upload-component accept="image" :max-count="1" @file-updated="handleFileUpdated" />
- </u-form-item>
- <u-form-item @click="openPicker" :label="currentType" labelPosition="top" borderBottom>
- <u-picker :show="show" :columns="columns" keyName="label" closeOnClickOverlay="true"
- immediateChange="true" @confirm="confirmPicker"></u-picker>
- </u-form-item>
- <u-form-item >
- <u--textarea v-if="formData.current == 'text'" v-model="formData.txtCount" placeholder="请输入视频中的文字内容" class="text-area" />
- <view class="form-area-item input-panel" v-if="formData.current == 'record'">
- <button @click="toggleRecording" class="record-btn" :class="{ recording: isRecording }">
- {{ isRecording ? '停止录音' : '开始录音' }}
- </button>
- <view class="record-status" v-if="isRecording">
- <view class="audio-wave">
- <view class="wave-bar" :style="{ height: '30%' }"></view>
- <view class="wave-bar" :style="{ height: '70%' }"></view>
- <view class="wave-bar" :style="{ height: '50%' }"></view>
- </view>
- <text class="status-text">录音中...</text>
- </view>
- </view>
-
- <!-- 音频选择区域 -->
- <view class="form-area-item input-panel" v-if="formData.current == 'audio'" :key="timeStamp">
- <upload-component accept="file" :max-count="1" @file-updated="handleFileUpdatedWav" />
- </view>
- <!-- 音频预览 -->
- <audio v-if="audioPath" class="audio-bar" :src="audioPath" name="语音试听"
- poster="../static/images/xijiao/avator.png" :action="audioAction" controls>
- </audio>
- </u-form-item>
- <u-form-item @click="openTimbrePicker" :label="timbreType" labelPosition="top" borderBottom>
- <u-picker :show="timbreShow" :columns="timbreContent" keyName="text" closeOnClickOverlay="true"
- immediateChange="true" @confirm="confirmTimbrePicker"></u-picker>
- </u-form-item>
- </view>
- </u--form>
- <!-- 生成按钮 -->
- <view class="btn-area">
- <u-button @click="jumpToList" class="create-project" :hover-class="button - hover">
- 生成数字人视频
- </u-button>
- </view>
- <!-- 提示弹窗 -->
- <uni-popup ref="alertDialog" type="dialog">
- <uni-popup-dialog type="warning" cancelText="确定" title="提示" :content="missingContent" @close="dialogClose"
- :style="{
- '--uni-dialog-border-radius': '12rpx',
- '--uni-dialog-title-color': '#BD3124'
- }"></uni-popup-dialog>
- </uni-popup>
- </view>
- </template>
- <script>
- import UploadComponent from './common/fileupload/index.vue';
- import { uploadPerson, audioCreateVideo, textCreateVideo } from "@/api/system/user"
- import store from '@/store'
- const recorderManager = uni.getRecorderManager();
- const innerAudioContext = uni.createInnerAudioContext();
- innerAudioContext.autoplay = true;
- export default {
- components: {
- UploadComponent
- },
- data() {
- return {
- timbreType: '选择音色',
- currentType: '选择内容',
- timbreShow: false,
- show: false,
- columns: [[
- { id: 'text', label: '文字输入' },
- { id: 'record', label: '语音录入' },
- { id: 'audio', label: '音频选择' }]
- ],
- // 表单数据
- formData: {
- current: '', // 生成任务类型(text/record/audio)
- videoName: '', // 视频名称
- imagefile: '', // 形象文件路径
- audioFile: '', // 音频文件路径
- txtCount: '', // 文字内容
- timbre: '' // 音色选择
- },
- // 音色选项(修复原数据value重复问题)
- timbreContent: [[
- { value: '1', text: '青年男' },
- { value: '2', text: '青年女' },
- { value: '3', text: '中年男' },
- { value: '4', text: '中年女' }
- ]],
- imagePath: '', // 形象图片本地路径
- isRecording: false, // 录音状态
- audioPath: null, // 录音/音频本地路径
- recorderManager: null,
- innerAudioContext: null,
- showPerson: false, // 是否显示音频文件
- timeStamp: null, // 音频文件时间戳(用于刷新)
- audioAction: { method: 'pause' },
- fileInfo: {}, // 音频文件信息
- missingContent: '', // 缺失项提示文本
- personPath: '', // 人物形象路径
- createVideoPath: '' // 生成的视频路径
- }
- },
- onLoad() {
- // 初始化录音管理器
- this.recorderManager = uni.getRecorderManager();
- this.innerAudioContext = uni.createInnerAudioContext();
- // 监听录音停止
- this.recorderManager.onStop((res) => {
- console.log('录音结束,临时路径:', res.tempFilePath);
- this.audioPath = res.tempFilePath;
- this.isRecording = false;
- });
- // 监听录音错误
- this.recorderManager.onError((err) => {
- uni.showToast({ title: '录音出错', icon: 'none' });
- });
- },
- onShow() {
- // 页面显示时加载本地存储的音频文件
- this.timeStamp = new Date().getTime();
- this.fileInfo.path = uni.getStorageSync('filePath');
- this.fileInfo.name = uni.getStorageSync('fileName');
- if (this.fileInfo.name) {
- this.showPerson = true;
- this.uploadAudioPath(this.fileInfo.path);
- }
- },
- methods: {
- handleFileUpdatedWav(data){
- this.formData.audioFile = data.url
- },
- openTimbrePicker() {
- this.timbreShow = true;
- },
- // 音色选择
- confirmTimbrePicker(data) {
- console.log('选择结果:', data.value[0]);
- this.formData.timbre = data.value[0].value
- this.timbreType = data.value[0].text
- this.timbreShow = false;
- },
- openPicker() {
- this.show = true;
- },
- confirmPicker(data) {
- console.log('选择结果:', data.value[0]);
- this.formData.current = data.value[0].id
- this.currentType = data.value[0].label
- this.show = false;
- },
- handleFileUpdated(data) {
- this.formData.imagefile = data.url
- },
- // 选择音频文件(跳转文件列表)
- chooseFile() {
- if (!this.imagePath) {
- uni.showToast({ title: '请先选择形象照片', icon: 'none' });
- return;
- }
- uni.navigateTo({ url: "/pages/root-filelist/root-filelist" });
- },
- // 录音控制(开始/停止)
- toggleRecording() {
- if (this.isRecording) {
- // 停止录音并上传
- this.recorderManager.stop();
- this.uploadAudioPath(this.audioPath);
- } else {
- // 开始录音(清空已有录音)
- if (this.audioPath) this.audioPath = null;
- this.recorderManager.start({
- duration: 600000, // 最大录音时间(10分钟)
- sampleRate: 44100,
- numberOfChannels: 1,
- encodeBitRate: 192000,
- format: 'wav'
- });
- this.isRecording = true;
- }
- },
- // 验证表单并提交生成视频
- jumpToList() {
- // 表单验证
- const missingFields = [];
- // if (!this.formData.videoName.trim()) missingFields.push('视频名称');
- // if (!this.formData.imagefile) missingFields.push('人物形象');
- // // 内容验证(文字/语音至少一项)
- // const hasTextContent = this.formData.txtCount.trim() && this.formData.timbre;
- // const hasVoiceContent = this.audioPath;
- // if (!hasTextContent && !hasVoiceContent) missingFields.push('文字内容或语音输入');
- // // 显示缺失提示
- // if (missingFields.length > 0) {
- // this.missingContent = `请完善以下信息:${missingFields.join('、')}`;
- // this.$refs.alertDialog.open();
- // return;
- // }
- // 跳转生成页面
- uni.navigateTo({ url: 'list/creatingVideo/creatingVideo' });
- // 调用生成接口
- if (this.formData.current === 'record' || this.formData.current === 'audio') {
- // 音频生成视频
- audioCreateVideo(this.formData).then(response => {
- if (response.status === 200) this.createVideoPath = response.data;
- this.openWebsocket();
- }).catch(() => {
- uni.showToast({ title: '视频生成请求已提交', icon: 'none' });
- });
- } else if (this.formData.current === 'text') {
- // 文字生成视频
- textCreateVideo(this.formData).then(response => {
- if (response.status === 200) this.createVideoPath = response.data;
- this.openWebsocket();
- });
- }
- },
- openWebsocket() {
- uni.connectSocket({
- url: 'ws://192.168.0.2:8080/ws',
- header: {
- 'Content-Type': 'application/json'
- },
- method: 'POST',
- data: {
- 'videoId': this.createVideoPath
- },
- success: (res) => {
- console.log('连接成功', res);
- },
- fail: (err) => {
- console.error('连接失败', err);
- }
- });
- },
- // 关闭提示弹窗
- dialogClose() {
- console.log('弹窗关闭');
- }
- }
- }
- </script>
- <style scoped lang="scss">
- /* 基础变量定义 */
- $primary: #BD3124; // 主色调(红色)
- $primary-light: #F8E1E3; // 主色浅色
- $text-main: #333; // 主要文本色
- $text-secondary: #666; // 次要文本色
- $border-radius: 12rpx; // 统一圆角
- $shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); // 基础阴影
- /* 页面容器 */
- .content {
- display: flex;
- flex-direction: column;
- padding: 20rpx;
- background-color: #f9f9f9;
- min-height: 100vh;
- }
- /* 轮播步骤区 */
- .uni-margin-wrap {
- width: 100%;
- margin-bottom: 30rpx;
- border-radius: $border-radius;
- overflow: hidden;
- box-shadow: $shadow;
- }
- .swiper {
- height: 320rpx;
- }
- .swiper-item {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- background: linear-gradient(135deg, $primary, #E65C68); // 渐变背景
- color: #fff;
- padding: 40rpx;
- text-align: center;
- }
- .step-number {
- width: 60rpx;
- height: 60rpx;
- background-color: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 30rpx;
- font-weight: bold;
- margin-bottom: 20rpx;
- }
- .step-text {
- font-size: 28rpx;
- line-height: 1.6;
- white-space: pre-line; // 支持换行
- }
- /* 表单区域 */
- .form-area {
- width: 100%;
- }
- .form-card {
- background-color: #fff;
- border-radius: $border-radius;
- padding: 30rpx 20rpx;
- box-shadow: $shadow;
- }
- .form-area-item {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-bottom: 35rpx;
- padding: 0 10rpx;
- }
- .form-area-item-text {
- width: 160rpx;
- font-size: 28rpx;
- color: $text-main;
- font-weight: 500;
- }
- /* 输入控件样式 */
- .form-input {
- flex: 1;
- height: 80rpx;
- line-height: 80rpx;
- padding: 0 20rpx;
- border: 1px solid #eee;
- border-radius: 8rpx;
- font-size: 26rpx;
- transition: all 0.3s;
- &:focus {
- border-color: $primary;
- box-shadow: 0 0 0 2rpx rgba(189, 49, 36, 0.2);
- }
- }
- .text-area {
- flex: 1;
- min-height: 160rpx;
- padding: 20rpx;
- border: 1px solid #eee;
- border-radius: 8rpx;
- font-size: 26rpx;
- line-height: 1.6;
- resize: none;
- transition: all 0.3s;
- &:focus {
- border-color: $primary;
- box-shadow: 0 0 0 2rpx rgba(189, 49, 36, 0.2);
- }
- }
- /* 按钮样式 */
- .btn-primary {
- height: 70rpx;
- line-height: 70rpx;
- padding: 0 25rpx;
- border-radius: 8rpx;
- font-size: 26rpx;
- background-color: $primary;
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- &:hover {
- background-color: #C94030;
- transform: scale(0.98);
- }
- }
- .record-btn {
- height: 70rpx;
- line-height: 70rpx;
- padding: 0 30rpx;
- border-radius: 8rpx;
- font-size: 26rpx;
- background-color: #f5f5f5;
- color: $text-main;
- transition: all 0.2s;
- &.recording {
- background-color: $primary;
- color: #fff;
- }
- &:hover {
- transform: scale(0.98);
- }
- }
- /* 图片预览 */
- .image-preview {
- width: 80rpx;
- height: 80rpx;
- margin-left: 20rpx;
- border-radius: 8rpx;
- overflow: hidden;
- border: 1px solid #eee;
- }
- .preview-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- /* 录音动画 */
- .record-status {
- display: flex;
- align-items: center;
- margin-left: 20rpx;
- }
- .audio-wave {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6rpx;
- height: 40rpx;
- }
- .wave-bar {
- width: 6rpx;
- background-color: $primary;
- border-radius: 3rpx;
- animation: wave 1s infinite ease-in-out;
- }
- .wave-bar:nth-child(2) {
- animation-delay: 0.2s;
- }
- .wave-bar:nth-child(3) {
- animation-delay: 0.4s;
- }
- @keyframes wave {
- 0%,
- 100% {
- transform: scaleY(0.5);
- }
- 50% {
- transform: scaleY(1);
- }
- }
- .status-text {
- font-size: 24rpx;
- color: $text-secondary;
- margin-left: 10rpx;
- }
- /* 音频文件信息 */
- .file-info {
- display: flex;
- flex-direction: column;
- margin-left: 20rpx;
- }
- .file-name {
- font-size: 24rpx;
- color: $text-main;
- width: 200rpx;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .file-type {
- font-size: 22rpx;
- color: #999;
- }
- /* 音频播放器 */
- .audio-bar {
- width: 100%;
- margin: 20rpx auto;
- }
- /* 生成按钮 */
- .btn-area {
- display: flex;
- justify-content: center;
- margin: 40rpx 0;
- padding: 0 20rpx;
- }
- .create-project {
- width: 100%;
- height: 90rpx;
- line-height: 90rpx;
- background-color: $primary;
- color: #fff;
- border-radius: 45rpx;
- font-size: 30rpx;
- font-weight: 500;
- box-shadow: 0 6rpx 12rpx rgba(189, 49, 36, 0.2);
- transition: all 0.3s;
- }
- .button-hover {
- transform: translateY(2rpx);
- box-shadow: 0 3rpx 6rpx rgba(189, 49, 36, 0.2);
- }
- /* 提示文本 */
- .none-file {
- margin-left: 20rpx;
- font-size: 24rpx;
- color: #999;
- }
- /* 选项容器 */
- .content-type-wrap,
- .timbre-wrap {
- flex: 1;
- /deep/ .uni-data-checkbox {
- display: flex;
- flex-wrap: wrap;
- gap: 15rpx 20rpx;
- }
- /deep/ .uni-checkbox {
- margin-right: 5rpx;
- }
- /deep/ .uni-checkbox-input.checked {
- background-color: $primary !important;
- border-color: $primary !important;
- }
- }
- /* 功能区块面板 */
- .input-panel {
- padding: 20rpx;
- background-color: #fcfcfc;
- border-radius: 8rpx;
- margin-top: 10rpx;
- }
- </style>
|