You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1491 lines
38 KiB

<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue';
import { BASE_PATH } from '@/api/request';
import { getToken } from '@/utils/token';
import feedback from '@/utils/feedback';
import { ElTooltip } from 'element-plus';
import {
searchExcelData,
searchExcelDataPage,
deleteBatch,
getBatchListPage,
deleteBatchData,
type ExcelRowData
} from '@/api/price/excel';
import FilePreview from '@/components/file/preview.vue';
// ==================== 状态定义 ====================
/** 搜索关键字 - 双向绑定到搜索框 */
const searchKeyword = ref('');
/** 当前批次ID - 用于数据列表刷新 */
const currentBatchId = ref('');
/** 搜索加载状态 - 控制下拉 loading 动画 */
const searchLoading = ref(false);
/** 导入按钮加载状态 */
const uploadLoading = ref(false);
/** 弹窗显示状态 */
const detailDialogVisible = ref(false);
/** 当前 Tab 页签 */
const activeTab = ref('table-list');
/** 批次列表数据 - 存储所有导入的表格 */
const batchList = ref<ExcelRowData[]>([]);
/** 批次列表加载状态 */
const batchLoading = ref(false);
/** 批次列表分页参数 */
const batchPagination = ref({
current: 1,
size: 10,
total: 0
});
/** 批次列表搜索关键字 */
const batchKeyword = ref('');
/** 搜索结果数据 - 搜索匹配到的所有数据 */
const searchResultList = ref<ExcelRowData[]>([]);
/** 搜索结果加载状态 */
const searchResultLoading = ref(false);
/** 保存的搜索关键字 - 用于在 autocomplete 选择后恢复原始关键字进行匹配 */
const savedKeyword = ref('');
/** 当前选中的行数据 - 用于卡片详情展示 */
const currentRow = ref<ExcelRowData | null>(null);
/** 动态列名数组 - 从 rowData JSON 解析生成 */
const detailColumns = ref<string[]>([]);
/** 文件预览弹窗状态 */
const previewVisible = ref(false);
/** 预览文件列表 */
const previewFiles = ref<{ fileName: string; filePath: string }[]>([]);
// ==================== 计算属性 ====================
/** 格式化日期 */
const formatDate = (dateStr: string) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
/** 获取纯文本(去掉HTML标签),用于悬浮提示显示 */
const getRawText = (html: string): string => {
if (!html) return '';
return html.replace(/<[^>]*>/g, '');
};
/**
* 高亮关键字
* @param text - 原始文本
* @returns 包含高亮的 HTML 字符串
*/
const highlightKeyword = (text: string): string => {
if (!text) return '';
const keyword = searchKeyword.value.toLowerCase().trim();
if (!keyword) return text;
const regex = new RegExp(`(${escapeRegExp(keyword)})`, 'gi');
return text.replace(regex, '<mark class="highlight">$1</mark>');
};
/**
* 获取匹配最佳的字段值(类似百度搜索)
* @param item - 选中的数据项
* @param keyword - 用户输入的关键字
* @returns 匹配最佳的字段 { key, value }
*/
const getBestMatchField = (item: ExcelRowData, keyword: string) => {
const keywordLower = keyword.toLowerCase();
if (activeTab.value === 'table-list') {
// 表格 Tab:直接使用 batchName
return { key: 'batchName', value: item.batchName || item.fileName || '' };
}
// 数据 Tab:解析 rowData,找到匹配次数最多的字段
try {
const rowObj = JSON.parse(item.rowData);
let bestField = { key: '', value: '', matchCount: 0 };
Object.entries(rowObj).forEach(([key, val]) => {
const text = String(val);
const textLower = text.toLowerCase();
// 统计关键字匹配次数
const count = (textLower.match(new RegExp(escapeRegExp(keywordLower), 'gi')) || []).length;
// 优先选择匹配次数多的,其次选择文本更长的
if (count > bestField.matchCount ||
(count === bestField.matchCount && text.length > bestField.value.length)) {
bestField = { key, value: text, matchCount: count };
}
});
return bestField;
} catch {
return { key: '', value: '' };
}
};
// ==================== 搜索相关方法 ====================
/**
* 执行搜索 - el-autocomplete 的 fetch-suggestions 回调格式
*
* 【核心逻辑】
* 1. el-autocomplete 会传入 (queryString, callback)
* 2. 根据当前 Tab 调用不同接口:
* - 表格列表 Tab: 调用批次列表接口
* - 数据列表 Tab: 调用数据搜索接口
* 3. 将结果通过 callback 传递给下拉组件显示
*
* @param queryString - 用户输入的搜索关键字
* @param callback - 回调函数,将结果数组传入下拉组件
*/
const performSearch = (queryString: string, callback: (results: ExcelRowData[]) => void) => {
// 【边界判断】关键字为空时,直接返回空数组
if (!queryString || !queryString.trim()) {
callback([]);
return;
}
// 保存用户输入的关键字,用于选择后匹配
savedKeyword.value = queryString.trim();
// 【请求开始】开启 loading 状态
searchLoading.value = true;
if (activeTab.value === 'table-list') {
/**
* 【表格列表 Tab】调用批次列表接口
* 接口地址: POST /price/excel/batch/page
*/
getBatchListPage(queryString.trim(), 1, 20)
.then((res: any) => {
if (res?.records) {
callback(res.records);
} else {
callback([]);
}
})
.catch((error) => {
console.error('搜索失败:', error);
callback([]);
})
.finally(() => {
searchLoading.value = false;
});
} else {
/**
* 【数据列表 Tab】调用数据搜索接口
* 接口地址: POST /price/excel/search
*/
searchExcelData(queryString.trim(), 20)
.then((res: any) => {
if (res?.records) {
callback(res.records);
} else {
callback([]);
}
})
.catch((error) => {
console.error('搜索失败:', error);
callback([]);
})
.finally(() => {
searchLoading.value = false;
});
}
};
/**
* 选中联想结果 - 切换到数据列表 Tab
*
* 【核心逻辑】
* 1. 保持搜索关键字不变
* 2. 切换到数据列表 Tab
* 3. 【重要】不重新请求,数据已经在列表中
*
* @param item - 选中的 ExcelRowData 对象
*/
const handleSelectSuggestion = (item: ExcelRowData) => {
// 使用保存的原始关键字进行匹配,而不是已被 autocomplete 覆盖的 searchKeyword
const keyword = savedKeyword.value.toLowerCase().trim();
// 获取匹配最佳的字段值
const bestMatch = getBestMatchField(item, keyword);
// 填入搜索框并自动触发搜索
nextTick(() => {
searchKeyword.value = bestMatch.value;
handleSearchBtnClick();
});
};
/**
* 点击搜索按钮 - 根据当前 Tab 执行搜索
*/
const handleSearchBtnClick = () => {
if (activeTab.value === 'table-list') {
/**
* 【表格列表 Tab】调用批次列表接口
*/
batchPagination.value.current = 1;
batchKeyword.value = searchKeyword.value;
loadBatchList();
} else {
/**
* 【数据列表 Tab】调用数据搜索接口
*/
activeTab.value = 'data-list';
loadSearchResults(searchKeyword.value);
}
};
/**
* 清空搜索框 - 重置筛选效果
*/
const handleClearSearch = () => {
if (activeTab.value === 'table-list') {
// 表格列表 Tab:重置分页和批次关键字,重新加载
batchKeyword.value = '';
batchPagination.value.current = 1;
loadBatchList();
} else {
// 数据列表 Tab:用空关键字刷新当前批次数据
loadSearchResults('', currentBatchId.value);
}
};
/**
* 解析 rowData JSON 字符串为对象
*
* @param rowData - JSON 字符串
* @returns 解析后的对象,解析失败返回空对象
*/
const parseRowData = (rowData: string) => {
try {
return JSON.parse(rowData);
} catch {
return {};
}
};
/**
* 格式化 rowData 为带高亮的键值对列表
*
* 【核心逻辑】
* 解析 rowData JSON,将每个键值对格式化为可读列表
* 搜索关键字会高亮显示
*
* @param item - ExcelRowData 对象
* @returns 包含高亮的 HTML 字符串
*/
const getCardPreview = (item: ExcelRowData) => {
try {
const rowObj = JSON.parse(item.rowData);
const keyword = searchKeyword.value.toLowerCase().trim();
// 转换为键值对数组
const entries = Object.entries(rowObj).map(([key, value]) => {
let displayValue = typeof value === 'object'
? JSON.stringify(value)
: String(value);
// 高亮匹配的关键字
const highlightText = (text: string) => {
if (!keyword) return text;
const regex = new RegExp(`(${escapeRegExp(keyword)})`, 'gi');
return text.replace(regex, '<mark class="highlight">$1</mark>');
};
return {
key: highlightText(key),
value: highlightText(displayValue)
};
});
return entries;
} catch {
return [];
}
};
/**
* 转义正则特殊字符
*/
const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
// ==================== 表格列表相关方法 ====================
/**
* 加载所有批次列表
*
* 【核心逻辑】
* 1. 调用搜索接口(关键字为空)获取所有数据
* 2. 按批次分组汇总信息
*/
const loadBatchList = async () => {
debugger
batchLoading.value = true;
try {
const res: any = await getBatchListPage(
batchKeyword.value,
batchPagination.value.current,
batchPagination.value.size
);
if (res?.records) {
batchList.value = res.records;
batchPagination.value.total = res.total || res.records.length;
}
} catch (error) {
console.error('加载批次列表失败:', error);
} finally {
batchLoading.value = false;
}
};
/**
* 查看批次下的所有数据
*
* @param batch - 批次信息
*/
const viewBatchData = (batch: ExcelRowData) => {
/**
* 【UI 操作】切换到数据列表 Tab
*/
activeTab.value = 'data-list';
/**
* 【数据赋值】保存批次名称到搜索关键字
*/
searchKeyword.value = batch.batchName;
/**
* 【数据赋值】保存批次ID用于刷新
*/
currentBatchId.value = batch.batchId;
/**
* 【数据赋值】加载该批次的所有数据
*/
loadSearchResults('', batch.batchId);
};
// ==================== 数据列表相关方法 ====================
/**
* 加载搜索结果
*
* @param keyword - 搜索关键字(可选)
* @param batchId - 批次ID(可选)
*/
const loadSearchResults = async (keyword: string = '', batchId?: string) => {
searchResultLoading.value = true;
try {
/**
* 【API 请求】分页搜索数据
* 接口地址: POST /price/excel/search
*/
const res: any = await searchExcelDataPage({
keyword,
batchId,
current: 1,
size: 100 // 获取更多数据用于卡片展示
});
if (res?.records) {
/**
* 【数据赋值】保存搜索结果
*/
searchResultList.value = res.records;
}
} catch (error) {
console.error('加载搜索结果失败:', error);
feedback.msgError('加载数据失败');
} finally {
searchResultLoading.value = false;
}
};
/**
* 查看数据卡片详情
*
* @param item - ExcelRowData 对象
*/
const viewDataDetail = (item: ExcelRowData) => {
/**
* 【数据赋值】保存当前行数据
*/
currentRow.value = item;
/**
* 【数据解析】从 rowData JSON 中提取列名
*/
try {
const rowObj = JSON.parse(item.rowData);
/**
* 【数据赋值】将 JSON 对象的键名作为表格列名
*/
detailColumns.value = Object.keys(rowObj);
} catch {
detailColumns.value = [];
}
/**
* 【UI 操作】打开详情弹窗
*/
detailDialogVisible.value = true;
};
// ==================== 导入相关方法 ====================
/**
* 上传前校验
*
* 【校验规则】
* 1. 文件格式必须是 .xls 或 .xlsx
* 2. 文件大小不能超过 100MB
*
* @param file - 上传的文件对象
* @returns 校验通过返回 true,否则返回 false
*/
const beforeUpload = (file: File) => {
// 【文件格式校验】只允许 Excel 文件
const isExcel = file.name.endsWith('.xls') || file.name.endsWith('.xlsx');
if (!isExcel) {
feedback.msgError('只能上传 Excel 文件');
return false;
}
// 【文件大小校验】限制 100MB
const isLt100M = file.size / 1024 / 1024 < 100;
if (!isLt100M) {
feedback.msgError('文件大小不能超过 100MB');
return false;
}
// 【UI 状态】开启上传 loading
uploadLoading.value = true;
return true;
};
/**
* 上传成功回调
*
* @param response - 后端返回的响应数据
*/
const handleUploadSuccess = (response: any) => {
// 【UI 状态】关闭上传 loading
uploadLoading.value = false;
// 【响应判断】根据 code 判断是否成功
if (response?.code === 200) {
feedback.msgSuccess('导入成功');
/**
* 【数据刷新】重新加载表格列表
*/
loadBatchList();
} else {
feedback.msgError(response?.message || '导入失败');
}
};
/**
* 上传失败回调
*/
const handleUploadError = () => {
// 【UI 状态】关闭上传 loading
uploadLoading.value = false;
feedback.msgError('上传失败');
};
// ==================== 弹窗方法 ====================
/**
* 关闭弹窗
*/
const handleCloseDialog = () => {
// 【UI 操作】关闭弹窗
detailDialogVisible.value = false;
// 【数据清理】清空已选数据
currentRow.value = null;
detailColumns.value = [];
};
/**
* 打开文件预览
* @param row - 批次数据行
*/
const handleFilePreview = (row: ExcelRowData) => {
debugger
previewFiles.value = [{ fileName: row.fileName, filePath: row.filePath }];
previewVisible.value = true;
};
// ==================== 分页处理 ====================
/**
* 批次列表分页变化
*/
const handleBatchPageChange = (page: number) => {
batchPagination.value.current = page;
loadBatchList();
};
/**
* 批次列表每页条数变化
*/
const handleBatchSizeChange = (size: number) => {
batchPagination.value.size = size;
batchPagination.value.current = 1;
loadBatchList();
};
// ==================== 删除操作 ====================
/**
* 删除批次
*/
const handleDeleteBatch = async (batch: ExcelRowData) => {
try {
await feedback.confirm('确定删除该批次吗?删除后数据将无法恢复!', '确定');
await deleteBatchData(batch.batchName);
feedback.msgSuccess('删除成功');
loadBatchList();
} catch (error: any) {
if (error !== 'cancel') {
feedback.msgError(error.message);
}
}
};
// ==================== 响应式监听 ====================
/**
* 监听搜索关键字变化,当清空时自动刷新当前 Tab 数据
*/
watch(searchKeyword, (newVal, oldVal) => {
// 当关键字从有值变为空时,刷新数据
if (oldVal && oldVal.length > 0 && (!newVal || newVal.length === 0)) {
handleClearSearch();
}
});
// ==================== 初始化 ====================
/**
* 【生命周期】组件挂载时加载数据
*/
loadBatchList();
</script>
<template>
<div class="container">
<!-- 页面标题 -->
<h2 class="mb-20">价格库搜索</h2>
<!-- ==================== 搜索区域 ==================== -->
<div class="search-section mb-20">
<el-row :gutter="20">
<el-col :span="20">
<el-autocomplete
v-model="searchKeyword"
:fetch-suggestions="performSearch"
:placeholder="activeTab === 'table-list' ? '输入关键字搜索表格...' : '输入关键字搜索表格数据...'"
:trigger-on-focus="false"
clearable
:debounce="300"
:loading="searchLoading"
value-key="fileName"
class="search-input"
popper-class="search-suggestions-popper"
@select="handleSelectSuggestion"
@clear="handleClearSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<!-- 自定义下拉项模板 -->
<template #default="{ item }">
<div class="suggestion-item">
<template v-if="activeTab === 'table-list'">
<!-- 表格列表 Tab:显示批次名称 -->
<div class="batch-name-item">
<el-icon><Document /></el-icon>
<span v-html="highlightKeyword(item.batchName || item.fileName || '')"></span>
</div>
</template>
<template v-else>
<!-- 数据列表 Tab:显示行数据预览 -->
<div class="row-data-list">
<el-tooltip
v-for="(field, index) in getCardPreview(item)"
:key="index"
:content="getRawText(field.value)"
placement="bottom"
effect="light"
:enterable="true"
:show-after="300"
:hide-after="0"
>
<div class="row-field">
<span class="field-key" v-html="field.key"></span>
<span class="field-separator">:</span>
<span class="field-value" v-html="field.value"></span>
</div>
</el-tooltip>
</div>
</template>
</div>
</template>
</el-autocomplete>
</el-col>
<el-col :span="4">
<el-button type="primary" class="search-btn" @click="handleSearchBtnClick">
<el-icon><Search /></el-icon>
搜索
</el-button>
</el-col>
</el-row>
</div>
<!-- ==================== 导入区域 ==================== -->
<div class="upload-section">
<div class="upload-card">
<div class="upload-content">
<div class="upload-icon-wrapper">
<el-icon class="upload-icon"><Upload /></el-icon>
</div>
<div class="upload-text">
<div class="upload-title">上传 Excel 文件</div>
<div class="upload-tips">支持 .xls .xlsx 格式单个文件不超过 100MB</div>
</div>
</div>
<el-upload
:action="`${BASE_PATH}/price/excel/import`"
:headers="{ Authorization: getToken() }"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:show-file-list="false"
:accept="'.xls,.xlsx'"
class="upload-trigger"
>
<el-button type="primary" :loading="uploadLoading" class="upload-btn">
<el-icon v-if="!uploadLoading"><Upload /></el-icon>
{{ uploadLoading ? '上传中...' : '选择文件' }}
</el-button>
</el-upload>
</div>
</div>
<!-- ==================== 列表展示区域 ==================== -->
<div class="list-section">
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" class="content-tabs">
<!-- 表格列表 Tab -->
<el-tab-pane label="表格列表" name="table-list">
<template #label>
<div class="tab-label">
<el-icon><FolderOpened /></el-icon>
<span>表格列表</span>
<el-badge :value="batchPagination.total" type="primary" />
</div>
</template>
<!-- 表格列表 - 表格布局 -->
<div class="batch-table" v-loading="batchLoading">
<el-empty v-if="batchList.length === 0" description="暂无导入的表格" />
<el-table
v-else
:data="batchList"
border
stripe
style="width: 100%"
@row-click="viewBatchData"
>
<el-table-column label="文件名" prop="fileName" min-width="180">
<template #default="{ row }">
<div class="file-name-cell">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name-link" @click.stop="handleFilePreview(row)">{{ row.fileName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="批次名称" prop="batchName" min-width="200">
<template #default="{ row }">
<el-tag type="info" effect="plain">{{ row.batchName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="上传时间" prop="uploadTime" width="160">
<template #default="{ row }">
<span class="time-text">
<el-icon><Clock /></el-icon>
{{ formatDate(row.uploadTime) }}
</span>
</template>
</el-table-column>
<el-table-column label="上传人" prop="uploadUserName" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" type="success">{{ row.uploadUserName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="数据条数" prop="rowIndex" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" type="primary">
<el-icon><List /></el-icon>
{{ row.rowIndex }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ row }">
<el-button type="primary" size="small" @click.stop="viewBatchData(row)">
查看
</el-button>
<el-button type="danger" size="small" @click.stop="handleDeleteBatch(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper" v-if="batchList.length > 0">
<el-pagination
v-model:current-page="batchPagination.current"
v-model:page-size="batchPagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="batchPagination.total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleBatchPageChange"
@size-change="handleBatchSizeChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 数据列表 Tab -->
<el-tab-pane label="数据列表" name="data-list">
<template #label>
<div class="tab-label">
<el-icon><DataLine /></el-icon>
<span>数据列表</span>
<el-badge :value="searchResultList.length" type="success" />
</div>
</template>
<!-- 搜索结果提示 -->
<div class="search-tip" v-if="searchKeyword">
<el-icon><InfoFilled /></el-icon>
当前显示:<strong>{{ searchKeyword }}</strong> 相关的 {{ searchResultList.length }} 条数据
</div>
<!-- 数据列表 - 卡片布局 -->
<div class="card-list" v-loading="searchResultLoading">
<el-empty v-if="searchResultList.length === 0" description="请通过上方搜索框搜索数据" />
<div
v-for="(item, index) in searchResultList"
:key="item.id || index"
class="data-card"
@click="viewDataDetail(item)"
>
<div class="data-card-header">
<div class="data-source">
<el-icon class="source-icon"><Document /></el-icon>
<span class="source-name clickable" @click.stop="handleFilePreview(item)">{{ item.fileName }}</span>
<el-tag size="small" type="info">{{ item.sheetName }}</el-tag>
</div>
<span class="row-number">第 {{ item.rowIndex }} 行</span>
</div>
<div class="data-card-body">
<div class="row-data-list">
<el-tooltip
v-for="(field, fIdx) in getCardPreview(item)"
:key="fIdx"
:content="getRawText(field.value)"
placement="bottom"
effect="light"
:enterable="true"
:show-after="300"
:hide-after="0"
>
<div class="row-field">
<span class="field-key" v-html="field.key"></span>
<span class="field-separator">:</span>
<span class="field-value" v-html="field.value"></span>
</div>
</el-tooltip>
</div>
</div>
<div class="data-card-footer">
<span class="batch-name" v-if="item.batchName">
<el-icon><Folder /></el-icon>
{{ item.batchName }}
</span>
<el-button type="primary" link size="small">
查看详情
<el-icon><View /></el-icon>
</el-button>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- ==================== 数据详情弹窗 ==================== -->
<el-dialog
v-model="detailDialogVisible"
title="数据详情"
width="900px"
:close-on-click-modal="false"
@close="handleCloseDialog"
class="detail-dialog"
>
<!-- 数据源信息卡片 -->
<div class="info-card mb-16">
<div class="card-header">
<el-icon><Document /></el-icon>
<span>数据来源</span>
</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="文件名">
<el-tag type="primary" effect="plain">{{ currentRow?.fileName }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Sheet名称">
<el-tag type="success" effect="plain">{{ currentRow?.sheetName }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Excel行号">
<span class="row-index">第 {{ currentRow?.rowIndex }} 行</span>
</el-descriptions-item>
<el-descriptions-item label="批次名称">
{{ currentRow?.batchName || '默认批次' }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 数据内容展示 -->
<div class="data-card">
<div class="card-header">
<el-icon><DataLine /></el-icon>
<span>数据内容</span>
</div>
<!-- 表格视图 -->
<div class="table-section">
<div class="section-title">表格视图</div>
<el-table
:data="[parseRowData(currentRow?.rowData || '')]"
border
size="small"
stripe
max-height="300"
class="detail-table"
>
<el-table-column
v-for="col in detailColumns"
:key="col"
:prop="col"
:label="col"
min-width="120"
show-overflow-tooltip
/>
</el-table>
</div>
<!-- JSON原始数据 -->
<el-collapse>
<el-collapse-item title="查看原始JSON" name="json">
<pre class="json-block">{{ currentRow?.rowData }}</pre>
</el-collapse-item>
</el-collapse>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseDialog">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 文件预览组件 -->
<FilePreview :files="previewFiles" :show="previewVisible" @update:show="previewVisible = $event" />
</div>
</template>
<style scoped lang="scss">
// ==================== 搜索区域样式 ====================
.search-section {
.search-input {
width: 100%;
:deep(.el-input__wrapper) {
width: 100%;
}
}
.search-btn {
width: 100%;
}
}
// ==================== 导入区域样式 ====================
.upload-section {
margin-bottom: 20px;
}
.upload-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
.upload-content {
display: flex;
align-items: center;
gap: 20px;
.upload-icon-wrapper {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
backdrop-filter: blur(10px);
.upload-icon {
font-size: 32px;
color: #fff;
}
}
.upload-text {
.upload-title {
font-size: 20px;
font-weight: 600;
color: #fff;
margin-bottom: 6px;
}
.upload-tips {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}
}
}
.upload-trigger {
.upload-btn {
padding: 12px 32px;
font-size: 16px;
font-weight: 500;
background: #fff;
color: #667eea;
border: none;
border-radius: 10px;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.el-icon {
margin-right: 8px;
}
}
}
}
// ==================== 列表区域样式 ====================
.list-section {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid rgba(198, 208, 251, 1);
}
.content-tabs {
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
}
.tab-label {
display: flex;
align-items: center;
gap: 6px;
.el-badge {
margin-left: 4px;
}
}
// ==================== 表格列表样式 ====================
.batch-table {
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
.file-icon {
color: var(--primary-color);
font-size: 18px;
}
.file-name-link {
color: var(--primary-color);
cursor: pointer;
transition: all 0.2s;
&:hover {
text-decoration: underline;
font-weight: 500;
}
}
}
.file-name-text {
display: inline-flex;
align-items: center;
gap: 4px;
color: #666;
}
.time-text {
display: inline-flex;
align-items: center;
gap: 4px;
color: #999;
font-size: 13px;
}
}
// ==================== 数据列表卡片 ====================
.search-tip {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #f0f6ff;
border-radius: 6px;
margin-bottom: 16px;
font-size: 13px;
color: #666;
strong {
color: var(--primary-color);
}
}
.card-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.data-card {
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 12px;
padding: 16px 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
&:hover {
border-color: #409eff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
transform: translateY(-1px);
}
.data-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 2px solid #f0f2f5;
.data-source {
display: flex;
align-items: center;
gap: 10px;
.source-icon {
color: #409eff;
font-size: 20px;
}
.source-name {
font-weight: 600;
font-size: 15px;
color: #303133;
&.clickable {
color: var(--primary-color);
cursor: pointer;
transition: all 0.2s;
&:hover {
text-decoration: underline;
font-weight: 500;
}
}
}
}
.row-number {
font-size: 13px;
color: #606266;
background: #f4f4f5;
padding: 4px 12px;
border-radius: 12px;
font-weight: 500;
}
}
.data-card-body {
margin-bottom: 12px;
.row-data-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
}
.row-field {
display: flex;
align-items: baseline;
gap: 6px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 8px;
font-size: 13px;
line-height: 1.4;
border-left: 3px solid #409eff;
overflow: hidden;
.field-key {
color: #409eff;
font-weight: 600;
white-space: nowrap;
}
.field-separator {
color: #909399;
}
.field-value {
color: #303133;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.data-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 2px solid #f0f2f5;
.batch-name {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #909399;
}
}
}
// ==================== 下拉列表样式 ====================
.suggestion-item {
padding: 12px;
margin-bottom: 8px;
background: rgba(240, 247, 255, 0.6);
border: 1px solid #b3d8fd;
border-left: 4px solid #66b1ff;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background: rgba(64, 158, 255, 0.1);
border-color: #409eff;
transform: translateX(3px);
}
&:last-child {
margin-bottom: 0;
}
.row-data-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px 12px;
}
.row-field {
display: flex;
align-items: baseline;
gap: 4px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
overflow: hidden;
.field-key {
color: #409eff;
font-weight: 600;
white-space: nowrap;
}
.field-separator {
color: #909399;
}
.field-value {
color: #606266;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
// ==================== 悬浮提示样式 ====================
.el-tooltip__popper {
font-size: 13px !important;
line-height: 1.6 !important;
max-width: 400px !important;
padding: 10px 14px !important;
background: rgba(50, 50, 50, 0.95) !important;
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
.el-tooltip__arrow::before {
background: rgba(50, 50, 50, 0.95) !important;
}
}
:deep(.highlight) {
background: linear-gradient(135deg, #ffe58f 0%, #ffd591 100%);
color: #d48806;
padding: 1px 4px;
border-radius: 4px;
font-weight: 700;
box-shadow: 0 1px 2px rgba(212, 136, 6, 0.2);
}
// ==================== 弹窗样式 ====================
.info-card {
background: linear-gradient(135deg, #f8f9ff 0%, #eef1ff 100%);
border: 1px solid #d9e2ff;
border-radius: 8px;
padding: 16px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: var(--primary-color);
font-weight: 600;
font-size: 14px;
}
.row-index {
color: var(--primary-color);
font-weight: 600;
}
}
.data-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 16px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
color: var(--primary-color);
font-weight: 600;
font-size: 14px;
}
.table-section {
margin-bottom: 16px;
.section-title {
font-size: 13px;
color: #666;
margin-bottom: 10px;
font-weight: 500;
}
.detail-table {
border-radius: 6px;
overflow: hidden;
}
}
.json-block {
margin: 0;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
font-size: 12px;
color: #606266;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
// ==================== Element Plus 深度样式 ====================
:deep(.el-autocomplete) {
width: 100%;
.el-input {
width: 100%;
}
}
:deep(.el-table) {
border-radius: 6px;
.el-table__header th {
background-color: #f5f7fa;
color: #333;
font-weight: 600;
}
.el-table__row:hover > td {
background-color: #f0f6ff;
}
}
:deep(.el-dialog__body) {
padding: 16px 24px;
}
:deep(.el-dialog__footer) {
border-top: 1px solid #ebeef5;
padding: 12px 24px;
}
:deep(.el-collapse) {
border: none;
.el-collapse-item__header {
font-size: 13px;
color: #666;
background: transparent;
border: none;
}
.el-collapse-item__wrap {
border: none;
background: transparent;
}
.el-collapse-item__content {
padding-bottom: 0;
}
}
.dialog-footer {
text-align: right;
}
// 下拉建议弹窗
:deep(.search-suggestions-popper) {
.el-autocomplete-suggestion__wrap {
padding: 8px 12px;
}
li {
padding: 0 !important;
background: transparent !important;
}
}
// 批次名称下拉项样式
.batch-name-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 4px;
font-size: 14px;
color: #333;
.el-icon {
color: #409eff;
flex-shrink: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// 分页组件样式
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding: 16px 0;
:deep(.el-pagination) {
.el-pagination__total {
font-size: 14px;
color: #606266;
}
}
}
</style>