3 Commits 64aa5f5fc9 ... 08e77c814a

Autor SHA1 Mensagem Data
  王晓东 08e77c814a feat: 标签管理模块 1 mês atrás
  王晓东 35ea8f5328 refactor: 请求换成usepagination 1 mês atrás
  王晓东 bbab74dfb3 docs: update readme 1 mês atrás

+ 18 - 4
README.md

@@ -1,4 +1,10 @@
-# 运行环境要求
+# Anycall 管理后台前端项目
+
+## 项目 git 地址
+
+http://code.wehome.cn:3000/xy001/fara-fe-admin
+
+## 运行环境要求
 
 node v20.19.0
 
@@ -11,7 +17,7 @@ pnpm config set registry https://registry.npmmirror.com
 ```
 
 
-# 运行命令
+## 运行命令
 
 开发
 
@@ -21,8 +27,16 @@ pnpm dev
 
 pnpm build:prod
 
-admin
-wwadmin
 
 
+## 项目地址
+
+测试环境
+
+https://dev.nexthuman.cn/fara/admin/#/
+
+
+正式环境
+
+https://cueme.ai/fara/admin/#/
 

+ 56 - 0
src/api/modules/model.ts

@@ -1,5 +1,6 @@
 import { request } from '@/api'
 import type { TModel } from '@/types/model'
+import type { TCate } from '@/types/index'
 
 // 获取大模型列表
 export async function fetchModel(){
@@ -53,3 +54,58 @@ export async function updateGlobalPrompt(params: {
 }
 
 
+export async function fetchTagCates(){
+  return await request<TCate[]>(`/anycall/tag/cates`)
+}
+
+/**
+ * 标签列表项类型定义
+ */
+export type TTagListItem = {
+  /** 唯一标识 */
+  id: string;
+
+  /** 标签一级类目 */
+  cate1: string;
+
+  /** 标签名(英文) */
+  name: string;
+
+  /** 跨语言标签名(只用于辅助检索,不用于打标) */
+  alias: string[];
+
+  /** 外部QAId */
+  outQAId: string;
+
+  /** 添加时间(Unix时间戳) */
+  ctime: number;
+}
+export async function fetchTagList(params: {
+  page: number,
+  size: number,
+  cate1: string,
+}){
+  return request<{
+    total: number,
+    content: TTagListItem[],
+  }>(`/anycall/tag/search`, params)
+}
+// 新增/更新标签
+export async function saveTag(params: {
+  id?: string,
+  cate1: string,
+  name: string,
+  alias?: string[]
+}){
+  return request(`/anycall/tag/save`, params)
+}
+// 删除标签
+export async function deleteTag(params: {
+  id: string,
+}){
+  return request(`/anycall/tag/delete`, params)
+}
+
+
+
+

+ 91 - 0
src/components/TagSelector.vue

@@ -0,0 +1,91 @@
+<script setup lang="ts">
+import { fetchTagCates } from '@/api/modules/model'
+
+// 定义组件属性
+interface Props {
+  modelValue?: string
+  disabled?: boolean
+  placeholder?: string
+  style?: string | object
+  className?: string
+}
+
+// 定义组件事件
+interface Emits {
+  (e: 'update:modelValue', value: string | undefined): void
+  (e: 'update:key', value: string | undefined): void
+}
+
+// 设置默认属性
+const props = withDefaults(defineProps<Props>(), {
+  disabled: false,
+  placeholder: '请选择',
+  style: 'width: 140px;',
+})
+
+// 定义事件触发器
+const emit = defineEmits<Emits>()
+
+// 创建本地响应式变量用于v-model绑定
+const localValue = ref(props.modelValue)
+
+// 监听props中的modelValue变化,同步到本地变量
+watch(
+  () => props.modelValue,
+  (newValue) => {
+    localValue.value = newValue
+  },
+  { immediate: true }
+)
+
+
+const options = ref<Array<{value: string, name: string}>>([])
+
+const fetchData = async () => {
+  try {
+    const res = await fetchTagCates()
+    if (res.code === 0) {
+      options.value = res.data
+      // // 将API返回的数据转换为select需要的格式
+      // options.value = res.data.content.map((item: any) => ({
+      //   value: item.id,
+      //   name: item.name
+      // }))
+    } else {
+      console.error('获取 失败:', res.msg)
+    }
+  } catch (error) {
+    console.error('获取 失败:', error)
+  }
+}
+
+const updateModelValue = (value: string) => {
+  emit('update:modelValue', value)
+  const f = options.value.find(item => item.value === value)
+  if(f){
+    emit('update:key', f.name)
+  }
+
+}
+
+
+onMounted(() => {
+  fetchData()
+})
+
+</script>
+
+<template>
+  <el-select
+    v-model="localValue"
+    :placeholder="placeholder"
+    :style="style"
+    :disabled="disabled"
+    :class="className"
+    clearable
+    filterable
+    @update:model-value="updateModelValue"
+  >
+    <el-option v-for="item in options" :key="item.value" :label="item.name" :value="item.value" />
+  </el-select>
+</template>

+ 11 - 0
src/main.ts

@@ -13,6 +13,7 @@ import pinia from './store'
 import uiProvider from './ui/provider'
 import '@/utils/systemCopyright'
 import '@/utils/baidu'
+import { setGlobalOptions } from 'vue-request';
 
 import 'vxe-table/lib/style.css'
 import 'vxe-pc-ui/lib/style.css'
@@ -37,4 +38,14 @@ if (icons.isOfflineUse) {
   }
 }
 
+// vue-request 全局配置
+setGlobalOptions({
+  manual: true,
+  pagination: {
+    totalKey: 'data.total',
+    currentKey: 'page',
+    pageSizeKey: 'size',
+  },
+});
+
 app.mount('#app')

+ 32 - 0
src/router/modules/model/tag.ts

@@ -0,0 +1,32 @@
+
+import type { RouteRecordRaw } from 'vue-router'
+
+function Layout() {
+  return import('@/layouts/index.vue')
+}
+
+const routes: RouteRecordRaw = {
+  path: '/tag',
+  component: Layout,
+  name: 'tag',
+  meta: {
+    title: '标签管理',
+    icon: 'i-mi:tag',
+    singleMenu: true,
+  },
+  children: [
+    {
+      path: '',
+      name: 'tagList',
+      component: () => import('@/views/model/tag-list/index.vue'),
+      meta: {
+        title: '标签列表',
+        breadcrumb: false,
+        menu: false,
+        activeMenu: '/tag',
+      },
+    },
+  ],
+}
+
+export default routes

+ 2 - 0
src/router/routes.ts

@@ -8,6 +8,7 @@ import VoiceManagement from './modules/voice'
 import Recomendation from './modules/recomendation'
 import Model from './modules/model/model'
 import GlobalPrompt from './modules/model/globalPrompt'
+import tag from './modules/model/tag'
 
 
 function Layout() {
@@ -87,6 +88,7 @@ const asyncRoutes: Route.recordMainRaw[] = [
     children: [
       Model,
       GlobalPrompt,
+      tag,
     ],
   },
 ]

+ 7 - 0
src/types/index.ts

@@ -11,3 +11,10 @@ export interface PageResponse<T = any> {
   content: T[]
   result: T[]
 }
+
+
+export type TCate = {
+  name: string,
+  value: string,
+  mark: string
+}

+ 161 - 0
src/views/model/tag-list/components/EditForm.vue

@@ -0,0 +1,161 @@
+<script setup lang="ts">
+import { ElDialog, ElForm, ElFormItem, ElInput, ElButton } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { saveTag } from '@/api/modules/model'
+import TagSelector from '@/components/TagSelector.vue'
+import { toast } from 'vue-sonner'
+
+// 定义表单数据类型
+type IFormData  = {
+  id?: string,
+  cate1: string,
+  name: string,
+  alias: string[]
+}
+
+// 定义组件的属性
+interface Props {
+  visible: boolean
+  modelValue?: IFormData | null
+}
+
+// 定义组件的事件
+interface Emits {
+  (e: 'update:visible', value: boolean): void
+  (e: 'cancel'): void
+  (e: 'refresh'): void
+}
+
+// 接收属性和事件
+const props = withDefaults(defineProps<Props>(), {
+  visible: false,
+  modelValue: null,
+  mode: 'create'
+})
+
+const emit = defineEmits<Emits>()
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => {
+    // 这里可以触发一个事件来通知父组件更新 visible 的值
+    emit('update:visible', value);
+  },
+});
+
+// 表单引用
+const editFormRef = ref<FormInstance>();
+const initData = {
+  id: undefined,
+  cate1: '',
+  name: '',
+  alias: []
+}
+// 响应式数据 - 直接定义所有必需字段
+const formData = ref<IFormData>({
+  ...initData
+});
+
+
+// 表单验证规则
+const formRules = ref<FormRules>({
+  text: [
+    { required: true, message: '输入名称', trigger: 'blur' },
+  ],
+})
+
+// 监听可见性变化
+watch(() => props.visible, (newVisible) => {
+  if(!newVisible){
+    resetForm()
+    formData.value = {
+      ...initData
+    }
+  }else {
+    nextTick(()=> {
+      if(props.modelValue){
+        formData.value = {
+          ...props.modelValue!
+        }
+      }
+    })
+  }
+
+})
+
+
+// 重置表单
+function resetForm() {
+  console.log('reset')
+  if (editFormRef.value) {
+    editFormRef.value.resetFields()
+  }
+  formData.value = {
+    id: undefined,
+    cate1: '',
+    name: '',
+    alias: []
+  }
+}
+
+// 处理确认
+async function handleConfirm() {
+  if (!editFormRef.value) return
+
+  try {
+    await editFormRef.value.validate()
+
+    const { code } = await saveTag({
+      ...formData.value,
+      id: props.modelValue?.id
+    })
+    if (code === 0) {
+      toast.success('操作成功')
+      emit('refresh')
+    }
+    // 关闭对话框
+    emit('update:visible', false)
+  } catch (error) {
+    // 验证失败,不做处理
+  }
+}
+
+
+function handleCancel() {
+  emit('cancel')
+  emit('update:visible', false)
+}
+
+
+function handleClose() {
+  emit('update:visible', false)
+}
+
+</script>
+<template>
+  <div>
+
+    <ElDialog :title="!props.modelValue?.id  ? '新增标签' : '更新标签'" v-model="dialogVisible" align-center @close="handleClose"
+      width="400" :z-index="2000">
+      <ElForm ref="editFormRef" :model="formData" :rules="formRules" label-width="120px" class="mt-4 space-y-4 w-full">
+
+        <ElFormItem label="标签名(英文)" prop="name" label-width="120">
+           <ElInput type="text" v-model="formData.name" />
+        </ElFormItem>
+        <ElFormItem label="标签分类" prop="cate1" label-width="120">
+           <TagSelector v-model="formData.cate1"></TagSelector>
+        </ElFormItem>
+        <ElFormItem label="标签别名" prop="alias" label-width="120">
+           <ElInputTag v-model="formData.alias" trigger="Space" placeholder="按 空格键 增加标签" />
+        </ElFormItem>
+
+      </ElForm>
+      <template #footer>
+        <div class="flex justify-end space-x-2">
+          <ElButton @click="handleCancel">取消</ElButton>
+          <ElButton type="primary" @click="handleConfirm">确定</ElButton>
+        </div>
+      </template>
+    </ElDialog>
+  </div>
+</template>

+ 70 - 0
src/views/model/tag-list/components/SearchForm.vue

@@ -0,0 +1,70 @@
+<script setup lang="ts">
+import { Refresh, Search } from '@element-plus/icons-vue'
+import { ElButton, ElCol, ElForm, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus'
+import { ref } from 'vue'
+import TagSelector from '@/components/TagSelector.vue'
+
+type TSearchParams = {
+  cate1: string,
+}
+interface Props {
+  modelValue: TSearchParams
+  loading?: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: TSearchParams): void
+  (e: 'search'): void
+  (e: 'reset'): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  loading: false,
+})
+
+const emit = defineEmits<Emits>()
+
+
+const formRef = ref()
+
+const searchParams = computed({
+  get: () => props.modelValue,
+  set: value => emit('update:modelValue', value),
+})
+
+/**
+ * 搜索
+ */
+function handleSearch() {
+  emit('search')
+}
+
+/**
+ * 重置
+ */
+function handleReset() {
+  formRef.value?.resetFields()
+  emit('reset')
+}
+
+function handleClear() {
+  // 清空后自动触发搜索
+  handleSearch()
+}
+</script>
+
+<template>
+  <ElForm  :inline="true"  ref="formRef" :model="searchParams" label-width="80px">
+      <ElFormItem label="名称" prop="nameOrTags" label-width="120">
+          <TagSelector v-model="searchParams.cate1"></TagSelector>
+          </ElFormItem>
+          <ElFormItem>
+            <ElButton type="primary" :icon="Search" :loading="loading" @click="handleSearch">
+              查询
+            </ElButton>
+            <ElButton :icon="Refresh" @click="handleReset">
+              重置
+            </ElButton>
+          </ElFormItem>
+    </ElForm>
+</template>

+ 139 - 0
src/views/model/tag-list/index.vue

@@ -0,0 +1,139 @@
+<script setup lang="ts">
+
+defineOptions({
+  name: 'tagList',
+})
+import { ElButton, ElOption, ElPagination, ElSelect, ElTable, ElTableColumn, ElTag } from 'element-plus'
+
+import { usePagination } from 'vue-request'
+import { fetchTagList, deleteTag } from '@/api/modules/model'
+import type { TTagListItem } from '@/api/modules/model'
+import { formatDateGeneral } from '@/utils'
+import EditForm from './components/EditForm.vue'
+import SearchForm from './components/SearchForm.vue'
+import { toast } from 'vue-sonner'
+
+
+const editFormVisible = ref(false)
+const currentData = ref<TTagListItem|null>(null)
+// 搜索参数
+const searchParams = ref({
+  cate1: '',
+})
+
+const { data, run, loading, current, total, pageSize, changePageSize } = usePagination(fetchTagList, {
+  defaultParams: [{ page: 1, size: 20, cate1: '' }],
+});
+
+async function fetchData(page?: number) {
+  run({
+    cate1: searchParams.value.cate1,
+    page: page ?? current.value,
+    size: pageSize.value,
+  })
+}
+
+function handleCreate () {
+  editFormVisible.value = true
+  currentData.value = null
+}
+
+function handleEdit(row: TTagListItem) {
+  currentData.value = row
+  editFormVisible.value = true
+}
+
+function handleRefresh () {
+  fetchData()
+}
+
+function handleSearch () {
+  fetchData()
+}
+
+function handleReset () {
+  searchParams.value = {
+    cate1: '',
+  }
+  fetchData(1)
+}
+
+async function handleDelete(row: TTagListItem) {
+  const {code} = await deleteTag({id: row.id})
+  if(code === 0){
+    toast.success('删除成功')
+    fetchData()
+    return
+  }
+}
+
+
+
+onMounted(() => {
+  fetchData()
+})
+
+
+</script>
+
+<template>
+  <div class="absolute-container">
+    <div class="p-4 pb-0 bg-white dark-bg-black/50">
+      <SearchForm v-model="searchParams" @search="handleSearch" @reset="handleReset"/>
+    </div>
+    <FaPageMain class="flex-1 overflow-auto" main-class="flex-1 flex flex-col overflow-auto">
+      <div class="pb-4">
+        <div class="flex items-center gap-4">
+          <ElButton type="primary" @click="handleCreate">新增标签</ElButton>
+        </div>
+      </div>
+      <ElTable
+        v-loading="loading"
+        ref="tableRef" :data="data?.data?.content" stripe highlight-current-row border height="100%"
+      >
+        <ElTableColumn label="ID" prop="id" min-width="240" show-overflow-tooltip />
+        <ElTableColumn label="标签分类" prop="cate1" min-width="120" show-overflow-tooltip />
+        <ElTableColumn label="标签名(英文)" prop="name" min-width="150" show-overflow-tooltip />
+        <ElTableColumn label="标签别名" prop="alias" min-width="200">
+          <template #default="scope">
+            <ElTag v-for="(item, index) in scope.row.alias" :key="index" class="mr-1 mb-1">
+              {{ item }}
+            </ElTag>
+            <span v-if="!scope.row.alias || scope.row.alias.length === 0" class="text-gray-400">暂无别名</span>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn label="外部QA ID" prop="outQaId" min-width="180" show-overflow-tooltip />
+        <ElTableColumn label="创建时间" prop="ctime" width="180">
+          <template #default="scope">
+            {{ formatDateGeneral(scope.row.ctime) }}
+          </template>
+        </ElTableColumn>
+        <ElTableColumn fixed="right" label="操作" min-width="120">
+          <template #default="{row}">
+            <ElButton link type="primary" size="small" @click="()=> handleEdit(row)">编辑</ElButton>
+            <ElPopconfirm title="确认删除?" @confirm="()=> handleDelete(row)">
+              <template #reference>
+                  <ElButton link type="danger" size="small">删除</ElButton>
+                </template>
+            </ElPopconfirm>
+          </template>
+        </ElTableColumn>
+      </ElTable>
+      <div class="p-4">
+        <ElPagination v-model:current-page="current" v-model:page-size="pageSize" :total="total"
+          :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
+          @size-change="changePageSize" @current-change="fetchData" />
+      </div>
+    </FaPageMain>
+    <EditForm v-model:visible="editFormVisible" @refresh="handleRefresh" />
+  </div>
+</template>
+<style scoped>
+.absolute-container {
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 21 - 76
src/views/role-management/index.vue

@@ -14,13 +14,9 @@ import EditCallingsForm from './components/EditCallingsForm.vue'
 import SearchForm from './components/SearchForm.vue'
 
 import type { TAgent, TAgentDetail } from '@/types/role'
-import { anycallPage, deleteClone, updateTopFlag, type TRole } from '@/api/modules/anycallService'
-import { formatDateGeneral } from '@/utils'
+import { anycallPage, deleteClone, updateTopFlag, } from '@/api/modules/anycallService'
 import { toast } from 'vue-sonner'
-
-const tableRef = ref()
-
-const loading = ref(false)
+import { usePagination } from 'vue-request'
 
 // 控制 EditForm 显示状态
 const editFormVisible = ref(false)
@@ -34,71 +30,29 @@ const editCallingsFormVisible = ref(false)
 const importedResultDialogVisible = ref(false)
 const importedFile = ref<UploadFile | null>(null)
 const importedResult = ref<{name: string, msg: string}[]>([
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
-  // {name: 'test1.csv', msg: '成功'},
+
 ])
 // 搜索参数
 const searchParams = ref({
-  // name: '',
   keywords: '',
 })
-const dataList = ref<TAgent[]>([]);
 
-// 分页信息
-const pagination = ref({
-  total: 0,
-  size: 20,
-  page:1,
-})
 
-async function fetchData() {
-  loading.value = true
-  const res = await anycallPage({
+const { data, run, loading, current, total, pageSize, changePageSize } = usePagination(anycallPage, {
+  defaultParams: [{ page: 1, size: 20 }],
+});
+
+async function fetchData(page?: number) {
+  run({
     keywords: searchParams.value.keywords,
-    page: pagination.value.page,
-    size: pagination.value.size,
+    page: page ?? current.value,
+    size: pageSize.value,
   })
-  if(res.code === 0){
-    dataList.value = res.data.content
-    pagination.value.total = res.data.total
-  }
-  loading.value = false
-}
-function handlePageChange(page: number) {
-  pagination.value.page = page
-  fetchData()
 }
 
-function handleSizeChange(size: number) {
-  pagination.value.size = size
-  pagination.value.page = 1
-  fetchData()
-}
 
 function handleRefresh () {
-  pagination.value.page = 1 // 搜索时重置到第一页
-  fetchData()
+  fetchData(1)
 }
 const handleFormCancel = () => {
   editFormVisible.value = false
@@ -155,14 +109,11 @@ const handleEditCallings = (data: TAgent) => {
 }
 
 const handleSearch = () => {
-  pagination.value.page = 1
-  fetchData()
+  fetchData(1)
 }
 const handleReset = () => {
-  pagination.value.page = 1
-  pagination.value.size = 10
   searchParams.value.keywords = ''
-  fetchData()
+  fetchData(1)
 }
 
 const handleDelete = async (cloneId:string) => {
@@ -175,8 +126,8 @@ const handleDelete = async (cloneId:string) => {
   }
 }
 
-onMounted(async () => {
-  await fetchData()
+onMounted(() => {
+  fetchData()
 })
 
 
@@ -198,7 +149,7 @@ onMounted(async () => {
       </div>
       <ElTable
         v-loading="loading"
-        ref="tableRef" :data="dataList" stripe highlight-current-row border height="100%"
+        ref="tableRef" :data="data?.data?.content" stripe highlight-current-row border height="100%"
       >
         <ElTableColumn label="id" prop="id" min-width="240" />
         <ElTableColumn label="Role Name" prop="name" min-width="220" />
@@ -259,7 +210,7 @@ onMounted(async () => {
               </el-dropdown>
               <ElPopconfirm title="确定删除吗?" @confirm="handleDelete(row.id)">
                 <template #reference>
-                  <ElButton link type="primary" size="small">
+                  <ElButton link type="danger" size="small">
                   删除
                   </ElButton>
                 </template>
@@ -269,15 +220,9 @@ onMounted(async () => {
         </ElTableColumn>
       </ElTable>
       <div class="p-4">
-        <ElPagination
-          v-model:current-page="pagination.page"
-          v-model:page-size="pagination.size"
-          :total="pagination.total"
-          :page-sizes="[10, 20, 50, 100]"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handlePageChange"
-        />
+        <ElPagination v-model:current-page="current" v-model:page-size="pageSize" :total="total"
+          :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
+          @size-change="changePageSize" />
       </div>
     </FaPageMain>
     <EditForm v-model:visible="editFormVisible" :model-value="currentData" :mode="editMode" @refresh="handleRefresh"

+ 16 - 97
src/views/voice-management/index.vue

@@ -14,12 +14,7 @@ import type { TVoice } from '@/types/voice'
 import { toast } from 'vue-sonner'
 import { voiceList, activeVoice } from '@/api/modules/voice'
 import { formatDateGeneral } from '@/utils'
-
-const tableRef = ref()
-const loading = ref(false)
-const cloneLoading = ref(false)
-const router = useRouter()
-const route = useRoute()
+import { usePagination } from 'vue-request'
 
 // 搜索参数
 const searchParams = ref<TSearchParams>({
@@ -27,7 +22,6 @@ const searchParams = ref<TSearchParams>({
   type: 1,
   gender: null
 })
-const dataList = ref<TVoice[]>([]);
 const editFormVisible = ref(false)
 const editCloneVisible = ref(false)
 const editMode = ref<'create' | 'edit'>('create')
@@ -39,74 +33,24 @@ const currentData = ref<{
     "gender": number,
 }|null>(null)
 
-const currentAudio = ref<{
-    src: string;
-    srcName: string;
-    duration: number;
-}|undefined>()
-
-// 从URL获取初始分页参数
-const getInitialPagination = () => {
-  const page = Number(route.query.page) || 1
-  const size = Number(route.query.size) || 20
-  return {
-    total: 0,
-    page: page > 0 ? page : 1,
-    size: [10, 20, 50, 100].includes(size) ? size : 20,
-  }
-}
-
-// 分页信息
-const pagination = ref(getInitialPagination())
 
-// 更新URL中的分页参数
-const updateUrlParams = (page: number, size: number) => {
-  const query = {
-    ...route.query,
-    page: page.toString(),
-    size: size.toString(),
-  }
+const { data, run, loading, current, total, pageSize, changePageSize } = usePagination(voiceList, {
+  defaultParams: [{ page: 1, size: 20 }],
+});
 
-  // 使用replace模式更新URL,避免产生历史记录
-  router.replace({
-    path: route.path,
-    query
-  })
-}
-
-async function fetchData() {
-  loading.value = true
+async function fetchData(page?: number) {
   const gender = searchParams.value.gender === -1 ? null : searchParams.value.gender
-  const res = await voiceList({
+  run({
     gender,
     name: searchParams.value.name,
     type: searchParams.value.type,
-    page: pagination.value.page,
-    size: pagination.value.size,
+    page: page ?? current.value,
+    size: pageSize.value,
   })
-  console.log('res', res)
-  if(res.code === 0){
-    dataList.value = res.data.content
-    pagination.value.total = res.data.total
-  }
-  loading.value = false
-}
-function handlePageChange(page: number) {
-  pagination.value.page = page
-  updateUrlParams(page, pagination.value.size)
-  fetchData()
 }
 
-function handleSizeChange(size: number) {
-  pagination.value.size = size
-  pagination.value.page = 1
-  updateUrlParams(1, size)
-  fetchData()
-}
 
 function handleSearch () {
-  pagination.value.page = 1 // 搜索时重置到第一页
-  updateUrlParams(1, pagination.value.size)
   fetchData()
 }
 
@@ -116,29 +60,10 @@ function handleReset () {
     type: 1,
     gender: null,
   }
-  pagination.value.page = 1 // 重置时重置到第一页
-  updateUrlParams(1, pagination.value.size)
-  fetchData()
+  fetchData(1)
 }
 
 
-// 监听URL参数变化,用于处理浏览器后退/前进
-watch(
-  () => route.query,
-  (newQuery) => {
-    const newPage = Number(newQuery.page) || 1
-    const newSize = Number(newQuery.size) || 20
-
-    // 只有当参数真正发生变化时才更新
-    if (newPage !== pagination.value.page || newSize !== pagination.value.size) {
-      pagination.value.page = newPage > 0 ? newPage : 1
-      pagination.value.size = [10, 20, 50, 100].includes(newSize) ? newSize : 20
-      fetchData()
-    }
-  },
-  { deep: true }
-)
-
 
 const handleEdit = (data: TVoice) => {
   editMode.value = 'edit'
@@ -170,9 +95,8 @@ const handleActive = async (id: string) => {
 }
 
 
-onMounted(async () => {
-  updateUrlParams(pagination.value.page, pagination.value.size)
-  await fetchData()
+onMounted(() => {
+  fetchData()
 })
 
 
@@ -190,7 +114,8 @@ onMounted(async () => {
         </ElSpace>
       </div>
       <ElTable
-        ref="tableRef" :data="dataList" stripe highlight-current-row border height="100%"
+        ref="tableRef" :data="data?.data?.content" stripe highlight-current-row border height="100%"
+        v-loading="loading"
       >
         <ElTableColumn label="ID" prop="id" width="280" />
         <ElTableColumn label="Name" prop="name" min-width="200"/>
@@ -238,15 +163,9 @@ onMounted(async () => {
         </ElTableColumn>
       </ElTable>
       <div class="p-4">
-        <ElPagination
-          v-model:current-page="pagination.page"
-          v-model:page-size="pagination.size"
-          :total="pagination.total"
-          :page-sizes="[10, 20, 50, 100]"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handlePageChange"
-        />
+        <ElPagination v-model:current-page="current" v-model:page-size="pageSize" :total="total"
+          :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
+          @size-change="changePageSize" />
       </div>
     </FaPageMain>
     <EditForm v-model="currentData" v-model:visible="editFormVisible" :mode="editMode" @refresh="fetchData"></EditForm>