index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <template>
  2. <fs-page>
  3. <!-- 添加一个容器来分割页面 -->
  4. <div class="digital-human-container">
  5. <!-- 左侧列表区域 -->
  6. <div class="list-area">
  7. <fs-crud ref="crudRef" v-bind="crudBinding" @add-digital-human="openAddForm" @edit-digital-human="openEditForm">
  8. <!-- 自定义行操作按钮 -->
  9. <template #cell-rowHandle-middle="scope">
  10. <!-- 查看按钮 -->
  11. <el-button
  12. size="small"
  13. type="text"
  14. @click="openViewForm(scope.row)"
  15. >
  16. 查看<el-icon><View /></el-icon>
  17. </el-button>
  18. <!-- 编辑按钮 -->
  19. <el-button
  20. size="small"
  21. type="text"
  22. @click="openEditForm(scope.row)"
  23. >
  24. 编辑<el-icon><Edit /></el-icon>
  25. </el-button>
  26. <!-- 删除按钮 -->
  27. <el-button
  28. size="small"
  29. type="text"
  30. @click="handleDelete(scope.row)"
  31. class="text-danger"
  32. >
  33. 删除<el-icon><Delete /></el-icon>
  34. </el-button>
  35. </template>
  36. </fs-crud>
  37. </div>
  38. <!-- 将侧边栏表单改为弹窗 -->
  39. <el-dialog
  40. v-model="showForm"
  41. :title="getDialogTitle()"
  42. width="500px"
  43. :close-on-click-modal="false"
  44. :destroy-on-close="true"
  45. >
  46. <!-- 查看模式使用 el-descriptions 展示数据 -->
  47. <el-descriptions v-if="formMode === 'view'" :column="1" border>
  48. <el-descriptions-item label="名称">{{ formData.name }}</el-descriptions-item>
  49. <el-descriptions-item label="头像">
  50. <img v-if="formData.avatar_url" :src="formData.avatar_url" class="avatar" />
  51. <span v-else>无头像</span>
  52. </el-descriptions-item>
  53. <el-descriptions-item label="模板视频">
  54. <video
  55. v-if="formData.template_video_url"
  56. :src="formData.template_video_url"
  57. class="video-preview"
  58. controls>
  59. </video>
  60. <span v-else>无视频</span>
  61. </el-descriptions-item>
  62. <el-descriptions-item label="状态">
  63. <el-tag :type="formData.status === 1 ? 'success' : 'info'">
  64. {{ formData.status === 1 ? '启用' : '禁用' }}
  65. </el-tag>
  66. </el-descriptions-item>
  67. <el-descriptions-item label="语音ID">
  68. {{ getVoiceName(formData.config.voice_type) }}
  69. </el-descriptions-item>
  70. <el-descriptions-item label="风格">
  71. {{ getStyleName(formData.config.style) }}
  72. </el-descriptions-item>
  73. <el-descriptions-item label="描述">{{ formData.description || '无' }}</el-descriptions-item>
  74. </el-descriptions>
  75. <!-- 添加/编辑表单内容 -->
  76. <el-form v-else ref="formRef" :model="formData" label-width="100px" class="form-content">
  77. <el-form-item label="名称" prop="name" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]">
  78. <el-input v-model="formData.name" placeholder="请输入数字人名称"></el-input>
  79. </el-form-item>
  80. <el-form-item label="头像" prop="avatar_url" :rules="[{ required: true, message: '请上传头像', trigger: 'change' }]">
  81. <el-upload
  82. class="avatar-uploader"
  83. :action="`${apiBaseUrl}/api/system/admin_upload/`"
  84. :headers="{
  85. 'Authorization': `JWT ${Session.get('token') || ''}`
  86. }"
  87. :data="{
  88. tenant_id: '1'
  89. }"
  90. :show-file-list="false"
  91. :on-success="handleAvatarSuccess"
  92. :before-upload="beforeAvatarUpload">
  93. <img v-if="formData.avatar_url" :src="formData.avatar_url" class="avatar" />
  94. <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  95. </el-upload>
  96. </el-form-item>
  97. <el-form-item label="模板视频" prop="template_video_url">
  98. <el-upload
  99. class="video-uploader"
  100. :action="`${apiBaseUrl}/api/system/admin_upload/`"
  101. :headers="{
  102. 'Authorization': `JWT ${Session.get('token') || ''}`
  103. }"
  104. :data="{
  105. tenant_id: '1'
  106. }"
  107. :show-file-list="false"
  108. :on-success="handleVideoSuccess"
  109. :before-upload="beforeVideoUpload"
  110. accept="video/*">
  111. <video
  112. v-if="formData.template_video_url"
  113. :src="formData.template_video_url"
  114. class="video-preview"
  115. controls>
  116. </video>
  117. <div v-else class="video-uploader-placeholder">
  118. <el-icon class="video-uploader-icon"><Plus /></el-icon>
  119. <div>点击上传视频</div>
  120. </div>
  121. </el-upload>
  122. </el-form-item>
  123. <el-form-item label="状态" prop="status">
  124. <el-switch v-model="formData.status" :active-value="1" :inactive-value="0"></el-switch>
  125. </el-form-item>
  126. <el-form-item label="语音ID" prop="config.voice_type">
  127. <el-select v-model="formData.config.voice_type" placeholder="请选择语音">
  128. <el-option v-for="(item,index) in voiceList" :key="index" :label="item.label" :value="item.value"></el-option>
  129. <!-- 可以根据实际情况添加更多选项 -->
  130. </el-select>
  131. </el-form-item>
  132. <el-form-item label="风格" prop="config.style">
  133. <el-select v-model="formData.config.style" placeholder="请选择风格">
  134. <el-option label="专业" value="professional"></el-option>
  135. <el-option label="友好" value="friendly"></el-option>
  136. <el-option label="严肃" value="serious"></el-option>
  137. <!-- 可以根据实际情况添加更多选项 -->
  138. </el-select>
  139. </el-form-item>
  140. <el-form-item label="描述" prop="description">
  141. <el-input v-model="formData.description" type="textarea" placeholder="请输入描述"></el-input>
  142. </el-form-item>
  143. </el-form>
  144. <template #footer>
  145. <div class="dialog-footer">
  146. <el-button @click="closeForm">{{ formMode === 'view' ? '关闭' : '取消' }}</el-button>
  147. <el-button v-if="formMode !== 'view'" type="primary" @click="submitForm">提交</el-button>
  148. </div>
  149. </template>
  150. </el-dialog>
  151. </div>
  152. </fs-page>
  153. </template>
  154. <script lang="ts" setup name="areas">
  155. import { onMounted, ref, reactive } from 'vue';
  156. import { useFs } from '@fast-crud/fast-crud';
  157. import { createCrudOptions } from './crud';
  158. import { GetPermission, AddObj, UpdateObj, DelObj,GetVoiceList } from './api';
  159. import * as api from './api'; // Import the full api module
  160. import { handleColumnPermission } from '/@/utils/columnPermission';
  161. import { ElMessage, ElMessageBox } from 'element-plus';
  162. import { Close, Plus, Edit, Delete, View } from '@element-plus/icons-vue';
  163. import { Session } from '/@/utils/storage';
  164. const apiBaseUrl = import.meta.env.VITE_API_URL;
  165. const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
  166. // 表单模式:add 或 edit
  167. const formMode = ref('add');
  168. const showForm = ref(false);
  169. const defaultFormData = {
  170. name: '',
  171. description: '',
  172. avatar_url: '',
  173. status: 1,
  174. template_video_url: '',
  175. config: {
  176. voice_type: '',
  177. style: 'professional'
  178. }
  179. };
  180. const formData = ref({...defaultFormData});
  181. // Add formRef reference
  182. const formRef = ref();
  183. // 打开添加表单
  184. const openAddForm = () => {
  185. formMode.value = 'add';
  186. formData.value = {...defaultFormData};
  187. showForm.value = true;
  188. };
  189. // 打开编辑表单
  190. const openEditForm = (row: any) => {
  191. console.log('编辑行数据:', row);
  192. formMode.value = 'edit';
  193. // 深拷贝行数据
  194. const rowData = JSON.parse(JSON.stringify(row));
  195. // 确保 config 是对象格式
  196. if (typeof rowData.config === 'string') {
  197. try {
  198. rowData.config = JSON.parse(rowData.config);
  199. } catch (e) {
  200. console.error('解析config失败:', e);
  201. rowData.config = {...defaultFormData.config};
  202. }
  203. } else if (!rowData.config) {
  204. rowData.config = {...defaultFormData.config};
  205. }
  206. formData.value = rowData;
  207. showForm.value = true;
  208. };
  209. // 打开查看表单(只读模式)
  210. const openViewForm = (row: any) => {
  211. console.log('查看行数据:', row);
  212. formMode.value = 'view';
  213. // 深拷贝行数据
  214. const rowData = JSON.parse(JSON.stringify(row));
  215. // 确保 config 是对象格式
  216. if (typeof rowData.config === 'string') {
  217. try {
  218. rowData.config = JSON.parse(rowData.config);
  219. } catch (e) {
  220. console.error('解析config失败:', e);
  221. rowData.config = {...defaultFormData.config};
  222. }
  223. } else if (!rowData.config) {
  224. rowData.config = {...defaultFormData.config};
  225. }
  226. formData.value = rowData;
  227. showForm.value = true;
  228. };
  229. // 关闭表单
  230. const closeForm = () => {
  231. showForm.value = false;
  232. };
  233. // 头像上传成功处理
  234. const handleAvatarSuccess = (res: any, file: any) => {
  235. // 根据实际接口返回格式调整
  236. console.log(res);
  237. formData.value.avatar_url = res.url;
  238. };
  239. // 头像上传前验证
  240. const beforeAvatarUpload = (file: any) => {
  241. const isImage = file.type.startsWith('image/');
  242. const isLt2M = file.size / 1024 / 1024 < 2;
  243. if (!isImage) {
  244. ElMessage.error('上传头像图片只能是图片格式!');
  245. }
  246. if (!isLt2M) {
  247. ElMessage.error('上传头像图片大小不能超过 2MB!');
  248. }
  249. return isImage && isLt2M;
  250. };
  251. // 视频上传成功处理
  252. const handleVideoSuccess = (res: any, file: any) => {
  253. // 根据实际接口返回格式调整
  254. console.log(res);
  255. formData.value.template_video_url = res.url;
  256. };
  257. // 视频上传前验证
  258. const beforeVideoUpload = (file: any) => {
  259. const isVideo = file.type.startsWith('video/');
  260. const isLt50M = file.size / 1024 / 1024 < 50;
  261. if (!isVideo) {
  262. ElMessage.error('请上传视频格式文件!');
  263. }
  264. if (!isLt50M) {
  265. ElMessage.error('上传视频大小不能超过 50MB!');
  266. }
  267. return isVideo && isLt50M;
  268. };
  269. // 提交表单
  270. const submitForm = async () => {
  271. try {
  272. await formRef.value.validate();
  273. // 处理配置对象
  274. const submitData = {...formData.value};
  275. if (submitData.config && typeof submitData.config === 'object') {
  276. // 确保 config 符合类型要求
  277. const config = {
  278. voice_type: submitData.config.voice_type || '',
  279. style: submitData.config.style || ''
  280. };
  281. // 将config转为字符串
  282. submitData.config = JSON.stringify(config) as any; // 使用类型断言解决类型错误
  283. }
  284. // 在发送请求前,确保config是字符串类型
  285. if (formMode.value === 'add') {
  286. await AddObj(submitData);
  287. ElMessage.success('添加成功');
  288. } else {
  289. await UpdateObj(submitData);
  290. ElMessage.success('更新成功');
  291. }
  292. closeForm();
  293. crudExpose.doRefresh();
  294. } catch (error) {
  295. console.error('表单提交错误:', error);
  296. ElMessage.error('提交失败,请检查表单信息');
  297. }
  298. };
  299. // 处理删除
  300. const handleDelete = (row: any) => {
  301. ElMessageBox.confirm(
  302. `确定要删除数字人 "${row.name}" 吗?`,
  303. '删除确认',
  304. {
  305. confirmButtonText: '确定',
  306. cancelButtonText: '取消',
  307. type: 'warning',
  308. }
  309. )
  310. .then(async () => {
  311. try {
  312. await DelObj(row.id);
  313. ElMessage.success('删除成功');
  314. crudExpose.doRefresh();
  315. } catch (error) {
  316. console.error('删除失败:', error);
  317. ElMessage.error('删除失败,请稍后重试');
  318. }
  319. })
  320. .catch(() => {
  321. // 用户取消删除操作
  322. });
  323. };
  324. // Define the fetchData function
  325. const fetchData = () => {
  326. crudExpose.doRefresh();
  327. };
  328. const voiceList = ref([
  329. { label: '龙小夏', value: 'longxiaoxia' },
  330. { label: '龙小春', value: 'longxiaochun' },
  331. { label: '龙小瑶', value: 'longxiaoyao' },
  332. { label: '龙小婧', value: 'longxiaojing' },
  333. { label: '龙青周', value: 'longqingzhou' },
  334. { label: '女声清新风格', value: 'female1' },
  335. { label: '女声稳重风格', value: 'female2' },
  336. { label: '男声清新风格', value: 'male1' },
  337. { label: '男声稳重风格', value: 'male2' },
  338. ]);
  339. /* 获取语音列表 */
  340. const getVoiceList = async () => {
  341. /* const response = await GetVoiceList();
  342. voiceList.value = response.data; */
  343. };
  344. // 页面打开后获取列表数据
  345. onMounted(async () => {
  346. // 设置列权限
  347. const newOptions = await handleColumnPermission(GetPermission, crudOptions);
  348. // 配置新增按钮,点击时打开右侧表单而不是弹窗
  349. if (newOptions.actionbar?.buttons?.add) {
  350. newOptions.actionbar.buttons.add.click = () => {
  351. openAddForm();
  352. };
  353. }
  354. //重置crudBinding
  355. resetCrudOptions(newOptions);
  356. // 刷新
  357. crudExpose.doRefresh();
  358. getVoiceList();
  359. });
  360. // 获取对话框标题
  361. const getDialogTitle = () => {
  362. if (formMode.value === 'add') return '新增数字人';
  363. if (formMode.value === 'edit') return '编辑数字人';
  364. return '查看数字人';
  365. };
  366. // 语音ID映射
  367. const voiceOptions = [
  368. { label: '小明(高级)', value: 'xiaoming_voice_premium' },
  369. { label: '小红', value: 'xiaohong_voice' }
  370. ];
  371. // 风格映射
  372. const styleOptions = [
  373. { label: '专业', value: 'professional' },
  374. { label: '友好', value: 'friendly' },
  375. { label: '严肃', value: 'serious' }
  376. ];
  377. // 获取语音名称
  378. const getVoiceName = (voiceId: string) => {
  379. const option = voiceOptions.find(item => item.value === voiceId);
  380. return option ? option.label : voiceId;
  381. };
  382. // 获取风格名称
  383. const getStyleName = (style: string) => {
  384. const option = styleOptions.find(item => item.value === style);
  385. return option ? option.label : style;
  386. };
  387. </script>
  388. <style scoped>
  389. .digital-human-container {
  390. display: flex;
  391. height: 100%;
  392. }
  393. .list-area {
  394. flex: 1;
  395. overflow: auto;
  396. }
  397. /* 移除侧边栏相关样式,添加弹窗相关样式 */
  398. .avatar-uploader {
  399. text-align: center;
  400. border: 1px dashed #d9d9d9;
  401. border-radius: 6px;
  402. cursor: pointer;
  403. position: relative;
  404. overflow: hidden;
  405. transition: border-color 0.3s;
  406. }
  407. .avatar-uploader:hover {
  408. border-color: #409EFF;
  409. }
  410. .avatar-uploader-icon {
  411. font-size: 28px;
  412. color: #8c939d;
  413. width: 100px;
  414. height: 100px;
  415. line-height: 100px;
  416. text-align: center;
  417. }
  418. .avatar {
  419. width: 100px;
  420. height: 100px;
  421. display: block;
  422. object-fit: cover;
  423. }
  424. .dialog-footer {
  425. text-align: right;
  426. }
  427. .video-uploader {
  428. text-align: center;
  429. border: 1px dashed #d9d9d9;
  430. border-radius: 6px;
  431. cursor: pointer;
  432. position: relative;
  433. overflow: hidden;
  434. transition: border-color 0.3s;
  435. }
  436. .video-uploader:hover {
  437. border-color: #409EFF;
  438. }
  439. .video-uploader-icon {
  440. font-size: 28px;
  441. color: #8c939d;
  442. width: 100px;
  443. height: 100px;
  444. line-height: 100px;
  445. text-align: center;
  446. }
  447. .video-preview {
  448. width: 200px;
  449. height: auto;
  450. max-height: 150px;
  451. display: block;
  452. object-fit: contain;
  453. }
  454. .video-uploader-placeholder {
  455. text-align: center;
  456. padding: 20px;
  457. border: 1px dashed #d9d9d9;
  458. border-radius: 6px;
  459. }
  460. /* 添加删除按钮的危险样式 */
  461. .text-danger {
  462. color: #F56C6C;
  463. }
  464. .text-danger:hover {
  465. color: #f78989;
  466. }
  467. /* 添加查看模式的样式 */
  468. .el-descriptions {
  469. margin-bottom: 20px;
  470. }
  471. </style>