feat(预览): 支持图片文件预览和Markdown相对路径解析
- 新增图片类型标签页,用于预览本地图片文件 - 在useTabs中根据文件扩展名自动识别图片文件并设置tab类型 - 为Preview组件添加文件路径属性,用于解析Markdown中的相对图片路径 - 实现Markdown渲染器对相对路径的解析,将相对路径转换为file://协议URL - 添加图片点击预览功能,点击Markdown中的图片可放大查看
This commit is contained in:
parent
6a8b3902a2
commit
3a9a325f80
|
|
@ -7,7 +7,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="preview-pane" v-show="mode !== 'source'" :style="{ width: mode === 'split' ? '50%' : '100%' }">
|
||||
<Preview :content="localContent" />
|
||||
<Preview :content="localContent" :file-path="tab.filePath" />
|
||||
</div>
|
||||
|
||||
<!-- 模式切换浮动按钮 -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="image-tab">
|
||||
<div v-if="fileUrl" class="image-container">
|
||||
<a-image :src="fileUrl" :preview="true" />
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
无法预览该图片
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const toFileUrl = (absolutePath) => {
|
||||
if (!absolutePath) return '';
|
||||
const normalized = absolutePath.replace(/\\/g, '/');
|
||||
return encodeURI(`file:///${normalized}`);
|
||||
};
|
||||
|
||||
const fileUrl = computed(() => toFileUrl(props.tab.filePath));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-tab {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
margin: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin: auto;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,24 +1,86 @@
|
|||
<template>
|
||||
<div class="markdown-preview" v-html="htmlContent"></div>
|
||||
<div class="markdown-preview" v-html="htmlContent" @click="handleClick"></div>
|
||||
<a-modal v-model:open="imagePreviewOpen" :footer="null" width="80vw" centered>
|
||||
<img v-if="imagePreviewSrc" :src="imagePreviewSrc" style="max-width: 100%; max-height: 75vh; display: block; margin: 0 auto;" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
const props = defineProps({
|
||||
content: String
|
||||
content: String,
|
||||
filePath: String
|
||||
});
|
||||
|
||||
const isAbsoluteOrSpecialUrl = (rawUrl) => {
|
||||
if (!rawUrl) return true;
|
||||
if (rawUrl.startsWith('#')) return true;
|
||||
if (rawUrl.startsWith('//')) return true;
|
||||
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(rawUrl);
|
||||
};
|
||||
|
||||
const getBaseDirFileUrl = (filePath) => {
|
||||
if (!filePath) return '';
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const lastSlashIndex = normalized.lastIndexOf('/');
|
||||
const dirPath = lastSlashIndex >= 0 ? normalized.slice(0, lastSlashIndex + 1) : normalized;
|
||||
return encodeURI(`file:///${dirPath}`);
|
||||
};
|
||||
|
||||
const resolveToFileUrl = (rawUrl, baseDirFileUrl) => {
|
||||
if (!rawUrl || !baseDirFileUrl) return rawUrl || '';
|
||||
if (isAbsoluteOrSpecialUrl(rawUrl)) return rawUrl;
|
||||
try {
|
||||
return new URL(rawUrl, baseDirFileUrl).href;
|
||||
} catch {
|
||||
return rawUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
});
|
||||
|
||||
const defaultImageRule = md.renderer.rules.image;
|
||||
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const src = token.attrGet('src');
|
||||
const resolved = resolveToFileUrl(src, env?.baseDirFileUrl);
|
||||
if (resolved) token.attrSet('src', resolved);
|
||||
if (defaultImageRule) return defaultImageRule(tokens, idx, options, env, self);
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
const defaultLinkOpenRule = md.renderer.rules.link_open;
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const href = token.attrGet('href');
|
||||
const resolved = resolveToFileUrl(href, env?.baseDirFileUrl);
|
||||
if (resolved) token.attrSet('href', resolved);
|
||||
if (defaultLinkOpenRule) return defaultLinkOpenRule(tokens, idx, options, env, self);
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
const htmlContent = computed(() => {
|
||||
return md.render(props.content || '');
|
||||
const env = { baseDirFileUrl: getBaseDirFileUrl(props.filePath) };
|
||||
return md.render(props.content || '', env);
|
||||
});
|
||||
|
||||
const imagePreviewOpen = ref(false);
|
||||
const imagePreviewSrc = ref('');
|
||||
|
||||
const handleClick = (event) => {
|
||||
const target = event?.target;
|
||||
if (!target || target.tagName !== 'IMG') return;
|
||||
const src = target.getAttribute('src') || '';
|
||||
if (!src) return;
|
||||
imagePreviewSrc.value = src;
|
||||
imagePreviewOpen.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@
|
|||
v-show="tab.id === state.activeTabId"
|
||||
:tab="tab"
|
||||
/>
|
||||
<ImageTab
|
||||
v-else-if="tab.type === 'image'"
|
||||
v-show="tab.id === state.activeTabId"
|
||||
:tab="tab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-if="!activeTab" class="empty-state">
|
||||
|
|
@ -26,6 +31,7 @@
|
|||
import TabBar from './TabBar.vue';
|
||||
import HomeTab from './HomeTab.vue';
|
||||
import FileTab from './FileTab.vue';
|
||||
import ImageTab from './ImageTab.vue';
|
||||
import { useTabs } from '../composables/useTabs';
|
||||
|
||||
const { state, activeTab } = useTabs();
|
||||
|
|
|
|||
|
|
@ -32,10 +32,13 @@ export function useTabs() {
|
|||
return;
|
||||
}
|
||||
|
||||
const isImageFile = (name) => /\.(png|jpe?g|gif|webp|bmp|svg|ico)$/i.test(name || '');
|
||||
const tabType = isImageFile(fileNode.name) ? 'image' : 'file';
|
||||
|
||||
// 创建新 Tab
|
||||
const newTab = {
|
||||
id: uuidv4(),
|
||||
type: 'file',
|
||||
type: tabType,
|
||||
title: fileNode.name,
|
||||
filePath: fileNode.path,
|
||||
content: '', // 内容稍后加载,或在组件挂载时加载
|
||||
|
|
|
|||
Loading…
Reference in New Issue