Commit 81203f07 by haojie

音频脚本逻辑修改前最后一次提交

parent f5bb479e
...@@ -49,7 +49,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; ...@@ -49,7 +49,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import Loading from '@/components/Loading/FirstCircle.vue'; import Loading from '@/components/Loading/FirstCircle.vue';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
import { injectWindow } from '@/utils/pyqt'; import { callPyjsInWindow, injectWindow } from '@/utils/pyqt';
import { scriptTypePhonetics } from '@/service/CreateLive'; import { scriptTypePhonetics } from '@/service/CreateLive';
const props = withDefaults( const props = withDefaults(
...@@ -126,6 +126,8 @@ const closeInterval = () => { ...@@ -126,6 +126,8 @@ const closeInterval = () => {
const confirm = () => { const confirm = () => {
openInterval(); openInterval();
onPlay(); onPlay();
// 通知python开始播放了
// callPyjsInWindow('')
}; };
// 主视频可以播放 // 主视频可以播放
......
...@@ -57,6 +57,7 @@ export default function () { ...@@ -57,6 +57,7 @@ export default function () {
if (res.code == 0 && res.data.length) { if (res.code == 0 && res.data.length) {
// 是否为error // 是否为error
if (res.data[0] === 'error') { if (res.data[0] === 'error') {
console.log('不需要洗稿了');
stopConfuse.value = true; stopConfuse.value = true;
} else { } else {
console.log('洗稿回调成功'); console.log('洗稿回调成功');
......
import { computed, onBeforeUnmount, ref, watch } from 'vue'; import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { createLiveKeys, createLiveVersion, filterFiled } from '@/service/CreateLive'; import { createLiveKeys, createLiveVersion, filterFiled, getAudioStartTimeAndEndTime } from '@/service/CreateLive';
import { getLiveTtsCallback, createLiveTask, liveTts, liveTaskRegenerate } from '@/utils/api/userApi'; import { getLiveTtsCallback, createLiveTask, liveTts, liveTaskRegenerate } from '@/utils/api/userApi';
import { ecursionDeepCopy, isDev, show_message } from '@/utils/tool'; import { ecursionDeepCopy, isDev, show_message } from '@/utils/tool';
import { useLiveInfoSubmit } from '@/hooks/useStoreCommit'; import { useLiveInfoSubmit } from '@/hooks/useStoreCommit';
...@@ -224,14 +224,14 @@ export const processTextCallback = () => { ...@@ -224,14 +224,14 @@ export const processTextCallback = () => {
}); });
if (res.code == 0) { if (res.code == 0) {
if (isDev()) { if (isDev()) {
let params = {
data: {
audio_address:
'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/e56d8750eb3a44f9930f73703489acb1.wav?Expires=1692087730&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=FtKSld5Dn55po9GyTm%2BefRKmPqw%3D',
task_id: 0,
},
};
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
let params = {
data: {
audio_address:
'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/e56d8750eb3a44f9930f73703489acb1.wav?Expires=1692087730&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=FtKSld5Dn55po9GyTm%2BefRKmPqw%3D',
task_id: 0,
},
};
res.data.push(params); res.data.push(params);
} }
} }
...@@ -244,65 +244,65 @@ export const processTextCallback = () => { ...@@ -244,65 +244,65 @@ export const processTextCallback = () => {
closeInterval(); closeInterval();
let list = JSON.parse(JSON.stringify(createLiveInfo.value[createLiveKeys.textScriptList])); let list = JSON.parse(JSON.stringify(createLiveInfo.value[createLiveKeys.textScriptList]));
let audio_list = []; let audio_list = [];
// 本次任务不是洗稿任务才修改store res.data.forEach((item: any) => {
if (!isConfuse) { // 根据task_id更新数组对象
res.data.forEach((item: any) => { let data = item.data;
// 根据task_id更新数组对象 if (
let data = item.data; data &&
if ( data.audio_address &&
data && (typeof data.task_id === 'string' || typeof data.task_id === 'number')
data.audio_address && ) {
(typeof data.task_id === 'string' || typeof data.task_id === 'number') audio_list.push(data.audio_address);
) { let index = list.findIndex((it: any) => it.task_id == data.task_id);
audio_list.push(data.audio_address); if (index !== -1) {
let index = list.findIndex((it: any) => it.task_id == data.task_id); list[index].audio_address = data.audio_address;
if (index !== -1) { commitInfo({
list[index].audio_address = data.audio_address; [createLiveKeys.textScriptList]: list,
commitInfo({ });
[createLiveKeys.textScriptList]: list,
});
}
} else {
show_message('缺少音频或id');
} }
}); } else {
show_message('缺少音频或id');
if (!audio_list.length) {
throw new CustomException('没有要处理的音频');
} }
});
// 获取音频时长
let durationList = await getAudioStartTimeAndEndTime(res.data);
console.log(durationList, '不洗稿durationList');
let resultList = await audioStart(audio_list, true); if (!audio_list.length) {
// 修改store的type_content throw new CustomException('没有要处理的音频');
commitInfo({ }
[createLiveKeys.textScriptValue]: resultList,
});
// 提交
await submit(type);
// 需要洗稿 let resultList = await audioStart(audio_list, true);
if (createLiveInfo.value[createLiveKeys.isDisorganize]) { // 修改store的type_content
if (createLiveVersion === 'v1') { commitInfo({
await startConfuse(); [createLiveKeys.textScriptValue]: resultList,
} else if (createLiveVersion === 'v2') { });
// 异步执行 // 提交
store.dispatch('asyncCreateLive/confuse', { await submit(type);
user_id: userInfo.value.id,
live_task_id: live_task_id.value, // 需要洗稿
// 洗稿id if (createLiveInfo.value[createLiveKeys.isDisorganize]) {
task_id: `${userInfo.value.id}-${live_task_id.value}`, if (createLiveVersion === 'v1') {
// 音频任务回调uid await startConfuse();
audio_task_id: v4(), } else if (createLiveVersion === 'v2') {
createLiveInfo: ecursionDeepCopy(createLiveInfo.value), // 异步执行
}); store.dispatch('asyncCreateLive/confuse', {
// user_id: userInfo.value.id,
loading.value = false; live_task_id: live_task_id.value,
submitSuccessed(); // 洗稿id
} task_id: `${userInfo.value.id}-${live_task_id.value}`,
} else { // 音频任务回调uid
console.log('不用洗稿'); audio_task_id: v4(),
createLiveInfo: ecursionDeepCopy(createLiveInfo.value),
});
//
loading.value = false; loading.value = false;
submitSuccessed(); submitSuccessed();
} }
} else {
console.log('不用洗稿');
loading.value = false;
submitSuccessed();
} }
} }
} else { } else {
......
...@@ -3,19 +3,28 @@ ...@@ -3,19 +3,28 @@
overlayClassName="placement-user-info" overlayClassName="placement-user-info"
placement="bottom-left" placement="bottom-left"
trigger="click" trigger="click"
:attach="getDomId"
:showArrow="false" :showArrow="false"
v-model:visible="user_popup_visible" v-model:visible="user_popup_visible"
> >
<template #content> <template #content>
<div class="s-header-user-info-dropdown"> <div
class="s-header-user-info-dropdown"
:style="{
width: headerUserInfo ? headerUserInfo.clientWidth + 'px' : '',
}"
>
<div class="user-info-dropdown-footer" @click="logout"> <div class="user-info-dropdown-footer" @click="logout">
<img :src="Imgs.logout" alt="" /> <img :src="Imgs.logout" alt="" />
<div class="dropdown-footer-text">退出</div> <div class="dropdown-footer-text">退出</div>
</div> </div>
</div> </div>
</template> </template>
<div class="custom-header-user-info"> <div class="custom-header-user-info" ref="headerUserInfo">
<UserSvg></UserSvg> <div class="user-name">
{{ userInfo.email ?? '' }}
</div>
<img :src="Imgs.user" alt="" />
<div class="user-message-dot" v-if="mes_num">{{ mes_num }}</div> <div class="user-message-dot" v-if="mes_num">{{ mes_num }}</div>
<div v-else></div> <div v-else></div>
</div> </div>
...@@ -24,19 +33,24 @@ ...@@ -24,19 +33,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { Popup as TPopup } from 'tdesign-vue-next'; import { Popup as TPopup } from 'tdesign-vue-next';
import UserSvg from '@/assets/svg/header/user.svg';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
const Imgs = { const Imgs = {
logout: new URL('../../assets/svg/user/userdropdown/logout.svg', import.meta.url).href, logout: new URL('../../assets/svg/user/userdropdown/logout.svg', import.meta.url).href,
user: new URL('../../assets/svg/header/user.svg', import.meta.url).href,
}; };
// 消息数量 // 消息数量
const mes_num = ref<number>(0); const mes_num = ref<number>(0);
const store = useStore(); const store = useStore();
const userInfo = computed(() => store.getters['user/userInfo']); const userInfo = computed(() => store.getters['user/userInfo']);
const headerUserInfo = ref(null);
const user_popup_visible = ref<boolean>(false); const user_popup_visible = ref<boolean>(false);
const getDomId = () => {
return headerUserInfo.value;
};
onMounted(() => { onMounted(() => {
if (!Object.keys(userInfo.value).length) { if (!Object.keys(userInfo.value).length) {
store.dispatch('user/getUserInfo'); store.dispatch('user/getUserInfo');
...@@ -115,9 +129,19 @@ const logout = () => { ...@@ -115,9 +129,19 @@ const logout = () => {
} }
} }
.custom-header-user-info { .custom-header-user-info {
border: 1px solid #7a7a7a;
border-radius: 8px;
padding: 4px 8px;
.dja(space-between); .dja(space-between);
& > :not(:last-child) { img {
margin: 0 8px; width: 30px;
height: 30px;
}
.user-name {
color: white;
font-size: @size-16;
font-weight: 600;
margin-right: 8px;
} }
.user-message-dot { .user-message-dot {
position: absolute; position: absolute;
......
...@@ -502,6 +502,7 @@ const audioSplit = async () => { ...@@ -502,6 +502,7 @@ const audioSplit = async () => {
// 音频提交 // 音频提交
const audioSubmit = async () => { const audioSubmit = async () => {
// 切割
try { try {
loading.value = true; loading.value = true;
await audioSplit(); await audioSplit();
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
</template> </template>
<script lang="tsx" setup> <script lang="tsx" setup>
import { onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import Loading from '@/components/loading.vue'; import Loading from '@/components/loading.vue';
import DigitalPeopleDiaog from './components/digitalPeopleDiaog.vue'; import DigitalPeopleDiaog from './components/digitalPeopleDiaog.vue';
import CardOne from '@/components/cardOne.vue'; import CardOne from '@/components/cardOne.vue';
...@@ -79,12 +79,15 @@ import routerConfig from '@/router/tool'; ...@@ -79,12 +79,15 @@ import routerConfig from '@/router/tool';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getDigitalPeopleList, uploadToAly } from '@/service/Common'; import { getDigitalPeopleList, uploadToAly } from '@/service/Common';
import Button from '@/components/Button.vue'; import Button from '@/components/Button.vue';
import { callPyjsInWindow } from '@/utils/pyqt'; import { callPyjsInWindow, injectWindow } from '@/utils/pyqt';
import { jumpToCreateLivePage } from '@/router/jump'; import { jumpToCreateLivePage } from '@/router/jump';
import { audioMerge } from '@/utils/audio'; import { audioMerge, downloadAudioList } from '@/utils/audio';
import { randomIntFormList } from '@/utils/tool';
import { useStore } from 'vuex';
const router = useRouter(); const router = useRouter();
const store = useStore();
const userToken = computed(() => store.getters['user/token']);
// 当前tab栏 // 当前tab栏
const currentTab = ref('1'); const currentTab = ref('1');
// 弹窗状态 // 弹窗状态
...@@ -194,26 +197,35 @@ const getList = async () => { ...@@ -194,26 +197,35 @@ const getList = async () => {
} }
}; };
const startTest = async () => { const startTest = async () => {
let list = [ // let list = [
'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-114e482462-6047-47be-a30c-b85a17c95665.mp3', // // 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-114e482462-6047-47be-a30c-b85a17c95665.mp3',
'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11efe3f1ea-b445-42e2-93b0-39a70fb7c6f5.mp3', // // 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11efe3f1ea-b445-42e2-93b0-39a70fb7c6f5.mp3',
'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11dd2372d8-73ff-4526-bf59-b1618a3f218f.mp3', // // 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11dd2372d8-73ff-4526-bf59-b1618a3f218f.mp3',
// 'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/ad08fa0a70ae4ea88d11ad5e394ce045.wav?Expires=1692149777&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=D93qMT1DovslSOVa9oufV2cGZxE%3D', // 'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/ad08fa0a70ae4ea88d11ad5e394ce045.wav?Expires=1692149777&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=D93qMT1DovslSOVa9oufV2cGZxE%3D',
// 'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/ad08fa0a70ae4ea88d11ad5e394ce045.wav?Expires=1692149777&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=D93qMT1DovslSOVa9oufV2cGZxE%3D', // 'http://nls-cloud-cn-shanghai.oss-cn-shanghai.aliyuncs.com/jupiter-flow/tmp/ad08fa0a70ae4ea88d11ad5e394ce045.wav?Expires=1692149777&OSSAccessKeyId=LTAIUpwNp2H7pBG5&Signature=D93qMT1DovslSOVa9oufV2cGZxE%3D',
]; // ];
let blob = await audioMerge(list); // let blob = await audioMerge(list);
console.log(blob); // console.log(blob);
if (blob) { // if (blob) {
// uploadToAly([blob]); // // uploadToAly([blob]);
} else { // } else {
console.log('文件错误'); // console.log('文件错误');
} // }
// let list = [
// 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-114e482462-6047-47be-a30c-b85a17c95665.mp3',
// 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11efe3f1ea-b445-42e2-93b0-39a70fb7c6f5.mp3',
// 'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-11dd2372d8-73ff-4526-bf59-b1618a3f218f.mp3',
// ];
// const { blobList } = await downloadAudioList(list);
// console.log(blobList, '下载完毕,传给python');
// callPyjsInWindow('mergeAudio', blobList, false, 0, false);
}; };
onMounted(() => { onMounted(() => {
// 获取我的数字人 // 获取我的数字人
getList(); getList();
startTest(); startTest();
callPyjsInWindow('setToken', userToken.value);
}); });
</script> </script>
......
...@@ -4,6 +4,7 @@ import { audioStart } from '@/service/Common'; ...@@ -4,6 +4,7 @@ import { audioStart } from '@/service/Common';
import store from '@/store'; import store from '@/store';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { writeLog } from '@/utils/pyqt'; import { writeLog } from '@/utils/pyqt';
import { getDurationOfAudioFile } from '@/utils/audio';
/** /**
* 创建直播的版本 * 创建直播的版本
...@@ -243,6 +244,7 @@ export const filterFiled = (item: any, type: string = '') => { ...@@ -243,6 +244,7 @@ export const filterFiled = (item: any, type: string = '') => {
params.content = audioConversion(it.children); params.content = audioConversion(it.children);
params.old_content = it.audio_url; params.old_content = it.audio_url;
} else { } else {
// 创建
if (typeof it === 'string') { if (typeof it === 'string') {
params.content = it; params.content = it;
params.old_content = it; params.old_content = it;
...@@ -303,6 +305,45 @@ export const regenerate = async (list: any[], item: any, live_id: any) => { ...@@ -303,6 +305,45 @@ export const regenerate = async (list: any[], item: any, live_id: any) => {
} }
}; };
// 计算音频块列表的开始时间和结束时间
export const getAudioStartTimeAndEndTime = async (list: any[]) => {
try {
let durationList = [];
for (let i = 0; i < list.length; i++) {
let data = list[i].data;
let params: any = {
audio_url: data.audio_address,
};
// 计算音频块的起始与结束时间点
let duration = await getDurationOfAudioFile(data.audio_address);
console.log(duration);
params.duration = duration;
// 默认值
params.start = 0;
params.end = duration;
durationList.forEach((it: any) => {
// 开始时间
params.start += it.duration;
});
durationList.push(params);
if (i !== 0) {
durationList.forEach((it: any, index: number) => {
// 不包括自己
if (index !== durationList.length - 1) {
// 结束时间
durationList[i].end += it.duration;
}
});
}
}
return durationList;
} catch (e) {
console.log(e);
}
};
// 洗稿获取音频回调 // 洗稿获取音频回调
export const getAudioCallback = (audio_task_id: string, len: number, liveInfo: any, live_id: any) => { export const getAudioCallback = (audio_task_id: string, len: number, liveInfo: any, live_id: any) => {
let interval = null; let interval = null;
...@@ -337,18 +378,20 @@ export const getAudioCallback = (audio_task_id: string, len: number, liveInfo: a ...@@ -337,18 +378,20 @@ export const getAudioCallback = (audio_task_id: string, len: number, liveInfo: a
console.log(res.data); console.log(res.data);
if (res.data.length >= len) { if (res.data.length >= len) {
closeInterval(); closeInterval();
let audio_list = []; let audioList = [];
// 洗稿任务 res.data.forEach(async (item: any, index: number) => {
res.data.forEach((item: any) => {
let data = item.data; let data = item.data;
if (data && data.audio_address) { if (data && data.audio_address) {
audio_list.push(data.audio_address); audioList.push(data.audio_address);
} else { } else {
console.log('洗稿缺少参数'); console.log('洗稿缺少参数');
show_message('洗稿缺少参数'); show_message('洗稿缺少参数');
} }
}); });
let resultList = await audioStart(audio_list, true); // 获取音频时长
let durationList = await getAudioStartTimeAndEndTime(res.data);
console.log(durationList, '洗稿durationList');
let resultList = await audioStart(audioList, true);
// 提交 // 提交
await regenerate(resultList, liveInfo, live_id); await regenerate(resultList, liveInfo, live_id);
} }
......
import audiobufferToWav from 'audiobuffer-to-wav'; import audiobufferToWav from 'audiobuffer-to-wav';
import { writeLog } from '@/utils/pyqt'; import { callPyjsInWindow, writeLog } from '@/utils/pyqt';
// import lamejs from 'lamejs'; // import lamejs from 'lamejs';
import { getFile } from './tool'; import { getFile } from './tool';
import audioConversion from '@/worker/audioConversion.js?worker'; import audioConversion from '@/worker/audioConversion.js?worker';
...@@ -10,11 +10,25 @@ export const createAudioContext = () => { ...@@ -10,11 +10,25 @@ export const createAudioContext = () => {
const audioContext = createAudioContext(); const audioContext = createAudioContext();
// 等待python合并mp3文件
export const pyAudioMerge = async (list: Blob[]) => {
// <Blob>
return new Promise((reslove) => {
callPyjsInWindow('mergeAudio', list);
reslove(111);
});
};
// 获取音频文件的时长 // 获取音频文件的时长
export const getDurationOfAudioFile = (file: File) => { export const getDurationOfAudioFile = (file: File | string) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const audio = new Audio(); let audio = null;
audio.src = URL.createObjectURL(file); if (typeof file === 'string') {
audio = new Audio(file);
} else {
audio = new Audio();
audio.src = URL.createObjectURL(file);
}
audio.onloadedmetadata = () => { audio.onloadedmetadata = () => {
resolve(audio.duration); resolve(audio.duration);
...@@ -200,8 +214,10 @@ export async function audioMerge(filePaths: string[]) { ...@@ -200,8 +214,10 @@ export async function audioMerge(filePaths: string[]) {
return blob; return blob;
} else if (fileType == 'mp3') { } else if (fileType == 'mp3') {
// mp3--耗时操作,放入worker中 // mp3--耗时操作,放入worker中
const blob = await mp3Worker(blobList); // const blob = await mp3Worker(blobList);
return blob; // return blob;
// 通知python处理
await pyAudioMerge(blobList);
} }
} catch (error) { } catch (error) {
writeLog({ writeLog({
......
...@@ -24,12 +24,25 @@ export const injectWindow = (key: string, value: any, parent: string = '') => { ...@@ -24,12 +24,25 @@ export const injectWindow = (key: string, value: any, parent: string = '') => {
/** /**
* 调用python指定方法 * 调用python指定方法
*/ */
export const callPyjsInWindow = (name: string, value: any = '', callback: boolean = false, timeout: number = 1000) => { export const callPyjsInWindow = (
name: string,
value: any = '',
callback: boolean = false,
timeout: number = 1000,
toJson: boolean = true,
) => {
try { try {
const getValue = () => {
if (toJson) {
return JSON.stringify(value);
} else {
return value;
}
};
if (callback) { if (callback) {
// 返回一个Promise // 返回一个Promise
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
window.pyjs[name](JSON.stringify(value), function (response: any) { window.pyjs[name](getValue(), function (response: any) {
// 在这里处理返回的值 // 在这里处理返回的值
resolve(response); resolve(response);
}); });
...@@ -38,7 +51,7 @@ export const callPyjsInWindow = (name: string, value: any = '', callback: boolea ...@@ -38,7 +51,7 @@ export const callPyjsInWindow = (name: string, value: any = '', callback: boolea
}, timeout); }, timeout);
}); });
} }
return window.pyjs[name](JSON.stringify(value)); return window.pyjs[name](getValue());
} catch (e) { } catch (e) {
writeLog({ writeLog({
name: 'call python error', name: 'call python error',
......
...@@ -358,8 +358,18 @@ export const randomInt = (min: number, max: number) => { ...@@ -358,8 +358,18 @@ export const randomInt = (min: number, max: number) => {
// 从数组中随机取一个值 // 从数组中随机取一个值
export const randomIntFormList = (list: any[]) => { export const randomIntFormList = (list: any[]) => {
let index = randomInt(0, list.length - 1); try {
return list[index]; let index = randomInt(0, list.length - 1);
return list[index];
} catch (e) {
writeLog({
name: '从列表中取随机值失败',
value: list,
error: e,
});
console.log(e);
return list[0];
}
}; };
// 循环耗时测试 // 循环耗时测试
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment