Write the Code. Change the World.

分类目录
7月 10

后台管理系统,个人审美还是更喜欢 element-plus 多一点。如果组件更丰富点就更好了。

https://element-plus.org/zh-CN/guide/installation.html

安装

# 安装
pnpm add element-plus

# 自动导入,安装unplugin-vue-components 和 unplugin-auto-import 这两款插件
pnpm add -D unplugin-vue-components unplugin-auto-import

配置自动导入
编辑 vite.config.ts

import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

追加 AutoImportComponents 配置。完整的配置文件如下:

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vite.dev/config/
export default defineConfig({
  base: '/admin/',
  build: {
    outDir: 'admin',
    emptyOutDir: true,
    chunkSizeWarningLimit: 3000,
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js', // 入口文件名
        chunkFileNames: 'assets/[name].[hash].js', // chunk 文件名
        assetFileNames: 'assets/[name].[hash].[ext]', // 静态资源文件名
      },
    },
  },
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

下边进行尝试使用

使用

编辑 src/views/home/index.vue,增加按钮组件,尝试下效果。

<template>
  <div>
    <h1>HI, {{ user.name }} , Welcome to the Home Page</h1>
    <h2>Email: {{ user.email }}</h2>
    <h2>Api: {{ api }}</h2>
    <div>
      <el-button type="primary" @click="test">Click Me</el-button>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const user = ref<User>({
  id: 1,
  name: 'John Doe',
  email: 'john.doe@example.com',
  createdAt: new Date(),
})

const api = ref(import.meta.env.VITE_API_BASE_URL)

function test() {
  alert('Button Clicked!')
}
</script>

<style scoped>
h1 {
  color: #42b983;
  font-size: 2em;
}
</style>

运行起来,效果如下图所示。

7月 09

vite7: https://vite.dev/blog/announcing-vite7

node: https://nodejs.org/en/download

创建初始化

现在创建一个叫 forgetting 的项目

pnpm self-update

pnpm create vue@latest

# 引导构建

┌  Vue.js - The Progressive JavaScript Framework
│
◇  请输入项目名称:
│  forgetting
│
◇  请选择要包含的功能: (↑/↓ 切换,空格选择,a 全选,回车确认)
│  TypeScript, Router(单页面应用开发), Pinia(状态管理), ESLint(错误预防), Prettier(代码格式化)
│
◇  选择要包含的试验特性: (↑/↓ 切换,空格选择,a 全选,回车确认)
│  Oxlint(试验阶段), rolldown-vite(试验阶段)

正在初始化项目 C:\Users\Windows\Desktop\study\vue\forgetting...
│
└  项目初始化完成,可执行以下命令:

创建完成后,执行下边命令,运行起来。

   cd forgetting
   pnpm install
   pnpm format
   pnpm dev

提交版本。

git init -b main
git add .
git commit -m 'initialize'

删除默认页面,组件,样式

rm -rf ./src/views/*
rm -rf ./src/components/*
rm -rf ./src/assets/logo.svg

新增一个首页,运行起来

touch ./src/views/home/index.vue

# 添加以下内容
<template>
    <div>
        <h1>Welcome to the Home Page</h1>
    </div>
</template>

修改 App.vue

<template>
  <RouterView />
</template>

调整路由 router.ts

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../views/home/index.vue'),
    },
  ],
})

export default router

去掉 assets/main.css 中的其他代码,保留。

@import './base.css';

#app {
  margin: 0;
  padding: 0;
  font-weight: normal;
}

a,
.green {
  text-decoration: none;
  color: hsla(160, 100%, 37%, 1);
  transition: 0.4s;
  padding: 3px;
}

重新运行

pnpm dev

> forgetting@0.0.0 dev C:\Users\Windows\Desktop\study\vue\forgetting
> vite

Port 5173 is in use, trying another one...

  ROLLDOWN-VITE v7.0.6  ready in 858 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose
  ➜  Vue DevTools: Open http://localhost:5174/__devtools__/ as a separate window
  ➜  Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
  ➜  press h + enter to show help

可以执行代码检查

pnpm lint

> forgetting@0.0.0 lint C:\Users\Windows\Desktop\study\vue\forgetting
> run-s lint:*

> forgetting@0.0.0 lint:oxlint C:\Users\Windows\Desktop\study\vue\forgetting
> oxlint . --fix -D correctness --ignore-path .gitignore

Found 0 warnings and 0 errors.
Finished in 12ms on 8 files with 87 rules using 16 threads.

> forgetting@0.0.0 lint:eslint C:\Users\Windows\Desktop\study\vue\forgetting
> eslint . --fix

C:\Users\Windows\Desktop\study\vue\forgetting\src\views\home\index.vue
  1:1  error  Component name "index" should always be multi-word  vue/multi-word-component-names

✖ 1 problem (1 error, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.
ERROR: "lint:eslint" exited with 1.
 ELIFECYCLE  Command failed with exit code 1.

官方默认的vue文件名是以大写字母开头,多单词的形式。或以中横线连接。个人喜欢以名字+动词的形式来定义文件。即以单数名字作为文件夹名,动词作为文件名。比如,想要一个创建订单的页面可以这样:order/create.vue,如果想要一个订单列表的页面就是 order/list.vue。 这种规则和默认规则相冲,eslint 检查出错误来。所以可以手动修改该策略。

对于组件命名,还是喜欢用大写字母开头,多单词的形式。 components/MediaPlayer/MediaPlayer.vue

修改 eslint.config.ts,增加规则来允许上边所说的消息单个单词的文件命名。

  {
    rules: {
      'vue/multi-word-component-names': 'off',
    },
  }

需要扩展和覆盖规则都可以在这里进行。因为是刚创建的项目,所以配置很干净除了新增这一条,其他都是默认的,完整如下:

import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'

// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup

export default defineConfigWithVueTs(
  {
    name: 'app/files-to-lint',
    files: ['**/*.{ts,mts,tsx,vue}'],
  },

  globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),

  pluginVue.configs['flat/essential'],
  vueTsConfigs.recommended,
  ...pluginOxlint.configs['flat/recommended'],
  skipFormatting,
  {
    rules: {
      'vue/multi-word-component-names': 'off',
    },
  }
)

再执行 pnpm lint 就不会报错了。

执行 pnpm format 格式化代码。中间不规范操作,都可以通过这个来自动纠正。

提交版本。 git add . && git commit -m '删除默认页面、组件、样式,配置支持单个单词文件名的eslint规则'

7月 03

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

https://vite-pwa-org-zh.netlify.app/guide/

渐进式 Web 应用(Progressive Web App,PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体验就像一个特定平台的应用程序。

它像网站一样,PWA 可以通过一个代码库在多个平台和设备上运行。它也像一个特定平台的应用程序一样,可以安装在设备上,可以离线和在后台运行,并且可以与设备和其他已安装的应用程序集成。

特点

  1. 体验类似原生应用,而不是浏览器打开的网站。、
  2. 离线使用部分功能。

学习参考

https://juejin.cn/post/7497868344223989794

https://juejin.cn/post/7294554207096750090

https://ionic.nodejs.cn/vue/pwa

5月 29

微信小程序 xr-frame 有问题,bug。在使用微信小程序 xr-frame 功能时,测试版,体验版都没问题,发布版就有问题。其实,这个不是 xr-frame 的问题。在提交小程序审核的时候,一定要把使用隐私保护添加上,里边使用摄像头的权限也要添加上。

5月 08

https://docs.unity3d.com/cn/2021.3/Manual/webgl-interactingwithbrowserscripting.html

unity 调用 js

请使用 .jslib 扩展名将包含 JavaScript 代码的文件放置在 Assets 文件夹中的“Plugins”子文件夹下。格式如下。

mergeInto(LibraryManager.library, {

  Hello: function () {
    window.alert("Hello, world!");
  },

  HelloString: function (str) {
    window.alert(UTF8ToString(str));
  }
});
3月 07

https://developers.weixin.qq.com/miniprogram/dev/component/xr-frame/overview/#%E6%A6%82%E8%BF%B0

基于xr-frame实现微信小程序的图片扫描识别AR功能,通过编写节点就能完成。这里使用 uniapp 构建。

基本操作

  1. 使用 uniapp 创建一个默认项目。 文件->创建->项目

  1. 在项目根目录下创建 wxcomponents 目录。在该目录下创建 xrtracker 目录,这个目录用来存放微信小程序的代码。在这个目录中创建。 index.json,index.wxml,index.js 文件。

index.json

{
    "component": true,
    "renderer": "xr-frame",
    "usingComponents": {}
}

index.wxml

<xr-scene>
    <xr-camera id="camera" clear-color="0.2 0.4 0.6 1" camera-orbit-control/>
</xr-scene>

index.js

Component({
    properties: {
    },
    data:{
    },
    lifetimes: {

    },
    methods: {

    }
})
  1. 这里直接在默认的 pages/index/index.vue 中修改。内容如下。

    <template>
    <xr-tracker></xr-tracker>
    </template>
  2. 修改 pages.json,将微信小程序组件引入进来。

            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "uni-app",
                // #ifdef MP-WEIXIN
                "usingComponents": {
                    "xr-tracker": "/wxcomponents/xrtracker/index"
                }
                // #endif
            }
  3. 还得打开 manifest.json 文件,在 mp-weixin 节点增加以下配置。

    "mp-weixin" : {
        "appid" : "xxxx",
        "setting" : {
            "urlCheck" : false,
            "es6" : true,
            "postcss" : false,
            "minified" : true
        },
        "usingComponents" : true,
        "lazyCodeLoading" : "requiredComponents"
    },

做好上边几步,可以开始运行起来。如下图所示 。到此,一个基本的 xr-frame 调用就完成了。

下边来调整样式,使得显示好看一些 。修改 pages/index/index.vue 如下:

<template>
    <xr-tracker disable-scroll :width="renderWidth" :height="renderHeight" :style="style"></xr-tracker>
</template>

<script setup>
    import { ref, computed } from 'vue'
    import { onLoad } from '@dcloudio/uni-app'

    const width = ref(300)
    const height = ref(300)
    const renderWidth = ref(300)
    const renderHeight = ref(300)

    const style = computed(() => {
        return `width:${width.value}px;height:${height.value}px;`
    })

    onLoad(() => {
        const windowInfo = uni.getWindowInfo()
        width.value = windowInfo.windowWidth
        height.value = windowInfo.windowHeight
        renderWidth.value = windowInfo.windowWidth * windowInfo.pixelRatio
        renderHeight.value = windowInfo.windowHeight * windowInfo.pixelRatio
    })
</script>

识图

https://developers.weixin.qq.com/miniprogram/dev/component/xr-frame/ar/tracker.html#%E4%BA%8C%E7%BB%B4Marker

https://developers.weixin.qq.com/miniprogram/dev/component/xr-frame/ar/#%E4%B8%8D%E5%90%8CAR%E8%BF%BD%E8%B8%AA%E5%99%A8%E7%9A%84%E5%9D%90%E6%A0%87%E7%B3%BB%E5%B7%AE%E5%BC%82

要实现识图过程,得用到 ar 追踪器。使用到的标签有根标签 xr-scene,有资源标签 xr-assets,节点标签 xr-node,还有追踪器标签 xr-ar-tracker。以及 xr-camera 和 xr-light 等。

这里至少需要一张图片url(识别对象),一个模型文件(通常是模型。其他也是可以)。如果涉及到声音,还需要音频文件。为了方便,这里定义三个属性给到外部。

部分代码如下。

<xr-scene ar-system="modes:Marker" bind:ready="handleReady" bind:ar-ready="handleARReady">
    <!-- 资源加载 -->
  <xr-assets bind:progress="handleAssetsProgress" bind:loaded="handleAssetsLoaded">
    <xr-asset-load type="gltf" asset-id="gltf-model" src="{{modelUrl}}" />
  </xr-assets>

  <xr-env env-data="xr-frame-team-workspace-day" />

  <xr-node wx:if="{{arReady}}">
    <xr-ar-tracker mode="Marker" src="{{markerImgUrl}}" bind:ar-tracker-switch="handleTrackerSwitch">
      <xr-gltf position="0 0 0" scale="1 1 1" rotation="-108 -90 90" anim-autoplay model="gltf-model" bind:gltf-loaded="handleGLTFLoaded" />
    </xr-ar-tracker>

    <xr-camera id="camera" node-id="camera" clear-color="0 0 0 0" position="1 1 2" background="ar" is-ar-camera camera-orbit-control/>
  </xr-node>

  <xr-node node-id="lights">
    <xr-light type="ambient" color="1 1 1" intensity="1" />
    <xr-light type="directional" rotation="180 0 0" color="1 1 1" intensity="3" />
  </xr-node>
</xr-scene>

然后修改 index.js 文件,来适配 wxml 文件。比如在识别成功后播放音频。

        handleTrackerSwitch({ detail }) {
            const { value } = detail;
            if (value) {
                console.log("识别成功,展示模型");

                if (this.audioContext) {
                    this.audioContext.play()
                }
            } else {
                console.log("识别失败或 Marker 失去跟踪");
            }
        },
1月 22

The left-hand side of an assignment expression may not be an optional property access.t ts 中,这个报错通常是在给可能为空或undefined的对象赋值引起的。

使用可选链操作符 ?. 来给一个可能为 undefined 的对象赋值也会引起上边这个错误。如:

obj?.name = '123'

?.链式操作符仅用于读取属性或调用方法,而不能用于赋值。

解决方法

  1. 使用 if 判断
if (obj) {
    obj.name = '123'
}
  1. 使用逻辑与操作符 &&

    obj && (obj.name = '123')
  2. 使用非空断言操作符
    如果你确定对象不会是 undefined 或 null,可以使用非空断言操作符(!)

    obj!.name = value

    但这种方法比较危险,因为如果 obj 确实是 undefined 或 null,运行时会抛出错误

1月 10

前端 html 页面中,选择上传视频文件,获取视频文件的长度以及截取封面是个很常见的功能。前端 js 也能实现这个功能。

流程: file -> loadedmetadata(获取视频元信息)->currentTime(定格到视频的位置)->绘制到 canvas->转换成图片

  1. 通过 input(file) 选择文件。
    https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file

    <div>
    <label class="upload-btn" for="video">
        <span class="text">上传</span>
    </label>
    <input class="hidden" id="video" name="video" type="file" accept="video/mp4" @change="changeVideo" />
    </div>
  2. 获取视频元信息。
    https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video

使用 createObjectURL 创建的URL是一个blob:开头的临时路径,这个路径可以在浏览器中直接访问,访问到的内容就是上传的视频文件。当页面关闭后,此路径也随之失效。

function changeVideo() {
    const fileInput: HTMLInputElement = document.getElementById('video') as HTMLInputElement
    const files: FileList = fileInput?.files as FileList
    const file = files[0]

    const video = document.createElement('video')
    video.src = URL.createObjectURL(file)
    video.addEventListener('loadedmetadata', function () {
        console.log(video.duration)
    })
}
  1. 定格到视频位置

    // 设置视频自动播放
    video.autoplay = true
    // 设置视频播放的时间(方便截图)
    video.currentTime = 1
  2. 绘制 canvas

    const canvas = document.createElement("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext("2d");
    if (ctx) {
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    // document.body.appendChild(canvas);
    }
  3. 将 canvas 转换成图片

    canvas.toBlob((blob) => {
    if (blob) {
    const url = URL.createObjectURL(blob);
    }
    });

完整的 ts。


interface VideoInfo {
    name: string
    width: number
    height: number
    thumbnail?: string
    duration?: number
}

function getVideoInfo(file: File, maxWidth = 320) {
    return new Promise<VideoInfo>((resolve) => {
        const index = file.name.lastIndexOf('.')
        const name = index > 0 ? file.name.substring(0, index) : ''
        const videoMedia: VideoInfo = {
            name,
            width: 0,
            height: 0
        }

        const video = document.createElement('video')
        video.src = URL.createObjectURL(file)
        video.addEventListener('loadedmetadata', function () {
            videoMedia.width = video.videoWidth
            videoMedia.height = video.videoHeight
            videoMedia.duration = video.duration
        })

        // 监听视频跳转完成事件
        video.addEventListener('seeked', function () {
            // 创建画布并绘制视频帧
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')

            if (video.videoWidth > maxWidth) {
                canvas.width = maxWidth
                canvas.height = Math.round((maxWidth / video.videoWidth) * this.videoHeight)
            } else {
                canvas.width = video.videoWidth
                canvas.height = video.videoHeight
            }

            if (ctx) {
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
                canvas.toBlob((blob) => {
                    if (blob) {
                        videoMedia.thumbnail = URL.createObjectURL(blob)
                        resolve(videoMedia)
                    } else {
                        resolve(videoMedia)
                    }
                })
            } else {
                resolve(videoMedia)
            }
            // 释放创建的临时URL
            // URL.revokeObjectURL(video.src)
        })

        // 设置视频自动播放
        video.autoplay = true
        // 设置视频播放的时间(方便截图)
        video.currentTime = 1
    })
}