main
parent
a54e26eff6
commit
e370e1089e
|
@ -4,7 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "npm run build && node ./devops/deploy.js"
|
||||
},
|
||||
|
|
|
@ -1,26 +1,71 @@
|
|||
import request from "@/utils/request";
|
||||
import qs from 'qs'
|
||||
import qs from "qs";
|
||||
|
||||
export function addSystemCharterAPI(data: Record<string, any>) {
|
||||
return request({
|
||||
url: "/system/charter/add",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
return request({
|
||||
url: "/system/charter/add",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function editSystemCharterAPI(data: Record<string, any>) {
|
||||
return request({
|
||||
url: "/system/charter/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
return request({
|
||||
url: "/system/charter/update",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const querySystemCharterAPI = (data: Record<string, any> = {}): Promise<API.ResponstList<never[]>> => {
|
||||
return request({
|
||||
url: "/system/charter/getList?" + qs.stringify(data),
|
||||
method: "get",
|
||||
}) as any;
|
||||
return request({
|
||||
url: "/system/charter/getList?" + qs.stringify(data),
|
||||
method: "get",
|
||||
}) as any;
|
||||
};
|
||||
|
||||
/**
|
||||
* 克隆角色
|
||||
*/
|
||||
interface ICloneSpeaker {
|
||||
roleId: number;
|
||||
roleName: string;
|
||||
url: string;
|
||||
}
|
||||
export const cloneSpeakerAPI = (data: ICloneSpeaker): Promise<API.ResponstList<never[]>> => {
|
||||
return request({
|
||||
url: "/system/charter/cloneSpeaker",
|
||||
method: "post",
|
||||
data: data,
|
||||
}) as any;
|
||||
};
|
||||
|
||||
// 获取任务列表
|
||||
export const getTaskListAPI = (params: { current: number; pageSize: number }) => {
|
||||
return request.get("/system/charter/taskList", { params });
|
||||
};
|
||||
|
||||
// 添加获取详情接口
|
||||
export const getSystemCharterDetailAPI = (id: string) => {
|
||||
return request.get(`/system/charter/detail/${id}`);
|
||||
};
|
||||
|
||||
// 获取克隆历史记录
|
||||
export const getCloneHistoryAPI = (roleId: string) => {
|
||||
return request.get(`/system/charter/cloneHistory/${roleId}`);
|
||||
};
|
||||
|
||||
// 添加重试接口
|
||||
export const retryCloneTaskAPI = (taskId: string) => {
|
||||
return request.post(`/system/charter/retryTask/${taskId}`);
|
||||
};
|
||||
|
||||
// 添加删除任务接口
|
||||
export const deleteTaskAPI = (taskId: string) => {
|
||||
return request.delete(`/system/charter/deleteTask/${taskId}`);
|
||||
};
|
||||
|
||||
// 添加检查克隆任务接口
|
||||
export const checkCloneTaskAPI = (roleId: string) => {
|
||||
return request.get(`/system/charter/checkCloneTask/${roleId}`);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<el-drawer v-model="drawerVisible" :title="`${character?.roleName} 克隆历史`" size="800px" direction="rtl" @close="handleClose">
|
||||
<div class="source-audio p-4 mb-4 bg-gray-50 rounded">
|
||||
<div class="text-gray-500 mb-2 font-medium">源声音</div>
|
||||
<audio v-if="character?.originAudioUrl" controls class="w-full" :src="character.originAudioUrl" @play="handleAudioPlay">您的浏览器不支持音频播放</audio>
|
||||
<div v-else class="text-gray-400 text-sm">暂无源声音</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="historyList" v-loading="loading" style="width: 100%">
|
||||
<el-table-column label="克隆音频" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<audio controls class="w-full" :src="row.url" @play="handleAudioPlay">您的浏览器不支持音频播放</audio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="任务完成时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getCloneHistoryAPI, retryCloneTaskAPI } from "@/api/SystemCharter";
|
||||
import AudioManager from "@/utils/audioManager";
|
||||
import { TaskStatus, TaskStatusType, TaskStatusText } from "@/utils/dict";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
character: {
|
||||
id: string;
|
||||
roleName: string;
|
||||
originAudioUrl?: string;
|
||||
} | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const drawerVisible = ref(props.visible);
|
||||
const loading = ref(false);
|
||||
const historyList = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
drawerVisible.value = val;
|
||||
if (val && props.character) {
|
||||
fetchHistory();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => drawerVisible.value,
|
||||
(val) => {
|
||||
emit("update:visible", val);
|
||||
}
|
||||
);
|
||||
|
||||
const fetchHistory = async () => {
|
||||
if (!props.character?.id) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getCloneHistoryAPI(props.character.id);
|
||||
historyList.value = res.data.data.map((item: any) => ({
|
||||
url: item.cloneUrl,
|
||||
status: item.status,
|
||||
attempts: item.attempts,
|
||||
createdAt: item.createdAt,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("获取历史记录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
return TaskStatusType[status.toLowerCase() as keyof typeof TaskStatus] || "info";
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
return TaskStatusText[status.toLowerCase() as keyof typeof TaskStatus] || status;
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAudioPlay = (event: Event) => {
|
||||
const audio = event.target as HTMLAudioElement;
|
||||
AudioManager.play(audio);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
AudioManager.stopAll();
|
||||
};
|
||||
|
||||
const handleRetry = async (row: any) => {
|
||||
try {
|
||||
row.retrying = true;
|
||||
await retryCloneTaskAPI(row.id);
|
||||
ElMessage.success("重试任务已提交");
|
||||
// 刷新列表
|
||||
fetchHistory();
|
||||
} catch (error) {
|
||||
ElMessage.error("重试失败");
|
||||
} finally {
|
||||
row.retrying = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
audio {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-panel {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button {
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-current-time-display,
|
||||
audio::-webkit-media-controls-time-remaining-display {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.source-audio {
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.source-audio audio {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="克隆角色音频" width="500px" @close="handleClose">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>克隆角色音频</span>
|
||||
<el-button type="primary" link @click="showHistory">
|
||||
<el-icon class="mr-1"><List /></el-icon>
|
||||
克隆历史
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="角色名称">
|
||||
<span>{{ character?.roleName }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="语音样本">
|
||||
<el-upload class="upload-demo w-full" drag action="#" :show-file-list="false" :before-upload="beforeUpload" :http-request="handleVoiceUpload" accept="audio/*" :disabled="uploading">
|
||||
<div v-if="!form.voiceSample">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
<template v-if="!uploading">
|
||||
拖拽文件或 <em>点击上传</em>
|
||||
<div class="text-gray-400 text-sm mt-1">支持 mp3, wav 格式</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<div>正在上传...</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center flex-col">
|
||||
<div class="flex items-center mb-2">
|
||||
<el-icon class="text-green-500 mr-2"><Check /></el-icon>
|
||||
<span>已上传音频文件</span>
|
||||
<el-button type="danger" link class="ml-2" @click.stop="handleRemoveAudio">删除</el-button>
|
||||
</div>
|
||||
<audio ref="audioPlayer" controls class="w-full max-w-[300px]" :src="form.voiceSample">您的浏览器不支持音频播放</audio>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<CloneHistory
|
||||
v-model:visible="historyVisible"
|
||||
:character="
|
||||
currentCharacter || {
|
||||
id: character.id,
|
||||
roleName: character.roleName,
|
||||
originAudioUrl: character.originAudioUrl,
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确认克隆</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage, ElLoading, UploadProps } from "element-plus";
|
||||
import { UploadFilled, Check, Loading, List } from "@element-plus/icons-vue";
|
||||
import { cloneSpeakerAPI, checkCloneTaskAPI, getSystemCharterDetailAPI } from "@/api/SystemCharter";
|
||||
import createOss, { OssPath } from "@/utils/oss";
|
||||
import AudioManager from "@/utils/audioManager";
|
||||
import CloneHistory from "./CloneHistory.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
character: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(props.visible);
|
||||
const form = ref({
|
||||
voiceSample: null,
|
||||
});
|
||||
|
||||
const audioPlayer = ref<HTMLAudioElement | null>(null);
|
||||
|
||||
// 添加 loading 状态变量
|
||||
const uploading = ref(false);
|
||||
|
||||
// 添加历史记录相关状态
|
||||
const historyVisible = ref(false);
|
||||
|
||||
// 添加 currentCharacter 变量
|
||||
const currentCharacter = ref<any>(null);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 dialogVisible 变化
|
||||
watch(
|
||||
() => dialogVisible.value,
|
||||
(val) => {
|
||||
emit("update:visible", val);
|
||||
}
|
||||
);
|
||||
|
||||
const beforeUpload: UploadProps["beforeUpload"] = (rawFile) => {
|
||||
const isVoiceUpload = rawFile.type.indexOf("audio") > -1;
|
||||
if (!isVoiceUpload) {
|
||||
ElMessage.error("仅支持音频文件");
|
||||
return false;
|
||||
}
|
||||
if (rawFile.size / 1024 / 1024 > 20) {
|
||||
ElMessage.error("音频文件大小不能超过20M");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleVoiceUpload = async (params: any) => {
|
||||
const loading = ElLoading.service({
|
||||
text: "检测中...",
|
||||
background: "rgba(255, 255, 255, 0.7)",
|
||||
});
|
||||
try {
|
||||
// 先检查是否可以上传
|
||||
await checkCloneTaskAPI(props.character.id);
|
||||
|
||||
loading.close();
|
||||
|
||||
uploading.value = true;
|
||||
const oss = await createOss();
|
||||
const timestamp = Date.now();
|
||||
const fileExt = params.file.name.split(".").pop(); // 获取文件后缀名
|
||||
const fileName = `${props.character.roleName}_${timestamp}.${fileExt}`; // 构建新的文件名
|
||||
|
||||
const result = await oss.put(`${OssPath.CLONE_VOICE}/${props.character.roleName}/${fileName}`, params.file);
|
||||
form.value.voiceSample = result.url;
|
||||
ElMessage.success("上传成功");
|
||||
} catch (error: any) {
|
||||
console.error("克隆音频-->>", error);
|
||||
throw error;
|
||||
} finally {
|
||||
loading.close();
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.value.voiceSample) {
|
||||
ElMessage.error("请上传音频样本");
|
||||
return;
|
||||
}
|
||||
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
await cloneSpeakerAPI({
|
||||
roleId: props.character.id,
|
||||
roleName: props.character.roleName,
|
||||
url: form.value.voiceSample,
|
||||
});
|
||||
ElMessage.success("克隆任务已提交");
|
||||
dialogVisible.value = false;
|
||||
} catch (error) {
|
||||
console.error("克隆任务已提交", error);
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAudio = () => {
|
||||
AudioManager.stopAll();
|
||||
form.value.voiceSample = null;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
AudioManager.stopAll();
|
||||
form.value = {
|
||||
voiceSample: null,
|
||||
};
|
||||
historyVisible.value = false;
|
||||
};
|
||||
|
||||
// 修改显示历史记录方法
|
||||
const showHistory = async () => {
|
||||
// 先获取最新的角色信息
|
||||
try {
|
||||
const detail = await getSystemCharterDetailAPI(props.character.id);
|
||||
currentCharacter.value = {
|
||||
...props.character,
|
||||
...detail.data,
|
||||
};
|
||||
historyVisible.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error("获取角色信息失败");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
audio {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-panel {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button {
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-current-time-display,
|
||||
audio::-webkit-media-controls-time-remaining-display {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="`${character?.roleName} 音频试听`" width="500px" @close="handleClose">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="audio-item">
|
||||
<div class="text-gray-500 mb-2">原始音频:</div>
|
||||
<audio
|
||||
controls
|
||||
class="w-full"
|
||||
:src="character?.originUrl"
|
||||
@play="handleAudioPlay"
|
||||
ref="originAudio"
|
||||
>
|
||||
您的浏览器不支持音频播放
|
||||
</audio>
|
||||
</div>
|
||||
<div class="audio-item">
|
||||
<div class="text-gray-500 mb-2">克隆音频:</div>
|
||||
<audio
|
||||
controls
|
||||
class="w-full"
|
||||
:src="character?.cloneUrl"
|
||||
@play="handleAudioPlay"
|
||||
ref="cloneAudio"
|
||||
>
|
||||
您的浏览器不支持音频播放
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import AudioManager from "@/utils/audioManager";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
character: {
|
||||
roleName: string;
|
||||
originUrl: string;
|
||||
cloneUrl: string;
|
||||
} | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(props.visible);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => dialogVisible.value,
|
||||
(val) => {
|
||||
emit("update:visible", val);
|
||||
}
|
||||
);
|
||||
|
||||
const handleAudioPlay = (event: Event) => {
|
||||
const audio = event.target as HTMLAudioElement;
|
||||
AudioManager.play(audio);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
AudioManager.stopAll();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
audio {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-panel {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button {
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-play-button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
audio::-webkit-media-controls-current-time-display,
|
||||
audio::-webkit-media-controls-time-remaining-display {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,296 @@
|
|||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="克隆任务列表" width="1200px">
|
||||
<div class="task-list">
|
||||
<el-table :data="taskList" v-loading="loading" style="width: 100%">
|
||||
<el-table-column label="角色名称" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getTaskData(row.data).roleName }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音频样本" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<audio controls class="task-audio" :src="getTaskData(row.data).url" @play="handleAudioPlay" ref="audioRefs">您的浏览器不支持音频播放</audio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重试次数" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="text-center">{{ row.attempts }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" align="center" width="230">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
|
||||
<template v-if="row.status.toLowerCase() === TaskStatus.FAILED">
|
||||
<div class="flex items-center gap-1">
|
||||
<el-button type="primary" link size="small" :loading="row.retrying" @click="handleRetry(row)">重试</el-button>
|
||||
<el-button style="margin-left: 5px" type="danger" link size="small" :loading="row.deleting" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button style="margin-left: 5px" v-if="row.error" type="warning" link size="small" @click="showError(row)">错误详情</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息抽屉 -->
|
||||
<el-drawer v-model="errorDrawer" title="错误详情" size="500px" direction="rtl" :destroy-on-close="true">
|
||||
<div v-if="currentError" class="error-content">
|
||||
<div class="mb-4">
|
||||
<div class="text-gray-500 mb-1">角色名称</div>
|
||||
<div>{{ getTaskData(currentError.data).roleName }}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="text-gray-500 mb-1">创建时间</div>
|
||||
<div>{{ formatDate(currentError.createdAt) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500 mb-1">错误信息</div>
|
||||
<div class="error-message">{{ currentError.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getTaskListAPI, retryCloneTaskAPI, deleteTaskAPI } from "@/api/SystemCharter";
|
||||
import AudioManager from "@/utils/audioManager";
|
||||
import { TaskStatus, TaskStatusType, TaskStatusText } from "@/utils/dict";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(props.visible);
|
||||
const loading = ref(false);
|
||||
const taskList = ref([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
// 添加音频元素的引用
|
||||
const audioRefs = ref<HTMLAudioElement[]>([]);
|
||||
|
||||
// 添加错误相关状态
|
||||
const errorDrawer = ref(false);
|
||||
const currentError = ref<any>(null);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
dialogVisible.value = val;
|
||||
if (val) {
|
||||
fetchTaskList(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => dialogVisible.value,
|
||||
(val) => {
|
||||
emit("update:visible", val);
|
||||
if (!val) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
return TaskStatusType[status.toLowerCase() as keyof typeof TaskStatus] || "info";
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
return TaskStatusText[status.toLowerCase() as keyof typeof TaskStatus] || status;
|
||||
};
|
||||
|
||||
const fetchTaskList = async (showLoading = true) => {
|
||||
if (showLoading) {
|
||||
loading.value = true;
|
||||
}
|
||||
try {
|
||||
const res = await getTaskListAPI({
|
||||
current: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
taskList.value = res.data;
|
||||
total.value = res.meta.total;
|
||||
} catch (error) {
|
||||
ElMessage.error("获取任务列表失败");
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
fetchTaskList(true);
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
fetchTaskList(true);
|
||||
};
|
||||
|
||||
// 定时刷新任务列表
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(() => {
|
||||
if (dialogVisible.value) {
|
||||
fetchTaskList(false);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加日期格式化函数
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
// 添加任务数据解析函数
|
||||
const getTaskData = (dataStr: string) => {
|
||||
try {
|
||||
return JSON.parse(dataStr);
|
||||
} catch {
|
||||
return { roleName: "-", url: "" };
|
||||
}
|
||||
};
|
||||
|
||||
// 修改音频播放处理方法
|
||||
const handleAudioPlay = (event: Event) => {
|
||||
const audio = event.target as HTMLAudioElement;
|
||||
AudioManager.play(audio);
|
||||
};
|
||||
|
||||
// 修改关闭处理方法
|
||||
const handleClose = () => {
|
||||
AudioManager.stopAll();
|
||||
};
|
||||
|
||||
// 修改重试处理方法
|
||||
const handleRetry = async (row: any) => {
|
||||
try {
|
||||
row.retrying = true;
|
||||
await retryCloneTaskAPI(row.id);
|
||||
ElMessage.success("重试任务已提交");
|
||||
// 刷新列表
|
||||
fetchTaskList(false);
|
||||
} catch (error) {
|
||||
ElMessage.error("重试失败");
|
||||
} finally {
|
||||
row.retrying = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加删除处理方法
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm("确定要删除这个克隆任务吗?", "删除确认", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
row.deleting = true;
|
||||
await deleteTaskAPI(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
// 刷新列表
|
||||
fetchTaskList(false);
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
} finally {
|
||||
row.deleting = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加显示错误信息方法
|
||||
const showError = (row: any) => {
|
||||
currentError.value = row;
|
||||
errorDrawer.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-audio {
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.task-audio::-webkit-media-controls-panel {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.task-audio::-webkit-media-controls-play-button {
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.task-audio::-webkit-media-controls-play-button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
.task-audio::-webkit-media-controls-current-time-display,
|
||||
.task-audio::-webkit-media-controls-time-remaining-display {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 可选:添加重试按钮的样式 */
|
||||
.el-button.el-button--primary.el-button--link {
|
||||
padding: 0 4px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* 添加删除按钮样式 */
|
||||
.el-button.el-button--danger.el-button--link {
|
||||
padding: 0 4px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f56c6c;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: 12px;
|
||||
background-color: #fef0f0;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
|
@ -1,124 +1,180 @@
|
|||
<template>
|
||||
<div v-if="!isAdd" class="m-4">
|
||||
<ElButton @click="addSystemCharter()">添加系统人物</ElButton>
|
||||
<div class="mx-2 my-2">
|
||||
<ElTable :data="dataSource">
|
||||
<el-table-column label="角色背景" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.bg" style="height: 200px" class="ml-4 mt-2" :src="row.bg" :zoom-rate="1.2"
|
||||
:max-scale="7" :min-scale="0.2" :preview-src-list="[row.bg]" :initial-index="4"
|
||||
fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="roleName" label="角色名称" />
|
||||
<el-table-column prop="roleSetting" label="角色描述">
|
||||
<template #default="scope">
|
||||
<div class="truncate">{{ scope.row.roleSetting }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="prologue" label="开场白" />
|
||||
<el-table-column>
|
||||
<template #default="{ row }">
|
||||
<ElButton @click="handleEdit(row)" type="warning">编辑</ElButton>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ElTable>
|
||||
<div class="flex justify-end mt-2 mr-2">
|
||||
<ElPagination v-model:current-page="paginationInfo.currentPage"
|
||||
v-model:page-size="paginationInfo.pageSize" background size="small" layout="sizes,prev, pager, next"
|
||||
:total="count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="m-4 flex mt-6">
|
||||
<el-row style="width: 100%" :gutter="20">
|
||||
<el-col :span="18">
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="submitForm" label-width="auto" style="max-width: 1200px"
|
||||
:rules="rules">
|
||||
<el-form-item>
|
||||
<div style="text-align: right; width: 100%">
|
||||
<el-button type="primary" @click="handleSubmit()">{{ submitForm.id ? "修改" : "添加"
|
||||
}}</el-button>
|
||||
<el-button @click="handleBack()">返回</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色背景">
|
||||
<el-upload class="my-4 upload-warpper w-1/2" drag action="#" :show-file-list="false"
|
||||
:before-upload="beforeUpload" :http-request="handleFileUpload">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<el-icon class="el-icon--upload">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
<div class="el-upload__text mt-2">点击或拖拽上图片 <span
|
||||
class="text-red-500">(图片建议1920x1920)</span></div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="submitForm.bg">
|
||||
<el-image style="height: 200px" class="ml-4 mt-2" :src="submitForm.bg" :zoom-rate="1.2"
|
||||
:max-scale="7" :min-scale="0.2" :preview-src-list="[submitForm.bg]"
|
||||
:initial-index="4" fit="cover" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="submitForm.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述" prop="roleSetting">
|
||||
<el-input v-model="submitForm.roleSetting" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色简介" prop="intro">
|
||||
<el-input v-model="submitForm.intro" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色开场白" prop="prologue">
|
||||
<el-input v-model="submitForm.prologue" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="对话示例">
|
||||
<ElButton @click="addChartLines()" type="primary">添加对话</ElButton>
|
||||
<el-row :gutter="20" v-for="(item, index) in submitForm.roleLines" :key="index"
|
||||
style="width: 100%; margin-top: 10px">
|
||||
<el-col :span="12">
|
||||
<ElSelect v-model="item.sender_name" @change="roleLinesChange(index)"
|
||||
style="width: 100%" placeholder="选择机器人类型">
|
||||
<el-option label="用户" value="用户"></el-option>
|
||||
<el-option :label="submitForm.roleName"
|
||||
:value="submitForm.roleName"></el-option>
|
||||
</ElSelect>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.text" style="width: 100%" placeholder="输入对话文本" />
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-icon style="font-size: 20px; cursor: pointer; margin-top: 8px"
|
||||
@click="handleRemoveSampleItem(index)">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div style="width: 100%">
|
||||
<el-input v-model="formatJson" :rows="13" type="textarea" placeholder="粘贴格式化" />
|
||||
<el-button type="primary" style="margin-top: 10px" @click="handleAutoFormat">自动填充</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-if="!isAdd" class="m-4">
|
||||
<div class="sticky-header">
|
||||
<div class="flex justify-between items-center mb-1 bg-white py-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-input v-model="searchKeyword" placeholder="搜索角色名称" class="w-[200px]" clearable @input="handleSearch">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="cloneStatus" placeholder="克隆状态" clearable class="w-[150px]" @change="handleSearch">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="已克隆" :value="true" />
|
||||
<el-option label="未克隆" :value="false" />
|
||||
</el-select>
|
||||
<ElButton @click="addSystemCharter()">添加系统人物</ElButton>
|
||||
<ElButton @click="showTaskList" type="info">克隆任务列表</ElButton>
|
||||
</div>
|
||||
<ElPagination v-model:current-page="paginationInfo.currentPage" v-model:page-size="paginationInfo.pageSize" background size="small" layout="sizes,prev, pager, next" :total="count" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="item in dataSource" :key="item.id" class="mb-4">
|
||||
<el-card :body-style="{ padding: '0px' }" class="character-card">
|
||||
<div class="image-container">
|
||||
<el-image
|
||||
v-if="item.bg"
|
||||
:src="item.bg"
|
||||
class="character-image"
|
||||
fit="cover"
|
||||
:preview-src-list="[item.bg]"
|
||||
:initial-index="0"
|
||||
:preview-teleported="true"
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<h3 class="text-base font-bold mb-1 truncate">{{ item.roleName }}</h3>
|
||||
<p class="text-gray-600 text-xs mb-2 line-clamp-2">{{ item.roleSetting }}</p>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="text-xs text-gray-500">
|
||||
剩余克隆次数:
|
||||
<span :class="{ 'text-red-500': item.remainingCloneCount === 0, 'text-green-500': item.remainingCloneCount > 0 }">
|
||||
{{ item.remainingCloneCount || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ElButton @click="handleEdit(item)" type="warning" size="small">编辑</ElButton>
|
||||
<ElButton v-if="!item.activate" @click="handleClone(item)" type="primary" size="small" class="ml-2" :disabled="item.remainingCloneCount === 0">克隆音频</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="add-page">
|
||||
<div class="sticky-header mb-4">
|
||||
<div class="flex justify-between items-center bg-white py-2">
|
||||
<h2 class="text-xl font-bold">{{ submitForm.id ? "编辑" : "添加" }}系统人物</h2>
|
||||
<div>
|
||||
<el-button @click="handleBack()">返回列表</el-button>
|
||||
<el-button type="primary" @click="handleSubmit()">{{ submitForm.id ? "保存修改" : "确认添加" }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="18">
|
||||
<el-card class="form-card">
|
||||
<el-form ref="formRef" :model="submitForm" label-width="100px" :rules="rules" class="character-form">
|
||||
<el-form-item label="角色背景" class="upload-section">
|
||||
<div class="upload-preview" v-if="submitForm.bg">
|
||||
<el-image :src="submitForm.bg" class="preview-image" :preview-src-list="[submitForm.bg]" fit="cover" />
|
||||
<div class="upload-mask" @click="handleRemoveImage">
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>删除图片</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-upload v-else class="upload-area" drag action="#" :show-file-list="false" :before-upload="beforeUpload" :http-request="handleFileUpload">
|
||||
<div class="upload-content">
|
||||
<el-icon class="upload-icon"><UploadFilled /></el-icon>
|
||||
<div class="upload-text">
|
||||
<p>点击或拖拽上传图片</p>
|
||||
<p class="text-gray-400 text-sm">(建议尺寸 1920x1920)</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="submitForm.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色描述" prop="roleSetting">
|
||||
<el-input v-model="submitForm.roleSetting" type="textarea" :rows="4" placeholder="请详细描述角色的性格、背景等特征" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色简介" prop="intro">
|
||||
<el-input v-model="submitForm.intro" type="textarea" :rows="3" placeholder="请输入角色简短介绍" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="开场白" prop="prologue">
|
||||
<el-input v-model="submitForm.prologue" type="textarea" :rows="3" placeholder="请输入角色的开场白" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="对话示例">
|
||||
<div class="dialog-section">
|
||||
<div class="dialog-header">
|
||||
<span class="font-bold">示例对话</span>
|
||||
<el-button type="primary" link @click="addChartLines()">
|
||||
<el-icon><Plus /></el-icon>添加对话
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="dialog-list">
|
||||
<el-row v-for="(item, index) in submitForm.roleLines" :key="index" :gutter="20" class="dialog-item">
|
||||
<el-col :span="6">
|
||||
<ElSelect v-model="item.sender_name" @change="roleLinesChange(index)" class="w-full" placeholder="选择发言者">
|
||||
<el-option label="用户" value="用户" />
|
||||
<el-option :label="submitForm.roleName" :value="submitForm.roleName" />
|
||||
</ElSelect>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input v-model="item.text" type="textarea" :rows="1" placeholder="输入对话内容" />
|
||||
</el-col>
|
||||
<el-col :span="2" class="flex items-center justify-center">
|
||||
<el-button type="danger" link @click="handleRemoveSampleItem(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="format-card">
|
||||
<div class="format-header">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>快速导入</span>
|
||||
</div>
|
||||
<el-input v-model="formatJson" type="textarea" :rows="13" placeholder="粘贴格式化的JSON数据" class="format-input" />
|
||||
<el-button type="primary" class="w-full mt-4" @click="handleAutoFormat">
|
||||
<el-icon><Check /></el-icon>自动填充
|
||||
</el-button>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<CloneSpeak v-model:visible="cloneDialogVisible" :character="currentCharacter" />
|
||||
<TaskList v-model:visible="taskListVisible" />
|
||||
<PreviewVoice v-model:visible="previewDialogVisible" :character="currentPreviewAudio" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, watch } from "vue";
|
||||
import { addSystemCharterAPI, querySystemCharterAPI, editSystemCharterAPI } from "@/api/SystemCharter";
|
||||
import { ElButton, ElLoading, ElMessage, ElTable, FormInstance, ElPagination, UploadProps } from "element-plus";
|
||||
import { Remove, UploadFilled } from "@element-plus/icons-vue";
|
||||
import Oss from "@/utils/oss";
|
||||
import { addSystemCharterAPI, querySystemCharterAPI, editSystemCharterAPI, getSystemCharterDetailAPI } from "@/api/SystemCharter";
|
||||
import { ElButton, ElLoading, ElMessage, FormInstance, ElPagination, UploadProps } from "element-plus";
|
||||
import { UploadFilled, Delete, Plus, Document, Check, Search } from "@element-plus/icons-vue";
|
||||
import createOss, { OssPath } from "@/utils/oss";
|
||||
import CloneSpeak from "./CloneSpeak.vue";
|
||||
import TaskList from "./TaskList.vue";
|
||||
import PreviewVoice from "./PreviewVoice.vue";
|
||||
|
||||
const dataSource = ref([]);
|
||||
const paginationInfo = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const count = ref(0);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
@ -129,151 +185,452 @@ const formatJson = ref("");
|
|||
const submitForm = ref();
|
||||
|
||||
const rules = ref({
|
||||
roleName: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||
roleSetting: [{ required: true, message: "请输入角色描述", trigger: "blur" }],
|
||||
prologue: [{ required: true, message: "请输入角色开场白", trigger: "blur" }],
|
||||
// intro: [{ required: true, message: "请输入角色简介", trigger: "blur" }],
|
||||
roleName: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||
roleSetting: [{ required: true, message: "请输入角色描述", trigger: "blur" }],
|
||||
prologue: [{ required: true, message: "请输入角色开场白", trigger: "blur" }],
|
||||
// intro: [{ required: true, message: "请输入角色简介", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 在 script setup 部分添加以下响应变量
|
||||
const cloneDialogVisible = ref(false);
|
||||
const currentCharacter = ref<any>(null);
|
||||
const taskListVisible = ref(false);
|
||||
const previewDialogVisible = ref(false);
|
||||
const currentPreviewAudio = ref<{
|
||||
originUrl: string;
|
||||
cloneUrl: string;
|
||||
roleName: string;
|
||||
} | null>(null);
|
||||
|
||||
// 添加搜索相关变量
|
||||
const searchKeyword = ref("");
|
||||
|
||||
// 添加克隆状态变量
|
||||
const cloneStatus = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
handleReset();
|
||||
initSystemCharterDataSource();
|
||||
handleReset();
|
||||
initSystemCharterDataSource();
|
||||
});
|
||||
|
||||
watch(() => [paginationInfo.currentPage, paginationInfo.pageSize], () => {
|
||||
initSystemCharterDataSource()
|
||||
})
|
||||
watch(
|
||||
() => [paginationInfo.currentPage, paginationInfo.pageSize],
|
||||
() => {
|
||||
initSystemCharterDataSource();
|
||||
}
|
||||
);
|
||||
|
||||
const handleReset = () => {
|
||||
submitForm.value = {
|
||||
id: null,
|
||||
roleName: "",
|
||||
roleSetting: "", // 描述
|
||||
prologue: "", // 角色简介
|
||||
intro: "", // 开场白
|
||||
roleLines: [],
|
||||
bg: ''
|
||||
};
|
||||
submitForm.value = {
|
||||
id: null,
|
||||
roleName: "",
|
||||
roleSetting: "", // 描述
|
||||
prologue: "", // 角色简介
|
||||
intro: "", // 开场白
|
||||
roleLines: [],
|
||||
bg: "",
|
||||
};
|
||||
};
|
||||
|
||||
const addSystemCharter = () => {
|
||||
handleReset();
|
||||
isAdd.value = true;
|
||||
handleReset();
|
||||
isAdd.value = true;
|
||||
};
|
||||
|
||||
const initSystemCharterDataSource = () => {
|
||||
querySystemCharterAPI({ current: paginationInfo.currentPage, pageSize: paginationInfo.pageSize }).then((result) => {
|
||||
dataSource.value = result.data;
|
||||
count.value = result.meta.total;
|
||||
});
|
||||
const params = {
|
||||
current: paginationInfo.currentPage,
|
||||
pageSize: paginationInfo.pageSize,
|
||||
roleName: searchKeyword.value,
|
||||
originAudioUrl: cloneStatus.value ?? undefined, // 转换为布尔值或 undefined
|
||||
};
|
||||
|
||||
querySystemCharterAPI(params).then((result) => {
|
||||
dataSource.value = result.data;
|
||||
count.value = result.meta.total;
|
||||
});
|
||||
};
|
||||
|
||||
// 添加搜索处理方法
|
||||
const handleSearch = () => {
|
||||
paginationInfo.currentPage = 1; // 重置页码
|
||||
initSystemCharterDataSource();
|
||||
};
|
||||
|
||||
// 监听搜索关键词变化
|
||||
watch(
|
||||
() => searchKeyword.value,
|
||||
() => {
|
||||
handleSearch();
|
||||
}
|
||||
);
|
||||
|
||||
// 添加系统对话
|
||||
const addChartLines = () => {
|
||||
submitForm.value?.roleLines.push({
|
||||
sender_type: null,
|
||||
sender_name: null,
|
||||
text: "",
|
||||
});
|
||||
submitForm.value?.roleLines.push({
|
||||
sender_type: null,
|
||||
sender_name: null,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
// 选择了对话类型
|
||||
const roleLinesChange = (index) => {
|
||||
console.error(index);
|
||||
console.error(index);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
const _submitForm = JSON.parse(JSON.stringify(submitForm.value));
|
||||
_submitForm.roleLines = JSON.stringify(_submitForm.roleLines);
|
||||
if (!submitForm.value.id) {
|
||||
await addSystemCharterAPI(_submitForm);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
await editSystemCharterAPI(_submitForm);
|
||||
ElMessage.success("修改成功");
|
||||
}
|
||||
await formRef.value?.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
const _submitForm = JSON.parse(JSON.stringify(submitForm.value));
|
||||
_submitForm.roleLines = JSON.stringify(_submitForm.roleLines);
|
||||
if (!submitForm.value.id) {
|
||||
await addSystemCharterAPI(_submitForm);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
await editSystemCharterAPI(_submitForm);
|
||||
ElMessage.success("修改成功");
|
||||
}
|
||||
|
||||
initSystemCharterDataSource();
|
||||
isAdd.value = false;
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
initSystemCharterDataSource();
|
||||
isAdd.value = false;
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveSampleItem = (index) => {
|
||||
submitForm.value.roleLines.splice(index, 1);
|
||||
ElMessage.success("删除成功");
|
||||
submitForm.value.roleLines.splice(index, 1);
|
||||
ElMessage.success("删除成功");
|
||||
};
|
||||
|
||||
const handleEdit = (row) => {
|
||||
handleReset();
|
||||
const _currentRow = JSON.parse(JSON.stringify(row));
|
||||
Object.keys(submitForm.value).forEach((key) => {
|
||||
if (key === "roleLines") {
|
||||
submitForm.value[key] = JSON.parse(_currentRow[key]);
|
||||
} else {
|
||||
submitForm.value[key] = _currentRow[key];
|
||||
}
|
||||
});
|
||||
isAdd.value = true;
|
||||
const handleEdit = async (row: any) => {
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
// 先重置表单
|
||||
handleReset();
|
||||
// 获取详细数据
|
||||
const detail = await getSystemCharterDetailAPI(row.id);
|
||||
// 处理数据
|
||||
const detailData = detail.data;
|
||||
Object.keys(submitForm.value).forEach((key) => {
|
||||
if (key === "roleLines") {
|
||||
submitForm.value[key] = JSON.parse(detailData[key]);
|
||||
} else {
|
||||
submitForm.value[key] = detailData[key];
|
||||
}
|
||||
});
|
||||
isAdd.value = true;
|
||||
} catch (error) {
|
||||
ElMessage.error("获取详情失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
isAdd.value = false;
|
||||
isAdd.value = false;
|
||||
};
|
||||
|
||||
// 自动填充
|
||||
const handleAutoFormat = () => {
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
const object = JSON.parse(formatJson.value);
|
||||
const roleName = object.reply_constraints.sender_name;
|
||||
const roleSettingItem = object.bot_setting.find((item) => item.bot_name.trim() == roleName.trim());
|
||||
const sampleMessages = object.sample_messages.map((item) => {
|
||||
return {
|
||||
sender_type: item.sender_type,
|
||||
sender_name: item.sender_type == "BOT" ? roleName : item.sender_name,
|
||||
text: item.text,
|
||||
};
|
||||
});
|
||||
submitForm.value = {
|
||||
id: null,
|
||||
roleName: roleName,
|
||||
roleSetting: roleSettingItem.content, // 描述
|
||||
prologue: "", // 角色简介
|
||||
intro: "", // 开场白
|
||||
roleLines: sampleMessages,
|
||||
};
|
||||
formatJson.value = "";
|
||||
ElMessage.success("格式化成功");
|
||||
} catch {
|
||||
ElMessage.error("格式化失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
const loading = ElLoading.service();
|
||||
try {
|
||||
const object = JSON.parse(formatJson.value);
|
||||
const roleName = object.reply_constraints.sender_name;
|
||||
const roleSettingItem = object.bot_setting.find((item) => item.bot_name.trim() == roleName.trim());
|
||||
const sampleMessages = object.sample_messages.map((item) => {
|
||||
return {
|
||||
sender_type: item.sender_type,
|
||||
sender_name: item.sender_type == "BOT" ? roleName : item.sender_name,
|
||||
text: item.text,
|
||||
};
|
||||
});
|
||||
submitForm.value = {
|
||||
id: null,
|
||||
roleName: roleName,
|
||||
roleSetting: roleSettingItem.content, // 描述
|
||||
prologue: "", // 角色简介
|
||||
intro: "", // 开场白
|
||||
roleLines: sampleMessages,
|
||||
};
|
||||
formatJson.value = "";
|
||||
ElMessage.success("格式化成功");
|
||||
} catch {
|
||||
ElMessage.error("格式化失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUpload: UploadProps["beforeUpload"] = (rawFile) => {
|
||||
if (!(rawFile.type.indexOf('image') > -1)) {
|
||||
ElMessage.error("仅支持图片");
|
||||
return false;
|
||||
} else if (rawFile.size / 1024 / 1024 > 3) {
|
||||
ElMessage.error("文件大小不能超过3M");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
if (!(rawFile.type.indexOf("image") > -1)) {
|
||||
ElMessage.error("仅支持图片");
|
||||
return false;
|
||||
} else if (rawFile.size / 1024 / 1024 > 3) {
|
||||
ElMessage.error("文件大小不能超过3M");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFileUpload = async (params) => {
|
||||
const result = await (await Oss).put(`/system-character-bg/${params.file.name}`, params.file)
|
||||
submitForm.value.bg = result.url
|
||||
const oss = createOss();
|
||||
const result = await oss.put(`${OssPath.CHARACTER_BG}/${params.file.name}`, params.file);
|
||||
submitForm.value.bg = result.url;
|
||||
};
|
||||
|
||||
// 添加删除图片方法
|
||||
const handleRemoveImage = () => {
|
||||
submitForm.value.bg = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 修改克隆方法
|
||||
const handleClone = (item: any) => {
|
||||
currentCharacter.value = item;
|
||||
cloneDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const showTaskList = () => {
|
||||
taskListVisible.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.character-card {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.character-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-container {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 140%;
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-image-viewer__wrapper) {
|
||||
z-index: 2100;
|
||||
}
|
||||
|
||||
:deep(.el-image-viewer__img) {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
margin: -1rem -1rem 1rem -1rem;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.add-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.character-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: #909399;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.upload-preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.upload-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.upload-preview:hover .upload-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f8f9fa;
|
||||
z-index: 1;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dialog-list {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.dialog-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dialog-section::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dialog-section::-webkit-scrollbar-thumb {
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dialog-section::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* :deep(.el-textarea__inner) {
|
||||
min-height: 32px !important;
|
||||
resize: none;
|
||||
line-height: 1.5;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner:focus) {
|
||||
box-shadow: none;
|
||||
} */
|
||||
|
||||
.dialog-item .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-item .el-col:last-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.format-card {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
.format-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.format-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 可选:添加搜索框样式 */
|
||||
.el-input {
|
||||
--el-input-height: 36px;
|
||||
}
|
||||
|
||||
/* 调整下拉框样式 */
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* 添加剩余次数样式 */
|
||||
.remaining-count {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
class AudioManager {
|
||||
private static currentAudio: HTMLAudioElement | null = null;
|
||||
|
||||
public static play(audio: HTMLAudioElement) {
|
||||
// 如果有其他音频在播放,先停止它
|
||||
if (this.currentAudio && this.currentAudio !== audio && !this.currentAudio.paused) {
|
||||
this.currentAudio.pause();
|
||||
this.currentAudio.currentTime = 0;
|
||||
}
|
||||
// 设置当前音频
|
||||
this.currentAudio = audio;
|
||||
}
|
||||
|
||||
public static stopAll() {
|
||||
if (this.currentAudio && !this.currentAudio.paused) {
|
||||
this.currentAudio.pause();
|
||||
this.currentAudio.currentTime = 0;
|
||||
}
|
||||
this.currentAudio = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioManager;
|
|
@ -0,0 +1,23 @@
|
|||
// 任务状态
|
||||
export const TaskStatus = {
|
||||
PENDING: "pending",
|
||||
PROCESSING: "processing",
|
||||
COMPLETED: "completed",
|
||||
FAILED: "failed",
|
||||
} as const;
|
||||
|
||||
// 任务状态类型映射
|
||||
export const TaskStatusType = {
|
||||
[TaskStatus.PENDING]: "warning",
|
||||
[TaskStatus.PROCESSING]: "primary",
|
||||
[TaskStatus.COMPLETED]: "success",
|
||||
[TaskStatus.FAILED]: "danger",
|
||||
} as const;
|
||||
|
||||
// 任务状态文本映射
|
||||
export const TaskStatusText = {
|
||||
[TaskStatus.PENDING]: "等待中",
|
||||
[TaskStatus.PROCESSING]: "任务处理中",
|
||||
[TaskStatus.COMPLETED]: "已完成",
|
||||
[TaskStatus.FAILED]: "任务失败",
|
||||
} as const;
|
|
@ -1,10 +1,18 @@
|
|||
import aliOss from 'ali-oss'
|
||||
import aliOss from "ali-oss";
|
||||
|
||||
const Oss = new aliOss({
|
||||
export enum OssPath {
|
||||
CHARACTER_BG = "system-character-bg",
|
||||
CLONE_VOICE = "system-character-speak",
|
||||
}
|
||||
|
||||
const createOss = (bucket = "stayby-static") => {
|
||||
return new aliOss({
|
||||
region: "oss-cn-shanghai",
|
||||
accessKeyId: "LTAI5tEday8PJNaMTz5mp8g4",
|
||||
accessKeySecret: "ck84eTxx4aSTjornlYrCy8RkurCHfc",
|
||||
bucket: "stayby-static",
|
||||
bucket,
|
||||
secure: true,
|
||||
});
|
||||
export default Oss
|
||||
});
|
||||
};
|
||||
|
||||
export default createOss;
|
||||
|
|
|
@ -8,7 +8,7 @@ export let isRelogin = { show: false };
|
|||
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000,
|
||||
timeout: 1000 * 30,
|
||||
});
|
||||
|
||||
// request拦截器
|
||||
|
@ -73,8 +73,6 @@ service.interceptors.response.use(
|
|||
(error) => {
|
||||
console.error(error);
|
||||
let { message, code } = error.response.data;
|
||||
if (code == 401) {
|
||||
}
|
||||
if (message == "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message.includes("timeout")) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"root":["./src/global.d.ts","./src/main.ts","./src/vite-env.d.ts","./src/api/systemcharter/index.ts","./src/app.vue","./src/components/helloworld.vue","./src/pages/systemcharter/index.vue"],"version":"5.6.2"}
|
||||
{"root":["./src/global.d.ts","./src/main.ts","./src/vite-env.d.ts","./src/api/systemcharter/index.ts","./src/utils/audiomanager.ts","./src/utils/dict.ts","./src/utils/oss.ts","./src/app.vue","./src/components/helloworld.vue","./src/pages/systemcharter/clonehistory.vue","./src/pages/systemcharter/clonespeak.vue","./src/pages/systemcharter/previewvoice.vue","./src/pages/systemcharter/tasklist.vue","./src/pages/systemcharter/index.vue"],"errors":true,"version":"5.6.2"}
|
Loading…
Reference in New Issue