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模板