index.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <script setup lang="ts">
  2. defineOptions({
  3. name: 'Voice Management',
  4. })
  5. import { Plus } from '@element-plus/icons-vue'
  6. import { ElButton, ElDialog, ElEmpty, ElInput, ElOption, ElPagination, ElSelect, ElTable, ElTableColumn, ElTag } from 'element-plus'
  7. import SearchForm from './components/SearchForm.vue'
  8. import type { TSearchParams } from './components/SearchForm.vue'
  9. import EditForm from './components/EditForm.vue'
  10. import EditClone from './components/EditClone.vue'
  11. import type { TVoice } from '@/types/voice'
  12. import { toast } from 'vue-sonner'
  13. import { voiceList, activeVoice } from '@/api/modules/voice'
  14. import { formatDateGeneral } from '@/utils'
  15. const tableRef = ref()
  16. const loading = ref(false)
  17. const cloneLoading = ref(false)
  18. const router = useRouter()
  19. const route = useRoute()
  20. // 搜索参数
  21. const searchParams = ref<TSearchParams>({
  22. name: '',
  23. type: 1,
  24. gender: null
  25. })
  26. const dataList = ref<TVoice[]>([]);
  27. const editFormVisible = ref(false)
  28. const editCloneVisible = ref(false)
  29. const editMode = ref<'create' | 'edit'>('create')
  30. const currentData = ref<{
  31. "id": string|undefined,
  32. "name": string,
  33. "photoUrl": string,
  34. "feature": string
  35. "gender": number,
  36. }|null>(null)
  37. const currentAudio = ref<{
  38. src: string;
  39. srcName: string;
  40. duration: number;
  41. }|undefined>()
  42. // 从URL获取初始分页参数
  43. const getInitialPagination = () => {
  44. const page = Number(route.query.page) || 1
  45. const size = Number(route.query.size) || 20
  46. return {
  47. total: 0,
  48. page: page > 0 ? page : 1,
  49. size: [10, 20, 50, 100].includes(size) ? size : 20,
  50. }
  51. }
  52. // 分页信息
  53. const pagination = ref(getInitialPagination())
  54. // 更新URL中的分页参数
  55. const updateUrlParams = (page: number, size: number) => {
  56. const query = {
  57. ...route.query,
  58. page: page.toString(),
  59. size: size.toString(),
  60. }
  61. // 使用replace模式更新URL,避免产生历史记录
  62. router.replace({
  63. path: route.path,
  64. query
  65. })
  66. }
  67. async function fetchData() {
  68. loading.value = true
  69. const gender = searchParams.value.gender === -1 ? null : searchParams.value.gender
  70. const res = await voiceList({
  71. gender,
  72. name: searchParams.value.name,
  73. type: searchParams.value.type,
  74. page: pagination.value.page,
  75. size: pagination.value.size,
  76. })
  77. console.log('res', res)
  78. if(res.code === 0){
  79. dataList.value = res.data.content
  80. pagination.value.total = res.data.total
  81. }
  82. loading.value = false
  83. }
  84. function handlePageChange(page: number) {
  85. pagination.value.page = page
  86. updateUrlParams(page, pagination.value.size)
  87. fetchData()
  88. }
  89. function handleSizeChange(size: number) {
  90. pagination.value.size = size
  91. pagination.value.page = 1
  92. updateUrlParams(1, size)
  93. fetchData()
  94. }
  95. function handleSearch () {
  96. pagination.value.page = 1 // 搜索时重置到第一页
  97. updateUrlParams(1, pagination.value.size)
  98. fetchData()
  99. }
  100. function handleReset () {
  101. searchParams.value = {
  102. name: '',
  103. type: 1,
  104. gender: null,
  105. }
  106. pagination.value.page = 1 // 重置时重置到第一页
  107. updateUrlParams(1, pagination.value.size)
  108. fetchData()
  109. }
  110. // 监听URL参数变化,用于处理浏览器后退/前进
  111. watch(
  112. () => route.query,
  113. (newQuery) => {
  114. const newPage = Number(newQuery.page) || 1
  115. const newSize = Number(newQuery.size) || 20
  116. // 只有当参数真正发生变化时才更新
  117. if (newPage !== pagination.value.page || newSize !== pagination.value.size) {
  118. pagination.value.page = newPage > 0 ? newPage : 1
  119. pagination.value.size = [10, 20, 50, 100].includes(newSize) ? newSize : 20
  120. fetchData()
  121. }
  122. },
  123. { deep: true }
  124. )
  125. const handleEdit = (data: TVoice) => {
  126. editMode.value = 'edit'
  127. currentData.value = {
  128. id: data.id,
  129. name: data.name ?? '',
  130. photoUrl: data.photoUrl ?? '',
  131. feature: data.feature ?? '',
  132. gender: data.gender,
  133. }
  134. editFormVisible.value = true
  135. }
  136. const switchLoading = ref(false)
  137. const handleActive = async (id: string) => {
  138. switchLoading.value = true
  139. try {
  140. const { code } = await activeVoice({ id })
  141. if(code === 0){
  142. toast.success('激活成功')
  143. fetchData()
  144. }
  145. switchLoading.value = false
  146. } catch (error) {
  147. switchLoading.value = false
  148. return
  149. }
  150. }
  151. onMounted(async () => {
  152. updateUrlParams(pagination.value.page, pagination.value.size)
  153. await fetchData()
  154. })
  155. </script>
  156. <template>
  157. <div class="absolute-container">
  158. <div class="p-4 pb-0 bg-white dark-bg-black/50">
  159. <SearchForm v-model="searchParams" @search="handleSearch" @reset="handleReset"></SearchForm>
  160. </div>
  161. <FaPageMain class="flex-1 overflow-auto" main-class="flex-1 flex flex-col overflow-auto">
  162. <div class="pb-4">
  163. <ElSpace>
  164. <ElButton type="primary" :icon="Plus" @click="()=> editCloneVisible = true">克隆声音</ElButton>
  165. </ElSpace>
  166. </div>
  167. <ElTable
  168. ref="tableRef" :data="dataList" stripe highlight-current-row border height="100%"
  169. >
  170. <ElTableColumn label="ID" prop="id" width="280" />
  171. <ElTableColumn label="Name" prop="name" min-width="200"/>
  172. <ElTableColumn label="Avatar" prop="photoUrl">
  173. <template #default="{row}">
  174. <ElImage :src="row.photoUrl" fit="cover" class="w-12 h-12 rounded" />
  175. </template>
  176. </ElTableColumn>
  177. <ElTableColumn label="Gender" prop="gender">
  178. <template #default="{row}">
  179. {{ row.gender === 1 ? '男':"女" }}
  180. </template>
  181. </ElTableColumn>
  182. <ElTableColumn label="feature" prop="feature" width="280" />
  183. <ElTableColumn label="激活状态" prop="status" width="120">
  184. <template #default="{row}">
  185. <ElTag type="success" v-if="row.status === 5">
  186. 已激活
  187. </ElTag>
  188. <ElButton v-else-if="row.status === 3" @click="() => handleActive(row.voiceId)" :disabled="switchLoading">
  189. 激活
  190. </ElButton>
  191. <div v-else>...</div>
  192. <!-- <ElSwitch
  193. v-model="row.status"
  194. :active-value="5"
  195. :inactive-value="3"
  196. :disabled="switchLoading"
  197. @click="() => handleActive(row.voiceId)"
  198. />
  199. 待激活
  200. </div> -->
  201. </template>
  202. </ElTableColumn>
  203. <ElTableColumn label="create time" prop="ctime" width="280">
  204. <template #default="{row}">
  205. {{ formatDateGeneral(row.ctime) }}
  206. </template>
  207. </ElTableColumn>
  208. <ElTableColumn fixed="right" label="操作" min-width="240">
  209. <template #default="{row}">
  210. <ElButton link type="primary" size="small" @click="handleEdit(row)">编辑</ElButton>
  211. </template>
  212. </ElTableColumn>
  213. </ElTable>
  214. <div class="p-4">
  215. <ElPagination
  216. v-model:current-page="pagination.page"
  217. v-model:page-size="pagination.size"
  218. :total="pagination.total"
  219. :page-sizes="[10, 20, 50, 100]"
  220. layout="total, sizes, prev, pager, next, jumper"
  221. @size-change="handleSizeChange"
  222. @current-change="handlePageChange"
  223. />
  224. </div>
  225. </FaPageMain>
  226. <EditForm v-model="currentData" v-model:visible="editFormVisible" :mode="editMode" @refresh="fetchData"></EditForm>
  227. <EditClone v-model:visible="editCloneVisible" @refresh="fetchData"></EditClone>
  228. </div>
  229. </template>
  230. <style scoped>
  231. .absolute-container {
  232. position: absolute;
  233. display: flex;
  234. flex-direction: column;
  235. width: 100%;
  236. height: 100%;
  237. }
  238. </style>