将开源版本的修改打补丁应用到商业版

This commit is contained in:
wsm
2026-04-09 19:36:52 +08:00
parent d8e3a63df1
commit 7cf7a03983
105 changed files with 18321 additions and 2386 deletions
@@ -0,0 +1,257 @@
<!-- 文件上传弹窗组件 -->
<template>
<a-modal
:visible="visible"
title="选择文件密级并上传"
:width="600"
:confirm-loading="uploading"
@ok="handleUpload"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-form layout="vertical" style="padding: 0 20px">
<a-form-item label="文件密级" required>
<a-radio-group v-model:value="selectedSecretLevel">
<a-radio
v-for="option in secretLevelOptions"
:key="option.value"
:value="option.value"
>
<a-tag :color="getSecretLevelColor(option.value)">
{{ option.label }}
</a-tag>
</a-radio>
</a-radio-group>
<!-- <div class="secret-tip">-->
<!-- <Icon icon="ant-design:info-circle-outlined" />-->
<!-- 文件密级将决定谁能查看此文件请谨慎选择-->
<!-- </div>-->
</a-form-item>
<a-form-item label="选择文件" required>
<a-upload
:before-upload="beforeUpload"
:file-list="tempFileList"
@remove="handleRemoveFile"
:multiple="multiple"
:max-count="multiple ? undefined : 1"
>
<a-button>
<Icon icon="ant-design:file-add-outlined" />
{{ multiple ? '选择文件(可多选)' : '选择文件' }}
</a-button>
</a-upload>
</a-form-item>
<a-alert
v-if="selectedFiles.length > 0"
:message="`已选择 ${selectedFiles.length} 个文件`"
type="info"
show-icon
>
<template #description>
<div class="selected-files-summary">
<div v-for="(f, idx) in selectedFiles" :key="f.uid" class="summary-item">
{{ idx + 1}}. {{ f.name }}{{ formatFileSize(f.size) }}
</div>
</div>
</template>
</a-alert>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { useMessage } from '/src/hooks/web/useMessage';
import { uploadMyFile } from '/src/api/common/api';
const { createMessage } = useMessage();
const props = defineProps({
visible: {
type: Boolean,
required: true
},
secretLevelOptions: {
type: Array,
default: () => []
},
businessId: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'upload-success', 'upload-error']);
const selectedSecretLevel = ref<number>(1);
const selectedFiles = ref<any[]>([]);
const tempFileList = ref<any[]>([]);
const uploading = ref<boolean>(false);
function getSecretLevelColor(level: number) {
const colors = {
1: 'success',
2: 'processing',
3: 'warning',
4: 'error'
};
return colors[level] || 'default';
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
function beforeUpload(file) {
const isValidSize = file.size / 1024 / 1024 < 500;
if (!isValidSize) {
createMessage.error(`文件"${file.name}"大小超过500MB,已跳过`);
return false;
}
if (!props.multiple) {
selectedFiles.value = [file];
tempFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
} else {
selectedFiles.value = [...selectedFiles.value, file];
tempFileList.value = [...tempFileList.value, {
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
}
return false;
}
function handleRemoveFile(file) {
selectedFiles.value = selectedFiles.value.filter(f => f.uid !== file.uid);
tempFileList.value = tempFileList.value.filter(f => f.uid !== file.uid);
}
async function handleUpload() {
if (selectedFiles.value.length === 0) {
createMessage.warning('请先选择文件');
return;
}
if (!selectedSecretLevel.value) {
createMessage.warning('请选择文件密级');
return;
}
uploading.value = true;
try {
const uploadPromises = selectedFiles.value.map(file => {
const formData = new FormData();
formData.append('file', file);
formData.append('secretLevel', selectedSecretLevel.value.toString());
if (props.businessId) {
formData.append('business_id', props.businessId);
}
return uploadMyFile('/sys/file/upload', formData).then(res => ({
rawFile: file,
response: res
}));
});
const results = await Promise.allSettled(uploadPromises);
let successCount = 0;
let failCount = 0;
const successResults: any[] = [];
results.forEach((r) => {
if (r.status === 'fulfilled') {
const result = r.value?.response?.data;
if (result && result.success) {
successCount++;
successResults.push({
fileId: result.result.fileId,
fileName: r.value.rawFile.name,
filePath: result.result.savePath,
fileSize: r.value.rawFile.size,
secretLevel: selectedSecretLevel.value,
fileUploadType: result.result.fileUploadType,
bound: !!props.businessId
});
} else {
failCount++;
}
} else {
failCount++;
}
});
if (successCount > 0) {
createMessage.success(`成功上传 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
emit('upload-success', { success: true, results: successResults });
handleCancel();
} else {
createMessage.error('所有文件上传失败');
emit('upload-error', new Error('所有文件上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
createMessage.error('文件上传失败');
emit('upload-error', error);
} finally {
uploading.value = false;
}
}
function handleCancel() {
emit('update:visible', false);
setTimeout(() => {
selectedSecretLevel.value = 1;
selectedFiles.value = [];
tempFileList.value = [];
}, 300);
}
watch(() => props.visible, (newVal) => {
if (newVal && props.secretLevelOptions.length > 0) {
selectedSecretLevel.value = props.secretLevelOptions[0].value;
}
});
</script>
<style lang="less" scoped>
.secret-tip {
margin-top: 8px;
color: #faad14;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.selected-files-summary {
max-height: 150px;
overflow-y: auto;
.summary-item {
padding: 2px 0;
font-size: 12px;
color: #595959;
}
}
</style>
@@ -0,0 +1,467 @@
<!-- 文件上传下载删除组件 -->
<template>
<div class="s-upload-file-container">
<!-- 上传按钮区域 -->
<div class="upload-btn-wrapper" v-if="!disabled">
<a-button type="primary" @click="handleOpenUploadModal">
<Icon icon="ant-design:upload-outlined" />
选择文件上传
</a-button>
</div>
<!-- 文件列表区域 -->
<div class="file-list-wrapper">
<div v-if="displayFileList.length > 0" class="file-list-header">
<span class="file-count"> {{ displayFileList.length }} 个文件</span>
<a-button
type="link"
size="small"
:loading="downloadingAll"
@click="handleDownloadAll"
>
<Icon icon="ant-design:download-outlined" />
下载全部
</a-button>
</div>
<a-spin :spinning="loading">
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
<div v-else class="file-list">
<div
v-for="file in displayFileList"
:key="file.fileId || file.id"
class="file-item"
>
<!-- 文件信息 -->
<div class="file-info">
<Icon icon="ant-design:file-outlined" class="file-icon" />
<span class="file-name">{{ file.fileName }}</span>
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
{{ getSecretLevelLabel(file.secretLevel) }}
</a-tag>
<a-tag v-if="file.pending" color="orange" class="secret-tag">
待保存
</a-tag>
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div>
<!-- 操作按钮 -->
<div class="file-actions">
<a-button
type="link"
size="small"
@click="handleDownload(file)"
>
<Icon icon="ant-design:download-outlined" />
下载
</a-button>
<a-button
v-if="!disabled"
type="link"
size="small"
danger
@click="handleDelete(file)"
>
<Icon icon="ant-design:delete-outlined" />
删除
</a-button>
</div>
</div>
</div>
</a-spin>
</div>
<!-- 上传弹窗 -->
<SFileUploadModal
v-model:visible="uploadModalVisible"
:secret-level-options="secretLevelOptions"
:business-id="bussinessId"
:multiple="multiple"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { defHttp } from '/src/utils/http/axios';
import { useMessage } from '/src/hooks/web/useMessage';
import SFileUploadModal from './SFileUploadModal.vue';
const { createMessage, createConfirm } = useMessage();
const props = defineProps({
bussinessId: {
type: String,
default: ''
},
bussinessSecretLevel: {
type: Number,
default: 1,
validator: (value: number) => [1, 2, 3, 4].includes(value)
},
disabled: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
const serverFileList = ref<any[]>([]);
const pendingFileList = ref<any[]>([]);
const loading = ref<boolean>(false);
const uploadModalVisible = ref<boolean>(false);
const binding = ref<boolean>(false);
const downloadingAll = ref<boolean>(false);
const displayFileList = computed(() => {
return [
...serverFileList.value.map(f => ({ ...f, pending: false })),
...pendingFileList.value.map(f => ({ ...f, pending: true }))
];
});
const SECRET_LEVEL_CONFIG = {
1: { label: '非密', color: 'success' },
2: { label: '内部', color: 'processing' },
3: { label: '秘密', color: 'warning' },
4: { label: '机密', color: 'error' }
};
function getSecretLevelInfo(level: number) {
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
}
function getSecretLevelLabel(level: number) {
return getSecretLevelInfo(level).label;
}
function getSecretLevelColor(level: number) {
return getSecretLevelInfo(level).color;
}
function formatFileSize(bytes: number): string {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
const secretLevelOptions = computed(() => {
const level = props.bussinessSecretLevel;
const options = [];
options.push({ label: '非密', value: 1 });
if (level >= 2) options.push({ label: '内部', value: 2 });
if (level >= 3) options.push({ label: '秘密', value: 3 });
if (level >= 4) options.push({ label: '机密', value: 4 });
return options;
});
function handleOpenUploadModal() {
uploadModalVisible.value = true;
}
async function handleUploadSuccess(result: any) {
if (result.results && Array.isArray(result.results)) {
result.results.forEach((fileInfo: any) => {
if (!fileInfo.bound) {
pendingFileList.value.push({
fileId: fileInfo.fileId,
fileName: fileInfo.fileName,
filePath: fileInfo.filePath,
fileSize: fileInfo.fileSize,
secretLevel: fileInfo.secretLevel,
fileUploadType: fileInfo.fileUploadType
});
}
});
}
if (props.bussinessId) {
await loadFileList();
}
emit('upload-success', result);
}
function handleUploadError(error: any) {
emit('upload-error', error);
}
async function handleDownload(file: any) {
try {
const fileUrl = file.filePath;
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
const data = await defHttp.get(
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
{ isTransformResponse: false }
);
if (!data || data.size === 0) {
createMessage.warning('文件下载失败');
return;
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data]), fileName);
} else {
const blobUrl = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}
} catch (error) {
console.error('下载失败:', error);
createMessage.error('文件下载失败');
}
}
async function handleDownloadAll() {
const files = displayFileList.value;
if (files.length === 0) return;
downloadingAll.value = true;
let successCount = 0;
let failCount = 0;
for (const file of files) {
try {
await handleDownload(file);
successCount++;
} catch (error) {
failCount++;
console.error(`下载文件"${file.fileName}"失败:`, error);
}
}
downloadingAll.value = false;
if (successCount > 0) {
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
} else {
createMessage.error('所有文件下载失败');
}
}
function handleDelete(file: any) {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: `确定要删除文件"${file.fileName}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
loading.value = true;
await defHttp.get({
url: '/sys/file/delete',
params: { id: file.fileId || file.id }
});
createMessage.success('文件删除成功');
if (file.pending) {
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
} else {
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
}
emit('delete-success', file.fileId || file.id);
} catch (error) {
console.error('删除失败:', error);
createMessage.error('文件删除失败');
} finally {
loading.value = false;
}
}
});
}
async function loadFileList() {
if (!props.bussinessId) {
return;
}
try {
loading.value = true;
const result = await defHttp.get({
url: '/sys/file/getFileInfoByBussinessId',
params: { bussinessId: props.bussinessId }
});
if (Array.isArray(result)) {
serverFileList.value = result;
} else {
serverFileList.value = [];
}
} catch (error) {
console.error('加载文件列表失败:', error);
serverFileList.value = [];
createMessage.error('加载文件列表失败');
} finally {
loading.value = false;
}
}
//这个是在表单id还没有生成之前上传了附件,附件表没有存储bussinessId。当业务表保存后有id了,再把bussinessId更新到这些附件上去
async function bindBussinessId(newBussinessId: string) {
if (!newBussinessId) {
createMessage.warning('bussinessId 不能为空');
return false;
}
if (pendingFileList.value.length === 0) {
return true;
}
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
try {
binding.value = true;
await defHttp.get({
url: '/sys/file/updateFilesInfo',
params: {
bussinessId: newBussinessId,
fileIds: fileIds
}
});
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
pendingFileList.value = [];
await loadFileList();
return true;
} catch (error) {
console.error('关联文件失败:', error);
createMessage.error('关联文件失败,请重试');
return false;
} finally {
binding.value = false;
}
}
function getPendingFileIds(): string[] {
return pendingFileList.value.map(f => f.fileId);
}
function hasPendingFiles(): boolean {
return pendingFileList.value.length > 0;
}
defineExpose({
bindBussinessId,
getPendingFileIds,
hasPendingFiles,
loadFileList
});
watch(() => props.bussinessId, (newVal) => {
if (newVal && newVal.length > 0) {
loadFileList();
} else {
serverFileList.value = [];
}
}, { immediate: true });
</script>
<style lang="less" scoped>
.s-upload-file-container {
width: 100%;
max-width: 100%;
.upload-btn-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.file-list-wrapper {
width: 100%;
min-height: 100px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
.file-count {
font-size: 13px;
color: #8c8c8c;
}
}
}
.file-list {
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
.file-icon {
font-size: 16px;
color: #1890ff;
flex-shrink: 0;
}
.file-name {
font-size: 14px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.secret-tag {
margin-left: 4px;
font-weight: 500;
flex-shrink: 0;
}
.file-size {
font-size: 12px;
color: #8c8c8c;
flex-shrink: 0;
}
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
}
}
}
</style>
@@ -0,0 +1,231 @@
# SUploadFile 文件上传组件使用指南
## 组件介绍
`SUploadFile` 是一个支持密级管理的通用文件上传下载删除组件,适用于 JeecgBoot 项目。
## 组件位置
```
jeecgboot-vue3/src/components/semri/fileComponent/SUploadFile.vue
```
## 功能特性
- ✅ 文件加密上传(支持密级选择)
- ✅ 文件解密下载
- ✅ 文件删除
- ✅ 密级权限控制(文件密级 <= 业务密级)
- ✅ 支持禁用状态(禁用后不能上传和删除)
- ✅ 文件列表展示(带密级标签)
- ✅ 支持业务关联(通过 businessId
## Props 参数
| 参数 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| bussinessId | String | '' | 否 | 业务ID,用于关联和查询文件 |
| bussinessSecretLevel | Number | 3 | 否 | 业务密级(1-非密,2-内部,3-秘密,4-机密) |
| disabled | Boolean | false | 否 | 是否禁用(禁用后不能上传和删除,但可以下载) |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| upload-success | result | 上传成功时触发,返回上传结果 |
| delete-success | fileId | 删除成功时触发,返回文件ID |
| upload-error | error | 上传失败时触发,返回错误信息 |
## 使用示例
### 基础用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '',
secretLevel: 3 // 秘密
});
</script>
```
### 带事件监听的用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
:disabled="formData.readOnly"
@upload-success="handleUploadSuccess"
@delete-success="handleDeleteSuccess"
@upload-error="handleUploadError"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import { createMessage } from '/@/hooks/web/useMessage';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '123456',
secretLevel: 3, // 秘密
readOnly: false
});
// 上传成功处理
function handleUploadSuccess(result) {
console.log('上传成功:', result);
createMessage.success('文件上传完成');
}
// 删除成功处理
function handleDeleteSuccess(fileId) {
console.log('删除成功,文件ID:', fileId);
createMessage.success('文件删除完成');
}
// 上传失败处理
function handleUploadError(error) {
console.error('上传失败:', error);
}
</script>
```
### 不同密级的使用
```vue
<template>
<div>
<!-- 业务密级为非密只能上传非密文件 -->
<SUploadFile
bussiness-id="001"
:bussiness-secret-level="1"
/>
<!-- 业务密级为内部可以上传非密内部文件 -->
<SUploadFile
bussiness-id="002"
:bussiness-secret-level="2"
/>
<!-- 业务密级为秘密可以上传非密内部秘密文件 -->
<SUploadFile
bussiness-id="003"
:bussiness-secret-level="3"
/>
<!-- 业务密级为机密可以上传所有密级的文件 -->
<SUploadFile
bussiness-id="004"
:bussiness-secret-level="4"
/>
</div>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
### 禁用状态(只读)
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
bussiness-id="123456"
:bussiness-secret-level="3"
:disabled="true"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
## 密级规则
### 文件密级选项规则
组件会根据业务密级自动生成可选的文件密级:
| 业务密级 | 可选文件密级 |
|---------|------------|
| 1-非密 | 1-非密 |
| 2-内部 | 1-非密,2-内部 |
| 3-秘密 | 1-非密,2-内部,3-秘密 |
| 4-机密 | 1-非密,2-内部,3-秘密,4-机密 |
### 密级显示颜色
- **非密**:绿色 (success)
- **内部**:蓝色 (processing)
- **秘密**:橙色 (warning)
- **机密**:红色 (error)
## API 接口
组件内部调用的后端接口:
- **上传文件**: `POST /sys/file/upload`
- 参数:file, secretLevel, business_id
- **获取文件列表**: `GET /sys/file/getFileInfoByBussinessId`
- 参数:bussinessId
- **下载文件**: `GET /sys/file/download`
- 参数:fileUrl
- **删除文件**: `GET /sys/file/delete`
- 参数:id
## 注意事项
1. **文件大小限制**:单个文件不超过 100MB
2. **密级校验**:文件密级必须 <= 业务密级
3. **禁用状态**:禁用时不显示上传和删除按钮,但可以下载文件
4. **业务ID**:如果提供了业务ID,组件会自动加载该业务关联的文件列表
5. **Token 处理**:组件自动处理 token,无需手动添加
## 常见问题
### Q: 为什么上传按钮不显示?
A: 检查是否设置了 `disabled="true"`,禁用状态下不会显示上传按钮。
### Q: 为什么只能选择某些密级?
A: 文件密级受业务密级限制,只能选择小于等于业务密级的文件密级。
### Q: 如何获取上传后文件的ID?
A: 监听 `upload-success` 事件,返回的结果中包含 `fileId`
### Q: 删除文件时如何自定义确认文案?
A: 当前版本删除确认文案是固定的,如需自定义请修改组件源码。
## 更新日志
### v1.0.0 (2026-03-27)
- 初始版本发布
- 支持文件上传、下载、删除功能
- 支持密级管理
- 支持禁用状态