feat:Roaming CDR自定义导出功能

This commit is contained in:
zhongzm
2025-10-20 17:04:16 +08:00
parent 81f287e9ff
commit 719a08d993
4 changed files with 730 additions and 4 deletions

View File

@@ -0,0 +1,458 @@
<template>
<a-modal
v-model:open="visible"
:title="t('common.exportCustom')"
width="750px"
:confirm-loading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="export-custom-container">
<!-- 列配置区域 -->
<div class="columns-config">
<div class="config-header">
<h4>{{ t('common.exportColumns') }}</h4>
<a-button type="link" @click="resetToDefault">
{{ t('common.resetToDefault') }}
</a-button>
</div>
<div class="columns-list">
<Container
@drop="onDrop"
:get-child-payload="getChildPayload"
drag-class="drag-ghost"
drop-class="drop-ghost"
>
<Draggable
v-for="(column, index) in customColumns"
:key="column.key"
class="column-item"
>
<div class="column-controls">
<a-checkbox
v-model:checked="column.visible"
@change="updateColumnVisibility(column)"
/>
<div class="drag-handle">
<HolderOutlined />
</div>
</div>
<div class="column-info">
<div class="column-name">
<a-input
v-model:value="column.title"
:placeholder="t('common.columnName')"
size="small"
/>
</div>
<!-- 隐藏col_数字标签不影响用户使用 -->
<!-- <div class="column-key">
<a-tag size="small" color="blue">{{ column.key }}</a-tag>
</div> -->
</div>
</Draggable>
</Container>
</div>
</div>
<!-- 预览区域 -->
<div class="preview-section">
<h4>{{ t('common.preview') }}</h4>
<div class="preview-table">
<a-table
:columns="previewColumns"
:data-source="previewData"
:pagination="false"
size="small"
:scroll="{ x: 400 }"
/>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { Modal, message } from 'ant-design-vue/es';
import { Container, Draggable } from 'vue3-smooth-dnd';
import useI18n from '@/hooks/useI18n';
import { ColumnsType } from 'ant-design-vue/es/table';
interface CustomColumn {
key: string;
title: string;
visible: boolean;
originalTitle: string;
dataIndex?: string;
customRender?: any;
columnIndex?: number; // Excel列索引
}
interface Props {
open: boolean;
originalColumns: ColumnsType;
sampleData: any[];
}
interface Emits {
(e: 'update:open', value: boolean): void;
(e: 'confirm', config: CustomColumn[]): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const { t } = useI18n();
const visible = ref(false);
const confirmLoading = ref(false);
const customColumns = ref<CustomColumn[]>([]);
// 监听props变化
watch(() => props.open, (newVal) => {
visible.value = newVal;
if (newVal) {
initCustomColumns();
}
});
watch(visible, (newVal) => {
emit('update:open', newVal);
});
// 初始化自定义列配置
function initCustomColumns() {
// 如果传入的是完整的列配置包含columnIndex直接使用
if (props.originalColumns.length > 0 && (props.originalColumns[0] as any).columnIndex !== undefined) {
const columns: CustomColumn[] = props.originalColumns.map(col => ({
key: (col as any).key,
title: (col as any).title,
visible: true,
originalTitle: (col as any).originalTitle || (col as any).title,
dataIndex: (col as any).dataIndex,
columnIndex: (col as any).columnIndex
}));
// 尝试从本地存储加载配置
const savedConfig = localStorage.getItem('sgwc-cdr-export-config');
if (savedConfig) {
try {
const saved = JSON.parse(savedConfig);
console.log('Loaded saved config:', saved);
// 基于originalTitle匹配保存的配置
const mergedColumns = columns.map(col => {
const savedCol = saved.find((s: any) => s.originalTitle === col.originalTitle);
if (savedCol) {
console.log(`Matched column: ${col.originalTitle}, visible: ${savedCol.visible}, title: ${savedCol.title}`);
return {
...col,
visible: savedCol.visible !== undefined ? savedCol.visible : true,
title: savedCol.title || col.title
};
}
return col;
});
// 按照保存的顺序排列(如果存在)
const sortedColumns = sortColumnsByConfig(mergedColumns, saved);
console.log('Final columns after merge and sort:', sortedColumns);
customColumns.value = sortedColumns;
} catch (error) {
console.error('Failed to load saved export config:', error);
customColumns.value = columns;
}
} else {
console.log('No saved config found, using default columns');
customColumns.value = columns;
}
return;
}
// 兼容旧的表格列配置
const columns: CustomColumn[] = props.originalColumns
.filter(col => col.key !== 'id' && (col as any).dataIndex) // 过滤掉操作列
.map(col => ({
key: col.key as string,
title: col.title as string,
visible: true,
originalTitle: col.title as string,
dataIndex: (col as any).dataIndex as string,
customRender: (col as any).customRender
}));
// 尝试从本地存储加载配置
const savedConfig = localStorage.getItem('sgwc-cdr-export-config');
if (savedConfig) {
try {
const saved = JSON.parse(savedConfig);
// 合并保存的配置和当前列,只合并特定字段
const mergedColumns = columns.map(col => {
const savedCol = saved.find((s: CustomColumn) => s.key === col.key);
if (savedCol) {
return {
...col,
visible: savedCol.visible, // 只合并可见性
title: savedCol.title !== savedCol.originalTitle ? savedCol.title : col.title // 只合并自定义的标题
};
}
return col;
});
customColumns.value = mergedColumns;
} catch (error) {
console.error('Failed to load saved export config:', error);
customColumns.value = columns;
}
} else {
customColumns.value = columns;
}
}
// 按照保存的配置排序列
function sortColumnsByConfig(columns: CustomColumn[], savedConfig: CustomColumn[]): CustomColumn[] {
if (!savedConfig || savedConfig.length === 0) {
return columns;
}
// 创建一个映射originalTitle -> savedOrder
const orderMap = new Map<string, number>();
savedConfig.forEach((col, index) => {
orderMap.set(col.originalTitle, index);
});
// 排序:已保存的列按保存顺序,新列放在最后
return [...columns].sort((a, b) => {
const orderA = orderMap.has(a.originalTitle) ? orderMap.get(a.originalTitle)! : 9999;
const orderB = orderMap.has(b.originalTitle) ? orderMap.get(b.originalTitle)! : 9999;
return orderA - orderB;
});
}
// 预览列配置
const previewColumns = computed(() => {
return customColumns.value
.filter(col => col.visible)
.map(col => ({
title: col.title,
dataIndex: col.dataIndex || col.key,
key: col.key,
customRender: col.customRender
}));
});
// 预览数据只显示前2条降低预览高度
const previewData = computed(() => {
return props.sampleData.slice(0, 1);
});
// 更新列可见性
function updateColumnVisibility(column: CustomColumn) {
// 确保至少有一列可见
const visibleCount = customColumns.value.filter(col => col.visible).length;
if (visibleCount === 0) {
column.visible = true;
message.warning(t('common.atLeastOneColumn'));
}
}
// 拖拽相关函数
function getChildPayload(index: number) {
return customColumns.value[index];
}
function onDrop(dropResult: any) {
const { removedIndex, addedIndex } = dropResult;
if (removedIndex !== null && addedIndex !== null) {
const item = customColumns.value[removedIndex];
customColumns.value.splice(removedIndex, 1);
customColumns.value.splice(addedIndex, 0, item);
}
}
// 重置为默认配置
function resetToDefault() {
Modal.confirm({
title: t('common.confirm'),
content: t('common.resetConfirm'),
onOk() {
// 清除本地存储的配置
localStorage.removeItem('sgwc-cdr-export-config');
// 重新初始化
initCustomColumns();
message.success(t('common.resetSuccess'));
}
});
}
// 确认导出
function handleOk() {
const visibleColumns = customColumns.value.filter(col => col.visible);
if (visibleColumns.length === 0) {
message.error(t('common.selectAtLeastOneColumn'));
return;
}
confirmLoading.value = true;
// 保存配置到本地存储
try {
// 保存时只保存必要的字段,用于下次加载时恢复配置
const configToSave = customColumns.value.map((col, index) => ({
originalTitle: col.originalTitle || col.title, // 用于匹配列
title: col.title, // 自定义标题
visible: col.visible, // 可见性
order: index // 顺序
}));
localStorage.setItem('sgwc-cdr-export-config', JSON.stringify(configToSave));
console.log('Export config saved:', configToSave);
} catch (error) {
console.error('Failed to save export config:', error);
}
// 延迟一下让用户看到loading状态
setTimeout(() => {
emit('confirm', customColumns.value);
confirmLoading.value = false;
visible.value = false;
}, 500);
}
// 取消
function handleCancel() {
visible.value = false;
}
onMounted(() => {
if (props.open) {
initCustomColumns();
}
});
// 调试功能:清除有问题的缓存
function clearProblematicCache() {
localStorage.removeItem('sgwc-cdr-export-config');
console.log('Cleared problematic cache');
}
</script>
<style lang="less" scoped>
.export-custom-container {
.columns-config {
margin-bottom: 12px;
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
}
.columns-list {
border: 1px solid #d9d9d9;
border-radius: 6px;
max-height: 200px;
overflow-y: auto;
.column-item {
display: flex;
align-items: center;
padding: 6px 10px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
&:hover {
background-color: #fafafa;
}
&:last-child {
border-bottom: none;
}
.column-controls {
display: flex;
align-items: center;
margin-right: 12px;
.drag-handle {
margin-left: 8px;
cursor: move;
color: #999;
&:hover {
color: #1890ff;
}
}
}
.column-info {
flex: 1;
display: flex;
align-items: center;
.column-name {
flex: 1;
}
.column-key {
display: none; // 隐藏标签
}
}
}
}
}
.preview-section {
h4 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: 600;
}
.preview-table {
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
max-height: 180px;
:deep(.ant-table-wrapper) {
.ant-table {
font-size: 12px;
}
.ant-table-thead > tr > th {
padding: 8px 8px;
font-size: 12px;
}
.ant-table-tbody > tr > td {
padding: 6px 8px;
font-size: 12px;
}
.ant-table-body {
max-height: 120px;
overflow-y: auto;
}
}
}
}
}
// 拖拽样式
.drag-ghost {
opacity: 0.5;
background: #f0f0f0;
}
.drop-ghost {
background: #e6f7ff;
}
</style>

View File

@@ -41,6 +41,11 @@ export default {
columnSetText: 'Column Setting',
columnSetTitle: 'Column Display / Sorting',
sizeText: 'Density',
preview:'Preview',
exportCustom:'Custom Export',
exportColumns:'Column Definition',
resetToDefault:'Reset to default columns',
exportDefault:'Export',
size: {
default: 'Default',
middle: 'Medium',

View File

@@ -41,6 +41,10 @@ export default {
columnSetText: '列设置',
columnSetTitle: '列展示/排序',
sizeText: '密度',
exportCustom:'自定义导出',
exportColumns:'列定义',
resetToDefault:'重置为默认列',
exportDefault:'全部导出',
size: {
default: '默认',
middle: '中等',

View File

@@ -22,6 +22,8 @@ import saveAs from 'file-saver';
import { useClipboard } from '@vueuse/core';
import dayjs, { type Dayjs } from 'dayjs';
import { dayjsRanges } from '@/hooks/useRangePicker';
import ExportCustomModal from '@/components/ExportCustomModal/index.vue';
import * as XLSX from 'xlsx';
const { copy } = useClipboard({ legacy: true });
const { t } = useI18n();
const ws = new WS();
@@ -397,9 +399,243 @@ function fnExportList() {
});
}
/**自定义导出 - 先获取后端数据来确定可用列 */
function fnExportCustom() {
if (modalState.confirmLoading || tablePagination.total === 0) return;
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
// 先获取后端标准格式的完整数据
const querys = toRaw(queryParams);
querys.pageNum = 1;
querys.pageSize = Math.min(tablePagination.total, 10); // 只获取前10条用于分析列结构
querys.startTime = Number(querys.startTime);
querys.endTime = Number(querys.endTime);
exportSGWCDataCDR(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 解析后端Excel文件获取可用的列
parseExcelColumns(res.data);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.catch(error => {
console.error('Export error:', error);
message.error({
content: t('common.operateError'),
duration: 3,
});
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
}
/**解析Excel获取可用的列信息 */
function parseExcelColumns(excelBlob: Blob) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target?.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON格式
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length === 0) {
message.error(t('common.noData'));
return;
}
// 获取表头(第一行)
const headers = jsonData[0] as string[];
const dataRows = jsonData.slice(1, 4); // 取前3行作为示例数据
// 构建可用列配置
const availableColumns = headers.map((header, index) => ({
key: `col_${index}`,
title: header,
originalTitle: header,
dataIndex: `col_${index}`,
columnIndex: index,
visible: true
}));
exportAvailableColumns.value = availableColumns;
// 构建示例数据
const sampleData = dataRows.map((row: any) => {
const obj: any = { id: Math.random() };
headers.forEach((header, index) => {
obj[`col_${index}`] = row[index] || '';
});
return obj;
});
exportSampleData.value = sampleData;
// 打开自定义导出对话框
exportCustomVisible.value = true;
} catch (error) {
console.error('Parse Excel error:', error);
message.error(t('common.operateError'));
}
};
reader.readAsArrayBuffer(excelBlob);
}
/**处理自定义导出确认 */
function handleExportCustomConfirm(config: any[]) {
exportCustomConfig.value = config;
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
// 先获取后端标准格式的完整数据
const querys = toRaw(queryParams);
querys.pageNum = 1;
querys.pageSize = tablePagination.total;
querys.startTime = Number(querys.startTime);
querys.endTime = Number(querys.endTime);
exportSGWCDataCDR(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 后端返回标准格式数据后,在前端进行自定义处理
processCustomExport(res.data, config);
message.success({
content: t('common.operateOk'),
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.catch(error => {
console.error('Export error:', error);
message.error({
content: t('common.operateError'),
duration: 3,
});
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
}
/**处理自定义导出 - 基于后端数据在前端自定义处理 */
function processCustomExport(excelBlob: Blob, config: any[]) {
// 读取后端返回的Excel文件
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target?.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON格式
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length === 0) {
message.error(t('common.noData'));
return;
}
// 获取表头(第一行)
const originalHeaders = jsonData[0] as string[];
const dataRows = jsonData.slice(1);
// 根据配置处理数据
const processedData = processDataWithConfig(originalHeaders, dataRows, config);
// 生成新的Excel文件
generateCustomExcelFile(processedData);
} catch (error) {
console.error('Process custom export error:', error);
message.error(t('common.operateError'));
}
};
reader.readAsArrayBuffer(excelBlob);
}
/**根据配置处理数据 */
function processDataWithConfig(originalHeaders: string[], dataRows: any[], config: any[]) {
// 获取可见的列配置
const visibleColumns = config.filter(col => col.visible);
// 处理表头
const newHeaders = visibleColumns.map(col => col.title);
// 处理数据行 - 使用columnIndex直接访问
const newDataRows = dataRows.map(row => {
return visibleColumns.map(col => {
// 使用columnIndex字段直接访问对应列的数据
const columnIndex = col.columnIndex;
return columnIndex !== undefined ? (row[columnIndex] || '') : '';
});
});
return {
headers: newHeaders,
data: newDataRows
};
}
/**生成自定义Excel文件 */
function generateCustomExcelFile(processedData: { headers: string[], data: any[] }) {
// 创建工作簿
const wb = XLSX.utils.book_new();
// 准备Excel数据
const excelData = [processedData.headers, ...processedData.data];
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(excelData);
// 设置列宽
const colWidths = processedData.headers.map(() => ({ wch: 20 }));
ws['!cols'] = colWidths;
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, 'SGWC CDR');
// 生成Excel文件并下载
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
saveAs(blob, `sgwc_cdr_custom_export_${Date.now()}.xlsx`);
}
/**实时数据开关 */
const realTimeData = ref<boolean>(false);
/**自定义导出配置 */
const exportCustomVisible = ref<boolean>(false);
const exportCustomConfig = ref<any[]>([]);
const exportAvailableColumns = ref<any[]>([]);
const exportSampleData = ref<any[]>([]);
/**
* 实时数据
*/
@@ -619,10 +855,25 @@ onBeforeUnmount(() => {
{{ t('common.deleteText') }}
</a-button>
<a-button type="dashed" @click.prevent="fnExportList()">
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="dashed">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="export-default" @click="fnExportList()">
<template #icon><ExportOutlined /></template>
{{ t('common.exportDefault') }}
</a-menu-item>
<a-menu-item key="export-custom" @click="fnExportCustom()">
<template #icon><SettingOutlined /></template>
{{ t('common.exportCustom') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
@@ -830,6 +1081,14 @@ onBeforeUnmount(() => {
</template>
</a-table>
</a-card>
<!-- 自定义导出配置模态框 -->
<ExportCustomModal
v-model:open="exportCustomVisible"
:original-columns="exportAvailableColumns"
:sample-data="exportSampleData"
@confirm="handleExportCustomConfirm"
/>
</PageContainer>
</template>