Compare commits
No commits in common. "master" and "route" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
||||
.DS_Store
|
||||
15
README.md
15
README.md
@ -1,9 +1,7 @@
|
||||
# 暨大排課表
|
||||
只是一個Vue練習題目,[DEMO](https://snsd0805.com/NCNU_Course/),建議用電腦開
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
# 題目
|
||||
暨大生在學期前在苦惱選課的時候都會使用 @x3388638 學長開發的 [自己的課表自己排 2.0](https://github.com/x3388638/KeBiau)
|
||||
@ -28,22 +26,15 @@
|
||||
- 可選擇科系、**可篩選通識領域**
|
||||
- 選課預覽
|
||||
- 可安排假日課程
|
||||
- 分享
|
||||
- 產生專屬連結跟同學分享自己的課表
|
||||
- 匯出jpg
|
||||
- 產生課表.jpg,放成桌布、印出來,永遠不會忘記去上課
|
||||
|
||||
# 可能會新增的功能(非常可能不會)
|
||||
- [x] 儲存
|
||||
- [x] 匯出
|
||||
- [x] 分享
|
||||
- [ ] 匯出
|
||||
- [ ] 分享
|
||||
- [ ] 時間為「另訂」,額外處理
|
||||
- [x] 把版排好(選課框框改成可下拉(才可以同時看到課表))
|
||||
|
||||
# 課程爬蟲使用說明
|
||||
> 因為學校教務系統更新通識分類的部份很慢,因此目前的程式碼已經修改成無法對應「通識課程分類」的版本,
|
||||
> 實際上線的 data 是依靠「工人智慧」,
|
||||
> 如果有需要爬取資料,建議使用較舊版本的 python code
|
||||
|
||||
安裝所需套件
|
||||
```
|
||||
|
||||
20
api.py
20
api.py
@ -5,23 +5,19 @@ import sqlite3
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/.*": {"origins": ["https://course.snsd0805.com"]}})
|
||||
CORS(app, resources={r"/.*": {"origins": ["https://snsd0805.com"]}})
|
||||
|
||||
def facebookAuth(token):
|
||||
url = "https://graph.facebook.com/v9.0/me?access_token={}"
|
||||
|
||||
try:
|
||||
response = requests.get(url.format(token), timeout=5)
|
||||
except:
|
||||
return False, None, None
|
||||
else:
|
||||
data = json.loads(response.text)
|
||||
response = requests.get(url.format(token))
|
||||
data = json.loads(response.text)
|
||||
|
||||
# 若 access code 通過 facebook 驗證
|
||||
if response.status_code == 200:
|
||||
return True, data['id'], data['name']
|
||||
else:
|
||||
return False, None, None
|
||||
# 若 access code 通過 facebook 驗證
|
||||
if response.status_code == 200:
|
||||
return True, data['id'], data['name']
|
||||
else:
|
||||
return False, None, None
|
||||
|
||||
@app.route('/courseTable', methods=["GET"])
|
||||
def get():
|
||||
|
||||
137
generalCourse.in
137
generalCourse.in
@ -1,137 +0,0 @@
|
||||
department 特色通識—在地實踐
|
||||
994017
|
||||
994057
|
||||
994065
|
||||
994068
|
||||
994071
|
||||
994075
|
||||
994076
|
||||
994077
|
||||
994078
|
||||
994080
|
||||
994086
|
||||
994089
|
||||
994112
|
||||
994113
|
||||
994114
|
||||
department 特色通識—綠概念
|
||||
993062
|
||||
994001
|
||||
994012
|
||||
994020
|
||||
994024
|
||||
994074
|
||||
994027
|
||||
department 特色通識—東南亞
|
||||
992106
|
||||
994030
|
||||
994096
|
||||
994098
|
||||
994099
|
||||
994102
|
||||
994103
|
||||
994105
|
||||
994108
|
||||
994109
|
||||
994110
|
||||
994111
|
||||
994010
|
||||
department 自然—生命與科學
|
||||
993001
|
||||
993002
|
||||
993022
|
||||
993054
|
||||
993086
|
||||
993093
|
||||
993106
|
||||
993126
|
||||
993131
|
||||
993132
|
||||
993133
|
||||
993137
|
||||
993145
|
||||
993008
|
||||
department 自然—工程與科技
|
||||
993023
|
||||
993052
|
||||
993055
|
||||
993060
|
||||
993064
|
||||
993075
|
||||
993116
|
||||
993156
|
||||
993157
|
||||
993013
|
||||
993066
|
||||
993111
|
||||
993120
|
||||
993143
|
||||
department 社會—社經與管理
|
||||
991094
|
||||
992033
|
||||
992035
|
||||
992110
|
||||
992120
|
||||
992129
|
||||
992141
|
||||
992143
|
||||
992177
|
||||
992191
|
||||
992193
|
||||
992203
|
||||
992205
|
||||
992213
|
||||
992214
|
||||
992216
|
||||
992217
|
||||
992223
|
||||
992062
|
||||
992211
|
||||
992232
|
||||
department 社會—法政與教育
|
||||
984003
|
||||
992076
|
||||
992108
|
||||
992112
|
||||
992178
|
||||
992179
|
||||
992180
|
||||
992188
|
||||
992206
|
||||
992234
|
||||
992185
|
||||
department 人文—歷史哲學與文化
|
||||
991068
|
||||
991075
|
||||
991087
|
||||
991140
|
||||
991144
|
||||
991154
|
||||
991163
|
||||
991192
|
||||
991199
|
||||
991212
|
||||
992073
|
||||
992087
|
||||
992171
|
||||
994044
|
||||
department 人文—文學與藝術
|
||||
460135
|
||||
991040
|
||||
991062
|
||||
991065
|
||||
991069
|
||||
991167
|
||||
991170
|
||||
991183
|
||||
991190
|
||||
991193
|
||||
991201
|
||||
991203
|
||||
991207
|
||||
991209
|
||||
991210
|
||||
991211
|
||||
992176
|
||||
991032
|
||||
991071
|
||||
178
getData.py
178
getData.py
@ -4,39 +4,33 @@ import os
|
||||
import csv
|
||||
from bs4 import BeautifulSoup as bs
|
||||
|
||||
USERNAME = ""
|
||||
PASSWORD = ""
|
||||
YEAR = 1111
|
||||
|
||||
session = requests.Session()
|
||||
header = {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0',
|
||||
'Cookie': '輸入登入暨大教務系統後所得到的cookie'
|
||||
}
|
||||
|
||||
mainURL = "https://ccweb.ncnu.edu.tw/student/"
|
||||
courses = []
|
||||
generalCourse = []
|
||||
|
||||
def login(username, password):
|
||||
global session
|
||||
response = session.get('https://ccweb.ncnu.edu.tw/student/login.php')
|
||||
root = bs(response.text, 'html.parser')
|
||||
loginToken = root.find('input', {'name': 'token'}).get('value')
|
||||
def getGeneralCourseData(year):
|
||||
'''
|
||||
透過年份取得 通識課程分類的csv檔
|
||||
供後續課程對應。
|
||||
|
||||
先儲存到 generalCourse list,後續再用 courseID 對應通識分類
|
||||
'''
|
||||
|
||||
# request login page
|
||||
response = session.post(
|
||||
"https://ccweb.ncnu.edu.tw/student/login.php",
|
||||
data={
|
||||
'token': loginToken,
|
||||
'modal': '0',
|
||||
'username': username,
|
||||
'password': password,
|
||||
'type': 'a'
|
||||
}
|
||||
)
|
||||
# 教務系統有開放 年度的query
|
||||
# 但實際操作後似乎僅開放當前學年度
|
||||
response = requests.get(mainURL+"aspmaker_student_common_rank_courses_viewlist.php?x_studentid=0&z_studentid=LIKE&x_year={}&z_year=%3D&cmd=search&export=csv".format(year), headers=header)
|
||||
data = response.text
|
||||
|
||||
# 成功的話 return http 302, redirect
|
||||
if len(response.history)!=0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
courses = data.split('\r\n')[1:-1]
|
||||
for course in courses:
|
||||
course = course.split(',')
|
||||
generalCourse.append(course)
|
||||
|
||||
def curlDepartmentCourseTable(year):
|
||||
'''
|
||||
@ -45,94 +39,74 @@ def curlDepartmentCourseTable(year):
|
||||
'''
|
||||
print("取得所有課程資料:")
|
||||
|
||||
# 切換年度,應該是用 cookie 儲存當前閱覽的年份
|
||||
url = 'https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?cmd=search&t=aspmaker_course_opened_detail_view&z_year=%3D&x_year={}&z_courseid=%3D&x_courseid=&z_cname=LIKE&x_cname=&z_deptid=%3D&x_deptid=&z_division=LIKE&x_division=&z_grade=%3D&x_grade=&z_teachers=LIKE&x_teachers=&z_not_accessible=LIKE&x_not_accessible='
|
||||
response = session.get(url.format(year))
|
||||
response = requests.get(mainURL+"aspmaker_course_opened_semester_stat_viewlist.php?x_year={}&recperpage=ALL".format(year), headers=header)
|
||||
data = response.text
|
||||
root = bs(data, "html.parser")
|
||||
|
||||
count = 1
|
||||
departmentsTR = root.findAll('tr')[1:] # 清除 thead
|
||||
for tr in departmentsTR:
|
||||
name = tr.findAll('td')[4].find('span').find('span').string # 取得 科系名稱
|
||||
link = mainURL + tr.find('a').get('data-url').replace('amp;', '') # 清除不必要符號, 取得 連結
|
||||
print("擷取{}課程... ({}/{})...".format(name, count, len(departmentsTR)))
|
||||
count += 1
|
||||
extractDepartmentCourseTable(name, link) # 透過連結 開始擷取 各科系課程
|
||||
|
||||
# 取得 所有課程的 csv
|
||||
response = session.get('https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?export=csv')
|
||||
with open("allCourses.csv", "wb") as fp:
|
||||
fp.write(response.content)
|
||||
|
||||
def extractDepartmentCourseTable(year):
|
||||
def extractDepartmentCourseTable(departmentName, link):
|
||||
'''
|
||||
透過各科系連結取得課程資訊
|
||||
若為通識類別還要跟csv檔資料做對應,取得正確通識類別
|
||||
|
||||
對應後存取到 output.json
|
||||
'''
|
||||
with open("allCourses.csv") as fp:
|
||||
csvData = fp.read()
|
||||
response = requests.get(link, headers=header)
|
||||
data = response.text
|
||||
root = bs(data, "html.parser")
|
||||
|
||||
ans = []
|
||||
courses = csvData.split('"\n')[1:-1]
|
||||
for course in courses:
|
||||
course = course.replace('\n', '.')
|
||||
# print(course)
|
||||
data = course[1:].split('","')
|
||||
|
||||
courseTR = root.findAll('tr')[1:] # 清除 thead
|
||||
for tr in courseTR:
|
||||
courseObj = {}
|
||||
tds = tr.find_all('td')
|
||||
|
||||
baseLink = "https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?cmd=search&t=aspmaker_course_opened_detail_view&z_year=%3D&x_year={}&x_courseid={}"
|
||||
courseObj['link'] = baseLink.format(year, data[1].zfill(6))
|
||||
courseObj['year'] = data[0]
|
||||
courseObj['number'] = data[1]
|
||||
courseObj['class'] = data[2]
|
||||
courseObj['name'] = data[3]
|
||||
courseObj['department'] = data[4]
|
||||
courseObj['graduated'] = data[6]
|
||||
courseObj['grade'] = data[7]
|
||||
courseObj['teacher'] = data[8]
|
||||
courseObj['place'] = data[9]
|
||||
courseObj['time'] = data[13].replace(' ', '')
|
||||
courseObj['credit'] = data[14]
|
||||
|
||||
ans.append(courseObj)
|
||||
|
||||
with open("歷年課程資料/{}_output.json".format(year), 'w') as fp:
|
||||
json.dump(ans, fp, ensure_ascii=False)
|
||||
|
||||
def updateGeneralCourse():
|
||||
with open("歷年課程資料/{}_output.json".format(YEAR)) as fp:
|
||||
courses = json.load(fp)
|
||||
|
||||
with open("generalCourse.in") as fp:
|
||||
line = fp.readline()
|
||||
while line:
|
||||
count = 0
|
||||
line = line.split()
|
||||
if len(line) == 2:
|
||||
department = line[1]
|
||||
else:
|
||||
for course in courses:
|
||||
if course['number'] == line[0]:
|
||||
course['department'] = department
|
||||
count += 1
|
||||
if count == 0 and len(line) != 2:
|
||||
print("{} 可能輸入錯誤 - {}".format(line[0], department))
|
||||
line = fp.readline()
|
||||
|
||||
print("還沒有對應到的課程:")
|
||||
for course in courses:
|
||||
if course['department'] == "99, 通識":
|
||||
course['department'] = "99, 通識(未分類)"
|
||||
print("{} {}".format(course['number'], course['name']))
|
||||
|
||||
with open("歷年課程資料/{}_output.json".format(YEAR), "w") as fp:
|
||||
json.dump(courses, fp, ensure_ascii=False)
|
||||
courseObj['link'] = mainURL + tds[0].find('a').get('href')
|
||||
courseObj['year'] = tds[1].find('span').string
|
||||
courseObj['number'] = tds[2].find('span').string
|
||||
courseObj['class'] = tds[3].find('span').string
|
||||
courseObj['name'] = tds[4].find('span').string
|
||||
courseObj['department'] = tds[5].find('span').string
|
||||
courseObj['graduated'] = tds[6].find('span').string
|
||||
courseObj['grade'] = tds[7].find('span').string
|
||||
courseObj['teacher'] = tds[8].find('span').string
|
||||
courseObj['place'] = tds[9].find('span').string
|
||||
courseObj['time'] = tds[11].find('span').string
|
||||
|
||||
if courseObj['department']=="99, 通識" :
|
||||
flag = False
|
||||
for row in generalCourse:
|
||||
if row[2] == '"{}"'.format(courseObj['number']):
|
||||
courseObj['department'] = row[0].replace('"', '')
|
||||
generalCourse.remove(row)
|
||||
flag = True
|
||||
break
|
||||
if not flag:
|
||||
print(" - 找不到對應的通識類別: {} ( {} )".format(courseObj['name'], courseObj['number']))
|
||||
|
||||
courses.append(courseObj)
|
||||
|
||||
with open('output.json', 'w') as fp:
|
||||
json.dump(courses, fp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
username = USERNAME
|
||||
password = PASSWORD
|
||||
if login(username, password):
|
||||
print("登入成功!")
|
||||
break
|
||||
else:
|
||||
print("登入失敗!")
|
||||
|
||||
curlDepartmentCourseTable(YEAR)
|
||||
extractDepartmentCourseTable(YEAR)
|
||||
updateGeneralCourse()
|
||||
year = input("年份: ")
|
||||
|
||||
getGeneralCourseData(year)
|
||||
curlDepartmentCourseTable(year)
|
||||
|
||||
print("\n\n=====================")
|
||||
print("未列入追蹤的通識課程")
|
||||
print("=====================\n")
|
||||
|
||||
for notIn in generalCourse:
|
||||
if "體育:" not in notIn[5]:
|
||||
print(" - 未列入追蹤的新通識課程: {}".format(notIn))
|
||||
@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="description" content="你還在用紙筆或Excel在安排下學期的課表嗎?「暨大排課表」幫你篩選衝堂、科系分類、通識課程分類,讓你輕鬆排課表!" />
|
||||
<meta name="author" content="snsd0805" />
|
||||
<title>暨大排課表</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="author" content="" />
|
||||
<title>暨大排課表</title>
|
||||
<!-- Favicon-->
|
||||
<link rel="icon" type="image/x-icon" href="assets/img/favicon.ico" />
|
||||
<!-- Font Awesome icons (free version)-->
|
||||
@ -28,8 +28,6 @@
|
||||
<div class="container"><small>Copyright © 暨大排課表 2020</small></div>
|
||||
</div>
|
||||
|
||||
<!-- html2canvas -->
|
||||
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
||||
<!-- Bootstrap core JS-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@ -19,24 +19,19 @@ var chooseDepartment = {
|
||||
},
|
||||
template: `
|
||||
|
||||
<div class="mx-auto">
|
||||
<div>
|
||||
<h5>1. 「課程名稱」直接搜尋</h5>
|
||||
<input class="form-control" type='text' v-model='initFounded'>
|
||||
</div>
|
||||
<div class="text-center my-2">
|
||||
<h5>或</h5>
|
||||
</div>
|
||||
<div>
|
||||
<h5>1. 選擇類別</h5>
|
||||
<select class="custom-select mr-sm-2" v-model="initSelect">
|
||||
<option v-for="(item, index) in departments" :key="index"
|
||||
v-bind:value="item"
|
||||
>
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<h5>1. 「課程名稱」直接搜尋</h5>
|
||||
<input class="form-control" type='text' v-model='initFounded'>
|
||||
<br>或<br><br>
|
||||
<h5>1. 選擇類別</h5>
|
||||
<select class="custom-select mr-sm-2" v-model="initSelect">
|
||||
<option v-for="(item, index) in departments" :key="index"
|
||||
v-bind:value="item"
|
||||
>
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
@ -1,54 +1,69 @@
|
||||
var coursesList = {
|
||||
props: ['courses', 'selected_d', 'selected_c', 'find_name'],
|
||||
data: function () {
|
||||
data: function(){
|
||||
return {
|
||||
selectedTime: [],
|
||||
foundedCourses: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
'getTime': function (timeString) {
|
||||
let num;
|
||||
const timeRegex = new RegExp(/^\d[\da-z]*[a-z]$/);
|
||||
return timeRegex.test(timeString)
|
||||
? [...timeString].reduce((res, c) => {
|
||||
if (Number.isInteger(+c)) {
|
||||
num = c;
|
||||
return res;
|
||||
} else {
|
||||
return [...res, num + c];
|
||||
}
|
||||
}, [])
|
||||
: [];
|
||||
'getTime': function(timeString){
|
||||
if(timeString==null){
|
||||
return ""
|
||||
}
|
||||
|
||||
ans = []
|
||||
number = ""
|
||||
for(var i of timeString){
|
||||
if(i>="0" && i<="9"){
|
||||
number = i
|
||||
}else if(i>="a" && i<="z"){
|
||||
ans.push(number+i)
|
||||
}
|
||||
else{
|
||||
ans.push(timeString)
|
||||
break
|
||||
}
|
||||
}
|
||||
return ans
|
||||
},
|
||||
'isOK': function (course) {
|
||||
'isOK': function(course){
|
||||
var time = this.getTime(course.time)
|
||||
// console.log(course.name, " ", time)
|
||||
const isConflict = time.some((t) => this.selectedTime.includes(t))
|
||||
|
||||
return time.length && !isConflict
|
||||
for(t of time){
|
||||
for(st of this.selectedTime){
|
||||
if(t==st)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
'log': function (name, data) {
|
||||
'log': function(name, data){
|
||||
console.log(name, data)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'selected_c': function () {
|
||||
'selected_c': function(){
|
||||
var temp = []
|
||||
for (var c of this.selected_c) {
|
||||
if (c.temp == false) {
|
||||
for(var c of this.selected_c){
|
||||
if(c.temp==false){
|
||||
temp.push(c.time)
|
||||
}
|
||||
}
|
||||
this.selectedTime = temp
|
||||
},
|
||||
'find_name': function () {
|
||||
const target = this.find_name.toLowerCase();
|
||||
this.foundedCourses = this.courses.filter((c) => c.name.toLowerCase().includes(target));
|
||||
'find_name': function(){
|
||||
var temp = []
|
||||
for(var c of this.courses){
|
||||
if(c.name.indexOf(this.find_name) != -1){
|
||||
temp.push(c)
|
||||
}
|
||||
}
|
||||
this.foundedCourses = temp
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="mx-auto mb-4">
|
||||
<div>
|
||||
<h5>2. 安排課程</h5>
|
||||
<p style="color: orange" v-if="find_name"> ※ 已套用「名稱」搜尋: <br>{{find_name}}</p>
|
||||
<div style="width:275px;height:500px;overflow:auto">
|
||||
@ -57,18 +72,20 @@ var coursesList = {
|
||||
<tr v-for="(course, index) in foundedCourses" :key="index"
|
||||
v-on:mouseenter="$emit('show-temp', course)" v-on:mouseleave="$emit('delete-temp', course)">
|
||||
<td>
|
||||
<div class="container row py-2 px-0">
|
||||
<div class="col-12 pr-1">
|
||||
<b>{{ course.name }} (<a v-bind:href="course.link" target="_blank">詳</a>)</b>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<b>{{ course.name }} (<a v-bind:href="course.link">詳</a>)</b>
|
||||
—— {{ (course.department.indexOf(', ')!=-1) ?(course.department.split(', ')[1]) :(course.department) }}
|
||||
</div>
|
||||
<div class="col-sm-8 pr-1">
|
||||
{{ course.teacher }} ‧ {{ course.time }}
|
||||
</div>
|
||||
<div class="col-sm-4 pr-1">
|
||||
<button v-if="isOK(course)" type="button" v-on:click="$emit('add-course', course)" class="btn btn-primary">
|
||||
<span>+</span>
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
{{ course.teacher }} ‧ {{ course.time }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button v-if="isOK(course)" type="button" v-on:click="$emit('add-course', course)" class="btn btn-primary">
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -79,19 +96,21 @@ var coursesList = {
|
||||
v-if="course.department == selected_d"
|
||||
v-on:mouseenter="$emit('show-temp', course)" v-on:mouseleave="$emit('delete-temp', course)">
|
||||
<td>
|
||||
<div class="container row py-2 px-0">
|
||||
<div class="col-12 pr-1">
|
||||
<b>{{ course.name }} (<a v-bind:href="course.link" target="_blank">詳</a>)</b>
|
||||
</div>
|
||||
<div class="col-sm-8 pr-1">
|
||||
{{ course.teacher }} ‧ {{ course.time }}
|
||||
</div>
|
||||
<div class="col-sm-4 pr-1">
|
||||
<button v-if="isOK(course)" type="button" v-on:click="$emit('add-course', course)" class="btn btn-primary">
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<b>{{ course.name }} (<a v-bind:href="course.link">詳</a>)</b>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
{{ course.teacher }} ‧ {{ course.time }}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<button v-if="isOK(course)" type="button" v-on:click="$emit('add-course', course)" class="btn btn-primary">
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
var mainWindow = {
|
||||
data: function () {
|
||||
data: function(){
|
||||
return {
|
||||
'courses': [],
|
||||
'selectCourses': [],
|
||||
@ -7,31 +7,29 @@ var mainWindow = {
|
||||
'selectDepartment': '',
|
||||
'foundName': "",
|
||||
"user": "",
|
||||
'token': "",
|
||||
'is_print': false,
|
||||
'creditNum': 0,
|
||||
'token': ""
|
||||
}
|
||||
},
|
||||
created() {
|
||||
var main = this
|
||||
window.fbAsyncInit = function () {
|
||||
window.fbAsyncInit = function() {
|
||||
FB.init({
|
||||
appId: '',
|
||||
cookie: true,
|
||||
xfbml: true,
|
||||
version: 'v9.0'
|
||||
appId : '',
|
||||
cookie : true,
|
||||
xfbml : true,
|
||||
version : 'v9.0'
|
||||
});
|
||||
|
||||
|
||||
FB.AppEvents.logPageView();
|
||||
main.getCourseTable()
|
||||
};
|
||||
|
||||
(function (d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) { return; }
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "https://connect.facebook.net/en_US/sdk.js";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
|
||||
(function(d, s, id){
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) {return;}
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "https://connect.facebook.net/en_US/sdk.js";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));
|
||||
},
|
||||
mounted() {
|
||||
@ -39,165 +37,135 @@ var mainWindow = {
|
||||
axios
|
||||
.get("./output.json")
|
||||
.then(response => (main.courses = response.data))
|
||||
.then(function () {
|
||||
for (var course of main.courses) {
|
||||
.then(function(){
|
||||
for(var course of main.courses){
|
||||
// console.log(course.name)
|
||||
if (main.departments.indexOf(course.department) == -1) {
|
||||
if(main.departments.indexOf(course.department)==-1){
|
||||
main.departments.push(course.department)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
.then(function(){
|
||||
main.departments.sort()
|
||||
main.selectDepartment = main.departments[15]
|
||||
})
|
||||
// Collapse Navbar
|
||||
var navbarCollapse = function () {
|
||||
if ($("#mainNav").offset().top > 100) {
|
||||
$("#mainNav").addClass("navbar-shrink");
|
||||
} else {
|
||||
$("#mainNav").removeClass("navbar-shrink");
|
||||
}
|
||||
};
|
||||
// Collapse now if page is not at top
|
||||
navbarCollapse();
|
||||
// Collapse the navbar when page is scrolled
|
||||
$(window).scroll(navbarCollapse);
|
||||
},
|
||||
methods: {
|
||||
'login': function () {
|
||||
'login': function(){
|
||||
var main = this
|
||||
FB.login(function () {
|
||||
FB.login(function(){
|
||||
main.getCourseTable()
|
||||
})
|
||||
},
|
||||
'logout': function () {
|
||||
'logout': function(){
|
||||
var main = this
|
||||
FB.logout(function (response) {
|
||||
FB.logout(function(response) {
|
||||
main.user = ""
|
||||
main.token = ""
|
||||
});
|
||||
},
|
||||
'getCourseTable': function () {
|
||||
'getCourseTable': function(){
|
||||
var main = this
|
||||
FB.getLoginStatus(function (response) {
|
||||
FB.getLoginStatus(function(response) {
|
||||
main.statusChangeCallback(response);
|
||||
});
|
||||
},
|
||||
'statusChangeCallback': function (response) {
|
||||
if (response.status == "connected") {
|
||||
'statusChangeCallback': function(response){
|
||||
if(response.status == "connected"){
|
||||
this.token = response.authResponse.accessToken
|
||||
|
||||
var main = this
|
||||
FB.api('/me', function (response) { main.user = response })
|
||||
FB.api('/me', function(response){main.user = response.name})
|
||||
|
||||
fetch('https://api.snsd0805.com/courseTable?token=' + this.token)
|
||||
.then(function (response) {
|
||||
fetch('https://api.snsd0805.com/courseTable?token='+this.token)
|
||||
.then(function(response){
|
||||
return response.json()
|
||||
}).then(function (jsonData) {
|
||||
}).then(function(jsonData){
|
||||
console.log(jsonData)
|
||||
main.selectCourses = JSON.parse(jsonData['data'])
|
||||
|
||||
var courseSet = new Set()
|
||||
for (var course of main.selectCourses) {
|
||||
if (!courseSet.has(course.number+course.class)) { // 用 courseID + 班別 判斷是否重複
|
||||
main.creditNum += parseFloat(course.credit)
|
||||
courseSet.add(course)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
alert("錯誤: " + err)
|
||||
.catch(function(err){
|
||||
alert("錯誤: "+err)
|
||||
})
|
||||
}
|
||||
},
|
||||
'saveCourseTable': function () {
|
||||
'saveCourseTable': function(){
|
||||
var main = this
|
||||
if (this.token != "") {
|
||||
filteredCourses = []
|
||||
for(var tempCourse of main.selectCourses){
|
||||
if(tempCourse.temp == false){
|
||||
filteredCourses.push(tempCourse);
|
||||
}
|
||||
}
|
||||
fetch('https://api.snsd0805.com/courseTable', {
|
||||
if(this.token!=""){
|
||||
fetch('https://api.snsd0805.com/courseTable',{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'token': main.token,
|
||||
'data': filteredCourses
|
||||
'data': main.selectCourses
|
||||
})
|
||||
})
|
||||
.then(function (response) {
|
||||
.then(function(response){
|
||||
return response.json()
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status == "saved") {
|
||||
.then(function(response){
|
||||
if(response.status=="saved"){
|
||||
alert("已儲存")
|
||||
} else {
|
||||
}else{
|
||||
alert("錯誤")
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
alert("錯誤: " + err)
|
||||
.catch(function(err){
|
||||
alert("錯誤: "+err)
|
||||
})
|
||||
|
||||
} else {
|
||||
}else{
|
||||
this.login()
|
||||
}
|
||||
},
|
||||
'getTime': function (timeString) {
|
||||
let num;
|
||||
const timeRegex = new RegExp(/^\d[\da-z]*[a-z]$/);
|
||||
return timeRegex.test(timeString)
|
||||
? [...timeString].reduce((res, c) => {
|
||||
if (Number.isInteger(+c)) {
|
||||
num = c;
|
||||
return res;
|
||||
} else {
|
||||
return [...res, num + c];
|
||||
}
|
||||
}, [])
|
||||
: [];
|
||||
'getTime': function(timeString){
|
||||
ans = []
|
||||
number = ""
|
||||
for(var i of timeString){
|
||||
if(i>="0" && i<="9"){
|
||||
number = i
|
||||
}else if(i>="a" && i<="z"){
|
||||
ans.push(number+i)
|
||||
}
|
||||
else{
|
||||
ans.push(timeString)
|
||||
break
|
||||
}
|
||||
}
|
||||
return ans
|
||||
},
|
||||
'select': function (department) {
|
||||
'select': function(department){
|
||||
this.selectDepartment = department
|
||||
},
|
||||
'founded': function (courseName) {
|
||||
'founded': function(courseName){
|
||||
this.foundName = courseName
|
||||
},
|
||||
'addCourse': function (course) {
|
||||
'addCourse': function(course){
|
||||
var time = this.getTime(course.time)
|
||||
for (var t of time) {
|
||||
for(var t of time){
|
||||
this.selectCourses.push({
|
||||
'time': t,
|
||||
'name': course.name,
|
||||
'temp': false,
|
||||
'number': course.number,
|
||||
'class': course.class,
|
||||
'credit': course.credit,
|
||||
'link': course.link
|
||||
'temp': false
|
||||
})
|
||||
}
|
||||
this.creditNum += parseFloat(course.credit)
|
||||
},
|
||||
'removeCourse': function (course) {
|
||||
console.log("remove " + course.name)
|
||||
for (var i = this.selectCourses.length - 1; i >= 0; i--) {
|
||||
if (this.selectCourses[i].number === course.number && this.selectCourses[i].class === course.class) {
|
||||
'removeCourse': function(course){
|
||||
console.log("remove "+course)
|
||||
for(var i=this.selectCourses.length-1;i>=0;i--){
|
||||
if(this.selectCourses[i].name == course){
|
||||
this.selectCourses.splice(i, 1)
|
||||
}
|
||||
}
|
||||
this.creditNum -= parseFloat(course.credit)
|
||||
},
|
||||
'saveTemp': function (course) {
|
||||
if (course == null) {
|
||||
} else {
|
||||
'saveTemp': function(course){
|
||||
if(course==null){
|
||||
}else{
|
||||
this.tempCourse = []
|
||||
var time = this.getTime(course.time)
|
||||
for (var t of time) {
|
||||
for(var t of time){
|
||||
this.selectCourses.push({
|
||||
'time': t,
|
||||
'name': course.name,
|
||||
@ -206,38 +174,13 @@ var mainWindow = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'deleteTemp': function (course) {
|
||||
for (var i = this.selectCourses.length - 1; i >= 0; i--) {
|
||||
if (this.selectCourses[i].name == course.name && this.selectCourses[i].temp == true) {
|
||||
'deleteTemp': function(course){
|
||||
for(var i=this.selectCourses.length-1;i>=0;i--){
|
||||
if(this.selectCourses[i].name == course.name && this.selectCourses[i].temp == true){
|
||||
this.selectCourses.splice(i, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
'generatePic': function () {
|
||||
var main = this
|
||||
const doPrint = new Promise((resolve, reject) => {
|
||||
main.is_print = true;
|
||||
resolve();
|
||||
});
|
||||
doPrint
|
||||
.then(() =>{
|
||||
html2canvas(document.getElementById('course-table-div')).then(function (canvas) {
|
||||
var a = document.createElement('a');
|
||||
a.href = canvas.toDataURL("image/jpeg").replace("image/jpeg", "image/octet-stream");
|
||||
a.download = '課表.jpg';
|
||||
a.click();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
main.is_print = false;
|
||||
})
|
||||
},
|
||||
'share': function () {
|
||||
if (this.user != "")
|
||||
$('#share').modal('show');
|
||||
else
|
||||
this.login()
|
||||
},
|
||||
},
|
||||
components: {
|
||||
'course-table': courseTable,
|
||||
@ -259,7 +202,7 @@ var mainWindow = {
|
||||
<li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href="https://github.com/snsd0805/NCNU_Course">Github</a></li>
|
||||
|
||||
<li v-if="token==''" class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href='#' v-on:click="login()">Facebook登入</a></li>
|
||||
<li v-if="token!=''" class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href="#" v-on:click="logout()">登出Facebook—{{user.name}}</a></li>
|
||||
<li v-if="token!=''" class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href="#" v-on:click="logout()">登出Facebook—{{user}}</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@ -279,33 +222,21 @@ var mainWindow = {
|
||||
<div class="divider-custom-line"></div>
|
||||
</div>
|
||||
<div class="divider-custom">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div v-if="token!=''"><button class="btn btn-danger" @click="saveCourseTable()">儲存</button></div>
|
||||
<div v-if="token==''"><button class="btn btn-danger" @click="saveCourseTable()">儲存(登入FB)</button></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div><button class="btn btn-success" @click="generatePic()">下載圖檔</button></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div><button class="btn btn-primary" @click="share()">分享課表</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="token!=''"><button class="btn btn-info" @click="saveCourseTable()">儲存</button></div>
|
||||
<div v-if="token==''"><button class="btn btn-info" @click="saveCourseTable()">儲存(必須登入Facebook帳號)</button></div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<div class="row mx-auto mb-2">
|
||||
<div class="row">
|
||||
<choose-department
|
||||
v-bind:departments="departments"
|
||||
v-bind:selected="selectDepartment"
|
||||
v-on:selectok="select"
|
||||
v-on:foundedok="founded"
|
||||
>
|
||||
</choose-department>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row mx-auto mb-2">
|
||||
</choose-department><br>
|
||||
</div><br><br>
|
||||
<div class="row">
|
||||
<course-anslist
|
||||
v-bind:courses="courses"
|
||||
v-bind:selected_d="selectDepartment"
|
||||
@ -316,26 +247,13 @@ var mainWindow = {
|
||||
v-on:delete-temp="deleteTemp"
|
||||
>
|
||||
</course-anslist>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
已經選了 {{ creditNum }} 學分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><br><br>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="col-lg-9 table-responsive " >
|
||||
<course-table
|
||||
id="course-table-div"
|
||||
v-bind:selectCourses="selectCourses"
|
||||
v-bind:select_c="selectCourses"
|
||||
v-bind:is_print="is_print"
|
||||
v-bind:is_shared="false"
|
||||
v-on:remove-course="removeCourse"
|
||||
></course-table>
|
||||
@ -356,32 +274,9 @@ var mainWindow = {
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul>
|
||||
<li>已經更新為 1101 新學期課表(包含通識課分類)</li>
|
||||
<li>有發現 Bug 可以到 <a href='https://github.com/snsd0805/NCNU_Course/issues'>GitHub</a> 發 issue 或 <a href='mailto: levi900227@gmail.com'>mail</a></li>
|
||||
<li>請善用「連接 Facebook」功能來儲存課表!</li>
|
||||
</ul>
|
||||
2021 07/16 更新
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="share" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">分享課表</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
請複製以下網址給你的朋友,跟他分享你的課表<br><br>
|
||||
https://course.snsd0805.com/#/share/{{user.id}}
|
||||
已經更新為 1092 新課表<br>
|
||||
但因學校未更新通識課資料,因此還沒有「通識課程分類」<br><br>
|
||||
2021 01/14 更新
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>
|
||||
@ -392,4 +287,4 @@ var mainWindow = {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,12 @@
|
||||
var courseDiv = {
|
||||
props: ['course', 'is_shared', 'is_print'],
|
||||
props: ['course', 'is_shared'],
|
||||
template: `
|
||||
<div style='border: 5px #1abc9c solid; text-align: center;'>
|
||||
{{ course.name }}
|
||||
<a v-bind:href="course.link" target="_blank"><i class="fas fa-info-circle"></i></a>
|
||||
<button type="button"
|
||||
v-if="!is_shared"
|
||||
v-on:click="$emit('remove-course', course)"
|
||||
v-on:click="$emit('remove-course', course.name)"
|
||||
class="btn btn-danger btn-sm"
|
||||
:style="{'display': is_print ? 'none' : 'inline-block'}"
|
||||
>
|
||||
刪
|
||||
</button>
|
||||
@ -24,7 +22,7 @@ var tempDiv = {
|
||||
`
|
||||
}
|
||||
var courseTable = {
|
||||
props: ['select_c', 'is_shared', 'is_print'],
|
||||
props: ['select_c', 'is_shared'],
|
||||
data: function(){
|
||||
return {
|
||||
'courses': {},
|
||||
@ -53,11 +51,7 @@ var courseTable = {
|
||||
for(var c of this.select_c){
|
||||
this.courses[c.time] = {
|
||||
'name': c.name,
|
||||
'number': c.number,
|
||||
'class': c.class,
|
||||
'temp': c.temp,
|
||||
'credit': c.credit,
|
||||
'link': c.link
|
||||
'temp': c.temp
|
||||
}
|
||||
|
||||
if(c.time[0]==6 || c.time[0]==7){
|
||||
@ -118,7 +112,6 @@ var courseTable = {
|
||||
v-if="exist(week+String.fromCharCode(97+((hour<5)?(hour-1):(hour-2)))) && !courses[week+String.fromCharCode(97+((hour<5)?(hour-1):(hour-2)))].temp"
|
||||
v-bind:course="courses[week+String.fromCharCode(97+((hour<5)?(hour-1):(hour-2)))]"
|
||||
v-bind:is_shared="is_shared"
|
||||
v-bind:is_print="is_print"
|
||||
v-on:remove-course="removeCourseHandler"
|
||||
></course-div>
|
||||
<temp-div
|
||||
|
||||
@ -41,7 +41,18 @@
|
||||
offset: 80
|
||||
});
|
||||
|
||||
|
||||
// Collapse Navbar
|
||||
var navbarCollapse = function() {
|
||||
if ($("#mainNav").offset().top > 100) {
|
||||
$("#mainNav").addClass("navbar-shrink");
|
||||
} else {
|
||||
$("#mainNav").removeClass("navbar-shrink");
|
||||
}
|
||||
};
|
||||
// Collapse now if page is not at top
|
||||
navbarCollapse();
|
||||
// Collapse the navbar when page is scrolled
|
||||
$(window).scroll(navbarCollapse);
|
||||
|
||||
// Floating label headings for the contact form
|
||||
$(function() {
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
[Unit]
|
||||
Description=NCNU-Course Python Backend API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=python3 api.py
|
||||
Restart=always
|
||||
|
||||
WorkingDirectory=/var/www/html/NCNU_Course
|
||||
User=course
|
||||
|
||||
RestartSec=10s
|
||||
|
||||
StandardOutput=syslog
|
||||
StandardOutput=syslog
|
||||
SyslogIdentifier=ncnu-course
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user