RoleManagement.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  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. async handlePhotoUpload(file) {
  276. // cdnService.uploadVoice(file);
  277. // this.file = file;
  278. let formData = {
  279. file: file.raw
  280. }
  281. cdnService.upload(formData).then(res => {
  282. if (res.data.code === 0) {
  283. this.photo = res.data.data;
  284. }else {
  285. this.photo = '';
  286. }
  287. console.log(res.data.data);
  288. console.log(this.photo);
  289. });
  290. },
  291. handleVoiceUpload(file) {
  292. let response = cdnService.uploadVoice(file);
  293. console.log(response.data);
  294. // this.voiceFileName = file.name;
  295. // todo 实际项目中应上传文件到服务器
  296. },
  297. async onPageChange(page) {
  298. this.currentPage = page
  299. // 这里可以添加加载数据的逻辑
  300. let pageData = {
  301. page: page,
  302. size: this.perPage,
  303. name: ""
  304. };
  305. const response = await anycallService.anycallPage(pageData);
  306. this.roles = response.data.data.content.map(item => ({
  307. id: item.id || '',
  308. name: item.name || '',
  309. prompt: item.prompt || '',
  310. callings: item.callings || '',
  311. language: item.language || '',
  312. photo: item.photo || '',
  313. voice: item.voice || '',
  314. voiceName: item.voiceName || '',
  315. topFlag: item.topFlag || '',
  316. }));
  317. this.rolesCount = response.data.data.total;
  318. },
  319. async changeTopFlag(row) {
  320. // console.log(row.id);
  321. // console.log(row.id);
  322. // console.log(row.topFlag !== true);
  323. let data = {
  324. cloneId: row.id,
  325. topFlag: row.topFlag !== true
  326. };
  327. console.log(data);
  328. const response = await anycallService.updateTopFlag(data);
  329. ElMessage.success('推荐状态修改成功')
  330. },
  331. async addCallings(id,callings){
  332. let data = {
  333. cloneId: id,
  334. callings: callings
  335. };
  336. // const response = await anycallService.updateCallings(data);
  337. ElMessage.success('添加成功')
  338. const store = useStore()
  339. this.roles = store.roles
  340. },
  341. async updateCallings(id,callings){
  342. let data = {
  343. cloneId: id,
  344. callings: callings
  345. };
  346. const response = await anycallService.updateCallings(data);
  347. ElMessage.success('callings修改成功')
  348. const store = useStore()
  349. this.roles = store.roles
  350. },
  351. // 处理Excel上传
  352. handleExcelUpload(file) {
  353. this.importFileName = file.name;
  354. this.importDialogVisible = true;
  355. return false; // 阻止默认上传
  356. },
  357. // 确认导入
  358. confirmImport() {
  359. // 模拟导入处理
  360. ElMessage.success(`文件 ${this.importFileName} 导入成功`);
  361. this.importDialogVisible = false;
  362. this.importFileName = '';
  363. // 实际项目中应解析Excel文件并添加角色
  364. },
  365. // 上传前检查头像
  366. beforeUploadAvatar(file) {
  367. const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type) ||
  368. file.name.endsWith('.jpg') || file.name.endsWith('.jpeg') ||
  369. file.name.endsWith('.png') || file.name.endsWith('.gif');
  370. const isLt5M = file.size / 1024 / 1024 < 5;
  371. if (!isImage) {
  372. ElMessage.error('请上传图片格式文件!');
  373. }
  374. if (!isLt5M) {
  375. ElMessage.error('上传文件大小不能超过5MB!');
  376. }
  377. return isImage && isLt5M;
  378. },
  379. // 上传前检查声音文件 - 只接受原始声音源文件
  380. beforeUploadVoice(file) {
  381. const isAudio = ['audio/mpeg', 'audio/wav', 'audio/mp3'].includes(file.type) || file.name.endsWith('.mp3') || file.name.endsWith('.wav');
  382. const isLt20M = file.size / 1024 / 1024 < 20;
  383. if (!isAudio) {
  384. ElMessage.error('请上传音频格式文件!');
  385. }
  386. if (!isLt20M) {
  387. ElMessage.error('上传文件大小不能超过20MB!');
  388. }
  389. return isAudio && isLt20M;
  390. },
  391. // 处理声音上传成功
  392. handleVoiceUploadSuccess(file) {
  393. // 模拟上传成功,实际应用中应该根据服务器返回设置文件信息
  394. this.roleForm.voiceFileName = file.name;
  395. ElMessage.success('原始声音文件上传成功');
  396. // 模拟系统自动克隆过程
  397. this.roleForm.isCloning = true;
  398. // 模拟克隆过程的延迟
  399. setTimeout(() => {
  400. // 生成克隆文件名
  401. const originalFileName = file.name;
  402. const clonedFileName = originalFileName.replace('original-', 'cloned-');
  403. // 如果不在表单提交过程中,直接更新表单状态
  404. if (!this.dialogVisible) {
  405. this.roleForm.clonedVoiceFileName = clonedFileName;
  406. this.roleForm.isCloning = false;
  407. this.roleForm.clonedVoice = true;
  408. ElMessage.success('已从原始声音源文件成功克隆声音');
  409. }
  410. // 如果是在表单提交过程中,更新声音数据中的克隆状态
  411. else if (this.roleForm.voiceId) {
  412. const store = useStore();
  413. store.updateVoice(this.roleForm.voiceId, {
  414. clonedVoice: clonedFileName,
  415. isCloning: false
  416. });
  417. // 刷新本地数据
  418. this.voices = store.voices;
  419. this.roles = store.roles;
  420. ElMessage.success('角色的声音克隆已完成');
  421. }
  422. }, 2000); // 模拟2秒的克隆时间
  423. },
  424. // 移除声音文件
  425. removeVoiceFile() {
  426. this.roleForm.voiceFileName = '';
  427. this.roleForm.clonedVoiceFileName = '';
  428. this.roleForm.isCloning = false;
  429. this.roleForm.clonedVoice = false;
  430. this.roleForm.voiceId = '';
  431. },
  432. // 自定义HTTP请求处理器 - 模拟上传过程,阻止实际请求
  433. handleHttpRequest(options) {
  434. // 立即阻止所有默认行为,这是最关键的一步
  435. if (options && options.onSuccess && typeof options.onSuccess === 'function') {
  436. console.log('Custom HTTP request handler triggered, BLOCKING actual request');
  437. // 创建一个完整的模拟成功响应对象
  438. const mockResponse = {
  439. code: 200,
  440. message: 'Success',
  441. data: {
  442. fileName: options.file.name,
  443. url: '#'
  444. },
  445. status: 200,
  446. statusText: 'OK'
  447. };
  448. // 直接调用成功回调函数,模拟成功上传
  449. setTimeout(() => {
  450. try {
  451. options.onSuccess(mockResponse, options.file);
  452. } catch (error) {
  453. console.error('Error in onSuccess callback:', error);
  454. }
  455. }, 100);
  456. // 在所有条件下都返回false,确保阻止默认行为
  457. return false;
  458. }
  459. console.error('Invalid options or missing onSuccess callback');
  460. // 即使出错也返回false阻止请求
  461. return false;
  462. },
  463. // 根据ID获取声音
  464. getVoiceById(voiceId) {
  465. return this.voices.find(v => v.id === voiceId);
  466. },
  467. // 播放声音
  468. playVoice(voiceId, isCloned = false) {
  469. // 这里是模拟播放声音的逻辑
  470. // 在实际应用中,应该根据voiceId获取声音文件并播放
  471. if (isCloned) {
  472. const voice = this.getVoiceById(voiceId);
  473. if (voice) {
  474. ElMessage.success(`正在播放克隆声音: ${voice.clonedVoice}`);
  475. } else {
  476. ElMessage.success('正在播放克隆声音');
  477. }
  478. } else {
  479. // 查找对应的声音名称
  480. const voice = this.getVoiceById(voiceId);
  481. if (voice) {
  482. ElMessage.success(`正在播放声音: ${voice.name}`);
  483. } else {
  484. ElMessage.success('正在播放原始声音');
  485. }
  486. }
  487. },
  488. // 处理头像上传成功
  489. handleAvatarUploadSuccess(file) {
  490. try {
  491. // 模拟上传成功,实际应用中应该根据服务器返回设置图片URL
  492. // 使用FileReader读取图片文件,以便在本地预览
  493. const reader = new FileReader();
  494. reader.onload = (e) => {
  495. this.roleForm.avatar = e.target.result;
  496. };
  497. reader.readAsDataURL(file);
  498. ElMessage.success('头像上传成功');
  499. } catch (error) {
  500. console.error('Error uploading avatar:', error);
  501. ElMessage.error('处理头像图片失败');
  502. this.handleUploadError();
  503. }
  504. },
  505. // 处理上传错误
  506. handleUploadError(err, file, fileList) {
  507. // 阻止默认错误处理
  508. if (err && err.status === 404) {
  509. ElMessage.warning('模拟环境中上传端点不可用');
  510. return false;
  511. }
  512. ElMessage.error('上传失败,请重试');
  513. return false;
  514. },
  515. // 移除头像
  516. removeAvatar() {
  517. this.roleForm.avatar = '';
  518. ElMessage.success('Avatar removed');
  519. },
  520. showAddDialog() {
  521. this.roleForm = {
  522. id: '',
  523. name: '',
  524. description: '',
  525. gender: '',
  526. language: '',
  527. avatar: '',
  528. voiceId: '',
  529. voiceFileName: '',
  530. clonedVoiceFileName: '',
  531. isCloning: false,
  532. clonedVoice: false
  533. }
  534. this.addDialogVisible = true
  535. },
  536. editRole(role) {
  537. this.dialogType = 'edit'
  538. this.currentRole = role
  539. // 初始化表单数据
  540. this.roleForm = { ...role }
  541. // 如果有voiceId,查找对应的声音文件信息
  542. if (role.voiceId) {
  543. const voice = this.getVoiceById(role.voiceId)
  544. if (voice) {
  545. this.roleForm.voiceFileName = voice.originalVoice
  546. this.roleForm.clonedVoiceFileName = voice.clonedVoice
  547. this.roleForm.clonedVoice = !!voice.clonedVoice
  548. }
  549. }
  550. this.dialogVisible = true
  551. },
  552. confirmDelete(role) {
  553. ElMessageBox.confirm(
  554. `Are you sure you want to delete role "${role.name}"?`,
  555. 'Warning',
  556. {
  557. confirmButtonText: 'Delete',
  558. cancelButtonText: 'Cancel',
  559. type: 'warning'
  560. }
  561. ).then(() => {
  562. const store = useStore()
  563. store.deleteRole(role.id)
  564. this.roles = store.roles
  565. ElMessage.success('Role deleted successfully')
  566. }).catch(() => {})
  567. },
  568. submitForm() {
  569. this.$refs.roleFormRef.validate((valid) => {
  570. if (valid) {
  571. const store = useStore()
  572. // 先创建或更新声音数据
  573. if (this.dialogType === 'add' || !this.roleForm.voiceId) {
  574. // 创建新声音
  575. // 检查克隆是否已完成,如果未完成则标记为克隆中
  576. const isCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
  577. const newVoice = {
  578. id: Date.now().toString(),
  579. name: `${this.roleForm.name} Voice`,
  580. originalVoice: this.roleForm.voiceFileName,
  581. clonedVoice: isCloning ? null : this.roleForm.clonedVoiceFileName,
  582. gender: this.roleForm.gender,
  583. description: `${this.roleForm.name}的声音`,
  584. uploadTime: new Date().toLocaleDateString(),
  585. isCloning: isCloning
  586. }
  587. store.addVoice(newVoice)
  588. this.roleForm.voiceId = newVoice.id
  589. } else {
  590. // 更新现有声音
  591. const isUpdatingCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
  592. store.updateVoice(this.roleForm.voiceId, {
  593. originalVoice: this.roleForm.voiceFileName,
  594. clonedVoice: isUpdatingCloning ? null : this.roleForm.clonedVoiceFileName,
  595. gender: this.roleForm.gender,
  596. isCloning: isUpdatingCloning
  597. })
  598. }
  599. // 然后创建或更新角色
  600. if (this.dialogType === 'add') {
  601. store.addRole({
  602. ...this.roleForm,
  603. id: Date.now().toString(),
  604. createdAt: new Date().toLocaleString()
  605. })
  606. ElMessage.success('角色创建成功')
  607. } else {
  608. store.updateRole(this.currentRole.id, this.roleForm)
  609. ElMessage.success('角色更新成功')
  610. }
  611. // 更新本地数据
  612. this.roles = store.roles
  613. this.voices = store.voices
  614. this.dialogVisible = false
  615. }
  616. })
  617. }
  618. }
  619. }
  620. </script>
  621. <style>
  622. .role-management {
  623. padding: 20px;
  624. }
  625. .header {
  626. display: flex;
  627. justify-content: space-between;
  628. align-items: center;
  629. margin-bottom: 20px;
  630. }
  631. .header h2 {
  632. font-size: 24px;
  633. font-weight: 600;
  634. margin: 0;
  635. }
  636. .header-actions {
  637. display: flex;
  638. gap: 10px;
  639. }
  640. .import-btn {
  641. border-color: #dcdfe6;
  642. }
  643. /* 克隆中状态样式 */
  644. .loading-container {
  645. display: flex;
  646. align-items: center;
  647. gap: 5px;
  648. color: #909399;
  649. }
  650. .loading-icon {
  651. animation: rotate 1s linear infinite;
  652. font-size: 14px;
  653. }
  654. .loading-text {
  655. font-size: 12px;
  656. }
  657. @keyframes rotate {
  658. from {
  659. transform: rotate(0deg);
  660. }
  661. to {
  662. transform: rotate(360deg);
  663. }
  664. }
  665. .role-table {
  666. margin-top: 20px;
  667. border-radius: 8px;
  668. overflow: hidden;
  669. }
  670. .role-info {
  671. display: flex;
  672. align-items: center;
  673. gap: 12px;
  674. }
  675. .role-details {
  676. display: flex;
  677. flex-direction: column;
  678. }
  679. .role-name {
  680. font-weight: 500;
  681. }
  682. .role-id {
  683. font-size: 12px;
  684. color: #909399;
  685. }
  686. .action-buttons {
  687. display: flex;
  688. gap: 10px;
  689. }
  690. .delete-btn {
  691. color: #f56c6c;
  692. }
  693. .role-dialog .el-form-item {
  694. margin-bottom: 20px;
  695. }
  696. .avatar-uploader {
  697. display: inline-block;
  698. width: 100px;
  699. height: 100px;
  700. border-radius: 50%;
  701. overflow: hidden;
  702. border: 1px dashed #d9d9d9;
  703. cursor: pointer;
  704. position: relative;
  705. background-color: #f0f2f5;
  706. display: flex;
  707. align-items: center;
  708. justify-content: center;
  709. }
  710. .avatar-uploader:hover {
  711. border-color: #409eff;
  712. }
  713. .avatar {
  714. width: 100%;
  715. height: 100%;
  716. object-fit: cover;
  717. }
  718. .avatar-actions {
  719. margin-top: 10px;
  720. }
  721. .avatar-uploader .el-button {
  722. width: 100%;
  723. height: 100%;
  724. display: flex;
  725. flex-direction: column;
  726. align-items: center;
  727. justify-content: center;
  728. background-color: transparent;
  729. border: none;
  730. }
  731. .upload-voice {
  732. margin-bottom: 10px;
  733. }
  734. .file-name {
  735. display: flex;
  736. align-items: center;
  737. padding: 8px 12px;
  738. background-color: #f5f7fa;
  739. border-radius: 4px;
  740. margin-top: 5px;
  741. }
  742. .file-name .el-button {
  743. margin-left: auto;
  744. }
  745. .cloning-status {
  746. display: flex;
  747. align-items: center;
  748. padding: 8px 12px;
  749. margin-top: 5px;
  750. color: #67c23a;
  751. }
  752. .cloning-status .el-icon {
  753. margin-right: 5px;
  754. animation: spin 1s linear infinite;
  755. }
  756. @keyframes spin {
  757. 0% { transform: rotate(0deg); }
  758. 100% { transform: rotate(360deg); }
  759. }
  760. </style>