feat: 报表导出支持
This commit is contained in:
parent
64b36285fb
commit
6e447ec96e
@ -1,9 +1,11 @@
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||
//
|
||||
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||
//
|
||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||
|
||||
using OfficeOpenXml;
|
||||
|
||||
namespace Admin.NET.Core.Service;
|
||||
|
||||
/// <summary>
|
||||
@ -297,4 +299,125 @@ public class SysReportConfigService : IDynamicApiController, ITransient
|
||||
|
||||
return isSelectQuery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出报表到Excel
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="AppFriendlyException"></exception>
|
||||
[ApiDescriptionSettings(Name = "ExportToExcel"), HttpPost]
|
||||
[DisplayName("导出报表到Excel")]
|
||||
public async Task<FileStreamResult> ExportToExcel(ReportConfigExecuteSqlScriptInput input)
|
||||
{
|
||||
var entity = await _reportConfigRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Bah(ErrorCodeEnum.D1002);
|
||||
|
||||
// 执行Sql脚本
|
||||
var data = await ExecuteSqlScript(input);
|
||||
// 重新创建忽略大小写的字典
|
||||
data = data.Select(u => new Dictionary<string, object>(u, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
var layoutConfig = GetLayoutConfig(input);
|
||||
var fields = layoutConfig.Fields.Where(f => f.Visible).ToList();
|
||||
|
||||
// 按字段原始顺序处理分组
|
||||
var orderedGroups = OrderedGroupFields(fields);
|
||||
|
||||
using var package = new ExcelPackage();
|
||||
var worksheet = package.Workbook.Worksheets.Add("ReportData");
|
||||
|
||||
int currentRow = 1;
|
||||
int startCol = 1;
|
||||
foreach (var group in orderedGroups)
|
||||
{
|
||||
int colCount = group.Count();
|
||||
worksheet.Cells[currentRow, startCol, currentRow, startCol + colCount - 1].Merge = true;
|
||||
worksheet.Cells[currentRow, startCol].Value = group.First().GroupTitle;
|
||||
startCol += colCount;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
|
||||
// 处理列标题(使用Title或FieldName)
|
||||
var curColIndex = 0;
|
||||
foreach (var field in orderedGroups.SelectMany(group => group))
|
||||
{
|
||||
worksheet.Cells[currentRow, curColIndex + 1].Value = string.IsNullOrEmpty(field.Title) ? field.FieldName : field.Title;
|
||||
curColIndex++;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
|
||||
// 填充数据
|
||||
foreach (var item in data)
|
||||
{
|
||||
curColIndex = 0;
|
||||
foreach (var field in orderedGroups.SelectMany(group => group))
|
||||
{
|
||||
worksheet.Cells[currentRow, curColIndex + 1].Value = item[field.FieldName];
|
||||
curColIndex++;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
// 处理汇总行
|
||||
var summaryFields = fields.Where(f => f.IsSummary).ToList();
|
||||
if (summaryFields.Count > 0)
|
||||
{
|
||||
worksheet.Cells[currentRow, 1].Value = "汇总";
|
||||
foreach (var field in summaryFields)
|
||||
{
|
||||
int colIndex = fields.FindIndex(f => f.FieldName == field.FieldName) + 1;
|
||||
decimal sum = data.Sum(r =>
|
||||
{
|
||||
decimal.TryParse(r[field.FieldName]?.ToString(), out decimal val);
|
||||
return val;
|
||||
});
|
||||
worksheet.Cells[currentRow, colIndex].Value = sum;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
// 自动调整列宽
|
||||
worksheet.Cells[1, 1, currentRow - 1, fields.Count].AutoFitColumns();
|
||||
|
||||
var stream = new MemoryStream();
|
||||
package.SaveAs(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
var fileName = entity.Name + $"_{DateTime.Now:yyyyMMddHHmmss}.xlsx";
|
||||
return new FileStreamResult(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = fileName };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按照字段原始顺序处理分组
|
||||
/// </summary>
|
||||
private List<IGrouping<string, SysReportField>> OrderedGroupFields(List<SysReportField> fields)
|
||||
{
|
||||
// GroupTitle 没有值,填充特定值作为独自分组
|
||||
foreach (var field in fields.Where(field => string.IsNullOrWhiteSpace(field.GroupTitle)))
|
||||
field.GroupTitle = $"-{field.FieldName}-";
|
||||
|
||||
// 按分组标题分组
|
||||
var groupFields = fields.GroupBy(field => field.GroupTitle).ToList();
|
||||
|
||||
// 按字段原始顺序处理分组
|
||||
var orderedGroups = new List<IGrouping<string, SysReportField>>();
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var group = groupFields.First(g => g.Key == field.GroupTitle);
|
||||
if (orderedGroups.Any(u => u.Key == group.Key)) continue;
|
||||
|
||||
orderedGroups.Add(group);
|
||||
}
|
||||
|
||||
// 还原 GroupTitle 为空
|
||||
foreach (var field in fields.Where(field => field.GroupTitle == $"-{field.FieldName}-"))
|
||||
field.GroupTitle = "";
|
||||
|
||||
return orderedGroups;
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ import { Configuration } from '../configuration';
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
|
||||
import { AddReportConfigInput } from '../models';
|
||||
import { AdminNETResultFileStreamResult } from '../models';
|
||||
import { AdminNETResultListDictionaryStringObject } from '../models';
|
||||
import { AdminNETResultReportConfigParseSqlOutput } from '../models';
|
||||
import { AdminNETResultSqlSugarPagedListReportConfigOutput } from '../models';
|
||||
@ -225,6 +226,54 @@ export const SysReportConfigApiAxiosParamCreator = function (configuration?: Con
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 导出报表到Excel
|
||||
* @param {ReportConfigExecuteSqlScriptInput} [body]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
apiSysReportConfigExportToExcelPost: async (body?: ReportConfigExecuteSqlScriptInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/sysReportConfig/exportToExcel`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, 'https://example.com');
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication Bearer required
|
||||
// http bearer authentication required
|
||||
if (configuration && configuration.accessToken) {
|
||||
const accessToken = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken()
|
||||
: await configuration.accessToken;
|
||||
localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
|
||||
|
||||
const query = new URLSearchParams(localVarUrlObj.search);
|
||||
for (const key in localVarQueryParameter) {
|
||||
query.set(key, localVarQueryParameter[key]);
|
||||
}
|
||||
for (const key in options.params) {
|
||||
query.set(key, options.params[key]);
|
||||
}
|
||||
localVarUrlObj.search = (new URLSearchParams(query)).toString();
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
|
||||
localVarRequestOptions.data = needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
|
||||
|
||||
return {
|
||||
url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 获取报表布局配置
|
||||
@ -486,6 +535,20 @@ export const SysReportConfigApiFp = function(configuration?: Configuration) {
|
||||
return axios.request(axiosRequestArgs);
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 导出报表到Excel
|
||||
* @param {ReportConfigExecuteSqlScriptInput} [body]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async apiSysReportConfigExportToExcelPost(body?: ReportConfigExecuteSqlScriptInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminNETResultFileStreamResult>>> {
|
||||
const localVarAxiosArgs = await SysReportConfigApiAxiosParamCreator(configuration).apiSysReportConfigExportToExcelPost(body, options);
|
||||
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
|
||||
return axios.request(axiosRequestArgs);
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 获取报表布局配置
|
||||
@ -591,6 +654,16 @@ export const SysReportConfigApiFactory = function (configuration?: Configuration
|
||||
async apiSysReportConfigExecuteSqlScriptPost(body?: ReportConfigExecuteSqlScriptInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultListDictionaryStringObject>> {
|
||||
return SysReportConfigApiFp(configuration).apiSysReportConfigExecuteSqlScriptPost(body, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 导出报表到Excel
|
||||
* @param {ReportConfigExecuteSqlScriptInput} [body]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async apiSysReportConfigExportToExcelPost(body?: ReportConfigExecuteSqlScriptInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminNETResultFileStreamResult>> {
|
||||
return SysReportConfigApiFp(configuration).apiSysReportConfigExportToExcelPost(body, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary 获取报表布局配置
|
||||
@ -685,6 +758,17 @@ export class SysReportConfigApi extends BaseAPI {
|
||||
public async apiSysReportConfigExecuteSqlScriptPost(body?: ReportConfigExecuteSqlScriptInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultListDictionaryStringObject>> {
|
||||
return SysReportConfigApiFp(this.configuration).apiSysReportConfigExecuteSqlScriptPost(body, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @summary 导出报表到Excel
|
||||
* @param {ReportConfigExecuteSqlScriptInput} [body]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SysReportConfigApi
|
||||
*/
|
||||
public async apiSysReportConfigExportToExcelPost(body?: ReportConfigExecuteSqlScriptInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminNETResultFileStreamResult>> {
|
||||
return SysReportConfigApiFp(this.configuration).apiSysReportConfigExportToExcelPost(body, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @summary 获取报表布局配置
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Admin.NET 通用权限开发平台
|
||||
* 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 全局返回结果
|
||||
*
|
||||
* @export
|
||||
* @interface AdminNETResultFileStreamResult
|
||||
*/
|
||||
export interface AdminNETResultFileStreamResult {
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
code?: number;
|
||||
|
||||
/**
|
||||
* 类型success、warning、error
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
type?: string | null;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
message?: string | null;
|
||||
|
||||
/**
|
||||
* 数据
|
||||
*
|
||||
* @type {Blob}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
result?: Blob | null;
|
||||
|
||||
/**
|
||||
* 附加数据
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
extras?: any | null;
|
||||
|
||||
/**
|
||||
* 时间
|
||||
*
|
||||
* @type {Date}
|
||||
* @memberof AdminNETResultFileStreamResult
|
||||
*/
|
||||
time?: Date;
|
||||
}
|
||||
@ -32,6 +32,7 @@ export * from './admin-netresult-create-pay-transaction-output';
|
||||
export * from './admin-netresult-data-set';
|
||||
export * from './admin-netresult-data-table';
|
||||
export * from './admin-netresult-dictionary-string-string';
|
||||
export * from './admin-netresult-file-stream-result';
|
||||
export * from './admin-netresult-generate-qrimage-output';
|
||||
export * from './admin-netresult-generate-signature-output';
|
||||
export * from './admin-netresult-get-refund-domestic-refund-by-out-refund-number-response';
|
||||
|
||||
@ -414,9 +414,6 @@ onMounted(() => {
|
||||
const openDialog = (row: any) => {
|
||||
state.ruleForm = JSON.parse(JSON.stringify(row));
|
||||
|
||||
state.fieldListData = state.ruleForm.fields ? JSON.parse(state.ruleForm.fields) : [];
|
||||
state.paramListData = state.ruleForm.params ? JSON.parse(state.ruleForm.params) : [];
|
||||
|
||||
state.isShowDialog = true;
|
||||
ruleFormRef.value?.resetFields();
|
||||
|
||||
@ -431,6 +428,11 @@ const openDialog = (row: any) => {
|
||||
if (apiParamsMonacoEditor == null || apiParamsMonacoEditor == undefined) initApiParamsMonacoEditor();
|
||||
apiParamsMonacoEditor!.setValue(state.ruleForm.apiParams ?? '');
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
state.fieldListData = state.ruleForm.fields ? JSON.parse(state.ruleForm.fields) : [];
|
||||
state.paramListData = state.ruleForm.params ? JSON.parse(state.ruleForm.params) : [];
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
@ -566,6 +568,7 @@ defineExpose({ openDialog });
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-dialog__body) {
|
||||
height: calc(100vh - 18px) !important;
|
||||
max-height: calc(100vh - 116px) !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@ -6,8 +6,9 @@
|
||||
<TableSearch ref="tableSearch" :search="state.search" @search="onSearch" v-model="state.queryParams" />
|
||||
</template>
|
||||
<template #toolbar_buttons>
|
||||
<!-- <el-button @click="handleExport"> {{ $t('导出') }} </el-button> -->
|
||||
<el-button @click="handleExport"> {{ $t('导出') }} </el-button>
|
||||
</template>
|
||||
<template #toolbar_tools> </template>
|
||||
</vxe-grid>
|
||||
</el-card>
|
||||
</div>
|
||||
@ -26,6 +27,7 @@ import TableSearch from '/@/components/table/search.vue';
|
||||
import { getAPI } from '/@/utils/axios-utils';
|
||||
import { SysReportConfigApi } from '/@/api-services/api';
|
||||
import { SysReportField, SysReportLayoutConfig, SysReportParam } from '/@/api-services/models';
|
||||
import { downloadByData, getFileName } from '/@/utils/download';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -170,13 +172,12 @@ const handleExport = () => {
|
||||
cancelButtonText: t('取消'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// TODO: 导出实现
|
||||
// getAPI(SysReportConfigApi)
|
||||
// .apiSysReportConfigExportPost(Object.assign(queryParamHandle(state.queryParam)), { responseType: 'blob' })
|
||||
// .then((res) => {
|
||||
// var fileName = getFileName(res.headers);
|
||||
// downloadByData(res.data as any, fileName);
|
||||
// });
|
||||
getAPI(SysReportConfigApi)
|
||||
.apiSysReportConfigExportToExcelPost({ id: Number(reportConfigId), execParams: state.queryParams }, { responseType: 'blob' })
|
||||
.then((res) => {
|
||||
var fileName = getFileName(res.headers);
|
||||
downloadByData(res.data as any, fileName);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ const state = reactive({
|
||||
} as EmptyObjectType,
|
||||
localPageParam: {
|
||||
pageSize: 50 as number,
|
||||
defaultSort: { field: 'id', order: 'asc', descStr: 'desc' },
|
||||
defaultSort: { field: 'id', order: 'desc', descStr: 'desc' },
|
||||
},
|
||||
title: '',
|
||||
/** 已选择的分组Id */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user