| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- <template>
- <view class="file-upload-container">
- <!-- 上传按钮 -->
- <view v-if="!disabled && fileList.length < limit" class="upload-section">
- <u-button
- type="primary"
- size="default"
- @click="chooseFile"
- :loading="uploading"
- icon="plus"
- class="upload-btn"
- >
- 选择文件
- </u-button>
- </view>
- <!-- 上传提示 -->
- <view v-if="showTip && !disabled" class="upload-tip">
- <text class="tip-text">
- 请上传
- <text v-if="fileSize" class="tip-highlight">大小不超过 {{ fileSize }}MB</text>
- <text v-if="fileType.length > 0" class="tip-highlight">
- 格式为 {{ fileType.join('/') }}
- </text>
- 的文件
- </text>
- </view>
- <!-- 文件列表 -->
- <view v-if="fileList.length > 0" class="file-list">
- <view v-for="(file, index) in fileList" :key="file.uid || index" class="file-item">
- <view class="file-info">
- <u-icon name="file-text" size="32" color="#909399" />
- <view class="file-detail">
- <text class="file-name">{{ getFileName(file.name) }}</text>
- <text class="file-size">{{ formatFileSize(file.size) }}</text>
- </view>
- </view>
- <view class="file-actions">
- <u-button
- type="primary"
- size="mini"
- plain
- @click="handleDownload(file)"
- :disabled="!file.ossId"
- class="action-btn"
- >
- 下载
- </u-button>
- <u-button
- v-if="!disabled"
- type="error"
- size="mini"
- plain
- @click="handleDelete(index)"
- class="action-btn"
- >
- 删除
- </u-button>
- </view>
- </view>
- </view>
- <!-- 上传进度 -->
- <u-modal
- v-model="showProgress"
- title="文件上传中"
- :show-cancel-button="false"
- :show-confirm-button="false"
- width="70%"
- z-index="10075"
- >
- <view class="upload-progress">
- <u-line-progress
- :percent="uploadPercent"
- :show-percent="true"
- height="12"
- active-color="#2979ff"
- />
- <text class="progress-text">{{ uploadPercent }}%</text>
- </view>
- </u-modal>
- <!-- 删除确认弹窗 -->
- <u-modal
- :show="showDeleteConfirm"
- title="提示"
- content="确定要删除这个文件吗?"
- show-cancel-button
- show-confirm-button
- confirm-text="确定"
- cancel-text="取消"
- @confirm="confirmDelete"
- @cancel="cancelDelete"
- z-index="10080"
- ></u-modal>
- <!-- 清空文件确认弹窗 -->
- <u-modal
- :showl="showClearConfirm"
- title="提示"
- content="确定要清空所有文件吗?"
- show-cancel-button
- show-confirm-button
- confirm-text="确定"
- cancel-text="取消"
- @confirm="confirmClear"
- @cancel="cancelClear"
- z-index="10080"
- ></u-modal>
- </view>
- </template>
- <script>
- import { uploadFile, downloadFile } from '@/common/request.js';
- import { REQUEST_CONFIG } from '@/common/config.js';
- export default {
- name: 'FileUpload',
- props: {
- value: {
- type: [String, Object, Array],
- default: () => [],
- },
- // 数量限制
- limit: {
- type: Number,
- default: 5,
- },
- // 大小限制(MB)
- fileSize: {
- type: Number,
- default: 5,
- },
- // 文件类型限制
- fileType: {
- type: Array,
- default: () => ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf'],
- },
- // 是否显示提示
- isShowTip: {
- type: Boolean,
- default: true,
- },
- // 禁用组件
- disabled: {
- type: Boolean,
- default: false,
- },
- },
- emits: ['input', 'submit', 'cancel'],
- data() {
- return {
- fileList: [],
- uploading: false,
- showProgress: false,
- uploadPercent: 0,
- showDeleteConfirm: false,
- deleteIndex: -1,
- showClearConfirm: false,
- };
- },
- computed: {
- showTip() {
- return this.isShowTip && (this.fileType.length > 0 || this.fileSize);
- },
- },
- watch: {
- value: {
- handler: 'initFileList',
- immediate: true,
- deep: true,
- },
- },
- methods: {
- // 初始化文件列表
- async initFileList(val) {
- if (val) {
- let list = [];
- if (Array.isArray(val)) {
- list = val;
- } else if (typeof val === 'string' && val.trim()) {
- // 如果是字符串,调用API获取文件详情
- try {
- const ids = val.split(',').filter((id) => id.trim());
- const response = await this.request({
- url: `/resource/oss/listByIds/${encodeURIComponent(ids.join(','))}`,
- method: 'GET',
- });
- if (response.code === 200) {
- list = response.data.map((oss) => ({
- name: oss.originalName,
- url: oss.url,
- ossId: oss.ossId,
- size: oss.size,
- }));
- }
- } catch (error) {
- console.error('获取文件详情失败:', error);
- }
- }
- this.fileList = list.map((item, index) => ({
- ...item,
- uid: item.uid || Date.now() + index,
- }));
- } else {
- this.fileList = [];
- }
- },
- // 选择文件
- chooseFile() {
- // 检查文件数量限制
- if (this.fileList.length >= this.limit) {
- uni.showToast({
- title: `最多只能上传${this.limit}个文件`,
- icon: 'none',
- });
- return;
- }
- uni.chooseFile({
- count: this.limit - this.fileList.length,
- type: 'file',
- success: (res) => {
- console.log('选择文件成功:', res);
- // 验证文件
- const validFiles = [];
- res.tempFiles.forEach((file) => {
- if (this.validateFile(file)) {
- validFiles.push(file);
- }
- });
- if (validFiles.length > 0) {
- this.uploadFiles(validFiles);
- }
- },
- fail: (error) => {
- console.error('选择文件失败:', error);
- uni.showToast({
- title: '选择文件失败',
- icon: 'none',
- });
- },
- });
- },
- // 验证文件
- validateFile(file) {
- // 检查文件类型
- if (this.fileType.length > 0) {
- const fileName = file.name || '';
- const fileExt = fileName.split('.').pop()?.toLowerCase();
- if (!fileExt || !this.fileType.includes(fileExt)) {
- uni.showToast({
- title: `文件格式不正确,请上传${this.fileType.join('/')}格式文件`,
- icon: 'none',
- });
- return false;
- }
- }
- // 检查文件名
- if (file.name && file.name.includes(',')) {
- uni.showToast({
- title: '文件名不能包含英文逗号',
- icon: 'none',
- });
- return false;
- }
- // 检查文件大小
- if (this.fileSize && file.size) {
- const fileSizeMB = file.size / 1024 / 1024;
- if (fileSizeMB > this.fileSize) {
- uni.showToast({
- title: `文件大小不能超过${this.fileSize}MB`,
- icon: 'none',
- });
- return false;
- }
- }
- return true;
- },
- // 上传文件
- async uploadFiles(files) {
- this.uploading = true;
- this.showProgress = true;
- this.uploadPercent = 0;
- try {
- const uploadPromises = files.map((file) => this.uploadSingleFile(file));
- const results = await Promise.all(uploadPromises);
- // 处理上传结果
- const successFiles = results.filter((result) => result.success);
- if (successFiles.length > 0) {
- this.fileList.push(
- ...successFiles.map((result) => ({
- name: result.data.fileName,
- url: result.data.url,
- ossId: result.data.ossId,
- size: result.originalSize,
- uid: Date.now() + Math.random(),
- })),
- );
- this.emitChange();
- uni.showToast({
- title: `成功上传${successFiles.length}个文件`,
- icon: 'success',
- });
- }
- const failedCount = files.length - successFiles.length;
- if (failedCount > 0) {
- uni.showToast({
- title: `${failedCount}个文件上传失败`,
- icon: 'none',
- });
- }
- } catch (error) {
- console.error('文件上传失败:', error);
- uni.showToast({
- title: '文件上传失败',
- icon: 'none',
- });
- } finally {
- this.uploading = false;
- this.showProgress = false;
- this.uploadPercent = 0;
- }
- },
- // 上传单个文件
- uploadSingleFile(file) {
- return new Promise((resolve) => {
- const uploadTask = uni.uploadFile({
- url: `${REQUEST_CONFIG.serverUrl}/resource/oss/upload`,
- filePath: file.path,
- name: 'file',
- header: {
- authorization: uni.getStorageSync('access_token')
- ? `Bearer ${uni.getStorageSync('access_token')}`
- : '',
- clientid:
- uni.getStorageSync('clientid') || 'e5cd7e4891bf95d1d19206ce24a7b32e',
- },
- success: (res) => {
- try {
- const data = JSON.parse(res.data);
- if (data.code === 200) {
- resolve({
- success: true,
- data: data.data,
- originalSize: file.size,
- });
- } else {
- console.error('上传失败:', data.msg);
- resolve({ success: false, error: data.msg });
- }
- } catch (error) {
- console.error('解析上传响应失败:', error);
- resolve({ success: false, error: '解析响应失败' });
- }
- },
- fail: (error) => {
- console.error('上传请求失败:', error);
- resolve({ success: false, error: error.errMsg || '上传失败' });
- },
- });
- // 监听上传进度
- uploadTask.onProgressUpdate((res) => {
- this.uploadPercent = res.progress;
- });
- });
- },
- // 下载文件
- async handleDownload(file) {
- if (!file.ossId) {
- uni.showToast({
- title: '无法获取文件下载地址',
- icon: 'none',
- });
- return;
- }
- try {
- uni.showLoading({
- title: '正在下载文件...',
- mask: true,
- });
- const downloadUrl = `/resource/oss/download/${file.ossId}`;
- const response = await downloadFile(downloadUrl, file.name);
- // const response = await downloadFile(downloadUrl, image.name);
- if (response.statusCode === 200) {
- uni.hideLoading();
- uni.saveFile({
- tempFilePath: response.tempFilePath,
- success: function (res) {
- uni.showToast({
- title: '下载成功',
- icon: 'success',
- });
- },
- fail: function () {
- uni.showToast({
- title: '保存失败',
- icon: 'none',
- });
- },
- });
- }
- // 在移动端,文件会自动保存到本地
- uni.hideLoading();
- uni.showToast({
- title: '文件下载完成',
- icon: 'success',
- });
- } catch (error) {
- console.error('下载文件失败:', error);
- uni.hideLoading();
- uni.showToast({
- title: '下载文件失败',
- icon: 'none',
- });
- }
- },
- // 删除文件
- handleDelete(index) {
- console.log('handleDelete', index);
- this.deleteIndex = index;
- this.showDeleteConfirm = true;
- },
- // 确认删除
- async confirmDelete() {
- try {
- const file = this.fileList[this.deleteIndex];
- // 如果有ossId,调用删除API
- if (file.ossId) {
- try {
- await this.request({
- url: `/resource/oss/${file.ossId}`,
- method: 'DELETE',
- });
- } catch (error) {
- console.error('删除文件失败:', error);
- }
- }
- this.fileList.splice(this.deleteIndex, 1);
- this.emitChange();
- uni.showToast({
- title: '删除成功',
- icon: 'success',
- });
- } catch (error) {
- console.error('删除文件失败:', error);
- uni.showToast({
- title: '删除失败',
- icon: 'none',
- });
- } finally {
- this.showDeleteConfirm = false;
- this.deleteIndex = -1;
- }
- },
- // 取消删除
- cancelDelete() {
- this.showDeleteConfirm = false;
- this.deleteIndex = -1;
- },
- // 获取文件名
- getFileName(name) {
- if (!name) return '未知文件';
- if (name.lastIndexOf('/') > -1) {
- return name.slice(name.lastIndexOf('/') + 1);
- }
- return name;
- },
- // 格式化文件大小
- formatFileSize(size) {
- if (!size) return '';
- if (size < 1024) {
- return size + 'B';
- } else if (size < 1024 * 1024) {
- return (size / 1024).toFixed(1) + 'KB';
- } else {
- return (size / 1024 / 1024).toFixed(1) + 'MB';
- }
- },
- // 发出变更事件
- emitChange() {
- const value = this.listToString(this.fileList);
- console.log('longwu---emitChange', value);
- this.$emit('input', value);
- },
- // 转换为字符串
- listToString(list, separator = ',') {
- return list
- .filter((item) => item.ossId)
- .map((item) => item.ossId)
- .join(separator);
- },
- // 显示确认对话框
- showConfirm(content) {
- return new Promise((resolve) => {
- uni.showModal({
- title: '提示',
- content,
- success: resolve,
- });
- });
- },
- // 提交文件
- handleSubmit() {
- if (this.fileList.length === 0) {
- uni.showToast({
- title: '请先选择文件',
- icon: 'none',
- });
- return;
- }
- // 发出提交事件,传递文件列表
- this.$emit('submit', {
- fileList: this.fileList,
- ossIds: this.listToString(this.fileList),
- });
- },
- // 取消操作
- handleCancel() {
- if (this.fileList.length > 0) {
- this.showClearConfirm = true;
- } else {
- this.$emit('cancel');
- }
- },
- // 确认清空文件
- confirmClear() {
- try {
- this.fileList = [];
- this.emitChange();
- this.$emit('cancel');
- uni.showToast({
- title: '已清空文件列表',
- icon: 'success',
- });
- } catch (error) {
- console.error('清空文件失败:', error);
- uni.showToast({
- title: '清空文件失败',
- icon: 'none',
- });
- } finally {
- this.showClearConfirm = false;
- }
- },
- // 取消清空文件
- cancelClear() {
- this.showClearConfirm = false;
- },
- // 请求方法
- async request(options) {
- const { request } = await import('@/common/request.js');
- return request(options);
- },
- },
- };
- </script>
- <style scoped>
- .file-upload-container {
- width: 90%;
- text-align: left;
- }
- .upload-section {
- margin-bottom: 20rpx;
- display: flex;
- justify-content: flex-start;
- align-items: flex-start;
- text-align: left;
- }
- .upload-btn {
- width: 160rpx;
- height: 64rpx;
- font-size: 26rpx;
- margin-left: 0;
- margin-right: auto;
- }
- .upload-tip {
- margin-bottom: 20rpx;
- padding: 16rpx;
- background: #f5f7fa;
- border-radius: 8rpx;
- }
- .tip-text {
- font-size: 24rpx;
- color: #606266;
- }
- .tip-highlight {
- color: #f56c6c;
- font-weight: 500;
- }
- .file-list {
- border: 1rpx solid #dcdfe6;
- border-radius: 8rpx;
- overflow: hidden;
- max-height: 400rpx;
- overflow-y: auto;
- }
- .file-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24rpx;
- border-bottom: 1rpx solid #ebeef5;
- }
- .file-item:last-child {
- border-bottom: none;
- }
- .file-info {
- display: flex;
- align-items: center;
- flex: 1;
- margin-right: 20rpx;
- }
- .file-detail {
- margin-left: 16rpx;
- flex: 1;
- }
- .file-name {
- display: block;
- font-size: 28rpx;
- color: #303133;
- margin-bottom: 8rpx;
- word-break: break-all;
- }
- .file-size {
- font-size: 24rpx;
- color: #909399;
- }
- .file-actions {
- display: flex;
- gap: 16rpx;
- }
- .action-btn {
- height: 60rpx;
- padding: 0 24rpx;
- font-size: 24rpx;
- }
- .upload-progress {
- padding: 40rpx 0;
- text-align: center;
- }
- .progress-text {
- display: block;
- margin-top: 20rpx;
- font-size: 24rpx;
- color: #606266;
- }
- </style>
|