vite+vue

npm配置

npm config get registry                                                                 //查看镜像

npm config set registry https://registry.npmmirror.com                 //切换淘宝镜像

npm config set registry https://registry.npmjs.org                          //切回官方镜像

npm install -s element-ui@2.5.1                                                   //安装指定版本依赖

npm install -s element-ui@latest                                                  //安装最新版本依赖

npm update -s element-ui@latest                                                //更新依赖版本

vue常用api

import {
  ref, customRef, isRef, reactive, isReactive, toRefs, computed, watch,onWatcherCleanup, 
  watchEffect, nextTick, onMounted, onUpdated, onUnmounted, provide,
  inject, defineComponent, defineProps, defineEmits, defineSlots, defineExpose,
  defineModel, defineOptions, getCurrentInstance, useSlots, useAttrs, createApp
} from 'vue'
const my_name = ref('nick_name')
function useDebouncedRef(value, delay = 200) { //设置更新延迟delay,默认200ms
  let timeout //用于存储定时器
  return customRef((track, trigger) => {
    return {
      get() {
        track() //收集依赖
        return value
      },
      set(newValue) { //当值发生改变,获取新值
        clearTimeout(timeout) //清除上一个定时器
        timeout = setTimeout(() => { //设置定时器
          value = newValue //赋予新值
          trigger()//触发更新,不写则页面数据不更新
        }, delay)
      }
    }
  })
}
const phone = reactive({ name: 'vivo', money: 3333 })
isRef(my_name)
isReactive(phone)
const refs_phone = toRefs(phone)
computed(() => {
  return my_name.value + refs_phone.name.value
})
watch(
  () => my_name.value,
  (new_val, old_val) => {
    console.log(new_val, old_val);
    onWatcherCleanup(() => {
      // 下一次监听前做一些事情,和第三个参数功能一样
    })
  },
  {
    immediate: true,
    deep: true
  }
)
watchEffect(() => {
  console.log(my_name.value, phone.name);
})
nextTick(() => {
  phone.name = 'iPhone'
})
onMounted(() => {
  phone.money = 9999
})
onUpdated(() => {
  phone.money -= 1111
})
onUnmounted(() => {
  phone.money = 0
})

provide('phone', phone)

inject('money')

const my_component = defineComponent({
  name: 'my_component',
  emits: [],
  props: [],
  setup(props, { emit, slots, expose }) {
    return () => (
      'my_component的内容'  //使用jsx语法时,这里可以返回div等
    )
  }
})
//3.5版本增加解构默认赋值功能
const props = defineProps<{
  size?: string
  color?: string
}>()
const emit = defineEmits<{
  (event_name: 'name1', payload: string): void
  (event_name: 'name2', payload: number): void
}>()
defineSlots<{
  phone_color(scoped: { msg: string }): any
}>()
defineExpose<{
  func1(): void,
  obj1: Record<string, any>
}>({
  func1: () => { },
  obj1: {}
})

const new_msg = defineModel<string>('msg')

defineOptions({
  name: '组件名'
})

console.log('当前组件为', getCurrentInstance());

const slots = useSlots()

const attrs = useAttrs()

createApp('组件', { 传给组件的属性名: 属性值 }).mount('组件的父元素');

创建vue项目

npm init vite

 Eslint

安装Eslint

npm init @eslint/config

 .editorconfig

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

安装prettier

 pnpm i prettier -D

 创建.prettierrc.cjs文件

module.exports = {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  singleQuote: false,
  semi: true,
  trailingComma: "es5",
  bracketSpacing: true,
};

 关联eslint和prettier

pnpm i eslint-config-prettier eslint-plugin-prettier -D

 新版eslint

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
//加入
import commpnParser from 'vue-eslint-parser'
import prettier from 'eslint-plugin-prettier'

export default [
  {
    files: ["**/*.{js,mjs,cjs,ts,vue}"]
  },
  {
    languageOptions: { 
      globals: {
        ...globals.browser, ...globals.node
      } 
    }
  },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs["flat/essential"],
  {
    files: ["**/*.vue"], 
    languageOptions: {
      parserOptions: {
        parser: tseslint.parser
      }
    }
  },
  //加入
  {
    ignores: [
      '**/*.config.js',
      'dist/**',
      'node_modules/**',
      '!**/eslint.config.js',
    ],
    languageOptions: {
      // 1.11 定义可用的全局变量
      globals: {
        
      },
      // 1.12 扩展
      // ecmaVersion: "latest",
      // sourceType: "module",
      parser: commpnParser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        parser: '@typescript-eslint/parser',
        jsxPragma: 'React',
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
  },
  {
    plugins: {
      prettier,
    },
    rules: {
      // 开启这条规则后,会将prettier的校验规则传递给eslint,这样eslint就可以按照prettier的方式来进行代码格式的校验
      'prettier/prettier': 'error',
      // eslint(https://eslint.bootcss.com/docs/rules/)
      'no-var': 'error', // 要求使用 let 或 const 而不是 var
      'no-multiple-empty-lines': ['warn', { max: 2 }], // 不允许多个空行
      'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'no-unexpected-multiline': 'error', // 禁止空余的多行
      'no-useless-escape': 'off', // 禁止不必要的转义字符
      // typeScript (https://typescript-eslint.io/rules)
      '@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
      '@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
      '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
      '@typescript-eslint/semi': 'off',
      // eslint-plugin-vue (https://eslint.vuejs.org/rules/)
      'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
      'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
      'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
      'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
    },
  },
];

旧版eslint

module.exports = {
  globals: { a: "readonly" },
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-essential",
    "plugin:prettier/recommended",
    "eslint-config-prettier",
  ],
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
    parser: "@typescript-eslint/parser",
    sourceType: "module",

    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ["@typescript-eslint", "vue", "prettier"],
  rules: {
    "prettier/prettier": "error",
    "arrow-body-style": "off",
    "prefer-arrow-callback": "off",
  },
  settings: {
    vue: {
      version: "detect",
    },
  },
};

 .editorconfig

root = true

[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100

[*.{yml,yaml,json}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

Tailwind CSS

pnpm i less

pnpm install -D tailwindcss postcss autoprefixer

npx tailwindcss init

 生成tailwind.config.js文件

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,vue,html}'],
  theme: {
    extend: {}
  },
  plugins: []
}

 创建postcss.config.cjs文件

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

 style.css

@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  background-color: #fff;
  color: #000;
}
/* 主题 */
html.dark {
  background-color: #000;
  color: #fff;
}
@media (prefers-color-scheme: dark){
  html {
    background-color: #000;
    color: #fff;
  }
}
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html,body,#app{
  width: 100%;
  height: 100%;
}
/*width:1000px ---> 1rem:100px */
html{
  font-size: calc(10vw);
}
@media screen and (min-width:1000px) {
  html{
    font-size: 100px;
  }
}
/*default---> width:1000px---> font-size:16px */
body{
  font-size: 0.16rem;
}
.overflow{ 
  white-space: nowrap; 
  overflow: hidden; 
  text-overflow: ellipsis;
}
.overflow-1{ 
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1; 
}
.safe {
  padding-bottom: calc(env(safe-area-inset-bottom));
}

 js定义主题

export const changeTheme = () => {
  const match = matchMedia("(prefers-color-scheme:dark)");
  if (match.matches) {
    document.documentElement.classList.add("dark");
  }
  //系统的主题颜色发生改变
  match.addEventListener("change", () => {
    if (match.matches) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  });
};

 获取字体大小

getComputedStyle(document.documentElement).fontSize

封装网络请求

pnpm i axios 

/* eslint-disable @typescript-eslint/no-explicit-any */
import { router } from "@/router";
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";

interface BaseReturn<T> {
  data: T;
  code: number;
  msg: string;
}

type NODE_ENV = "development" | "production";
enum Url {
  development = "/api",
  production = "https://127.0.0.1:8080",
}
const node_env = process.env.NODE_ENV as NODE_ENV;

class Request {
  private instance: AxiosInstance;
  constructor(config: AxiosRequestConfig) {
    this.instance = axios.create(config);
    this.instance.interceptors.request.use((config) => {
      config.headers.Authorization = localStorage.getItem("token") || "";
      return config;
    });
    this.instance.interceptors.response.use((res) => {
      if (res.data.err_code !== 200) {
        console.log("请求出错=========");
        if (res.data.err_code === 401) {
          localStorage.removeItem("token");
          //登陆过期了
        }
        if (res.data.err_code === 403) {
          //没有权限
        }
      }
      return res.data;
    });
  }
  request<T>(config: AxiosRequestConfig) {
    return new Promise<BaseReturn<T>>((resolve) => {
      this.instance.request<any, BaseReturn<T>>(config).then((res) => {
        resolve(res);
      });
    });
  }
}
const useBaseRequest = new Request({
  baseURL: Url[node_env],
  timeout: 50000,
});
const useGet = <T>(url: string, params: any = {}) => {
  return useBaseRequest.request<T>({
    url,
    params,
    method: "GET",
  });
};
const usePost = <T>(url: string, data: any = {}) => {
  return useBaseRequest.request<T>({
    url,
    data,
    method: "POST",
  });
};
export { useBaseRequest, useGet, usePost };

vite.config.ts相关配置

使用env变量,转发网络请求,使用jsx,自动引入组件,配置@路径

安装插件

pnpm i -D unplugin-vue-components

pnpm i path

pnpm i --save-dev @types/node

pnpm i @vitejs/plugin-vue-jsx

pnpm i unplugin-auto-import

pnpm i reflect-metadata

 vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import Components from "unplugin-vue-components/vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import AutoImport from "unplugin-auto-import/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import { loadEnv } from "vite";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  return {
    server: {
      port: +env.VITE_PORT as number,
      proxy: {
        "/api": {
          target: env.VITE_BASE_API_URL || "http://localhost",
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ""),
        },
      },
    },
    plugins: [
      vue(),
      vueJsx(),
      Components({
        dts: true,
        resolvers: [
          (name) => {
            if (name.startsWith("My")) {
              return `@/components/${name}.vue`;
            }
          },
          ElementPlusResolver(),
        ],
      }),
      AutoImport({
        resolvers: [ElementPlusResolver()],
      }),
    ],
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "./src"),
      },
    },
  };
});

 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "experimentalDecorators": true,//支持装饰器写法
    "emitDecoratorMetadata": true,//支持Reflect的Metadata扩展
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    //配置@,src路径提示
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "noImplicitAny": false,//允许使用any

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","./components.d.ts"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

    创建src/components/types/components.d.ts文件为自动引入的组件标注类型

import MyHelloWorld from "../MyHelloWorld.vue";
declare module "@vue/runtime-core" { //如果不生效,使用"vue"模块,或修改tsconfig.json
  export interface GlobalComponents {
    MyHelloWorld: typeof MyHelloWorld;
  }
}

main.ts

import { createApp } from "vue";
import "element-plus/dist/index.css";
import "element-plus/theme-chalk/dark/css-vars.css";
import "./style.css";
import App from "./App.vue";
import { router } from "./router";
import { pinia } from "./store";
import Vconsole from "vconsole";
import { changeTheme } from "@/utils/changeTheme";
import { ExceptionInterceptor } from "@/utils/ReactiveClass";
//注册所有图标
import * as ElementPlusIconsVue from "@element-plus/icons-vue";

new Vconsole();
const app = createApp(App);
app.use(router);
app.use(pinia);
changeTheme();
ExceptionInterceptor();
//注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}
app.mount("#app");

KeepAlive

<script setup lang="ts"></script>

<template>
  <router-view class="w-full h-full" v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" v-if="$route.meta.keepAlive" />
    </keep-alive>
    <component :is="Component" v-if="!$route.meta.keepAlive" />
  </router-view>
</template>

<style scoped lang="less"></style>

使用Transition组件

<Transition name='xxx'>
    <div v-if='???'></div>
</Transition>

.xxx-enter-active,
.xxx-leave-active {
  transition: all 0.3s ease;
}

.xxx-enter-from,
.xxx-leave-to {
  transform: translateY(-100%);
}

v-bind

vue使用v-bind()可以在css中直接访问js变量

图片懒加载

export function useLazyImg(imgs: Array<HTMLImageElement>) {
  const io = new IntersectionObserver(function (entires) {
    //图片进入视口时就执行回调
    entires.forEach((item) => {
      // 获取目标元素
      const oImg = item.target;
      // 当图片进入视口的时候,就赋值图片的真实地址
      if (item.intersectionRatio > 0 && item.intersectionRatio <= 1) {
        oImg.setAttribute("src", oImg.getAttribute("data-url")!);
        io.unobserve(oImg);
      }
    });
  });
  Array.from(imgs).forEach((element) => {
    io.observe(element); //给每一个图片设置监听
  });
}

交通灯切换问题

interface IntervalRenderItem {
  duration: number;
}
export class IntervalRender<T extends IntervalRenderItem> {
  private intervalRenderList: T[] = [];
  private currentIndex: number = 0;
  private currentTime: number;
  constructor(intervalRenderList: T[]) {
    this.intervalRenderList = intervalRenderList;
    this.currentTime = Date.now();
  }
  private totalTime(): number {
    return this.intervalRenderList.reduce((x, y) => x + y.duration, 0);
  }
  get getCurrentItem() {
    return this.intervalRenderList[this.currentIndex];
  }
  //距离上一次调用过去的时间
  private currentTimeToBeforeTime() {
    return Date.now() - this.currentTime;
  }
  private update() {
    // 过去了多少伦
    const circleNum = Math.floor(this.currentTimeToBeforeTime() / this.totalTime());
    this.currentTime += circleNum * this.totalTime();
    //不足一轮还剩下多久
    let residualTime = this.currentTimeToBeforeTime() % this.totalTime();
    //剩余时间比当前项的时间多
    while (residualTime - this.getCurrentItem.duration > 0) {
      residualTime -= this.getCurrentItem.duration;
      this.currentTime += this.getCurrentItem.duration;
      this.currentIndex = (this.currentIndex + 1) % this.intervalRenderList.length;
    }
  }
  private requestAnimationFrameHandle;
  start() {
    this.update();
    this.requestAnimationFrameHandle = requestAnimationFrame(() => {
      this.start();
    });
  }
  stop() {
    cancelAnimationFrame(this.requestAnimationFrameHandle);
  }

  setIndex(index: number): void {
    this.currentIndex = index;
    this.currentTime = Date.now(); //重置上次切换的时间点
    this.update();
  }
  getRes(): { current: T; remainTime: number; index: number } {
    this.update();
    return {
      current: this.getCurrentItem,
      remainTime: this.getCurrentItem.duration - this.currentTimeToBeforeTime(),
      index: this.currentIndex,
    };
  }
}

路由拦截

pnpm i nprogress

pnpm i vue-router

pnpm i @types/nprogress -D

import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "./routes";

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
router.beforeEach(async (to, _from, next) => {
  NProgress.start();
  if (to.meta.title) {
    document.title = to.meta.title as string;
  }
  next();
});
router.afterEach(() => {
  NProgress.done(); // 在即将进入新的页面组件前,关闭掉进度条
});
export { router };

 routes.ts

import { RouteRecordRaw } from "vue-router";

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    redirect: "/home",
  },
  {
    path: "/home",
    component: () => import("@/pages/home/HomePage.vue"),
    redirect: "/home/animate",
    children: [
      {
        path: "animate",
        component: () => import("@/pages/home/animate/AnimateList.vue"),
      },
    ],
  },
  {
    path: "/login",
    component: () => import("@/pages/login/LogIn.vue"),
  },
  {
    path: "/:pathMatch(.*)",
    component: () => import("@/pages/404/NotFound.tsx"),
  },
];

状态管理

pnpm i pinia pinia-plugin-persistedstate

 store/index.ts

import { createPinia } from "pinia";
import { createPersistedState } from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(
  createPersistedState({
    storage: localStorage,
  })
);
export { pinia };

 store/modules/index.ts

import { defineStore } from "pinia";

const user_store = defineStore("user_store", {
  state: () => {
    return {
      token: "token",
    };
  },
  getters: {},
  actions: {},
  persist: true,
});
export { user_store };

大屏缩放

pnpm i v-scale-screen 

H5项目键盘弹出与tabbar冲突 

const docmHeight =  ref(0)
const hidshow = ref(true)
 
onMounted(() => {
    docmHeight.value = document.documentElement.clientHeight;//获取当前屏幕高度
    window.onresize = () => {//屏幕高度变化时判断
      return (() => {
        let showHeight = document.body.clientHeight;
        hidshow.value = docmHeight.value > showHeight ? false : true;
      })();
    };
})

组件级的权限控制 

<template>
  <div class='BtnPermition'>
    <slot v-if="show" name="default" :permission="permission"></slot>
  </div>
</template>

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

const props = defineProps<{
  permission: string | string[]
}>()
defineSlots<{
  default: (scoped: { permission: typeof props.permission }) => any
}>()
const user_role_info = JSON.parse(localStorage.getItem('user_role_info') || '[]') as Array<any>
const roleCodeArr = user_role_info.map((item) => item.roleCode || '') as Array<string>
const show = computed(() => {
  if (roleCodeArr.includes('admin')) {
    return true
  }
  if (typeof props.permission === 'string') {
    return roleCodeArr.includes(props.permission)
  } else {
    return props.permission.some(item => roleCodeArr.includes(item))
  }
})
</script>

动态表单

动态表单使用双向链表做parent,next

定义类型

/**
 * 基于双向链表的表单
 */
//动态表单项的类型
type DynamicFormItemType = 'input' | 'select' | 'checkbox' | 'radio' | 'datePicker';
//动态表单项的参数类型
interface ItemPayload {
  model: string;
  label: string;
  value: any; //用来在表单项中存储对应的表单值,不然只能用modelForm[model]获取
  disabled?: boolean;
  options?: Array<{
    label: string;
    value: any;
  }>;
  rule?: FormItemRule[];
  [key: string]: any;
}
//创建动态表单项的函数参数类型
//当前项的next为当前项和之前所有项功能作用下的结果,parent的主要作用是获取之前项
interface DynamicFormItem {
  type: FormItemType; //是一个什么类型的表单
  payload: ItemPayload; //表单需要的参数
  next: (current: DynamicFormItem, acients: DynamicFormItem[]) => DynamicFormItem[] | DynamicFormItem | null; //(回调函数)表单的下一项是什么,由当前表单和之前的表单共同决定
  parent: DynamicFormItem | null; //表单的上一项是什么
}
interface CreateDynamicFormItem {
  (
    dynamicFormItemType: DynamicFormItem['type'],
    payload: DynamicFormItem['payload'],
    next?: DynamicFormItem['next'],
    parent?: DynamicFormItem['parent']
  ): DynamicFormItem;
}

 编写生成动态表单表单的函数

import { isReactive, reactive } from 'vue';

/**
 *
 * @param dynamicFormItemType
 * @param payload
 * @param next
 * @param parent
 * @returns
 */
export const createDynamicForm: CreateDynamicFormItem = (dynamicFormItemType, payload, next, parent) => {
  if (!next) {
    next = () => null;
  }
  if (!parent) {
    parent = null;
  }
  const nextFunc: DynamicFormItem['next'] = (current, acients) => {
    let nextItem = next!(current, acients);
    if (!nextItem) {
      return null;
    }
    if (Array.isArray(nextItem)) {
      nextItem.forEach((item) => {
        item.parent = current;
        if (!isReactive(item)) {
          item = reactive(item);
        }
      });
    } else {
      nextItem.parent = current;
      if (!isReactive(nextItem)) {
        nextItem = reactive(nextItem);
      }
    }
    return nextItem;
  };
  const dynamicFormItem: DynamicFormItem = reactive({
    type: dynamicFormItemType,
    payload,
    next: nextFunc,
    parent,
  });
  return dynamicFormItem;
};

 使用递归组件渲染表单

<template>
  <div class='DynamicForm' v-if="formItem">
    <a-form-item :label="formItem.payload.label" :name="formItem.payload.model" :rules="formItem.payload.rules"
      :labelCol="{ span: 6 }" :wrapperCol="{ span: 18 }">
      <a-input :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'input'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请输入" />
      <a-textarea :rows="3" :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'textarea'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请输入" />
      <a-select :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'select'"
        v-model:value="modelForm[formItem.payload.model]" placeholder="请选择">
        <a-select-option v-for="ele in formItem.payload.options" :key="ele.value" :value="ele.value">
          {
  
  { ele.label }}
        </a-select-option>
      </a-select>
      <a-checkbox-group :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'checkbox'"
        v-model:value="modelForm[formItem.payload.model]" :name="formItem.payload.model"
        :options="formItem.payload.options" />
      <a-date-picker :disabled="formItem.payload.disabled || false" v-if="formItem.type === 'datePicker'"
        v-model:value="modelForm[formItem.payload.model]" valueFormat="YYYY-MM-DD" style="width: 100%;" />
    </a-form-item>
    <div v-if="Array.isArray(nextFormItem()) && (nextFormItem() as DynamicFormItem[])?.length">
      <DynamicForm v-for="(item, i) in nextFormItem()" :key="i" :formItem="item" :modelForm="modelForm"></DynamicForm>
    </div>
    <div v-if="!Array.isArray(nextFormItem())">
      <DynamicForm :formItem="(nextFormItem() as (DynamicFormItem | null))" :modelForm="modelForm"></DynamicForm>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onUnmounted, watchEffect } from "vue"
const props = defineProps<{
  formItem: DynamicFormItem | null;
  modelForm: Record<string, any>
}>()
const nextFormItem = (): DynamicFormItem | DynamicFormItem[] | null => {
  let current: DynamicFormItem | null = props.formItem
  if (!current) {
    return null
  }
  const acients = [] as DynamicFormItem[]
  acients.unshift(current)
  while (current.parent) {
    current = current.parent
    acients.unshift(current)
  }
  //调用回调函数,将当前项和之前所有项传递过去,在那边做判断
  return props.formItem!.next(props.formItem!, acients)
}
watchEffect(() => {
  if (props.formItem) {
    // 需要对每个表单项更新value用来存储当前表单对应的值
    props.formItem.payload.value = props.modelForm[props.formItem.payload.model]
  }
})
onUnmounted(() => {
  if (props.formItem) {
    delete props.modelForm[props.formItem.payload.model]
  }
})
</script>

<style lang="less" scoped></style>

 测试一下

import { createDynamicForm } from './createDynamicForm';

const item1 = createDynamicForm(
  'select',
  {
    model: 'type',
    label: '类型',
    options: [
      { label: '快递信息', value: 1 },
      { label: '工艺信息', value: 2 },
    ],
    value: '',
  },
  (current, _acients) => {
    if (current.payload.value === 1) {
      return item2;
    } else if (current.payload.value === 2) {
      return item3;
    } else {
      return null;
    }
  }
);

const item2 = createDynamicForm('input', {
  label: '快递单号',
  value: '',
  model: 'code',
});

const item3 = createDynamicForm('select', {
  model: 'flow',
  label: '工艺流程',
  options: [
    { label: '打包', value: '打包' },
    { label: '裁剪', value: '裁剪' },
  ],
  value: [],
});

export default item1;

//返回的是当前项及以后的item的model,value。{ 绑定的字段: 对应的值 }
export function collectFormValues(formItem: DynamicFormItem | DynamicFormItem[] | null) {
  const formValues: { [key: string]: any } = {};
  function collect(item: DynamicFormItem | DynamicFormItem[] | null) {
    if (item) {
      //递归到它的后一项时,可能为数组
      if (Array.isArray(item) && item.length) {
        item.forEach((ele) => {
          const { name, value } = ele.payload;
          formValues[name] = value;
          collect(ele.next(ele, []));
        });
      } else if (!Array.isArray(item)) {
        const { name, value } = item.payload;
        formValues[name] = value;
        collect(item.next(item, []));
      }
    }
  }
  //从当前项收集
  collect(formItem);
  return formValues;
}

canvas时钟 

<template>
  <canvas class="clock" ref="canvasRef"> </canvas>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { getTime } from "./formatTime";
const canvasRef = ref<HTMLCanvasElement | null>();
const ctx = ref<CanvasRenderingContext2D | null>();
const initCanvas = () => {
  const width = canvasRef.value?.clientWidth || 0;
  const height = canvasRef.value?.clientHeight || 0;
  if (canvasRef.value && width && height) {
    canvasRef.value.width = width;
    canvasRef.value.height = height;
  }
  ctx.value = canvasRef.value?.getContext("2d");
  ctx.value?.translate(width / 2, height / 2);
  ctx.value?.rotate(-Math.PI / 2);
  ctx.value?.save();
};
const animate = ref();
const render = (init_width: number, init_height: number) => {
  const width = Math.max(init_width, init_height);
  const height = Math.min(init_width, init_height);
  ctx.value?.clearRect(-width / 2, -height / 2, width, height);
  const { hour, minute, second, timeType } = getTime(Date.now());
  ctx.value!.rotate(Math.PI / 2);
  ctx.value!.font = "20px Arial"; // 设置字体
  ctx.value!.fillStyle = "blue"; // 设置填充颜色
  ctx.value!.fillText(
    `${
      timeType == "PM"
        ? hour + 12 > 9
          ? hour + 12
          : "0" + (hour + 12)
        : hour > 9
        ? hour
        : "0" + hour
    }:${minute > 9 ? minute : "0" + minute}:${
      second > 9 ? second : "0" + second
    } ${timeType}`,
    -10 * 5,
    -20
  ); // 在 (50,50) 绘制文字
  ctx.value?.restore();
  ctx.value?.save();
  for (let i = 0; i < 12; i++) {
    //小时刻度
    ctx.value?.beginPath();
    ctx.value?.moveTo(height / 2 - 10, 0);
    ctx.value?.lineTo(height / 2 - 20, 0);
    ctx.value?.stroke();
    ctx.value?.closePath();
    ctx.value?.rotate(Math.PI / 6);
  }
  ctx.value?.restore();
  ctx.value?.save();
  for (let i = 0; i < 60; i++) {
    //分钟刻度
    ctx.value?.beginPath();
    ctx.value?.moveTo(height / 2 - 10, 0);
    ctx.value?.lineTo(height / 2 - 15, 0);
    ctx.value?.stroke();
    ctx.value?.closePath();
    ctx.value?.rotate(Math.PI / 30);
  }
  ctx.value?.restore();
  ctx.value?.save();
  //绘制秒针
  ctx.value?.rotate(((2 * Math.PI) / 60) * +second);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-20, 0);
  ctx.value?.lineTo(height / 2 - 20, 0);
  ctx.value!.strokeStyle = "red";
  ctx.value!.lineWidth = 1;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
  //绘制分针
  ctx.value?.rotate(((+minute * 6 + +second / 10) * Math.PI) / 180);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-15, 0);
  ctx.value?.lineTo(height / 2 - 50, 0);
  ctx.value!.strokeStyle = "green";
  ctx.value!.lineWidth = 5;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
  //绘制时针
  ctx.value?.rotate(((+hour * 30 + +minute / 2) * Math.PI) / 180);
  ctx.value?.beginPath();
  ctx.value?.moveTo(-10, 0);
  ctx.value?.lineTo(height / 2 - 70, 0);
  ctx.value!.strokeStyle = "blue";
  ctx.value!.lineWidth = 5;
  ctx.value?.stroke();
  ctx.value?.closePath();
  ctx.value?.restore();
  ctx.value?.save();
};
const renderWithSize = () => {
  render(canvasRef.value?.clientWidth || 0, canvasRef.value?.clientHeight || 0);
  animate.value = requestAnimationFrame(renderWithSize);
};
onMounted(() => {
  initCanvas();
  renderWithSize();
});
onUnmounted(() => {
  cancelAnimationFrame(animate.value!);
});
</script>

<style scoped>
.clock {
  width: 100%;
  height: 300px;
}
</style>

装饰器,创建响应式的类

pnpm i class-validator

import { Validator, validate } from "class-validator";
import { reactive } from "vue";

// 自定义类装饰器 Reactive
export function Reactive<T extends { new (...args: any[]): {} }>(
  my_constructor: T
) {
  return class extends my_constructor {
    constructor(...args: any[]) {
      super(...args);
      //把类的实例变成响应式的
      return reactive(this); //返回的是对象,该对象会覆盖constructor返回的对象实例
    }
  };
}

/**
 * 自定义表单异常
 * @constructor (message)自定义异常
 */
export class FormException extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FormException";
  }
}
/**
 * 初始化监听异常的方法
 */
export const ExceptionInterceptor = () => {
  window.addEventListener("error", function (event) {
    if (event.error instanceof FormException) {
      console.log(event.error.message);
    }
  });
  window.addEventListener("unhandledrejection", (event) => {
    const { reason } = event;
    if (reason instanceof FormException) {
      console.log(reason.message);
    }
  });
};

//将它的子类用@Reactive变成响应式的类,就可以使用这些方法给响应式对象赋值了
export class FormValidator extends Validator {
  [key: string]: any;

  //初始化
  init<T extends Record<string, any>>(form: T) {
    for (const key in form) {
      if (Object.prototype.hasOwnProperty.call(this, key)) {
        this[key] = form[key] as any;
      }
    }
  }

  //重置各个属性的值
  reset() {
    const instance = Reflect.construct(this.constructor, []);
    const keys = Object.keys(instance);
    keys.forEach((key) => {
      this[key] = instance[key];
    });
  }

  //表单提交
  submit(): Promise<this> {
    // eslint-disable-next-line no-async-promise-executor
    return new Promise(async (resolve) => {
      const err = await validate(this);
      if (err.length > 0) {
        //需要配合class-validator中的验证注解使用,str为验证失败的提示信息
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const str = Object.values(err[0].constraints!)[0];
        throw new FormException(str);
      }
      resolve(this);
    });
  }
}

也可以使用自定义装饰器

import 'reflect-metadata'
class Err {
  msg
  constructor(msg = '不能为空') {
    this.msg = msg
  }
}
// 自定义属性装饰器 IsNotEmpty
function IsNotEmpty(target: any, propertyName: string) {
  Reflect.defineMetadata('IsNotEmpty', true, target, propertyName)
}
// 校验函数
function validateNotEmpty(
  target: any,
  _propertyName: string,
  descriptor: PropertyDescriptor
) {
  //缓存方法原来的值
  const method = descriptor.value
  //对方法更改并执行
  descriptor.value = function (...args: any[]) {
    for (const key in this) {
      const needVal = Reflect.getMetadata('IsNotEmpty', target, key)
      if (needVal) {
        const val = this[key as keyof PropertyDescriptor]
        if (!val || (typeof val === 'string' && !val.trim())) {
          throw new Err(`${key}不能为空`)
        }
      }
    }
    //在最后执行原来的方法
    return method.apply(this, args)
  }
}
 
 
// 使用装饰器
class Example {
  @IsNotEmpty
  public name: string
 
  constructor(name: string) {
    this.name = name
  }
 
  @validateNotEmpty
  public greet() {
    console.log(`Hello, ${this.name}!`)
  }
}
 
// 测试,此时name为空,会捕获到错误
const example = new Example('   ')
try {
  example.greet()
} catch (err) {
  console.log(err)
}

vconsole

pnpm i vconsole

threejs

pnpm i threejs

<template>
    <div ref='MyThreePage'>

    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
// @ts-ignore
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
const MyThreePage = ref();
// 画布
const scene = new THREE.Scene();
// 相机
const camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
//控制器
let orbitControls
// 渲染函数
const render = new THREE.WebGLRenderer();
const initThree = () => {
    camera.position.z = 3;
    camera.position.x = 3;
    camera.position.y = 3;
    camera.lookAt(0, 0, 0);
    render.setSize(window.innerWidth, window.innerHeight);
}
const addGui = (mesh) => {
    // GUI
    const GUIObj = {
        full() {
            document.documentElement.requestFullscreen();
        },
    };
    const gui = new GUI();
    gui.add(GUIObj, "full").name("全屏");
    const folder1 = gui.addFolder("位置");
    folder1.add(mesh.position, "x").min(-10).max(10).step(1).name("X");
    folder1.add(mesh.position, "y").min(-10).max(10).step(1).name("Y");
    folder1.add(mesh.position, "z").min(-10).max(10).step(1).name("Z");
}
const createThing = (geometry, material) => {
    // 网
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
    return mesh;
}
const addXYZ_Controls = () => {
    // 坐标系
    const xyz = new THREE.AxesHelper(10);
    scene.add(xyz);
    // 控制器
    orbitControls = new OrbitControls(camera, render.domElement);
    orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.12;
}
//渲染动画
const animate = () => {
    requestAnimationFrame(animate);
    orbitControls.update();
    render.render(scene, camera);
};
const resize = () => {
    render.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
}
onMounted(() => {
    MyThreePage.value.appendChild(render.domElement);
    initThree()
    addXYZ_Controls()
    addGui(createThing(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({
        color: 0x999999,
    })))
    animate();
    //窗口改变,重新设置场景大小
    window.addEventListener("resize", resize);
});
onUnmounted(() => {

})
</script>

<style lang="less" scoped>
.MyThreePage {
    width: 100%;
    height: 100%;
}
</style>

echarts

pnpm i echarts echarts-liquidfill

<template>
  <div class='DataVolumeRanking' ref="echart">
  </div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts'
import { onMounted, ref } from "vue"
const echart = ref<HTMLDivElement>()
let myChart: IEChart
const setOption = (data: any[]) => {
  const option = {
    title: {
      text: '单位:条',
      right: 10,
      top: 10,
      textStyle: {
        color: '#A9B4DF',
        fontSize: 14
      }
    },
    grid: {
      left: '2%',
      right: '8%',
      bottom: '3%',
      top: '0%',
      containLabel: true
    },
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'none'
      },
      formatter: function (params) {
        return params[0].name + ' : ' + params[0].value
      }
    },
    xAxis: {
      splitLine: {
        show: false
      },
      axisLine: {
        show: false
      },
      axisLabel: {
        show: true,
        textStyle: {
          color: '#fff'
        },
      },
    },
    yAxis: {
      axisLabel: {
        show: true,
        textStyle: {
          color: '#fff'
        },
      },
      splitLine: {
        show: false
      },
      axisLine: {
        show: false
      },
      data: ['name1', 'name2', 'name3']
    },
    series: [
      {
        name: '值',
        type: 'bar',
        label: {
          show: true,
          position: 'right',
          color: '#fff'
        },
        itemStyle: {
          normal: {
            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
              offset: 0,
              color: 'rgb(37, 44, 79)'
            }, {
              offset: 1,
              color: 'rgb(76, 101, 169)'
            }]),
          },
        },
        barWidth: 30,
        data: data
      }
    ]
  }
  myChart.setOption(option)
}
onMounted(() => {
  myChart = echarts.init(echart.value)
  setOption([239, 181, 154, 144, 135, 117, 74, 72, 67, 55])
})
</script>

<style lang="less" scoped>
.DataVolumeRanking {
  width: 100%;
  height: 100%;
}
</style>

 canvas签名

<script setup lang="ts">
import { onUnmounted } from 'vue'
import { onMounted } from 'vue'
import { ref } from 'vue'
 
interface Props {
  width: string  //画布宽度
  height: string  //画布高度
  color: string  //画笔颜色
  bgc: string  //画布背景
  lineWidth: number  //画笔宽度,px
  saveText: string  //保存的文字
  clearText: string  //清除的文字
}
const props = withDefaults(defineProps<Partial<Props>>(), {
  width: '100vw',
  height: '3rem',
  color: '#000',
  bgc: '#fff',
  lineWidth: 1,
  saveText: '保存',
  clearText: '清空'
})
 
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent)
 
const canvas = ref<HTMLCanvasElement>()
const ctx = ref<CanvasRenderingContext2D | null>()
const canSign = ref(false)
//起始位置的X Y
const clientX = ref(0)
const clientY = ref(0)
 
// 当前应该从哪里开始画
const get_draw_point = (e: MouseEvent | TouchEvent) => {
  // 画布距离页面左右的距离
  const x = canvas.value!.getBoundingClientRect().left
  const y = canvas.value!.getBoundingClientRect().top
  //设置起始位置
  if (!mobileStatus) {
    clientX.value = (e as MouseEvent).clientX - x
    clientY.value = (e as MouseEvent).clientY - y
  } else {
    if ((e as TouchEvent).touches) {
      clientX.value = (e as TouchEvent).touches[0].clientX - x
      clientY.value = (e as TouchEvent).touches[0].clientY - y
    }
  }
}
// 开始
const handleMouseDown = (e: MouseEvent | TouchEvent) => {
  ctx.value!.beginPath()
  canSign.value = true
  get_draw_point(e)
  //移动画笔
  ctx.value!.moveTo(clientX.value, clientY.value)
}
// 鼠标移动或者手指在手机上滑动的事件
const handleMouseMove = (e: MouseEvent | TouchEvent) => {
  if (!canSign.value) {
    return
  }
  get_draw_point(e)
  //画线
  ctx.value!.lineTo(clientX.value, clientY.value)
  ctx.value!.stroke()
}
// 结束
const handleMouseUp = () => {
  canSign.value = false
}
onMounted(() => {
  document.addEventListener(
    mobileStatus ? 'touchmove' : 'mousemove',
    handleMouseMove
  )
  // 初始化的配置
  const init_canvas = document.getElementById('canvas') as HTMLCanvasElement
  canvas.value = init_canvas
  //设置canvas宽高属性
  canvas.value.width = canvas.value.clientWidth
  canvas.value.height = canvas.value.clientHeight
  //画布背景的样式
  ctx.value = canvas.value!.getContext('2d')
  ctx.value!.fillStyle = props.bgc
  ctx.value!.fillRect(0, 0, canvas.value.width, canvas.value.height)
  //画笔的样式
  ctx.value!.lineWidth = props.lineWidth
  ctx.value!.strokeStyle = props.color
})
onUnmounted(() => {
  document.removeEventListener(
    mobileStatus ? 'touchmove' : 'mousemove',
    handleMouseMove
  )
})
 
// 下载或者清除
const a = ref<HTMLAnchorElement>()
const img_url = ref('')
const click_btn = (type: 'save' | 'clear') => {
  if (btn_event[type]) {
    btn_event[type]()
  }
}
const btn_event = {
  clear() {
    if (ctx.value) {
      ctx.value!.fillStyle = props.bgc
      ctx.value!.fillRect(0, 0, canvas.value!.width, canvas.value!.height)
    }
  },
  save() {
    const base64 = canvas.value?.toDataURL() || ''
    const base64_data = atob(base64.split(',')[1])
    let length = base64_data.length
    const u8arr = new Uint8Array(length)
    while (length--) {
      u8arr[length] = base64_data.charCodeAt(length)
    }
    const url = URL.createObjectURL(new Blob([u8arr]))
    // 移动端
    if (mobileStatus) {
      img_url.value = url
    } else {
      a.value!.href = url
      a.value!.click()
    }
  }
}
// 另一种保存canvas图片的方法(web端)
const save = () => {
  // 将canvas上的内容转成blob流
  canvas.value!.toBlob((blob) => {
    //也可以使用blob创建file文件,上传文件
    const url = URL.createObjectURL(blob!)
    a.value!.href = url 
    a.value!.click()
  })
}
</script>
 
<template>
  <div>
    <div :style="{ width: width, height: height }">
      <canvas
        id="canvas"
        @touchstart="handleMouseDown"
        @touchend="handleMouseUp"
        @mousedown="handleMouseDown"
        @mouseup="handleMouseUp"
        class="w-full h-full"
      ></canvas>
    </div>
    <div
      class="text-[0.36rem] text-[#fff] mt-[0.66rem] px-[2rem] flex justify-between"
    >
      <div @click="click_btn('save')" class="bg-[skyblue] p-[0.2rem]">
        {
  
  { saveText }}
      </div>
      <div @click="click_btn('clear')" class="bg-[red] p-[0.2rem]">
        {
  
  { clearText }}
      </div>
      <a download="sign.png" ref="a" href="" class="hidden">下载图片</a>
    </div>
    <!-- 移动端保存图片的方法 -->
    <div @click="img_url = ''" v-if="img_url" class="mask w-[100vw] h-[100vh]">
      <img :src="img_url" alt="" />
      <div class="mt-[0.2rem] text-[0.44rem] text-center text-[#fff]">
        长按保存
      </div>
    </div>
  </div>
</template>
 
<style scoped lang="less">
.mask {
  position: absolute;
  left: 0;
  top: 0;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  img {
    max-height: 60%;
  }
}
</style>

Nginx

######vue去掉路由#配置
######加上这个
location / {
  try_files $uri $uri/ /index.html;
}

######或者
location / {
	root /web-server/front-project/dist;
	try_files $uri $uri/ @router;
	index index.html;
}
# @router配置
location @router {
	rewrite ^.*$ /index.html last;
}
# 静态资源代理
location /myblog_static {
	alias /web-server/front-project/dist//myblog_static/;
}


######代理和负载均衡
upstream name{
   	 server 127.0.0.1:8080 weight=1;
   	 server 127.0.0.1:8081 weight=1;
}
server {
     listen 80;
     server_name  localhost;
	 location /path/ {
   	    proxy_pass http://name;
	 }
}

参考模板

我的vue模板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值