moment.js

1
2
3
4
5
6
npm install moment --save   # npm
yarn add moment # Yarn
Install-Package Moment.js # NuGet
spm install moment --save # spm
meteor add momentjs:moment # meteor
bower install moment --save # bower (deprecated)

Unix 轉換

format格式化
如何使用引入:import moment from ‘moment’,與時間轉換:moment(‘timestamp值’).format(‘YYYY-MM-DDTHH:mm:ss.SSS’);

1
2
3
4
5
6
7
8
9
10
11
12
//引入
import moment from 'moment';
//目前時間搓
const TimeStamp = moment().valueOf()
console.log('TimeStamp', TimeStamp);
//目前時間搓
const nowTimeStamp = Date.now();
console.log('nowTimeStamp', nowTimeStamp);
const nowTime = moment(nowTimeStamp).format('YYYY-MM-DDTHH:mm:ss.SSS');
console.log('現在時間', nowTime);
console.log('現在時間格式化',moment().format());

moment(‘要驗證的日期’).isBetween(‘起始日’, ‘截止日’);

使用情境:驗證是否有在一段時間以內或是在截止日以前

1
2
3
moment('2023-03-29').isBetween('2023-03-01', '2023-03-29'); // true

moment('2023-04-29').isBetween('2023-03-01', '2023-03-29'); // false

相對時間:

距離現在幾個月:moment(“20221031”, “YYYYMMDD”).fromNow()
moment(“20221031”, “YYYYMMDD”).fromNow();
解釋:距離現在幾個月

距離開始小時:moment().startOf(‘day’).fromNow()
moment().startOf(‘day’).fromNow();
解釋:距離今天開始時間

距離結束小時:moment().endOf(‘day’).fromNow()
moment().startOf(‘day’).fromNow();
解釋:距離今天結束時間

距離小時結束分鐘:moment().startOf(‘hour’).fromNow();
解釋:距離小時結束分鐘

1
2
3
4
5
6
7
console.log('現在時間:', moment().format());//2023-01-07T13:44:41+08:00
console.log('20221031距離現在:', moment("20221031", "YYYYMMDD").fromNow());//2 months ago
console.log('20230620距離現在:', moment("20230620", "YYYYMMDD").fromNow());//in 5 months
console.log('距離今天開始小時startOf('day'):', moment().startOf('day').fromNow());//14 hours ago
console.log('距離今天結束時間endOf('day'):', moment().endOf('day').fromNow());//in 10 hours
console.log('距離小時結束分鐘startOf('hour'):',moment().startOf('hour').fromNow())//37 minutes ago

運算

增加天數變換日期:moment().add(天數, ‘days’).calendar();

現在的日期加上七天

1
moment().add(7, 'days').calendar();//01/14/2023

減去天數變換日期:moment().subtract (天數, ‘days’).calendar();

現在的日期減去七天

1
moment().subtract(7, 'days').calendar(); //12/31/2022

###距離今天還有幾天: from & to:不會有負值 例如:-1,-2

1
2
3
4
5
const a = moment([2023, 0, 1]);
const b = moment([2023, 0, 12]);
const c = moment([2023, 0, 20]);
console.log('距離', a.from(b),'天');
console.log('距離', a.to(c), '天');

查詢

isBefore 是用來查詢是否在早於後面的時間

1
2
moment('2023-10-20').isBefore('2023-10-21') // true
moment('2023-10-20').isBefore('2023-12-31', 'year') // false

isSame 是用來查詢是否和後面的時間相等

1
2
moment('2022-10-20').isSame('2023-12-31', 'year') // false
moment('2023-10-20').isSame('2023-01-01', 'year') // true

isAfter 是用來查詢是否和晚於後面的時間

1
2
moment('2022-10-20').isAfter('2023-10-19') // true
moment('2010-10-20').isAfter('2010-01-01', 'year') // false

isSame 是用來查詢是否在設定的時間範圍內

1
2
moment('2010-10-20').isBetween('2010-10-19', '2010-10-25') // true
moment('2010-10-20').isBetween('2010-01-01', '2012-01-01', 'year') // false

更換語言

1
2
3
moment.locale('zh-tw', {
months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
}

Promise

「我承諾幫你做某件事情,能不能成功還不一定,但是我做完之後會把結果告訴你」的意思

Promise 是一個代表非同步運作的最終狀態的物件 (成功或失敗)

callback的特性:

控制的反轉=>可靠性不足=>無法控制第三方的函式會不會在正確的時間點呼叫以及呼叫正確的次數的callback函式。

Promise的特性:

可靠:Promise 回來的東西是可信任的,一旦回傳就無法被更改。
控制回歸:將執行 callback 的控制權回歸自己的程式。

pending承諾 一直沒有回應

resolve()成功兌現 或 fulfilled 被兌現 =>.then()

reject()失敗 =>.catch() 或.then 的第二個參數

1
2
3
4
5
6
var defer = new Promise(function(resolve, reject) { 
// 如果成功, resolve()
// 如果失敗, reject()
})
.then(laterOn)
.catch(printError);

Vite TypeScript 請求攔截與響應攔截封裝

新增 axios/index.ts

引入import Axios, { AxiosRequestConfig, AxiosResponse, AxiosPromise } from ‘axios’;

请求攔截 axios.interceptors.request.use((config:any) => {return config;},(error)=>{return Promise.rejest(error);})

響應攔截 axios.interceptors.response.use((response) => {return response;},(error)=>{return Promise.rejest(error);})

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
import Axios, { AxiosRequestConfig, AxiosResponse, AxiosPromise } from 'axios';

const server = Axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
// timeout: 15000,
// withCredentials: config.apiConfig.withCredentials
})

// 请求攔截
axios.interceptors.request.use((config:any) => {

//如果localStorage.token有數據,參閱知識點
if ( localStorage.token ) {
//解釋token的使用方式
config.headers.Authorization=localStorage.token;

}
return config;
},
(error) => {
return Promise.reject(error)
})

//響應攔截
axios.interceptors.response.use((response) => {
// 這裡是對響應進行統一處理
return response
},
(error) => {
// 可以對錯誤進行統一處理
if (error.response.status === 401) {
localStorage.removeItem('token');
}
// console.log(error.response.status)
return Promise.reject(error)
})

/**
* 封装post请求,在api模塊直接調用
* -url 請求地址
* -params 參數
* -isQs 是否將參數序列化
*/
export const postApi = (url: string, params?: any, isQs?: boolean): AxiosPromise<any> => {
return server({
url,
method: 'post',
data: isQs ? stringify(params) : params
})
}
/**
* 封装get请求,在api模塊直接調用
* -url 請求地址
* -params 參數
*/

export const getApi = (url: string, query: any): AxiosPromise<any> => {
return server({
url,
method: 'get',
params: query
})
}

/**
* 封装put请求,在api模塊直接調用
* -url 請求地址
* -params 參數
*/

export const editApi = (url: string, params?: any, isQs?: boolean): AxiosPromise<any> => {
return server({
url,
method: 'put',
data: isQs ? stringify(params) : params
})
}

/**
* 封装delete请求,在api模塊直接調用
* -url 請求地址
* -id 參數
*/

export const removeApi = (url: string, id?: any, isQs?: boolean): AxiosPromise<any> => {
return server({
url,
method: 'delete',
data: isQs ? stringify(id) : id
})
}

知識點

HTTP Token 使用方式: Basic Token v.s Bearer Token

使用Token的方式

1.使用Authorization HEADER:
Ex: Authorization: Bearer QQxhZGRpbjpvcGVuIHNlc2FtQQ==
2.使用Form Body:
不建議將Token加入Body,使用時 ,並使用GET 之外的 HTTP verbs (POST、DELETE、PUT)
HEADER: Content-Type: “application/x-www-form-urlencoded”
3.直接使用在URI :
一般來說不會使用這種方法,除非是Authorization header不支援,或者protect resource 支援該方法。
Ex: GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1

Token Response HTTP Response Status code 為200時代表成功

Basic Token: 當授權為「不」通過時,回傳401 code並告知哪個 Protect Scope 錯誤

Bearer Token: 當授權為「不」通過時,回傳400、401、403 code,並根據狀況回傳對應的錯誤。

創建api模塊管理所有的api接口

1
2
3
4
5
import { postApi } from '@/axios/index'

// 登入
export const Login = (params: any) => postApi('/auth/login', params, true)
export const Register = (params: any) => postApi('/auth/register', params, true)
github ts axios 封裝

Vuex

安裝Vuex

1
2
3
npm install vuex@next --save
yarn add vuex@next --save

main.js引入vuex

1
2
3
4
5
6
7
8
9
import Vuex from 'vuex'
Vue.use(Vuex)

export const app = new Vue({
el: '#app',
store,
components: { App },
template: '<App/>'
})

新增store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
state: {
//如同data
msg: 'Vuex',
} ,
actions: {
//呼叫mutations處理分同步,第一個參數:context,第二個參數:payload
}
mutations: {
如同methods,只有mutations才能操作state資料狀態
第一個參數:state,第二個參數:payload
},
getters: {
//狀態延伸
}
})

單一狀態樹,頁面調用msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h1>{{msg}}</h1>
</div>
</template>

<script>
import { app } from '../main'
import Vue from 'vue'
import { mapState } from 'vuex'
export default {

computed: {
...mapState({
msg: state => state.msg
})
}
}
</script>

modules模組概念
新增store/modules/values.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
import axios from "axios"

export default {
// actions, mutations, getters 是屬於區域變數
namespaced: true,
state: {

},
actions: {
getDayObjects(context) {
const api = `${process.env.APIPATH}/api/Teacher/LoadTeacherOpenClass`;
axios.post(api, raw, {
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${process.env.TOKEN}` }
})
.then((res) => {
if (res.data.IsSuccess) {
context.commit('DAYOBJECTS', res.data.DayObjects);
}
})
.catch((error) => {
console.log(error);
})
}
},
mutations: {
DAYOBJECTS(state, payload) {
state.DayObjects = payload;
},
}
getters: {
DayObjects: state => state.DayObjects,
}
}

新增store/index.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
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import dayobjectsModules from './modules/dayobjects'
import modalModules from './modules/modal'
export default new Vuex.Store({
state: {
//如同data
msg: 'Vuex',
} ,
actions: {
//呼叫mutations處理分同步,第一個參數:context,第二個參數:payload
}
mutations: {
如同methods,只有mutations才能操作state資料狀態
第一個參數:state,第二個參數:payload
},
getters: {
//狀態延伸
},
modules: {
dayobjectsModules,
},
})

單一狀態樹,頁面調用msg

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
<template>
<div>
<h1>{{msg}}</h1>
</div>
</template>

<script>
import { app } from '../main'
import Vue from 'vue'
import { mapState,mapActions,mapGetters } from 'vuex'
export default {
created(){
this.getDayObjects();
},
methods: {
...mapActions('dayobjectsModules', ['getDayObjects']),
},
computed: {
...mapState({
msg: state => state.msg
}),
...mapGetters('dayobjectsModules', ['DayObjects']),
}
}
</script>

Emailjs 寄信

申請Emailjs帳號

Emailjs 官網

Email Service獲取到一組 Service Id =>import.meta.env.VITE_SENDMAIL_SERVICE

選擇Gmail

Email Templates 這邊建立樣板,獲取到一組 Templates Id =>import.meta.env.VITE_SENDMAIL_TEMPLATE

Account =>API keys=>Public Key,獲取到一組 Public Key => import.meta.env.VITE_SENDMAIL_USERID

Vite使用方式參考官網
Vite使用方式參考官網

安裝emailjs-com

emailjs 安裝

1
npm install emailjs-com --save

綁定表單的name

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
<template>
<div>
<el-form @submit.prevent="sendMail">
<el-form-item label="姓名" prop="name">
<el-input name="form_name" v-model="Forms.name" placeholder="請輸入你的姓名" type="test" />
</el-form-item>
<el-form-item label="email" prop="email">
<el-input name="form_email" v-model="Forms.email" placeholder="請輸入你的email" type="email" />
</el-form-item>
<el-form-item label="行動電話" prop="phone">
<el-input name="form_phone" v-model="Forms.phone" placeholder="請輸入你的行動電話" type="text" />
</el-form-item>
</el-form>
</div>
</template>

<script setup lang="ts">
import emailjs from 'emailjs-com';
const form_name = ref<string | null>('');
const form_email = ref<string | null>('');
const form_phone = ref<string | null>('');


//送出的表單
const sendMail = (e: { target: string | HTMLFormElement; }) => {
try {
const templateParams = {

form_name: form_name,
form_email: form_email,
form_phone: form_phone,

}
// console.log(templateParams)

emailjs.sendForm(import.meta.env.VITE_SENDMAIL_SERVICE, import.meta.env.VITE_SENDMAIL_TEMPLATE, e.target,
import.meta.env.VITE_SENDMAIL_USERID, templateParams)

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


</script>

Pinia Composition API(defineStore,storeToRefs)

安裝pinia

1
npm install --save pinia

在main.ts引入pinia

1
2
3
4
5
6
7
8

import { createPinia } from 'pinia'

const pinia = createPinia()

const app = createApp(App)
app.use(pinia)
app.mount('#app')

Vite Ssr 專案 main.ts引入方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createSSRApp } from 'vue'
+ import { createPinia } from 'pinia'
import App from './App.vue'
+ const pinia = createPinia()

// SSR requires a fresh app instance per request, therefore we export a function
// that creates a fresh app instance. If using Vuex, we'd also be creating a
// fresh store here.
export function createApp() {
const app = createSSRApp(App);
+ app.use(pinia)
return { app }
}

官方寫法:

stores/counter.ts

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
//引入defineStore
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
//state如同data
state: () => {
return {
count: 0,
cardLists:[],
forms: {
email: '',
password:'',
}
}
},
//如同computed
getters: {
doubleCount: (state) => state.count * 2,
},
//如同method=>

actions: {
addCount() {
this.count++
},
async fetchApi() {
try {
const api = 'https://api.coindesk.com/v1/bpi/currentprice.json';
/* let headers = {
"Content-Type": "application/json",
"Accept": "application/json",
// Authentication: 'secret'
// "Authorization": `Bearer ${token}`,
}*/
const res = await fetch(api, { headers: headers, method: "GET" });
// this方法
this.cardLists = await res.json();
console.log('lists.value', this.cardLists)

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

},
async loginIn () {
try {

const api = `${import.meta.env.VITE_API_DEFAULT_URL}/auth/login`

let headers = {
"Content-Type": "application/json",
"Accept": "application/json",
// "Authorization": `Bearer ${token}`,
}
//以下是API文件中提及必寫的主體参數
let body = {
email: this.forms.email,
password:this.forms.password,
}
fetch(api, {
method: "POST",
headers: headers,
body: JSON.stringify(body)//****重點 */
})
.then(res => res.json())
.then(json => {
console.log('login',json)
});
}
catch (error) {
console.log(error)
}
}
},
})

在頁面使用

  • 引入counter的useCounterStore
  • 宣告useCounterStore()

引入

宣告

解構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<button @click="addCount" class="add_btn">add count</button>
<div>{{counter.count}}</div>
{{ counter.cardLists }}
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
//引入Store
import { useCounterStore } from '../stores/counter'
//宣告
const counter = useCounterStore();
const { addCount,fetchApi } =counter;
onMounted(() => {
fetchApi();
})
</script>

pinia 選項寫法 gitHub
pinia 選項寫法 gitHub

改為 Composition API

*重點:
1.引入vue Composition API
2.改為Function 寫法與Composition API
3.必須 return

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
import { defineStore } from 'pinia';
//引入vue Composition API
import { ref,computed } from 'vue';
//改為Function 寫法與Composition API
export const useCounterStore = defineStore('counter', ()=>
{
const count = ref(0);

//computed
const doubleCount = computed(() =>
{
return count.value * 2
})

const addCount = () =>
{
count.value ++
}
const cardLists=ref([])
const fetchApi = async() =>
{
try {
const res = await axios.get('https://api.coindesk.com/v1/bpi/currentprice.json');
cardLists.value = res.data;
} catch (error) {
console.log(error)
}

}
//必須輸出 return
return {
count,addCount,doubleCount,
cardLists,fetchApi
}
})

在頁面使用

  • 引入counter的useCounterStore
  • 宣告useCounterStore()

html 加入 store.命名

使用 store action時 store.addCount()

*重點:
1.store.名稱
2.store.function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<p>{{store.count}}</p>
<pre>
{{ store.cardLists }}
</pre>
<button @click="clickAdd">Add</button>
<button @click="store.fetchApi">fetchApi</button>
</div>
</template>

<script setup lang="ts">
//引入Store
import { useCounterStore } from '@/stores/counter.js';

//宣告store = useCounterStore()
const store = useCounterStore();

const clickAdd = () => {
//使用 store action時 store.addCount()
store.addCount()
}
</script>

counter pinia寫法引入Store 方式寫法

解構方式引入

1.引入 import {storeToRefs } from ‘pinia’
2.//解構store,數據取出counter, cardLists
const { counter, cardLists } = storeToRefs(store);
3.function 解構 const { fetchApi } = store;

使用

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
<template>
<div>
<button @click="clickAdd">Add</button>
<h1>{{ counter }}</h1>

<div> {{ cardLists }}</div>
<pre>
{{ cardLists }}
</pre>
</div>
</template>

<script setup lang="ts">
import { ref,onMounted } from 'vue';

//引入 import {storeToRefs } from 'pinia'
import {storeToRefs } from 'pinia';
// @ts-ignore
import { useCounterStore } from '../stores/counter.js';
//宣告store = useCounterStore()
const store = useCounterStore();
//解構store,數據取出counter, cardLists
const { counter, cardLists } = storeToRefs(store);
const { fetchApi, addCount } = store;

const clickAdd = () => {
addCount()
}
onMounted(() => {
fetchApi();
});
</script>


Pinia Composition API

範例

stores/user.ts

  • acceptHMRUpdatePinia 支援熱模組替換,因此您可以直接在您的應用程式中編輯您的商店並與它們進行交互,而無需重新加載頁面,從而允許您保留現有狀態、添加甚至刪除狀態、操作和 getter。
  • 使用$patch修改state 數據
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
// @ts-check 引入pinia的defineStore與acceptHMRUpdate
import { defineStore, acceptHMRUpdate } from 'pinia'

/**模擬登入*/
function apiLogin(a: string, p: string) {
if (a === 'ed' && p === 'ed') return Promise.resolve({ isAdmin: true })
if (p === 'ed') return Promise.resolve({ isAdmin: false })
return Promise.reject(new Error('invalid credentials'))
}

export const useUserStore = defineStore({
id: 'user',
state: () => ({
name: 'Eduardo',
isAdmin: true,
}),

actions: {
logout() {
// 登出以後 =>使用$patch修改state 數據
this.$patch({
name: '',
isAdmin: false,
})

// 我們可以做其他事情,例如重定向用戶
},

/**
* 嘗試登入用戶
*/
async login(user: string, password: string) {
const userData = await apiLogin(user, password)
// 登入以後 =>使用$patch修改state 數據
this.$patch({
name: user,
...userData,

})
console.log(userData,)
},
},
})

if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}


stores/cart.ts

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
// @ts-check 引入pinia的defineStore與acceptHMRUpdate
import { defineStore, acceptHMRUpdate } from 'pinia'
//引入useUserStore
import { useUserStore } from './user'

export const useCartStore = defineStore({
id: 'cart',
state: () => ({
rawItems: [] as string[],
}),
getters: {
items: (state): Array<{ name: string; amount: number }> =>
//https://medium.com/@davelin18yufan/js%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3reduce%E7%9A%84%E7%94%A8%E6%B3%95-95738c3f9114
state.rawItems.reduce((items, item) => {
//尋找
const existingItem = items.find((it) => it.name === item)
//console.log('現有項目existingItem',existingItem)
if (!existingItem) {
//console.log('19',existingItem)
items.push({ name: item, amount: 1 })
} else {
//console.log('22',existingItem.amount++)
existingItem.amount++
}
//console.log('items',items)
return items
}, [] as Array<{ name: string; amount: number }>),
},
actions: {
//新增Item
addItem(name: string) {
this.rawItems.push(name)
},
//刪除
removeItem(name: string) {
//lastIndexOf搜尋最後一次出現的
//https://www.w3schools.com/jsref/jsref_lastindexof.asp
const i = this.rawItems.lastIndexOf(name)
//splice() 方法可以藉由刪除既有元素並
if (i > -1) this.rawItems.splice(i, 1)
},

async purchaseItems() {
const user = useUserStore()
if (!user.name) return

console.log('購買', this.items)
const n = this.items.length
this.rawItems = []

return n
},
},
})

if (import.meta.hot) {
console.log('import.meta.hot', import.meta.hot);
import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot))
}

頁面引用

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
<script setup lang="ts">
//引入Store
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
//宣告
const cart = useCartStore();
const user = useUserStore();
const itemName = ref<string>('');

// 新增Item到購物車
const addItemToCart =()=>{
if (!itemName.value) return
cart.addItem(itemName.value)
itemName.value = ''
}
// 清空購物車
const clearCart =()=>{
if (window.confirm('您確定要清空購物車嗎?')) {
cart.rawItems = []
}
}
// 購買
const buy =async()=>{
const n = await cart.purchaseItems()
console.log(`買 ${n} items`)
cart.rawItems = []
}
</script>

<template>
<div>
<input type="text" v-model="itemName" />
<button class="add_btn" @click="addItemToCart">Add</button>
<!--新增的Item-->
<ul>
<li v-for="item in cart.items" :key="item">
{{item.name}}
<!--刪除的按鈕-->
<button class="delete_btn" @click="cart.removeItem(item.name)"
type="button">X</button>
</li>
</ul>
<!--購買按鈕-->
<button class="buy_btn" :disabled="!user.name">Buy</button>
<!--清除購物車按鈕-->
<button
class="delete_btn"
:disabled="!cart.items.length"
@click="clearCart"
type="button"
data-testid="clear"
>Clear the cart</button>
</div>

</template>

github

改Composition API寫法

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
// @ts-check 引入pinia的defineStore與acceptHMRUpdate
import { defineStore, acceptHMRUpdate } from 'pinia'
//引入vue Composition API
import { ref,computed } from 'vue';
//引入useUserStore
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {

const rawItems = ref<any[]>([]);
const items = computed(() =>{
rawItems.value.reduce((items: any[], item: any) => {
const existingItem = items.find((i:any) => i.name === item)
if (!existingItem) {
items.push({ name: item, amount: 1 })
} else {

existingItem.amount++
}
return items
},[] as Array<{ }>)
})

const addItem = (name: string) => {
console.log('新增的input 數據:', name);
rawItems.value.push(name)
}
const removeItem = (name: string) => {
console.log('刪除:',name)
const i = rawItems.value.indexOf(name)
//splice() 方法可以藉由刪除既有元素並
if (i > -1) rawItems.value.splice(i, 1)
}
const purchaseItems = () => {
const user = useUserStore()
if (!user.name) return

console.log('購買', items)
// const n = items.length
rawItems.value = []

//return n
}
return {
rawItems,items,addItem ,removeItem,purchaseItems
}
})

頁面使用

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
<template>
<div>
{{title}}
<input type="text" v-model="itemName" />
<button class="add_btn" @click="addItemToCart">Add</button>
<ul>
<li v-for="item in items" :key="item" v-show="items.length!=0">
{{item.name}}
<button class="delete_btn" @click="removeItem(item.name)"
type="button">X</button>
</li>
</ul>
<button class="buy_btn"
@click="buy"
:disabled="!name">Buy</button>
<button
class="delete_btn"
@click="clearCart"
type="button"
data-testid="clear"
>Clear the cart</button>
</div>
</template>

<route>
{
path: '/aboutus',
name: 'AboutUs',
meta: {
layout: 'frontLayout',
}
}
</route>

<script setup lang="ts">
//引入pinia
import {storeToRefs} from 'pinia';
import {ref,onMounted} from 'vue'

//引入Store
import { useCartStore } from '../stores/cartTest'
import { useUserStore } from '../stores/userTest'
const title =ref<string>('關於我們');
//宣告
const cart = useCartStore();
const user = useUserStore();
//解構store
const { rawItems,items } = storeToRefs(cart)
const { addItem ,removeItem,purchaseItems } = cart;
const { name,isAdmin } = storeToRefs(user);
// const { logout,login } = user;

const itemName = ref<string>('');
const addItemToCart =()=>{
if (!itemName.value)
return
cart.addItem(itemName.value)
itemName.value = ''

}
const clearCart =()=>{
if (window.confirm('您確定要清空購物車嗎?')) {
rawItems.value = []
}
}
const buy =async()=>{
const n = await purchaseItems()
console.log(`買 ${n} items`)
rawItems.value = []
}
onMounted(() => {
// getItems()
});
</script>

組合式Api寫法

Cookie &localStorage & sessionStorage

存入資料setItem(‘資料名稱’:資料)

1
localStorage.setItem(‘Name',Lara)

在 DevTools Application 中查看 localStrong

1
sessionStorage.setItem(‘Name',Lara)

在 DevTools Application 中查看 sessionStrong

取出資料getItem(‘資料名稱’)

1
2
localStorage.getItem(‘Name')
sessionStorage.getItem(‘name')

移除資料removeItem(‘資料名稱’)

1
2
localStorage.removeItem(‘Name')
sessionStorage.removeItem(‘Name')

移除全部 clear()

1
2
localStorage.clear();
sessionStorage.clear();

存入localStorage(sessionStorage):必須將陣列轉字串=>JSON.stringify();localStorage.setItem(‘User’, JSON.stringify(數據))

錯誤示範:存入[object,object]

正確必須轉換成:JSON.stringify()才存入setItem

1
2
3
4
5
6
7
8
let user = [
{ name: 'Lara',
phone: '0911123456',
birthday:'2022-12-19'}
]
var userString = JSON.stringify(user)
localStorage.setItem('User', userString);
sessionStorage.setItem('User', userString);

將localStorage(sessionStorage)取出,將資料轉換回原本的格式 JSON.parse()

1
2
JSON.parse(localStorage.getItem('User'))
JSON.parse(sessionStorage.getItem('User'))

localStorage&sessionStorage 資料儲存的格式:皆需要為字串

localStorage:跨瀏覽器分頁做使用,資料無期效限制,資料將永久被保留

使用場景:帳號登入、購物車、遊戲分數,或任何其他伺服器應該記住的資訊
缺點:
1.約 5MB 的容量空間
2.資料儲存的格式 key 和 value 都只能接受「字串 」

sessionStorage:生命週期較短每次分頁或瀏覽器關閉,資料將被清空。

使用場景:判斷用戶是否登錄

安全考慮

並非所有數據都適合儲存在這些其中。
因為網站中存有XSS 風險,打開控制台,就可以隨意修改值,就能對你的localStorage 肆意妄為。
所以不要將敏感數據儲存在這其中。

Firebase 語法整理

ref路徑

1
2
import firebase from "@/config/firebaseConfig.js";
firebase.database().ref('schedules/');

child

讀取 on,once

1
2
3
4
5
6
const teacherLists = ref<any>({})
const getTeachers = () => {
teachers.once('value', (snapshot: { val: () => any; }) => {
teacherLists.value = snapshot.val();
})
}

讀取 on:程式會在背景待命,一旦資料變動的時候,就會重新印出新的結果,即時更新訊息的聊天室

1
2
3
4
myData.on('value', (snapshot)=>{
var data = snapshot.val()
console.log(data)
})

新增 set:直接取代當前路徑中的所有資料(具有破壞性)

1
2
3
4
5
let query = {
id: Math.floor(Date.now() / 1000),
title: scheduleForm.value.title,
}
firebase.database().ref('schedules/' + query.id).set(query);

新增 push:產生了一組隨機key值

1
2
var todos = db.ref('todos');
todos.push({content:'去看電影'});

update:只修改相關資料,不會直接取代路徑中的所有資料

1
2
3
4
5
6
7
8
9
10
11
var Data = {
name: name,
mail:mail,
phone:phone
}
// 分別將資料寫進資料庫'user/'底下
// 也會同時在'uses/'下開啟一個name的子節點,並且寫入相同的data
var updates = {};
updates['/users/' + newPostKey] = postData;
updates['/users/' + name + '/' + newPostKey] = postData;
return firebase.database().ref().update(updates);

remove

1
2
3
const toDelete = (item: any) => {
teachers.child(key).remove();
}

orderByChild 排序

orderByChild()依據資料某一個特定子節點來做排序
orderByKey 排序:不需要帶參數,資料少的排前面,資料大排後面
orderByValue 排序:不需要帶參數,升冪值進行排序

過濾條件:startAt, endAt, equalTo

startAt:比如說我們想要過濾掉,體重60公斤以下的人,可以使用startAt(60)。

1
2
3
4
5
userRef.orderByChild("weight").startAt(60).once("value",  (snapshot)=> {
snapshot.forEach( (item)=> {
console.log(item.val());
});
});

過濾條件endAt:過濾掉,體重80公斤以上的人,可以使用startAt(50)。

1
2
3
4
5
userRef.orderByChild("weight").endAt(80).once("value", (snapshot)=> {
snapshot.forEach(function (item) {
console.log(item.val());
});
});

過濾條件equalTo:等於:體重等於40公斤的人時,就要使用equalTo(50),想要知道是誰的話,可以加上key (item.key)

1
2
3
4
5
6
userRef.orderByChild("weight")
.equalTo(40).once("value", (snapshot)=> {
snapshot.forEach( (item)=> {
console.log(item.val(), item.key);
});
});

limitToFirst, limitToLast 等與限制筆數

limitToFirst:只需要其中第一筆資料,體重70大於公斤的會員第一筆資料

1
2
3
4
5
userRef.orderByChild("weight").startAt(50).limitToFirst(1).once("value",  (snapshot)=> {
snapshot.forEach( (item)=> {
console.log(item.val());
});
});

limitToLast:從最尾端起算的幾筆

Vite fullcalendar安裝與事件

Fullcalendar官網Vue3

安裝fullcalendar/vue3@5.9.0

1
2
npm install @fullcalendar/vue3@5.9.0
npm install --save @fullcalendar/core@5.9.0 @fullcalendar/daygrid@5.9.0 @fullcalendar/interaction@5.9.0 @fullcalendar/timegrid@5.9.0 @fullcalendar/list@5.9.0

src下新增event-utils.ts

設定初始事件

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
import { EventInput } from '@fullcalendar/vue3'

let eventGuid = 0
let todayStr = new Date().toISOString().replace(/T.*$/, '') // YYYY-MM-DD of today

export const INITIAL_EVENTS: EventInput[] = [
{
id: "001",
title: '週六去旅行',
start: '2022-12-03'+ 'T08:00:00'
},
{
id: "003",
title: '週日去審計新村',
start: '2022-12-04'+ 'T12:00:00'
},
{
id: "002",
title: '週日下午茶',
start: '2022-12-04'+ 'T16:00:00'
},
]

export function createEventId() {
return String(eventGuid++)
}

views/Home.vue

加入點擊物件

為您的活動輸入新標題

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
<template>

<div class="calendar_area">
<FullCalendar :options='calendarOptions'></FullCalendar>

<!--openScheduleDialog-->
<el-dialog class="openScheduleDialog" v-model="openScheduleDialog" :title="openScheduleDialogTitle">
<!--新增-->
<div>
<el-form ref="ruleFormRef" :model="scheduleForm" :rules="scheduleRules" class="search-ruleForm">
<div class="schedule_info">
<el-form-item label="課程主題" prop="title">
<el-input name="form_name" v-model="scheduleForm.title" placeholder="請輸入你的課程主題" />
</el-form-item>

<el-form-item label="整天" prop="allDay">
<el-radio-group v-model="scheduleForm.allDay">
<el-radio label="true" value="true">是</el-radio>
<el-radio label="false" value="false">否</el-radio>
</el-radio-group>
</el-form-item>

<div class="allDay_area" v-show="scheduleForm.allDay==='true'">
<div class="color_area">
<div class="isAllday">是否整天?</div>
<el-tabs v-model="scheduleForm.thisDay" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="是" name="true">
.
</el-tab-pane>
<el-tab-pane label="否" name="false">
<el-form-item label="課程結束時間">
<el-date-picker v-model="scheduleForm.endValue" type="date" placeholder="課程結束時間"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
{{ endValue }}
<span style="color:red">{{endValueErrorMessage}}</span>
</el-form-item>
</el-tab-pane>

</el-tabs>
<el-form-item style="display:none" label="當日" prop="thisDay">
<el-radio-group v-model="scheduleForm.thisDay" @click="thisDayClick">
<el-radio label="true" value="true">.</el-radio>
<el-radio label="false" value="false">
<el-date-picker v-model="scheduleForm.endValue" type="date" placeholder="課程結束時間"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
{{ endValue }}
<span style="color:red">{{endValueErrorMessage}}</span>
</el-radio>
</el-radio-group>
</el-form-item>

<el-form-item style="display:none" label="課程結束時間" v-show="!allDayShow">
<el-date-picker v-model="scheduleForm.endValue" type="date" placeholder="課程結束時間"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
{{ endValue }}
<span style="color:red">{{endValueErrorMessage}}</span>
</el-form-item>
</div>
<div class="color_area">
<el-form-item label="底色">
<el-color-picker v-model="scheduleForm.backgroundColor" show-alpha
:predefine="predefineColors" />
</el-form-item>
<el-form-item label="邊框">
<el-color-picker v-model="scheduleForm.borderColor" show-alpha
:predefine="predefineColors" />
</el-form-item>
<el-form-item label="文字顏色">
<el-color-picker v-model="scheduleForm.textColor" show-alpha
:predefine="predefineColors" />
</el-form-item>
</div>
</div>
<div v-show="scheduleForm.allDay === 'false'">
<el-form-item class="time_area">
<span>開始時間</span>
<el-select v-model="scheduleForm.conversionStartTime" class="startTime" placeholder="Select"
size="large">
<el-option v-for="(startTime,index) in 24" :key="index" :label="startTime +':00'"
:value="startTime + ':00'" @click="getStartTime(startTime)" />
</el-select>
<span>結束時間</span>
<el-select v-model="scheduleForm.conversionEndTime" class="endTime" placeholder="Select"
size="large">
<el-option v-for="(endTime,index) in 24" :key="index" :label="endTime +':00'"
:value="endTime + ':00'" @click="getEndTime(endTime)" />
</el-select>
<span style="color:red">{{ selectTimeErrorMessage }}</span>
</el-form-item>
<el-form-item label="邊框">
<el-color-picker v-model="scheduleForm.borderColor" show-alpha
:predefine="predefineColors" />
</el-form-item>

</div>
<div class="curriculum_content">
<el-form-item label="課程簡介" prop="content">
<el-input v-model="scheduleForm.content" type="textarea" />
</el-form-item>
</div>
<div class="btn_area">
<el-button @click="resetForm(ruleFormRef)">取消</el-button>
<el-button @click="saveSchedule" class="addSave" type="primary">新增</el-button>
</div>
</div>

</el-form>
</div>
</el-dialog>

<!--刪除-->
<el-dialog class="openDelScheduleDialog" v-model="openDelScheduleDialog" :title="openScheduleDialogTitle">

<div class="delDialog">
<div class="sure"><b>你確定要刪除{{ DeleteItem }}課程?</b></div>
<div class="info">
<div><span class="title">課程主題</span>{{ scheduleClickItem.title }}</div>
<div v-show="scheduleClickItem.allDay"><span class="allday">全天</span>是</div>
<div><span class="start">開始時間</span>{{ scheduleClickItem.start }}</div>
<div><span class="end">結束時間</span>{{ scheduleClickItem.end }}</div>
<span>課程描述</span>

<div class="info_content">{{ scheduleClickItem.content }}</div>
</div>
<div class="btn_area">
<el-button @click="openDelScheduleDialog =false">取消</el-button>
<el-button @click="deleteEventClick" class="addSave" type="primary">刪除</el-button>
</div>

</div>
</el-dialog>
</div>
</template>

<script setup lang='ts'>
import { ref, onMounted } from 'vue';

//FullCalendar引入
import '@fullcalendar/core/vdom' // solve problem with Vite
import FullCalendar, { CalendarOptions, EventApi, DateSelectArg, EventClickArg } from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import listPlugin from '@fullcalendar/list'
// Fullcalendar 初始化數據 event-utils
// import { INITIAL_EVENTS, createEventId } from '../event-utils'
//element 表單 Type
import type { FormInstance, FormRules } from 'element-plus';
//element 頁籤 Type
import type { TabsPaneContext } from 'element-plus';
//element 成功或失敗警告訊息 Type
import { ElMessage } from 'element-plus';

//firebase資料庫
import firebase from "@/config/firebaseConfig.js";
//moment時間套件
import moment from 'moment';

//element 表單驗證規則
const ruleFormRef = ref<FormInstance>();
const dialogTableVisible = ref<boolean>(false);
//刪除數據
const openEventClick = (clickInfo: EventClickArg) => {
dialogTableVisible.value = true;
// if (confirm(`你確定 '${clickInfo.event.title}'`)) {
// clickInfo.event.remove()
// }
};
//表單Type scheduleFormType
interface scheduleFormType{
id?: number |null |any,
title?: string | null,
start?: any | null,
thisDay?: any,
endValue?: any,
end?: any | null,
startTime?: any ,
endTime?: any,
conversionStartTime?: any,
conversionEndTime?: any,
startDay?: string | null,
allDay?: any | boolean |null,
backgroundColor?: string | null,
borderColor?: string | null,
textColor?: string | null,
content?: string | null,
}
//設置表單
const scheduleForm = ref<scheduleFormType>({
id: null,
title: '',
start: '',
end: '',
allDay: '',
backgroundColor: '#00ced1',
borderColor: '#ddd',
textColor: '#333',
content: '',
})
//顏色套件
const predefineColors = ref<string[]>([
'#333',
'#ffffff',
'#CACFD2',
'#009999',
'#33cccc',
'#33ccff',
'#6699ff',
'#3366ff',
'#6666ff',
'#9966ff',
'#9900cc',
'#cc3399',
'#cc6699',
'#ff6666',
'#ffcc00',
'#009900',
'#006600',
'#006699',
'#3366cc',
])
//scheduleForm 驗證
const scheduleRules = reactive<FormRules>({
title: [
{ required: true, message: '課程主題不能為空!', trigger: 'blur' },
],
thisDay: [
{ required: true, message: '不能為空!', trigger: 'change', },
],
})
//scheduleForm 驗證
const scheduleRules = reactive<FormRules>({
title: [
{ required: true, message: '課程主題不能為空!', trigger: 'blur' },
],
thisDay: [
{ required: true, message: '不能為空!', trigger: 'change', },
],
})


//新增事件
const handleDateSelect = (selectInfo: DateSelectArg) => {
let title = prompt('請為您的活動輸入新標題')
let calendarApi = selectInfo.view.calendar
calendarApi.unselect() //清除日期選擇
if (title) {
calendarApi.addEvent({
id: Math.floor(Date.now() / 1000),
title: scheduleForm.value.title,
start: moment.unix(scheduleForm.value.startTime).format("yyyy-MM-DDTHH:mm"),
end: moment.unix(scheduleForm.value.endTime).format("yyyy-MM-DDTHH:mm"),
backgroundColor: scheduleForm.value.backgroundColor,
borderColor: scheduleForm.value.borderColor,
textColor: scheduleForm.value.textColor,
content: scheduleForm.value.content,
})
}
}

//處理事件
const handleEvents = (events: EventApi[]) => {
//currentEvents = events
};

//獲取數據
const GetCalendar = () => {
calendarOptions.events = [];
//轉為陣列
}
//FullCalendar calendarOptions設置
const calendarOptions = ref<any>({
timeZone: 'UTC',
plugins: [
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
listPlugin,// needed for dateClick
],
// 頭標題
headerToolbar: {
//初始設置 left: 'prev,next today',
left: 'prev',
center: 'title',
right: 'next'
},
//螢幕尺寸767以下顯示
initialView: window.screen.width < 767 ? 'listWeek' : 'dayGridMonth',
//高度設置
height: window.screen.width < 767 ? 800 : 800,
//*****初始化,新增事件初始化改為空值陣列,
initialEvents: [],
//新增以後將獲得的事件陣列數據放回events,
events: scheduleCalendarList.value,
//時間設置
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
//刪除事件
eventClick: openDeleteEvent,
//新增事件
select: addSchedule,
editable: false,//允許編輯
droppable: false, //允許將事情放到日曆上
selectable: true, //允許選擇
selectMirror: true,//允許選擇鏡像
dayMaxEvents: true,//允許一天最多事件時就顯示彈出窗口
weekends: true,//顯示週末
eventsSet:handleEvents,
drop: dropEvents;
// 更新遠程數據庫
// eventAdd:
// eventChange:
// eventRemove:

})

//onMounted
onMounted(() => {
GetCalendar();
});
</script>

FullCalendar顯示在畫面時必須為陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Firebase 數據位置
const schedules = firebase.database().ref('schedules/');
//設置 scheduleCalendarList為陣列
const scheduleCalendarList = ref<any[]>([])
//once讀取數據
schedules.once('value', (snapshot: {
forEach(arg0: (item: any) => void): unknown; val: () => any;
}) => {
scheduleLists.value = snapshot.val();
//
snapshot.forEach((item: any) => {
scheduleCalendarList.value.push(item.val());
})
})

時間的轉換:moment.unix(scheduleForm.value.startTime).format(“yyyy-MM-DDTHH:mm”)

Date.parse(selectInfo.startStr) 轉為時間搓(timestamp)

讀取單一事件時必須獲取key

點擊獲取id=>讀取點擊的資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const scheduleClickItem = ref<any>({});
const DeleteId = ref<any>('');
const openDeleteEvent = (clickInfo: EventClickArg) => {
DeleteItem.value = clickInfo.event.title;
DeleteId.value = clickInfo.event.id;
console.log('點擊數據id', clickInfo.event.id,'點擊數據title', clickInfo.event.title,'點擊數據',clickInfo.event);
//Firebase 數據位置+DeleteId.value
const scheduleItem= firebase.database().ref('schedules/' + DeleteId.value);
//Firebase讀取此筆資料once
scheduleItem.once('value', (snapshot: { val: () => any; }) => {
scheduleClickItem.value = snapshot.val();
console.log('點擊獲得Key讀取這個點擊的數據', snapshot.val());
})
})

刪除事件

1
2
3
4
5
6
7
8
9
10
11
12
const deleteEventClick = () => {
//clickInfo.event.remove();
//Firebase刪除 remove():資料庫名.child(Key).remove()
schedules.child(DeleteId.value).remove()
//element 成功或失敗警告訊息
ElMessage({
message: '新增成功!',
type: 'success',
})
//刷新
parent.location.reload();
})

安裝fullcalendar/vue3@6.1.10

1
2
npm install @fullcalendar/vue3@6.1.10
npm install --save @fullcalendar/core@6.1.10 @fullcalendar/daygrid@6.1.10 @fullcalendar/interaction@6.1.10 @fullcalendar/timegrid@6.1.10 @fullcalendar/list@6.1.10

使用頁面

  • 獲取數據
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
<template>
<div class="title"><b>{{ pageName }}</b></div>
<div class="checkCalendar">
<!--搜尋-->
<div class="checkWorkData_means">
<div class="btns_group">
<!---模糊搜尋 綁定-->
<el-input class="mt-3" placeholder="search" v-model="checkWorkDataCalendarSearch" />
<i class="icon-ym icon-ym-search"></i>

</div>
</div>
<FullCalendar :options="calendarOptions"></FullCalendar>
</div>
</template>

<route lang="yaml">
meta:
layout: frontLayout
</route>

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import {storeToRefs } from 'pinia'
import FullCalendar from '@fullcalendar/vue3';
import { CalendarOptions , EventClickArg } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list';
import { usePageStore } from '../../stores/titleTag';

import { workCheckTYpe, getSetGroupType } from '../../types/workCheck'
const storePageTag = usePageStore();
const {btnPrintClose}= storeToRefs(storePageTag);
const {getPageClass,changeTitleTag} = storePageTag;
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
// "Authorization": 'Bearer ' + getToken
};
const pageClass=ref<string>('pageWorkCalendar');
localStorage.setItem('pageClass',pageClass.value);
const pageName = ref<string>('出勤行事曆');
//模糊搜尋綁定
const checkWorkDataCalendarSearch =ref<string>('');
const scheduleCalendarLists=ref<workCheckTYpe[]>([]);

//獲取所有數據
const allWorkLists = ref<workCheckTYpe[]>([]);
const getAllDayWork = async () => {
try {
const api = `${import.meta.env.VITE_API_DEFAULT_URL}/api/workChecks`;
const res = await fetch(api, { headers: headers, method: "GET" });
if (res.status === 200) {
allWorkLists.value = await res.json();
}
}
catch (err:any) {
ElMessage.error(err)
}
}

//過濾
const filterCheckWorkDataCalendar = computed(() =>{
let postList_array = allWorkLists.value;
console.log('allWorkLists',allWorkLists.value)
if(checkWorkDataCalendarSearch.value === '') {
return postList_array;
}
checkWorkDataCalendarSearch.value = checkWorkDataCalendarSearch.value.trim().toLowerCase();
postList_array = postList_array.filter(function (opt) {
if ( opt.title.toLowerCase().indexOf(checkWorkDataCalendarSearch.value) !== -1){
return opt;
}
})
return postList_array;
});

interface headerToolbarType{
left:string,
center:string,
right:string,
}
// web headerToolbar顯示方式
const webDisableHeaderToolbar=ref<headerToolbarType>({
left: 'prev',
center: 'title,dayGridMonth',
right: 'next'
})
// mobile headerToolbar顯示方式
const mobileDisableHeaderToolbar=ref<headerToolbarType>({
left: 'prev',
center: 'title,dayGridMonth,listWeek,timeGridDay',
right: 'next'
})
//
const calendarOptions = ref<any>({
timeZone: 'Asia/Taipei',
locale:'zh-TW',
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin,listPlugin],
headerToolbar:window.screen.width < 767?mobileDisableHeaderToolbar:webDisableHeaderToolbar ,
//螢幕尺寸767以下顯示
initialView: window.screen.width < 767 ? 'listWeek' : 'dayGridMonth',
//高度設置
height: window.screen.width < 767 ? 800 : 800,
//*****初始化,新增事件初始化改為空值陣列,
// initialEvents: scheduleCalendarList.value,
events: [],
//時間設置
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
//刪除事件
// eventClick: openDeleteEvent,
//新增事件
// select: addSchedule,
editable: false,//允許拖拉縮放
droppable: true, //允許將事情放到日曆上
selectable: true, //允許選擇
selectMirror: true,//允許選擇鏡像
dayMaxEvents: true,//允許一天最多事件時就顯示彈出窗口
weekends: true,//顯示週末
//eventsSet:handleEvents,
// drop: dropEvents;
// 更新遠程數據庫
// eventAdd:
// eventChange:
// eventRemove:
});
onMounted(() => {
getPageClass();
changeTitleTag(pageName.value);
getCalendar();
getAllDayWork();
calendarOptions.value.events=filterCheckWorkDataCalendar;
})
</script>