push to master
2
.firebase/hosting.cHVibGlj.cache
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
index.html,1639197671433,cb28bcedfa5298c2ddee09c62c272657328775ae951e372209d166f505c798ad
|
||||||
|
favicon.ico,1639122716339,682d6968805db4fd6e11e95e9dc02ab78ae1a5e4cbb7d73cd9c2a192e839ceae
|
5
.firebaserc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "jm-expense"
|
||||||
|
}
|
||||||
|
}
|
26
deploy.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# abort on errors
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# navigate into the build output directory
|
||||||
|
cd dist
|
||||||
|
|
||||||
|
# if you are deploying to a custom domain
|
||||||
|
# echo 'www.example.com' > CNAME
|
||||||
|
|
||||||
|
git init
|
||||||
|
git add -A
|
||||||
|
git commit -m 'deploy'
|
||||||
|
|
||||||
|
# if you are deploying to https://<USERNAME>.github.io
|
||||||
|
# git push -f https://github.com/<USERNAME>/<USERNAME>.GitHub.io.git master
|
||||||
|
|
||||||
|
# if you are deploying to https://<USERNAME>.Github.io/<REPO>
|
||||||
|
# git push -f https://github.com/<USERNAME>/<REPO>.git master:gh-pages
|
||||||
|
git push -f https://github.com/KarolChang/jm-expense-vue-ts.git master:gh-pages
|
||||||
|
|
||||||
|
cd -
|
12
firebase.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"hosting": {
|
||||||
|
"public": "dist",
|
||||||
|
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
192
package-lock.json
generated
@ -8,7 +8,12 @@
|
|||||||
"name": "jm-expense-vue-ts",
|
"name": "jm-expense-vue-ts",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"pinia": "^2.0.6",
|
||||||
|
"sweetalert2": "^11.3.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"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"
|
||||||
@ -1749,6 +1754,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@soda/friendly-errors-webpack-plugin": {
|
"node_modules/@soda/friendly-errors-webpack-plugin": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
||||||
@ -2970,6 +2985,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/cli-service/node_modules/dotenv": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/cli-shared-utils": {
|
"node_modules/@vue/cli-shared-utils": {
|
||||||
"version": "4.5.15",
|
"version": "4.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.15.tgz",
|
||||||
@ -3840,6 +3864,14 @@
|
|||||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-code-frame": {
|
"node_modules/babel-code-frame": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
||||||
@ -4197,6 +4229,18 @@
|
|||||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@ -6410,10 +6454,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "8.6.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@ -7639,7 +7682,6 @@
|
|||||||
"version": "1.14.6",
|
"version": "1.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
|
||||||
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==",
|
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -11476,6 +11518,56 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-01mP4+KapIcTNSYLhQESy6GW0N8vY5wX3UqOwkC87e7DPjEusNJ8bENrKqdvZaRHbB2rDMOONeAbwMa3+n1/rw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.0.0-beta.20.1",
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.4.0",
|
||||||
|
"typescript": ">=4.4.4",
|
||||||
|
"vue": "^2.6.14 || ^3.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/vue-demi": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pinkie": {
|
"node_modules/pinkie": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||||
@ -14184,6 +14276,14 @@
|
|||||||
"boolbase": "~1.0.0"
|
"boolbase": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sweetalert2": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-C0TFp0VLxgx+PmhJ0mL8qzx+iYjnCLdDbvQHKY6KAGI+xwawMvLkStPgw2LmJl6itaDhR/qLQStPFIbr1VK9Ow==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://sweetalert2.github.io/#donations"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/table": {
|
"node_modules/table": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
||||||
@ -14855,7 +14955,7 @@
|
|||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
|
||||||
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
|
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -17840,6 +17940,12 @@
|
|||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@popperjs/core": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"@soda/friendly-errors-webpack-plugin": {
|
"@soda/friendly-errors-webpack-plugin": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
||||||
@ -18699,7 +18805,8 @@
|
|||||||
"version": "4.5.15",
|
"version": "4.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.15.tgz",
|
||||||
"integrity": "sha512-fqap+4HN+w+InDxlA3hZTOGE0tzBTgXhKLoDydhywqgmhQ1D9JA6Feh94ze6tG8DsWX58/ujYUqA8jAz17FJtg==",
|
"integrity": "sha512-fqap+4HN+w+InDxlA3hZTOGE0tzBTgXhKLoDydhywqgmhQ1D9JA6Feh94ze6tG8DsWX58/ujYUqA8jAz17FJtg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@vue/cli-service": {
|
"@vue/cli-service": {
|
||||||
"version": "4.5.15",
|
"version": "4.5.15",
|
||||||
@ -18763,6 +18870,14 @@
|
|||||||
"webpack-chain": "^6.4.0",
|
"webpack-chain": "^6.4.0",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/cli-shared-utils": {
|
"@vue/cli-shared-utils": {
|
||||||
@ -18925,7 +19040,8 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
||||||
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@vue/reactivity": {
|
"@vue/reactivity": {
|
||||||
"version": "3.2.24",
|
"version": "3.2.24",
|
||||||
@ -19193,7 +19309,8 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
"acorn-walk": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
@ -19223,13 +19340,15 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
||||||
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"alphanum-sort": {
|
"alphanum-sort": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -19496,6 +19615,14 @@
|
|||||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.14.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"babel-code-frame": {
|
"babel-code-frame": {
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
||||||
@ -19788,6 +19915,12 @@
|
|||||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"version": "5.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||||
|
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@ -21573,10 +21706,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dotenv": {
|
"dotenv": {
|
||||||
"version": "8.6.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"dotenv-expand": {
|
"dotenv-expand": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
@ -22567,8 +22699,7 @@
|
|||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.6",
|
"version": "1.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
|
||||||
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==",
|
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -25566,6 +25697,23 @@
|
|||||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pinia": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-01mP4+KapIcTNSYLhQESy6GW0N8vY5wX3UqOwkC87e7DPjEusNJ8bENrKqdvZaRHbB2rDMOONeAbwMa3+n1/rw==",
|
||||||
|
"requires": {
|
||||||
|
"@vue/devtools-api": "^6.0.0-beta.20.1",
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==",
|
||||||
|
"requires": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pinkie": {
|
"pinkie": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||||
@ -27847,6 +27995,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sweetalert2": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-C0TFp0VLxgx+PmhJ0mL8qzx+iYjnCLdDbvQHKY6KAGI+xwawMvLkStPgw2LmJl6itaDhR/qLQStPFIbr1VK9Ow=="
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
||||||
@ -28369,7 +28522,7 @@
|
|||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
|
||||||
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
|
"integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.4.10",
|
"version": "3.4.10",
|
||||||
@ -28734,7 +28887,8 @@
|
|||||||
"vue-class-component": {
|
"vue-class-component": {
|
||||||
"version": "8.0.0-rc.1",
|
"version": "8.0.0-rc.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz",
|
||||||
"integrity": "sha512-w1nMzsT/UdbDAXKqhwTmSoyuJzUXKrxLE77PCFVuC6syr8acdFDAq116xgvZh9UCuV0h+rlCtxXolr3Hi3HyPQ=="
|
"integrity": "sha512-w1nMzsT/UdbDAXKqhwTmSoyuJzUXKrxLE77PCFVuC6syr8acdFDAq116xgvZh9UCuV0h+rlCtxXolr3Hi3HyPQ==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vue-eslint-parser": {
|
"vue-eslint-parser": {
|
||||||
"version": "7.11.0",
|
"version": "7.11.0",
|
||||||
|
13
package.json
@ -8,7 +8,12 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"pinia": "^2.0.6",
|
||||||
|
"sweetalert2": "^11.3.0",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"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"
|
||||||
@ -41,7 +46,13 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"parser": "@typescript-eslint/parser"
|
"parser": "@typescript-eslint/parser"
|
||||||
},
|
},
|
||||||
"rules": {}
|
"rules": {},
|
||||||
|
"globals": {
|
||||||
|
"defineProps": "readonly",
|
||||||
|
"defineEmits": "readonly",
|
||||||
|
"defineExpose": "readonly",
|
||||||
|
"withDefaults": "readonly"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
26
public/404.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<!-- <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>
|
||||||
|
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
||||||
|
<script>
|
||||||
|
sessionStorage.redirect = location.href;
|
||||||
|
</script>
|
||||||
|
<meta http-equiv="refresh" content="0;URL='/jm-expense-vue-ts'"></meta>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong
|
||||||
|
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
|
enable it to continue.</strong
|
||||||
|
>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 59 KiB |
@ -1,15 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
<script src="https://kit.fontawesome.com/ccfd93e9a7.js" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>
|
||||||
|
;(function () {
|
||||||
|
let redirect = sessionStorage.redirect
|
||||||
|
delete sessionStorage.redirect
|
||||||
|
if (redirect && redirect !== location.href) {
|
||||||
|
history.replaceState(null, null, redirect)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<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 <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please
|
||||||
|
enable it to continue.</strong
|
||||||
|
>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
74
src/App.vue
@ -1,30 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Navbar from './components/Navbar.vue'
|
||||||
|
// import { useStore } from './store/index'
|
||||||
|
// const store = useStore()
|
||||||
|
// const getRecords = store.fetchRecords
|
||||||
|
// getRecords()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="nav">
|
<div>
|
||||||
<router-link to="/">Home</router-link> |
|
<Navbar>
|
||||||
<router-link to="/about">About</router-link>
|
<template #main>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade-fast" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</template>
|
||||||
|
</Navbar>
|
||||||
</div>
|
</div>
|
||||||
<router-view/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@400;700&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url('https://png.pngtree.com/thumb_back/fh260/background/20190222/ourmid/pngtree-minimalistic-cat-claws-seamless-background-design-backgroundpinkcat-clawseamless-backgroundbanner-image_57663.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .is-primary {
|
||||||
|
color: hsl(150, 50%, 50%);
|
||||||
|
} */
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
|
font-family: 'Zen Maru Gothic', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav {
|
/* transition */
|
||||||
padding: 30px;
|
.slide-x-enter-active {
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav a {
|
.slide-x-leave-active {
|
||||||
font-weight: bold;
|
transition: transform 0.2s ease-in;
|
||||||
color: #2c3e50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav a.router-link-exact-active {
|
.slide-x-enter-from {
|
||||||
color: #42b983;
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
.slide-x-enter-to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-x-leave-from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
.slide-x-leave-to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-fast-enter-active,
|
||||||
|
.fade-fast-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-fast-enter-from,
|
||||||
|
.fade-fast-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.fade-fast-enter-active,
|
||||||
|
.fade-fast-leave-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
27
src/apis/expense.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { apiHelper } from './helper'
|
||||||
|
import { RecordInput } from '../models/Record'
|
||||||
|
import { MessageInput } from '../models/LineBot'
|
||||||
|
|
||||||
|
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: { to: string[]; messages: MessageInput[] }) {
|
||||||
|
return apiHelper.post('/lineBot/push', data)
|
||||||
|
}
|
||||||
|
}
|
6
src/apis/helper.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const baseURL = 'http://jm-expense-mysql.herokuapp.com'
|
||||||
|
export const apiHelper = axios.create({
|
||||||
|
baseURL
|
||||||
|
})
|
BIN
src/assets/capoo.gif
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
src/assets/jianmiau-login.png
Normal file
After Width: | Height: | Size: 551 KiB |
BIN
src/assets/jianmiau.jpeg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/karol-login.png
Normal file
After Width: | Height: | Size: 412 KiB |
BIN
src/assets/karol.png
Normal file
After Width: | Height: | Size: 142 KiB |
@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from 'vue-class-component';
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
26
src/components/Navbar.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import Sidebar from './Sidebar.vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex">
|
||||||
|
<transition name="slide-x">
|
||||||
|
<Sidebar v-show="sidebarOpen" @open="sidebarOpen = false" />
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div class="m-2" style="width: 100vw">
|
||||||
|
<div class="d-flex">
|
||||||
|
<i class="fas fa-bars fa-2x" @click="sidebarOpen = !sidebarOpen"></i>
|
||||||
|
<h4 class="ms-3">{{ route.meta.pageTitle }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="m-3">
|
||||||
|
<slot name="main" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
94
src/components/Sidebar.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { routes } from '../router/index'
|
||||||
|
import { useRoute, RouteRecordRaw } from 'vue-router'
|
||||||
|
import { useStore } from '../store/index'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import { ConfirmBox } from '../utils/swal'
|
||||||
|
const route = useRoute()
|
||||||
|
const store = useStore()
|
||||||
|
const getUser = store.switchUser
|
||||||
|
const emit = defineEmits(['open'])
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
return routes.filter((route: RouteRecordRaw) => route.meta?.show)
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeItem = () => {
|
||||||
|
emit('open', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}]`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex flex-column flex-shrink-0 p-3 text-white bg-dark" 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>
|
||||||
|
</router-link>
|
||||||
|
<hr />
|
||||||
|
<ul class="nav nav-pills flex-column mb-auto">
|
||||||
|
<li class="nav-item" v-for="(item, index) in items" :key="index">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: item.name }"
|
||||||
|
:class="'nav-link ' + (item.name === route.name ? 'text-info' : 'text-white')"
|
||||||
|
@click="changeItem"
|
||||||
|
>{{ item.meta?.pageTitle }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<div class="dropdown">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
|
||||||
|
id="dropdownUser1"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="store.currentUser === '建喵'"
|
||||||
|
src="../assets/jianmiau-login.png"
|
||||||
|
alt=""
|
||||||
|
width="50"
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
48
src/components/Spinner.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="spinner">
|
||||||
|
<div class="bouncing-loader">
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.spinner {
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouncing-loader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouncing-loader > div {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin: 3rem 0.2rem;
|
||||||
|
background: #921aff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bouncing-loader 0.6s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouncing-loader > div:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouncing-loader > div:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bouncing-loader {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: translateY(-1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,9 @@
|
|||||||
import { createApp } from 'vue'
|
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'
|
||||||
|
|
||||||
createApp(App).use(router).mount('#app')
|
import 'bootstrap'
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
|
|
||||||
|
createApp(App).use(router).use(createPinia()).mount('#app')
|
||||||
|
11
src/models/LineBot.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface MessageInput {
|
||||||
|
type: string
|
||||||
|
// text
|
||||||
|
text?: string
|
||||||
|
// sticker
|
||||||
|
packageId?: string
|
||||||
|
stickerId?: string
|
||||||
|
// image or video
|
||||||
|
originalContentUrl?: string
|
||||||
|
previewImageUrl?: string
|
||||||
|
}
|
21
src/models/Log.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export type RecorderType = '建喵' | '豬涵'
|
||||||
|
|
||||||
|
export type ActionType = '新增' | '編輯' | '結算'
|
||||||
|
|
||||||
|
export class Log {
|
||||||
|
recorder!: RecorderType
|
||||||
|
action!: ActionType
|
||||||
|
item?: string
|
||||||
|
merchant?: string
|
||||||
|
amount?: number
|
||||||
|
date?: Date
|
||||||
|
itemBefore?: string
|
||||||
|
merchantBefore?: string
|
||||||
|
amountBefore?: number
|
||||||
|
dateBefore?: Date
|
||||||
|
closeAmount?: number
|
||||||
|
RecordId?: number
|
||||||
|
RecordIds?: string
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
}
|
23
src/models/Record.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type RecorderType = '建喵' | '豬涵'
|
||||||
|
|
||||||
|
export class Record {
|
||||||
|
id!: number
|
||||||
|
date!: Date
|
||||||
|
item?: string
|
||||||
|
merchant?: string
|
||||||
|
amount!: number
|
||||||
|
recorder?: RecorderType
|
||||||
|
isClosed?: boolean
|
||||||
|
deletedAt?: Date | null
|
||||||
|
createdAt!: Date
|
||||||
|
updatedAt!: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecordInput {
|
||||||
|
date!: Date
|
||||||
|
item!: string
|
||||||
|
merchant!: string
|
||||||
|
amount!: string
|
||||||
|
recorder?: RecorderType
|
||||||
|
editor?: RecorderType
|
||||||
|
}
|
@ -1,16 +1,51 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
const root = '/jm-expense-vue-ts'
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
redirect: `${root}/`
|
||||||
component: Home
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: `${root}/`,
|
||||||
name: 'About',
|
name: 'Home',
|
||||||
component: () => import('../views/About.vue')
|
component: () => import('../views/Home.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '首頁',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/record`,
|
||||||
|
name: 'Record',
|
||||||
|
component: () => import('../views/Record.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '未結算紀錄',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/closedRecord`,
|
||||||
|
name: 'ClosedRecord',
|
||||||
|
component: () => import('../views/ClosedRecord.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '已結算紀錄',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${root}/logs`,
|
||||||
|
name: 'Logs',
|
||||||
|
component: () => import('../views/Logs.vue'),
|
||||||
|
meta: {
|
||||||
|
pageTitle: '更動紀錄',
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('../views/NotFound.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
14
src/store/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useStore = defineStore('index', {
|
||||||
|
state: () => ({
|
||||||
|
currentUser: localStorage.getItem('jm-user')
|
||||||
|
}),
|
||||||
|
// getter: {}
|
||||||
|
actions: {
|
||||||
|
switchUser(user: string) {
|
||||||
|
localStorage.setItem('jm-user', user)
|
||||||
|
this.currentUser = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
18
src/utils/helper.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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 '日'
|
||||||
|
}
|
||||||
|
}
|
13
src/utils/swal.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
export const Toast = Swal.mixin({
|
||||||
|
toast: true,
|
||||||
|
position: 'bottom-end',
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 3000 // toast 停留多久
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ConfirmBox = Swal.mixin({
|
||||||
|
showConfirmButton: true,
|
||||||
|
showCancelButton: true
|
||||||
|
})
|
@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
181
src/views/ClosedRecord.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import recordAPI from '../apis/expense'
|
||||||
|
import { Record } from '../models/Record'
|
||||||
|
import { showWeekDay } from '../utils/helper'
|
||||||
|
import Spinner from '../components/Spinner.vue'
|
||||||
|
type SearchMode = '月份' | '日期'
|
||||||
|
|
||||||
|
// data
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const records = ref<Record[]>([])
|
||||||
|
const searchMode = ref<SearchMode>('月份')
|
||||||
|
const year = ref(new Date().getFullYear())
|
||||||
|
const month = ref(new Date().getMonth() + 1)
|
||||||
|
const startDate = ref<Date | string>('')
|
||||||
|
const finishDate = ref<Date | string>('')
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchRecords = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await recordAPI.getAll()
|
||||||
|
records.value = data.filter((item: Record) => item.isClosed === true)
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// created
|
||||||
|
fetchRecords()
|
||||||
|
</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>
|
||||||
|
<table class="table table-striped table-info table-hover" v-if="filteredRecord.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">項目</th>
|
||||||
|
<th scope="col">商家</th>
|
||||||
|
<th scope="col">金額</th>
|
||||||
|
<th scope="col">日期</th>
|
||||||
|
<th scope="col" id="column-item">首次記錄者</th>
|
||||||
|
<th scope="col" id="column-item">首次記錄時間</th>
|
||||||
|
<th scope="col" id="column-item">更新時間</th>
|
||||||
|
</tr>
|
||||||
|
</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 id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||||
|
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
#column-item {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,18 +1,137 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<div class="home">
|
import { ref } from 'vue'
|
||||||
<img alt="Vue logo" src="../assets/logo.png">
|
import recordAPI from '../apis/expense'
|
||||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
// import lineBotAPI from '../apis/lineBot'
|
||||||
</div>
|
import { Record } from '../models/Record'
|
||||||
</template>
|
import Spinner from '../components/Spinner.vue'
|
||||||
|
|
||||||
<script>
|
class MonthData {
|
||||||
// @ is an alias to /src
|
total!: number
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'
|
closedAmount!: number
|
||||||
|
rate!: number
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
const isLoading = ref<boolean>(true)
|
||||||
name: 'Home',
|
const records = ref<Record[]>([])
|
||||||
components: {
|
const thisMonthData = ref<MonthData>(new MonthData())
|
||||||
HelloWorld
|
const lastMonthData = ref<MonthData>(new MonthData())
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchRecords = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await recordAPI.getAll()
|
||||||
|
records.value = data
|
||||||
|
// monthData
|
||||||
|
const nowYear = new Date().getFullYear()
|
||||||
|
const nowMonth = new Date().getMonth() + 1
|
||||||
|
thisMonthData.value = calculateMonthData(nowYear, nowMonth)
|
||||||
|
lastMonthData.value = calculateMonthData(
|
||||||
|
nowMonth - 1 === 0 ? nowYear - 1 : nowYear,
|
||||||
|
nowMonth - 1 === 0 ? 12 : nowMonth - 1
|
||||||
|
)
|
||||||
|
isLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simple function
|
||||||
|
const calculateMonthData = (year: number, month: number) => {
|
||||||
|
const recordsArr = records.value?.filter(
|
||||||
|
(record) => new Date(record.date).getFullYear() === year && new Date(record.date).getMonth() + 1 === month
|
||||||
|
)
|
||||||
|
let total = 0
|
||||||
|
let closedAmount = 0
|
||||||
|
for (let record of recordsArr) {
|
||||||
|
total += record.amount
|
||||||
|
if (record.isClosed === true) {
|
||||||
|
closedAmount += record.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rate
|
||||||
|
if (total === 0) {
|
||||||
|
rate = 0
|
||||||
|
} else {
|
||||||
|
rate = Math.round(closedAmount / total) * 100
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
closedAmount,
|
||||||
|
rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test
|
||||||
|
// const btnClick = async function () {
|
||||||
|
// try {
|
||||||
|
// const input = { message: 'HELLO', userId: 'Ub3557f7c812e4e78293959fe4fccd414' }
|
||||||
|
// const { data } = await lineBotAPI.pushMsg(input)
|
||||||
|
// console.log('data', data)
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('error', error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fetchRecords()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
<!-- <button class="btn btn-info" @click="btnClick">Message</button> -->
|
||||||
|
|
||||||
|
<div class="list-group list-group-checkable">
|
||||||
|
<label class="list-group-item py-3 mb-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8 text-start">
|
||||||
|
<h4>
|
||||||
|
本月總額 $<strong>{{ thisMonthData.total }}</strong>
|
||||||
|
</h4>
|
||||||
|
<div class="progress mt-2" style="height: 20px">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
:style="`width: ${thisMonthData.rate}%`"
|
||||||
|
:aria-valuenow="thisMonthData.rate"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
>
|
||||||
|
{{ thisMonthData.rate }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 text-start">
|
||||||
|
<div>未結算$ {{ thisMonthData.total - thisMonthData.closedAmount }}</div>
|
||||||
|
<div>已結算$ {{ thisMonthData.closedAmount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="list-group-item py-3 mb-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8 text-start">
|
||||||
|
<h4>
|
||||||
|
上月總額 $<strong>{{ lastMonthData.total }}</strong>
|
||||||
|
</h4>
|
||||||
|
<div class="progress mt-2" style="height: 20px">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
:style="`width: ${lastMonthData.rate}%`"
|
||||||
|
:aria-valuenow="lastMonthData.rate"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
>
|
||||||
|
{{ lastMonthData.rate }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 text-start">
|
||||||
|
<div>未結算$ {{ lastMonthData.total - lastMonthData.closedAmount }}</div>
|
||||||
|
<div>已結算$ {{ lastMonthData.closedAmount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
||||||
|
217
src/views/Logs.vue
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { showWeekDay } from '../utils/helper'
|
||||||
|
import recordAPI from '../apis/expense'
|
||||||
|
import { Log } from '../models/Log'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import Spinner from '../components/Spinner.vue'
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ title: '總覽', btnColor: 'secondary' },
|
||||||
|
{ title: '新增', btnColor: 'primary' },
|
||||||
|
{ title: '編輯', btnColor: 'success' },
|
||||||
|
{ title: '結算', btnColor: 'danger' }
|
||||||
|
]
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const logs = ref<Log[]>([])
|
||||||
|
const nowTab = ref('總覽')
|
||||||
|
|
||||||
|
const filteredLogs = computed(() => {
|
||||||
|
if (nowTab.value === '總覽') return logs.value
|
||||||
|
return logs.value.filter((log) => log.action === nowTab.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchLogs = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await recordAPI.getLogs()
|
||||||
|
logs.value = 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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
).toLocaleDateString()}</td></tr>`
|
||||||
|
}
|
||||||
|
html += `</tbody></table>${
|
||||||
|
document.documentElement.scrollWidth >= 500 ? '' : '<style>table{font-size:0.5em;}</style>'
|
||||||
|
}`
|
||||||
|
await Swal.fire({
|
||||||
|
title: '結算紀錄',
|
||||||
|
html
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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() + ` (${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>
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card mb-3" v-for="(log, index) in filteredLogs" :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">
|
||||||
|
<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 === '編輯'">
|
||||||
|
<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.RecordIds)"></i>
|
||||||
|
</h6>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#records:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: salmon;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 499px) {
|
||||||
|
#pc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 500px) {
|
||||||
|
#mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
9
src/views/NotFound.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://www.lifewire.com/thmb/-zkZkHWjYJ1eAIRKzvAksPDMrzg=/3000x2000/filters:no_upscale():max_bytes(150000):strip_icc()/404-not-found-error-explained-2622936-Final-fde7be1b7e2e499c9f039d97183e7f52.jpg"
|
||||||
|
alt="404"
|
||||||
|
style="height: 70vh; width: 70vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
399
src/views/Record.vue
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import recordAPI from '../apis/expense'
|
||||||
|
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 { useStore } from '../store/index'
|
||||||
|
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)
|
||||||
|
|
||||||
|
// methods
|
||||||
|
const fetchRecords = async function () {
|
||||||
|
try {
|
||||||
|
const { data } = await recordAPI.getAll()
|
||||||
|
records.value = data.filter((item: Record) => item.isClosed === false)
|
||||||
|
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-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) {
|
||||||
|
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: '成功編輯資料!'
|
||||||
|
})
|
||||||
|
} 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)
|
||||||
|
if (index !== undefined) {
|
||||||
|
if (index !== -1) {
|
||||||
|
closeRecords.value?.splice(index, 1)
|
||||||
|
closeRecordsAmount.value -= amount
|
||||||
|
} else {
|
||||||
|
closeRecords.value?.push(id)
|
||||||
|
closeRecordsAmount.value += amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelBtnClick = () => {
|
||||||
|
isCloseStatus.value = false
|
||||||
|
closeRecords.value = []
|
||||||
|
closeRecordsAmount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const toCloseBtnClick = async () => {
|
||||||
|
const { isConfirmed } = await ConfirmBox.fire({
|
||||||
|
icon: 'info',
|
||||||
|
title: '確定結算資料?',
|
||||||
|
text: `結算金額為 $${closeRecordsAmount.value} [結算者: ${store.currentUser}]`
|
||||||
|
})
|
||||||
|
if (isConfirmed) {
|
||||||
|
closeRecord(closeRecordsAmount.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRecord = async (amount: number) => {
|
||||||
|
try {
|
||||||
|
if (store.currentUser) {
|
||||||
|
await recordAPI.close({
|
||||||
|
records: closeRecords.value.toString(),
|
||||||
|
totalAmount: amount,
|
||||||
|
recorder: store.currentUser
|
||||||
|
})
|
||||||
|
isCloseStatus.value = false
|
||||||
|
fetchRecords()
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: '成功結算資料!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error', error)
|
||||||
|
Toast.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: '結算資料失敗!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// created
|
||||||
|
fetchRecords()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="!isLoading">
|
||||||
|
<div class="d-flex justify-content-between mb-3" style="width: 100vw">
|
||||||
|
<button type="button" class="btn btn-primary text-end" @click="createBtnClick">新增資料</button>
|
||||||
|
<div>
|
||||||
|
<template v-if="isCloseStatus">
|
||||||
|
<span class="badge bg-info text-dark me-3 align-middle p-2">結算金額 ${{ closeRecordsAmount }}</span>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="cancelBtnClick">取消結算</button>
|
||||||
|
<button type="button" class="btn btn-success ms-3" @click="toCloseBtnClick">確定結算</button>
|
||||||
|
</template>
|
||||||
|
<button v-else type="button" class="btn btn-danger" @click="closeBtnClick">開始結算</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped 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>
|
||||||
|
<th scope="col" id="column-item">首次記錄者</th>
|
||||||
|
<th scope="col" id="column-item">首次記錄時間</th>
|
||||||
|
<th scope="col" id="column-item">更新時間</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(record, index) in records"
|
||||||
|
:key="index"
|
||||||
|
:class="{ 'table-warning': closeRecords?.includes(record.id) }"
|
||||||
|
@click="checkboxClick(record.id, record.amount)"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
v-if="isCloseStatus"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="closeRecords?.includes(record.id)"
|
||||||
|
/>
|
||||||
|
<i v-else class="fas fa-edit" @click="editBtnClick(record.id)"></i>
|
||||||
|
</td>
|
||||||
|
<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 id="column-item">{{ new Date(record.createdAt).toLocaleString() }}</td>
|
||||||
|
<td id="column-item">{{ new Date(record.updatedAt).toLocaleString() }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="img-fluid"
|
||||||
|
src="https://memeprod.sgp1.digitaloceanspaces.com/user-wtf/1581909112681.jpg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spinner v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
span {
|
||||||
|
font-size: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
i[class~='fa-edit']:hover {
|
||||||
|
color: rgb(30, 197, 57);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
#column-item {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
3
vue.config.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
publicPath: process.env.NODE_ENV === 'production' ? '/jm-expense-vue-ts/' : '/'
|
||||||
|
}
|