ChengYuXuan 5 år sedan
förälder
incheckning
bd712ff04e

+ 3 - 1
package.json

@@ -19,7 +19,8 @@
     "vue": "^2.6.11",
     "vue": "^2.6.11",
     "vue-router": "^3.2.0",
     "vue-router": "^3.2.0",
     "vuetify": "^2.2.11",
     "vuetify": "^2.2.11",
-    "vuex": "^3.4.0"
+    "vuex": "^3.4.0",
+    "vuex-persistedstate": "3.2.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@koa/cors": "^3.1.0",
     "@koa/cors": "^3.1.0",
@@ -35,6 +36,7 @@
     "@vue/cli-service": "~4.5.0",
     "@vue/cli-service": "~4.5.0",
     "@vue/eslint-config-prettier": "^6.0.0",
     "@vue/eslint-config-prettier": "^6.0.0",
     "@vue/eslint-config-typescript": "^5.0.2",
     "@vue/eslint-config-typescript": "^5.0.2",
+    "cross-env": "^7.0.3",
     "eslint": "^6.7.2",
     "eslint": "^6.7.2",
     "eslint-config-prettier": "^7.1.0",
     "eslint-config-prettier": "^7.1.0",
     "eslint-plugin-prettier": "^3.3.0",
     "eslint-plugin-prettier": "^3.3.0",

+ 49 - 0
src/api/axios.config.ts

@@ -0,0 +1,49 @@
+import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
+
+export default function axiosConfig() {
+  axios.interceptors.response.use(
+    function(response: AxiosResponse) {
+      if (process.env.NODE_ENV === 'development') {
+        console.log('[AXIOS] response value: ', response)
+      }
+
+      const responseData = response.data
+
+      if (responseData.err === 0) {
+        return Promise.resolve(response)
+      } else {
+        return Promise.reject(response)
+      }
+    },
+    function(error: AxiosError) {
+      if (process.env.NODE_ENV === 'development') {
+        console.error('[AXIOS] response error: ', error.response)
+      }
+
+      if (error.response) {
+        if (error.response.status === 401) {
+          jumpToLogin()
+        }
+        return Promise.reject(error.response)
+      }
+
+      return Promise.reject(error)
+    }
+  )
+}
+
+const portal = process.env.portalUrl
+
+export const getLoginUrl = () => {
+  const location = window.location
+  return `${portal}/api/interaction?client_id=seec-eval&redirect_uri=${encodeURIComponent(
+    location.origin + '/'
+  )}&scope=openid`
+}
+
+/**
+ * 跳转到登陆界面
+ */
+export const jumpToLogin = () => {
+  window.location.href = getLoginUrl()
+}

+ 1 - 0
src/api/prefix.ts

@@ -1,3 +1,4 @@
 const BASE_URL = '/api'
 const BASE_URL = '/api'
 
 
 export const EXAM_MODULE = BASE_URL + '/exam'
 export const EXAM_MODULE = BASE_URL + '/exam'
+export const QUESTION_MODULE = BASE_URL + '/question'

+ 12 - 0
src/api/question.ts

@@ -0,0 +1,12 @@
+import axios from 'axios'
+import { QUESTION_MODULE } from './prefix'
+import { Pageable, QuestionSerializer, ReceivedData } from '@/api/types'
+
+export const getQuestions = (params: Pageable) => {
+  return axios.get<ReceivedData<Array<QuestionSerializer>>>(
+    `${QUESTION_MODULE}/`,
+    {
+      params
+    }
+  )
+}

+ 11 - 0
src/api/types.d.ts

@@ -29,6 +29,8 @@ export type Pageable<T = {}> = T & {
 
 
 export type LocalDateTime = string
 export type LocalDateTime = string
 
 
+export type UserRole = 'STUDENT' | 'TEACHER'
+
 export type ExamStatus =
 export type ExamStatus =
   | 'READY' // 考试已准备好,考试尚未开始
   | 'READY' // 考试已准备好,考试尚未开始
   | 'AVAILABLE' // 考试可用,表示考试正在进行
   | 'AVAILABLE' // 考试可用,表示考试正在进行
@@ -112,3 +114,12 @@ export interface ExamQuestionSerializer {
   multiChoiceQuestions?: Array<ChoiceQuestionSerializer>
   multiChoiceQuestions?: Array<ChoiceQuestionSerializer>
   codeQuestions?: Array<CodeQuestionSerializer>
   codeQuestions?: Array<CodeQuestionSerializer>
 }
 }
+
+export interface UserSerializer {
+  id?: number
+  username?: string
+  email?: string
+  phone?: string
+  role?: UserRole
+  createdAt?: LocalDateTime
+}

+ 4 - 0
src/main.ts

@@ -3,7 +3,11 @@ import App from './App.vue'
 import router from './router'
 import router from './router'
 import store from './store'
 import store from './store'
 import vuetify from './plugins/vuetify'
 import vuetify from './plugins/vuetify'
+import axiosConfig from '@/api/axios.config'
 Vue.config.productionTip = false
 Vue.config.productionTip = false
+
+axiosConfig()
+
 new Vue({
 new Vue({
   router,
   router,
   store,
   store,

+ 4 - 0
src/mock/api/question/get.json

@@ -0,0 +1,4 @@
+{
+  "err": 0,
+  "res|10": ["@QUESTION_DETAIL_SERIALIZER"]
+}

+ 10 - 1
src/mock/serializers.js

@@ -35,7 +35,8 @@ const serializersPlugin = (mock) => ({
       joined: '@BOOLEAN',
       joined: '@BOOLEAN',
       isReentryPermitted: '@BOOLEAN',
       isReentryPermitted: '@BOOLEAN',
       inviteCode: '@WORD(5)',
       inviteCode: '@WORD(5)',
-      'difficulty|0-100': 1
+      'difficulty|0-100': 1,
+      comment: '@CPARAGRAPH'
     }),
     }),
   PAGE_SERIALIZER: () => ({
   PAGE_SERIALIZER: () => ({
     page: 1,
     page: 1,
@@ -133,7 +134,15 @@ const serializersPlugin = (mock) => ({
       'singleChoiceQuestions|10': ['@SINGLE_CHOICE_QUESTION_SERIALIZER'],
       'singleChoiceQuestions|10': ['@SINGLE_CHOICE_QUESTION_SERIALIZER'],
       'multiChoiceQuestions|5': ['@MULTI_CHOICE_QUESTION_SERIALIZER'],
       'multiChoiceQuestions|5': ['@MULTI_CHOICE_QUESTION_SERIALIZER'],
       'codeQuestions|2': ['@CODE_QUESTION_SERIALIZER']
       'codeQuestions|2': ['@CODE_QUESTION_SERIALIZER']
+    }),
+  QUESTION_DETAIL_SERIALIZER: () => {
+    mock({
+      'id|1-1000': 1,
+      stem: '@CPARAGRAPH',
+      knowledgeId: '@CWORD',
+      skillId: '@CWORD'
     })
     })
+  }
 })
 })
 
 
 module.exports = function(random, mock) {
 module.exports = function(random, mock) {

+ 17 - 7
src/store/index.ts

@@ -1,24 +1,34 @@
 import Vue from 'vue'
 import Vue from 'vue'
 import Vuex, { GetterTree, MutationTree } from 'vuex'
 import Vuex, { GetterTree, MutationTree } from 'vuex'
-import { ExamSerializer } from '@/api/types'
+import { UserSerializer } from '@/api/types'
+import createPersistedState from 'vuex-persistedstate'
 
 
 Vue.use(Vuex)
 Vue.use(Vuex)
 
 
 export type RootState = {
 export type RootState = {
-  examInfo: ExamSerializer
+  user: UserSerializer
+  token?: string
 }
 }
 
 
-const state = (): RootState => ({
-  examInfo: {}
+export const state = (): RootState => ({
+  user: {},
+  token: ''
 })
 })
 
 
-const mutations: MutationTree<RootState> = {
-  examInfo: (state, examInfo) => (state.examInfo = examInfo)
+export const mutations: MutationTree<RootState> = {
+  setToken: (state, token) => (state.token = token),
+  setUser: (state, user) => (state.user = user)
+}
+
+export const getters: GetterTree<RootState, RootState> = {
+  isStudent: (state) => state.user.role === 'STUDENT',
+  isTeacher: (state) => state.user.role === 'TEACHER'
 }
 }
 
 
 export default new Vuex.Store({
 export default new Vuex.Store({
   state: state,
   state: state,
   mutations: mutations,
   mutations: mutations,
   actions: {},
   actions: {},
-  modules: {}
+  modules: {},
+  plugins: [createPersistedState()]
 })
 })

+ 20 - 10
src/views/exam/ExamPane.vue

@@ -140,28 +140,38 @@
       <v-container fluid class="pa-2" v-if="currentQuestionIndex >= 0">
       <v-container fluid class="pa-2" v-if="currentQuestionIndex >= 0">
         <choice-pane
         <choice-pane
           ref="choicePane"
           ref="choicePane"
-          v-if="isChoiceProblem"
+          v-show="isChoiceProblem"
           @nextQuestion="nextQuestion"
           @nextQuestion="nextQuestion"
           @saveChoiceAnswer="saveChoiceAnswer"
           @saveChoiceAnswer="saveChoiceAnswer"
           :total-question-num="curTypeQuestionNum"
           :total-question-num="curTypeQuestionNum"
           :question="curChoiceQuestion"
           :question="curChoiceQuestion"
         ></choice-pane>
         ></choice-pane>
-        <div v-else>
+        <template v-show="!isChoiceProblem">
           <!--          用一个不显示的tabs,同时渲染所有的代码题面板,控制代码题的切换,使得代码切换保留已写代码-->
           <!--          用一个不显示的tabs,同时渲染所有的代码题面板,控制代码题的切换,使得代码切换保留已写代码-->
-          <v-tabs v-show="false" v-model="codeIndex">
-            <v-tab v-for="i in codeQuestionNum" :key="i"> {{ i }}</v-tab>
-          </v-tabs>
-          <v-tabs-items v-model="codeIndex">
-            <v-tab-item v-for="i in codeQuestionNum" :key="i">
+          <!--          <v-tabs v-show="false" v-model="codeIndex">-->
+          <!--            <v-tab v-for="i in codeQuestionNum" :key="i"> {{ i }}</v-tab>-->
+          <!--          </v-tabs>-->
+          <v-window v-model="codeIndex">
+            <v-window-item v-for="i in codeQuestionNum" :key="i">
               <code-pane
               <code-pane
                 :question="codeQuestions[i - 1]"
                 :question="codeQuestions[i - 1]"
                 :total-question-num="curTypeQuestionNum"
                 :total-question-num="curTypeQuestionNum"
                 @nextQuestion="nextQuestion"
                 @nextQuestion="nextQuestion"
                 @submitCode="saveSubmittedCode"
                 @submitCode="saveSubmittedCode"
               ></code-pane>
               ></code-pane>
-            </v-tab-item>
-          </v-tabs-items>
-        </div>
+            </v-window-item>
+          </v-window>
+          <!--          <v-tabs-items v-model="codeIndex">-->
+          <!--            <v-tab-item v-for="i in codeQuestionNum" :key="i">-->
+          <!--              <code-pane-->
+          <!--                :question="codeQuestions[i - 1]"-->
+          <!--                :total-question-num="curTypeQuestionNum"-->
+          <!--                @nextQuestion="nextQuestion"-->
+          <!--                @submitCode="saveSubmittedCode"-->
+          <!--              ></code-pane>-->
+          <!--            </v-tab-item>-->
+          <!--          </v-tabs-items>-->
+        </template>
       </v-container>
       </v-container>
       <v-snackbar v-model="noticeUnfinished">
       <v-snackbar v-model="noticeUnfinished">
         还有题目未作答
         还有题目未作答

+ 20 - 4
src/views/teacher/ExamCreate.vue

@@ -158,10 +158,12 @@
           label="备注"
           label="备注"
           class="ma-4 mb-8"
           class="ma-4 mb-8"
         ></v-text-field>
         ></v-text-field>
-
         <v-btn color="success" class="ma-4" @click="onCreateExam">
         <v-btn color="success" class="ma-4" @click="onCreateExam">
           提交创建
           提交创建
         </v-btn>
         </v-btn>
+        <v-btn color="primary" class="ma-4" @click="selectQuestion = true">
+          手动选择试题
+        </v-btn>
       </v-form>
       </v-form>
     </v-container>
     </v-container>
 
 
@@ -188,19 +190,27 @@
         <v-card-title>样卷</v-card-title>
         <v-card-title>样卷</v-card-title>
       </v-container>
       </v-container>
     </v-container>
     </v-container>
+
+    <v-container v-if="selectQuestion">
+      <v-overlay :value="true" :dark="false" absolute>
+        <question-list></question-list>
+      </v-overlay>
+    </v-container>
   </v-container>
   </v-container>
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
 import Vue from 'vue'
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
 import PageHeader from '@/components/PageHeader.vue'
+import QuestionList from '@/views/teacher/components/QuestionList.vue'
 import { createExam } from '@/api/exam'
 import { createExam } from '@/api/exam'
-import { ExamSerializer } from '@/api/types'
+import { ExamSerializer, ID } from '@/api/types'
 
 
 export default Vue.extend({
 export default Vue.extend({
   name: 'ExamCreate',
   name: 'ExamCreate',
   components: {
   components: {
-    PageHeader
+    PageHeader,
+    QuestionList
   },
   },
 
 
   data() {
   data() {
@@ -244,7 +254,9 @@ export default Vue.extend({
       endDate: '',
       endDate: '',
       endDateFormatted: '',
       endDateFormatted: '',
       endTime: '',
       endTime: '',
-      comment: ''
+      comment: '',
+      selectedQuestions: [] as Array<ID>,
+      selectQuestion: false
     }
     }
   },
   },
   computed: {
   computed: {
@@ -266,6 +278,10 @@ export default Vue.extend({
   },
   },
 
 
   methods: {
   methods: {
+    saveSelectedQuestions(questions: Array<ID>) {
+      console.log(questions)
+      this.selectedQuestions = questions
+    },
     formatDate(date: string): string {
     formatDate(date: string): string {
       if (!date) return ''
       if (!date) return ''
 
 

+ 114 - 8
src/views/teacher/components/QuestionList.vue

@@ -1,21 +1,127 @@
 <template>
 <template>
-  <div></div>
+  <v-container>
+    <v-card>
+      <!--      <v-card-title>已选题目</v-card-title>-->
+      <!--      <v-list class="px-10 flex">-->
+      <!--        <v-list-item-group multiple>-->
+      <!--          <v-divider></v-divider>-->
+      <!--          <template v-for="question in selectedQuestions">-->
+      <!--            <v-list-item :key="question.id" :value="question.id">-->
+      <!--              <template v-slot:default="{ active }">-->
+      <!--                <v-list-item-action>-->
+      <!--                  <v-checkbox hide-details :input-value="active"> </v-checkbox>-->
+      <!--                </v-list-item-action>-->
+      <!--                <v-list-item-content>-->
+      <!--                  <v-list-item-title class="text-wrap">-->
+      <!--                    {{ question.stem }}</v-list-item-title-->
+      <!--                  >-->
+      <!--                </v-list-item-content>-->
+      <!--              </template>-->
+      <!--            </v-list-item>-->
+      <!--            <v-divider :key="' ' + question.id"></v-divider>-->
+      <!--          </template>-->
+      <!--        </v-list-item-group>-->
+      <!--      </v-list>-->
+      <!--      <v-card-title>题目列表</v-card-title>-->
+      <!--      <v-list class="px-10 flex">-->
+      <!--        <v-list-item-group v-model="selectedId" multiple>-->
+      <!--          <v-divider></v-divider>-->
+      <!--          <template v-for="question in questions">-->
+      <!--            <v-list-item :key="question.id" :value="question.id">-->
+      <!--              <template v-slot:default="{ active }">-->
+      <!--                <v-list-item-action>-->
+      <!--                  <v-checkbox hide-details :input-value="active"> </v-checkbox>-->
+      <!--                </v-list-item-action>-->
+      <!--                <v-list-item-content>-->
+      <!--                  <v-list-item-title class="text-wrap">-->
+      <!--                    {{ question.stem }}</v-list-item-title-->
+      <!--                  >-->
+      <!--                </v-list-item-content>-->
+      <!--              </template>-->
+      <!--            </v-list-item>-->
+      <!--            <v-divider :key="' ' + question.id"></v-divider>-->
+      <!--          </template>-->
+      <!--        </v-list-item-group>-->
+      <!--        <v-card-actions>-->
+      <!--          <v-btn color="primary" @click="fetchData">下一页</v-btn>-->
+      <!--        </v-card-actions>-->
+      <!--      </v-list>-->
+      <v-data-table
+        v-model="selectedId"
+        item-key="id"
+        show-select
+        :headers="headers"
+        :items="questions"
+        show-expand
+        style="width: 50vw"
+        class="text-wrap"
+      >
+        <template v-slot:expanded-item="{ headers, item }">
+          <td :colspan="headers.length">
+            {{ item.stem }}
+          </td>
+        </template>
+      </v-data-table>
+    </v-card>
+  </v-container>
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
 import Vue, { PropType } from 'vue'
 import Vue, { PropType } from 'vue'
-import { ChoiceQuestionSerializer, Pageable } from '@/api/types'
+import { ChoiceQuestionSerializer, ID, Pageable } from '@/api/types'
+import { getQuestions } from '@/api/question'
 
 
 export default Vue.extend({
 export default Vue.extend({
   name: 'QuestionList',
   name: 'QuestionList',
-  props: {
-    questions: {
-      required: true,
-      type: Object as PropType<Pageable<ChoiceQuestionSerializer>>
+  props: {},
+  data() {
+    return {
+      questions: [] as Array<ChoiceQuestionSerializer>,
+      selectedId: [] as Array<ID>,
+      selectedQuestions: [] as Array<ChoiceQuestionSerializer>,
+      headers: [
+        { text: '技能点', value: 'skillId' },
+        { text: '知识点', value: 'knowledgeId' },
+        // { text: '题干', value: 'stem', sortable: false },
+        { text: '题干', value: 'data-table-expand' }
+      ]
     }
     }
   },
   },
-  data() {
-    return {}
+  watch: {
+    selectedId: {
+      deep: true,
+      handler: function(newVal: Array<ID>, oldVal: Array<ID>): void {
+        if (newVal.length > oldVal.length) {
+          const changedId = newVal[newVal.length - 1]
+          const changeQuestion = this.questions.find((val) => {
+            return val.id === changedId
+          })
+          if (changeQuestion !== undefined) {
+            this.selectedQuestions.push(changeQuestion)
+          }
+        } else {
+          const changeId = oldVal.filter((val) => {
+            return !newVal.find((value) => {
+              return value === val
+            })
+          })[0]
+          this.selectedQuestions = this.selectedQuestions.filter((val) => {
+            return val.id !== changeId
+          })
+        }
+      }
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  mounted() {},
+  methods: {
+    fetchData() {
+      getQuestions({}).then(({ data }) => {
+        this.questions = data.res ?? []
+      })
+    }
   }
   }
 })
 })
 </script>
 </script>

+ 1 - 1
vue.config.js

@@ -2,7 +2,7 @@ const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
 
 
 module.exports = {
 module.exports = {
   transpileDependencies: ['vuetify'],
   transpileDependencies: ['vuetify'],
-  // productionSourceMap: false,
+  productionSourceMap: process.env.NODE_ENV === 'development',
   devServer: {
   devServer: {
     proxy: {
     proxy: {
       '^/api': {
       '^/api': {

+ 21 - 1
yarn.lock

@@ -2950,6 +2950,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
     safe-buffer "^5.0.1"
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
     sha.js "^2.4.8"
 
 
+cross-env@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.npm.taobao.org/cross-env/download/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha1-hlJkspZ33AFbqEGJGJZd0jL8VM8=
+  dependencies:
+    cross-spawn "^7.0.1"
+
 cross-spawn@^5.0.1:
 cross-spawn@^5.0.1:
   version "5.1.0"
   version "5.1.0"
   resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
   resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@@ -2970,7 +2977,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
     shebang-command "^1.2.0"
     shebang-command "^1.2.0"
     which "^1.2.9"
     which "^1.2.9"
 
 
-cross-spawn@^7.0.0:
+cross-spawn@^7.0.0, cross-spawn@^7.0.1:
   version "7.0.3"
   version "7.0.3"
   resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha1-9zqFudXUHQRVUcF34ogtSshXKKY=
   integrity sha1-9zqFudXUHQRVUcF34ogtSshXKKY=
@@ -8058,6 +8065,11 @@ shelljs@^0.8.3:
     interpret "^1.0.0"
     interpret "^1.0.0"
     rechoir "^0.6.2"
     rechoir "^0.6.2"
 
 
+shvl@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npm.taobao.org/shvl/download/shvl-2.0.2.tgz#eca7decb9bbd4e8dd93f06ea9ab94036319cd351"
+  integrity sha1-7Kfey5u9To3ZPwbqmrlANjGc01E=
+
 signal-exit@^3.0.0, signal-exit@^3.0.2:
 signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.3"
   version "3.0.3"
   resolved "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   resolved "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@@ -9177,6 +9189,14 @@ vuetify@^2.2.11:
   resolved "https://registry.npm.taobao.org/vuetify/download/vuetify-2.4.0.tgz#6aa2616b0fecb0614cf85052b0ffbb5cad80bf92"
   resolved "https://registry.npm.taobao.org/vuetify/download/vuetify-2.4.0.tgz#6aa2616b0fecb0614cf85052b0ffbb5cad80bf92"
   integrity sha1-aqJhaw/ssGFM+FBSsP+7XK2Av5I=
   integrity sha1-aqJhaw/ssGFM+FBSsP+7XK2Av5I=
 
 
+vuex-persistedstate@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.npm.taobao.org/vuex-persistedstate/download/vuex-persistedstate-3.2.0.tgz#41d3ea304404769ac653ec020de80c95ce16243d"
+  integrity sha1-QdPqMEQEdprGU+wCDegMlc4WJD0=
+  dependencies:
+    deepmerge "^4.2.2"
+    shvl "^2.0.2"
+
 vuex@^3.4.0:
 vuex@^3.4.0:
   version "3.6.0"
   version "3.6.0"
   resolved "https://registry.npm.taobao.org/vuex/download/vuex-3.6.0.tgz#95efa56a58f7607c135b053350833a09e01aa813"
   resolved "https://registry.npm.taobao.org/vuex/download/vuex-3.6.0.tgz#95efa56a58f7607c135b053350833a09e01aa813"