QuestionEmbeddingList.vue 8.5 KB
<template>
  <div>
    <BasicTable @register="registerTable" :rowSelection="rowSelection">
      <template #tableTitle>
        <a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
        <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
        <a-button
          type="primary"
          preIcon="ant-design:import-outlined"
          @click="handleOpenImportModal"
        >导入ZIP</a-button>
        <a-dropdown v-if="selectedRowKeys.length > 0">
          <template #overlay>
            <a-menu>
              <a-menu-item key="1" @click="batchHandleDelete">
                <Icon icon="ant-design:delete-outlined" />删除
              </a-menu-item>
            </a-menu>
          </template>
          <a-button>批量操作<Icon icon="mdi:chevron-down" /></a-button>
        </a-dropdown>
      </template>
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
      </template>
    </BasicTable>
    <QuestionEmbeddingModal @register="registerModal" @success="handleSuccess" />

    <!-- 导入ZIP模态框 -->
    <BasicModal
      v-model:visible="importModalVisible"
      title="导入ZIP文件"
      @ok="handleImportSubmit"
      @cancel="handleImportCancel"
      width="600px"
      :okButtonProps="{ disabled: !selectedKnowledgeId || !currentFile }"
    >
      <BasicForm @register="registerImportForm" :showActionButtonGroup="false" />
      <div style="margin-top: 20px;margin-left: 5%">
        <input
          type="file"
          ref="fileInput"
          accept=".zip"
          @change="handleFileChange"
          style="display: none"
        >
        <a-button
          type="primary"
          @click="triggerFileInput"
        >
          <UploadOutlined /> 选择ZIP文件
        </a-button>
        <div v-if="currentFile" style="margin-top: 8px;">
          已选择文件: {{ currentFile.name }} ({{ formatFileSize(currentFile.size) }})
        </div>
        <div v-if="uploadProgress > 0" style="margin-top: 8px;">
          上传进度: {{ uploadProgress }}%
          <a-progress
            :percent="uploadProgress"
            :stroke-color="progressColor"
            :status="uploadStatus"
          />
        </div>
      </div>
    </BasicModal>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { BasicModal } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { message } from 'ant-design-vue';
import QuestionEmbeddingModal from './components/QuestionEmbeddingModal.vue';
import { columns, searchFormSchema } from './QuestionEmbedding.data';
import { list, deleteOne, batchDelete, getImportZipUrl, getExportUrl, listKnowledge } from './QuestionEmbedding.api';
import { useUserStore } from '/@/store/modules/user';
import { getToken } from '/@/utils/auth';

const queryParam = reactive<any>({});
const [registerModal, { openModal }] = useModal();
const importModalVisible = ref(false);
const selectedKnowledgeId = ref('');
const fileInput = ref<HTMLInputElement | null>(null);
const currentFile = ref<File | null>(null);
const uploadProgress = ref(0);
const progressColor = ref('#1890ff');
const uploadStatus = ref<'normal' | 'exception' | 'active' | 'success'>('normal');

// 知识库选择表单
const [registerImportForm] = useForm({
  labelWidth: 100,
  showActionButtonGroup: false,
  schemas: [
    {
      field: 'knowledgeId',
      label: '知识库',
      component: 'ApiSelect',
      required: true,
      componentProps: {
        placeholder: '请选择知识库',
        api: listKnowledge,
        labelField: 'name',
        valueField: 'id',
        onChange: (value: string) => {
          selectedKnowledgeId.value = value;
        }
      },
    },
  ],
});

const { tableContext, onExportXls } = useListPage({
  tableProps: {
    title: '问答向量库',
    api: list,
    columns,
    formConfig: {
      schemas: searchFormSchema,
      autoSubmitOnEnter: true,
      showAdvancedButton: true
    },
    actionColumn: {
      width: 120,
      fixed: 'right'
    },
    beforeFetch: (params) => Object.assign(params, queryParam)
  },
  exportConfig: {
    name: '问答向量库',
    url: getExportUrl
  }
});

const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;

function handleOpenImportModal() {
  importModalVisible.value = true;
  selectedKnowledgeId.value = '';
  resetFileInput();
}

function handleImportCancel() {
  importModalVisible.value = false;
  resetFileInput();
}

function triggerFileInput() {
  fileInput.value?.click();
}

function handleFileChange(e: Event) {
  const input = e.target as HTMLInputElement;
  if (input.files && input.files.length > 0) {
    currentFile.value = input.files[0];
    uploadProgress.value = 0;
    uploadStatus.value = 'normal';
    console.log('文件选择成功:', {
      name: currentFile.value.name,
      size: currentFile.value.size,
      type: currentFile.value.type
    });
  } else {
    currentFile.value = null;
  }
}

function resetFileInput() {
  if (fileInput.value) {
    fileInput.value.value = '';
  }
  currentFile.value = null;
  uploadProgress.value = 0;
  uploadStatus.value = 'normal';
}

function formatFileSize(bytes: number): string {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// 获取用户token
const userStore = useUserStore();
const token = getToken();

async function handleImportSubmit() {
  if (!selectedKnowledgeId.value || !currentFile.value) {
    message.warning('请完整填写上传信息');
    return;
  }

  const formData = new FormData();
  formData.append('file', currentFile.value, currentFile.value.name);
  formData.append('knowledgeId', selectedKnowledgeId.value);

  try {
    uploadProgress.value = 0;
    uploadStatus.value = 'active';

    // 使用fetch API并添加认证头
    const response = await fetch("/jeecgboot" + getImportZipUrl, {
      method: 'POST',
      body: formData,
      headers: {
        'X-Access-Token': token, // JEECG标准认证头
        'Authorization': `Bearer ${token}` // 备用认证头
      },
      credentials: 'include' // 携带cookie
    });

    if (response.status === 401) {
      message.error('登录已过期,请重新登录');
      userStore.logout(true);
      return;
    }

    if (!response.ok) {
      throw new Error(`上传失败: ${response.statusText}`);
    }

    const result = await response.json();
    if (result.success) {
      message.success('上传成功');
      importModalVisible.value = false;
      handleSuccess();
    } else {
      throw new Error(result.message || '上传处理失败');
    }
  } catch (error) {
    uploadStatus.value = 'exception';
    message.error(error.message || '上传失败');
    console.error('上传错误:', error);
  }
}

function handleSuperQuery(params: any) {
  Object.assign(queryParam, params);
  reload();
}

function handleAdd() {
  openModal(true, { isUpdate: false, showFooter: true });
}

function handleEdit(record: any) {
  openModal(true, { record, isUpdate: true, showFooter: true });
}

function handleDetail(record: any) {
  openModal(true, { record, isUpdate: true, showFooter: false });
}

async function handleDelete(record: any) {
  await deleteOne({ id: record.id }, handleSuccess);
}

async function batchHandleDelete() {
  await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}

function handleSuccess() {
  selectedRowKeys.value = [];
  reload();
}

function getTableAction(record: any) {
  return [
    { label: '编辑', onClick: handleEdit.bind(null, record) }
  ];
}

function getDropDownAction(record: any) {
  return [
    { label: '详情', onClick: handleDetail.bind(null, record) },
    {
      label: '删除',
      popConfirm: {
        title: '确认删除此问答?',
        confirm: handleDelete.bind(null, record)
      }
    }
  ];
}
</script>

<style scoped>
.upload-area {
  margin-top: 20px;
  padding: 20px;
  border: 1px dashed #d9d9d9;
  border-radius: 4px;
  text-align: center;
  cursor: pointer;
}
.upload-area:hover {
  border-color: #1890ff;
}
</style>