完成图片、文件兼容修改语音icon 回复头像显示
This commit is contained in:
2
App.vue
2
App.vue
@@ -344,6 +344,8 @@ export default {
|
||||
this.$store.dispatch("user/getSelfInfo");
|
||||
this.$store.dispatch("conversation/getConversationList");
|
||||
this.$store.dispatch("conversation/getUnReadCount");
|
||||
this.$store.dispatch("contact/getFriendList");
|
||||
this.$store.dispatch("contact/getGrouplist");
|
||||
this.$store.dispatch("contact/getBlacklist");
|
||||
this.$store.dispatch("contact/getRecvFriendApplications");
|
||||
this.$store.dispatch("contact/getSentFriendApplications");
|
||||
|
||||
@@ -49,7 +49,7 @@ export default {
|
||||
computed: {
|
||||
getAvatarUrl() {
|
||||
if (this.src) {
|
||||
return this.src;
|
||||
return this.getFixedSourceUrl(this.src);
|
||||
}
|
||||
if (this.isGroup) {
|
||||
return defaultGroupIcon;
|
||||
@@ -65,6 +65,13 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFixedSourceUrl(url) {
|
||||
// 如果 url 以 http://47.117.71.33/api/object/ 开头,则替换为带端口的
|
||||
if (typeof url === 'string' && url.startsWith('http://47.117.71.33/api/object/')) {
|
||||
return url.replace('http://47.117.71.33/api/object/', 'http://47.117.71.33:15219/api/object/');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
errorHandle() {
|
||||
this.avatarText = this.desc ? this.desc.slice(0, 1) : "未知";
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"name" : "德讯",
|
||||
"appid" : "__UNI__D705A34",
|
||||
"description" : "",
|
||||
"versionName" : "toc_base_open 3.3.4",
|
||||
"versionCode" : 334,
|
||||
"versionName" : "fad-im 3.4.5",
|
||||
"versionCode" : 345,
|
||||
"transformPx" : false,
|
||||
"app-plus" : {
|
||||
"bounce" : "none",
|
||||
@@ -11,9 +11,9 @@
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : false,
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : false,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
"modules" : {
|
||||
@@ -21,8 +21,7 @@
|
||||
"Camera" : {},
|
||||
"Record" : {},
|
||||
"Geolocation" : {},
|
||||
"Maps" : {},
|
||||
"Push" : {}
|
||||
"Maps" : {}
|
||||
},
|
||||
"distribute" : {
|
||||
"android" : {
|
||||
|
||||
@@ -41,6 +41,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(["storeFriendList"]),
|
||||
getIndexData() {
|
||||
|
||||
return formatChooseData(this.storeFriendList);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -101,6 +101,73 @@
|
||||
<EmojiPicker @select="onEmojiSelect" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文件选择弹窗 -->
|
||||
<view v-if="showFileSelectModal" class="file-select-mask" @touchstart="showFileSelectModal = false">
|
||||
<view class="file-select-container" @touchstart.stop>
|
||||
<view class="file-select-header">
|
||||
<text class="file-select-title">选择文件</text>
|
||||
<text class="file-select-close" @click="showFileSelectModal = false">×</text>
|
||||
</view>
|
||||
<view class="file-select-tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentFileTab === 'chat' }"
|
||||
@click="currentFileTab = 'chat'"
|
||||
>
|
||||
聊天文件
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentFileTab === 'system' }"
|
||||
@click="currentFileTab = 'system'"
|
||||
>
|
||||
系统文件
|
||||
</view>
|
||||
</view>
|
||||
<view class="file-select-content">
|
||||
<view v-if="currentFileTab === 'chat'" class="chat-files">
|
||||
<view v-if="chatFiles.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无聊天文件</text>
|
||||
</view>
|
||||
<view v-else class="file-list">
|
||||
<view
|
||||
v-for="file in chatFiles"
|
||||
:key="file.clientMsgID"
|
||||
class="file-item"
|
||||
@click="selectChatFile(file)"
|
||||
>
|
||||
<image :src="getFileIcon(file)" class="file-icon" mode="aspectFit" />
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ getFileName(file) }}</text>
|
||||
<text class="file-size">{{ getFileSize(file) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="currentFileTab === 'system'" class="system-files">
|
||||
<view class="file-options">
|
||||
<view class="option-item" @click="chooseSystemFile('document')">
|
||||
<image src="/static/images/file_message/file_pdf.png" class="option-icon" mode="aspectFit" />
|
||||
<text class="option-text">文档</text>
|
||||
</view>
|
||||
<view class="option-item" @click="chooseSystemFile('image')">
|
||||
<image src="/static/images/file_message/file_image.png" class="option-icon" mode="aspectFit" />
|
||||
<text class="option-text">图片</text>
|
||||
</view>
|
||||
<view class="option-item" @click="chooseSystemFile('video')">
|
||||
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
|
||||
<text class="option-text">视频</text>
|
||||
</view>
|
||||
<view class="option-item" @click="chooseSystemFile('audio')">
|
||||
<image src="/static/images/file_message/file_unknown.png" class="option-icon" mode="aspectFit" />
|
||||
<text class="option-text">音频</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -169,6 +236,9 @@ export default {
|
||||
recordingTime: 0,
|
||||
recordingTimer: null,
|
||||
showEmojiPicker: false,
|
||||
showFileSelectModal: false,
|
||||
currentFileTab: 'chat',
|
||||
chatFiles: [],
|
||||
recorderManager: null,
|
||||
recordFilePath: '',
|
||||
recordFileDuration: 0,
|
||||
@@ -188,12 +258,21 @@ export default {
|
||||
footerOutsideFlag(newVal) {
|
||||
this.onClickActionBarOutside();
|
||||
},
|
||||
showFileSelectModal(newVal) {
|
||||
if (newVal) {
|
||||
// 当弹窗显示时,获取聊天文件列表
|
||||
this.getChatFiles();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setKeyboardListener();
|
||||
// 只在浏览器环境下添加事件监听
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('plusready', function() {
|
||||
// plus 相关代码
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disposeKeyboardListener();
|
||||
@@ -344,38 +423,275 @@ export default {
|
||||
}
|
||||
},
|
||||
async chooseFileAndSend() {
|
||||
if (!uni.chooseMessageFile) {
|
||||
return;
|
||||
// 显示文件选择弹窗,类似微信的文件选择
|
||||
this.showFileSelectModal = true;
|
||||
},
|
||||
|
||||
// 获取聊天文件列表
|
||||
getChatFiles() {
|
||||
// 从store中获取当前会话的消息列表,筛选出文件消息
|
||||
const messages = this.$store.getters.storeHistoryMessageList || [];
|
||||
this.chatFiles = messages.filter(msg => {
|
||||
return msg.contentType === 105; // 文件消息类型
|
||||
}).slice(-20); // 只显示最近20个文件
|
||||
},
|
||||
|
||||
// 选择聊天文件
|
||||
selectChatFile(file) {
|
||||
// 直接转发这个文件消息
|
||||
this.forwardFileMessage(file);
|
||||
this.showFileSelectModal = false;
|
||||
},
|
||||
|
||||
// 转发文件消息
|
||||
async forwardFileMessage(file) {
|
||||
try {
|
||||
const uuid = IMSDK.uuid();
|
||||
const fileElem = file.fileElem;
|
||||
|
||||
// 创建新的文件消息
|
||||
const message = await IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
|
||||
filePath: fileElem.filePath || '',
|
||||
fileName: fileElem.fileName,
|
||||
uuid: uuid,
|
||||
sourceUrl: fileElem.sourceUrl,
|
||||
fileSize: fileElem.fileSize,
|
||||
fileType: fileElem.fileType,
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
this.sendMessage(message);
|
||||
} catch (e) {
|
||||
console.log('转发文件失败:', e);
|
||||
uni.showToast({
|
||||
title: '转发文件失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件图标
|
||||
getFileIcon(file) {
|
||||
const fileName = file.fileElem?.fileName?.toLowerCase() || '';
|
||||
if (fileName.endsWith('.pdf')) {
|
||||
return '/static/images/file_message/file_pdf.png';
|
||||
} else if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
|
||||
return '/static/images/file_message/file_word.png';
|
||||
} else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) {
|
||||
return '/static/images/file_message/file_excel.png';
|
||||
} else if (fileName.endsWith('.ppt') || fileName.endsWith('.pptx')) {
|
||||
return '/static/images/file_message/file_ppt.png';
|
||||
} else if (fileName.endsWith('.zip') || fileName.endsWith('.rar') || fileName.endsWith('.7z')) {
|
||||
return '/static/images/file_message/file_zip.png';
|
||||
} else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png') || fileName.endsWith('.gif')) {
|
||||
return '/static/images/file_message/file_image.png';
|
||||
} else {
|
||||
return '/static/images/file_message/file_unknown.png';
|
||||
}
|
||||
},
|
||||
|
||||
// 获取文件名
|
||||
getFileName(file) {
|
||||
return file.fileElem?.fileName || '未知文件';
|
||||
},
|
||||
|
||||
// 获取文件大小
|
||||
getFileSize(file) {
|
||||
const size = file.fileElem?.fileSize;
|
||||
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';
|
||||
}
|
||||
},
|
||||
|
||||
// 选择系统文件
|
||||
chooseSystemFile(type) {
|
||||
switch (type) {
|
||||
case 'document':
|
||||
// 选择文档
|
||||
uni.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'file',
|
||||
success: async (res) => {
|
||||
const file = res.tempFiles[0];
|
||||
await this.processFileAndSend(file);
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '请从聊天记录中选择文档',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'image':
|
||||
// 选择图片
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album'],
|
||||
success: async (res) => {
|
||||
const file = {
|
||||
name: 'image.jpg',
|
||||
size: 0,
|
||||
path: res.tempFilePaths[0],
|
||||
type: 'image'
|
||||
};
|
||||
await this.processFileAndSend(file);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'video':
|
||||
// 选择视频
|
||||
uni.chooseVideo({
|
||||
sourceType: ['album'],
|
||||
success: async (res) => {
|
||||
const file = {
|
||||
name: 'video.mp4',
|
||||
size: res.size || 0,
|
||||
path: res.tempFilePath,
|
||||
type: 'video'
|
||||
};
|
||||
await this.processFileAndSend(file);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'audio':
|
||||
// 选择音频
|
||||
uni.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'file',
|
||||
success: async (res) => {
|
||||
const file = res.tempFiles[0];
|
||||
await this.processFileAndSend(file);
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '请从聊天记录中选择音频文件',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
this.showFileSelectModal = false;
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async processFileAndSend(file) {
|
||||
try {
|
||||
const nameIdx = file.name.lastIndexOf("/") + 1;
|
||||
const fileName = file.name.slice(nameIdx);
|
||||
const typeIdx = file.name.lastIndexOf(".") + 1;
|
||||
const fileType = file.name.slice(typeIdx);
|
||||
const { data: { url } } = await IMSDK.asyncApi(IMMethods.UploadFile, IMSDK.uuid(), {
|
||||
filepath: file.path,
|
||||
// 处理文件名
|
||||
let fileName = file.name;
|
||||
if (!fileName) {
|
||||
const pathParts = file.path.split('/');
|
||||
fileName = pathParts[pathParts.length - 1] || 'unknown';
|
||||
}
|
||||
|
||||
// 处理文件类型
|
||||
let fileType = file.type;
|
||||
if (!fileType) {
|
||||
const nameIdx = fileName.lastIndexOf(".") + 1;
|
||||
fileType = fileName.slice(nameIdx) || 'unknown';
|
||||
}
|
||||
|
||||
const uuid = IMSDK.uuid();
|
||||
|
||||
// 如果是图片类型,直接创建图片消息
|
||||
if (fileType.toLowerCase().includes('image') || fileType.toLowerCase().includes('jpg') || fileType.toLowerCase().includes('jpeg') || fileType.toLowerCase().includes('png') || fileType.toLowerCase().includes('gif')) {
|
||||
// 创建图片消息
|
||||
const message = await IMSDK.asyncApi(
|
||||
IMMethods.CreateImageMessageFromFullPath,
|
||||
IMSDK.uuid(),
|
||||
file.path
|
||||
);
|
||||
this.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先保存到本地
|
||||
uni.saveFile({
|
||||
tempFilePath: file.path,
|
||||
success: (saveRes) => {
|
||||
let savedFilePath = saveRes.savedFilePath;
|
||||
// 用 plus.io 转换为原生绝对路径
|
||||
if (typeof plus !== 'undefined') {
|
||||
plus.io.resolveLocalFileSystemURL(savedFilePath, (entry) => {
|
||||
IMSDK.asyncApi(IMMethods.UploadFile, uuid, {
|
||||
name: fileName,
|
||||
contentType: fileType,
|
||||
uuid: IMSDK.uuid(),
|
||||
});
|
||||
// 创建文件消息
|
||||
const message = await IMSDK.asyncApi(IMMethods.CreateFileMessage, IMSDK.uuid(), {
|
||||
uuid: uuid,
|
||||
filepath: entry.fullPath
|
||||
})
|
||||
.then(({ data }) => {
|
||||
let sourceUrl = data.url;
|
||||
IMSDK.asyncApi('createFileMessageByURL', IMSDK.uuid(), {
|
||||
filePath: entry.fullPath,
|
||||
fileName: fileName,
|
||||
fileSize: file.size,
|
||||
sourceUrl: url,
|
||||
uuid: uuid,
|
||||
sourceUrl: sourceUrl,
|
||||
fileSize: file.size || 0,
|
||||
fileType: fileType,
|
||||
})
|
||||
.then((message) => {
|
||||
IMSDK.asyncApi('sendMessageNotOss', IMSDK.uuid(), {
|
||||
recvID: this.storeCurrentConversation.userID,
|
||||
groupID: this.storeCurrentConversation.groupID,
|
||||
message: message,
|
||||
offlinePushInfo: {
|
||||
title: '你有新消息',
|
||||
desc: '新消息',
|
||||
ex: '',
|
||||
iOSPushSound: '+1',
|
||||
iOSBadgeCount: true,
|
||||
}
|
||||
})
|
||||
.then(({ data }) => {
|
||||
// 1. 本地新增消息
|
||||
this.pushNewMessage(message);
|
||||
// 2. 更新消息状态
|
||||
this.updateOneMessage({
|
||||
message: data,
|
||||
isSuccess: true,
|
||||
});
|
||||
this.$emit("scrollToBottom");
|
||||
})
|
||||
.catch(({ errCode, errMsg }) => {
|
||||
// 发送失败时也要更新消息状态为失败
|
||||
this.updateOneMessage({
|
||||
message: message,
|
||||
type: UpdateMessageTypes.KeyWords,
|
||||
keyWords: [
|
||||
{ key: "status", value: MessageStatus.Failed },
|
||||
{ key: "errCode", value: errCode },
|
||||
],
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 忽略错误提示
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 忽略错误提示
|
||||
});
|
||||
}, () => {
|
||||
// 忽略路径转换失败
|
||||
});
|
||||
this.sendMessage(message);
|
||||
} catch (e) {
|
||||
}
|
||||
},
|
||||
fail: function (err) {
|
||||
},
|
||||
fail: () => {
|
||||
// 忽略保存失败
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('处理文件失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// from comp
|
||||
@@ -648,4 +964,177 @@ export default {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.file-select-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 998;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.file-select-container {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.file-select-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.file-select-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-select-close {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.file-select-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #4a9cfc;
|
||||
border-bottom-color: #4a9cfc;
|
||||
background: rgba(74, 156, 252, 0.05);
|
||||
}
|
||||
|
||||
.file-select-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-files {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.system-files {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.file-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<view class="file-message" @click="downloadFile">
|
||||
<image
|
||||
:src="fileIcon"
|
||||
class="file-icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ fileName }}</text>
|
||||
<text class="file-size">{{ fileSizeText }}</text>
|
||||
</view>
|
||||
<image
|
||||
:src="downloadIcon"
|
||||
class="download-icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
source: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileName: function() {
|
||||
return this.source && this.source.fileElem && this.source.fileElem.fileName || '未知文件';
|
||||
},
|
||||
fileSizeText: function() {
|
||||
const size = this.source && this.source.fileElem && this.source.fileElem.fileSize;
|
||||
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';
|
||||
}
|
||||
},
|
||||
fileIcon: function() {
|
||||
const fileName = this.fileName.toLowerCase();
|
||||
if (fileName.endsWith('.pdf')) {
|
||||
return '/static/images/file_message/file_pdf.png';
|
||||
} else if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
|
||||
return '/static/images/file_message/file_word.png';
|
||||
} else if (fileName.endsWith('.xls') || fileName.endsWith('.xlsx')) {
|
||||
return '/static/images/file_message/file_excel.png';
|
||||
} else if (fileName.endsWith('.ppt') || fileName.endsWith('.pptx')) {
|
||||
return '/static/images/file_message/file_ppt.png';
|
||||
} else if (fileName.endsWith('.zip') || fileName.endsWith('.rar') || fileName.endsWith('.7z')) {
|
||||
return '/static/images/file_message/file_zip.png';
|
||||
} else if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png') || fileName.endsWith('.gif')) {
|
||||
return '/static/images/file_message/file_image.png';
|
||||
} else {
|
||||
return '/static/images/file_message/file_unknown.png';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: '/static/images/file_message/file_download.png'
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getFixedSourceUrl: function(url) {
|
||||
// 如果 url 以 http://47.117.71.33/api/object/ 开头,则替换为带端口的
|
||||
if (typeof url === 'string' && url.startsWith('http://47.117.71.33/api/object/')) {
|
||||
return url.replace('http://47.117.71.33/api/object/', 'http://47.117.71.33:15219/api/object/');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
downloadFile: function() {
|
||||
const sourceUrl = this.source && this.source.fileElem && this.source.fileElem.sourceUrl;
|
||||
if (!sourceUrl) {
|
||||
uni.showToast({
|
||||
title: '文件链接无效',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fixedUrl = this.getFixedSourceUrl(sourceUrl);
|
||||
|
||||
// 在APP环境下直接下载
|
||||
// #ifdef APP-PLUS
|
||||
uni.downloadFile({
|
||||
url: fixedUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '文件已保存',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// 在其他环境下打开链接
|
||||
// #ifndef APP-PLUS
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '是否打开文件链接?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// #ifdef H5
|
||||
window.open(fixedUrl);
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setClipboardData({
|
||||
data: fixedUrl,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '链接已复制',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f5f6fa;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
margin: 4px 0;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,16 @@
|
||||
@click="clickMediaItem"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<u-loading-icon color="red"></u-loading-icon>
|
||||
<view class="custom-loading" :style="{ width: '120px', height: maxHeight + 'px' }">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner">
|
||||
<view class="spinner-ring"></view>
|
||||
<view class="spinner-ring"></view>
|
||||
<view class="spinner-ring"></view>
|
||||
</view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</u--image>
|
||||
</view>
|
||||
@@ -28,13 +37,22 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
|
||||
getImgUrl() {
|
||||
if (!this.message || !this.message.pictureElem) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
this.message.pictureElem.snapshotPicture?.url ??
|
||||
this.message.pictureElem.sourcePath
|
||||
this.message.pictureElem.snapshotPicture && this.message.pictureElem.snapshotPicture.url ?
|
||||
this.getFixedSourceUrl(this.message.pictureElem.snapshotPicture.url) :
|
||||
this.getFixedSourceUrl(this.message.pictureElem.sourcePicture.url)
|
||||
);
|
||||
},
|
||||
maxHeight() {
|
||||
if (!this.message || !this.message.pictureElem || !this.message.pictureElem.sourcePicture) {
|
||||
return 120;
|
||||
}
|
||||
const imageHeight = this.message.pictureElem.sourcePicture.height;
|
||||
const imageWidth = this.message.pictureElem.sourcePicture.width;
|
||||
const aspectRatio = imageHeight / imageWidth;
|
||||
@@ -42,10 +60,20 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFixedSourceUrl(url) {
|
||||
if (typeof url === 'string' && url.startsWith('http://47.117.71.33/api/object/')) {
|
||||
return url.replace('http://47.117.71.33/api/object/', 'http://47.117.71.33:15219/api/object/');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
clickMediaItem() {
|
||||
if (!this.message || !this.message.pictureElem || !this.message.pictureElem.sourcePicture) {
|
||||
return;
|
||||
}
|
||||
uni.previewImage({
|
||||
current: 0,
|
||||
urls: [this.message.pictureElem.sourcePicture.url],
|
||||
urls: [this.getFixedSourceUrl(this.message.pictureElem.sourcePicture.url)],
|
||||
indicator: "none",
|
||||
});
|
||||
},
|
||||
@@ -61,6 +89,7 @@ export default {
|
||||
position: relative;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
.play_icon {
|
||||
width: 48px;
|
||||
@@ -78,4 +107,71 @@ export default {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.spinner-ring {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 3px solid transparent;
|
||||
border-top: 3px solid #4a9cfc;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.2s linear infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
border-top-color: #6bb6ff;
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
border-top-color: #8cc8ff;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,8 +20,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
getContent() {
|
||||
console.log(this.message);
|
||||
return parseBr(this.message.textElem.content);
|
||||
return this.message && this.message.textElem && this.message.textElem.content ?
|
||||
parseBr(this.message.textElem.content) : '';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="voice-message" @click="playVoice">
|
||||
<image
|
||||
:src="isPlaying ? audioIcon : recordIcon"
|
||||
class="audio-icon flipped"
|
||||
:class="['audio-icon', { 'flipped': isSender }]"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="duration">{{ durationText }}</text>
|
||||
@@ -15,11 +15,15 @@ export default {
|
||||
source: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isSender: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
durationText() {
|
||||
const d = this.source.soundElem && this.source.soundElem.duration;
|
||||
const d = this.source && this.source.soundElem && this.source.soundElem.duration;
|
||||
return d ? `${d}''` : '';
|
||||
}
|
||||
},
|
||||
@@ -46,7 +50,7 @@ export default {
|
||||
this.innerAudioContext = null;
|
||||
}
|
||||
this.innerAudioContext = uni.createInnerAudioContext();
|
||||
const rawUrl = this.source.soundElem && this.source.soundElem.sourceUrl || '';
|
||||
const rawUrl = this.source && this.source.soundElem && this.source.soundElem.sourceUrl || '';
|
||||
this.innerAudioContext.src = this.getFixedSourceUrl(rawUrl);
|
||||
this.isPlaying = true;
|
||||
this.innerAudioContext.play();
|
||||
|
||||
@@ -41,16 +41,16 @@
|
||||
<TextMessageRender :message="source" />
|
||||
</template>
|
||||
<template v-else-if="source.contentType === 102">
|
||||
<MediaMessageRender :source="source" />
|
||||
<MediaMessageRender :message="source" />
|
||||
</template>
|
||||
<template v-else-if="source.contentType === 103">
|
||||
<VoiceMessageRender :source="source" />
|
||||
<VoiceMessageRender :source="source" :isSender="isSender" />
|
||||
</template>
|
||||
<template v-else-if="source.contentType === 104">
|
||||
<view style="color:#999">[暂未实现] 视频消息</view>
|
||||
</template>
|
||||
<template v-else-if="source.contentType === 105">
|
||||
<view style="color:#999">[暂未实现] 文件消息</view>
|
||||
<FileMessageRender :source="source" />
|
||||
</template>
|
||||
<template v-else-if="source.contentType === 106">
|
||||
<view style="color:#999">[暂未实现] @消息</view>
|
||||
@@ -93,6 +93,7 @@ import TextMessageRender from "./TextMessageRender.vue";
|
||||
import MediaMessageRender from "./MediaMessageRender.vue";
|
||||
import ErrorMessageRender from "./ErrorMessageRender.vue";
|
||||
import VoiceMessageRender from './VoiceMessageRender.vue'
|
||||
import FileMessageRender from './FileMessageRender.vue'
|
||||
import { noticeMessageTypes } from "@/constant";
|
||||
import { tipMessaggeFormat, formatMessageTime } from "@/util/imCommon";
|
||||
|
||||
@@ -106,7 +107,8 @@ export default {
|
||||
TextMessageRender,
|
||||
MediaMessageRender,
|
||||
ErrorMessageRender,
|
||||
VoiceMessageRender
|
||||
VoiceMessageRender,
|
||||
FileMessageRender
|
||||
},
|
||||
props: {
|
||||
source: Object,
|
||||
@@ -131,15 +133,18 @@ export default {
|
||||
);
|
||||
},
|
||||
formattedMessageTime() {
|
||||
return formatMessageTime(this.source.sendTime);
|
||||
return this.source && this.source.sendTime ? formatMessageTime(this.source.sendTime) : '';
|
||||
},
|
||||
showTextRender() {
|
||||
return textRenderTypes.includes(this.source.contentType);
|
||||
return this.source && this.source.contentType ? textRenderTypes.includes(this.source.contentType) : false;
|
||||
},
|
||||
showMediaRender() {
|
||||
return mediaRenderTypes.includes(this.source.contentType);
|
||||
return this.source && this.source.contentType ? mediaRenderTypes.includes(this.source.contentType) : false;
|
||||
},
|
||||
getNoticeContent() {
|
||||
if (!this.source || !this.source.contentType) {
|
||||
return "";
|
||||
}
|
||||
const isNoticeMessage = noticeMessageTypes.includes(
|
||||
this.source.contentType
|
||||
);
|
||||
@@ -151,13 +156,13 @@ export default {
|
||||
);
|
||||
},
|
||||
isSuccessMessage() {
|
||||
return this.source.status === MessageStatus.Succeed;
|
||||
return this.source && this.source.status === MessageStatus.Succeed;
|
||||
},
|
||||
isFailedMessage() {
|
||||
return this.source.status === MessageStatus.Failed;
|
||||
return this.source && this.source.status === MessageStatus.Failed;
|
||||
},
|
||||
showSending() {
|
||||
return this.source.status === MessageStatus.Sending && !this.sendingDelay;
|
||||
return this.source && this.source.status === MessageStatus.Sending && !this.sendingDelay;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -166,7 +171,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
setSendingDelay() {
|
||||
if (this.source.status === MessageStatus.Sending) {
|
||||
if (this.source && this.source.status === MessageStatus.Sending) {
|
||||
setTimeout(() => {
|
||||
this.sendingDelay = false;
|
||||
}, 2000);
|
||||
|
||||
@@ -53,7 +53,6 @@ export default {
|
||||
let parsedMessage;
|
||||
try {
|
||||
parsedMessage = JSON.parse(this.source.latestMsg);
|
||||
console.log(parsedMessage);
|
||||
|
||||
} catch (e) {}
|
||||
if (!parsedMessage) return "";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<view class="page_container">
|
||||
<view class="login">
|
||||
<view class="logo">
|
||||
<img src="static/images/logo.png" alt="" />
|
||||
<view class="title">欢迎使用福安德内部平台</view>
|
||||
<img src="/static/logo.png" alt="" />
|
||||
<view class="title">欢迎使用德迅,福安德信息科技有限公司内部交流平台</view>
|
||||
</view>
|
||||
<u-tabs :list="list" @click="click"></u-tabs>
|
||||
<u-form
|
||||
@@ -209,8 +209,8 @@ export default {
|
||||
this.$store.dispatch("user/getSelfInfo");
|
||||
this.$store.dispatch("conversation/getConversationList");
|
||||
this.$store.dispatch("conversation/getUnReadCount");
|
||||
// this.$store.dispatch("contact/getFriendList");
|
||||
// this.$store.dispatch("contact/getGrouplist");
|
||||
this.$store.dispatch("contact/getFriendList");
|
||||
this.$store.dispatch("contact/getGrouplist");
|
||||
this.$store.dispatch("contact/getBlacklist");
|
||||
this.$store.dispatch("contact/getRecvFriendApplications");
|
||||
this.$store.dispatch("contact/getSentFriendApplications");
|
||||
@@ -316,21 +316,27 @@ export default {
|
||||
);
|
||||
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 64rpx;
|
||||
// color: $u-primary;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 13 KiB |
81
uni_modules/uni-file-picker/changelog.md
Normal file
81
uni_modules/uni-file-picker/changelog.md
Normal file
@@ -0,0 +1,81 @@
|
||||
## 1.0.12(2025-04-14)
|
||||
- 修复 支付宝小程序 上传样式问题
|
||||
## 1.0.10(2024-07-09)
|
||||
- 优化 vue3兼容性
|
||||
## 1.0.9(2024-07-09)
|
||||
- 修复 value 属性不兼容vue3的bug
|
||||
## 1.0.8(2024-03-20)
|
||||
- 补充 删除文件时返回文件下标
|
||||
## 1.0.7(2024-02-21)
|
||||
- 新增 微信小程序选择视频时改用chooseMedia,并返回视频缩略图
|
||||
## 1.0.6(2024-01-06)
|
||||
- 新增 微信小程序不再调用chooseImage,而是调用chooseMedia
|
||||
## 1.0.5(2024-01-03)
|
||||
- 新增 上传文件至云存储携带本地文件名称
|
||||
## 1.0.4(2023-03-29)
|
||||
- 修复 手动上传删除一个文件后不能再上传的bug
|
||||
## 1.0.3(2022-12-19)
|
||||
- 新增 sourceType 属性, 可以自定义图片和视频选择的来源
|
||||
## 1.0.2(2022-07-04)
|
||||
- 修复 在uni-forms下样式不生效的bug
|
||||
## 1.0.1(2021-11-23)
|
||||
- 修复 参数为对象的情况下,url在某些情况显示错误的bug
|
||||
## 1.0.0(2021-11-19)
|
||||
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
|
||||
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker)
|
||||
## 0.2.16(2021-11-08)
|
||||
- 修复 传入空对象 ,显示错误的Bug
|
||||
## 0.2.15(2021-08-30)
|
||||
- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug
|
||||
## 0.2.14(2021-08-23)
|
||||
- 新增 参数中返回 fileID 字段
|
||||
## 0.2.13(2021-08-23)
|
||||
- 修复 腾讯云传入fileID 不能回显的bug
|
||||
- 修复 选择图片后,不能放大的问题
|
||||
## 0.2.12(2021-08-17)
|
||||
- 修复 由于 0.2.11 版本引起的不能回显图片的Bug
|
||||
## 0.2.11(2021-08-16)
|
||||
- 新增 clearFiles(index) 方法,可以手动删除指定文件
|
||||
- 修复 v-model 值设为 null 报错的Bug
|
||||
## 0.2.10(2021-08-13)
|
||||
- 修复 return-type="object" 时,无法删除文件的Bug
|
||||
## 0.2.9(2021-08-03)
|
||||
- 修复 auto-upload 属性失效的Bug
|
||||
## 0.2.8(2021-07-31)
|
||||
- 修复 fileExtname属性不指定值报错的Bug
|
||||
## 0.2.7(2021-07-31)
|
||||
- 修复 在某种场景下图片不回显的Bug
|
||||
## 0.2.6(2021-07-30)
|
||||
- 修复 return-type为object下,返回值不正确的Bug
|
||||
## 0.2.5(2021-07-30)
|
||||
- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题
|
||||
## 0.2.3(2021-07-28)
|
||||
- 优化 调整示例代码
|
||||
## 0.2.2(2021-07-27)
|
||||
- 修复 vue3 下赋值错误的Bug
|
||||
- 优化 h5平台下上传文件导致页面卡死的问题
|
||||
## 0.2.0(2021-07-13)
|
||||
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
|
||||
## 0.1.1(2021-07-02)
|
||||
- 修复 sourceType 缺少默认值导致 ios 无法选择文件
|
||||
## 0.1.0(2021-06-30)
|
||||
- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改
|
||||
## 0.0.11(2021-06-30)
|
||||
- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题
|
||||
## 0.0.10(2021-06-29)
|
||||
- 优化 文件上传后进度条消失时机
|
||||
## 0.0.9(2021-06-29)
|
||||
- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug
|
||||
## 0.0.8(2021-06-15)
|
||||
- 修复 删除文件时无法触发 v-model 的Bug
|
||||
## 0.0.7(2021-05-12)
|
||||
- 新增 组件示例地址
|
||||
## 0.0.6(2021-04-09)
|
||||
- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug
|
||||
## 0.0.5(2021-04-09)
|
||||
- 优化 更新组件示例
|
||||
## 0.0.4(2021-04-09)
|
||||
- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔
|
||||
## 0.0.3(2021-02-05)
|
||||
- 调整为uni_modules目录规范
|
||||
- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug
|
||||
@@ -0,0 +1,287 @@
|
||||
'use strict';
|
||||
|
||||
const ERR_MSG_OK = 'chooseAndUploadFile:ok';
|
||||
const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
|
||||
|
||||
function chooseImage(opts) {
|
||||
const {
|
||||
count,
|
||||
sizeType = ['original', 'compressed'],
|
||||
sourceType,
|
||||
extension
|
||||
} = opts
|
||||
return new Promise((resolve, reject) => {
|
||||
// 微信由于旧接口不再维护,针对微信小程序平台改用chooseMedia接口
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count,
|
||||
sizeType,
|
||||
sourceType,
|
||||
mediaType: ['image'],
|
||||
extension,
|
||||
success(res) {
|
||||
res.tempFiles.forEach(item => {
|
||||
item.path = item.tempFilePath;
|
||||
})
|
||||
resolve(normalizeChooseAndUploadFileRes(res, 'image'));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count,
|
||||
sizeType,
|
||||
sourceType,
|
||||
extension,
|
||||
success(res) {
|
||||
resolve(normalizeChooseAndUploadFileRes(res, 'image'));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function chooseVideo(opts) {
|
||||
const {
|
||||
count,
|
||||
camera,
|
||||
compressed,
|
||||
maxDuration,
|
||||
sourceType,
|
||||
extension
|
||||
} = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
// 微信由于旧接口不再维护,针对微信小程序平台改用chooseMedia接口
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count,
|
||||
compressed,
|
||||
maxDuration,
|
||||
sourceType,
|
||||
extension,
|
||||
mediaType: ['video'],
|
||||
success(res) {
|
||||
const {
|
||||
tempFiles,
|
||||
} = res;
|
||||
resolve(normalizeChooseAndUploadFileRes({
|
||||
errMsg: 'chooseVideo:ok',
|
||||
tempFiles: tempFiles.map(item => {
|
||||
return {
|
||||
name: item.name || '',
|
||||
path: item.tempFilePath,
|
||||
thumbTempFilePath: item.thumbTempFilePath,
|
||||
size:item.size,
|
||||
type: (res.tempFile && res.tempFile.type) || '',
|
||||
width:item.width,
|
||||
height:item.height,
|
||||
duration:item.duration,
|
||||
fileType: 'video',
|
||||
cloudPath: '',
|
||||
}
|
||||
}),
|
||||
}, 'video'));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseVideo({
|
||||
camera,
|
||||
compressed,
|
||||
maxDuration,
|
||||
sourceType,
|
||||
extension,
|
||||
success(res) {
|
||||
const {
|
||||
tempFilePath,
|
||||
duration,
|
||||
size,
|
||||
height,
|
||||
width
|
||||
} = res;
|
||||
resolve(normalizeChooseAndUploadFileRes({
|
||||
errMsg: 'chooseVideo:ok',
|
||||
tempFilePaths: [tempFilePath],
|
||||
tempFiles: [{
|
||||
name: (res.tempFile && res.tempFile.name) || '',
|
||||
path: tempFilePath,
|
||||
size,
|
||||
type: (res.tempFile && res.tempFile.type) || '',
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
fileType: 'video',
|
||||
cloudPath: '',
|
||||
}, ],
|
||||
}, 'video'));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
function chooseAll(opts) {
|
||||
const {
|
||||
count,
|
||||
extension
|
||||
} = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
let chooseFile = uni.chooseFile;
|
||||
if (typeof wx !== 'undefined' &&
|
||||
typeof wx.chooseMessageFile === 'function') {
|
||||
chooseFile = wx.chooseMessageFile;
|
||||
}
|
||||
if (typeof chooseFile !== 'function') {
|
||||
return reject({
|
||||
errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。',
|
||||
});
|
||||
}
|
||||
chooseFile({
|
||||
type: 'all',
|
||||
count,
|
||||
extension,
|
||||
success(res) {
|
||||
resolve(normalizeChooseAndUploadFileRes(res));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeChooseAndUploadFileRes(res, fileType) {
|
||||
res.tempFiles.forEach((item, index) => {
|
||||
if (!item.name) {
|
||||
item.name = item.path.substring(item.path.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (fileType) {
|
||||
item.fileType = fileType;
|
||||
}
|
||||
item.cloudPath =
|
||||
Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.'));
|
||||
});
|
||||
if (!res.tempFilePaths) {
|
||||
res.tempFilePaths = res.tempFiles.map((file) => file.path);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function uploadCloudFiles(files, max = 5, onUploadProgress) {
|
||||
files = JSON.parse(JSON.stringify(files))
|
||||
const len = files.length
|
||||
let count = 0
|
||||
let self = this
|
||||
return new Promise(resolve => {
|
||||
while (count < max) {
|
||||
next()
|
||||
}
|
||||
|
||||
function next() {
|
||||
let cur = count++
|
||||
if (cur >= len) {
|
||||
!files.find(item => !item.url && !item.errMsg) && resolve(files)
|
||||
return
|
||||
}
|
||||
const fileItem = files[cur]
|
||||
const index = self.files.findIndex(v => v.uuid === fileItem.uuid)
|
||||
fileItem.url = ''
|
||||
delete fileItem.errMsg
|
||||
|
||||
uniCloud
|
||||
.uploadFile({
|
||||
filePath: fileItem.path,
|
||||
cloudPath: fileItem.cloudPath,
|
||||
fileType: fileItem.fileType,
|
||||
onUploadProgress: res => {
|
||||
res.index = index
|
||||
onUploadProgress && onUploadProgress(res)
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
fileItem.url = res.fileID
|
||||
fileItem.index = index
|
||||
if (cur < len) {
|
||||
next()
|
||||
}
|
||||
})
|
||||
.catch(res => {
|
||||
fileItem.errMsg = res.errMsg || res.message
|
||||
fileItem.index = index
|
||||
if (cur < len) {
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function uploadFiles(choosePromise, {
|
||||
onChooseFile,
|
||||
onUploadProgress
|
||||
}) {
|
||||
return choosePromise
|
||||
.then((res) => {
|
||||
if (onChooseFile) {
|
||||
const customChooseRes = onChooseFile(res);
|
||||
if (typeof customChooseRes !== 'undefined') {
|
||||
return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ?
|
||||
res : chooseRes);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then((res) => {
|
||||
if (res === false) {
|
||||
return {
|
||||
errMsg: ERR_MSG_OK,
|
||||
tempFilePaths: [],
|
||||
tempFiles: [],
|
||||
};
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
function chooseAndUploadFile(opts = {
|
||||
type: 'all'
|
||||
}) {
|
||||
if (opts.type === 'image') {
|
||||
return uploadFiles(chooseImage(opts), opts);
|
||||
} else if (opts.type === 'video') {
|
||||
return uploadFiles(chooseVideo(opts), opts);
|
||||
}
|
||||
return uploadFiles(chooseAll(opts), opts);
|
||||
}
|
||||
|
||||
export {
|
||||
chooseAndUploadFile,
|
||||
uploadCloudFiles
|
||||
};
|
||||
@@ -0,0 +1,658 @@
|
||||
<template>
|
||||
<view class="uni-file-picker">
|
||||
<view v-if="title" class="uni-file-picker__header">
|
||||
<text class="file-title">{{ title }}</text>
|
||||
<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
|
||||
</view>
|
||||
<upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly"
|
||||
:image-styles="imageStyles" :files-list="filesList" :limit="limitLength" :disablePreview="disablePreview"
|
||||
:delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
|
||||
<slot>
|
||||
<view class="icon-add"></view>
|
||||
<view class="icon-add rotate"></view>
|
||||
</slot>
|
||||
</upload-image>
|
||||
<upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly"
|
||||
:list-styles="listStyles" :files-list="filesList" :showType="showType" :delIcon="delIcon"
|
||||
@uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
|
||||
<slot><button type="primary" size="mini">选择文件</button></slot>
|
||||
</upload-file>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
chooseAndUploadFile,
|
||||
uploadCloudFiles
|
||||
} from './choose-and-upload-file.js'
|
||||
import {
|
||||
get_file_ext,
|
||||
get_extname,
|
||||
get_files_and_is_max,
|
||||
get_file_info,
|
||||
get_file_data
|
||||
} from './utils.js'
|
||||
import uploadImage from './upload-image.vue'
|
||||
import uploadFile from './upload-file.vue'
|
||||
let fileInput = null
|
||||
/**
|
||||
* FilePicker 文件选择上传
|
||||
* @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=4079
|
||||
* @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定
|
||||
* @property {Boolean} disabled = [true|false] 组件禁用
|
||||
* @value true 禁用
|
||||
* @value false 取消禁用
|
||||
* @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮
|
||||
* @value true 只读
|
||||
* @value false 取消只读
|
||||
* @property {String} return-type = [array|object] 限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖
|
||||
* @value array 规定 value 属性的类型为数组
|
||||
* @value object 规定 value 属性的类型为对象
|
||||
* @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效
|
||||
* @value true 禁用图片预览
|
||||
* @value false 取消禁用图片预览
|
||||
* @property {Boolean} del-icon = [true|false] 是否显示删除按钮
|
||||
* @value true 显示删除按钮
|
||||
* @value false 不显示删除按钮
|
||||
* @property {Boolean} auto-upload = [true|false] 是否自动上传,值为true则只触发@select,可自行上传
|
||||
* @value true 自动上传
|
||||
* @value false 取消自动上传
|
||||
* @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分
|
||||
* @property {String} title 组件标题,右侧显示上传计数
|
||||
* @property {String} mode = [list|grid] 选择文件后的文件列表样式
|
||||
* @value list 列表显示
|
||||
* @value grid 宫格显示
|
||||
* @property {String} file-mediatype = [image|video|all] 选择文件类型
|
||||
* @value image 只选择图片
|
||||
* @value video 只选择视频
|
||||
* @value all 选择所有文件
|
||||
* @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同
|
||||
* @property {Object} list-style mode:list 时的样式
|
||||
* @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同
|
||||
* @event {Function} select 选择文件后触发
|
||||
* @event {Function} progress 文件上传时触发
|
||||
* @event {Function} success 上传成功触发
|
||||
* @event {Function} fail 上传失败触发
|
||||
* @event {Function} delete 文件从列表移除时触发
|
||||
*/
|
||||
export default {
|
||||
name: 'uniFilePicker',
|
||||
components: {
|
||||
uploadImage,
|
||||
uploadFile
|
||||
},
|
||||
options: {
|
||||
virtualHost: true
|
||||
},
|
||||
emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Array, Object],
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: [Array, Object],
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disablePreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 自动上传
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 最大选择个数 ,h5只能限制单选或是多选
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9
|
||||
},
|
||||
// 列表样式 grid | list | list-card
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'grid'
|
||||
},
|
||||
// 选择文件类型 image/video/all
|
||||
fileMediatype: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
},
|
||||
// 文件类型筛选
|
||||
fileExtname: {
|
||||
type: [Array, String],
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
listStyles: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
// 是否显示边框
|
||||
border: true,
|
||||
// 是否显示分隔线
|
||||
dividline: true,
|
||||
// 线条样式
|
||||
borderStyle: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
imageStyles: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
returnType: {
|
||||
type: String,
|
||||
default: 'array'
|
||||
},
|
||||
sizeType: {
|
||||
type: Array,
|
||||
default () {
|
||||
return ['original', 'compressed']
|
||||
}
|
||||
},
|
||||
sourceType: {
|
||||
type: Array,
|
||||
default () {
|
||||
return ['album', 'camera']
|
||||
}
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
default: '' // 默认上传到 unicloud 内置存储 extStorage 扩展存储
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
localValue: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(newVal, oldVal) {
|
||||
this.setValue(newVal, oldVal)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
modelValue: {
|
||||
handler(newVal, oldVal) {
|
||||
this.setValue(newVal, oldVal)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filesList() {
|
||||
let files = []
|
||||
this.files.forEach(v => {
|
||||
files.push(v)
|
||||
})
|
||||
return files
|
||||
},
|
||||
showType() {
|
||||
if (this.fileMediatype === 'image') {
|
||||
return this.mode
|
||||
}
|
||||
return 'list'
|
||||
},
|
||||
limitLength() {
|
||||
if (this.returnType === 'object') {
|
||||
return 1
|
||||
}
|
||||
if (!this.limit) {
|
||||
return 1
|
||||
}
|
||||
if (this.limit >= 9) {
|
||||
return 9
|
||||
}
|
||||
return this.limit
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// TODO 兼容不开通服务空间的情况
|
||||
if (!(uniCloud.config && uniCloud.config.provider)) {
|
||||
this.noSpace = true
|
||||
uniCloud.chooseAndUploadFile = chooseAndUploadFile
|
||||
}
|
||||
this.form = this.getForm('uniForms')
|
||||
this.formItem = this.getForm('uniFormsItem')
|
||||
if (this.form && this.formItem) {
|
||||
if (this.formItem.name) {
|
||||
this.rename = this.formItem.name
|
||||
this.form.inputChildrens.push(this)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 公开用户使用,清空文件
|
||||
* @param {Object} index
|
||||
*/
|
||||
clearFiles(index) {
|
||||
if (index !== 0 && !index) {
|
||||
this.files = []
|
||||
this.$nextTick(() => {
|
||||
this.setEmit()
|
||||
})
|
||||
} else {
|
||||
this.files.splice(index, 1)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.setEmit()
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 公开用户使用,继续上传
|
||||
*/
|
||||
upload() {
|
||||
let files = []
|
||||
this.files.forEach((v, index) => {
|
||||
if (v.status === 'ready' || v.status === 'error') {
|
||||
files.push(Object.assign({}, v))
|
||||
}
|
||||
})
|
||||
return this.uploadFiles(files)
|
||||
},
|
||||
async setValue(newVal, oldVal) {
|
||||
const newData = async (v) => {
|
||||
const reg = /cloud:\/\/([\w.]+\/?)\S*/
|
||||
let url = ''
|
||||
if(v.fileID){
|
||||
url = v.fileID
|
||||
}else{
|
||||
url = v.url
|
||||
}
|
||||
if (reg.test(url)) {
|
||||
v.fileID = url
|
||||
v.url = await this.getTempFileURL(url)
|
||||
}
|
||||
if(v.url) v.path = v.url
|
||||
return v
|
||||
}
|
||||
if (this.returnType === 'object') {
|
||||
if (newVal) {
|
||||
await newData(newVal)
|
||||
} else {
|
||||
newVal = {}
|
||||
}
|
||||
} else {
|
||||
if (!newVal) newVal = []
|
||||
for(let i =0 ;i < newVal.length ;i++){
|
||||
let v = newVal[i]
|
||||
await newData(v)
|
||||
}
|
||||
}
|
||||
this.localValue = newVal
|
||||
if (this.form && this.formItem &&!this.is_reset) {
|
||||
this.is_reset = false
|
||||
this.formItem.setValue(this.localValue)
|
||||
}
|
||||
let filesData = Object.keys(newVal).length > 0 ? newVal : [];
|
||||
this.files = [].concat(filesData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件
|
||||
*/
|
||||
choose() {
|
||||
if (this.disabled) return
|
||||
if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType ===
|
||||
'array') {
|
||||
uni.showToast({
|
||||
title: `您最多选择 ${this.limitLength} 个文件`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.chooseFiles()
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件并上传
|
||||
*/
|
||||
chooseFiles() {
|
||||
const _extname = get_extname(this.fileExtname)
|
||||
// 获取后缀
|
||||
uniCloud
|
||||
.chooseAndUploadFile({
|
||||
type: this.fileMediatype,
|
||||
compressed: false,
|
||||
sizeType: this.sizeType,
|
||||
sourceType: this.sourceType,
|
||||
// TODO 如果为空,video 有问题
|
||||
extension: _extname.length > 0 ? _extname : undefined,
|
||||
count: this.limitLength - this.files.length, //默认9
|
||||
onChooseFile: this.chooseFileCallback,
|
||||
onUploadProgress: progressEvent => {
|
||||
this.setProgress(progressEvent, progressEvent.index)
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
this.setSuccessAndError(result.tempFiles)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('选择失败', err)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件回调
|
||||
* @param {Object} res
|
||||
*/
|
||||
async chooseFileCallback(res) {
|
||||
const _extname = get_extname(this.fileExtname)
|
||||
const is_one = (Number(this.limitLength) === 1 &&
|
||||
this.disablePreview &&
|
||||
!this.disabled) ||
|
||||
this.returnType === 'object'
|
||||
// 如果这有一个文件 ,需要清空本地缓存数据
|
||||
if (is_one) {
|
||||
this.files = []
|
||||
}
|
||||
|
||||
let {
|
||||
filePaths,
|
||||
files
|
||||
} = get_files_and_is_max(res, _extname)
|
||||
if (!(_extname && _extname.length > 0)) {
|
||||
filePaths = res.tempFilePaths
|
||||
files = res.tempFiles
|
||||
}
|
||||
|
||||
let currentData = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (this.limitLength - this.files.length <= 0) break
|
||||
files[i].uuid = Date.now()
|
||||
let filedata = await get_file_data(files[i], this.fileMediatype)
|
||||
filedata.progress = 0
|
||||
filedata.status = 'ready'
|
||||
this.files.push(filedata)
|
||||
currentData.push({
|
||||
...filedata,
|
||||
file: files[i]
|
||||
})
|
||||
}
|
||||
this.$emit('select', {
|
||||
tempFiles: currentData,
|
||||
tempFilePaths: filePaths
|
||||
})
|
||||
res.tempFiles = files
|
||||
// 停止自动上传
|
||||
if (!this.autoUpload || this.noSpace) {
|
||||
res.tempFiles = []
|
||||
}
|
||||
res.tempFiles.forEach((fileItem, index) => {
|
||||
this.provider && (fileItem.provider = this.provider);
|
||||
const fileNameSplit = fileItem.name.split('.')
|
||||
const ext = fileNameSplit.pop()
|
||||
const fileName = fileNameSplit.join('.').replace(/[\s\/\?<>\\:\*\|":]/g, '_')
|
||||
fileItem.cloudPath = fileName + '_' + Date.now() + '_' + index + '.' + ext
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 批传
|
||||
* @param {Object} e
|
||||
*/
|
||||
uploadFiles(files) {
|
||||
files = [].concat(files)
|
||||
return uploadCloudFiles.call(this, files, 5, res => {
|
||||
this.setProgress(res, res.index, true)
|
||||
})
|
||||
.then(result => {
|
||||
this.setSuccessAndError(result)
|
||||
return result;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 成功或失败
|
||||
*/
|
||||
async setSuccessAndError(res, fn) {
|
||||
let successData = []
|
||||
let errorData = []
|
||||
let tempFilePath = []
|
||||
let errorTempFilePath = []
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i]
|
||||
const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index
|
||||
|
||||
if (index === -1 || !this.files) break
|
||||
if (item.errMsg === 'request:fail') {
|
||||
this.files[index].url = item.path
|
||||
this.files[index].status = 'error'
|
||||
this.files[index].errMsg = item.errMsg
|
||||
// this.files[index].progress = -1
|
||||
errorData.push(this.files[index])
|
||||
errorTempFilePath.push(this.files[index].url)
|
||||
} else {
|
||||
this.files[index].errMsg = ''
|
||||
this.files[index].fileID = item.url
|
||||
const reg = /cloud:\/\/([\w.]+\/?)\S*/
|
||||
if (reg.test(item.url)) {
|
||||
this.files[index].url = await this.getTempFileURL(item.url)
|
||||
}else{
|
||||
this.files[index].url = item.url
|
||||
}
|
||||
|
||||
this.files[index].status = 'success'
|
||||
this.files[index].progress += 1
|
||||
successData.push(this.files[index])
|
||||
tempFilePath.push(this.files[index].fileID)
|
||||
}
|
||||
}
|
||||
|
||||
if (successData.length > 0) {
|
||||
this.setEmit()
|
||||
// 状态改变返回
|
||||
this.$emit('success', {
|
||||
tempFiles: this.backObject(successData),
|
||||
tempFilePaths: tempFilePath
|
||||
})
|
||||
}
|
||||
|
||||
if (errorData.length > 0) {
|
||||
this.$emit('fail', {
|
||||
tempFiles: this.backObject(errorData),
|
||||
tempFilePaths: errorTempFilePath
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取进度
|
||||
* @param {Object} progressEvent
|
||||
* @param {Object} index
|
||||
* @param {Object} type
|
||||
*/
|
||||
setProgress(progressEvent, index, type) {
|
||||
const fileLenth = this.files.length
|
||||
const percentNum = (index / fileLenth) * 100
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
let idx = index
|
||||
if (!type) {
|
||||
idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid)
|
||||
}
|
||||
if (idx === -1 || !this.files[idx]) return
|
||||
// fix by mehaotian 100 就会消失,-1 是为了让进度条消失
|
||||
this.files[idx].progress = percentCompleted - 1
|
||||
// 上传中
|
||||
this.$emit('progress', {
|
||||
index: idx,
|
||||
progress: parseInt(percentCompleted),
|
||||
tempFile: this.files[idx]
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {Object} index
|
||||
*/
|
||||
delFile(index) {
|
||||
this.$emit('delete', {
|
||||
index,
|
||||
tempFile: this.files[index],
|
||||
tempFilePath: this.files[index].url
|
||||
})
|
||||
this.files.splice(index, 1)
|
||||
this.$nextTick(() => {
|
||||
this.setEmit()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件名和后缀
|
||||
* @param {Object} name
|
||||
*/
|
||||
getFileExt(name) {
|
||||
const last_len = name.lastIndexOf('.')
|
||||
const len = name.length
|
||||
return {
|
||||
name: name.substring(0, last_len),
|
||||
ext: name.substring(last_len + 1, len)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理返回事件
|
||||
*/
|
||||
setEmit() {
|
||||
let data = []
|
||||
if (this.returnType === 'object') {
|
||||
data = this.backObject(this.files)[0]
|
||||
this.localValue = data?data:null
|
||||
} else {
|
||||
data = this.backObject(this.files)
|
||||
if (!this.localValue) {
|
||||
this.localValue = []
|
||||
}
|
||||
this.localValue = [...data]
|
||||
}
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:modelValue', this.localValue)
|
||||
// #endif
|
||||
// #ifndef VUE3
|
||||
this.$emit('input', this.localValue)
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理返回参数
|
||||
* @param {Object} files
|
||||
*/
|
||||
backObject(files) {
|
||||
let newFilesData = []
|
||||
files.forEach(v => {
|
||||
newFilesData.push({
|
||||
extname: v.extname,
|
||||
fileType: v.fileType,
|
||||
image: v.image,
|
||||
name: v.name,
|
||||
path: v.path,
|
||||
size: v.size,
|
||||
fileID:v.fileID,
|
||||
url: v.url,
|
||||
// 修改删除一个文件后不能再上传的bug, #694
|
||||
uuid: v.uuid,
|
||||
status: v.status,
|
||||
cloudPath: v.cloudPath
|
||||
})
|
||||
})
|
||||
return newFilesData
|
||||
},
|
||||
async getTempFileURL(fileList) {
|
||||
fileList = {
|
||||
fileList: [].concat(fileList)
|
||||
}
|
||||
const urls = await uniCloud.getTempFileURL(fileList)
|
||||
return urls.fileList[0].tempFileURL || ''
|
||||
},
|
||||
/**
|
||||
* 获取父元素实例
|
||||
*/
|
||||
getForm(name = 'uniForms') {
|
||||
let parent = this.$parent;
|
||||
let parentName = parent.$options.name;
|
||||
while (parentName !== name) {
|
||||
parent = parent.$parent;
|
||||
if (!parent) return false;
|
||||
parentName = parent.$options.name;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uni-file-picker {
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.uni-file-picker__header {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 10px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-count {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.icon-add {
|
||||
width: 50px;
|
||||
height: 5px;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<view class="uni-file-picker__files">
|
||||
<view v-if="!readonly" class="files-button" @click="choose">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- :class="{'is-text-box':showType === 'list'}" -->
|
||||
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
|
||||
<!-- ,'is-list-card':showType === 'list-card' -->
|
||||
|
||||
<view class="uni-file-picker__lists-box" v-for="(item ,index) in list" :key="index" :class="{
|
||||
'files-border':index !== 0 && styles.dividline}"
|
||||
:style="index !== 0 && styles.dividline &&borderLineStyle">
|
||||
<view class="uni-file-picker__item">
|
||||
<!-- :class="{'is-text-image':showType === 'list'}" -->
|
||||
<!-- <view class="files__image is-text-image">
|
||||
<image class="header-image" :src="item.logo" mode="aspectFit"></image>
|
||||
</view> -->
|
||||
<view class="files__name">{{item.name}}</view>
|
||||
<view v-if="delIcon&&!readonly" class="icon-del-box icon-files" @click="delFile(index)">
|
||||
<view class="icon-del icon-files"></view>
|
||||
<view class="icon-del rotate"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
|
||||
<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
|
||||
:backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
|
||||
</view>
|
||||
<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
|
||||
点击重试
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "uploadFile",
|
||||
emits:['uploadFiles','choose','delFile'],
|
||||
props: {
|
||||
filesList: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9
|
||||
},
|
||||
showType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
listStyles: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
// 是否显示边框
|
||||
border: true,
|
||||
// 是否显示分隔线
|
||||
dividline: true,
|
||||
// 线条样式
|
||||
borderStyle: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
readonly:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
let files = []
|
||||
this.filesList.forEach(v => {
|
||||
files.push(v)
|
||||
})
|
||||
return files
|
||||
},
|
||||
styles() {
|
||||
let styles = {
|
||||
border: true,
|
||||
dividline: true,
|
||||
'border-style': {}
|
||||
}
|
||||
return Object.assign(styles, this.listStyles)
|
||||
},
|
||||
borderStyle() {
|
||||
let {
|
||||
borderStyle,
|
||||
border
|
||||
} = this.styles
|
||||
let obj = {}
|
||||
if (!border) {
|
||||
obj.border = 'none'
|
||||
} else {
|
||||
let width = (borderStyle && borderStyle.width) || 1
|
||||
width = this.value2px(width)
|
||||
let radius = (borderStyle && borderStyle.radius) || 5
|
||||
radius = this.value2px(radius)
|
||||
obj = {
|
||||
'border-width': width,
|
||||
'border-style': (borderStyle && borderStyle.style) || 'solid',
|
||||
'border-color': (borderStyle && borderStyle.color) || '#eee',
|
||||
'border-radius': radius
|
||||
}
|
||||
}
|
||||
let classles = ''
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`
|
||||
}
|
||||
return classles
|
||||
},
|
||||
borderLineStyle() {
|
||||
let obj = {}
|
||||
let {
|
||||
borderStyle
|
||||
} = this.styles
|
||||
if (borderStyle && borderStyle.color) {
|
||||
obj['border-color'] = borderStyle.color
|
||||
}
|
||||
if (borderStyle && borderStyle.width) {
|
||||
let width = borderStyle && borderStyle.width || 1
|
||||
let style = borderStyle && borderStyle.style || 0
|
||||
if (typeof width === 'number') {
|
||||
width += 'px'
|
||||
} else {
|
||||
width = width.indexOf('px') ? width : width + 'px'
|
||||
}
|
||||
obj['border-width'] = width
|
||||
|
||||
if (typeof style === 'number') {
|
||||
style += 'px'
|
||||
} else {
|
||||
style = style.indexOf('px') ? style : style + 'px'
|
||||
}
|
||||
obj['border-top-style'] = style
|
||||
}
|
||||
let classles = ''
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`
|
||||
}
|
||||
return classles
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
uploadFiles(item, index) {
|
||||
this.$emit("uploadFiles", {
|
||||
item,
|
||||
index
|
||||
})
|
||||
},
|
||||
choose() {
|
||||
this.$emit("choose")
|
||||
},
|
||||
delFile(index) {
|
||||
this.$emit('delFile', index)
|
||||
},
|
||||
value2px(value) {
|
||||
if (typeof value === 'number') {
|
||||
value += 'px'
|
||||
} else {
|
||||
value = value.indexOf('px') !== -1 ? value : value + 'px'
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.uni-file-picker__files {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.files-button {
|
||||
// border: 1px red solid;
|
||||
}
|
||||
|
||||
.uni-file-picker__lists {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-picker__mask {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.uni-file-picker__lists-box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uni-file-picker__item {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
padding-right: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.files-border {
|
||||
border-top: 1px #eee solid;
|
||||
}
|
||||
|
||||
.files__name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 25px;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.icon-files {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: static;
|
||||
background-color: initial;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
// .icon-files .icon-del {
|
||||
// background-color: #333;
|
||||
// width: 12px;
|
||||
// height: 1px;
|
||||
// }
|
||||
|
||||
|
||||
.is-list-card {
|
||||
border: 1px #eee solid;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.files__image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-text-box {
|
||||
border: 1px #eee solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.is-text-image {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.icon-del-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
margin: auto 0;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0;
|
||||
right: 5px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
// border-radius: 50%;
|
||||
// background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-del {
|
||||
width: 15px;
|
||||
height: 1px;
|
||||
background-color: #333;
|
||||
// border-radius: 1px;
|
||||
}
|
||||
|
||||
/* #ifdef H5 */
|
||||
@media all and (min-width: 768px) {
|
||||
.uni-file-picker__files {
|
||||
max-width: 375px;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<view class="uni-file-picker__container">
|
||||
<view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle">
|
||||
<view class="file-picker__box-content" :style="borderStyle">
|
||||
<image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image>
|
||||
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
|
||||
<view class="icon-del"></view>
|
||||
<view class="icon-del rotate"></view>
|
||||
</view>
|
||||
<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress">
|
||||
<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4"
|
||||
:backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" />
|
||||
</view>
|
||||
<view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)">
|
||||
点击重试
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
|
||||
<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "uploadImage",
|
||||
emits:['uploadFiles','choose','delFile'],
|
||||
props: {
|
||||
filesList: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
disabled:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disablePreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9
|
||||
},
|
||||
imageStyles: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
border: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
readonly:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
let styles = {
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
border: {}
|
||||
}
|
||||
return Object.assign(styles, this.imageStyles)
|
||||
},
|
||||
boxStyle() {
|
||||
const {
|
||||
width = 'auto',
|
||||
height = 'auto'
|
||||
} = this.styles
|
||||
let obj = {}
|
||||
if (height === 'auto') {
|
||||
if (width !== 'auto') {
|
||||
obj.height = this.value2px(width)
|
||||
obj['padding-top'] = 0
|
||||
} else {
|
||||
obj.height = 0
|
||||
}
|
||||
} else {
|
||||
obj.height = this.value2px(height)
|
||||
obj['padding-top'] = 0
|
||||
}
|
||||
|
||||
if (width === 'auto') {
|
||||
if (height !== 'auto') {
|
||||
obj.width = this.value2px(height)
|
||||
} else {
|
||||
obj.width = '33.3%'
|
||||
}
|
||||
} else {
|
||||
obj.width = this.value2px(width)
|
||||
}
|
||||
|
||||
let classles = ''
|
||||
for(let i in obj){
|
||||
classles+= `${i}:${obj[i]};`
|
||||
}
|
||||
return classles
|
||||
},
|
||||
borderStyle() {
|
||||
let {
|
||||
border
|
||||
} = this.styles
|
||||
let obj = {}
|
||||
const widthDefaultValue = 1
|
||||
const radiusDefaultValue = 3
|
||||
if (typeof border === 'boolean') {
|
||||
obj.border = border ? '1px #eee solid' : 'none'
|
||||
} else {
|
||||
let width = (border && border.width) || widthDefaultValue
|
||||
width = this.value2px(width)
|
||||
let radius = (border && border.radius) || radiusDefaultValue
|
||||
radius = this.value2px(radius)
|
||||
obj = {
|
||||
'border-width': width,
|
||||
'border-style': (border && border.style) || 'solid',
|
||||
'border-color': (border && border.color) || '#eee',
|
||||
'border-radius': radius
|
||||
}
|
||||
}
|
||||
let classles = ''
|
||||
for(let i in obj){
|
||||
classles+= `${i}:${obj[i]};`
|
||||
}
|
||||
return classles
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uploadFiles(item, index) {
|
||||
this.$emit("uploadFiles", item)
|
||||
},
|
||||
choose() {
|
||||
this.$emit("choose")
|
||||
},
|
||||
delFile(index) {
|
||||
this.$emit('delFile', index)
|
||||
},
|
||||
prviewImage(img, index) {
|
||||
let urls = []
|
||||
if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){
|
||||
this.$emit("choose")
|
||||
}
|
||||
if(this.disablePreview) return
|
||||
this.filesList.forEach(i => {
|
||||
urls.push(i.url)
|
||||
})
|
||||
|
||||
uni.previewImage({
|
||||
urls: urls,
|
||||
current: index
|
||||
});
|
||||
},
|
||||
value2px(value) {
|
||||
if (typeof value === 'number') {
|
||||
value += 'px'
|
||||
} else {
|
||||
if (value.indexOf('%') === -1) {
|
||||
value = value.indexOf('px') !== -1 ? value : value + 'px'
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.uni-file-picker__container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
flex-wrap: wrap;
|
||||
margin: -5px;
|
||||
}
|
||||
|
||||
.file-picker__box {
|
||||
position: relative;
|
||||
// flex: 0 0 33.3%;
|
||||
width: 33.3%;
|
||||
height: 0;
|
||||
padding-top: 33.33%;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.file-picker__box-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 5px;
|
||||
border: 1px #eee solid;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-picker__progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* border: 1px red solid; */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-picker__progress-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-picker__mask {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.file-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-add {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.icon-del-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-del {
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
110
uni_modules/uni-file-picker/components/uni-file-picker/utils.js
Normal file
110
uni_modules/uni-file-picker/components/uni-file-picker/utils.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 获取文件名和后缀
|
||||
* @param {String} name
|
||||
*/
|
||||
export const get_file_ext = (name) => {
|
||||
const last_len = name.lastIndexOf('.')
|
||||
const len = name.length
|
||||
return {
|
||||
name: name.substring(0, last_len),
|
||||
ext: name.substring(last_len + 1, len)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展名
|
||||
* @param {Array} fileExtname
|
||||
*/
|
||||
export const get_extname = (fileExtname) => {
|
||||
if (!Array.isArray(fileExtname)) {
|
||||
let extname = fileExtname.replace(/(\[|\])/g, '')
|
||||
return extname.split(',')
|
||||
} else {
|
||||
return fileExtname
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件和检测是否可选
|
||||
*/
|
||||
export const get_files_and_is_max = (res, _extname) => {
|
||||
let filePaths = []
|
||||
let files = []
|
||||
if(!_extname || _extname.length === 0){
|
||||
return {
|
||||
filePaths,
|
||||
files
|
||||
}
|
||||
}
|
||||
res.tempFiles.forEach(v => {
|
||||
let fileFullName = get_file_ext(v.name)
|
||||
const extname = fileFullName.ext.toLowerCase()
|
||||
if (_extname.indexOf(extname) !== -1) {
|
||||
files.push(v)
|
||||
filePaths.push(v.path)
|
||||
}
|
||||
})
|
||||
if (files.length !== res.tempFiles.length) {
|
||||
uni.showToast({
|
||||
title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`,
|
||||
icon: 'none',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
filePaths,
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取图片信息
|
||||
* @param {Object} filepath
|
||||
*/
|
||||
export const get_file_info = (filepath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src: filepath,
|
||||
success(res) {
|
||||
resolve(res)
|
||||
},
|
||||
fail(err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取封装数据
|
||||
*/
|
||||
export const get_file_data = async (files, type = 'image') => {
|
||||
// 最终需要上传数据库的数据
|
||||
let fileFullName = get_file_ext(files.name)
|
||||
const extname = fileFullName.ext.toLowerCase()
|
||||
let filedata = {
|
||||
name: files.name,
|
||||
uuid: files.uuid,
|
||||
extname: extname || '',
|
||||
cloudPath: files.cloudPath,
|
||||
fileType: files.fileType,
|
||||
thumbTempFilePath: files.thumbTempFilePath,
|
||||
url: files.path || files.path,
|
||||
size: files.size, //单位是字节
|
||||
image: {},
|
||||
path: files.path,
|
||||
video: {}
|
||||
}
|
||||
if (type === 'image') {
|
||||
const imageinfo = await get_file_info(files.path)
|
||||
delete filedata.video
|
||||
filedata.image.width = imageinfo.width
|
||||
filedata.image.height = imageinfo.height
|
||||
filedata.image.location = imageinfo.path
|
||||
} else {
|
||||
delete filedata.image
|
||||
}
|
||||
return filedata
|
||||
}
|
||||
86
uni_modules/uni-file-picker/package.json
Normal file
86
uni_modules/uni-file-picker/package.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "uni-file-picker",
|
||||
"displayName": "uni-file-picker 文件选择上传",
|
||||
"version": "1.0.12",
|
||||
"description": "文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"图片上传",
|
||||
"文件上传"
|
||||
],
|
||||
"repository": "https://github.com/dcloudio/uni-ui",
|
||||
"engines": {
|
||||
"HBuilderX": ""
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
|
||||
"type": "component-vue"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": ["uni-scss"],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "n",
|
||||
"app-harmony": "u",
|
||||
"app-uvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "y",
|
||||
"百度": "y",
|
||||
"字节跳动": "y",
|
||||
"QQ": "y"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
},
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
uni_modules/uni-file-picker/readme.md
Normal file
11
uni_modules/uni-file-picker/readme.md
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
## FilePicker 文件选择上传
|
||||
|
||||
> **组件名:uni-file-picker**
|
||||
> 代码块: `uFilePicker`
|
||||
|
||||
|
||||
文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
|
||||
|
||||
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-file-picker)
|
||||
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839
|
||||
Reference in New Issue
Block a user