feat(m9): add CSV export for contract-sn report

This commit is contained in:
2026-05-25 01:37:37 +08:00
parent 46f28d2d97
commit c088c0ed71
4 changed files with 84 additions and 1 deletions
@@ -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<ProjectHealthRow> getProjectHealth() {
return reportService.getProjectHealth();
}
@GetMapping("/export")
public ResponseEntity<Resource> 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);
}
}
@@ -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<ContractSnReportRow> 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;
@@ -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 });
}
@@ -4,6 +4,7 @@
<div class="toolbar">
<span class="title">合同 SN 报表</span>
<div class="actions">
<el-button @click="handleExport" :loading="exporting">导出 CSV</el-button>
<el-button type="primary" :loading="loading" @click="load">刷新</el-button>
</div>
</div>
@@ -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
}
}
</script>
<style scoped>