mirror of
https://github.com/KarolChang/jm-expense-vue-ts.git
synced 2024-12-25 11:18:36 +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 name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> -->
|
||||
<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>
|
||||
sessionStorage.redirect = location.href;
|
||||
@ -15,12 +17,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong
|
||||
<!-- <strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||
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>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</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>
|
||||
# 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
|
||||
|
||||
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 name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> -->
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
@ -20,12 +22,17 @@
|
||||
})()
|
||||
</script>
|
||||
<noscript>
|
||||
<strong
|
||||
<!-- <strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||
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>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueform/multiselect": "^2.3.1",
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootswatch": "^5.1.3",
|
||||
"core-js": "^3.6.5",
|
||||
"dayjs": "^1.10.7",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.14.28",
|
||||
"esbuild-darwin-64": "^0.14.28",
|
||||
"firebase": "^9.6.3",
|
||||
"luxon": "^1.0.0",
|
||||
"pinia": "^2.0.6",
|
||||
"sweetalert2": "^11.3.0",
|
||||
"vue": "^3.2.27",
|
||||
"vue": "^3.2.29",
|
||||
"vue-class-component": "^8.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": {
|
||||
"@vitejs/plugin-vue": "^1.6.1",
|
||||
"vite": "^2.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^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/eslint-config-typescript": "^7.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"typescript": "~4.1.5"
|
||||
|
49
src/App.vue
49
src/App.vue
@ -1,25 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import Navbar from '@/components/Navbar.vue'
|
||||
import { onLoad } from './cocos/config'
|
||||
import { useRoute } from 'vue-router'
|
||||
const route = useRoute()
|
||||
|
||||
// created
|
||||
onLoad()
|
||||
if (import.meta.env.NODE_ENV === 'production') {
|
||||
onLoad()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Navbar>
|
||||
<template #main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-fast" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
</Navbar>
|
||||
</div>
|
||||
<!-- <div> -->
|
||||
<Navbar>
|
||||
<template #main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-fast" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
</Navbar>
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@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;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.color-orange {
|
||||
color: coral;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: coral;
|
||||
}
|
||||
|
||||
/* transition */
|
||||
.slide-x-enter-active {
|
||||
transition: transform 0.3s ease-out;
|
||||
@ -101,4 +117,11 @@ body {
|
||||
.slide-right-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
th,
|
||||
td,
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<!-- <style src="@vueform/multiselect/themes/default.css"></style> -->
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { apiHelper } from './helper'
|
||||
import { ExpenseInput } from '../models/Expense'
|
||||
import { CategoryInput } from '../models/Category'
|
||||
import { ExpenseInput, CategoryInput } from '@/models'
|
||||
|
||||
export default {
|
||||
expense: {
|
||||
@ -12,6 +11,12 @@ export default {
|
||||
},
|
||||
create(data: ExpenseInput) {
|
||||
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: {
|
||||
|
@ -5,10 +5,10 @@ export const apiHelper = axios.create({
|
||||
baseURL
|
||||
})
|
||||
|
||||
// const baseURL_Line = 'https://linebot20220114.herokuapp.com/'
|
||||
// export const apiHelperLine = axios.create({
|
||||
// baseURL: baseURL_Line
|
||||
// })
|
||||
const baseURL_Line = 'http://linebot20220114.herokuapp.com'
|
||||
export const apiHelperLine = axios.create({
|
||||
baseURL: baseURL_Line
|
||||
})
|
||||
|
||||
export interface Message {
|
||||
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 { RecordInput } from '../models/Record'
|
||||
import { RecordInput } from '@/models'
|
||||
|
||||
export default {
|
||||
getAll() {
|
||||
@ -14,13 +14,13 @@ export default {
|
||||
edit(id: number, data: RecordInput) {
|
||||
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)
|
||||
},
|
||||
getLogs() {
|
||||
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])
|
||||
}
|
||||
}
|
||||
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']
|
||||
if (target) {
|
||||
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">
|
||||
import { inject } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||
import expenseAPI from '../../apis/expense'
|
||||
import { CategoryInput } from '../../models/Category'
|
||||
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||
import expenseAPI from '@/apis/expense'
|
||||
import { CategoryInput } from '@/models'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
refetch: () => {}
|
||||
}>()
|
||||
// inject
|
||||
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||
|
||||
// methods
|
||||
const btnClick = async () => {
|
||||
@ -15,6 +14,17 @@ const btnClick = async () => {
|
||||
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 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="col-auto me-3">
|
||||
<label for="swal-name" class="col-form-label">名稱</label>
|
||||
@ -28,7 +38,7 @@ const btnClick = async () => {
|
||||
<label for="swal-icon" class="col-form-label">Icon</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="text" id="swal-icon" class="form-control" >
|
||||
<input type="text" id="swal-icon" class="form-control" >
|
||||
</div>
|
||||
<a class="ms-4 mt-1" href="https://fontawesome.com/v5.15/icons?d=gallery&p=2" target="_blank">
|
||||
<span class="badge bg-info text-dark">找Icon</span>
|
||||
@ -44,6 +54,7 @@ const btnClick = async () => {
|
||||
</div>
|
||||
`,
|
||||
preConfirm: () => {
|
||||
const type = (document.getElementById('swal-expense') as HTMLInputElement).checked
|
||||
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
||||
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
||||
@ -56,6 +67,7 @@ const btnClick = async () => {
|
||||
}
|
||||
return {
|
||||
input: {
|
||||
type: type ? '支出' : '收入',
|
||||
name,
|
||||
icon: icon === '' ? null : icon,
|
||||
photoUrl: photoUrl === '' ? null : photoUrl
|
||||
@ -73,12 +85,13 @@ const btnClick = async () => {
|
||||
|
||||
const createExpense = async function (formValues: { input: CategoryInput }) {
|
||||
try {
|
||||
await expenseAPI.category.create(formValues.input)
|
||||
console.log('formValues.input', formValues.input)
|
||||
const { data } = await expenseAPI.category.create(formValues.input)
|
||||
refetchCategories()
|
||||
Toast.fire({
|
||||
icon: 'success',
|
||||
title: `成功建立類別[${formValues.input.name}]`
|
||||
title: `成功建立類別[${data.data.name}]`
|
||||
})
|
||||
props.refetch!()
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
Toast.fire({
|
@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||
import expenseAPI from '../../apis/expense'
|
||||
import { Category } from '../../models/Category'
|
||||
import { inject } from 'vue'
|
||||
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||
import expenseAPI from '@/apis/expense'
|
||||
import { Category } from '@/models'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
refetch: () => {}
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
// inject
|
||||
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||
|
||||
// methods
|
||||
const btnClick = async () => {
|
||||
try {
|
||||
@ -27,12 +30,14 @@ const btnClick = async () => {
|
||||
const deleteCategory = async function (id: number) {
|
||||
try {
|
||||
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({
|
||||
icon: 'success',
|
||||
title: `成功刪除類別[${data.deletedCategory.name}]`
|
||||
title: `成功刪除類別[${data.data.name}]`
|
||||
})
|
||||
props.refetch()
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
Toast.fire({
|
||||
@ -45,3 +50,9 @@ const deleteCategory = async function (id: number) {
|
||||
<template>
|
||||
<i class="fas fa-trash" @click="btnClick"></i>
|
||||
</template>
|
||||
<style scoped>
|
||||
i:hover {
|
||||
color: rgb(226, 19, 19);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||
import expenseAPI from '../../apis/expense'
|
||||
import { Category, CategoryInput } from '../../models/Category'
|
||||
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||
import expenseAPI from '@/apis/expense'
|
||||
import { Category, CategoryInput } from '@/models'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
refetch: () => {}
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
// inject
|
||||
const refetchCategories = inject<() => {}>('refetchCategories')!
|
||||
|
||||
// methods
|
||||
const btnClick = async () => {
|
||||
try {
|
||||
@ -17,6 +20,21 @@ const btnClick = async () => {
|
||||
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 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="col-auto me-3">
|
||||
<label for="swal-name" class="col-form-label">名稱</label>
|
||||
@ -55,6 +73,7 @@ const btnClick = async () => {
|
||||
</div>
|
||||
`,
|
||||
preConfirm: () => {
|
||||
const type = (document.getElementById('swal-expense') as HTMLInputElement).checked
|
||||
const name = (document.getElementById('swal-name') as HTMLInputElement).value
|
||||
const icon = (document.getElementById('swal-icon') as HTMLInputElement).value
|
||||
const photoUrl = (document.getElementById('swal-photoUrl') as HTMLInputElement).value
|
||||
@ -67,6 +86,7 @@ const btnClick = async () => {
|
||||
}
|
||||
return {
|
||||
input: {
|
||||
type: type ? '支出' : '收入',
|
||||
name,
|
||||
icon: icon === '' ? null : icon,
|
||||
photoUrl: photoUrl === '' ? null : photoUrl
|
||||
@ -75,21 +95,24 @@ const btnClick = async () => {
|
||||
}
|
||||
})
|
||||
if (formValues) {
|
||||
editCategoryName(formValues)
|
||||
editCategory(formValues)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const editCategoryName = async function (formValues: { input: CategoryInput }) {
|
||||
const editCategory = async function (formValues: { input: CategoryInput }) {
|
||||
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({
|
||||
icon: 'success',
|
||||
title: `成功建立類別[${formValues.input.name}]`
|
||||
title: `成功編輯類別[${data.data.name}]`
|
||||
})
|
||||
props.refetch()
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
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">
|
||||
import { inject } from 'vue'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||
import recordAPI from '../../apis/record'
|
||||
import { RecordInput } from '../../models/Record'
|
||||
import { useStore } from '../../store/index'
|
||||
const store = useStore()
|
||||
import { Toast, ConfirmBox } from '@/utils/swal'
|
||||
import recordAPI from '@/apis/record'
|
||||
import { pushMsgToBoth } from '@/utils/lineBotMsg'
|
||||
import { RecordInput } from '@/models'
|
||||
import { CallParent } from '@/cocos/config'
|
||||
import { useStore } from '@/store/index'
|
||||
import { useRouter } from 'vue-router'
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
// inject
|
||||
const refetchRecords = inject<() => {}>('refetchRecords')!
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
view: 'Home' | 'Record'
|
||||
refetch?: () => {}
|
||||
}>()
|
||||
|
||||
// methods
|
||||
@ -52,24 +57,13 @@ const btnClick = async () => {
|
||||
<input type="date" id="swal-date" class="form-control" value="${new Date().toJSON().substring(0, 10)}">
|
||||
</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: () => {
|
||||
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 recorder = (document.getElementById('swal-recorder') as HTMLInputElement).value
|
||||
if (!item || !merchant || !amount || !date || !recorder) {
|
||||
if (!item || !merchant || !amount || !date) {
|
||||
Swal.showValidationMessage('所有資料都是必填!若紀錄者為空,請登入~')
|
||||
}
|
||||
return {
|
||||
@ -78,7 +72,7 @@ const btnClick = async () => {
|
||||
merchant,
|
||||
amount,
|
||||
date,
|
||||
recorder
|
||||
UserId: store.currentUser?.id
|
||||
} as RecordInput
|
||||
}
|
||||
}
|
||||
@ -99,11 +93,15 @@ const createRecord = async function (formValues: { input: RecordInput }) {
|
||||
title: '成功建立資料!'
|
||||
})
|
||||
if (props.view === 'Record') {
|
||||
props.refetch!()
|
||||
refetchRecords()
|
||||
} else {
|
||||
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) {
|
||||
console.error('error', error)
|
||||
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>
|
||||
<template>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" @click="btnClick">新增資料</button>
|
||||
<button type="button" class="btn btn-success" @click="btnClick">新增資料</button>
|
||||
</div>
|
||||
</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">
|
||||
import { ref } from 'vue'
|
||||
import { ref, provide } from 'vue'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
import UserRP from '@/components/RightPanel/UserRP.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useStore } from '@/store/index'
|
||||
const store = useStore()
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="d-flex">
|
||||
<!-- 登入後 -->
|
||||
<div class="d-flex" v-if="store.firebaseUser">
|
||||
<transition name="slide-x">
|
||||
<Sidebar v-show="sidebarOpen" @open="sidebarOpen = false" />
|
||||
<Sidebar v-show="sidebarOpen" @openUserRP="userRPOpen = true" />
|
||||
</transition>
|
||||
|
||||
<div class="m-2" style="width: 100vw">
|
||||
@ -20,7 +29,14 @@ const sidebarOpen = ref(false)
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<slot name="main" />
|
||||
<transition name="slide-right">
|
||||
<UserRP v-show="userRPOpen" />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 未登入 -->
|
||||
<div v-else>
|
||||
<slot name="main" />
|
||||
</div>
|
||||
</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">
|
||||
import { computed } from 'vue'
|
||||
import { routes } from '../router/index'
|
||||
import { computed, inject, Ref } from 'vue'
|
||||
import router, { routes } from '../router/index'
|
||||
import { useRoute, RouteRecordRaw } from 'vue-router'
|
||||
import { useStore } from '../store/index'
|
||||
import Swal from 'sweetalert2'
|
||||
import { ConfirmBox } from '../utils/swal'
|
||||
import { Toast } from '@/utils/swal'
|
||||
import { getAuth, signOut } from 'firebase/auth'
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
const getUser = store.switchUser
|
||||
const emit = defineEmits(['open'])
|
||||
const emit = defineEmits(['openUserRP'])
|
||||
|
||||
// inject
|
||||
const sidebarOpen = inject<Ref<boolean>>('sidebarOpen')!
|
||||
|
||||
// computed
|
||||
const items = computed(() => {
|
||||
return routes.filter((route: RouteRecordRaw) => route.meta?.show)
|
||||
})
|
||||
|
||||
const changeItem = () => {
|
||||
emit('open', false)
|
||||
// methods
|
||||
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 { value: user } = await ConfirmBox.fire({
|
||||
title: '切換使用者',
|
||||
input: 'select',
|
||||
inputOptions: { 建喵: '建喵', 豬涵: '豬涵' },
|
||||
inputPlaceholder: 'who are you?',
|
||||
showCancelButton: true
|
||||
})
|
||||
if (user) {
|
||||
getUser(user)
|
||||
Swal.fire(`使用者已切換至 [${user}]`)
|
||||
}
|
||||
const openUserRP = () => {
|
||||
sidebarOpen.value = false
|
||||
emit('openUserRP', true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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
|
||||
:to="{ name: 'Home' }"
|
||||
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>
|
||||
<hr />
|
||||
<ul class="nav nav-pills flex-column mb-auto">
|
||||
<hr class="my-1" />
|
||||
<ul class="nav nav-pills mb-auto">
|
||||
<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
|
||||
: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 }"
|
||||
:class="'nav-link ' + (item.name === route.name ? 'text-info' : 'text-white')"
|
||||
@click="changeItem"
|
||||
:class="'nav-link fw-bold ' + (item.name === route.name ? 'text-danger' : 'text-white')"
|
||||
@click="sidebarOpen = false"
|
||||
>{{ item.meta?.pageTitle }}</router-link
|
||||
>
|
||||
</li>
|
||||
@ -63,32 +98,24 @@ const switchModalOpen = async () => {
|
||||
aria-expanded="false"
|
||||
>
|
||||
<img
|
||||
v-if="store.currentUser === '建喵'"
|
||||
src="../assets/jianmiau-login.png"
|
||||
alt=""
|
||||
width="50"
|
||||
:src="store.firebaseUser?.photoURL || '../assets/capoo.gif'"
|
||||
alt="photo"
|
||||
width="55"
|
||||
height="55"
|
||||
class="rounded-circle me-2"
|
||||
/>
|
||||
<img
|
||||
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>
|
||||
<strong>{{ store.firebaseUser?.displayName }}</strong>
|
||||
</a>
|
||||
<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><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>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <transition name="slide-right">
|
||||
<UserRP v-show="userRPOpen" />
|
||||
</transition> -->
|
||||
</div>
|
||||
</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 router from './router'
|
||||
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/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 {
|
||||
id!: number
|
||||
name!: string
|
||||
icon?: string
|
||||
photoUrl?: string
|
||||
deletedAt?: Date | null
|
||||
icon!: string
|
||||
// photoUrl?: string
|
||||
type!: CategoryType
|
||||
deletedAt!: Date | null
|
||||
createdAt!: Date
|
||||
updatedAt!: Date
|
||||
}
|
||||
|
||||
export class CategoryInput {
|
||||
name!: string
|
||||
icon?: string
|
||||
photoUrl?: string
|
||||
type!: CategoryType
|
||||
icon!: string
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { Category } from './Category'
|
||||
import { User } from './User'
|
||||
|
||||
export class Expense {
|
||||
id!: number
|
||||
date!: Date
|
||||
item?: string
|
||||
item!: string
|
||||
amount!: number
|
||||
note?: string
|
||||
deletedAt?: Date | null
|
||||
deletedAt!: Date | null
|
||||
createdAt!: Date
|
||||
updatedAt!: Date
|
||||
Category!: Category
|
||||
User!: User
|
||||
}
|
||||
|
||||
export class ExpenseInput {
|
||||
@ -18,4 +20,5 @@ export class ExpenseInput {
|
||||
amount!: string
|
||||
note?: string
|
||||
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 = '建喵' | '豬涵'
|
||||
|
||||
export type ActionType = '新增' | '編輯' | '結算'
|
||||
import { User } from './User'
|
||||
import { Record } from './Record'
|
||||
type ActionType = '新增' | '編輯' | '結算'
|
||||
|
||||
export class Log {
|
||||
recorder!: RecorderType
|
||||
action!: ActionType
|
||||
item?: string
|
||||
merchant?: string
|
||||
@ -18,4 +17,6 @@ export class Log {
|
||||
RecordIds?: string
|
||||
createdAt!: 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 {
|
||||
id!: number
|
||||
date!: Date
|
||||
item?: string
|
||||
merchant?: string
|
||||
item!: string
|
||||
merchant!: string
|
||||
amount!: number
|
||||
recorder?: RecorderType
|
||||
isClosed?: boolean
|
||||
deletedAt?: Date | null
|
||||
isClosed!: boolean
|
||||
deletedAt!: Date | null
|
||||
createdAt!: Date
|
||||
updatedAt!: Date
|
||||
User!: User
|
||||
}
|
||||
|
||||
export class RecordInput {
|
||||
@ -18,6 +19,5 @@ export class RecordInput {
|
||||
item!: string
|
||||
merchant!: string
|
||||
amount!: string
|
||||
recorder?: RecorderType
|
||||
editor?: RecorderType
|
||||
UserId!: number
|
||||
}
|
||||
|
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 { useStore } from '../store/index'
|
||||
import Default from '@/views/Default.vue'
|
||||
|
||||
const root = '/jm-expense-vue-ts'
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
@ -6,6 +10,24 @@ export const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
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}/`,
|
||||
name: 'Home',
|
||||
@ -51,6 +73,93 @@ export const routes: RouteRecordRaw[] = [
|
||||
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(.*)*',
|
||||
name: 'NotFound',
|
||||
@ -63,4 +172,28 @@ const router = createRouter({
|
||||
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
|
||||
|
@ -1,16 +1,55 @@
|
||||
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', {
|
||||
state: () => ({
|
||||
currentUser: localStorage.getItem('jm-user')
|
||||
// currentUser: localStorage.getItem('jm-user')
|
||||
firebaseUser: null as null | FirebaseUser,
|
||||
currentUser: null as null | User
|
||||
}),
|
||||
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: {
|
||||
switchUser(user: string) {
|
||||
localStorage.setItem('jm-user', user)
|
||||
this.currentUser = user
|
||||
// switchUser(user: string) {
|
||||
// localStorage.setItem('jm-user', 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">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, provide } from 'vue'
|
||||
import recordAPI from '../apis/record'
|
||||
import { Record } from '../models/Record'
|
||||
import { showWeekDay } from '../utils/helper'
|
||||
import { Record } from '@/models/index'
|
||||
import { getDay } from '../utils/dateFormat'
|
||||
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
|
||||
const isLoading = ref<boolean>(true)
|
||||
const records = ref<Record[]>([])
|
||||
const searchMode = ref<SearchMode>('月份')
|
||||
const year = ref()
|
||||
const month = ref()
|
||||
const startDate = ref<Date | string>('')
|
||||
const finishDate = ref<Date | string>('')
|
||||
const dateFilterData = ref<DateFilterData>({
|
||||
searchMode: '月份',
|
||||
filter: {
|
||||
year: 0,
|
||||
month: 0,
|
||||
startDate: '',
|
||||
finishDate: ''
|
||||
}
|
||||
})
|
||||
|
||||
// methods
|
||||
const fetchRecords = async function () {
|
||||
try {
|
||||
const { data } = await recordAPI.getAll()
|
||||
records.value = data.filter((item: Record) => item.isClosed === true && item.deletedAt === null)
|
||||
year.value = new Date(records.value[0].date).getFullYear()
|
||||
month.value = new Date(records.value[0].date).getMonth() + 1
|
||||
records.value = data.data.filter((item: Record) => item.isClosed === true)
|
||||
dateFilterData.value.filter = {
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const radioClick = (radio: SearchMode) => {
|
||||
searchMode.value = radio
|
||||
}
|
||||
|
||||
// computed
|
||||
const filteredRecord = computed(() => {
|
||||
if (searchMode.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()
|
||||
)
|
||||
}
|
||||
}
|
||||
return dateFilter(dateFilterData.value, records.value)
|
||||
})
|
||||
|
||||
// created
|
||||
fetchRecords()
|
||||
|
||||
// provide
|
||||
provide('dateFilterData', dateFilterData)
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="!isLoading">
|
||||
<div class="d-flex mb-4">
|
||||
<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>
|
||||
<DateFilter />
|
||||
<table class="table table-striped table-info table-hover" v-if="filteredRecord.length">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -139,21 +67,11 @@ fetchRecords()
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(record, index) in filteredRecord" :key="index">
|
||||
<td>
|
||||
{{ record.item }}
|
||||
</td>
|
||||
<td>
|
||||
{{ record.merchant }}
|
||||
</td>
|
||||
<td>
|
||||
{{ record.amount }}
|
||||
</td>
|
||||
<td>
|
||||
{{ new Date(record.date).toLocaleDateString() + ` (${showWeekDay(record.date)})` }}
|
||||
</td>
|
||||
<td id="column-item">
|
||||
{{ record.recorder }}
|
||||
</td>
|
||||
<td>{{ record.item }}</td>
|
||||
<td>{{ record.merchant }}</td>
|
||||
<td>{{ record.amount }}</td>
|
||||
<td>{{ new Date(record.date).toLocaleDateString() + ' ' + getDay(record.date) }}</td>
|
||||
<td id="column-item">{{ record.User.displayName }}</td>
|
||||
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||
</tr>
|
||||
@ -162,8 +80,8 @@ fetchRecords()
|
||||
<img
|
||||
v-else
|
||||
class="img-fluid"
|
||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||
alt=""
|
||||
src="https://stickershop.line-scdn.net/stickershop/v1/sticker/208430466/iPhone/sticker_animation@2x.png"
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
<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,175 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import expenseAPI from '../apis/expense'
|
||||
import { Expense, ExpenseInput } from '../models/Expense'
|
||||
import Swal from 'sweetalert2'
|
||||
import { Toast, ConfirmBox } from '../utils/swal'
|
||||
import { showWeekDay } from '../utils/helper'
|
||||
import { ref, provide, computed, watch } from 'vue'
|
||||
import expenseAPI from '@/apis/expense'
|
||||
import { Expense, Category } from '@/models'
|
||||
import { getDay } from '../utils/dateFormat'
|
||||
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
|
||||
const isLoading = ref<boolean>(true)
|
||||
const expenses = ref<Expense[]>([])
|
||||
const rightPanelOpen = ref(false)
|
||||
const expenses = ref<Expense[]>()
|
||||
const categories = ref<Category[]>()
|
||||
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
|
||||
const fetchExpenses = async function () {
|
||||
try {
|
||||
const { data } = await expenseAPI.expense.getAll()
|
||||
expenses.value = data.filter((item: Expense) => item.deletedAt === null)
|
||||
isLoading.value = false
|
||||
expenses.value = data.data
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const createBtnClick = async () => {
|
||||
const fetchCategories = 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">
|
||||
<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)
|
||||
}
|
||||
const { data } = await expenseAPI.category.getAll()
|
||||
categories.value = data.data
|
||||
categoryFilters.value = categories.value!.map((item: Category) => item.id)
|
||||
} catch (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
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div v-if="!isLoading">
|
||||
<div v-if="expenses && categories">
|
||||
<!-- buttons -->
|
||||
<div class="d-flex mb-3" style="width: 100vw">
|
||||
<button type="button" class="btn btn-primary me-3" @click="createBtnClick">新增資料</button>
|
||||
<button type="button" class="btn btn-warning" @click="rightPanelOpen = !rightPanelOpen">查看類別</button>
|
||||
<CreateExpenseModalButton />
|
||||
<button type="button" class="btn btn-warning text-dark" @click="categoryRPOpen = true">查看類別</button>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(expense, index) in filteredExpenses" :key="index">
|
||||
<tr :class="expense.Category.type === '支出' ? 'table-success' : 'table-warning'">
|
||||
<td>
|
||||
<EditExpenseModalButton :expense="expense" />
|
||||
</td>
|
||||
<td>
|
||||
<DeleteExpenseModalButton :expense="expense" />
|
||||
</td>
|
||||
<td>
|
||||
<i :class="expense.Category.icon"></i>
|
||||
</td>
|
||||
<td>{{ expense.item }}</td>
|
||||
<td>{{ expense.amount }}</td>
|
||||
<td>{{ expense.note }}</td>
|
||||
<td>{{ new Date(expense.date).toLocaleDateString() + ' ' + getDay(expense.date) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</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>
|
||||
<table class="table table-striped table-success table-hover" v-if="expenses.length">
|
||||
<thead>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(expense, index) in expenses" :key="index">
|
||||
<td>
|
||||
<i class="fas fa-edit" @click="editBtnClick(expense.id)"></i>
|
||||
</td>
|
||||
<td>
|
||||
{{ expense.Category.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ expense.item }}
|
||||
</td>
|
||||
<td>
|
||||
{{ expense.amount }}
|
||||
</td>
|
||||
<td>
|
||||
{{ expense.note }}
|
||||
</td>
|
||||
<td>
|
||||
{{ new Date(expense.date).toLocaleDateString() + ` (${showWeekDay(expense.date)})` }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- <img
|
||||
v-else
|
||||
class="img-fluid"
|
||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||
alt=""
|
||||
/> -->
|
||||
<transition name="slide-right">
|
||||
<RightPanel v-show="rightPanelOpen" />
|
||||
<CategoryRP v-if="categoryRPOpen" />
|
||||
</transition>
|
||||
</div>
|
||||
<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">
|
||||
import { ref } from 'vue'
|
||||
import recordAPI from '../apis/record'
|
||||
import { Record } from '../models/Record'
|
||||
import Spinner from '../components/Spinner.vue'
|
||||
import CreateRecordModalButton from '../components/modalButton/CreateRecordModalButton.vue'
|
||||
import recordAPI from '@/apis/record'
|
||||
import lineBotAPI from '@/apis/lineBot'
|
||||
import { Record } from '@/models'
|
||||
import { CallParent } from '@/cocos/config'
|
||||
// components
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
import CreateRecordModalButton from '@/components/ModalButton/Record/CreateRecordModalButton.vue'
|
||||
|
||||
// class
|
||||
class MonthData {
|
||||
total!: number
|
||||
closedAmount!: number
|
||||
@ -21,7 +25,7 @@ const lastMonthData = ref<MonthData>(new MonthData())
|
||||
const fetchRecords = async function () {
|
||||
try {
|
||||
const { data } = await recordAPI.getAll()
|
||||
records.value = data.filter((item: Record) => item.deletedAt === null)
|
||||
records.value = data.data
|
||||
// monthData
|
||||
const nowYear = new Date().getFullYear()
|
||||
const nowMonth = new Date().getMonth() + 1
|
||||
@ -67,20 +71,24 @@ fetchRecords()
|
||||
|
||||
// 笨蛋才按我
|
||||
const handle = async () => {
|
||||
const input = {
|
||||
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
||||
messages: { type: 'text', text: '卡比覺得促咪!' }
|
||||
try {
|
||||
const input = {
|
||||
to: [import.meta.env.VITE_KAROL_USERID, import.meta.env.VITE_JIANMIAU_USERID],
|
||||
messages: { type: 'text', text: '卡比覺得促咪!' }
|
||||
}
|
||||
await lineBotAPI.push(input)
|
||||
CallParent('Speak', '按我了 你是笨蛋')
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
console.log('input', input)
|
||||
await recordAPI.pushLineMsg(input)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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" />
|
||||
<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 class="list-group list-group-checkable">
|
||||
<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">
|
||||
import { computed, ref } from 'vue'
|
||||
import { showWeekDay } from '../utils/helper'
|
||||
import recordAPI from '../apis/record'
|
||||
import { Log } from '../models/Log'
|
||||
import { getDay } from '../utils/dateFormat'
|
||||
import recordAPI from '@/apis/record'
|
||||
import { Log, Record } from '@/models'
|
||||
import Swal from 'sweetalert2'
|
||||
import Spinner from '../components/Spinner.vue'
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
|
||||
const tabs = [
|
||||
{ title: '總覽', btnColor: 'secondary' },
|
||||
@ -12,43 +12,36 @@ const tabs = [
|
||||
{ title: '編輯', btnColor: 'success' },
|
||||
{ title: '結算', btnColor: 'danger' }
|
||||
]
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(true)
|
||||
const logs = ref<Log[]>([])
|
||||
const nowTab = ref('總覽')
|
||||
const logCount = ref<any>({ 總覽: 5, 新增: 5, 編輯: 5, 結算: 5 })
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
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 () {
|
||||
try {
|
||||
const { data } = await recordAPI.getLogs()
|
||||
logs.value = data
|
||||
logs.value = data.data
|
||||
isLoading.value = false
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const listIconClick = async (recordIds: string | undefined) => {
|
||||
if (!recordIds) return
|
||||
const recordIdArr = recordIds.split(',')
|
||||
const recordArr = []
|
||||
for (const id of recordIdArr) {
|
||||
const { data } = await recordAPI.getOne(Number(id))
|
||||
recordArr.push(data)
|
||||
}
|
||||
const listIconClick = async (records: Record[]) => {
|
||||
if (!records.length) return
|
||||
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) {
|
||||
const { data } = await recordAPI.getOne(Number(id))
|
||||
html += `<tr><td>${data.item}</td><td>${data.merchant}</td><td>${data.amount}</td><td>${new Date(
|
||||
data.date
|
||||
for (const record of records) {
|
||||
html += `<tr><td>${record.item}</td><td>${record.merchant}</td><td>${record.amount}</td><td>${new Date(
|
||||
record.date
|
||||
).toLocaleDateString()}</td></tr>`
|
||||
}
|
||||
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({
|
||||
title: '結算紀錄',
|
||||
@ -61,115 +54,52 @@ fetchLogs()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isLoading && logs.length">
|
||||
<template v-if="logs.length">
|
||||
<div id="pc">
|
||||
<div class="btn-group mb-3" role="group" aria-label="Basic radio toggle button group">
|
||||
<template v-for="(tab, index) in tabs" :key="index">
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="btnradio"
|
||||
:id="tab.title"
|
||||
autocomplete="off"
|
||||
:checked="nowTab === tab.title"
|
||||
/>
|
||||
<label :class="`btn btn-outline-${tab.btnColor}`" :for="tab.title" @click="nowTab = tab.title">{{
|
||||
tab.title
|
||||
}}</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
||||
<div class="col-2 mt-4">
|
||||
<div class="fw-bold">
|
||||
{{ new Date(log.createdAt).toLocaleDateString() }}
|
||||
<!-- {{ new Date(log.createdAt).toLocaleDateString() + ` (${showWeekDay(log.createdAt)})` }} -->
|
||||
</div>
|
||||
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="bg-white border p-3">
|
||||
<div class="d-flex">
|
||||
<img
|
||||
v-if="log.recorder === '建喵'"
|
||||
src="../assets/jianmiau.jpeg"
|
||||
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">
|
||||
<strong class="px-1 me-1" style="background-color: yellow">{{ log.recorder }}</strong>
|
||||
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
||||
<i class="fas fa-chevron-right m-2"></i>
|
||||
<template v-if="log.action === '新增' || log.action === '編輯'">
|
||||
<strong style="color: blue">{{ log.item }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchant }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amount }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.date || '').toLocaleDateString() }}</strong>
|
||||
</template>
|
||||
<template v-if="log.action === '編輯'">
|
||||
<br />
|
||||
<h6>
|
||||
原紀錄:<strong style="color: blue">{{ log.itemBefore }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchantBefore }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amountBefore }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.dateBefore || '').toLocaleDateString() }}</strong>
|
||||
</h6>
|
||||
</template>
|
||||
<template v-if="log.action === '結算'">
|
||||
<span>
|
||||
結算金額
|
||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||
</span>
|
||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.RecordIds)"></i>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isLoading">
|
||||
<div id="pc" style="width: 100vw">
|
||||
<div class="btn-group mb-3" role="group" aria-label="Basic radio toggle button group">
|
||||
<template v-for="(tab, index) in tabs" :key="index">
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="btnradio"
|
||||
:id="tab.title"
|
||||
autocomplete="off"
|
||||
:checked="nowTab === tab.title"
|
||||
/>
|
||||
<label :class="`btn btn-outline-${tab.btnColor}`" :for="tab.title" @click="nowTab = tab.title">{{
|
||||
tab.title
|
||||
}}</label>
|
||||
</template>
|
||||
</div>
|
||||
<!-- mobile -->
|
||||
<div id="mobile" class="card text-center">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item" v-for="tab in tabs" :key="tab.title" @click="nowTab = tab.title">
|
||||
<span class="nav-link" :class="{ active: nowTab === tab.title }">{{ tab.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
||||
<div class="col-3 mt-4">
|
||||
<div class="fw-bold">
|
||||
{{ new Date(log.createdAt).toLocaleDateString() + ' ' + getDay(log.createdAt) }}
|
||||
</div>
|
||||
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="card mb-3" v-for="(log, index) in filteredLogs.slice(0, logCount[nowTab])" :key="index">
|
||||
<div class="card-header">
|
||||
{{
|
||||
new Date(log.createdAt).toLocaleDateString() +
|
||||
` (${showWeekDay(log.createdAt)}) ` +
|
||||
new Date(log.createdAt).toLocaleTimeString()
|
||||
}}
|
||||
</div>
|
||||
<div class="card-body d-flex">
|
||||
<div class="col-9">
|
||||
<div class="bg-white border p-3">
|
||||
<div class="d-flex">
|
||||
<img
|
||||
v-if="log.recorder === '建喵'"
|
||||
v-if="log.User.displayName === '建喵'"
|
||||
src="../assets/jianmiau.jpeg"
|
||||
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">
|
||||
<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>
|
||||
<i class="fas fa-chevron-right m-2"></i>
|
||||
<template v-if="log.action === '新增' || log.action === '編輯'">
|
||||
<h6>
|
||||
<strong style="color: blue">{{ log.item }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchant }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amount }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.date || '').toLocaleDateString() }}</strong>
|
||||
</h6>
|
||||
<strong style="color: blue">{{ log.item }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchant }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amount }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.date || '').toLocaleDateString() }}</strong>
|
||||
</template>
|
||||
<template v-if="log.action === '編輯'">
|
||||
<br />
|
||||
<h6>
|
||||
原紀錄:<strong style="color: blue">{{ log.itemBefore }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchantBefore }}</strong> |
|
||||
@ -178,36 +108,90 @@ fetchLogs()
|
||||
</h6>
|
||||
</template>
|
||||
<template v-if="log.action === '結算'">
|
||||
<h6>
|
||||
<span>
|
||||
結算金額
|
||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||
</span>
|
||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.RecordIds)"></i>
|
||||
</h6>
|
||||
<span>
|
||||
結算金額
|
||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||
</span>
|
||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.Records)"></i>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button
|
||||
v-if="logCount[nowTab] < filteredLogs.length"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="logCount[nowTab] += 5"
|
||||
>
|
||||
更多紀錄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<img
|
||||
v-else
|
||||
class="img-fluid"
|
||||
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<!-- mobile -->
|
||||
<div id="mobile" class="card text-center" style="width: 100vw">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item" v-for="tab in tabs" :key="tab.title" @click="nowTab = tab.title">
|
||||
<span class="nav-link" :class="{ active: nowTab === tab.title }">{{ tab.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="card mb-3" v-for="(log, index) in filteredLogs.slice(0, logCount[nowTab])" :key="index">
|
||||
<div class="card-header">
|
||||
{{
|
||||
new Date(log.createdAt).toLocaleDateString() +
|
||||
' ' +
|
||||
getDay(log.createdAt) +
|
||||
new Date(log.createdAt).toLocaleTimeString()
|
||||
}}
|
||||
</div>
|
||||
<div class="card-body d-flex">
|
||||
<img
|
||||
v-if="log.User.displayName === '建喵'"
|
||||
src="../assets/jianmiau.jpeg"
|
||||
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">
|
||||
<strong class="px-1 me-1" style="background-color: yellow">{{ log.User.displayName }}</strong>
|
||||
<strong style="color: salmon">{{ log.action }}紀錄</strong>
|
||||
<i class="fas fa-chevron-right m-2"></i>
|
||||
<template v-if="log.action === '新增' || log.action === '編輯'">
|
||||
<h6>
|
||||
<strong style="color: blue">{{ log.item }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchant }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amount }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.date || '').toLocaleDateString() }}</strong>
|
||||
</h6>
|
||||
</template>
|
||||
<template v-if="log.action === '編輯'">
|
||||
<h6>
|
||||
原紀錄:<strong style="color: blue">{{ log.itemBefore }}</strong> |
|
||||
<strong style="color: brown">{{ log.merchantBefore }}</strong> |
|
||||
<strong style="color: orange">$ {{ log.amountBefore }}</strong> |
|
||||
<strong style="color: green">{{ new Date(log.dateBefore || '').toLocaleDateString() }}</strong>
|
||||
</h6>
|
||||
</template>
|
||||
<template v-if="log.action === '結算'">
|
||||
<h6>
|
||||
<span>
|
||||
結算金額
|
||||
<strong style="color: orange">$ {{ log.closeAmount }} </strong>
|
||||
</span>
|
||||
<i class="far fa-list-alt fa-lg ms-2" id="records" @click="listIconClick(log.Records)"></i>
|
||||
</h6>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button
|
||||
v-if="logCount[nowTab] < filteredLogs.length"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="logCount[nowTab] += 5"
|
||||
>
|
||||
更多紀錄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Spinner v-else />
|
||||
</template>
|
||||
@ -217,6 +201,9 @@ fetchLogs()
|
||||
cursor: pointer;
|
||||
color: salmon;
|
||||
}
|
||||
li:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@media screen and (max-width: 499px) {
|
||||
#pc {
|
||||
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">
|
||||
import { ref } from 'vue'
|
||||
import recordAPI from '../apis/record'
|
||||
import { Record, RecordInput } from '../models/Record'
|
||||
import Swal from 'sweetalert2'
|
||||
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 { ref, provide } from 'vue'
|
||||
import recordAPI from '@/apis/record'
|
||||
import { Record } from '@/models'
|
||||
import { getDay } from '../utils/dateFormat'
|
||||
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()
|
||||
|
||||
// data
|
||||
const isLoading = ref<boolean>(true)
|
||||
const records = ref<Record[]>([])
|
||||
const isCloseStatus = ref<boolean>(false)
|
||||
const closeRecords = ref<number[]>([])
|
||||
const closeRecordsAmount = ref<number>(0)
|
||||
const propDataForRecordButtons = ref({
|
||||
isCloseStatus: false,
|
||||
closeRecords: [] as number[],
|
||||
closeRecordsAmount: 0
|
||||
})
|
||||
|
||||
// methods
|
||||
const fetchRecords = async function () {
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const editBtnClick = async function (id: number) {
|
||||
try {
|
||||
const { data } = await recordAPI.getOne(id)
|
||||
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="${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 }) {
|
||||
try {
|
||||
await recordAPI.edit(formValues.id, formValues.input)
|
||||
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}編輯了一筆紀錄 →\n${formValues.input.merchant}-${
|
||||
formValues.input.item
|
||||
} $${formValues.input.amount}`
|
||||
}
|
||||
}
|
||||
await recordAPI.pushLineMsg(input)
|
||||
} 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 = []
|
||||
let total = 0
|
||||
for (let record of records.value) {
|
||||
recordIds.push(record.id)
|
||||
total += record.amount
|
||||
}
|
||||
closeRecords.value = recordIds
|
||||
closeRecordsAmount.value = total
|
||||
}
|
||||
|
||||
const checkboxClick = (id: number, amount: number) => {
|
||||
if (isCloseStatus.value) {
|
||||
const index = closeRecords.value?.findIndex((recordId) => recordId === id)
|
||||
console.log('checkboxClick')
|
||||
if (propDataForRecordButtons.value.isCloseStatus) {
|
||||
const index = propDataForRecordButtons.value.closeRecords.findIndex((recordId) => recordId === id)
|
||||
if (index !== undefined) {
|
||||
if (index !== -1) {
|
||||
closeRecords.value?.splice(index, 1)
|
||||
closeRecordsAmount.value -= amount
|
||||
propDataForRecordButtons.value.closeRecords.splice(index, 1)
|
||||
propDataForRecordButtons.value.closeRecordsAmount -= amount
|
||||
} else {
|
||||
closeRecords.value?.push(id)
|
||||
closeRecordsAmount.value += amount
|
||||
propDataForRecordButtons.value.closeRecords.push(id)
|
||||
propDataForRecordButtons.value.closeRecordsAmount += 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 handleCloseFunction = (func: string) => {
|
||||
if (func === 'cancelBtnClick') {
|
||||
propDataForRecordButtons.value.isCloseStatus = false
|
||||
propDataForRecordButtons.value.closeRecords = []
|
||||
propDataForRecordButtons.value.closeRecordsAmount = 0
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (func === 'closeBtnClick') {
|
||||
propDataForRecordButtons.value.isCloseStatus = true
|
||||
const recordIds = []
|
||||
let total = 0
|
||||
for (let record of records.value) {
|
||||
recordIds.push(record.id)
|
||||
total += record.amount
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
Toast.fire({
|
||||
icon: 'error',
|
||||
title: '結算資料失敗!'
|
||||
})
|
||||
propDataForRecordButtons.value.closeRecords = recordIds
|
||||
propDataForRecordButtons.value.closeRecordsAmount = total
|
||||
}
|
||||
if (func === 'closeRecord') {
|
||||
propDataForRecordButtons.value.isCloseStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
// created
|
||||
fetchRecords()
|
||||
|
||||
// provide
|
||||
provide('refetchRecords', fetchRecords)
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="!isLoading">
|
||||
<div class="d-flex mb-3" style="width: 100vw">
|
||||
<template v-if="!isCloseStatus">
|
||||
<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">
|
||||
<RecordButtons :propData="propDataForRecordButtons" @closeFunction="handleCloseFunction" />
|
||||
<table class="table table-info table-hover" v-if="records.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<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
|
||||
v-for="(record, index) in records"
|
||||
:key="index"
|
||||
:class="{ 'table-warning': closeRecords?.includes(record.id) }"
|
||||
:class="{ 'table-success': propDataForRecordButtons.closeRecords?.includes(record.id) }"
|
||||
@click="checkboxClick(record.id, record.amount)"
|
||||
>
|
||||
<td>
|
||||
<input
|
||||
v-if="isCloseStatus"
|
||||
v-if="propDataForRecordButtons.isCloseStatus"
|
||||
class="form-check-input"
|
||||
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>
|
||||
{{ record.item }}
|
||||
@ -273,10 +120,10 @@ fetchRecords()
|
||||
{{ record.amount }}
|
||||
</td>
|
||||
<td>
|
||||
{{ new Date(record.date).toLocaleDateString() + ` (${showWeekDay(record.date)})` }}
|
||||
{{ new Date(record.date).toLocaleDateString() + ' ' + getDay(record.date) }}
|
||||
</td>
|
||||
<td id="column-item">
|
||||
{{ record.recorder }}
|
||||
{{ record.User.displayName }}
|
||||
</td>
|
||||
<td id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||
@ -294,11 +141,6 @@ fetchRecords()
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
th,
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
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,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["webpack-env"],
|
||||
"types": ["webpack-env", "vite/client"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
|
||||
"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 = {
|
||||
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