javascript 上傳表單 (input type=file)

javascript 上傳表單 ;FileReader 表單上傳,檔案資料表示為 base64(單個)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const previewFile = () =>{
const preview = document.querySelector("img");
const mask = document.querySelector(".mask");
const file = document.querySelector("input[type=file]").files[0];
const reader = new FileReader();
//
reader.addEventListener(
"load",
() => {
// 將圖片檔案轉換為 Base64 字串
preview.src = reader.result;
preview.classList.add("active");
mask.classList.add("maskActive");
},
false,
);

if (file) {
console.log(file)
reader.readAsDataURL(file);
}
}

表單上傳多個文件

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
const previewFiles = () => {
const preview = document.querySelector("#preview");
const files = document.querySelector("input[type=file]").files;

const readAndPreview = (file) => {
//確保“file.name”符合我們的擴充標準
if (/\.(jpe?g|png|gif)$/i.test(file.name) ) {
const reader = new FileReader();

reader.addEventListener("load",() => {
//Image()
const image = new Image();
image.height = 100;
image.title = file.name;
image.src = reader.result;
preview.appendChild(image);
},false);

reader.readAsDataURL(file);
}
}

if (files) {
Array.prototype.forEach.call(files, readAndPreview);
}
}

const picker = document.querySelector("#browse");
picker.addEventListener("change", previewFiles);

參考資料

css Hover箭頭轉向

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<div class="one" >
<span><a href="#"></a></span>
</div>

<div class="two" >
<a href="#"></a>
</div>

<style lang="scss">
body{
padding:0px;
margin:50px;
}
.one{
margin:0;
padding:0;
span{
height:50px;
text-align:center;
a{
border:3px solid red;
width:35px;/*箭頭大小*/
height:35px;/*箭頭大小*/
border-width:3px 0 0 3px;
display:inline-block;
-moz-transform-origin:center center;
-moz-transform:rotate(135deg);
-moz-transition:all .3s ease-in .1s;
-webkit-transform-origin:center center;
-webkit-transform:rotate(135deg);
-webkit-transition:all .3s ease-in .1s;
-o-transform-origin:center center;
-o-transform:rotate(135deg);
-o-transition:all .3s ease-in .1s;
-ms-transform-origin:center center;
-ms-transform:rotate(135deg);
-ms-transition:all .3s ease-in .1s;
transform-origin:center center;
transform:rotate(135deg);
transition:all .3s ease-in .1s;
&:hover{
-moz-transform:rotate(225deg);
-moz-transition:all .3s ease-in .1s;
-webkit-transform:rotate(225deg);
-webkit-transition:all .3s ease-in .1s;
-o-transform:rotate(225deg);
-o-transition:all .3s ease-in .1s;
-ms-transform:rotate(225deg);
-ms-transition:all .3s ease-in .1s;
transform:rotate(225deg);
transition:all .3s ease-in .1s;
}/*a:hover*/
}/*a*/
}/*span*/
}/*one*/

.two{
margin-top:50px;
a {
width: 0;
height: 0;
border-right: 30px solid transparent;
border-bottom: 30px solid blue;
border-left: 30px solid transparent;
display:inline-block;
-moz-transform-origin:center center;
-moz-transform:rotate(90deg);
-moz-transition:all .3s ease-in .1s;
-webkit-transform-origin:center center;
-webkit-transform:rotate(90deg);
-webkit-transition:all .3s ease-in .1s;
-o-transform-origin:center center;
-o-transform:rotate(90deg);
-o-transition:all .3s ease-in .1s;
-ms-transform-origin:center center;
-ms-transform:rotate(90deg);
-ms-transition:all .3s ease-in .1s;
transform-origin:center center;
transform:rotate(90deg);
transition:all .3s ease-in .1s;
&:hover {
-moz-transform:rotate(180deg);
-moz-transition:all .3s ease-in .1s;
-webkit-transform:rotate(180deg);
-webkit-transition:all .3s ease-in .1s;
-o-transform:rotate(180deg);
-o-transition:all .3s ease-in .1s;
-ms-transform:rotate(180deg);
-ms-transition:all .3s ease-in .1s;
transform:rotate(180deg);
transition:all .3s ease-in .1s;
}
}
}

</style>

js 點擊新增時間

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
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
<button class="addBtn">Add</button>
<h1 class="countdownTitle">點擊新增分鐘</h1>
<!-- 顯示預計回來的時間 -->
<p class="newTimeStemp"></p>

<script>
//綁定標題
const countdownClockTitle = document.querySelector(".countdownTitle");
//顯示預計回來的時間
const newTimeStemp = document.querySelector(".newTimeStemp");
//綁定點擊按鈕
const addBtn = document.querySelector(".addBtn");
//定義countdown
let countdown;
// 時間的換算
const showTimer = (inputSeconds) => {
const minutes = Math.floor(inputSeconds / 60);//除以60取整數
const remainderSeconds = inputSeconds % 60;//除以60取余數
//塞進去畫面
countdownClockTitle.textContent = `
${minutes}:${remainderSeconds < 10
? "0" + remainderSeconds
: remainderSeconds}
`;
}
//顯示時間
const showTimeStemp = (done) => {
const newTime = new Date(done);
const hours = newTime.getHours();
const minutes = newTime.getMinutes();
newTimeStemp.textContent = `
點擊時間 ${hours}:${minutes}
`
}
const timer =(inputSeconds) => {
console.log('timer()',inputSeconds)
const now = Date.now();
const done = now + inputSeconds * 1000;
showTimer(inputSeconds);
showTimeStemp(done);
// 倒數計時
countdown = setInterval(()=>{
const tiemleft = Math.round((done-Date.now())/ 1000);
if(tiemleft<= 0){
clearInterval(countdown);
return
}
showTimer(tiemleft)
}, 1000)
}

const handleAddBtn = ()=> {
// 清除倒數計時
clearInterval(countdown); // clear countdown
// 綁定表單
const timerInput = Number(1800);
// timerInput.value=1800;
if(timerInput > 0){
// console.log(timerInput.value)
timer(timerInput)
}

}
addBtn.addEventListener("click", handleAddBtn)
</script>
### js setInterval 的應用

js 表單

表單選擇與表單全選checkbox checked && checkbox All checked

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
js
// 選單選擇數據
const radioLists=[
'Html','Css','JavaScript','Node','Vue'
]
// 渲染到選擇表單
const radioGroup = document.querySelector('.radio_group');
let arr = "";
radioLists.forEach((item) => {
//加法赋值运算符
arr += `<label class="radioBox" for="${item}" name="fav_language" >${item}<input type="checkbox" value="${item}" id="${item}"><span class="square-mark"></span><label>`;
})
radioGroup.innerHTML = arr;

// 綁定可選擇表單
const checkboxLists =document.querySelectorAll('.radioBox input');
// 綁定全選表單Class selectAll
const selectAll= document.querySelector('.selectAll');

//監控點擊全選表單
selectAll.addEventListener('change', function () {
// 綁定所有複選表單
checkboxLists.forEach(function (item,index) {
item.checked = this.checked;
}, this)
// 原來的選擇數據
const originalData =[
'Html','Css','JavaScript','Node','Vue'
]

//***提示 =>提示 全選變不選時 數據要清空
selectAll.checked ? selectObject =originalData : selectObject=[];
innerHtmlUl(selectObject)
})

//這裡選擇的數據必須全局
var selectObject =[];
//選擇項目或是不選項目時的數據必須通過這個函式來執行
function innerHtmlUl (selectObject){
const ulContent = document.querySelector('ul');
let arry = "";
selectObject.forEach((x) => {
//加法赋值运算符
arry += `<li >${x}<li>`;
})
ulContent.innerHTML = arry ;
}

// 遍歷 checkboxLists
for (let i = 0; i < checkboxLists.length; i++) {
//監控單個 checkboxLists[i]
checkboxLists[i].addEventListener( "change",function (){
if(checkboxLists[i].checked){
selectObject.push(checkboxLists[i].value);
if(selectObject.length>=checkboxLists.length) {
selectObject =radioLists
}
innerHtmlUl(selectObject)
}else{
selectObject.splice(checkboxLists[i].value,1);
innerHtmlUl(selectObject)
}
selectObject.length==checkboxLists.length ? selectAll.checked=true:selectAll.checked=false;
})
}

Vite Vue 麵包屑拖曳(語法糖)

Vite Vue 麵包屑拖曳(語法糖)

  • 點擊左邊菜單項目新增到麵包屑:陣列新增項目 => 使用 push()
  • 過濾掉相同的菜單項目並顯示在麵包屑:
    1.定義一個空陣列
    2.點擊菜單按鈕=>如果if從localStorage.getItem回來的數據不為null=> 原來的陣列等於localStorage.get回來的數據 ;無論localStorage.getItem回來的數據是否是null;都必須新增項目並過濾Fotmat (forEach遍歷 如果 空陣列與原陣列沒有發現相同那就新增push項目並且將數據localStorage.setItem)
  • 刪除麵包屑項目:獲取JSON.parse(localStorage.getItem('horizontalFilter')as string) =>如果 length 大於 0 =>刪除splice(id,1) 並且在setItem到localStorage 並且回調 在渲染的陣列get回來畫面上
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<template>
<div class="w-fill admin">
<div class="side left">
<div class="tabs" v-for="(item) in sideLists" :key="item.id">
<div class="tab">
<input type="radio" :id="item.id" name="rd">
<label class="tab-label hvr-underline-from-right" :for="item.id">
<i :class="item.icon"></i>
{{ item.title }}
</label>

<div class="tab-content">
<ul v-if="item.children">
<li class="hvr-underline-from-right" v-for="(i,id) in item.children"
:key="id"
:class="active === i.id ? 'isActive' : ''"
@click="sendClickActive(i)">
<router-link :to="i.href"> {{ i.title }}</router-link>
</li>
</ul>
</div>
</li>
</div>
</div>
</div><!---/side-->
<div class="rightContent">
<!--麵包屑--->
<div class="horizontal_menu">
<!--打開就看到首頁按鈕-->
<ul class="horizontalNav home">
<li v-for="item in homeItem" :key="item.title">
<router-link :to="item.href">{{ item.title }} </router-link>
</li>
</ul>

<ul class="horizontalNav">
<li v-for="item in horizontalfilter" :key="item.title">
<router-link :to="item.href">{{ item.title }} </router-link>
</li>
</ul>
</div>

<router-view />
</div><!---/rightContent-->
</div>
</template>

<script setup lang="ts">
import { ref, computed ,onMounted} from "vue"
import { useRouter } from 'vue-router'

interface horizontalNavType{
id:string,
title:string |any ,
href:string | any,
}
// 麵包屑陣列
const horizontal=ref<horizontalNavType[]>([]);
// 過濾的麵包屑陣列
const horizontalfilter=ref<horizontalNavType[]>([]);
// ***過濾掉重複函式
const horizontalfilterFormat =(horizontal:any) =>{
let filteredItems = horizontal;
const getResult: any[] = [];
filteredItems.forEach((item:any) => {
if (!getResult.find((r: { title: string; })=> r.title === item.title)) {
getResult.push(item);
localStorage.setItem('horizontalFilter',JSON.stringify( getResult)) ;
}
})
}
// 點擊左邊菜單新增到使用的麵包屑
const clickActiveSide = (i: horizontalNavType) => {
activeSide.value = i.id;
try{
let query: any={
id:i.id,
title:i.title,
href:i.href
}
if(JSON.parse(localStorage.getItem('horizontalFilter')as string)!=null){
let get =JSON.parse(localStorage.getItem('horizontalFilter')as string);
horizontal.value=get;
}
horizontal.value.push(query);
horizontalfilterFormat(horizontal.value)
horizontalfilter.value= JSON.parse(localStorage.getItem('horizontalFilter')as string);
}
catch(err){
console.log(err)
}
}
// 刪除麵包屑項目
const deleteHorizontalNav =(title:any |string)=>{
const lists =JSON.parse(localStorage.getItem('horizontalFilter')as string);
if(lists.length>0 ){
lists.splice(title,1);
localStorage.setItem('horizontalFilter', JSON.stringify(lists)) ; horizontalfilter.value= JSON.parse(localStorage.getItem('horizontalFilter')as string);
}else{
router.push({ path: '/admin'})
}
}
const activeSide = ref<string>('')
const isActiveSide = ref<boolean>(false);
interface sideListType{
id:string,
title:string,
icon:string,
children:object,
}
//左手邊菜單
const sideLists=ref<sideListType[]>([
{
id:'buildSet',
title: "建檔設定",
icon:"fa-solid fa-address-book",
children: [
{id:'buildSet1-1',title: "工程資料建檔",href:"/one"}
{id:'buildSet1-2',title: "供料資料建檔",href:"/two"}
{id:'buildSet1-3',title: "供料資料建檔",href:"/three"}
{id:'buildSet1-4',title: "供料資料建檔",href:"/four"}
]
},
{
id:'dataSearch',
title: "資料搜尋",
icon:"fa-solid fa-address-book",
children: [
{id:'dataSearch1-1',title: "業主資料查詢",href:"/one"}
{id:'dataSearch1-2',title: "工程資料查詢",href:"/two"}
{id:'dataSearch1-3',title: "採購合約查詢",href:"/three"}

]
}
])
//首頁麵包屑定義
const homeItem =ref<any[]>([]);
// 打開時 讓選單可以出現首頁
const homeBtn=()=>{
const query={
id:'home',
title:'首頁',
href:'/admin',
}
homeItem.value.push(query);
}
onMounted(() => {
homeBtn();
localStorage.removeItem('horizontalFilter')
});
</script>



<style lang="scss" >
//左邊菜單
.admin {
display: flex;
width:100%;
//Side
.side{
border-right: 2px solid #c6e2ff;
background-color: #c6e2ff;
flex-direction: column;
max-width: 240px;
min-height: 100vh;
overflow-y: calc(100vh - 0px);
padding-top: 25px;
width: 240px;
ul {
list-style-type:none;
margin-block-start: 0em;
margin-block-end: 0em;
padding-inline-start: 35px;
}
.tabs {
overflow: hidden;
.tab{
color: #333;
overflow: hidden;
width: 100%;
input {
display: none;
&:checked{
~ .tab-content {
max-height: 100vh;
padding: .2em 0px .2em 1em;
}
}
}
.tab-label {
cursor: pointer;
display: flex;
justify-content: space-around;
padding: 0.6em;
font-size: 16px;
i {
color: #3F51B5;
font-size: 20px;
}
&:after {
content: "❯";
width: 1em;
height: 1em;
text-align: center;
transition: all 0.35s;
}
}
.tab-content{
max-height: 0;
padding: 0 1em;
color: #2c3e50;
transition: all 0.35s;
}
}
}
}

}


.rightContent{
display: flex;
flex-direction: column;
width: calc(100% - 240px);
max-width: 100%;

.horizontal_menu{
display: flex;
.horizontalNav{
display: flex;
list-style-type: none;
margin-block-start: 0em;
margin-block-end: 0em;
padding: 5px 0px;
li{
display: flex;
list-style-type: none;
margin: 0% 4px;
a{
text-decoration: none;
background-color: #409eff;
color: white;
padding: 8px 18px;
border-radius: 8px 8px;
-webkit-border-radius: 8px 8px;
i{
color: white;
&:hover{
color#ddd;
}
}
}
}
}
}
}
<style>

特別講解過濾重複陣列

  • forEach() js 遍歷
  • find() js 傳回通過測試的陣列中第一個元素的值)
  • push js 新增
  • localStorage.setItem為陣列時必須使用JSON.stringify()
  • localStorage.getItem為陣列時必須使用JSON.parse(localStorage.getItem('horizontalFilter')as string)
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const horizontalfilterFormat =(horizontal:any) =>{
    let filteredItems = horizontal;
    const getResult: any[] = [];
    filteredItems.forEach((item:any) => {
    if (!getResult.find((r: { title: string; })=> r.title === item.title)) {
    getResult.push(item);
    localStorage.setItem('horizontalFilter',JSON.stringify( getResult)) ;
    }
    })
    }

    gitHib

    增加可以拖動功能

    安裝swiperjs

    1
    2
    3
    ```
    npm install swiper --save

    檔案node_modules/swiper/swiper-bundle.css另存到@/assets/swiper-bundle.css 並且到main.ts全局加入

    1
    import '@/assets/css/swiper-bundle.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
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    <template>
    <div class="horizontal_menu">
    <ul class="horizontalNav">
    <li v-for="item in homeItem" :key="item.title">
    <router-link :to="item.href">{{ item.title }} </router-link>
    </li>
    </ul>
    <swiper
    :modules="modules"
    :slides-per-view="3"
    :space-between="10"
    :loop="false"
    :autoplay="false"
    @swiper="onSwiper"
    @slideChange="onSlideChange"
    >
    <swiper-slide v-for="item in horizontalfilter" :key="item.title">
    <a :href="item.href">{{ item.title }}
    <i class="icon-cross"
    @click.prevent="deleteHorizontalNav(item.title)">
    </i>
    </a>
    </swiper-slide>
    </swiper>
    </div>
    </template>


    <script setup lang="ts">
    import { ref, computed ,onMounted} from "vue"
    import { useRouter } from 'vue-router'
    import { homeMenuType } from "@/types/navMenuType"
    import { Swiper, SwiperSlide } from 'swiper/vue'//引入
    import { Autoplay, Pagination, Navigation, Scrollbar }from 'swiper/modules'//解構取出

    interface horizontalNavType{
    id:string,
    title:string |any ,
    href:string | any,
    }
    const horizontal=ref<horizontalNavType[]>([]);
    const horizontalfilter=ref<horizontalNavType[]>([]);
    //過濾掉重複
    const horizontalfilterFormat =(horizontal:any) =>{
    let filteredItems = horizontal;
    const getResult: any[] = [];
    filteredItems.forEach((item:any) => {
    if (!getResult.find((r: { title: string; })=> r.title === item.title)) {
    getResult.push(item);
    localStorage.setItem('horizontalFilter',JSON.stringify( getResult)) ;
    }
    })
    }
    // Swiper定義
    const modules = [Autoplay, Pagination, Navigation, Scrollbar]
    const onSwiper = (swiper:any) => {
    console.log(swiper);
    };
    const onSlideChange = () => {
    console.log('slide change');
    };

    // 點擊左邊菜單新增到使用頁眉
    const clickActiveSide = (i: horizontalNavType) => {
    activeSide.value = i.id;
    try{
    let query: any={
    id:i.id,
    title:i.title,
    href:i.href
    }
    if(JSON.parse(localStorage.getItem('horizontalFilter')as string)!=null){
    let get =JSON.parse(localStorage.getItem('horizontalFilter')as string);
    horizontal.value=get;
    }
    horizontal.value.push(query);
    horizontalfilterFormat(horizontal.value)
    horizontalfilter.value= JSON.parse(localStorage.getItem('horizontalFilter')as string);
    }
    catch(err){
    console.log(err)
    }
    }

    const deleteHorizontalNav =(title:any |string)=>{
    const lists =JSON.parse(localStorage.getItem('horizontalFilter')as string);
    if(lists.length>0 ){
    lists.splice(title,1);
    localStorage.setItem('horizontalFilter', JSON.stringify(lists)) ; horizontalfilter.value= JSON.parse(localStorage.getItem('horizontalFilter')as string);
    }else{
    router.push({ path: '/admin'})
    }
    }
    const homeItem =ref<any[]>([])
    // 打開時 讓選單可以出現首頁
    const homeBtn=()=>{
    const query={
    id:'home',
    title:'首頁',
    href:'/admin',
    }
    homeItem.value.push(query);
    }
    onMounted(() => {
    homeBtn();
    localStorage.removeItem('horizontalFilter')
    });
    </script>



    <style lang="scss">
    .horizontal_menu{
    display: flex;
    justify-content: flex-start;
    .horizontalNav{
    display: flex;
    padding-inline-start: 0px;
    width: 55px;
    li{
    margin: auto 8px;
    }
    a{
    width: 80px;
    display: block;
    text-align: center;
    line-height: 38px;
    background-color: cornflowerblue;
    color:white;
    padding: 8px 14px;
    height: 38px;
    border-radius: 8px;
    }
    }
    .swiper{
    margin-left: 15px;
    overflow: visible;
    display: flex;
    align-items: center;
    &.swiper-horizontal{
    text-align: left;
    margin-left: 45px;
    }
    .swiper-wrapper{
    display: flex;
    justify-content: flex-start;
    overflow: visible;
    align-items: center;
    .swiper-slide{
    width: auto!important;background-color: cornflowerblue;
    color: white;
    overflow: visible;
    display: flex;
    align-items: center;
    padding-left: 8px;
    padding-right: 8px;
    border-radius: 7px;
    height: 38px;
    a{
    color: white;
    display: flex;
    align-items: center;
    .icon-cross{
    margin-left: 5px;
    }
    }
    }
    }
    }
    </style>

    gitHub 麵包屑拖曳

    js相同陣列過濾去除

    css手風琴菜單

    Vite 上傳照片 使用fileReader

    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
    <template>
    <el-form
    ref="ruleFormRef"
    :model="ruleForm"
    :rules="rules"
    class="add_person_set_form">
    <div class="col_4">
    <el-form-item class="avatar_input" prop="photo1">
    <div class="pic_img" v-show="file!=''">
    <img :src="file" />
    </div>
    <div class="file_discription">
    {{fileContent}}
    </div>
    <label for="imgFile" class="file-upload">
    <i class="fa fa-cloud-upload"></i>
    上傳照片
    <input
    class="form-control"
    type="file" id="imgFile" accept=".jpg,.jpeg,.png"
    @change="fileChangehandler" />
    </label>
    </el-form-item>
    </div>
    </el-form>
    </template>

    <script setup lang="ts">
    import { ref,reactive, onMounted,computed } from 'vue'
    import type { FormInstance, FormRules } from 'element-plus'
    const ruleFormRef = ref<FormInstance>()
    // 表單
    const ruleForm=ref<any>({
    photo1: "",//照片})

    //檔案路徑
    const file =ref<string>('');
    //檔案名稱
    const fileContent=ref<string>('');
    const fileChangehandler =(e:any)=>{
    console.log(e.target)
    //建立 fileReader 物件
    const reader = new FileReader
    //readAsDataURL 方法: 開始讀取指定的 Blob,讀取完成後屬性 result 將以 data: URL 格式(base64 編碼)的字串來表示讀入的資料內容。
    reader.readAsDataURL(e.target.files[0])
    //onload 事件: 於讀取完成時觸發。
    reader.onload = () => {
    //reader.result 為檔案的內容
    file.value = reader.result;
    console.log('file.value:', file.value)
    fileContent.value=e.target.files[0].name;
    }
    reader.onerror = (error) => {
    console.log('Error:', error)
    }
    }

    =>建立 fileReader 物件
    =>readAsDataURL 方法: 開始讀取指定的 Blob,讀取完成後屬性 result 將以 data: URL 格式(base64 編碼)的字串來表示讀入的資料內容。
    =>onload 事件: 於讀取完成時觸發。
    =>onerror 錯誤訊息
    參考資料

    reduce()

    reduce()

    reduce() 方法將一個累加器及陣列中每項元素(由左至右)傳入回呼函式,將陣列化為單一值。

    1
    arr.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)

    callback:用於處理陣列中每個元素的函式,可傳入四個參數

    • accumulator用來累積回呼函式回傳值的累加器(accumulator)或 initialValue(若有提供的話,詳如下敘)。累加器是上一次呼叫後,所回傳的累加數值。
    • currentValue原陣列目前所迭代處理中的元素。
    • currentIndex 選擇性原陣列目前所迭代處理中的元素之索引。若有傳入 initialValue,則由索引 0 之元素開始,若無則自索引 1 之元素開始。
    • array 選擇性呼叫 reduce() 方法的陣列。
    • initialValue 選擇性於第一次呼叫 callback 時要傳入的累加器初始值。若沒有提供初始值,則原陣列的第一個元素將會被當作初始的累加器。假如於一個空陣列呼叫 reduce() 方法且沒有提供累加器初始值,將會發生錯誤。
    回傳值
    reduce js 累加總和
    reduce 計算相同元素數量並物件取出值轉為陣列
    這裡用到

    js 遇到的問題

    檢查某元素是否在陣列中 indexOf

    -1=>沒有找到
    2=>索引2 =>找到的元素

    1
    2
    3
    4
    5
    const myHeroes = ['蝙蝠俠','蜘蛛人','水行俠','黑寡婦'];
    console.log('檢查蟻人是否在陣列中',myHeroes.indexOf('蟻人'));
    console.log('檢查水行俠是否在陣列中',myHeroes.indexOf('水行俠'));
    //檢查蟻人是否在陣列中 -1
    //檢查水行俠是否在陣列中 2

    合併陣列 javascript(ES6)擴展運算子 Spread Operator 或是 concat ()

    1
    2
    3
    4
    5
    6
    7
    8
    var number1=[1,2,3];
    var number2=[4,5,6];
    let newnumber = [...number1,...number2];
    console.log(newnumber)
    //(6) [1, 2, 3, 4, 5, 6]
    const array3 = number1.concat(number2);
    console.log(array3)
    //(6) [1, 2, 3, 4, 5, 6]

    字符串轉字符數組(分割字符串) javascript(ES6)擴展運算子 Spread Operator 或是 split(‘’)

    1
    2
    3
    4
    5
    6
    7
    var one='Hello Word';
    var array1 =[...one]
    console.log('array1',array1) ;
    //(10) ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'd']
    const split=one.split('')
    console.log('array1 split',split) ;
    //(10) ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'd']

    去掉重複的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //使用filter 與indexOf
    let arr = ['B', 'A', 'E', 'C', 'A', 'F', 'G', 'E'];
    let newArr = arr.filter( (element, index, arry)=> {
    return arry.indexOf(element) === index;
    });
    console.log('使用filter 與indexOf',newArr)
    //(6) ['B', 'A', 'E', 'C', 'F', 'G']

    //使用Array.from() 與new Set()
    const uniqueArray = Array.from(new Set(arr));
    console.log('使用Array.from() 與new Set()',uniqueArray)
    //(6) ['B', 'A', 'E', 'C', 'F', 'G']

    //使用...new Set()
    const uniqueArr = [...new Set(arr)];
    console.log('使用...new Set()',uniqueArr)
    //(6) ['B', 'A', 'E', 'C', 'F', 'G']

    //Array.from()與 map遍歷key()
    const uniqueArrMap = Array.from(new Map(arr.map((item) => [item, 1])).keys());
    console.log('uniqueArrMap',uniqueArrMap)

    物件轉陣列

    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
    //物件形式
    const roles = {
    '001': {
    name: '海綿寶寶',
    expert: '吸收很快',
    },
    '002': {
    name: '耶夢',
    expert: '任意門',
    },
    '004': {
    name: '超人',
    expert: '英雄',
    },
    };

    //使用Object.values與forEach()
    let str = [];
    str=Object.values(roles).forEach((item) => {
    console.log(item);
    })
    console.log(str)

    //使用Object.keys與forEach()
    let object = [];
    object=Object.keys(roles).forEach((key) => {
    console.log(roles[key]);
    })
    console.log(object)

    let objectEntries = [];
    Object.entries(roles).forEach((obj) => {
    console.log(obj[0], obj[1]);
    })
    console.log(objectEntries)

    Vite Tailwind 安裝

    建立專案

    一安裝 Tailwind

    1
    npm install -D tailwindcss postcss autoprefixer

    二建立的是 tailwind 的設定檔

    1
    npx tailwindcss init -p

    建立完成後會發現專案根目錄多出了 tailwind.config,jspostcss.config.js這兩個檔案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //tailwind.config.js
    /** @type {import('tailwindcss').Config} */
    export default {
    content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
    ],
    theme: {
    extend: {},
    },
    plugins: [],
    }

    將 Tailwind 指令加入您的 CSS 中

    在src資料夾內新增style.css

    1
    2
    3
    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    安裝了 autoprefixer以後style 會自動產生
    從main.ts引入style.css(Vite Vue 專案)

    1
    2
    3
    4
    5
    6
    7
    import { createApp } from 'vue'
    import App from './App.vue'
    import './style.css' //引入 tailwindcss

    const app = createApp(App);

    app.mount("#app");

    從entry-client.ts引入style.css

    在App.vue測試

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       <h1 class="text-3xl font-bold underline">
    Hello world!
    </h1>
    <div class="p-6 max-w-sm mx-auto rounded-xl shadow-lg flex items-center space-x-4 bg-slate-900 p-8 pt-7">

    <div>
    <div class="text-xl font-medium text-white">ChitChat</div>
    <p class="text-white">You have a new message!</p>
    </div>
    </div>

    參考官網

    使用 Vite 安裝 Tailwind CSS

    Vite出勤系統

    最近開是在做EIP 系統,因此出勤系統就是要思考邏輯性的呈現

    出勤打卡邏輯

    打開打卡頁面(獲取出勤設定定位地圖與定位座標)『如果已經在打卡範圍』,出現google Map 地圖 =>獲取當日過濾後的上班/外勤上班打卡列表(包含遲到:『如果已經打過卡了』或獲取當日過濾後的下班/外勤下班打卡列表(包含早退:『如果已經打過卡了』,『點擊』下班打卡或外勤下班打卡:alert 已經打過卡了),如果沒有打過上班卡/出勤上班卡 =>得到 『上班正常』或『外勤上班正常』或『上班遲到』或『外勤上班遲到』

    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
    <template>
    <ul class="set_work_check">
    <li v-for="(item,id) in setCheckModesMainMenu" :key="id">
    <router-link
    :to="item.href"
    :class="{activePage: item.title===pageName}"> {{item.title}}</router-link>
    </li>
    </ul>
    <div class="setInfo">
    <div>{{office}}</div>
    <div>上班時間:{{flexibleTime}}</div>
    <div>距離:{{distance}}公尺</div>
    </div>
    <ul class="work_check">
    <li>
    <div class="work_check_item">
    <a @click="checkOnWork" v-show="showOnWorkCheck" >上班 </a>
    <div class="already_check" v-for="(item,workCheck_Id) in currentDayonWorkLists" :key="workCheck_Id">
    <a :class="{activeCheck: item.onWorkStatus,isLate:item.title==='上班遲到'}" v-show="item.onWorkStatus">上班</a>
    <div class="check_info" v-show="item.onWorkStatus">
    <div class="text" :class="{isFoul:isLate,isQualified:!isLate}">{{ item.title }}</div>
    <div class="text">{{onWorkCheckTime}}</div>
    <div class="text">經度:{{item.lat}}</div>
    <div class="text">緯度:{{item.lng}}</div>
    </div>
    </div>
    </div>
    <div class="work_check_item">
    <a v-show="showOffWorkCheck" @click="checkOffWork">下班</a>
    <div class="already_check" v-for="(item,workCheck_Id) in currentDayoffWorkLists" :key="workCheck_Id">
    <a :class="{activeCheck: item.offWorkStatus,isLate:item.title==='下班早退'}" v-show="item.offWorkStatus">下班</a>
    <div class="check_info" v-show="item.offWorkStatus">
    <div class="text" :class="{isFoul:isLeaveEarly,isQualified:!isLeaveEarly}">{{ item.title }}</div>
    <div class="text">{{offWorkCheckTime}}</div>
    <div class="text">經度:{{item.lat}}</div>
    <div class="text">緯度:{{item.lng}}</div>
    </div>
    </div>
    </div>
    </li>
    <li>
    <div class="work_check_item">
    <a v-show="showOnBusinessWorkCheck" @click="checkOnBusinessWork">外勤上班</a>
    <div class="already_check" v-for="(item,workCheck_Id) in currentDayonBusinessworkLists" :key="workCheck_Id">
    <a :class="{activeCheck: item.onWorkStatus,isLate:item.title==='外勤上班遲到'}" v-show="item.onWorkStatus">外勤上班</a>
    <div class="check_info" v-show="item.onWorkStatus">
    <div class="text" :class="{isFoul:isLate,isQualified:!isLate}">{{ item.title }}</div>
    <div class="text">{{onBusinessWorkTime}}</div>
    <div class="text">經度:{{item.lat}}</div>
    <div class="text">緯度:{{item.lng}}</div>
    </div>
    </div>
    </div>
    <div class="work_check_item">
    <a v-show="showOffBusinessWorkCheck" @click="checkOffBusinessWork">外勤下班</a>
    <div class="already_check" v-for="(item,workCheck_Id) in currentDayoffBusinessworkLists" :key="workCheck_Id">
    <a :class="{activeCheck: item.offWorkStatus,isLate:item.title==='外勤下班早退'}" v-show="item.offWorkStatus">外勤下班</a>
    <div class="check_info" v-show="item.offWorkStatus">
    <div class="text" :class="{isFoul:isLeaveEarly,isQualified:!isLeaveEarly}">{{ item.title }}</div>
    <div class="text">{{offBusinessWorkTime}}</div>
    <div class="text">經度:{{item.lat}}</div>
    <div class="text">緯度:{{item.lng}}</div>
    </div>
    </div>
    </div>
    </li>
    </ul>

    <div v-show="showMap">
    <iframe :src="setMap" width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
    </div>
    </template>

    Route

    1
    2
    3
    4
    5
    <route lang="yaml">
    meta:
    layout: LayoutBack
    requireAutd: true
    </route>

    關於定位與定位點的圓周距離

    Geolocation API 這是瀏覽器內建的 API,getCurrentPosition() 方法傳回裝置的目前位置。
    js定位與範圍內 & Vue語法糖寫法 定位與使用套件

    Math.PI 3.14
    參考資料
    算出圓周
    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
    const radius =(p: number) =>{
    console.log('算出圓周',p * Math.PI / 180.0)//2.1058120410500933
    return p * Math.PI / 180.0;
    }
    // 計算距離,參數分別爲第一點的緯度,經度;第二點的緯度,經度
    const getDistance =(lat1: number,lng1: number,lat2: number,lng2: number) =>{
    const radLat1 = radius(lat1);
    const radLat2 = radius(lat2);
    // 緯度距離
    const a = radLat1 - radLat2;
    const b = radius(lng1) - radius(lng2);
    let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
    Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));

    s = s * 6378.137 ;// 地球半徑;
    s = Math.round(s * 10000) / 10000; //單位為 km
    return s;
    }
    // 定位
    const showMap=ref<boolean>(false);
    const getMap=ref<string>('');
    const lat = ref<string>('');
    const lng = ref<string>('');
    const latNow = ref<number| null>(null);
    const lngNow = ref<number | null>(null);
    // 圓周距離
    const distance =ref<number>(500);
    const location =() =>{
    if ("geolocation" in navigator) {
    try {
    setTimeout(() =>{
    // 獲取當前位置
    navigator.geolocation.getCurrentPosition((position) => {
    // 解構方式獲取
    const { latitude, longitude } = position.coords;
    latNow.value=latitude;
    lngNow.value=longitude;
    if(setMap.value.length===0){
    alert('定位不成功!');
    }else{
    // b單位為 km
    let dis = getDistance(Number(lat.value) ,Number(lng.value),latitude,longitude);
    // 距離公尺
    let removing =dis * 100;
    // 距離公尺<圓周距離限制
    if(removing<distance.value){
    showMap.value=true;
    }else{
    alert('你不在打卡範圍內,請檢查定位設定並重新登入!')
    }
    }
    })
    }, 5000)
    }
    catch (error:any) {
    switch(error.code){
    case error.PERMISSION_DENIED: alert('使用者拒絕了地理定位請求。');
    break;
    case error.POSITION_UNAVAILABLE: alert('位置資訊不可用。');
    break;
    case error.TIMEOUT:alert("取得用戶位置的請求逾時。");
    break;
    case error.UNKNOWN_ERROR:alert("出現未知錯誤。");
    break;
    }
    }
    }else{
    alert("Sorry, 你的裝置不支援地理位置功能。")
    }
    }

    表單type 取得的 time 為 ‘10:00’這種格式如何改為時間搓,才能夠判斷遲到或是早退

    邏輯:獲得現在時間 =>切割時間與分鐘 => 設定時間與分鐘 =>獲得的時間字符getTime()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let now = new Date();
    //表單type 取得的 time 為 '10:00'
    const hourMinute = '10:00';
    //split() 方法可以用來根據你指定的分隔符號,將字串切割成一個字串陣列。
    var [hour, minute] = hourMinute.split(':');
    now.setHours(hour) ;//setHours()方法依據本地時間來設定日期物件中的小時
    now.setMinutes(minute);//setMinutes()方法依據本地時間來設定日期物件中的分鐘
    now.setSeconds(0);//setMinutes()方法用於設定日期物件的秒字段
    now.setMilliseconds(0);//方法用於指定時間的毫秒字段
    console.log('塞入的彈性時間',now) // Sat Apr 20 2024 10:00:00 GMT+0800 (台北標準時間);
    console.log('時間搓',now.getTime()) // now.getTime()
    參考資料 上班與下班打卡邏輯大致上是過濾當日的全部上班與下班,來判斷是否出現警告是否打過卡: 過濾當日上班正常或是上班遲到都必須顯示於畫面上
    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
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    <script setup lang="ts">
    import axios from "axios";
    import {ref,onMounted} from 'vue';
    import moment from 'moment';
    import dayjs from 'dayjs'
    const getToken =localStorage.getItem("token");
    const headers = {"Authorization": 'Bearer '+getToken};
    const pageName=ref<string>('出勤打卡');
    interface menuType{
    id:string,
    title:string,
    href:string,
    }
    const setCheckModesMainMenu=ref<menuType[]>([
    { id: 'setWorkCheckList', title: '出勤設定列表', href: '/admin/set_work_check' },
    { id: 'checkWorkCheck', title: '出勤打卡', href: '/admin/checkWorkCheck' },
    { id: 'checkWorkLists', title: '出勤列表', href: '/admin/checkWorkLists' },
    { id: 'checkCalendar', title: '出勤行事曆', href: '/admin/calendar' },
    ])
    // 獲取 ip
    const ip = ref<string>('');
    const getIpClient = async () =>{
    try {
    const api='https://api.ipify.org?format=json';
    const res = await axios.get(api);
    if(res.status===200) {
    ip.value = res.data.ip;
    }
    }
    catch (error) {
    console.error(error);
    }
    }

    const isOnWorkMessage=ref<string>('');
    const isOnBusinessWorkMessage = ref<string>('');
    const isLate=ref<boolean>(true);
    const borderColor=ref<string>('');
    // 是否遲到
    const isLateHow = (time:any) => {
    const i=time;
    console.log('i',i);
    i > timeStamp.value ? isOnWorkMessage.value = '上班遲到' : isOnWorkMessage.value = '上班正常';
    i > timeStamp.value ? isOnBusinessWorkMessage.value = '外勤上班遲到' : isOnBusinessWorkMessage.value = '外勤上班正常';
    i > timeStamp.value ? isLate.value = true : isLate.value = false;
    i > timeStamp.value ? borderColor.value = 'red' : borderColor.value = '#00bcd4';
    }
    interface checkWorkList{
    _id:string,//id
    workCheck_Id:number,//打卡id
    userId:string,//使用者id
    userName:string,//使用者名稱
    title:string,//打卡名稱
    start:string,//打卡時間
    ip:string, // 打卡ip
    onWorkStatus:boolean,//是否已上班
    offWorkStatus:boolean,//是否已下班
    borderColor:string, // 外框顏色
    lat:string,//經度
    lng:string,//緯度
    }
    const showOnWorkCheck=ref<boolean>(true);
    const currentDayonWorkLists=ref<checkWorkList[]>([]);
    const currentDayoffWorkLists=ref<checkWorkList[]>([]);
    const currentDayonBusinessworkLists = ref<checkWorkList[]> ([]);
    const currentDayoffBusinessworkLists= ref<checkWorkList[]> ([]);
    const onWorkCheckTime=ref<string>('');
    const offWorkCheckTime=ref<string>('');
    const onBusinessWorkTime=ref<string>('');
    const offBusinessWorkTime=ref<string>('');
    let currentDay =dayjs(Date.now()).format('YYYY-MM-DD');
    const getCurrentDayWork = async () => {
    try{
    const api=`${import.meta.env.VITE_API_URL}/auth/workChecks`;
    const res = await axios.get(api,{ headers });
    if(res.status===200){
    //獲取當天上班打卡
    currentDayonWorkLists.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.onWorkStatus != null
    && item.title === '上班正常'
    ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.onWorkStatus != null
    && item.title === '上班遲到'
    ) {
    showOnWorkCheck.value = false;
    onWorkCheckTime.value = dayjs(item.start).format('YYYY-MM-DD HH:mm:ss');
    return item;
    }
    })
    console.log('166',currentDayonWorkLists.value)
    // 獲取當天下班打卡
    currentDayoffWorkLists.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.offWorkStatus != null && item.title === '下班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.offWorkStatus != null && item.title === '下班早退'
    ) {
    showOffWorkCheck.value = false;
    offWorkCheckTime.value = dayjs(item.start).format('YYYY-MM-DD HH:mm:ss');
    return item;
    }
    })
    // 獲取當天外勤上班打卡
    currentDayonBusinessworkLists.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.onWorkStatus != null && item.title === '外勤上班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.onWorkStatus != null && item.title === '外勤上班遲到'
    ) {
    showOnBusinessWorkCheck.value = false;
    onBusinessWorkTime.value = dayjs(item.start).format('YYYY-MM-DD HH:mm:ss')
    return item;
    }
    })

    // 外勤下班打卡列表
    currentDayoffBusinessworkLists.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.offWorkStatus != null && item.title === '外勤下班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.offWorkStatus != null && item.title === '外勤下班早退'
    ) {
    showOffBusinessWorkCheck.value = false;
    offBusinessWorkTime.value = dayjs(item.start).format('YYYY-MM-DD HH:mm:ss')
    return item;
    }
    })
    }

    }
    catch(err){
    console.log('err',err)
    }
    }
    const currentDayAllOnWorkList =ref<checkWorkList[]>([]);
    const currentDayAllOffWorkList =ref<checkWorkList[]>([])
    // 獲取當日所有上班打卡列表(包含出勤上班與上班)=>必須加入判斷 是否是本人的數據
    const getCurrentDayAllWork = async () => {
    try {
    const api = `${import.meta.env.VITE_API_URL}/auth/workChecks`;
    const res = await axios.get(api,{ headers });
    if (res.status === 200) {
    // 獲取當日所有上班打卡列表
    currentDayAllOnWorkList.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.title === '上班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.title === '上班遲到' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.title === '外勤上班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.onWorkStatus && item.title === '外勤上班遲到'
    ) {
    console.log('item',item)
    return item;
    }

    })
    console.log('獲取當日所有上班打卡列表', currentDayAllOnWorkList.value);
    // 獲取當日所有下班列表
    currentDayAllOffWorkList.value = res.data.filter(function (item: any) {
    if (
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.title === '下班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.title === '下班早退' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.title === '外勤下班正常' ||
    item.userName===localStorage.getItem('userName') &&
    dayjs(item.start).format('YYYY-MM-DD').indexOf(currentDay) !== -1 && item.offWorkStatus && item.title === '外勤下班早退'
    ) {
    return item;
    }
    })
    }
    }
    catch (err) {
    console.error(err);
    }
    }

    // 上班打卡
    const checkOnWork = async () =>{
    try {
    if( currentDayAllOnWorkList.value.length>0 ){
    alert('已經打過上班卡了')
    }
    else{
    let time=Date.now();
    isLateHow(time);
    const api=`${import.meta.env.VITE_API_URL}/auth/workCheck`;
    let query ={
    workCheck_Id: Date.now(),
    userId: localStorage.getItem('userId'),
    userName: localStorage.getItem('userName'),
    start: dayjs(Date.now()).format('YYYY-MM-DD HH:mm'),
    onWorkStatus: true,
    offWorkStatus: false,
    title: isOnWorkMessage.value,
    borderColor: borderColor.value,
    ip: String(ip.value),
    lat:latNow.value,
    lng:lngNow.value,
    }

    console.log(query)
    const res = await axios.post(api,query, { headers });

    if(res.status===200){
    alert('打卡成功!');
    getCurrentDayWork();
    getCurrentDayAllWork();
    }
    }
    }
    catch(err){
    console.log('err',err)
    }
    }
    const showOnBusinessWorkCheck =ref<boolean>(true);
    // 外勤上班打卡
    const checkOnBusinessWork = async () =>{
    try {
    if( currentDayAllOnWorkList.value.length>0 ){
    alert('已經打過上班卡了')
    }
    else{
    let time=Date.now();
    isLateHow(time);
    const api=`${import.meta.env.VITE_API_URL}/auth/workCheck`;
    let query ={
    workCheck_Id: Date.now(),
    userId: localStorage.getItem('userId'),
    userName: localStorage.getItem('userName'),
    start: dayjs(Date.now()).format('YYYY-MM-DDTHH:mm'),
    onWorkStatus: true,
    offWorkStatus: false,
    title: isOnBusinessWorkMessage.value,
    borderColor: borderColor.value,
    ip: String(ip.value),
    lat:latNow.value,
    lng:lngNow.value,
    }

    console.log(query)
    const res = await axios.post(api,query, { headers });

    if(res.status===200){
    showOnBusinessWorkCheck.value = false;
    alert('打卡成功!');
    getCurrentDayWork();
    getCurrentDayAllWork();
    }
    }
    }
    catch(err){
    console.log('err',err)
    }
    }
    // 是否早退
    const isOffWorkMessage=ref<string>('');
    const isOffBusinessWorkMessage=ref<string>('');
    const isLeaveEarly=ref<boolean>(true);
    const isLeaveEarlyHow = (time:any) => {
    const i=time;
    i < endTimeStamp.value ? isOffWorkMessage.value = '下班早退' : isOffWorkMessage.value = '下班正常';
    i < endTimeStamp.value ? isOffBusinessWorkMessage.value = '外勤下班早退' : isOffBusinessWorkMessage.value = '外勤下班正常';
    i < endTimeStamp.value ? isLeaveEarly.value = true : isLeaveEarly.value = false;
    i < endTimeStamp.value ? borderColor.value = 'red' : borderColor.value = '#00bcd4';
    }
    const showOffWorkCheck =ref<boolean>(true);
    // 下班打卡
    const checkOffWork =async()=>{
    try {
    if( currentDayAllOffWorkList.value.length>0){
    alert('已經打過下班卡了')
    }
    else{
    let time=Date.now();
    isLeaveEarlyHow(time);
    const api=`${import.meta.env.VITE_API_URL}/auth/workCheck`;
    let query ={
    workCheck_Id: Date.now(),
    userId: localStorage.getItem('userId'),
    userName: localStorage.getItem('userName'),
    start: dayjs(Date.now()).format('YYYY-MM-DDTHH:mm'),
    onWorkStatus: true,
    offWorkStatus: true,
    title: isOffWorkMessage.value,
    borderColor: borderColor.value,
    ip: String(ip.value),
    lat:latNow.value,
    lng:lngNow.value,
    }
    console.log(query)
    const res = await axios.post(api,query, { headers });

    if(res.status===200){
    showOffWorkCheck.value = false;
    getCurrentDayWork();
    getCurrentDayAllWork();
    alert('打卡成功!');
    }
    }
    }
    catch(err){
    console.log('err',err)
    }
    }
    const showOffBusinessWorkCheck =ref<boolean>(true);
    //外勤下班打卡
    const checkOffBusinessWork =async()=>{
    try {
    if( currentDayAllOffWorkList.value.length>0){
    alert('已經打過下班卡了')
    }
    else{
    let time=Date.now();
    isLeaveEarlyHow(time);
    const api=`${import.meta.env.VITE_API_URL}/auth/workCheck`;
    let query ={
    workCheck_Id: Date.now(),
    userId: localStorage.getItem('userId'),
    userName: localStorage.getItem('userName'),
    start: dayjs(Date.now()).format('YYYY-MM-DDTHH:mm'),
    onWorkStatus: true,
    offWorkStatus: true,
    title: isOffBusinessWorkMessage.value,
    borderColor: borderColor.value,
    ip: String(ip.value),
    lat:latNow.value,
    lng:lngNow.value,
    }
    console.log(query)
    const res = await axios.post(api,query, { headers });

    if(res.status===200){
    showOffBusinessWorkCheck.value = false;
    getCurrentDayWork();
    getCurrentDayAllWork();
    alert('打卡成功!');
    }
    }
    }
    catch(err){
    console.log(err)
    }
    }
    //
    const getUsers = async () =>{
    try {
    const api=`${import.meta.env.VITE_API_URL}/auth/users`;
    let query ={
    workCheck_Id:Date.now(),
    userId:localStorage.getItem('userId'),
    userName:localStorage.getItem('userName'),
    start:Date.now(),
    onWorkStatus:true,
    title:'上班打卡',
    borderColor:'red',
    ip: String(ip.value),
    lat:latNow.value,
    lng:lngNow.value,
    }
    const res = await axios.post(api,query, { headers });
    if(res.status===200){

    }
    }
    catch (error) {
    console.error(error);
    }
    }

    // 算出圓周
    const radius =(p: number) =>{
    return p * Math.PI / 180.0;
    }
    // 計算距離,參數分別爲第一點的緯度,經度;第二點的緯度,經度
    const getDistance =(lat1: number,lng1: number,lat2: number,lng2: number) =>{
    const radLat1 = radius(lat1);
    const radLat2 = radius(lat2);
    // 緯度距離
    const a = radLat1 - radLat2;
    const b = radius(lng1) - radius(lng2);
    let s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
    Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));

    s = s * 6378.137 ;// 地球半徑;
    s = Math.round(s * 10000) / 10000; //單位為 km
    return s;
    }
    // 定位
    const showMap=ref<boolean>(false);
    const getMap=ref<string>('');
    const lat = ref<string>('');
    const lng = ref<string>('');
    const latNow = ref<number| null>(null);
    const lngNow = ref<number | null>(null);
    // 圓周距離
    const distance =ref<number>(500);
    const location =() =>{
    if ("geolocation" in navigator) {
    try {
    setTimeout(() =>{
    // 獲取當前位置
    navigator.geolocation.getCurrentPosition((position) => {
    // 解構方式獲取
    const { latitude, longitude } = position.coords;
    latNow.value=latitude;
    lngNow.value=longitude;
    if(setMap.value.length===0){
    alert('定位不成功!');
    }else{
    // b單位為 km
    let dis = getDistance(Number(lat.value) ,Number(lng.value),latitude,longitude);
    // 距離公尺
    let removing =dis * 100;
    // 距離公尺<圓周距離限制
    if(removing<distance.value){
    showMap.value=true;
    }else{
    alert('你不在打卡範圍內,請檢查定位設定並重新登入!')
    }
    }
    })
    }, 5000)
    }
    catch (error:any) {
    switch(error.code){
    case error.PERMISSION_DENIED: alert('使用者拒絕了地理定位請求。');
    break;
    case error.POSITION_UNAVAILABLE: alert('位置資訊不可用。');
    break;
    case error.TIMEOUT:alert("取得用戶位置的請求逾時。");
    break;
    case error.UNKNOWN_ERROR:alert("出現未知錯誤。");
    break;
    }
    }
    }else{
    alert("Sorry, 你的裝置不支援地理位置功能。")
    }
    }
    interface checkSetingListType {
    _id:string,
    mode:number,//模式
    name:string,//地點
    url:string,//地圖
    flexibleTime:string,//彈性上班
    flexibleHour:string,//工時
    distance:number,//圓周
    lat:string,//經度
    lng:string,//緯度
    personGroup:Object,//群組
    buildDate:number,
    updataDate:number,
    }
    const checkSetingList=ref<checkSetingListType[]>([]);
    const flexibleTime=ref<number | any >(null);
    const setMap=ref<string>('');
    const office=ref<string>('');
    const flexibleHour =ref<string>('');
    // 獲得出勤設定
    const getSetMode = async() =>{
    try {
    const api=`${import.meta.env.VITE_API_URL}/auth/setCheckModes`;
    const res = await axios.get(api, { headers });
    if (res.status === 200) {
    setTimeout(() =>{
    checkSetingList.value=res.data.filter((element:any)=>{
    if(element.name==='台中大墩11街'){
    return element
    }else if (element.name!='台中大墩11街'){
    return element
    }
    })
    console.log('台中辦公室',checkSetingList.value);
    lat.value=checkSetingList.value[0].lat;
    lng.value=checkSetingList.value[0].lng;
    flexibleTime.value=checkSetingList.value[0].flexibleTime;
    setMap.value=checkSetingList.value[0].url;
    console.log(setMap.value)
    distance.value=checkSetingList.value[0].distance;
    office.value=checkSetingList.value[0].name;
    flexibleHour.value=checkSetingList.value[0].flexibleHour;
    // 將彈性時間改為時間搓
    formatFlexibleTime(flexibleTime.value);
    }, 3000)
    location()
    }
    }
    catch(err){
    console.log('err',err)
    }
    }

    const changeFlexibleTime =ref<any>('');
    const timeStamp=ref<any>(null);
    const endTimeStamp=ref<any>(null);//下班的時間搓
    // 將彈性時間改為時間搓 塞入timeStamp 1.獲取flexibleTime
    const formatFlexibleTime =(flexibleTime:any) =>{
    let now = new Date();
    // 原生時間表單表單type 取得的 time 為 '10:00'
    const hourMinute = flexibleTime;
    //split() 方法可以用來根據你指定的分隔符號,將字串切割成一個字串陣列。
    var [hour, minute] = hourMinute.split(':');
    // 設定日期的小時、分鐘、秒和毫秒
    now.setHours(hour) ;//setHours()方法依據本地時間來設定日期物件中的小時
    now.setMinutes(minute);//setMinutes()方法依據本地時間來設定日期物件中的分鐘
    now.setSeconds(0);//setMinutes()方法用於設定日期物件的秒字段
    now.setMilliseconds(0);//方法用於指定時間的毫秒字段
    changeFlexibleTime.value = now;
    // console.log('changeFlexibleTime',changeFlexibleTime.value);
    // Sat Apr 20 2024 10:00:00 GMT+0800 (台北標準時間);
    const changeFormat =dayjs(changeFlexibleTime.value).format('YYYY-MM-DD HH:mm:ss');
    console.log('changeFormat',changeFormat);
    timeStamp.value=now.getTime()//
    console.log('timeStamp.value',timeStamp.value);
    //算出下班時間
    if(changeFormat!=''){
    const endTmeFormat =dayjs(changeFormat).add(Number(flexibleHour.value), 'hours').format('YYYY-MM-DD HH:mm:ss');
    console.log('下班時間',endTmeFormat);
    const endTime=dayjs(endTmeFormat,'YYYY-MM-DD HH:mm:ss');
    // console.log('endTime',endTime);
    endTimeStamp.value=endTime.valueOf();
    console.log('endTimeStamp.value',endTimeStamp.value);
    }
    // console.log('changeFormat',changeFormat);
    const time = dayjs(changeFormat,'YYYY-MM-DD HH:mm:ss');
    //**valueOf() 方法傳回字串的原始值。
    timeStamp.value=time.valueOf();
    //console.log('timeStamp.value',timeStamp.value);
    }
    onMounted(()=>{
    getSetMode(); // 出勤設定
    getIpClient();
    getCurrentDayWork();
    getCurrentDayAllWork();

    })
    </script>

    json-server 模擬Api

    json-server db.json

    1
    2
    3
    4
    5
    // 安裝
    npm install --save json-server
    // json-server版本號
    json-server -v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let fruits = [
    {item: 'apple', quantity: 0},
    {item: 'banana', quantity: 1},
    {item: 'cherry', quantity: 2}
    ]
    // 這面這行就是告訴電腦我想要怎麼過濾
    let fruitsNew = fruits.filter(e => e.quantity > 0)
    console.log(fruitsNew)
    /*
    印出:
    [
    { item: 'banana', quantity: 1 },
    { item: 'cherry', quantity: 2 }
    ]
    */

    // 過濾重複元素

    1
    2
    3
    4
    5
    6
    var array=["a", 1, 2, 3, 2, 3, "b", "a"]; 
    var result=array.filter(function(element, index, arr){
    return arr.indexOf(element) === index; });
    console.log(result);
    // ["a", 1, 2, 3, "b"];