网址访问小工具(模拟浏览器)

网址访问小工具(模拟浏览器)

文章说明

本篇文章主要是我写的一个小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

源码下载

网址访问小工具

相关推荐
高山我梦口香糖34 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_7482352437 分钟前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing3 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600953 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js