feat: html格式转图片网页工具

master
LCJ-MinYa 10 months ago
parent affd1ea9ee
commit 347f59895a

@ -34,6 +34,7 @@
"echarts": "^5.5.1", "echarts": "^5.5.1",
"element-plus": "^2.8.0", "element-plus": "^2.8.0",
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
"html2canvas": "^1.4.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"marked": "^15.0.7", "marked": "^15.0.7",

@ -47,6 +47,9 @@ importers:
github-markdown-css: github-markdown-css:
specifier: ^5.8.1 specifier: ^5.8.1
version: 5.8.1 version: 5.8.1
html2canvas:
specifier: ^1.4.1
version: 1.4.1
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
@ -1269,6 +1272,10 @@ packages:
balanced-match@2.0.0: balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1457,6 +1464,9 @@ packages:
resolution: {integrity: sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==} resolution: {integrity: sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==}
engines: {node: '>=12 || >=16'} engines: {node: '>=12 || >=16'}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
css-select@4.3.0: css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
@ -1993,6 +2003,10 @@ packages:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
htmlparser2@8.0.2: htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
@ -3242,6 +3256,9 @@ packages:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'} engines: {node: '>=10'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -3357,6 +3374,9 @@ packages:
util@0.10.4: util@0.10.4:
resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@8.3.2: uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true hasBin: true
@ -4650,6 +4670,8 @@ snapshots:
balanced-match@2.0.0: {} balanced-match@2.0.0: {}
base64-arraybuffer@1.0.2: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
boolbase@1.0.0: {} boolbase@1.0.0: {}
@ -4850,6 +4872,10 @@ snapshots:
css-functions-list@3.2.2: {} css-functions-list@3.2.2: {}
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
css-select@4.3.0: css-select@4.3.0:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
@ -5528,6 +5554,11 @@ snapshots:
html-tags@3.3.1: {} html-tags@3.3.1: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
htmlparser2@8.0.2: htmlparser2@8.0.2:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@ -6755,6 +6786,10 @@ snapshots:
yallist: 4.0.0 yallist: 4.0.0
optional: true optional: true
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
text-table@0.2.0: {} text-table@0.2.0: {}
thenify-all@1.6.0: thenify-all@1.6.0:
@ -6885,6 +6920,10 @@ snapshots:
dependencies: dependencies:
inherits: 2.0.3 inherits: 2.0.3
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
uuid@8.3.2: {} uuid@8.3.2: {}
vite-plugin-cdn-import@1.0.1(rollup@4.21.0)(vite@5.4.1(@types/node@20.16.1)(sass@1.77.8)): vite-plugin-cdn-import@1.0.1(rollup@4.21.0)(vite@5.4.1(@types/node@20.16.1)(sass@1.77.8)):

@ -1,206 +1,180 @@
// import "@/utils/sso"; // import "@/utils/sso";
import Cookies from "js-cookie"; import Cookies from 'js-cookie';
import { getConfig } from "@/config"; import { getConfig } from '@/config';
import NProgress from "@/utils/progress"; import NProgress from '@/utils/progress';
import { buildHierarchyTree } from "@/utils/tree"; import { buildHierarchyTree } from '@/utils/tree';
import remainingRouter from "./modules/remaining"; import remainingRouter from './modules/remaining';
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from '@/store/modules/multiTags';
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from '@/store/modules/permission';
import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils"; import { isUrl, openLink, storageLocal, isAllEmpty } from '@pureadmin/utils';
import { import {
ascending, ascending,
getTopMenu, getTopMenu,
initRouter, initRouter,
isOneOfArray, isOneOfArray,
getHistoryMode, getHistoryMode,
findRouteByPath, findRouteByPath,
handleAliveRoute, handleAliveRoute,
formatTwoStageRoutes, formatTwoStageRoutes,
formatFlatteningRoutes formatFlatteningRoutes,
} from "./utils"; } from './utils';
import { import { type Router, createRouter, type RouteRecordRaw, type RouteComponent } from 'vue-router';
type Router, import { type DataInfo, userKey, removeToken, multipleTabsKey } from '@/utils/auth';
createRouter,
type RouteRecordRaw,
type RouteComponent
} from "vue-router";
import {
type DataInfo,
userKey,
removeToken,
multipleTabsKey
} from "@/utils/auth";
/** src/router/modules .ts remaining.ts /** src/router/modules .ts remaining.ts
* https://github.com/mrmlnc/fast-glob#basic-syntax * https://github.com/mrmlnc/fast-glob#basic-syntax
* https://cn.vitejs.dev/guide/features.html#negative-patterns * https://cn.vitejs.dev/guide/features.html#negative-patterns
*/ */
const modules: Record<string, any> = import.meta.glob( const modules: Record<string, any> = import.meta.glob(['./modules/**/*.ts', '!./modules/**/remaining.ts'], {
["./modules/**/*.ts", "!./modules/**/remaining.ts"], eager: true,
{ });
eager: true
}
);
/** 原始静态路由(未做任何处理) */ /** 原始静态路由(未做任何处理) */
const routes = []; const routes = [];
Object.keys(modules).forEach(key => { Object.keys(modules).forEach((key) => {
routes.push(modules[key].default); routes.push(modules[key].default);
}); });
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */ /** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes( export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))) formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
); );
/** 用于渲染菜单,保持原始层级 */ /** 用于渲染菜单,保持原始层级 */
export const constantMenus: Array<RouteComponent> = ascending( export const constantMenus: Array<RouteComponent> = ascending(routes.flat(Infinity)).concat(...remainingRouter);
routes.flat(Infinity)
).concat(...remainingRouter);
/** 不参与菜单的路由 */ /** 不参与菜单的路由 */
export const remainingPaths = Object.keys(remainingRouter).map(v => { export const remainingPaths = Object.keys(remainingRouter).map((v) => {
return remainingRouter[v].path; return remainingRouter[v].path;
}); });
/** 创建路由实例 */ /** 创建路由实例 */
export const router: Router = createRouter({ export const router: Router = createRouter({
history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY), history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY),
routes: constantRoutes.concat(...(remainingRouter as any)), routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true, strict: true,
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => { return new Promise((resolve) => {
if (savedPosition) { if (savedPosition) {
return savedPosition; return savedPosition;
} else { } else {
if (from.meta.saveSrollTop) { if (from.meta.saveSrollTop) {
const top: number = const top: number = document.documentElement.scrollTop || document.body.scrollTop;
document.documentElement.scrollTop || document.body.scrollTop; resolve({ left: 0, top });
resolve({ left: 0, top }); }
} }
} });
}); },
}
}); });
/** 重置路由 */ /** 重置路由 */
export function resetRouter() { export function resetRouter() {
router.getRoutes().forEach(route => { router.getRoutes().forEach((route) => {
const { name, meta } = route; const { name, meta } = route;
if (name && router.hasRoute(name) && meta?.backstage) { if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name); router.removeRoute(name);
router.options.routes = formatTwoStageRoutes( router.options.routes = formatTwoStageRoutes(formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))));
formatFlatteningRoutes( }
buildHierarchyTree(ascending(routes.flat(Infinity))) });
) usePermissionStoreHook().clearAllCachePage();
);
}
});
usePermissionStoreHook().clearAllCachePage();
} }
/** 路由白名单 */ /** 路由白名单 */
const whiteList = ["/login"]; const whiteList = ['/login', '/htmlToImg'];
const { VITE_HIDE_HOME } = import.meta.env; const { VITE_HIDE_HOME } = import.meta.env;
router.beforeEach((to: ToRouteType, _from, next) => { router.beforeEach((to: ToRouteType, _from, next) => {
if (to.meta?.keepAlive) { if (to.meta?.keepAlive) {
handleAliveRoute(to, "add"); handleAliveRoute(to, 'add');
// 页面整体刷新和点击标签页刷新 // 页面整体刷新和点击标签页刷新
if (_from.name === undefined || _from.name === "Redirect") { if (_from.name === undefined || _from.name === 'Redirect') {
handleAliveRoute(to); handleAliveRoute(to);
}
} }
} const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey); NProgress.start();
NProgress.start(); const externalLink = isUrl(to?.name as string);
const externalLink = isUrl(to?.name as string); if (!externalLink) {
if (!externalLink) { to.matched.some((item) => {
to.matched.some(item => { if (!item.meta.title) return '';
if (!item.meta.title) return ""; const Title = getConfig().Title;
const Title = getConfig().Title; if (Title) document.title = `${item.meta.title} | ${Title}`;
if (Title) document.title = `${item.meta.title} | ${Title}`; else document.title = item.meta.title as string;
else document.title = item.meta.title as string; });
});
}
/** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
function toCorrectRoute() {
whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
}
if (Cookies.get(multipleTabsKey) && userInfo) {
// 无权限跳转403页面
if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
next({ path: "/error/403" });
} }
// 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面 /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") { function toCorrectRoute() {
next({ path: "/error/404" }); whiteList.includes(to.fullPath) && to.fullPath === '/login' ? next(_from.fullPath) : next();
} }
if (_from?.name) { if (Cookies.get(multipleTabsKey) && userInfo) {
// name为超链接 // 无权限跳转403页面
if (externalLink) { if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
openLink(to?.name as string); next({ path: '/error/403' });
NProgress.done(); }
} else { // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
toCorrectRoute(); if (VITE_HIDE_HOME === 'true' && to.fullPath === '/welcome') {
} next({ path: '/error/404' });
} else { }
// 刷新 if (_from?.name) {
if ( // name为超链接
usePermissionStoreHook().wholeMenus.length === 0 && if (externalLink) {
to.path !== "/login" openLink(to?.name as string);
) { NProgress.done();
initRouter().then((router: Router) => { } else {
if (!useMultiTagsStoreHook().getMultiTagsCache) { toCorrectRoute();
const { path } = to; }
const route = findRouteByPath( } else {
path, // 刷新
router.options.routes[0].children if (usePermissionStoreHook().wholeMenus.length === 0 && to.path !== '/login') {
); initRouter().then((router: Router) => {
getTopMenu(true); if (!useMultiTagsStoreHook().getMultiTagsCache) {
// query、params模式路由传参数的标签页不在此处处理 const { path } = to;
if (route && route.meta?.title) { const route = findRouteByPath(path, router.options.routes[0].children);
if (isAllEmpty(route.parentId) && route.meta?.backstage) { getTopMenu(true);
// 此处为动态顶级路由(目录) // query、params模式路由传参数的标签页不在此处处理
const { path, name, meta } = route.children[0]; if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", { if (isAllEmpty(route.parentId) && route.meta?.backstage) {
path, // 此处为动态顶级路由(目录)
name, const { path, name, meta } = route.children[0];
meta useMultiTagsStoreHook().handleTags('push', {
}); path,
} else { name,
const { path, name, meta } = route; meta,
useMultiTagsStoreHook().handleTags("push", { });
path, } else {
name, const { path, name, meta } = route;
meta useMultiTagsStoreHook().handleTags('push', {
path,
name,
meta,
});
}
}
}
// 确保动态路由完全加入路由列表并且不影响静态路由注意动态路由刷新时router.beforeEach可能会触发两次第一次触发动态路由还未完全添加第二次动态路由才完全添加到路由列表如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断这样就只会触发一次
if (isAllEmpty(to.name)) router.push(to.fullPath);
}); });
}
} }
} toCorrectRoute();
// 确保动态路由完全加入路由列表并且不影响静态路由注意动态路由刷新时router.beforeEach可能会触发两次第一次触发动态路由还未完全添加第二次动态路由才完全添加到路由列表如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断这样就只会触发一次 }
if (isAllEmpty(to.name)) router.push(to.fullPath);
});
}
toCorrectRoute();
}
} else {
if (to.path !== "/login") {
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
removeToken();
next({ path: "/login" });
}
} else { } else {
next(); if (to.path !== '/login') {
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
removeToken();
next({ path: '/login' });
}
} else {
next();
}
} }
}
}); });
router.afterEach(() => { router.afterEach(() => {
NProgress.done(); NProgress.done();
}); });
export default router; export default router;

@ -37,4 +37,14 @@ export default [
rank: 103, rank: 103,
}, },
}, },
{
path: '/htmlToImg',
name: 'HtmlToImg',
component: () => import('@/views/utils/htmlToImg.vue'),
meta: {
title: 'HTML转图片',
showLink: false,
rank: 104,
},
},
] satisfies Array<RouteConfigsTable>; ] satisfies Array<RouteConfigsTable>;

@ -0,0 +1,207 @@
<template>
<div class="flex justify-center">
<div class="container flex min-h-screen p-8">
<div class="flex flex-col w-[40%] pr-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">HTML 转图片工具</h1>
<p class="mt-2 text-gray-600">将您的 HTML 代码转换为高质量图片</p>
</div>
<div class="flex-1 flex flex-col">
<div class="flex items-center justify-between mb-4">
<label class="text-sm font-medium text-gray-700">输入 HTML 代码</label>
<div class="flex items-center space-x-2">
<!-- <el-button
type="primary"
text
class="text-sm whitespace-nowrap !rounded-button"
@click="importFile"
>
<el-icon class="mr-1"><Upload /></el-icon>
</el-button> -->
<el-button
text
class="text-sm text-gray-500 whitespace-nowrap !rounded-button"
@click="clearContent"
>
<el-icon class="mr-1"><Delete /></el-icon>
</el-button>
</div>
</div>
<el-input
v-model="htmlContent"
type="textarea"
:rows="10"
class="flex-1"
placeholder="在此处粘贴您的 HTML 代码..."
resize="none"
/>
<div class="mt-6 flex items-center justify-end">
<!-- <div class="flex items-center space-x-4">
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-2">宽度</label>
<el-input-number
v-model="width"
:min="1"
:max="3840"
class="w-[100px]"
controls-position="right"
/>
</div>
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-2">高度</label>
<el-input-number
v-model="height"
:min="1"
:max="2160"
class="w-[100px]"
controls-position="right"
/>
</div>
</div> -->
<el-button
type="primary"
class="whitespace-nowrap !rounded-button"
@click="preview"
>
<el-icon class="mr-2"><View /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="flex flex-col w-[60%] bg-gray-50 rounded-lg p-8">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-medium text-gray-900">预览效果</h2>
<div class="flex items-center space-x-4">
<el-button
class="whitespace-nowrap !rounded-button"
@click="toggleFullscreen"
>
<el-icon class="mr-1"><FullScreen /></el-icon>
</el-button>
<el-button
type="success"
class="whitespace-nowrap !rounded-button"
@click="exportImage"
>
<el-icon class="mr-2"><Download /></el-icon>
</el-button>
</div>
</div>
<div class="flex-1 bg-white rounded-lg border border-gray-200 p-6">
<div
class="flex bg-white items-center justify-center h-full text-gray-400"
ref="previewRef"
>
<div
class="text-center"
v-if="!previewContent"
>
<el-icon class="text-6xl mb-4"><Picture /></el-icon>
<p>预览区域</p>
<p class="text-sm mt-2">点击"预览效果"按钮查看渲染结果</p>
</div>
<div
v-else
v-html="previewContent"
class="w-full h-full"
ref="htmlContentRef"
></div>
</div>
</div>
<div class="mt-6">
<el-alert
type="info"
show-icon
:closable="false"
>
<template #title>
<span class="font-medium">使用提示</span>
</template>
<ul class="list-disc pl-5 space-y-1 mt-2">
<li>支持完整的 HTML 代码转换</li>
<li>可自定义输出图片尺寸</li>
<li>支持导出 PNGJPG 格式</li>
<li>建议代码格式化后再进行转换</li>
</ul>
</el-alert>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Upload, Delete, View, FullScreen, Download, Picture } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import html2canvas from 'html2canvas';
const htmlContent = ref('');
const htmlContentRef = ref(null);
const width = ref(1920);
const height = ref(1080);
const previewContent = ref('');
const previewRef = ref<HTMLElement | null>(null);
const importFile = () => {
ElMessage.info('导入文件功能开发中...');
};
const clearContent = () => {
htmlContent.value = '';
previewContent.value = '';
};
const preview = () => {
if (!htmlContent.value) {
ElMessage.warning('请先输入 HTML 代码');
return;
}
previewContent.value = htmlContent.value;
};
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
previewRef.value?.requestFullscreen();
} else {
document.exitFullscreen();
}
};
const exportImage = () => {
if (!previewContent.value) {
ElMessage.warning('请先预览效果');
return;
}
html2canvas(previewRef.value).then((canvas) => {
const imgData = canvas.toDataURL('image/png');
const img = new Image();
img.src = imgData;
const link = document.createElement('a');
link.download = 'preview.png';
link.href = imgData;
link.click();
});
};
</script>
<style scoped>
:deep(.el-textarea__inner) {
height: 100%;
}
:deep(.el-input-number .el-input__inner) {
text-align: left;
}
.container {
max-width: 1440px;
}
</style>
Loading…
Cancel
Save