|
|
@@ -0,0 +1,802 @@
|
|
|
+<template>
|
|
|
+ <div class="documents-container">
|
|
|
+ <div class="header-section">
|
|
|
+ <div class="title-info">
|
|
|
+ <h2>{{ currentTitle }}</h2>
|
|
|
+ <div class="statistics-bar">
|
|
|
+ <span class="stat-item">
|
|
|
+ <el-icon><Document /></el-icon>
|
|
|
+ 全部数据: <span class="stat-value">{{ statistics.allTotal }}</span>
|
|
|
+ </span>
|
|
|
+ <span class="stat-item">
|
|
|
+ <el-icon><CircleCheck /></el-icon>
|
|
|
+ 已入库: <span class="stat-value success">{{ statistics.totalEntered }}</span>
|
|
|
+ </span>
|
|
|
+ <span class="stat-item" v-if="searchQuery.keyword || searchQuery.primaryCategoryId || searchQuery.year">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ 检索结果: <span class="stat-value">{{ total }}</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="action-buttons">
|
|
|
+ <el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
|
|
+ <el-icon><Delete /></el-icon> 批量删除
|
|
|
+ </el-button>
|
|
|
+ <el-button type="warning" :disabled="selectedIds.length === 0" @click="handleBatchEnter">
|
|
|
+ <el-icon><CircleCheck /></el-icon> 批量入库
|
|
|
+ </el-button>
|
|
|
+ <el-button type="success" class="upload-btn" @click="handleUpload">
|
|
|
+ <el-icon><Upload /></el-icon> 上传文档
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 板块切换 Tabs -->
|
|
|
+ <el-tabs v-model="activeTable" class="table-tabs" @tab-change="handleTableChange">
|
|
|
+ <el-tab-pane label="编制依据" name="basis" />
|
|
|
+ <el-tab-pane label="施工方案" name="work" />
|
|
|
+ <el-tab-pane label="办公制度" name="job" />
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
+ <el-card class="search-card">
|
|
|
+ <div class="search-bar">
|
|
|
+ <el-input
|
|
|
+ v-model="searchQuery.keyword"
|
|
|
+ placeholder="搜索文档标题、正文内容或标准号..."
|
|
|
+ class="search-input"
|
|
|
+ clearable
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+
|
|
|
+ <div class="filter-group">
|
|
|
+ <el-select v-model="searchQuery.primaryCategoryId" placeholder="一级分类" clearable @change="handlePrimaryCategoryChange" class="filter-select">
|
|
|
+ <el-option label="全部一级分类" :value="null" />
|
|
|
+ <el-option
|
|
|
+ v-for="item in primaryCategories"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-select v-model="searchQuery.secondaryCategoryId" placeholder="二级分类" clearable :disabled="!searchQuery.primaryCategoryId" class="filter-select">
|
|
|
+ <el-option label="全部二级分类" :value="null" />
|
|
|
+ <el-option
|
|
|
+ v-for="item in secondaryCategories"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-select v-model="searchQuery.year" placeholder="年份" clearable class="filter-select-year">
|
|
|
+ <el-option label="全部年份" :value="null" />
|
|
|
+ <el-option
|
|
|
+ v-for="year in yearOptions"
|
|
|
+ :key="year"
|
|
|
+ :label="year"
|
|
|
+ :value="year"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-select v-model="searchQuery.whether_to_enter" placeholder="入库状态" clearable class="filter-select">
|
|
|
+ <el-option label="全部状态" :value="null" />
|
|
|
+ <el-option label="未入库" :value="0" />
|
|
|
+ <el-option label="已入库" :value="1" />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-button type="primary" @click="handleSearch" class="search-btn">
|
|
|
+ <el-icon><Filter /></el-icon> 检索
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <div class="content-section" v-loading="loading">
|
|
|
+ <el-empty v-if="documents.length === 0" description="暂无文档数据" />
|
|
|
+ <el-table
|
|
|
+ v-else
|
|
|
+ :data="documents"
|
|
|
+ style="width: 100%"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ :row-class-name="tableRowClassName"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="55" :selectable="canSelect" />
|
|
|
+ <el-table-column prop="title" label="标题" min-width="250" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="standard_no" label="标准号" width="150" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="document_type" label="标准类型" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="professional_field" label="专业领域" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="year" label="年份" width="80" align="center" />
|
|
|
+ <el-table-column prop="release_date" label="发布日期" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatSimpleDate(scope.row.release_date) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="whether_to_enter" label="状态" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tag :type="isEntered(scope.row.whether_to_enter) ? 'success' : 'info'">
|
|
|
+ {{ isEntered(scope.row.whether_to_enter) ? '已入库' : '未入库' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="转换进度" width="200">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="conversion-progress-wrapper">
|
|
|
+ <el-progress
|
|
|
+ :percentage="scope.row.conversion_progress || 0"
|
|
|
+ :stroke-width="20"
|
|
|
+ :text-inside="true"
|
|
|
+ :status="getProgressStatus(scope.row)"
|
|
|
+ />
|
|
|
+ <div v-if="scope.row.conversion_status === 3" class="error-msg-text" :title="scope.row.conversion_error">
|
|
|
+ <el-icon><Warning /></el-icon>
|
|
|
+ <span>失败原因: {{ scope.row.conversion_error }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="220" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button link type="primary" @click="handleView(scope.row)">查看详情</el-button>
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="warning"
|
|
|
+ @click="handleConvert(scope.row)"
|
|
|
+ :disabled="scope.row.conversion_status === 1 || scope.row.conversion_status === 2"
|
|
|
+ >
|
|
|
+ {{ scope.row.conversion_status === 1 ? '转换中' : (scope.row.conversion_status === 2 ? '已转换' : '开始转换') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+...
|
|
|
+ <!-- 文档预览对话框 -->
|
|
|
+ <el-dialog v-model="previewVisible" :title="previewTitle" width="85%" top="5vh" custom-class="preview-dialog" @closed="previewUrl = ''">
|
|
|
+ <div v-loading="previewLoading" class="preview-content">
|
|
|
+ <iframe
|
|
|
+ v-if="previewUrl"
|
|
|
+ :src="proxyPreviewUrl"
|
|
|
+ width="100%"
|
|
|
+ height="100%"
|
|
|
+ frameborder="0"
|
|
|
+ allow="fullscreen"
|
|
|
+ @load="previewLoading = false"
|
|
|
+ ></iframe>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <div class="pagination-container" v-if="total > 0">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="searchQuery.page"
|
|
|
+ v-model:page-size="searchQuery.size"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="total"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传文档对话框 -->
|
|
|
+ <el-dialog v-model="uploadDialogVisible" title="上传文档" width="500px">
|
|
|
+ <el-form :model="uploadForm" label-width="100px">
|
|
|
+ <el-form-item label="文档标题" required>
|
|
|
+ <el-input v-model="uploadForm.title" placeholder="请输入文档标题" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="一级分类" required>
|
|
|
+ <el-select v-model="uploadForm.primary_category_id" placeholder="请选择一级分类" @change="handleUploadPrimaryChange">
|
|
|
+ <el-option v-for="item in primaryCategories" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="二级分类">
|
|
|
+ <el-select v-model="uploadForm.secondary_category_id" placeholder="请选择二级分类">
|
|
|
+ <el-option v-for="item in uploadSecondaryCategories" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="年份">
|
|
|
+ <el-select v-model="uploadForm.year" placeholder="请选择年份">
|
|
|
+ <el-option v-for="year in yearOptions" :key="year" :label="year" :value="year" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="文档内容">
|
|
|
+ <el-input v-model="uploadForm.content" type="textarea" :rows="4" placeholder="请输入文档内容" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="uploadDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitUpload" :loading="submitting">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<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 } from '@element-plus/icons-vue'
|
|
|
+import request from '@/api/request'
|
|
|
+import { useAuthStore } from '@/stores/auth'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+// 接口定义
|
|
|
+interface Category {
|
|
|
+ id: string | number
|
|
|
+ name: string
|
|
|
+ parent_id?: string | number | null
|
|
|
+}
|
|
|
+
|
|
|
+interface DocumentItem {
|
|
|
+ id: number
|
|
|
+ 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
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+}
|
|
|
+
|
|
|
+// 状态变量
|
|
|
+const loading = ref(false)
|
|
|
+const submitting = ref(false)
|
|
|
+const uploadDialogVisible = ref(false)
|
|
|
+const previewVisible = ref(false)
|
|
|
+const previewLoading = ref(false)
|
|
|
+const previewTitle = ref('')
|
|
|
+const previewUrl = ref('')
|
|
|
+const total = ref(0)
|
|
|
+const statistics = ref({
|
|
|
+ allTotal: 0,
|
|
|
+ totalEntered: 0
|
|
|
+})
|
|
|
+const authStore = useAuthStore()
|
|
|
+const activeTable = ref('basis')
|
|
|
+const documents = ref<DocumentItem[]>([])
|
|
|
+
|
|
|
+const currentTitle = computed(() => {
|
|
|
+ const titles: Record<string, string> = {
|
|
|
+ basis: '编制依据文档中心',
|
|
|
+ work: '施工方案文档中心',
|
|
|
+ job: '办公制度文档管理中心'
|
|
|
+ }
|
|
|
+ return titles[activeTable.value] || '文档管理中心'
|
|
|
+})
|
|
|
+const selectedIds = ref<number[]>([])
|
|
|
+const primaryCategories = ref<Category[]>([])
|
|
|
+const secondaryCategories = ref<Category[]>([])
|
|
|
+const uploadSecondaryCategories = ref<Category[]>([])
|
|
|
+
|
|
|
+const searchQuery = reactive({
|
|
|
+ keyword: '',
|
|
|
+ primaryCategoryId: null as string | number | null,
|
|
|
+ secondaryCategoryId: null as string | number | null,
|
|
|
+ year: null as number | null,
|
|
|
+ whether_to_enter: null as number | null,
|
|
|
+ page: 1,
|
|
|
+ size: 20
|
|
|
+})
|
|
|
+
|
|
|
+const uploadForm = reactive({
|
|
|
+ title: '',
|
|
|
+ content: '',
|
|
|
+ primary_category_id: null as string | number | null,
|
|
|
+ secondary_category_id: null as string | number | null,
|
|
|
+ year: new Date().getFullYear()
|
|
|
+})
|
|
|
+
|
|
|
+const yearOptions = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - i)
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+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}`
|
|
|
+})
|
|
|
+
|
|
|
+// 方法
|
|
|
+const formatSimpleDate = (date: string | null) => {
|
|
|
+ if (!date) return '-'
|
|
|
+ return dayjs(date).format('YYYY-MM-DD')
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (date: string) => {
|
|
|
+ return date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 为已入库的行添加特定类名
|
|
|
+const tableRowClassName = ({ row }: { row: DocumentItem }) => {
|
|
|
+ if (isEntered(row.whether_to_enter)) {
|
|
|
+ return 'row-entered'
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+// 判断是否可以勾选(已入库的数据不可勾选)
|
|
|
+const canSelect = (row: DocumentItem) => {
|
|
|
+ return !isEntered(row.whether_to_enter)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectionChange = (selection: DocumentItem[]) => {
|
|
|
+ selectedIds.value = selection.map(item => item.id)
|
|
|
+}
|
|
|
+
|
|
|
+const handleBatchEnter = async () => {
|
|
|
+ if (selectedIds.value.length === 0) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-enter', {
|
|
|
+ ids: selectedIds.value,
|
|
|
+ table_type: activeTable.value
|
|
|
+ })
|
|
|
+ if (res.code === 0) {
|
|
|
+ ElMessage.success(res.message || '入库成功')
|
|
|
+ fetchDocuments()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '入库失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('批量入库失败:', error)
|
|
|
+ ElMessage.error('网络错误')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDelete = async (row: DocumentItem) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要删除文档 "${row.title}" 吗?此操作不可恢复。`,
|
|
|
+ '确认删除',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-delete', {
|
|
|
+ ids: [row.id],
|
|
|
+ table_type: activeTable.value
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code === 0) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ fetchDocuments()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除文档失败:', error)
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleBatchDelete = async () => {
|
|
|
+ if (selectedIds.value.length === 0) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要批量删除选中的 ${selectedIds.value.length} 条文档吗?此操作不可恢复。`,
|
|
|
+ '确认批量删除',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const res = await request.post<any, ApiResponse>('/api/v1/documents/batch-delete', {
|
|
|
+ ids: selectedIds.value,
|
|
|
+ table_type: activeTable.value
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code === 0) {
|
|
|
+ ElMessage.success(res.message || '批量删除成功')
|
|
|
+ selectedIds.value = []
|
|
|
+ fetchDocuments()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '批量删除失败')
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('批量删除失败:', error)
|
|
|
+ ElMessage.error('操作失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleView = (row: DocumentItem) => {
|
|
|
+ if (!row.file_url) {
|
|
|
+ ElMessage.warning('该文档暂无详情链接')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ previewTitle.value = row.title
|
|
|
+ previewUrl.value = row.file_url
|
|
|
+ previewLoading.value = true
|
|
|
+ previewVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const fetchPrimaryCategories = async () => {
|
|
|
+ try {
|
|
|
+ const res = await request.get<any, ApiResponse<Category[]>>('/api/v1/documents/categories/primary')
|
|
|
+ if (res.code === 0) {
|
|
|
+ primaryCategories.value = res.data
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取一级分类失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchSecondaryCategories = async (primaryId: string | number, isUpload = false) => {
|
|
|
+ try {
|
|
|
+ const res = await request.get<any, ApiResponse<Category[]>>(`/api/v1/documents/categories/secondary?primaryId=${primaryId}`)
|
|
|
+ if (res.code === 0) {
|
|
|
+ if (isUpload) {
|
|
|
+ uploadSecondaryCategories.value = res.data
|
|
|
+ } else {
|
|
|
+ secondaryCategories.value = res.data
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取二级分类失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePrimaryCategoryChange = (val: string | number | null) => {
|
|
|
+ searchQuery.secondaryCategoryId = null
|
|
|
+ if (val) {
|
|
|
+ fetchSecondaryCategories(val)
|
|
|
+ } else {
|
|
|
+ secondaryCategories.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleUploadPrimaryChange = (val: string | number | null) => {
|
|
|
+ uploadForm.secondary_category_id = null
|
|
|
+ if (val) {
|
|
|
+ fetchSecondaryCategories(val, true)
|
|
|
+ } else {
|
|
|
+ uploadSecondaryCategories.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTableChange = (val: string) => {
|
|
|
+ activeTable.value = val as string
|
|
|
+ searchQuery.page = 1
|
|
|
+ fetchDocuments()
|
|
|
+}
|
|
|
+
|
|
|
+const fetchDocuments = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ let url = '/api/v1/documents/list'
|
|
|
+ if (searchQuery.keyword) {
|
|
|
+ url = '/api/v1/documents/search'
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await request.get<any, ApiResponse<PageResult<DocumentItem>>>(url, {
|
|
|
+ params: {
|
|
|
+ ...searchQuery,
|
|
|
+ table_type: activeTable.value
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (res.code === 0) {
|
|
|
+ documents.value = res.data.items
|
|
|
+ total.value = res.data.total
|
|
|
+ statistics.value = {
|
|
|
+ allTotal: res.data.all_total || 0,
|
|
|
+ totalEntered: res.data.total_entered || 0
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动检查是否需要开启轮询
|
|
|
+ const hasConverting = documents.value.some(doc => doc.conversion_status === 1)
|
|
|
+ if (hasConverting) {
|
|
|
+ startPolling()
|
|
|
+ } else {
|
|
|
+ stopPolling()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '获取文档列表失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取文档列表失败:', error)
|
|
|
+ ElMessage.error('网络错误,请稍后再试')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const isEntered = (val: any) => {
|
|
|
+ if (val === null || val === undefined) return false
|
|
|
+ if (typeof val === 'number') return val === 1
|
|
|
+ if (typeof val === 'string') return val === '1' || val.toLowerCase() === 'true'
|
|
|
+ if (typeof val === 'boolean') return val === true
|
|
|
+ return !!val
|
|
|
+}
|
|
|
+
|
|
|
+const getProgressStatus = (row: DocumentItem) => {
|
|
|
+ if (row.conversion_status === 1) return 'warning' // 转换中
|
|
|
+ if (row.conversion_status === 2) return 'success' // 转换完成 (100)
|
|
|
+ if (row.conversion_status === 3) return 'exception' // 转换失败
|
|
|
+ return '' // 未开始或默认 (蓝色)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = async () => {
|
|
|
+ searchQuery.page = 1
|
|
|
+ fetchDocuments()
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (val: number) => {
|
|
|
+ searchQuery.size = val
|
|
|
+ fetchDocuments()
|
|
|
+}
|
|
|
+
|
|
|
+const handleCurrentChange = (val: number) => {
|
|
|
+ searchQuery.page = val
|
|
|
+ fetchDocuments()
|
|
|
+}
|
|
|
+
|
|
|
+const handleUpload = () => {
|
|
|
+ uploadForm.title = ''
|
|
|
+ uploadForm.content = ''
|
|
|
+ uploadForm.primary_category_id = null
|
|
|
+ uploadForm.secondary_category_id = null
|
|
|
+ uploadDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const submitUpload = async () => {
|
|
|
+ if (!uploadForm.title) return ElMessage.warning('请输入标题')
|
|
|
+ if (!uploadForm.primary_category_id) return ElMessage.warning('请选择一级分类')
|
|
|
+
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const res = await request.post<any, ApiResponse>('/api/v1/documents/add', {
|
|
|
+ ...uploadForm,
|
|
|
+ table_type: activeTable.value
|
|
|
+ })
|
|
|
+ if (res.code === 0) {
|
|
|
+ ElMessage.success('上传成功')
|
|
|
+ uploadDialogVisible.value = false
|
|
|
+ fetchDocuments()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '上传失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('上传文档失败:', error)
|
|
|
+ ElMessage.error('网络错误')
|
|
|
+ } finally {
|
|
|
+ submitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDownload = (row: DocumentItem) => {
|
|
|
+ ElMessage.info(`下载文档: ${row.title}`)
|
|
|
+}
|
|
|
+
|
|
|
+const handleConvert = async (row: DocumentItem) => {
|
|
|
+ try {
|
|
|
+ const res = await request.post<any, ApiResponse>('/api/v1/documents/convert', {
|
|
|
+ id: row.id,
|
|
|
+ table_type: activeTable.value
|
|
|
+ })
|
|
|
+ if (res.code === 0) {
|
|
|
+ ElMessage.success('转换任务已启动')
|
|
|
+ fetchDocuments() // 立即刷新一次状态
|
|
|
+ startPolling() // 确保开启轮询
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.message || '启动转换失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('转换失败:', error)
|
|
|
+ ElMessage.error('网络错误')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 轮询转换状态
|
|
|
+let pollingTimer: any = null
|
|
|
+const startPolling = () => {
|
|
|
+ if (pollingTimer) return
|
|
|
+ pollingTimer = setInterval(() => {
|
|
|
+ fetchDocuments()
|
|
|
+ }, 1000)
|
|
|
+}
|
|
|
+
|
|
|
+const stopPolling = () => {
|
|
|
+ if (pollingTimer) {
|
|
|
+ clearInterval(pollingTimer)
|
|
|
+ pollingTimer = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchPrimaryCategories()
|
|
|
+ fetchDocuments()
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ stopPolling()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.documents-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.header-section {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.title-info h2 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 24px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.statistics-bar {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.conversion-progress-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 5px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-msg-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #f56c6c;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item .el-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.success {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-btn {
|
|
|
+ background-color: #67c23a;
|
|
|
+ border-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.search-bar {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input :deep(.el-input__wrapper) {
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-select {
|
|
|
+ width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-select-year {
|
|
|
+ width: 140px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-btn {
|
|
|
+ padding: 0 30px;
|
|
|
+ height: 40px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.content-section {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ min-height: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ margin-top: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content {
|
|
|
+ height: 75vh;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content iframe {
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.preview-dialog) {
|
|
|
+ .el-dialog__body {
|
|
|
+ padding: 10px 20px 20px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 隐藏已入库行的复选框 */
|
|
|
+:deep(.row-entered .el-table-column--selection .el-checkbox) {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+</style>
|