一.为什么学typescript
var三大框架都是用ts写的,做个类比,如果你不知道阿拉伯数字和加减法,怎么解应用题?同理不会ts如何使用vue开发工作项目?
二.开发环境准备
1.安装node,https://nodejs.org/en/,下载安装,一路默认下一步 ,输入node -v显示版本成功
2.安装ts,npm install -g typescript
,输入tsc -v显示版本 说明成功
3.安装vscode https://code.visualstudio.com/
三.新建一个Helloworld 文件夹,用vscode打开,然后新建app.ts,里面就写一句
console.log('hello world')
然后在vscode 终端输入 tsc app.ts ,如果报错 先输入 set-ExecutionPolicy RemoteSigned
最后看到生成一个编译同名app.js说明成功了。你说我想看看打印效果,整个index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="app.js"></script>
</head>
<body>
</body>
</html>
console.log('hello world')
改成console.log('真香')
终端按上箭头,重新tsc app.ts,看浏览器控制台,能看到真香。
三、typescript基础知识
1.模块管理
传统教程该讲数据类型了,我们先放放直接将核心的模块管理,因为基础的数据类型number string 不讲你也会,高级的数据类型讲到了现在你也不明白怎么用。就说一件事,ts下怎么定义各种数据类型省的遇见ts里面类型报错,你就懵圈了,这里跟js是不一样的。
就简要提一嘴,常见的就是定义数组:
let num = 0;
let str = '彬哥好帅';
//上面的没区别,下面说不一样的
//js这么写是没问题的:
//定义数组,第一种:
let person: string[] = ['张三', '帅彬'];
//第一种有个缺点,比如我要处理一个数组过程中split的时候,可能会出现,[1,'张三',2,'帅彬'],这样你定义成string就报错两种类型
let worker: [number, string] = [1, "张三",2,"帅彬"];
//记住以上两种数组就够用了,接下来我们说说数组里面套对象{},这个最常用,以前我们在js下定义一个数组含对象
//js下
let worker = [
{1, "张三"},
{2, "帅彬"}
];
//ts下 这样写
interface Worker {
id: number;
name: string;
}
let worker: Worker[] = [
{ id: 1, name: "张三" },
{ id: 2, name: "帅彬" }
];
//你在项目中很可能遇到一种情况就是你通过对象的key的值去索引对象的value,比如worker[//这里是什么也可能是动态的变量],
//这种情况怎么定义?
interface Worker {
id: number;
[key: string]: string;//关键是这一句,后面你甭管它叫啥只要是string类型编译就不错
}
let worker: Worker[] = [
{ id: 1, name: "张三" },
{ id: 2, jobName: "帅彬" }
];
最后一句,数据类型你不知道也不要用any,不要用any,不要用any,因为往往报any的时候,意味着你代码写的有问题。
我们回到vue中立刻就要会用到的模块管理,也就是组件。
我们先定义一个组件,你叫一个文件也好,一个模块也好不用管它叫啥,反正就这么个玩意,就叫mod1.ts
各个文件里面代码是这样子的:
//mod1.ts
//我定义了一个a 并导出
export let a = 12;
//app.ts
//因为我定义的是a 所以导入的名字也必须跟mod1.ts里面的一致,也得是a还得用花括号括起来,另外mod1路径用./同级 mod1不用加后缀
import {a} from './mod1'
console.log(a);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>
index.html注意一下几点:
1.script type="module" 必须写
2.要扔到body里面
3.要想支持import和export 必须用服务器环境类似这样 http://localhost/HelloWorld/,可以随便装个wamp之类的然后扔到www文件夹下
4.这些都记不住也没事按照我说的做即可,我只是说明mod1.ts和app.ts里面的内容,这两个文件内容是核心
vscode终端输入:
tsc --module ES2015 app.ts
这个命令也不用记,就是为了让程序跑起来,我们继续核心,修改下mod1.ts
export let a = 12; //mod1.ts//我定义了一个a 并导出export let a = 12;
每次改东西tsc 太费劲,我们用监视命令,依然不用记,只看文件中源码即可,用了这个命令你就不用管tsc了,改动自动变
tsc --w --module ES2015 app.ts mod1.ts
修改mod1.ts
export let a = 12;
export let b = 5;
export let c = 8;
修改app.ts
import {a,b,c} from './mod1'
console.log(a,b,c);
浏览器控制台,出现 12,5,8,大家看从mod1里面导入了三个mod1中导出的, a,b,c等导出的名字太多了记不住怎么办?我也不想去mod1.ts里面去找导出的叫什么名字,那就不记了,直接修改app.ts,这样都被我放到了arrAbc数组里
import * as arrAbc from './mod1'
console.log(arrAbc.a,arrAbc.b,arrAbc.c);//12,5,8
你说 我想默认一个导出,也不想记名字,怎么办?
修改mod1.ts
let a = 666;
export default a;
export let b = 5;
export let c = 8;
修改,app.ts
// import * as arrAbc from './mod1'
// console.log(arrAbc.a,arrAbc.b,arrAbc.c);
import numA from './mod1'
import{b,c} from './mod1'
console.log(numA,b,c);
刷浏览器,出现666,12,5,8
总结一下:
1.通过 export default方式导入,在导入时不需要加 {} 在一个文件或模块中,名字随便起,比如numA
2.export default只能有一个 export default后面不能跟const或let的关键词
export default 一个文件只能有一个
对了,你不定义名字都没事,比如,修改mod1.ts
let a = 666;
export default {
"name":"大彬哥",
"age":18
};
export let b = 5;
export let c = 8;
修改app.ts
import teacher from './mod1'
console.log(teacher.name,teacher.age);
刷浏览器,结果就出来了,大彬哥 18,这种方式 vue中用的最多
2.类型定义(vue用到再说)
总结,到这里你就彻底明白了vue中 import了或者export 什么import * as 什么的。就有了写组件或者引用别人库的基础。有了这个基础我们就可以做应用题了,我们装上Vue最新版,看看还有啥用不明白的。
四、vue基础知识
一、我们怎么创建vue项目?
npm create vite@latest
你不用管上面vite不vite,你就按照这个命令跑一把梭,然后你就会看到,
我们从index.html看,有这么一句,
<script type="module" src="/src/main.ts"></script>
是不是我们刚才说的模块管理的东西?顺藤摸瓜,看看main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
引入了vue模块下的 createApp,还有其它一堆,明显我们现在只关心
import App from './App.vue'
继续顺藤摸瓜,App.vue,依然是三段论,script、template、style
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
核心就两句话,第一句:
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
这里引入了一个组件,setup是啥你也不用管,反正你知道引入了HelloWorld.vue就OK(后面我会讲),然后是
<HelloWorld msg="Vite + Vue" />
给HelloWorld的msg属性服了一个"Vite + Vue" 的值,再努力一下就到底了!看看components里面的HelloWorld组件,敲黑板划,重点来了:
<script setup lang="ts">
import { ref } from "vue";
defineProps<{ msg: string }>();
const count = ref(0);
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
</div>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>
当然我们写项目不可能这么简单,更多情况下是一个父组件通过数组迭代子组件,就像下面这样,App.vue里面:
<script setup lang="ts">
import { ref } from "vue";
import HelloWorld from './components/HelloWorld.vue'
const fatherItems = ref([
{ id: 1, title: "春天", content: "我把裤衩一脱,春姑娘就来了"},
{ id: 2, title: "夏天", content: "我把短裤一穿,夏姑娘就来了" }
])
</script>
<template>
<HelloWorld v-for="item in fatherItems" :key="item.id" :title="item.title" :content="item.content" />
</template>
<style scoped>
</style>
Helloword.vue
<script setup>
defineProps(["title","content"]);
</script>
<template>
<h4>{{ title }}</h4>
<p>{{ content }}</p>
</template>
展示效果如下:
好了,到这我们做项目就够用,整太复杂就懵逼了。接下来我们就直接撸项目。
五、开始撸项目
1.看看市场需求文档
我们看看需求文档,bula,bula一堆,我看到的是:
第一个问题:我为什么要开发霍兰德职业兴趣测试系统?
1.手工测试速度太慢(客户和自己),开发着玩玩
2.值得我做一天搞定
3.市面上的都需要付费,搞完以后我可以让别人付费说白了就是
想做
能做
值得做第二个问题:要实现什么内容?
功能:https://baike.baidu.com/item/...
翻译翻译:
原理:据兴趣的不同,人格可分为研究型
第一步:人分六种,(I)、艺术型(A)、社会型(S)、企业型(E)、传统型(C)、现实型(R)六个维度
第二步:十题一测 每种通过10道题测试,最后判断你是哪一种,得出你是什么人格,比如SEC
第三步:对号入座,根据索引找对应工作
因为是我自己用,所以没什么要说的。但是你要是公司开发,咋说呢,活当然要干的漂亮,但是不好开发的尽量开发之前就砍掉,不管你是威逼还是理由,反正尽可能扼杀需求在摇篮之中,如果额杀不了也要记住,需求上的失误,不要到代码实现层去沟通。
你只是大自然的搬运工,把市场需求搬运给计算机,不用带太多脑子。
2.整个产品文档
原理:据兴趣的不同,人格可分为研究型
第一步:人分六种,(I)、艺术型(A)、社会型(S)、企业型(E)、传统型(C)、现实型(R)六个维度
第二步:十题一测 每种通过10道题测试,最后判断你是哪一种,得出你是什么人格,比如SEC
第三步:对号入座,根据索引找对应工作
3.项目开发文档
接到市场需求和产品需求文档,我的想法:你们给我圆润的走开,翻滚吧牛宝宝。我不想干活。(简称,滚犊子),既然推不掉就想怎么实现:
在使用 Vue.js 来实现霍兰德职业测试的应用程序时,我考虑到的:
1.准备问卷:需要准备一份问卷,其中包含若干个问题,用于测试人的兴趣、倾向和人格
特点。(用ant-design-vue,本场演出就我自己,又不想界面丑爆了)
2.创建表单:你需要使用 Vue.js 的模板语法来创建表单,包括问卷中的各个问题和答案选项。
3.处理表单提交:当用户完成填写并提交表单时,你需要使用 Vue.js 的方法来处理表单提
交,并计算出测试结果。(用pinia状态管理,肯定会用到跨组件通信,顺便还可以装下13)
4.展示测试结果:最后,你可以使用 Vue.js 的指令来展示测试结果,并向用户提供建议的职
业列表。(用个vue-chartjs雷达图,用了很多次很稳定,也是一款装13的利器)
以上纯属脑袋中意淫,咳咳,头脑风暴。然后开始写项目开发文档。大家以为我是这么想的:
开发思路:
1.创建 Vue.js 项目:使用 构建工具创建 Vue.js 项目。
2.准备测试题目和选项:准备好测试题目和选项,可以使用数组格式存储。
3.创建组件:创建一个测试组件,用于显示测试题目和选项,并处理用户的选择。
4.实现组件逻辑:在组件中实现逻辑,包括显示当前测试题目和选项,处理用户的选择,计
算测试结果等。
5.显示测试结果:使用另一个组件显示测试结果,可以根据测试结果提供建议的职业。
其实我是这样想的:
1.有没有现成的开源的项目,我改吧改吧完事儿
2.有没有网上现成的,让我扒一个改吧改吧,能实现完事儿
但是我是一个脱离了低级趣味的人呢,我决定自己实现一个,毕竟我的头发还允许我再折腾一把。顺着刚才的开发思路,要实现这个目标我得有三个步骤:
第一步:先整一个vue表单组件,能够输入数据,能切换上一题下一题
第二步:把输入数据能装一个数组,完成各种计算,提供给雷达图
第三步:通过雷达图展示数据
想明白了,就一把梭上代码。
别的不说我先用router 搭一个 三个页面切换的SPA应用。首先我们先在src>views下创建三个文件,分别是Home.vue,CareerTest.vue和About.vue
其中Home.vue 内容如下,其余只是文字不同,略。
<template>
<div>{{ msg }}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
setup() {
// 在 `setup` 方法里声明变量
const msg = "我是Home页";
// 将需要在 `<template />` 里使用的变量 `return` 出去
return {
msg,
};
},
});
</script>
<style scoped>
</style>
注意这里我没有用
<script setup>
const msg = "我是Home页";
</script>
<template>
<div>{{msg}}</div>
</template>
这样的语法糖,不写setup是标准的写法,两种都写写,大家就都会用了。至于区别也不用特意记,写着写着也就会了。其实你一对比就知道,无非就是省了return,还有不用写setup方法,当然还有组件不用注册跟this什么的,这些你不用记,记住:
鲁迅说过,只要项目中你用不到又不影响你撸项目效率和效果的功能,那就是没有用的功能。
配置路由,src>router(src下面建一个router文件夹,下面都这么简写),下面两个文件 index.vue和routes.vue,为什么这么做,因为这样最适合实际工作架构清晰
其中 index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,但对复杂项目来说,往往需要配置上二级、三级路由,逻辑和配置都放到一个文件的话,太臃肿了。
所以如果项目稍微复杂一些,可以像上面这个结构一样拆分成两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。
如果项目更加复杂,例如做一个 Admin 后台,可以按照业务模块,再把 routes 拆分得更细,例如 game.ts / member.ts / order.ts 等业务模块,再统一导入到 index.ts 文件里。这里我用最新的router4 写法,传统的写法,这里会报错,你不用担心为什么我这么写,你照猫画HelloKity就行了。
//router>index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import routes from './routes';
const router = createRouter({
routes,//核心就这个单词而已别的可以都不用管,这个来自于routes.vue
})
export default router;
别写那些引入vue啦 引入vue-router等写法,那个是过去式的写法了,她就像你的前任,忘了她吧。彬哥敲代码养你。
我们再看看routes.vue怎么写
import Home from '../views/Home.vue';
import CareerTest from '../views/CareerTest.vue';
import About from '../views/About.vue';
// 有多少路由都可以在这里面配置
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/Career-Test',
name: 'CareerTest',
component: CareerTest
},
{
path: '/about',
name: 'About',
component: About
}
];
export default routes;
我们搭了三个页面,又配置了路由,总得弄个导航点击才能切换吧,往App.ts里面写,
<script setup lang="ts">
</script>
<template>
<div>
<router-link to="/">首页</router-link>
<router-link to="/career-test">职业测评</router-link>
<router-link to="/about">关于我们</router-link>
<router-view />
</div>
</template>
<style scoped>
</style>
然后,你要再main.js上搞事情,就game over了。你得通知整个SPA应用,我要全局使用路由,如下:
import { createApp } from 'vue'
import App from './App.vue'
// import './assets/main.css'
import router from './router';
createApp(App).use(router).mount('#app')
页面会报错,因为我们用了router但是没有装啊!
npm create vite@latest
装的是小纯洁版本,什么都自己装,自己开发,省的项目臃肿。
我们先整一个router装上,官网:https://router.vuejs.org/zh/i...
命令:
npm install vue-router@4
最后,切换好用了
总结一下:
1.用components下面默认的HelloWorld.vue,在新建的views下面另存三个页面,Home、About和CareerTest.vue
2.新建文件夹router,下面两个文件一个是index.ts做入口配置,一个是routers.vue专门写路径对应
3.App.vue下面弄三个router-link和一个router-view,三个链接切换,一个显示。
4.main.ts里面import和use router。
5.安装router
大家注意我是从项目角度倒着操作的,一般教程是会告诉你先装router然后在弄main.ts,但是那样容易让你懵逼。我们从功能出发,缺什么补什么,你就明白为什么这么做了。比如现在这么丑爆的LowB,你是不是想整个美观的界面?我们就用ant-design-vue UI库造一下。
1.在四vue基础知识的基础上,先装个ant-design-vue先搭个架子。
npm i --save ant-design-vue@next
2.打开官网复制代码,https://2x.antdv.com/componen... 到App.vue,
<style scoped>
</style>
<template>
<a-layout>
<a-layout-header :style="{ position: 'fixed', zIndex: 1, width: '100%' }">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
v-model:selectedKeys="selectedKeys"
:style="{ lineHeight: '64px' }"
>
<router-link to="/"
><a-menu-item key="1">首页</a-menu-item></router-link
>
<router-link to="/Career-Test"
><a-menu-item key="2">职业测评</a-menu-item></router-link
>
<router-link to="/About"
><a-menu-item key="3">关于我们</a-menu-item></router-link
>
</a-menu>
</a-layout-header>
<a-layout-content :style="{ padding: '0 50px', marginTop: '64px' }">
<a-breadcrumb :style="{ margin: '16px 0' }">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<div :style="{padding: '24px', minHeight: '380px' }">
<router-view />
</div>
</a-layout-content>
<a-layout-footer :style="{ textAlign: 'center' }">
彬哥头发多 ©2022 Created by Leolau
</a-layout-footer>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
return {
selectedKeys: ref<string[]>(["2"]),
};
},
});
</script>
<style>
#components-layout-demo-fixed .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 24px 16px 0;
float: left;
}
.site-layout .site-layout-background {
background: #fff;
}
[data-theme="dark"] .site-layout .site-layout-background {
background: #141414;
}
</style>
显然页面是乱的因为我们只是装了antdv没有使用它,怎么搞?打开main.ts,引入组件和css,然后全局use一下。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css'
createApp(App).use(router).use(Antd).mount('#app')//注意这里我用了两个use,没用.use(Antd,router),还是那句话坑我给你屏蔽了你不需要知道用就好了
刷新,页面如下就鸟了:
这个就是套Html的活儿,细心点就行。
总结,
1.其实上面的路由配置写法很多,但是我没讲是因为不想增加复杂度同时很多写法都有坑我给屏蔽了你不需要知道
2.你是要开发项目,不是为了秀技术,知不知道茴香豆的茴字有四种写法不影响你写好文章
到这里,基本的架子打完了,剩下的活无非就是,拉组件,然后写逻辑。接下来我们先把组件结构搞清楚。
六、整站的组件组织结构
架构解释两点:
1.为什么我要从CareerTest跳到HollandTest?因为一级导航只想作为page页解释说明和展示,涉及业务的跳转到对应的page,这样业务page和组件一体化同时避免了 CareerTest的臃肿与混乱
2.数据和样式分别放到单独的文件夹,同理。尤其是数据这块拆分处理实际项目尤其重要,比如这个项目60个选择题,如果都扔到components里面就太臃肿了。也利于未来数据交互与扩展。
啥也不说先撸一个CareerTest的界面,我们使用antdv的cart组件,有UI库撸代码就是快,直接复制粘贴代码地址:
https://2x.antdv.com/componen... 里面的栅格卡片
<template>
<div class="gutter-example">
<p class="title">人格的六种类型</p>
<p>
霍兰德职业兴趣自测(Self-Directed Search)是由美国职业指导专家霍兰德(John
Holland)根据他本人大量的职业咨询经验及其职业类型理论编制的测评工具。霍兰德认为,个人职业兴趣特性与职业之间应有一种内在的对应关系。根据兴趣的不同,人格可分为研究型(I)、艺术型(A)、社会型(S)、企业型(E)、传统型(C)、现实型(R)六个维度,每个人的性格都是这六个维度的不同程度组合。
</p>
<a-row :gutter="[16, 16]" type="flex" justify="center">
<a-col :span="12">
<router-link to="/holland-test"
><a-button type="primary" class="testBtn" block size="large"
>进入测试系统</a-button
>
</router-link>
</a-col>
</a-row>
<div style="padding: 0px 20px 20px 20px">
<a-row :gutter="16">
<a-col :span="8" class="vGap">
<a-card title="现实型(Realistic)" :bordered="false">
<span>共同特征:</span>
<p>
愿意使用工具从事操作性工作,动手能力强,做事手脚灵活,动作协调。偏好于具体任务,不善言辞,做事保守,较为谦虚。
</p>
</a-card>
</a-col>
<a-col :span="8" class="vGap">
<a-card title="研究型(Investigative)" :bordered="false">
<span>共同特征:</span>
<p>
思想家而非实干家,抽象思维能力强,求知欲强,肯动脑,善思考,不愿动手。喜欢独立的和富有创造性的工作。
</p>
</a-card>
</a-col>
<a-col :span="8" class="vGap">
<a-card title="艺术型(Artistic)" :bordered="false">
<span>共同特征:</span>
<p>
有创造力,乐于创造新颖、与众不同的成果,渴望表现自己的个性,实现自身的价值。做事理想化,追求完美,不重实际。
</p>
</a-card>
</a-col>
<a-col :span="8" class="vGap">
<a-card title="社会型(Social)" :bordered="false">
<span>共同特征:</span>
<p>
喜欢与人交往、不断结交新的朋友、善言谈、愿意教导别人。喜欢关心社会问题、渴望发挥自己的社会作用。
</p>
</a-card>
</a-col>
<a-col :span="8" class="vGap">
<a-card title="企业型(Enterprise)" :bordered="false">
<span>共同特征:</span>
<p>
追求权力、权威和物质财富,具有领导才能。喜欢竞争、敢冒风险、有野心、抱负。
</p>
</a-card>
</a-col>
<a-col :span="8" class="vGap">
<a-card title="传统型(Conventional)" :bordered="false">
<span>共同特征:</span>
<p>
尊重权威和规章制度,喜欢按计划办事,细心、有条理,习惯接受他人的指挥和领导,自己不谋求领导职务。
</p>
</a-card>
</a-col>
</a-row>
</div>
</div>
</template>
<style scoped>
.gutter-example :deep(.ant-row > div) {
border: 0;
}
.gutter-box {
padding: 5px 0;
}
.title {
text-align: center;
font-size: 24px;
font-weight: bold;
}
.ant-card-body span {
font-weight: bold;
}
.testBtn {
margin: 5px 0;
display: block;
}
.vGap {
margin-top: 20px;
}
</style>
这里强调两点:
1.你不管怎么套UI,你一定要写成6个card的重复
<a-col :span="8" class="vGap">
<a-card title="Card title" :bordered="false">
<p>card content</p>
</a-card>
</a-col>
……重复六次
别写成3个一组中间加间距,那样后台一套数据就乱了,容易跟你打起来。我这里是写死的就不从数据里面取了。
2.我加了一个vGap类,不是说你用UI库,就不自己搞事情了。别被UI库限制住了结果自己不会写改样式了。
到这里大家看到我,整了一个跳转 ,我们按照架构图来:
<router-link to="/holland-test">
<a-button type="primary" class="testBtn" block size="large">进入测试系统</a-button>
然后我们创建一个HollandTest.vue的组件,里用到了HollandLib组件views>HollandTest.vue文件如下
<script setup lang="ts">
import { Modal } from "ant-design-vue";
import { defineComponent, ref } from "vue";
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import HollandLib from "../components/HollandLib.vue";
const router = useRouter();
onMounted(() => {
Modal.confirm({
title: () => "温馨提示",
okText: () => "开始吧>>",
cancelText: "我再想想……",
content:
() => `测评选项没有对错之分,如果您想对自己有一个真实的认知,请根据自己的实际情况选择是或否√
本测试题共60题,测试时间为20分钟。
现在,请找到一个安静的场所...`,
onCancel() {
router.push({ path: "/Career-Test" });
},
});
});
</script>
<template>
<HollandLib />
</template>
这里我用了一个模态确认框,https://2x.antdv.com/componen... 用的是自定义按钮文字的模态框
重点是这句,
const router = useRouter();//这是新写法 相当于原来的$router
router.push({ path: "/Career-Test" });
点击取消按钮跳转。另外,定义组件用setup语法糖 非常的嗨皮,你不用return,你就放心大胆的定义各种变量和使用各种组件,剩下的交给setup.
敲黑板划重点要写核心结构了。
七、核心测试功能实现
<template>
<div>
<a-row type="flex" justify="center" align="middle" class="rowContainer">
<a-col :span="4" class="previous">
<div>
<a-button>上一题</a-button>
</div>
</a-col>
<a-col :span="8" class="process">
<div>
<a-progress type="line" :percent="75" :format="() => `12/60`" />
</div>
</a-col>
<a-col :span="4"><div class="timeC">04:33</div></a-col>
</a-row>
<a-row
:gutter="[16, 16]"
type="flex"
justify="center"
align="middle"
class="rowContainer"
>
<a-col class="gutter-row" :span="16">
<div class="gutter-box">
<a-card title="1.我喜欢把一件事情做完后再做另一件事。">
<a-button
size="large"
block
class="yesBtn"
@click="nextQuestion('是')"
>是</a-button
>
<a-button size="large" block @click="nextQuestion('否')"
>否</a-button
>
</a-card>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup>
</script>
<style scoped>
.gutter-box {
margin-top: 10px;
}
.ant-card {
padding: 30px;
}
.previous {
text-align: left;
}
.process {
min-height: 35px;
}
.timeC {
text-align: right;
}
.yesBtn {
margin: 10px 0;
}
</style>
关于怎么套页面我就不多墨迹,你看就明白了,我们中心在功能上。
1.测评页套数据
将数据处理完然后套到card上,data>QuestionsData.ts
现在QuestionsData.ts是这样的:
export const quesStr = `
1、我喜欢把一件事情做完后再做另一件事。
2、在工作中我喜欢独自筹划,不愿受别人干涉。
……
`;
这里注意市场给你的是word文档,你需要记事本透一下格式,第二千万别自己一个个写数组,容易把自己写死。也别要求需求方按照你的格式给你数据,那是你的事情跟人家没有半毛钱关系,过分要求别人做不该人家干的事儿,容易被怼死或者打死。
还有就是能不用数据库就别搞太复杂,一是为了响应速度,第二是为了开发速度够用就好。
当下我们只要处理成
['1、我喜欢把一件事情做完后再做另一件事。',
'2、在工作中我喜欢独自筹划,不愿受别人干涉'。
]
的形式,注意每一个问题后面有不定数量空格需要处理
代码也很简单一句话:
let questArrr = quesStr.split('\n').map(s => s.trim()).filter(s => s);
// 定义一个响应式数组questions 来提供卡片内容
let questions = reactive(questArrr);
然后在模板里面套questions[0]就可以了,代码如下:
<template>
<div>
<a-row type="flex" justify="center" align="middle" class="rowContainer">
<a-col :span="4" class="previous">
<div>
<a-button>上一题</a-button>
</div>
</a-col>
<a-col :span="8" class="process">
<div>
<a-progress type="line" :percent="75" :format="() => `12/60`" />
</div>
</a-col>
<a-col :span="4"><div class="timeC">04:33</div></a-col>
</a-row>
<a-row
:gutter="[16, 16]"
type="flex"
justify="center"
align="middle"
class="rowContainer"
>
<a-col class="gutter-row" :span="16">
<div class="gutter-box">
<a-card :title="questions[0]">
<a-button
size="large"
block
class="yesBtn"
@click="nextQuestion('是')"
>是</a-button
>
<a-button size="large" block @click="nextQuestion('否')"
>否</a-button
>
</a-card>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup>
import { quesStr } from "../data/QuestionsData";
import { ref, reactive } from "vue";
let questArrr = quesStr
.split("\n")
.map((s) => s.trim())
.filter((s) => s);
// 定义一个响应式数组questions 来提供卡片内容
let questions = reactive(questArrr);
</script>
<style scoped>
.gutter-box {
margin-top: 10px;
}
.ant-card {
padding: 30px;
}
.previous {
text-align: left;
}
.process {
min-height: 35px;
}
.timeC {
text-align: right;
}
.yesBtn {
margin: 10px 0;
}
</style>
接下来要实现上一题下一题,还有把你选择的结果用一个resArr数组接收,为计算最终结果给雷达图做准备。代码如下:
<template>
<div>
<a-row type="flex" justify="center" align="middle" class="rowContainer">
<a-col :span="4" class="previous">
<div>
<a-button @click="previousQuestion">上一题</a-button>
</div>
</a-col>
<a-col :span="8" class="process">
<div>
<a-progress type="line" :percent="75" :format="() => `12/60`" />
</div>
</a-col>
<a-col :span="4"><div class="timeC">04:33</div></a-col>
</a-row>
<a-row
:gutter="[16, 16]"
type="flex"
justify="center"
align="middle"
class="rowContainer"
>
<a-col class="gutter-row" :span="16">
<div class="gutter-box">
{{ resArr }}
<a-card :title="questions[curIndex]">
<a-button
size="large"
block
class="yesBtn"
@click="nextQuestion('是')"
>是</a-button
>
<a-button size="large" block @click="nextQuestion('否')"
>否</a-button
>
</a-card>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { quesStr } from "../data/QuestionsData";
import { ref, reactive } from "vue";
let questArrr = quesStr
.split("\n")
.map((s) => s.trim())
.filter((s) => s);
// 定义一个响应式数组questions 来提供卡片内容
let questions = reactive(questArrr);
// 点击任意一个选项就是下一题
// 定义一个题号index
let curIndex = ref(0);
// 定义一个选择结果的数组
const resArr = ref([] as any[]);
function nextQuestion(answer: any) {
resArr.value.splice(curIndex.value, 1, answer);
curIndex.value++;
if (curIndex.value == questions.length) {
curIndex.value = questions.length - 1;
}
}
function previousQuestion() {
curIndex.value--;
if (curIndex.value == -1) {
alert("第一题");
curIndex.value = 0;
}
}
</script>
<style scoped>
.gutter-box {
margin-top: 10px;
}
.ant-card {
padding: 30px;
}
.previous {
text-align: left;
}
.process {
min-height: 35px;
}
.timeC {
text-align: right;
}
.yesBtn {
margin: 10px 0;
}
</style>
效果如下:
处理一下上一题,第一题就没必要显示上一题了,
<a-button v-if="curIndex > 0" @click="previousQuestion">上一题</a-button>
最后一题,提示是否提交的模态框
import { Modal } from "ant-design-vue";
function showCompop() {
Modal.confirm({
title: () => "温馨提示",
okText: () => "提交",
cancelText: "取消",
content: () => `恭喜你所有问题已经完成,确认提交查看结果吗?`,
onOk() {
router.push({
path: "/result",
});
},
onCancel() {
router.push({ path: "/holland-test" });
},
});
}
路由跳转还是老方法,就不赘述。另存一个Result.vue,在routes.ts里面加配置具体如下:
import Home from '../views/Home.vue';
import CareerTest from '../views/CareerTest.vue';
import About from '../views/About.vue';
import HollandTest from '../views/HollandTest.vue';
// 有多少路由都可以在这里面配置
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/Career-Test',
name: 'CareerTest',
component: CareerTest
},
{
path: '/About',
name: 'About',
component: About
},
{
path: '/holland-test',
name: '霍兰德测试',
component: HollandTest
},
{
path: '/result',
name: '测试结果',
component: Result
},
];
export default routes;
1.结果页套数据
我需要整一个雷达图,然后一段文字说一下测评结果。
npm i vue-chartjs chart.js
然后我们套一个界面,依然用antdv
<template>
<div style="background: #fff; padding: 14px 24px; min-height: 380px">
<a-descriptions
title="职业兴趣报告:"
bordered
size="middle"
layout="vertical"
>
<a-descriptions-item
label="您的职业兴趣代码是"
:labelStyle="{
textAlign: 'center',
display: 'block',
fontWeight: 'bold',
}"
:span="1"
:contentStyle="{ textAlign: 'center', display: 'block' }"
>
<div class="codeBox">
<div class="code1">S</div>
<div class="code2">E</div>
<div class="code3">C</div>
<p class="code">SEC</p>
</div>
</a-descriptions-item>
<a-descriptions-item
label="您的各类型占比"
:labelStyle="{
textAlign: 'center',
display: 'block',
fontWeight: 'bold',
}"
:contentStyle="{ textAlign: 'center', display: 'block' }"
:span="2"
>
<div style="max-width: 350px; margin: 0 auto">
<Radar id="my-chart-id" :data="chartData" />
</div>
</a-descriptions-item>
<a-descriptions-item label="CAS型适合的职业" :span="3">
<a-badge
text="厨师、林务员、跳水员、潜水员、染色员、电器修理、眼镜制作、电工、纺织机器装配工、服务员、装玻璃工人"
/>
</a-descriptions-item>
<a-descriptions-item label="形态" :span="1">现实型R</a-descriptions-item>
<a-descriptions-item label="人格倾向" :span="1"
>喜欢与人交往、不断结交新的朋友、善言谈、愿意教导别人。关心社会问题、渴望发挥自己的社会作用。寻求广泛的人际关系,比较看重社会义务和社会道德</a-descriptions-item
>
<a-descriptions-item label="典型职业" :span="3"
><div class="codeBox">工人 农民 土木工程师</div></a-descriptions-item
>
<a-descriptions-item label="形态" :span="1">现实型R</a-descriptions-item>
<a-descriptions-item label="人格倾向" :span="1"
>具有顺从、坦率、谦虚、自然、坚毅、实际、有礼、害羞、稳健、节俭的特征,表现为
1、喜爱实用性的职业或情境,以从事所喜好的活动,避免社会性的职业或情境
2、用具体实际的能力解决工作及其他方面的问题,较缺乏人际关系方面的能力。
3、重视具体的事物,如金钱,权力、地位等。</a-descriptions-item
>
<a-descriptions-item label="典型职业" :span="3"
><div class="codeBox">喜欢要求与人打交道的工作,能够不断结交新的朋友,从事提供信息、启迪、帮助、培训、开发或治疗等事务,并具备相应能力。如: 教育工作者(教师、教育行政人员),社会工作者(咨询人员、公关人员)。</div></a-descriptions-item
>
<a-descriptions-item label="形态" :span="1">现实型R</a-descriptions-item>
<a-descriptions-item label="人格倾向" :span="1"
>具有顺从、坦率、谦虚、自然、坚毅、实际、有礼、害羞、稳健、节俭的特征,表现为
1、喜爱实用性的职业或情境,以从事所喜好的活动,避免社会性的职业或情境
2、用具体实际的能力解决工作及其他方面的问题,较缺乏人际关系方面的能力。
3、重视具体的事物,如金钱,权力、地位等。</a-descriptions-item
>
<a-descriptions-item label="典型职业" :span="3"
><div class="codeBox">工人 农民 土木工程师</div></a-descriptions-item
>
<a-descriptions-item
label="温馨提示"
:labelStyle="{
textAlign: 'left',
display: 'block',
fontWeight: 'bold',
}"
:contentStyle="{ textAlign: 'left' }"
:span="3"
>
<p>
除了关注得分最高的前三项CAS,找到适合自己发展的职业领域外,还希望您也特别关注一下自己最不适合发展的职业领域,即得分最低的那个类型所指向的领域,避免长时间地在不适合自己发展的职业领域探索。
</p>
</a-descriptions-item>
</a-descriptions>
</div>
</template>
<script setup lang="ts">
import { Radar } from "vue-chartjs";
import { ref, reactive } from "vue";
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
} from "chart.js";
ChartJS.register(
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend
);
let chartData = reactive({
labels: [
"研究型(I)",
"艺术型(A)",
"社会型(S)",
"企业型(E)",
"传统型(C)",
"现实型(R)",
],
datasets: [
{
label: "您的各类型占比",
backgroundColor: "rgba(255,99,132,0.2)",
borderColor: "rgba(255,99,132,1)",
pointBackgroundColor: "rgba(255,99,132,1)",
pointBorderColor: "#fff",
pointHoverBackgroundColor: "#fff",
pointHoverBorderColor: "rgba(255,99,132,1)",
data: [0, 0, 0, 0, 0, 0],
},
],
});
</script>
<style scoped>
.codeBox {
position: relative;
width: 300px;
}
.code1,
.code2,
.code3 {
width: 100px;
height: 100px;
position: absolute;
border-radius: 50px;
line-height: 100px;
font-size: 40px;
color: #fff;
}
.code {
position: absolute;
color: black;
z-index: 100;
left: 125px;
top: 80px;
font-size: 30px;
}
.code1 {
background: green;
left: 100px;
top: -110px;
}
.code2 {
background: orange;
left: 50px;
top: -25px;
}
.code3 {
background: blue;
left: 150px;
top: -25px;
}
</style>
报告部分用了描述列表https://2x.antdv.com/componen...
八、数据对接
现在我们测试选项页能把测试题结果拿到了,然后结果报告页静态页面也出来了,现在最关键的点就来了,
如何将测试结果数组处理成 报告页的数据展示出来。
这里分三步:
1.把数据传过来
2.把数据处理成用来展示的
3.套数据展示
九、状态管理与pinia
处理第一步数据的时候,我们需要把数据components>从HollandLib.vue 的resArr传到views>Result.vue里面。
我们当然可以用通过向父组件传递数据然后传递给兄弟组件,但是大量数据的时候就很乱了,所以我们使用的Vue 状态管理库pinia。
别墨迹 先装上再说,官网https://pinia.vuejs.org/zh/ge...
npm install pinia
打开main.ts跟使用router和ant-design-vue一样。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css'
import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(router).use(Antd).use(pinia).mount('#app')//注意这里我用了多个个use,没用.use(Antd,router),还是那句话坑我给你屏蔽了你不需要知道用就好了
装上了就开整,你不要一看到状态管理就觉得复杂和高大上。其实简单的令人发指。我做个类比你就秒懂。
你家水果可以放厨房柜子里,也可以放客厅冰箱里,你家里好几个人都吃水果,洗衣部分随手放在水池子边上或者其它两个位置,下一个人吃,就得很麻烦的找来找去。另外你一会拿个香蕉一会拿个苹果忒麻烦了吧。怎么办呢?
你家里就规定了,水果都放在冰箱的保鲜区,用水果篮子洗和放水果吃完都要放回去。
我们在src>stores>userCheckStore.ts,内容如下:
import { defineStore } from 'pinia'
import { reactive } from 'vue'
//defineStore以冰箱 userCheckStore
export const userCheckStore = defineStore('QueuserChecktionRes', {
// sate就是保鲜区,resArr就是苹果、香蕉各种你要拿走的水果(数据),你不用管为啥我用reactive,你包一层不错,有时候你不包可能就错,所以不用记照着搞就行了
state: () => {
return {
resArr: reactive([])
}
},
})
然后我们在各个组件用resArr用它,怎么用HollandLib.vue下
import { userCheckStore } from "../stores/userCheckStore";
import { storeToRefs } from "pinia";
const store = userCheckStore();
const {resArr} = storeToRefs(store);
userCheckStore 就是我们冰箱,store就是保鲜区,resArr是苹果,是不是很简单?storeToRefs是为了响应式数据的,你也不用管是啥,你就包上就完事儿了。反正作为新手,我讲storeToRefs包不包的区别意义也不大。上面那段话你想在哪个组件用就在那个组件这么写就行。然后我们说下怎么把香蕉放回冰箱,也就是修改数据。
store.resArr.splice(curIndex.value, 1, answer);
就跟修改对象没区别注意前面带上store.resArr(改的是保鲜区里的香蕉,你不能改错了,这样所有组件都能接收到你的修改。
store.name = 'xxxx'这样的形式,因为我玩的是数组,所以splice了一下,道理一样。这样做目的是我把每道题的选项上一题的时候能修改成点击的那个值,最后是[][]['是','否','是'],这个数组views>Result.vue能拿到,就可以计算和嵌套数据了,计算数据这个跟ts和vue3无关,是原生js基础的东西。这里去强调一点要熟练使用数组、对象和正则处理数据,而嵌套数据就跟我们前面七、核心测试功能实现嵌套问题卡没区别了。
最后是打包上线,
npm run build
把dist文件家里的内容扔到服务器根目录下就可以了。
十、关于开发效率
最后我说一下如何提高开发效率,用框架肯定是快,但是前提是原生js的语法和算法要很了解,还有逻辑清晰。
比如给大家举个例子:resultData.ts这个数据是市场部们给的,
export let sixTypeStr = `
1、社会型:(S)
共同特征:喜欢与人交往、不断结交新的朋友、善言谈、愿意教导别人。关心社会问题、渴望发挥自己的社会作用。寻求广泛的人际关系,比较看重社会义务和社会道德
典型职业:喜欢要求与人打交道的工作,能够不断结交新的朋友,从事提供信息、启迪、帮助、培训、开发或治疗等事务,并具备相应能力。如: 教育工作者(教师、教育行政人员),社会工作者(咨询人员、公关人员)。
2、企业型:(E)
共同特征:追求权力、权威和物质财富,具有领导才能。喜欢竞争、敢冒风险、有野心、抱负。为人务实,习惯以利益得失,权利、地位、金钱等来衡量做事的价值,做事有较强的目的性。
典型职业:喜欢要求具备经营、管理、劝服、监督和领导才能,以实现机构、政治、社会及经济目标的工作,并具备相应的能力。如项目经理、销售人员,营销管理人员、政府官员、企业领导、法官、律师。
3、常规型:(C)
共同特点:尊重权威和规章制度,喜欢按计划办事,细心、有条理,习惯接受他人的指挥和领导,自己不谋求领导职务。喜欢关注实际和细节情况,通常较为谨慎和保守,缺乏创造性,不喜欢冒险和竞争,富有自我牺牲精神。
典型职业:喜欢要求注意细节、精确度、有系统有条理,具有记录、归档、据特定要求或程序组织数据和文字信息的职业,并具备相应能力。如:秘书、办公室人员、记事员、会计、行政助理、图书馆管理员、出纳员、打字员、投资分析员。
4、实际型:(R)
共同特点:愿意使用工具从事操作性工作,动手能力强,做事手脚灵活,动作协调。偏好于具体任务,不善言辞,做事保守,较为谦虚。缺乏社交能力,通常喜欢独立做事。
典型职业:喜欢使用工具、机器,需要基本操作技能的工作。对要求具备机械方面才能、体力或从事与物件、机器、工具、运动器材、植物、动物相关的职业有兴趣,并具备相应能力。如:技术性职业(计算机硬件人员、摄影师、制图员、机械装配工),技能性职业(木匠、厨师、技工、修理工、农民、一般劳动)。
5、调研型:(I)
共同特点:思想家而非实干家,抽象思维能力强,求知欲强,肯动脑,善思考,不愿动手。喜欢独立的和富有创造性的工作。知识渊博,有学识才能,不善于领导他人。考虑问题理性,做事喜欢精确,喜欢逻辑分析和推理,不断探讨未知的领域。
典型职业:喜欢智力的、抽象的、分析的、独立的定向任务,要求具备智力或分析才能,并将其用于观察、估测、衡量、形成理论、最终解决问题的工作,并具备相应的能力。 如科学研究人员、教师、工程师、电脑编程人员、医生、系统分析员。
6、艺术型:(A)
共同特点:有创造力,乐于创造新颖、与众不同的成果,渴望表现自己的个性,实现自身的价值。做事理想化,追求完美,不重实际。具有一定的艺术才能和个性。善于表达、怀旧、心态较为复杂。
典型职业:喜欢的工作要求具备艺术修养、创造力、表达能力和直觉,并将其用于语言、行为、声音、颜色和形式的审美、思索和感受,具备相应的能力。不善于事务性工作。如艺术方面(演员、导演、艺术设计师、雕刻家、建筑师、摄影家、广告制作人),音乐方面(歌唱家、作曲家、乐队指挥),文学方面(小说家、诗人、剧作家)。`;
拿到以后多余空格不用管,你用编辑器格式化就全没了,但是空格你肯定不能手动删除吧?很多人想都不想直接split
let resultArr = resultStr.split("\n");
也确实该这么做,但是问题就来了,
这就要求你熟练的使用数组方法,正则了和字符串操作方法。
还有再处理sixTypeStr
export let sixTypeStr = `
1、社会型:(S)
共同特征:喜欢与人交往、不断结交新的朋友、善言谈、愿意教导别人。关心社会问题、渴望发挥自己的社会作用。寻求广泛的人际关系,比较看重社会义务和社会道德
典型职业:喜欢要求与人打交道的工作,能够不断结交新的朋友,从事提供信息、启迪、帮助、培训、开发或治疗等事务,并具备相应能力。如: 教育工作者(教师、教育行政人员),社会工作者(咨询人员、公关人员)。
2、企业型:(E)
共同特征:追求权力、权威和物质财富,具有领导才能。喜欢竞争、敢冒风险、有野心、抱负。为人务实,习惯以利益得失,权利、地位、金钱等来衡量做事的价值,做事有较强的目的性。
典型职业:喜欢要求具备经营、管理、劝服、监督和领导才能,以实现机构、政治、社会及经济目标的工作,并具备相应的能力。如项目经理、销售人员,营销管理人员、政府官员、企业领导、法官、律师。
3、常规型:(C)
时候,你split完会是这样,
你其实要的是 三个一组,包含社会型,共同特征,还有职业。一般人又会去循环处理数组再扔到一个对象里,这样做当然更好更灵活,但是你处理的时候又会涉及到又要隔3个循环,还有考虑数组中对象类型如何对应,比如S要对应前三项,E要对应456项目,这样又要做一个数组放六种类型……
其实到这里已经可以用了,你只要找到包含字母S的,然后下标+1 +2你要的数据就可以嵌套了,一句话搞定。
const index = sixTypeArr.findIndex((item) => item.includes(searchChar[i]));
使用了数组的findIndex和includes方法,所以大家可以看到,熟练的使用数组操作能够极大的提升开发效率与降低代码量。
十一,与需求方和产品沟通与扯皮
很多程序员要跟产品、测试和需求方关系非常紧张,吵架。很大部分原因是没有搞明白程序员是干什么的。
程序员是把需求方的需求准确无误的告诉电脑高效的执行,这是最低需求。
比如我测试结果是ECA,但是并没有ECA类型的测试结果与职业匹配对照结果,这个问题第一做好提醒需求方,第二你处理好这种情况的提示即可。至于你通过威逼利诱砍掉你不想开发的功能那是进阶了,至少你得做到前面这点,你才是一个合格的程序员。
十二,写在最后
1.程序员不要自嗨,够用就好。
因为这个工具我自己和公司用,另外想通过这个项目让大家迅速的能够用vue3开发项目,而不是卡住大家,所以我省略了很多的细节,比如路由我实际开发的时候使用v-for循环的,但是讲的时候,我就直接写死了,因为不想增加大家理解复杂度。
2.不要手里有锤子,眼里都是钉子
程序是为了实现功能和业务服务,而程序永远不是唯一的实现方式(但是往往是高效的方式),能不用程序就能高效解决,那就不要写程序。
最后奉上效果图:
最后网站展示效果:https://leolau2012.github.io/
关于这个教程Follow的过程中,有人任何问题和卡顿,欢迎留言或私信。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。