摘要:在實際開發(fā)過程中發(fā)現(xiàn),考試系統(tǒng)各個表集合都是需要關(guān)聯(lián),這種非關(guān)系型數(shù)據(jù)庫,做起來反而麻煩了不少。數(shù)據(jù)中既有試卷的信息,也有很多題目。題目都屬于該試卷,改試卷又屬于當前登錄系統(tǒng)的老師即創(chuàng)建試卷的老師。
這是我畢業(yè)項目,從0到1,前后臺獨立開發(fā)完成。功能不多,在此記錄,溫故而知新!項目github地址:https://github.com/FinGet/Exam ,博客地址:https://finget.github.io/
更新記錄:2018-4-9,md5加密安裝mongodb
window下安裝mongodb,需要參考的可以移步我的博客中:win10安裝mongodb
項目初始化本次項目使用的是express4 + vue2+ + elementUI1+ + mongodb3.4+
先看項目文件目錄結(jié)構(gòu):
我頁面用的vue所以server/views和server/public都沒有用
項目建立用的是vue-cli:
vue init webpack exam
項目中前后臺是寫在一個項目中的:
npm i -g express-generator // 在項目文件根目錄下 express server
由于前后臺都是寫在一個項目中的,我就將server下的package.json和vue下的package.json合并了
npm i axios --save
首先axios不支持vue.use()式聲明
// 在main.js中如下聲明使用 import axios from "axios"; Vue.prototype.$axios=axios; // 那么在其他vue組件中就可以this.$axios調(diào)用使用elementUI
npm i element-ui --save
import ElementUI from "element-ui" // 加載ElementUI import "element-ui/lib/theme-default/index.css" Vue.use(ElementUI) // 全局使用elementUIvue-lazyload 圖片懶加載
npm i vue-lazyload --save
// main.js import VueLazyLoad from "vue-lazyload" Vue.use(VueLazyLoad, { // 全局使用圖片懶加載 loading: "static/loading-svg/loading-bars.svg", // 圖片還沒加載時的svg圖片 try: 1 // default 1 })
使用懶加載:
logoSrc:require("../common/img/logo.png") // 不能寫成:mongoose 操作mongodb的
npm i mongoose --save
就不一一列舉所有的插件了(沒有用vuex)開發(fā)上的一些事 前臺相關(guān) sessionStorage
// commonFun.js //獲取sessionStorage function getSessionStorage(key, format) { var data; if (sessionStorage.getItem(key)) { if (format == "json") { data = JSON.parse(sessionStorage.getItem(key)); } else { data = sessionStorage.getItem(key); } } else { data = false } return data; } //寫入sessionStorage function setSessionStorage(key, content, format) { var data; if (format == "json") { data = JSON.stringify(content); } else { data = content; } sessionStorage.setItem(key, data); } export var mySessionStorage = { get: getSessionStorage, set: setSessionStorage }
全局掛載
// main.js import * as commonFun from "./common/js/commonFun.js" Vue.prototype.$mySessionStorage = commonFun.mySessionStorage;
在頁面中使用
this.$mySessionStorage.set(key,content,format); this.$mySessionStorage.get(key);登錄檢測
// main.js // 登錄判斷 router.beforeEach((to, from, next) => { var userdata = getUserData(); if (to.path != "/managelogin"&&to.name!="404"&&to.path != "/"&&to.path != "/frontregister"&&to.path!="/manageregister") { // 判斷是否登錄 if(!userdata.userName){ ElementUI.Message.error("抱歉,您還沒有登錄!"); if(to.path.indexOf("front")>0){ router.push({path:"/"}); } else { router.push({path:"/managelogin"}); } } else { next(); } } else { next(); } })面包屑導(dǎo)航
綁定面包屑要根據(jù)實際情況來定,但是this.$router.currentRoute.matched是最主要的
{{item.meta.breadName}}
路由部分:
根據(jù)實際情況來,不能套用,要看你的路由怎么寫的 this.$router.currentRoute.path
:default-active="activeIndex"
// conponents/sidebar.vue //初始化列表active狀態(tài) ... methods:{ initActiveIndex(){ // var str =this.$router.currentRoute.path; this.activeIndex=this.$router.currentRoute.path; // console.log(str) } }, watch:{ "$route":"initActiveIndex" }, created(){ this.initActiveIndex(); } ...配置代理
要想請求到后臺數(shù)據(jù),這一步是必須的
配置代理之后,localhost:8088/api/ -> localhost:3000/api/
config/index.js proxyTable: { // proxy all requests starting with /api to jsonplaceholder "/api": { target: "http://127.0.0.1:3000/api", // 端口號根據(jù)后臺設(shè)置來,默認是3000 changeOrigin: true, pathRewrite: { "^/api": "" // 若target中沒有/api、這里又為空,則404; } } },ElementUi動態(tài)增加表單的表單驗證 大坑
query要用path來引入,params要用name來引入// 最重要的是prop 一定要帶上`.optionContent`,也就是你綁定值的key 添加選項
goToExam(id){ // params傳參只能用name引入 this.$router.push({name:"ForntExam",params:{id:id}}); }Elementui 單選框?qū)ι蠁芜x題
單選題(只有一個正確答案)
{{index+1}} 、{{item.name}}()
{{options[index1]}}、{{item1}}
init(){ if(this.id == "" || !this.id ){ this.$router.push({path:"forntexamindex"}); return } else { this.$axios.get("/api/getExamInfo",{ params:{ id: this.id } }).then(response => { let res = response.data; if(res.status == "0") { for(let key in this.paperData) { this.paperData[key] = res.result[key]; } res.result._questions.forEach(item => { if(item.type=="single"){ item.sanswer = ""; // 重要的在這 給他新增一個屬性,用來存答案 this.singleQuestions.push(item); } else if(item.type == "multi"){ item.sanswer = []; // 多選題 this.multiQuestions.push(item); } else if(item.type == "Q&A") { item.sanswer = ""; this.QAQuestions.push(item); } else if(item.type == "judgement"){ item.sanswer = ""; this.judgeQuestions.push(item); } }) } }).catch(err => { this.$message.error(err); }) } }后臺相關(guān) 連接數(shù)據(jù)庫
在server根目錄下新建db.js
// db.js var mongoose = require("mongoose"); var dbUrl = "mongodb://127.0.0.1:27017/examSystem"; var db = mongoose.connect(dbUrl); db.connection.on("error",function(error) { console.log("數(shù)據(jù)庫鏈接失敗:"+ error); }); db.connection.on("connected",function() { console.log("數(shù)據(jù)庫鏈接成功!"); }); db.connection.on("disconnected",function() { console.log("Mongoose connection disconnected"); }); module.exports = db;
// server/app.js // 鏈接數(shù)據(jù)庫 require("./db");配置seesion
需要express-session 和 cookie-parser插件
// app.js // 加載解析session的中間件 // session 的 store 有四個常用選項:1)內(nèi)存 2)cookie 3)緩存 4)數(shù)據(jù)庫 // 數(shù)據(jù)庫 session。除非你很熟悉這一塊,知道自己要什么,否則還是老老實實用緩存吧 需要用到(connect-mongo插件 line 7) // app.use(sessionParser({ 會在數(shù)據(jù)庫中新建一個session集合存儲session // secret: "express", // store: new mongoStore({ // url:"mongodb://127.0.0.1:27017/examSystem", // collection:"session" // }) // })); // 默認使用內(nèi)存來存 session,對于開發(fā)調(diào)試來說很方便 app.use(sessionParser({ secret: "12345", // 建議使用 128 個字符的隨機字符串 name: "userInfo", cookie: { maxAge: 1800000 }, // 時間可以長點 resave:true, rolling:true, saveUninitialized:false }));配置后臺路由
默認的使用方式:
// appi.js var index = require("./routes/index"); app.use("/", index);
// routes/index var express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function(req, res, next) { res.render("index", { title: "Express" }); }); module.exports = router;
我之前做的一個電子商城采用的這種方式:github地址
我的項目中:
// app.js var indexs = require("./routes/index"); var routes = require("./routes/routes"); indexs(app); routes(app);
// routes/index.js module.exports = function(app) { app.get("/api", (req, res) => { res.render("index", {title: "Express"}); }) }
兩種方式有什么不同:
如果你有多個路由文件 (例如goods.js,index.js,users.js……),你都需要去app.js中引入
// app.js var index = require("./routes/index"); var users = require("./routes/users"); var goods = require("./routes/goods"); app.use("/", index); app.use("/users", users); app.use("/goods", goods);
在前臺請求的時候:
// goods.js .... router.get("/list", function (req, res, next) { ... }
// xxx.vue ... this.$axios.get("/goods/list").then()... // 不能忘了加上goods,也就是你在app.js中定義的一級路由 ...
如果沒看懂,可以去GitHub上看一下實際代碼,有助于理解
第二種方式
不用在app.js中引入各個路由文件,一個route.js就搞定了
// route.js var Teacher = require("../controllers/teacher"), Student = require("../controllers/student"); module.exports = function(app) { /*----------------------教師用戶----------------------*/ app.post("/api/register",Teacher.register); // 用戶登錄 app.post("/api/login", Teacher.signup); // 登出 app.post("/api/logout", Teacher.signout); // 獲取用戶信息 app.post("/api/getUserInfo",Teacher.getUserInfo); // 修改用戶信息 app.post("/api/updateUser", Teacher.updateUser); // 獲取試卷(分頁、模糊查詢) app.get("/api/mypapers", Teacher.getPapers); // 保存試卷 app.post("/api/savePaper", Teacher.savePaper); // 發(fā)布試卷 app.post("/api/publishPaper", Teacher.publishPaper); // 刪除試卷 app.post("/api/deletePaper", Teacher.deletePaper); // 查找試卷 app.post("/api/findPaper", Teacher.findPaper); // 修改試題 app.post("/api/updateQuestion", Teacher.updateQuestion); // 修改試卷 app.post("/api/updatePaper", Teacher.updatePaper); // 獲取所有的考試 app.get("/api/getAllExams",Teacher.getAllExams); // 獲取已考試的試卷 app.get("/api/getExams",Teacher.getExams); // 獲取學(xué)生考試成績 app.get("/api/getScores", Teacher.getScores); // 批閱試卷 app.get("/api/getCheckPapers", Teacher.getCheckPapers); // 打分提交 app.get("/api/submitScore", Teacher.submitScore); /*----------------------學(xué)生用戶----------------------*/ // 學(xué)生注冊 app.post("/api/studentregister",Student.register); // 學(xué)生登錄 app.post("/api/studentlogin", Student.signup); // 學(xué)生登出 app.post("/api/studentlogout", Student.signout); // 修改信息 app.post("/api/updateStudent", Student.updateStudent); // 獲取考試記錄 app.get("/api/getexamlogs", Student.getExamLogs); // 獲取個人信息 app.get("/api/studentinfo", Student.getInfo); // 獲取考試信息 app.get("/api/getExamsPaper",Student.getExams); // 獲取試卷信息 app.get("/api/getExamInfo",Student.getExamInfo); // 提交考試信息 app.post("/api/submitExam",Student.submitExam); }
可以看到,我將每個路由的方法都是提取出去的,這樣可以避免這個文件不會有太多的代碼,可讀性降低,將代碼分離開來,也有助于維護
在使用的時候:
// xxx.vue ... this.$axios.get("/api/getexamlogs").then()... ...數(shù)據(jù)庫的相關(guān)操作
我這次用mongodb,主要是因為可以用js來操作,對我來說比較簡單,mysql我不會用。在實際開發(fā)過程中發(fā)現(xiàn),考試系統(tǒng)各個表(集合)都是需要關(guān)聯(lián),mongodb這種非關(guān)系型數(shù)據(jù)庫,做起來反而麻煩了不少。在此將一些數(shù)據(jù)庫增刪改查的方法回顧一下。
如果對mongodb,mongoose沒有基礎(chǔ)的了解,建議看一看mongoose深入淺出 ,mongoose基礎(chǔ)操作
// controllers/student.js const Student = require("../model/student"); var mongoose = require("mongoose"); var Schema = mongoose.Schema; var student = new Student({ userId: 12001, // 學(xué)號 userName: "張三", // 用戶名 passWord: "123321", // 密碼 grade: 3, // 年級 1~6 分別代表一年級到六年級 class: 3, // 班級 exams:[{ // 參加的考試 _paper:Schema.Types.ObjectId("5a40a4ef485a584d44764ff1"), // 這個是_id,在mongodb自動生成的,從數(shù)據(jù)庫復(fù)制過來,初始化一個學(xué)生,應(yīng)該是沒有參加考試的 score:100, date: new Date(), answers: [] }] }) // 保存 student.save((err,doc) => { console.log(err); });
exports.register = function (req,res) { let userInfo = req.body.userInfo; // req.body 獲取post方式傳遞的參數(shù) Student.findOne(userInfo,(err,doc) => { if(err) { ... } else { if(doc) { res.json({ status:"2", msg: "用戶已存在" }) } else { userInfo.exams = []; // userInfo 是個對象,包含了用戶相關(guān)的信息 Student.create(userInfo,(err1,doc1) => { if(err1) { ... }else { if(doc1) { ... } else { ... } } }) } } }) };
如下圖是我的student集合:
在該集合中,學(xué)生參加過的考試記錄,存在exams數(shù)組中,當想實現(xiàn)分頁查詢幾條數(shù)據(jù)的時候,需要用到$slice
$slice:[start,size] 第一個參數(shù)表示,數(shù)組開始的下標,第二個表示截取的數(shù)量
在后臺接收到前臺傳遞的pageSize和pageNumber時,需要計算出當前需要截取的下標,即let skip = (pageNumber-1)*pageSize
exports.getExamLogs = function (req, res){ let userName =req.session.userName; let name = req.param("name"); // 通過req.param()取到的值都是字符串,而limit()需要一個數(shù)字作為參數(shù) let pageSize = parseInt(req.param("pageSize")); let pageNumber = parseInt(req.param("pageNumber")); let skip = (pageNumber-1)*pageSize; // 跳過幾條 let reg = new RegExp(name,"i"); // 在nodejs中,必須要使用RegExp,來構(gòu)建正則表達式對象。 Student.findOne({"userName":userName},{"exams":{$slice:[skip,pageSize]}}).populate({path:"exams._paper",match:{name: reg}}) .exec((err,doc) => { if (err) { ... } else { if (doc) { res.json({ status: "0", msg:"success", result:doc, count: doc.exams.length?doc.exams.length:0 }) } else { ... } } }) };
每個試卷都是獨立的文檔,通過他們的名稱name實現(xiàn)模糊查詢
// 獲取考試信息 exports.getExams = function (req,res) { let userName =req.session.userName; let name = req.param("name"); // 通過req.param()取到的值都是字符串,而limit()需要一個數(shù)字作為參數(shù) let pageSize = parseInt(req.param("pageSize")); let pageNumber = parseInt(req.param("pageNumber")); let skip = (pageNumber-1)*pageSize; // 跳過幾條 let reg = new RegExp(name,"i"); // 在nodejs中,必須要使用RegExp,來構(gòu)建正則表達式對象。 Student.findOne({"userName":userName},(err,doc)=>{ if(err) { res.json({ status: "1", msg: err.message }) } else { if(doc) { // 關(guān)鍵在這里 Paper.find({startTime:{$exists:true},name:reg}).skip(skip).limit(pageSize).populate({path:"_questions"}).exec((err1,doc1)=>{ .... }) };
先通過populate查詢除關(guān)聯(lián)文檔,在模糊分頁查詢
exports.getPapers = function (req, res) { // console.log(req.session.userName); let name = req.param("name"), // 通過req.param()取到的值都是字符串,而limit()需要一個數(shù)字作為參數(shù) pageSize = parseInt(req.param("pageSize")), pageNumber = parseInt(req.param("pageNumber")), userName = req.session.userName; let skip = (pageNumber-1)*pageSize; // 跳過幾條 let reg = new RegExp(name,"i"); // 在nodejs中,必須要使用RegExp,來構(gòu)建正則表達式對象。 let params = { name: reg }; Teacher.findOne({"userName":userName}).populate({path:"_papers",match:{name: reg},options:{skip:skip,limit:pageSize}}) .exec((err, doc) => { .... }) };
mongodb本來就是非關(guān)系型的數(shù)據(jù)庫,但是有很多時候不同的集合直接是需要關(guān)聯(lián)的,這是就用到了mongoose提供的populate
直接看圖,不同集合直接的關(guān)聯(lián),用的就是_id,比如下圖中,學(xué)生參加的考試,關(guān)聯(lián)了試卷,試卷里面又關(guān)聯(lián)了題目
怎么查詢呢:
Student.findOne({}).populate({path:"exams._paper"}).exec(....)
更多的可以看看我項目中的實際代碼都在server/controllers下面
在系統(tǒng)中,教師可以增加試卷,這個時候我就不知道該怎么保存前臺傳過來的數(shù)據(jù)。數(shù)據(jù)中既有試卷的信息,也有很多題目。題目都屬于該試卷,改試卷又屬于當前登錄系統(tǒng)的老師(即創(chuàng)建試卷的老師)。
怎么才能讓試卷、教師、問題關(guān)聯(lián)起來啊,ref存的是_id,然而這些新增的數(shù)據(jù),是保存之后才有_id的。
exports.savePaper = function (req, res) { let paperForm = req.body.paperForm; let userName = req.session.userName; if(paperForm == {}){ res.json({ status:"5", msg: "數(shù)據(jù)不能為空" }) } // 第一步查找當前登錄的教師 Teacher.findOne({"userName": userName}, (err,doc)=>{ if (err) { ... } else { if (doc) { let paperData = { name:paperForm.name, totalPoints:paperForm.totalPoints, time:paperForm.time, _teacher: doc._id, // 這里就可以拿到教師的_id _questions: [], examnum:0 } // 第二步創(chuàng)建試卷 Paper.create(paperData,function (err1,doc1) { if (err1) { ... } else { if (doc1) { doc._papers.push(doc1._id); // 教師中添加該試卷的_id doc.save(); // 很重要 不save則沒有數(shù)據(jù) // 第三步 創(chuàng)建問題 paperForm._questions.forEach(item => { item._papers = []; item._papers.push(doc1._id); // 試卷中存入試卷的_id,因為此時已經(jīng)創(chuàng)建了試卷,所以可以拿到_id item._teacher = doc._id; // 試卷中存入教師的_id }) Question.create(paperForm._questions,function (err2,doc2) { if (err2) { ... } else { if (doc2) { doc2.forEach(item => { doc1._questions.push(item._id); // 當問題創(chuàng)建成功,則在試卷中存入問題的_id }) doc1.save(); res.json({ status:"0", msg: "success" }) } else { ... } } }) } else { ... } } }) } else { ... } } }) };
刪除某一個試卷,既要刪除教師中對應(yīng)的試卷_id,也要刪除問題中對應(yīng)的試卷_id
// 刪除試卷 exports.deletePaper = function (req, res) { let id = req.body.id; let userName = req.session.userName; // 第一步 刪除教師中的_id _papers是一個數(shù)組,所以用到了`$pull` Teacher.update({"userName":userName},{"$pull":{"_papers":{$in:id}}}, (err,doc)=>{ if (err) { res.json({ status:"1", msg: err.message }) } else { if (doc) { // 第二步 刪除試卷 即 移除一個文檔 Paper.remove({"_id":{$in:id}},function (err1,doc1){ if(err1) { res.json({ status:"1", msg: err1.message }) } else { if (doc1) { // 第三步 updateMany刪除多個問題中的_id 這里并沒有刪除試卷中包含的問題,是為了以后題庫做準備 Question.updateMany({"_papers":{$in:id}},{"$pull":{"_papers":{$in:id}}},function (err2,doc2) { if(err2){ ... } else { if (doc2){ ... } } }) } else { ... } } }) } else { ... } } }) };
// 修改試卷-修改試卷 exports.updatePaper = function (req,res) { let userName = req.session.userName; let params = req.body.params; let paperParams = { // 試卷需要更新的字段 name: params.name, totalPoints: params.totalPoints, time: params.time } let updateQuestion = []; // 需要更新的題目 let addQuestion = []; // 需要新增的題目 params._questions.forEach(item => { if(item._id) { // 通過判斷是否有_id區(qū)分已有的或者是新增的 updateQuestion.push(item); } else { addQuestion.push(item); } }) Teacher.findOne({"userName":userName},(err,doc)=>{ if (err) { ... } else { if (doc) { Paper.findOneAndUpdate({"_id":params._id},paperParams,(err1,doc1) => { if(err1) { ... }else { if(doc1){ updateQuestion.forEach((item,index)=>{ // 循環(huán)更新題目,好像很傻的方法,可能有更好的辦法 Question.update({"_id":item._id},item,(err2,doc2)=>{ if(err2){ res.json({ status:"1", msg: err2.message }) }else { if(doc2){ if(index == (updateQuestion.length-1)){ if (addQuestion.length>0){ addQuestion.forEach(item => { item._papers = []; item._papers.push(doc1._id); item._teacher = doc._id; }) // 創(chuàng)建新增題目 Question.create(addQuestion,(err3,doc3) => { if(err3) { ... } else { if(doc3) { doc3.forEach(item => { doc1._questions.push(item._id); // 還要將新增的題目關(guān)聯(lián)到試卷當中 }) doc1.save(); // 很重要 不save則沒有數(shù)據(jù) res.json({ status:"0", msg: "success" }) // .......................判斷太長省略........................ }) };
// 打分提交 exports.submitScore = function (req, res) { let name = req.param("userName"), date = req.param("date"), score = req.param("score") - 0, userName = req.session.userName; Teacher.findOne({"userName":userName},(err,doc) => { if(err) { ... } else { if(doc) { Student.update({"userName":name,"exams.date":date},{$set:{"exams.$.score":score,"exams.$.isSure":true}},(err1, doc1) => { if(err1) { ... } else { if(doc1) { ... } else { ... } } }) } else { ... } } }) };md5加密
//student.js const crypto = require("crypto"); let mdHash = function(data){ // hash 的定義要寫在這個方法內(nèi),不然會報錯Digest already called **** const hash = crypto.createHash("md5"); return hash.update(data).digest("hex"); } // 使用 //注冊 exports.register = function (req,res) { let userInfo = req.body.userInfo; 獲取到前臺傳過來的密碼,先加密再存儲 userInfo.passWord = mdHash(userInfo.passWord); ...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/19255.html
摘要:安裝配置文件找到,或者創(chuàng)建一個文件,將如下需要替換的字段換成自己的配置即可。默認是不需要修改配置文件的不同環(huán)境會加載不同的配置文件,在此之前你應(yīng)該對有所了解。學(xué)習群,美女多多。老司機快上車,來不及解釋了。 前言 很多小伙伴問我怎么在自己公司的項目里面添加配置mock,在vue項目里面都知道怎么配置mock,在大型前端項目里面就一臉疑惑了。showImg(https://segmentf...
摘要:簡介最近寫了一個基于權(quán)限管理系統(tǒng)的后臺模板,包含了正常項目開發(fā)所需的框架功能,開發(fā)者使用的時候只需要專注于項目的業(yè)務(wù)邏輯就好。同時接下來會讓你擁有一個自己完全掌控的框架。 簡介 最近寫了一個基于vue2.0+element-ui權(quán)限管理系統(tǒng)的后臺模板,包含了正常項目開發(fā)所需的框架功能,開發(fā)者使用的時候只需要專注于項目的業(yè)務(wù)邏輯就好。同時接下來會讓你擁有一個自己完全掌控的框架。 源碼地址...
摘要:直接上預(yù)覽鏈接基于換膚自定義主題項目增加主題組件在項目的下添加文件夾文件獲取地址項目增加自定義主題自定義添加主題下載地址項目引入和使用選擇你想要隨主題改變的元素在里面,不希望隨主題改變的可以注釋掉選擇皮膚之后把記錄存在里面。 0. 直接上 預(yù)覽鏈接 [vue2.0-基于elementui換膚[自定義主題]](https://mgbq.github.io/vue-pe... 1. ...
摘要:直接上預(yù)覽鏈接基于換膚自定義主題項目增加主題組件在項目的下添加文件夾文件獲取地址項目增加自定義主題自定義添加主題下載地址項目引入和使用選擇你想要隨主題改變的元素在里面,不希望隨主題改變的可以注釋掉選擇皮膚之后把記錄存在里面。 0. 直接上 預(yù)覽鏈接 [vue2.0-基于elementui換膚[自定義主題]](https://mgbq.github.io/vue-pe... 1. ...
閱讀 2975·2021-11-16 11:51
閱讀 2608·2021-09-22 15:02
閱讀 3723·2021-08-04 10:21
閱讀 3605·2019-08-30 15:43
閱讀 1947·2019-08-30 11:04
閱讀 3599·2019-08-29 17:14
閱讀 490·2019-08-29 12:16
閱讀 2933·2019-08-28 18:31