chenkun 1 month ago
parent
commit
8ecb13a509
5 changed files with 719 additions and 475 deletions
  1. 123 0
      src/api/document.ts
  2. 72 0
      src/utils/download.ts
  3. 286 26
      src/views/basic-info/Index.vue
  4. 238 157
      src/views/documents/Index.vue
  5. 0 292
      test_frontend.html

+ 123 - 0
src/api/document.ts

@@ -0,0 +1,123 @@
+import request from './request'
+
+// --- 类型定义 ---
+
+export interface DocumentItem {
+  id: string
+  source_id: string
+  title: string
+  content: string
+  source_type: 'basis' | 'work' | 'job'
+  table_type?: 'basis' | 'work' | 'job' // 兼容旧代码
+  primary_category_id: string
+  secondary_category_id: string
+  year: number
+  file_url: string
+  file_extension: string
+  whether_to_enter: number
+  conversion_status: number
+  conversion_progress: number
+  converted_file_name?: string
+  converted_file_url?: string
+  conversion_error?: string
+  error_message?: string // 兼容旧代码
+  created_by?: string
+  created_time: string
+  updated_time: string
+  created_at?: string // 兼容旧代码
+  updated_at?: string // 兼容旧代码
+  // 基础信息扩展字段
+  standard_no?: string
+  issuing_authority?: string
+  release_date?: string
+  document_type?: string
+  professional_field?: string
+  validity?: string
+  project_name?: string
+  project_section?: string
+}
+
+export interface DocumentQueryParams {
+  page?: number
+  size?: number
+  keyword?: string
+  table_type?: string | null
+  whether_to_enter?: number | null
+}
+
+export interface ApiResponse<T = any> {
+  code: number
+  message: string
+  data: T
+  timestamp: string
+}
+
+export interface PageResult<T> {
+  items: T[]
+  total: number
+  page: number
+  size: number
+  all_total?: number
+  total_entered?: number
+}
+
+// --- API 方法 ---
+
+const prefix = '/api/v1/sample'
+
+export const documentApi = {
+  // 获取文档列表
+  getList(params: DocumentQueryParams): Promise<ApiResponse<PageResult<DocumentItem>>> {
+    return request.get(`${prefix}/documents/list`, { params })
+  },
+
+  // 获取文档详情
+  getDetail(id: string): Promise<ApiResponse<DocumentItem>> {
+    return request.get(`${prefix}/documents/detail/${id}`)
+  },
+
+  // 添加文档
+  add(data: Partial<DocumentItem>): Promise<ApiResponse<{ id: string }>> {
+    return request.post(`${prefix}/documents/add`, data)
+  },
+
+  // 编辑文档
+  edit(data: Partial<DocumentItem>): Promise<ApiResponse<null>> {
+    return request.post(`${prefix}/documents/edit`, data)
+  },
+
+  // 文档入库
+  enter(id: string): Promise<ApiResponse<null>> {
+    return request.post(`${prefix}/documents/enter`, { id })
+  },
+
+  // 批量入库
+  batchEnter(ids: string[]): Promise<ApiResponse<null>> {
+    return request.post(`${prefix}/documents/batch-enter`, { ids })
+  },
+
+  // 批量删除
+  batchDelete(ids: string[]): Promise<ApiResponse<null>> {
+    return request.post(`${prefix}/documents/batch-delete`, { ids })
+  },
+
+  // 启动转换
+  convert(id: string, tableType?: string): Promise<ApiResponse<null>> {
+    return request.post(`${prefix}/documents/convert`, { id, table_type: tableType })
+  },
+
+  // 代理查看内容
+  proxyView(url: string, token?: string): Promise<any> {
+    return request.get(`${prefix}/documents/proxy-view`, {
+      params: { url, token }
+    })
+  },
+
+  // 获取上传预签名 URL
+  getUploadUrl(filename: string, contentType: string): Promise<ApiResponse<{ upload_url: string, file_url: string }>> {
+    return request.post(`${prefix}/documents/upload-url`, {
+      filename,
+      content_type: contentType
+    })
+  }
+}

+ 72 - 0
src/utils/download.ts

@@ -0,0 +1,72 @@
+import request from '@/api/request'
+import { getToken } from '@/utils/auth'
+
+/**
+ * 强制下载文件
+ * @param url 文件原始链接
+ * @param filename 下载后的文件名
+ */
+export const downloadFile = async (url: string, filename?: string) => {
+  if (!url) return
+  
+  const token = getToken()
+  if (!token) return
+  
+  // 构建下载代理 URL
+  const downloadUrl = `/api/v1/sample/documents/download?url=${encodeURIComponent(url)}${filename ? `&filename=${encodeURIComponent(filename)}` : ''}&token=${token}`
+  
+  try {
+    // 使用 fetch 预检下载,这样可以捕获错误响应
+    const response = await fetch(downloadUrl)
+    
+    // 如果响应不是 200,说明后端报错了
+    if (!response.ok) {
+      const errorData = await response.json().catch(() => ({}))
+      const errorMsg = errorData.message || `下载失败 (HTTP ${response.status})`
+      
+      // 使用 Element Plus 的消息提示(假设已全局引入或在此引入)
+      // 这里为了简单,先用 window.alert,实际项目中建议用 ElMessage
+      import('element-plus').then(({ ElMessage }) => {
+        ElMessage.error(errorMsg)
+      }).catch(() => {
+        alert(errorMsg)
+      })
+      return
+    }
+    
+    // 如果是 200,则正常下载
+    const blob = await response.blob()
+    const blobUrl = window.URL.createObjectURL(blob)
+    
+    const link = document.createElement('a')
+    link.href = blobUrl
+    
+    // 优先使用后端返回的文件名(如果 Content-Disposition 存在)
+    const disposition = response.headers.get('Content-Disposition')
+    let finalFilename = filename || 'downloaded_file'
+    
+    if (disposition && disposition.includes('filename*=')) {
+      const match = disposition.match(/filename\*=UTF-8''(.+)/)
+      if (match && match[1]) {
+        finalFilename = decodeURIComponent(match[1])
+      }
+    } else if (disposition && disposition.includes('filename=')) {
+      const match = disposition.match(/filename="?(.+?)"?(;|$)/)
+      if (match && match[1]) {
+        finalFilename = match[1]
+      }
+    }
+    
+    link.setAttribute('download', finalFilename)
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    window.URL.revokeObjectURL(blobUrl)
+    
+  } catch (error) {
+    console.error('下载过程出错:', error)
+    import('element-plus').then(({ ElMessage }) => {
+      ElMessage.error('网络连接失败或服务器错误')
+    })
+  }
+}

+ 286 - 26
src/views/basic-info/Index.vue

@@ -1,9 +1,9 @@
-d:\image\screenImage\局部截取_20260114_093322.png<template>
+<template>
   <div class="basic-info-container">
     <el-card class="box-card search-card">
       <div class="search-header">
         <span class="title">{{ pageTitle }}</span>
-        <el-button type="primary" :icon="Plus">新增{{ moduleName }}</el-button>
+        <el-button type="primary" :icon="Plus" @click="handleAdd">新增{{ moduleName }}</el-button>
       </div>
       
       <el-form :model="searchForm" label-width="80px" class="search-form" label-position="top">
@@ -116,27 +116,22 @@ d:\image\screenImage\局部截取_20260114_093322.png<template>
             {{ formatDateTime(scope.row.created_at) }}
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="220" fixed="right">
+        <el-table-column label="操作" width="180" fixed="right" align="center">
           <template #default="scope">
-            <el-button-group>
-              <el-tooltip content="查看" placement="top">
+            <div class="action-buttons">
+              <el-tooltip content="详情" placement="top">
                 <el-button link type="primary" @click="handleAction('view', scope.row)">
-                  <el-icon><View /></el-icon> 查看
+                  <el-icon><View /></el-icon>
                 </el-button>
               </el-tooltip>
-              <el-tooltip content="预览" placement="top">
-                <el-button link type="success" @click="handleAction('preview', scope.row)">
-                  <el-icon><Monitor /></el-icon> 预览
-                </el-button>
-              </el-tooltip>
-              <el-tooltip content="下载" placement="top">
-                <el-button link type="warning" @click="handleAction('download', scope.row)">
-                  <el-icon><Download /></el-icon> 下载
+              <el-tooltip content="下载" placement="top" v-if="scope.row.file_url">
+                <el-button link type="success" @click="handleAction('download', scope.row)">
+                  <el-icon><Download /></el-icon>
                 </el-button>
               </el-tooltip>
               <el-tooltip content="编辑" placement="top">
                 <el-button link type="primary" @click="handleAction('edit', scope.row)">
-                  <el-icon><Edit /></el-icon> 编辑
+                  <el-icon><Edit /></el-icon>
                 </el-button>
               </el-tooltip>
               <el-tooltip content="删除" placement="top">
@@ -144,7 +139,7 @@ d:\image\screenImage\局部截取_20260114_093322.png<template>
                   <el-icon><Delete /></el-icon>
                 </el-button>
               </el-tooltip>
-            </el-button-group>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -161,25 +156,171 @@ d:\image\screenImage\局部截取_20260114_093322.png<template>
         />
       </div>
     </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog v-model="formDialogVisible" :title="formTitle" width="600px">
+      <el-form :model="editForm" label-width="110px">
+        <el-form-item :label="titleLabel" required>
+          <el-input v-model="editForm.title" :placeholder="'请输入' + titleLabel" />
+        </el-form-item>
+        
+        <template v-if="infoType === 'basis'">
+          <el-form-item label="标准编号">
+            <el-input v-model="editForm.standard_no" placeholder="请输入标准编号" />
+          </el-form-item>
+        </template>
+
+        <el-form-item :label="authorityLabel">
+          <el-input v-model="editForm.issuing_authority" :placeholder="'请输入' + authorityLabel" />
+        </el-form-item>
+
+        <el-form-item :label="dateLabel">
+          <el-date-picker v-model="editForm.release_date" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
+        </el-form-item>
+
+        <template v-if="infoType === 'basis' || infoType === 'job'">
+          <el-form-item label="文件类型">
+            <el-select v-model="editForm.document_type" placeholder="请选择文件类型" style="width: 100%">
+              <el-option v-for="item in documentTypeOptions" :key="item" :label="item" :value="item" />
+            </el-select>
+          </el-form-item>
+        </template>
+
+        <template v-if="infoType === 'basis'">
+          <el-form-item label="专业领域">
+            <el-select v-model="editForm.professional_field" placeholder="请选择专业领域" style="width: 100%">
+              <el-option v-for="item in professionalFieldOptions" :key="item" :label="item" :value="item" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="时效性">
+            <el-select v-model="editForm.validity" placeholder="请选择时效性" style="width: 100%">
+              <el-option label="现行" value="现行" />
+              <el-option label="已废止" value="已废止" />
+              <el-option label="被替代" value="被替代" />
+            </el-select>
+          </el-form-item>
+        </template>
+
+        <template v-if="infoType === 'work'">
+          <el-form-item label="项目名称">
+            <el-input v-model="editForm.project_name" placeholder="请输入项目名称" />
+          </el-form-item>
+          <el-form-item label="项目标段">
+            <el-input v-model="editForm.project_section" placeholder="请输入项目标段" />
+          </el-form-item>
+        </template>
+
+        <el-form-item label="文件链接">
+          <el-input v-model="editForm.file_url" placeholder="请输入文件预览链接 (URL)" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="formDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitForm" :loading="submitting">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 查看详情对话框 -->
+    <el-dialog v-model="detailDialogVisible" title="详情信息" width="600px">
+      <el-descriptions :column="1" border>
+        <el-descriptions-item :label="titleLabel">{{ currentItem?.title }}</el-descriptions-item>
+        <el-descriptions-item label="标准编号" v-if="infoType === 'basis'">{{ currentItem?.standard_no || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="authorityLabel">{{ currentItem?.issuing_authority || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="dateLabel">{{ formatDate(currentItem?.release_date) }}</el-descriptions-item>
+        <el-descriptions-item label="文档类型" v-if="infoType === 'basis' || infoType === 'job'">{{ currentItem?.document_type || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="专业领域" v-if="infoType === 'basis'">{{ currentItem?.professional_field || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="时效性" v-if="infoType === 'basis'">
+          <el-tag :type="getValidityType(currentItem?.validity)">{{ currentItem?.validity || '现行' }}</el-tag>
+        </el-descriptions-item>
+        <template v-if="infoType === 'work'">
+          <el-descriptions-item label="项目名称">{{ currentItem?.project_name || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="项目标段">{{ currentItem?.project_section || '-' }}</el-descriptions-item>
+        </template>
+        <el-descriptions-item label="创建人">{{ currentItem?.created_by }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ formatDateTime(currentItem?.created_at) }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <el-button @click="detailDialogVisible = false">关闭</el-button>
+        <el-button type="primary" @click="handleAction('edit', currentItem)">编辑</el-button>
+        <el-button type="success" @click="handleAction('preview', currentItem)" v-if="currentItem?.file_url">预览文件</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 预览对话框 -->
+    <el-dialog v-model="previewVisible" :title="previewTitle" width="85%" top="5vh" custom-class="preview-dialog" @closed="previewUrl = ''">
+      <template #header>
+        <div class="dialog-header-custom">
+          <span>{{ previewTitle }}</span>
+          <div class="header-actions">
+            <el-button type="primary" link @click="openInNewWindow">
+              <el-icon><Monitor /></el-icon> 在新窗口打开
+            </el-button>
+          </div>
+        </div>
+      </template>
+      <div v-loading="previewLoading" class="preview-content" style="height: 70vh;">
+        <iframe 
+          v-if="previewUrl" 
+          :src="proxyPreviewUrl" 
+          width="100%" 
+          height="100%" 
+          frameborder="0"
+          @load="previewLoading = false"
+        ></iframe>
+        <div v-else-if="!previewLoading" class="no-preview">
+          <el-empty description="无法预览该文件" />
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted, computed, watch, reactive } from 'vue'
 import { useRoute } from 'vue-router'
-import { Search, View, Monitor, Download, Edit, Delete, Refresh, Plus } from '@element-plus/icons-vue'
+import { Search, View, Monitor, Download, Edit, Delete, Refresh, Plus, Document } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import request from '@/api/request'
 import type { ApiResponse } from '@/types/auth'
 import dayjs from 'dayjs'
+import { downloadFile } from '@/utils/download'
+import { useAuthStore } from '@/stores/auth'
 
 const route = useRoute()
+const authStore = useAuthStore()
 const loading = ref(false)
+const submitting = ref(false)
 const tableData = ref([])
 const total = ref(0)
 const currentPage = ref(1)
 const pageSize = ref(20)
 
+// 对话框状态
+const formDialogVisible = ref(false)
+const detailDialogVisible = ref(false)
+const previewVisible = ref(false)
+const previewLoading = ref(false)
+const previewUrl = ref('')
+const previewTitle = ref('')
+
+// 当前操作的数据
+const currentItem = ref<any>(null)
+const editForm = reactive<any>({
+  id: null,
+  title: '',
+  standard_no: '',
+  issuing_authority: '',
+  release_date: '',
+  document_type: '',
+  professional_field: '',
+  validity: '现行',
+  project_name: '',
+  project_section: '',
+  file_url: ''
+})
+
+const formTitle = computed(() => editForm.id ? `编辑${moduleName.value}` : `新增${moduleName.value}`)
+
 // 检索表单接口
 interface SearchForm {
   title: string
@@ -272,7 +413,7 @@ const loadData = async () => {
       total: number
       page: number
       size: number
-    }>>('/api/v1/basic-info/list', {
+    }>>('/api/v1/sample/basic-info/list', {
       params: {
         type: infoType.value,
         page: currentPage.value,
@@ -345,30 +486,116 @@ const getValidityType = (validity: string) => {
   }
 }
 
-const handleAction = (action: string, row: any) => {
+const proxyPreviewUrl = computed(() => {
+  if (!previewUrl.value) return ''
+  // 使用后端代理接口查看外部网页,附加 token 以通过认证
+  const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
+  return `${baseUrl}/api/v1/sample/documents/proxy-view?url=${encodeURIComponent(previewUrl.value)}&token=${authStore.token}`
+})
+
+const handleAction = async (action: string, row: any) => {
+  currentItem.value = row
   switch (action) {
     case 'view':
-      ElMessage.info('查看详情: ' + row.title)
+      if (row.file_url) {
+        // 如果有文件链接,直接进入预览
+        previewTitle.value = row.title
+        previewUrl.value = row.file_url
+        previewVisible.value = true
+        previewLoading.value = true
+      } else {
+        // 否则进入详情对话框
+        detailDialogVisible.value = true
+      }
       break
     case 'preview':
-      ElMessage.info('预览文件: ' + row.title)
+      if (row.file_url) {
+        previewTitle.value = row.title
+        previewUrl.value = row.file_url
+        previewVisible.value = true
+        previewLoading.value = true
+      } else {
+        ElMessage.warning('该文档暂无预览链接')
+      }
       break
     case 'download':
-      ElMessage.info('下载文件: ' + row.title)
+      if (row.file_url) {
+        downloadFile(row.file_url, row.title)
+      } else {
+        ElMessage.warning('该文档暂无下载链接')
+      }
       break
     case 'edit':
-      ElMessage.info('编辑信息: ' + row.title)
+      Object.keys(editForm).forEach(key => {
+        editForm[key] = row[key] || ''
+      })
+      editForm.id = row.id
+      formDialogVisible.value = true
       break
     case 'delete':
       ElMessageBox.confirm('确定要删除该条信息吗?', '提示', {
         type: 'warning'
-      }).then(() => {
-        ElMessage.success('删除成功')
+      }).then(async () => {
+        try {
+          const res = await request.post<ApiResponse>(`/api/v1/sample/basic-info/delete?type=${infoType.value}&id=${row.id}`)
+          const resData = (res as unknown) as ApiResponse
+          if (resData.code === 0) {
+            ElMessage.success('删除成功')
+            loadData()
+          } else {
+            ElMessage.error(resData.message || '删除失败')
+          }
+        } catch (error) {
+          console.error('删除失败:', error)
+          ElMessage.error('网络错误')
+        }
       }).catch(() => {})
       break
   }
 }
 
+const handleAdd = () => {
+  Object.keys(editForm).forEach(key => {
+    editForm[key] = key === 'validity' ? '现行' : ''
+  })
+  editForm.id = null
+  formDialogVisible.value = true
+}
+
+const submitForm = async () => {
+  if (!editForm.title) {
+    return ElMessage.warning('请输入名称')
+  }
+  
+  submitting.value = true
+  try {
+    const isEdit = !!editForm.id
+    const url = isEdit ? `/api/v1/sample/basic-info/edit?type=${infoType.value}&id=${editForm.id}` : `/api/v1/sample/basic-info/add?type=${infoType.value}`
+    
+    const res = await request.post<ApiResponse>(url, editForm)
+    const resData = (res as unknown) as ApiResponse
+    
+    if (resData.code === 0) {
+      ElMessage.success(isEdit ? '更新成功' : '新增成功')
+      formDialogVisible.value = false
+      loadData()
+    } else {
+      ElMessage.error(resData.message || '操作失败')
+    }
+  } catch (error) {
+    console.error('操作失败:', error)
+    ElMessage.error('网络错误')
+  } finally {
+    submitting.value = false
+  }
+}
+
+const openInNewWindow = () => {
+  if (previewUrl.value) {
+    window.open(previewUrl.value, '_blank')
+  }
+}
+
 // 监听路由变化,切换类型时重新加载数据
 watch(() => route.path, () => {
   currentPage.value = 1
@@ -398,7 +625,7 @@ onMounted(() => {
   margin-bottom: 20px;
 }
 
-.title {
+.search-header .title {
   font-size: 18px;
   font-weight: bold;
 }
@@ -430,10 +657,43 @@ onMounted(() => {
   justify-content: flex-end;
 }
 
+.action-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 4px;
+}
+
+.action-buttons .el-button {
+  padding: 4px;
+  margin: 0;
+}
+
 :deep(.el-table .cell) {
   white-space: nowrap;
 }
 
+.preview-content {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.no-preview {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  background-color: #f5f7fa;
+}
+
+.dialog-header-custom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+  padding-right: 30px;
+}
+
 .header-actions {
   display: flex;
   gap: 10px;

+ 238 - 157
src/views/documents/Index.vue

@@ -131,7 +131,9 @@
                 :status="getProgressStatus(scope.row)"
               />
               <div class="converted-file-name" v-if="scope.row.converted_file_name">
-                <el-icon><Link /></el-icon> {{ scope.row.converted_file_name }}
+                <el-link type="primary" :underline="false" @click="handleDownloadConverted(scope.row)">
+                  <el-icon><Link /></el-icon> 下载转换后文件
+                </el-link>
               </div>
             </div>
           </template>
@@ -156,61 +158,72 @@
           </template>
         </el-table-column>
 
-        <el-table-column label="操作" width="140" fixed="right" align="center">
-            <template #default="scope">
-              <div class="action-buttons">
-                <el-tooltip content="编辑文档" placement="top">
-                  <el-button 
-                    link 
-                    type="primary" 
-                    @click="handleEdit(scope.row)"
-                    class="action-btn-icon"
-                  >
-                    <el-icon><Edit /></el-icon>
-                  </el-button>
-                </el-tooltip>
-                
-                <el-tooltip content="查看详情" placement="top">
-                  <el-button 
-                    link 
-                    type="primary" 
-                    @click="handleView(scope.row)"
-                    class="action-btn-icon"
-                  >
-                    <el-icon><View /></el-icon>
-                  </el-button>
-                </el-tooltip>
-                
-                <el-tooltip 
-                  :content="scope.row.conversion_status === 1 ? '转换中' : (scope.row.conversion_status === 2 ? '重新转换' : '开始转换')" 
-                  placement="top"
+        <el-table-column label="操作" width="260" fixed="right" align="center">
+          <template #default="scope">
+            <div class="action-buttons">
+              <el-tooltip content="编辑" placement="top">
+                <el-button 
+                  link 
+                  type="primary" 
+                  @click="handleEdit(scope.row)"
+                  class="action-btn-icon"
                 >
-                  <el-button 
-                    link 
-                    type="warning" 
-                    @click="handleConvert(scope.row)"
-                    :disabled="scope.row.conversion_status === 1"
-                    class="action-btn-icon"
-                  >
-                    <el-icon><Switch /></el-icon>
-                  </el-button>
-                </el-tooltip>
-
-                <el-tooltip content="删除文档" placement="top">
-                  <el-button 
-                    link 
-                    type="danger" 
-                    @click="handleDelete(scope.row)"
-                    class="action-btn-icon"
-                  >
-                    <el-icon><Delete /></el-icon>
-                  </el-button>
-                </el-tooltip>
-              </div>
-            </template>
-          </el-table-column>
+                  <el-icon><Edit /></el-icon>
+                </el-button>
+              </el-tooltip>
+              
+              <el-tooltip content="详情" placement="top">
+                <el-button 
+                  link 
+                  type="primary" 
+                  @click="handleView(scope.row)"
+                  class="action-btn-icon"
+                >
+                  <el-icon><View /></el-icon>
+                </el-button>
+              </el-tooltip>
+              
+              <el-tooltip content="下载" placement="top" v-if="scope.row.file_url">
+                <el-button 
+                  link 
+                  type="success" 
+                  @click="handleDownload(scope.row)"
+                  class="action-btn-icon"
+                >
+                  <el-icon><Download /></el-icon>
+                </el-button>
+              </el-tooltip>
+              
+              <el-tooltip 
+                :content="scope.row.conversion_status === 1 ? '转换中' : (scope.row.conversion_status === 2 ? '重新转换' : '开始转换')" 
+                placement="top"
+              >
+                <el-button 
+                  link 
+                  type="warning" 
+                  @click="handleConvert(scope.row)"
+                  :disabled="scope.row.conversion_status === 1"
+                  class="action-btn-icon"
+                >
+                  <el-icon><Switch /></el-icon>
+                </el-button>
+              </el-tooltip>
+
+              <el-tooltip content="删除" placement="top">
+                <el-button 
+                  link 
+                  type="danger" 
+                  @click="handleDelete(scope.row)"
+                  class="action-btn-icon"
+                >
+                  <el-icon><Delete /></el-icon>
+                </el-button>
+              </el-tooltip>
+            </div>
+          </template>
+        </el-table-column>
       </el-table>
-...
+
     <!-- 文档预览对话框 -->
     <el-dialog v-model="previewVisible" :title="previewTitle" width="85%" top="5vh" custom-class="preview-dialog" @closed="previewUrl = ''">
       <template #header>
@@ -272,7 +285,24 @@
           <el-input v-model="uploadForm.title" placeholder="请输入文档标题" />
         </el-form-item>
         <el-form-item label="文档链接">
-          <el-input v-model="uploadForm.file_url" placeholder="请输入文档链接 (URL)" />
+          <el-input v-model="uploadForm.file_url" placeholder="请输入文档链接 (URL) 或通过下方上传文件" />
+        </el-form-item>
+        <el-form-item label="文件上传">
+          <el-upload
+            class="upload-demo"
+            action="#"
+            :http-request="customUpload"
+            :limit="1"
+            :on-exceed="handleExceed"
+            :before-upload="beforeUpload"
+          >
+            <el-button type="primary">点击上传</el-button>
+            <template #tip>
+              <div class="el-upload__tip">
+                支持 PDF, Word, Excel, PPT, TXT 等文件,最大 50MB
+              </div>
+            </template>
+          </el-upload>
         </el-form-item>
         <el-form-item label="文档内容">
           <el-input v-model="uploadForm.content" type="textarea" :rows="4" placeholder="请输入文档内容" />
@@ -394,6 +424,9 @@
       </el-descriptions>
       <template #footer>
         <el-button @click="detailDialogVisible = false">关闭</el-button>
+        <el-button type="primary" @click="handleEditFromDetail" v-if="currentDoc">
+          <el-icon><Edit /></el-icon> 编辑文档
+        </el-button>
         <el-button type="success" @click="handleSingleEnter(currentDoc)" v-if="currentDoc && !isEntered(currentDoc.whether_to_enter)">加入知识库</el-button>
         <el-button type="primary" @click="handlePreview(currentDoc)" v-if="currentDoc?.file_url">预览原文档</el-button>
       </template>
@@ -404,55 +437,15 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Search, Filter, Upload, CircleCheck, Delete, Document, Warning, TopRight, Grid, DataAnalysis, Link, View, Switch, Edit, User } from '@element-plus/icons-vue'
+import { Search, Filter, Upload, CircleCheck, Delete, Document, Warning, TopRight, Grid, DataAnalysis, Link, View, Switch, Edit, User, Download } from '@element-plus/icons-vue'
 import request from '@/api/request'
+import axios from 'axios'
+import { downloadFile } from '@/utils/download'
 import { useAuthStore } from '@/stores/auth'
 import dayjs from 'dayjs'
+import { documentApi, type DocumentItem } from '@/api/document'
 
-// 接口定义
-interface DocumentItem {
-  id: string
-  source_id: string | null
-  source_type: string | null
-  title: string
-  content: string
-  document_type: string | null
-  professional_field: string | null
-  year: number | null
-  release_date: string | null
-  standard_no: string | null
-  status: string | null
-  whether_to_enter: number
-  file_url: string | null
-  conversion_status: number | null
-  conversion_progress: number | null
-  conversion_error: string | null
-  created_at: string
-  updated_at: string
-  created_by: string | null
-  updated_by: string | null
-  validity: string | null
-  converted_file_name: string | null
-  created_time: string | null
-  updated_time: string | null
-  file_extension: string | null
-}
-
-interface ApiResponse<T = any> {
-  code: number
-  message: string
-  data: T
-  timestamp: string
-}
-
-interface PageResult<T> {
-  items: T[]
-  total: number
-  page: number
-  size: number
-  all_total?: number
-  total_entered?: number
-}
+// 接口定义已移至 @/api/document
 
 // 状态变量
 const loading = ref(false)
@@ -478,7 +471,7 @@ const editForm = reactive({
   source_id: '',
   title: '',
   content: '',
-  table_type: 'basis',
+  table_type: 'basis' as 'basis' | 'work' | 'job',
   // 扩展字段 (子表特有属性)
   standard_no: '',
   issuing_authority: '',
@@ -508,7 +501,7 @@ const uploadForm = reactive({
   title: '',
   content: '',
   file_url: '',
-  table_type: 'basis',
+  table_type: 'basis' as 'basis' | 'work' | 'job',
   year: new Date().getFullYear()
 })
 
@@ -517,7 +510,7 @@ const proxyPreviewUrl = computed(() => {
   if (!previewUrl.value) return ''
   // 使用后端代理接口查看外部网页,附加 token 以通过认证
   const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
-  return `${baseUrl}/api/v1/documents/proxy-view?url=${encodeURIComponent(previewUrl.value)}&token=${authStore.token}`
+  return `${baseUrl}/api/v1/sample/documents/proxy-view?url=${encodeURIComponent(previewUrl.value)}&token=${authStore.token}`
 })
 
 const isHtmlPage = computed(() => {
@@ -615,17 +608,20 @@ const getKBTagType = (sourceType: string | null | undefined) => {
   return types[sourceType || ''] || 'info'
 }
 
-// 为已入库的行添加特定类名
+// 为已入库或未转化的行添加特定类名
 const tableRowClassName = ({ row }: { row: DocumentItem }) => {
   if (isEntered(row.whether_to_enter)) {
     return 'row-entered'
   }
+  if (row.conversion_status !== 2) {
+    return 'row-not-ready'
+  }
   return ''
 }
 
-// 判断是否可以勾选(已入库的数据不可勾选)
+// 判断是否可以勾选(已入库的数据不可勾选,未转化成功的数据也不可勾选
 const canSelect = (row: DocumentItem) => {
-  return !isEntered(row.whether_to_enter)
+  return !isEntered(row.whether_to_enter) && row.conversion_status === 2
 }
 
 const handleSelectionChange = (selection: DocumentItem[]) => {
@@ -635,10 +631,17 @@ const handleSelectionChange = (selection: DocumentItem[]) => {
 const handleBatchEnter = async () => {
   if (selectedIds.value.length === 0) return
   
+  // 过滤掉未转化的 ID (理论上 UI 已经拦截,但双重保险)
+  const readyIds = documents.value
+    .filter(doc => selectedIds.value.includes(doc.id) && doc.conversion_status === 2)
+    .map(doc => doc.id)
+    
+  if (readyIds.length === 0) {
+    return ElMessage.warning('选中的文档中没有已完成转化的,无法入库')
+  }
+
   try {
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-enter', { 
-      ids: selectedIds.value
-    })
+    const res = await documentApi.batchEnter(readyIds)
     if (res.code === 0) {
       ElMessage.success(res.message || '入库成功')
       fetchDocuments()
@@ -653,17 +656,23 @@ const handleBatchEnter = async () => {
 
 const handleSingleEnter = async (doc: DocumentItem | null) => {
   if (!doc) return
+  
+  if (doc.conversion_status !== 2) {
+    return ElMessage.warning('该文档尚未转化完成,暂不允许入库')
+  }
+
   try {
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-enter', {
-      ids: [doc.id]
-    })
+    const res = await documentApi.batchEnter([doc.id])
     if (res.code === 0) {
       ElMessage.success('成功加入知识库')
       detailDialogVisible.value = false
       fetchDocuments()
+    } else {
+      ElMessage.error(res.message || '入库失败')
     }
   } catch (error) {
     console.error('入库失败:', error)
+    ElMessage.error('入库失败')
   }
 }
 
@@ -679,9 +688,7 @@ const handleDelete = async (row: DocumentItem) => {
       }
     )
     
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-delete', {
-      ids: [row.id]
-    })
+    const res = await documentApi.batchDelete([row.id])
     
     if (res.code === 0) {
       ElMessage.success('删除成功')
@@ -711,9 +718,7 @@ const handleBatchDelete = async () => {
       }
     )
     
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-delete', {
-      ids: selectedIds.value
-    })
+    const res = await documentApi.batchDelete(selectedIds.value)
     
     if (res.code === 0) {
       ElMessage.success(res.message || '批量删除成功')
@@ -743,11 +748,7 @@ const getProgressStatus = (row: DocumentItem) => {
 const fetchDocuments = async () => {
   loading.value = true
   try {
-    const res = await request.get<any, ApiResponse<PageResult<DocumentItem>>>('/api/v1/documents/list', {
-      params: {
-        ...searchQuery
-      }
-    })
+    const res = await documentApi.getList(searchQuery)
     if (res.code === 0) {
       documents.value = res.data.items
       total.value = res.data.total
@@ -788,11 +789,7 @@ const stopPolling = () => {
 
 const refreshDocumentsSilently = async () => {
   try {
-    const res = await request.get<any, ApiResponse<PageResult<DocumentItem>>>('/api/v1/documents/list', {
-      params: {
-        ...searchQuery
-      }
-    })
+    const res = await documentApi.getList(searchQuery)
     if (res.code === 0) {
       documents.value = res.data.items
       total.value = res.data.total
@@ -824,9 +821,58 @@ const handleCurrentChange = (val: number) => {
   fetchDocuments()
 }
 
+const beforeUpload = (file: File) => {
+  const isLt50M = file.size / 1024 / 1024 < 50
+  if (!isLt50M) {
+    ElMessage.error('上传文件大小不能超过 50MB!')
+    return false
+  }
+  return true
+}
+
+const handleExceed = () => {
+  ElMessage.warning('只能上传一个文件,请先移除已上传的文件')
+}
+
+const customUpload = async (options: any) => {
+  const { file, onSuccess, onError } = options
+  try {
+    // 1. 获取预签名 URL
+    const res = await documentApi.getUploadUrl(file.name, file.type || 'application/octet-stream')
+
+    if (res.code !== 0) {
+      throw new Error(res.message || '获取上传链接失败')
+    }
+
+    const { upload_url, file_url } = res.data
+
+    // 2. 直接上传到 MinIO (PUT 请求)
+    await axios.put(upload_url, file, {
+      headers: {
+        'Content-Type': file.type || 'application/octet-stream'
+      }
+    })
+
+    // 3. 上传成功,更新表单
+    uploadForm.file_url = file_url
+    if (!uploadForm.title) {
+      // 如果标题为空,自动填充文件名(去掉后缀)
+      uploadForm.title = file.name.replace(/\.[^/.]+$/, "")
+    }
+    
+    ElMessage.success('文件上传成功')
+    onSuccess(res.data)
+  } catch (error: any) {
+    console.error('文件上传失败:', error)
+    ElMessage.error(error.message || '文件上传失败')
+    onError(error)
+  }
+}
+
 const handleUpload = () => {
   uploadForm.title = ''
   uploadForm.content = ''
+  uploadForm.file_url = ''
   uploadDialogVisible.value = true
 }
 
@@ -836,9 +882,7 @@ const submitUpload = async () => {
   }
   submitting.value = true
   try {
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/add', {
-      ...uploadForm
-    })
+    const res = await documentApi.add(uploadForm)
     if (res.code === 0) {
       ElMessage.success('上传成功')
       uploadDialogVisible.value = false
@@ -853,7 +897,7 @@ const submitUpload = async () => {
 
 const handleEdit = async (row: DocumentItem) => {
   try {
-    const res = await request.get<any, ApiResponse>(`/api/v1/documents/detail/${row.id}`)
+    const res = await documentApi.getDetail(row.id)
     if (res.code === 0 && res.data) {
       const data = res.data
       editForm.id = data.id
@@ -889,9 +933,7 @@ const submitEdit = async () => {
   }
   submitting.value = true
   try {
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/edit', {
-      ...editForm
-    })
+    const res = await documentApi.edit(editForm)
     if (res.code === 0) {
       ElMessage.success('更新成功')
       editDialogVisible.value = false
@@ -913,6 +955,14 @@ const handleView = (row: DocumentItem) => {
   }
 }
 
+const handleEditFromDetail = () => {
+  if (currentDoc.value) {
+    const doc = currentDoc.value
+    detailDialogVisible.value = false
+    handleEdit(doc)
+  }
+}
+
 const handlePreview = (row: DocumentItem | null) => {
   if (!row || !row.file_url) return
   previewTitle.value = row.title
@@ -921,11 +971,28 @@ const handlePreview = (row: DocumentItem | null) => {
   previewLoading.value = true
 }
 
+const handleDownload = (row: DocumentItem) => {
+  if (row.file_url) {
+    downloadFile(row.file_url, row.title)
+  } else {
+    ElMessage.warning('该文档暂无下载链接')
+  }
+}
+
+const handleDownloadConverted = (row: DocumentItem) => {
+  if (row.converted_file_name) {
+    // 提取后缀,默认为 md
+    const ext = row.converted_file_name.split('.').pop() || 'md'
+    const filename = `${row.title}_转换后.${ext}`
+    downloadFile(row.converted_file_name, filename)
+  } else {
+    ElMessage.warning('该文档暂无转换后的文件')
+  }
+}
+
 const handleConvert = async (row: DocumentItem) => {
   try {
-    const res = await request.post<any, ApiResponse>('/api/v1/documents/convert', {
-      id: row.id
-    })
+    const res = await documentApi.convert(row.id)
     if (res.code === 0) {
       ElMessage.success(res.message || '转换任务已启动')
       fetchDocuments()
@@ -1028,10 +1095,36 @@ onUnmounted(() => {
   text-decoration: underline;
 }
 
-.action-buttons {
+.action-btn {
+  padding: 4px 8px;
+  height: auto;
+  font-size: 13px;
   display: flex;
-  gap: 8px;
+  align-items: center;
+  gap: 4px;
+}
+
+.action-btn-icon {
+  padding: 4px;
+  height: 28px;
+  width: 28px;
+  display: flex;
+  align-items: center;
   justify-content: center;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.action-btn-icon:hover {
+  background-color: #f5f7fa;
+}
+
+.action-btn-icon .el-icon {
+  font-size: 16px;
+}
+
+.action-btn .el-icon {
+  font-size: 14px;
 }
 
 .file-info-cell {
@@ -1264,21 +1357,8 @@ onUnmounted(() => {
 .action-buttons {
   display: flex;
   justify-content: center;
-  gap: 12px;
-}
-
-.action-btn-icon {
-  padding: 8px !important;
-  font-size: 20px !important;
-  transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
-}
-
-.action-btn-icon:hover {
-  transform: scale(1.2);
-}
-
-.action-btn-icon .el-icon {
-  font-size: 20px;
+  align-items: center;
+  gap: 4px;
 }
 
 .dialog-header-custom {
@@ -1316,8 +1396,9 @@ onUnmounted(() => {
   }
 }
 
-/* 隐藏已入库行的复选框 */
-:deep(.row-entered .el-table-column--selection .el-checkbox) {
+/* 隐藏已入库或未转化完成行的复选框 */
+:deep(.row-entered .el-table-column--selection .el-checkbox),
+:deep(.row-not-ready .el-table-column--selection .el-checkbox) {
   display: none;
 }
 </style>

+ 0 - 292
test_frontend.html

@@ -1,292 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>前端API测试</title>
-    <style>
-        body {
-            font-family: Arial, sans-serif;
-            max-width: 1200px;
-            margin: 0 auto;
-            padding: 20px;
-            background: #f5f5f5;
-        }
-        .container {
-            background: white;
-            padding: 20px;
-            border-radius: 8px;
-            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-            margin-bottom: 20px;
-        }
-        h1 {
-            color: #333;
-            border-bottom: 2px solid #409eff;
-            padding-bottom: 10px;
-        }
-        h2 {
-            color: #666;
-            margin-top: 20px;
-        }
-        button {
-            background: #409eff;
-            color: white;
-            border: none;
-            padding: 10px 20px;
-            border-radius: 4px;
-            cursor: pointer;
-            margin: 5px;
-        }
-        button:hover {
-            background: #66b1ff;
-        }
-        .success {
-            background: #d4edda;
-            border-left: 4px solid #28a745;
-            padding: 15px;
-            margin: 10px 0;
-        }
-        .error {
-            background: #f8d7da;
-            border-left: 4px solid #dc3545;
-            padding: 15px;
-            margin: 10px 0;
-        }
-        #output {
-            background: #f9f9f9;
-            padding: 15px;
-            border-radius: 4px;
-            min-height: 100px;
-            white-space: pre-wrap;
-            font-family: monospace;
-            max-height: 600px;
-            overflow-y: auto;
-        }
-        table {
-            width: 100%;
-            border-collapse: collapse;
-            margin-top: 10px;
-        }
-        th, td {
-            border: 1px solid #ddd;
-            padding: 8px;
-            text-align: left;
-        }
-        th {
-            background-color: #409eff;
-            color: white;
-        }
-        tr:nth-child(even) {
-            background-color: #f2f2f2;
-        }
-    </style>
-</head>
-<body>
-    <div class="container">
-        <h1>🔍 前端API测试工具</h1>
-        
-        <h2>步骤 1: 登录</h2>
-        <div>
-            <input type="text" id="username" placeholder="用户名" value="admin" style="padding: 8px; margin-right: 10px;">
-            <input type="password" id="password" placeholder="密码" value="admin123" style="padding: 8px; margin-right: 10px;">
-            <button onclick="testLogin()">登录</button>
-        </div>
-        
-        <h2>步骤 2: 测试API</h2>
-        <div>
-            <button onclick="testGetUsers()">获取用户列表</button>
-            <button onclick="testGetMenus()">获取菜单</button>
-            <button onclick="testGetRoles()">获取角色列表</button>
-        </div>
-        
-        <h2>输出:</h2>
-        <div id="output">等待操作...</div>
-        
-        <div id="userTable"></div>
-    </div>
-
-    <script>
-        let accessToken = localStorage.getItem('access_token');
-        
-        function log(message, type = 'info') {
-            const output = document.getElementById('output');
-            const timestamp = new Date().toLocaleTimeString();
-            const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️';
-            output.textContent += `[${timestamp}] ${prefix} ${message}\n`;
-            output.scrollTop = output.scrollHeight;
-        }
-        
-        function clearOutput() {
-            document.getElementById('output').textContent = '';
-            document.getElementById('userTable').innerHTML = '';
-        }
-        
-        async function testLogin() {
-            clearOutput();
-            log('开始登录测试...');
-            
-            const username = document.getElementById('username').value;
-            const password = document.getElementById('password').value;
-            
-            try {
-                const response = await fetch('http://localhost:8000/api/v1/auth/login', {
-                    method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json'
-                    },
-                    body: JSON.stringify({ username, password })
-                });
-                
-                log(`状态码: ${response.status}`);
-                
-                const data = await response.json();
-                log(`响应: ${JSON.stringify(data, null, 2)}`);
-                
-                if (data.code === 0) {
-                    accessToken = data.data.access_token;
-                    localStorage.setItem('access_token', accessToken);
-                    log('登录成功!Token已保存', 'success');
-                    log(`Token: ${accessToken.substring(0, 50)}...`);
-                } else {
-                    log(`登录失败: ${data.message}`, 'error');
-                }
-            } catch (error) {
-                log(`请求失败: ${error.message}`, 'error');
-            }
-        }
-        
-        async function testGetUsers() {
-            if (!accessToken) {
-                log('请先登录!', 'error');
-                return;
-            }
-            
-            clearOutput();
-            log('开始获取用户列表...');
-            
-            try {
-                const response = await fetch('http://localhost:8000/api/v1/system/admin/users?page=1&page_size=20', {
-                    method: 'GET',
-                    headers: {
-                        'Authorization': `Bearer ${accessToken}`,
-                        'Content-Type': 'application/json'
-                    }
-                });
-                
-                log(`状态码: ${response.status}`);
-                
-                const data = await response.json();
-                
-                if (data.code === 0) {
-                    log(`获取成功!总数: ${data.data.total}`, 'success');
-                    log(`用户数据: ${JSON.stringify(data.data, null, 2)}`);
-                    
-                    // 显示用户表格
-                    displayUserTable(data.data.items);
-                } else {
-                    log(`获取失败: ${data.message}`, 'error');
-                }
-            } catch (error) {
-                log(`请求失败: ${error.message}`, 'error');
-            }
-        }
-        
-        async function testGetMenus() {
-            if (!accessToken) {
-                log('请先登录!', 'error');
-                return;
-            }
-            
-            clearOutput();
-            log('开始获取菜单...');
-            
-            try {
-                const response = await fetch('http://localhost:8000/api/v1/system/user/menus', {
-                    method: 'GET',
-                    headers: {
-                        'Authorization': `Bearer ${accessToken}`,
-                        'Content-Type': 'application/json'
-                    }
-                });
-                
-                log(`状态码: ${response.status}`);
-                
-                const data = await response.json();
-                
-                if (data.code === 0) {
-                    log(`获取成功!菜单数: ${data.data.length}`, 'success');
-                    log(`菜单数据: ${JSON.stringify(data.data, null, 2)}`);
-                } else {
-                    log(`获取失败: ${data.message}`, 'error');
-                }
-            } catch (error) {
-                log(`请求失败: ${error.message}`, 'error');
-            }
-        }
-        
-        async function testGetRoles() {
-            if (!accessToken) {
-                log('请先登录!', 'error');
-                return;
-            }
-            
-            clearOutput();
-            log('开始获取角色列表...');
-            
-            try {
-                const response = await fetch('http://localhost:8000/api/v1/system/roles/all', {
-                    method: 'GET',
-                    headers: {
-                        'Authorization': `Bearer ${accessToken}`,
-                        'Content-Type': 'application/json'
-                    }
-                });
-                
-                log(`状态码: ${response.status}`);
-                
-                const data = await response.json();
-                
-                if (data.code === 0) {
-                    log(`获取成功!角色数: ${data.data.length}`, 'success');
-                    log(`角色数据: ${JSON.stringify(data.data, null, 2)}`);
-                } else {
-                    log(`获取失败: ${data.message}`, 'error');
-                }
-            } catch (error) {
-                log(`请求失败: ${error.message}`, 'error');
-            }
-        }
-        
-        function displayUserTable(users) {
-            const tableDiv = document.getElementById('userTable');
-            
-            let html = '<h2>用户列表</h2><table>';
-            html += '<tr><th>用户名</th><th>邮箱</th><th>手机</th><th>角色</th><th>状态</th><th>管理员</th></tr>';
-            
-            users.forEach(user => {
-                html += '<tr>';
-                html += `<td>${user.username}</td>`;
-                html += `<td>${user.email}</td>`;
-                html += `<td>${user.phone || '-'}</td>`;
-                html += `<td>${user.roles || '未分配'}</td>`;
-                html += `<td>${user.is_active ? '激活' : '禁用'}</td>`;
-                html += `<td>${user.is_superuser ? '是' : '否'}</td>`;
-                html += '</tr>';
-            });
-            
-            html += '</table>';
-            tableDiv.innerHTML = html;
-        }
-        
-        // 页面加载时检查token
-        window.onload = function() {
-            if (accessToken) {
-                log('检测到已保存的Token', 'success');
-                log(`Token: ${accessToken.substring(0, 50)}...`);
-            } else {
-                log('未检测到Token,请先登录');
-            }
-        };
-    </script>
-</body>
-</html>