首页
留言
友链
关于
Search
1
思源笔记docker私有化部署及使用体验分享
2,421 阅读
2
windows11 远程提示:为安全考虑,已锁定该用户帐户,原因是登录尝试或密码更改尝试过多。
1,110 阅读
3
Pointer-Focus:一款功能强大的教学、录屏辅助软件
615 阅读
4
解决 nginxProxyManager 申请证书时的SSL失败问题
610 阅读
5
使用cspell对项目做拼写规范检查
581 阅读
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
登录
Search
标签搜索
docker
DevOps
magic-boot
Linux
酷壳
RabbitMQ
gitlab
Node
git
工具
MybatisPlus
clickhouse
Syncthing
规范
前端
产品
nginx
markdown
axios
H5
朱治龙
累计撰写
139
篇文章
累计收到
7
条评论
首页
栏目
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
页面
留言
友链
关于
搜索到
43
篇与
Web前端
的结果
2024-09-30
Docker搭建一款功能强大、易于使用的图床程序和图像处理工具
软件介绍EasyImages是一款功能强大的图床程序和图像处理工具,它在多个方面展现出其独特的优势和特点,无论是作为个人用户还是开发者来说都是一个不错的选择。基本特点自托管特性 :EasyImages提供自托管功能,这意味着用户可以完全控制自己的图片数据和隐私,无需依赖第三方图床服务,从而避免了数据隐私风险、服务限制和图片加载延迟等问题。批量上传与多种URL形式 :支持批量上传图片,并能返回多种形式的URL,包括图片直链、Markdown格式URL、BBCCode、Html、缩略图等,满足用户在不同场景下的需求。在线图片管理 :提供一个直观的在线图片管理界面,用户可以方便地查看、编辑、组织和管理上传的图片,支持创建文件夹、排序图片、批量删除或打包图片等功能。上传限制与格式转换 :支持设置上传图片的最低宽度和高度,确保图片质量;同时,能够自动将上传的图片转换为不同的格式,以满足网站或应用的需求。水印功能:支持自动为上传的图片添加文字或图片水印,有助于保护图片版权和防止滥用。资源占用少 :对于安装环境和服务器性能要求非常低,甚至不需要数据库支持,使得在较小的服务器上也能轻松运行且不会占用过多资源。安全性与控制 :提供黑白名单上传功能,允许用户设置哪些IP地址或用户可以上传图片;同时,支持上传日志IP定位和限制每日上传次数等功能,以增强图床的安全性。数据统计与网站统计 :提供数据统计功能,帮助用户跟踪图片的上传情况、资源占用情况和存储使用情况等;此外,还支持网站统计功能,以提供更全面的信息。API支持 :提供了全面的API支持,使开发者能够在其项目中方便地上传和管理图片。图像处理功能除了作为图床程序外,EasyImages还具备强大的图像处理功能。它是一款轻量级且高效的JavaScript库,专为简化和加速图像处理而设计。基于HTML5的Canvas API构建,充分利用了浏览器的硬件加速能力,使得在处理大量或高分辨率的图片时也能保持流畅的性能。EasyImages提供了多种常用的图像操作功能,如缩放、裁剪、旋转、添加水印等。通过简洁的API接口,开发者可以快速地在自己的项目中集成并实现复杂的图片处理需求。此外,它还具有良好的社区支持和丰富的示例代码可供参考学习。相关连接官方网站:https://icret.github.io/EasyImages2.0/#/GitHub地址:https://github.com/icret/EasyImages2.0Docker镜像:https://hub.docker.com/r/ddsderek/easyimage应用部署根据官网推荐的方式,使用 docker compose 部署,编排文件内容如下:services: easyimage: image: ddsderek/easyimage:latest container_name: easyimage networks: - net-zzl ports: - 8113:80 environment: - TZ=Asia/Shanghai - PUID=1000 - PGID=1000 - DEBUG=false volumes: - ./config:/app/web/config - ./data:/app/web/i restart: unless-stopped networks: net-zzl: name: bridge_zzl external: true使用 docker compose up -d 启动服务后,进入如下图所示的安装环境检测页面:单击「下一步」按钮,进入「网站基础配置」页面:填写信息后,单击「开始安装」按钮,安装完成后进入下图所示的管理员登录页面:输入账号、密码后,进入如下图所示的主界面:
2024年09月30日
28 阅读
0 评论
0 点赞
2024-05-12
RabbitMQ学习:③基本使用
初始配置添加用户使用默认的 guest 账号登录后,可以在 Admin → Users中添加用户:添加 Virtual Host在Admin → Virtual Hosts 中添加虚拟机:给用户授权点击 Virtual Host 的名称,进入详情界面,可在Permissions中给新建的用户设置权限:建立连接1、新建Maven 项目2、导入依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.x2m</groupId> <artifactId>rabbitmq</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.21.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> </project>3、创建工具类连接 RabbitMQpublic static Connection getConnection() { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5672); factory.setUsername("zhuzl"); factory.setPassword("123456"); factory.setVirtualHost("/test"); Connection conn = null; try { conn = factory.newConnection(); } catch (IOException e) { throw new RuntimeException(e); } catch (TimeoutException e) { throw new RuntimeException(e); } return conn; }4、代码层面获取连接后,在管理界面可以看到如下连接信息:5、点击Name 可查看连接详情如下:6、调试界面的连接信息:
2024年05月12日
21 阅读
0 评论
0 点赞
2024-05-12
RabbitMQ学习:②基本概念
本文核心内容参加官网链接:https://www.rabbitmq.com/tutorials/amqp-conceptsPublisher:生产者(发布消息到R啊波比跳MQ中的Exchange)Consumer:消费者(监听RabbitMQ中的Queue中的消息)Exchange:交换机(和生产者建立连接并接收生产者的消息)Queue:队列(Exchange会将消息分发到指定的Queue,Queue和消费者进行交互)Routes:路由(交换机以什么样的策略将消息发布到 Queue)RabbitMQ 的完整架构图:
2024年05月12日
48 阅读
0 评论
0 点赞
2024-04-06
用户年度报告项目复盘
成果展示一图胜千言,我们先用视频来看实现好的最终效果,如果感兴趣我们再往下面了解技术实现细节。{gird column="2" gap="15"}{gird-item}{dplayer src="/usr/uploads/2024/07/1631300825.mp4"/}{mtitle title="并行版年度计算报告"/}预览网址(未对接接口版本,PC端直接访问会调整到扫码页面):http://para-yearly-report.work.zhuzhilong.com/{/gird-item}{gird-item}{dplayer src="/usr/uploads/2024/07/1449014954.mp4"/}{mtitle title="北龙版年度报告"/}预览网址(未对接接口版本,PC端直接访问会调整到扫码页面):http://blsc-yearly-report.work.zhuzhilong.com/{/gird-item}{/gird}缘起2023 年 12 月 20 日,网易云音乐 2023 年度听歌报告正式上线,一些是年度报告的部分截图:体验完网易云音乐的年度听歌报告后,感觉咱们也可以推出一款类似的报告,以下是跟运营同事的沟通截图:运营同事效率也是相当的高,第二天一早便梳理了我们年度报告的脑图:做好基础的准备工作后,当天跟领导确认了本项目的可行性:得到了领导的认可,就可以开展下一步的工作了。接着运营同事针对脑图的数据做了进一步的细化,梳理了需求文档:运营同事在需求文档中规划了每页展示的内容,下一步就可以交给设计的同事开展相关页面的设计工作了,为此专门拉了个小群沟通相关设计细节:不得不说负责设计的同事也是超级给力,很快就提供了高质量的设计稿:而且超级专业地提供了相关页面的动效设计说明:开干编写设计文档需求有了,设计也有了,我们就可以开工干活了。编码工作之前,我们需要根据需求和设计稿,进行技术相关的详细设计文档编写工作,这样才能更好的指导后续的编码开发工作,为此我详细编写了40页超过4000字的详细设计文档:规划年报数据在上面的详设文档中详细的规划了每一页所需的数据,每一页数据的来源,以及每个用户年报数据的组织形式、存储方式。有了这些数据做支撑后,就是怎么将这些数据按运营要求收集起来,由于数据分散在不同的业务系统里,如用户系统、计费系统、用户行为系统。需要将相关的数据收集后并归总,为此,我们设计了数据合并机制。用户年报页面实现现在需要的数据也有了,只需要将年报页面按照 UI 效果进行实施即可。由于整体项目采用前后端分离的机制,用户看到的年报页面也采用独立的 git 工程进行维护,完成代码后单独打包部署即可。整个前端页面,使用 vite 进行工程构建,使用 vue3 进行界面驱动,使用 swiper + vant-ui 实现基本页面布局,使用 echarts + vue-echarts 实现工程的图表效果,使用 animate.css 实现界面动效,工程的package.json 信息如下:{ "name": "console-yearlyreport-ui", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "dev:blsc": "vite --mode blsc.development", "devIstio": "vite build --mode devIstio", "devIstio:blsc": "vite build --mode blsc.devIstio", "stage": "vite build --mode stage", "stage:blsc": "vite build --mode blsc.stage", "build": "vite build", "build:report": "vite build", "build:blsc": "vite build --mode blsc.production" }, "dependencies": { "animate.css": "^4.1.1", "axios": "0.21.4", "dayjs": "^1.11.10", "echarts": "^5.4.3", "lodash": "^4.17.21", "pinia": "^2.1.7", "swiper": "^11.0.5", "vant": "^4.8.3", "vue": "^3.3.11", "vue-echarts": "^6.6.8" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.2", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.70.0", "terser": "^5.27.0", "vite": "^5.0.8", "vite-plugin-compression": "^0.5.1" } }前端工程的整体目录结构:下面就一些核心代码做一些梳理main.js该文件为项目入口文件,主要对环境做了些特殊处理,如未登录则跳转到登录页;PC端访问则跳转到扫码页面,引导用户使用手机扫码访问等。import { createApp } from 'vue' import { createPinia } from 'pinia' import 'animate.css' import './style.css' import '@/styles/index.scss' import { registerComponents } from './components/index' import initMatomo from './core/matomo/index.js' import App from './App.vue' const { VITE_APP_DIST_MODE: distMode } = import.meta.env /** * 检测浏览器环境,如果是PC端浏览器访问活动页则直接重定向到二维码扫码页面 */ const checkBrowser = () => { // 根据user-agent 判断是否跳转到pc页面 if (window.matchMedia && window.matchMedia("(min-width: 768px)").matches) { window.location.href = `pc_${distMode}.html` return false } return true } /** * 检测用户登录情况,如果localStorage 中不包含 token 则自动重定向到移动端授权页面 */ const checkLogin = () => { const token = localStorage.getItem('token') if (!token) { localStorage.setItem('authForExternalPage', `/yearly-report-2023/` + location.search) location.href='/auth' return false } return true } const checkEnv = async () => { if (!checkBrowser()) { return } // // 检测用户登录情况 if (!checkLogin()) { return } const pinia = createPinia() const app = createApp(App) // 注册全局组件 registerComponents(app) app.use(pinia) app.mount('#app') initMatomo(app, null) // 根节点添加样式名,便于对部分样式根据主题进行灵活控制 $('html').addClass(`theme-${distMode}`) } checkEnv() App.vue这是页面入口的核心文件,在这个文件里使用swiper组织了所有页面,并根据各种不同的条件做了不同展示处理(未登录、资源预加载时显示加载中、加载完成后展示封面页;封面页未勾选协议则无法滑动等); 并从整体上收集用户行为数据,完整代码如下:<template> <div v-if="existReportData === false"> <not-exist-data-page /> </div> <div v-else-if="pageResourceLoading" class="page-loading"> <van-circle v-model:current-rate="resourceLoadPercent" :rate="100" :speed="100" size="180px" :layer-color="themeColors.themeLight6" :color="gradientColor" :stroke-width="50" :text="resourceLoadPercent + '%'" /> <p class="loading-tips-text">页面加载中,请稍候</p> <!-- <van-progress :stroke-width="8" style="width: 90%" color="linear-gradient(to right, #be99ff, #7232dd)" :percentage="resourceLoadPercent" /> --> </div> <swiper v-else :slides-per-view="1" :direction="'vertical'" :space-between="0" effect="fade" @swiper="onSwiper" @slideChange="onSlideChange" @slideChangeTransitionEnd="onSlideChangeTransitionEnd" > <swiper-slide class="swiper-page0" data-page-name="封面页"><enter-page /></swiper-slide> <swiper-slide class="swiper-page1" data-page-name="第一页"><page1 /></swiper-slide> <swiper-slide class="swiper-page2" data-page-name="第二页"><page2 /></swiper-slide> <!-- <swiper-slide class="swiper-page3" data-page-name="第三页"><page3 /></swiper-slide> --> <swiper-slide class="swiper-page4" data-page-name="第四页"><page4 /></swiper-slide> <swiper-slide class="swiper-page5" data-page-name="第五页"><page5 /></swiper-slide> <swiper-slide class="swiper-page6" data-page-name="第六页"><page6 /></swiper-slide> </swiper> <img v-show="showUpArrow" src="/images/icon_arrow_up.png" class="arrow-up animate__animated animate__infinite animate__fadeInUp animate__slower" alt="提示上划" /> </template> <script setup> import { defineComponent, onMounted, ref, watch } from "vue"; import { Swiper, SwiperSlide } from "swiper/vue"; import "swiper/css"; import { checkReportData } from './api/yearlyReportAPI' import EnterPage from "./pages/EnterPage.vue"; import NotExistDataPage from "./pages/NotExistDataPage.vue"; import Page1 from "./pages/Page1.vue"; import Page2 from "./pages/Page2.vue"; // import Page3 from "./pages/Page3.vue"; import Page4 from "./pages/Page4.vue"; import Page5 from "./pages/Page5.vue"; import Page6 from "./pages/Page6.vue"; import { imagesLoad } from './core/imgLoad.min.js' import resourceImgs from './assets/imgResources.json' import themeColors from '@/core/themeColors.js' import { getShortUserId, getQueryString } from './core/utils.js' defineComponent({ components: { Swiper, SwiperSlide, }, }); const existReportData = ref() // 页面资源加载 const pageResourceLoading = ref(true) const resourceLoadPercent = ref(0) const gradientColor = { '0%': themeColors.themeLight3, '100%': themeColors.theme, } window.currentSwiper = null; const currentSwiperIndex = ref(0); const showUpArrow = ref(false); watch(currentSwiperIndex, () => { if (window.currentSwiper === null) { return; } if (currentSwiperIndex.value === 0) { window.currentSwiper.disable(); } // 封面页和最后一页不显示向上箭头 if (currentSwiperIndex.value === 0 || currentSwiperIndex.value === window.currentSwiper.slides.length - 1) { showUpArrow.value = false; return; } showUpArrow.value = true; }); const clearAnimation = () => { const prevSlideEl = window.currentSwiper.slides[window.currentSwiper.previousIndex]; $(prevSlideEl) .find(".js-animation") .each(function (idx, item) { const $this = $(item); const animation = $this.attr("data-animation"); $this .removeClass("animate__animated") .removeClass("animate__" + animation) .removeAttr("style"); }); }; const addAnimation = () => { clearAnimation(); const currentSlideEl = window.currentSwiper.slides[window.currentSwiper.activeIndex]; $(currentSlideEl) .find(".js-animation") .each(function (index, item) { var $this = $(item); var animation = $this.attr("data-animation"); var duration = $this.attr("data-duration"); var delay = $this.attr("data-delay"); $this .addClass("animate__animated") .addClass("animate__" + animation) .css({ "animation-duration": duration + "s", "animation-delay": delay + "s" }); }); // 入口页灯效单独处理 if (currentSwiperIndex.value === 0) { const $el = $('.swiper-page0 .bg-light') if ($el.hasClass('hide')) { setTimeout(() => { $el.removeClass('hide') }, 1500) } } }; /** * 添加Matomo 事件埋点 */ const addMatomoEvent = () => { const pageName = $('.swiper-slide-active').attr('data-page-name') if (pageName) { window._paq && window._paq.push(['trackEvent', '2023年度报告', '查看', pageName]) } } /** * 根据URL参数添加用户来源埋点 */ const addSourceEvent = () => { const sourceMap = { direct: '直接访问', cloud: '云桌面', console: 'PC端控制台', mconsole: '移动端控制台', kbs: 'KBS内容页', email: '邮件海报' } const sourceInQuery = getQueryString('utm_source') let fromLabel = '直接访问' if (sourceInQuery) { const sourceLabel = sourceMap[sourceInQuery] if (sourceLabel) { fromLabel = sourceLabel } } window._paq && window._paq.push(['trackEvent', '2023年度报告', '来源', fromLabel]) } const onSwiper = (s) => { addMatomoEvent() addSourceEvent() // 默认不允许用户滑动,仅同意协议并单击「点击回顾」按钮后才可滑动 s.disable(); if (window.currentSwiper === null) { window.currentSwiper = s; } addAnimation(); }; const onSlideChange = () => { currentSwiperIndex.value = window.currentSwiper.activeIndex; addAnimation(); if (currentSwiperIndex.value === 1) { $('.swiper-page0 .bg-light').addClass('hide') } }; const onSlideChangeTransitionEnd = () => { addMatomoEvent() } onMounted(async () => { // 检测用户数据 const checkRes = await checkReportData() if (checkRes.exist === false) { existReportData.value = false window._paq && window._paq.push(['trackEvent', '2023年度报告', '查看', '无数据页']) return } if (checkRes.lastUpdateTime) { localStorage.setItem('yearlyReportDataUpdateTime_' + getShortUserId(), checkRes.lastUpdateTime) } new imagesLoad(resourceImgs, function (num) { resourceLoadPercent.value = num }, function () { resourceLoadPercent.value = 100 setTimeout(() => { pageResourceLoading.value = false }, 300) }) }) </script> <style lang="scss" scoped> .page-loading { width: 100%; height: 100vh; padding-bottom: 20vh; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; background: $theme-light-8; // background-color: #F8FDFF; } .loading-tips-text { font-size: 16px; color:$theme-light-6; font-weight: bold; text-align: center; } .swiper { width: 100%; height: 100vh; } .swiper-slide { height: 100vh; } .arrow-up { position: absolute; width: 30px; margin-left: -15px; left: 50%; bottom: 30px; z-index: 100; pointer-events: none; } </style> 优先从本地缓存获取用户年报数据用户年度计算报告的数据属于生成后基本上不会怎么变化的数据,而这种活动一般也是短期的,为减少活动推广期间可能出现的高并发场景对服务器资源的损耗,拟将用户指标数据在第一次获取后缓存在客户端,客户端存在用户缓存数据的情况下将直接使用缓存数据进行界面展示,流程如下图所示:核心代码如下:/** * 截取部分userId用于作为用户特征串 */ export function getShortUserId() { const userId = localStorage.getItem('userid') if (userId) { if (userId.length > 40) { return userId.substring(0, 40) } return userId } } /** * 获取用户年报数据,优先从缓存里取 */ export async function getUserReportData() { const shortUserId = getShortUserId() const reportDataInLocal = localStorage.getItem('yearlyReportData_' + shortUserId) if (reportDataInLocal) { const reportDataObj = JSON.parse(reportDataInLocal) const lastUpdateTimeInLocal = reportDataObj.lastUpdateTime const lastUpdateTimeFromServer = localStorage.getItem('yearlyReportDataUpdateTime_' + shortUserId) if(lastUpdateTimeInLocal === lastUpdateTimeFromServer) { return reportDataObj.reportData } } const res = await getReportData() if (res.reportData) { res.reportData = JSON.parse(res.reportData) localStorage.setItem('yearlyReportData_' + shortUserId, JSON.stringify(res)) } return res.reportData }UI 效果以下图片为实际实现好的界面截图并行版本{gird column="4" gap="15"}{gird-item}{mtitle title="资源预加载"/}{/gird-item}{gird-item}{mtitle title="封面页"/}{/gird-item}{gird-item}{mtitle title="第一页"/}{/gird-item}{gird-item}{mtitle title="第二页"/}{/gird-item}{gird-item}{mtitle title="第二页查看机时"/}{/gird-item}{gird-item}{mtitle title="第三页"/}{/gird-item}{gird-item}{mtitle title="第四页(0点-6点)"/}{/gird-item}{gird-item}{mtitle title="第四页(6点-12点)"/}{/gird-item}{gird-item}{mtitle title="第四页(12点-18点)"/}{/gird-item}{gird-item}{mtitle title="第四页(18点-24点)"/}{/gird-item}{gird-item}{mtitle title="第五页"/}{/gird-item}{gird-item}{mtitle title="第六页"/}{/gird-item}{/gird}北龙版本{gird column="4" gap="15"}{gird-item}{mtitle title="资源预加载"/}{/gird-item}{gird-item}{mtitle title="封面页"/}{/gird-item}{gird-item}{mtitle title="第一页"/}{/gird-item}{gird-item}{mtitle title="第二页"/}{/gird-item}{gird-item}{mtitle title="第二页查看机时"/}{/gird-item}{gird-item} {mtitle title="第三页"/}{/gird-item}{gird-item}{mtitle title="第四页(0点-6点)"/}{/gird-item}{gird-item}{mtitle title="第四页(6点-12点)"/}{/gird-item}{gird-item}{mtitle title="第四页(12点-18点)"/}{/gird-item}{gird-item}{mtitle title="第四页(18点-24点)"/}{/gird-item}{gird-item} {mtitle title="第五页"/}{/gird-item}{gird-item} {mtitle title="第六页"/}{/gird-item}{/gird}运营后台为了便于运营人员核对用户指标数据及预览用户年报效果,我们使用 MagicBoot 快速开发了一个运营后台,相关界面效果如下:{gird column="2" gap="15"}{gird-item}{mtitle title="活动用户列表界面"/}{/gird-item}{gird-item}{mtitle title="查看用户指标数据,供运营核对指标数据"/}{/gird-item}{gird-item}{mtitle title="运营人员可扫码预览用户年度计算报告"/}{/gird-item}{gird-item}{mtitle title="后台年度报告预览效果"/}{/gird-item}{/gird}运营大屏为了便于运营人员及时了解用户访问大屏情况,我们使用 DataGear 制作了一个运营大屏,界面效果如下:详设文档本活动的技术开发工作能顺利开展,前期的详细设计是重中之重,详细设计中把方方面面的问题都考虑到了,在开发和上线阶段才能做到从容不迫,以下是详设文档附件(由于部分材料涉密,该文档仅部分人员可访问): 以下内容已加密,请输入密码查看:
2024年04月06日
40 阅读
0 评论
1 点赞
2023-09-03
超全的正则表达式速查手册
一、校验数字的表达式数字:^[0-9]*$n位的数字:^\d{n}$至少n位的数字:^\d{n,}$m-n位的数字:^\d{m,n}$零和非零开头的数字:^(0|[1-9][0-9]*)$非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})?$正数、负数、和小数:^(-|+)?\d+(.\d+)?$有两位小数的正实数:^[0-9]+(.[0-9]{2})?$有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$非零的正整数:^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$或 ^\+?[1-9][0-9]*$非零的负整数:^-[1-9][]0-9″$ 或 ^-[1-9]\d$ 非负整数:^\d+$ 或 ^[1-9]\d*|0$非正整数:^-[1-9]\d*|0$或 ^((-\d+)|(0+))$非负浮点数:^\d+(.\d+)?$或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))$或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$正浮点数:^[1-9]\d.\d|0.\d[1-9]\d$或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$负浮点数:^-([1-9]\d.\d|0.\d[1-9]\d)$或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$浮点数:^(-?\d+)(.\d+)?$或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$二、校验字符的表达式汉字:^[\u4e00-\u9fa5]{0,}$英文和数字:^[A-Za-z0-9]+$或 ^[A-Za-z0-9]{4,40}$长度为3-20的所有字符:^.{3,20}$由26个英文字母组成的字符串:^[A-Za-z]+$由26个大写英文字母组成的字符串:^[A-Z]+$由26个小写英文字母组成的字符串:^[a-z]+$由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$可以输入含有^%&’,;=?$” 等字符:[^%&’,;=?$\x22]+禁止输入含有~的字符: [^~\x22]+其它.匹配除 n 以外的任何字符。/[\u4E00-\u9FA5]/ 汉字/[\uFF00-\uFFFF]/ 全角符号/[\u0000-\u00FF]/ 半角符号三、特殊需求表达式Email 地址:1^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)$1域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?InternetURL:[a-zA-z]+://[^\s] 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=])?$手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$电话号码(“XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX):^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}身份证号(15位、18位数字):^\d{15}|\d{18}$短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.\d)(?=.[a-z])(?=.*[A-Z]).{8,10}$日期格式:^\d{4}-\d{1,2}-\d{1,2}一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$钱的输入格式有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]*$这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$这样就允许用户只写一位小数.下面我们该考虑数字中的 逗号 了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$备注:这就是最终结果了,别忘了+可以用*替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+.[x|X][m|M][l|L]$中文字符的正则表达式:[\u4e00-\u9fa5]双字节字符:^\x00-\xff)空白行的正则表达式:\n\s*\r (可以用来删除空白行)HTML标记的正则表达式:<(\S?)[^>]>.?</\1>|<.? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)首尾空白字符的正则表达式:^\s|\s (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)腾讯QQ号:[1-9][0-9]{4,}(腾讯QQ号从10000开始)中国邮政编码:[1-9]\d{5}(?!\d)(中国邮政编码为6位数字)IP地址:\d+.\d+.\d+.\d+(提取IP地址时有用)IP地址:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d).){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))IP-v4地址:\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b (提取IP地址时有用)校验IP-v6地址: (([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))子网掩码:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))校验日期:^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$(“yyyy-mm-dd“ 格式的日期校验,已考虑平闰年。)抽取注释:<!–(.*?)–>查找CSS属性:^\s[a-zA-Z\-]+\s[:]{1}\s[a-zA-Z0-9\s.#]+[;]{1}提取页面超链接:(<a\s(?!.\brel=)[^>])(href=”https?:\/\/)((?!(?:(?:www\.)?’.implode(‘|(?:www\.)?’, $follow_list).’))[^” rel=”external nofollow” ]+)”((?!.\brel=)[^>])(?:[^>])>提取网页图片:\< [img][^\\>][src] = [\”\’]{0,1}([^\”\’\ >]*)提取网页颜色代码:^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$文件扩展名效验:^([a-zA-Z]\:|\\)\\([^\\]+\\)[^\/:?”<>|]+\.txt(l)?$判断IE版本:^.MSIE 5-8?(?!.Trident\/[5-9]\.0).*$
2023年09月03日
20 阅读
0 评论
0 点赞
2023-07-19
使用cspell对项目做拼写规范检查
项目中的拼写问题主要涉及变量命名、方法名、样式名等字符串的规范,使用cspell能在很大程度上减少由于输入或记错导致的错误单词的出现,也能让命名更规范更易于理解。这篇文字是在我们已有项目的实践。
2023年07月19日
581 阅读
0 评论
0 点赞
2023-06-24
将vue-cli搭建的Vue前端工程更新构建工具为pnpm+vite经验分享
缘起我们团队负责的超算云服务控制台前端工程(console-ui)从提交第一行代码(2021-03-18)至今已 2 年多时间,两年多的时间内随着需求的增加,前端工程也越来越庞大,陈旧的基础设施暴露出了如下问题:1、Node.js 版本仍然使用 2021 年发布的 14.x,而最新的 Node 大版本已更新到20.x,仍然使用旧版本导致一些构建工具版本不能随意更新,也同时留下一定的安全隐患。2、项目目前仍使用两年前发行的 Webpack4.46.0 版本作为项目构建工具,随着项目逐渐庞大,Webpack 在开发时编译项目时间通常达到 2 分钟以上,严重影响开发效率。3、前端生态日新月异,守着老旧的生态,不便于团队成长。借本次升级 console-ui 核心 vue版本的契机,考虑同时升级到新的构建工具。Vite 从 2021 年发布以来,在构建速度及开发体验上一直口碑不错,经过两年多的迭代,也趋于稳定,完全可以应用于产线了。且根据官方的介绍 Vite 4.3 开始的版本比之前的版本在开发及构建速度上又有了成倍的提升:发行日志见链接:https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md开始折腾1、升级 Node.js目前 console-ui 构建还是用的Node.js 14.x, 而现在 Node.js 的 LTS 版本是18.x,按照 Node.js 的版本发行规律,预计目前最新的 20.x 版本在今年的 10 月份将成为下一个 LTS 版本,且 Node.js 的向下兼容性较强,所以目前咱们可以拥抱 Node.js 20.x 相关生态。所以本次升级就直接选用 Node.js 20.x 了。开发机是使用nvm管理 Node.js 多版本的。使用如下命令安装并使用Node.js 20:nvm install 20.3.0 nvm use 20.3.02、升级构建工具在升级过程如下:创建初始工程使用官方开始页面提供的命令初始化一个初始工程:pnpm create vite console-ui --template vue3、console-ui 已有功能最简验证3.1、支持多主题console-ui的设定是可以打包不同主题的页面,构建时根据 env 变量打包不同主题的版本,然后采用不同主题文件。经验证vite也支持,vite.config.js 核心配置代码如下:css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/themes/${env.VITE_APP_DIST_MODE}.scss";` } } }由于不同的主题有些不同的资源文件(如favicon.ico、主题相关的图片等)需要引用,在这里我们在public 目录下新建2个目录用于管理不同主题资源文件的管理,然后配置不同的publicDir:publicDir: `./public/${distMode}`,3.2、验证多页面支持console-ui提供针对普通用户的 UI 界面,同时也提供管理员使用的维护界面,这两套界面在原工程是使用多页面机制实现的。经验证,vite 也能很好的支持,核心配置代码如下:plugins: [ createHtmlPlugin({ minify: true, pages: [ { entry: '/src/main.js', filename: 'index.html', template: 'index.html', injectOptions: { data: { title: '超算云服务控制台' } } }, { entry: 'src/pages/admin/index.js', filename: 'admin.html', template: 'admin.html', injectOptions: { data: { title: '超算运服务控制台-系统维护' } } } ] }) ]4、开始整合整合时会涉及一些资源文件引用的改造,一般改动会比较多,为不影响主分支开发,建议单独开个分支处理。整合过程比较繁琐,就不一一罗列了,主要涉及如下事项:替换 ENV 变量:vue-cli 工程的环境变量都是以Vue_APP 开头的变量,可以全局替换为VITE_APP开头,而且 Webpack 工程一般使用 process.env.XXX 访问ENV变量,而 Vite 是使用 import.meta.env.XXX 访问 ENV 变量,为了少改动,可以使用如下方式继续使用process.env.XXX 方式访问:define: { 'process.env': { ...env } }调整资源引用问题:Webpack 项目多数使用require('xxx')的方式在 JS 里引入资源,确实特别的方便,但是这一套在 Vite支持不是很好,即便有插件支持(如vite-plugin-commonjs)但依然没有 Webpack 的 require 机制强大。一些不需要参与打包的资源可以放在 public 目录,然后使用URL路径访问,参与打包的文件,可改为 import XXX from 'xxxx.png'的方式引入整体改造过程提交记录截图如下:优化整合后发现打包的文件比原 Webpack 打包的 dist 目录更大,前端工程层面的优化,主要以从以下几点着手处理:压缩:可采用vite-plugin-compression插件对打包后的文件进行了压缩处理静态资源上CDN:优化过程主要使用rollup-plugin-visualizer 对打包后的文件进行分析,对一些占用打包空间较大的文件使用CDN引入的方式进行优化按需引入:如lodash、moment.js等,尽量按需引入,减少非必须的语言包等,能很大程度的减小打包体积。附上本次改造核心的完整 vite.config.js 文件内容:import path from 'path' import vitePluginCommonjs from 'vite-plugin-commonjs' import { createHtmlPlugin } from 'vite-plugin-html' import { visualizer } from 'rollup-plugin-visualizer' import viteCompression from 'vite-plugin-compression' import externalGlobals from 'rollup-plugin-external-globals' // import eslintPlugin from 'vite-plugin-eslint' import { defineConfig, loadEnv } from 'vite' import legacy from '@vitejs/plugin-legacy' import vue2 from '@vitejs/plugin-vue2' export default ({ mode }) => { const env = loadEnv(mode, process.cwd()) const distMode = env.VITE_APP_DIST_MODE || 'paratera' const cdn = { scripts: mode.includes('development') ? [] : [ 'https://statics.paratera.com/console/libs/vue.2.7.14.min.js', // 'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js', // 'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js', 'https://statics.paratera.com/console/libs/element-ui.2.15.13.min.js', 'https://statics.paratera.com/console/libs/exceljs.4.3.0.min.js' ] } // https://vitejs.dev/config/ const viteConfig = { base: '/', publicDir: `./public/${distMode}`, define: { 'process.env': { ...env } }, plugins: [ vue2(), // eslintPlugin(), createHtmlPlugin({ minify: true, pages: [ { entry: '/src/main.js', filename: 'index.html', template: 'index.html', injectOptions: { data: { title: '超算云服务控制台', cdn }, tags: [ { injectTo: 'body-prepend', tag: 'div', attrs: { id: 'tag1' } } ] } }, { entry: 'src/pages/admin/index.js', filename: 'admin.html', template: 'admin.html', injectOptions: { data: { title: '超算运服务控制台-系统维护', cdn // injectScript: `<script src="./inject.js"></script>`, } } } ] }), vitePluginCommonjs(), legacy({ targets: ['ie >= 11'], additionalLegacyPolyfills: ['regenerator-runtime/runtime'] }), viteCompression({ verbose: true, // 是否在控制台输出压缩结果 disable: false, // 是否禁用,相当于开关在这里 threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b,1b=8B, 1B=1024KB 那我们这里相当于 9kb多吧,就会压缩 algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] ext: '.gz' // 文件后缀 }) ], resolve: { extensions: ['.js', '.vue', '.json'], alias: [ { find: /@\/.+/, replacement: (val) => { return val.replace(/^@/, path.resolve(__dirname, './src/')) } }, { // this is required for the SCSS modules find: /^~(.*)$/, replacement: '$1' } ] }, css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/themes/${distMode}.scss";` } } }, server: { port: 8686 }, build: { // target: 'es2015', // cssTarget: 'chrome80', // brotliSize: false, // chunkSizeWarningLimit: 2000, // commonjsOptions: { // // 改为 ture 后就会转化 require 语法 // transformMixedEsModules: true // }, rollupOptions: { input: { index: path.resolve(process.cwd(), 'index.html'), admin: path.resolve(process.cwd(), 'admin.html') }, external: [ // 'vue', 'element-ui', 'exceljs' ], plugins: [ externalGlobals({ vue: 'Vue', // 'vue-router': 'VueRouter', // vuex: 'Vuex', 'element-ui': 'ELEMENT', 'exceljs': 'ExcelJS' }) ], output: { chunkFileNames: 'assets/js/chunks/[name].[hash].js', assetFileNames: (chunkInfo) => { // 用后缀名称进行区别处理 // 处理其他资源文件名 e.g. css png 等 let subDir = 'images' const extName = path.extname(chunkInfo.name) if (['.css'].includes(extName)) { subDir = 'css' } if (['.woff', '.ttf'].includes(extName)) { subDir = 'fonts' } return `assets/${subDir}/[name].[hash].[ext]` }, // 入口文件名 entryFileNames: 'assets/js/[name].[hash].js' } } } } const lifecycle = process.env.npm_lifecycle_event if (lifecycle.includes(':report')) { viteConfig.plugins.push(visualizer({ open: true, brotliSize: true, gzipSize: true, filename: './dist/report.html' })) } return defineConfig(viteConfig) } 升级前后 package.json 比对:成效对比项优化前(Webpack)优化后(Vite)安装依赖66.60s17.2开发启动110228ms2536ms打包部署123.77s1m 57s对比截图安装依赖:Webpack ↓Vite ↓开发启动:Webpack ↓Vite ↓打包截图:Webpack ↓Vite ↓
2023年06月24日
46 阅读
0 评论
0 点赞
2023-06-10
随动帮助文档实现
背景介绍我们项目提供了在线的项目文档,便于用户在使用过程中有什么问题可以通过直接查文档解决,也在一定程度上减少了公司运营及客服成本。在实际使用过程中,用户反馈造作文档的使用不是很方便,基本上要打开新Tab页签,不能方便的一遍对照文档,一边操作。解决方案为解决以上问题,在参考阿里云及腾讯云相关产品的文档模式后,引入了随动帮助文档机制。该机制主要实现模式如下:1、在相关功能指定位置添加文档入口2、单击文档名称后,在页面左下角显示文档弹出层(一下简称「文档查看器」),主要是系统的大部分功能使用右侧展开的抽屉组件 (Drawer)进行显示。3、打开的文档查看器,可垂直方向最大,可上下左右四边及四个角落通过拖拽调整大小4、可拖拽待页面任意位置5、文档查看器不影响页面中其他组件的交互该功能是系统每个功能模块都有可能会用到的,所以将起提取为一个公共组件。组件只需传入功能标识,然后调用内容管理系统的接口获取数据,如果有数据则显示文档入口,由于一个功能极有可能会对应有多篇帮助文档,用户从文档入口单击将展开文档列表,用户单击需要查看的文档,则在左下角显示文档。实现过程示例中均使用模拟的数据,实际数据在生产环境调用内容管理系统的接口获取。实现文档查看器组件HelpModal.vue文档查看器的拖拽及大小调整均通过指令控制<template> <el-dialog ref="dlg" v-dragAndResize :modal="false" top="auto" :visible.sync="isShow" custom-class="help-modal-wrap" width="400px" :close-on-click-modal="false" append-to-body > <div slot="title" class="dialog-title"> <h3>功能指南</h3> <div class="panel-actions"> <em class="panel-action console-svg svg-vertical-expand margin-right-xs" :class="{ 'svg-vertical-expand': !verticalMaxed, 'svg-vertical-collapse': verticalMaxed }" :title="verticalMaxed ? '还原' :'垂直最大化'" @click="setDlgMaxHeight" /> </div> </div> <div class="dlg-body-inner"> <div class="content-wrap"> <h2 class="article-title"> 如何使用一键转区 </h2> <div class="article-subtitle"> <span class="article-time">更新时间:2022-08-09 10:10</span> </div> <div class="article-content"> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 可使用【一键转区】功能,将超算账号的数据转移至指定的超算上进行使用,如超算中心A3分区的sc0001账号(即为源目标)转移至超算分区A6的sc0001(即为目的目标),具体操作如下:</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 1. 单击【一键转区】中的【申请一键转区】</span> </p> <p> <span style="font-size:14px"><img src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100845_a3269g5dky.png" title="" alt="image.png" ></span> </p> <p> <span style="font-size:14px"><img src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100855_atmxepobl8.png" title="" alt="image.png" ></span> </p> <p><span style="font-size:14px"> </span></p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:14px"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 2. 在【源目标】处选择可用的超算分区及超算账号,例如A3超算分区的sc0001;</span></span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 在【目的目标】处选择匹配的超算分区,不用选择转区后的超算账号,例如A6超算分区;</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 在【源目标账号环境信息】处填软件使用时的环境要求。</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 3. 单击【确定】,会有提示框告知源目标超算账号保存3个月,3个月后超算账号回收的提醒。</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 4. 在【一键转区】的页面查看工单进展。</span> </p> </div> </div> <div class="outer-link-wrap"> <el-button type="primary" class="btn-open-in-window" @click="openDocLink"> 新窗口查看文档 </el-button> </div> </div> </el-dialog> </template> <script> import { setStyle, on, off, addClass } from 'element-ui/lib/utils/dom' export default { name: 'HelpModal', directives: { dragAndResize: { bind(el, binding, vnode, oldVnode) { addClass(el, 'dlg-wrap-help') // 弹框可拉伸最小宽高 const minWidth = 300 const minHeight = 300 // 获取弹框头部(这部分可双击全屏) const dialogHeaderEl = el.querySelector('.el-dialog__header') // 弹窗 const dragDom = el.querySelector('.el-dialog') dragDom.className = dragDom.className + ' dialog-can-resize' // // 给弹窗加上overflow auto;不然缩小时框内的标签可能超出dialog; // dragDom.style.overflow = 'auto' // 清除选择头部文字效果 dialogHeaderEl.onselectstart = new Function('return false') // 头部加上可拖动cursor dialogHeaderEl.style.cursor = 'move' // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null) // 初始设置在页面左下角 const initialHeight = window.innerHeight * 2 / 3 setStyle(dragDom, 'height', initialHeight + 'px') setStyle(dragDom, 'top', window.innerHeight * 1 / 3 - 10 + 'px') const moveDown = (e) => { e.preventDefault() // 鼠标按下,计算当前元素距离可视区的距离 const disX = e.clientX - dialogHeaderEl.offsetLeft const disY = e.clientY - dialogHeaderEl.offsetTop // 获取到的值带px 正则匹配替换 let styL, styT // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px if (sty.left.includes('%')) { styL = +document.body.clientWidth * (+sty.left.replace(/%/g, '') / 100) styT = +document.body.clientHeight * (+sty.top.replace(/%/g, '') / 100) } else { styL = +sty.left.replace(/px/g, '') styT = +sty.top.replace(/px/g, '') } document.onmousemove = function(e) { e.preventDefault() // 通过事件委托,计算移动的距离 const l = e.clientX - disX const t = e.clientY - disY // 移动当前元素 dragDom.style.left = `${l + styL}px` dragDom.style.top = `${t + styT}px` // 将此时的位置传出去 // binding.value({x:e.pageX,y:e.pageY}) } document.onmouseup = function(e) { document.onmousemove = null document.onmouseup = null } } on(dialogHeaderEl, 'mousedown', moveDown) // 添加位置调整锚点 const buildResizeElements = (dragDom) => { const directions = ['top', 'right', 'bottom', 'left', 'top-right', 'top-left', 'bottom-right', 'bottom-left'] for (const direction of directions) { const resizeEl = document.createElement('div') resizeEl.className = `resize-handle resize-${direction}` resizeEl.setAttribute('resize', direction) dragDom.appendChild(resizeEl) } } buildResizeElements(dragDom) let moveOpts = null on(document, 'mousedown', (event) => { if (event.which !== 1 && event.type === 'mousedown') { return } const { target } = event if (!target.className.includes('resize-handle')) { return } const resizeDirection = target.getAttribute('resize') moveOpts = { resizeDirection: resizeDirection, clientX: event.clientX, clientY: event.clientY, startWidth: dragDom.offsetWidth, startHeight: dragDom.offsetHeight, offsetLeft: dragDom.offsetLeft, offsetTop: dragDom.offsetTop } const screenWidth = window.innerWidth const screenHeight = window.innerHeight const mouseMoveHandler = (event) => { event.preventDefault() const style = dragDom.style const resizeDirection = moveOpts.resizeDirection const { startWidth, startHeight } = moveOpts let left = moveOpts.offsetLeft let top = moveOpts.offsetTop let width = moveOpts.startWidth let height = moveOpts.startHeight let clientX = event.clientX let clientY = event.clientY let x = (clientX = screenWidth <= clientX ? screenWidth : clientX) <= 0 ? 0 : clientX let y = (clientY = screenHeight <= clientY ? screenHeight : clientY) <= 0 ? 0 : clientY x = event.clientX - moveOpts.clientX y = event.clientY - moveOpts.clientY switch (resizeDirection) { case 'top': top = y + top height = -y + height break case 'right': width = x + width break case 'bottom': height = y + height break case 'left': left = x + left width = -x + width break case 'top-left': left = x + left top = y + top width = -x + width height = -y + height break case 'top-right': top = y + top width = x + width height = -y + height break case 'bottom-right': width = x + startWidth height = y + startHeight break case 'bottom-left': left = x + left width = -x + startWidth height = y + startHeight } left = left <= 0 ? 0 : left top = top <= 0 ? 0 : top style.left = left + 'px' style.top = top + 'px' style.width = Math.max(minWidth, width) + 'px' style.height = Math.max(minHeight, height) + 'px' } const mouseUpHandler = (event) => { console.error('mouseUp', event) off(document, 'mousemove', mouseMoveHandler) off(document, 'mouseup', mouseUpHandler) } on(document, 'mousemove', mouseMoveHandler) on(document, 'mouseup', mouseUpHandler) }) } } }, props: { show: { type: Boolean, required: true, default: false }, docId: { type: Number, required: true } }, data() { return { originStyle: null, verticalMaxed: false } }, computed: { isShow: { get() { return this.show }, set(val) { this.$emit('update:show', val) } } }, created() { this.init() }, methods: { async init() { }, setDlgMaxHeight() { const $dlg = this.$refs['dlg'] console.log('dlg', $dlg) const dlgEl = $dlg.$el.childNodes[0] const dlgStyle = dlgEl.style if (this.verticalMaxed) { setStyle(dlgEl, 'top', this.originStyle.top) setStyle(dlgEl, 'height', this.originStyle.height) } else { this.originStyle = { top: dlgStyle.top, height: dlgStyle.height } setStyle(dlgEl, 'top', 0) setStyle(dlgEl, 'height', window.innerHeight + 'px') } this.verticalMaxed = !this.verticalMaxed }, openDocLink() { window.open('https://kbs.paratera.com/info/1272') } } } </script> <style lang="scss"> @import "./modal-resize.scss"; .help-modal-wrap { border-radius: 0; pointer-events: all; margin: 0; left: 10px; top: calc(100vh - 100% - 10px); // transition: all ease 0.2s; .el-dialog__header { padding: 10px; background-image: url(./header-bg-2.jpg); background-repeat: no-repeat; background-position: 100% 0%; background-size:cover; background-color: $theme-light-1; color:#fff; position: relative; em.console-svg { font-size: 16px; cursor: pointer } .el-dialog__headerbtn { top: 15px; right: 15px; } .el-dialog__close { color:#fff; } } .el-dialog__body { flex: auto; overflow: auto; height: 100%; padding: 0; } .dlg-body-inner { box-sizing: border-box; height: 100%; // position: relative; // overflow: auto; // min-height: 352px; // max-height: calc(100vh - 60px); } } </style> <style lang="scss" scoped> @import "../../dashboard/modals/ArticleContent.scss"; :deep(.dlg-wrap-help) { pointer-events: none; } .dialog-title { h3 { font-size: 16px; font-weight:normal; } } .panel-actions { position: absolute; right: 30px; top: 10px; } .panel-action { width: 24px; height:24px; line-height: 24px; text-align: center; display:inline-block; margin: 0 3px; cursor: pointer; &:hover { background-color:rgba(0,0,0,0.15) } } .content-wrap { height: calc(100% - 95px); overflow: auto; padding: 10px; } article { padding-top: 20px; } .article-title { text-align: left; color: #333333; padding-top: 10px; font-size: 22px; font-weight: normal; } .article-subtitle { position: relative; width: 100%; padding-top: 10px; text-align: left; color: #999; font-size: 14px; } .article-content { padding: 10px; } .outer-link-wrap { padding: 10px; position: static; background-color: #fff; width: 100%; bottom: 0; } .btn-open-in-window { display: block; width: 100%; box-sizing: border-box; margin: 0 auto; } </style> <style> .dlg-wrap-help { pointer-events: none; z-index: 9000!important; } </style> modal-resize.scss.dialog-can-resize .resize-handle { position: absolute; z-index: 100; display: block } .dialog-can-resize .resize-top { cursor: n-resize; top: -3px; left: 0px; height: 7px; width: 100% } .dialog-can-resize .resize-bottom { cursor: s-resize; bottom: -3px; left: 0px; height: 7px; width: 100% } .dialog-can-resize .resize-right { cursor: e-resize; right: -3px; top: 0px; width: 7px; height: 100% } .dialog-can-resize .resize-left { cursor: w-resize; left: -3px; top: 0px; width: 7px; height: 100% } .dialog-can-resize .resize-bottom-right { cursor: se-resize; width: 20px; height: 20px; right: -8px; bottom: -8px; background: url('./resize_corner.png') no-repeat; opacity: .4; filter: alpha(opacity=40) } .dialog-can-resize .resize-bottom-left { cursor: sw-resize; width: 16px; height: 16px; left: -8px; bottom: -8px } .dialog-can-resize .resize-top-left { cursor: nw-resize; width: 16px; height: 16px; left: -8px; top: -8px } .dialog-can-resize .resize-top-right { cursor: ne-resize; width: 16px; height: 16px; right: -8px; top: -8px } .dialog-can-resize .aui-min,.dialog-can-resize .aui-max { display: block } .dialog-can-resize .resize-top-right:before,.dialog-can-resize .resize-bottom-right:before,.dialog-can-resize .resize-bottom-left:before,.dialog-can-resize .resize-top-left:before { position: absolute; content: ""; -ms-transition: all .2s; -webkit-transition: all .2s; -moz-transition: all .2s; -o-transition: all .2s; transition: all .2s; width: 15px; height: 15px; opacity: 0; pointer-events: none; border: 1px solid #1890ff; border-radius: 0 } .dialog-can-resize .resize-top-right:hover:before,.dialog-can-resize .resize-bottom-right:hover:before,.dialog-can-resize .resize-bottom-left:hover:before,.dialog-can-resize .resize-top-left:hover:before,.dialog-can-resize .resize-top-right:active:before,.dialog-can-resize .resize-bottom-right:active:before,.dialog-can-resize .resize-bottom-left:active:before,.dialog-can-resize .resize-top-left:active:before { opacity: 1 } .dialog-can-resize .resize-top-right:before { border-left: none; border-bottom: none; border-radius: 0 4px 0 0; left: -8px; top: 8px } .dialog-can-resize .resize-bottom-right:before { border-left: none; border-top: none; border-radius: 0 0 4px 0; left: -4px; top: -4px } .dialog-can-resize .resize-bottom-left:before { border-right: none; border-top: none; border-radius: 0 0 0 4px; left: 8px; top: -8px } .dialog-can-resize .resize-top-left:before { border-right: none; border-bottom: none; border-radius: 4px 0 0 0; left: 8px; top: 8px } 实现文档入口组件<template> <div v-if="docList.length > 0" class="fn-doc-wrap"> <el-popover position="bottom" trigger="hover" > <el-button slot="reference" type="text" class="padding-xs"> <em class="console-svg svg-fn-help" /> </el-button> <ul class="doc-list"> <li v-for="docItem in docList" :key="docItem.id" @click="showHelpModal(docItem.id)"> <p v-text="docItem.title" /><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-tool-arrow" data-v-68bcb17a=""><path d="M13.7031 11.5259L13.7031 4.77588L6.95314 4.77589M4.30093 14.177L13.3949 5.08301" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg> </li> </ul> </el-popover> </div> </template> <script> export default { name: 'ParaFnDoc', desc: '随动帮助文档入口组件,使用过程中只需要传入关键词即可', props: { keyword: { type: String, required: true } }, data() { return { docList: [] } }, created() { this.init() }, methods: { init() { // TODO 根据关键词获取文档列表 this.docList = [ { id: 1, title: '一键转区功能说明' }, { id: 2, title: '为什么一键转区功能不可用?' }, { id: 3, title: '为什么一键转区提交后没有及时生效?' } ] }, showHelpModal(docId) { window.ParaCommon.showHelpModal(docId) } } } </script> <style lang="scss" scoped> .fn-doc-wrap { display: inline-block; vertical-align: middle; } .doc-list li { line-height: 22px; margin-bottom: 5px; padding: 7px 10px; list-style:none; display: flex; align-items: center; justify-content: center; &:hover { background-color: #f7f8fa; cursor: pointer; .icon-tool-arrow { stroke: #1e80ff; } } p { flex: 1 } .link-arrow { color:blue } } </style> 实现后的效果{dplayer src="/usr/uploads/2023/06/592593977.mp4"/}
2023年06月10日
14 阅读
0 评论
0 点赞
2023-05-30
项目原型工程搭建实践分享:前端数据mock
背景介绍大部分项目有产品经理参与前期的需求及原型制作,但近两年我们组负责的项目没有专职产品经理及UI工程师参与。由于项目已上线正式交付给用户使用,很多新需求不便于直接在ui工程大刀阔斧的改动,毕竟从需求 → 原型 → 前端UI → 后端接口 → 前后端联调 → 正式上线,一般都会有一个不短的周期。直接在前端ui工程跟进需求的话,中间极有可能穿插其他的bug修复及功能调整相关的短期改动需求,导致原型相关的代码跟ui代码混在一起,难免会由于粗心造成一些可大可小的线上事故。项目分析针对以上实际情况,最终项目组决定由前端工程师来负责项目的原型工作,原型效果由需求方确认后再着手后续的前后端开发工作。本着节约工作量考虑,原型工程使用 ui工程相同的代码,那样后期在原型工程的改动也可以很方便的迁移到ui工程。为达到这个目的,需要将在原型工程请求的后端数据改用 mock 方式。mock 数据有多种可行的实现方式:大部分API管理应用(如Torna、YApi、apizza等)都在提供管理接口的核心功能上带有给接口提供mock数据机制,然后开发时代理到API管理应用的mock路径本地维护一套mock数据,然后起一个mock服务提供接口请求本地维护一套接口json数据,拦截 ajax 请求响应 mock 的数据以上实现方式都有各自的优缺点:方案1:受制于接口管理平台,大部分接口管理平台不能自定义接口path,那样就会造成mock,路径跟实际路径不一致。而且在不同的平台维护,极容易造成最终的数据不一致的情况。方案2:可使用Node.js自写一套解析json提供mock服务的程序,该方式能最大程度的还原浏览器网络请求过程,要花更多的时间定义json 数据;由于实际使用时存在路径变量的情况,复杂的路由路径跟 mock数据的匹配方式也要花不少时间。若有足够多的时间处理这个路径匹配机制,该方案是最佳,整个前端工程的代码不用调整(那样也更更方便的跟ui工程做代码比对及合并),只需在原型工程代理一个不同的服务路径即可。方案3:有成熟的技术实现方案,能最快的响应现有需求,但需在已有 ui 工程上调整部分网络请求相关的文件。最终我在实现过程中是选择了第三种,主要是原型工程都是前端工程师维护,相关的mock数据也都在一个平台直接管理是最节省时间的。实现方案为实现该方案,在原有 ui 工程的基础上,主要做了开展了如下工作:1、模拟用户认证,添加 src/core/mock.js 文件,可根据系统实际情况模拟数据即可,核心内容如下:/** * 原型工程为跟console-ui整体结构保持一致,为跳过用户认证逻辑,本处添加一些环境初始化相关的数据 */ export function mockInit() { // 模拟token 进行 sessionStorage.setItem('token', 'p_RQAnFyIFZtGDS_iZwThGs5z8tWOM0BDSN03z1EWkA.Q0Ayf2dx4aaFfoJzLfS3YiRoqdScvk-OHJn7Kb_lRQU') // 模拟用户信息 sessionStorage.setItem('userid', 'SELF-DAN4_QiFBwU7CF-AQkPTatwW6tjKV_FjyDDUpkGYSvA') sessionStorage.setItem('para-token', 'TmdOGodfh75ueoAZsaeznT5qpeZJ_eaAyWJCwcao8Ro-2082234049') sessionStorage.setItem('userLoginLogId', '2744') } export const mockFormResponse = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(null) }, 1000) }) } 2、main.js 引入并执行初始化方法:import { mockInit } from './core/mock' mockInit() 3、 添加 src/api/mock 目录,然后添加mockData子目录用于管理相关的 mock 数据,基本的目录结构如下图所示:4、 添加 src/api/mock/index.js 主要用于获取所有 mockData 目录下以 .json结尾的文件,并提供 mock数据,核心代码如下:/** * 用于将 mockData 目录下的所有以.json 文件结尾的mock文件整合为一个,对外提供mock服务 */ import lodashGet from 'lodash/get' import { mockFormResponse } from '../../core/mock.js' const modulesFiles = require.context('./mockData', false, /\.json$/) const mockDataObj = {} modulesFiles.keys().map(modulePath => { const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') mockDataObj[moduleName] = modulesFiles(modulePath) }) export default { /** * * @param {String} mockPath mock数据路径,一般是「模块名.mock数据对象名」 * @param {Number} timeout 延时时长,单位为ms,默认为1秒 */ getMockData: (mockPath, timeout) => { if (!timeout) { timeout = 1000 } if (mockPath === 'common.formMock') { return mockFormResponse() } const mockData = lodashGet(mockDataObj, mockPath) return new Promise((resolve, reject) => { setTimeout(() => { if (mockData) { resolve(mockData) } else { reject(new Error('数据暂未定义')) } }, timeout) }) }, mockFormResponse }5、改造 Ajax 请求逻辑.本系统使用的 axios 作为 http 库,并且使用层面做了全局封装。而 axios 提供了 adapter 机制可以很方便的实现我们的 mock 场景,核心代码如下:import axios from 'axios' import mockService from '../api/mock/index.js' const service = axios.create({ baseURL: process.env.VUE_APP_GATEWAY_URL, // url = base url + request url adapter: async(config) => { // mock数据拦截 if (config.mockCode) { const data = await mockService.getMockData(config.mockCode) return new Promise((resolve, reject) => { resolve({ data, status: 200 }) }) } }, // withCredentials: true, send cookies when cross-domain requests timeout: 60000 // request timeout })6、改造所有 api 目录下所有获取数据的方法,在原有基础上统一添加 mockCode 参数。示例如下:api 请求方法种添加 mockCode:对应模块的.json mock 文件种添加需响应的数据:7、改造表格封装组件。为便于表格数据的异步加载,我们基于 el-table 做了些封装,额可以传入一个url 就可方便的分页获取表格数据,这一套机制是不走我们api 目录封装的方法的,故需要在请求时也需要指定 mockCode,具体示例代码如下图所示:为适配该场景,在发起请求前如果存在mockCode,则在请求参数种添加mockCode:总结通过该工作,扩展了解决问题的多角度思维模式,面对需求只要前期做好规划,梳理出可行的实现方案,最后的技术实现往往是最简单的。
2023年05月30日
18 阅读
0 评论
0 点赞
2023-05-28
windows 安装nvm 及基本使用
近些年 Node.js 的版本升级比较快,而我们很多项目维护周期也会维持很多年,而维护一些老项目的时候,在一些新版本 Node.js上会报一大堆的错,为了尽快修复 bug,没必要花太多的时间升级一大堆的依赖,而且贸然升级,对于线上在用的项目也存在诸多不确定性方面的隐患。对于前端开发者而言,选择一款方便的多 Node.js 版本控制工具,俨然是非常有必要的。而nvm 是一款可以用于管理多版本 Node.js 的工具,可以根据实际情况,轻松切换项目所需的node 版本,在开发过程中非常便捷,不用手工卸载原版本再下载安装新Node版本。下载github发布页:https://github.com/coreybutler/nvm-windows/releases我选择nvm-setup.exe文件进行下载,也有用于免安装的绿色版文件(nvm-noinstall.zip),那样得下载后解压并手工设置环境变量。安装下载后是一个nvm-setup.exe,安装过程一路 Next即可:安装完成后,打开cmd命令行,输入 nvm version 能正常显示类似下图所示版本信息即可:使用查看可安装版本:nvm list availableD:\GitRoot\SE\dmc\magic-boot>nvm list available | CURRENT | LTS | OLD STABLE | OLD UNSTABLE | |--------------|--------------|--------------|--------------| | 21.6.2 | 20.11.1 | 0.12.18 | 0.11.16 | | 21.6.1 | 20.11.0 | 0.12.17 | 0.11.15 | | 21.6.0 | 20.10.0 | 0.12.16 | 0.11.14 | | 21.5.0 | 20.9.0 | 0.12.15 | 0.11.13 | | 21.4.0 | 18.19.1 | 0.12.14 | 0.11.12 | | 21.3.0 | 18.19.0 | 0.12.13 | 0.11.11 | | 21.2.0 | 18.18.2 | 0.12.12 | 0.11.10 | | 21.1.0 | 18.18.1 | 0.12.11 | 0.11.9 | | 21.0.0 | 18.18.0 | 0.12.10 | 0.11.8 | | 20.8.1 | 18.17.1 | 0.12.9 | 0.11.7 | | 20.8.0 | 18.17.0 | 0.12.8 | 0.11.6 | | 20.7.0 | 18.16.1 | 0.12.7 | 0.11.5 | | 20.6.1 | 18.16.0 | 0.12.6 | 0.11.4 | | 20.6.0 | 18.15.0 | 0.12.5 | 0.11.3 | | 20.5.1 | 18.14.2 | 0.12.4 | 0.11.2 | | 20.5.0 | 18.14.1 | 0.12.3 | 0.11.1 | | 20.4.0 | 18.14.0 | 0.12.2 | 0.11.0 | | 20.3.1 | 18.13.0 | 0.12.1 | 0.9.12 | | 20.3.0 | 18.12.1 | 0.12.0 | 0.9.11 | | 20.2.0 | 18.12.0 | 0.10.48 | 0.9.10 | This is a partial list. For a complete list, visit https://nodejs.org/en/download/releases安装指定版本Node:nvm install 版本号D:\GitRoot\SE\dmc\magic-boot>nvm install 21 Downloading node.js version 21.6.2 (64-bit)... Extracting node and npm... Complete npm v10.2.4 installed successfully. Installation complete. If you want to use this version, type nvm use 21.6.2查看已安装版本:nvm listD:\GitRoot\SE\dmc\magic-boot>nvm list 21.6.2 * 20.10.0 (Currently using 64-bit executable) 更多使用方法可以输入 nvm --help 命令可查看:D:\GitRoot\SE\dmc\magic-boot>nvm --help Running version 1.1.12. Usage: nvm arch : Show if node is running in 32 or 64 bit mode. nvm current : Display active version. nvm debug : Check the NVM4W process for known problems (troubleshooter). nvm install <version> [arch] : The version can be a specific version, "latest" for the latest current version, or "lts" for the most recent LTS version. Optionally specify whether to install the 32 or 64 bit version (defaults to system arch). Set [arch] to "all" to install 32 AND 64 bit versions. Add --insecure to the end of this command to bypass SSL validation of the remote download server. nvm list [available] : List the node.js installations. Type "available" at the end to see what can be installed. Aliased as ls. nvm on : Enable node.js version management. nvm off : Disable node.js version management. nvm proxy [url] : Set a proxy to use for downloads. Leave [url] blank to see the current proxy. Set [url] to "none" to remove the proxy. nvm node_mirror [url] : Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url. nvm npm_mirror [url] : Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to default url. nvm uninstall <version> : The version must be a specific version. nvm use [version] [arch] : Switch to use the specified version. Optionally use "latest", "lts", or "newest". "newest" is the latest installed version. Optionally specify 32/64bit architecture. nvm use <arch> will continue using the selected version, but switch to 32/64 bit mode. nvm root [path] : Set the directory where nvm should store different versions of node.js. If <path> is not set, the current root will be displayed. nvm [--]version : Displays the current running version of nvm for Windows. Aliased as v.
2023年05月28日
16 阅读
0 评论
0 点赞
2023-05-24
记一次使用v-charts绘制图表出现重影的处理经历分享
背景介绍目前维护的项目属于多年积累的老项目了,使用的 vue2 + vue-router + vuex + element-ui + webpack 的技术架构。涉及图表相关的库主要使用 echarts@4.9.0 和 饿了么前端团队基于 echarts 4.x 封装的 v-charts 1.19.0。近期接运营要求,需要增加一些图表效果,基本界面效果实现如下:问题但在实现过程中,在区域缩放组件拖动调整数据显示范围时,发现x轴会出现重影的现象,如下图所示:排查过程初期怀疑跟图表配置项的 xAxis 相关配置信息有关,随将 position: 'bottom' 配置项注释掉,发现重影现象没有了,但是上下出现了重复的x 轴label如下图所示:将echarts 图表配置信息拷贝到 echarts 实例页面(https://echarts.apache.org/v4/examples/zh/editor.html?c=bar-large)进行验证,发现在该页面不存在重影现象:配置信息如下:var option = { "color": ["#004CA1", "#32D4E3", "#03A2DC", "#B581E3", "#0F7DFA", "#c4b4e4"], "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "legend": { "show": true, "top": -5, "data": ["CPU", "GPU"] }, "grid": { "right": "5%", "top": "10%", "bottom": "20%", "containLabel": true }, "dataZoom": [ { "type": "inside" }, { "type": "slider", "left": 77, "right": 77 } ], "xAxis": { "data": [ "2022-11-26", "2022-11-27", "2022-11-28", "2022-11-29", "2022-11-30", "2022-12-01", "2022-12-02", "2022-12-03", "2022-12-04", "2022-12-05", "2022-12-06", "2022-12-07", "2022-12-08", "2022-12-09", "2022-12-10", "2022-12-11", "2022-12-12", "2022-12-13", "2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17", "2022-12-18", "2022-12-19", "2022-12-20", "2022-12-21", "2022-12-22", "2022-12-23", "2022-12-24", "2022-12-25", "2022-12-26", "2022-12-27", "2022-12-28", "2022-12-29", "2022-12-30", "2022-12-31", "2023-01-01", "2023-01-02", "2023-01-03", "2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07", "2023-01-08", "2023-01-09", "2023-01-10", "2023-01-11", "2023-01-12", "2023-01-13", "2023-01-14", "2023-01-15", "2023-01-16", "2023-01-17", "2023-01-18", "2023-01-19", "2023-01-20", "2023-01-21", "2023-01-22", "2023-01-23", "2023-01-24", "2023-01-25", "2023-01-26", "2023-01-27", "2023-01-28", "2023-01-29", "2023-01-30", "2023-01-31", "2023-02-01", "2023-02-02", "2023-02-03", "2023-02-04", "2023-02-05", "2023-02-06", "2023-02-07", "2023-02-08", "2023-02-09", "2023-02-10", "2023-02-11", "2023-02-12", "2023-02-13", "2023-02-14", "2023-02-15", "2023-02-16", "2023-02-17", "2023-02-18", "2023-02-19", "2023-02-20", "2023-02-21", "2023-02-22", "2023-02-23", "2023-02-24", "2023-02-25", "2023-02-26", "2023-02-27", "2023-02-28", "2023-03-01", "2023-03-02", "2023-03-03", "2023-03-04", "2023-03-05", "2023-03-06", "2023-03-07", "2023-03-08", "2023-03-09", "2023-03-10", "2023-03-11", "2023-03-12", "2023-03-13", "2023-03-14", "2023-03-15", "2023-03-16", "2023-03-17", "2023-03-18", "2023-03-19", "2023-03-20", "2023-03-21", "2023-03-22", "2023-03-23", "2023-03-24", "2023-03-25", "2023-03-26", "2023-03-27", "2023-03-28", "2023-03-29", "2023-03-30", "2023-03-31", "2023-04-01", "2023-04-02", "2023-04-03", "2023-04-04", "2023-04-05", "2023-04-06", "2023-04-07", "2023-04-08", "2023-04-09", "2023-04-10", "2023-04-11", "2023-04-12", "2023-04-13", "2023-04-14", "2023-04-15", "2023-04-16", "2023-04-17", "2023-04-18", "2023-04-19", "2023-04-20", "2023-04-21", "2023-04-22", "2023-04-23", "2023-04-24", "2023-04-25", "2023-04-26", "2023-04-27", "2023-04-28", "2023-04-29", "2023-04-30", "2023-05-01", "2023-05-02", "2023-05-03", "2023-05-04", "2023-05-05", "2023-05-06", "2023-05-07", "2023-05-08", "2023-05-09", "2023-05-10", "2023-05-11", "2023-05-12", "2023-05-13", "2023-05-14", "2023-05-15", "2023-05-16", "2023-05-17", "2023-05-18", "2023-05-19", "2023-05-20", "2023-05-21", "2023-05-22", "2023-05-23", "2023-05-24" ], "type": "category", "silent": false, "axisLine": { "show": true }, "axisLabel": { "show": true, "interval": "auto", "showMinLabel": true, "showMaxLabel": true }, "scale": true, "splitLine": { "show": false }, "splitArea": { "show": true } }, "yAxis": { "type": "value", "axisLine": { "show": true } }, "series": [ { "type": "bar", "stack": "总量", "name": "CPU", "barMaxWidth": 40, "label": { "show": false, "position": "insideRight" }, "large": true, "data": [ 41284.35, 47697.4, 8413.61, 9252.11, 31770.33, 35508.78, 46398.67, 58060.04, 18176.99, 36094.06, 9143.29, 29802.65, 3537.02, 47931.77, 45087.27, 16314.9, 56488.59, 34108.47, 32244.21, 13635.22, 58377.44, 462.67, 56279.27, 2491.55, 13690.14, 29416.47, 35530.21, 18119.02, 23853.13, 33763.83, 7201.61, 26889.05, 51124.77, 53054.58, 48729.74, 37691.94, 46411.41, 25579.66, 30987.66, 18372.65, 45733.25, 56040.3, 13739.55, 16133.89, 53678.09, 1180.71, 4276.45, 11854.02, 46574.02, 6939.56, 18079.92, 25200.51, 47832.78, 25691.29, 14248.72, 4032.22, 39072.21, 12722.16, 42049.24, 27181.52, 29552.25, 51946, 48863.94, 22122, 39071.33, 50454.55, 59850.52, 23004.45, 51904.63, 28075.69, 38243.83, 32189.07, 1613.23, 40114.69, 30133.03, 28253.31, 38028.55, 25430.73, 23515.73, 9958.14, 38543.8, 36322.66, 57980.19, 57456.79, 27707.03, 269.52, 7980.29, 17138.2, 432.99, 48010.12, 1241.83, 39925.79, 41015.29, 38714.92, 18084.32, 28493.39, 51526.46, 22794.2, 32825.65, 53792.55, 18065.35, 4739.43, 39413.07, 37827.19, 9056.71, 49265.35, 35348.52, 10957.42, 30322.44, 22973.19, 47500.12, 48545.9, 946.19, 19273.15, 22761.02, 40868.44, 775.85, 49775.87, 28345, 52012.99, 30051.59, 45289.68, 36900.8, 31460.81, 51110.19, 12397.24, 34578.01, 47720.53, 15712.89, 6756.28, 25762.61, 54078.42, 50010.54, 16609.55, 13129.16, 32402.43, 26270.31, 5681.48, 31295.89, 5824.84, 12550.59, 50847.39, 32752.45, 18003.45, 53884.61, 32547.36, 47514.46, 5961.97, 24958.12, 51028.81, 26937.62, 31497.45, 16485.2, 13467.6, 35387.98, 14217.41, 18260.78, 45292.45, 15902.32, 15645.63, 48198.75, 26858.24, 28383.47, 5838.99, 35127.11, 26490.36, 21835.39, 3394.33, 55933.54, 55142.88, 39673.89, 4071.1, 44240.5, 39103.1, 45423, 15899.4, 372.21, 4810.24, 15848.72, 10573.71 ] }, { "type": "bar", "stack": "总量", "name": "GPU", "barMaxWidth": 40, "label": { "show": false, "position": "inside" }, "large": true, "data": [ 45656.8, 48919.63, 30686.44, 39105.29, 6583.58, 16579.54, 28757.36, 48880.32, 34710.97, 52267.01, 35244.55, 29465.79, 18699.38, 26708.27, 48610.16, 41065.65, 11168.56, 52782.18, 50920.27, 8510.37, 1054.53, 15473.91, 52598.26, 20234.36, 54710.49, 37693.7, 25479.18, 15601.02, 715.52, 47497.22, 30241.03, 15098.62, 36223.61, 39782.68, 31656.7, 52831.1, 4355.73, 13741.26, 2690.1, 8953.15, 40778.42, 40186.61, 29139.39, 34951.79, 29297.1, 13981.23, 50465.4, 52068.09, 24882.09, 11735.73, 24719.71, 26538.39, 54061.69, 17362.82, 27552.54, 39175.74, 35403, 45847.45, 39241.42, 21423.24, 28252.3, 15936.17, 51460.45, 38450.2, 36549.7, 8833.54, 49552.81, 11116.06, 7629.68, 23024.14, 46621.6, 48414.22, 34057.44, 16508.14, 8695.67, 9935.44, 23205.87, 459.17, 6058.89, 11538.15, 1691.72, 5674.09, 43659.65, 42399.6, 23885.54, 16819.74, 18714.28, 51726.61, 12924.85, 9741.92, 47726.78, 52780.24, 19201.93, 17926.32, 42666.5, 8034.12, 1942.08, 47110.64, 46218.2, 44514.71, 31623.2, 42708.9, 26215.12, 28139.43, 32497.47, 45742.64, 38990.36, 4446.69, 44520.51, 20765.67, 18069.18, 6911.16, 5579.52, 29354.69, 17215.26, 26342.73, 11537.4, 16678.75, 37653.87, 3944.92, 15016.5, 31703.34, 46996.72, 4255.04, 36405.66, 15890.87, 8085.44, 8727.37, 20420, 11387.02, 50816.76, 38010.23, 44139.63, 1437.55, 19776.06, 5491.32, 1402.23, 18269.63, 31190.68, 9546.1, 52578.27, 26100.02, 8729.68, 37343.21, 2299.31, 28782.83, 31684.85, 16185.26, 15100.92, 29155.27, 38192.58, 16588.44, 18377.32, 54143.88, 51351.67, 43105.14, 46469.82, 22805.7, 3730.43, 29585.85, 21114.32, 26529.31, 15564.97, 615.24, 32499.96, 43994.11, 36856.41, 19624.48, 7703.24, 17755.76, 47110.48, 54746.48, 23024.8, 3153.74, 39398.46, 31583.36, 9440.84, 10548.38, 8578.17, 17212.49 ] } ] } 那问题肯定基本上就能定位是 v-charts在封装的时候对相关的配置信息做了些变更。于是,使用 v-charts 组件的 afterSetOption 获取最终的配置信息:控制台打印的最终 echarts 配置信息如下:简单清理一些data数据后,使用 Beyond 进行数据对比,还真发现使用 v-charts 转换后的数据存在很大问题:然后进一步跟踪相关源代码,发现在图表组件初始化的时候,不知为何将 xAxis 初始化为2个对象的数组:然后在 optionsHandler 方法中将 extend 中的数据附加到option上就改变了原有数据然后 setExtend 方法会把xAxis对象数据复制到目标数组内的每一个对象值里:解决方案问题分析过程中,认真阅读了下相关的核心源码,发现 v-charts 组件在执行echarts.setOption()前提供了 afterConfig的扩展点,可以基于该扩展点对前期生成的 option 进行修改:为解决该问题,修改后的核心代码如下:修复后的效果总结项目开发过程中出现问题往往不可怕,最重要的是需要静下心来定位问题,当知道问题的产生缘由后,往往解决问题的方法也就水到渠成了。
2023年05月24日
16 阅读
0 评论
0 点赞
2023-03-14
用*输出一个等腰三角形
缘起工作间隙,得到一个刚学编程时的题目,题目如下:用输出一个等腰三角形。提示用户输入一个整数 ,代表输出的等边三角形由n行组成。例:输入n=5。输出: * *** ***** ******* *********朋友提供的代码如下:public static void main(String[] args) { System.out.println("请输入需要的行数(不可输入0):"); Scanner scanner = new Scanner(System.in); int rows = scanner.nextInt(); int rowIndex = rows - 1; //最后一行总个数 int total = 1 + (rows - 1) * 2; // 每行*个数 int num = 1; // 打印标记 boolean flag = false; if (rows != 0) { for (int i = 0; i < rows; i++) { for (int j = 0, k = 0; j < total; j++) { // 倒数第几行就从第几个下标开始 if (j == rowIndex - i) { flag = true; } if (flag && k < num) { System.out.print("*"); // 开始打印计数 k++; } else { System.out.print(" "); // 结束打印 flag = false; } } num = num + 2; System.out.println(); } } }整段代码看下来实现方式有些复杂,感觉有些陷在循环里了,脑袋里不跟着一行一行的跑代码的话,不怎么能想出来结果输出是怎样的。于是,我从头开始梳理了下需要输出的东西,主要是要从中找寻规律:第一步:手工输出前几个结果,从结果中找规律2: * *** 3: * *** ***** 4: * *** ***** ******* 5: * *** ***** ******* ********* 6: * *** ***** ******* ********* *********** 7: * *** ***** ******* ********* *********** *************第二步:分析得出如下规律:令 行数为1开始,当前行的输出情况如下:星号数量 = 当前行数 * 2 - 1左侧空格数量 = 总行数 - 当前行数得出规律后,就能够很好的着手后面的代码工作了,以下是我提供的代码:JS版:/** * 输出三角形 * @param rows 三角形行数 */ let showTriangle = (rows) => { console.log(`输出${rows}等腰三角形:`) // 如果小于2的话不能构成等腰三角形 if (rows < 2) { console.error('必须大于1') return; } for (let row = 1; row <= rows; row++) { // 输出空格 let rowStr = '' for (let blankIndex = 0; blankIndex < rows - row; blankIndex++) { rowStr += ' ' } // 输出星号 for (let starIndex = 0; starIndex < row * 2 - 1; starIndex++) { rowStr += '*' } console.log(rowStr) } } // 测试输出: for (let i = 0; i < 15; i++) { showTriangle(i) }输出结果:Java 版由于使用了 String.repeat(), JDK 版本应 >= 11,若 JDK 低于 11 也可以用循环实现。import java.util.InputMismatchException; import java.util.Scanner; /** * @author zhuzl */ public class ShowTriangle { public static void main(String[] args) { System.out.println("请输入三角形的行数(需大于1):"); Scanner scanner = new Scanner(System.in); int rows = scanner.nextInt(); printTriangle(rows); } /** * 打印等腰三角形 * * @param rows 三角形行数 */ private static void printTriangle(int rows) { if (rows < 2) { throw new InputMismatchException("必须大于1"); } for (int row = 1; row <= rows; row++) { System.out.println(" ".repeat(rows - row) + "*".repeat(row * 2 - 1)); } } }输出结果如下:
2023年03月14日
15 阅读
0 评论
0 点赞
2022-10-14
20 个 JS 工具函数助力高效开发
前言日常开发中,面对各种不同的需求,我们经常会用到以前开发过的一些工具函数,把这些工具函数收集起来,将大大提高我们的开发效率。1、校验数据类型export const typeOf = function(obj) { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() } // Example typeOf('zhuzl') // string typeOf([]) // array typeOf(new Date()) // date typeOf(null) // null typeOf(true) // boolean typeOf(() => { }) // function 2、防抖export const debounce = (() => { let timer = null return (callback, wait = 800) => { timer&&clearTimeout(timer) timer = setTimeout(callback, wait) } })() // Example // 如 vue 中使用 methods: { loadList() { debounce(() => { console.log('加载数据') }, 500) } }3、节流export const throttle = (() => { let last = 0 return (callback, wait = 800) => { let now = +new Date() if (now - last > wait) { callback() last = now } } })() 4、手机号脱敏export const hideMobile = (mobile) => { return mobile.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2") }5、开启全屏export const launchFullscreen = (element) => { if (element.requestFullscreen) { element.requestFullscreen() } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen() } else if (element.msRequestFullscreen) { element.msRequestFullscreen() } else if (element.webkitRequestFullscreen) { element.webkitRequestFullScreen() } }6、关闭全屏export const exitFullscreen = () => { if (document.exitFullscreen) { document.exitFullscreen() } else if (document.msExitFullscreen) { document.msExitFullscreen() } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen() } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen() } }7、大小写转换/** * 大小写转换 * @param str 待转换的字符串 * @param type 1-全大写 2-全小写 3-首字母大写 * @returns */ export const turnCase = (str, type) => { switch (type) { case 1: return str.toUpperCase() case 2: return str.toLowerCase() case 3: return str[0].toUpperCase() + str.substring(1).toLowerCase() default: return str } } // Example turnCase('vue', 1) // VUE turnCase('REACT', 2) // react turnCase('vue', 3) // Vue8、解析URL参数export const getSearchParams = () => { const searchPar = new URLSearchParams(window.location.search) const paramsObj = {} for (const [key, value] of searchPar.entries()) { paramsObj[key] = value } return paramsObj } // Example // 假设目前位于 https://****com/index?id=154513&age=18; getSearchParams(); // {id: "154513", age: "18"}9、判断手机是Andoird还是IOS/** * 1: ios * 2: android * 3: 其它 */ export const getOSType=() => { let u = navigator.userAgent, app = navigator.appVersion; let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); if (isIOS) { return 1; } if (isAndroid) { return 2; } return 3; }10、数组对象根据字段去重/** * 数组对象根据某字段去重 * @param arr 要去重的数组 * @param key 根据去重的字段名 * @returns */ export const uniqueArrayObject = (arr = [], key = 'id') => { if (arr.length === 0) return let list = [] const map = {} arr.forEach((item) => { if (!map[item[key]]) { map[item[key]] = item } }) list = Object.values(map) return list } // Example const responseList = [ { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, { id: 1, name: '黄老爷' }, { id: 2, name: '张麻子' }, { id: 3, name: '树哥' }, { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, ] uniqueArrayObject(responseList, 'id') // [{ id: 1, name: '树哥' },{ id: 2, name: '黄老爷' },{ id: 3, name: '张麻子' }] 11、滚动到页面顶部export const scrollToTop = () => { const height = document.documentElement.scrollTop || document.body.scrollTop; if (height > 0) { window.requestAnimationFrame(scrollToTop); window.scrollTo(0, height - height / 8); } }12、滚动到指定元素位置export const smoothScroll = element =>{ document.querySelector(element).scrollIntoView({ behavior: 'smooth' }) } // Example smoothScroll('#target') // 平滑滚动到 ID 为 target 的元素13、uuidexport const uuid = () => { const temp_url = URL.createObjectURL(new Blob()) const uuid = temp_url.toString() URL.revokeObjectURL(temp_url) //释放这个url return uuid.substring(uuid.lastIndexOf('/') + 1) } // Example uuid() // d9ffece8-0a9d-4ca2-bbab-0af7bc0f71df14、金额格式化/** * 金额格式化 * @param number 要格式化的数字 * @param decimals 保留几位小数 * @param dec_point 小数点符号 * @param thousands_sep 千分位符号 * @returns */ export const moneyFormat = (number, decimals, dec_point, thousands_sep) => { number = (number + '').replace(/[^0-9+-Ee.]/g, '') const n = !isFinite(+number) ? 0 : +number const prec = !isFinite(+decimals) ? 2 : Math.abs(decimals) const sep = typeof thousands_sep === 'undefined' ? ',' : thousands_sep const dec = typeof dec_point === 'undefined' ? '.' : dec_point let s = '' const toFixedFix = function(n, prec) { const k = Math.pow(10, prec) return '' + Math.ceil(n * k) / k } s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.') const re = /(-?\d+)(\d{3})/ while (re.test(s[0])) { s[0] = s[0].replace(re, '$1' + sep + '$2') } if ((s[1] || '').length < prec) { s[1] = s[1] || '' s[1] += new Array(prec - s[1].length + 1).join('0') } return s.join(dec) } // Example moneyFormat(10000000) // 10,000,000.00 moneyFormat(10000000, 3, '.', '-') // 10-000-000.00015、存储操作class MyCache { constructor(isLocal = true) { this.storage = isLocal ? localStorage : sessionStorage } setItem(key, value) { if (typeof (value) === 'object') value = JSON.stringify(value) this.storage.setItem(key, value) } getItem(key) { try { return JSON.parse(this.storage.getItem(key)) } catch (err) { return this.storage.getItem(key) } } removeItem(key) { this.storage.removeItem(key) } clear() { this.storage.clear() } key(index) { return this.storage.key(index) } length() { return this.storage.length } } const localCache = new MyCache() const sessionCache = new MyCache(false) export { localCache, sessionCache } // Example localCache.getItem('user') sessionCache.setItem('name','树哥') sessionCache.getItem('token') localCache.clear() 16、下载文件/** * 下载文件 * @param api API接口路径 * @param params 请求参数 * @param fileName 文件名 * @param type HTTP请求方式,默认为get */ const downloadFile = (api, params, fileName, type = 'get') => { axios({ method: type, url: api, responseType: 'blob', params: params }).then((res) => { let str = res.headers['content-disposition'] if (!res || !str) { return } let suffix = '' // 截取文件名和文件类型 if (str.lastIndexOf('.')) { fileName ? '' : fileName = decodeURI(str.substring(str.indexOf('=') + 1, str.lastIndexOf('.'))) suffix = str.substring(str.lastIndexOf('.'), str.length) } // 如果支持微软的文件下载方式(ie10+浏览器) if (window.navigator.msSaveBlob) { try { const blobObject = new Blob([res.data]); window.navigator.msSaveBlob(blobObject, fileName + suffix); } catch (e) { console.log(e); } } else { // 其他浏览器 let url = window.URL.createObjectURL(res.data) let link = document.createElement('a') link.style.display = 'none' link.href = url link.setAttribute('download', fileName + suffix) document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(link.href); } }).catch((err) => { console.log(err.message); }) } // Example downloadFile('/api/resources/download', {id}, '文件名')17、时间操作关于时间操作,没必要自己再写一大串代码了,强烈推荐使用 [Day.js](https://dayjs.gitee.io/zh-CN/) > Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样。 > 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js 。18、深拷贝此方法存在一定局限性:一些特殊情况没有处理: 例如Buffer对象、Promise、Set、Map。如果确实想要完备的深拷贝,推荐使用 lodash 中的 cloneDeep 方法。export const clone = parent => { // 判断类型 const isType = (obj, type) => { if (typeof obj !== "object") return false const typeString = Object.prototype.toString.call(obj) let flag; switch (type) { case "Array": flag = typeString === "[object Array]" break; case "Date": flag = typeString === "[object Date]" break; case "RegExp": flag = typeString === "[object RegExp]" break; default: flag = false } return flag; }; // 处理正则 const getRegExp = re => { var flags = "" if (re.global) flags += "g" if (re.ignoreCase) flags += "i" if (re.multiline) flags += "m" return flags; }; // 维护两个储存循环引用的数组 const parents = [] const children = [] const _clone = parent => { if (parent === null) return null if (typeof parent !== "object") return parent let child, proto if (isType(parent, "Array")) { // 对数组做特殊处理 child = [] } else if (isType(parent, "RegExp")) { // 对正则对象做特殊处理 child = new RegExp(parent.source, getRegExp(parent)) if (parent.lastIndex) child.lastIndex = parent.lastIndex } else if (isType(parent, "Date")) { // 对Date对象做特殊处理 child = new Date(parent.getTime()) } else { // 处理对象原型 proto = Object.getPrototypeOf(parent) // 利用Object.create切断原型链 child = Object.create(proto) } // 处理循环引用 const index = parents.indexOf(parent) if (index != -1) { // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 return children[index] } parents.push(parent) children.push(child) for (let i in parent) { // 递归 child[i] = _clone(parent[i]) } return child; } return _clone(parent) }19、模糊搜索/** * 数组对象模糊搜索 * @param list 原数组对象 * @param keyWord 查询的关键词 * @param attribute 数组需要检索的对象属性,默认问name * @returns */ export const fuzzyQuery = (list, keyWord, attribute = 'name') => { const reg = new RegExp(keyWord) const arr = [] for (let i = 0; i < list.length; i++) { if (reg.test(list[i][attribute])) { arr.push(list[i]) } } return arr } // Example const list = [ { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, { id: 4, name: '汤师爷' }, { id: 5, name: '胡万' }, { id: 6, name: '花姐' }, { id: 7, name: '小梅' } ] fuzzyQuery(list, '树', 'name') // [{id: 1, name: '树哥'}]20、遍历树节点export const foreachTree = (data, callback, childrenName = 'children') => { for (let i = 0; i < data.length; i++) { callback(data[i]) if (data[i][childrenName] && data[i][childrenName].length > 0) { foreachTree(data[i][childrenName], callback, childrenName) } } } // Example const treeData = [{ id: 1, label: '一级 1', children: [{ id: 4, label: '二级 1-1', children: [{ id: 9, label: '三级 1-1-1' }, { id: 10, label: '三级 1-1-2' }] }] }, { id: 2, label: '一级 2', children: [{ id: 5, label: '二级 2-1' }, { id: 6, label: '二级 2-2' }] }, { id: 3, label: '一级 3', children: [{ id: 7, label: '二级 3-1' }, { id: 8, label: '二级 3-2' }] }], // 假设我们要从树状结构数据中查找 id 为 9 的节点 let result foreachTree(data, (item) => { if (item.id === 9) { result = item } }) console.log('result', result) // {id: 9,label: "三级 1-1-1"} 关于本文作者:呛再首https://juejin.cn/post/7132714583399071758
2022年10月14日
22 阅读
0 评论
0 点赞
2022-09-06
扫码登录项目实践
背景说明随着移动端应用的普及,扫码登录在日常的登录认证过程中已经非常普及了,在提升用户便利的同时,由于减少了账号密码的输入,在一定程度上也起到了增强安全性的目的。随着控制台产品的迭代,PC端界面及功能已趋于稳定,考虑到目前移动终端更便于用户使用,经过公司内部层层审批,控制台移动端的项目顺利立项。作为充分为用户提供便利的移动端应用,扫码登录功能便自然的纳入了起基础功能。登录认证的本质扫码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!告诉系统我是谁向系统证明我是谁比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁;比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;那么扫码登录是怎么做到这两件事情的呢?扫码登录过程中移动端是已登录状态,这两件事对移动端而言都是明确的,登录过程中的重点是怎么把移动端的登录状态传递给PC端。在这个过程中主要就是通过扫码的二维码了,二维码本质上就是一串具有唯一性的字符串。通过这个字符串便可以将已登录方授权未登录端进行登录了。技术方案根据项目实际情况,参与扫码登录的应用为console-ui、consent、console-biz、app:console-ui :控制台前端,需登录的应用consent :用户认证端console-biz :控制台后端服务app :移动端应用,微信端H5网页实现流程在使用超算云服务控制台应用时,由 console-ui 判断用户是否登录,未登录的情况下将自动跳转到 consent 进行登录操作,本需求将修改consent登录界面,在已有的账号登录基础上增加扫码登录机制,默认采用账号登录方式,当用户切换登录方式为扫码登录时,将调用console-biz提供的接口获取 scanId,使用 QRCode.js 生成携带scanId参数的app端网页认证URL的二维码。用户使用微信扫一扫功能,可直接打开app端的指定页面,在该页面完成用户确认授权操作,consent通过轮询二维码状态接口,当获取到用户在 app端的确认操作后,携带 scanId 参数跳转到 console-ui,在console-ui 调用接口获取用户 token 信息完成登录。整体交互流程如下所示:二维码生命周期码登录的核心是对二维码生命周期的管理,为了更好地理解扫码登录过程,将上图中二维码生命周期相关的核心流程梳理如下:二维码状态数据字典字典项字典值说明未使用1获取scanId时设置初始状态为该值已扫码(未授权)2app端扫码后更新状态为已扫码已授权(登录)3在app授权界面单击“确认授权”已使用4在app端授权界面确认授权,然后在console-ui中获取到用户token后已过期5超过5分钟redis中不存在记录则返回该值取消授权(登录失败)6在app授权界面单击“取消授权”数据存储方案scanId作为整个扫码登录过程的核心,后端需存储scanId整个生命周期的状态值及需交换的用户数据,而每个 scanId 仅有5分钟的有效期,基于 Redis 的过期策略很容易就能实现这个目的,且比关系型数据库在数据存取上有更优异的表现。故本功能模块相关的数据统一存储于 Redis。后端实现方案后端基于现有的console-biz工程添加新的接口用于前端对接,相关接口见“接口设计”章节。前端实现方案在需求中参与前端开发工作的有consent、console-ui、app三端。1、consent:负责登录界面的交互及展示,基于QRCode.js实现二维码生成,轮询获取二维码状态。2、console-ui:在获取到consent授权回调后,通过scanId调用接口获取用户token实现用户在PC端登录。3、app:扫码后直接跳转到app指定的授权页面,进入授权页面后实现。扫码操作的入口有两个:微信扫一扫、app应用首页顶部的扫一扫。使用app应用首页的扫一扫功能需要解析获取二维码URL中的scanId,然后跳转到授权页面进行授权操作。4、单击某功能时使用component组件渲染对应功能的抽屉功能进行显示接口设计后端以 Restful API 方式为前端提供接口,根据项目情况涉及的接口列表如下:获取scanId接口(PC端调用)获取scanId状态接口(PC端轮询调用)app端扫码接口app端授权接口授权成功后获取登录信息接口(PC端调用)效果截图相关功能将在控制台移动端功能上线后,所有并行用户可线上体验。一、PC端截图:1、初始状态:2、刷新时有个 gif加载动图:3、已扫码:4、已失效:5、取消授权:二、移动端截图:1、正常状态:2、已被使用:3、单击取消授权:4、单击确认授权:5、授权操作时二维码已过期:
2022年09月06日
25 阅读
0 评论
0 点赞
2022-08-31
正则表达式-元字符
特殊单字符空白符范围量词
2022年08月31日
23 阅读
0 评论
0 点赞
2022-06-15
前端导出Excel项目实践
背景介绍近期项目有个「导出作业详情」的需求,之前接触大部分导出需求均为后端获取数据生成Excel文件或将Excel文件的下载地址或文件流提供给前端进行下载。在本需求中对下载的文件有如下需求点:命名:用户名作业详单-起止日期格式:xlsx,下载内容CPU与GPU分别显示在两个工作表里技术调研在以前的项目中有=接触过前端解析 Excel 导入数据的功能,使用的是 [xlsx](https://www.npmjs.com/package/xlsx),功能超级强大,便先了解一下这个库是否能满足咱们的需求,经过稍加深入地调研,了解到这个库仅侧重在 Excel 解析,我们的需求是要能根据后端提供的数据动态生成 Excel 文件,可能这个库就不适应了。经过经一步调研,发现一款采用MIT开源授权, Star 数近 10k 的开源库:exceljs,看描述为:读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。这不就正是我所需要的嘛,接下来的问题就是验证工作了,根据我们的需求,主要需要验证的有如下事项:1、是否可支持生成多工作表的Excel文件2、是否支持自定义单元格样式:如背景色、边框、对齐方式、格式化3、测试Excel文件生成效率经过对相关API的深入了解及验证,发现完全能满足我们的需求。其中生成效率,一次性生成40000多条数据,不到15秒即可完成Excel生成工作,也能满足我们的实际要求:技术点梳理详细见代码及相关注释。仅用于说明相关功能点,非完整代码。// 引入 exceljs 依赖 import ExcelJS from 'exceljs' // 创建工作簿 const workbook = new ExcelJS.Workbook() // 在工作簿中添加两个工作表 const cpuSheet = workbook.addWorksheet('CPU作业详单') const gpuSheet = workbook.addWorksheet('GPU作业详单') // 添加表头 const cpuHeaderRow = cpuSheet.addRow(['作业ID', '作业名称', '超算中心', '超算账号', '队列', '运行时长', '核数', '消费核时', '消费金额', '提交时间', '开始时间', '结束时间', '提交账号', '付费账号']) // 设置表头样式 // 行高 cpuHeaderRow.height = 26.5 // 字体 cpuHeaderRow.font = { bold: true } // 设置表头单元格样式 cpuHeaderRow.eachCell((cell, rowNumber) => { // 设置边框 cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } } // 填充背景 cell.fill = { type: 'pattern', pattern: 'darkTrellis', fgColor: { argb: 'F1F1F1FF' }, bgColor: { argb: 'F1F1F1FF' } } // 对齐方式 cell.alignment = { vertical: 'middle', horizontal: cpuLeftAlignCols.includes(rowNumber) ? 'left' : 'center', wrapText: true } }) // 根据内容适当调整部分列宽度 cpuSheet.getColumn(2).width = 30 // 作业名称 cpuSheet.getColumn(10).width = 20 // 提交时间 cpuSheet.getColumn(11).width = 20 // 开始时间 cpuSheet.getColumn(12).width = 20 // 结束时间 cpuSheet.getColumn(13).width = 15 // 提交账号 cpuSheet.getColumn(14).width = 15 // 付费账号 // 生成相关数据行 for (let i = 0; i < cpuJobList.length; i++) { const job = cpuJobList[i] const rowData = [job.jobId, job.jobName, job.cluster, job.user, job.partition, ...] const dataRow = cpuSheet.addRow(rowData) // 设置数据行样式 dataRow.height = 26.5 dataRow.eachCell((cell, rowNumber) => { // 设置单元格边框 cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } } // 设置对齐方式 cell.alignment = { vertical: 'middle', horizontal: cpuLeftAlignCols.includes(rowNumber) ? 'left' : 'center', wrapText: true } // 金额格式化 if (rowNumber === 10) { cell.numFmt = '"¥"#,##.##' } }) } // 保存Excel文件 const buf = await workbook.xlsx.writeBuffer() const fileName = `${userForFileName}作业详单_${dayjs(startDate).format('YYYYMMDD')}-${dayjs(endDate).format('YYYYMMDD')}.xlsx` // 基于 file-saver 实现保存文件到本地 saveAs(new Blob([buf]), fileName)最终整合到应用中的效果扩展打印相关// 设置页面方向 workSheet.pageSetup.orientation = 'portrait' // portrait || landscape // 设置页边距 workSheet.pageSetup.margins = { left: 0.3, right: 0.3, top: 0.2, bottom: 0.2, header: 0.2, footer: 0.2 } workSheet.pageSetup.horizontalCentered = true workSheet.pageSetup.verticalCentered = false // 每页均显示的行 workSheet.pageSetup.printTitlesRow = '1:3' // 指定打印哪些列 workSheet.pageSetup.printTitlesColumn = 'A:G'解析 Excelconst ExcelJS = require('exceljs') const excelfile = './test.xlsx' var workbook = new ExcelJS.Workbook() workbook.xlsx.readFile(excelfile).then(function() { // 获取第一个worksheet var worksheet = workbook.getWorksheet(1) // 编辑worksheet worksheet.eachRow(function(row, rowNumber) { var rowSize = row.cellCount var numValues = row.actualCellCount console.log('单元格数量/实际数量:' + rowSize+'/' + numValues) row.eachCell(function(cell, colNumber) { // cell.type单元格类型:6-公式 ;2-数值;3-字符串 let cellValue = cell.value if(cell.type === 6) { cellValue = cell.result } console.log('cell',rowNumber, colNumber, cellValue, cell.numFmt) }) }) })数值格式化问题前期为满足跟前端列表展现业务逻辑保持一致,金额及数值格式化均使用 Intl.NumberFormat进行格式化,但是该类格式化后的数据是字符串,不便于导出后对数据进行排序及数值比较等操作。由于官方文档的单元格格式化说明信息较简单,Excel里对数值格式化的方式较丰富,如下图:为完整的复原格式化后的内容,可以先在Excel文件中对相关单元格设置好格式后,使用上面的 解析 Excel 章节的代码解析获取单元格的 numFmt数据。下面列举本工程中用到的数值格式化 numFmt 值:数值格式化:#,##0.00_ 货币格式化:"¥"#,##0.00;"¥"-#,##0.00
2022年06月15日
111 阅读
0 评论
0 点赞
2022-06-15
前端库推荐:JSZip
jszip是一个用于创建、读取和编辑.zip文件的JavaScript库,且API的使用也很简单链接官网:https://stuk.github.io/jszip/Github:https://hub.fastgit.xyz/Stuk/jszip示例var zip = new JSZip(); zip.file("Hello.txt", "Hello World\n"); var img = zip.folder("images"); img.file("smile.gif", imgData, {base64: true}); zip.generateAsync({type:"blob"}) .then(function(content) { // see FileSaver.js saveAs(content, "example.zip"); });兼容性OperaFirefoxSafariChromeInternet ExplorerNode.jsYesYesYesYesYesYesTested with the latest versionTested with 3.0 / 3.6 / latest versionTested with the latest versionTested with the latest versionTested with IE 6 / 7 / 8 / 9 / 10Tested with node.js 0.10 / latest version
2022年06月15日
12 阅读
0 评论
0 点赞
2022-06-06
vue-cli工程打包生成gzip相关压缩文件
一、添加依赖yarn add vue-cli-plugin-compression -D二、更新vue.config.js文件添加zlib引用const zlib = require('zlib')module.exports 中添加如下配置信息:pluginOptions: { compression:{ brotli: { filename: '[file].br[query]', algorithm: 'brotliCompress', include: /\.(js|css|html|svg|json)(\?.*)?$/i, compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, }, minRatio: 0.8, }, gzip: { filename: '[file].gz[query]', algorithm: 'gzip', include: /\.(js|css|html|svg|json)(\?.*)?$/i, minRatio: 0.8, } } }效果打包后生成相应的.br和.gz结尾的文件
2022年06月06日
14 阅读
0 评论
0 点赞
2022-05-13
H5与小程序直播入门-学习笔记
链接:https://www.bilibili.com/video/BV1b7411T7t1直播原理HLS协议动态列表:基本上未使用静态列表全量列表:一般用于直播RTMP协议RTMP是Real Time Messaging Protocol (实时消息传输协议)的首字母缩写。该协议基于 TCP,是一个协议族,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash、AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。flvHTTP-FLV协议1、可以在一定程度上避免防火墙的干扰(例如,有的机房只允许80端口通过).2、可以很好的兼容HTTP 302跳转,做到灵活调度.3、可以使用HTTPS做加密通道.4、很好的支持移动端(Android, IOS).video详解controls:是否显示控制条controlslist:控制条显示内容autoplay:自动播放poster:封面图loop:循环播放preload:预加载,在不同设备上行为有些不一致volume:音量,通过 js 控制muted:是否静音<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>video 详解</title> </head> <body> <video src="./media/blsc.mp4" id="video" autoplay muted loop preload="auto" controls controlslist="nodownload nofullscreen" width="800" height="600" poster="./images/poster.jpg"></video> <script> var videoEl = document.getElementById('video') // 调整音量 videoEl.volumn = 0.5 // 设置进度,单位为秒 videoEl.currentTime = 60 // 切换视频地址 videoEl.src = './media/paratera.mp4' </script> <video id="sourceDemo" controls poster="./images/poster.jpg"> <source src="./media/blsc.mp4" type="video/mp4"> <source src="./media/paratera.mp4" type="video/mp4"><!-- 第一个错误时使用第二个地址进行播放 --> </video> <script> var videoEl = document.getElementById('sourceDemo') console.log('videoEl.currentSrc', videoEl.currentSrc) </script> </body> </html>video 事件<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>video 事件</title> </head> <body> <video src="./media/blsc.mp4" id="video" autoplay muted preload="auto" controls controlslist="nodownload nofullscreen" width="800" height="600" poster="./images/poster.jpg"></video> <script> var videoEl = document.getElementById('video') // 开始加载视频 video.addEventListener('loadstart', e => { console.log('loadStart', e) }) // 时长变化,不一定发生一次 video.addEventListener('durationchange', e => { console.log('durationchange', e, video.duration) }) // 视频的元数据下载完成 video.addEventListener('loadedmetadata', e => { console.log('loadedmetadata', e) }) // 数据加载完成 video.addEventListener('loadeddata', e => { console.log('loadeddata', e) }) // 播放进度 video.addEventListener('progress', e => { console.log('progress', e) }) // 有帧可以播放 video.addEventListener('canplay', e => { console.log('canplay', e) }) // 可以流畅播放 video.addEventListener('canplaythrough', e => { console.log('canplaythrough', e) }) // 监听播放 video.addEventListener('play', e => { console.log('play', e) }) // 监听暂停 video.addEventListener('pause', e => { console.log('pause', e) }) // seeking,点进度条加载,开始查找 video.addEventListener('seeking', e => { console.log('seeking', e) }) // seeked,下载数据完成后,标记seek结束 video.addEventListener('seeked', e => { console.log('seeked', e) }) // waiting,解码过程中 video.addEventListener('waiting', e => { console.log('waiting', e) }) // playing,暂停到播放状态之间会触发playing video.addEventListener('playing', e => { console.log('playing', e) }) // timeupdate,一般用于更新播放进度条 video.addEventListener('timeupdate', e => { console.log('timeupdate', e) }) // ended:播放结束 video.addEventListener('ended', e => { console.log('ended', e) }) // error:异常捕获,浏览器自带重试机制 video.addEventListener('error', e => { console.log('error', e) }) </script> </body> </html>
2022年05月13日
39 阅读
0 评论
0 点赞
2022-04-20
Web安全-学习笔记
Web安全,也可以叫做Web应用安全。互联网本来是安全的,自从有了研究安全的人之后,互联网就变的不安全了。HTTP协议与会话管理当我们访问一个网址的时候,这中间发生了什么?输入网址浏览器查找域名的IP地址浏览器给Web服务器发送一个HTTP请求服务端处理请求服务端发回一个HTTP响应Web应用的组成与网页的渲染浏览器特性与安全策略Cookie的安全策略内容安全策略XSSXSS,全称跨站脚本(Cross Site Scripting), 一种注入式攻击方式。XSS成因对于用户输入没有严格控制而直接输出到页面对非预期输入的信任XSS的危害盗取各类用户账号,如机器登录账号、用户网银账号、各类管理员账号窃取数据非法转账挂马……XSS的分类存储型(持久型)反射型(非持久型)http://www.xx.com/search.html?key_ pro="><script>confirm(1501)</script>DOM型其实DOM型也属于反射型的一种,不过比较特殊,所以一般也当做一种单独类其他XSS类别mXSS(突变型XSS)UXSS(通用型XSS)Flash XSSUTF-7 XSSMHTML XSS - 仅IE低版本CSS XSS - 仅IE低版本VBScript XSS - 仅IECSRFCSRF,全称跨站伪造请求(Cross-site request forgery),也称为one click attack/session riding,还可以缩写为XSRF。CSRF与XSS的区别XSS:利用对用户输入的不严谨然后执行JS语句CSRF:通过伪造受信任用户发送请求CSRF可以通过XSS来实现CSRF的防御方法通过验证码进行防御检查请求来源增加请求参数token
2022年04月20日
27 阅读
0 评论
0 点赞
1
2
3