mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat: add ONLYOFFICE document preview for contract attachments
DocumentPreviewController provides preview config and file streaming endpoints. ContractDetailView adds 'preview' button to attachment list and opens ONLYOFFICE iframe dialog. Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package cn.craftlabs.platform.api.preview;
|
||||||
|
|
||||||
|
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachment;
|
||||||
|
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachmentMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
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.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/preview")
|
||||||
|
public class DocumentPreviewController {
|
||||||
|
|
||||||
|
private final PlatformContractAttachmentMapper attachmentMapper;
|
||||||
|
|
||||||
|
@Value("${onlyoffice.url:http://craftsupport.cn:8088}")
|
||||||
|
private String onlyofficeUrl;
|
||||||
|
|
||||||
|
public DocumentPreviewController(PlatformContractAttachmentMapper attachmentMapper) {
|
||||||
|
this.attachmentMapper = attachmentMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{attachmentId}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getPreviewConfig(@PathVariable("attachmentId") Long attachmentId) {
|
||||||
|
PlatformContractAttachment attachment = attachmentMapper.selectById(attachmentId);
|
||||||
|
if (attachment == null) {
|
||||||
|
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "附件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = "";
|
||||||
|
String fileName = attachment.getFileName();
|
||||||
|
if (fileName != null && fileName.contains(".")) {
|
||||||
|
ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> config = new java.util.LinkedHashMap<>();
|
||||||
|
config.put("document", Map.of(
|
||||||
|
"fileType", ext,
|
||||||
|
"key", "attachment_" + attachmentId,
|
||||||
|
"title", attachment.getFileName(),
|
||||||
|
"url", getFileUrl(attachmentId),
|
||||||
|
"permissions", Map.of("download", false, "edit", false, "print", false)
|
||||||
|
));
|
||||||
|
config.put("editorConfig", Map.of(
|
||||||
|
"mode", "view",
|
||||||
|
"customization", Map.of("autosave", false, "chat", false, "compactHeader", true)
|
||||||
|
));
|
||||||
|
config.put("documentServerUrl", onlyofficeUrl);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{attachmentId}/file")
|
||||||
|
public ResponseEntity<Resource> getFile(@PathVariable("attachmentId") Long attachmentId) {
|
||||||
|
PlatformContractAttachment attachment = attachmentMapper.selectById(attachmentId);
|
||||||
|
if (attachment == null) {
|
||||||
|
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "附件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
java.io.File file = new java.io.File(attachment.getFilePath());
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "文件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystemResource resource = new FileSystemResource(file);
|
||||||
|
String contentType = attachment.getContentType();
|
||||||
|
if (contentType == null) contentType = "application/octet-stream";
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType(contentType))
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + attachment.getFileName() + "\"")
|
||||||
|
.body(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileUrl(Long attachmentId) {
|
||||||
|
return "/api/v1/preview/" + attachmentId + "/file";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,9 +86,18 @@
|
|||||||
<template #default="{ row }">{{ (row.fileSize / 1024).toFixed(1) }} KB</template>
|
<template #default="{ row }">{{ (row.fileSize / 1024).toFixed(1) }} KB</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="上传时间" width="170" />
|
<el-table-column prop="createdAt" label="上传时间" width="170" />
|
||||||
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="previewAttachment(row)">预览</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="previewDialogVisible" :title="previewTitle" width="90%" top="5vh" destroy-on-close>
|
||||||
|
<iframe :src="previewUrl" style="width:100%;height:75vh;border:none;border-radius:4px" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<h3 class="section-title">最近审计</h3>
|
<h3 class="section-title">最近审计</h3>
|
||||||
<el-table v-loading="auditLoading" :data="auditRows" border stripe size="small" style="width: 100%">
|
<el-table v-loading="auditLoading" :data="auditRows" border stripe size="small" style="width: 100%">
|
||||||
<el-table-column label="时间" width="180">
|
<el-table-column label="时间" width="180">
|
||||||
@@ -169,6 +178,15 @@ const auditRows = ref([]);
|
|||||||
|
|
||||||
const attachments = ref([]);
|
const attachments = ref([]);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
|
const previewDialogVisible = ref(false);
|
||||||
|
const previewUrl = ref("");
|
||||||
|
const previewTitle = ref("");
|
||||||
|
|
||||||
|
function previewAttachment(row) {
|
||||||
|
previewTitle.value = row.fileName || "预览";
|
||||||
|
previewUrl.value = `${window.location.origin}/api/v1/preview/${row.id}`;
|
||||||
|
previewDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
const lineDialogVisible = ref(false);
|
const lineDialogVisible = ref(false);
|
||||||
const lineEditingId = ref(null);
|
const lineEditingId = ref(null);
|
||||||
@@ -307,8 +325,8 @@ function formatAuditTime(row) {
|
|||||||
async function loadNameMaps() {
|
async function loadNameMaps() {
|
||||||
try {
|
try {
|
||||||
const [cRes, pRes] = await Promise.all([
|
const [cRes, pRes] = await Promise.all([
|
||||||
listCustomers({ page: 0, size: 500 }),
|
listCustomers({ page: 0, size: 200 }),
|
||||||
listProjects({ page: 0, size: 500 }),
|
listProjects({ page: 0, size: 200 }),
|
||||||
]);
|
]);
|
||||||
const cBody = cRes.data && typeof cRes.data === "object" ? cRes.data : {};
|
const cBody = cRes.data && typeof cRes.data === "object" ? cRes.data : {};
|
||||||
const pBody = pRes.data && typeof pRes.data === "object" ? pRes.data : {};
|
const pBody = pRes.data && typeof pRes.data === "object" ? pRes.data : {};
|
||||||
|
|||||||
Reference in New Issue
Block a user