diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java index 80250fd..357686c 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/report/ReportController.java @@ -4,11 +4,18 @@ import cn.craftlabs.platform.api.service.ReportService; import cn.craftlabs.platform.api.web.dto.CallbackStatsResponse; import cn.craftlabs.platform.api.web.dto.ContractSnReportRow; import cn.craftlabs.platform.api.web.dto.ProjectHealthRow; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.util.List; @RestController @@ -39,4 +46,27 @@ public class ReportController { public List getProjectHealth() { return reportService.getProjectHealth(); } + + @GetMapping("/export") + public ResponseEntity exportReport( + @RequestParam String type, + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) Long contractId) { + + String csv = reportService.exportReport(type, projectId, contractId); + + byte[] bytes = csv.getBytes(StandardCharsets.UTF_8); + byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; + byte[] withBom = new byte[bom.length + bytes.length]; + System.arraycopy(bom, 0, withBom, 0, bom.length); + System.arraycopy(bytes, 0, withBom, bom.length, bytes.length); + + ByteArrayResource resource = new ByteArrayResource(withBom); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=report-" + type + "-" + LocalDate.now() + ".csv") + .contentType(MediaType.parseMediaType("text/csv; charset=utf-8")) + .body(resource); + } } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/ReportService.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/ReportService.java index 6c62a15..317946e 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/ReportService.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/ReportService.java @@ -162,6 +162,35 @@ public class ReportService { }).collect(Collectors.toList()); } + public String exportReport(String type, Long projectId, Long contractId) { + StringBuilder sb = new StringBuilder(); + + if ("contract-sn".equals(type)) { + sb.append("合同名称,客户,行项,应发,实发,已激活,缺口,状态\n"); + List rows = getContractSnReport(projectId, contractId); + for (ContractSnReportRow r : rows) { + sb.append(escapeCsv(r.getContractTitle())).append(","); + sb.append(escapeCsv(r.getCustomerName())).append(","); + sb.append(escapeCsv(r.getLineItemName())).append(","); + sb.append(r.getExpectedCount()).append(","); + sb.append(r.getIssuedCount()).append(","); + sb.append(r.getActivatedCount()).append(","); + sb.append(r.getGapCount()).append(","); + sb.append(escapeCsv(r.getStatus())).append("\n"); + } + } + + return sb.toString(); + } + + private String escapeCsv(String value) { + if (value == null) return ""; + if (value.contains(",") || value.contains("\"") || value.contains("\n")) { + return "\"" + value.replace("\"", "\"\"") + "\""; + } + return value; + } + private String resolveCustomerName(Long customerId) { if (customerId == null) { return null; diff --git a/web/delivery-platform-ui/src/api/platform.js b/web/delivery-platform-ui/src/api/platform.js index f680555..a90a44e 100644 --- a/web/delivery-platform-ui/src/api/platform.js +++ b/web/delivery-platform-ui/src/api/platform.js @@ -373,6 +373,9 @@ export function updateNotificationConfig(body) { export function getContractSnReport(params) { return axios.get('/api/v1/reports/contract-sn', { params }); } +export function exportReport(params) { + return axios.get('/api/v1/reports/export', { params, responseType: 'blob' }); +} export function getCallbackStats(params) { return axios.get('/api/v1/reports/callback-stats', { params }); } diff --git a/web/delivery-platform-ui/src/views/ContractSnReportView.vue b/web/delivery-platform-ui/src/views/ContractSnReportView.vue index 1c847ae..a78847f 100644 --- a/web/delivery-platform-ui/src/views/ContractSnReportView.vue +++ b/web/delivery-platform-ui/src/views/ContractSnReportView.vue @@ -4,6 +4,7 @@
合同 SN 报表
+ 导出 CSV 刷新
@@ -49,11 +50,12 @@ import { ref, reactive, onMounted } from "vue"; import { ElMessage } from "element-plus"; import { useAuthStore } from "../stores/auth"; -import { getContractSnReport } from "../api/platform"; +import { getContractSnReport, exportReport } from "../api/platform"; import { apiErrorMessage } from "../utils/apiErrorMessage"; const auth = useAuthStore(); const loading = ref(false); +const exporting = ref(false); const rows = ref([]); const kpi = reactive({ totalLineItems: 0, totalIssued: 0, totalActivated: 0, totalGap: 0 }); @@ -83,6 +85,25 @@ async function load() { loading.value = false; } } + +async function handleExport() { + exporting.value = true + try { + const response = await exportReport({ type: 'contract-sn' }) + const url = window.URL.createObjectURL(new Blob([response.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `contract-sn-report-${new Date().toISOString().slice(0, 10)}.csv`) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } catch (e) { + ElMessage.error('导出失败') + } finally { + exporting.value = false + } +}