客户组

This commit is contained in:
‘huanghaiixia’
2025-12-17 16:06:44 +08:00
parent 1eee4dcd60
commit a27bf90e9d
16 changed files with 2773 additions and 615 deletions

View File

@ -0,0 +1,138 @@
<template>
<div>
<Upload v-if="!disabled" style="margin-bottom: 10px;" :file-list="dataFile" :showUploadList="false"
v-model:value="tableId" v-model:tableName="tableName" v-model:columnName="columnName"
:btnTip="btnTip" @change="changeUplod" :multiple="true" :dataDelete="true"
:showDownloadIcon="false"/>
<a-table :columns="columns" :data-source="dataFile" >
<template #bodyCell="{ column,record,index, text }">
<template v-if="column.dataIndex === 'fileOrg'">
<a @click="handleDownload(record)">{{record.fileOrg}}</a>
</template>
<template v-if="column.dataIndex === 'docDesc' && !disabled">
<a-input :placeholder="t('请输入附件说明')" :disabled="disabled" v-model:value="record.docDesc" />
</template>
<template v-if="column.dataIndex === 'operation'">
<a style="margin-right: 10px" @click="btnCheck('delete', record, index)">删除</a>
<ArrowUpOutlined style="margin-right: 10px;" class="btn" @click="btnCheck('up', record, index)" />
<ArrowDownOutlined class="btn" @click="btnCheck('down', record, index)" />
</template>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from '/@/hooks/web/useI18n';
import Upload from '/@/components/Form/src/components/Upload.vue';
import {ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue';
import { parseDownloadUrl} from '/@/api/system/file';
import { downloadByUrl } from '/@/utils/file/download';
import { nextTick, ref, watch, computed } from 'vue';
const dataFile = ref([]);
const { t } = useI18n();
const columns = ref([
{ title: t('序号'), dataIndex: 'index', sorter: true, customRender: (column) => `${column.index + 1}`},
{ title: t('附件名称'), dataIndex: 'fileOrg', sorter: true},
{ title: t('附件说明'), dataIndex: 'docDesc', sorter: true},
{ title: t('操作'), dataIndex: 'operation', sorter: true},
]);
const tableId = ref<string>();
const tableName = ref<string>();
const columnName = ref<string>();
const props = defineProps({
value: String,
disabled: Boolean,
tableName: {
type: String,
default: ''
},
columnName: {
type: String,
default: ''
},
maxNumber: Number,
accept: String,
name: String,
multiple: Boolean,
maxSize: Number,
btnTip: {
type: String,
default: '上传'
},
tip: String,
fileList: Array,
});
const emit = defineEmits(['update:value', 'change', 'click', 'update:tableName', 'update:columnName']);
function changeUplod (val) {
dataFile.value = []
val.forEach(v => {
v.fileOrg = v.fileOrg || v.fileName,
v.filePath = v.filePath || v.fileUrl,
v.fileSize = v.fileSize
dataFile.value.push(v)
})
console.log(val, 532, dataFile.value)
}
const handleDownload = (info) => {
const url = parseDownloadUrl(info.response ? info.response.data.fileUrl : info.fileUrl);
const fileName = info.response ? info.response.data.fileOrg : info.fileOrg;
downloadByUrl({ url, fileName: fileName});
};
const btnCheck = (btn, record, index) => {
if (btn == 'delete') {
dataFile.value.splice(index, 1)
}
if (btn == 'up') {
if (index === 0) {
return
}
dataFile.value[index] = dataFile.value.splice(index-1, 1, dataFile.value[index])[0];
}
if (btn == 'down') {
if (index === dataFile.value.length - 1) {
return
}
dataFile.value[index] = dataFile.value.splice(index+1, 1, dataFile.value[index])[0];
}
}
const getFileList = () => {
return dataFile.value
}
watch(
() => props.disabled,
(val) => {
if (val) {
let idx2 = columns.value.findIndex(v =>v.dataIndex == 'operation')
idx2>-1 && columns.value.splice(idx2, 1)
}
},
{
immediate: true
}
);
watch(
() => props.fileList,
async (val) => {
dataFile.value = val || []
tableId.value = props.value
tableName.value = props.tableName
columnName.value = props.columnName
},
{
immediate: true
}
);
defineExpose({
getFileList
})
</script>
<style lang="less" scoped>
.btn {
color: #5e95ff;
font-size: 20px;
font-weight: bold;
}
</style>

View File

@ -1,511 +0,0 @@
<template>
<div>
<div v-if="listType === 'dragger'">
<a-upload-dragger
:file-list="fileList"
:maxCount="maxNumber"
:accept="accept"
:name="name"
:disabled="disabled"
:multiple="multiple"
:beforeUpload="beforeUpload"
listType="picture"
:show-upload-list="{ showDownloadIcon, showPreviewIcon, showRemoveIcon }"
@remove="handleRemove"
@download="handleDownload"
@preview="handlePreview"
@drop="handleClick"
@click="handleClick"
class="list-upload dragger-upload"
:style="style"
>
<div class="dragger-text">
<Icon icon="ep:upload-filled" color="#5e95ff" :size="24" />
<div class="mt-2 text-xs">点击或将文件拖拽到这里上传</div>
</div>
<div class="dragger-tip">{{ tip }}</div>
</a-upload-dragger>
</div>
<div v-else-if="listType === 'picture'">
<a-upload
:file-list="fileList"
:maxCount="maxNumber"
:accept="accept"
:name="name"
:disabled="disabled"
:multiple="multiple"
:beforeUpload="beforeUpload"
:listType="listType"
:show-upload-list="{ showDownloadIcon, showPreviewIcon, showRemoveIcon }"
@remove="handleRemove"
@download="handleDownload"
@preview="handlePreview"
@click="handleClick"
class="list-upload"
:style="style"
>
<plus-outlined />
</a-upload>
</div>
<a-upload
:file-list="fileListWithHeader"
:maxCount="maxNumber"
:accept="accept"
:name="name"
:disabled="disabled"
:multiple="multiple"
:beforeUpload="beforeUpload"
:listType="listType"
:show-upload-list="showUploadList"
@remove="handleRemove"
@download="handleDownload"
@preview="handlePreview"
@click="handleClick"
v-else
>
<plus-outlined v-if="listType == 'picture-card'" />
<div :style="style" v-else>
<a-button :loading="loading" :disabled="loading" v-if="!disabled">
<upload-outlined />
{{ btnTip || '点击上传' }}
</a-button>
<!-- <div v-if="VITE_GLOB_UPLOAD_ALERT_TIP?.trim()" style="color: red; margin-top: 8px">
{{ VITE_GLOB_UPLOAD_ALERT_TIP }}
</div> -->
</div>
<template #itemRender="{ file, actions }">
<template v-if="file.__header && showDownloadIcon">
<!-- <div class="file-list-header" style="display: flex; align-items: center; padding: 4px 0">
<input type="checkbox" :checked="isAllSelected" @change="toggleSelectAll" style="margin-right: 8px" />全选
<a-button type="primary" size="small" :disabled="!selectedIds.length" @click="handleBatchDownload" style="margin-left: 8px">批量打包下载</a-button>
</div> -->
</template>
<template v-else>
<a-space class="file-space">
<!-- <input v-if="showDownloadIcon" type="checkbox" :checked="selectedIds.includes(file.id)" @change="(e) => toggleSelectOne(file.id, e)" style="margin-right: 8px" /> -->
<PaperClipOutlined />
<span class="file-name-span" @click="actions.preview">{{ file.name }}</span>
<a-tooltip v-if="showDownloadIcon" title="下载">
<span @click="actions.download" class="file-outlined-span"><DownloadOutlined /></span>
</a-tooltip>
<a-tooltip v-if="!disabled && showRemoveIcon" title="删除">
<span @click="actions.remove" class="file-outlined-span"><DeleteOutlined /></span>
</a-tooltip>
<!-- <a-tooltip v-if="'.doc,.docx,.xls,.xlsx,.pdf'.includes(file.fileType)" title="编辑文档">
<span @click="editFile(file)" class="file-outlined-span"><EditOutlined /></span>
</a-tooltip> -->
</a-space>
</template>
</template>
</a-upload>
<a-modal :bodyStyle="bodyStyle" :width="800" :visible="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancel"> <iframe v-if="previewVisible" :src="previewFile" class="iframe-box"></iframe>; </a-modal>
<a-modal wrap-class-name="full-modal" width="100%" :visible="wpsPreviewVisible" :title="previewTitle" :footer="null" @cancel="handleCancelWps">
<div v-if="wpsPreviewVisible" id="office-container"></div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { nextTick, ref, watch, computed } from 'vue';
import { Upload } from 'ant-design-vue';
import { UploadOutlined, PlusOutlined, DownloadOutlined, DeleteOutlined, EditOutlined, PaperClipOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { deleteSingleFile, getAppToken, getFileList, parseDownloadUrl, getZipFiles } from '/@/api/system/file';
import { downloadByUrl } from '/@/utils/file/download';
import { uploadMultiApi } from '/@/api/sys/upload';
import Icon from '/@/components/Icon/index';
import { Base64 } from 'js-base64';
import { getAppEnvConfig } from '/@/utils/env';
import WebOfficeSDK from '/@/assets/libs/open-jssdk-v0.1.3.es.js';
import { getToken } from '/@/utils/auth';
import { useRoute } from 'vue-router';
const route = useRoute();
const { VITE_GLOB_UPLOAD_ALERT_TIP } = getAppEnvConfig();
const { createSuccessModal } = useMessage();
const props = defineProps({
value: String,
tableName: String,
columnName: String,
maxNumber: Number,
accept: String,
name: String,
disabled: Boolean,
multiple: Boolean,
maxSize: Number,
api: Function,
style: Object,
listType: {
type: String,
default: 'text'
},
tip: String,
showPreviewIcon: {
type: Boolean,
default: true
},
showRemoveIcon: {
type: Boolean,
default: true
},
showDownloadIcon: {
type: Boolean,
default: true
},
showUploadList: {
type: Boolean,
default: true
},
fileList: Array,
btnTip: String
});
const fileList = ref<any[]>([]);
const list = ref<any[]>([]);
const { notification } = useMessage();
const tableId = ref<string>(props.value);
const tableName = ref<string>(props.tableName);
const columnName = ref<string>(props.columnName);
const bindValues = (data:any)=>{
if(data){
tableId.value = data.tableId;
tableName.value = data.tableName;
columnName.value = data.columnName;
}else{
tableId.value = props.value || '';
tableName.value = props.tableName || '';
columnName.value = props.columnName || '';
}
}
const deleteFlag = ref(false);
const emit = defineEmits(['update:value', 'change', 'click','update:tableName', 'update:columnName']);
const loading = ref(false);
const previewVisible = ref(false);
const wpsPreviewVisible = ref(false);
const previewFile = ref('');
const previewTitle = ref('');
watch(
() => props.fileList,
async (val) => {
console.log(val, 43)
if (val) {
fileList.value = props.fileList
if (fileList.value.length) {
fileList.value.forEach((x) => {
x.name = x.fileName || x.fileOrg;
x.url = x.fileUrl || x.filePath;
x.thumbUrl = x.thUrl || x.filePath;
x.status = 'done'; //没有则不会展示下载按钮
x.fileType =x.fileType || ('.' + x.filePath.split('?')[0]?.split('.')?.pop())
});
}
bindValues(fileList.value[0]);
} else {
bindValues(undefined);
}
if (!val) {
fileList.value = [];
}
},
{
immediate: true
}
);
watch(
() => props.value,
async (val) => {
if (val) {
fileList.value = await getFileList({tableName: props.tableName, columnName: props.columnName,tableId: props.value});
if (fileList.value.length) {
fileList.value.forEach((x) => {
x.name = x.fileName;
x.url = x.fileUrl;
x.thumbUrl = x.thUrl;
x.status = 'done'; //没有则不会展示下载按钮
});
}
bindValues(fileList.value[0]);
} else {
bindValues(undefined);
}
if (!val) {
fileList.value = [];
}
},
{
immediate: true
}
);
watch(
() => list.value,
async (val) => {
if (deleteFlag.value) return;
if (val.length) {
let arr: any[] = val.filter((o) => {
return !o.status;
});
if (arr.length <= 0) return;
try {
let res = await uploadMultiApi(
{
name: 'file',
file: arr
},
tableId.value, tableName.value, columnName.value
);
let fileArr = res||[]
fileArr.forEach((x) => {
x.status = 'done'; //没有则不会展示下载按钮
x.url = x.fileUrl;
x.thumbUrl = x.thUrl;
x.fileSize = x.fileSize
x.name = x.fileOrg;
});
bindValues(res[0]);
fileList.value = [...fileList.value, ...fileArr]
emit('update:value', tableId.value);
emit('update:tableName', tableName.value);
emit('update:columnName', columnName.value);
emit('change', fileList.value);
loading.value = false;
} catch (error) {
console.error(error);
loading.value = false;
}
}
}
);
const bodyStyle = { height: '500px' };
const beforeUpload = (file) => {
if (props.maxSize && file.size / 1024 / 1024 > props.maxSize) {
notification.error({
message: 'Tip',
description: `文件大小不能超过${props.maxSize}MB`
});
return false || Upload.LIST_IGNORE;
}
if (props.maxNumber && fileList.value.length + list.value.length === props.maxNumber!) {
notification.error({
message: 'Tip',
description: `文件数量不能超过${props.maxNumber}个!`
});
return false;
}
list.value = [...list.value, file];
deleteFlag.value = false;
loading.value = true;
return Upload.LIST_IGNORE;
};
function handleClick() {
list.value = [];
}
const handleRemove = async (info) => {
const id = info.response ? info.response.data.id : info.id;
const index = fileList.value.findIndex((x) => x.id === id);
fileList.value.splice(index, 1);
emit('update:value', tableId.value);
emit('change', fileList.value);
notification.success({
message: 'Tip',
description: '删除成功!'
});
};
const handleDownload = (info) => {
console.log(info, 434)
const url = parseDownloadUrl(info.response ? info.response.data.fileUrl : (info.presignedUrl || info.fileUrl));
const fileName = info.response ? info.response.data.fileOrg : info.fileOrg;
downloadByUrl({ url, fileName: fileName });
};
const handleCancelWps = () => {
wpsPreviewVisible.value = false;
previewTitle.value = '';
};
const refreshToken = async () => {
return getToken();
};
const editFile = async (file) => {
wpsPreviewVisible.value = true;
previewTitle.value = file.name || file.fileName;
let appToken = await getAppToken({ _w_fileid: file.id });
let containerNode = document.getElementById('office-container');
containerNode.style.height = 'calc(100vh - 50px)';
containerNode.style.width = '100%';
let webOfficeSdk = WebOfficeSDK.config({
mount: containerNode,
url: appToken.wpsUrl + '&_w_tokentype=1',
refreshToken: refreshToken
});
webOfficeSdk.setToken({
token: appToken.token,
timeout: 10 * 60 * 1000
});
/*await webOfficeSdk.ready();
const app = webOfficeSdk.Application;
// 获取总页数
const totalPages = await app.ActiveDocument.Range.Information(
app.Enum.WdInformation.wdNumberOfPagesInDocument
);
console.log("总页数为:", totalPages);*/
};
const handlePreview = async (file) => {
// const fileUrl = file.presignedUrl|| file.response?.data?.fileUrl || file.fileUrl;
// console.log(fileUrl, 'fileUrl', file)
// window.open(fileUrl)
const fileUrl = file.presignedUrl|| file.response?.data?.fileUrl || file.fileUrl;
const fileName = file.response?.data?.fileOrg || file.fileOrg;
let fileFullUrl = fileUrl.includes('http://') || fileUrl.includes('https://') ? fileUrl : location.origin + getAppEnvConfig().VITE_GLOB_API_URL + fileUrl;
fileFullUrl+="&fullfilename="+fileName;
previewFile.value = getAppEnvConfig().VITE_GLOB_UPLOAD_PREVIEW + encodeURIComponent(Base64.encode(fileFullUrl));
};
const handleCancel = () => {
previewVisible.value = false;
previewTitle.value = '';
};
const selectedIds = ref<string[]>([]);
const isAllSelected = computed(() => fileList.value.length > 0 && selectedIds.value.length === fileList.value.length);
const fileListWithHeader = computed(() => {
// 只在有文件时插入头部
if (fileList.value.length) {
return [{ __header: true, uid: '__header__' }, ...fileList.value];
}
return fileList.value;
});
function toggleSelectAll(e: Event) {
const checked = (e.target as HTMLInputElement).checked;
selectedIds.value = checked ? fileList.value.map((f) => f.id) : [];
}
function toggleSelectOne(id: string, e: Event) {
const checked = (e.target as HTMLInputElement).checked;
if (checked) {
selectedIds.value = [...selectedIds.value, id];
} else {
selectedIds.value = selectedIds.value.filter((item) => item !== id);
}
}
async function handleBatchDownload() {
if (!selectedIds.value.length) return;
// getZipFiles 返回下载url
let formName = '';
try {
formName = (route.query.formName as string) || '';
// 获取当前页面得form name
} catch (error) {
console.warn(error);
}
const res = await getZipFiles({ fileIds: selectedIds.value.join(','), insertionFileName: formName });
if (!res) {
notification.error({
message: 'Tip',
description: '批量下载失败,请稍后重试!'
});
return;
} else if (res.type === 'async') {
createSuccessModal({ title: 'Tip', content: res.msg });
return;
} else if (res.type === 'synced') {
downloadByUrl({ url: res.url, fileName: res.name || 'files.zip' });
}
}
</script>
<style lang="less" scoped>
.list-upload {
:deep(.ant-upload) {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 128px;
height: 128px;
background: #fafafa;
border: 1px dashed #d9d9d9;
}
:deep(.anticon-plus) {
font-size: 20px;
color: #999;
}
.dragger-tip {
background: rgb(0 0 0 / 60%);
color: #fff;
height: 100%;
border-radius: 2px;
display: none;
padding: 8px 5px;
width: 100%;
}
.dragger-text {
padding: 16px 0;
}
&:hover .dragger-tip {
display: block;
}
&:hover .dragger-text {
display: none;
}
}
:deep(.ant-upload) {
padding: 0 !important;
}
.iframe-box {
width: 100%;
height: 100%;
}
.file-name-span {
margin-right: 16px;
color: #1890ff;
cursor: pointer;
}
.file-outlined-span {
cursor: pointer;
}
.file-space:hover {
background-color: #f5f5f5;
}
.file-space {
display: flex;
align-items: center;
padding: 5px 0;
}
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
}
}
</style>