网址访问小工具(模拟浏览器)
文章说明
本篇文章主要是我写的一个小demo,感觉效果还蛮不错的,作为一个记录新想法的实现思路;介绍了模拟浏览器页面的一些页面实现的小细节。
采用vue3结合electron设计的简单的网址访问小工具, 类似一个简单版的浏览器, 只不过有一些网页打不开, 然后目前提示信息功能做的还不是很完善, 等待后续继续补充
目前最大化、最小化、关闭按钮功能还没完善好; 同时部分网页无法在iframe中打开, 具体原因还未查明; 目前url输入框也还未添加记忆功能, 等待后续完善
在Tab栏中样式没有调整的很精细, 那个底部的弧形边框还没有很好的实现思路
核心代码
App.vue(Tab栏和页面展示)
html
<script setup>
import {reactive} from "vue";
import SinglePage from "@/components/SinglePage.vue";
let pageId = 1;
const data = reactive({
pages: [
{
id: pageId,
name: "新建标签页",
}
],
currentPage: pageId,
status: "normal",
});
function addTab() {
pageId++;
data.pages.push({
id: pageId,
name: "新建标签页",
});
data.currentPage = data.pages[data.pages.length - 1].id;
}
function closeTab() {
for (let i = 0; i < data.pages.length; i++) {
if (data.pages[i].id === data.currentPage) {
data.pages.splice(i, 1);
break;
}
}
if (data.pages.length === 0) {
addTab();
} else {
data.currentPage = data.pages[data.pages.length - 1].id;
}
}
function toTab(item) {
data.currentPage = item.id;
}
function closeWindow() {
if (confirm("确认关闭吗?")) {
alert("关闭窗体");
}
}
function maximize() {
document.documentElement.requestFullscreen().then(() => {
data.status = "maximized";
}).catch((err) => {
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
}
function normal() {
if (document.fullscreenElement) {
document.exitFullscreen().then(() => {
data.status = "normal";
}).catch((err) => {
alert(`Error attempting to exit full-screen mode: ${err.message} (${err.name})`);
});
}
}
function minimize() {
}
</script>
<template>
<div class="container" @contextmenu.prevent>
<div class="header">
<div class="tab-container">
<i class="iconfont icon-work"></i>
<template v-for="item in data.pages" :key="item.id">
<div :class="item.id === data.currentPage ? ' current-tab-item ' : ''" class="tab-item" @click="toTab(item)">
<div style="flex: 1">{{ item.name }}</div>
<i class="iconfont icon-close" @click.stop="closeTab"></i>
</div>
</template>
<i class="iconfont icon-add" @click="addTab"></i>
</div>
<div class="icon-container">
<i class="iconfont icon-minimize" @click="minimize"></i>
<i v-show="data.status === 'normal'" class="iconfont icon-maximize" @click="maximize"></i>
<i v-show="data.status !== 'normal'" class="iconfont icon-maximized" @click="normal"></i>
<i class="iconfont icon-close" @click="closeWindow"></i>
</div>
</div>
<template v-for="item in data.pages" :key="item.id">
<div v-show="data.currentPage === item.id" style="flex: 1; overflow: hidden">
<SinglePage/>
</div>
</template>
</div>
</template>
<style lang="scss">
@import "@/css/iconfont.css";
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
.header {
height: 35px;
width: 100%;
background-color: #cdcdcd;
display: flex;
align-items: center;
.tab-container {
flex: 1;
display: flex;
align-items: center;
.icon-work, .icon-add {
height: 35px;
width: 35px;
color: black;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-work {
margin-right: 10px;
&:hover {
background-color: #bdbdbd;
}
}
.icon-add {
height: 30px;
width: 30px;
margin-left: 10px;
&:hover {
background-color: #adadad;
border-radius: 8px;
}
}
.tab-item {
width: 260px;
height: 33px;
color: #000000;
font-size: 12px;
background-color: transparent;
display: flex;
align-items: center;
padding: 10px;
border-radius: 5px;
&:hover {
background-color: #dadada;
}
.icon-close {
font-size: 12px;
padding: 3px;
color: #0d0d0d;
&:hover {
background-color: #d0d0d0;
border-radius: 4px;
}
}
}
.current-tab-item {
background-color: #f7f7f7;
&:hover {
background-color: #f7f7f7;
}
}
}
.icon-container {
width: fit-content;
height: 100%;
display: flex;
.iconfont {
width: 40px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #000000;
&:hover {
background-color: #b8b8b8;
}
}
.icon-close {
&:hover {
background-color: #e81123;
color: #ffffff;
}
}
}
}
}
</style>
页面iframe展示组件
html
<script setup>
import {nextTick, onMounted, reactive, ref, watch} from "vue";
const data = reactive({
urls: [""],
current: 0,
url: "",
loading: false,
});
function last() {
if (data.current === 0) {
return;
}
data.current--;
data.url = data.urls[data.current];
inputRef.value.value = data.url;
}
function next() {
if (data.current === data.urls.length - 1) {
return;
}
data.current++;
data.url = data.urls[data.current];
inputRef.value.value = data.url;
}
const inputRef = ref();
const pageRef = ref();
onMounted(() => {
nextTick(() => {
inputRef.value.focus();
});
});
watch(() => data.url, () => {
data.loading = true;
nextTick(() => {
const iframe = pageRef.value.getElementsByTagName("iframe")[0];
iframe.onload = function () {
data.loading = false;
}
});
});
function changeUrl(event) {
const url = event.target.value;
data.url = url;
data.urls.push(url);
data.current = data.urls.length - 1;
inputRef.value.blur();
}
function reload() {
if (!data.url) {
return;
}
data.url = data.urls[data.current] + "&time" + Date.now();
}
</script>
<template>
<div class="page-container" @contextmenu.prevent ref="pageRef">
<div class="url-input">
<i :class="data.urls.length <= 0 || data.current === 0 ? ' gray-iconfont ' : ''" class="iconfont icon-last"
@click="last"></i>
<i :class="data.urls.length <= 0 || data.current === data.urls.length - 1 ? ' gray-iconfont ' : ''"
class="iconfont icon-next" @click="next"></i>
<i class="iconfont icon-refresh" @click="reload"></i>
<input ref="inputRef" spellcheck="false" @change="changeUrl($event)"/>
</div>
<div class="iframe-container">
<img v-show="!data.url" :src="require('@/css/background.png')" alt=""/>
<iframe v-show="data.url && !data.loading" :src="data.url"></iframe>
<img v-show="data.loading" :src="require('@/css/loading.gif')" alt="" class="loading"/>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "@/css/iconfont.css";
.page-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
.url-input {
height: 40px;
width: 100%;
background-color: #f7f7f7;
display: flex;
align-items: center;
padding-left: 6px;
.iconfont {
width: 40px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #000000;
&:hover {
background-color: #e4e4e4;
border-radius: 5px;
}
}
.gray-iconfont {
color: #cccccc;
}
.icon-next {
transform: rotate(180deg);
}
input {
width: 1300px;
height: 28px;
border-radius: 30px;
border: 1px solid #d1d1d1;
outline: none;
font-size: 14px;
padding: 0 14px;
color: #270057;
margin-left: 15px;
&:focus {
border: 1px solid #2169eb;
}
}
}
.iframe-container {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
.loading {
width: 100px;
height: 100px;
}
iframe {
border: none;
outline: none;
width: 100%;
height: 100%;
}
}
}
</style>
运行截图
默认首页
访问页面
开启新Tab