前端上传文件

联系人添加附件
pull/853/head
zh 2025-05-21 22:11:33 +08:00
parent c261bfcc6e
commit eef2316f4d
8 changed files with 341 additions and 3 deletions

View File

@ -7,7 +7,7 @@ VITE_DEV=true
VITE_BASE_URL='http://localhost:48080' VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务 # 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server VITE_UPLOAD_TYPE=client
# 接口地址 # 接口地址
VITE_API_URL=/admin-api VITE_API_URL=/admin-api

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -10,6 +10,10 @@ export interface FilePresignedUrlRespVO {
url: string url: string
// 文件路径 // 文件路径
path: string path: string
// 文件类型
category: string
// 编号
categoryId: number
} }
// 查询文件列表 // 查询文件列表

View File

@ -10,7 +10,7 @@ export const getUploadUrl = (): string => {
return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload' return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
} }
export const useUpload = (directory?: string) => { export const useUpload = (directory?: string, category?: string, categoryId?: number) => {
// 后端上传地址 // 后端上传地址
const uploadUrl = getUploadUrl() const uploadUrl = getUploadUrl()
// 是否使用前端直连上传 // 是否使用前端直连上传
@ -23,6 +23,8 @@ export const useUpload = (directory?: string) => {
const fileName = await generateFileName(options.file) const fileName = await generateFileName(options.file)
// 1.2 获取文件预签名地址 // 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory) const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
presignedInfo.category = category ?? ''
presignedInfo.categoryId = categoryId ?? 0
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持) // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
return axios return axios
.put(presignedInfo.uploadUrl, options.file, { .put(presignedInfo.uploadUrl, options.file, {
@ -72,6 +74,8 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
configId: vo.configId, configId: vo.configId,
url: vo.url, url: vo.url,
path: vo.path, path: vo.path,
category: vo.category,
categoryId: vo.categoryId,
name: file.name, name: file.name,
type: file.type, type: file.type,
size: file.size size: file.size

View File

@ -0,0 +1,103 @@
<template>
<Dialog v-model="dialogVisible" title="上传文件">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="uploadUrl"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:http-request="httpRequest"
accept=".jpg, .png, .gif, .txt, .doc, .docx, .xls"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入 jpgpnggiftxtdocdocxxls格式文件
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitFileForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'InfraFileForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const fileList = ref([])
const data = ref({ path: '', category: 'crm_consumer', categoryId: undefined as number | undefined })
const uploadRef = ref()
const props = defineProps<{ customerId: number }>()
const { uploadUrl, httpRequest } = useUpload(undefined, 'crm_consumer', props.customerId )
/** 打开弹窗 */
const open = (customerId: number) => {
data.value.categoryId = customerId
dialogVisible.value = true
resetForm()
}
/** 重置表单 */
const resetForm = () => {
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件变化处理 */
const handleFileChange = (file) => {
data.value.path = file.name
data.value.category = 'crm_consumer'
data.value.categoryId = props.customerId
}
/** 提交上传 */
const submitFileForm = () => {
if (fileList.value.length === 0) {
message.error('请上传文件')
return
}
unref(uploadRef)?.submit()
}
/** 上传成功处理 */
const emit = defineEmits(['success'])
const submitFormSuccess = () => {
dialogVisible.value = false
formLoading.value = false
uploadRef.value?.clearFiles()
message.success(t('common.createSuccess'))
emit('success')
}
/** 上传错误处理 */
const submitFormError = () => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 文件超出提示 */
const handleExceed = () => {
message.error('最多只能上传一个文件!')
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,207 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件路径" prop="path">
<el-input
v-model="queryParams.path"
placeholder="请输入文件路径"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="文件类型" prop="type">
<el-input
v-model="queryParams.type"
placeholder="请输入文件类型"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" plain @click="openForm">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="文件名" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="文件路径" align="center" prop="path" :show-overflow-tooltip="true" />
<el-table-column label="URL" align="center" prop="url" :show-overflow-tooltip="true" />
<el-table-column
label="文件大小"
align="center"
prop="size"
width="120"
:formatter="fileSizeFormatter"
/>
<el-table-column label="文件类型" align="center" prop="type" width="180px" />
<el-table-column label="文件内容" align="center" prop="url" width="110px">
<template #default="{ row }">
<el-image
v-if="row.type.includes('image')"
class="h-80px w-80px"
lazy
:src="row.url"
:preview-src-list="[row.url]"
preview-teleported
fit="cover"
/>
<el-link
v-else-if="row.type.includes('pdf')"
type="primary"
:href="row.url"
:underline="false"
target="_blank"
>预览</el-link
>
<el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank"
>下载</el-link
>
</template>
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="copyToClipboard(scope.row.url)">
复制链接
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:file:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<FileForm :customerId="props.customerId" ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { fileSizeFormatter } from '@/utils'
import { dateFormatter } from '@/utils/formatTime'
import * as FileApi from '@/api/infra/file'
import FileForm from './FileForm.vue'
defineOptions({ name: 'InfraFile' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const total = ref(0)
const list = ref([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
type: undefined,
path: undefined,
createTime: [],
category: 'crm_consumer',
categoryId: undefined as number | undefined //
})
const queryFormRef = ref()
const formRef = ref()
const props = defineProps<{ customerId: number }>()
/** 查询列表 */
const getList = async () => {
// customerId
queryParams.category = 'crm_consumer'
queryParams.categoryId = props.customerId
loading.value = true
try {
const data = await FileApi.getFilePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 打开上传表单 */
const openForm = () => {
formRef.value.open(props.customerId)
}
/** 复制链接 */
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
})
}
/** 删除文件 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await FileApi.deleteFile(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 初始化 */
onMounted(() => {
queryParams.category = 'crm_consumer'
queryParams.categoryId = props.customerId
getList()
})
</script>

View File

@ -68,7 +68,7 @@
<OperateLogV2 :log-list="logList" /> <OperateLogV2 :log-list="logList" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="附件"> <el-tab-pane label="附件">
<FileList :is-show-tip="false" v-model="fileUrl" :limit="1" /> <FileList :customerId="customerId" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
@ -95,6 +95,7 @@ import { BizTypeEnum } from '@/api/crm/permission'
import type { OperateLogVO } from '@/api/system/operatelog' import type { OperateLogVO } from '@/api/system/operatelog'
import { getOperateLogPage } from '@/api/crm/operateLog' import { getOperateLogPage } from '@/api/crm/operateLog'
import CustomerDistributeForm from '@/views/crm/customer/pool/CustomerDistributeForm.vue' import CustomerDistributeForm from '@/views/crm/customer/pool/CustomerDistributeForm.vue'
import FileList from '../components/FileList.vue'
defineOptions({ name: 'CrmCustomerDetail' }) defineOptions({ name: 'CrmCustomerDetail' })

9
yudao-ui-admin-vue3.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>