前言
❝最近有接到一個需求,要求前端支持上傳制定后綴文件,且支持頁面預(yù)覽,上傳簡單,那么預(yù)覽該怎么實(shí)現(xiàn)呢,尤其是不同類型的文件預(yù)覽方案,那么下面就我這個需求的實(shí)現(xiàn),答案在下面 👇:
❞
「具體的預(yù)覽需求:」預(yù)覽需要支持的文件類型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video
,另外對于不同文檔還需要有定位的功能。例如:pdf
定位到頁碼,txt
和markdown
定位到文字并滾動到指定的位置,音視頻定位到具體的時間等等。
「⚠️ 補(bǔ)充: 我的需求是需要先將文件上傳到后臺,所以我拿到的是url
地址去展示,對于markdown
和txt
的文件需要先用fetch
獲取,其他的展示則直接使用url
鏈接就可以。」
不同文件的實(shí)現(xiàn)方式不同,下面分類講解,總共分為以下幾類:
- 自有標(biāo)簽文件:
png、jpg、jpeg、audio、video
office
類型的文件: docx、xlsx、ppt
自有標(biāo)簽文件:png、jpg、jpeg、audio、video
❝對于圖片、音視頻的預(yù)覽,直接使用對應(yīng)的標(biāo)簽即可,如下:
❞
圖片:png、jpg、jpeg
「示例代碼:」
<img src={url} key={docId} alt={name} width="100%" />;
「預(yù)覽效果如下:」
音頻:audio
「示例代碼:」
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
「預(yù)覽效果如下:」
視頻:video
「示例代碼:」
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
「預(yù)覽效果如下:」
「關(guān)于音視頻的定位的完整代碼:」
import React, { useRef, useEffect } from 'react';
interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}
function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
// 音視頻定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);
return (
<div>
{type === 'audio' ? (
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
) : (
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
)}
</div>
);
}
export default AudioAndVideo;
純文字的文件: markdown & txt
❝對于markdown、txt
類型的文件,如果拿到的是文件的url
的話,則無法直接顯示,需要請求到內(nèi)容,再進(jìn)行展示。
❞
markdown
文件
❝在展示markdown
文件時,需要滿足字體高亮、代碼高亮、如果有字體高亮,需要滾動到字體所在位置、如果有外部鏈接,需要新開tab頁面再打開。
❞
需要引入兩個庫:
marked
:它的作用是將markdown
文本轉(zhuǎn)換(解析)為HTML
。
highlight
: 它允許開發(fā)者在網(wǎng)頁上高亮顯示代碼。
「字體高亮的代碼實(shí)現(xiàn):」
❝高亮的樣式,可以在行間樣式定義
❞
const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};
「代碼高亮的代碼實(shí)現(xiàn):」
❝需要借助hljs
這個庫進(jìn)行轉(zhuǎn)換
❞
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
}
},
});
「鏈接跳轉(zhuǎn)新tab
頁的代碼實(shí)現(xiàn):」
marked.use({
renderer: {
// 鏈接跳轉(zhuǎn)
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
「滾動到高亮的位置的代碼實(shí)現(xiàn):」
❝需要配合上面的代碼高亮的方法
❞
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
「完整的代碼如下:」
❝入?yún)⒌?code style="margin: 0px 2px; padding: 2px 4px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 4px; background-color: rgba(27, 31, 35, 0.05); font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; word-break: break-all;">docUrl 是markdown
文件的線上ur
l地址,searchText
是需要高亮的內(nèi)容。
❞
import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';
const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};
// Markdown展示組件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);
const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};
useEffect(() => {
// 如果沒有搜索內(nèi)容,直接加載原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加載Markdown文件失?。?#39;, error));
}, [searchText, docUrl]);
useEffect(() => {
if (markdownRef.current) {
// 支持代碼高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
},
// 鏈接跳轉(zhuǎn)
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 當(dāng)markdown更新后,檢查是否需要滾動到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);
return (
<div style={preStyle}>
<div ref={markdownRef} />
</div>
);
}
export default MarkdownViewer;
「預(yù)覽效果如下:」
txt
文件預(yù)覽展示
❝支持高亮和滾動到指定位置
❞
「支持高亮的代碼:」
function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}
「完整代碼:」
import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';
function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);
function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}
useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加載文本文件出錯:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);
useEffect(() => {
// 處理高亮段落的滾動邏輯
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
return () => clearTimeout(timer);
}, [paragraphs]);
return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;
// 確定這個段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
</p>
);
})}
</div>
);
}
export default TextFileViewer;
「預(yù)覽效果如下:」
office
類型的文件: docx、xlsx、ppt
❝docx、xlsx、ppt
文件的預(yù)覽,用的是office
的線上預(yù)覽鏈接 + 我們文件的線上url
即可。
❞
❝關(guān)于定位:用這種方法我暫時嘗試是無法定位頁碼的,所以定位的功能我采取的是后端將office
文件轉(zhuǎn)成pdf
,再進(jìn)行定位,如果只是純展示,忽略這個問題即可。
❞
「示例代碼:」
<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
width="100%"
height="500px"
frameBorder="0"
></iframe>
「預(yù)覽效果如下:」
embed
引入文件:pdf
❝在pdf
文檔預(yù)覽時,可以采用embed
的方式,這個httpsUrl
就是你的pdf
文檔的鏈接地址
❞
「示例代碼:」
<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
關(guān)于定位,其實(shí)是地址上拼接的頁碼sourcePage
,如下:
const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;
<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
「預(yù)覽效果如下:」
iframe
:引入外部完整的網(wǎng)站
❝除了上面的各種文件,我們還需要預(yù)覽一些外部的網(wǎng)址,那就要用到iframe
的方式
❞
「示例代碼:」
<iframe
title="網(wǎng)址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>
「預(yù)覽效果如下:」
總結(jié): 到這里我們支持的所有文件都講述完了。
該文章在 2024/5/15 14:46:25 編輯過