Write the Code. Change the World.

8月 28

el-tooltip 和 el-popover 嵌套使用的时候,可能会遇到你意向不到的情况。怎么处理呢。

  • el-tooltip 嵌套 el-popover
  • 在嵌套 el-popover 的时候,加一层 div

仅此而已,就会出现你想看到的效果了。

<el-tooltip content="我不知道风是往哪一个方向吹" placement="top">
        <div>
              <el-popover placement="bottom-start" trigger="click">
              </el-popover>
        </div>
</el-tooltip>

就这样咯。

8月 24

前端项目,打包后,运行起来就可能会出现一些提示。描述语是 Violation 。看着不舒服,还是要解决的。列举一下

[Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event.

出现报错的原因:

Chrome51 版本以后,Chrome 增加了新的事件捕获机制-Passive Event Listeners;

Passive Event Listeners:被动事件侦听器是 DOM 规范中的一项新功能,它使开发人员能够通过消除滚动来阻止触摸和滚轮事件侦听器来选择加入以获得更好的滚动性能。

开发人员可以使用 {passive: true} 来设置监听器后内部不会调用 preventDefault() 来阻止默认滑动行为,Chrome 浏览器称这类型的监听器为被动(passive)监听器。

Passive Event Listeners 就是告诉前页面内的事件监听器内部是否会调用 preventDefault 函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。

目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件。

参考这篇课解决 https://juejin.cn/post/7230806990452588581

[Violation] Forced reflow while executing JavaScript

8月 23

列举列举

interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "applet": HTMLAppletElement;
    "area": HTMLAreaElement;
    "article": HTMLElement;
    "aside": HTMLElement;
    "audio": HTMLAudioElement;
    "b": HTMLElement;
    "base": HTMLBaseElement;
    "bdi": HTMLElement;
    "bdo": HTMLElement;
    "blockquote": HTMLQuoteElement;
    "body": HTMLBodyElement;
    "br": HTMLBRElement;
    "button": HTMLButtonElement;
    "canvas": HTMLCanvasElement;
    "caption": HTMLTableCaptionElement;
    "cite": HTMLElement;
    "code": HTMLElement;
    "col": HTMLTableColElement;
    "colgroup": HTMLTableColElement;
    "data": HTMLDataElement;
    "datalist": HTMLDataListElement;
    "dd": HTMLElement;
    "del": HTMLModElement;
    "details": HTMLDetailsElement;
    "dfn": HTMLElement;
    "dialog": HTMLDialogElement;
    "dir": HTMLDirectoryElement;
    "div": HTMLDivElement;
    "dl": HTMLDListElement;
    "dt": HTMLElement;
    "em": HTMLElement;
    "embed": HTMLEmbedElement;
    "fieldset": HTMLFieldSetElement;
    "figcaption": HTMLElement;
    "figure": HTMLElement;
    "font": HTMLFontElement;
    "footer": HTMLElement;
    "form": HTMLFormElement;
    "frame": HTMLFrameElement;
    "frameset": HTMLFrameSetElement;
    "h1": HTMLHeadingElement;
    "h2": HTMLHeadingElement;
    "h3": HTMLHeadingElement;
    "h4": HTMLHeadingElement;
    "h5": HTMLHeadingElement;
    "h6": HTMLHeadingElement;
    "head": HTMLHeadElement;
    "header": HTMLElement;
    "hgroup": HTMLElement;
    "hr": HTMLHRElement;
    "html": HTMLHtmlElement;
    "i": HTMLElement;
    "iframe": HTMLIFrameElement;
    "img": HTMLImageElement;
    "input": HTMLInputElement;
    "ins": HTMLModElement;
    "kbd": HTMLElement;
    "label": HTMLLabelElement;
    "legend": HTMLLegendElement;
    "li": HTMLLIElement;
    "link": HTMLLinkElement;
    "main": HTMLElement;
    "map": HTMLMapElement;
    "mark": HTMLElement;
    "marquee": HTMLMarqueeElement;
    "menu": HTMLMenuElement;
    "meta": HTMLMetaElement;
    "meter": HTMLMeterElement;
    "nav": HTMLElement;
    "noscript": HTMLElement;
    "object": HTMLObjectElement;
    "ol": HTMLOListElement;
    "optgroup": HTMLOptGroupElement;
    "option": HTMLOptionElement;
    "output": HTMLOutputElement;
    "p": HTMLParagraphElement;
    "param": HTMLParamElement;
    "picture": HTMLPictureElement;
    "pre": HTMLPreElement;
    "progress": HTMLProgressElement;
    "q": HTMLQuoteElement;
    "rp": HTMLElement;
    "rt": HTMLElement;
    "ruby": HTMLElement;
    "s": HTMLElement;
    "samp": HTMLElement;
    "script": HTMLScriptElement;
    "section": HTMLElement;
    "select": HTMLSelectElement;
    "slot": HTMLSlotElement;
    "small": HTMLElement;
    "source": HTMLSourceElement;
    "span": HTMLSpanElement;
    "strong": HTMLElement;
    "style": HTMLStyleElement;
    "sub": HTMLElement;
    "summary": HTMLElement;
    "sup": HTMLElement;
    "table": HTMLTableElement;
    "tbody": HTMLTableSectionElement;
    "td": HTMLTableDataCellElement;
    "template": HTMLTemplateElement;
    "textarea": HTMLTextAreaElement;
    "tfoot": HTMLTableSectionElement;
    "th": HTMLTableHeaderCellElement;
    "thead": HTMLTableSectionElement;
    "time": HTMLTimeElement;
    "title": HTMLTitleElement;
    "tr": HTMLTableRowElement;
    "track": HTMLTrackElement;
    "u": HTMLElement;
    "ul": HTMLUListElement;
    "var": HTMLElement;
    "video": HTMLVideoElement;
    "wbr": HTMLElement;
}
8月 23

很多很多年前,网页端图片裁切都会想到 cropper.js 。到了当前, cropper.js 还是值得一选。这个是基于 js,ts 的插件,不局限你使用 vue 还是 使用 react 或其他的。反正,还是很好用。当下 2.0 版本更好用。

官网: https://fengyuanchen.github.io/cropperjs/v2/zh/

演练场:https://fengyuanchen.github.io/cropperjs/v2/zh/playground.html

这个插件越来越自由,也越来越集中。它只关注输入(img dom)和输出(canvas)[转换成 base64 可能会存在跨域问题]。大只分以下几个模块(每个模块都可以单独使用)。

Cropper

CropperCanvas

CropperImage

CropperSelection

CropperShade

CropperGrid

CropperViewer
# 等等

这些,都各自有各自的渲染职责。加上就有,配置就改变。不加则没有。想加就加,想减就减。是真的好用。 先看一个我做的一个 demo。是基于vue3 + ts 实现的。(不对。里边还加了一些 element-plus 的组件)

构建的时候,也很方便。使用其接口就可以。更好的是是基于组件的方式来处理。最终的目的是什么,是要渲染到 dom 中,让人看到。 组件不就是最直接的方式吗。这个时候,你可以很方便的使用 css 去美化你的产品。只需要少量关注一些配置就好。比如裁切比例,覆盖比列等。

继续阅读

8月 17

有封装好的 useDark useToggle 这些插件,做项目就很方便。可是用的时候,会遇到一些问题。其实到最后,不是问题了。比如改变主题的时候,transition 失效了。因为没 transition,强行改变背景颜色等就会很生硬。transition 失效是因为 userColorMode 在处理改变的时候,先禁用 transition 再取消禁用导致的。在创建 useDark 的时候,options 的 disableTransition 可以控制该行为,只是不传值的时候,默认就是 true,就是禁用 trasition。那么默认传入 disableTransition 为 false 的值就可以搞定了。

https://github.com/vueuse/vueuse/blob/main/packages/core/useColorMode/index.ts#L122

const isDark = useDark({disableTransition: false})
 const toggleDark = useToggle(isDark)

只需上边这样实现即可。

继续阅读

8月 15

type 和 interface 相关

https://juejin.cn/post/7063521133340917773

https://blog.csdn.net/qq_42345237/article/details/124895617

namespace 和 modules 相关

https://zhuanlan.zhihu.com/p/56583269

Pick、Omit、Extract、Exclude 等

https://juejin.cn/post/7202153645441892407
https://www.jianshu.com/p/cb8923a7a31b

泛型相关

https://zhuanlan.zhihu.com/p/149767010

https://zhuanlan.zhihu.com/p/149965449

文档

https://www.typescriptlang.org/zh/docs/

https://nodejs.cn/typescript/get-started/

额外

https://developer.mozilla.org/zh-CN/docs/Web

8月 14

前端本地测试,mock 可以先用着。终究还是得和真实的服务环境对接。这个时候请求接口,往往就会遇到跨域的问题。对于 laravel框架,对于 laravel10,只需要建立一个中间件,在中间件中对请求进行处理就好。需要的时候,带上中间件,不需要的时候,去掉它就可以。还是很方便的。

Do

php artisan make:middleware EnableCrossRequestMiddleware

# 完整代码如下
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnableCrossRequestMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        $origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
        $allow_origin = [
            'http://localhost:5173',
        ];

        if (in_array($origin, $allow_origin)) {
            $IlluminateResponse = 'Illuminate\Http\Response';
            $SymfonyResopnse = 'Symfony\Component\HttpFoundation\Response';
            $headers = [
                'Access-Control-Allow-Origin' => $origin,
                'Access-Control-Allow-Methods' => 'POST,GET,OPTIONS,PUT,PATCH,DELETE',
                'Access-Control-Allow-Headers' => 'Accept,Content-Type,Referer,User-Agent,Origin,X-Requested-With,X-XSRF-TOKEN,X-CSRF-TOKEN,Authorization,Time'
            ];

            if ($response instanceof $IlluminateResponse) {
                foreach ($headers as $key => $value) {
                    $response->header($key, $value);
                }
                return $response;
            }

            if ($response instanceof $SymfonyResopnse) {
                foreach ($headers as $key => $value) {
                    $response->headers->set($key, $value);
                }
                return $response;
            }
        }
        return $response;
    }
}

然后,在 ./app/Http/Kernel.php 的 $middleware 中添加一个配置。

    protected $middleware = [
        …
        \App\Http\Middleware\EnableCrossRequestMiddleware::class,
    ];

这样就完事了。

本来是想将中间件放在中间件组里(middlewareGroups)或使用 alias (放在 $middlewareAliases)的方式来使用,但都失败了。暂时还没找到原因。

之所以想这样弄,是因为不是所有的请求都需要设置请求头。

继续阅读

8月 13

经过前边的处理,用户登录,侧边栏渲染,路由跳转,登录信息展示,用户退出。这些功能最小化完成。如下图所示。

在线预览,可切换角色。新用户注册,有一个默认角色。只有测试账号有多角色。

https://www.zeipan.com/admin

用户登录

打开后台域名任意 url 。在路由全局守卫中。 如果用户已登录(存在 token),先判断用户信息是否初始化,如果没有,先请求用户信息(返回数据不仅有用户信息,还有该用户对应的后台权限【用户生产菜单】),处理存储用户信息,添加路由信息。再重定向。如果已初始化,就继续。如果用户没登录,先重定向到登录页面。

调用接口,请求登录。获取用户的 token。存储 token。

通过 token,请求用户信息(数据中包括用户的权限数据),存储用户信息以及权限数据。处理权限数据,生成菜单。

8月 04

做了一些基础的配置。现在准备开始做布局了。这里只要两种布局,空白布局和后台布局。空白布局什么时候用呢,登录页面会用到。后台布局,就后台真正内容会用到。始终喜欢极简风格,所以方方面面都不要搞的太复杂。后台布局页面主要有左侧菜单栏,顶部信息栏,中间内如区域构成。

要做好这些,先温习一下 vue3 的 vue-router 以及 transition 还有 element plus 的 menu 组件。当然,loading 相关的也会考虑进去。

额外一点。左侧菜单的内容(和路由会一一映射),请求 api 的权限,全部有后端生成。前端不做权限的控制。也就是后端返回过来的菜单是什么样子,前端就渲染成什么样子。

https://router.vuejs.org/zh/

https://cn.vuejs.org/guide/built-ins/transition.html

https://element-plus.org/zh-CN/component/menu.html#collapse-%E6%8A%98%E5%8F%A0%E9%9D%A2%E6%9D%BF

基础布局

先看看构成后台的三部分。这里建立了一个 layout,拆分成了三部分。

src/views
├── HomeView.vue
├── layouts
│   ├── components
│   │   ├── AppMain
│   │   │   └── AppMain.vue
│   │   ├── NavBar
│   │   │   └── NavBar.vue
│   │   └── SideBar
│   │       └── SideBar.vue
│   └── index.vue
└── test.vue

路由是这样的。

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Layout from '@/views/layouts/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    }, 
    {
      path: '/test',
      name: 'test',
      component: Layout,
      children: [
        {
          path: '',
          name: 'test-child',
          component: () => import('@/views/test.vue')
        }
      ]
    }
  ]
})

export default router

这里只是初步尝试,没有做更细致的逻辑编写。

希望你的屏幕足够宽,希望一屏足够承载你的业务。如果不够,如果不可以。那么滚动条在所难免。那么滚动条怎么出现最好看呢。

一种方式如上图所示。还有一种,可以只在 content 区域左右上下滚动。个人觉得那种不好看,交互也不是很好。最好就是一屏足够承载吧。

side bar 和 nav bar 都没使用背景色。滑动中间区域会看到穿透。真正做时,会有背景色,z-index 也会高出一些。

白天以及暗黑主题安排上,侧边栏展开收缩也安排上。

还有移动端的布局(小程序、h5、app 等等),特别是小程序。有的公司为了一套代码,给很多企业或多项目用。底部 tabbar 采用非原生写法,滚动条从顶滚到底,不仅滚动条丑,底部切换还会闪动。体验特不好。有时候为了好的滚动条效果,加入了 scroll-view 组件去控制。

这里,如果左侧菜单足够多,也会出现滚动条。这个在 side bar 内部去处理就好了。

继续阅读

8月 03

丰富的互联网资源,用 svg 文件作为 icon 已经很常见了。这里使用 iconify 来处理 icon。 iconify 可以理解为一个管理封装各个 icon 的工具。

https://iconify.design/

https://element-plus.org/zh-CN/component/icon.html

开始

# 安装
pnpm install unplugin-icons unplugin-vue-components -D

# 修改 vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @use "@/assets/styles/element.scss";
          @use "@/assets/styles/element-dark.scss";
          @use "@/assets/styles/main.scss";
          @use "@/assets/styles/preflight.scss";
        `,
      },
    },
  },
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver({
        importStyle: "sass",
        directives: true
      })],
    }),
    Components({
      dts: "./components.d.ts", 
      resolvers: [
        IconsResolver({
          prefix: "i", // 默认为i,设置为false则不显示前缀
          enabledCollections: ["ep"],
          alias: {
            'icon': "ep", //配置别名
          }
        }),
        ElementPlusResolver({
          importStyle: "sass",
          directives: true
        })
      ],
    }),
    Icons({
      autoInstall: true, // 是否自动安装对应的图标库,默认为true
      scale: 1, // 图标缩放,默认为1
      defaultStyle: "", // 图标style
      defaultClass: "", // 图标class
      compiler: 'vue3', // 编译方式,可选值:'vue2', 'vue3', 'jsx'
      jsx: "react", // jsx风格:'react' or 'preact'
    })
  ]
})

继续阅读