Commit 189821fc by zhangxingmin

Merge remote-tracking branch 'origin/dev' into prod

parents 5d620379 75ee40be
......@@ -27,11 +27,12 @@
"dayjs": "^1.11.18",
"echarts": "5.6.0",
"element-plus": "^2.13.5",
"file-saver": "2.0.5",
"file-saver": "^2.0.5",
"fuse.js": "6.6.2",
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"jszip": "^3.10.1",
"nprogress": "0.2.0",
"p-limit": "^7.3.0",
"pinia": "3.0.2",
......
......@@ -505,17 +505,17 @@ function handleModelChange(value, item) {
}
}
localModel.value = newModel
console.log('子组件用户操作后,modelvalue值==', newModel)
// console.log('子组件用户操作后,modelvalue值==', newModel)
nextTick(() => {
if (!isEqualShallow(props.modelValue, newModel)) {
console.log('如果新旧值不一样,反馈给父组件', newModel)
// console.log('如果新旧值不一样,反馈给父组件', newModel)
emit('update:modelValue', newModel)
} else {
console.log('🚫 跳过 emit:认为相等')
}
})
if (item.type === 'select') {
console.log('如果是select类型,反馈给父组件', item.prop, value, item)
// console.log('如果是select类型,反馈给父组件', item.prop, value, item)
emit('selectChange', item.prop, value, item)
} else if (item.type == 'upload') {
// 传给父组件最新的上传值newModel
......@@ -553,7 +553,7 @@ async function loadDictOptions(dictType) {
dictStore.setDict(dictType, options)
return options
} catch (err) {
console.error(`加载字典 ${dictType} 失败`, err)
// console.error(`加载字典 ${dictType} 失败`, err)
ElMessage.error(`字典 ${dictType} 加载失败`)
return []
}
......
......@@ -114,7 +114,7 @@ const useDictStore = defineStore('dict', {
//设置最新的保险公司列表
setAllInsuranceCompanyList(list) {
this.allInsuranceCompanyList = list
}
},
}
})
......
......@@ -82,3 +82,5 @@ export function useDictLists1(typeLists) {
}
})
}
// src/utils/zipDownload.js
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import axios from 'axios';
/**
* 批量下载文件并打包成 ZIP
* @param {Array} files - 文件列表,每项包含 { url: '文件地址', name: '文件名(含后缀)' }
* @param {String} zipName - 生成的压缩包名称,默认 'attachments.zip'
* @param {Function} onProgress - 可选,进度回调 (current, total)
*/
export const downloadFilesAsZip = async (files, zipName = 'attachments.zip', onProgress) => {
if (!files || files.length === 0) {
console.warn('没有可下载的文件');
return;
}
const zip = new JSZip();
const total = files.length;
let loadedCount = 0;
// 创建加载提示(可选,结合 Element Plus ElLoading 使用)
// 这里只返回 Promise,UI 层的 loading 建议在调用处控制
try {
// 并行或串行请求文件内容
// 注意:如果文件非常多且大,建议限制并发数,避免浏览器卡顿或触发限流
const promises = files.map(async (file) => {
try {
// 关键点:响应类型必须为 arraybuffer
const response = await axios.get(file.url, {
responseType: 'arraybuffer',
// 如果涉及跨域或需要认证,需在此配置 headers
// headers: { 'Authorization': 'Bearer...' }
});
// 将文件内容添加到 zip
// file.name 应该包含扩展名,例如 "合同.pdf"
zip.file(file.name, response.data);
loadedCount++;
if (onProgress) {
onProgress(loadedCount, total);
}
} catch (error) {
console.error(`文件 ${file.name} 下载失败`, error);
// 可以选择抛出错误中断,或者继续处理其他文件
// throw error;
}
});
// 等待所有文件获取完成
await Promise.all(promises);
// 生成 ZIP 文件
const content = await zip.generateAsync({ type: 'blob' });
// 触发下载
saveAs(content, zipName);
return true;
} catch (error) {
console.error('打包下载失败:', error);
throw error;
}
};
\ No newline at end of file
......@@ -2,11 +2,30 @@
<div class="uploadContainer">
<CardOne title="材料信息">
<template #headerRight>
<div>
<div style="margin-top: 20px;">
<el-button
type="primary"
:loading="downloading"
@click="handleBatchDownloadSelected"
>
{{ downloading ? '正在打包中...' : '下载材料包' }}
</el-button>
<div v-if="downloading" style="margin-top: 10px;">
<el-progress
:percentage="progressPercentage"
:format="progressFormat"
/>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
已处理文件:{{ currentCount }} / {{ totalCount }}
</div>
</div>
</div>
<!-- <div>
<el-button @click="downloadFile" type="primary" :loading="downLoading"
>下载材料包</el-button
>
</div>
</div> -->
</template>
<template #content>
<el-table v-loading="loading" :data="fileTableList" boder>
......@@ -134,6 +153,9 @@
</template>
<script setup name="FileUpload">
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { downloadFilesAsZip } from '@/utils/zipDownload'; // 引入刚才封装的工具
import CommonDialog from '@/components/commonDialog'
import CardOne from '@/components/formCard/cardOne'
import { getToken } from '@/utils/auth'
......@@ -177,6 +199,125 @@ const data = reactive({
}
})
// 下载材料包
// 状态管理
const downloading = ref(false);
const currentCount = ref(0);
const totalCount = ref(0);
const progressPercentage = computed(() => {
if (totalCount.value === 0) return 0;
return Math.floor((currentCount.value / totalCount.value) * 100);
});
const progressFormat = (percentage) => `${currentCount.value}/${totalCount.value}`;
// 2. 核心处理方法
const handleBatchDownloadSelected = async () => {
let apiMaterialDtoList = []
if (!props.idsObj.appointmentBizId) {
apiMaterialDtoList = fileTableList.value.map(item => {
return {
dataPerson: item.dataPerson, //资料人(字典)
dataPersonName: item.dataPersonName, //资料人(字典)
dataType: item.dataType, //资料类型(字典)
dataTypeName: item.dataTypeName, //资料类型(字典)
fileUrlList: item.fileBizIdList.map(item2 => ({
fileUrl: item2.url,
fileName: item2.originalName
}))
}
})
}else{
apiMaterialDtoList = fileTableList.value.map(item => {
return {
dataPerson: item.dataPerson, //资料人(字典)
dataPersonName: item.dataPersonName, //资料人(字典)
dataType: item.dataType, //资料类型(字典)
dataTypeName: item.dataTypeName, //资料类型(字典)
fileUrlList: item.fileUrlList
}
})
}
if (!apiMaterialDtoList || apiMaterialDtoList.length === 0) {
ElMessage.warning('没有要下载的材料');
return;
}
// --- 步骤 1: 数据清洗与扁平化 ---
const flatFileList = [];
let hasFiles = false;
apiMaterialDtoList.forEach((item,index) => {
// 安全检查:确保 fileUrlList 存在且是数组
const urls = item.fileUrlList;
if (!urls || !Array.isArray(urls) || urls.length === 0) {
return; // 跳过没有文件的行
}
hasFiles = true;
// 生成安全的业务前缀
// 规则:[人员类型]_[资料类型]_[业务ID]
// 例如:POLICYHOLDER_FRONT_2216
const safePerson = (item.dataPersonName || 'UNKNOWN').replace(/[\/\\:*?"<>|]/g, '_');
const safeType = (item.dataTypeName || 'FILE').replace(/[\/\\:*?"<>|]/g, '_');
const filePrefix = `${safePerson}_${safeType}`;
urls.forEach((fileItem, fIndex) => {
// 兼容 fileUrlList 可能是字符串数组 或 对象数组
let fileUrl = fileItem.fileUrl;
let originalFileName = fileItem.fileName;
// --- 关键:构建最终文件名 ---
const finalFileName = `${filePrefix}_${originalFileName}`;
if (fileUrl) {
flatFileList.push({
url: fileUrl,
name: finalFileName,
// 可选:保留元数据用于调试
_meta: {
type: item.dataType,
note: item.precautions
}
});
}
});
});
if (!hasFiles) {
ElMessage.warning('选中的项中没有包含任何附件');
return;
}
// --- 步骤 2: 执行下载 ---
totalCount.value = flatFileList.length;
currentCount.value = 0;
downloading.value = true;
try {
const zipName = `预约附件包_${new Date().toISOString().slice(0, 10)}.zip`;
await downloadFilesAsZip(flatFileList, zipName, (current, total) => {
currentCount.value = current;
});
ElMessage.success(`成功打包 ${flatFileList.length} 个文件`);
} catch (error) {
ElMessage.error('下载过程中出现异常,请查看控制台');
console.error(error);
} finally {
downloading.value = false;
}
};
const { queryParams, form } = toRefs(data)
// 新增:用于存储已上传成功的文件列表
const uploadedFiles = ref([])
......@@ -298,6 +439,7 @@ const handleView = row => {
imageUrl.value = row.fileUrl
imageViewerVisible.value = true
}
// 下载材料包
const downloadFile = () => {
let apiMaterialDtoList = []
......@@ -371,7 +513,7 @@ function handleBeforeUpload(file) {
proxy.$modal.msgError(`上传文件大小不能超过 ${fileSize.value} MB!`)
return false
}
// proxy.$modal.loading('正在上传文件,请稍候...')
proxy.$modal.loading('正在上传文件,请稍候...')
}
// 文件个数超出
function handleExceed() {
......@@ -380,6 +522,7 @@ function handleExceed() {
// 文件上传成功回调
const uploadSuccess = (res, file, fileList) => {
console.log('上传成功', res, file)
proxy.$modal.closeLoading();
if (res.code === 200) {
// 构造前端使用的文件对象(保留原始 file 信息 + 后端返回的 url 等)
const uploadedFile = {
......
......@@ -742,14 +742,14 @@ const remittanceConfig = [
type: 'upload',
prop: 'paymentVoucherList',
label: '支付凭证',
uploadType: 'image',
uploadType: 'file',
multiple: true,
limit: 5,
maxSize: 5 * 1024 * 1024, // 5MB
accept: '.png,.jpg,.jpeg,.webp', // ✅ 改成扩展名!
limit: 10,
maxSize: 10 * 1024 * 1024, // 10MB
accept: '.png,.jpg,.jpeg,.webp,.pdf,.doc,.docx,.xls,.xlsx,.txt', // ✅ 改成扩展名!
action: import.meta.env.VITE_APP_BASE_API + '/oss/api/oss/upload',
headers: { Authorization: 'Bearer ' + getToken() },
listType: 'picture-card',
listType: 'text',
defaultValue: [],
span: 24,
rules: [{ required: true, message: '请上传支付凭证', trigger: 'blur' }]
......@@ -758,14 +758,14 @@ const remittanceConfig = [
type: 'upload',
prop: 'accountVerificationList',
label: '账户证明',
uploadType: 'image',
uploadType: 'file',
multiple: true,
limit: 5,
maxSize: 5 * 1024 * 1024, // 5MB
accept: '.png,.jpg,.jpeg,.webp', // ✅ 改成扩展名!
limit: 10,
maxSize: 10 * 1024 * 1024, // 10MB
accept: '.png,.jpg,.jpeg,.webp,.pdf,.doc,.docx,.xls,.xlsx,.txt', // ✅ 改成扩展名!
action: import.meta.env.VITE_APP_BASE_API + '/oss/api/oss/upload',
headers: { Authorization: 'Bearer ' + getToken() },
listType: 'picture-card',
listType: 'text',
defaultValue: [],
span: 24,
rules: [{ required: true, message: '请上传账户证明', trigger: 'blur' }]
......@@ -776,7 +776,7 @@ const remittanceConfig = [
label: '其他资料',
uploadType: 'file',
multiple: true,
limit: 5,
limit: 10,
maxSize: 10 * 1024 * 1024, // 5MB
accept: '.png,.jpg,.jpeg,.webp,.pdf,.doc,.docx,.xls,.xlsx,.txt', // ✅ 支持多种格式
action: import.meta.env.VITE_APP_BASE_API + '/oss/api/oss/upload',
......
......@@ -239,7 +239,7 @@ import CommonDialog from '@/components/commonDialog'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Edit, Search, Share, Upload } from '@element-plus/icons-vue'
import { getPolicyfollow, getProductList } from '@/api/sign/underwritingMain'
import { uploadOssFileList,delUploadFile,getUserSaleExpandList } from '@/api/common'
import { uploadOssFileList,delUploadFile } from '@/api/common'
import { getProcessDetail } from '@/api/sign/fna'
import { premiumReconciliationList } from '@/api/sign/policy'
import { loadDicts, getDictLabel } from '@/utils/useDict'
......@@ -541,6 +541,7 @@ const policyInfoFormConfig = ref([
type: 'select',
prop: 'professionalInvestor',
label: '专业投资者',
defaultValue :'No',
options: [
{ label: '是', value: 'Yes' },
{ label: '否', value: 'No' }
......@@ -1039,8 +1040,9 @@ const activeTab = ref('basic')
// ✅ 新增:切换前确认
// ========================
const handleBeforeLeave = async (newTabName, oldTabName) => {
console.log('切换前确认-----------------------', newTabName, oldTabName)
console.log(tabDirty.value)
// console.log('切换前确认-----------------------', newTabName, oldTabName)
// console.log(tabDirty.value)
if(props.mode === 'viewDetail'){return;}
if (tabDirty.value[oldTabName]) {
try {
await ElMessageBox.confirm(`“${getTabLabel(oldTabName)}” 未提交,确定要切换吗?`, '提示', {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment