mirror of
https://github.com/KarolChang/jm-expense-vue-ts.git
synced 2024-12-26 11:48:35 +00:00
chore: vue-cli to vite
This commit is contained in:
parent
d89915f826
commit
3a335f578a
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "jm-expense-2022"
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,8 +5,10 @@
|
|||||||
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> -->
|
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> -->
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
||||||
|
<title>JM Expense</title>
|
||||||
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
||||||
<script>
|
<script>
|
||||||
sessionStorage.redirect = location.href;
|
sessionStorage.redirect = location.href;
|
||||||
@ -15,12 +17,17 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong
|
<!-- <strong
|
||||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
enable it to continue.</strong
|
enable it to continue.</strong
|
||||||
|
> -->
|
||||||
|
<strong
|
||||||
|
>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to
|
||||||
|
continue.</strong
|
||||||
>
|
>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
@ -21,6 +21,7 @@ git commit -m 'deploy'
|
|||||||
|
|
||||||
# if you are deploying to https://<USERNAME>.Github.io/<REPO>
|
# if you are deploying to https://<USERNAME>.Github.io/<REPO>
|
||||||
# git push -f https://github.com/<USERNAME>/<REPO>.git master:gh-pages
|
# git push -f https://github.com/<USERNAME>/<REPO>.git master:gh-pages
|
||||||
|
git push origin master
|
||||||
git push -f https://github.com/KarolChang/jm-expense-vue-ts.git master:gh-pages
|
git push -f https://github.com/KarolChang/jm-expense-vue-ts.git master:gh-pages
|
||||||
|
|
||||||
cd -
|
cd -
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": "dist",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,8 +5,10 @@
|
|||||||
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> -->
|
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> -->
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
|
||||||
|
<title>JM Expense</title>
|
||||||
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -20,12 +22,17 @@
|
|||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong
|
<!-- <strong
|
||||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
enable it to continue.</strong
|
enable it to continue.</strong
|
||||||
|
> -->
|
||||||
|
<strong
|
||||||
|
>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to
|
||||||
|
continue.</strong
|
||||||
>
|
>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
29174
package-lock.json
generated
29174
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -3,34 +3,38 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"lint": "vue-cli-service lint"
|
"serve": "vite preview",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueform/multiselect": "^2.3.1",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
|
"bootswatch": "^5.1.3",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"esbuild": "^0.14.28",
|
||||||
|
"esbuild-darwin-64": "^0.14.28",
|
||||||
"firebase": "^9.6.3",
|
"firebase": "^9.6.3",
|
||||||
|
"luxon": "^1.0.0",
|
||||||
"pinia": "^2.0.6",
|
"pinia": "^2.0.6",
|
||||||
"sweetalert2": "^11.3.0",
|
"sweetalert2": "^11.3.0",
|
||||||
"vue": "^3.2.27",
|
"vue": "^3.2.29",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-class-component": "^8.0.0-0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"vuefire": "^2.2.5"
|
"vue3-date-time-picker": "^2.4.4",
|
||||||
|
"weekstart": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^1.6.1",
|
||||||
|
"vite": "^2.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^4.18.0",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
|
||||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
|
||||||
"@vue/cli-service": "~4.5.0",
|
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.1.5"
|
||||||
|
31
src/App.vue
31
src/App.vue
@ -1,25 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Navbar from '@/components/Navbar.vue'
|
import Navbar from '@/components/Navbar.vue'
|
||||||
import { onLoad } from './cocos/config'
|
import { onLoad } from './cocos/config'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
// created
|
// created
|
||||||
|
if (import.meta.env.NODE_ENV === 'production') {
|
||||||
onLoad()
|
onLoad()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- <div> -->
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<template #main>
|
<template #main>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="fade-fast" mode="out-in">
|
<transition name="fade-fast" mode="out-in">
|
||||||
<component :is="Component" />
|
<component :is="Component" :key="route.fullPath" />
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
</template>
|
</template>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;700&display=swap');
|
||||||
|
|
||||||
@ -40,6 +43,19 @@ body {
|
|||||||
font-family: 'Zen Maru Gothic', sans-serif;
|
font-family: 'Zen Maru Gothic', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
display: block;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-orange {
|
||||||
|
color: coral;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-orange {
|
||||||
|
background-color: coral;
|
||||||
|
}
|
||||||
|
|
||||||
/* transition */
|
/* transition */
|
||||||
.slide-x-enter-active {
|
.slide-x-enter-active {
|
||||||
transition: transform 0.3s ease-out;
|
transition: transform 0.3s ease-out;
|
||||||
@ -101,4 +117,11 @@ body {
|
|||||||
.slide-right-leave-to {
|
.slide-right-leave-to {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<!-- <style src="@vueform/multiselect/themes/default.css"></style> -->
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { apiHelper } from './helper'
|
import { apiHelper } from './helper'
|
||||||
import { ExpenseInput } from '../models/Expense'
|
import { ExpenseInput, CategoryInput } from '@/models'
|
||||||
import { CategoryInput } from '../models/Category'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
expense: {
|
expense: {
|
||||||
@ -12,6 +11,12 @@ export default {
|
|||||||
},
|
},
|
||||||
create(data: ExpenseInput) {
|
create(data: ExpenseInput) {
|
||||||
return apiHelper.post('/expense/create', data)
|
return apiHelper.post('/expense/create', data)
|
||||||
|
},
|
||||||
|
edit(id: number, data: ExpenseInput) {
|
||||||
|
return apiHelper.put(`/expense/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return apiHelper.delete(`/expense/delete/${id}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
|
@ -5,10 +5,10 @@ export const apiHelper = axios.create({
|
|||||||
baseURL
|
baseURL
|
||||||
})
|
})
|
||||||
|
|
||||||
// const baseURL_Line = 'https://linebot20220114.herokuapp.com/'
|
const baseURL_Line = 'http://linebot20220114.herokuapp.com'
|
||||||
// export const apiHelperLine = axios.create({
|
export const apiHelperLine = axios.create({
|
||||||
// baseURL: baseURL_Line
|
baseURL: baseURL_Line
|
||||||
// })
|
})
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
type: string
|
type: string
|
||||||
|
7
src/apis/lineBot.ts
Normal file
7
src/apis/lineBot.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { apiHelperLine, LineInput } from './helper'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
push(data: LineInput) {
|
||||||
|
return apiHelperLine.post('/push', data)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { apiHelper, LineInput } from './helper'
|
import { apiHelper, LineInput } from './helper'
|
||||||
import { RecordInput } from '../models/Record'
|
import { RecordInput } from '@/models'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAll() {
|
getAll() {
|
||||||
@ -14,13 +14,13 @@ export default {
|
|||||||
edit(id: number, data: RecordInput) {
|
edit(id: number, data: RecordInput) {
|
||||||
return apiHelper.put(`/record/edit/${id}`, data)
|
return apiHelper.put(`/record/edit/${id}`, data)
|
||||||
},
|
},
|
||||||
close(data: { records: string; totalAmount: number; recorder: string }) {
|
delete(id: number) {
|
||||||
|
return apiHelper.delete(`/record/delete/${id}`)
|
||||||
|
},
|
||||||
|
close(data: { records: string; totalAmount: number; UserId: number }) {
|
||||||
return apiHelper.put('/close', data)
|
return apiHelper.put('/close', data)
|
||||||
},
|
},
|
||||||
getLogs() {
|
getLogs() {
|
||||||
return apiHelper.get('/log/all')
|
return apiHelper.get('/log/all')
|
||||||
},
|
|
||||||
pushLineMsg(data: LineInput) {
|
|
||||||
return apiHelper.post('/lineBot/push', data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
src/apis/user.ts
Normal file
53
src/apis/user.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { apiHelper } from './helper'
|
||||||
|
import { FirebaseUserInput, UserCreateInput, UserEditInput, RoleInput, PermissionInput } from '@/models'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
user: {
|
||||||
|
firebase_email_register(data: FirebaseUserInput) {
|
||||||
|
return apiHelper.post('/user/register', data)
|
||||||
|
},
|
||||||
|
create(data: UserCreateInput) {
|
||||||
|
return apiHelper.post('/user/create', data)
|
||||||
|
},
|
||||||
|
edit(id: number, data: UserEditInput) {
|
||||||
|
return apiHelper.put(`/user/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
getAll() {
|
||||||
|
return apiHelper.get('/user/all')
|
||||||
|
},
|
||||||
|
getUserByEmail(email: string) {
|
||||||
|
return apiHelper.get(`/user/${email}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
getAll() {
|
||||||
|
return apiHelper.get('/role/all')
|
||||||
|
},
|
||||||
|
getOne(id: number) {
|
||||||
|
return apiHelper.get(`/role/${id}`)
|
||||||
|
},
|
||||||
|
create(data: RoleInput) {
|
||||||
|
return apiHelper.post('/role/create', data)
|
||||||
|
},
|
||||||
|
edit(id: number, data: RoleInput) {
|
||||||
|
return apiHelper.put(`/role/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return apiHelper.delete(`/role/delete/${id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
getAll() {
|
||||||
|
return apiHelper.get('/permission/all')
|
||||||
|
},
|
||||||
|
create(data: PermissionInput) {
|
||||||
|
return apiHelper.post('/permission/create', data)
|
||||||
|
},
|
||||||
|
edit(id: number, data: PermissionInput) {
|
||||||
|
return apiHelper.put(`/permission/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return apiHelper.delete(`/permission/delete/${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
src/assets/logo1.png
Normal file
BIN
src/assets/logo1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 172 KiB |
BIN
src/assets/logo2.png
Normal file
BIN
src/assets/logo2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@ -9,10 +9,10 @@ export const onLoad = async function () {
|
|||||||
URLscheme[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
|
URLscheme[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CallParent('_alert', '我愛豬涵')
|
// CallParent('_alert', '我愛豬涵')
|
||||||
}
|
}
|
||||||
|
|
||||||
const CallParent = async function (method: string, ...param: any[]) {
|
export const CallParent = async function (method: string, ...param: any[]) {
|
||||||
const target: string = URLscheme['host']
|
const target: string = URLscheme['host']
|
||||||
if (target) {
|
if (target) {
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
|
92
src/components/DateFilter.vue
Normal file
92
src/components/DateFilter.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, Ref } from 'vue'
|
||||||
|
import { formatDate } from '@/utils/dateFormat'
|
||||||
|
import { DateFilterData } from '@/definition/interface'
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const dateFilterData = inject<Ref<DateFilterData>>('dateFilterData')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const format = (date: Date) => {
|
||||||
|
return formatDate(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStartDate = (date: Date) => {
|
||||||
|
if (date === null) {
|
||||||
|
dateFilterData.value.filter.startDate = ''
|
||||||
|
} else {
|
||||||
|
dateFilterData.value.filter.startDate = formatDate(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFinishDate = (date: Date) => {
|
||||||
|
if (date === null) {
|
||||||
|
dateFilterData.value.filter.finishDate = ''
|
||||||
|
} else {
|
||||||
|
dateFilterData.value.filter.finishDate = formatDate(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex m-2 ms-3">
|
||||||
|
<div class="form-check me-3">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="flexRadioDefault"
|
||||||
|
id="flexRadioDefault1"
|
||||||
|
@click="dateFilterData.searchMode = '月份'"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="flexRadioDefault1">月份搜尋</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="flexRadioDefault"
|
||||||
|
id="flexRadioDefault2"
|
||||||
|
@click="dateFilterData.searchMode = '日期'"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="flexRadioDefault2">日期搜尋</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex mb-3" v-if="dateFilterData.searchMode === '月份'">
|
||||||
|
<div class="ms-3 my-auto">
|
||||||
|
<select class="form-select" aria-label="Default select example" v-model="dateFilterData.filter.year">
|
||||||
|
<option v-for="n in 100" :key="n" :value="n + 2020">{{ n + 2020 }}年</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="ms-3 my-auto">
|
||||||
|
<select class="form-select" aria-label="Default select example" v-model="dateFilterData.filter.month">
|
||||||
|
<option v-for="n in 12" :key="n" :value="n">{{ n }}月</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="d-flex mb-3">
|
||||||
|
<div class="ms-3 my-auto">
|
||||||
|
<Datepicker
|
||||||
|
:modelValue="dateFilterData.filter.startDate"
|
||||||
|
@update:modelValue="setStartDate"
|
||||||
|
:format="format"
|
||||||
|
:previewFormat="format"
|
||||||
|
:enableTimePicker="false"
|
||||||
|
autoApply
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mx-2 my-auto">~</div>
|
||||||
|
<div class="my-auto">
|
||||||
|
<Datepicker
|
||||||
|
:modelValue="dateFilterData.filter.finishDate"
|
||||||
|
@update:modelValue="setFinishDate"
|
||||||
|
:format="format"
|
||||||
|
:previewFormat="format"
|
||||||
|
:enableTimePicker="false"
|
||||||
|
autoApply
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
65
src/components/FilterBox.vue
Normal file
65
src/components/FilterBox.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject, Ref, watch } from 'vue'
|
||||||
|
import { Category } from '@/models'
|
||||||
|
|
||||||
|
// data
|
||||||
|
const allChosen = ref<boolean>(true)
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
allCategories: number[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const categoriesByType = inject<Ref<Category[]>>('categoriesByType')!
|
||||||
|
const categoryFilters = inject<Ref<number[]>>('categoryFilters')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const handleCheckboxClick = (categoryId: number) => {
|
||||||
|
if (!categoryFilters.value.includes(categoryId)) {
|
||||||
|
categoryFilters.value.push(categoryId)
|
||||||
|
} else {
|
||||||
|
categoryFilters.value.forEach((id: number, index) => {
|
||||||
|
if (categoryId === id) {
|
||||||
|
categoryFilters.value.splice(index, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch
|
||||||
|
watch(allChosen, () => {
|
||||||
|
if (allChosen.value) {
|
||||||
|
categoryFilters.value = props.allCategories
|
||||||
|
} else {
|
||||||
|
categoryFilters.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-secondary me-3 p-3">
|
||||||
|
<div class="form-check form-switch mb-4">
|
||||||
|
<label class="form-check-label" for="flexSwitchCheckChecked">全選</label>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id="flexSwitchCheckChecked"
|
||||||
|
v-model="allChosen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2" v-for="(category, index) in categoriesByType" :key="index">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:value="category.id"
|
||||||
|
:id="category.name"
|
||||||
|
:checked="categoryFilters.includes(category.id)"
|
||||||
|
@click="handleCheckboxClick(category.id)"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="category.name">{{ category.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
148
src/components/Game/NumberAB.vue
Normal file
148
src/components/Game/NumberAB.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
interface GuessLog_AB {
|
||||||
|
number: number[]
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// variable
|
||||||
|
const nums: number[] = Array.from({ length: 10 }, (_, i) => i)
|
||||||
|
const answer: number[] = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const index = Math.floor(Math.random() * nums.length)
|
||||||
|
answer.push(nums[index])
|
||||||
|
nums.splice(index, 1)
|
||||||
|
}
|
||||||
|
console.log('answer', answer)
|
||||||
|
|
||||||
|
// data
|
||||||
|
const nowGuessNumberArr = ref<number[]>([])
|
||||||
|
const guessedNumber = ref<GuessLog_AB[]>([])
|
||||||
|
const isWinning = ref<boolean>(false)
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const numClick = (num: number) => {
|
||||||
|
if (nowGuessNumberArr.value.length >= 4) {
|
||||||
|
return alert('數字已滿!')
|
||||||
|
}
|
||||||
|
if (nowGuessNumberArr.value.includes(num)) {
|
||||||
|
return alert('數字重複!')
|
||||||
|
}
|
||||||
|
nowGuessNumberArr.value.push(num)
|
||||||
|
}
|
||||||
|
const enterClick = () => {
|
||||||
|
console.log('enterClick')
|
||||||
|
if (nowGuessNumberArr.value.length !== 4) {
|
||||||
|
return alert('數字未達四位!')
|
||||||
|
}
|
||||||
|
let ACount = 0
|
||||||
|
let BCount = 0
|
||||||
|
nowGuessNumberArr.value.forEach((num, i) => {
|
||||||
|
if (num === answer[i]) {
|
||||||
|
ACount += 1
|
||||||
|
} else if (answer.includes(num)) {
|
||||||
|
BCount += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
guessedNumber.value.push({
|
||||||
|
number: nowGuessNumberArr.value,
|
||||||
|
status: `${ACount ? ACount + 'A' : ''}${BCount ? BCount + 'B' : ''}`
|
||||||
|
})
|
||||||
|
// 判斷是否猜對
|
||||||
|
if (ACount === 4) {
|
||||||
|
isWinning.value = true
|
||||||
|
return alert(`恭喜答對!你真幸運~\n總次數:${guessedNumber.value.length}次`)
|
||||||
|
}
|
||||||
|
// 重置
|
||||||
|
nowGuessNumberArr.value = []
|
||||||
|
}
|
||||||
|
const resetClick = () => {
|
||||||
|
nowGuessNumberArr.value = []
|
||||||
|
guessedNumber.value = []
|
||||||
|
isWinning.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="container" v-if="!isWinning">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-sm-6">
|
||||||
|
<div class="d-flex justify-content-center mb-3">
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[0] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[1] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[2] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[3] }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center">
|
||||||
|
<div v-for="index in 10" :key="index" @click="numClick(index - 1)" class="num-btn">
|
||||||
|
{{ index - 1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center mb-3">
|
||||||
|
<div class="btn-customer reset-btn" v-if="isWinning" @click="resetClick">RESET</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="btn-customer back-btn col-6" @click="nowGuessNumberArr.pop()">BACK</div>
|
||||||
|
<div class="btn-customer enter-btn col-6" @click="enterClick">ENTER</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-sm-6">
|
||||||
|
<h4 class="text-center">GUESS RECORDS</h4>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item mx-auto" v-for="(obj, index) in guessedNumber" :key="index">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div :class="'num-block bg-' + obj.status[num]" v-for="num in obj.number" :key="num">{{ num }}</div>
|
||||||
|
<h3 class="ms-2">{{ obj.status }}</h3>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img v-else src="https://i.imgur.com/OmkEpSv.jpg" alt="" class="img-fluid" @click="resetClick" />
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.num-block {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-btn {
|
||||||
|
width: 15%;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-customer {
|
||||||
|
width: 35%;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 5%;
|
||||||
|
margin: 5px 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enter-btn {
|
||||||
|
background-color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background-color: darkturquoise;
|
||||||
|
}
|
||||||
|
</style>
|
158
src/components/Game/NumberWordle.vue
Normal file
158
src/components/Game/NumberWordle.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
type StatusColor = 'success' | 'warning' | 'secondary' | undefined
|
||||||
|
interface GuessLog {
|
||||||
|
number: number[]
|
||||||
|
status: StatusColor[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// variable
|
||||||
|
const nums: number[] = Array.from({ length: 10 }, (_, i) => i)
|
||||||
|
const answer: number[] = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const index = Math.floor(Math.random() * nums.length)
|
||||||
|
answer.push(nums[index])
|
||||||
|
nums.splice(index, 1)
|
||||||
|
}
|
||||||
|
console.log('answer', answer)
|
||||||
|
|
||||||
|
// data
|
||||||
|
const nowGuessNumberArr = ref<number[]>([])
|
||||||
|
const guessedNumberStatus = ref<StatusColor[]>(Array(10))
|
||||||
|
const guessedNumber = ref<GuessLog[]>([])
|
||||||
|
const isWinning = ref<boolean>(false)
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const numClick = (num: number) => {
|
||||||
|
if (nowGuessNumberArr.value.length >= 4) {
|
||||||
|
return alert('數字已滿!')
|
||||||
|
}
|
||||||
|
if (nowGuessNumberArr.value.includes(num)) {
|
||||||
|
return alert('數字重複!')
|
||||||
|
}
|
||||||
|
nowGuessNumberArr.value.push(num)
|
||||||
|
}
|
||||||
|
const enterClick = () => {
|
||||||
|
console.log('enterClick')
|
||||||
|
if (nowGuessNumberArr.value.length !== 4) {
|
||||||
|
return alert('數字未達四位!')
|
||||||
|
}
|
||||||
|
let successCount = 0
|
||||||
|
nowGuessNumberArr.value.forEach((num, i) => {
|
||||||
|
if (num === answer[i]) {
|
||||||
|
guessedNumberStatus.value[num] = 'success'
|
||||||
|
successCount += 1
|
||||||
|
} else if (answer.includes(num)) {
|
||||||
|
guessedNumberStatus.value[num] = 'warning'
|
||||||
|
} else {
|
||||||
|
guessedNumberStatus.value[num] = 'secondary'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
guessedNumber.value.push({
|
||||||
|
number: nowGuessNumberArr.value,
|
||||||
|
// status: guessedNumberStatus.value
|
||||||
|
status: [...guessedNumberStatus.value]
|
||||||
|
})
|
||||||
|
// 判斷是否猜對
|
||||||
|
if (successCount === 4) {
|
||||||
|
isWinning.value = true
|
||||||
|
return alert(`恭喜答對!你真幸運~\n總次數:${guessedNumber.value.length}次`)
|
||||||
|
}
|
||||||
|
// 重置
|
||||||
|
nowGuessNumberArr.value = []
|
||||||
|
}
|
||||||
|
const resetClick = () => {
|
||||||
|
nowGuessNumberArr.value = []
|
||||||
|
guessedNumber.value = []
|
||||||
|
guessedNumberStatus.value = Array(10)
|
||||||
|
isWinning.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="container" v-if="!isWinning">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-sm-6">
|
||||||
|
<div class="d-flex justify-content-center mb-3">
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[0] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[1] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[2] }}</div>
|
||||||
|
<div class="num-block">{{ nowGuessNumberArr[3] }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center">
|
||||||
|
<div
|
||||||
|
v-for="(status, index) in guessedNumberStatus"
|
||||||
|
:key="index"
|
||||||
|
@click="numClick(index)"
|
||||||
|
:class="`num-btn bg-${status ? status : 'light'}`"
|
||||||
|
>
|
||||||
|
{{ index }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center mb-3">
|
||||||
|
<div class="btn-customer reset-btn" v-if="isWinning" @click="resetClick">RESET</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="btn-customer back-btn col-6" @click="nowGuessNumberArr.pop()">BACK</div>
|
||||||
|
<div class="btn-customer enter-btn col-6" @click="enterClick">ENTER</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-sm-6">
|
||||||
|
<h4 class="text-center">GUESS RECORDS</h4>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item mx-auto" v-for="(obj, index) in guessedNumber" :key="index">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div :class="'num-block bg-' + obj.status[num]" v-for="num in obj.number" :key="num">{{ num }}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img v-else src="https://i.imgur.com/OmkEpSv.jpg" alt="" class="img-fluid" @click="resetClick" />
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.num-block {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-btn {
|
||||||
|
width: 15%;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-customer {
|
||||||
|
width: 35%;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 5%;
|
||||||
|
margin: 5px 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enter-btn {
|
||||||
|
background-color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background-color: darkturquoise;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
import expenseAPI from '../../apis/expense'
|
import expenseAPI from '@/apis/expense'
|
||||||
import { CategoryInput } from '../../models/Category'
|
import { CategoryInput } from '@/models'
|
||||||
|
|
||||||
// props
|
// inject
|
||||||
const props = defineProps<{
|
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||||
refetch: () => {}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const btnClick = async () => {
|
const btnClick = async () => {
|
||||||
@ -15,6 +14,17 @@ const btnClick = async () => {
|
|||||||
const { value: formValues } = await ConfirmBox.fire({
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
title: '新增類別',
|
title: '新增類別',
|
||||||
html: `
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-name" class="col-form-label">種類</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto my-auto">
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="swal-expense" value="支出" checked>
|
||||||
|
<label class="form-check-label me-3" for="swal-expense">支出</label>
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="swal-income" value="收入">
|
||||||
|
<label class="form-check-label" for="swal-income">收入</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="col-auto me-3">
|
<div class="col-auto me-3">
|
||||||
<label for="swal-name" class="col-form-label">名稱</label>
|
<label for="swal-name" class="col-form-label">名稱</label>
|
||||||
@ -44,6 +54,7 @@ const btnClick = async () => {
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
|
const type = (document.getElementById('swal-expense') as HTMLInputElement).checked
|
||||||
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||||
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
||||||
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
||||||
@ -56,6 +67,7 @@ const btnClick = async () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
input: {
|
input: {
|
||||||
|
type: type ? '支出' : '收入',
|
||||||
name,
|
name,
|
||||||
icon: icon === '' ? null : icon,
|
icon: icon === '' ? null : icon,
|
||||||
photoUrl: photoUrl === '' ? null : photoUrl
|
photoUrl: photoUrl === '' ? null : photoUrl
|
||||||
@ -73,12 +85,13 @@ const btnClick = async () => {
|
|||||||
|
|
||||||
const createExpense = async function (formValues: { input: CategoryInput }) {
|
const createExpense = async function (formValues: { input: CategoryInput }) {
|
||||||
try {
|
try {
|
||||||
await expenseAPI.category.create(formValues.input)
|
console.log('formValues.input', formValues.input)
|
||||||
|
const { data } = await expenseAPI.category.create(formValues.input)
|
||||||
|
refetchCategories()
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: `成功建立類別[${formValues.input.name}]`
|
title: `成功建立類別[${data.data.name}]`
|
||||||
})
|
})
|
||||||
props.refetch!()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
Toast.fire({
|
Toast.fire({
|
@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
import { inject } from 'vue'
|
||||||
import expenseAPI from '../../apis/expense'
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
import { Category } from '../../models/Category'
|
import expenseAPI from '@/apis/expense'
|
||||||
|
import { Category } from '@/models'
|
||||||
|
|
||||||
// props
|
// props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
refetch: () => {}
|
|
||||||
category: Category
|
category: Category
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const btnClick = async () => {
|
const btnClick = async () => {
|
||||||
try {
|
try {
|
||||||
@ -27,12 +30,14 @@ const btnClick = async () => {
|
|||||||
const deleteCategory = async function (id: number) {
|
const deleteCategory = async function (id: number) {
|
||||||
try {
|
try {
|
||||||
const { data } = await expenseAPI.category.delete(id)
|
const { data } = await expenseAPI.category.delete(id)
|
||||||
console.log('data', data)
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchCategories()
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: `成功刪除類別[${data.deletedCategory.name}]`
|
title: `成功刪除類別[${data.data.name}]`
|
||||||
})
|
})
|
||||||
props.refetch()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
@ -45,3 +50,9 @@ const deleteCategory = async function (id: number) {
|
|||||||
<template>
|
<template>
|
||||||
<i class="fas fa-trash" @click="btnClick"></i>
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
</template>
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(226, 19, 19);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,15 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
import expenseAPI from '../../apis/expense'
|
import expenseAPI from '@/apis/expense'
|
||||||
import { Category, CategoryInput } from '../../models/Category'
|
import { Category, CategoryInput } from '@/models'
|
||||||
|
|
||||||
// props
|
// props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
refetch: () => {}
|
|
||||||
category: Category
|
category: Category
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const btnClick = async () => {
|
const btnClick = async () => {
|
||||||
try {
|
try {
|
||||||
@ -17,6 +20,21 @@ const btnClick = async () => {
|
|||||||
const { value: formValues } = await ConfirmBox.fire({
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
title: '編輯類別',
|
title: '編輯類別',
|
||||||
html: `
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-name" class="col-form-label">種類</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto my-auto">
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="swal-expense" value="支出" ${
|
||||||
|
category.type === '支出' ? 'checked' : ''
|
||||||
|
}>
|
||||||
|
<label class="form-check-label me-3" for="swal-expense">支出</label>
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="swal-income" value="收入" ${
|
||||||
|
category.type === '收入' ? 'checked' : ''
|
||||||
|
}>
|
||||||
|
<label class="form-check-label" for="swal-income">收入</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="col-auto me-3">
|
<div class="col-auto me-3">
|
||||||
<label for="swal-name" class="col-form-label">名稱</label>
|
<label for="swal-name" class="col-form-label">名稱</label>
|
||||||
@ -55,6 +73,7 @@ const btnClick = async () => {
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
|
const type = (document.getElementById('swal-expense') as HTMLInputElement).checked
|
||||||
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||||
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
||||||
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
||||||
@ -67,6 +86,7 @@ const btnClick = async () => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
input: {
|
input: {
|
||||||
|
type: type ? '支出' : '收入',
|
||||||
name,
|
name,
|
||||||
icon: icon === '' ? null : icon,
|
icon: icon === '' ? null : icon,
|
||||||
photoUrl: photoUrl === '' ? null : photoUrl
|
photoUrl: photoUrl === '' ? null : photoUrl
|
||||||
@ -75,21 +95,24 @@ const btnClick = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (formValues) {
|
if (formValues) {
|
||||||
editCategoryName(formValues)
|
editCategory(formValues)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editCategoryName = async function (formValues: { input: CategoryInput }) {
|
const editCategory = async function (formValues: { input: CategoryInput }) {
|
||||||
try {
|
try {
|
||||||
await expenseAPI.category.edit(props.category.id, formValues.input)
|
const { data } = await expenseAPI.category.edit(props.category.id, formValues.input)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchCategories()
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: `成功建立類別[${formValues.input.name}]`
|
title: `成功編輯類別[${data.data.name}]`
|
||||||
})
|
})
|
||||||
props.refetch()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
Toast.fire({
|
Toast.fire({
|
120
src/components/ModalButton/Expense/CreateExpenseModalButton.vue
Normal file
120
src/components/ModalButton/Expense/CreateExpenseModalButton.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, computed, Ref } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import expenseAPI from '@/apis/expense'
|
||||||
|
import { ExpenseInput, Category } from '@/models'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchExpenses = inject<() => {}>('refetchExpenses')!
|
||||||
|
const categories = inject<Ref<Category[]>>('categories')!
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const selectionsHtml = computed(() => {
|
||||||
|
let html = `<select class="form-select" id="swal-categoryId">`
|
||||||
|
categories.value.forEach((category: Category) => (html += `<option value="${category.id}">${category.name}</option>`))
|
||||||
|
html += `</select>`
|
||||||
|
return html
|
||||||
|
})
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '新增記帳',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-categoryId" class="col-form-label">類別</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
${selectionsHtml.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-item" class="col-form-label">項目</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-item" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-amount" class="col-form-label">金額</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="number" id="swal-amount" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-note" class="col-form-label">備註</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-note" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-date" class="col-form-label">日期</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="date" id="swal-date" class="form-control" value="${new Date().toJSON().substring(0, 10)}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const categoryId = (document.getElementById('swal-categoryId') as HTMLInputElement).value
|
||||||
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
|
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
||||||
|
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
||||||
|
const note = (document.getElementById('swal-note') as HTMLInputElement).value
|
||||||
|
if (!item || !amount || !date) {
|
||||||
|
Swal.showValidationMessage('除了[備註],所有資料都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
UserId: store.currentUser?.id,
|
||||||
|
CategoryId: Number(categoryId),
|
||||||
|
item,
|
||||||
|
amount,
|
||||||
|
note,
|
||||||
|
date
|
||||||
|
} as ExpenseInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
createExpense(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createExpense = async function (formValues: { input: ExpenseInput }) {
|
||||||
|
try {
|
||||||
|
await expenseAPI.expense.create(formValues.input)
|
||||||
|
refetchExpenses()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功建立記帳!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '新增記帳失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
// fetchCategories()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button type="button" class="btn btn-primary me-3" @click="btnClick">新增記帳</button>
|
||||||
|
</template>
|
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import expenseAPI from '@/apis/expense'
|
||||||
|
import { Expense } from '@/models'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
expense: Expense
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchExpenses = inject<() => {}>('refetchExpenses')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
title: `確定刪除記帳[${props.expense.item}]嘛?`,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
deleteExpense(props.expense.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpense = async function (id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await expenseAPI.expense.delete(id)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchExpenses()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功刪除記帳[${data.data.item}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '刪除記帳失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(226, 19, 19);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
139
src/components/ModalButton/Expense/EditExpenseModalButton.vue
Normal file
139
src/components/ModalButton/Expense/EditExpenseModalButton.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, Ref } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import expenseAPI from '@/apis/expense'
|
||||||
|
import { Expense, ExpenseInput, Category } from '@/models'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
expense: Expense
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchExpenses = inject<() => {}>('refetchExpenses')!
|
||||||
|
const categories = inject<Ref<Category[]>>('categories')!
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const selectionsHtml = computed(() => {
|
||||||
|
let html = `<select class="form-select" id="swal-categoryId">`
|
||||||
|
categories.value.forEach((category: Category) => {
|
||||||
|
if (category.id === props.expense.Category.id) {
|
||||||
|
html += `<option selected value="${category.id}">${category.name}</option>`
|
||||||
|
} else {
|
||||||
|
html += `<option value="${category.id}">${category.name}</option>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
html += `</select>`
|
||||||
|
return html
|
||||||
|
})
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const expense = props.expense
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '編輯記帳',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-categoryId" class="col-form-label">類別</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
${selectionsHtml.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-item" class="col-form-label">項目</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${expense.item}" type="text" id="swal-item" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-amount" class="col-form-label">金額</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${expense.amount}" type="number" id="swal-amount" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-note" class="col-form-label">備註</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${expense.note}" type="text" id="swal-note" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-date" class="col-form-label">日期</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${new Date(expense.date)
|
||||||
|
.toJSON()
|
||||||
|
.substring(0, 10)}" type="date" id="swal-date" class="form-control">
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const categoryId = (document.getElementById('swal-categoryId') as HTMLInputElement).value
|
||||||
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
|
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
||||||
|
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
||||||
|
const note = (document.getElementById('swal-note') as HTMLInputElement).value
|
||||||
|
if (!item || !amount || !date) {
|
||||||
|
Swal.showValidationMessage('除了[備註],所有資料都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
UserId: store.currentUser?.id,
|
||||||
|
CategoryId: Number(categoryId),
|
||||||
|
item,
|
||||||
|
amount,
|
||||||
|
note,
|
||||||
|
date
|
||||||
|
} as ExpenseInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
editExpense(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editExpense = async function (formValues: { input: ExpenseInput }) {
|
||||||
|
try {
|
||||||
|
const { data } = await expenseAPI.expense.edit(props.expense.id, formValues.input)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchExpenses()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功編輯記帳[${data.data.item}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯記帳失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-edit" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, computed } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { PermissionInput, ActionTypePermission } from '@/models'
|
||||||
|
const actions: ActionTypePermission[] = ['查看', '新增', '編輯', '刪除', '停用', '操作']
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchPermissions = inject<() => {}>('refetchPermissions')!
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const selectionsHtml = computed(() => {
|
||||||
|
let html = `<select class="form-select" id="swal-action">`
|
||||||
|
actions.forEach((action: ActionTypePermission) => (html += `<option value="${action}">${action}</option>`))
|
||||||
|
html += `</select>`
|
||||||
|
return html
|
||||||
|
})
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '新增權限',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-action" class="col-form-label">ACTION</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
${selectionsHtml.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-item" class="col-form-label">ITEM</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-item" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-description" class="col-form-label">DESCRIPTION</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-description" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const action = (document.getElementById('swal-action') as HTMLInputElement).value
|
||||||
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
|
const description = (document.getElementById('swal-description') as HTMLInputElement).value
|
||||||
|
if (!action || !item) {
|
||||||
|
Swal.showValidationMessage('除了[DESCRIPTION],所有欄位都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
action,
|
||||||
|
item,
|
||||||
|
description
|
||||||
|
} as PermissionInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
createPermission(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPermission = async function (formValues: { input: PermissionInput }) {
|
||||||
|
try {
|
||||||
|
await userAPI.permission.create(formValues.input)
|
||||||
|
refetchPermissions()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功建立權限!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '新增權限失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button type="button" class="btn btn-primary" @click="btnClick">新增權限</button>
|
||||||
|
</template>
|
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Permission } from '@/models'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
permission: Permission
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchPermissions = inject<() => {}>('refetchPermissions')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
title: `確定刪除權限[${props.permission.action}-${props.permission.item}]嘛?`,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
deletePermission(props.permission.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePermission = async function (id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.permission.delete(id)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchPermissions()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功刪除權限[${data.data.action}-${data.data.item}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '刪除權限失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(226, 19, 19);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, computed } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Permission, PermissionInput, ActionTypePermission } from '@/models'
|
||||||
|
const actions: ActionTypePermission[] = ['查看', '新增', '編輯', '刪除', '停用', '操作']
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
permission: Permission
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchPermissions = inject<() => {}>('refetchPermissions')!
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const selectionsHtml = computed(() => {
|
||||||
|
let html = `<select class="form-select" id="swal-action">`
|
||||||
|
actions.forEach((action: ActionTypePermission) => {
|
||||||
|
if (action === props.permission.action) {
|
||||||
|
html += `<option selected value="${action}">${action}</option>`
|
||||||
|
} else {
|
||||||
|
html += `<option value="${action}">${action}</option>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
html += `</select>`
|
||||||
|
return html
|
||||||
|
})
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const permission = props.permission
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '編輯權限',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-action" class="col-form-label">ACTION</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
${selectionsHtml.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-item" class="col-form-label">ITEM</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${permission.item}" type="text" id="swal-item" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-description" class="col-form-label">DESCRIPTION</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${
|
||||||
|
permission.description === null ? '' : permission.description
|
||||||
|
}" type="text" id="swal-description" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const action = (document.getElementById('swal-action') as HTMLInputElement).value
|
||||||
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
|
const description = (document.getElementById('swal-description') as HTMLInputElement).value
|
||||||
|
if (!action || !item) {
|
||||||
|
Swal.showValidationMessage('除了[DESCRIPTION],所有資料都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
action,
|
||||||
|
item,
|
||||||
|
description
|
||||||
|
} as PermissionInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
editPermission(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editPermission = async function (formValues: { input: PermissionInput }) {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.permission.edit(props.permission.id, formValues.input)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchPermissions()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功編輯權限[${data.data.action}-${data.data.item}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯權限失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-edit" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,17 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
import recordAPI from '../../apis/record'
|
import recordAPI from '@/apis/record'
|
||||||
import { RecordInput } from '../../models/Record'
|
import { pushMsgToBoth } from '@/utils/lineBotMsg'
|
||||||
import { useStore } from '../../store/index'
|
import { RecordInput } from '@/models'
|
||||||
const store = useStore()
|
import { CallParent } from '@/cocos/config'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRecords = inject<() => {}>('refetchRecords')!
|
||||||
|
|
||||||
// props
|
// props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
view: 'Home' | 'Record'
|
view: 'Home' | 'Record'
|
||||||
refetch?: () => {}
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
@ -52,24 +57,13 @@ const btnClick = async () => {
|
|||||||
<input type="date" id="swal-date" class="form-control" value="${new Date().toJSON().substring(0, 10)}">
|
<input type="date" id="swal-date" class="form-control" value="${new Date().toJSON().substring(0, 10)}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-recorder" class="col-form-label">紀錄者</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<select id="swal-recorder" class="form-select">
|
|
||||||
<option selected>${store.currentUser}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
`,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
const merchant = (document.getElementById('swal-merchant') as HTMLInputElement).value
|
const merchant = (document.getElementById('swal-merchant') as HTMLInputElement).value
|
||||||
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
||||||
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
||||||
const recorder = (document.getElementById('swal-recorder') as HTMLInputElement).value
|
if (!item || !merchant || !amount || !date) {
|
||||||
if (!item || !merchant || !amount || !date || !recorder) {
|
|
||||||
Swal.showValidationMessage('所有資料都是必填!若紀錄者為空,請登入~')
|
Swal.showValidationMessage('所有資料都是必填!若紀錄者為空,請登入~')
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -78,7 +72,7 @@ const btnClick = async () => {
|
|||||||
merchant,
|
merchant,
|
||||||
amount,
|
amount,
|
||||||
date,
|
date,
|
||||||
recorder
|
UserId: store.currentUser?.id
|
||||||
} as RecordInput
|
} as RecordInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,11 +93,15 @@ const createRecord = async function (formValues: { input: RecordInput }) {
|
|||||||
title: '成功建立資料!'
|
title: '成功建立資料!'
|
||||||
})
|
})
|
||||||
if (props.view === 'Record') {
|
if (props.view === 'Record') {
|
||||||
props.refetch!()
|
refetchRecords()
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: 'Record' })
|
router.push({ name: 'Record' })
|
||||||
}
|
}
|
||||||
lineBotPush(formValues.input)
|
// lineBot push
|
||||||
|
pushMsgToBoth(
|
||||||
|
`${store.nickName}${store.icon}新增了一筆紀錄 →\n${formValues.input.merchant}-${formValues.input.item} ${formValues.input.amount}`
|
||||||
|
)
|
||||||
|
CallParent('Speak', `成功紀錄${formValues.input.amount}元`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
@ -112,26 +110,9 @@ const createRecord = async function (formValues: { input: RecordInput }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineBotPush = async (recordInput: RecordInput) => {
|
|
||||||
try {
|
|
||||||
const input = {
|
|
||||||
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
|
||||||
messages: {
|
|
||||||
type: 'text',
|
|
||||||
text: `${store.currentUser + store.icon}新增了一筆紀錄 →\n${recordInput.merchant}-${recordInput.item} $${
|
|
||||||
recordInput.amount
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await recordAPI.pushLineMsg(input)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-primary" @click="btnClick">新增資料</button>
|
<button type="button" class="btn btn-success" @click="btnClick">新增資料</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import recordAPI from '@/apis/record'
|
||||||
|
import { Record } from '@/models'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
record: Record
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRecords = inject<() => {}>('refetchRecords')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
title: `確定刪除紀錄[${props.record.item}]嘛?`,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
deleteRecord(props.record.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRecord = async function (id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await recordAPI.delete(id)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchRecords()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功刪除紀錄[${data.data.item}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '刪除紀錄失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(226, 19, 19);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
117
src/components/ModalButton/Record/EditRecordModalButton.vue
Normal file
117
src/components/ModalButton/Record/EditRecordModalButton.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import recordAPI from '@/apis/record'
|
||||||
|
import { pushMsgToBoth } from '@/utils/lineBotMsg'
|
||||||
|
import { Record, RecordInput } from '@/models'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
record: Record
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRecords = inject<() => {}>('refetchRecords')!
|
||||||
|
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const record = props.record
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '資料編輯',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-item" class="col-form-label">項目</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${record.item}" type="text" id="swal-item" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-merchant" class="col-form-label">商家</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${record.merchant}" type="text" id="swal-merchant" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-amount" class="col-form-label">金額</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${record.amount}" type="number" id="swal-amount" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-date" class="col-form-label">日期</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${new Date(record.date)
|
||||||
|
.toISOString()
|
||||||
|
.substring(0, 10)}" type="date" id="swal-date" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
||||||
|
const merchant = (document.getElementById('swal-merchant') as HTMLInputElement).value
|
||||||
|
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
||||||
|
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
||||||
|
if (!item || !merchant || !amount || !date) {
|
||||||
|
Swal.showValidationMessage('所有資料都是必填!若編輯者為空,請登入~')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: record.id as number,
|
||||||
|
input: {
|
||||||
|
item,
|
||||||
|
merchant,
|
||||||
|
amount,
|
||||||
|
date,
|
||||||
|
UserId: store.currentUser?.id
|
||||||
|
} as RecordInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
editRecord(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editRecord = async function (formValues: { id: number; input: RecordInput }) {
|
||||||
|
try {
|
||||||
|
await recordAPI.edit(formValues.id, formValues.input)
|
||||||
|
refetchRecords()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功編輯資料!'
|
||||||
|
})
|
||||||
|
// lineBot push
|
||||||
|
pushMsgToBoth(
|
||||||
|
`${store.nickName}${store.icon}編輯了一筆紀錄 →\n${formValues.input.merchant}-${formValues.input.item} $${formValues.input.amount}`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯資料失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-edit" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
75
src/components/ModalButton/Role/CreateRoleModalButton.vue
Normal file
75
src/components/ModalButton/Role/CreateRoleModalButton.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { RoleInput } from '@/models'
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRoles = inject<() => {}>('refetchRoles')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '新增角色',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-name" class="col-form-label">中文名稱</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-name" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-nameEn" class="col-form-label">英文名稱</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-nameEn" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||||
|
const name_en = (document.getElementById('swal-nameEn') as HTMLInputElement).value
|
||||||
|
if (!name || !name_en) {
|
||||||
|
Swal.showValidationMessage('所有欄位都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
name,
|
||||||
|
name_en
|
||||||
|
} as RoleInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
createExpense(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createExpense = async function (formValues: { input: RoleInput }) {
|
||||||
|
try {
|
||||||
|
await userAPI.role.create(formValues.input)
|
||||||
|
refetchRoles()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功建立角色!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '新增角色失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button type="button" class="btn btn-primary" @click="btnClick">新增角色</button>
|
||||||
|
</template>
|
58
src/components/ModalButton/Role/DeleteRoleModalButton.vue
Normal file
58
src/components/ModalButton/Role/DeleteRoleModalButton.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Role } from '@/models'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
role: Role
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRoles = inject<() => {}>('refetchRoles')!
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
title: `確定刪除角色[${props.role.name}]嘛?`,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
deleteRole(props.role.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRole = async function (id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.role.delete(id)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
refetchRoles()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功刪除角色[${data.data.name}]`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '刪除角色失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(226, 19, 19);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
87
src/components/ModalButton/Role/EditRoleModalButton.vue
Normal file
87
src/components/ModalButton/Role/EditRoleModalButton.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Role, RoleInput } from '@/models'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
role: Role
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRoles = inject<() => {}>('refetchRoles')!
|
||||||
|
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const record = props.role
|
||||||
|
const { value: formValues } = await ConfirmBox.fire({
|
||||||
|
title: '角色編輯',
|
||||||
|
html: `
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-name" class="col-form-label">中文名稱</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${record.name}" type="text" id="swal-name" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-nameEn" class="col-form-label">英文名稱</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${record.name_en}" type="text" id="swal-nameEn" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||||
|
const name_en = (document.getElementById('swal-nameEn') as HTMLInputElement).value
|
||||||
|
if (!name || !name_en) {
|
||||||
|
Swal.showValidationMessage('所有資料都是必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: record.id as number,
|
||||||
|
input: {
|
||||||
|
name,
|
||||||
|
name_en
|
||||||
|
} as RoleInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
editRole(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editRole = async function (formValues: { id: number; input: RoleInput }) {
|
||||||
|
try {
|
||||||
|
await userAPI.role.edit(formValues.id, formValues.input)
|
||||||
|
refetchRoles()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功編輯角色!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯角色失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-edit" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,16 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, provide } from 'vue'
|
||||||
import Sidebar from '@/components/Sidebar.vue'
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
|
import UserRP from '@/components/RightPanel/UserRP.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref<boolean>(false)
|
||||||
|
const userRPOpen = ref<boolean>(false)
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('sidebarOpen', sidebarOpen)
|
||||||
|
provide('userRPOpen', userRPOpen)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex">
|
<!-- 登入後 -->
|
||||||
|
<div class="d-flex" v-if="store.firebaseUser">
|
||||||
<transition name="slide-x">
|
<transition name="slide-x">
|
||||||
<Sidebar v-show="sidebarOpen" @open="sidebarOpen = false" />
|
<Sidebar v-show="sidebarOpen" @openUserRP="userRPOpen = true" />
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div class="m-2" style="width: 100vw">
|
<div class="m-2" style="width: 100vw">
|
||||||
@ -20,7 +29,14 @@ const sidebarOpen = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
<div class="m-3">
|
<div class="m-3">
|
||||||
<slot name="main" />
|
<slot name="main" />
|
||||||
|
<transition name="slide-right">
|
||||||
|
<UserRP v-show="userRPOpen" />
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 未登入 -->
|
||||||
|
<div v-else>
|
||||||
|
<slot name="main" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
83
src/components/RecordButtons.vue
Normal file
83
src/components/RecordButtons.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import recordAPI from '@/apis/record'
|
||||||
|
import CreateRecordModalButton from '@/components/ModalButton/Record/CreateRecordModalButton.vue'
|
||||||
|
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||||
|
import { pushMsgToBoth } from '@/utils/lineBotMsg'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
|
const emit = defineEmits(['closeFunction'])
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const refetchRecords = inject<() => {}>('refetchRecords')!
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
propData: {
|
||||||
|
isCloseStatus: boolean
|
||||||
|
closeRecords: number[]
|
||||||
|
closeRecordsAmount: number
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const toCloseBtnClick = async () => {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: '確定結算資料?',
|
||||||
|
text: `結算金額為 $${props.propData.closeRecordsAmount} [結算者: ${store.currentUser?.displayName}]`
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
closeRecord(props.propData.closeRecordsAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRecord = async (amount: number) => {
|
||||||
|
try {
|
||||||
|
if (store.currentUser) {
|
||||||
|
const { data } = await recordAPI.close({
|
||||||
|
records: props.propData.closeRecords.toString(),
|
||||||
|
totalAmount: amount,
|
||||||
|
UserId: store.currentUser?.id
|
||||||
|
})
|
||||||
|
emit('closeFunction', 'closeRecord')
|
||||||
|
refetchRecords()
|
||||||
|
if (data.data.recordsNotFound.length || data.data.recordsClosedBefore.length) {
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功結算資料!有部分資料未能找到或是已經結算過。'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功結算資料!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// lineBot push
|
||||||
|
pushMsgToBoth(`${store.nickName}${store.icon}結算紀錄 → 總金額 $${amount}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '結算資料失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="d-flex mb-3" style="width: 100vw">
|
||||||
|
<template v-if="!propData.isCloseStatus">
|
||||||
|
<CreateRecordModalButton view="Record" class="me-3" />
|
||||||
|
<button type="button" class="btn btn-danger" @click="emit('closeFunction', 'closeBtnClick')">開始結算</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="btn btn-info fw-bold">結算金額 ${{ propData.closeRecordsAmount }}</div>
|
||||||
|
<button type="button" class="btn btn-secondary ms-3" @click="emit('closeFunction', 'cancelBtnClick')">
|
||||||
|
取消結算
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success ms-3" @click="toCloseBtnClick">確定結算</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -1,65 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import expenseAPI from '../apis/expense'
|
|
||||||
import { Category } from '../models/Category'
|
|
||||||
import CreateCategoryModalButton from '@/components/modalButton/CreateCategoryModalButton.vue'
|
|
||||||
import DeleteCategoryModalButton from '@/components/modalButton/DeleteCategoryModalButton.vue'
|
|
||||||
import EditCategoryModalButton from '@/components/modalButton/EditCategoryModalButton.vue'
|
|
||||||
|
|
||||||
// data
|
|
||||||
const isLoading = ref<boolean>(false)
|
|
||||||
const categories = ref<Category[]>([])
|
|
||||||
|
|
||||||
// methods
|
|
||||||
const fetchCategories = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await expenseAPI.category.getAll()
|
|
||||||
categories.value = data.filter((item: Category) => item.deletedAt === null)
|
|
||||||
isLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// created
|
|
||||||
fetchCategories()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="panel" v-if="!isLoading">
|
|
||||||
<div class="d-flex flex-column flex-shrink-0 p-3 text-white bg-light" style="width: 250px; height: 100vh">
|
|
||||||
<h5 class="text-dark mb-3">
|
|
||||||
記帳類別
|
|
||||||
<CreateCategoryModalButton :refetch="fetchCategories" />
|
|
||||||
</h5>
|
|
||||||
<div class="scroll">
|
|
||||||
<ul class="list-group">
|
|
||||||
<li class="list-group-item d-flex mb-2 border" v-for="(category, index) in categories" :key="index">
|
|
||||||
<i :class="`${category.icon} fa-2x text-warning me-3`"></i>
|
|
||||||
<div class="text-nowrap">
|
|
||||||
<span>{{ category.name }}</span>
|
|
||||||
<EditCategoryModalButton :refetch="fetchCategories" :category="category" />
|
|
||||||
<DeleteCategoryModalButton class="ms-2" :refetch="fetchCategories" :category="category" />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style scoped>
|
|
||||||
#panel {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
i[class~='fa-trash']:hover {
|
|
||||||
color: rgb(226, 19, 19);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
64
src/components/RightPanel/CategoryRP.vue
Normal file
64
src/components/RightPanel/CategoryRP.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, Ref, ref } from 'vue'
|
||||||
|
import { Category } from '@/models'
|
||||||
|
import CreateCategoryModalButton from '@/components/ModalButton/Category/CreateCategoryModalButton.vue'
|
||||||
|
import DeleteCategoryModalButton from '@/components/ModalButton/Category/DeleteCategoryModalButton.vue'
|
||||||
|
import EditCategoryModalButton from '@/components/ModalButton/Category/EditCategoryModalButton.vue'
|
||||||
|
import RightPanel from '@/components/RightPanel/RightPanel.vue'
|
||||||
|
import type { CategoryType } from '@/models'
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const categories = inject<Ref<Category[]>>('categories')!
|
||||||
|
const categoryRPOpen = inject<Ref<boolean>>('categoryRPOpen')!
|
||||||
|
|
||||||
|
// data
|
||||||
|
const nowTab = ref<CategoryType>('支出')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RightPanel @RPOpen="categoryRPOpen = false">
|
||||||
|
<template #title>
|
||||||
|
<h5 class="text-dark mb-3">
|
||||||
|
記帳類別
|
||||||
|
<CreateCategoryModalButton />
|
||||||
|
</h5>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<ul class="nav nav-tabs mb-2">
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="nav-link" :class="{ active: nowTab === '支出' }" @click="nowTab = '支出'">支出</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="nav-link" :class="{ active: nowTab === '收入' }" @click="nowTab = '收入'">收入</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="scroll">
|
||||||
|
<ul class="list-group">
|
||||||
|
<template v-for="(category, index) in categories" :key="index">
|
||||||
|
<li class="list-group-item d-flex mb-2 border" v-if="category.type === nowTab">
|
||||||
|
<div class="col-2">
|
||||||
|
<i :class="`${category.icon} fa-2x text-warning`"></i>
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-nowrap text-start ms-3">
|
||||||
|
<span class="align-middle">{{ category.name }}</span>
|
||||||
|
<EditCategoryModalButton :category="category" class="align-middle" />
|
||||||
|
<DeleteCategoryModalButton class="ms-2 align-middle" :category="category" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RightPanel>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.scroll {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
32
src/components/RightPanel/RightPanel.vue
Normal file
32
src/components/RightPanel/RightPanel.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['RPOpen'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="panel">
|
||||||
|
<div class="d-flex flex-column flex-shrink-0 p-3 text-white bg-light" style="width: 250px; height: 100vh">
|
||||||
|
<i class="fas fa-chevron-right hide-icon" @click="emit('RPOpen', false)"></i>
|
||||||
|
<slot name="title" />
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
#panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 22px;
|
||||||
|
left: 25px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-icon:hover {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
</style>
|
131
src/components/RightPanel/UserRP.vue
Normal file
131
src/components/RightPanel/UserRP.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject, Ref } from 'vue'
|
||||||
|
import RightPanel from '@/components/RightPanel/RightPanel.vue'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { UserEditInput } from '@/models'
|
||||||
|
import { Toast } from '@/utils/swal'
|
||||||
|
import { useStore } from '@/store/index'
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const userRPOpen = inject<Ref<boolean>>('userRPOpen')!
|
||||||
|
|
||||||
|
// data
|
||||||
|
const editField = ref<'displayName' | 'photoURL' | ''>('')
|
||||||
|
const user = ref(new UserEditInput())
|
||||||
|
const isLoading = ref<boolean>(false)
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const editUser = async function () {
|
||||||
|
try {
|
||||||
|
if (store.currentUser) {
|
||||||
|
isLoading.value = true
|
||||||
|
const { data } = await userAPI.user.edit(store.currentUser.id, user.value)
|
||||||
|
store.getCurrentUser(store.currentUser.email)
|
||||||
|
if (data.status !== 'success') {
|
||||||
|
throw new Error(`[SERVER ERROR] ${data.message}`)
|
||||||
|
}
|
||||||
|
editField.value = ''
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功編輯個人資料!'
|
||||||
|
})
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯個人資料失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
if (store.currentUser) {
|
||||||
|
user.value = {
|
||||||
|
displayName: store.currentUser.displayName,
|
||||||
|
photoURL: store.currentUser.photoURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RightPanel @RPOpen="userRPOpen = false">
|
||||||
|
<template #title>
|
||||||
|
<h5 class="text-dark mb-3">個人資料</h5>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="text-dark fw-bold">
|
||||||
|
<span class="badge rounded-pill bg-success ms-2 mb-1">信箱</span>
|
||||||
|
<span class="ms-2">{{ store.currentUser?.email }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-dark fw-bold">
|
||||||
|
<span class="badge rounded-pill bg-success ms-2">名稱</span>
|
||||||
|
<span class="ms-2">
|
||||||
|
<template v-if="editField === 'displayName'">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control d-inline-block"
|
||||||
|
id="displayName"
|
||||||
|
style="width: 120px"
|
||||||
|
v-model="user.displayName"
|
||||||
|
/>
|
||||||
|
<div class="spinner-border spinner-border-sm text-success ms-2" role="status" v-if="isLoading">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<i v-else class="far fa-check-circle fa-lg ms-2" @click="editUser"></i>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ user.displayName }}</span>
|
||||||
|
<i class="far fa-edit ms-2" @click="editField = 'displayName'"></i>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-dark fw-bold">
|
||||||
|
<span class="badge rounded-pill bg-success ms-2">大頭貼(url)</span>
|
||||||
|
<span class="ms-2">
|
||||||
|
<template v-if="editField === 'photoURL'">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="input-group mt-2">
|
||||||
|
<textarea class="form-control" aria-label="With textarea" v-model="user.photoURL"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="spinner-border spinner-border-sm text-success ms-2 mt-4" role="status" v-if="isLoading">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<i v-else class="far fa-check-circle fa-lg ms-2 mt-4" @click="editUser"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<img :src="user.photoURL || ''" alt="photoURL" width="55" height="55" class="rounded-circle" />
|
||||||
|
<i class="far fa-edit ms-2" @click="editField = 'photoURL'"></i>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RightPanel>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
span {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[class~='badge'] {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
i:hover {
|
||||||
|
color: rgb(86, 116, 42);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,54 +1,89 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, inject, Ref } from 'vue'
|
||||||
import { routes } from '../router/index'
|
import router, { routes } from '../router/index'
|
||||||
import { useRoute, RouteRecordRaw } from 'vue-router'
|
import { useRoute, RouteRecordRaw } from 'vue-router'
|
||||||
import { useStore } from '../store/index'
|
import { useStore } from '../store/index'
|
||||||
import Swal from 'sweetalert2'
|
import { Toast } from '@/utils/swal'
|
||||||
import { ConfirmBox } from '../utils/swal'
|
import { getAuth, signOut } from 'firebase/auth'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const getUser = store.switchUser
|
const emit = defineEmits(['openUserRP'])
|
||||||
const emit = defineEmits(['open'])
|
|
||||||
|
|
||||||
|
// inject
|
||||||
|
const sidebarOpen = inject<Ref<boolean>>('sidebarOpen')!
|
||||||
|
|
||||||
|
// computed
|
||||||
const items = computed(() => {
|
const items = computed(() => {
|
||||||
return routes.filter((route: RouteRecordRaw) => route.meta?.show)
|
return routes.filter((route: RouteRecordRaw) => route.meta?.show)
|
||||||
})
|
})
|
||||||
|
|
||||||
const changeItem = () => {
|
// methods
|
||||||
emit('open', false)
|
const logout = () => {
|
||||||
|
const auth = getAuth()
|
||||||
|
signOut(auth)
|
||||||
|
.then(() => {
|
||||||
|
store.logout()
|
||||||
|
sidebarOpen.value = false
|
||||||
|
router.push({ name: 'Login' })
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '離開豬豬世界囉~'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('error', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchModalOpen = async () => {
|
const openUserRP = () => {
|
||||||
const { value: user } = await ConfirmBox.fire({
|
sidebarOpen.value = false
|
||||||
title: '切換使用者',
|
emit('openUserRP', true)
|
||||||
input: 'select',
|
|
||||||
inputOptions: { 建喵: '建喵', 豬涵: '豬涵' },
|
|
||||||
inputPlaceholder: 'who are you?',
|
|
||||||
showCancelButton: true
|
|
||||||
})
|
|
||||||
if (user) {
|
|
||||||
getUser(user)
|
|
||||||
Swal.fire(`使用者已切換至 [${user}]`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="d-flex flex-column flex-shrink-0 p-3 text-white bg-dark" style="width: 180px; height: 100vh">
|
<div class="d-flex flex-column flex-shrink-0 p-3 text-white bg-primary" style="width: 180px; height: 100vh">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'Home' }"
|
:to="{ name: 'Home' }"
|
||||||
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none"
|
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none"
|
||||||
>
|
>
|
||||||
<span class="fs-4"><i class="fas fa-star"></i> JM記帳 <i class="fas fa-star"></i></span>
|
<div class="fs-4 mx-auto"><i class="fas fa-star"></i> JM記帳 <i class="fas fa-star"></i></div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<hr />
|
<hr class="my-1" />
|
||||||
<ul class="nav nav-pills flex-column mb-auto">
|
<ul class="nav nav-pills mb-auto">
|
||||||
<li class="nav-item" v-for="(item, index) in items" :key="index">
|
<li class="nav-item" v-for="(item, index) in items" :key="index">
|
||||||
|
<template v-if="item.children?.length">
|
||||||
|
<button
|
||||||
|
class="nav-link fw-bold text-white"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
:data-bs-target="`#collapse-${index}`"
|
||||||
|
aria-expanded="false"
|
||||||
|
:aria-controls="`#collapse-${index}`"
|
||||||
|
>
|
||||||
|
<span class="me-3">{{ item.meta?.pageTitle }}</span>
|
||||||
|
<i class="fa-solid fa-angle-down"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse" :id="`collapse-${index}`">
|
||||||
|
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||||
|
<li v-for="child in item.children" :key="child.name">
|
||||||
<router-link
|
<router-link
|
||||||
|
:to="{ name: child.name }"
|
||||||
|
:class="'nav-link fw-bold ' + (child.name === route.name ? 'text-danger' : 'text-white')"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
>{{ child.meta?.pageTitle }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
:to="{ name: item.name }"
|
:to="{ name: item.name }"
|
||||||
:class="'nav-link ' + (item.name === route.name ? 'text-info' : 'text-white')"
|
:class="'nav-link fw-bold ' + (item.name === route.name ? 'text-danger' : 'text-white')"
|
||||||
@click="changeItem"
|
@click="sidebarOpen = false"
|
||||||
>{{ item.meta?.pageTitle }}</router-link
|
>{{ item.meta?.pageTitle }}</router-link
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
@ -63,32 +98,24 @@ const switchModalOpen = async () => {
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="store.currentUser === '建喵'"
|
:src="store.firebaseUser?.photoURL || '../assets/capoo.gif'"
|
||||||
src="../assets/jianmiau-login.png"
|
alt="photo"
|
||||||
alt=""
|
width="55"
|
||||||
width="50"
|
|
||||||
height="55"
|
height="55"
|
||||||
class="rounded-circle me-2"
|
class="rounded-circle me-2"
|
||||||
/>
|
/>
|
||||||
<img
|
<strong>{{ store.firebaseUser?.displayName }}</strong>
|
||||||
v-else-if="store.currentUser === '豬涵'"
|
|
||||||
src="../assets/karol-login.png"
|
|
||||||
alt=""
|
|
||||||
width="50"
|
|
||||||
height="55"
|
|
||||||
class="rounded-circle me-2"
|
|
||||||
/>
|
|
||||||
<img v-else src="../assets/capoo.gif" alt="" width="50" height="55" class="rounded-circle me-2" />
|
|
||||||
<strong>{{ store.currentUser ? store.currentUser : '未登入' }}</strong>
|
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
|
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
|
||||||
<!-- <li><a class="dropdown-item" href="#">New project...</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Settings</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Profile</a></li> -->
|
|
||||||
<!-- <li><hr class="dropdown-divider" /></li> -->
|
<!-- <li><hr class="dropdown-divider" /></li> -->
|
||||||
<li><a class="dropdown-item" @click="switchModalOpen">切換帳號</a></li>
|
<li><a class="dropdown-item" @click="openUserRP">個人資料</a></li>
|
||||||
|
<li><a class="dropdown-item" @click="logout">登出</a></li>
|
||||||
|
<!-- <li><a class="dropdown-item" @click="switchModalOpen">切換帳號</a></li> -->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <transition name="slide-right">
|
||||||
|
<UserRP v-show="userRPOpen" />
|
||||||
|
</transition> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
26
src/components/global.ts
Normal file
26
src/components/global.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// import Vue from 'vue'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from '@/App.vue'
|
||||||
|
|
||||||
|
function capitalizeFirstLetter(string: string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireComponent = require.context(
|
||||||
|
'.',
|
||||||
|
false,
|
||||||
|
/\.vue$/
|
||||||
|
//找到components文件夾下以.vue命名的文件
|
||||||
|
)
|
||||||
|
|
||||||
|
requireComponent.keys().forEach((fileName) => {
|
||||||
|
const componentConfig = requireComponent(fileName)
|
||||||
|
|
||||||
|
const componentName = capitalizeFirstLetter(
|
||||||
|
fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
|
||||||
|
//因爲得到的filename格式是: './dataList.vue', 所以這裏我們去掉頭和尾,只保留真正的文件名
|
||||||
|
)
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.component(componentName, componentConfig.default || componentConfig)
|
||||||
|
})
|
11
src/definition/interface.ts
Normal file
11
src/definition/interface.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { SearchMode } from './type'
|
||||||
|
|
||||||
|
export interface DateFilterData {
|
||||||
|
searchMode: SearchMode
|
||||||
|
filter: {
|
||||||
|
year: number
|
||||||
|
month: number
|
||||||
|
startDate: string
|
||||||
|
finishDate: string
|
||||||
|
}
|
||||||
|
}
|
1
src/definition/type.ts
Normal file
1
src/definition/type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type SearchMode = '月份' | '日期'
|
30
src/firebase/auth.ts
Normal file
30
src/firebase/auth.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { getAuth, onAuthStateChanged, User as FirebaseUser } from 'firebase/auth'
|
||||||
|
import { useStore } from '../store/index'
|
||||||
|
|
||||||
|
// export const getFirebaseUser = () => {
|
||||||
|
// onAuthStateChanged(getAuth(), (user: FirebaseUser | null) => {
|
||||||
|
// const store = useStore()
|
||||||
|
// if (user) {
|
||||||
|
// console.log('[auth] Get Firebase User', user)
|
||||||
|
// store.login(user)
|
||||||
|
// } else {
|
||||||
|
// store.logout()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const getFirebaseUser = () => {
|
||||||
|
const store = useStore()
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
onAuthStateChanged(getAuth(), async (user: FirebaseUser | null) => {
|
||||||
|
console.log(`[firebase] onAuthStateChanged`)
|
||||||
|
if (user) {
|
||||||
|
console.log('[auth] Get Firebase User', user)
|
||||||
|
store.login(user)
|
||||||
|
} else {
|
||||||
|
store.logout()
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
15
src/firebase/config.ts
Normal file
15
src/firebase/config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { initializeApp } from 'firebase/app'
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||||
|
authDomain: import.meta.env.VITE_FIREBASE_AUTO_DOMAIN,
|
||||||
|
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||||
|
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||||
|
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
||||||
|
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initFirebase = () => {
|
||||||
|
initializeApp(firebaseConfig)
|
||||||
|
}
|
21
src/main.ts
21
src/main.ts
@ -2,9 +2,26 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { firestorePlugin } from 'vuefire'
|
import { getFirebaseUser } from '@/firebase/auth'
|
||||||
|
import { getAuth, onAuthStateChanged, User as FirebaseUser } from 'firebase/auth'
|
||||||
|
import { initFirebase } from '@/firebase/config'
|
||||||
|
import Datepicker from 'vue3-date-time-picker'
|
||||||
|
import 'vue3-date-time-picker/dist/main.css'
|
||||||
|
|
||||||
import 'bootstrap'
|
import 'bootstrap'
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
import 'bootswatch/dist/minty/bootstrap.min.css'
|
||||||
|
|
||||||
createApp(App).use(router).use(createPinia()).mount('#app')
|
console.log('[main] Initialize Firebase')
|
||||||
|
initFirebase()
|
||||||
|
|
||||||
|
const startApp = onAuthStateChanged(getAuth(), async (user: FirebaseUser | null) => {
|
||||||
|
const app = createApp(App)
|
||||||
|
console.log('[main] Initialize Vue App')
|
||||||
|
app.use(createPinia())
|
||||||
|
await getFirebaseUser()
|
||||||
|
app.use(router)
|
||||||
|
app.component('Datepicker', Datepicker)
|
||||||
|
app.mount('#app')
|
||||||
|
startApp()
|
||||||
|
})
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
|
export type CategoryType = '支出' | '收入'
|
||||||
|
|
||||||
export class Category {
|
export class Category {
|
||||||
id!: number
|
id!: number
|
||||||
name!: string
|
name!: string
|
||||||
icon?: string
|
icon!: string
|
||||||
photoUrl?: string
|
// photoUrl?: string
|
||||||
deletedAt?: Date | null
|
type!: CategoryType
|
||||||
|
deletedAt!: Date | null
|
||||||
createdAt!: Date
|
createdAt!: Date
|
||||||
updatedAt!: Date
|
updatedAt!: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CategoryInput {
|
export class CategoryInput {
|
||||||
name!: string
|
name!: string
|
||||||
icon?: string
|
type!: CategoryType
|
||||||
photoUrl?: string
|
icon!: string
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { Category } from './Category'
|
import { Category } from './Category'
|
||||||
|
import { User } from './User'
|
||||||
|
|
||||||
export class Expense {
|
export class Expense {
|
||||||
id!: number
|
id!: number
|
||||||
date!: Date
|
date!: Date
|
||||||
item?: string
|
item!: string
|
||||||
amount!: number
|
amount!: number
|
||||||
note?: string
|
note?: string
|
||||||
deletedAt?: Date | null
|
deletedAt!: Date | null
|
||||||
createdAt!: Date
|
createdAt!: Date
|
||||||
updatedAt!: Date
|
updatedAt!: Date
|
||||||
Category!: Category
|
Category!: Category
|
||||||
|
User!: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpenseInput {
|
export class ExpenseInput {
|
||||||
@ -18,4 +20,5 @@ export class ExpenseInput {
|
|||||||
amount!: string
|
amount!: string
|
||||||
note?: string
|
note?: string
|
||||||
CategoryId!: number
|
CategoryId!: number
|
||||||
|
UserId!: number
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
export interface MessageInput {
|
|
||||||
type: string
|
|
||||||
// text
|
|
||||||
text?: string
|
|
||||||
// sticker
|
|
||||||
packageId?: string
|
|
||||||
stickerId?: string
|
|
||||||
// image or video
|
|
||||||
originalContentUrl?: string
|
|
||||||
previewImageUrl?: string
|
|
||||||
}
|
|
@ -1,9 +1,8 @@
|
|||||||
export type RecorderType = '建喵' | '豬涵'
|
import { User } from './User'
|
||||||
|
import { Record } from './Record'
|
||||||
export type ActionType = '新增' | '編輯' | '結算'
|
type ActionType = '新增' | '編輯' | '結算'
|
||||||
|
|
||||||
export class Log {
|
export class Log {
|
||||||
recorder!: RecorderType
|
|
||||||
action!: ActionType
|
action!: ActionType
|
||||||
item?: string
|
item?: string
|
||||||
merchant?: string
|
merchant?: string
|
||||||
@ -18,4 +17,6 @@ export class Log {
|
|||||||
RecordIds?: string
|
RecordIds?: string
|
||||||
createdAt!: Date
|
createdAt!: Date
|
||||||
updatedAt!: Date
|
updatedAt!: Date
|
||||||
|
User!: User
|
||||||
|
Records!: Record[]
|
||||||
}
|
}
|
||||||
|
20
src/models/Permission.ts
Normal file
20
src/models/Permission.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Role } from '@/models/index'
|
||||||
|
|
||||||
|
export type ActionTypePermission = '查看' | '新增' | '編輯' | '刪除' | '停用' | '操作'
|
||||||
|
|
||||||
|
export class Permission {
|
||||||
|
id!: number
|
||||||
|
action!: ActionTypePermission
|
||||||
|
item!: string
|
||||||
|
description!: string
|
||||||
|
deletedAt!: Date | null
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
Roles!: Role[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PermissionInput {
|
||||||
|
action!: ActionTypePermission
|
||||||
|
item!: string
|
||||||
|
description!: string
|
||||||
|
}
|
@ -1,16 +1,17 @@
|
|||||||
export type RecorderType = '建喵' | '豬涵'
|
// export type RecorderType = '建喵' | '豬涵'
|
||||||
|
import { User } from './User'
|
||||||
|
|
||||||
export class Record {
|
export class Record {
|
||||||
id!: number
|
id!: number
|
||||||
date!: Date
|
date!: Date
|
||||||
item?: string
|
item!: string
|
||||||
merchant?: string
|
merchant!: string
|
||||||
amount!: number
|
amount!: number
|
||||||
recorder?: RecorderType
|
isClosed!: boolean
|
||||||
isClosed?: boolean
|
deletedAt!: Date | null
|
||||||
deletedAt?: Date | null
|
|
||||||
createdAt!: Date
|
createdAt!: Date
|
||||||
updatedAt!: Date
|
updatedAt!: Date
|
||||||
|
User!: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordInput {
|
export class RecordInput {
|
||||||
@ -18,6 +19,5 @@ export class RecordInput {
|
|||||||
item!: string
|
item!: string
|
||||||
merchant!: string
|
merchant!: string
|
||||||
amount!: string
|
amount!: string
|
||||||
recorder?: RecorderType
|
UserId!: number
|
||||||
editor?: RecorderType
|
|
||||||
}
|
}
|
||||||
|
16
src/models/Role.ts
Normal file
16
src/models/Role.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Permission } from '@/models/index'
|
||||||
|
|
||||||
|
export class Role {
|
||||||
|
id!: number
|
||||||
|
name!: string
|
||||||
|
name_en!: string
|
||||||
|
deletedAt!: Date | null
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
Permissions!: Permission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoleInput {
|
||||||
|
name!: string
|
||||||
|
name_en!: string
|
||||||
|
}
|
35
src/models/User.ts
Normal file
35
src/models/User.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Record, Log, Expense, Role } from '@/models/index'
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id!: number
|
||||||
|
email!: string
|
||||||
|
displayName!: string
|
||||||
|
photoURL!: string
|
||||||
|
firebaseUid!: string
|
||||||
|
active!: boolean
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
Records!: Record[]
|
||||||
|
Logs!: Log[]
|
||||||
|
Expenses!: Expense[]
|
||||||
|
Role!: Role
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FirebaseUserInput {
|
||||||
|
email!: string
|
||||||
|
password!: string
|
||||||
|
displayName!: string
|
||||||
|
photoURL!: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserCreateInput {
|
||||||
|
email!: string
|
||||||
|
displayName!: string
|
||||||
|
photoURL!: string | null
|
||||||
|
firebaseUid!: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserEditInput {
|
||||||
|
displayName?: string
|
||||||
|
photoURL?: string
|
||||||
|
}
|
9
src/models/index.ts
Normal file
9
src/models/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { Record, RecordInput } from './Record'
|
||||||
|
export { Log } from './Log'
|
||||||
|
export { Expense, ExpenseInput } from './Expense'
|
||||||
|
export { Category, CategoryInput } from './Category'
|
||||||
|
export type { CategoryType } from './Category'
|
||||||
|
export { User, FirebaseUserInput, UserCreateInput, UserEditInput } from './User'
|
||||||
|
export { Role, RoleInput } from './Role'
|
||||||
|
export { Permission, PermissionInput } from './Permission'
|
||||||
|
export type { ActionTypePermission } from './Permission'
|
@ -1,4 +1,8 @@
|
|||||||
|
import { Toast } from '@/utils/swal'
|
||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||||
|
import { useStore } from '../store/index'
|
||||||
|
import Default from '@/views/Default.vue'
|
||||||
|
|
||||||
const root = '/jm-expense-vue-ts'
|
const root = '/jm-expense-vue-ts'
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
@ -6,6 +10,24 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
redirect: `${root}/`
|
redirect: `${root}/`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/register`,
|
||||||
|
name: 'Register',
|
||||||
|
component: () => import('../views/Register.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '註冊頁',
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/login`,
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('../views/Login.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '登入頁',
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `${root}/`,
|
path: `${root}/`,
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@ -51,6 +73,93 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
show: true
|
show: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/tools`,
|
||||||
|
name: 'Tools',
|
||||||
|
component: () => import('../views/Tools.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '小工具',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/game`,
|
||||||
|
name: 'Game',
|
||||||
|
component: () => import('../views/Game.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '小遊戲',
|
||||||
|
show: true
|
||||||
|
// auth: ['root', 'admin', 'member']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/admin`,
|
||||||
|
name: 'Admin',
|
||||||
|
redirect: { name: 'Admin-Role' },
|
||||||
|
component: Default,
|
||||||
|
meta: {
|
||||||
|
pageTitle: '管理面板',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: 'role',
|
||||||
|
// name: 'Admin-Role',
|
||||||
|
// component: () => import('../views/Role.vue'),
|
||||||
|
// meta: {
|
||||||
|
// pageTitle: '角色管理',
|
||||||
|
// show: true
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: 'permission',
|
||||||
|
// name: 'Admin-Permission',
|
||||||
|
// component: () => import('../views/Permission.vue'),
|
||||||
|
// meta: {
|
||||||
|
// pageTitle: '權限管理',
|
||||||
|
// show: true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/role',
|
||||||
|
name: 'Admin-Role',
|
||||||
|
component: () => import('@/views/Role.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '角色管理',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: ':id/access',
|
||||||
|
// name: 'Admin-Role-Access',
|
||||||
|
// component: () => import('@/views/Access.vue'),
|
||||||
|
// meta: {
|
||||||
|
// pageTitle: '角色管理 / 設置權限[角色名稱]',
|
||||||
|
// show: false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/role/:id/access',
|
||||||
|
name: 'Admin-Role-Access',
|
||||||
|
component: () => import('@/views/Access.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '角色管理 / 設置權限[角色名稱]',
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/permission',
|
||||||
|
name: 'Admin-Permission',
|
||||||
|
component: () => import('../views/Permission.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '權限管理',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
@ -63,4 +172,28 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const store = useStore()
|
||||||
|
console.log('[router]firebaseUser', store.firebaseUser)
|
||||||
|
console.log('[router]currentUser ID', store.currentUser?.id)
|
||||||
|
// 目前不能使用 currentUser
|
||||||
|
if (!store.firebaseUser) {
|
||||||
|
if (to.name !== 'Login' && to.name !== 'Register') {
|
||||||
|
next({ name: 'Login' })
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '請先登入'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (to.name === 'Login' || to.name === 'Register') {
|
||||||
|
next({ name: 'Home' })
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
@ -1,16 +1,55 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { User as FirebaseUser } from 'firebase/auth'
|
||||||
|
import { User } from '@/models'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
|
||||||
export const useStore = defineStore('index', {
|
export const useStore = defineStore('index', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
currentUser: localStorage.getItem('jm-user')
|
// currentUser: localStorage.getItem('jm-user')
|
||||||
|
firebaseUser: null as null | FirebaseUser,
|
||||||
|
currentUser: null as null | User
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
icon: (state) => (state.currentUser === '豬涵' ? '🐷' : '🐣')
|
nickName: (state) => {
|
||||||
|
if (state.currentUser?.email === 'super000999888@gmail.com') {
|
||||||
|
return '豬涵'
|
||||||
|
} else if (state.currentUser?.email === 'bir840124@gmail.com') {
|
||||||
|
return '建喵'
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: (state) => {
|
||||||
|
if (state.currentUser?.email === 'super000999888@gmail.com') {
|
||||||
|
return '🐷'
|
||||||
|
} else if (state.currentUser?.email === 'bir840124@gmail.com') {
|
||||||
|
return '🐣'
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
switchUser(user: string) {
|
// switchUser(user: string) {
|
||||||
localStorage.setItem('jm-user', user)
|
// localStorage.setItem('jm-user', user)
|
||||||
this.currentUser = user
|
// this.currentUser = user
|
||||||
|
// }
|
||||||
|
async getCurrentUser(email: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.user.getUserByEmail(email)
|
||||||
|
this.currentUser = data.data
|
||||||
|
console.log('[getCurrentUser] this.currentUser', this.currentUser)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
login(user: FirebaseUser) {
|
||||||
|
this.firebaseUser = user
|
||||||
|
this.getCurrentUser(user.email!)
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.firebaseUser = null
|
||||||
|
this.currentUser = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
24
src/utils/dateFilter.ts
Normal file
24
src/utils/dateFilter.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { dayjs } from '@/utils/dayjs'
|
||||||
|
import { DateFilterData } from '@/definition/interface'
|
||||||
|
import { Record } from '@/models/Record'
|
||||||
|
|
||||||
|
export const dateFilter = (filterData: DateFilterData, data: any[]) => {
|
||||||
|
const { searchMode, filter } = filterData
|
||||||
|
if (searchMode === '月份') {
|
||||||
|
return data.filter((item: Record) => dayjs(item.date).isSame(`${filter.year}-${filter.month}`, 'month'))
|
||||||
|
} else {
|
||||||
|
if (!filter.startDate && !filter.finishDate) return []
|
||||||
|
if (filter.startDate && !filter.finishDate) {
|
||||||
|
return data.filter((item: Record) => dayjs(item.date).isSameOrAfter(filter.startDate))
|
||||||
|
} else if (!filter.startDate && filter.finishDate) {
|
||||||
|
return data.filter((item: Record) => dayjs(item.date).isSameOrBefore(filter.finishDate))
|
||||||
|
} else {
|
||||||
|
return data.filter(
|
||||||
|
(item: Record) =>
|
||||||
|
dayjs(item.date).isBetween(filter.startDate, filter.finishDate) ||
|
||||||
|
dayjs(item.date).isSame(dayjs(filter.startDate)) ||
|
||||||
|
dayjs(item.date).isSame(dayjs(filter.finishDate))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/utils/dateFormat.ts
Normal file
9
src/utils/dateFormat.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { dayjs } from '@/utils/dayjs'
|
||||||
|
|
||||||
|
export const formatDate = (date: Date) => {
|
||||||
|
return dayjs(date).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDay = (date: Date) => {
|
||||||
|
return dayjs(date).locale('zh-tw').format('dddd')
|
||||||
|
}
|
17
src/utils/dayjs.ts
Normal file
17
src/utils/dayjs.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import isBetween from 'dayjs/plugin/isBetween'
|
||||||
|
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
|
||||||
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||||
|
import updateLocale from 'dayjs/plugin/updateLocale'
|
||||||
|
import 'dayjs/locale/zh-tw'
|
||||||
|
|
||||||
|
dayjs.extend(isBetween)
|
||||||
|
dayjs.extend(isSameOrAfter)
|
||||||
|
dayjs.extend(isSameOrBefore)
|
||||||
|
dayjs.extend(updateLocale)
|
||||||
|
|
||||||
|
dayjs.updateLocale('zh-tw', {
|
||||||
|
weekdays: ['(日)', '(一)', '(二)', '(三)', '(四)', '(五)', '(六)']
|
||||||
|
})
|
||||||
|
|
||||||
|
export { dayjs }
|
@ -1,18 +0,0 @@
|
|||||||
export const showWeekDay = (date: Date) => {
|
|
||||||
switch (new Date(date).getDay()) {
|
|
||||||
case 1:
|
|
||||||
return '一'
|
|
||||||
case 2:
|
|
||||||
return '二'
|
|
||||||
case 3:
|
|
||||||
return '三'
|
|
||||||
case 4:
|
|
||||||
return '四'
|
|
||||||
case 5:
|
|
||||||
return '五'
|
|
||||||
case 6:
|
|
||||||
return '六'
|
|
||||||
case 0:
|
|
||||||
return '日'
|
|
||||||
}
|
|
||||||
}
|
|
16
src/utils/lineBotMsg.ts
Normal file
16
src/utils/lineBotMsg.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import lineBotAPI from '@/apis/lineBot'
|
||||||
|
|
||||||
|
export const pushMsgToBoth = async (text: string) => {
|
||||||
|
try {
|
||||||
|
const input = {
|
||||||
|
to: [import.meta.env.VITE_KAROL_USERID, import.meta.env.VITE_JIANMIAU_USERID],
|
||||||
|
messages: {
|
||||||
|
type: 'text',
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await lineBotAPI.push(input)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
117
src/views/Access.vue
Normal file
117
src/views/Access.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Role, Permission } from '@/models/index'
|
||||||
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
// import CreateRoleModalButton from '@/components/ModalButton/Role/CreateRoleModalButton.vue'
|
||||||
|
// import EditRoleModalButton from '@/components/ModalButton/Role/EditRoleModalButton.vue'
|
||||||
|
// import DeleteRoleModalButton from '@/components/ModalButton/Role/DeleteRoleModalButton.vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// data
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const editMode = ref<boolean>(false)
|
||||||
|
const role = ref<Role>(new Role())
|
||||||
|
const permissions = ref<Permission[]>([])
|
||||||
|
const editPermissionId = ref<number[]>([])
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchRole = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.role.getOne(Number(route.params.id))
|
||||||
|
role.value = data.data
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPermissions = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.permission.getAll()
|
||||||
|
permissions.value = data.data
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEditBtn = () => {
|
||||||
|
editMode.value = true
|
||||||
|
editPermissionId.value = role.value.Permissions.map((p) => p.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxClick = (id: number) => {
|
||||||
|
if (editMode.value) {
|
||||||
|
if (!editPermissionId.value.includes(id)) {
|
||||||
|
editPermissionId.value.push(id)
|
||||||
|
} else {
|
||||||
|
const index = editPermissionId.value.findIndex((pid) => pid === id)
|
||||||
|
editPermissionId.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editPermissions = () => {
|
||||||
|
editMode.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
fetchRole()
|
||||||
|
fetchPermissions()
|
||||||
|
|
||||||
|
// provide
|
||||||
|
// provide('refetchRoles', fetchRoles)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
{{ editPermissionId }}
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<!-- <CreateRoleModalButton /> -->
|
||||||
|
<button v-if="!editMode" @click="startEditBtn" class="btn btn-primary">開始編輯</button>
|
||||||
|
<template v-else>
|
||||||
|
<button @click="editMode = false" class="btn btn-secondary me-3">取消編輯</button>
|
||||||
|
<button @click="editPermissions" class="btn btn-warning">確定編輯</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped table-danger table-hover" v-if="permissions.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" v-if="editMode">#</th>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">ACTION</th>
|
||||||
|
<th scope="col">ITEM</th>
|
||||||
|
<th scope="col">DESCRIPTION</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(permission, index) in permissions" :key="index" @click="checkboxClick(permission.id)">
|
||||||
|
<td v-if="editMode">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="flexCheckChecked"
|
||||||
|
:checked="editPermissionId.includes(permission.id)"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="flexCheckChecked"></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ permission.id }}</td>
|
||||||
|
<td>{{ permission.action }}</td>
|
||||||
|
<td>{{ permission.item }}</td>
|
||||||
|
<td>{{ permission.description }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
@ -1,130 +1,58 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, provide } from 'vue'
|
||||||
import recordAPI from '../apis/record'
|
import recordAPI from '../apis/record'
|
||||||
import { Record } from '../models/Record'
|
import { Record } from '@/models/index'
|
||||||
import { showWeekDay } from '../utils/helper'
|
import { getDay } from '../utils/dateFormat'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '../components/Spinner.vue'
|
||||||
type SearchMode = '月份' | '日期'
|
import DateFilter from '@/components/DateFilter.vue'
|
||||||
|
import { DateFilterData } from '@/definition/interface'
|
||||||
|
import { dateFilter } from '@/utils/dateFilter'
|
||||||
|
import { dayjs } from '@/utils/dayjs'
|
||||||
|
|
||||||
// data
|
// data
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const records = ref<Record[]>([])
|
const records = ref<Record[]>([])
|
||||||
const searchMode = ref<SearchMode>('月份')
|
const dateFilterData = ref<DateFilterData>({
|
||||||
const year = ref()
|
searchMode: '月份',
|
||||||
const month = ref()
|
filter: {
|
||||||
const startDate = ref<Date | string>('')
|
year: 0,
|
||||||
const finishDate = ref<Date | string>('')
|
month: 0,
|
||||||
|
startDate: '',
|
||||||
|
finishDate: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const fetchRecords = async function () {
|
const fetchRecords = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getAll()
|
const { data } = await recordAPI.getAll()
|
||||||
records.value = data.filter((item: Record) => item.isClosed === true && item.deletedAt === null)
|
records.value = data.data.filter((item: Record) => item.isClosed === true)
|
||||||
year.value = new Date(records.value[0].date).getFullYear()
|
dateFilterData.value.filter = {
|
||||||
month.value = new Date(records.value[0].date).getMonth() + 1
|
year: dayjs(records.value[0].date).year(),
|
||||||
|
month: dayjs(records.value[0].date).month() + 1,
|
||||||
|
startDate: dayjs(records.value[0].date).startOf('month').format('YYYY-MM-DD'),
|
||||||
|
finishDate: dayjs(records.value[0].date).endOf('month').format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const radioClick = (radio: SearchMode) => {
|
|
||||||
searchMode.value = radio
|
|
||||||
}
|
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
const filteredRecord = computed(() => {
|
const filteredRecord = computed(() => {
|
||||||
if (searchMode.value === '月份') {
|
return dateFilter(dateFilterData.value, records.value)
|
||||||
return records.value?.filter(
|
|
||||||
(item: Record) =>
|
|
||||||
new Date(item.date).getFullYear() === year.value && new Date(item.date).getMonth() + 1 === month.value
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (!startDate.value && !finishDate.value) return []
|
|
||||||
if (startDate.value && !finishDate.value) {
|
|
||||||
return records.value?.filter(
|
|
||||||
(item: Record) => new Date(item.date).getTime() >= new Date(startDate.value).getTime()
|
|
||||||
)
|
|
||||||
} else if (!startDate.value && finishDate.value) {
|
|
||||||
return records.value?.filter(
|
|
||||||
(item: Record) => new Date(item.date).getTime() <= new Date(finishDate.value + ' 23:59:59').getTime()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return records.value?.filter(
|
|
||||||
(item: Record) =>
|
|
||||||
new Date(item.date).getTime() >= new Date(startDate.value).getTime() &&
|
|
||||||
new Date(item.date).getTime() <= new Date(finishDate.value + ' 23:59:59').getTime()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// created
|
// created
|
||||||
fetchRecords()
|
fetchRecords()
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('dateFilterData', dateFilterData)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<div class="d-flex mb-4">
|
<DateFilter />
|
||||||
<div class="m-2 me-5">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="flexRadioDefault"
|
|
||||||
id="flexRadioDefault1"
|
|
||||||
@click="radioClick('月份')"
|
|
||||||
checked
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="flexRadioDefault1">月份搜尋</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="flexRadioDefault"
|
|
||||||
id="flexRadioDefault2"
|
|
||||||
@click="radioClick('日期')"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="flexRadioDefault2">日期搜尋</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="searchMode === '月份'">
|
|
||||||
<div>
|
|
||||||
<label for="year" class="form-label">西元年</label>
|
|
||||||
<select class="form-select" aria-label="Default select example" v-model="year">
|
|
||||||
<option v-for="n in 100" :key="n" :value="n + 2020">{{ n + 2020 }}年</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<label for="finishDate" class="form-label">月份</label>
|
|
||||||
<select class="form-select" aria-label="Default select example" v-model="month">
|
|
||||||
<option v-for="n in 12" :key="n" :value="n">{{ n }}月</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div>
|
|
||||||
<label for="startDate" class="form-label">開始日期</label>
|
|
||||||
<input
|
|
||||||
v-model="startDate"
|
|
||||||
type="date"
|
|
||||||
id="startDate"
|
|
||||||
class="form-control"
|
|
||||||
aria-describedby="passwordHelpInline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<label for="finishDate" class="form-label">結束日期</label>
|
|
||||||
<input
|
|
||||||
v-model="finishDate"
|
|
||||||
type="date"
|
|
||||||
id="finishDate"
|
|
||||||
class="form-control"
|
|
||||||
aria-describedby="passwordHelpInline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped table-info table-hover" v-if="filteredRecord.length">
|
<table class="table table-striped table-info table-hover" v-if="filteredRecord.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -139,21 +67,11 @@ fetchRecords()
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(record, index) in filteredRecord" :key="index">
|
<tr v-for="(record, index) in filteredRecord" :key="index">
|
||||||
<td>
|
<td>{{ record.item }}</td>
|
||||||
{{ record.item }}
|
<td>{{ record.merchant }}</td>
|
||||||
</td>
|
<td>{{ record.amount }}</td>
|
||||||
<td>
|
<td>{{ new Date(record.date).toLocaleDateString() + ' ' + getDay(record.date) }}</td>
|
||||||
{{ record.merchant }}
|
<td id="column-item">{{ record.User.displayName }}</td>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ record.amount }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ new Date(record.date).toLocaleDateString() + ` (${showWeekDay(record.date)})` }}
|
|
||||||
</td>
|
|
||||||
<td id="column-item">
|
|
||||||
{{ record.recorder }}
|
|
||||||
</td>
|
|
||||||
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||||
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -162,8 +80,8 @@ fetchRecords()
|
|||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
class="img-fluid"
|
class="img-fluid"
|
||||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||||
alt=""
|
alt="img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Spinner v-else />
|
<Spinner v-else />
|
||||||
|
3
src/views/Default.vue
Normal file
3
src/views/Default.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
@ -1,136 +1,158 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, provide, computed, watch } from 'vue'
|
||||||
import expenseAPI from '../apis/expense'
|
import expenseAPI from '@/apis/expense'
|
||||||
import { Expense, ExpenseInput } from '../models/Expense'
|
import { Expense, Category } from '@/models'
|
||||||
import Swal from 'sweetalert2'
|
import { getDay } from '../utils/dateFormat'
|
||||||
import { Toast, ConfirmBox } from '../utils/swal'
|
|
||||||
import { showWeekDay } from '../utils/helper'
|
|
||||||
import Spinner from '@/components/Spinner.vue'
|
import Spinner from '@/components/Spinner.vue'
|
||||||
import RightPanel from '@/components/RightPanel.vue'
|
import CategoryRP from '@/components/RightPanel/CategoryRP.vue'
|
||||||
|
import CreateExpenseModalButton from '@/components/ModalButton/Expense/CreateExpenseModalButton.vue'
|
||||||
|
import EditExpenseModalButton from '@/components/ModalButton/Expense/EditExpenseModalButton.vue'
|
||||||
|
import DeleteExpenseModalButton from '@/components/ModalButton/Expense/DeleteExpenseModalButton.vue'
|
||||||
|
import FilterBox from '@/components/FilterBox.vue'
|
||||||
|
import DateFilter from '@/components/DateFilter.vue'
|
||||||
|
import { DateFilterData } from '@/definition/interface'
|
||||||
|
import { dateFilter } from '@/utils/dateFilter'
|
||||||
|
import { dayjs } from '@/utils/dayjs'
|
||||||
|
|
||||||
// data
|
// data
|
||||||
const isLoading = ref<boolean>(true)
|
const expenses = ref<Expense[]>()
|
||||||
const expenses = ref<Expense[]>([])
|
const categories = ref<Category[]>()
|
||||||
const rightPanelOpen = ref(false)
|
const categoryRPOpen = ref<boolean>(false)
|
||||||
|
const categoryFilters = ref<number[]>([])
|
||||||
|
const dateFilterData = ref<DateFilterData>({
|
||||||
|
searchMode: '月份',
|
||||||
|
filter: {
|
||||||
|
year: dayjs().year(),
|
||||||
|
month: dayjs().month() + 1,
|
||||||
|
startDate: dayjs().startOf('month').format('YYYY-MM-DD hh:mm:ss'),
|
||||||
|
finishDate: dayjs().endOf('month').format('YYYY-MM-DD hh:mm:ss')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const selectedType = ref<'支出' | '收入' | 'ALL'>('ALL')
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const allCategories = computed<number[]>(() => {
|
||||||
|
if (selectedType.value !== 'ALL') {
|
||||||
|
return categories
|
||||||
|
.value!.filter((category: Category) => category.type === selectedType.value)
|
||||||
|
.map((item: Category) => item.id)
|
||||||
|
} else {
|
||||||
|
return categories.value!.map((item: Category) => item.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoriesByType = computed(() => {
|
||||||
|
if (selectedType.value === 'ALL') {
|
||||||
|
return categories.value
|
||||||
|
} else {
|
||||||
|
return categories.value!.filter((category: Category) => category.type === selectedType.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredExpenses = computed(() => {
|
||||||
|
let chosenExpenses: Expense[] = []
|
||||||
|
// category filter
|
||||||
|
chosenExpenses = expenses.value!.filter((expense: Expense) => categoryFilters.value.includes(expense.Category.id))
|
||||||
|
// date filter
|
||||||
|
return dateFilter(dateFilterData.value, chosenExpenses)
|
||||||
|
})
|
||||||
|
|
||||||
|
const amountSum = computed(() => {
|
||||||
|
let income = 0
|
||||||
|
let expense = 0
|
||||||
|
filteredExpenses.value.forEach((item: Expense) => {
|
||||||
|
if (item.Category.type === '支出') {
|
||||||
|
expense += item.amount
|
||||||
|
} else {
|
||||||
|
income += item.amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
income,
|
||||||
|
expense
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const fetchExpenses = async function () {
|
const fetchExpenses = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await expenseAPI.expense.getAll()
|
const { data } = await expenseAPI.expense.getAll()
|
||||||
expenses.value = data.filter((item: Expense) => item.deletedAt === null)
|
expenses.value = data.data
|
||||||
isLoading.value = false
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBtnClick = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const { value: formValues } = await ConfirmBox.fire({
|
const { data } = await expenseAPI.category.getAll()
|
||||||
title: '新增資料',
|
categories.value = data.data
|
||||||
html: `
|
categoryFilters.value = categories.value!.map((item: Category) => item.id)
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-categoryId" class="col-form-label">類別</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="text" id="swal-categoryId" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-item" class="col-form-label">項目</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="text" id="swal-item" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-amount" class="col-form-label">金額</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="number" id="swal-amount" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-note" class="col-form-label">備註</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="text" id="swal-note" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-date" class="col-form-label">日期</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="date" id="swal-date" class="form-control" value="${new Date().toJSON().substring(0, 10)}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
preConfirm: () => {
|
|
||||||
const categoryId = (document.getElementById('swal-categoryId') as HTMLInputElement).value
|
|
||||||
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
|
||||||
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
|
||||||
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
|
||||||
const note = (document.getElementById('swal-note') as HTMLInputElement).value
|
|
||||||
if (!item || !amount || !date) {
|
|
||||||
Swal.showValidationMessage('除了[備註],所有資料都是必填!')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
input: {
|
|
||||||
CategoryId: Number(categoryId),
|
|
||||||
item,
|
|
||||||
amount,
|
|
||||||
note,
|
|
||||||
date
|
|
||||||
} as ExpenseInput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (formValues) {
|
|
||||||
createExpense(formValues)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createExpense = async function (formValues: { input: ExpenseInput }) {
|
|
||||||
try {
|
|
||||||
await expenseAPI.expense.create(formValues.input)
|
|
||||||
fetchExpenses()
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'success',
|
|
||||||
title: '成功建立資料!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: '新增資料失敗!'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editBtnClick = (id: number) => {
|
|
||||||
console.log('editBtnClick', id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// created
|
// created
|
||||||
fetchExpenses()
|
fetchExpenses()
|
||||||
|
fetchCategories()
|
||||||
|
|
||||||
|
// watch
|
||||||
|
watch(selectedType, (newVal) => {
|
||||||
|
if (newVal !== 'ALL') {
|
||||||
|
categoryFilters.value = categories
|
||||||
|
.value!.filter((category: Category) => category.type === selectedType.value)
|
||||||
|
.map((item: Category) => item.id)
|
||||||
|
} else {
|
||||||
|
categoryFilters.value = categories.value!.map((item: Category) => item.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('refetchExpenses', fetchExpenses)
|
||||||
|
provide('refetchCategories', fetchCategories)
|
||||||
|
provide('categories', categories)
|
||||||
|
provide('categoryFilters', categoryFilters)
|
||||||
|
provide('dateFilterData', dateFilterData)
|
||||||
|
provide('categoryRPOpen', categoryRPOpen)
|
||||||
|
provide('categoriesByType', categoriesByType)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="expenses && categories">
|
||||||
|
<!-- buttons -->
|
||||||
<div class="d-flex mb-3" style="width: 100vw">
|
<div class="d-flex mb-3" style="width: 100vw">
|
||||||
<button type="button" class="btn btn-primary me-3" @click="createBtnClick">新增資料</button>
|
<CreateExpenseModalButton />
|
||||||
<button type="button" class="btn btn-warning" @click="rightPanelOpen = !rightPanelOpen">查看類別</button>
|
<button type="button" class="btn btn-warning text-dark" @click="categoryRPOpen = true">查看類別</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-striped table-success table-hover" v-if="expenses.length">
|
<!-- filter -->
|
||||||
|
<div class="d-flex mb-4" style="width: 100%">
|
||||||
|
<div class="mt-4" style="width: 50px">
|
||||||
|
<label for="type" style="float: left; font-size: 0.7em">TYPE</label>
|
||||||
|
<select class="form-select" id="type" aria-label="Default select example" v-model="selectedType">
|
||||||
|
<option selected>ALL</option>
|
||||||
|
<option>支出</option>
|
||||||
|
<option>收入</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DateFilter style="width: 250px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- sum -->
|
||||||
|
<div class="d-flex align-items-center mb-4" style="width: 100%">
|
||||||
|
<span class="badge bg-warning text-dark fs-3">收入 $ {{ amountSum.income }}</span>
|
||||||
|
<i class="fas fa-minus mx-2"></i>
|
||||||
|
<span class="badge bg-success fs-3">支出 $ {{ amountSum.expense }}</span>
|
||||||
|
<i class="fas fa-equals mx-2"></i>
|
||||||
|
<span class="badge bg-info fs-3">$ {{ amountSum.income - amountSum.expense }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<FilterBox :allCategories="allCategories" />
|
||||||
|
<table class="table table-striped table-success table-hover" v-if="filteredExpenses?.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="table-light">
|
||||||
|
<th scope="col">#</th>
|
||||||
<th scope="col">#</th>
|
<th scope="col">#</th>
|
||||||
<th scope="col">類別</th>
|
<th scope="col">類別</th>
|
||||||
<th scope="col">項目</th>
|
<th scope="col">項目</th>
|
||||||
@ -140,36 +162,34 @@ fetchExpenses()
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(expense, index) in expenses" :key="index">
|
<template v-for="(expense, index) in filteredExpenses" :key="index">
|
||||||
|
<tr :class="expense.Category.type === '支出' ? 'table-success' : 'table-warning'">
|
||||||
<td>
|
<td>
|
||||||
<i class="fas fa-edit" @click="editBtnClick(expense.id)"></i>
|
<EditExpenseModalButton :expense="expense" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ expense.Category.name }}
|
<DeleteExpenseModalButton :expense="expense" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ expense.item }}
|
<i :class="expense.Category.icon"></i>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ expense.amount }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ expense.note }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ new Date(expense.date).toLocaleDateString() + ` (${showWeekDay(expense.date)})` }}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ expense.item }}</td>
|
||||||
|
<td>{{ expense.amount }}</td>
|
||||||
|
<td>{{ expense.note }}</td>
|
||||||
|
<td>{{ new Date(expense.date).toLocaleDateString() + ' ' + getDay(expense.date) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- <img
|
<img
|
||||||
v-else
|
v-else
|
||||||
class="img-fluid"
|
class="img-fluid"
|
||||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||||
alt=""
|
alt="img"
|
||||||
/> -->
|
/>
|
||||||
|
</div>
|
||||||
<transition name="slide-right">
|
<transition name="slide-right">
|
||||||
<RightPanel v-show="rightPanelOpen" />
|
<CategoryRP v-if="categoryRPOpen" />
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<Spinner v-else />
|
<Spinner v-else />
|
||||||
|
71
src/views/Game.vue
Normal file
71
src/views/Game.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import NumberWordle from '@/components/Game/NumberWordle.vue'
|
||||||
|
import NumberAB from '@/components/Game/NumberAB.vue'
|
||||||
|
|
||||||
|
const tabs = ref(['猜數字(Wordle)', '猜數字(AB)'])
|
||||||
|
const nowTab = ref('猜數字(Wordle)')
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex justify-content-center m-2 mb-5">
|
||||||
|
<button
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
type="button"
|
||||||
|
:class="`btn btn-${nowTab !== tab ? 'outline-' : ''}danger me-2`"
|
||||||
|
@click="nowTab = tab"
|
||||||
|
>
|
||||||
|
{{ tab }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<NumberWordle v-if="nowTab === '猜數字(Wordle)'"></NumberWordle>
|
||||||
|
<NumberAB v-if="nowTab === '猜數字(AB)'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.num-block {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
background-color: #d3d3d3;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-btn {
|
||||||
|
width: 15%;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 8%;
|
||||||
|
margin: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-customer {
|
||||||
|
width: 35%;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 5%;
|
||||||
|
margin: 5px 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enter-btn {
|
||||||
|
background-color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background-color: darkturquoise;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import recordAPI from '../apis/record'
|
import recordAPI from '@/apis/record'
|
||||||
import { Record } from '../models/Record'
|
import lineBotAPI from '@/apis/lineBot'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import { Record } from '@/models'
|
||||||
import CreateRecordModalButton from '../components/modalButton/CreateRecordModalButton.vue'
|
import { CallParent } from '@/cocos/config'
|
||||||
|
// components
|
||||||
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
import CreateRecordModalButton from '@/components/ModalButton/Record/CreateRecordModalButton.vue'
|
||||||
|
|
||||||
|
// class
|
||||||
class MonthData {
|
class MonthData {
|
||||||
total!: number
|
total!: number
|
||||||
closedAmount!: number
|
closedAmount!: number
|
||||||
@ -21,7 +25,7 @@ const lastMonthData = ref<MonthData>(new MonthData())
|
|||||||
const fetchRecords = async function () {
|
const fetchRecords = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getAll()
|
const { data } = await recordAPI.getAll()
|
||||||
records.value = data.filter((item: Record) => item.deletedAt === null)
|
records.value = data.data
|
||||||
// monthData
|
// monthData
|
||||||
const nowYear = new Date().getFullYear()
|
const nowYear = new Date().getFullYear()
|
||||||
const nowMonth = new Date().getMonth() + 1
|
const nowMonth = new Date().getMonth() + 1
|
||||||
@ -67,20 +71,24 @@ fetchRecords()
|
|||||||
|
|
||||||
// 笨蛋才按我
|
// 笨蛋才按我
|
||||||
const handle = async () => {
|
const handle = async () => {
|
||||||
|
try {
|
||||||
const input = {
|
const input = {
|
||||||
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
to: [import.meta.env.VITE_KAROL_USERID, import.meta.env.VITE_JIANMIAU_USERID],
|
||||||
messages: { type: 'text', text: '卡比覺得促咪!' }
|
messages: { type: 'text', text: '卡比覺得促咪!' }
|
||||||
}
|
}
|
||||||
console.log('input', input)
|
await lineBotAPI.push(input)
|
||||||
await recordAPI.pushLineMsg(input)
|
CallParent('Speak', '按我了 你是笨蛋')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<div class="d-flex mb-3">
|
<div class="d-flex mb-3" style="width: 100vw">
|
||||||
<CreateRecordModalButton view="Home" class="me-3" />
|
<CreateRecordModalButton view="Home" class="me-3" />
|
||||||
<button type="button" class="btn btn-success me-3" @click="handle">笨蛋才按我</button>
|
<button type="button" class="btn btn-danger me-3" @click="handle">笨蛋才按我</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-checkable">
|
<div class="list-group list-group-checkable">
|
||||||
<label class="list-group-item py-3 mb-3">
|
<label class="list-group-item py-3 mb-3">
|
||||||
|
137
src/views/Login.vue
Normal file
137
src/views/Login.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
// import userAPI from '../apis/user'
|
||||||
|
// import { User } from '../models/User'
|
||||||
|
import { Toast } from '@/utils/swal'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
// import { useStore } from '../store/index'
|
||||||
|
// const store = useStore()
|
||||||
|
|
||||||
|
// import {
|
||||||
|
// getAuth,
|
||||||
|
// GoogleAuthProvider,
|
||||||
|
// signInWithEmailAndPassword,
|
||||||
|
// signInWithPopup,
|
||||||
|
// User as FirebaseUser
|
||||||
|
// } from 'firebase/auth'
|
||||||
|
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
|
||||||
|
// import { getAuth, GoogleAuthProvider, signInWithEmailAndPassword } from 'firebase/auth'
|
||||||
|
const auth = getAuth()
|
||||||
|
// const provider = new GoogleAuthProvider()
|
||||||
|
|
||||||
|
// data
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const signInEmail = () => {
|
||||||
|
if (!email.value || !password.value) {
|
||||||
|
return Toast.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: '信箱、密碼為必填選項!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
signInWithEmailAndPassword(auth, email.value, password.value)
|
||||||
|
.then((userCredential) => {
|
||||||
|
// Signed in
|
||||||
|
const user = userCredential.user
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `歡迎[${user.displayName}]進入豬豬世界🐷`
|
||||||
|
})
|
||||||
|
router.push({ name: 'Home' })
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorCode = error.code
|
||||||
|
const errorMessage = error.message
|
||||||
|
console.log('error', errorCode, errorMessage)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '豬豬世界不歡迎你 ☠️'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// const signInGoogle = () => {
|
||||||
|
// signInWithPopup(auth, provider)
|
||||||
|
// .then(async (result) => {
|
||||||
|
// // const credential = GoogleAuthProvider.credentialFromResult(result)
|
||||||
|
// // const token = credential?.accessToken
|
||||||
|
// const firebaseUser: FirebaseUser = result.user
|
||||||
|
// console.log('firebaseUser', firebaseUser)
|
||||||
|
// // 檢查是否有相同帳號
|
||||||
|
// const { data } = await userAPI.getAll()
|
||||||
|
// const user = data.data.find((user: User) => user.email === firebaseUser.email)
|
||||||
|
// if (!user) {
|
||||||
|
// await userAPI.create({
|
||||||
|
// email: user.email!,
|
||||||
|
// displayName: user.displayName!,
|
||||||
|
// photoURL: user.photoURL ? user.photoURL : null,
|
||||||
|
// firebaseUid: user.uid
|
||||||
|
// })
|
||||||
|
// Toast.fire({
|
||||||
|
// icon: 'success',
|
||||||
|
// title: `@GOOGLE@ 歡迎[${firebaseUser.displayName}]進入豬豬世界🐷`
|
||||||
|
// })
|
||||||
|
// router.push({ name: 'Home' })
|
||||||
|
// } else {
|
||||||
|
// Toast.fire({
|
||||||
|
// icon: 'error',
|
||||||
|
// title: `${firebaseUser.email} 這個信箱已被註冊過!`
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error('error', error)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-info register-btn" type="button" @click="router.push({ name: 'Register' })">前往註冊</button>
|
||||||
|
<div class="m-auto" style="width: 70vw">
|
||||||
|
<form>
|
||||||
|
<img src="@/assets/logo2.png" alt="" width="150" height="150" />
|
||||||
|
<h1 class="h3 mb-3 fw-normal">臭建喵記帳 Login</h1>
|
||||||
|
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="email" class="form-control" id="email-login" v-model="email" autocomplete="on" />
|
||||||
|
<label for="email-login">信箱</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password-login"
|
||||||
|
v-model="password"
|
||||||
|
autocomplete="on"
|
||||||
|
@keyup.enter="signInEmail"
|
||||||
|
/>
|
||||||
|
<label for="password-login">密碼</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="checkbox mb-3">
|
||||||
|
<label> <input type="checkbox" value="remember-me" /> Remember me </label>
|
||||||
|
</div> -->
|
||||||
|
<button
|
||||||
|
class="w-100 btn btn-lg mt-3"
|
||||||
|
type="button"
|
||||||
|
@click="signInEmail"
|
||||||
|
style="color: white; background-color: coral"
|
||||||
|
>
|
||||||
|
進入豬豬世界
|
||||||
|
</button>
|
||||||
|
<!-- <button class="w-100 btn btn-lg btn-success mt-3" type="button" @click="signInGoogle">Google登入</button> -->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.register-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { showWeekDay } from '../utils/helper'
|
import { getDay } from '../utils/dateFormat'
|
||||||
import recordAPI from '../apis/record'
|
import recordAPI from '@/apis/record'
|
||||||
import { Log } from '../models/Log'
|
import { Log, Record } from '@/models'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ title: '總覽', btnColor: 'secondary' },
|
{ title: '總覽', btnColor: 'secondary' },
|
||||||
@ -12,43 +12,36 @@ const tabs = [
|
|||||||
{ title: '編輯', btnColor: 'success' },
|
{ title: '編輯', btnColor: 'success' },
|
||||||
{ title: '結算', btnColor: 'danger' }
|
{ title: '結算', btnColor: 'danger' }
|
||||||
]
|
]
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(true)
|
||||||
const logs = ref<Log[]>([])
|
const logs = ref<Log[]>([])
|
||||||
const nowTab = ref('總覽')
|
const nowTab = ref('總覽')
|
||||||
const logCount = ref<any>({ 總覽: 5, 新增: 5, 編輯: 5, 結算: 5 })
|
const logCount = ref<any>({ 總覽: 5, 新增: 5, 編輯: 5, 結算: 5 })
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
if (nowTab.value === '總覽') return logs.value
|
if (nowTab.value === '總覽') return logs.value
|
||||||
return logs.value.filter((log) => log.action === nowTab.value)
|
return logs.value.filter((log: Log) => log.action === nowTab.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchLogs = async function () {
|
const fetchLogs = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getLogs()
|
const { data } = await recordAPI.getLogs()
|
||||||
logs.value = data
|
logs.value = data.data
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const listIconClick = async (recordIds: string | undefined) => {
|
const listIconClick = async (records: Record[]) => {
|
||||||
if (!recordIds) return
|
if (!records.length) return
|
||||||
const recordIdArr = recordIds.split(',')
|
|
||||||
const recordArr = []
|
|
||||||
for (const id of recordIdArr) {
|
|
||||||
const { data } = await recordAPI.getOne(Number(id))
|
|
||||||
recordArr.push(data)
|
|
||||||
}
|
|
||||||
let html = `<table class="table"><thead><tr><th scope="col">項目</th><th scope="col">商家</th><th scope="col">金額</th><th scope="col">日期</th></tr></thead><tbody>`
|
let html = `<table class="table"><thead><tr><th scope="col">項目</th><th scope="col">商家</th><th scope="col">金額</th><th scope="col">日期</th></tr></thead><tbody>`
|
||||||
for (const id of recordIdArr) {
|
for (const record of records) {
|
||||||
const { data } = await recordAPI.getOne(Number(id))
|
html += `<tr><td>${record.item}</td><td>${record.merchant}</td><td>${record.amount}</td><td>${new Date(
|
||||||
html += `<tr><td>${data.item}</td><td>${data.merchant}</td><td>${data.amount}</td><td>${new Date(
|
record.date
|
||||||
data.date
|
|
||||||
).toLocaleDateString()}</td></tr>`
|
).toLocaleDateString()}</td></tr>`
|
||||||
}
|
}
|
||||||
html += `</tbody></table>${
|
html += `</tbody></table>${
|
||||||
document.documentElement.scrollWidth >= 500 ? '' : '<style>table{font-size:0.5em;}</style>'
|
document.documentElement.scrollWidth >= 500 ? '' : '<style>table{font-size:0.7em;}</style>'
|
||||||
}`
|
}`
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
title: '結算紀錄',
|
title: '結算紀錄',
|
||||||
@ -61,9 +54,8 @@ fetchLogs()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading && logs.length">
|
<div v-if="!isLoading">
|
||||||
<template v-if="logs.length">
|
<div id="pc" style="width: 100vw">
|
||||||
<div id="pc">
|
|
||||||
<div class="btn-group mb-3" role="group" aria-label="Basic radio toggle button group">
|
<div class="btn-group mb-3" role="group" aria-label="Basic radio toggle button group">
|
||||||
<template v-for="(tab, index) in tabs" :key="index">
|
<template v-for="(tab, index) in tabs" :key="index">
|
||||||
<input
|
<input
|
||||||
@ -79,12 +71,10 @@ fetchLogs()
|
|||||||
}}</label>
|
}}</label>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
||||||
<div class="col-2 mt-4">
|
<div class="col-3 mt-4">
|
||||||
<div class="fw-bold">
|
<div class="fw-bold">
|
||||||
{{ new Date(log.createdAt).toLocaleDateString() }}
|
{{ new Date(log.createdAt).toLocaleDateString() + ' ' + getDay(log.createdAt) }}
|
||||||
<!-- {{ new Date(log.createdAt).toLocaleDateString() + ` (${showWeekDay(log.createdAt)})` }} -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -92,14 +82,14 @@ fetchLogs()
|
|||||||
<div class="bg-white border p-3">
|
<div class="bg-white border p-3">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<img
|
<img
|
||||||
v-if="log.recorder === '建喵'"
|
v-if="log.User.displayName === '建喵'"
|
||||||
src="../assets/jianmiau.jpeg"
|
src="../assets/jianmiau.jpeg"
|
||||||
class="my-auto img-thumbnail me-2"
|
class="my-auto img-thumbnail me-2"
|
||||||
width="60"
|
width="60"
|
||||||
height="60"
|
height="60"
|
||||||
/><img v-else src="../assets/karol.png" class="my-auto img-thumbnail me-2" width="60" height="60" />
|
/><img v-else src="../assets/karol.png" class="my-auto img-thumbnail me-2" width="60" height="60" />
|
||||||
<div class="text-start mt-2">
|
<div class="text-start mt-2">
|
||||||
<strong class="px-1 me-1" style="background-color: yellow">{{ log.recorder }}</strong>
|
<strong class="px-1 me-1" style="background-color: yellow">{{ log.User.displayName }}</strong>
|
||||||
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
||||||
<i class="fas fa-chevron-right m-2"></i>
|
<i class="fas fa-chevron-right m-2"></i>
|
||||||
<template v-if="log.action === '新增' || log.action === '編輯'">
|
<template v-if="log.action === '新增' || log.action === '編輯'">
|
||||||
@ -122,7 +112,7 @@ fetchLogs()
|
|||||||
結算金額
|
結算金額
|
||||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||||
</span>
|
</span>
|
||||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.RecordIds)"></i>
|
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.Records)"></i>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,7 +121,7 @@ fetchLogs()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- mobile -->
|
<!-- mobile -->
|
||||||
<div id="mobile" class="card text-center">
|
<div id="mobile" class="card text-center" style="width: 100vw">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<ul class="nav nav-tabs card-header-tabs">
|
<ul class="nav nav-tabs card-header-tabs">
|
||||||
<li class="nav-item" v-for="tab in tabs" :key="tab.title" @click="nowTab = tab.title">
|
<li class="nav-item" v-for="tab in tabs" :key="tab.title" @click="nowTab = tab.title">
|
||||||
@ -145,20 +135,21 @@ fetchLogs()
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
{{
|
{{
|
||||||
new Date(log.createdAt).toLocaleDateString() +
|
new Date(log.createdAt).toLocaleDateString() +
|
||||||
` (${showWeekDay(log.createdAt)}) ` +
|
' ' +
|
||||||
|
getDay(log.createdAt) +
|
||||||
new Date(log.createdAt).toLocaleTimeString()
|
new Date(log.createdAt).toLocaleTimeString()
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body d-flex">
|
<div class="card-body d-flex">
|
||||||
<img
|
<img
|
||||||
v-if="log.recorder === '建喵'"
|
v-if="log.User.displayName === '建喵'"
|
||||||
src="../assets/jianmiau.jpeg"
|
src="../assets/jianmiau.jpeg"
|
||||||
class="my-auto img-thumbnail me-2"
|
class="my-auto img-thumbnail me-2"
|
||||||
width="60"
|
width="60"
|
||||||
height="60"
|
height="60"
|
||||||
/><img v-else src="../assets/karol.png" class="my-auto img-thumbnail me-2" width="60" height="60" />
|
/><img v-else src="../assets/karol.png" class="my-auto img-thumbnail me-2" width="60" height="60" />
|
||||||
<div class="text-start mt-2">
|
<div class="text-start mt-2">
|
||||||
<strong class="px-1 me-1" style="background-color: yellow">{{ log.recorder }}</strong>
|
<strong class="px-1 me-1" style="background-color: yellow">{{ log.User.displayName }}</strong>
|
||||||
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
||||||
<i class="fas fa-chevron-right m-2"></i>
|
<i class="fas fa-chevron-right m-2"></i>
|
||||||
<template v-if="log.action === '新增' || log.action === '編輯'">
|
<template v-if="log.action === '新增' || log.action === '編輯'">
|
||||||
@ -183,7 +174,7 @@ fetchLogs()
|
|||||||
結算金額
|
結算金額
|
||||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||||
</span>
|
</span>
|
||||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.RecordIds)"></i>
|
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.Records)"></i>
|
||||||
</h6>
|
</h6>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -201,13 +192,6 @@ fetchLogs()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
class="img-fluid"
|
|
||||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Spinner v-else />
|
<Spinner v-else />
|
||||||
</template>
|
</template>
|
||||||
@ -217,6 +201,9 @@ fetchLogs()
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: salmon;
|
color: salmon;
|
||||||
}
|
}
|
||||||
|
li:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@media screen and (max-width: 499px) {
|
@media screen and (max-width: 499px) {
|
||||||
#pc {
|
#pc {
|
||||||
display: none;
|
display: none;
|
||||||
|
77
src/views/Permission.vue
Normal file
77
src/views/Permission.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, provide } from 'vue'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Permission } from '@/models/index'
|
||||||
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
import CreatePermissionModalButton from '@/components/ModalButton/Permission/CreatePermissionModalButton.vue'
|
||||||
|
import EditPermissionModalButton from '@/components/ModalButton/Permission/EditPermissionModalButton.vue'
|
||||||
|
import DeletePermissionModalButton from '@/components/ModalButton/Permission/DeletePermissionModalButton.vue'
|
||||||
|
|
||||||
|
// data
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const permissions = ref<Permission[]>([])
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchPermissions = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.permission.getAll()
|
||||||
|
permissions.value = data.data
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
fetchPermissions()
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('refetchPermissions', fetchPermissions)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<CreatePermissionModalButton />
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped table-danger table-hover" v-if="permissions.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!-- <th scope="col">#</th> -->
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">ACTION</th>
|
||||||
|
<th scope="col">ITEM</th>
|
||||||
|
<th scope="col">DESCRIPTION</th>
|
||||||
|
<th scope="col">#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(permission, index) in permissions" :key="index">
|
||||||
|
<!-- <td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="flexCheckChecked" checked />
|
||||||
|
<label class="form-check-label" for="flexCheckChecked"></label>
|
||||||
|
</div>
|
||||||
|
</td> -->
|
||||||
|
<td>{{ permission.id }}</td>
|
||||||
|
<td>{{ permission.action }}</td>
|
||||||
|
<td>{{ permission.item }}</td>
|
||||||
|
<td>{{ permission.description }}</td>
|
||||||
|
<td>
|
||||||
|
<EditPermissionModalButton :permission="permission" />
|
||||||
|
<DeletePermissionModalButton :permission="permission" class="ms-2" />
|
||||||
|
</td>
|
||||||
|
<!-- <td> -->
|
||||||
|
<!-- <DeletePermissionModalButton :permission="permission" /> -->
|
||||||
|
<!-- </td> -->
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
@ -1,243 +1,87 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, provide } from 'vue'
|
||||||
import recordAPI from '../apis/record'
|
import recordAPI from '@/apis/record'
|
||||||
import { Record, RecordInput } from '../models/Record'
|
import { Record } from '@/models'
|
||||||
import Swal from 'sweetalert2'
|
import { getDay } from '../utils/dateFormat'
|
||||||
import { Toast, ConfirmBox } from '../utils/swal'
|
|
||||||
import { showWeekDay } from '../utils/helper'
|
|
||||||
import Spinner from '../components/Spinner.vue'
|
|
||||||
import CreateRecordModalButton from '../components/modalButton/CreateRecordModalButton.vue'
|
|
||||||
import { useStore } from '../store/index'
|
import { useStore } from '../store/index'
|
||||||
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
import EditRecordModalButton from '@/components/ModalButton/Record/EditRecordModalButton.vue'
|
||||||
|
import RecordButtons from '@/components/RecordButtons.vue'
|
||||||
|
import DeleteRecordModalButton from '@/components/ModalButton/Record/DeleteRecordModalButton.vue'
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
// data
|
// data
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const records = ref<Record[]>([])
|
const records = ref<Record[]>([])
|
||||||
const isCloseStatus = ref<boolean>(false)
|
const propDataForRecordButtons = ref({
|
||||||
const closeRecords = ref<number[]>([])
|
isCloseStatus: false,
|
||||||
const closeRecordsAmount = ref<number>(0)
|
closeRecords: [] as number[],
|
||||||
|
closeRecordsAmount: 0
|
||||||
|
})
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const fetchRecords = async function () {
|
const fetchRecords = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getAll()
|
const { data } = await recordAPI.getAll()
|
||||||
records.value = data.filter((item: Record) => item.isClosed === false)
|
records.value = data.data.filter((item: Record) => item.isClosed === false)
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editBtnClick = async function (id: number) {
|
const checkboxClick = (id: number, amount: number) => {
|
||||||
try {
|
console.log('checkboxClick')
|
||||||
const { data } = await recordAPI.getOne(id)
|
if (propDataForRecordButtons.value.isCloseStatus) {
|
||||||
const { value: formValues } = await ConfirmBox.fire({
|
const index = propDataForRecordButtons.value.closeRecords.findIndex((recordId) => recordId === id)
|
||||||
title: '資料編輯',
|
if (index !== undefined) {
|
||||||
html: `
|
if (index !== -1) {
|
||||||
<div class="d-flex mb-2">
|
propDataForRecordButtons.value.closeRecords.splice(index, 1)
|
||||||
<div class="col-auto me-3">
|
propDataForRecordButtons.value.closeRecordsAmount -= amount
|
||||||
<label for="swal-item" class="col-form-label">項目</label>
|
} else {
|
||||||
</div>
|
propDataForRecordButtons.value.closeRecords.push(id)
|
||||||
<div class="col-auto">
|
propDataForRecordButtons.value.closeRecordsAmount += amount
|
||||||
<input value="${data.item}" type="text" id="swal-item" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-merchant" class="col-form-label">商家</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input value="${data.merchant}" type="text" id="swal-merchant" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-amount" class="col-form-label">金額</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input value="${data.amount}" type="number" id="swal-amount" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-date" class="col-form-label">日期</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input value="${new Date(data.date)
|
|
||||||
.toISOString()
|
|
||||||
.substring(0, 10)}" type="date" id="swal-date" class="form-control" >
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mb-2">
|
|
||||||
<div class="col-auto me-3">
|
|
||||||
<label for="swal-editor" class="col-form-label">編輯者</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<select id="swal-editor" class="form-select">
|
|
||||||
<option selected>${store.currentUser}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
preConfirm: () => {
|
|
||||||
const item = (document.getElementById('swal-item') as HTMLInputElement).value
|
|
||||||
const merchant = (document.getElementById('swal-merchant') as HTMLInputElement).value
|
|
||||||
const amount = (document.getElementById('swal-amount') as HTMLInputElement).value
|
|
||||||
const date = new Date((document.getElementById('swal-date') as HTMLInputElement).value)
|
|
||||||
const editor = (document.getElementById('swal-editor') as HTMLInputElement).value
|
|
||||||
if (!item || !merchant || !amount || !date || !editor) {
|
|
||||||
Swal.showValidationMessage('所有資料都是必填!若編輯者為空,請登入~')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: data.id as number,
|
|
||||||
input: {
|
|
||||||
item,
|
|
||||||
merchant,
|
|
||||||
amount,
|
|
||||||
date,
|
|
||||||
editor
|
|
||||||
} as RecordInput
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
if (formValues?.input) {
|
|
||||||
editRecord(formValues)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editRecord = async function (formValues: { id: number; input: RecordInput }) {
|
const handleCloseFunction = (func: string) => {
|
||||||
try {
|
if (func === 'cancelBtnClick') {
|
||||||
await recordAPI.edit(formValues.id, formValues.input)
|
propDataForRecordButtons.value.isCloseStatus = false
|
||||||
fetchRecords()
|
propDataForRecordButtons.value.closeRecords = []
|
||||||
Toast.fire({
|
propDataForRecordButtons.value.closeRecordsAmount = 0
|
||||||
icon: 'success',
|
|
||||||
title: '成功編輯資料!'
|
|
||||||
})
|
|
||||||
// lineBot push
|
|
||||||
const input = {
|
|
||||||
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
|
||||||
messages: {
|
|
||||||
type: 'text',
|
|
||||||
text: `${store.currentUser + store.icon}編輯了一筆紀錄 →\n${formValues.input.merchant}-${
|
|
||||||
formValues.input.item
|
|
||||||
} $${formValues.input.amount}`
|
|
||||||
}
|
}
|
||||||
}
|
if (func === 'closeBtnClick') {
|
||||||
await recordAPI.pushLineMsg(input)
|
propDataForRecordButtons.value.isCloseStatus = true
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: '編輯資料失敗!'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeBtnClick = () => {
|
|
||||||
if (!store.currentUser) {
|
|
||||||
Swal.fire('請先登入才能結算!', '', 'warning')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isCloseStatus.value = true
|
|
||||||
const recordIds = []
|
const recordIds = []
|
||||||
let total = 0
|
let total = 0
|
||||||
for (let record of records.value) {
|
for (let record of records.value) {
|
||||||
recordIds.push(record.id)
|
recordIds.push(record.id)
|
||||||
total += record.amount
|
total += record.amount
|
||||||
}
|
}
|
||||||
closeRecords.value = recordIds
|
propDataForRecordButtons.value.closeRecords = recordIds
|
||||||
closeRecordsAmount.value = total
|
propDataForRecordButtons.value.closeRecordsAmount = total
|
||||||
}
|
}
|
||||||
|
if (func === 'closeRecord') {
|
||||||
const checkboxClick = (id: number, amount: number) => {
|
propDataForRecordButtons.value.isCloseStatus = false
|
||||||
if (isCloseStatus.value) {
|
|
||||||
const index = closeRecords.value?.findIndex((recordId) => recordId === id)
|
|
||||||
if (index !== undefined) {
|
|
||||||
if (index !== -1) {
|
|
||||||
closeRecords.value?.splice(index, 1)
|
|
||||||
closeRecordsAmount.value -= amount
|
|
||||||
} else {
|
|
||||||
closeRecords.value?.push(id)
|
|
||||||
closeRecordsAmount.value += amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelBtnClick = () => {
|
|
||||||
isCloseStatus.value = false
|
|
||||||
closeRecords.value = []
|
|
||||||
closeRecordsAmount.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const toCloseBtnClick = async () => {
|
|
||||||
const { isConfirmed } = await ConfirmBox.fire({
|
|
||||||
icon: 'info',
|
|
||||||
title: '確定結算資料?',
|
|
||||||
text: `結算金額為 $${closeRecordsAmount.value} [結算者: ${store.currentUser}]`
|
|
||||||
})
|
|
||||||
if (isConfirmed) {
|
|
||||||
closeRecord(closeRecordsAmount.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeRecord = async (amount: number) => {
|
|
||||||
try {
|
|
||||||
if (store.currentUser) {
|
|
||||||
await recordAPI.close({
|
|
||||||
records: closeRecords.value.toString(),
|
|
||||||
totalAmount: amount,
|
|
||||||
recorder: store.currentUser
|
|
||||||
})
|
|
||||||
isCloseStatus.value = false
|
|
||||||
fetchRecords()
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'success',
|
|
||||||
title: '成功結算資料!'
|
|
||||||
})
|
|
||||||
// lineBot push
|
|
||||||
const input = {
|
|
||||||
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
|
||||||
messages: {
|
|
||||||
type: 'text',
|
|
||||||
text: `${store.currentUser + store.icon}結算紀錄 → 總金額 $${amount}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await recordAPI.pushLineMsg(input)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: '結算資料失敗!'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// created
|
// created
|
||||||
fetchRecords()
|
fetchRecords()
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('refetchRecords', fetchRecords)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<div class="d-flex mb-3" style="width: 100vw">
|
<RecordButtons :propData="propDataForRecordButtons" @closeFunction="handleCloseFunction" />
|
||||||
<template v-if="!isCloseStatus">
|
<table class="table table-info table-hover" v-if="records.length">
|
||||||
<CreateRecordModalButton view="Record" :refetch="fetchRecords" class="me-3" />
|
|
||||||
<button type="button" class="btn btn-danger" @click="closeBtnClick">開始結算</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="btn btn-info fw-bold">結算金額 ${{ closeRecordsAmount }}</div>
|
|
||||||
<button type="button" class="btn btn-secondary ms-3" @click="cancelBtnClick">取消結算</button>
|
|
||||||
<button type="button" class="btn btn-success ms-3" @click="toCloseBtnClick">確定結算</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped table-info table-hover" v-if="records.length">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="col">#</th>
|
||||||
|
<th scope="col"></th>
|
||||||
<th scope="col">項目</th>
|
<th scope="col">項目</th>
|
||||||
<th scope="col">商家</th>
|
<th scope="col">商家</th>
|
||||||
<th scope="col">金額</th>
|
<th scope="col">金額</th>
|
||||||
@ -251,17 +95,20 @@ fetchRecords()
|
|||||||
<tr
|
<tr
|
||||||
v-for="(record, index) in records"
|
v-for="(record, index) in records"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="{ 'table-warning': closeRecords?.includes(record.id) }"
|
:class="{ 'table-success': propDataForRecordButtons.closeRecords?.includes(record.id) }"
|
||||||
@click="checkboxClick(record.id, record.amount)"
|
@click="checkboxClick(record.id, record.amount)"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
v-if="isCloseStatus"
|
v-if="propDataForRecordButtons.isCloseStatus"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="closeRecords?.includes(record.id)"
|
:checked="propDataForRecordButtons.closeRecords?.includes(record.id)"
|
||||||
/>
|
/>
|
||||||
<i v-else class="fas fa-edit" @click="editBtnClick(record.id)"></i>
|
<EditRecordModalButton v-else :record="record" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<DeleteRecordModalButton :record="record" v-if="store.currentUser?.Role.name_en === 'root'" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ record.item }}
|
{{ record.item }}
|
||||||
@ -273,10 +120,10 @@ fetchRecords()
|
|||||||
{{ record.amount }}
|
{{ record.amount }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ new Date(record.date).toLocaleDateString() + ` (${showWeekDay(record.date)})` }}
|
{{ new Date(record.date).toLocaleDateString() + ' ' + getDay(record.date) }}
|
||||||
</td>
|
</td>
|
||||||
<td id="column-item">
|
<td id="column-item">
|
||||||
{{ record.recorder }}
|
{{ record.User.displayName }}
|
||||||
</td>
|
</td>
|
||||||
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||||
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||||
@ -294,11 +141,6 @@ fetchRecords()
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
th,
|
|
||||||
td {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-top: 0.1em;
|
margin-top: 0.1em;
|
||||||
}
|
}
|
||||||
|
104
src/views/Register.vue
Normal file
104
src/views/Register.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Toast } from '@/utils/swal'
|
||||||
|
import { FirebaseUserInput } from '@/models'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// data
|
||||||
|
const user = ref(new FirebaseUserInput())
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const register = async () => {
|
||||||
|
try {
|
||||||
|
const { displayName, email, password } = user.value
|
||||||
|
if (!displayName || !email || !password) {
|
||||||
|
return Toast.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: '名稱、信箱、密碼為必填選項!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
return Toast.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: '密碼至少要6位!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 註冊至 firebase
|
||||||
|
const { data } = await userAPI.user.firebase_email_register(user.value)
|
||||||
|
// 存入 DB
|
||||||
|
await userAPI.user.create({
|
||||||
|
email: user.value.email,
|
||||||
|
displayName: user.value.displayName,
|
||||||
|
photoURL: user.value.photoURL,
|
||||||
|
firebaseUid: data.data.uid
|
||||||
|
})
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '註冊成功!'
|
||||||
|
})
|
||||||
|
router.push({ name: 'Login' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '註冊失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-info login-btn" type="button" @click="router.push({ name: 'Login' })">前往登入</button>
|
||||||
|
<div class="m-auto" style="width: 70vw">
|
||||||
|
<form>
|
||||||
|
<img src="@/assets/logo2.png" alt="" width="150" height="150" />
|
||||||
|
<h1 class="h3 mb-3 fw-normal">臭建喵記帳 Register</h1>
|
||||||
|
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text" class="form-control" id="name-register" v-model="user.displayName" autocomplete="on" />
|
||||||
|
<label for="name-register">名稱</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="url" class="form-control" id="photoUrl-register" v-model="user.photoURL" autocomplete="on" />
|
||||||
|
<label for="photoUrl-register">大頭貼(url)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="email" class="form-control" id="email-register" v-model="user.email" autocomplete="on" />
|
||||||
|
<label for="email-register">信箱</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password-register"
|
||||||
|
v-model="user.password"
|
||||||
|
autocomplete="on"
|
||||||
|
/>
|
||||||
|
<label for="password-register">密碼</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="checkbox mb-3">
|
||||||
|
<label> <input type="checkbox" value="remember-me" /> Remember me </label>
|
||||||
|
</div> -->
|
||||||
|
<button
|
||||||
|
class="w-100 btn btn-lg mt-3"
|
||||||
|
type="button"
|
||||||
|
@click="register"
|
||||||
|
style="color: white; background-color: mediumaquamarine"
|
||||||
|
>
|
||||||
|
取得進入豬豬世界的門票
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.login-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
70
src/views/Role.vue
Normal file
70
src/views/Role.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, provide } from 'vue'
|
||||||
|
import userAPI from '@/apis/user'
|
||||||
|
import { Role } from '@/models/index'
|
||||||
|
import Spinner from '@/components/Spinner.vue'
|
||||||
|
import CreateRoleModalButton from '@/components/ModalButton/Role/CreateRoleModalButton.vue'
|
||||||
|
import EditRoleModalButton from '@/components/ModalButton/Role/EditRoleModalButton.vue'
|
||||||
|
import DeleteRoleModalButton from '@/components/ModalButton/Role/DeleteRoleModalButton.vue'
|
||||||
|
|
||||||
|
// data
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const roles = ref<Role[]>([])
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchRoles = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await userAPI.role.getAll()
|
||||||
|
roles.value = data.data
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
fetchRoles()
|
||||||
|
|
||||||
|
// provide
|
||||||
|
provide('refetchRoles', fetchRoles)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<CreateRoleModalButton />
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped table-danger table-hover" v-if="roles.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">中文名稱</th>
|
||||||
|
<th scope="col">英文名稱</th>
|
||||||
|
<th scope="col">啟用</th>
|
||||||
|
<th scope="col">#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(role, index) in roles" :key="index">
|
||||||
|
<td>{{ role.id }}</td>
|
||||||
|
<td>{{ role.name }}</td>
|
||||||
|
<td>{{ role.name_en }}</td>
|
||||||
|
<td>{{ role.deletedAt === null ? 'V' : 'X' }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{ name: 'Admin-Role-Access', params: { id: role.id } }">
|
||||||
|
<i class="fa-solid fa-circle-question"></i
|
||||||
|
></router-link>
|
||||||
|
<EditRoleModalButton :role="role" class="ms-2" />
|
||||||
|
<DeleteRoleModalButton :role="role" class="ms-2" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
30
src/views/Tools.vue
Normal file
30
src/views/Tools.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const num_1 = ref<number>()
|
||||||
|
const num_2 = ref<number>()
|
||||||
|
|
||||||
|
const plus = () => {
|
||||||
|
num_1.value = num_1.value! + num_2.value!
|
||||||
|
num_2.value = undefined
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">計算機</span>
|
||||||
|
<input type="number" class="form-control" v-model="num_1" />
|
||||||
|
<input type="number" class="form-control" v-model="num_2" />
|
||||||
|
<span class="input-group-text" @click="plus">加總</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-3 p-3 bg-info">
|
||||||
|
<p>~2022/2/7</p>
|
||||||
|
<p>加總 3931</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="exampleFormControlTextarea1" class="form-label">Note</label>
|
||||||
|
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -13,18 +13,12 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": ["webpack-env"],
|
"types": ["webpack-env", "vite/client"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"tests/**/*.ts",
|
|
||||||
"tests/**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
15
vite.config.js
Normal file
15
vite.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// vite.config.js
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,3 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? '/jm-expense-vue-ts/' : '/'
|
// publicPath: process.env.NODE_ENV === 'production' ? '/jm-expense-vue-ts/' : '/'
|
||||||
|
publicPath: import.meta.env.NODE_ENV === 'production' ? '/jm-expense-vue-ts/' : '/'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user