第一次小笔试
vue2 + vite + tailwindcss + vuex + vant2 + postcss-px-to-viewport-8-plugin
vue2 + vite + tailwindcss + vuex + UI框架随意 需要你自己整合前端框架
使用tailwind实现图1的界面,新增成员弹窗提示即可。
并实现图2的左滑功能,呼叫按钮弹窗提示即可,移除按钮是单个移除。
点击批量移除就是图三,实现批量移除功能。
所有数据都需要存本地,刷新后数据不丢失。
提交的代码不要包含测试用例,不要提交node_modules,不要提交.idea文件夹,不要提交.vscode文件夹,不要提交.gitignore文件,不要提交.git文件夹,不要提交.DS_Store文件
早上起床有昏昏的,没看清楚要求,搭环境一错再错。弄了两三个小时,用的vue2和h5插件进行编写。为了看起来我会一点,加了mock(写完才发现说不要写)、axios、vue-router,最后写完看了一下面试公司,发现招的好像是uniapp,我写的h5。因为我没用过vite主观意识是直接用vue写h5界面,实际上这个用uniapp一小时就写完了(哭)。
项目搭建的时候遇到了很多坑,很多版本冲突问题,代码不生效,需要调整到合适的版本才能用vue2+vite2等。
废话少说,直接看界面和代码吧。
压缩包
1.项目结构
└─src //主目录
├─api //api文件
├─mock //mock 只用了搜索添加,默认是三条数据
│ └─test
├─router //路由
├─store //持久化
└─views //视图
└─components
2.项目搭建代码
package.json
{
"name": "app",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --mode mock--force",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"autoprefixer": "^9.8.8",
"babel-plugin-import": "^1.13.8",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"mockjs": "^1.1.0",
"postcss": "^8.4.12",
"postcss-loader": "^8.1.1",
"sass": "^1.77.6",
"sass-loader": "^12.6.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
"vite": "^2.8.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-vue2": "^1.9.3"
},
"dependencies": {
"axios": "^1.7.2",
"path": "^0.12.7",
"postcss-import": "^11.0.0",
"postcss-px-to-viewport-8-plugin": "^1.2.2",
"vant": "^2.13.2",
"vue": "^2.7.16",
"vue-router": "^3.5.2",
"vue-template-compiler": "^2.7.16",
"vuex": "^3.6.2",
"vuex-persistedstate": "^3.2.1"
}
}
先创一个vite.config.js
import { defineConfig } from 'vite'; // 动态配置函数
import { createVuePlugin } from 'vite-plugin-vue2';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import postcsspxtoviewport from "postcss-px-to-viewport-8-plugin";
import { resolve } from 'path'
import { viteMockServe } from 'vite-plugin-mock';
export default () =>
defineConfig({
plugins: [
viteMockServe({
mockPath: "./src/mock",
enable:true, // 是否使用mock接口
}),
createVuePlugin(),
postcsspxtoviewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 750, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: false, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
// exclude: [],
// include: [], //如果设置了include,那将只有匹配到的文件才会被转换
landscape: false // 是否处理横屏情况
})
],
server: {
open: true, //自动打开浏览器
port: 3000 //端口号
},
resolve: {
// 别名
alias: [
{
find: '@',
replacement: '/src'
}
]
},
//css插件导入
css: {
postcss: {
plugins: [
tailwindcss,
autoprefixer,
]
}
}
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
//这里改了文件入口 如果是使用vite5直接选择的vue版本搭建的话,其实这些都有了,但是版本是vue3的。
<script type="module" src="/src/main.js"></script>
</body>
</html>
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
tailwind.config.js
module.exports = {
content: [
"./index.html",
'./src/**/*.{vue,js,ts,jsx,tsx}'
],
theme: {
screens: {
sm: '480px',
md: '768px',
lg: '976px',
xl: '1440px',
},
colors: {
'blue': '#1fb6ff',
'purple': '#7e5bef',
'pink': '#ff49db',
'orange': '#ff7849',
'green': '#13ce66',
'yellow': '#ffc82c',
'gray-dark': '#273444',
'gray': '#8492a6',
'gray-light': '#d3dce6',
},
fontFamily: {
sans: ['Graphik', 'sans-serif'],
serif: ['Merriweather', 'serif'],
},
extend: {
spacing: {
'128': '32rem',
'144': '36rem',
},
borderRadius: {
'4xl': '2rem',
}
}
},
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
App.vue
<template>
<div id="app" class="app">
<router-view></router-view>
</div>
</template>
<style >
#app {
/* ios底部安全距离 */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
*{
padding:0;
margin:0;
}
</style>
src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/index.js'
import store from './store'
import './tailwind.css'
// import Vant from 'vant';
import 'vant/lib/index.css';
import { Button } from 'vant';
import { Icon } from 'vant';
import { SwipeCell } from 'vant';
import { Checkbox, CheckboxGroup } from 'vant';
import { Skeleton } from 'vant';
import { Sticky } from 'vant';
import { Dialog } from 'vant';
import { Empty } from 'vant';
import { Popup } from 'vant';
import { Search } from 'vant';
Vue.use(Search);
Vue.use(Popup);
Vue.use(Empty);
Vue.use(Dialog);
Vue.use(Sticky);
Vue.use(Skeleton);
Vue.use(Checkbox);
Vue.use(CheckboxGroup);
Vue.use(SwipeCell);
Vue.use(Icon);
Vue.use(Button);
new Vue({
router,//注册router路由
store,
render: h => h(App)
}).$mount('#app')
主要界面
<template>
<div class="container mx-auto px-4">
<van-sticky>
<div
class="container top-box rounded shadow-lg bg-cyan-500 shadow-cyan-500/50"
>
<div class="title py-2 border-b border-slate-50 px-4">
{{ username }}
</div>
<div class="bar-content flex py-2 px-4 justify-between">
<div class="bar-item" v-for="(item, index) in barList" :key="index">
{{ item.name }}
</div>
</div>
</div>
</van-sticky>
<div class="team-box mt-5">
<div class="team-title flex items-center justify-between mb-2">
<span class="flex items-center">
<van-icon name="friends" size="25px" />
<span class="ml-1 font-semibold">成员列表</span>
</span>
<span class="font-medium" v-if="isLoaded">
共{{ linkList.length }}人
</span>
</div>
<div class="team-content" v-if="isLoaded">
<div
class="team-item border-b border-slate-50 items-center"
v-for="(item, index) in linkList"
:key="index"
:style="{ display: type != 1 ? 'flex' : '' }"
>
<div
:class="{ imgAnimation: type == 2, block: true }"
style="line-height: 100%"
>
<van-checkbox
v-model="item.checkDel"
v-show="type === 2"
></van-checkbox>
</div>
<van-swipe-cell right-width="auto" :disabled="type != 1">
<LinkItem :item="item" />
<template #right>
<div class="py-2">
<van-button square type="primary" text="呼叫" />
<van-button
square
type="danger"
text="移除订单"
@click="delOneHandle(index)"
/>
</div>
</template>
</van-swipe-cell>
</div>
<van-empty description="请添加成员!" v-if="linkList.length === 0" />
</div>
<div class="team-content" v-else>
<div
class="team-item border-b border-slate-50 items-center px-4 py-2"
v-for="item in 10"
:key="item"
>
<van-skeleton :row="2" :row-width="['80%', '80%']"> </van-skeleton>
</div>
</div>
</div>
<div class="block-view">占位</div>
<div class="bottom-tool fixed px-4 box-border bottom-0 left-0 right-0">
<div class="add-tool flex" v-show="type === 1">
<div class="btn box-border" @click="changeType(2)">批量移除</div>
<div class="btn box-border" @click="changeAddHandle">新增成员</div>
</div>
<div
class="add-tool flex items-center justify-between"
v-show="type === 2"
>
<div class="all-check">
<van-checkbox v-model="isAllDel" @change="changeAllDel"
>全选</van-checkbox
>
</div>
<div class="right-content">
<van-button
plain
type="primary"
style="margin-right: 10px"
@click="changeType(1)"
>取消</van-button
>
<van-button
type="primary"
:loading="loadingDel"
:disabled="!isLoaded"
loading-text=" "
loading-size="16px"
@click="delSelectHandle()"
>
确认移除
</van-button>
</div>
</div>
</div>
<!-- 添加成员弹出层 -->
<van-popup
v-model="showAddList"
position="bottom"
:style="{ height: '80%' }"
>
<div class="pop-content">
<div class="search-box bg-white fixed z-10">
<van-search
v-model="search"
show-action
placeholder="请输入搜索(名字/电话)"
@search="onSearch"
>
<span slot="action" @click="onSearch">搜索</span>
</van-search>
</div>
<div class="team-content">
<div
class="team-item flex border-b border-slate-50 items-center"
v-for="(item, index) in addList"
:key="index"
>
<span class="pl-2">
<van-checkbox v-model="item.checkAdd"></van-checkbox>
</span>
<LinkItem :item="item" />
</div>
</div>
<div
class="bottom-tool bg-white fixed px-4 py-1 box-border bottom-0 left-0 right-0"
>
<div class="add-tool flex justify-center">
<van-button
plain
type="primary"
style="margin-right: 10px"
@click="changeAddHandle"
>取消</van-button
>
<van-button
type="primary"
:loading="loadingDel"
:disabled="!isLoaded"
loading-text=" "
loading-size="16px"
@click="addSelectHandle()"
>
确认添加
</van-button>
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script>
import LinkItem from "./components/LinkItem.vue";
// import {getList} from '@/api/index.js'
import axios from "axios";
import { Dialog } from "vant";
export default {
name: "HomeView",
components: {
LinkItem,
},
data() {
return {
username: "",
barList: [
{ name: "无需签到" },
{ name: "市场推广" },
{ name: "人员管理" },
{ name: "收支账单" },
],
linkList: [], //成员列表
addList: [], //添加成员列表
loadingDel: false, //删除加载
isLoaded: false, //加载
isAllDel: false,
type: 1, //1按钮 2批量 3...
showAddList: false, //添加成员弹出层
search: "", //搜索文字
};
},
created() {
this.username = this.$store.state.userInfo.name;
this.mockList();
this.mockaddList();
// getList().then(res=>{
// console.log(res)
// })
},
methods: {
mockList() {
//mock
this.isLoaded = false;
// axios({
// url: "/api/link/list",
// method: "post",
// })
// .then((res) => {
// this.linkList = res.data.data.data.filter((item) => {
// item.checkDel = false; //添加属性
// return item;
// });
// setTimeout(() => {
// this.isLoaded = true;
// }, 1000);
// })
// .catch((error) => {
// Dialog({ message: error.toString() });
// });
this.linkList = this.$store.state.linkList
setTimeout(() => {
this.isLoaded = true;
}, 1000);
},
changeType(type) {
this.type = type;
},
changeAllDel(check) {
this.isAllDel = check;
this.linkList = this.linkList.filter((item) => {
item.checkDel = check;
return item;
});
},
changeAddHandle() {
this.showAddList = !this.showAddList;
},
//存储store
updateLinkList(){
this.$store.commit('changeLinkList',this.linkList)
},
delSelectHandle() {
Dialog.confirm({
title: "提示",
message: "是否删除所选成员",
})
.then(() => {
this.loadingDel = true;
const isAllDel = this.isAllDel;
if (isAllDel) {
//全选
this.linkList = [];
} else {
this.linkList = this.linkList.filter((item) => !item.checkDel);
}
this.updateLinkList();//更新store
setTimeout(() => {
if (isAllDel) this.type = 1; //全删了 就没了
this.loadingDel = false; //模拟删除
}, 1000);
this.isAllDel = false;
})
.catch(() => {
// on cancel
});
},
//添加成员
addSelectHandle() {
Dialog.confirm({
title: "提示",
message: "是否添加所选成员",
})
.then(() => {
this.loadingDel = true;
this.linkList.push(...this.addList.filter((item) => item.checkAdd));
this.updateLinkList();//更新store
setTimeout(() => {
this.changeAddHandle();
this.mockaddList();
this.loadingDel = false; //模拟删除
}, 1000);
})
.catch(() => {
// on cancel
});
},
//删除一个
delOneHandle(i) {
Dialog.confirm({
title: "提示",
message: "是否删除该成员",
})
.then(() => {
this.linkList.splice(i, 1);
this.updateLinkList();//更新store
})
.catch(() => {
// on cancel
});
},
onSearch(e) {
this.mockaddList();
},
mockaddList() {
//mock
axios({
url: "/api/link/get",
method: "post",
data: {
search: this.search,
},
})
.then((res) => {
console.log(res);
this.addList = res.data.data.data.filter((item) => {
item.checkAdd = false; //添加属性
return item;
});
})
.catch((error) => {
Dialog({ message: error.toString() });
});
},
},
};
</script>
<style lang="less" scoped>
.container {
.pop-content {
width: 100%;
.search-box {
width: 100%;
}
.team-content {
padding-top: 50px;
padding-bottom: 50px;
}
}
.top-box {
background-color: #fff;
width: 100%;
.title {
font-size: 20px;
}
.bar-content {
.bar-item {
}
}
}
.block-view {
height: 100px;
width: 100%;
opacity: 0;
background-color: #fff;
}
.bottom-tool {
background-color: #fff;
.add-tool {
margin: 0 auto;
height: 50px;
.btn {
width: calc(50% - 22px);
line-height: 50px;
text-align: center;
font-size: 16px;
position: relative;
color: #fff;
&:nth-child(1) {
background-color: #1989fa;
margin-right: 52px;
padding-left: 25px;
&::after {
content: "";
position: absolute;
right: -49px;
top: 0;
width: 0;
height: 0;
border-top: 50px solid #1989fa;
border-right: 50px solid transparent;
border-bottom: 50px solid transparent;
}
}
&:nth-child(2) {
background-color: #07c160;
padding-right: 25px;
&::after {
content: "";
position: absolute;
left: -49px;
top: 0;
width: 0;
height: 0;
border-top: 50px solid transparent;
border-right: 50px solid #07c160;
border-bottom: 50px solid transparent;
}
}
}
}
}
}
.imgAnimation {
animation-name: imgAnimation !important;
animation: imgAnimation 2s alternate infinite !important;
-webkit-animation: imgAnimation 2s alternate infinite !important;
-moz-animation: imgAnimation 2s alternate infinite !important;
}
@-webkit-keyframes imgAnimation {
0% {
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
@-moz-keyframes imgAnimation {
0% {
opacity: 0;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
</style>
LinkItem.vue
小组件
<template>
<div class="container px-4 py-2 ">
<div>{{item.name}}({{item.phone}})</div>
<div><van-icon name="idcard" />{{item.card_id}}</div>
</div>
</template>
<script>
export default {
props:{
item:{
type:Object,
default:[]
}
}
}
</script>
补充:PC端h5页面大小固定居中
@media only screen and (min-width: 900px) {
* {
max-width: 375px;
margin: 0 auto;
}
.van-overlay{
left: 50%!important;
transform: translateX(-50%);
}
.van-popup--bottom{
left: 50%!important;
transform: translateX(-50%);
}
}