RoleManagement.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. <template>
  2. <div class="role-management">
  3. <div class="header">
  4. <h2>Role Management</h2>
  5. <div class="header-actions">
  6. <el-upload
  7. class="import-btn"
  8. action="javascript:;"
  9. :show-file-list="false"
  10. :before-upload="handleExcelUpload"
  11. accept=".xlsx,.xls"
  12. >
  13. <el-button type="text" plain>
  14. <el-icon><Upload /></el-icon>Import
  15. </el-button>
  16. </el-upload>
  17. <!-- <el-button type="primary" @click="getRoleData">-->
  18. <!-- select Role-->
  19. <!-- </el-button>-->
  20. <el-button type="primary" @click="showAddDialog">
  21. Create Role
  22. </el-button>
  23. </div>
  24. </div>
  25. <el-table :data="roles" v-loading="loading" style="width: 100%" class="role-table">
  26. <el-table-column label="Role" min-width="100">
  27. <template #default="scope">
  28. <div class="role-info">
  29. <el-avatar :src="scope.row.avatar" v-if="scope.row.avatar"></el-avatar>
  30. <el-avatar v-else>{{ scope.row.name.charAt(0) }}</el-avatar>
  31. <div class="role-details">
  32. <div class="role-name">{{ scope.row.name }}</div>
  33. <div class="role-id" hidden>ID: {{ scope.row.id }}</div>
  34. </div>
  35. </div>
  36. </template>
  37. </el-table-column>
  38. <!-- <el-table-column prop="description" label="Persona" min-width="200" />-->
  39. <el-table-column prop="callings" label="callings" width="200">
  40. <template #default="scope">
  41. {{ scope.row.callings }}
  42. </template>
  43. </el-table-column>
  44. <el-table-column prop="prompt" label="prompt" width="200">
  45. <template #default="scope">
  46. {{ scope.row.prompt }}
  47. </template>
  48. </el-table-column>
  49. <!-- <el-table-column prop="gender" label="Gender" width="100">-->
  50. <!-- <template #default="scope">-->
  51. <!-- {{ scope.row.gender === 'male' ? 'Male' : scope.row.gender === 'female' ? 'Female' : 'Other' }}-->
  52. <!-- </template>-->
  53. <!-- </el-table-column>-->
  54. <el-table-column prop="language" label="Language" width="100">
  55. <template #default="scope">
  56. {{ scope.row.language === 'zh' ? 'Chinese' :
  57. scope.row.language === 'en' ? 'English' :
  58. scope.row.language === 'jp' ? 'Japanese' :
  59. scope.row.language === 'kr' ? 'Korean' : scope.row.language }}
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="USE VOICE" width="150">
  63. <template #default="scope">
  64. <div class="voice-details">
  65. <div class="voice-name">{{ scope.row.voiceName }}</div>
  66. <div class="voice-id" hidden>ID: {{ scope.row.voiceId }}</div>
  67. </div>
  68. </template>
  69. </el-table-column>
  70. <el-table-column label="Actions" width="150">
  71. <template #default="scope">
  72. <div class="action-buttons">
  73. <el-button type="text" @click="editRole(scope.row)">
  74. <el-icon><Edit /></el-icon>
  75. </el-button>
  76. <!-- <el-button type="primary" @click="editRole(scope.row)">-->
  77. <!-- 推荐-->
  78. <!-- </el-button>-->
  79. <el-button type="primary" @click="changeTopFlag(scope.row);onPageChange(currentPage);">
  80. {{ scope.row.topFlag ===true ? '取消推荐': '推荐' }}
  81. </el-button>
  82. <!-- <el-button type="text" class="delete-btn" @click="confirmDelete(scope.row)">-->
  83. <!-- <el-icon><Delete /></el-icon>-->
  84. <!-- </el-button>-->
  85. </div>
  86. </template>
  87. </el-table-column>
  88. </el-table>
  89. <!-- 导入确认对话框 -->
  90. <el-dialog
  91. title="导入角色"
  92. v-model="importDialogVisible"
  93. width="500px"
  94. >
  95. <div v-if="importFileName" class="import-file-info">
  96. 即将导入文件: {{ importFileName }}
  97. </div>
  98. <div v-else>
  99. 请先选择Excel文件
  100. </div>
  101. <template #footer>
  102. <el-button @click="importDialogVisible = false">取消</el-button>
  103. <el-button
  104. type="primary"
  105. @click="confirmImport"
  106. :disabled="!importFileName"
  107. >
  108. 确认导入
  109. </el-button>
  110. </template>
  111. </el-dialog>
  112. <!-- Add/Edit Role Dialog -->
  113. <el-dialog
  114. :title="dialogType === 'add' ? 'Create New Role' : 'Edit Role'"
  115. v-model="dialogVisible"
  116. width="500px"
  117. class="role-dialog"
  118. >
  119. <el-form
  120. ref="voiceFormRef"
  121. :model="roleForm"
  122. :rules="rules"
  123. label-width="120px"
  124. >
  125. <el-form-item label="role callings" prop="name">
  126. <el-input v-model="roleForm.callings" placeholder="Enter callings" />
  127. </el-form-item>
  128. </el-form>
  129. <template #footer>
  130. <el-button @click="dialogVisible = false">Cancel</el-button>
  131. <el-button type="primary" @click="updateCallings(roleForm.id,roleForm.callings); onPageChange(currentPage);dialogVisible = false">
  132. {{ dialogType === 'add' ? 'Create' : 'Save Changes' }}
  133. </el-button>
  134. </template>
  135. </el-dialog>
  136. <!-- Add Dialog -->
  137. <el-dialog
  138. :title="'Create New Role'"
  139. v-model="addDialogVisible"
  140. width="500px"
  141. class="role-dialog"
  142. >
  143. <el-form
  144. :model="roleForm"
  145. :rules="rules"
  146. label-width="120px"
  147. >
  148. <el-form-item label="name" prop="name">
  149. <el-input v-model="roleForm.name" placeholder="Enter name" />
  150. </el-form-item>
  151. <el-form-item label="prompt" prop="prompt">
  152. <el-input v-model="roleForm.prompt" placeholder="Enter prompt" />
  153. </el-form-item>
  154. <el-form-item label="Photo" prop="Photo">
  155. <el-input v-model="roleForm.photo" placeholder="Enter Photo" />
  156. </el-form-item>
  157. <el-form-item label="Language" prop="Language">
  158. <el-input v-model="roleForm.language" placeholder="Enter Language" />
  159. </el-form-item>
  160. <el-form-item label="照片文件">
  161. <el-upload
  162. class="avatar-uploader"
  163. action=""
  164. :auto-upload="false"
  165. :on-change="handlePhotoUpload"
  166. accept="image/*"
  167. >
  168. <el-button size="small" type="primary">
  169. <el-icon><Upload /></el-icon> 上传照片
  170. </el-button>
  171. <div v-if="photoFileName" class="photo-file-name">{{ photoFileName }}</div>
  172. </el-upload>
  173. </el-form-item>
  174. <el-form-item label="声音文件">
  175. <el-upload
  176. class="voice-uploader"
  177. action=""
  178. :auto-upload="false"
  179. :on-change="handleVoiceUpload"
  180. accept="audio/*"
  181. >
  182. <el-button size="small" type="primary">
  183. <el-icon><Upload /></el-icon> 上传声音文件
  184. </el-button>
  185. <div v-if="voiceFileName" class="voice-file-name">{{ voiceFileName }}</div>
  186. </el-upload>
  187. </el-form-item>
  188. </el-form>
  189. <template #footer>
  190. <el-button @click="addDialogVisible = false">Cancel</el-button>
  191. <el-button type="primary" @click="addCallings(roleForm.id,roleForm.callings); onPageChange(currentPage); addDialogVisible = false">
  192. {{ 'Save Changes' }}
  193. </el-button>
  194. </template>
  195. </el-dialog>
  196. <Pagination
  197. :current-page="currentPage"
  198. :per-page="perPage"
  199. :total-items="totalItems"
  200. :max-displayed-pages="maxDisplayedPages"
  201. :show-info="true"
  202. @page-changed="onPageChange"
  203. />
  204. </div>
  205. </template>
  206. <script>
  207. import { Edit, Delete, Upload, Headset, CircleClose, Loading } from '@element-plus/icons-vue'
  208. import { ElMessage, ElMessageBox } from 'element-plus'
  209. import { useStore } from '../store'
  210. import anycallService from '../service/anycallService.js';
  211. import Pagination from './util/Pagination.vue'
  212. import cdnService from "../service/cdnService";
  213. export default {
  214. name: 'RoleManagement',
  215. components: {
  216. Pagination,
  217. Edit,
  218. Delete,
  219. Upload,
  220. Headset,
  221. CircleClose,
  222. Loading
  223. },
  224. data() {
  225. const store = useStore()
  226. return {
  227. currentPage: 1,
  228. perPage: 10,
  229. totalItems: store.rolesCount,
  230. maxDisplayedPages: 5,
  231. file: null,
  232. roles: store.roles,
  233. voices: store.voices,
  234. loading: false,
  235. dialogVisible: false,
  236. addDialogVisible: false,
  237. dialogType: 'add', // 'add' or 'edit'
  238. importDialogVisible: false,
  239. importFileName: '',
  240. roleForm: {
  241. id: '',
  242. name: '',
  243. description: '',
  244. gender: '',
  245. language: '',
  246. avatar: '',
  247. voiceId: '',
  248. photoFileName: '',
  249. voiceFileName: '',
  250. clonedVoiceFileName: '',
  251. isCloning: false,
  252. clonedVoice: false
  253. },
  254. currentRole: null,
  255. rules: {
  256. name: [
  257. {required: true, message: '请输入角色名称', trigger: 'blur'}
  258. ],
  259. description: [
  260. {required: true, message: '请输入角色描述', trigger: 'blur'}
  261. ],
  262. gender: [
  263. {required: true, message: '请选择性别', trigger: 'change'}
  264. ],
  265. language: [
  266. {required: true, message: '请选择语言', trigger: 'change'}
  267. ],
  268. voiceFileName: [
  269. {required: true, message: '请上传原始声音文件', trigger: 'blur'}
  270. ]
  271. }
  272. };
  273. },
  274. methods: {
  275. handlePhotoUpload(file) {
  276. // cdnService.uploadVoice(file);
  277. this.file = file;
  278. let formData = {
  279. file: this.file.raw
  280. }
  281. let response = cdnService.upload(formData);
  282. console.log(response.data);
  283. // this.photoFileName = file.name;
  284. // todo 实际项目中应上传文件到服务器
  285. },
  286. handleVoiceUpload(file) {
  287. let response = cdnService.uploadVoice(file);
  288. console.log(response.data);
  289. // this.voiceFileName = file.name;
  290. // todo 实际项目中应上传文件到服务器
  291. },
  292. async onPageChange(page) {
  293. this.currentPage = page
  294. // 这里可以添加加载数据的逻辑
  295. let pageData = {
  296. page: page,
  297. size: this.perPage,
  298. name: ""
  299. };
  300. const response = await anycallService.anycallPage(pageData);
  301. this.roles = response.data.data.content.map(item => ({
  302. id: item.id || '',
  303. name: item.name || '',
  304. prompt: item.prompt || '',
  305. callings: item.callings || '',
  306. language: item.language || '',
  307. photo: item.photo || '',
  308. voice: item.voice || '',
  309. voiceName: item.voiceName || '',
  310. topFlag: item.topFlag || '',
  311. }));
  312. this.rolesCount = response.data.data.total;
  313. },
  314. async changeTopFlag(row) {
  315. // console.log(row.id);
  316. // console.log(row.id);
  317. // console.log(row.topFlag !== true);
  318. let data = {
  319. cloneId: row.id,
  320. topFlag: row.topFlag !== true
  321. };
  322. console.log(data);
  323. const response = await anycallService.updateTopFlag(data);
  324. ElMessage.success('推荐状态修改成功')
  325. },
  326. async addCallings(id,callings){
  327. let data = {
  328. cloneId: id,
  329. callings: callings
  330. };
  331. // const response = await anycallService.updateCallings(data);
  332. ElMessage.success('添加成功')
  333. const store = useStore()
  334. this.roles = store.roles
  335. },
  336. async updateCallings(id,callings){
  337. let data = {
  338. cloneId: id,
  339. callings: callings
  340. };
  341. const response = await anycallService.updateCallings(data);
  342. ElMessage.success('callings修改成功')
  343. const store = useStore()
  344. this.roles = store.roles
  345. },
  346. // 处理Excel上传
  347. handleExcelUpload(file) {
  348. this.importFileName = file.name;
  349. this.importDialogVisible = true;
  350. return false; // 阻止默认上传
  351. },
  352. // 确认导入
  353. confirmImport() {
  354. // 模拟导入处理
  355. ElMessage.success(`文件 ${this.importFileName} 导入成功`);
  356. this.importDialogVisible = false;
  357. this.importFileName = '';
  358. // 实际项目中应解析Excel文件并添加角色
  359. },
  360. // 上传前检查头像
  361. beforeUploadAvatar(file) {
  362. const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type) ||
  363. file.name.endsWith('.jpg') || file.name.endsWith('.jpeg') ||
  364. file.name.endsWith('.png') || file.name.endsWith('.gif');
  365. const isLt5M = file.size / 1024 / 1024 < 5;
  366. if (!isImage) {
  367. ElMessage.error('请上传图片格式文件!');
  368. }
  369. if (!isLt5M) {
  370. ElMessage.error('上传文件大小不能超过5MB!');
  371. }
  372. return isImage && isLt5M;
  373. },
  374. // 上传前检查声音文件 - 只接受原始声音源文件
  375. beforeUploadVoice(file) {
  376. const isAudio = ['audio/mpeg', 'audio/wav', 'audio/mp3'].includes(file.type) || file.name.endsWith('.mp3') || file.name.endsWith('.wav');
  377. const isLt20M = file.size / 1024 / 1024 < 20;
  378. if (!isAudio) {
  379. ElMessage.error('请上传音频格式文件!');
  380. }
  381. if (!isLt20M) {
  382. ElMessage.error('上传文件大小不能超过20MB!');
  383. }
  384. return isAudio && isLt20M;
  385. },
  386. // 处理声音上传成功
  387. handleVoiceUploadSuccess(file) {
  388. // 模拟上传成功,实际应用中应该根据服务器返回设置文件信息
  389. this.roleForm.voiceFileName = file.name;
  390. ElMessage.success('原始声音文件上传成功');
  391. // 模拟系统自动克隆过程
  392. this.roleForm.isCloning = true;
  393. // 模拟克隆过程的延迟
  394. setTimeout(() => {
  395. // 生成克隆文件名
  396. const originalFileName = file.name;
  397. const clonedFileName = originalFileName.replace('original-', 'cloned-');
  398. // 如果不在表单提交过程中,直接更新表单状态
  399. if (!this.dialogVisible) {
  400. this.roleForm.clonedVoiceFileName = clonedFileName;
  401. this.roleForm.isCloning = false;
  402. this.roleForm.clonedVoice = true;
  403. ElMessage.success('已从原始声音源文件成功克隆声音');
  404. }
  405. // 如果是在表单提交过程中,更新声音数据中的克隆状态
  406. else if (this.roleForm.voiceId) {
  407. const store = useStore();
  408. store.updateVoice(this.roleForm.voiceId, {
  409. clonedVoice: clonedFileName,
  410. isCloning: false
  411. });
  412. // 刷新本地数据
  413. this.voices = store.voices;
  414. this.roles = store.roles;
  415. ElMessage.success('角色的声音克隆已完成');
  416. }
  417. }, 2000); // 模拟2秒的克隆时间
  418. },
  419. // 移除声音文件
  420. removeVoiceFile() {
  421. this.roleForm.voiceFileName = '';
  422. this.roleForm.clonedVoiceFileName = '';
  423. this.roleForm.isCloning = false;
  424. this.roleForm.clonedVoice = false;
  425. this.roleForm.voiceId = '';
  426. },
  427. // 自定义HTTP请求处理器 - 模拟上传过程,阻止实际请求
  428. handleHttpRequest(options) {
  429. // 立即阻止所有默认行为,这是最关键的一步
  430. if (options && options.onSuccess && typeof options.onSuccess === 'function') {
  431. console.log('Custom HTTP request handler triggered, BLOCKING actual request');
  432. // 创建一个完整的模拟成功响应对象
  433. const mockResponse = {
  434. code: 200,
  435. message: 'Success',
  436. data: {
  437. fileName: options.file.name,
  438. url: '#'
  439. },
  440. status: 200,
  441. statusText: 'OK'
  442. };
  443. // 直接调用成功回调函数,模拟成功上传
  444. setTimeout(() => {
  445. try {
  446. options.onSuccess(mockResponse, options.file);
  447. } catch (error) {
  448. console.error('Error in onSuccess callback:', error);
  449. }
  450. }, 100);
  451. // 在所有条件下都返回false,确保阻止默认行为
  452. return false;
  453. }
  454. console.error('Invalid options or missing onSuccess callback');
  455. // 即使出错也返回false阻止请求
  456. return false;
  457. },
  458. // 根据ID获取声音
  459. getVoiceById(voiceId) {
  460. return this.voices.find(v => v.id === voiceId);
  461. },
  462. // 播放声音
  463. playVoice(voiceId, isCloned = false) {
  464. // 这里是模拟播放声音的逻辑
  465. // 在实际应用中,应该根据voiceId获取声音文件并播放
  466. if (isCloned) {
  467. const voice = this.getVoiceById(voiceId);
  468. if (voice) {
  469. ElMessage.success(`正在播放克隆声音: ${voice.clonedVoice}`);
  470. } else {
  471. ElMessage.success('正在播放克隆声音');
  472. }
  473. } else {
  474. // 查找对应的声音名称
  475. const voice = this.getVoiceById(voiceId);
  476. if (voice) {
  477. ElMessage.success(`正在播放声音: ${voice.name}`);
  478. } else {
  479. ElMessage.success('正在播放原始声音');
  480. }
  481. }
  482. },
  483. // 处理头像上传成功
  484. handleAvatarUploadSuccess(file) {
  485. try {
  486. // 模拟上传成功,实际应用中应该根据服务器返回设置图片URL
  487. // 使用FileReader读取图片文件,以便在本地预览
  488. const reader = new FileReader();
  489. reader.onload = (e) => {
  490. this.roleForm.avatar = e.target.result;
  491. };
  492. reader.readAsDataURL(file);
  493. ElMessage.success('头像上传成功');
  494. } catch (error) {
  495. console.error('Error uploading avatar:', error);
  496. ElMessage.error('处理头像图片失败');
  497. this.handleUploadError();
  498. }
  499. },
  500. // 处理上传错误
  501. handleUploadError(err, file, fileList) {
  502. // 阻止默认错误处理
  503. if (err && err.status === 404) {
  504. ElMessage.warning('模拟环境中上传端点不可用');
  505. return false;
  506. }
  507. ElMessage.error('上传失败,请重试');
  508. return false;
  509. },
  510. // 移除头像
  511. removeAvatar() {
  512. this.roleForm.avatar = '';
  513. ElMessage.success('Avatar removed');
  514. },
  515. showAddDialog() {
  516. this.roleForm = {
  517. id: '',
  518. name: '',
  519. description: '',
  520. gender: '',
  521. language: '',
  522. avatar: '',
  523. voiceId: '',
  524. voiceFileName: '',
  525. clonedVoiceFileName: '',
  526. isCloning: false,
  527. clonedVoice: false
  528. }
  529. this.addDialogVisible = true
  530. },
  531. editRole(role) {
  532. this.dialogType = 'edit'
  533. this.currentRole = role
  534. // 初始化表单数据
  535. this.roleForm = { ...role }
  536. // 如果有voiceId,查找对应的声音文件信息
  537. if (role.voiceId) {
  538. const voice = this.getVoiceById(role.voiceId)
  539. if (voice) {
  540. this.roleForm.voiceFileName = voice.originalVoice
  541. this.roleForm.clonedVoiceFileName = voice.clonedVoice
  542. this.roleForm.clonedVoice = !!voice.clonedVoice
  543. }
  544. }
  545. this.dialogVisible = true
  546. },
  547. confirmDelete(role) {
  548. ElMessageBox.confirm(
  549. `Are you sure you want to delete role "${role.name}"?`,
  550. 'Warning',
  551. {
  552. confirmButtonText: 'Delete',
  553. cancelButtonText: 'Cancel',
  554. type: 'warning'
  555. }
  556. ).then(() => {
  557. const store = useStore()
  558. store.deleteRole(role.id)
  559. this.roles = store.roles
  560. ElMessage.success('Role deleted successfully')
  561. }).catch(() => {})
  562. },
  563. submitForm() {
  564. this.$refs.roleFormRef.validate((valid) => {
  565. if (valid) {
  566. const store = useStore()
  567. // 先创建或更新声音数据
  568. if (this.dialogType === 'add' || !this.roleForm.voiceId) {
  569. // 创建新声音
  570. // 检查克隆是否已完成,如果未完成则标记为克隆中
  571. const isCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
  572. const newVoice = {
  573. id: Date.now().toString(),
  574. name: `${this.roleForm.name} Voice`,
  575. originalVoice: this.roleForm.voiceFileName,
  576. clonedVoice: isCloning ? null : this.roleForm.clonedVoiceFileName,
  577. gender: this.roleForm.gender,
  578. description: `${this.roleForm.name}的声音`,
  579. uploadTime: new Date().toLocaleDateString(),
  580. isCloning: isCloning
  581. }
  582. store.addVoice(newVoice)
  583. this.roleForm.voiceId = newVoice.id
  584. } else {
  585. // 更新现有声音
  586. const isUpdatingCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
  587. store.updateVoice(this.roleForm.voiceId, {
  588. originalVoice: this.roleForm.voiceFileName,
  589. clonedVoice: isUpdatingCloning ? null : this.roleForm.clonedVoiceFileName,
  590. gender: this.roleForm.gender,
  591. isCloning: isUpdatingCloning
  592. })
  593. }
  594. // 然后创建或更新角色
  595. if (this.dialogType === 'add') {
  596. store.addRole({
  597. ...this.roleForm,
  598. id: Date.now().toString(),
  599. createdAt: new Date().toLocaleString()
  600. })
  601. ElMessage.success('角色创建成功')
  602. } else {
  603. store.updateRole(this.currentRole.id, this.roleForm)
  604. ElMessage.success('角色更新成功')
  605. }
  606. // 更新本地数据
  607. this.roles = store.roles
  608. this.voices = store.voices
  609. this.dialogVisible = false
  610. }
  611. })
  612. }
  613. }
  614. }
  615. </script>
  616. <style>
  617. .role-management {
  618. padding: 20px;
  619. }
  620. .header {
  621. display: flex;
  622. justify-content: space-between;
  623. align-items: center;
  624. margin-bottom: 20px;
  625. }
  626. .header h2 {
  627. font-size: 24px;
  628. font-weight: 600;
  629. margin: 0;
  630. }
  631. .header-actions {
  632. display: flex;
  633. gap: 10px;
  634. }
  635. .import-btn {
  636. border-color: #dcdfe6;
  637. }
  638. /* 克隆中状态样式 */
  639. .loading-container {
  640. display: flex;
  641. align-items: center;
  642. gap: 5px;
  643. color: #909399;
  644. }
  645. .loading-icon {
  646. animation: rotate 1s linear infinite;
  647. font-size: 14px;
  648. }
  649. .loading-text {
  650. font-size: 12px;
  651. }
  652. @keyframes rotate {
  653. from {
  654. transform: rotate(0deg);
  655. }
  656. to {
  657. transform: rotate(360deg);
  658. }
  659. }
  660. .role-table {
  661. margin-top: 20px;
  662. border-radius: 8px;
  663. overflow: hidden;
  664. }
  665. .role-info {
  666. display: flex;
  667. align-items: center;
  668. gap: 12px;
  669. }
  670. .role-details {
  671. display: flex;
  672. flex-direction: column;
  673. }
  674. .role-name {
  675. font-weight: 500;
  676. }
  677. .role-id {
  678. font-size: 12px;
  679. color: #909399;
  680. }
  681. .action-buttons {
  682. display: flex;
  683. gap: 10px;
  684. }
  685. .delete-btn {
  686. color: #f56c6c;
  687. }
  688. .role-dialog .el-form-item {
  689. margin-bottom: 20px;
  690. }
  691. .avatar-uploader {
  692. display: inline-block;
  693. width: 100px;
  694. height: 100px;
  695. border-radius: 50%;
  696. overflow: hidden;
  697. border: 1px dashed #d9d9d9;
  698. cursor: pointer;
  699. position: relative;
  700. background-color: #f0f2f5;
  701. display: flex;
  702. align-items: center;
  703. justify-content: center;
  704. }
  705. .avatar-uploader:hover {
  706. border-color: #409eff;
  707. }
  708. .avatar {
  709. width: 100%;
  710. height: 100%;
  711. object-fit: cover;
  712. }
  713. .avatar-actions {
  714. margin-top: 10px;
  715. }
  716. .avatar-uploader .el-button {
  717. width: 100%;
  718. height: 100%;
  719. display: flex;
  720. flex-direction: column;
  721. align-items: center;
  722. justify-content: center;
  723. background-color: transparent;
  724. border: none;
  725. }
  726. .upload-voice {
  727. margin-bottom: 10px;
  728. }
  729. .file-name {
  730. display: flex;
  731. align-items: center;
  732. padding: 8px 12px;
  733. background-color: #f5f7fa;
  734. border-radius: 4px;
  735. margin-top: 5px;
  736. }
  737. .file-name .el-button {
  738. margin-left: auto;
  739. }
  740. .cloning-status {
  741. display: flex;
  742. align-items: center;
  743. padding: 8px 12px;
  744. margin-top: 5px;
  745. color: #67c23a;
  746. }
  747. .cloning-status .el-icon {
  748. margin-right: 5px;
  749. animation: spin 1s linear infinite;
  750. }
  751. @keyframes spin {
  752. 0% { transform: rotate(0deg); }
  753. 100% { transform: rotate(360deg); }
  754. }
  755. </style>