小程序使用聊天文件
微信小程序提供了访问聊天文件的能力,允许用户选择聊天记录中的文件并进行处理。这个功能主要基于 wx.chooseMessageFileAPI,可以访问用户在不同聊天会话中发送和接收的文件。
主要特性:
1, 文件类型支持
文档文件:pdf, doc, docx, ppt, pptx, xls, xlsx
图片文件:jpg, png, gif, bmp
音频文件:mp3, aac, wav, m4a
视频文件:mp4, mov, avi
压缩文件:zip, rar, 7z
其他文件:txt, json, xml 等
2, 访问权限
需要用户主动选择
只能访问用户明确选择的文件
保护用户隐私安全
3, 文件处理
获取文件临时路径
读取文件内容
保存到本地存储
上传到服务器
这是一个 JS 文件示例:
// pages/chat-files/chat-files.js
Page({
data: {
// 选择的文件列表
fileList: [],
// 文件类型筛选
fileType: 'all', // all, image, video, audio, document, other
// 排序方式
sortBy: 'time', // time, name, size, type
// 排序顺序
sortOrder: 'desc', // asc, desc
// 搜索关键词
searchKeyword: '',
// 加载状态
isLoading: false,
// 选中的文件索引
selectedFiles: [],
// 文件统计信息
fileStats: {
totalCount: 0,
totalSize: 0,
imageCount: 0,
videoCount: 0,
audioCount: 0,
documentCount: 0,
otherCount: 0
},
// 当前操作状态
currentOperation: '',
// 操作进度
operationProgress: 0,
// 错误信息
errorMsg: ''
},
onLoad() {
console.log('聊天文件页面加载');
this.loadFileListFromStorage();
this.checkFileSystemPermission();
},
onShow() {
// 页面显示时刷新文件列表
this.updateFileStats();
},
/**
* 检查文件系统权限
*/
checkFileSystemPermission() {
wx.getSetting({
success: (res) => {
if (!res.authSetting['scope.writePhotosAlbum']) {
console.log('未授权文件保存权限');
}
}
});
},
/**
* 从本地存储加载文件列表
*/
loadFileListFromStorage() {
try {
const savedFiles = wx.getStorageSync('chatFiles') || [];
this.setData({
fileList: savedFiles
});
this.updateFileStats();
console.log('文件列表加载成功,共', savedFiles.length, '个文件');
} catch (error) {
console.error('加载文件列表失败:', error);
this.setData({
errorMsg: '加载文件列表失败'
});
}
},
/**
* 保存文件列表到本地存储
*/
saveFileListToStorage() {
try {
wx.setStorageSync('chatFiles', this.data.fileList);
console.log('文件列表保存成功');
} catch (error) {
console.error('保存文件列表失败:', error);
this.setData({
errorMsg: '保存文件列表失败'
});
}
},
/**
* 选择聊天文件
*/
chooseChatFiles() {
this.setData({
isLoading: true,
currentOperation: '选择文件中...'
});
wx.chooseMessageFile({
count: 20, // 最多选择20个文件
type: 'all', // 所有类型文件
success: (res) => {
console.log('选择聊天文件成功', res);
const tempFiles = res.tempFiles;
const newFiles = tempFiles.map((file, index) => {
const fileInfo = this.analyzeFile(file);
return {
id: Date.now() + index,
tempFilePath: file.path,
name: file.name,
size: file.size,
type: fileInfo.type,
format: fileInfo.format,
createTime: new Date().toLocaleString(),
lastModified: Date.now(),
isSaved: false,
savedPath: '',
thumbnail: fileInfo.thumbnail,
...fileInfo
};
});
// 去重处理
const existingNames = new Set(this.data.fileList.map(f => f.name));
const uniqueNewFiles = newFiles.filter(file => !existingNames.has(file.name));
this.setData({
fileList: [...this.data.fileList, ...uniqueNewFiles],
isLoading: false,
currentOperation: '',
errorMsg: ''
});
this.saveFileListToStorage();
this.updateFileStats();
wx.showToast({
title: `成功选择${uniqueNewFiles.length}个文件`,
icon: 'success',
duration: 2000
});
},
fail: (err) => {
console.error('选择聊天文件失败', err);
this.setData({
isLoading: false,
currentOperation: '',
errorMsg: this.getChooseFileErrorMsg(err)
});
this.handleChooseFileError(err);
}
});
},
/**
* 按类型选择文件
*/
chooseFilesByType(fileType) {
const typeMap = {
'image': 'image',
'video': 'video',
'audio': 'file',
'document': 'file',
'all': 'all'
};
this.setData({
isLoading: true,
currentOperation: `选择${this.getTypeName(fileType)}文件中...`
});
wx.chooseMessageFile({
count: 10,
type: typeMap[fileType] || 'all',
success: (res) => {
const tempFiles = res.tempFiles;
const filteredFiles = tempFiles.filter(file => {
if (fileType === 'all') return true;
return this.getFileType(file.name) === fileType;
});
const newFiles = filteredFiles.map((file, index) => {
const fileInfo = this.analyzeFile(file);
return {
id: Date.now() + index,
tempFilePath: file.path,
name: file.name,
size: file.size,
type: fileInfo.type,
format: fileInfo.format,
createTime: new Date().toLocaleString(),
lastModified: Date.now(),
isSaved: false,
savedPath: '',
thumbnail: fileInfo.thumbnail,
...fileInfo
};
});
this.addFilesToLibrary(newFiles);
},
fail: (err) => {
this.handleChooseFileError(err);
}
});
},
/**
* 分析文件信息
*/
analyzeFile(file) {
const fileName = file.name.toLowerCase();
const fileExtension = fileName.split('.').pop();
const fileSize = file.size;
// 判断文件类型
const fileType = this.getFileType(fileName);
const format = fileExtension.toUpperCase();
// 生成缩略图(对于图片和视频)
let thumbnail = '';
if (fileType === 'image') {
thumbnail = file.path; // 图片直接使用原图作为缩略图
} else if (fileType === 'video' && file.path) {
// 视频文件可以生成缩略图(需要额外处理)
thumbnail = '/images/video-thumbnail.png';
}
return {
type: fileType,
format: format,
thumbnail: thumbnail,
extension: fileExtension
};
},
/**
* 获取文件类型
*/
getFileType(fileName) {
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const videoExtensions = ['mp4', 'mov', 'avi', 'mkv', 'wmv', 'flv'];
const audioExtensions = ['mp3', 'wav', 'aac', 'm4a', 'flac', 'ogg'];
const documentExtensions = ['pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'txt'];
const extension = fileName.split('.').pop().toLowerCase();
if (imageExtensions.includes(extension)) return 'image';
if (videoExtensions.includes(extension)) return 'video';
if (audioExtensions.includes(extension)) return 'audio';
if (documentExtensions.includes(extension)) return 'document';
return 'other';
},
/**
* 获取类型名称
*/
getTypeName(type) {
const nameMap = {
'image': '图片',
'video': '视频',
'audio': '音频',
'document': '文档',
'other': '其他',
'all': '所有'
};
return nameMap[type] || '文件';
},
/**
* 添加文件到库
*/
addFilesToLibrary(newFiles) {
if (newFiles.length === 0) {
this.setData({
isLoading: false,
currentOperation: ''
});
wx.showToast({
title: '没有选择有效的文件',
icon: 'none'
});
return;
}
// 去重处理
const existingNames = new Set(this.data.fileList.map(f => f.name));
const uniqueNewFiles = newFiles.filter(file => !existingNames.has(file.name));
this.setData({
fileList: [...this.data.fileList, ...uniqueNewFiles],
isLoading: false,
currentOperation: ''
});
this.saveFileListToStorage();
this.updateFileStats();
wx.showToast({
title: `成功添加${uniqueNewFiles.length}个文件`,
icon: 'success',
duration: 2000
});
},
/**
* 处理选择文件错误
*/
handleChooseFileError(err) {
this.setData({
isLoading: false,
currentOperation: ''
});
const errorMsg = this.getChooseFileErrorMsg(err);
this.setData({ errorMsg });
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
},
/**
* 获取选择文件错误信息
*/
getChooseFileErrorMsg(err) {
const errorMap = {
'chooseMessageFile:fail cancel': '用户取消了选择',
'chooseMessageFile:fail auth deny': '没有文件访问权限',
'chooseMessageFile:fail no message': '没有找到聊天文件',
'chooseMessageFile:fail file type not support': '不支持的文件类型',
'chooseMessageFile:fail file size exceed': '文件大小超限'
};
return errorMap[err.errMsg] || `选择文件失败: ${err.errMsg}`;
},
/**
* 保存文件到本地
*/
saveFileToLocal(fileIndex) {
const file = this.data.fileList[fileIndex];
if (!file) {
wx.showToast({
title: '文件不存在',
icon: 'none'
});
return;
}
if (file.isSaved) {
wx.showToast({
title: '文件已保存',
icon: 'none'
});
return;
}
this.setData({
currentOperation: '保存文件中...',
operationProgress: 0
});
// 根据文件类型选择保存方法
if (file.type === 'image') {
this.saveImageFile(file, fileIndex);
} else if (file.type === 'video') {
this.saveVideoFile(file, fileIndex);
} else {
this.saveGeneralFile(file, fileIndex);
}
},
/**
* 保存图片文件
*/
saveImageFile(file, fileIndex) {
wx.saveImageToPhotosAlbum({
filePath: file.tempFilePath,
success: () => {
this.markFileAsSaved(fileIndex, '相册');
wx.showToast({
title: '图片保存成功',
icon: 'success'
});
},
fail: (err) => {
this.handleSaveFileError(err, fileIndex);
}
});
},
/**
* 保存视频文件
*/
saveVideoFile(file, fileIndex) {
wx.saveVideoToPhotosAlbum({
filePath: file.tempFilePath,
success: () => {
this.markFileAsSaved(fileIndex, '相册');
wx.showToast({
title: '视频保存成功',
icon: 'success'
});
},
fail: (err) => {
this.handleSaveFileError(err, fileIndex);
}
});
},
/**
* 保存普通文件
*/
saveGeneralFile(file, fileIndex) {
wx.saveFile({
tempFilePath: file.tempFilePath,
success: (res) => {
this.markFileAsSaved(fileIndex, res.savedFilePath);
wx.showToast({
title: '文件保存成功',
icon: 'success'
});
},
fail: (err) => {
this.handleSaveFileError(err, fileIndex);
}
});
},
/**
* 标记文件为已保存
*/
markFileAsSaved(fileIndex, savedPath) {
const newFileList = [...this.data.fileList];
newFileList[fileIndex] = {
...newFileList[fileIndex],
isSaved: true,
savedPath: savedPath,
savedTime: new Date().toLocaleString()
};
this.setData({
fileList: newFileList,
currentOperation: '',
operationProgress: 100
});
this.saveFileListToStorage();
// 进度条动画
setTimeout(() => {
this.setData({ operationProgress: 0 });
}, 1000);
},
/**
* 处理保存文件错误
*/
handleSaveFileError(err, fileIndex) {
console.error('保存文件失败:', err);
this.setData({
currentOperation: '',
operationProgress: 0,
errorMsg: this.getSaveFileErrorMsg(err)
});
wx.showToast({
title: '保存失败',
icon: 'none'
});
},
/**
* 获取保存文件错误信息
*/
getSaveFileErrorMsg(err) {
const errorMap = {
'saveImageToPhotosAlbum:fail auth deny': '没有相册写入权限',
'saveVideoToPhotosAlbum:fail auth deny': '没有相册写入权限',
'saveFile:fail file not exist': '文件不存在',
'saveFile:fail insufficient space': '存储空间不足'
};
return errorMap[err.errMsg] || `保存失败: ${err.errMsg}`;
},
/**
* 批量保存选中的文件
*/
saveSelectedFiles() {
const selectedIndices = this.data.selectedFiles;
if (selectedIndices.length === 0) {
wx.showToast({
title: '请先选择要保存的文件',
icon: 'none'
});
return;
}
this.setData({
currentOperation: '批量保存文件中...',
operationProgress: 0
});
this.batchSaveFiles(selectedIndices);
},
/**
* 批量保存文件
*/
async batchSaveFiles(fileIndices) {
const total = fileIndices.length;
let successCount = 0;
let failCount = 0;
for (let i = 0; i < fileIndices.length; i++) {
const index = fileIndices[i];
const file = this.data.fileList[index];
if (file.isSaved) {
successCount++;
continue;
}
try {
await this.saveSingleFile(file, index);
successCount++;
} catch (error) {
failCount++;
console.error(`保存文件失败: ${file.name}`, error);
}
// 更新进度
const progress = Math.round((i + 1) / total * 100);
this.setData({
operationProgress: progress,
currentOperation: `保存文件中... (${i + 1}/${total})`
});
}
this.setData({
currentOperation: '',
selectedFiles: []
});
this.showBatchSaveResult(successCount, failCount);
},
/**
* 保存单个文件(Promise封装)
*/
saveSingleFile(file, fileIndex) {
return new Promise((resolve, reject) => {
const saveMethod = file.type === 'image' ? 'saveImageToPhotosAlbum' :
file.type === 'video' ? 'saveVideoToPhotosAlbum' : 'saveFile';
if (saveMethod === 'saveFile') {
wx.saveFile({
tempFilePath: file.tempFilePath,
success: (res) => {
this.markFileAsSaved(fileIndex, res.savedFilePath);
resolve();
},
fail: reject
});
} else {
const api = saveMethod === 'saveImageToPhotosAlbum' ?
wx.saveImageToPhotosAlbum : wx.saveVideoToPhotosAlbum;
api({
filePath: file.tempFilePath,
success: () => {
this.markFileAsSaved(fileIndex, '相册');
resolve();
},
fail: reject
});
}
});
},
/**
* 显示批量保存结果
*/
showBatchSaveResult(successCount, failCount) {
let title = '';
if (failCount === 0) {
title = `成功保存${successCount}个文件`;
} else if (successCount === 0) {
title = `保存失败,${failCount}个文件未保存`;
} else {
title = `成功${successCount}个,失败${failCount}个`;
}
wx.showModal({
title: '保存结果',
content: title,
showCancel: false,
success: () => {
this.setData({ selectedFiles: [] });
}
});
},
/**
* 选择/取消选择文件
*/
toggleFileSelection(index) {
const selectedFiles = [...this.data.selectedFiles];
const currentIndex = selectedFiles.indexOf(index);
if (currentIndex > -1) {
selectedFiles.splice(currentIndex, 1);
} else {
selectedFiles.push(index);
}
this.setData({ selectedFiles });
},
/**
* 全选/取消全选
*/
toggleSelectAll() {
if (this.data.selectedFiles.length === this.data.fileList.length) {
this.setData({ selectedFiles: [] });
} else {
const allIndices = this.data.fileList.map((_, index) => index);
this.setData({ selectedFiles: allIndices });
}
},
/**
* 预览文件
*/
previewFile(index) {
const file = this.data.fileList[index];
if (!file) return;
switch (file.type) {
case 'image':
this.previewImage(file);
break;
case 'video':
this.previewVideo(file);
break;
case 'document':
this.previewDocument(file);
break;
default:
this.openFile(file);
}
},
/**
* 预览图片
*/
previewImage(file) {
wx.previewImage({
urls: [file.tempFilePath],
current: file.tempFilePath,
success: () => {
console.log('图片预览成功');
},
fail: (err) => {
console.error('图片预览失败', err);
wx.showToast({
title: '预览失败',
icon: 'none'
});
}
});
},
/**
* 预览视频
*/
previewVideo(file) {
// 使用视频播放器预览
wx.navigateTo({
url: `/pages/video-player/video-player?url=${encodeURIComponent(file.tempFilePath)}&title=${encodeURIComponent(file.name)}`
});
},
/**
* 预览文档
*/
previewDocument(file) {
// 文档预览需要借助其他服务或直接打开
this.openFile(file);
},
/**
* 打开文件
*/
openFile(file) {
wx.openDocument({
filePath: file.tempFilePath,
fileType: file.extension,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败', err);
wx.showToast({
title: '无法打开此文件类型',
icon: 'none'
});
}
});
},
/**
* 删除文件
*/
deleteFile(index) {
const file = this.data.fileList[index];
if (!file) return;
wx.showModal({
title: '确认删除',
content: `确定要删除"${file.name}"吗?`,
success: (res) => {
if (res.confirm) {
const newFileList = this.data.fileList.filter((_, i) => i !== index);
this.setData({ fileList: newFileList });
this.saveFileListToStorage();
this.updateFileStats();
// 从选中列表中移除
const selectedFiles = this.data.selectedFiles.filter(i => i !== index)
.map(i => i > index ? i - 1 : i); // 调整索引
this.setData({ selectedFiles });
wx.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
},
/**
* 批量删除文件
*/
deleteSelectedFiles() {
const selectedIndices = this.data.selectedFiles;
if (selectedIndices.length === 0) {
wx.showToast({
title: '请先选择要删除的文件',
icon: 'none'
});
return;
}
const fileNames = selectedIndices.map(i => this.data.fileList[i].name).join('、');
wx.showModal({
title: '确认删除',
content: `确定要删除选中的${selectedIndices.length}个文件吗?`,
success: (res) => {
if (res.confirm) {
// 从大到小排序索引,避免删除时索引变化
const sortedIndices = [...selectedIndices].sort((a, b) => b - a);
let newFileList = [...this.data.fileList];
sortedIndices.forEach(index => {
newFileList.splice(index, 1);
});
this.setData({
fileList: newFileList,
selectedFiles: []
});
this.saveFileListToStorage();
this.updateFileStats();
wx.showToast({
title: `成功删除${selectedIndices.length}个文件`,
icon: 'success'
});
}
}
});
},
/**
* 清空所有文件
*/
clearAllFiles() {
if (this.data.fileList.length === 0) return;
wx.showModal({
title: '确认清空',
content: '确定要清空所有文件吗?此操作不可撤销',
success: (res) => {
if (res.confirm) {
this.setData({
fileList: [],
selectedFiles: [],
fileStats: this.getEmptyFileStats()
});
this.saveFileListToStorage();
wx.showToast({
title: '清空成功',
icon: 'success'
});
}
}
});
},
/**
* 更新文件统计信息
*/
updateFileStats() {
const stats = this.getEmptyFileStats();
this.data.fileList.forEach(file => {
stats.totalCount++;
stats.totalSize += file.size || 0;
switch (file.type) {
case 'image': stats.imageCount++; break;
case 'video': stats.videoCount++; break;
case 'audio': stats.audioCount++; break;
case 'document': stats.documentCount++; break;
default: stats.otherCount++; break;
}
});
this.setData({ fileStats: stats });
},
/**
* 获取空文件统计
*/
getEmptyFileStats() {
return {
totalCount: 0,
totalSize: 0,
imageCount: 0,
videoCount: 0,
audioCount: 0,
documentCount: 0,
otherCount: 0
};
},
/**
* 筛选文件
*/
filterFiles(type) {
this.setData({ fileType: type });
},
/**
* 搜索文件
*/
onSearchInput(e) {
this.setData({ searchKeyword: e.detail.value });
},
/**
* 排序文件
*/
sortFiles(sortBy, sortOrder) {
this.setData({
sortBy: sortBy,
sortOrder: sortOrder
});
},
/**
* 获取筛选后的文件列表
*/
getFilteredFiles() {
let files = this.data.fileList;
// 类型筛选
if (this.data.fileType !== 'all') {
files = files.filter(file => file.type === this.data.fileType);
}
// 搜索筛选
if (this.data.searchKeyword) {
const keyword = this.data.searchKeyword.toLowerCase();
files = files.filter(file =>
file.name.toLowerCase().includes(keyword)
);
}
// 排序
files.sort((a, b) => {
let aValue, bValue;
switch (this.data.sortBy) {
case 'name':
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case 'size':
aValue = a.size;
bValue = b.size;
break;
case 'type':
aValue = a.type;
bValue = b.type;
break;
case 'time':
default:
aValue = a.lastModified;
bValue = b.lastModified;
break;
}
if (this.data.sortOrder === 'asc') {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
}
});
return files;
},
/**
* 分享文件
*/
shareFile(index) {
const file = this.data.fileList[index];
if (!file) return;
wx.showShareMenu({
withShareTicket: true,
success: () => {
console.log('分享菜单显示成功');
}
});
},
/**
* 上传文件到服务器
*/
uploadFileToServer(index) {
const file = this.data.fileList[index];
if (!file) return;
wx.uploadFile({
url: 'https://your-server.com/upload', // 替换为实际的上传地址
filePath: file.tempFilePath,
name: 'file',
formData: {
'fileName': file.name,
'fileType': file.type
},
success: (res) => {
console.log('文件上传成功', res);
wx.showToast({
title: '上传成功',
icon: 'success'
});
},
fail: (err) => {
console.error('文件上传失败', err);
wx.showToast({
title: '上传失败',
icon: 'none'
});
}
});
},
/**
* 获取文件信息
*/
getFileInfo(index) {
const file = this.data.fileList[index];
if (!file) return null;
return {
name: file.name,
size: this.formatFileSize(file.size),
type: this.getTypeName(file.type),
format: file.format,
createTime: file.createTime,
isSaved: file.isSaved,
savedPath: file.savedPath
};
},
/**
* 格式化文件大小
*/
formatFileSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* 导出文件列表
*/
exportFileList() {
const fileList = this.data.fileList.map(file => ({
name: file.name,
type: file.type,
size: file.size,
format: file.format,
createTime: file.createTime,
isSaved: file.isSaved
}));
// 这里可以生成CSV或JSON文件供下载
const content = JSON.stringify(fileList, null, 2);
console.log('文件列表导出:', content);
wx.showToast({
title: '文件列表已生成',
icon: 'success'
});
},
/**
* 页面分享
*/
onShareAppMessage() {
return {
title: '我的聊天文件库',
path: '/pages/chat-files/chat-files'
};
}
});
主要功能说明
- 文件选择功能
chooseChatFiles(): 选择聊天文件
chooseFilesByType(): 按类型选择文件
支持多种文件格式和类型筛选
自动去重和文件信息分析
- 文件保存功能
saveFileToLocal(): 保存单个文件
saveSelectedFiles(): 批量保存文件
根据文件类型使用不同的保存 API
支持图片、视频、文档等各类文件
- 文件管理功能
deleteFile(): 删除单个文件
deleteSelectedFiles(): 批量删除
clearAllFiles(): 清空所有文件
本地存储管理
- 文件预览功能
previewFile(): 文件预览
previewImage(): 图片预览
previewVideo(): 视频预览
openFile(): 打开文档
- 文件筛选和搜索
按类型筛选(图片、视频、音频、文档等)
关键词搜索
多种排序方式(时间、名称、大小、类型)
- 批量操作
文件多选功能
批量保存
批量删除
操作进度显示
- 统计信息
文件数量统计
文件大小统计
各类型文件统计
实时更新
- 错误处理
完善的错误处理机制
友好的错误提示
权限检查和引导
这个示例提供了完整的聊天文件处理功能,包括文件选择、保存、管理、预览等操作,可以直接在小程序项目中使用。
通关密语:聊天文件