人员管理小demo
发表于:2023-03-22 |

需求

根据后端返回数据将信息分类成四部分,然后将信息展示,点击之后展示详情,分割线可以拖拽进行列宽调整
效果图

数据

数据结构
字段含义

实现

1.技术栈

  1. vue3-vue-router
  2. html-css(less)-js
  3. vite
  4. Splitpanes
  5. 搭建项目常用的一些配置

2.手把手实现

安装vite

需要先安装好node环境,才有下面的步骤

  • 使用NPM
    1
    npm create vite@latest
  • 使用Yarn
    1
    yarn create vite

创建项目

1
2
3
4
5
6
7
8
9
10
11
# npm 6.x
npm create vite@latest my-vue-app --template vue

# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

创建后项目的结构目录

安装依赖并启动

安装依赖

1
npm i

项目启动

1
npm run dev

项目启动后的样子

修改一些配置

  • 页面title修改
    index.html文件中title修改
  • style.css修改
    css修改
  • HelloWord.vue重命名,清空内容
    内容修改重命名
  • 修改App.vue
    修改App.vue
  • 导入json文件
    json文件
  • 新增.editorconfig文件用来格式化代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # https://editorconfig.org
    root = true

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

    [*.md]
    insert_final_newline = false
    trim_trailing_whitespace = false

在这些完成之后接下来只需要在Demo.vue中绘制页面即可

根据需求思考逻辑

首先,需求要求把这些数据分成四部分,那就需要在页面初始化的时候进行js筛选,下面我们来写这部分代码

  • 页面初始化导入json
    1
    2
    3
    4
    5
    6
    7
    <script setup>
    import { onMounted } from 'vue';
    import demoJson from "./demo.json"
    onMounted(()=>{
    console.log(demoJson)
    })
    </script>
    导入后控制台输出

接下来,我们通过简单的js方法把这些对象进行分类,下面为参考代码,你也可以用自己的方法

  • 筛选
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <script setup>
    import { onMounted } from 'vue';
    import demoJson from "./demo.json"
    onMounted(()=>{
    // 逻辑:当info中的recordType为0时是单飞数据,5是第一遍,3是第二遍,1是要素
    // 单飞数据
    const dataSingle = [];
    // 第一遍
    const dataFirst = [];
    // 第二遍
    const dataSecond = [];
    // 要素
    const dataEle = [];
    const keysArray = {
    0: dataSingle,
    5: dataFirst,
    3: dataSecond,
    1: dataEle,
    };
    Object.entries(demoJson).forEach(([key,value])=>{
    keysArray[value.info.recordType]&&keysArray[value.info.recordType].push({title:key,value})
    })
    console.log(keysArray)
    })
    </script>
    筛选后控制台输出

除此之外,我们还需要处理一个数据,就是公司中有未知单子,需要减去,参考字段含义这张图片中的unknowCount字段,因此我们在生成数据keysArray的时候还需要做一些手脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.entries(demoJson).forEach(([key, value]) => {
keysArray[value.info.recordType] &&
keysArray[value.info.recordType].push({
title: key,
value: {
...value,
unknowCounts: value?.unfinish?.company
? Object.values(value.unfinish.company).reduce((pre, cur) => {
return pre + (cur.unknowCount || 0);
}, 0)
: 0,
},
});
});

接下来进行赋值双向绑定数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { onMounted } from 'vue';
import demoJson from "./demo.json"
const dataList=ref([])
onMounted(()=>{
// ...... 省略部分代码
dataList.value= [
{ title: "单飞", data: dataSingle },
{ title: "第一遍", data: dataFirst },
{ title: "第二遍", data: dataSecond },
{ title: "要素", data: dataEle },
];
})
</script>

然后有了数据之后,我们就需要一个对应的页面去渲染数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div class="container">
<div v-for="item in dataList" :key="item.title">
<div class="inner-container">
<div class="title">{{ item.title }}</div>
<div class="body-container">
<div v-for="cItem in item.data" :key="cItem">
{{ handleStr(cItem) }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// ...省略部分js代码
const handleStr = (item) => {
const { title, value } = item;
const baseStr = `${title}[${value?.unfinish?.count || 0}]票`;
const addStr = value?.unfinish?.count
? `(${value.unfinish.count - value.unknowCounts}-${
value.unfinish.goodsNum
})品`
: "";
return baseStr + addStr;
};
</script>
<style scoped>
.container {
background: #fafafa;
display: flex;
width: 100%;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ */
user-select: none;
}
.inner-container{
cursor:pointer;
}
</style>

简单渲染

接下来把样式简单优化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<template>
<div class="container">
<div v-for="item in dataList" :key="item.title" class="data-container">
<div class="inner-container">
<div class="title">
<div class="bar"></div>
{{ item.title }}
</div>
<div class="body-container">
<div v-for="cItem in item.data" :key="cItem" class="data-item">
{{ handleStr(cItem) }}
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
background: #fafafa;
display: flex;
overflow: auto;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ */
user-select: none;
}
.data-container {
width: 25%;
height: 100%;
}
.inner-container {
cursor: pointer;
}

.title {
height: 56px;
background-color: #f1f4ff;
color: #444756;
font-size: 18px;
font-weight: 500;
line-height: 21px;
display: flex;
flex-shrink: 0;
align-items: center;
}

.bar {
background-color: #5069c1;
width: 5px;
height: 19px;
border-radius: 1px;
margin: 0 8px;
}

.body-container {
width: 100%;
display: flex;
flex-direction: column;
flex-wrap: wrap;
overflow: auto;
padding: 10px 20px;
box-sizing: border-box;
height: calc(100vh - 56px);
}

.data-item {
line-height: 24px;
font-size: 16px;
padding: 10px 5px;
white-space: nowrap;
color: #333;
border-radius: 4px;
}
</style>

样式优化

接下来我们添加可拖拽的分割线,这时就用到了之前技术栈中的那个可能平时你没听说过的Splitpanes库,用js也可以实现,但是能用库谁还自己搞呢?(导入库的步骤就省略了,文档里面有)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<splitpanes class="container">
<pane
v-for="item in dataList"
:key="item.title"
class="data-container"
min-size="15"
>
<!-- 省略 -->
</pane>
</splitpanes>
</template>
<script setup>
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
// 省略
</script>
<style>
.splitpanes--vertical > .splitpanes__splitter {
min-width: 4px;
background: #b5c7ff;
}
.splitpanes__pane {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2) inset;
}
</style>

将size尺寸持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<splitpanes class="container" @resize="handleResize">
<pane
v-for="(item, index) in dataList"
:key="item.title"
class="data-container"
:style="{ width: sizeMap[index] + '%' }"
min-size="15"
>
<!-- 省略 -->
</pane>
</splitpanes>
</template>
<script setup>
const sizeMap = ref([]);
onBeforeMount(() => {
const size = localStorage.getItem("sizeMap");
sizeMap.value = size ? JSON.parse(size) : [25, 25, 25, 25];
});
const handleResize = (data) => {
// 将size存起来保存至浏览器本地,下一次进来依旧数据一样(也可以使用后端保存)
const size = data.map((item) => item.size);
sizeMap.value = size;
localStorage.setItem("sizeMap", JSON.stringify(size));
};
</script>

到这里我们就还差最后一步了,点击展示popup,这里我使用tdesignpopup来写

  1. 安装依赖
    1
    npm i tdesign-vue-next
  2. 导入
    在main.js入口文件中
    1
    2
    3
    import { Popup as TPopup } from 'tdesign-vue-next';
    import 'tdesign-vue-next/es/style/index.css';
    createApp(App).use(TPopup).mount('#app');

接下来我们实现点击事件,单击将参数传递过去,加异步延迟避免参数变化导致的动画突兀,activeTitle用来决定激活要素是谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const getPopupVisible = (title) => {
return activeTitle.value === title;
};

const handlePopupVisChange = (visible) => {
if (!visible) {
activeTitle.value = "";
}
};
const activeTitle = ref("");
const detailData = ref({
createTime: "", // 入职时间
finishCount: 0, // 当天完成单量
unfinishCount: 0, // 手里还剩下多少单
companyOrder: {}, // 客户单量情况
title: "", // 用户名
workStatus: "", // 工作状态
unfinishKnowCount: "", // 已知品名
unfinishUnknowCount: 0, // 未知品名
recordTypeName: "", // 工作类型
});
let time = null;
const handleShowDetail = (item) => {
clearTimeout(time);
const { title, value } = item;
activeTitle.value = title;

time = setTimeout(() => {
detailData.value = {
title,
recordTypeName: value.info.recordTypeName,
finishCount: value.finish?.count ?? 0,
unfinishCount: value.unfinish?.count ?? 0,
createTime: value.info?.createTime ?? "未知",
companyOrder: value.unfinish?.company ?? {},
workStatus: value.info?.onLineTypeName ?? "",
unfinishUnknowCount: value.unknowCounts || 0,
unfinishKnowCount: `${
(value?.unfinish?.count ?? 0) - (value.unknowCounts || 0)
}${value.unfinish?.goodsNum || 0}品`,
};
}, 200);
};

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<div v-for="cItem in item.data" :key="cItem" class="data-item">
<t-popup
placement="right"
:visible="getPopupVisible(cItem.title)"
@onVisibleChange="handlePopupVisChange"
>
<div
class="line"
@click.stop="handleShowDetail(cItem)"
:class="getPopupVisible(cItem.title) ? 'active' : ''"
>
{{ handleStr(cItem) }}
</div>
<template #content>
<div class="card">
<div class="card-header">
<div class="header-line">
<div class="card-title">
{{ detailData.title }}
</div>
<div class="record-type">
{{ detailData.recordTypeName }}
</div>
</div>
<div class="margin">
入职时间:{{ detailData.createTime || "暂无" }}
</div>
<div class="margin">
今日完成单量:{{ detailData.finishCount }}
</div>
<div class="margin">
当前未完成单量:{{ detailData.unfinishCount }}
</div>
<div class="margin">
已知品名:{{ detailData.unfinishKnowCount }}
</div>
<div v-if="detailData.unfinishUnknowCount" class="margin">
未知品名:{{ detailData.unfinishUnknowCount }}
</div>
<template
v-if="Object.keys(detailData.companyOrder).length"
>
<t-divider style="margin: 12px 0">
<template #content>
<div class="card-subTitle">以下为未完成明细</div>
</template>
</t-divider>
<div
v-for="(key, value) in detailData.companyOrder"
:key="key"
class='margin'
>
<div class="card-subTitle">{{ value }}</div>
<div style="margin-top: 5px">
已知品名票数:{{ key.allCount }}
</div>
<div style="margin-top: 5px">
总品名数量:{{ key.goodsNum }}
</div>
<div v-if="key.unknowCount" style="margin-top: 5px">
未知品名票数:{{ key.unknowCount }}
</div>
<t-divider style="margin: 12px 0"></t-divider>
</div>
</template>
</div>
</div>
</template>
</t-popup>
</div>

css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
.t-popup__content {
padding: 0 !important;
}
.data-item {
line-height: 24px;
font-size: 16px;
padding: 10px 5px;
white-space: nowrap;
color: #333;
border-radius: 4px;
cursor: pointer;
}

.line {
line-height: 24px;
font-size: 16px;
padding: 10px 5px;
white-space: nowrap;
color: #333;
border-radius: 4px;
}

.card {
width: 245px;
max-height: calc(100vh - 300px);
min-height: 180px;
overflow: auto;
}

.card-title {
font-size: 16px;
font-weight: bold;
}

.card-subTitle {
font-size: 14px;
font-weight: bold;
}

.active {
background: #b5c7ff;
}

.card-header {
background-image: linear-gradient(to bottom, #dce8ff, #fff);
padding-left: 23px;
padding-top: 18px;
padding-right: 11px;
}

.header-line {
display: flex;
justify-content: space-between;
height: 20px;
padding-bottom: 10px;
}

.record-type {
display: flex;
align-items: center;
border: 1px solid #136bf9;
color: #136bf9;
height: 16px;
padding: 2px;
border-radius: 2px;
}

.margin {
margin-top: 5px;
}

然后我们需要在在点击其他地方时关闭这个这个popup,在html的父级元素上添加activeTitle清空

1
<splitpanes class="container" @click="() => (activeTitle = '')">

效果图

到这里,我们已经将功能完成了,但是这个html,我们一看就比较冗余

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="margin">
入职时间:{{ detailData.createTime || "暂无" }}
</div>
<div class="margin">
今日完成单量:{{ detailData.finishCount }}
</div>
<div class="margin">
当前未完成单量:{{ detailData.unfinishCount }}
</div>
<div class="margin">
已知品名:{{ detailData.unfinishKnowCount }}
</div>
<div v-if="detailData.unfinishUnknowCount" class="margin">
未知品名:{{ detailData.unfinishUnknowCount }}
</div>
....
<div style="margin-top: 5px">
已知品名票数:{{ key.allCount }}
</div>
<div style="margin-top: 5px">
总品名数量:{{ key.goodsNum }}
</div>
<div v-if="key.unknowCount" style="margin-top: 5px">
未知品名票数:{{ key.unknowCount }}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const personData=[
{
label:"入职时间",
value:"createTime",
emptyContent:"暂无"
},
{
label:"今日完成单量",
value:"finishCount",
},
{
label:"今日未完成单量",
value:"unfinishCount"
},
{
label:"已知品名",
value:"unfinishKnowCount"
},
{
label:"未知品名",
value:"unfinishUnknowCount",
check:true
}
]

const companyData=[
{
label:"已知品名票数",
value:"allCount",
},
{
label:"总品名票数",
value:"goodsNum"
},
{
label:"未知品名票数",
value:"unknowCount",
check:true
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div
class="margin"
v-for="pItem in personData"
:key="pItem.value"
>
<span v-if="!pItem.check || detailData[pItem.value]"
>{{ pItem.label }}:{{
detailData[pItem.value] || pItem.emptyContent || 0
}}
</span>
</div>

<div
class="margin"
v-for="cItem in companyData"
:key="item.value"
>
<span v-if="!cItem.check || key[cItem.value]">
{{ cItem.label }}:{{ key[cItem.value] }}
</span>
</div>

这样就把这个demo完成了,这里附上源码地址https://gitee.com/guJyang/man-manage-demo.git

上一篇:
组件记录-全屏
下一篇:
vue自定义指令 回车进入下一格