hookehuyr

refactor(utils): 提取统一的文件扩展名判断函数

- 新增 extractExtensionFromFile() 统一函数
- 优先使用 extension 字段
- 更新 documentIcons.js/tools.js/useFileOperation.js 使用统一函数
- 更新 API 类型定义和组件

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -40,6 +40,7 @@ const Api = {
file_name: string; // 附件名
file_size: string; // 附件大小
file_size_formatted: string; // 附件大小(转换过显示)
extension?: string; // 文件扩展名(优先使用)
}>;
cover_image: string; // 产品封面图
* };
......
......@@ -129,18 +129,24 @@ const emit = defineEmits({
/**
* 获取文档图标 URL
*
* @description 根据文件名获取对应的文档类型图标
* @description 优先使用 extension 字段,其次从 fileName 解析
* @returns {string} 图标 URL
*/
const iconUrl = props.fileName ? getDocumentIcon(props.fileName) : '';
const iconUrl = getDocumentIcon({
extension: props.extension,
fileName: props.fileName
});
/**
* 获取文档类型标签
*
* @description 根据文件名获取文档类型标签文本
* @description 优先使用 extension 字段,其次从 fileName 解析
* @returns {string} 文档类型标签
*/
const docTypeLabel = props.fileName ? getDocumentLabel(props.fileName) : '';
const docTypeLabel = getDocumentLabel({
extension: props.extension,
fileName: props.fileName
});
/**
* 使用收藏操作 composable
......
......@@ -10,6 +10,7 @@
import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro'
import { isVideoFile } from '@/utils/tools'
import { extractExtensionFromFile } from '@/utils/documentIcons'
/**
* 文件操作 Hook
......@@ -20,13 +21,18 @@ export function useFileOperation() {
/**
* 判断是否为图片文件
*
* @param {string} fileName - 文件名
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名或文件对象
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} 是否为图片文件
*/
const isImageFile = (fileName) => {
if (!fileName) return false
const isImageFile = (fileNameOrItem) => {
const extension = extractExtensionFromFile(fileNameOrItem)
if (!extension) return false
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
const extension = fileName.split('.').pop()?.toLowerCase() || ''
return imageExtensions.includes(extension)
}
/**
......@@ -36,12 +42,14 @@ export function useFileOperation() {
* @async
* @param {string} filePath - 本地文件路径
* @param {Object} item - 文件信息对象
* @param {string} item.fileName - 文件名(用于判断文件类型)
* @param {string} [item.fileName] - 文件名(用于判断文件类型)
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @returns {Promise<void>}
*
* @example
* const { openFile } = useFileOperation()
* await openFile(tempFilePath, { fileName: 'document.pdf' })
* await openFile(tempFilePath, { extension: 'pdf' }) // 优先使用 extension
*/
const openFile = async (filePath, item) => {
try {
......@@ -51,7 +59,8 @@ export function useFileOperation() {
success: () => {
console.log('文件打开成功')
// 文件打开后,延迟提示用户如果看不到内容该如何操作
const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''
// 使用统一的扩展名提取函数
const fileExt = extractExtensionFromFile(item)
const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']
if (unsupportedFormats.includes(fileExt)) {
......@@ -67,8 +76,8 @@ export function useFileOperation() {
fail: (err) => {
console.error('打开文件失败:', err)
// 获取文件扩展名
const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''
// 获取文件扩展名(使用统一的扩展名提取函数)
const fileExt = extractExtensionFromFile(item)
// 根据文件类型给出提示
let message = '文件打开失败'
......@@ -110,7 +119,8 @@ export function useFileOperation() {
* @async
* @param {Object} item - 文件信息对象
* @param {string} item.downloadUrl - 文件下载地址
* @param {string} item.fileName - 文件名
* @param {string} [item.fileName] - 文件名
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @returns {Promise<void>}
*
* @example
......@@ -119,10 +129,14 @@ export function useFileOperation() {
* downloadUrl: 'https://example.com/file.pdf',
* fileName: 'document.pdf'
* })
* await downloadAndOpenFile({
* downloadUrl: 'https://example.com/file.pdf',
* extension: 'pdf' // 优先使用 extension
* })
*/
const downloadAndOpenFile = async (item) => {
try {
// 下载文件
//下载文件
const downloadResult = await downloadFile({
url: item.downloadUrl
})
......@@ -139,8 +153,8 @@ export function useFileOperation() {
// 隐藏加载提示
hideLoading()
// 获取文件扩展名
const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''
// 获取文件扩展名(使用统一的扩展名提取函数)
const fileExt = extractExtensionFromFile(item)
// 微信小程序对 Office 文档支持有限,提前提示用户
const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']
......@@ -194,7 +208,8 @@ export function useFileOperation() {
* @async
* @param {Object} item - 文件信息对象
* @param {string} [item.downloadUrl] - 文件下载地址
* @param {string} item.fileName - 文件名
* @param {string} [item.fileName] - 文件名
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @returns {Promise<void>}
*
* @example
......@@ -203,6 +218,10 @@ export function useFileOperation() {
* downloadUrl: 'https://example.com/file.pdf',
* fileName: 'document.pdf'
* })
* await viewFile({
* downloadUrl: 'https://example.com/file.pdf',
* extension: 'pdf' // 优先使用 extension
* })
*/
const viewFile = async (item) => {
// 检查是否有下载地址
......@@ -215,8 +234,8 @@ export function useFileOperation() {
return
}
// 判断是否为图片文件
if (isImageFile(item.fileName)) {
// 判断是否为图片文件(优先使用 extension 字段)
if (isImageFile(item)) {
// 图片文件:使用图片预览
console.log('[文件操作] 检测到图片文件,使用图片预览')
try {
......@@ -235,8 +254,8 @@ export function useFileOperation() {
return
}
// 判断是否为视频文件
if (isVideoFile(item.fileName)) {
// 判断是否为视频文件(优先使用 extension 字段)
if (isVideoFile(item)) {
// 视频文件:跳转到视频播放页面
// 需要动态导入 navigateTo 以避免循环依赖
const Taro = await import('@tarojs/taro')
......
......@@ -28,7 +28,7 @@
<view class="flex gap-[24rpx] mb-[12rpx]">
<!-- Document Icon -->
<view class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start">
<image :src="getDocumentIcon(item.name)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
<image :src="getDocumentIcon({ extension: item.extension, fileName: item.name })" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
</view>
<!-- Title -->
......
......@@ -79,13 +79,13 @@
<div class="flex items-center justify-between">
<div class="flex items-center flex-1 mr-[24rpx]">
<image
:src="getDocumentIcon(doc.file_name)"
:src="getDocumentIcon({ extension: doc.extension, fileName: doc.file_name })"
class="w-[58rpx] h-[58rpx] mr-[24rpx]"
mode="aspectFit"
/>
<div class="flex flex-col">
<span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-2">{{ doc.file_name }}</span>
<span class="text-[#9CA3AF] text-[24rpx]">{{ getDocumentLabel(doc.file_name) }} · {{ doc.file_size_formatted }}</span>
<span class="text-[#9CA3AF] text-[24rpx]">{{ getDocumentLabel({ extension: doc.extension, fileName: doc.file_name }) }} · {{ doc.file_size_formatted }}</span>
</div>
</div>
<IconFont name="eye" size="14" color="#2563EB" @tap="viewDocument(doc)" />
......
......@@ -104,110 +104,179 @@ const EXTENSION_LABEL_MAP = {
const DEFAULT_LABEL = 'DOC';
/**
* 根据文件名获取文档图标路径
* 根据文件名或扩展名获取文档图标路径
*
* @description 从文件名中提取扩展名,返回对应的图标路径
* @param {string} fileName - 文件名(如:document.pdf)
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名(如:document.pdf)或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {string} 图标路径
*
* @example
* getDocumentIcon('报告.pdf') // 返回 PDF 图标
* getDocumentIcon({ extension: 'pdf' }) // 返回 PDF 图标(优先使用 extension)
* getDocumentIcon({ fileName: '报告.pdf' }) // 返回 PDF 图标
* getDocumentIcon('数据.xlsx') // 返回 Excel 图标
* getDocumentIcon('图片.png') // 返回 PNG 图标
*/
export function getDocumentIcon(fileName) {
if (!fileName || typeof fileName !== 'string') {
export function getDocumentIcon(fileNameOrItem) {
const extension = extractExtensionFromFile(fileNameOrItem);
if (!extension) {
return DEFAULT_ICON;
}
// 返回对应图标,找不到则返回默认图标
return EXTENSION_ICON_MAP[extension.toLowerCase()] || DEFAULT_ICON;
}
/**
* 从文件名或文件对象中提取扩展名(统一工具函数)
*
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段。
* 这是项目中所有文件类型判断的核心工具函数。
* @param {string|Object} fileNameOrItem - 文件名(如:document.pdf)或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {string} 扩展名(小写),空字符串表示无法提取
*
* @example
* extractExtensionFromFile('document.pdf') // 'pdf'
* extractExtensionFromFile({ extension: 'PDF' }) // 'pdf'(自动转小写)
* extractExtensionFromFile({ fileName: 'document.DOC' }) // 'doc'(从文件名解析)
* extractExtensionFromFile({ extension: 'pdf', fileName: 'backup.doc' }) // 'pdf'(优先使用 extension)
*/
export function extractExtensionFromFile(fileNameOrItem) {
let extension = '';
// 支持对象格式(优先使用 extension 字段)
if (typeof fileNameOrItem === 'object' && fileNameOrItem !== null) {
extension = fileNameOrItem.extension || '';
// 如果没有 extension 字段,尝试从 fileName 解析
if (!extension && fileNameOrItem.fileName) {
extension = extractExtensionFromString(fileNameOrItem.fileName);
}
} else if (typeof fileNameOrItem === 'string') {
// 兼容字符串格式,从文件名解析
extension = extractExtensionFromString(fileNameOrItem);
}
return extension.toLowerCase();
}
/**
* 从文件名字符串中提取扩展名
*
* @description 内部辅助函数,从文件名字符串中提取扩展名
* @param {string} fileName - 文件名
* @returns {string} 扩展名(小写)
* @private
*/
function extractExtensionFromString(fileName) {
if (!fileName || typeof fileName !== 'string') {
return '';
}
// 提取文件扩展名
const lastDotIndex = fileName.lastIndexOf('.');
// 没有扩展名或以点结尾(如 "file.")
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
return DEFAULT_ICON;
return '';
}
const extension = fileName.slice(lastDotIndex + 1).toLowerCase();
// 返回对应图标,找不到则返回默认图标
return EXTENSION_ICON_MAP[extension] || DEFAULT_ICON;
return fileName.slice(lastDotIndex + 1).toLowerCase();
}
/**
* 根据文件名获取文件类型标签
* 根据文件名或扩展名获取文件类型标签
*
* @description 从文件名中提取扩展名,返回对应的显示标签
* @param {string} fileName - 文件名(如:document.pdf)
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名(如:document.pdf)或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {string} 文件类型标签(如:PDF、Word、Excel)
*
* @example
* getDocumentLabel('报告.pdf') // 'PDF'
* getDocumentLabel({ extension: 'pdf' }) // 'PDF'(优先使用 extension)
* getDocumentLabel({ fileName: '报告.pdf' }) // 'PDF'
* getDocumentLabel('数据.xlsx') // 'Excel'
* getDocumentLabel('图片.png') // 'PNG'
*/
export function getDocumentLabel(fileName) {
if (!fileName || typeof fileName !== 'string') {
return DEFAULT_LABEL;
}
// 提取文件扩展名
const lastDotIndex = fileName.lastIndexOf('.');
export function getDocumentLabel(fileNameOrItem) {
const extension = extractExtensionFromFile(fileNameOrItem);
// 没有扩展名或以点结尾(如 "file.")
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
if (!extension) {
return DEFAULT_LABEL;
}
const extension = fileName.slice(lastDotIndex + 1).toLowerCase();
// 返回对应标签,找不到则返回默认标签
return EXTENSION_LABEL_MAP[extension] || DEFAULT_LABEL;
return EXTENSION_LABEL_MAP[extension.toLowerCase()] || DEFAULT_LABEL;
}
/**
* 根据文件名判断是否为 PDF 文件
* 根据文件名或扩展名判断是否为 PDF 文件
*
* @param {string} fileName - 文件名
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} 是否为 PDF 文件
*
* @example
* isPDF('document.pdf') // true
* isPDF({ extension: 'pdf' }) // true(优先使用 extension)
* isPDF({ fileName: 'document.pdf' }) // true
* isPDF('document.docx') // false
*/
export function isPDF(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
return extension === 'pdf';
export function isPDF(fileNameOrItem) {
const extension = extractExtensionFromFile(fileNameOrItem);
return extension.toLowerCase() === 'pdf';
}
/**
* 根据文件名判断是否为图片文件
* 根据文件名或扩展名判断是否为图片文件
*
* @param {string} fileName - 文件名
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} 是否为图片文件
*
* @example
* isImage('photo.jpg') // true
* isImage({ extension: 'jpg' }) // true(优先使用 extension)
* isImage('document.pdf') // false
*/
export function isImage(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
export function isImage(fileNameOrItem) {
const extension = extractExtensionFromFile(fileNameOrItem);
if (!extension) return false;
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
return imageExtensions.includes(extension);
return imageExtensions.includes(extension.toLowerCase());
}
/**
* 根据文件名判断是否为视频文件
* 根据文件名或扩展名判断是否为视频文件
*
* @param {string} fileName - 文件名
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名或文件对象(包含 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} 是否为视频文件
*
* @example
* isVideo('movie.mp4') // true
* isVideo({ extension: 'mp4' }) // true(优先使用 extension)
* isVideo('document.pdf') // false
*/
export function isVideo(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
export function isVideo(fileNameOrItem) {
const extension = extractExtensionFromFile(fileNameOrItem);
if (!extension) return false;
const videoExtensions = ['mp4', 'mov', 'avi', 'mkv'];
return videoExtensions.includes(extension);
return videoExtensions.includes(extension.toLowerCase());
}
......
......@@ -8,6 +8,7 @@
import dayjs from 'dayjs';
import Taro from '@tarojs/taro';
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
import { extractExtensionFromFile } from './documentIcons'
/**
* @description 格式化时间
......@@ -199,24 +200,26 @@ const buildApiUrl = (action, params = {}) => {
/**
* @description 判断文件是否为视频文件
* @param {string} fileName 文件名
* @param {string|Object} fileNameOrItem 文件名或文件对象(支持 extension 字段)
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} true=是视频文件,false=不是视频文件
*
* @example
* isVideoFile('video.mp4') // true
* isVideoFile({ extension: 'mp4' }) // true (优先使用 extension)
* isVideoFile('document.pdf') // false
* isVideoFile('presentation.mov') // true
*/
const isVideoFile = (fileName) => {
if (!fileName || typeof fileName !== 'string') return false;
const isVideoFile = (fileNameOrItem) => {
const extension = extractExtensionFromFile(fileNameOrItem);
// 提取文件扩展名
const ext = fileName.split('.').pop()?.toLowerCase() || '';
if (!extension) return false;
// 微信小程序支持的视频格式
const videoExtensions = ['mp4', 'm4v', 'mov'];
return videoExtensions.includes(ext);
return videoExtensions.includes(extension.toLowerCase());
};
export {
......