王晓东 1 месяц назад
Родитель
Сommit
08e77c814a

+ 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>

+ 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>

+ 1 - 1
src/views/role-management/index.vue

@@ -210,7 +210,7 @@ onMounted(() => {
               </el-dropdown>
               <ElPopconfirm title="确定删除吗?" @confirm="handleDelete(row.id)">
                 <template #reference>
-                  <ElButton link type="primary" size="small">
+                  <ElButton link type="danger" size="small">
                   删除
                   </ElButton>
                 </template>