信息检索模块新增爬虫设置、检索信息列表展示、信息详情查看

This commit is contained in:
Vic 2025-07-31 22:16:28 +08:00
parent 0108dd6068
commit 23fb041a08
10 changed files with 1551 additions and 452 deletions

View File

@ -23,7 +23,7 @@ export function batchAddAttachment(businessType: string, formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachment(formData: FormData) {
export function addAttachment(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/model`,
method: 'post',
@ -38,7 +38,7 @@ export function addAttachment(formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachmentByDefectMarkPic(formData: FormData) {
export function addAttachmentByDefectMarkPic(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/defect_mark_pic`,
method: 'post',
@ -53,7 +53,7 @@ export function addAttachmentByDefectMarkPic(formData: FormData) {
* @param businessType
* @param formData
*/
export function addAttachInsurance(formData: FormData) {
export function addAttachInsurance(formData: FormData) {
return request<AttachInfoData>({
url: `/attach-info/insurance_file`,
method: 'post',

View File

@ -489,9 +489,9 @@ export const systemRoutes: RouteRecordRaw[] = [
},
},
{
path: 'project-management/project-template/information-retrieval',
path: '/project-management/project-template/information-retrieval',
name: 'InformationRetrieval',
component: () => import ('@/views/default/error/404.vue'),
component: () => import ('@/views/project-management/bidding/information-retrieval/index.vue'),
meta: {
title: '信息检索(N)',
icon: 'trophy',
@ -740,11 +740,11 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
name: 'AttachmentManagement',
component: () => import('@/views/operation-platform/data-processing/data-storage/index.vue'),
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
meta: { title: '附件管理', icon: 'attachment', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/model-config',
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
name: 'ModelConfig',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
meta: { title: '模型配置', icon: 'robot', hidden: false },
@ -755,8 +755,23 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data',
name: 'PreprocessedData',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
component: () => import('@/components/ParentView/index.vue'),
redirect: '/construction-operation-platform/implementation-workflow/data-processing/data-storage',
meta: { title: '数据预处理', icon: 'filter', hidden: false },
children: [
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageBatchUpload',
name: 'ImageBatchUpload',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-preprocessing/index.vue'),
meta: { title: '批量上传', icon: 'file', hidden: false },
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/preprocessed-data/ImageSorting',
name: 'ImageSorting',
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/image-sorting/index.vue'),
meta: { title: '图像分拣', icon: 'attachment', hidden: false },
},
],
},
{
path: '/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection',

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -105,8 +105,16 @@ const formData = reactive({
const fetchBusinessTypes = async () => {
try {
const res = await getAttachBusinessTypes()
console.log("res:",res);
if (res.data) {
businessTypes.value = res.data
res.data.forEach(item => {
const key = Object.keys(item)[0];
const value = item[key];
businessTypes.value.push({
name: value,
code:key
});
});
}
} catch (error) {
console.error('获取业务类型失败:', error)

View File

@ -0,0 +1,603 @@
<template>
<GiPageLayout>
<div class="data-preprocessing-container">
<!-- 上传进度框 -->
<div v-if="uploadState.visible" class="upload-progress-fixed">
<a-card :title="`上传进度 (${uploadState.percent}%)`" :bordered="false">
<a-progress
:percent="uploadState.percent"
:status="uploadState.status"
:stroke-width="16"
/>
<div class="progress-details">
<p><icon-file /> {{ uploadState.currentFile || '准备中...' }}</p>
<p><icon-check-circle /> 已完成 {{ uploadState.uploadedCount }}/{{ uploadState.totalCount }}</p>
<p><icon-clock-circle /> 状态: {{ getStatusText(uploadState.status) }}</p>
</div>
</a-card>
</div>
<div class="step-panel">
<div class="step-header">
<h3>批量上传图片</h3>
</div>
<div class="data-selection">
<a-form :model="form" layout="vertical">
<!-- 项目选择 -->
<a-form-item label="所属项目" required>
<a-select
v-model="form.projectId"
placeholder="请选择项目"
allow-search
:filter-option="filterProjectOption"
>
<a-option
v-for="project in projectList"
:key="project.id"
:value="project.id"
:label="project.name"
/>
</a-select>
</a-form-item>
<!-- 图片来源选择 -->
<a-form-item label="图片来源" required>
<a-select
v-model="form.imageSource"
placeholder="请选择图片来源"
allow-search
:filter-option="filterSourceOption"
>
<a-option
v-for="source in imageSources"
:key="source.value"
:value="source.value"
:label="source.label"
/>
</a-select>
</a-form-item>
<!-- 文件夹操作 -->
<a-form-item label="文件操作">
<div class="folder-actions">
<a-upload
ref="uploadRef"
directory
:multiple="true"
:show-file-list="false"
accept="image/*"
:key="uploadKey"
@change="handleFolderSelect"
>
<template #upload-button>
<a-button type="outline">
<template #icon>
<icon-folder />
</template>
选择文件夹
</a-button>
</template>
</a-upload>
<a-button
type="outline"
status="warning"
@click="clearFileList"
:disabled="selectedFiles.length === 0"
>
<template #icon>
<icon-delete />
</template>
清空列表
</a-button>
<a-button
type="primary"
:loading="uploading"
:disabled="!canUpload"
@click="handleUpload"
>
<template #icon>
<icon-upload />
</template>
开始上传
</a-button>
</div>
</a-form-item>
<!-- 文件列表 -->
<a-form-item v-if="selectedFiles.length > 0">
<div class="file-list-container">
<div class="file-list-header">
<span>已选择 {{ selectedFiles.length }} 个文件选中 {{ checkedFiles.length }} </span>
<a-checkbox
v-model="selectAll"
:indeterminate="indeterminate"
@change="handleSelectAllChange"
>
全选
</a-checkbox>
</div>
<div class="file-table-wrapper">
<a-table
:data="selectedFiles"
:columns="fileColumns"
:row-selection="rowSelection"
:pagination="false"
row-key="uid"
size="small"
bordered
>
<!-- 表格列模板 -->
<template #thumbnail="{ record }">
<div class="thumbnail-cell">
<img
v-if="isImage(record.type)"
:src="record.preview"
class="thumbnail-image"
alt="预览"
/>
<div v-else class="file-icon">
<icon-file />
</div>
</div>
</template>
<template #fileType="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<template #fileSize="{ record }">
{{ formatFileSize(record.size) }}
</template>
</a-table>
</div>
</div>
</a-form-item>
</a-form>
</div>
</div>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { UploadItem } from '@arco-design/web-vue/es/upload'
import type { SelectOptionData, TableColumnData, TableRowSelection } from '@arco-design/web-vue'
import axios from 'axios'
import {
getProjectList,
getImageSources
} from '@/apis/industrial-image'
//
interface FileItem {
uid: string
name: string
type: string
size: number
file: File
preview?: string
}
type UploadStatus = 'waiting' | 'uploading' | 'success' | 'error'
//
const projectList = ref([])
const imageSources = ref([])
//
const fetchProjectList = async () => {
try {
const res = await getProjectList({ page: 1, pageSize: 1000 });
projectList.value = res.data.map(item => ({
name: item.projectName,
id: item.projectId
}))
} catch (error) {
Message.error('获取项目列表失败')
} finally {
}
}
//
const fetchImageSourceList = async () => {
try {
const res = await getImageSources();
res.data.forEach(item => {
const key = Object.keys(item)[0];
const value = item[key];
imageSources.value.push({
label: value,
value:key
});
});
} catch (error) {
Message.error('获取项目列表失败')
} finally {
}
}
const form = reactive({
projectId: undefined as number | undefined,
imageSource: undefined as string | undefined
})
const selectedFiles = ref<FileItem[]>([])
const checkedFiles = ref<string[]>([])
const uploading = ref(false)
const uploadRef = ref()
const uploadKey = ref(0)
const uploadState = reactive({
visible: false,
percent: 0,
status: 'waiting' as UploadStatus,
currentFile: '',
uploadedCount: 0,
totalCount: 0
})
//
const canUpload = computed(() => {
return checkedFiles.value.length > 0
&& !!form.projectId
&& !!form.imageSource
&& !uploading.value
})
const selectAll = ref(false)
const indeterminate = computed(() => {
return checkedFiles.value.length > 0 &&
checkedFiles.value.length < selectedFiles.value.length
})
//
const fileColumns: TableColumnData[] = [
{
title: '选择',
dataIndex: 'selection',
type: 'selection',
width: 60,
align: 'center'
},
{
title: '预览',
dataIndex: 'thumbnail',
slotName: 'thumbnail',
width: 100,
align: 'center'
},
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
tooltip: true,
width: 300
},
{
title: '类型',
dataIndex: 'type',
slotName: 'fileType',
width: 100,
align: 'center'
},
{
title: '大小',
dataIndex: 'size',
slotName: 'fileSize',
width: 100,
align: 'center'
}
]
//
const rowSelection = reactive<TableRowSelection>({
type: 'checkbox',
showCheckedAll: false,
selectedRowKeys: checkedFiles,
onChange: (rowKeys: string[]) => {
checkedFiles.value = rowKeys
selectAll.value = rowKeys.length === selectedFiles.value.length
}
})
//
const filterProjectOption = (inputValue: string, option: SelectOptionData) => {
return option.label.toLowerCase().includes(inputValue.toLowerCase())
}
const filterSourceOption = (inputValue: string, option: SelectOptionData) => {
return option.label.toLowerCase().includes(inputValue.toLowerCase())
}
//
const handleFolderSelect = async (fileList: UploadItem[]) => {
// 1.
clearFileList()
// 2.
const newFiles: FileItem[] = []
for (const item of fileList) {
const file = item.file
if (!file) continue
const fileType = file.type.split('/')[0] || 'unknown'
const preview = fileType === 'image' ? URL.createObjectURL(file) : undefined
newFiles.push({
uid: item.uid,
name: file.name,
type: fileType,
size: file.size,
file: file,
preview: preview
})
}
// 3.
selectedFiles.value = newFiles
checkedFiles.value = newFiles.map(f => f.uid)
selectAll.value = true
// 4. key
uploadKey.value++
}
//
const clearFileList = () => {
// URL
selectedFiles.value.forEach(file => {
if (file.preview) URL.revokeObjectURL(file.preview)
})
//
selectedFiles.value = []
checkedFiles.value = []
selectAll.value = false
}
//
const handleSelectAllChange = (checked: boolean) => {
checkedFiles.value = checked ? selectedFiles.value.map(f => f.uid) : []
}
//
const handleUpload = async () => {
if (!canUpload.value) {
Message.error('请完成所有必填项并选择文件')
return
}
//
Object.assign(uploadState, {
visible: true,
percent: 0,
status: 'uploading',
currentFile: '',
uploadedCount: 0,
totalCount: checkedFiles.value.length
})
uploading.value = true
try {
const filesToUpload = selectedFiles.value.filter(f => checkedFiles.value.includes(f.uid))
const formData = new FormData()
// FormData
filesToUpload.forEach(file => {
formData.append('files', file);
});
let url =`http://pms.dtyx.net:9158/image/${form.projectId}/${form.imageSource}/upload-batch`;
let res = await axios.post(
url,
formData,
{
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
uploadedBytes = progressEvent.loaded
const elapsedTime = (Date.now() - startTime) / 1000
const speed = uploadedBytes / elapsedTime
uploadState.speed = `${formatFileSize(speed)}/s`
const remainingBytes = totalBytes - uploadedBytes
uploadState.remainingTime = formatTime(remainingBytes / speed)
uploadState.percent = Math.round((uploadedBytes / totalBytes) * 100)
}
},
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
console.log("res:",res);
uploadState.status = 'success'
Message.success(`成功上传 ${filesToUpload.length} 个文件`)
} catch (error) {
uploadState.status = 'error'
Message.error('上传失败: ' + (error as Error).message)
} finally {
uploading.value = false
// 5
setTimeout(() => uploadState.visible = false, 5000)
}
}
//
const getStatusText = (status: UploadStatus) => {
const statusMap = {
waiting: '等待上传',
uploading: '上传中',
success: '上传成功',
error: '上传失败'
}
return statusMap[status] || status
}
const getFileTypeColor = (type: string) => {
const colors: Record<string, string> = {
image: 'arcoblue',
video: 'green',
audio: 'orange',
document: 'purple'
}
return colors[type.toLowerCase()] || 'gray'
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const isImage = (type: string) => type === 'image'
//
onMounted(() => {
fetchProjectList()
fetchImageSourceList()
})
</script>
<style scoped lang="less">
.data-preprocessing-container {
position: relative;
padding: 20px;
min-height: calc(100vh - 40px);
}
.step-panel {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.step-header {
margin-bottom: 24px;
border-bottom: 1px solid var(--color-border);
padding-bottom: 16px;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
}
.folder-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
> * {
flex-shrink: 0;
}
}
.file-list-container {
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 12px;
margin-top: 16px;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 0 8px;
}
.file-table-wrapper {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 4px;
}
.file-table {
width: 100%;
:deep(.arco-table-th) {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--color-fill-2);
}
}
.thumbnail-cell {
display: flex;
justify-content: center;
align-items: center;
height: 60px;
.thumbnail-image {
max-height: 60px;
max-width: 80px;
object-fit: contain;
border-radius: 2px;
}
.file-icon {
font-size: 24px;
color: var(--color-text-3);
}
}
/* 上传进度框样式 */
.upload-progress-fixed {
position: fixed;
bottom: 24px;
right: 24px;
width: 360px;
z-index: 1000;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border-radius: 8px;
animation: fadeIn 0.3s ease;
:deep(.arco-card-header) {
border-bottom: 1px solid var(--color-border);
padding-bottom: 12px;
}
:deep(.arco-progress-text) {
font-size: 14px;
}
}
.progress-details {
margin-top: 12px;
font-size: 13px;
color: var(--color-text-2);
p {
display: flex;
align-items: center;
margin: 6px 0;
.arco-icon {
margin-right: 8px;
font-size: 16px;
}
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -106,12 +106,14 @@ const getAudioUrl = (filePath: string): string => {
const openPreview = (item: PreviewItem) => {
currentPreviewItem.value = item
audioList.value = []
for (const audio of item.audios) {
let temp={
audioId:audio.audioId,
url:getAudioUrl(audio.filePath)
if(item.audios){
for (const audio of item.audios) {
let temp={
audioId:audio.audioId,
url:getAudioUrl(audio.filePath)
}
audioList.value.push(temp)
}
audioList.value.push(temp)
}
previewModalVisible.value = true

View File

@ -251,15 +251,22 @@ const fetchPartList = async (projectId: string, turbineId: string) => {
//
const handleFilterChange = async () => {
if (!filterParams.unit) return
// if (!filterParams.project) return
loading.image = true
try {
let params = {
turbineId: filterParams.unit
projectId: filterParams.project
}
if(filterParams.unit){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit
}
}
if(filterParams.component){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit,
partId: filterParams.component
}

View File

@ -0,0 +1,198 @@
<template>
<a-modal
:visible="visible"
title="招标详情"
width="800px"
:footer="false"
:mask-closable="false"
@update:visible="(val) => $emit('update:visible', val)"
>
<a-descriptions
:column="2"
bordered
:label-style="{ width: '120px', fontWeight: 'bold' }"
>
<a-descriptions-item label="项目名称">{{ detail.projectName }}</a-descriptions-item>
<a-descriptions-item label="招标单位">{{ detail.biddingUnit }}</a-descriptions-item>
<a-descriptions-item label="预算金额">{{ detail.budgetAmount }}万元</a-descriptions-item>
<a-descriptions-item label="截止时间">{{ detail.deadline }}</a-descriptions-item>
<a-descriptions-item label="爬取时间">{{ detail.crawlingTime }}</a-descriptions-item>
<a-descriptions-item label="项目地点">{{ detail.projectLocation }}</a-descriptions-item>
<a-descriptions-item label="项目周期">{{ detail.projectDuration }}</a-descriptions-item>
<a-descriptions-item label="招标范围">{{ detail.biddingScope }}</a-descriptions-item>
<a-descriptions-item label="资质要求">{{ detail.qualificationRequirements }}</a-descriptions-item>
<a-descriptions-item label="来源平台">
<a
:href="getPlatformUrl(detail.sourcePlatform)"
target="_blank"
class="platform-link"
>
{{ detail.sourcePlatform }}
</a>
</a-descriptions-item>
<a-descriptions-item label="招标文件">
<div class="file-display">
<a-link
v-if="detail.biddingDocuments"
:href="detail.biddingDocuments"
target="_blank"
class="file-link"
>
{{ displayedFileName }}
</a-link>
<span v-else class="no-file">暂无文件</span>
<a-upload
:show-file-list="false"
:before-upload="beforeUpload"
accept=".pdf,.doc,.docx"
style="margin-left: 10px"
>
<a-button
type="outline"
size="mini"
:loading="uploading"
>
<template #icon><icon-upload /></template>
{{ detail.biddingDocuments ? '重新上传' : '上传文件' }}
</a-button>
</a-upload>
</div>
</a-descriptions-item>
</a-descriptions>
<div class="content-section">
<h3>招标内容</h3>
<div class="content-text">{{ detail.biddingContent }}</div>
<ul v-if="detail.contentItems && detail.contentItems.length">
<li v-for="(item, index) in detail.contentItems" :key="index">{{ item }}</li>
</ul>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { BiddingDetail } from './types'
const props = defineProps<{
visible: boolean
detail: BiddingDetail
uploading?: boolean
}>()
const emit = defineEmits(['update:visible', 'upload'])
const displayedFileName = computed(() => {
if (!props.detail?.biddingDocuments) return ''
const url = props.detail.biddingDocuments
return url.split('/').pop() || '招标文件'
})
// URL
const platformUrls: Record<string, string> = {
'中国招标投标网': 'https://www.cebpubservice.com/',
'国能e招': 'https://www.negc.cn/',
'中国节能': 'https://www.cecec.cn/',
'三峡招标': 'https://epp.ctg.com.cn/'
}
const getPlatformUrl = (platformName: string): string => {
return platformUrls[platformName] || '#'
}
const handleUpload = () => {
emit('upload')
}
const beforeUpload = (file: File) => {
//
const isValidType = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
].includes(file.type)
// (10MB)
const isLt10M = file.size / 1024 / 1024 < 10
if (!isValidType) {
Message.error('只能上传PDF或Word文件!')
return false
}
if (!isLt10M) {
Message.error('文件大小不能超过10MB!')
return false
}
//
emit('upload', file)
// false
return false
}
</script>
<style scoped lang="less">
.file-display {
display: flex;
align-items: center;
}
.file-link {
color: #1890ff;
text-decoration: underline;
cursor: pointer;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
&:hover {
color: #40a9ff;
}
}
.no-file {
color: var(--color-text-3);
}
.platform-link {
color: #1890ff; /* 蓝色 */
text-decoration: underline; /* 下划线 */
cursor: pointer;
&:hover {
color: #40a9ff; /* 悬停时变浅蓝色 */
text-decoration: underline;
}
}
.content-section {
margin-top: 20px;
padding: 16px;
background-color: var(--color-fill-2);
border-radius: 4px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.content-text {
margin-bottom: 12px;
line-height: 1.6;
color: var(--color-text-2);
}
ul {
padding-left: 20px;
color: var(--color-text-2);
li {
margin-bottom: 8px;
line-height: 1.6;
}
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<a-modal
:visible="visible"
title="爬虫设置"
width="800px"
@cancel="handleCancel"
@ok="handleOk"
:mask-closable="false"
>
<a-form :model="form" layout="vertical">
<a-form-item label="爬取频率">
<a-select v-model="form.frequency">
<a-option value="hourly">每小时</a-option>
<a-option value="daily">每天</a-option>
<a-option value="weekly">每周</a-option>
<a-option value="monthly">每月</a-option>
</a-select>
</a-form-item>
<a-form-item label="关键词过滤">
<a-input-tag
v-model="form.keywords"
placeholder="输入关键词后回车"
allow-clear
/>
</a-form-item>
<a-form-item label="来源平台">
<a-checkbox-group v-model="form.platforms">
<a-row :gutter="[16, 16]">
<a-col :span="8">
<a-checkbox value="国能e招">国能e招</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="中国节能">中国节能</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="科幻集团">科幻集团</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="三峡招标">三峡招标</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="三峡采购">三峡采购</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="北京京能">北京京能</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="华润守正">华润守正</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
<a-form-item label="自动通知">
<a-switch v-model="form.autoNotify" />
</a-form-item>
<template v-if="form.autoNotify">
<a-form-item label="通知方式">
<a-checkbox-group v-model="form.notifyMethods">
<a-checkbox value="inApp">站内消息</a-checkbox>
<a-checkbox value="email">电子邮件</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item v-if="form.notifyMethods.includes('email')" label="通知邮箱">
<a-input v-model="form.notifyEmail" placeholder="请输入接收通知的邮箱" />
</a-form-item>
</template>
<a-divider />
<a-form-item label="爬虫日志">
<div class="log-container">
<div v-for="(log, index) in logs" :key="index" class="log-item">
[{{ log.time }}] {{ log.message }}
</div>
</div>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleOk">保存设置</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
interface LogEntry {
time: string
message: string
}
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'save'])
const form = reactive({
frequency: 'daily',
keywords: ['风电', '叶片', '检查', '运维'],
platforms: ['国能e招', '中国节能', '三峡招标'],
autoNotify: true,
notifyMethods: ['inApp', 'email'],
notifyEmail: 'user@example.com'
})
const logs = ref<LogEntry[]>([
{ time: '2025-07-30 22:00', message: '爬虫任务已启动' },
{ time: '2025-07-30 21:00', message: '爬虫任务已启动' },
{ time: '2023-11-01 09:30', message: '成功爬取中国招标投标网数据' },
{ time: '2023-11-01 09:32', message: '成功爬取某省公共资源交易中心数据' },
{ time: '2023-11-02 10:15', message: '爬取企业自有招标平台失败:连接超时' }
])
const handleCancel = () => {
emit('update:visible', false)
}
const handleOk = () => {
Message.success('设置保存成功')
emit('save', form)
emit('update:visible', false)
}
// Watch for email notification toggle
watch(() => form.notifyMethods, (newVal) => {
if (!newVal.includes('email')) {
form.notifyEmail = ''
}
})
</script>
<style scoped lang="less">
.log-container {
height: 200px;
overflow-y: auto;
padding: 8px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background-color: var(--color-fill-2);
}
.log-item {
padding: 4px 0;
font-family: monospace;
font-size: 13px;
color: var(--color-text-2);
}
.log-item:not(:last-child) {
border-bottom: 1px dashed var(--color-border-2);
}
</style>