mirror of
https://github.com/KarolChang/jm-expense-vue-ts.git
synced 2024-12-26 11:48:35 +00:00
add firebase
This commit is contained in:
parent
26e7a10147
commit
d89915f826
@ -1,2 +0,0 @@
|
|||||||
index.html,1639197671433,cb28bcedfa5298c2ddee09c62c272657328775ae951e372209d166f505c798ad
|
|
||||||
favicon.ico,1639122716339,682d6968805db4fd6e11e95e9dc02ab78ae1a5e4cbb7d73cd9c2a192e839ceae
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"projects": {
|
"projects": {
|
||||||
"default": "jm-expense"
|
"default": "jm-expense-2022"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "dist",
|
"public": "dist",
|
||||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
"ignore": [
|
||||||
"rewrites": [
|
"firebase.json",
|
||||||
{
|
"**/.*",
|
||||||
"source": "**",
|
"**/node_modules/**"
|
||||||
"destination": "/index.html"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2169
package-lock.json
generated
2169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,13 @@
|
|||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"firebase": "^9.6.3",
|
||||||
"pinia": "^2.0.6",
|
"pinia": "^2.0.6",
|
||||||
"sweetalert2": "^11.3.0",
|
"sweetalert2": "^11.3.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.2.27",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-class-component": "^8.0.0-0",
|
||||||
"vue-router": "^4.0.0-0"
|
"vue-router": "^4.0.0-0",
|
||||||
|
"vuefire": "^2.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||||
|
32
src/App.vue
32
src/App.vue
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Navbar from './components/Navbar.vue'
|
import Navbar from '@/components/Navbar.vue'
|
||||||
// import { useStore } from './store/index'
|
import { onLoad } from './cocos/config'
|
||||||
// const store = useStore()
|
|
||||||
// const getRecords = store.fetchRecords
|
// created
|
||||||
// getRecords()
|
onLoad()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -79,4 +79,26 @@ body {
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-active {
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-active {
|
||||||
|
transition: transform 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
.slide-right-enter-to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
.slide-right-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,27 +1,34 @@
|
|||||||
import { apiHelper } from './helper'
|
import { apiHelper } from './helper'
|
||||||
import { RecordInput } from '../models/Record'
|
import { ExpenseInput } from '../models/Expense'
|
||||||
import { MessageInput } from '../models/LineBot'
|
import { CategoryInput } from '../models/Category'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAll() {
|
expense: {
|
||||||
return apiHelper.get('/record/all')
|
getAll() {
|
||||||
|
return apiHelper.get('/expense/all')
|
||||||
|
},
|
||||||
|
getOne(id: number) {
|
||||||
|
return apiHelper.get(`/expense/${id}`)
|
||||||
|
},
|
||||||
|
create(data: ExpenseInput) {
|
||||||
|
return apiHelper.post('/expense/create', data)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getOne(id: number) {
|
category: {
|
||||||
return apiHelper.get(`/record/${id}`)
|
getAll() {
|
||||||
},
|
return apiHelper.get('/category/all')
|
||||||
create(data: RecordInput) {
|
},
|
||||||
return apiHelper.post('/record/create', data)
|
getOne(id: number) {
|
||||||
},
|
return apiHelper.get(`/category/${id}`)
|
||||||
edit(id: number, data: RecordInput) {
|
},
|
||||||
return apiHelper.put(`/record/edit/${id}`, data)
|
create(data: CategoryInput) {
|
||||||
},
|
return apiHelper.post('/category/create', data)
|
||||||
close(data: { records: string; totalAmount: number; recorder: string }) {
|
},
|
||||||
return apiHelper.put('/close', data)
|
edit(id: number, data: CategoryInput) {
|
||||||
},
|
return apiHelper.put(`/category/edit/${id}`, data)
|
||||||
getLogs() {
|
},
|
||||||
return apiHelper.get('/log/all')
|
delete(id: number) {
|
||||||
},
|
return apiHelper.delete(`/category/delete/${id}`)
|
||||||
pushLineMsg(data: { to: string[]; messages: MessageInput[] }) {
|
}
|
||||||
return apiHelper.post('/lineBot/push', data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,3 +4,18 @@ const baseURL = 'http://jm-expense-mysql.herokuapp.com'
|
|||||||
export const apiHelper = axios.create({
|
export const apiHelper = axios.create({
|
||||||
baseURL
|
baseURL
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// const baseURL_Line = 'https://linebot20220114.herokuapp.com/'
|
||||||
|
// export const apiHelperLine = axios.create({
|
||||||
|
// baseURL: baseURL_Line
|
||||||
|
// })
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
type: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineInput {
|
||||||
|
to: string | string[]
|
||||||
|
messages: Message | Message[]
|
||||||
|
}
|
||||||
|
26
src/apis/record.ts
Normal file
26
src/apis/record.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { apiHelper, LineInput } from './helper'
|
||||||
|
import { RecordInput } from '../models/Record'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAll() {
|
||||||
|
return apiHelper.get('/record/all')
|
||||||
|
},
|
||||||
|
getOne(id: number) {
|
||||||
|
return apiHelper.get(`/record/${id}`)
|
||||||
|
},
|
||||||
|
create(data: RecordInput) {
|
||||||
|
return apiHelper.post('/record/create', data)
|
||||||
|
},
|
||||||
|
edit(id: number, data: RecordInput) {
|
||||||
|
return apiHelper.put(`/record/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
close(data: { records: string; totalAmount: number; recorder: string }) {
|
||||||
|
return apiHelper.put('/close', data)
|
||||||
|
},
|
||||||
|
getLogs() {
|
||||||
|
return apiHelper.get('/log/all')
|
||||||
|
},
|
||||||
|
pushLineMsg(data: LineInput) {
|
||||||
|
return apiHelper.post('/lineBot/push', data)
|
||||||
|
}
|
||||||
|
}
|
26
src/cocos/config.ts
Normal file
26
src/cocos/config.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/** 外部帶進來的param */
|
||||||
|
const URLscheme: any = []
|
||||||
|
export const onLoad = async function () {
|
||||||
|
const url = window.location.search
|
||||||
|
if (url.indexOf('?') !== -1) {
|
||||||
|
const str: string = url.substr(1)
|
||||||
|
const strs: any[] = str.split('&')
|
||||||
|
for (let i = 0; i < strs.length; i++) {
|
||||||
|
URLscheme[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CallParent('_alert', '我愛豬涵')
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallParent = async function (method: string, ...param: any[]) {
|
||||||
|
const target: string = URLscheme['host']
|
||||||
|
if (target) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
method: method,
|
||||||
|
value: param
|
||||||
|
},
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Sidebar from './Sidebar.vue'
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
65
src/components/RightPanel.vue
Normal file
65
src/components/RightPanel.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<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>
|
99
src/components/modalButton/CreateCategoryModalButton.vue
Normal file
99
src/components/modalButton/CreateCategoryModalButton.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||||
|
import expenseAPI from '../../apis/expense'
|
||||||
|
import { CategoryInput } from '../../models/Category'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
refetch: () => {}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 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-icon" class="col-form-label">Icon</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-photoUrl" class="col-form-label">PhotoUrl</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" id="swal-photoUrl" class="form-control" >
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
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
|
||||||
|
if (name) {
|
||||||
|
if (!icon && !photoUrl) {
|
||||||
|
Swal.showValidationMessage('Icon和PhotoUrl欄位必須擇一填寫!')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Swal.showValidationMessage('名稱欄位必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
name,
|
||||||
|
icon: icon === '' ? null : icon,
|
||||||
|
photoUrl: photoUrl === '' ? null : photoUrl
|
||||||
|
} as CategoryInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
createExpense(formValues!)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createExpense = async function (formValues: { input: CategoryInput }) {
|
||||||
|
try {
|
||||||
|
await expenseAPI.category.create(formValues.input)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功建立類別[${formValues.input.name}]`
|
||||||
|
})
|
||||||
|
props.refetch!()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '新增類別失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<span class="badge bg-success" @click="btnClick">+</span>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
span:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: darkorange;
|
||||||
|
}
|
||||||
|
</style>
|
137
src/components/modalButton/CreateRecordModalButton.vue
Normal file
137
src/components/modalButton/CreateRecordModalButton.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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 { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
view: 'Home' | 'Record'
|
||||||
|
refetch?: () => {}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 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-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-merchant" class="col-form-label">商家</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input 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 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 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) {
|
||||||
|
Swal.showValidationMessage('所有資料都是必填!若紀錄者為空,請登入~')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
item,
|
||||||
|
merchant,
|
||||||
|
amount,
|
||||||
|
date,
|
||||||
|
recorder
|
||||||
|
} as RecordInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
createRecord(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRecord = async function (formValues: { input: RecordInput }) {
|
||||||
|
try {
|
||||||
|
await recordAPI.create(formValues.input)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功建立資料!'
|
||||||
|
})
|
||||||
|
if (props.view === 'Record') {
|
||||||
|
props.refetch!()
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'Record' })
|
||||||
|
}
|
||||||
|
lineBotPush(formValues.input)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '新增資料失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</template>
|
47
src/components/modalButton/DeleteCategoryModalButton.vue
Normal file
47
src/components/modalButton/DeleteCategoryModalButton.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||||
|
import expenseAPI from '../../apis/expense'
|
||||||
|
import { Category } from '../../models/Category'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
refetch: () => {}
|
||||||
|
category: Category
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
title: `確定刪除[${props.category.name}]類別嘛?`,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
deleteCategory(props.category.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCategory = async function (id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await expenseAPI.category.delete(id)
|
||||||
|
console.log('data', data)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功刪除類別[${data.deletedCategory.name}]`
|
||||||
|
})
|
||||||
|
props.refetch()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '刪除類別失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-trash" @click="btnClick"></i>
|
||||||
|
</template>
|
110
src/components/modalButton/EditCategoryModalButton.vue
Normal file
110
src/components/modalButton/EditCategoryModalButton.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { Toast, ConfirmBox } from '../../utils/swal'
|
||||||
|
import expenseAPI from '../../apis/expense'
|
||||||
|
import { Category, CategoryInput } from '../../models/Category'
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = defineProps<{
|
||||||
|
refetch: () => {}
|
||||||
|
category: Category
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const btnClick = async () => {
|
||||||
|
try {
|
||||||
|
const category = props.category
|
||||||
|
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="${category.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-icon" class="col-form-label">Icon</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${
|
||||||
|
category.icon === null ? '' : category.icon
|
||||||
|
}" 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>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="col-auto me-3">
|
||||||
|
<label for="swal-photoUrl" class="col-form-label">PhotoUrl</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input value="${
|
||||||
|
category.photoUrl === null ? '' : category.photoUrl
|
||||||
|
}" type="text" id="swal-photoUrl" class="form-control" >
|
||||||
|
</div>
|
||||||
|
<a class="ms-4 mt-1" href="${
|
||||||
|
category.photoUrl === null ? 'https://www.google.com/' : category.photoUrl
|
||||||
|
}" target="_blank">
|
||||||
|
<span class="badge bg-success">圖片連結</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
preConfirm: () => {
|
||||||
|
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
|
||||||
|
if (name) {
|
||||||
|
if (!icon && !photoUrl) {
|
||||||
|
Swal.showValidationMessage('Icon和PhotoUrl欄位必須擇一填寫!')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Swal.showValidationMessage('名稱欄位必填!')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: {
|
||||||
|
name,
|
||||||
|
icon: icon === '' ? null : icon,
|
||||||
|
photoUrl: photoUrl === '' ? null : photoUrl
|
||||||
|
} as CategoryInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (formValues) {
|
||||||
|
editCategoryName(formValues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editCategoryName = async function (formValues: { input: CategoryInput }) {
|
||||||
|
try {
|
||||||
|
await expenseAPI.category.edit(props.category.id, formValues.input)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: `成功建立類別[${formValues.input.name}]`
|
||||||
|
})
|
||||||
|
props.refetch()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '編輯類別失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<i class="fas fa-edit ms-2" @click="btnClick"></i>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
i:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import { firestorePlugin } from 'vuefire'
|
||||||
|
|
||||||
import 'bootstrap'
|
import 'bootstrap'
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
15
src/models/Category.ts
Normal file
15
src/models/Category.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export class Category {
|
||||||
|
id!: number
|
||||||
|
name!: string
|
||||||
|
icon?: string
|
||||||
|
photoUrl?: string
|
||||||
|
deletedAt?: Date | null
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CategoryInput {
|
||||||
|
name!: string
|
||||||
|
icon?: string
|
||||||
|
photoUrl?: string
|
||||||
|
}
|
21
src/models/Expense.ts
Normal file
21
src/models/Expense.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Category } from './Category'
|
||||||
|
|
||||||
|
export class Expense {
|
||||||
|
id!: number
|
||||||
|
date!: Date
|
||||||
|
item?: string
|
||||||
|
amount!: number
|
||||||
|
note?: string
|
||||||
|
deletedAt?: Date | null
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
Category!: Category
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpenseInput {
|
||||||
|
date!: Date
|
||||||
|
item!: string
|
||||||
|
amount!: string
|
||||||
|
note?: string
|
||||||
|
CategoryId!: number
|
||||||
|
}
|
@ -42,6 +42,15 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
show: true
|
show: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/expense`,
|
||||||
|
name: 'Expense',
|
||||||
|
component: () => import('../views/Expense.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '豬涵記帳',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
|
@ -4,7 +4,9 @@ export const useStore = defineStore('index', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
currentUser: localStorage.getItem('jm-user')
|
currentUser: localStorage.getItem('jm-user')
|
||||||
}),
|
}),
|
||||||
// getter: {}
|
getters: {
|
||||||
|
icon: (state) => (state.currentUser === '豬涵' ? '🐷' : '🐣')
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
switchUser(user: string) {
|
switchUser(user: string) {
|
||||||
localStorage.setItem('jm-user', user)
|
localStorage.setItem('jm-user', user)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import recordAPI from '../apis/expense'
|
import recordAPI from '../apis/record'
|
||||||
import { Record } from '../models/Record'
|
import { Record } from '../models/Record'
|
||||||
import { showWeekDay } from '../utils/helper'
|
import { showWeekDay } from '../utils/helper'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '../components/Spinner.vue'
|
||||||
@ -10,8 +10,8 @@ type SearchMode = '月份' | '日期'
|
|||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const records = ref<Record[]>([])
|
const records = ref<Record[]>([])
|
||||||
const searchMode = ref<SearchMode>('月份')
|
const searchMode = ref<SearchMode>('月份')
|
||||||
const year = ref(new Date().getFullYear())
|
const year = ref()
|
||||||
const month = ref(new Date().getMonth() + 1)
|
const month = ref()
|
||||||
const startDate = ref<Date | string>('')
|
const startDate = ref<Date | string>('')
|
||||||
const finishDate = ref<Date | string>('')
|
const finishDate = ref<Date | string>('')
|
||||||
|
|
||||||
@ -19,7 +19,9 @@ const finishDate = ref<Date | string>('')
|
|||||||
const fetchRecords = async function () {
|
const fetchRecords = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getAll()
|
const { data } = await recordAPI.getAll()
|
||||||
records.value = data.filter((item: Record) => item.isClosed === true)
|
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
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
|
176
src/views/Expense.vue
Normal file
176
src/views/Expense.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<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 Spinner from '@/components/Spinner.vue'
|
||||||
|
import RightPanel from '@/components/RightPanel.vue'
|
||||||
|
|
||||||
|
// data
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const expenses = ref<Expense[]>([])
|
||||||
|
const rightPanelOpen = ref(false)
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchExpenses = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await expenseAPI.expense.getAll()
|
||||||
|
expenses.value = data.filter((item: Expense) => item.deletedAt === null)
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBtnClick = 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)
|
||||||
|
}
|
||||||
|
} 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()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
<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>
|
||||||
|
</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" />
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import recordAPI from '../apis/expense'
|
import recordAPI from '../apis/record'
|
||||||
// import lineBotAPI from '../apis/lineBot'
|
|
||||||
import { Record } from '../models/Record'
|
import { Record } from '../models/Record'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '../components/Spinner.vue'
|
||||||
|
import CreateRecordModalButton from '../components/modalButton/CreateRecordModalButton.vue'
|
||||||
|
|
||||||
class MonthData {
|
class MonthData {
|
||||||
total!: number
|
total!: number
|
||||||
@ -11,43 +11,17 @@ class MonthData {
|
|||||||
rate!: number
|
rate!: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// data
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const records = ref<Record[]>([])
|
const records = ref<Record[]>([])
|
||||||
const thisMonthData = ref<MonthData>(new MonthData())
|
const thisMonthData = ref<MonthData>(new MonthData())
|
||||||
const lastMonthData = ref<MonthData>(new MonthData())
|
const lastMonthData = ref<MonthData>(new MonthData())
|
||||||
|
|
||||||
// cocos settings
|
|
||||||
/** 外部帶進來的param */
|
|
||||||
let URLscheme: any = []
|
|
||||||
const onLoad = async function () {
|
|
||||||
let url = window.location.search
|
|
||||||
if (url.indexOf('?') !== -1) {
|
|
||||||
let str: string = url.substr(1)
|
|
||||||
let strs: any[] = str.split('&')
|
|
||||||
for (let i = 0; i < strs.length; i++) {
|
|
||||||
URLscheme[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CallParent('_alert', '我愛豬涵')
|
|
||||||
}
|
|
||||||
const CallParent = async function (method: string, ...param: any[]) {
|
|
||||||
let target: string = URLscheme['host']
|
|
||||||
if (target) {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
method: method,
|
|
||||||
value: param
|
|
||||||
},
|
|
||||||
target
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const fetchRecords = async function () {
|
const fetchRecords = async function () {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getAll()
|
const { data } = await recordAPI.getAll()
|
||||||
records.value = data
|
records.value = data.filter((item: Record) => item.deletedAt === null)
|
||||||
// monthData
|
// monthData
|
||||||
const nowYear = new Date().getFullYear()
|
const nowYear = new Date().getFullYear()
|
||||||
const nowMonth = new Date().getMonth() + 1
|
const nowMonth = new Date().getMonth() + 1
|
||||||
@ -57,8 +31,6 @@ const fetchRecords = async function () {
|
|||||||
nowMonth - 1 === 0 ? 12 : nowMonth - 1
|
nowMonth - 1 === 0 ? 12 : nowMonth - 1
|
||||||
)
|
)
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
// cocos settings
|
|
||||||
CallParent('_closeBG')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
@ -92,18 +64,31 @@ const calculateMonthData = (year: number, month: number) => {
|
|||||||
|
|
||||||
// created
|
// created
|
||||||
fetchRecords()
|
fetchRecords()
|
||||||
onLoad()
|
|
||||||
|
// 笨蛋才按我
|
||||||
|
const handle = async () => {
|
||||||
|
const input = {
|
||||||
|
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
||||||
|
messages: { type: 'text', text: '卡比覺得促咪!' }
|
||||||
|
}
|
||||||
|
console.log('input', input)
|
||||||
|
await recordAPI.pushLineMsg(input)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="!isLoading">
|
||||||
|
<div class="d-flex mb-3">
|
||||||
|
<CreateRecordModalButton view="Home" class="me-3" />
|
||||||
|
<button type="button" class="btn btn-success me-3" @click="handle">笨蛋才按我</button>
|
||||||
|
</div>
|
||||||
<div class="list-group list-group-checkable">
|
<div class="list-group list-group-checkable">
|
||||||
<label class="list-group-item py-3 mb-3">
|
<label class="list-group-item py-3 mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8 text-start">
|
<div class="col-7 text-start">
|
||||||
<h4>
|
<h5>
|
||||||
本月總額 $<strong>{{ thisMonthData.total }}</strong>
|
本月總額 $<strong>{{ thisMonthData.total }}</strong>
|
||||||
</h4>
|
</h5>
|
||||||
<div class="progress mt-2" style="height: 20px">
|
<div class="progress mt-2" style="height: 20px">
|
||||||
<div
|
<div
|
||||||
class="progress-bar"
|
class="progress-bar"
|
||||||
@ -117,8 +102,9 @@ onLoad()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-start">
|
<div class="col-5 text-start">
|
||||||
<div>未結算$ {{ thisMonthData.total - thisMonthData.closedAmount }}</div>
|
<div>未結算$ {{ thisMonthData.total - thisMonthData.closedAmount }}</div>
|
||||||
|
|
||||||
<div>已結算$ {{ thisMonthData.closedAmount }}</div>
|
<div>已結算$ {{ thisMonthData.closedAmount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,10 +112,10 @@ onLoad()
|
|||||||
|
|
||||||
<label class="list-group-item py-3 mb-3">
|
<label class="list-group-item py-3 mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8 text-start">
|
<div class="col-7 text-start">
|
||||||
<h4>
|
<h5>
|
||||||
上月總額 $<strong>{{ lastMonthData.total }}</strong>
|
上月總額 $<strong>{{ lastMonthData.total }}</strong>
|
||||||
</h4>
|
</h5>
|
||||||
<div class="progress mt-2" style="height: 20px">
|
<div class="progress mt-2" style="height: 20px">
|
||||||
<div
|
<div
|
||||||
class="progress-bar"
|
class="progress-bar"
|
||||||
@ -143,7 +129,7 @@ onLoad()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 text-start">
|
<div class="col-5 text-start">
|
||||||
<div>未結算$ {{ lastMonthData.total - lastMonthData.closedAmount }}</div>
|
<div>未結算$ {{ lastMonthData.total - lastMonthData.closedAmount }}</div>
|
||||||
<div>已結算$ {{ lastMonthData.closedAmount }}</div>
|
<div>已結算$ {{ lastMonthData.closedAmount }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { showWeekDay } from '../utils/helper'
|
import { showWeekDay } from '../utils/helper'
|
||||||
import recordAPI from '../apis/expense'
|
import recordAPI from '../apis/record'
|
||||||
import { Log } from '../models/Log'
|
import { Log } from '../models/Log'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '../components/Spinner.vue'
|
||||||
@ -12,9 +12,10 @@ const tabs = [
|
|||||||
{ title: '編輯', btnColor: 'success' },
|
{ title: '編輯', btnColor: 'success' },
|
||||||
{ title: '結算', btnColor: 'danger' }
|
{ title: '結算', btnColor: 'danger' }
|
||||||
]
|
]
|
||||||
const isLoading = ref(false)
|
const isLoading = ref<boolean>(false)
|
||||||
const logs = ref<Log[]>([])
|
const logs = ref<Log[]>([])
|
||||||
const nowTab = ref('總覽')
|
const nowTab = ref('總覽')
|
||||||
|
const logCount = ref<any>({ 總覽: 5, 新增: 5, 編輯: 5, 結算: 5 })
|
||||||
|
|
||||||
const filteredLogs = computed(() => {
|
const filteredLogs = computed(() => {
|
||||||
if (nowTab.value === '總覽') return logs.value
|
if (nowTab.value === '總覽') return logs.value
|
||||||
@ -55,6 +56,7 @@ const listIconClick = async (recordIds: string | undefined) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -81,7 +83,8 @@ fetchLogs()
|
|||||||
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
<div class="row h-20 mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
||||||
<div class="col-2 mt-4">
|
<div class="col-2 mt-4">
|
||||||
<div class="fw-bold">
|
<div class="fw-bold">
|
||||||
{{ new Date(log.createdAt).toLocaleDateString() + ` (${showWeekDay(log.createdAt)})` }}
|
{{ new Date(log.createdAt).toLocaleDateString() }}
|
||||||
|
<!-- {{ new Date(log.createdAt).toLocaleDateString() + ` (${showWeekDay(log.createdAt)})` }} -->
|
||||||
</div>
|
</div>
|
||||||
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
<div class="fw-bold">{{ new Date(log.createdAt).toLocaleTimeString() }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +141,7 @@ fetchLogs()
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card mb-3" v-for="(log, index) in filteredLogs" :key="index">
|
<div class="card mb-3" v-for="(log, index) in filteredLogs.slice(0, logCount[nowTab])" :key="index">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
{{
|
{{
|
||||||
new Date(log.createdAt).toLocaleDateString() +
|
new Date(log.createdAt).toLocaleDateString() +
|
||||||
@ -186,6 +189,16 @@ fetchLogs()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import recordAPI from '../apis/expense'
|
import recordAPI from '../apis/record'
|
||||||
import { Record, RecordInput } from '../models/Record'
|
import { Record, RecordInput } from '../models/Record'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
import { Toast, ConfirmBox } from '../utils/swal'
|
import { Toast, ConfirmBox } from '../utils/swal'
|
||||||
import { showWeekDay } from '../utils/helper'
|
import { showWeekDay } from '../utils/helper'
|
||||||
import Spinner from '../components/Spinner.vue'
|
import Spinner from '../components/Spinner.vue'
|
||||||
|
import CreateRecordModalButton from '../components/modalButton/CreateRecordModalButton.vue'
|
||||||
import { useStore } from '../store/index'
|
import { useStore } from '../store/index'
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
@ -27,110 +28,6 @@ const fetchRecords = async function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBtnClick = 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-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-merchant" class="col-form-label">商家</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input 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 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 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-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) {
|
|
||||||
Swal.showValidationMessage('所有資料都是必填!若紀錄者為空,請登入~')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
input: {
|
|
||||||
item,
|
|
||||||
merchant,
|
|
||||||
amount,
|
|
||||||
date,
|
|
||||||
recorder
|
|
||||||
} as RecordInput
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (formValues) {
|
|
||||||
createRecord(formValues)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createRecord = async function (formValues: { input: RecordInput }) {
|
|
||||||
try {
|
|
||||||
await recordAPI.create(formValues.input)
|
|
||||||
fetchRecords()
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'success',
|
|
||||||
title: '成功建立資料!'
|
|
||||||
})
|
|
||||||
// line msg
|
|
||||||
// const input = {
|
|
||||||
// to: [process.env.VUE_APP_JIANMIAU_USERID, process.env.VUE_APP_KAROL_USERID],
|
|
||||||
// messages: [
|
|
||||||
// {
|
|
||||||
// type: 'text',
|
|
||||||
// text: `${store.currentUser}新增了一筆關於${formValues.input.item}的記帳`
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// await recordAPI.pushLineMsg(input)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error', error)
|
|
||||||
Toast.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: '新增資料失敗!'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editBtnClick = async function (id: number) {
|
const editBtnClick = async function (id: number) {
|
||||||
try {
|
try {
|
||||||
const { data } = await recordAPI.getOne(id)
|
const { data } = await recordAPI.getOne(id)
|
||||||
@ -219,6 +116,17 @@ const editRecord = async function (formValues: { id: number; input: RecordInput
|
|||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: '成功編輯資料!'
|
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) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
@ -290,6 +198,15 @@ const closeRecord = async (amount: number) => {
|
|||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: '成功結算資料!'
|
title: '成功結算資料!'
|
||||||
})
|
})
|
||||||
|
// lineBot push
|
||||||
|
const input = {
|
||||||
|
to: [process.env.VUE_APP_KAROL_USERID, process.env.VUE_APP_JIANMIAU_USERID],
|
||||||
|
messages: {
|
||||||
|
type: 'text',
|
||||||
|
text: `${store.currentUser + store.icon}結算紀錄 → 總金額 $${amount}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await recordAPI.pushLineMsg(input)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
@ -305,16 +222,17 @@ fetchRecords()
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<div class="d-flex justify-content-between mb-3" style="width: 100vw">
|
<div class="d-flex mb-3" style="width: 100vw">
|
||||||
<button type="button" class="btn btn-primary text-end" @click="createBtnClick">新增資料</button>
|
<template v-if="!isCloseStatus">
|
||||||
<div>
|
<CreateRecordModalButton view="Record" :refetch="fetchRecords" class="me-3" />
|
||||||
<template v-if="isCloseStatus">
|
<button type="button" class="btn btn-danger" @click="closeBtnClick">開始結算</button>
|
||||||
<span class="badge bg-info text-dark me-3 align-middle p-2">結算金額 ${{ closeRecordsAmount }}</span>
|
</template>
|
||||||
<button type="button" class="btn btn-secondary" @click="cancelBtnClick">取消結算</button>
|
|
||||||
<button type="button" class="btn btn-success ms-3" @click="toCloseBtnClick">確定結算</button>
|
<template v-else>
|
||||||
</template>
|
<div class="btn btn-info fw-bold">結算金額 ${{ closeRecordsAmount }}</div>
|
||||||
<button v-else type="button" class="btn btn-danger" @click="closeBtnClick">開始結算</button>
|
<button type="button" class="btn btn-secondary ms-3" @click="cancelBtnClick">取消結算</button>
|
||||||
</div>
|
<button type="button" class="btn btn-success ms-3" @click="toCloseBtnClick">確定結算</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-striped table-info table-hover" v-if="records.length">
|
<table class="table table-striped table-info table-hover" v-if="records.length">
|
||||||
<thead>
|
<thead>
|
||||||
@ -381,9 +299,8 @@ td {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
|
||||||
span {
|
span {
|
||||||
font-size: 0.5em;
|
margin-top: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
i[class~='fa-edit']:hover {
|
i[class~='fa-edit']:hover {
|
||||||
|
Loading…
Reference in New Issue
Block a user