├── .gitignore ├── .prettierrc ├── .vscode └── extensions.json ├── README.md ├── components.d.ts ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── ccuplus.svg ├── favicon.ico ├── member01.jpg ├── member02.jpg ├── member03.jpg ├── member04.png ├── search_box_icon.png └── vite.svg ├── samole.env ├── src ├── App.vue ├── assets │ ├── logo.png │ └── vue.svg ├── components │ ├── common │ │ ├── ClipLoadingSpinner.vue │ │ ├── error_box.vue │ │ ├── loadingSpinner.vue │ │ ├── option │ │ │ └── commonOption.vue │ │ └── optionButton │ │ │ └── kebabButton.vue │ ├── layout │ │ ├── footer.vue │ │ └── navbar.vue │ └── pages │ │ ├── home │ │ ├── intro.vue │ │ └── notify.vue │ │ ├── login │ │ └── loginarea.vue │ │ ├── main │ │ ├── classTable.vue │ │ ├── colorTemplate.vue │ │ ├── comment.vue │ │ ├── courseCard.vue │ │ ├── course_tab.vue │ │ ├── inputArea.vue │ │ ├── modal.vue │ │ ├── search_box.vue │ │ ├── serach_modes │ │ │ ├── course_name.vue │ │ │ ├── custom.vue │ │ │ ├── department.vue │ │ │ ├── teacher.vue │ │ │ └── time.vue │ │ └── timeSelection.vue │ │ └── tutorial │ │ └── tutorial.vue ├── css │ └── style.css ├── functions │ ├── ccuplus.ts │ ├── course_add.ts │ ├── course_color.ts │ ├── course_delete.ts │ ├── course_search.ts │ ├── general.ts │ ├── image_render.ts │ ├── rowspanizer.ts │ ├── save_course.ts │ ├── token.ts │ ├── tool.ts │ └── web_statistic.ts ├── main.ts ├── router │ └── index.ts ├── store │ ├── ccuplus.ts │ ├── course.ts │ ├── general.ts │ └── index.ts ├── views │ ├── page_admin.vue │ ├── page_error.vue │ ├── page_home.vue │ ├── page_login.vue │ ├── page_main.vue │ ├── page_record_error.vue │ └── page_tutorial.vue └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | /server 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | .env 27 | 28 | src/css/tailwind.css -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "semi": true, 6 | "experimentalTernaries": false, 7 | "singleQuote": false, 8 | "jsxSingleQuote": false, 9 | "quoteProps": "as-needed", 10 | "trailingComma": "all", 11 | "singleAttributePerLine": false, 12 | "htmlWhitespaceSensitivity": "css", 13 | "vueIndentScriptAndStyle": false, 14 | "proseWrap": "preserve", 15 | "insertPragma": false, 16 | "requirePragma": false, 17 | "tabWidth": 2, 18 | "useTabs": false, 19 | "embeddedLanguageFormatting": "auto", 20 | "printWidth": 70 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pineapple Schedule - A simple schedule web for CCU students 2 | 3 | ## Local development 4 | 5 | ### Prerequisites 6 | 7 | - [Node.js](https://nodejs.org/en/) (>= 10.16.0) 8 | - [npm](https://www.npmjs.com/) (>= 6.9.0) 9 | 10 | ### Installation 11 | 12 | ```bash 13 | npm install 14 | ``` 15 | 16 | ### Run 17 | 18 | ```bash 19 | npm run dev 20 | ``` 21 | 22 | ### Build 23 | 24 | ```bash 25 | npm run build 26 | ``` 27 | 28 | If you encounter the error "Could not find a declaration file for module 'vuex' during the build process, please refer to this article: https://github.com/vuejs/vuex/issues/2223 . 29 | 30 | ### Preview 31 | 32 | ```bash 33 | npm run preview 34 | ``` 35 | 36 | 37 | ### Tailwind CSS Hot Reload 38 | 39 | ```bash 40 | npm run hotfix 41 | ``` 42 | 43 | ## How to use backend API 44 | 45 | Rename the file "samole.env" in the directory to ".env" and make sure to use one of the following during runtime: localhost:5173, localhost:5585, localhost:8080. 46 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ClassTable: typeof import('./src/components/pages/main/classTable.vue')['default'] 11 | ClipLoadingSpinner: typeof import('./src/components/common/ClipLoadingSpinner.vue')['default'] 12 | ColorTemplate: typeof import('./src/components/pages/main/colorTemplate.vue')['default'] 13 | Comment: typeof import('./src/components/pages/main/comment.vue')['default'] 14 | CommonOption: typeof import('./src/components/common/option/commonOption.vue')['default'] 15 | Course_name: typeof import('./src/components/pages/main/serach_modes/course_name.vue')['default'] 16 | Course_tab: typeof import('./src/components/pages/main/course_tab.vue')['default'] 17 | CourseCard: typeof import('./src/components/pages/main/courseCard.vue')['default'] 18 | Custom: typeof import('./src/components/pages/main/serach_modes/custom.vue')['default'] 19 | Department: typeof import('./src/components/pages/main/serach_modes/department.vue')['default'] 20 | Error_box: typeof import('./src/components/common/error_box.vue')['default'] 21 | Footer: typeof import('./src/components/layout/footer.vue')['default'] 22 | InputArea: typeof import('./src/components/pages/main/inputArea.vue')['default'] 23 | Intro: typeof import('./src/components/pages/home/intro.vue')['default'] 24 | KebabButton: typeof import('./src/components/common/optionButton/kebabButton.vue')['default'] 25 | LoadingSpinner: typeof import('./src/components/common/loadingSpinner.vue')['default'] 26 | Loginarea: typeof import('./src/components/pages/login/loginarea.vue')['default'] 27 | Modal: typeof import('./src/components/pages/main/modal.vue')['default'] 28 | Navbar: typeof import('./src/components/layout/navbar.vue')['default'] 29 | Notify: typeof import('./src/components/pages/home/notify.vue')['default'] 30 | RouterLink: typeof import('vue-router')['RouterLink'] 31 | RouterView: typeof import('vue-router')['RouterView'] 32 | Search_box: typeof import('./src/components/pages/main/search_box.vue')['default'] 33 | Teacher: typeof import('./src/components/pages/main/serach_modes/teacher.vue')['default'] 34 | Time: typeof import('./src/components/pages/main/serach_modes/time.vue')['default'] 35 | TimeSelection: typeof import('./src/components/pages/main/timeSelection.vue')['default'] 36 | Tutorial: typeof import('./src/components/pages/tutorial/tutorial.vue')['default'] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | CCU Class 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pineappleschdedule", 3 | "private": true, 4 | "version": "1.0.6", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port=8080 && npx tailwindcss -i ./src/css/style.css -o ./src/css/tailwind.css --minify", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview", 10 | "hotfix": "npx tailwindcss -i ./src/css/style.css -o ./src/css/tailwind.css --minify --watch", 11 | "format": "prettier --ignore-path .gitignore --write \"src/**/*.{js,vue,ts}\"" 12 | }, 13 | "pre-commit": ["format"], 14 | "dependencies": { 15 | "@ant-design/icons": "^5.2.5", 16 | "@ant-design/icons-vue": "^6.1.0", 17 | "@chenfengyuan/vue-carousel": "^2.0.0", 18 | "@coleqiu/vue-drag-select": "^2.0.5", 19 | "@types/jquery": "^3.5.16", 20 | "@types/lodash": "^4.14.197", 21 | "@vue/cli": "^5.0.8", 22 | "animate.css": "^4.1.1", 23 | "ant-design-vue": "^4.0.0", 24 | "argon2": "^0.31.0", 25 | "axios": "^1.4.0", 26 | "dotenv": "^16.3.1", 27 | "dotenv-cli": "^7.2.1", 28 | "fund": "^1.0.0", 29 | "jquery": "^3.7.0", 30 | "prettier": "^3.2.4", 31 | "splitpanes": "^3.1.5", 32 | "update": "^0.7.4", 33 | "uuid": "^9.0.0", 34 | "vue": "^3.3.4", 35 | "vue-accessible-color-picker": "^4.1.4", 36 | "vue-cookies": "^1.8.3", 37 | "vue-html2canvas": "^0.0.4", 38 | "vue-lazyload": "^3.0.0", 39 | "vue-router": "^4.2.2", 40 | "vue-spinner": "^1.0.4", 41 | "vue3-draggable-resizable": "^1.6.5", 42 | "vuex": "^4.0.2" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^20.4.8", 46 | "@types/uuid": "^9.0.3", 47 | "@vitejs/plugin-vue": "^4.1.0", 48 | "autoprefixer": "^10.4.14", 49 | "postcss": "^8.4.24", 50 | "pre-commit": "^1.2.2", 51 | "tailwindcss": "^3.3.2", 52 | "typescript": "^5.0.2", 53 | "unplugin-vue-components": "^0.25.1", 54 | "vite": "^4.3.9", 55 | "vue-tsc": "^1.4.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/ccuplus.svg: -------------------------------------------------------------------------------- 1 | navi -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/favicon.ico -------------------------------------------------------------------------------- /public/member01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/member01.jpg -------------------------------------------------------------------------------- /public/member02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/member02.jpg -------------------------------------------------------------------------------- /public/member03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/member03.jpg -------------------------------------------------------------------------------- /public/member04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/member04.png -------------------------------------------------------------------------------- /public/search_box_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/public/search_box_icon.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samole.env: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'development' 2 | 3 | VITE_BACKEND_DEVICE=api.ccuclass.com 4 | VITE_CCUPLUS_DEVICE=ccu.plus/api/courses/ 5 | VITE_BACKEND_DEVICE_PORT= 6 | 7 | frontend 8 | VITE_UL_ROW=5 9 | VITE_TITLE_DEFAULT_COLOR="rgba(254, 215, 170, 0.5)" 10 | VITE_CARD_DEFAULT_COLOR="rgb(192 215 193 / 1)" 11 | VITE_CARDTEXT_DEFAULT_COLOR="rgb(0 0 0 / 1)" 12 | VITE_CARDTEXT_DEFAULT_STYLE="" -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCU-Class/FrontEnd/801876000ea1ae3c44f13c5845661664b22eafdd/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/ClipLoadingSpinner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 45 | 94 | -------------------------------------------------------------------------------- /src/components/common/error_box.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /src/components/common/loadingSpinner.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 73 | 74 | 105 | -------------------------------------------------------------------------------- /src/components/common/option/commonOption.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | 28 | -------------------------------------------------------------------------------- /src/components/common/optionButton/kebabButton.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 44 | 45 | 59 | -------------------------------------------------------------------------------- /src/components/layout/footer.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/layout/navbar.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /src/components/pages/home/intro.vue: -------------------------------------------------------------------------------- 1 | 184 | 185 | 253 | 254 | 259 | -------------------------------------------------------------------------------- /src/components/pages/home/notify.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 53 | -------------------------------------------------------------------------------- /src/components/pages/login/loginarea.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 81 | -------------------------------------------------------------------------------- /src/components/pages/main/classTable.vue: -------------------------------------------------------------------------------- 1 | 140 | 141 | 362 | -------------------------------------------------------------------------------- /src/components/pages/main/colorTemplate.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 67 | -------------------------------------------------------------------------------- /src/components/pages/main/comment.vue: -------------------------------------------------------------------------------- 1 | 156 | 157 | 203 | -------------------------------------------------------------------------------- /src/components/pages/main/courseCard.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 135 | -------------------------------------------------------------------------------- /src/components/pages/main/course_tab.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 134 | -------------------------------------------------------------------------------- /src/components/pages/main/inputArea.vue: -------------------------------------------------------------------------------- 1 | 143 | 144 | 408 | -------------------------------------------------------------------------------- /src/components/pages/main/modal.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 78 | -------------------------------------------------------------------------------- /src/components/pages/main/search_box.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 225 | -------------------------------------------------------------------------------- /src/components/pages/main/serach_modes/course_name.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 127 | -------------------------------------------------------------------------------- /src/components/pages/main/serach_modes/custom.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 86 | -------------------------------------------------------------------------------- /src/components/pages/main/serach_modes/department.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 214 | -------------------------------------------------------------------------------- /src/components/pages/main/serach_modes/teacher.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 130 | -------------------------------------------------------------------------------- /src/components/pages/main/serach_modes/time.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/pages/main/timeSelection.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 227 | -------------------------------------------------------------------------------- /src/components/pages/tutorial/tutorial.vue: -------------------------------------------------------------------------------- 1 | 129 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .selectDisplayColor { 6 | @apply bg-orange-300/50; 7 | } 8 | .conflict { 9 | @apply bg-red-300; 10 | } 11 | .drag-select-option--selected { 12 | background-color: rgb(254, 255, 166); 13 | opacity: 0.4; 14 | } 15 | img { 16 | display: unset; 17 | } 18 | .btn-head { 19 | @apply rounded-3xl px-5 py-2 mx-1 hover:bg-white duration-500; 20 | } 21 | .btn-head-mobile { 22 | @apply rounded-3xl text-left pl-3 px-3 py-2 my-1 hover:bg-white duration-500; 23 | } 24 | .btn-link { 25 | @apply text-center font-medium mx-2 px-5 py-3 border-2 border-orange-200 bg-orange-200 rounded-full hover:bg-transparent duration-300; 26 | } 27 | .btn-normal { 28 | @apply text-center shadow-2xl flex justify-center items-center text-lime-900 font-medium mx-2 px-5 py-1 border-2 border-lime-600 bg-lime-600/60 rounded-lg hover:bg-transparent duration-300; 29 | } 30 | .btn-comment { 31 | @apply text-center shadow-2xl font-medium mx-2 px-5 py-1 border-2 border-[#aefff7] bg-[#aefff7] rounded-lg hover:bg-transparent hover:text-white duration-500; 32 | } 33 | .virtualtablehead { 34 | @apply h-[1.9rem]; 35 | } 36 | .virtualtable { 37 | @apply min-w-[8.22rem] text-center py-1.5; 38 | } 39 | table { 40 | table-layout: fixed; 41 | } 42 | thead > tr > th { 43 | @apply text-center text-gray-900 py-1.5 font-semibold min-w-[9rem] border-b border-b-gray-900; 44 | } 45 | .table-head { 46 | @apply text-center text-gray-900 py-1.5 font-semibold min-w-[9rem]; 47 | } 48 | .title { 49 | @apply w-16 text-xl text-gray-700 font-serif; 50 | } 51 | .course { 52 | @apply rounded-md font-bold text-base; 53 | } 54 | [type="search"]::-webkit-search-cancel-button { 55 | width: 12px; 56 | height: 12px; 57 | border: 0; 58 | background-size: 16px; 59 | cursor: pointer; 60 | opacity: 0.4; 61 | transition: 0.2s; 62 | position: relative; 63 | } 64 | .result { 65 | position: absolute; 66 | visibility: hidden; 67 | } 68 | .result-show { 69 | visibility: visible; 70 | position: relative; 71 | } 72 | /* change search border when focus */ 73 | [type="search"] { 74 | outline: none; 75 | border-width: 0.1px; 76 | } 77 | [type="search"]:focus { 78 | outline: none; 79 | border-width: 0.1px; 80 | border-color: #ffb676; 81 | box-shadow: 0 0 5px #f8efcf; 82 | } 83 | [type="text"] { 84 | outline: none; 85 | border-width: 0.1px; 86 | } 87 | [type="text"]:focus { 88 | outline: none; 89 | border-width: 0.1px; 90 | border-color: #ffb676; 91 | box-shadow: 0 0 5px #f8efcf; 92 | } 93 | [type="password"] { 94 | outline: none; 95 | border-width: 0.1px; 96 | } 97 | [type="password"]:focus { 98 | outline: none; 99 | border-width: 0.1px; 100 | border-color: #ffb676; 101 | box-shadow: 0 0 5px #f8efcf; 102 | } 103 | .ant-switch { 104 | background-color: #ffb676; 105 | } 106 | .splitpanes { 107 | @apply bg-white; 108 | } 109 | .splitpanes--vertical > .splitpanes__splitter { 110 | width: 5px; 111 | background: linear-gradient(90deg, #ccc, #fff2bb); 112 | } 113 | .main_page_left { 114 | max-height: 95vh; 115 | overflow-y: auto; 116 | position: relative; 117 | } 118 | 119 | /* scroll bar */ 120 | 121 | /* Firefox */ 122 | * { 123 | scrollbar-width: thin; 124 | scrollbar-color: #ffe2c8 #fff; 125 | } 126 | 127 | /* Chrome, Edge, and Safari */ 128 | *::-webkit-scrollbar { 129 | @apply w-4; 130 | } 131 | 132 | *::-webkit-scrollbar-track { 133 | @apply bg-white rounded-lg; 134 | } 135 | 136 | *::-webkit-scrollbar-thumb { 137 | @apply bg-[#ffe2c8] rounded-lg border-2 border-white; 138 | } 139 | 140 | .small { 141 | position: relative; 142 | background-color: white; 143 | border-radius: 0.25rem; 144 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; 145 | } 146 | 147 | .border { 148 | border-radius: 0.75rem; 149 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; 150 | } 151 | 152 | .large { 153 | width: 20rem; 154 | height: 28rem; 155 | background-color: white; 156 | } 157 | #drag { 158 | @apply fixed top-0 left-0 z-50 hidden duration-500; 159 | } 160 | .search_list::-webkit-scrollbar { 161 | /* bar color */ 162 | @apply w-2 bg-transparent; 163 | } 164 | .search_list::-webkit-scrollbar-thumb { 165 | @apply bg-[#7e7e7e] rounded-lg border-2 border-white; 166 | } 167 | 168 | .card { 169 | @apply cursor-pointer justify-items-center h-full w-auto justify-center items-center select-none rounded-lg z-10; 170 | } 171 | .card-content { 172 | @apply justify-center items-center flex w-full h-full max-h-full; 173 | } 174 | .card:hover { 175 | transition: 0.1s linear; 176 | transform: scale(1.25); 177 | -webkit-transform: scale(1.25); 178 | z-index: 2; 179 | } 180 | .fliping-enter-active { 181 | transition: all 0.15s ease; 182 | -webkit-transition: all 0.15s ease; 183 | } 184 | .fliping-leave-active { 185 | transition: all 0.15s ease; 186 | -webkit-transition: all 0.15s ease; 187 | } 188 | 189 | .fliping-enter { 190 | transform: rotateY(180deg); 191 | -webkit-transform: rotateY(180deg); 192 | opacity: 0; 193 | } 194 | .fliping-leave-to { 195 | transform: rotateY(90deg); 196 | -webkit-transform: rotateY(90deg); 197 | opacity: 0; 198 | } 199 | .fliping-enter-to { 200 | transform: rotateY(0); 201 | -webkit-transform: rotateY(0); 202 | opacity: 0; 203 | } 204 | .fliping-leave { 205 | transform: rotateY(180deg); 206 | -webkit-transform: rotateY(180deg); 207 | opacity: 0; 208 | } 209 | .color-picker { 210 | @apply max-w-full max-h-full w-72 h-72; 211 | } 212 | [type="search"]::placeholder { 213 | @apply text-center; 214 | } 215 | .custom { 216 | @apply bg-transparent border-0 border-b-2 border-gray-500 w-28 text-center; 217 | } 218 | .custom:focus { 219 | @apply border-none outline-none; 220 | } 221 | -------------------------------------------------------------------------------- /src/functions/ccuplus.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "../store"; 3 | 4 | const env = import.meta.env; 5 | const apiSite = `${env.VITE_CCUPLUS_DEVICE}`; 6 | 7 | export async function show_comment(course_id: string) { 8 | window.scrollTo(0, 0); 9 | store.dispatch("pass_course_id", course_id); 10 | store.dispatch("display"); 11 | } 12 | 13 | export async function searchCourseOnCcuplus(course_id: string) { 14 | const apiUrl = apiSite + course_id; 15 | console.log(apiUrl); 16 | return new Promise((resolve, reject) => { 17 | axios 18 | .get(apiUrl) 19 | .then((response) => { 20 | resolve(response.data); 21 | }) 22 | .catch((error) => { 23 | console.error(error); 24 | reject(error); 25 | }); 26 | }); 27 | } 28 | 29 | export async function searchCommentsOnCcuplus(course_id: string) { 30 | const apiUrl = apiSite + course_id + "/comments"; 31 | return new Promise((resolve) => { 32 | axios 33 | .get(apiUrl) 34 | .then((response) => { 35 | console.log(typeof response.data); 36 | resolve(response.data); 37 | }) 38 | .catch((error) => { 39 | console.error(error); 40 | resolve(false); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/functions/course_add.ts: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | import { 3 | Course, 4 | courseToTime, 5 | courseToStartIndex, 6 | courseToEndIndex, 7 | WeekDayToInt, 8 | } from "./general"; 9 | import { recordcourse } from "./course_search"; 10 | import { v4 as uuidv4 } from "uuid"; 11 | import { rowspanize } from "./rowspanizer"; 12 | import _ from "lodash"; 13 | import { toRaw } from "vue"; 14 | import { splittime } from "./tool"; 15 | 16 | const env = import.meta.env; 17 | 18 | // a function that put the course in the database of manual input 19 | // and return the status of the operation 20 | 21 | export function classconflict(course: any) { 22 | let table: Course[][] = 23 | store.state.course.TotalCourseData[store.state.course.activeIndex] 24 | .classStorage; 25 | let time = splittime(course.class_time); 26 | for (let i = 0; i < time.length; i++) { 27 | let weekDayIndex = WeekDayToInt[time[i][0]]; // 2 is the offset of the first two columns 28 | let startHour = courseToStartIndex[time[i][1]]; 29 | let endHour = courseToEndIndex[time[i][2]]; 30 | for (let i = startHour; i < endHour; i++) { 31 | if (table[i][weekDayIndex].getIsCourse()) { 32 | // there is a course in the same time slot 33 | return true; 34 | } 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | function conflictMessage() { 41 | alert("新增課程失敗,請檢查是否衝堂"); 42 | } 43 | 44 | // 自定義課程 45 | function courseAdd( 46 | courseName: string, 47 | classRoom: string, 48 | weekDay: string, 49 | start: string, 50 | end: string, 51 | ) { 52 | // construct a course object here 53 | // push the course object to the local storage 54 | // store information in the database 55 | // return the status of the operation 56 | let activeIndex = store.state.course.activeIndex; 57 | let TotalCourseData = store.state.course.TotalCourseData; 58 | let table = _.cloneDeep( 59 | toRaw(TotalCourseData[activeIndex].classStorage), 60 | ); 61 | let Uuid = uuidv4(); 62 | let course = new Course({ 63 | start_time: courseToTime[start], 64 | course_name: courseName, 65 | classroom: classRoom, 66 | is_title: false, 67 | is_course: true, 68 | color: env.VITE_CARD_DEFAULT_COLOR, 69 | ID: "此為自定義課程", 70 | Credit: 0, 71 | is_custom: true, 72 | Teacher: "此為自定義課程", 73 | Memo: null, 74 | textColor: env.VITE_CARDTEXT_DEFAULT_COLOR, 75 | textStyle: env.VITE_CARDTEXT_DEFAULT_STYLE, 76 | uuid: Uuid, 77 | length: 0, 78 | department: "此為自定義課程", 79 | grade: "此為自定義課程", 80 | }); 81 | let weekDayIndex = WeekDayToInt[weekDay]; // 2 is the offset of the first two columns 82 | let startHour = courseToStartIndex[start]; 83 | let endHour = courseToEndIndex[end]; 84 | let startT = `${weekDay}${start}~${end}`; 85 | for (let i = startHour; i < endHour; i++) { 86 | if (table[i][weekDayIndex].getIsCourse()) { 87 | // there is a course in the same time slot 88 | return false; 89 | } else { 90 | // there is no course in the same time slot 91 | table[i][weekDayIndex] = _.cloneDeep(course); 92 | } 93 | } 94 | course.setStartTime(startT); 95 | store.dispatch("addCourseList", course); 96 | // 不要在這邊儲存localstorage,使用store 97 | // window.location.reload(); 98 | table = rowspanize(table); 99 | store.dispatch("addCourse", table); 100 | return true; 101 | } 102 | 103 | function searchAdd(course_list: Course[]) { 104 | // construct a course object here 105 | // push the course object to the local storage 106 | // store information in the database 107 | // return the status of the operation 108 | // console.log(course_list); 109 | let activeIndex = store.state.course.activeIndex; 110 | let TotalCourseData = store.state.course.TotalCourseData; 111 | let table = _.cloneDeep( 112 | toRaw(TotalCourseData[activeIndex].classStorage), 113 | ); 114 | // put the list of courses into the table 115 | for (let i = 0; i < course_list.length; i++) { 116 | let course = course_list[i]; 117 | // console.log(course); 118 | let weekDayIndex = WeekDayToInt[course.getWeekDay()]; // 2 is the offset of the first two columns 119 | let startHour = courseToStartIndex[course.getStartTime()]; 120 | let endHour = courseToEndIndex[course.getEndTime()]; 121 | // console.log(startHour, endHour); 122 | let startT = courseToTime[course.getStartTime()]; 123 | for (let j = startHour; j < endHour; j++) { 124 | // console.log(table[j][weekDayIndex]); 125 | if (table[j][weekDayIndex].getIsCourse()) { 126 | // there is a course in the same time slot 127 | return false; 128 | } else { 129 | table[j][weekDayIndex] = _.cloneDeep(course); 130 | table[j][weekDayIndex].setStartTime(startT); 131 | } 132 | } 133 | } 134 | // 不要在這邊儲存localstorage,使用store 135 | table = rowspanize(table); 136 | store.dispatch("addCourse", table); 137 | return true; 138 | } 139 | 140 | export function push_to_table(mode: Number, item: any) { 141 | if (mode == 1) { 142 | console.log(item); 143 | if (item.className === undefined) { 144 | alert("請輸入課程名稱"); 145 | return false; 146 | } else if (item.classRoom === undefined) { 147 | alert("請輸入教室"); 148 | return false; 149 | } else if (item.weekDay == "星期") { 150 | alert("請選擇星期"); 151 | return false; 152 | } else if (item.start == "始堂") { 153 | alert("請選擇開始時間"); 154 | return false; 155 | } else if (item.end == "終堂") { 156 | alert("請選擇結束時間"); 157 | return false; 158 | } 159 | let check = courseAdd( 160 | item.className, 161 | item.classRoom, 162 | item.weekDay, 163 | item.start, 164 | item.end, 165 | ); 166 | if (!check) { 167 | conflictMessage(); 168 | return false; 169 | } 170 | } else if (mode == 2) { 171 | recordcourse( 172 | item, 173 | store.state.course.selectedYear, 174 | store.state.course.selectedSemester, 175 | ); 176 | let time = splittime(item.class_time); 177 | let data = []; 178 | let Uuid = uuidv4(); 179 | for (let i = 0; i < time.length; i++) { 180 | data.push( 181 | new Course({ 182 | start_time: time[i][1], 183 | end_time: time[i][2], 184 | week_day: time[i][0], 185 | course_name: item.class_name, 186 | classroom: item.class_room, 187 | is_title: false, 188 | is_course: true, 189 | color: env.VITE_CARD_DEFAULT_COLOR, 190 | Credit: item.credit, 191 | ID: item.id, 192 | is_custom: false, 193 | Teacher: item.teacher, 194 | Memo: null, 195 | textColor: env.VITE_CARDTEXT_DEFAULT_COLOR, 196 | textStyle: env.VITE_CARDTEXT_DEFAULT_STYLE, 197 | uuid: Uuid, 198 | length: 0, 199 | department: item.department, 200 | grade: item.grade, 201 | }), 202 | ); 203 | } 204 | // 成功插入會回傳課程陣列,反之回傳false 205 | // 在做儲存 206 | let check = searchAdd(data); 207 | if (!check) { 208 | conflictMessage(); 209 | return false; 210 | } 211 | store.dispatch( 212 | "addCourseList", 213 | new Course({ 214 | start_time: item.class_time, 215 | end_time: item.class_time, 216 | week_day: item.class_time, 217 | course_name: item.class_name, 218 | classroom: item.class_room, 219 | is_title: false, 220 | is_course: true, 221 | color: env.VITE_CARD_DEFAAULT_COLOR, 222 | Credit: item.credit, 223 | ID: item.id, 224 | is_custom: false, 225 | Teacher: item.teacher, 226 | Memo: null, 227 | textColor: env.VITE_CARDTEXT_DEFAULT_COLOR, 228 | textStyle: env.VITE_CARDTEXT_DEFAULT_STYLE, 229 | uuid: Uuid, 230 | length: 0, 231 | department: item.department, 232 | grade: item.grade, 233 | }), 234 | ); 235 | store.dispatch("addCredit", Number(item.credit)); 236 | } 237 | return true; 238 | } 239 | -------------------------------------------------------------------------------- /src/functions/course_color.ts: -------------------------------------------------------------------------------- 1 | import { Course } from "./general.ts"; 2 | import store from "../store"; 3 | import _ from "lodash"; 4 | 5 | export function courseChangeColor(course: Course, color: string) { 6 | let curriculum = store.state.course.activeIndex; 7 | let temp = _.cloneDeep( 8 | store.state.course.TotalCourseData[curriculum].classStorage, 9 | ); 10 | for (let i = 0; i < temp.length; i++) { 11 | for (let j = 0; j < temp[i].length; j++) { 12 | if (course.getUuid() == temp[i][j].getUuid()) { 13 | temp[i][j].setColor(color); 14 | } 15 | } 16 | } 17 | store.dispatch("addCourse", temp); 18 | } 19 | 20 | export function courseTextChangeColor(course: Course, color: string) { 21 | let curriculum = store.state.course.activeIndex; 22 | let temp = _.cloneDeep( 23 | store.state.course.TotalCourseData[curriculum].classStorage, 24 | ); 25 | for (let i = 0; i < temp.length; i++) { 26 | for (let j = 0; j < temp[i].length; j++) { 27 | if (course.getUuid() == temp[i][j].getUuid()) { 28 | temp[i][j].setTextColor(color); 29 | } 30 | } 31 | } 32 | store.dispatch("addCourse", temp); 33 | } 34 | -------------------------------------------------------------------------------- /src/functions/course_delete.ts: -------------------------------------------------------------------------------- 1 | import { Course } from "./general"; 2 | import store from "../store"; 3 | import { rowspanize } from "./rowspanizer"; 4 | 5 | export async function courseDelete(item: Course) { 6 | // 比對 uuid 其他不管 7 | let TotalCourseData = store.state.course.TotalCourseData; 8 | let data = 9 | TotalCourseData[store.state.course.activeIndex].classStorage; 10 | let table: Course[][] = []; 11 | for (let i = 0; i < data.length; i++) { 12 | let row: Course[] = []; 13 | for (let j = 0; j < data[i].length; j++) { 14 | if (data[i][j].courseData.uuid == item.getUuid()) { 15 | row.push( 16 | new Course({ 17 | course_name: "", 18 | start_time: "", 19 | classroom: "", 20 | is_title: false, 21 | is_course: false, 22 | color: "", 23 | ID: null, 24 | Credit: null, 25 | is_custom: null, 26 | Teacher: null, 27 | Memo: null, 28 | textColor: "", 29 | textStyle: "", 30 | uuid: "", 31 | length: 0, 32 | department: "", 33 | grade: "", 34 | }), 35 | ); 36 | } else { 37 | row.push( 38 | new Course({ 39 | course_name: data[i][j].courseData.course_name, 40 | start_time: data[i][j].courseData.start_time, 41 | classroom: data[i][j].courseData.classroom, 42 | is_title: data[i][j].courseData.is_title, 43 | is_course: data[i][j].courseData.is_course, 44 | color: data[i][j].courseData.color, 45 | ID: data[i][j].courseData.ID, 46 | Credit: data[i][j].courseData.Credit, 47 | is_custom: data[i][j].courseData.is_custom, 48 | Teacher: data[i][j].courseData.Teacher, 49 | Memo: data[i][j].courseData.Memo, 50 | textColor: data[i][j].courseData.textColor, 51 | textStyle: data[i][j].courseData.textStyle, 52 | uuid: data[i][j].courseData.uuid, 53 | length: 0, 54 | department: data[i][j].courseData.department, 55 | grade: data[i][j].courseData.grade, 56 | }), 57 | ); 58 | } 59 | } 60 | table.push(row); 61 | } 62 | // 不要在這邊儲存localstorage,使用store 63 | await store.dispatch("deleteCourseList", item); 64 | table = rowspanize(table); 65 | store.dispatch("addCourse", table); 66 | return true; 67 | } 68 | 69 | export function decreaseCredit(credit: number) { 70 | store.dispatch("addCredit", -credit); 71 | } 72 | -------------------------------------------------------------------------------- /src/functions/course_search.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | //require dotenv 4 | const env = import.meta.env; 5 | 6 | const apiSite = `${env.VITE_BACKEND_DEVICE}/`; 7 | 8 | export async function searchSemster() { 9 | const apiUrl = apiSite + "semester/all"; 10 | 11 | return new Promise((resolve, reject) => { 12 | axios 13 | .get(apiUrl, {}) 14 | .then((response) => { 15 | resolve(response.data); 16 | }) 17 | .catch((error) => { 18 | // 在這裡處理錯誤 19 | console.error(error); 20 | reject(error); 21 | }); 22 | }); 23 | } 24 | 25 | export async function searchCourse( 26 | Input: string, 27 | year: number, 28 | semester: number, 29 | ) { 30 | const apiUrl = apiSite + "searchCourse"; 31 | const keyword = Input.trim(); 32 | // const delay = (n:number) => new Promise( r => setTimeout(r, n*1000)); 33 | //await delay(1); 34 | 35 | return new Promise((resolve, reject) => { 36 | // const startTime = performance.now(); 37 | axios 38 | .get(apiUrl, { 39 | params: { 40 | keyword: keyword, 41 | year: year, 42 | semester: semester, 43 | }, 44 | }) 45 | .then((response) => { 46 | // 在這裡處理回應資料 47 | // console.log(response.data) 48 | // const endTime = performance.now(); 49 | // console.log('查詢課程請求到回應時間:', endTime - startTime, '毫秒'); 50 | // 計算http request 時間 51 | // console.log('查詢課程請求到回應時間:', endTime - startTime, '毫秒'); 52 | resolve(response.data); 53 | }) 54 | .catch((error) => { 55 | // 在這裡處理錯誤 56 | console.error(error); 57 | reject(error); 58 | }); 59 | }); 60 | } 61 | 62 | export async function recordcourse( 63 | course: object, 64 | year: number, 65 | semester: number, 66 | ) { 67 | const apiUrl = apiSite + "record/userSelectClass"; 68 | //console.log(apiUrl) 69 | 70 | return new Promise((resolve, reject) => { 71 | axios 72 | .get(apiUrl, { 73 | params: { 74 | keyword: course, 75 | year: year, 76 | semester: semester, 77 | }, 78 | }) 79 | .then((response) => { 80 | // 在這裡處理回應資料 81 | resolve(response.data); 82 | }) 83 | .catch((error) => { 84 | // 在這裡處理錯誤 85 | console.error(error); 86 | reject(error); 87 | }); 88 | }); 89 | } 90 | 91 | export async function searchByTeacher( 92 | Input: string, 93 | year: number, 94 | semester: number, 95 | ) { 96 | const apiUrl = apiSite + "searchCourse/ByTeacher"; 97 | const keyword = Input.trim(); 98 | // const delay = (n:number) => new Promise( r => setTimeout(r, n*1000)); 99 | //await delay(1); 100 | 101 | return new Promise((resolve, reject) => { 102 | const startTime = performance.now(); 103 | axios 104 | .get(apiUrl, { 105 | params: { 106 | Teacher: keyword, 107 | year: year, 108 | semester: semester, 109 | }, 110 | }) 111 | .then((response) => { 112 | // 在這裡處理回應資料 113 | // console.log(response.data) 114 | const endTime = performance.now(); 115 | console.log( 116 | "查詢課程請求到回應時間:", 117 | endTime - startTime, 118 | "毫秒", 119 | ); 120 | // 計算http request 時間 121 | // console.log('查詢課程請求到回應時間:', endTime - startTime, '毫秒'); 122 | resolve(response.data); 123 | }) 124 | .catch((error) => { 125 | // 在這裡處理錯誤 126 | console.error(error); 127 | reject(error); 128 | }); 129 | }); 130 | } 131 | 132 | export async function searchCourseByTime( 133 | day: number, 134 | start: number, 135 | end: number, 136 | year: number, 137 | semester: number, 138 | ) { 139 | const apiUrl = apiSite + "searchCourse/ByTime"; 140 | 141 | return new Promise((resolve, reject) => { 142 | axios 143 | .get(apiUrl, { 144 | params: { 145 | day: day, 146 | start: start, 147 | end: end, 148 | year: year, 149 | semester: semester, 150 | }, 151 | }) 152 | .then((response) => { 153 | // 在這裡處理回應資料 154 | // console.log(response.data) 155 | resolve(response.data); 156 | }) 157 | .catch((error) => { 158 | // 在這裡處理錯誤 159 | console.error(error); 160 | reject(error); 161 | }); 162 | }); 163 | } 164 | 165 | export async function getDepartment(year: number, semester: number) { 166 | const apiUrl = apiSite + "searchCourse/getDepartment"; 167 | 168 | return new Promise((resolve, reject) => { 169 | axios 170 | .get(apiUrl, { 171 | params: { 172 | year: year, 173 | semester: semester, 174 | }, 175 | }) 176 | .then((response) => { 177 | resolve(response.data); 178 | }) 179 | .catch((error) => { 180 | // 在這裡處理錯誤 181 | console.error(error); 182 | reject(error); 183 | }); 184 | }); 185 | } 186 | 187 | export async function getGradeByDepartment( 188 | Department: string, 189 | year: number, 190 | semester: number, 191 | ) { 192 | const apiUrl = apiSite + "searchCourse/GetGardeByDepartment"; 193 | 194 | return new Promise((resolve, reject) => { 195 | axios 196 | .get(apiUrl, { 197 | params: { 198 | Department: Department, 199 | year: year, 200 | semester: semester, 201 | }, 202 | }) 203 | .then((response) => { 204 | resolve(response.data); 205 | }) 206 | .catch((error) => { 207 | // 在這裡處理錯誤 208 | console.error(error); 209 | reject(error); 210 | }); 211 | }); 212 | } 213 | 214 | export async function getCourseByDepartment( 215 | Department: string, 216 | year: number, 217 | semester: number, 218 | ) { 219 | const apiUrl = apiSite + "searchCourse/ByDepartment"; 220 | 221 | return new Promise((resolve, reject) => { 222 | axios 223 | .get(apiUrl, { 224 | params: { 225 | Department: Department, 226 | year: year, 227 | semester: semester, 228 | }, 229 | }) 230 | .then((response) => { 231 | resolve(response.data); 232 | }) 233 | .catch((error) => { 234 | // 在這裡處理錯誤 235 | console.error(error); 236 | reject(error); 237 | }); 238 | }); 239 | } 240 | 241 | export async function searchDepartmentByOther( 242 | id: number, 243 | class_name: string, 244 | teacher: string, 245 | class_room: string, 246 | credit: number, 247 | year: number, 248 | semester: number, 249 | ) { 250 | const apiUrl = apiSite + "searchCourse/searchDepartmentByOther"; 251 | 252 | return new Promise((resolve, reject) => { 253 | axios 254 | .get(apiUrl, { 255 | params: { 256 | id: id, 257 | class_name: class_name, 258 | teacher: teacher, 259 | class_room: class_room, 260 | credit: credit, 261 | year: year, 262 | semester: semester, 263 | }, 264 | }) 265 | .then((response) => { 266 | resolve(response.data); 267 | }) 268 | .catch((error) => { 269 | // 在這裡處理錯誤 270 | console.error(error); 271 | reject(error); 272 | }); 273 | }); 274 | } 275 | 276 | export async function searchGradeByOther( 277 | id: number, 278 | class_name: string, 279 | teacher: string, 280 | class_room: string, 281 | credit: number, 282 | year: number, 283 | semester: number, 284 | ) { 285 | const apiUrl = apiSite + "searchCourse/searchGradeByOther"; 286 | 287 | return new Promise((resolve, reject) => { 288 | axios 289 | .get(apiUrl, { 290 | params: { 291 | id: id, 292 | class_name: class_name, 293 | teacher: teacher, 294 | class_room: class_room, 295 | credit: credit, 296 | year: year, 297 | semester: semester, 298 | }, 299 | }) 300 | .then((response) => { 301 | resolve(response.data); 302 | }) 303 | .catch((error) => { 304 | // 在這裡處理錯誤 305 | console.error(error); 306 | reject(error); 307 | }); 308 | }); 309 | } 310 | -------------------------------------------------------------------------------- /src/functions/general.ts: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | const env = import.meta.env; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | // using a key-value pair to map courseID to time 6 | export const courseToTime: { [key: string]: string } = { 7 | "1": "07:10", 8 | "2": "08:10", 9 | "3": "09:10", 10 | "4": "10:10", 11 | "5": "11:10", 12 | "6": "12:10", 13 | "7": "13:10", 14 | "8": "14:10", 15 | "9": "15:10", 16 | "10": "16:10", 17 | "11": "17:10", 18 | "12": "18:10", 19 | "13": "19:10", 20 | "14": "20:10", 21 | "15": "21:10", 22 | A: "07:15", 23 | B: "08:45", 24 | C: "10:15", 25 | D: "11:45", 26 | E: "13:15", 27 | F: "14:45", 28 | G: "16:15", 29 | H: "17:45", 30 | I: "19:15", 31 | J: "20:45", 32 | }; 33 | 34 | export const courseToStartIndex: { [key: string]: number } = { 35 | "1": 0, 36 | "2": 2, 37 | "3": 4, 38 | "4": 6, 39 | "5": 8, 40 | "6": 10, 41 | "7": 12, 42 | "8": 14, 43 | "9": 16, 44 | "10": 18, 45 | "11": 20, 46 | "12": 22, 47 | "13": 24, 48 | "14": 26, 49 | "15": 28, 50 | A: 0, 51 | B: 3, 52 | C: 6, 53 | D: 9, 54 | E: 12, 55 | F: 15, 56 | G: 18, 57 | H: 21, 58 | I: 24, 59 | J: 27, 60 | }; 61 | export const courseToEndIndex: { [key: string]: number } = { 62 | "1": 2, 63 | "2": 4, 64 | "3": 6, 65 | "4": 8, 66 | "5": 10, 67 | "6": 12, 68 | "7": 14, 69 | "8": 16, 70 | "9": 18, 71 | "10": 20, 72 | "11": 22, 73 | "12": 24, 74 | "13": 26, 75 | "14": 28, 76 | "15": 30, 77 | A: 3, 78 | B: 6, 79 | C: 9, 80 | D: 12, 81 | E: 15, 82 | F: 18, 83 | G: 21, 84 | H: 24, 85 | I: 27, 86 | J: 30, 87 | }; 88 | export const WeekDayToInt: { [key: string]: number } = { 89 | 一: 3, 90 | 二: 4, 91 | 三: 5, 92 | 四: 6, 93 | 五: 7, 94 | 六: 8, 95 | }; 96 | 97 | interface CourseData { 98 | start_time: string; 99 | end_time?: string; 100 | week_day?: string; 101 | course_name: string; 102 | classroom: string; 103 | is_title: boolean; 104 | is_course: boolean; 105 | color: string; 106 | ID: string | null; 107 | Credit: number | null; 108 | is_custom: boolean | null; 109 | Teacher: string | null; 110 | Memo: string | null; 111 | textColor: string; 112 | textStyle: string; 113 | uuid: string; 114 | length: number; 115 | department: string; 116 | grade: string; 117 | } 118 | 119 | export class Course { 120 | private courseData!: CourseData; 121 | constructor(); 122 | constructor(courseData: CourseData); 123 | constructor(uuid: string); 124 | constructor(uuid: string, start_time: string); 125 | constructor(courseData?: CourseData | string, start_time?: string) { 126 | if (!courseData) { 127 | this.courseData = { 128 | course_name: "", 129 | start_time: "", 130 | classroom: "", 131 | is_title: false, 132 | is_course: false, 133 | color: "", 134 | ID: null, 135 | Credit: null, 136 | is_custom: null, 137 | Teacher: null, 138 | Memo: null, 139 | textColor: "", 140 | textStyle: "", 141 | uuid: "", 142 | length: 0, 143 | department: "", 144 | grade: "", 145 | }; 146 | } else if (typeof courseData === "string") { 147 | this.courseData = { 148 | course_name: "", 149 | start_time: "", 150 | classroom: "", 151 | is_title: false, 152 | is_course: false, 153 | color: "", 154 | ID: null, 155 | Credit: null, 156 | is_custom: null, 157 | Teacher: null, 158 | Memo: null, 159 | textColor: "", 160 | textStyle: "", 161 | uuid: courseData, 162 | length: 0, 163 | department: "", 164 | grade: "", 165 | }; 166 | if (start_time) this.courseData.start_time = start_time; 167 | } else if (typeof courseData === "object") { 168 | this.courseData = courseData as CourseData; 169 | } 170 | } 171 | public inputValue(courseData: CourseData): void { 172 | this.courseData = courseData; 173 | } 174 | public getStartTime(): string { 175 | return this.courseData.start_time; 176 | } 177 | public setStartTime(time: string): void { 178 | // console.log(time) 179 | this.courseData.start_time = time; 180 | } 181 | public getEndTime(): string { 182 | return this.courseData.end_time!; 183 | } 184 | public getWeekDay(): string { 185 | return this.courseData.week_day!; 186 | } 187 | public getCourseName(): string { 188 | return this.courseData.course_name; 189 | } 190 | public setCourseName(name: string): void { 191 | this.courseData.course_name = name; 192 | } 193 | public getClassroom(): string { 194 | return this.courseData.classroom; 195 | } 196 | public getIsTitle(): boolean { 197 | return this.courseData.is_title; 198 | } 199 | public setTitle(state: boolean): void { 200 | this.courseData.is_title = state; 201 | } 202 | public getIsCourse(): boolean { 203 | return this.courseData.is_course; 204 | } 205 | public getColor(): string { 206 | return this.courseData.color; 207 | } 208 | public setColor(color: string): void { 209 | this.courseData.color = color; 210 | } 211 | public getId(): string | null { 212 | return this.courseData.ID; 213 | } 214 | public getCredit(): number | null { 215 | return this.courseData.Credit; 216 | } 217 | public getIsCustom(): boolean | null { 218 | return this.courseData.is_custom; 219 | } 220 | public getTeacher(): string | null { 221 | return this.courseData.Teacher; 222 | } 223 | public getMemo(): string | null { 224 | return this.courseData.Memo; 225 | } 226 | public getTextColor(): string { 227 | return this.courseData.textColor; 228 | } 229 | public setTextColor(color: string): void { 230 | this.courseData.textColor = color; 231 | } 232 | public getTextStyle(): string { 233 | return this.courseData.textStyle; 234 | } 235 | public setTextStyle(style: string): void { 236 | this.courseData.textStyle = style; 237 | } 238 | public getUuid(): string { 239 | return this.courseData.uuid; 240 | } 241 | public setUuid(uuid: string): void { 242 | this.courseData.uuid = uuid; 243 | } 244 | public getLength(): number { 245 | return this.courseData.length; 246 | } 247 | public setLength(length: number): void { 248 | this.courseData.length = length; 249 | } 250 | public getDepartment(): string { 251 | return this.courseData.department; 252 | } 253 | public getGrade(): string { 254 | return this.courseData.grade; 255 | } 256 | public getCourseID(): string { 257 | return this.courseData.ID!; 258 | } 259 | } 260 | 261 | export function InitTable() { 262 | // retrurn InitTable 263 | // use uuid to decide length 264 | let data_table: Course[][] = []; 265 | var data = [ 266 | ["", "1", "A", "", "", "", "", "", ""], 267 | ["", "1", "A", "", "", "", "", "", ""], 268 | ["", "2", "A", "", "", "", "", "", ""], 269 | ["", "2", "B", "", "", "", "", "", ""], 270 | ["", "3", "B", "", "", "", "", "", ""], 271 | ["", "3", "B", "", "", "", "", "", ""], 272 | ["", "4", "C", "", "", "", "", "", ""], 273 | ["", "4", "C", "", "", "", "", "", ""], 274 | ["", "5", "C", "", "", "", "", "", ""], 275 | ["", "5", "D", "", "", "", "", "", ""], 276 | ["", "6", "D", "", "", "", "", "", ""], 277 | ["", "6", "D", "", "", "", "", "", ""], 278 | ["", "7", "E", "", "", "", "", "", ""], 279 | ["", "7", "E", "", "", "", "", "", ""], 280 | ["", "8", "E", "", "", "", "", "", ""], 281 | ["", "8", "F", "", "", "", "", "", ""], 282 | ["", "9", "F", "", "", "", "", "", ""], 283 | ["", "9", "F", "", "", "", "", "", ""], 284 | ["", "10", "G", "", "", "", "", "", ""], 285 | ["", "10", "G", "", "", "", "", "", ""], 286 | ["", "11", "G", "", "", "", "", "", ""], 287 | ["", "11", "H", "", "", "", "", "", ""], 288 | ["", "12", "H", "", "", "", "", "", ""], 289 | ["", "12", "H", "", "", "", "", "", ""], 290 | ["", "13", "I", "", "", "", "", "", ""], 291 | ["", "13", "I", "", "", "", "", "", ""], 292 | ["", "14", "I", "", "", "", "", "", ""], 293 | ["", "14", "J", "", "", "", "", "", ""], 294 | ["", "15", "J", "", "", "", "", "", ""], 295 | ["", "15", "J", "", "", "", "", "", ""], 296 | ]; 297 | for (let i = 0; i < data.length; i++) { 298 | let row: Course[] = []; 299 | for (let j = 0; j < data[i].length; j++) { 300 | if (j == 0) { 301 | row.push(new Course(uuidv4(), data[i][j])); 302 | } else if (j == 1) { 303 | let course: Course = new Course(uuidv4(), data[i][j]); 304 | course.setColor(env.VITE_TITLE_DEFAULT_COLOR); 305 | course.setCourseName(courseToTime[data[i][j]]); 306 | course.setTitle(true); 307 | if (i % 2 == 0) { 308 | row.push(course); 309 | } else { 310 | course.setUuid(data_table[i - 1][j].getUuid()); 311 | row.push(course); 312 | } 313 | } else if (j == 2) { 314 | let course: Course = new Course(uuidv4(), data[i][j]); 315 | course.setColor(env.VITE_TITLE_DEFAULT_COLOR); 316 | course.setCourseName(courseToTime[data[i][j]]); 317 | course.setTitle(true); 318 | if (i % 3 == 0) { 319 | row.push(course); 320 | } else { 321 | course.setUuid(data_table[i - 1][j].getUuid()); 322 | row.push(course); 323 | } 324 | } else { 325 | row.push(new Course()); 326 | } 327 | } 328 | data_table.push(row); 329 | } 330 | // localStorage.setItem("courseTable", JSON.stringify(data_table)); 331 | return data_table; 332 | } 333 | 334 | export function GetCourseTable() { 335 | return store.state.course.classStorage; 336 | } 337 | 338 | export function getArrayShape(array: any) { 339 | if (!Array.isArray(array)) { 340 | return []; // 不是陣列,返回空數組 341 | } 342 | 343 | const shape = []; 344 | 345 | let currentLevel = array; 346 | while (Array.isArray(currentLevel)) { 347 | shape.push(currentLevel.length); 348 | currentLevel = currentLevel[0]; 349 | } 350 | 351 | return shape; 352 | } 353 | -------------------------------------------------------------------------------- /src/functions/image_render.ts: -------------------------------------------------------------------------------- 1 | import html2canvas from "html2canvas"; 2 | 3 | // render a specific html element to an image 4 | export default function renderImage(element: string) { 5 | let htmlElement: any = document.getElementById(element); 6 | html2canvas(htmlElement).then(function (canvas) { 7 | var temp = document.createElement("a"); 8 | temp.href = canvas 9 | .toDataURL("image/png", 1) 10 | .replace("image/png", "image/octet-stream"); 11 | temp.download = "curriculum.png"; 12 | temp.click(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/functions/rowspanizer.ts: -------------------------------------------------------------------------------- 1 | import { Course } from "./general.ts"; 2 | import _ from "lodash"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | export function rowspanize(inputTable: Course[][]): Course[][] { 6 | let table = _.cloneDeep(inputTable); 7 | let tmpuuid1 = uuidv4(), 8 | tmpuuid2 = uuidv4(); 9 | for (let i = 0; i < table.length; i++) { 10 | for (let j = 0; j < table[i].length; j++) { 11 | table[i][j].setLength(0); 12 | if (j == 0 && table[i][j].getStartTime() != "") { 13 | table[i][j].setStartTime(""); 14 | } 15 | 16 | if (j < 3 && table[i][j].getUuid() == "") { 17 | // 沒更新的處理一下 18 | if (j == 0) { 19 | table[i][j].setUuid(uuidv4()); // 每格都不一樣 20 | } else if (j == 1) { 21 | table[i][j].setUuid(tmpuuid1); 22 | if ((i + 1) % 2 == 0) tmpuuid1 = uuidv4(); 23 | } else if (j == 2) { 24 | table[i][j].setUuid(tmpuuid2); 25 | if ((i + 1) % 3 == 0) tmpuuid2 = uuidv4(); 26 | } 27 | } 28 | } 29 | } 30 | for (let j = 0; j < table[0].length; j++) { 31 | let current: string = table[0][j].getUuid(); 32 | let currentIndex: number = 0; 33 | let len: number = 1; 34 | for (let i = 1; i < table.length; i++) { 35 | if (current === table[i][j].getUuid()) len++; 36 | else if (current != table[i][j].getUuid()) { 37 | table[currentIndex][j].setLength(len); 38 | len = 1; 39 | currentIndex = i; 40 | current = table[i][j].getUuid(); 41 | } 42 | } 43 | table[currentIndex][j].setLength(len); 44 | } 45 | // store.dispatch("addCourse", table); 46 | return table; 47 | } 48 | -------------------------------------------------------------------------------- /src/functions/save_course.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const env = import.meta.env; 4 | const apiSite = `${env.VITE_BACKEND_DEVICE}/`; 5 | 6 | export async function recordsharecourse(data: any) { 7 | const apiUrl = apiSite + "record/saveCourseRecord"; 8 | const json_data = JSON.stringify(data); 9 | return new Promise((resolve, reject) => { 10 | axios 11 | .post(apiUrl, { json_data }) // 直接发送 data 作为请求体 12 | .then((response) => { 13 | // 在這裡處理回應資料 14 | let link = 15 | apiSite + 16 | "record/redirectCourseRecord?record_id=" + 17 | response.data; 18 | resolve(link); 19 | }) 20 | .catch((error) => { 21 | // 在這裡處理錯誤 22 | console.error(error); 23 | reject(error); 24 | }); 25 | }); 26 | } 27 | 28 | export async function getsharecourse(record_id: string) { 29 | const apiUrl = apiSite + "record/getCourseRecord"; 30 | //console.log(apiUrl) 31 | // console.log(data) 32 | return new Promise((resolve, reject) => { 33 | axios 34 | .get(apiUrl, { 35 | params: { 36 | record_id: record_id, 37 | }, 38 | }) 39 | .then((response) => { 40 | // 在這裡處理回應資料 41 | let link = response.data; 42 | // console.log(link) 43 | resolve(link); 44 | }) 45 | .catch((error) => { 46 | // 在這裡處理錯誤 47 | console.error(error); 48 | reject(error); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/functions/token.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const env = import.meta.env; 3 | const apiSite = `${env.VITE_BACKEND_DEVICE}/`; 4 | 5 | const Token = { 6 | saveToken(token: string) { 7 | localStorage.setItem("token", token); 8 | }, 9 | loadToken() { 10 | return localStorage.getItem("token"); 11 | }, 12 | async varifyToken(token: string | null) { 13 | if (token === null) return false; 14 | const apiUrl = apiSite + "verifyToken"; 15 | return new Promise((resolve, reject) => { 16 | axios 17 | .post(apiUrl, { token }) 18 | .then((response) => { 19 | resolve(response.data.status); 20 | }) 21 | .catch((error) => { 22 | console.error(error); 23 | reject(false); 24 | }); 25 | }); 26 | }, 27 | }; 28 | 29 | export default Token; 30 | -------------------------------------------------------------------------------- /src/functions/tool.ts: -------------------------------------------------------------------------------- 1 | export function splittime(time: string) { 2 | // console.log(time) 3 | // 回傳值為二維陣列,為[][],內部陣列為[星期, 開始節次, 結束節次] 4 | let store = time.split(" "); 5 | store.splice(0, 1); 6 | let arr: [string, string, string][] = []; 7 | for (let i = 0; i < store.length; i++) { 8 | let temp = store[i][0]; 9 | // temp2 remove the first char 10 | // 去掉星期幾 只留時間 11 | let temp2 = store[i].slice(1); 12 | let TimeArr = temp2.split(","); 13 | // 把每個時段切開 14 | for (let j = 0; j < TimeArr.length; j++) { 15 | let single_data: [string, string, string] = [ 16 | temp, 17 | TimeArr[j], 18 | TimeArr[j], 19 | ]; 20 | arr.push(single_data); 21 | } 22 | } 23 | return arr; 24 | } 25 | -------------------------------------------------------------------------------- /src/functions/web_statistic.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | //require dotenv 4 | const env = import.meta.env; 5 | const apiSite = `${env.VITE_BACKEND_DEVICE}/`; 6 | export async function visitWeb(web_name: string | null) { 7 | const apiUrl = apiSite + "record/visistWebsite"; 8 | console.log(web_name); 9 | return new Promise((resolve, reject) => { 10 | axios 11 | .get(apiUrl, { 12 | params: { 13 | web_name: web_name, 14 | }, 15 | }) 16 | .then((response) => { 17 | resolve(response.data); 18 | }) 19 | .catch((error) => { 20 | console.error(error); 21 | reject(error); 22 | }); 23 | }); 24 | } 25 | export async function getVisitCount(web_name: string | null) { 26 | const apiUrl = apiSite + "statistic/getVisitCount"; 27 | return new Promise((resolve, reject) => { 28 | axios 29 | .get(apiUrl, { 30 | params: { 31 | web_name: web_name, 32 | }, 33 | }) 34 | .then((response) => { 35 | resolve(Number(response.data.value)); 36 | }) 37 | .catch((error) => { 38 | console.error(error); 39 | reject(error); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import Vue3DraggableResizable from "vue3-draggable-resizable"; 4 | import router from "./router"; 5 | import "./css/tailwind.css"; 6 | import * as Icons from "@ant-design/icons-vue"; 7 | import "vue3-draggable-resizable/dist/Vue3DraggableResizable.css"; 8 | import VueCarousel from "@chenfengyuan/vue-carousel"; 9 | import store from "./store"; 10 | import VueDragSelect from "@coleqiu/vue-drag-select"; 11 | import VueLazyload from "vue-lazyload"; 12 | 13 | const app = createApp(App); 14 | 15 | const icons: any = Icons; 16 | 17 | for (const i in icons) { 18 | app.component(i, icons[i]); 19 | } 20 | 21 | app.component(VueCarousel.name ?? "", VueCarousel); 22 | app.use(store); 23 | app.use(router); 24 | app.use(VueDragSelect); 25 | app.use(Vue3DraggableResizable); 26 | app.use(VueLazyload); 27 | app.mount("#app"); 28 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import Token from "../functions/token.ts"; 3 | 4 | const routes = [ 5 | { 6 | //error 404 7 | path: "/:pathMatch(.*)*", 8 | name: "NotFound", 9 | component: () => import("../views/page_error.vue"), 10 | }, 11 | { 12 | path: "/", 13 | name: "Home", 14 | component: () => import("../views/page_home.vue"), 15 | }, 16 | { 17 | path: "/main", 18 | name: "Main", 19 | component: () => import("../views/page_main.vue"), 20 | }, 21 | { 22 | path: "/login", 23 | name: "Login", 24 | component: () => import("../views/page_login.vue"), 25 | }, 26 | { 27 | path: "/admin", 28 | name: "Admin", 29 | component: () => import("../views/page_admin.vue"), 30 | meta: { requireAuth: true }, 31 | }, 32 | { 33 | path: "/tutorial", 34 | name: "Tutorial", 35 | component: () => import("../views/page_tutorial.vue"), 36 | }, 37 | { 38 | path: "/record/error", 39 | name: "RecordError", 40 | component: () => import("../views/page_record_error.vue"), 41 | }, 42 | ]; 43 | 44 | const router = createRouter({ 45 | history: createWebHashHistory(), 46 | routes, 47 | }); 48 | 49 | router.beforeEach(async (to, from, next) => { 50 | console.log( 51 | `to=${to.fullPath} from=${from.fullPath}, to.meta.requireAuth=${to.meta.requireAuth}`, 52 | ); 53 | if (to.meta.requireAuth) { 54 | try { 55 | const token: string | null = Token.loadToken(); 56 | const result = Boolean(await Token.varifyToken(token)); 57 | //console.log(`result=${result}`); 58 | if (result) next(); 59 | else { 60 | next(from.fullPath); 61 | alert("您的權限不足"); 62 | } 63 | } catch (err) { 64 | next(from.fullPath); 65 | } 66 | } else next(); 67 | }); 68 | 69 | export default router; 70 | -------------------------------------------------------------------------------- /src/store/ccuplus.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | 3 | interface State { 4 | course_id: string; 5 | } 6 | 7 | const ccuplus: Module = { 8 | state: { 9 | course_id: "", 10 | }, 11 | mutations: { 12 | pass_course_id(state: State, payload: string) { 13 | state.course_id = payload; 14 | }, 15 | purge(state: State) { 16 | state.course_id = ""; 17 | }, 18 | }, 19 | actions: { 20 | pass_course_id(context: any, payload: string) { 21 | context.commit("pass_course_id", payload); 22 | }, 23 | purge(context: any) { 24 | context.commit("purge"); 25 | }, 26 | }, 27 | }; 28 | 29 | export default ccuplus; 30 | -------------------------------------------------------------------------------- /src/store/course.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | import { Course, InitTable } from "../functions/general"; 3 | import { rowspanize } from "../functions/rowspanizer"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | import _ from "lodash"; 6 | import { toRaw } from "vue"; 7 | const env = import.meta.env; 8 | 9 | interface State { 10 | show: boolean; 11 | TotalCourseData: Array; 12 | show_ColorPick: boolean; 13 | chooseCard: Course | null; 14 | cardMode: number; 15 | defaultColor: string; 16 | showTable: boolean; 17 | show_credit: boolean; 18 | searchTime_status: boolean; 19 | timeSearchMode: boolean; 20 | timeSearchArgument: Array; 21 | runConflict: number; 22 | selectedYear: number; 23 | selectedSemester: number; 24 | activeIndex: number; 25 | } 26 | 27 | interface CourseData { 28 | name: string; 29 | id: string; 30 | classStorage: Array>; 31 | classListStorage: Array; 32 | credit: number; 33 | } 34 | 35 | function Transfer(data: any) { 36 | let temp: Course[][] = []; 37 | for (let i = 0; i < data.length; i++) { 38 | let t: Course[] = []; 39 | for (let j = 0; j < data[i].length; j++) { 40 | t.push(new Course(data[i][j]["courseData"])); 41 | } 42 | temp.push(t); 43 | } 44 | return temp; 45 | } 46 | 47 | function Transfer_class_list(data: any) { 48 | let temp: Course[] = []; 49 | for (let i = 0; i < data.length; i++) { 50 | temp.push(new Course(data[i]["courseData"])); 51 | } 52 | return temp; 53 | } 54 | 55 | // 僅回傳值 56 | function OldDataTransfer(): Array { 57 | console.log("OldDataTransfer"); 58 | let courseTable: string | null = 59 | localStorage.getItem("courseTable"); 60 | let courseList: string | null = localStorage.getItem("courseList"); 61 | let creditL: string | null = localStorage.getItem("credit"); 62 | if (courseTable != null || courseList != null || creditL != null) { 63 | console.log("not null"); 64 | localStorage.removeItem("courseTable"); 65 | localStorage.removeItem("courseList"); 66 | localStorage.removeItem("credit"); 67 | let TotalCourseData: Array = []; 68 | TotalCourseData.push({ 69 | name: "課表", 70 | id: uuidv4(), 71 | classStorage: rowspanize(Transfer(JSON.parse(courseTable!))), 72 | classListStorage: Transfer_class_list(JSON.parse(courseList!)), 73 | credit: Number(creditL), 74 | }); 75 | return TotalCourseData; 76 | } 77 | return [ 78 | { 79 | name: "課表", 80 | id: uuidv4(), 81 | classStorage: rowspanize(InitTable()), 82 | classListStorage: [], 83 | credit: 0, 84 | }, 85 | ]; 86 | } 87 | 88 | const store: Module = { 89 | state: { 90 | show: false, 91 | TotalCourseData: [], 92 | show_ColorPick: false, 93 | chooseCard: null, 94 | cardMode: 0, 95 | defaultColor: env.VITE_CARD_DEFAULT_COLOR, 96 | showTable: true, 97 | show_credit: false, 98 | searchTime_status: false, 99 | timeSearchMode: false, 100 | timeSearchArgument: [0, 0, 0], 101 | runConflict: 0, 102 | selectedYear: 113, 103 | selectedSemester: 1, 104 | activeIndex: 0, 105 | }, 106 | mutations: { 107 | set_yearNsemester(state: State, data: Array) { 108 | state.selectedYear = data[0]; 109 | state.selectedSemester = data[1]; 110 | }, 111 | display(state: State) { 112 | state.show = true; 113 | }, 114 | hidden(state: State) { 115 | state.show = false; 116 | }, 117 | show_credit(state: State) { 118 | state.show_credit = true; 119 | }, 120 | hidden_credit(state: State) { 121 | state.show_credit = false; 122 | }, 123 | initAll(state: State) { 124 | console.log("initAll"); 125 | let TotalCourseData = localStorage.getItem("TotalCourseData"); 126 | // 無資料或者是僅有舊資料 127 | // console.log(TotalCourseData); 128 | if (TotalCourseData === null) { 129 | console.log("initAll debug"); 130 | state.TotalCourseData = OldDataTransfer(); 131 | console.log(state.TotalCourseData); 132 | localStorage.setItem( 133 | "TotalCourseData", 134 | JSON.stringify(state.TotalCourseData), 135 | ); 136 | return; 137 | } 138 | state.TotalCourseData = JSON.parse(TotalCourseData); 139 | for (let i = 0; i < state.TotalCourseData.length; i++) { 140 | state.TotalCourseData[i].classStorage = rowspanize( 141 | Transfer(state.TotalCourseData[i].classStorage), 142 | ); 143 | state.TotalCourseData[i].classListStorage = 144 | Transfer_class_list( 145 | state.TotalCourseData[i].classListStorage, 146 | ); 147 | } 148 | }, 149 | // initCourseFromLocalstorage(state: State) { 150 | // let courseTable: string | null = 151 | // localStorage.getItem("courseTable"); 152 | // if (courseTable == null || courseTable == "") { 153 | // state.classStorage = InitTable(); 154 | // rowspanize(state.classStorage); 155 | // localStorage.setItem( 156 | // "courseTable", 157 | // JSON.stringify(state.classStorage), 158 | // ); 159 | // return; 160 | // } 161 | // let data = JSON.parse(courseTable!); 162 | // state.classStorage = Transfer(data); 163 | // rowspanize(state.classStorage); 164 | // }, 165 | addCourse(state: State, data: Array>) { 166 | // console.log(state.classStorage); 167 | state.TotalCourseData[state.activeIndex].classStorage = data; 168 | // console.log(state.classStorage); 169 | localStorage.setItem( 170 | "TotalCourseData", 171 | JSON.stringify(state.TotalCourseData), 172 | ); 173 | }, 174 | clearCourse(state: State) { 175 | // console.log("clear") 176 | state.TotalCourseData[state.activeIndex].classStorage = 177 | rowspanize(InitTable()); 178 | state.TotalCourseData[state.activeIndex].classListStorage = []; 179 | state.TotalCourseData[state.activeIndex].credit = 0; 180 | state.chooseCard = null; 181 | localStorage.setItem( 182 | "TotalCourseData", 183 | JSON.stringify(state.TotalCourseData), 184 | ); 185 | // state.classStorage = InitTable(); 186 | // rowspanize(state.classStorage); 187 | // console.log(state.classStorage) 188 | // state.classListStorage = []; 189 | // state.credit = 0; 190 | // state.chooseCard = null; 191 | // localStorage.setItem( 192 | // "courseTable", 193 | // JSON.stringify(state.classStorage), 194 | // ); 195 | // localStorage.setItem( 196 | // "courseList", 197 | // JSON.stringify(state.classListStorage), 198 | // ); 199 | // localStorage.setItem("credit", state.credit.toString()); 200 | }, 201 | // initCourseListFromLocalstorage(state: State) { 202 | // let courseList: string | null = 203 | // localStorage.getItem("courseList"); 204 | // if (courseList == null) { 205 | // state.classListStorage = []; 206 | // localStorage.setItem( 207 | // "courseList", 208 | // JSON.stringify(state.classListStorage), 209 | // ); 210 | // return; 211 | // } 212 | // let data = JSON.parse(courseList!); 213 | // state.classListStorage = Transfer_class_list(data); 214 | // }, 215 | addCourseList(state: State, Class: Course) { 216 | state.TotalCourseData[state.activeIndex].classListStorage.push( 217 | Class, 218 | ); 219 | // state.classListStorage.push(Class); 220 | // localStorage.setItem( 221 | // "courseList", 222 | // JSON.stringify(state.classListStorage), 223 | // ); 224 | localStorage.setItem( 225 | "TotalCourseData", 226 | JSON.stringify(state.TotalCourseData), 227 | ); 228 | }, 229 | deleteCourseList(state: State, Class: Course) { 230 | // let List = state.classListStorage; 231 | let List = 232 | state.TotalCourseData[state.activeIndex].classListStorage; 233 | let temp: Array = []; 234 | for (let i = 0; i < List.length; i++) { 235 | if (List[i].getUuid() == Class.getUuid()) { 236 | continue; 237 | } else { 238 | temp.push(List[i]); 239 | } 240 | } 241 | // state.classListStorage = temp; 242 | state.TotalCourseData[state.activeIndex].classListStorage = 243 | temp; 244 | // localStorage.setItem( 245 | // "courseList", 246 | // JSON.stringify(state.classListStorage), 247 | // ); 248 | localStorage.setItem( 249 | "TotalCourseData", 250 | JSON.stringify(state.TotalCourseData), 251 | ); 252 | }, 253 | // initCredit(state: State) { 254 | // let creditL: string | null = localStorage.getItem("credit"); 255 | // if (creditL == null) { 256 | // state.credit = 0; 257 | // localStorage.setItem("credit", state.credit.toString()); 258 | // return; 259 | // } 260 | // state.credit = Number(creditL); 261 | // }, 262 | addCredit(state: State, delta: number) { 263 | // state.credit += delta; 264 | state.TotalCourseData[state.activeIndex].credit += delta; 265 | // console.log(state.credit, delta) 266 | // localStorage.setItem("credit", state.credit.toString()); 267 | localStorage.setItem( 268 | "TotalCourseData", 269 | JSON.stringify(state.TotalCourseData), 270 | ); 271 | }, 272 | changeShowColorPick(state: State, Bool: boolean) { 273 | state.show_ColorPick = Bool; 274 | }, 275 | setChooseCard(state: State, Card: Course) { 276 | state.chooseCard = Card; 277 | }, 278 | setCardMode(state: State, mode: number) { 279 | state.cardMode = mode; 280 | }, 281 | setDefaultColor(state: State, color: string) { 282 | state.defaultColor = color; 283 | }, 284 | setShowTable(state: State, Bool: boolean) { 285 | state.showTable = Bool; 286 | }, 287 | setSearchTimeTable(state: State, Bool: boolean) { 288 | state.searchTime_status = Bool; 289 | }, 290 | changeTimeSearchMode(state: State) { 291 | state.timeSearchMode = !state.timeSearchMode; 292 | }, 293 | setTimeSearchMode(state: State, Bool: boolean) { 294 | state.timeSearchMode = Bool; 295 | }, 296 | settimeSearchArgument(state: State, arg: Array) { 297 | state.timeSearchArgument = arg; 298 | }, 299 | setrunConflictState(state: State, arg: number) { 300 | state.runConflict = arg; 301 | }, 302 | updateCourseList(state: State, data: Array) { 303 | // state.classListStorage = data; 304 | // localStorage.setItem( 305 | // "courseList", 306 | // JSON.stringify(state.classListStorage), 307 | // ); 308 | state.TotalCourseData[state.activeIndex].classListStorage = 309 | data; 310 | localStorage.setItem( 311 | "TotalCourseData", 312 | JSON.stringify(state.TotalCourseData), 313 | ); 314 | }, 315 | setactiveIndex(state: State, index: number) { 316 | state.activeIndex = index; 317 | }, 318 | addTabs(state: State, id: string | null) { 319 | if (id == null) { 320 | state.TotalCourseData.push({ 321 | name: `課表${state.TotalCourseData.length + 1}`, 322 | id: uuidv4(), 323 | classStorage: rowspanize(InitTable()), 324 | classListStorage: [], 325 | credit: 0, 326 | }); 327 | state.activeIndex = state.TotalCourseData.length - 1; 328 | } else { 329 | console.log(state.TotalCourseData); 330 | let length = state.TotalCourseData.length; 331 | for (let i = 0; i < length; i++) { 332 | if (state.TotalCourseData[i].id == id) { 333 | state.TotalCourseData.push( 334 | _.cloneDeep(toRaw(state.TotalCourseData[i])), 335 | ); 336 | state.activeIndex = state.TotalCourseData.length - 1; 337 | state.TotalCourseData[state.activeIndex].id = uuidv4(); 338 | state.TotalCourseData[state.activeIndex].name = 339 | state.TotalCourseData[state.activeIndex].name + " copy"; 340 | break; 341 | } 342 | } 343 | // console.log(state.TotalCourseData); 344 | } 345 | localStorage.setItem( 346 | "TotalCourseData", 347 | JSON.stringify(state.TotalCourseData), 348 | ); 349 | }, 350 | importTabs(state: State, data: any) { 351 | console.log(data); 352 | state.TotalCourseData.push({ 353 | name: data.name, 354 | id: uuidv4(), 355 | classStorage: rowspanize(Transfer(data.classStorage)), 356 | classListStorage: Transfer_class_list(data.classListStorage), 357 | credit: data.credit, 358 | }); 359 | state.activeIndex = state.TotalCourseData.length - 1; 360 | localStorage.setItem( 361 | "TotalCourseData", 362 | JSON.stringify(state.TotalCourseData), 363 | ); 364 | }, 365 | deleteTabs(state: State, id: string) { 366 | let temp: Array = []; 367 | for (let i = 0; i < state.TotalCourseData.length; i++) { 368 | if (state.TotalCourseData[i].id != id) { 369 | temp.push(state.TotalCourseData[i]); 370 | } 371 | } 372 | state.TotalCourseData = temp; 373 | localStorage.setItem( 374 | "TotalCourseData", 375 | JSON.stringify(state.TotalCourseData), 376 | ); 377 | state.activeIndex = 0; 378 | }, 379 | renameTabs(state: State, data: any) { 380 | let id = data.id; 381 | let name = data.name; 382 | for (let i = 0; i < state.TotalCourseData.length; i++) { 383 | if (state.TotalCourseData[i].id == id) { 384 | state.TotalCourseData[i].name = name; 385 | } 386 | } 387 | localStorage.setItem( 388 | "TotalCourseData", 389 | JSON.stringify(state.TotalCourseData), 390 | ); 391 | }, 392 | }, 393 | actions: { 394 | set_yearNsemester(context: any, data: Array) { 395 | context.commit("set_yearNsemester", data); 396 | }, 397 | display(context: any) { 398 | context.commit("display"); 399 | }, 400 | hidden(context: any) { 401 | context.commit("hidden"); 402 | }, 403 | show_credit(context: any) { 404 | context.commit("show_credit"); 405 | }, 406 | hidden_credit(context: any) { 407 | context.commit("hidden_credit"); 408 | }, 409 | initCourseFromLocalstorage(context: any) { 410 | context.commit("initCourseFromLocalstorage"); 411 | }, 412 | addCourse(context: any, data: any) { 413 | context.commit("addCourse", data); 414 | }, 415 | clearCourse(context: any) { 416 | context.commit("clearCourse"); 417 | }, 418 | initCourseListFromLocalstorage(context: any) { 419 | context.commit("initCourseListFromLocalstorage"); 420 | }, 421 | addCourseList(context: any, Class: Array) { 422 | context.commit("addCourseList", Class); 423 | }, 424 | initCredit(context: any) { 425 | context.commit("initCredit"); 426 | }, 427 | initAll(context: any) { 428 | context.commit("initAll"); 429 | // context.commit("initCredit"); 430 | // context.commit("initCourseListFromLocalstorage"); 431 | // context.commit("initCourseFromLocalstorage"); 432 | }, 433 | addCredit(context: any, delta: number) { 434 | context.commit("addCredit", delta); 435 | }, 436 | deleteCourseList(context: any, Class: Course) { 437 | context.commit("deleteCourseList", Class); 438 | }, 439 | changeShowColorPick(context: any, Bool: boolean) { 440 | context.commit("changeShowColorPick", Bool); 441 | }, 442 | setChooseCard(context: any, Card: Course) { 443 | context.commit("setChooseCard", Card); 444 | }, 445 | setCardMode(context: any, mode: number) { 446 | context.commit("setCardMode", mode); 447 | }, 448 | setDefaultColor(context: any, color: string) { 449 | // console.log(color) 450 | context.commit("setDefaultColor", color); 451 | }, 452 | setShowTable(context: any, Bool: boolean) { 453 | // console.log("change") 454 | context.commit("setShowTable", Bool); 455 | }, 456 | setSearchTimeTable(context: any, Bool: boolean) { 457 | context.commit("setSearchTimeTable", Bool); 458 | }, 459 | changeTimeSearchMode(context: any) { 460 | context.commit("changeTimeSearchMode"); 461 | context.commit("hidden"); 462 | context.commit("purge"); 463 | }, 464 | setTimeSearchMode(context: any, Bool: boolean) { 465 | context.commit("setTimeSearchMode", Bool); 466 | context.commit("hidden"); 467 | context.commit("purge"); 468 | }, 469 | settimeSearchArgument(context: any, arg: Array) { 470 | context.commit("settimeSearchArgument", arg); 471 | }, 472 | setrunConflictState(context: any, arg: number) { 473 | context.commit("setrunConflictState", arg); 474 | }, 475 | updateCourseList(context: any, data: any) { 476 | context.commit("updateCourseList", data); 477 | }, 478 | setactiveIndex(context: any, index: number) { 479 | context.commit("setactiveIndex", index); 480 | }, 481 | addTabs(context: any, id: string | null) { 482 | context.commit("addTabs", id); 483 | }, 484 | importTabs(context: any, data: any) { 485 | context.commit("importTabs", data); 486 | }, 487 | deleteTabs(context: any, id: string) { 488 | context.commit("deleteTabs", id); 489 | }, 490 | renameTabs(context: any, data: any) { 491 | context.commit("renameTabs", data); 492 | }, 493 | }, 494 | }; 495 | 496 | export default store; 497 | -------------------------------------------------------------------------------- /src/store/general.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | 3 | interface State { 4 | main_modal_show: boolean; 5 | tab_is_editing: Array; 6 | } 7 | 8 | const general: Module = { 9 | state: { 10 | main_modal_show: false, 11 | tab_is_editing: [], 12 | }, 13 | mutations: { 14 | open_main_modal(state: State) { 15 | state.main_modal_show = true; 16 | }, 17 | close_main_modal(state: State) { 18 | state.main_modal_show = false; 19 | }, 20 | init_tab_is_editing(state: State, payload: number) { 21 | state.tab_is_editing = new Array(payload).fill(false); 22 | }, 23 | set_tab_is_editing( 24 | state: State, 25 | payload: { index: number; value: boolean }, 26 | ) { 27 | state.tab_is_editing[payload.index] = payload.value; 28 | }, 29 | }, 30 | actions: { 31 | open_main_modal(context: any) { 32 | context.commit("open_main_modal"); 33 | }, 34 | close_main_modal(context: any) { 35 | context.commit("close_main_modal"); 36 | }, 37 | init_tab_is_editing(context: any, payload: number) { 38 | context.commit("init_tab_is_editing", payload); 39 | }, 40 | }, 41 | }; 42 | 43 | export default general; 44 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import course from "./course"; 3 | import ccuplus from "./ccuplus"; 4 | import general from "./general"; 5 | 6 | const store = createStore({ 7 | modules: { 8 | course: course, 9 | ccuplus: ccuplus, 10 | general: general, 11 | }, 12 | }); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /src/views/page_admin.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /src/views/page_error.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/views/page_home.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /src/views/page_login.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /src/views/page_main.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 81 | -------------------------------------------------------------------------------- /src/views/page_record_error.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | > 19 | -------------------------------------------------------------------------------- /src/views/page_tutorial.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "index.html", 5 | "./src/**/*.{js,ts,jsx,tsx,vue}", 6 | "./src/**/*.vue", 7 | "./src/*.vue" 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "allowJs": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "allowJs": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Components from 'unplugin-vue-components/vite'; 3 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; 4 | import vue from '@vitejs/plugin-vue' 5 | import path from 'path'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), 10 | Components({ 11 | resolvers: [AntDesignVueResolver({ 12 | importStyle: false 13 | })], 14 | }), 15 | 16 | ], 17 | define:{ 18 | "process.env":{} 19 | }, 20 | resolve: { 21 | alias:{ 22 | "@" : path.resolve(__dirname,"./src"), 23 | "@components" : path.resolve(__dirname,"./src/components"), 24 | "@functions" : path.resolve(__dirname,"./src/functions"), 25 | 'vue': 'vue/dist/vue.esm-bundler.js', 26 | } 27 | } 28 | }) 29 | 30 | --------------------------------------------------------------------------------