@@ -51,6 +56,11 @@ const props = defineProps({
type: String,
default: "/common/upload"
},
+ // 是否上传受保护文件
+ isPrivate: {
+ type: Boolean,
+ default: false
+ },
// 上传携带的参数
data: {
type: Object
@@ -92,7 +102,8 @@ const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); // 上传文件服务器地址
+const uploadAction = props.isPrivate && props.action === "/common/upload" ? "/common/files/upload" : props.action;
+const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + uploadAction); // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
@@ -173,6 +184,14 @@ function handleUploadSuccess(res, file) {
}
}
+// 下载受保护文件
+function handleFileDownload(event, file) {
+ if (typeof file.url === "string" && file.url.startsWith("/common/files/")) {
+ event.preventDefault();
+ proxy.$download.file(file.url);
+ }
+}
+
// 删除文件
function handleDelete(index) {
fileList.value.splice(index, 1);
diff --git a/ruoyi-fastapi-frontend/src/main.js b/ruoyi-fastapi-frontend/src/main.js
index f1f6fe2..9bd467b 100644
--- a/ruoyi-fastapi-frontend/src/main.js
+++ b/ruoyi-fastapi-frontend/src/main.js
@@ -37,6 +37,8 @@ import RightToolbar from '@/components/RightToolbar'
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
+// 业务附件上传组件
+import BusinessFileUpload from "@/components/BusinessFileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
@@ -61,6 +63,7 @@ app.config.globalProperties.selectDictLabels = selectDictLabels
app.component('DictTag', DictTag)
app.component('Pagination', Pagination)
app.component('FileUpload', FileUpload)
+app.component('BusinessFileUpload', BusinessFileUpload)
app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
diff --git a/ruoyi-fastapi-frontend/src/plugins/download.js b/ruoyi-fastapi-frontend/src/plugins/download.js
index 1a89efd..34520e8 100644
--- a/ruoyi-fastapi-frontend/src/plugins/download.js
+++ b/ruoyi-fastapi-frontend/src/plugins/download.js
@@ -6,8 +6,107 @@ import errorCode from '@/utils/errorCode'
import { blobValidate } from '@/utils/ruoyi'
const baseURL = import.meta.env.VITE_APP_BASE_API
+const DOWNLOAD_CHUNK_SIZE = 8 * 1024 * 1024
+const DOWNLOAD_CHUNK_RETRY_COUNT = 2
let downloadLoadingInstance;
+function parseContentRange(contentRange) {
+ const rangeMatch = /^bytes (\d+)-(\d+)\/(\d+)$/i.exec(contentRange || '')
+ if (!rangeMatch) {
+ throw new Error('分段下载响应缺少有效的Content-Range')
+ }
+ return {
+ start: Number(rangeMatch[1]),
+ end: Number(rangeMatch[2]),
+ total: Number(rangeMatch[3])
+ }
+}
+
+function isRetryableDownloadError(error) {
+ return !error.response || error.response.status >= 500
+}
+
+async function requestDownloadChunk(url, start) {
+ let lastError
+ for (let retryCount = 0; retryCount <= DOWNLOAD_CHUNK_RETRY_COUNT; retryCount++) {
+ try {
+ return await axios({
+ method: 'get',
+ url,
+ responseType: 'blob',
+ headers: {
+ 'Authorization': 'Bearer ' + getToken(),
+ 'Range': `bytes=${start}-${start + DOWNLOAD_CHUNK_SIZE - 1}`
+ }
+ })
+ } catch (error) {
+ lastError = error
+ if (!isRetryableDownloadError(error) || retryCount === DOWNLOAD_CHUNK_RETRY_COUNT) {
+ throw error
+ }
+ }
+ }
+ throw lastError
+}
+
+function requestFullDownload(url) {
+ return axios({
+ method: 'get',
+ url,
+ responseType: 'blob',
+ headers: { 'Authorization': 'Bearer ' + getToken() }
+ })
+}
+
+async function downloadByRange(url) {
+ const chunks = []
+ let filename
+ let contentType
+ let nextStart = 0
+
+ while (true) {
+ let response
+ try {
+ response = await requestDownloadChunk(url, nextStart)
+ } catch (error) {
+ if (nextStart !== 0 || error.response?.status !== 416) {
+ throw error
+ }
+ response = await requestFullDownload(url)
+ }
+ if (!blobValidate(response.data)) {
+ return { errorData: response.data }
+ }
+
+ filename = filename || response.headers['download-filename']
+ contentType = contentType || response.data.type
+ if (response.status !== 206) {
+ return {
+ blob: new Blob([response.data], { type: contentType }),
+ filename
+ }
+ }
+
+ const contentRange = parseContentRange(response.headers['content-range'])
+ if (
+ contentRange.start !== nextStart ||
+ contentRange.end < contentRange.start ||
+ contentRange.total <= contentRange.end ||
+ response.data.size !== contentRange.end - contentRange.start + 1
+ ) {
+ throw new Error('分段下载响应范围不一致')
+ }
+ chunks.push(response.data)
+ if (contentRange.end + 1 === contentRange.total) {
+ return {
+ blob: new Blob(chunks, { type: contentType }),
+ filename
+ }
+ }
+ nextStart = contentRange.end + 1
+ }
+}
+
export default {
name(name, isDelete = true) {
var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
@@ -26,22 +125,34 @@ export default {
}
})
},
- resource(resource) {
+ async resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
- axios({
- method: 'get',
- url: url,
- responseType: 'blob',
- headers: { 'Authorization': 'Bearer ' + getToken() }
- }).then((res) => {
- const isBlob = blobValidate(res.data);
- if (isBlob) {
- const blob = new Blob([res.data])
- this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
- } else {
- this.printErrMsg(res.data);
+ try {
+ const result = await downloadByRange(url)
+ if (result.errorData) {
+ await this.printErrMsg(result.errorData)
+ return
}
- })
+ this.saveAs(result.blob, decodeURIComponent(result.filename))
+ } catch (error) {
+ console.error(error)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ }
+ },
+ async file(resource) {
+ var url = baseURL + resource;
+ try {
+ const result = await downloadByRange(url)
+ if (result.errorData) {
+ await this.printErrMsg(result.errorData)
+ return
+ }
+ const fallbackFileName = resource.split('/').pop();
+ this.saveAs(result.blob, result.filename ? decodeURIComponent(result.filename) : fallbackFileName)
+ } catch (error) {
+ console.error(error)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ }
},
zip(url, name) {
var url = baseURL + url
diff --git a/ruoyi-fastapi-frontend/src/utils/transportCryptoPolicy.js b/ruoyi-fastapi-frontend/src/utils/transportCryptoPolicy.js
index b0b3783..d85cc5c 100644
--- a/ruoyi-fastapi-frontend/src/utils/transportCryptoPolicy.js
+++ b/ruoyi-fastapi-frontend/src/utils/transportCryptoPolicy.js
@@ -7,7 +7,9 @@ const EXCLUDED_URL_PATTERNS = [
'/transport/crypto/frontend-config',
'/transport/crypto/public-key',
'/common/download',
- '/common/download/resource'
+ '/common/download/resource',
+ '/common/files/',
+ '/system/file/download/'
]
const TRANSPORT_FRONTEND_CONFIG_CACHE_KEY = 'transportCryptoFrontendConfig'
const TRANSPORT_FRONTEND_CONFIG_URL = '/transport/crypto/frontend-config'
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileAclDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileAclDrawer.vue
new file mode 100644
index 0000000..930ed07
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileAclDrawer.vue
@@ -0,0 +1,383 @@
+
+
+
+
+
内置权限
+
+
+
+
+ {{ builtinSourceLabel(scope.row.source) }}
+
+
+
+
+
+ {{ scope.row.subjectName || scope.row.subjectId || "-" }}
+
+
+
+
+
+ {{ scope.row.enabled ? "允许下载" : "已移除" }}
+
+
+
+
+
+
+ {{ scope.row.denyOverridable ? "可以覆盖" : "不可覆盖" }}
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleSubjectVisible(scope.row, visible)
+ "
+ placeholder="输入名称搜索"
+ style="width: 100%"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileAuditDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileAuditDrawer.vue
new file mode 100644
index 0000000..cc56a13
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileAuditDrawer.vue
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+ 重置
+
+
+
+
+ {{ actionLabel(scope.row.action) }}
+
+
+
+
+ {{ resultLabel(scope.row.result) }}
+
+
+
+
+
+
+
+ {{ scope.row.bytesSent ? formatFileSize(scope.row.bytesSent) : "-" }}
+
+
+
+
+ {{ parseTime(scope.row.accessTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ actionLabel(detail.action) }}
+
+
+ {{ resultLabel(detail.result) }}
+
+
+ {{ detail.actorName || "-" }}
+
+
+ {{ detail.ipAddress || "-" }}
+
+
+ {{ detail.requestId || "-" }}
+
+
+ {{ detail.traceId || "-" }}
+
+
+ {{ parseTime(detail.accessTime) }}
+
+
+ {{ detail.userAgent || "-" }}
+
+
+ {{ detail.errorMessage || "-" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileDetailDialog.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileDetailDialog.vue
new file mode 100644
index 0000000..c91d35e
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileDetailDialog.vue
@@ -0,0 +1,170 @@
+
+
+
+
+ {{ detail.fileId }}
+
+
+ {{ detail.originalName }}
+
+
+ {{ detail.storedName }}
+
+
+ {{ accessTypeLabel(detail.accessType) }}
+
+
+ {{ statusLabel(detail.status) }}
+
+
+ {{ detail.createBy || "-" }}
+
+
+ {{ detail.uploadUserId || "-" }}
+
+
+
+ {{ uploaderPermissionLabel(detail) }}
+
+ {{ uploaderPermissionLabel(detail) }}
+
+
+ {{ detail.ownerName || detail.ownerUserId || "-" }}
+
+
+ {{ detail.deptName || detail.deptId || "-" }}
+
+
+ {{ storageStatusLabel(detail.storageStatus) }}
+
+
+ {{ detail.aclEntryCount || 0 }}
+
+
+
+ {{ detail.referenceCount }} 项
+
+ 0 项
+
+
+ {{ detail.aclVersion ?? "-" }}
+
+
+ {{ formatFileSize(detail.fileSize) }}
+
+
+ {{ detail.contentType || "-" }}
+
+
+ {{ detail.extension || "-" }}
+
+
+ {{ parseTime(detail.createTime) }}
+
+
+ {{ parseTime(detail.expireTime) || "-" }}
+
+
+ {{ parseTime(detail.deletedTime) || "-" }}
+
+
+ {{ detail.storageKey }}
+
+
+ {{ detail.fileHash }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileReconcileDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileReconcileDrawer.vue
new file mode 100644
index 0000000..501dc83
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileReconcileDrawer.vue
@@ -0,0 +1,770 @@
+
+
+
+
+
+
+
待处理
+
{{ stats.openCount }}
+
需要确认或修复的异常
+
+
+
严重异常
+
{{ stats.criticalCount }}
+
文件缺失、错位或内容不一致
+
+
+
警告异常
+
{{ stats.warningCount }}
+
孤立文件或回收状态异常
+
+
+
隔离文件
+
{{ stats.quarantinedCount }}
+
仅管理员可恢复或永久删除
+
+
+
最近任务
+
+
+ {{ runStatusLabel(stats.latestRun?.status) }}
+
+
+
+ {{ stats.latestRun ? parseTime(stats.latestRun.startedTime) : "尚未执行" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+ 重置
+
+
+
+
+
+
+
+ {{ severityLabel(scope.row.severity) }}
+
+
+
+
+
+ {{ issueTypeLabel(scope.row.issueType) }}
+
+
+
+
+ {{ scope.row.originalName || "未登记文件" }}
+ {{ scope.row.fileId || "-" }}
+
+
+
+
+
+ 预期:{{ formatLocation(scope.row.expectedRoot, scope.row.expectedKey) }}
+
+
+ 实际:{{ formatLocation(scope.row.actualRoot, scope.row.actualKey) }}
+
+
+
+
+
+ 预期:{{ formatOptionalSize(scope.row.expectedSize) }}
+
+ 实际:{{ formatOptionalSize(scope.row.actualSize) }}
+
+
+
+
+
+
+ {{ issueStatusLabel(scope.row.status) }}
+
+
+
+
+
+
+ {{ parseTime(scope.row.lastSeenTime) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ actionLabel(action) }}
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ {{ runStatusLabel(scope.row.status) }}
+
+
+
+
+
+ {{ scope.row.triggerType === "scheduled" ? "定时任务" : "手动" }}
+
+
+
+ {{ scope.row.checkHash ? "是" : "否" }}
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.startedTime) }}
+
+
+
+ {{ scope.row.finishedTime ? parseTime(scope.row.finishedTime) : "-" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileReferenceDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileReferenceDrawer.vue
new file mode 100644
index 0000000..43e5289
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileReferenceDrawer.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ {{ scope.row.businessName || "-" }}
+
+
+
+
+ {{
+ scope.row.retentionExpireTime
+ ? parseTime(scope.row.retentionExpireTime)
+ : "永久保留"
+ }}
+
+
+
+
+
+ 兼容字段
+
+ 引用关系
+
+
+
+ {{ scope.row.createBy || "-" }}
+
+
+
+ {{ parseTime(scope.row.createTime) || "-" }}
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionPolicyDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionPolicyDrawer.vue
new file mode 100644
index 0000000..007e52b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionPolicyDrawer.vue
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status === "0" ? "启用" : "停用" }}
+
+
+
+
+ {{ scope.row.remark || "-" }}
+
+
+
+ {{ parseTime(scope.row.updateTime) || "-" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 停用
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionReminderDrawer.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionReminderDrawer.vue
new file mode 100644
index 0000000..bda2989
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileRetentionReminderDrawer.vue
@@ -0,0 +1,437 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+ 重置
+
+
+
+
+
+ 扫描
+
+
+
+
+ 已读
+
+
+
+
+
+
+
+ {{ scope.row.ownerName || "-" }}
+
+
+ {{ scope.row.deptName || "-" }}
+
+
+
+
+ {{ scope.row.noticeType === "expired" ? "已到期" : "即将到期" }}
+
+
+
+
+ {{ parseTime(scope.row.expireTime) }}
+
+
+
+
+ {{ scope.row.status === "0" ? "未读" : "已读" }}
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+ {{ scope.row.readBy || "-" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentRow.originalName }}
+
+
+ {{ parseTime(currentRow.expireTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileSearchForm.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileSearchForm.vue
new file mode 100644
index 0000000..f5138ba
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileSearchForm.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileStatistics.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileStatistics.vue
new file mode 100644
index 0000000..c5b9515
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileStatistics.vue
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
文件总数
+
{{ stats.totalCount }}
+
+
+
+
+
+
+
+
+
+
占用空间
+
{{ formatFileSize(stats.totalSize) }}
+
+
+
+
+
+
+
+
+
+
公开文件空间
+
{{ formatFileSize(stats.publicSize) }}
+
+
+
+
+
+
+
+
+
+
受保护文件空间
+
{{ formatFileSize(stats.privateSize) }}
+
+
+
+
+
+
+
+
+
+
已过期文件
+
{{ stats.expiredCount }}
+
+
+
+
+
+
+
+
+
+
即将过期授权
+
{{ stats.aclExpiringCount }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileTable.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileTable.vue
new file mode 100644
index 0000000..6515cb7
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileTable.vue
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+ 公开
+ 受保护
+
+
+
+
+ {{ formatFileSize(scope.row.fileSize) }}
+
+
+
+
+ {{ scope.row.ownerName || scope.row.ownerUserId || "-" }}
+
+
+
+
+ {{ scope.row.deptName || scope.row.deptId || "-" }}
+
+
+
+
+
+ {{ expirationLabel(scope.row.expireTime) }}
+
+
+
+
+
+
+
+ {{ scope.row.aclEntryCount }} 项
+ · 即将过期
+
+
+ {{ scope.row.aclEntryCount || 0 }} 项
+
+
+
+
+
+ {{ scope.row.referenceCount }} 项
+
+ 0 项
+
+
+
+
+
+ {{ storageStatusLabel(scope.row.storageStatus) }}
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+ 正常
+
+ 已删除
+
+ 清理中
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/FileTransferDialog.vue b/ruoyi-fastapi-frontend/src/views/system/file/components/FileTransferDialog.vue
new file mode 100644
index 0000000..eea96ea
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/FileTransferDialog.vue
@@ -0,0 +1,175 @@
+
+
+
+
+ visible && searchUsers('')"
+ @change="handleUserChange"
+ placeholder="输入用户名称搜索"
+ style="width: 100%"
+ >
+
+
+
+
+
+
+
+
+
+ {{
+ form.retainUploaderAccess
+ ? "原上传人继续拥有内置下载权限,匹配的显式拒绝仍可覆盖。"
+ : "原上传人不再因上传身份获得下载权限,上传记录仍会保留。"
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/components/fileFormatters.js b/ruoyi-fastapi-frontend/src/views/system/file/components/fileFormatters.js
new file mode 100644
index 0000000..519f7c6
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/components/fileFormatters.js
@@ -0,0 +1,146 @@
+import { parseTime } from "@/utils/ruoyi";
+
+export function formatFileSize(size) {
+ const bytes = Number(size || 0);
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
+}
+
+export function accessTypeLabel(accessType) {
+ return accessType === "public" ? "公开文件" : "受保护文件";
+}
+
+export function statusLabel(status) {
+ return {
+ active: "正常",
+ deleted: "已删除",
+ purging: "清理中"
+ }[status] || status;
+}
+
+export function expirationLabel(expireTime) {
+ if (!expireTime) return "永久有效";
+ const remainingTime = new Date(expireTime).getTime() - Date.now();
+ if (remainingTime <= 0) return "已过期";
+ if (remainingTime <= 7 * 24 * 60 * 60 * 1000) return "即将过期";
+ return parseTime(expireTime, "{y}-{m}-{d}");
+}
+
+export function expirationTagType(expireTime) {
+ if (!expireTime) return "info";
+ const remainingTime = new Date(expireTime).getTime() - Date.now();
+ if (remainingTime <= 0) return "danger";
+ return remainingTime <= 7 * 24 * 60 * 60 * 1000 ? "warning" : "success";
+}
+
+export function isAclExpiring(expireTime) {
+ if (!expireTime) return false;
+ const remainingTime = new Date(expireTime).getTime() - Date.now();
+ return remainingTime > 0 && remainingTime <= 7 * 24 * 60 * 60 * 1000;
+}
+
+export function storageStatusLabel(storageStatus) {
+ return {
+ normal: "正常",
+ missing: "文件缺失",
+ quarantined: "已隔离",
+ invalid: "异常"
+ }[storageStatus] || "未知";
+}
+
+export function storageStatusTagType(storageStatus) {
+ return {
+ normal: "success",
+ missing: "danger",
+ quarantined: "warning",
+ invalid: "danger"
+ }[storageStatus] || "info";
+}
+
+export function actionLabel(action) {
+ return {
+ upload: "上传",
+ download: "下载",
+ acl_update: "授权变更",
+ transfer: "归属转移",
+ delete: "移入回收站",
+ restore: "恢复",
+ purge: "永久清理",
+ reconcile: "存储对账",
+ retention_extend: "保留延期",
+ retention_dispose: "到期处置"
+ }[action] || action;
+}
+
+export function resultLabel(result) {
+ return {
+ allowed: "已授权",
+ denied: "已拒绝",
+ completed: "已完成",
+ failed: "失败"
+ }[result] || result;
+}
+
+export function resultTagType(result) {
+ return {
+ allowed: "primary",
+ denied: "danger",
+ completed: "success",
+ failed: "danger"
+ }[result] || "info";
+}
+
+export function parseOperationDetail(operationDetail) {
+ if (!operationDetail) return [];
+ try {
+ const detail = JSON.parse(operationDetail);
+ return Object.entries(detail).map(([key, value]) => ({
+ label: auditDetailKeyLabel(key),
+ value: typeof value === "object" ? JSON.stringify(value, null, 2) : String(value ?? "-")
+ }));
+ } catch {
+ return [{ label: "操作详情", value: operationDetail }];
+ }
+}
+
+function auditDetailKeyLabel(key) {
+ return {
+ previousAclVersion: "原权限版本",
+ newAclVersion: "新权限版本",
+ entryCount: "授权项数量",
+ allowCount: "允许项数量",
+ denyCount: "拒绝项数量",
+ subjectTypeCounts: "主体类型统计",
+ previousOwnerUserId: "原所有者ID",
+ previousDeptId: "原所属部门ID",
+ newOwnerUserId: "新所有者ID",
+ newOwnerName: "新所有者",
+ newDeptId: "新所属部门ID",
+ reason: "操作原因",
+ originalName: "原始文件名",
+ accessType: "访问类型",
+ referenceCount: "业务引用数量",
+ previousStatus: "原状态",
+ newStatus: "新状态",
+ truncated: "详情已截断",
+ preview: "详情预览",
+ issueId: "对账异常ID",
+ issueType: "异常类型",
+ action: "处理动作",
+ actualRoot: "实际存储区域",
+ actualKey: "实际相对路径",
+ expectedRoot: "预期存储区域",
+ expectedKey: "预期相对路径",
+ previousExpireTime: "原到期时间",
+ newExpireTime: "新到期时间",
+ expireTime: "到期时间",
+ range: "请求范围",
+ rangeStart: "分段起始字节",
+ rangeEnd: "分段结束字节",
+ fileSize: "文件总大小",
+ releasedReferenceCount: "解除引用数量",
+ releasedReferences: "解除引用明细"
+ }[key] || key;
+}
diff --git a/ruoyi-fastapi-frontend/src/views/system/file/index.vue b/ruoyi-fastapi-frontend/src/views/system/file/index.vue
new file mode 100644
index 0000000..f427a7f
--- /dev/null
+++ b/ruoyi-fastapi-frontend/src/views/system/file/index.vue
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
+ 授权
+
+
+
+
+ 转移
+
+
+
+
+ 恢复
+
+
+
+
+ 清理
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+ 策略
+
+
+
+
+ 对账
+
+
+
+
+ 提醒
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+