Kaynağa Gözat

style: 页码条、创建题目样式

ChenYangfan 6 yıl önce
ebeveyn
işleme
42b7272d42

+ 7 - 6
src/components/Basic/CButton.vue

@@ -29,7 +29,7 @@ export default {
       type: Boolean,
       default: false
     },
-    // 使用白色字体、深色背景的最显眼按钮
+    // 使用白色字体、深色背景的最显眼按钮,优先级大于 text
     standout: {
       type: Boolean,
       default: false
@@ -74,11 +74,11 @@ export default {
     },
     classList () {
       return [
-        this.text
-          ? (this.theme ? 'c-button-text--' + this.theme : 'c-button-text--normal')
+        this.standout
+          ? (this.theme ? 'c-button-standout--' + this.theme : 'c-button-standout--normal')
           : (
-            this.standout
-              ? (this.theme ? 'c-button-standout--' + this.theme : 'c-button-standout--normal')
+            this.text
+              ? (this.theme ? 'c-button-text--' + this.theme : 'c-button-text--normal')
               : (this.theme ? 'c-button--' + this.theme : 'c-button--normal')
           ),
         this.large ? 'c-button--large' : '',
@@ -99,7 +99,7 @@ button {
   border: none;
   outline: none;
   cursor: pointer;
-  display: flex;
+  display: inline-flex;
   align-items: center;
   justify-content: center;
   -webkit-tap-highlight-color: transparent;
@@ -123,6 +123,7 @@ button {
 }
 
 .c-button--block {
+  display: flex;
   width: 100%;
 }
 

+ 1 - 0
src/components/Basic/IconButton.vue

@@ -79,6 +79,7 @@ export default {
   color: $icon-normal-color;
   transition: 0.2s;
   overflow: hidden;
+  clip-path: border-box;
 
   &:hover {
     background: $theme-background-grey;

+ 7 - 1
src/components/Basic/SearchBar.vue

@@ -2,13 +2,14 @@
   <div class="search-bar">
     <label>
       <input
+        ref="input"
         type="text"
         :value="value"
         :placeholder="placeholder || '输入关键词搜索'"
         @input="$emit('input', $event.target.value)"
       />
     </label>
-    <c-icon bold :size="24">search</c-icon>
+    <c-icon bold :size="24" @click="handleIconClick">{{this.value ? 'close' : 'search'}}</c-icon>
   </div>
 </template>
 
@@ -29,6 +30,11 @@ export default {
     return {
       iconMap: iconMap
     }
+  },
+  methods: {
+    handleIconClick () {
+      this.value ? this.value = '' : this.$refs.input.focus()
+    }
   }
 }
 </script>

+ 9 - 229
src/components/Editor.vue

@@ -37,7 +37,7 @@ export default {
       type: String
     }
   },
-  data() {
+  data () {
     return {
       editor: null
     }
@@ -45,14 +45,14 @@ export default {
   watch: {
     initContent: {
       immediate: true,
-      handler(val) {
+      handler (val) {
         // eslint-disable-next-line no-unused-expressions
         this.editor?.setContent(markdownToHTML(val))
         this.$emit('update-markdown', val)
       }
     }
   },
-  mounted() {
+  mounted () {
     this.editor = new Editor({
       extensions: [
         new Image(),
@@ -72,238 +72,18 @@ export default {
         this.$emit('update-json', data.getJSON())
         this.$emit(
           'update-markdown',
-            jsonToMarkdown(data.getJSON())?.trim() ?? ''
+          jsonToMarkdown(data.getJSON())?.trim() ?? ''
         )
       }
     })
   },
-  beforeDestroy() {
-      // eslint-disable-next-line no-unused-expressions
-      this.editor?.destroy()
+  beforeDestroy () {
+    // eslint-disable-next-line no-unused-expressions
+    this.editor?.destroy()
   }
 }
 </script>
-<style lang="scss">
-.editor p.is-editor-empty:first-child::before {
-  content: attr(data-empty-text);
-  float: left;
-  color: #aaa;
-  pointer-events: none;
-  height: 0;
-  font-style: italic;
-}
-
-.editor {
-  font-family: 'Inter';
-  font-size: 18px;
-  color: black;
-  line-height: 1.5;
-
-  &__content {
-
-    strong {
-      font-weight: bold;
-    }
-
-    em {
-      font-style: italic;
-    }
-
-    &:focus {
-      outline: none;
-    }
-
-    * {
-      margin: 0;
-      padding: 0;
-      box-sizing: border-box;
-      text-size-adjust: 100%;
-      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-      -webkit-touch-callout: none;
-      -webkit-font-smoothing: antialiased;
-      -moz-osx-font-smoothing: grayscale;
-      text-rendering: optimizeLegibility;
-
-      &:focus {
-        outline: none;
-      }
-    }
-
-    *::before,
-    *::after {
-      box-sizing: border-box;
-    }
-
-    a {
-      color: inherit;
-    }
-
-    h1,
-    h2,
-    h3,
-    p,
-    ul,
-    ol,
-    pre,
-    blockquote {
-      margin: 1rem 0;
-
-      &:first-child {
-        margin-top: 0;
-      }
-
-      &:last-child {
-        margin-bottom: 0;
-      }
-    }
-
-    h1,
-    h2,
-    h3 {
-      line-height: 1.3;
-    }
-
-    .button {
-      font-weight: bold;
-      display: inline-flex;
-      border: 0;
-      color: black;
-      padding: 0.2rem 0.5rem;
-      margin-right: 0.2rem;
-      border-radius: 3px;
-      cursor: pointer;
-      background-color: rgba(black, 0.1);
-
-      &:hover {
-        background-color: rgba(black, 0.15);
-      }
-    }
-
-    .message {
-      background-color: rgba(black, 0.05);
-      color: rgba(black, 0.7);
-      padding: 1rem;
-      border-radius: 6px;
-      margin-bottom: 1.5rem;
-      font-style: italic;
-    }
-
-    overflow-wrap: break-word;
-    word-wrap: break-word;
-    word-break: break-word;
-
-    * {
-      caret-color: currentColor;
-    }
-
-    pre {
-      padding: 0.7rem 1rem;
-      border-radius: 5px;
-      background: black;
-      color: white;
-      font-size: 0.8rem;
-      overflow-x: auto;
-
-      code {
-        display: block;
-      }
-    }
-
-    p code {
-      display: inline-block;
-      padding: 0 0.4rem;
-      border-radius: 5px;
-      font-size: 0.8rem;
-      font-weight: bold;
-      background: rgba(black, 0.1);
-      color: rgba(black, 0.8);
-    }
-
-    ul,
-    ol {
-      padding-left: 1rem;
-    }
-
-    li > p,
-    li > ol,
-    li > ul {
-      margin: 0;
-    }
-
-    a {
-      color: inherit;
-    }
-
-    blockquote {
-      border-left: 3px solid rgba(black, 0.1);
-      color: rgba(black, 0.8);
-      padding-left: 0.8rem;
-      font-style: italic;
-
-      p {
-        margin: 0;
-      }
-    }
-
-    img {
-      max-width: 100%;
-      border-radius: 3px;
-    }
-
-    table {
-      border-collapse: collapse;
-      table-layout: fixed;
-      width: 100%;
-      margin: 0;
-      overflow: hidden;
-
-      td, th {
-        min-width: 1em;
-        border: 2px solid grey;
-        padding: 3px 5px;
-        vertical-align: top;
-        box-sizing: border-box;
-        position: relative;
-        > * {
-          margin-bottom: 0;
-        }
-      }
-
-      th {
-        font-weight: bold;
-        text-align: left;
-      }
-
-      .selectedCell:after {
-        z-index: 2;
-        position: absolute;
-        content: "";
-        left: 0; right: 0; top: 0; bottom: 0;
-        background: rgba(200, 200, 255, 0.4);
-        pointer-events: none;
-      }
-
-      .column-resize-handle {
-        position: absolute;
-        right: -2px; top: 0; bottom: 0;
-        width: 4px;
-        z-index: 20;
-        background-color: #adf;
-        pointer-events: none;
-      }
-    }
-
-    .tableWrapper {
-      margin: 1em 0;
-      overflow-x: auto;
-    }
-
-    .resize-cursor {
-      cursor: ew-resize;
-      cursor: col-resize;
-    }
-
-  }
-}
 
+<style lang="scss">
+@import '~@/style/quiz-editor.scss';
 </style>

+ 67 - 114
src/components/Pagination.vue

@@ -1,65 +1,49 @@
 <template>
   <div class="pagination-wrapper">
-    <div class="pagination">
-      <span class="pagination__arrow icon" @click="handleChange({ page: page - 1 })">&lt;</span>
-      <span
-        :class="{ 'pagination__page': true, '--active': page === 1 }"
-        @click="handleChange({ page: 1 })"
-      >1</span>
-      <span
-        v-if="hasLeftDot"
-        class="pagination__hidden-arrow"
-        @click="handleChange({ page: page - SHOW_PAGE_NUM })"
-      >
-        <span class="icon">
-          《
-        </span>
-        <span class="pagination__hidden-ellipsis">•••</span>
-      </span>
-      <span
+    <div class="pagination mt-16">
+      <c-button :theme="theme" :size="32" :left="12" :right="4" @click="handleChange({ page: page - 1 })">
+        <c-icon bold :theme="theme" :size="16">keyboard_arrow_left</c-icon>
+      </c-button>
+      <c-button text :theme="theme" @click="handleChange({ page: 1 })" :standout="page === 1">1</c-button>
+      <c-button :theme="theme" class="hidden-arrow-left" v-if="hasLeftDot" :size="32" :left="4" :right="4"
+                @click="handleChange({ page: page - SHOW_PAGE_NUM })">
+        <c-icon :theme="theme" :size="18">skip_previous</c-icon>
+      </c-button>
+      <c-button
+        :theme="theme" text :standout="page === item"
         v-for="item in centerPages"
-        :key="item"
-        :class="{ 'pagination__page': true, '--active': page === item }"
+        :key="item" :right="4" :left="4"
         @click="handleChange({ page: item })"
-      >{{ item }}</span>
-      <span
-        v-if="hasRightDot"
-        class="pagination__hidden-arrow --right"
-        @click="handleChange({ page: page + SHOW_PAGE_NUM })"
-      >
-        <span class="icon">
-          《
-        </span>
-        <span class="pagination__hidden-ellipsis">•••</span>
-      </span>
-      <span
-        v-if="totalPage > 1"
-        :class="{ 'pagination__page': true, '--active': page === totalPage }"
-        @click="handleChange({ page: totalPage })"
-      >{{ totalPage }}</span>
-      <span
-        class="pagination__arrow --right icon"
-        @click="handleChange({ page: page + 1 })"
-      >&lt;</span>
+      >{{ item }}
+      </c-button>
+      <c-button :theme="theme" class="hidden-arrow-left" v-if="hasRightDot" :size="32" :left="4" :right="4"
+                @click="handleChange({ page: page + SHOW_PAGE_NUM })">
+        <c-icon :theme="theme" :size="18">skip_next</c-icon>
+      </c-button>
+      <c-button text :theme="theme" @click="handleChange({ page: totalPage })" :standout="page === totalPage">
+        {{ totalPage }}
+      </c-button>
+      <c-button :theme="theme" :size="32" :right="20" :left="4" @click="handleChange({ page: page + 1 })">
+        <c-icon :theme="theme" bold :size="16">keyboard_arrow_right</c-icon>
+      </c-button>
     </div>
-    <div class="pagination-input">
-      <input
-        v-model="inputPage"
-        class="search-input"
-        style="width: 50px; margin-right: 5px"
-        @keypress="handlePageInputEvent"
-      />
-      <span>of {{ totalPage }}</span>
+    <div class="mt-16">
+      <c-input v-model="inputPage" class="page-input" @keypress="handlePageInputEvent"></c-input>
+      <span>/ {{ totalPage }}</span>
     </div>
   </div>
 </template>
 
 <script>
 import iconMap from '@/utils/icon-map'
+import CButton from '@/components/Basic/CButton'
+import CIcon from '@/components/Basic/CIcon'
+import CInput from '@/components/Basic/CInput'
 
 const SHOW_PAGE_NUM = 3
 
 export default {
+  components: { CInput, CIcon, CButton },
   props: {
     page: {
       default: 1,
@@ -72,9 +56,13 @@ export default {
     totalElements: {
       type: Number,
       default: 0
+    },
+    theme: {
+      type: String,
+      default: 'blue'
     }
   },
-  data() {
+  data () {
     return {
       inputPage: String(this.page),
       iconMap,
@@ -82,25 +70,27 @@ export default {
     }
   },
   computed: {
-    totalPage() {
+    totalPage () {
       return Math.ceil(this.totalElements / this.size)
     },
-    hasLeftDot() {
+    hasLeftDot () {
       return this.page >= SHOW_PAGE_NUM && this.totalPage >= SHOW_PAGE_NUM + 2
     },
-    hasRightDot() {
+    hasRightDot () {
       return (
         this.page <= this.totalPage - (SHOW_PAGE_NUM - 1) &&
         this.totalPage >= SHOW_PAGE_NUM + 2
       )
     },
-    centerPages() {
+    centerPages () {
       const theCenter =
         this.page < ((SHOW_PAGE_NUM + 1) / 2)
           ? ((SHOW_PAGE_NUM + 1) / 2)
-          : this.page > this.totalPage - 2
-            ? this.totalPage - 2
-            : this.page
+          : (
+            this.page > this.totalPage - 2
+              ? this.totalPage - 2
+              : this.page
+          )
 
       return Array(SHOW_PAGE_NUM)
         .fill(0)
@@ -109,12 +99,12 @@ export default {
     }
   },
   methods: {
-    handleChange({ page = this.page, size = this.size }) {
+    handleChange ({ page = this.page, size = this.size }) {
       const thePage =
         page < 1 ? 1 : page > this.totalPage ? this.totalPage : page
       this.$emit('change', { page: thePage, size })
     },
-    handlePageInputEvent(event) {
+    handlePageInputEvent (event) {
       if (event.key === 'Enter') {
         event.preventDefault()
         this.handleChange({ page: this.inputPage })
@@ -127,24 +117,14 @@ export default {
 <style lang="scss" scoped>
 @import "~@/style/theme.scss";
 
-$pagination-font-color: inherit;
-$pagination-font-color--hover: inherit;
-$pagination-page-background--hover: $theme-blue-transparent;
-$pagination-page-background--active: $theme-blue;
-
-.icon{
-  font-family: 'iconfont', sans-serif;
-}
-
 .pagination-wrapper {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   justify-content: center;
-  color: $pagination-font-color;
 }
 
-.pagination-input{
+.pagination-input {
   display: flex;
   align-items: center;
 }
@@ -153,57 +133,30 @@ $pagination-page-background--active: $theme-blue;
   display: flex;
   align-items: center;
   justify-content: center;
+}
 
-  &__arrow,
-  &__page,
-  &__hidden-arrow {
-    line-height: 1em;
-    display: block;
-    transition: background-color 0.3s;
-    padding: 8px 10px;
-    margin: 0 4px;
-    cursor: pointer;
-    border-radius: 2px;
+.c-button {
+  border-radius: 1000px;
+  width: 34px;
+  height: 34px;
+  margin: 0 8px;
+}
 
-    &:hover {
-      color: $pagination-font-color--hover;
-      background: $pagination-page-background--hover;
-    }
-  }
-  &__page.--active {
-    color: white;
-    background-color: $pagination-page-background--active;
-  }
+.page-input {
+  width: 64px;
+  height: 32px;
+  border-radius: 0;
+  margin-right: 8px;
+  color: #7a8a9a;
 
-  &__arrow {
-    &.--right {
-      transform: rotate(180deg);
-    }
+  & + span{
+    color: #5a6a7a;
   }
+}
 
-  &__hidden-arrow {
-    position: relative;
-    .icon {
-      position: absolute;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-      opacity: 0;
-    }
-    &:hover {
-      .icon {
-        opacity: 1;
-      }
-      .pagination__hidden-ellipsis {
-        opacity: 0;
-      }
-    }
-
-    &.--right {
-      .icon {
-        transform: translate(-50%, -50%) rotate(180deg);
-      }
-    }
+@media (max-width: 450px) {
+  .c-button {
+    margin: 0 4px;
   }
 }
 </style>

+ 10 - 32
src/components/QuestionKindBar.vue

@@ -1,21 +1,22 @@
 <template>
   <div class="question-kind-bar">
-    <div v-for="eachKind of QuestionKind.default" :class="eachKind === value ? 'active' : ''"
-         :key="eachKind"
-         @click="handleChangeState(eachKind)">{{ QuestionKind.naming[eachKind] }}
-    </div>
+    <c-button v-for="eachKind of QuestionKind.default"
+              :right="12" class="mb-16"
+              :theme="eachKind === value ? 'blue' : 'normal'"
+              :key="eachKind" @click="handleChangeState(eachKind)">
+      {{ QuestionKind.naming[eachKind] }}
+    </c-button>
   </div>
 </template>
 
 <script>
 import * as QuestionKind from '@/server/enums/question-kind'
+import CButton from '@/components/Basic/CButton'
 
 export default {
+  components: { CButton },
   props: {
-    value: {
-      type: String,
-      required: true
-    }
+    value: String
   },
   data () {
     return {
@@ -31,28 +32,5 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-  @import '~@/style/theme.scss';
-
-  .question-kind-bar {
-    display: flex;
-    align-items: center;
-    justify-content: space-around;
-    font-size: 14px;
-    font-weight: bold;
-    position: relative;
-    margin: 0 0 24px 0;
-
-    & > div {
-      cursor: pointer;
-      color: #9aaaba;
-      display: inline-block;
-      padding: 4px 8px;
-      transition: .2s;
-    }
-
-    .active {
-      background: $theme-green-transparent;
-      color: $theme-green;
-    }
-  }
+@import '~@/style/theme.scss';
 </style>

+ 220 - 0
src/style/quiz-editor.scss

@@ -0,0 +1,220 @@
+.editor {
+  font-family: 'Inter';
+  font-size: 16px;
+  color: black;
+  line-height: 1.5;
+
+  &__content {
+
+    strong {
+      font-weight: bold;
+    }
+
+    em {
+      font-style: italic;
+    }
+
+    &:focus {
+      outline: none;
+    }
+
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+      text-size-adjust: 100%;
+      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+      -webkit-touch-callout: none;
+      -webkit-font-smoothing: antialiased;
+      -moz-osx-font-smoothing: grayscale;
+      text-rendering: optimizeLegibility;
+
+      &:focus {
+        outline: none;
+      }
+    }
+
+    *::before,
+    *::after {
+      box-sizing: border-box;
+    }
+
+    a {
+      color: inherit;
+    }
+
+    h1,
+    h2,
+    h3,
+    p,
+    ul,
+    ol,
+    pre,
+    blockquote {
+      margin: 1rem 0;
+
+      &:first-child {
+        margin-top: 0;
+      }
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    h1,
+    h2,
+    h3 {
+      line-height: 1.3;
+    }
+
+    .button {
+      font-weight: bold;
+      display: inline-flex;
+      border: 0;
+      color: black;
+      padding: 0.2rem 0.5rem;
+      margin-right: 0.2rem;
+      border-radius: 3px;
+      cursor: pointer;
+      background-color: rgba(black, 0.1);
+
+      &:hover {
+        background-color: rgba(black, 0.15);
+      }
+    }
+
+    .message {
+      background-color: rgba(black, 0.05);
+      color: rgba(black, 0.7);
+      padding: 1rem;
+      border-radius: 6px;
+      margin-bottom: 1.5rem;
+      font-style: italic;
+    }
+
+    overflow-wrap: break-word;
+    word-wrap: break-word;
+    word-break: break-word;
+
+    * {
+      caret-color: currentColor;
+    }
+
+    pre {
+      padding: 0.7rem 1rem;
+      border-radius: 5px;
+      background: black;
+      color: white;
+      font-size: 0.8rem;
+      overflow-x: auto;
+
+      code {
+        display: block;
+      }
+    }
+
+    p code {
+      display: inline-block;
+      padding: 0 0.4rem;
+      border-radius: 5px;
+      font-size: 0.8rem;
+      font-weight: bold;
+      background: rgba(black, 0.1);
+      color: rgba(black, 0.8);
+    }
+
+    ul,
+    ol {
+      padding-left: 1rem;
+    }
+
+    li > p,
+    li > ol,
+    li > ul {
+      margin: 0;
+    }
+
+    a {
+      color: inherit;
+    }
+
+    blockquote {
+      border-left: 3px solid rgba(black, 0.1);
+      color: rgba(black, 0.8);
+      padding-left: 0.8rem;
+      font-style: italic;
+
+      p {
+        margin: 0;
+      }
+    }
+
+    img {
+      max-width: 100%;
+      border-radius: 3px;
+    }
+
+    table {
+      border-collapse: collapse;
+      table-layout: fixed;
+      width: 100%;
+      margin: 0;
+      overflow: hidden;
+
+      td, th {
+        min-width: 1em;
+        border: 2px solid grey;
+        padding: 3px 5px;
+        vertical-align: top;
+        box-sizing: border-box;
+        position: relative;
+        > * {
+          margin-bottom: 0;
+        }
+      }
+
+      th {
+        font-weight: bold;
+        text-align: left;
+      }
+
+      .selectedCell:after {
+        z-index: 2;
+        position: absolute;
+        content: "";
+        left: 0; right: 0; top: 0; bottom: 0;
+        background: rgba(200, 200, 255, 0.4);
+        pointer-events: none;
+      }
+
+      .column-resize-handle {
+        position: absolute;
+        right: -2px; top: 0; bottom: 0;
+        width: 4px;
+        z-index: 20;
+        background-color: #adf;
+        pointer-events: none;
+      }
+    }
+
+    .tableWrapper {
+      margin: 1em 0;
+      overflow-x: auto;
+    }
+
+    .resize-cursor {
+      cursor: ew-resize;
+      cursor: col-resize;
+    }
+  }
+}
+
+.editor p.is-editor-empty:first-child::before {
+  content: attr(data-empty-text);
+  float: left;
+  color: #aabaca;
+  pointer-events: none;
+  height: 0;
+  /*font-style: italic;*/
+}

+ 13 - 7
src/views/quiz/CreateQuestion.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="create-question">
     <back-title>创建题目</back-title>
-    <question-kind-bar v-model="currentKind" />
-    <question-editor-factory :kind="currentKind" @submit="create($event, currentKind)" />
+    <question-kind-bar v-model="currentKind"/>
+    <question-editor-factory :kind="currentKind" @submit="create($event, currentKind)"/>
   </div>
 </template>
 
@@ -16,11 +16,11 @@ import QuestionEditorFactory from '@/views/quiz/components/QuestionEditorFactory
 export default {
   name: 'CreateQuestion',
   components: { QuestionEditorFactory, QuestionKindBar, BackTitle },
-  data() {
+  data () {
     return { currentKind: QuestionKind.choice }
   },
   methods: {
-    create(questionBody, kind) {
+    create (questionBody, kind) {
       createQuestion({ ...questionBody, kind })
         .then(() => this.$setSuccessMessage('创建成功'))
         .then(() => this.$router.go(-1))
@@ -33,9 +33,15 @@ export default {
 </script>
 
 <style lang="scss">
+.create-question {
+  margin: 0 auto;
+  max-width: 625px;
+  padding: 32px 24px;
+}
+
+@media (min-width: 900px) {
   .create-question {
-    margin: 0 auto;
-    max-width: 1100px;
-    padding: 64px 24px;
+    padding: 64px 48px;
   }
+}
 </style>

+ 19 - 3
src/views/quiz/Questions.vue

@@ -1,6 +1,13 @@
 <template>
   <div class="questions">
-    <back-title>题库界面</back-title>
+    <back-title>
+      题库
+      <router-link :to="{ name: 'create-question' }">
+        <c-button text icon="add" theme="green" :left="12">
+          创建题目
+        </c-button>
+      </router-link>
+    </back-title>
     <div>
       <question-table />
     </div>
@@ -10,16 +17,25 @@
 <script>
 import BackTitle from '@/components/Basic/BackTitle'
 import QuestionTable from '@/views/quiz/components/QuestionTable'
+import CButton from '@/components/Basic/CButton'
 export default {
   name: 'Questions',
-  components: { QuestionTable, BackTitle }
+  components: { CButton, QuestionTable, BackTitle }
 }
 </script>
 
 <style lang="scss" scoped>
 .questions {
   margin: 0 auto;
-  max-width: 1100px;
+  max-width: 900px;
   padding: 64px 24px;
+  min-height: 90vh;
+  align-items: center;
+}
+
+@media (min-width: 600px) {
+  .questions {
+    padding: 64px 48px;
+  }
 }
 </style>

+ 45 - 36
src/views/quiz/components/ChoiceQuestionEditor.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="choice-question-editor">
     <editor-section title="题干">
       <editor
         :init-content="initValue.stem"
@@ -10,34 +10,33 @@
       <section
         v-for="(item, index) in value.options"
         :key="index"
-        class="choice"
+        class="choice mb-8"
       >
-        <div class="choice__index">{{ index }}</div>
+        <icon-button :left="-6" theme="red" :size="32" @click="removeOption(index)">
+          remove
+        </icon-button>
+        <icon-button :right="4" :theme="index === value.answer? 'green': 'inherit'"
+                     :size="32" title="选为正确答案" :no-bold="index === value.answer"
+                     @click="index === value.answer? value.answer = '': value.answer = index">
+          {{index === value.answer ? 'check_box' : 'check'}}
+        </icon-button>
+        <div class="choice__index" :class="index === value.answer ? 'green-text' : null"
+        >{{ index }}
+        </div>
         <editor
           class="choice__content"
+          :class="index === value.answer ? 'green-text' : null"
           :init-content="item"
           @update-markdown="updateOptions(index, $event)"
         />
-        <button
-          @click="removeOption(index)"
-        >
-          删除
-        </button>
-        <button
-          :style="{ background: index === value.answer? 'green': 'inherit' }"
-          @click="index === value.answer? value.answer = '': value.answer = index"
-        >
-          √
-        </button>
       </section>
       <section>
-        <button class="icon blue-icon" @click="addNewOption">
-          +
-        </button>
+        <c-button icon="add" theme="blue" @click="addNewOption">新建选项</c-button>
       </section>
     </editor-section>
     <editor-section title="正确答案">
-      {{ value.answer || '请选择答案' }}
+      <span v-if="value.answer">{{ value.answer }}</span>
+      <span v-else class="answer-placeholder">[ 未设置 ] 请在选项中选择答案</span>
     </editor-section>
     <editor-section title="解析">
       <editor
@@ -45,7 +44,7 @@
         @update-markdown="value.analysis = $event"
       />
     </editor-section>
-    <button @click="handleSubmit">{{ submitText }}</button>
+    <c-button theme="green" standout icon="check" @click="handleSubmit">{{ submitText }}</c-button>
   </div>
 </template>
 
@@ -54,10 +53,12 @@ import Editor from '@/components/Editor'
 import EditorSection from '@/views/quiz/components/EditorSection'
 import getCharFromIndex from '@/views/quiz/utils/getCharFromIndex'
 import { createDefaultChoiceQuestion } from '@/views/quiz/utils/default'
+import CButton from '@/components/Basic/CButton'
+import IconButton from '@/components/Basic/IconButton'
 
 export default {
   name: 'ChoiceQuestionEditor',
-  components: { EditorSection, Editor },
+  components: { IconButton, CButton, EditorSection, Editor },
   props: {
     initValue: {
       type: Object,
@@ -69,10 +70,10 @@ export default {
     },
     submitText: {
       type: String,
-      default: '提交'
+      default: '创建题目'
     }
   },
-  data() {
+  data () {
     return {
       value: createDefaultChoiceQuestion()
     }
@@ -80,37 +81,42 @@ export default {
   watch: {
     value: {
       deep: true,
-      handler(nextValue) {
+      handler (nextValue) {
         return this.$emit('input', nextValue)
       }
     },
     initValue: {
       deep: true,
       immediate: true,
-      handler(nextInitValue) {
+      handler (nextInitValue) {
         this.value.options = nextInitValue.options
         this.value.answer = nextInitValue.answer
       }
     }
   },
   methods: {
-
-    handleSubmit() {
+    handleSubmit () {
       const { stem, options, answer, analysis } = this.value
-      if (!stem) this.$setErrorMessage('请输入题干')
-      else if (!options?.A) this.$setErrorMessage('请输入选项')
-      else if (!answer) this.$setErrorMessage('请选择答案')
-      else if (!analysis) this.$setErrorMessage('请输入解析')
-      else this.$emit('submit', this.value)
+      if (!stem) {
+        this.$setErrorMessage('请输入题干')
+      } else if (!options?.A) {
+        this.$setErrorMessage('请输入选项')
+      } else if (!answer) {
+        this.$setErrorMessage('请选择答案')
+      } else if (!analysis) {
+        this.$setErrorMessage('请输入解析')
+      } else {
+        this.$emit('submit', this.value)
+      }
     },
 
-    updateOptions(index, text) {
+    updateOptions (index, text) {
       if (this.value.options) {
         this.value.options[index] = text
       }
     },
 
-    addNewOption() {
+    addNewOption () {
       const options = this.value.options || {}
       const choice = getCharFromIndex(Object.keys(options).length)
       this.value.options = {
@@ -119,7 +125,7 @@ export default {
       }
     },
 
-    removeOption(index) {
+    removeOption (index) {
       // 同时删除答案
       this.value.answer = ''
 
@@ -138,7 +144,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.choice {
-  display: flex;
+@import "~@/style/theme.scss";
+@import "~@/views/quiz/style/question-editor.scss";
+
+.green-text {
+  color: darken($theme-green, 5);
 }
 </style>

+ 7 - 4
src/views/quiz/components/EditorSection.vue

@@ -1,6 +1,6 @@
 <template>
-  <section>
-    <h3>{{ title }}:</h3>
+  <section class="editor-section">
+    <h3 class="mb-8">{{ title  }}:</h3>
     <slot />
   </section>
 </template>
@@ -17,6 +17,9 @@ export default {
 }
 </script>
 
-<style scoped>
-
+<style lang="scss" scoped>
+h3 {
+  font-size: 18px;
+  color: #5a6a7a;
+}
 </style>

+ 14 - 11
src/views/quiz/components/QuestionCard.vue

@@ -1,29 +1,31 @@
 <template>
-  <div>
+  <div class="question-card">
     <div class="question-card__header mb-8">
       <span>
         <span class="question-id">{{ questionId }}</span>
-        <span :class="`tag tag--${QuestionKind.color[kind]}`">{{ QuestionKind.naming[kind] }}</span>
+        <c-tag theme="green">{{ QuestionKind.naming[kind] }}</c-tag>
       </span>
       <span>
         <a v-if="selectable" class="action" style="cursor: pointer" @click="$emit('toggle-select', questionId)">
-          {{ selected ? "取消选择" : "选择" }}
+          {{ selected ? '取消选择' : '选择' }}
         </a>
-        <router-link class="action" style="margin-left: 16px;" :to="{ name: 'edit-question', params: { questionId }}">修改</router-link>
+        <router-link class="action" style="margin-left: 16px;"
+                     :to="{ name: 'edit-question', params: { questionId }}">修改</router-link>
         <router-link class="action" style="margin-left: 16px;" :to="{ name: 'question-detail', params: { questionId }}">详情</router-link>
       </span>
     </div>
-    <markdown-section :raw="stem" />
+    <markdown-section :raw="stem"/>
   </div>
 </template>
 
 <script>
 import * as QuestionKind from '@/server/enums/question-kind'
 import MarkdownSection from '@/views/quiz/components/MarkdownSection'
+import CTag from '@/components/Basic/CTag'
 
 export default {
   name: 'QuestionCard',
-  components: { MarkdownSection },
+  components: { CTag, MarkdownSection },
   props: {
     stem: {
       type: String,
@@ -46,7 +48,7 @@ export default {
       default: false
     }
   },
-  data() {
+  data () {
     return { QuestionKind }
   }
 }
@@ -55,19 +57,20 @@ export default {
 <style lang="scss" scoped>
 @import "~@/style/theme.scss";
 
-.question-id{
+.question-id {
   font-weight: bold;
 }
-.question-card__header{
+
+.question-card__header {
   display: flex;
   align-items: center;
   justify-content: space-between;
 }
 
-.action{
+.action {
   color: $theme-blue;
 
-  &:hover{
+  &:hover {
     color: darken($theme-blue, 10);
   }
 }

+ 11 - 6
src/views/quiz/components/QuestionEditorFactory.vue

@@ -1,16 +1,21 @@
 <template>
-  <choice-question-editor v-if="kind === QuestionKind.choice" :initValue="initValue" @submit="handleSubmit" />
-  <true-false-question-editor v-else-if="kind === QuestionKind.trueOrFalse" :initValue="initValue" @submit="handleSubmit" />
+  <slide-in-same-transition>
+    <keep-alive>
+      <choice-question-editor v-if="kind === QuestionKind.choice" :initValue="initValue" @submit="handleSubmit"/>
+      <true-false-question-editor v-else-if="kind === QuestionKind.trueOrFalse" :initValue="initValue"
+                                  @submit="handleSubmit"/>
+    </keep-alive>
+  </slide-in-same-transition>
 </template>
 
 <script>
 import QuestionKind from '@/server/enums/question-kind'
 import ChoiceQuestionEditor from '@/views/quiz/components/ChoiceQuestionEditor'
 import TrueFalseQuestionEditor from '@/views/quiz/components/TrueFalseQuestionEditor'
-
+import SlideInSameTransition from '@/animations/SlideInSameTransition'
 export default {
   name: 'QuestionEditorFactory',
-  components: { TrueFalseQuestionEditor, ChoiceQuestionEditor },
+  components: { TrueFalseQuestionEditor, ChoiceQuestionEditor, SlideInSameTransition },
   props: {
     initValue: {
       type: Object,
@@ -21,11 +26,11 @@ export default {
       default: ''
     }
   },
-  data() {
+  data () {
     return { QuestionKind }
   },
   methods: {
-    handleSubmit(event) {
+    handleSubmit (event) {
       this.$emit('submit', { ...this.initValue, ...event })
     }
   }

+ 11 - 15
src/views/quiz/components/QuestionTable.vue

@@ -1,14 +1,8 @@
 <template>
-  <div>
-    <input class="mb-16" v-model="inputStem" placeholder="题干搜索" @input="debounceFetchQuestions" />
-    <router-link :to="{ name: 'create-question' }">
-      <button>
-        创建题目
-      </button>
-    </router-link>
+  <div class="question-table">
+    <search-bar class="mb-16" v-model="inputStem" placeholder="题干搜索" @input="debounceFetchQuestions"/>
     <div>
       <question-card
-        class="question-card"
         v-for="item in questionList"
         :key="item.id"
         :kind="item.kind"
@@ -19,7 +13,8 @@
         @toggle-select="handleToggleSelectQuestion(item)"
       />
     </div>
-    <pagination class="mt-16" :page="page.number" :size="page.size" :total-elements="page.totalElements" @change="fetchQuestionList"></pagination>
+    <pagination class="mt-16" :page="page.number" :size="page.size" :total-elements="page.totalElements"
+                @change="fetchQuestionList"></pagination>
   </div>
 </template>
 
@@ -29,10 +24,11 @@ import { getQuestionList } from '@/server/question'
 import Pagination from '@/components/Pagination'
 import { markdownToHTML } from '@/utils/markdown'
 import QuestionCard from '@/views/quiz/components/QuestionCard'
+import SearchBar from '@/components/Basic/SearchBar'
 
 export default {
   name: 'QuestionTable',
-  components: { QuestionCard, Pagination },
+  components: { SearchBar, QuestionCard, Pagination },
   props: {
     selectable: {
       type: Boolean,
@@ -50,7 +46,7 @@ export default {
   filters: {
     markdownToHTML: markdownToHTML
   },
-  data() {
+  data () {
     return {
       inputStem: '',
       questionList: [],
@@ -60,11 +56,11 @@ export default {
       debounceFetchQuestions: debounce(() => this.fetchQuestionList())
     }
   },
-  mounted() {
+  mounted () {
     this.fetchQuestionList()
   },
   methods: {
-    fetchQuestionList({
+    fetchQuestionList ({
       stem = this.inputStem,
       page = 1,
       size = 10
@@ -75,11 +71,11 @@ export default {
           this.page = page
         })
     },
-    judgeIsSelected(question) {
+    judgeIsSelected (question) {
       return Boolean(this.selectedQuestions
         .find(item => item.id === question.id))
     },
-    handleToggleSelectQuestion(question) {
+    handleToggleSelectQuestion (question) {
       if (this.judgeIsSelected(question)) {
         this.$emit('select', this.selectedQuestions.filter(item => item.id !== question.id))
       } else {

+ 31 - 19
src/views/quiz/components/TrueFalseQuestionEditor.vue

@@ -6,14 +6,11 @@
         @update-markdown="value.stem = $event"
       />
     </editor-section>
-    <editor-section title="答案">
-      <button
-        :style="{ background: value.answer? 'green': 'red' }"
-        @click="value.answer = !value.answer"
-      >
-        <span v-if="value.answer" >√</span>
-        <span v-else >×</span>
-      </button>
+    <editor-section class="choice-answer-area" title="答案">
+      <icon-button :theme="value.answer? 'green': 'red'" title="点击修改答案"
+                   @click="value.answer = !value.answer" class="mb-8">
+        {{value.answer ? 'check' : 'close'}}
+      </icon-button>
     </editor-section>
     <editor-section title="解析">
       <editor
@@ -21,7 +18,7 @@
         @update-markdown="value.analysis = $event"
       />
     </editor-section>
-    <button @click="handleSubmit">{{ submitText }}</button>
+    <c-button theme="green" standout icon="check" @click="handleSubmit">{{ submitText }}</c-button>
   </div>
 </template>
 
@@ -29,10 +26,12 @@
 import Editor from '@/components/Editor'
 import EditorSection from '@/views/quiz/components/EditorSection'
 import { createDefaultTrueFalseQuestion } from '@/views/quiz/utils/default'
+import IconButton from '@/components/Basic/IconButton'
+import CButton from '@/components/Basic/CButton'
 
 export default {
   name: 'TrueFalseQuestionEditor',
-  components: { EditorSection, Editor },
+  components: { CButton, IconButton, EditorSection, Editor },
   props: {
     initValue: {
       type: Object,
@@ -44,10 +43,10 @@ export default {
     },
     submitText: {
       type: String,
-      default: '提交'
+      default: '创建题目'
     }
   },
-  data() {
+  data () {
     return {
       value: createDefaultTrueFalseQuestion()
     }
@@ -55,29 +54,42 @@ export default {
   watch: {
     value: {
       deep: true,
-      handler(nextValue) {
+      handler (nextValue) {
         return this.$emit('input', nextValue)
       }
     },
     initValue: {
       deep: true,
       immediate: true,
-      handler(nextInitValue) {
+      handler (nextInitValue) {
         this.value.answer = nextInitValue.answer
       }
     }
   },
   methods: {
-    handleSubmit() {
+    handleSubmit () {
       const { stem, answer, analysis } = this.value
-      if (!stem) this.$setErrorMessage('请输入题干')
-      else if (!answer) this.$setErrorMessage('请选择答案')
-      else if (!analysis) this.$setErrorMessage('请输入解析')
-      else this.$emit('submit', this.value)
+      if (!stem) {
+        this.$setErrorMessage('请输入题干')
+      } else if (!answer) {
+        this.$setErrorMessage('请选择答案')
+      } else if (!analysis) {
+        this.$setErrorMessage('请输入解析')
+      } else {
+        this.$emit('submit', this.value)
+      }
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+@import '~@/style/theme.scss';
+@import "~@/views/quiz/style/question-editor.scss";
+
+.choice-answer-area {
+  display: flex;
+  align-items: center;
+  padding-top: 20px;
+}
 </style>

+ 29 - 0
src/views/quiz/style/question-editor.scss

@@ -0,0 +1,29 @@
+@import '~@/style/theme.scss';
+
+.choice {
+  display: flex;
+  align-items: center;
+
+  &__index {
+    margin-right: 4px;
+    &::after {
+      content: '. ';
+      margin-left: 2px;
+    }
+  }
+}
+
+.editor-section {
+  margin: 16px -12px;
+  background: transparent;
+  padding: 12px 12px 12px 12px;
+  transition: .2s;
+
+  &:hover {
+    background: $theme-background-grey;
+  }
+}
+
+.answer-placeholder {
+  color: $theme-orange;
+}