任一存 2 years ago
commit
f6d7df5765
59 changed files with 3498 additions and 0 deletions
  1. 13 0
      .env.production
  2. 25 0
      .gitignore
  3. 24 0
      README.md
  4. 5 0
      babel.config.js
  5. 72 0
      package.json
  6. 6 0
      postcss.config.js
  7. BIN
      public/favicon.ico
  8. 23 0
      public/index.html
  9. 39 0
      src/App.vue
  10. 31 0
      src/api/topic.json
  11. BIN
      src/assets/default_avatar.png
  12. BIN
      src/assets/images/bg.png
  13. BIN
      src/assets/images/contbg.png
  14. BIN
      src/assets/images/false.png
  15. BIN
      src/assets/images/fingerprint.png
  16. BIN
      src/assets/images/title.png
  17. BIN
      src/assets/images/true.png
  18. 60 0
      src/components/BaseCell.vue
  19. 57 0
      src/components/NavBar.vue
  20. 61 0
      src/components/SvgIcon/index.vue
  21. 62 0
      src/components/common/detail.vue
  22. 20 0
      src/components/global.js
  23. 9 0
      src/icons/index.js
  24. 1 0
      src/icons/svg/notice.svg
  25. 1 0
      src/icons/svg/tianjiachengyuan.svg
  26. 22 0
      src/icons/svgo.yml
  27. 57 0
      src/layout/index.vue
  28. 25 0
      src/main.js
  29. 19 0
      src/mixins/detail.js
  30. 60 0
      src/mixins/form.js
  31. 3 0
      src/mixins/index.js
  32. 84 0
      src/mixins/list.js
  33. 47 0
      src/mixins/list_bak.js
  34. 15 0
      src/permission.js
  35. 77 0
      src/plugins/cache.js
  36. 20 0
      src/plugins/index.js
  37. 17 0
      src/router/index.js
  38. 26 0
      src/router/modules/topic.js
  39. 17 0
      src/router/routes.js
  40. 4 0
      src/store/getters.js
  41. 16 0
      src/store/index.js
  42. 37 0
      src/store/modules/dialog.js
  43. 7 0
      src/store/modules/upload.js
  44. 132 0
      src/styles/index.less
  45. 216 0
      src/styles/reset.less
  46. 3 0
      src/styles/tailwind.less
  47. 914 0
      src/styles/vant-ui.less
  48. 29 0
      src/utils/auth.js
  49. 33 0
      src/utils/common.js
  50. 6 0
      src/utils/errorCode.js
  51. 31 0
      src/utils/jsencrypt.js
  52. 155 0
      src/utils/request.js
  53. 6 0
      src/utils/settings.js
  54. 7 0
      src/utils/validate.js
  55. 262 0
      src/utils/webPublic.js
  56. 259 0
      src/views/topic/index.vue
  57. 240 0
      src/views/topic/result.vue
  58. 41 0
      tailwind.config.js
  59. 102 0
      vue.config.js

+ 13 - 0
.env.production

@@ -0,0 +1,13 @@
+# 页面标题
+VUE_APP_TITLE = 安徽小康h5
+
+# 生产环境配置
+ENV = 'production'
+NODE_ENV = production
+
+# 安徽小康h5/生产环境
+VUE_APP_BASE_API = '/prod-api'
+
+# 测试地址
+VUE_APP_API_ROOT=http://10.111.90.37/prod-api 
+

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+.env
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+yarn.lock
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# app
+
+## Project setup
+```
+yarn install
+```
+
+### Compiles and hot-reloads for development
+```
+yarn serve
+```
+
+### Compiles and minifies for production
+```
+yarn build
+```
+
+### Lints and fixes files
+```
+yarn lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 72 - 0
package.json

@@ -0,0 +1,72 @@
+{
+  "name": "llxxh",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build --mode production",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "axios": "^1.1.3",
+    "core-js": "^3.6.5",
+    "js-cookie": "^3.0.1",
+    "jsencrypt": "3.0.0-rc.1",
+    "lodash.throttle": "^4.1.1",
+    "nprogress": "^0.2.0",
+    "vant": "^2.12.51",
+    "vue": "^2.6.11",
+    "vue-router": "^3.5.3",
+    "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.18.10",
+    "@babel/eslint-parser": "^7.18.9",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "autoprefixer": "^9",
+    "babel-eslint": "^10.1.0",
+    "babel-plugin-import": "^1.13.5",
+    "eslint": "^6.7.2",
+    "eslint-config-standard": "^16.0.3",
+    "eslint-plugin-import": "^2.25.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^5.1.1",
+    "eslint-plugin-standard": "^5.0.0",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^4.1.3",
+    "less-loader": "7.1.0",
+    "lint-staged": "^9.5.0",
+    "postcss": "^8.3.3",
+    "postcss-html": "^1.3.0",
+    "svg-sprite-loader": "^6.0.11",
+    "svgo": "^2.8.0",
+    "tailwindcss": "npm:@tailwindcss/postcss7-compat",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "lint-staged": {
+    "src/**/*.{js,jsx,css,scss,vue}": "eslint --fix"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {}
+  }
+}

BIN
public/favicon.ico


+ 23 - 0
public/index.html

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width,initial-scale=1.0">
+  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+  <!-- <script src="https://app.eyh.cn/js/JSSdk.js"></script> -->
+  <title>安徽小康</title>
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+        Please enable it to continue.</strong>
+  </noscript>
+  <div id="app"></div>
+  <!-- built files will be auto injected -->
+</body>
+
+
+</html>

+ 39 - 0
src/App.vue

@@ -0,0 +1,39 @@
+<template>
+  <div id="app">
+    <!-- <NavBar :title="title" /> -->
+    <keep-alive>
+      <router-view />
+    </keep-alive>
+  </div>
+</template>
+
+<script>
+// import NavBar from "@components/NavBar.vue";
+export default {
+  name: "App",
+  components: {
+    // NavBar,
+  },
+  data() {
+    return {
+      title: "",
+    };
+  },
+  watch: {
+    $route: {
+      deep: true,
+      handler(val) {
+        document.title = val.meta.title || "";
+        this.title = val.meta.title || "";
+      },
+    },
+  },
+}
+</script>
+
+<style land="less" scoped>
+#app {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 31 - 0
src/api/topic.json

@@ -0,0 +1,31 @@
+{
+    "topicList": [
+        {
+            "title": "“小康”一词最早出自于()",
+            "option": [
+                "《诗经》",
+                "《尚书》",
+                "《礼记》"
+            ],
+            "answer": 0
+        },
+        {
+            "title": "我国实现全面建成小康社会是()",
+            "option": [
+                "2019年",
+                "2020年",
+                "2021年"
+            ],
+            "answer": 1
+        },
+        {
+            "title": "1978年,在全国率先推行“大包干”的是凤阳县()",
+            "option": [
+                "小岗村",
+                "大湾村",
+                "大岗村"
+            ],
+            "answer": 0
+        }
+    ]
+}

BIN
src/assets/default_avatar.png


BIN
src/assets/images/bg.png


BIN
src/assets/images/contbg.png


BIN
src/assets/images/false.png


BIN
src/assets/images/fingerprint.png


BIN
src/assets/images/title.png


BIN
src/assets/images/true.png


+ 60 - 0
src/components/BaseCell.vue

@@ -0,0 +1,60 @@
+<template>
+  <van-cell class="base-cell" :center="center">
+    <template v-if="title" #title>
+      <span class="base-cell__title">{{ title }}</span>
+    </template>
+    <template v-if="icon || svgIcon" #icon>
+      <span class="base-cell__icon">
+        <van-icon v-if="icon" :name="icon" />
+        <svg-icon v-if="svgIcon" :icon-class="svgIcon" />
+      </span>
+    </template>
+    <template class="base-cell__label" #label>
+      <slot name="label"></slot>
+    </template>
+    <template #default>
+      <span class="base-cell__value">{{ value }}</span>
+    </template>
+  </van-cell>
+</template>
+
+<script>
+export default {
+  name: "BaseCell",
+  props: {
+    title: {
+      type: String,
+      default: "",
+    },
+    icon: {
+      // icon\svgIcon二者只能存在一个
+      type: String,
+      default: "",
+    },
+    svgIcon: {
+      type: String,
+      default: "",
+    },
+    value: {
+      type: String,
+      default: "",
+    },
+    center: {
+      type: Boolean,
+      default: true,
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.base-cell {
+  padding: 1.6rem;
+  &__title,
+  &__value {
+    font-size: 1.4rem;
+  }
+  &__icon {
+    margin-right: 0.9rem;
+  }
+}
+</style>

+ 57 - 0
src/components/NavBar.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="nav-bar h-16">
+    <van-nav-bar
+      left-text=""
+      :border="false"
+      left-arrow
+      class="navBar"
+      :title="title"
+      @click-left="goBack"
+    />
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    title: {
+      type: String,
+      require: true,
+    },
+  },
+  watch: {
+    title(val) {
+      this.title = val;
+    },
+  },
+  methods: {
+    goBack() {
+      this.$router.go(-1);
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.nav-bar {
+  line-height: 4rem;
+
+  ::v-deep .van-nav-bar {
+    height: 100%;
+  }
+
+  ::v-deep .van-nav-bar .van-icon {
+    font-size: 1.8rem;
+    color: $nav-color;
+  }
+
+  ::v-deep .van-nav-bar__title {
+    font-size: 1.8rem;
+    font-weight: bold;
+  }
+
+  ::v-deep .van-nav-bar__content {
+    height: 100%;
+  }
+}
+</style>

+ 61 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+import { isExternal } from 'src/utils/validate'
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className
+      } else {
+        return 'svg-icon'
+      }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
+</style>

+ 62 - 0
src/components/common/detail.vue

@@ -0,0 +1,62 @@
+<!-- 文章详情 - 公共 -->
+<template>
+  <div class="com-family com-article-detail">
+    <div class="title-cont">
+      <h3>{{ title || "" }}</h3>
+      <div class="text-cont flex justify-between items-center">
+        <span class="author" v-if="author && platform">{{ author || "" }} 发布在 {{ platform || '' }}</span>
+        <span>{{ time || "" }}</span></div>
+    </div>
+    <div class="content" v-html="content || ''"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ArticleDetail',
+  props: ["title", "author", "platform", "time", "content"]
+}
+</script>
+
+<style lang="less" scoped>
+.com-article-detail {
+  min-height: 100%;
+  background: #ffffff;
+  box-sizing: border-box;
+  padding: 1.5rem;
+  font-size: 1.4rem;
+  font-weight: 400;
+  color: #666666;
+  line-height: 2.4rem;
+  .title-cont {
+    padding-bottom: 2.2rem;
+    font-size: 1.2rem;
+    font-weight: 400;
+    color: #999999;
+    line-height: 1.7rem;
+    h3 {
+      font-size: 1.8rem;
+      font-weight: 600;
+      color: #333333;
+      line-height: 2.6rem;
+    }
+    .text-cont {
+      margin-top: 1.6rem;
+      overflow: hidden;
+    }
+    .author {
+      color: @primary-color;
+    }
+  }
+  .content {
+    width: 100%;
+    overflow: hidden;
+    color: #666;
+    padding-bottom: 1.5rem;
+    img,
+    video {
+      max-width: 100%;
+    }
+  }
+}
+</style>

+ 20 - 0
src/components/global.js

@@ -0,0 +1,20 @@
+
+import Vue from 'vue'
+
+const requireComponent = require.context(
+  '.',
+  false,
+  /(App|Base)\w+\.(vue|js)$/
+)
+
+requireComponent.keys().forEach((fileName) => {
+  const componentConfig = requireComponent(fileName)
+  const componentName = fileName
+    .replace(/^\.\//, '')
+    .replace(/\.\w+$/, '')
+    .split('-')
+    .map((kebab) => kebab.charAt(0).toUpperCase() + kebab.slice(1))
+    .join('')
+
+  Vue.component(componentName, componentConfig.default || componentConfig)
+})

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from 'src/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/notice.svg


File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/tianjiachengyuan.svg


+ 22 - 0
src/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 57 - 0
src/layout/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="layout">
+    <router-view></router-view>
+    <!-- 弹框 -->
+    <van-dialog
+      v-model="dialogShow"
+      closeOnClickOverlay
+      :showConfirmButton="dialogParesCom.showConfirmButton || false"
+      :class="dialogClassCom"
+      :width="dialogWidthCom"
+      @close="closeDialog"
+    >
+      <component v-bind:is="dialogTemplateCom" v-if="dialogTemplateCom"></component>
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, mapMutations } from 'vuex'
+export default {
+  name: "Layout",
+  data() {
+    return {
+      active: 0,
+      dialogShow: false,
+    }
+  },
+  computed: {
+    ...mapState({
+      // 对话框
+      dialogVisibleCom: (state) => state.Dialog.dialogVisible,
+      dialogTemplateCom: (state) => state.Dialog.dialogTemplate,
+      dialogWidthCom: (state) => state.Dialog.dialogWidth,
+      dialogClassCom: (state) => state.Dialog.dialogClass,
+      dialogParesCom: (state) => state.Dialog.dialogPares,
+    }),
+  },
+  watch: {
+    dialogVisibleCom(val) {
+      this.dialogShow = val;
+    },
+  },
+  created() {
+    this.dialogShow = this.dialogVisibleCom;
+  },
+  methods: {
+    ...mapMutations(["closeDialog"]),
+  }
+};
+</script>
+<style lang="less" scoped>
+.layout {
+  background: #fff;
+  overflow: auto;
+  min-height: 100vh;
+}
+</style>

+ 25 - 0
src/main.js

@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import App from './App.vue'
+import Vant from 'vant'
+import { Lazyload } from 'vant';
+import axios from 'axios'
+import router from './router'
+import store from './store'
+import 'vant/lib/index.less'
+import 'src/styles/index.less'
+import 'src/icons'
+import '@components/global.js'
+import './permission'
+import { listToastObj } from "./utils/settings"
+// import Vconsole from 'vconsole'
+// Vue.use(new Vconsole())
+Vue.config.productionTip = false
+Vue.prototype.listToastSet = listToastObj;
+Vue.use(Vant);
+Vue.use(Lazyload);
+new Vue({
+  axios,
+  router,
+  store,
+  render: h => h(App),
+}).$mount('#app')

+ 19 - 0
src/mixins/detail.js

@@ -0,0 +1,19 @@
+import { gotoPage } from '/src/utils/common'
+export default {
+  name: "DetailMixin",
+  data() {
+    return {
+      currentId: Number(this.$route.params.id),
+      detailData: {},                 
+      detailParam: {},     // 获取表单已有数据请求参,用于编辑表单请求
+      getDetailApi: () => {},  // 获取详情 API
+    }
+  },
+  async mounted() {
+    const res = await this.getDetailApi(this.currentId)
+    this.detailData = res.data
+  },
+  methods: {
+    gotoPage: gotoPage,
+  }
+}

+ 60 - 0
src/mixins/form.js

@@ -0,0 +1,60 @@
+import { Toast } from 'vant'
+import { deepClone } from 'src/utils/common.js'
+export default {
+  name: "FormMixin",
+  data() {
+    return {
+      currentId: Number(this.$route.params.id), //默认获取路由当前id
+      formData: {} || [],        // 表单数据
+      formParam: {} || [],       // 获取表单已有数据请求参,用于编辑表单请求
+      getInitformApi: () => { },  // 获取表单已有数据API, 详情接口
+      addFormApi: () => { },     // 提交表单API
+      editFormApi: () => { },     // 编辑表单API
+      addBackRouter: '', // 表单提交成功后的路由跳转,传递路由name值, 默认则go(-1)
+      editBackRouter: '', // 表单提交成功后的路由跳转,传递路由name值, 默认则go(-1)
+      otherRules: [],  // 表单其他验证 
+    }
+  },
+  methods: {
+    deepClone: deepClone,
+    async init() {
+      //没有特定请求参就默认路由id
+      let param = Object.values(this.formParam).length > 0 ? this.formParam : this.currentId
+      const res = await this.getInitformApi(param)
+      this.formData = await this.deepClone(res.data)
+    },
+    otherRuleChange() {
+      let res = this.otherRules.find(item => {
+        let reg = item.reg
+        let value = this.formData[item.name]
+        return !reg.test(value)
+      })
+      return res ? res.message : ""
+    },
+    async handleSubmit() {
+      await this.$refs['formData'].validate().then(async () => {
+        let otherRules = await this.otherRuleChange()
+        if (otherRules) {
+          Toast.fail(otherRules)
+          return
+        }
+        if(this.currentId) {
+          await this.editFormApi({
+            id: this.currentId,
+            ...this.formData
+          })
+          this.$toast.success('表单提交成功')
+          this.addBackRouter? this.$router.push({ name: this.addBackRouter }) : this.$router.go(-1)
+        } else {
+          await this.addFormApi({
+            ...this.formData
+          })
+          this.$toast.success('表单提交成功')
+          this.editBackRouter? this.$router.push({ name: this.editBackRouter }) : this.$router.go(-1)
+        }
+      }).catch(() => {
+        Toast.fail('请完善表单')
+      })
+    }
+  }
+}

+ 3 - 0
src/mixins/index.js

@@ -0,0 +1,3 @@
+export { default as ListMixin } from './list'
+export { default as FormMixin } from './form'
+export { default as DetailMixin } from './detail'

+ 84 - 0
src/mixins/list.js

@@ -0,0 +1,84 @@
+import { gotoPage } from '@utils/common.js'
+import throttle from 'lodash.throttle'
+
+export default {
+  name: "ListMixin",
+  data() {
+    return {
+      initretrieval: {},    // 初始默认检索  非名称检索的其他检索,需要添加默认值
+      tableData: [], // 列表数据
+      total: 0,
+      queryParams: {           // 列表检索分页
+        pageNum: 1,
+        pageSize: 10
+      },
+      tableForm: {}, // 列表筛选条件
+      refreshing: false,    // 下拉状态 
+      loading: false,       // 是否加载更多数据
+      finished: false,      // 数据是否全部加载完成
+      finishedText: "已加载完成",
+      isHasUpLoading: false, // 兼容其他列表加载方式,默认为false,即不需要上拉加载, 如需要上拉加载,在组件中将该值设为true
+      getListApi: () => { }
+    }
+  },
+  created() {
+    if(!this.isHasUpLoading) {
+      this.getList()
+    }
+  },
+  methods: {
+    gotoPage: gotoPage,
+    initListData() {
+      //初始化列表数据
+      this.finished = false
+      this.queryParams.pageNum = 1
+      this.tableData = []
+      throttle(this.getlist, 1000)
+    },
+    async getList() {
+      //获取列表数据
+      let params = { ...this.tableForm, ...this.queryParams }
+      await this.getListApi(params).then((res) => {
+        this.tableData.push(...res.rows)
+        this.total = res.total
+        this.loading = false
+        this.queryParams.pageNum ++
+        if (res.rows.length < this.queryParams.pageSize || this.tableData.length == res.total) {
+          this.finishedText = this.tableData.length > 0 ? "已加载完成" : '暂无数据'
+          this.finished = true
+        }
+
+      }).catch(() => {
+        this.finishedText = "系统出错,请联系管理员"
+        this.finished = true
+      })
+    },
+    resetListData() {
+      //点击搜索的去除使用 暂时用不到
+      this.tableForm = this.initretrieval
+      this.tableData = []
+      this.queryParams.pageNum = 1
+    },
+    onRefresh() {
+      //下拉加载
+      setTimeout(async () => {
+        if (this.refreshing) {
+          this.initListData()
+          this.refreshing = false
+        }
+      }, 1000)
+    },
+    onLoad() {
+      //上拉加载
+      setTimeout(async () => {
+        if (!this.finished) {
+          await this.getList()
+        }
+      }, 1000)
+    },
+    handlePushDetail(id) {
+      // 跳转至详情页
+      this.$router.push({ name: this.detailRouteName, params: { id } })
+    }
+  },
+}

+ 47 - 0
src/mixins/list_bak.js

@@ -0,0 +1,47 @@
+export default {
+  data() {
+    return {
+      tableData: null,
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+      }, // 分页
+      tableFrom: {}, // 筛选条件
+      loading: true,
+      noData: false,
+      getListApi: () => { }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    async getList() {
+      const res = await this.getListApi({
+        ...this.tableFrom,
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize
+      }).catch(error => this.$message.error(error.message))
+      if (res.rows && res.rows.length > 0) {
+        this.tableData = res.rows
+        this.total = res.total
+      } else {
+        this.noData = true
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    handlePageChange({ page }) {
+      this.queryParams.pageNum = page
+      this.getList()
+    },
+    handlePushDetail(id) {
+      // 跳转至详情页
+      this.$router.push({ name: this.detailRouteName, params: { id } })
+    }
+  }
+}

+ 15 - 0
src/permission.js

@@ -0,0 +1,15 @@
+import router from './router'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+
+NProgress.configure({ showSpinner: false })
+
+router.beforeEach((to, from, next) => {
+    NProgress.start()
+    next()
+
+})
+
+router.afterEach(() => {
+    NProgress.done()
+})

+ 77 - 0
src/plugins/cache.js

@@ -0,0 +1,77 @@
+const sessionCache = {
+  set (key, value) {
+    if (!sessionStorage) {
+      return
+    }
+    if (key != null && value != null) {
+      sessionStorage.setItem(key, value)
+    }
+  },
+  get (key) {
+    if (!sessionStorage) {
+      return null
+    }
+    if (key == null) {
+      return null
+    }
+    return sessionStorage.getItem(key)
+  },
+  setJSON (key, jsonValue) {
+    if (jsonValue != null) {
+      this.set(key, JSON.stringify(jsonValue))
+    }
+  },
+  getJSON (key) {
+    const value = this.get(key)
+    if (value != null) {
+      return JSON.parse(value)
+    }
+  },
+  remove (key) {
+    sessionStorage.removeItem(key);
+  }
+}
+const localCache = {
+  set (key, value) {
+    if (!localStorage) {
+      return
+    }
+    if (key != null && value != null) {
+      localStorage.setItem(key, value)
+    }
+  },
+  get (key) {
+    if (!localStorage) {
+      return null
+    }
+    if (key == null) {
+      return null
+    }
+    return localStorage.getItem(key)
+  },
+  setJSON (key, jsonValue) {
+    if (jsonValue != null) {
+      this.set(key, JSON.stringify(jsonValue))
+    }
+  },
+  getJSON (key) {
+    const value = this.get(key)
+    if (value != null) {
+      return JSON.parse(value)
+    }
+  },
+  remove (key) {
+    localStorage.removeItem(key);
+  }
+}
+
+export default {
+  /**
+   * 会话级缓存
+   */
+  session: sessionCache,
+  /**
+   * 本地缓存
+   */
+  local: localCache
+}

+ 20 - 0
src/plugins/index.js

@@ -0,0 +1,20 @@
+import tab from './tab'
+import auth from './auth'
+import cache from './cache'
+import modal from './modal'
+import download from './download'
+
+export default {
+  install(Vue) {
+    // 页签操作
+    Vue.prototype.$tab = tab
+    // 认证对象
+    Vue.prototype.$auth = auth
+    // 缓存对象
+    Vue.prototype.$cache = cache
+    // 模态框对象
+    Vue.prototype.$modal = modal
+    // 下载文件
+    Vue.prototype.$download = download
+  }
+}

+ 17 - 0
src/router/index.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+import routes from './routes'
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  mode: 'hash',
+  base: process.env.BASE_URL,
+  routes
+})
+// router.beforeEach(async (to, from, next) => {
+//   next()
+// })
+// router.afterEach(() => {
+//   window.scrollTo(0, 0)
+// })
+export default router

+ 26 - 0
src/router/modules/topic.js

@@ -0,0 +1,26 @@
+// 首页
+import Layout from 'src/layout'
+
+export default {
+  path: '/',
+  name: 'Topic',
+  component: Layout,
+  children: [
+    {
+      path: '/topic',
+      name: 'topicIndex',
+      meta: {
+        title: '题库'
+      },
+      component: () => import('src/views/topic/index')
+    },
+    {
+      path: '/result',
+      name: 'resultIndex',
+      meta: {
+        title: '结果'
+      },
+      component: () => import('src/views/topic/result')
+    }
+  ]
+}

+ 17 - 0
src/router/routes.js

@@ -0,0 +1,17 @@
+import TopicRouter from './modules/topic'
+import Layout from 'src/layout'
+const constantRoutes = [
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/topic',
+    meta: {
+      title: '首页'
+    }
+  }
+]
+const routes = [
+  ...constantRoutes,
+  TopicRouter,
+]
+export default routes

+ 4 - 0
src/store/getters.js

@@ -0,0 +1,4 @@
+const getters = {
+  uploadAction: state => state.upload.uploadAction, // 上传通用接口
+}
+export default getters

+ 16 - 0
src/store/index.js

@@ -0,0 +1,16 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import getters from './getters'
+import upload from './modules/upload'
+import Dialog from './modules/dialog'
+// vuex数据持久化,用到时可引入
+// import persistedState from 'vuex-persistedState'
+Vue.use(Vuex)
+export default new Vuex.Store({
+  modules: {
+    upload,
+    Dialog
+  },
+  // plugins: [persistedState()],
+  getters
+})

+ 37 - 0
src/store/modules/dialog.js

@@ -0,0 +1,37 @@
+// 公共对话框
+const state = {
+    dialogVisible: false,
+    dialogTemplate: "", //子组件
+    dialogWidth: "215px",
+    dialogClass: "",// "urge-dialog"
+    dialogPares: {}, // 传递给子组件的参数 - showConfirmButton 默认是false
+  };
+  
+  const getters = {};
+  
+  const mutations = {
+    openDialog(state, data) {
+      state.dialogVisible = true;
+      state.dialogTemplate = data.template;
+      state.dialogWidth = data.width || "215px";
+      state.dialogClass = data.class || "";
+      state.dialogPares = data.pares || {};
+    },
+    closeDialog(state) {
+      state.dialogVisible = false;
+      state.dialogTemplate = "";
+      state.dialogWidth = "";
+      state.dialogClass = "";
+      state.dialogPares = {};
+    }
+  };
+  
+  const actions = {};
+  
+  export default {
+    state,
+    getters,
+    mutations,
+    actions
+  };
+  

+ 7 - 0
src/store/modules/upload.js

@@ -0,0 +1,7 @@
+// 存放上传的相关变量、方法的中间件
+export default {
+  namespaced: true,
+  state: {
+    uploadAction: ''// 上传接口action
+  }
+}

+ 132 - 0
src/styles/index.less

@@ -0,0 +1,132 @@
+@import './reset.less';
+@import './tailwind.less';
+@import '//at.alicdn.com/t/c/font_3853376_cqgthq61xcl.css';
+html {
+  font-size: 10px;
+}
+
+body {
+  width: 100%;
+  height: 100%;
+  background-color: #f8f8f8;
+  font-size: 1.6rem;
+}
+
+body,
+button,
+input,
+select,
+textarea {
+  font-family: 'PingFangSC-Regular', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Microsoft YaHei', 'sans-serif', '\5b8b\4f53';
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+:root {
+  --color-primary: #315EA3;
+  // --color-success: #30B973;
+  --color-warning: #F49532;
+  --color-danger: #F87484;
+  --color-border: #F9F9F9;
+  --color-primary-bg: rgba(49, 94, 163, 0.1);
+  // --color-success-bg: rgba(48, 185, 115, 0.1);
+  --color-warning-bg: rgba(244, 149, 50, 0.1);
+  --color-danger-bg: rgba(248, 116, 132, 0.1);
+}
+
+.com-family {
+  font-family: PingFangSC-Semibold, PingFang SC;
+}
+
+.clearfix:after {
+  /*伪元素是行内元素 正常浏览器清除浮动方法*/
+  content: "";
+  display: block;
+  height: 0;
+  clear: both;
+  visibility: hidden;
+}
+.clearfix {
+  *zoom: 1;
+  /*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
+}
+
+.text-nowrap {
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.text-nowrap-ellipsis {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.com-card {
+  background: #FFFFFF;
+  border-radius: 8px;
+  overflow: hidden;
+  margin-bottom: 16px;
+}
+
+// 公共按钮
+.com-btn-cont {
+  text-align: center;
+  .van-button {
+    min-width: 150px;
+    margin: 0 8px;
+    border: 0;
+    border-radius: 8px;
+    overflow: hidden;
+    &.van-button--plain.van-button--info {
+      background: var(--color-primary-bg);
+    }
+    &.van-button--plain.van-button--danger {
+      background: var(--color-danger-bg);
+    }
+    &.van-button--plain.van-button--warning {
+      background: var(--color-warning-bg);
+    }
+  }
+}
+
+// 公共弹框
+.urge-dialog {
+  position: fixed;
+  overflow: visible;
+  .van-dialog__content {
+    border-radius: 1.6rem;
+  }
+  &>div {
+    background-color: #FFFFFF;
+  }
+  &::before {
+    content: "";
+    display: block;
+    position: absolute;
+    z-index: -1;
+    top: 16px;
+    bottom: -16px;
+    left: 16px;
+    right: 16px;
+    background: #315EA3;
+    border-radius: 8px;
+  }
+}
+.com-dialog-cont {
+  box-sizing: border-box;
+  padding: 16px 24px;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  font-size: 14px;
+  font-weight: 500;
+  color: #333333;
+  line-height: 20px;
+}

+ 216 - 0
src/styles/reset.less

@@ -0,0 +1,216 @@
+// reset
+html,
+body {
+  height: 100%;
+  width: 100%;
+}
+
+body {
+  color: #333;
+  background: #fff;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+}
+
+
+.amap-logo {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.amap-copyright {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+// 活动详情显示重置样式开始
+section {
+  max-width: 100%;
+}
+
+//活动详情显示重置样式结束
+
+body,
+div,
+dl,
+dt,
+dd,
+ul,
+ol,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+pre,
+code,
+form,
+fieldset,
+legend,
+input,
+textarea,
+p,
+blockquote,
+th,
+td,
+hr,
+button,
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav {
+  display: block;
+}
+
+audio,
+canvas,
+video {
+  display: inline-block;
+}
+
+
+
+input,
+select,
+textarea {
+  font-size: 100%;
+  outline: none;
+}
+
+//&:focus
+//  border 1px solid #1d8ce0
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+th {
+  text-align: inherit;
+}
+
+fieldset,
+img {
+  border: 0;
+}
+
+iframe {
+  display: block;
+}
+
+abbr,
+acronym {
+  border: 0;
+  font-variant: normal;
+}
+
+del {
+  text-decoration: line-through;
+}
+
+address,
+caption,
+cite,
+code,
+dfn,
+em,
+th,
+var {
+  font-style: normal;
+  font-weight: 500;
+}
+
+ol,
+ul {
+  list-style: none;
+}
+
+caption,
+th {
+  text-align: left;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: 100%;
+  font-weight: 500;
+}
+
+q:before,
+q:after {
+  content: '';
+}
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -.5em;
+}
+
+sub {
+  bottom: -.25em;
+}
+
+a {
+  color: #333333;
+  outline: 0;
+  transition: color .3s, background-color .3s;
+  text-decoration: none;
+
+  &:hover {
+    color: #ed5f01;
+    cursor: pointer;
+  }
+}
+
+ins,
+a {
+  text-decoration: none;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-size: 1em;
+}
+
+pre {
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  font-family: auto;
+}

+ 3 - 0
src/styles/tailwind.less

@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

+ 914 - 0
src/styles/vant-ui.less

@@ -0,0 +1,914 @@
+// Color Palette
+@black: #000;
+@white: #fff;
+@gray-1: #f7f8fa;
+@gray-2: #f2f3f5;
+@gray-3: #ebedf0;
+@gray-4: #dcdee0;
+@gray-5: #c8c9cc;
+@gray-6: #969799;
+@gray-7: #646566;
+@gray-8: #323233;
+@gray-9: #f9f9f9;
+@gray-10: #999;
+@gray-11: #f0f0f0;
+@red: #ee0a24;
+@blue: #1989fa;
+// @orange: #ff976a;
+@orange: #F49532;
+@orange-dark: #ed6a0c;
+@orange-light: #fffbe8;
+@primary-color: #315EA3;
+@green: #07c160;
+
+// Gradient Colors
+@gradient-red: linear-gradient(to right, #ff6034, #ee0a24);
+@gradient-orange: linear-gradient(to right, #ffd01e, #ff8917);
+
+// Component Colors
+@text-color: @gray-8;
+@active-color: @gray-2;
+@active-opacity: 0.7;
+@disabled-opacity: 0.5;
+@background-color: @gray-1;
+@background-color-light: #fafafa;
+@text-link-color: #576b95;
+
+// Padding
+@padding-base: 0.4rem;
+@padding-xs: @padding-base * 2;
+@padding-sm: @padding-base * 3;
+@padding-md: @padding-base * 4;
+@padding-lg: @padding-base * 6;
+@padding-xl: @padding-base * 8;
+
+// Font
+@font-size-xs: 1rem;
+@font-size-sm: 1.2rem;
+@font-size-md: 1.4rem;
+@font-size-lg: 1.6rem;
+@font-weight-bold: 500;
+@line-height-xs: 1.4rem;
+@line-height-sm: 1.8rem;
+@line-height-md: 2rem;
+@line-height-lg: 2.2rem;
+@base-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue',
+  Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB',
+  'Microsoft Yahei', sans-serif;
+@price-integer-font-family: Avenir-Heavy, PingFang SC, Helvetica Neue, Arial,
+  sans-serif;
+
+// Animation
+@animation-duration-base: 0.3s;
+@animation-duration-fast: 0.2s;
+@animation-timing-function-enter: ease-out;
+@animation-timing-function-leave: ease-in;
+
+// Border
+@border-color: @gray-3;
+@border-width-base: 1px;
+@border-radius-sm: 2px;
+@border-radius-md: 4px;
+@border-radius-lg: 8px;
+@border-radius-max: 999px;
+
+// ActionSheet
+@action-sheet-max-height: 80%;
+@action-sheet-header-height: 4.8rem;
+@action-sheet-header-font-size: @font-size-lg;
+@action-sheet-description-color: @gray-6;
+@action-sheet-description-font-size: @font-size-md;
+@action-sheet-description-line-height: @line-height-md;
+@action-sheet-item-background: @white;
+@action-sheet-item-font-size: @font-size-lg;
+@action-sheet-item-line-height: @line-height-lg;
+@action-sheet-item-text-color: @text-color;
+@action-sheet-item-disabled-text-color: @gray-5;
+@action-sheet-subname-color: @gray-6;
+@action-sheet-subname-font-size: @font-size-sm;
+@action-sheet-subname-line-height: @line-height-sm;
+@action-sheet-close-icon-size: 2.2rem;
+@action-sheet-close-icon-color: @gray-5;
+@action-sheet-close-icon-active-color: @gray-6;
+@action-sheet-close-icon-padding: 0 @padding-md;
+@action-sheet-cancel-text-color: @gray-7;
+@action-sheet-cancel-padding-top: @padding-xs;
+@action-sheet-cancel-padding-color: @background-color;
+@action-sheet-loading-icon-size: 2.2rem;
+
+// AddressEdit
+@address-edit-padding: @padding-sm;
+@address-edit-buttons-padding: @padding-xl @padding-base;
+@address-edit-button-margin-bottom: @padding-sm;
+@address-edit-detail-finish-color: @primary-color;
+@address-edit-detail-finish-font-size: @font-size-sm;
+
+// AddressList
+@address-list-padding: @padding-sm @padding-sm 8rem;
+@address-list-disabled-text-color: @gray-6;
+@address-list-disabled-text-padding: @padding-base * 5 0 @padding-md;
+@address-list-disabled-text-font-size: @font-size-md;
+@address-list-disabled-text-line-height: @line-height-md;
+@address-list-add-button-z-index: 999;
+@address-list-item-padding: @padding-sm;
+@address-list-item-text-color: @text-color;
+@address-list-item-disabled-text-color: @gray-5;
+@address-list-item-font-size: 1.3rem;
+@address-list-item-line-height: @line-height-sm;
+@address-list-item-radio-icon-color: @red;
+@address-list-edit-icon-size: 2rem;
+
+// Badge
+@badge-size: 1.6rem;
+@badge-color: @white;
+@badge-padding: 0 0.3rem;
+@badge-font-size: @font-size-sm;
+@badge-font-weight: @font-weight-bold;
+@badge-border-width: @border-width-base;
+@badge-background-color: @red;
+@badge-dot-color: @red;
+@badge-dot-size: 0.8rem;
+@badge-font-family: -apple-system-font, Helvetica Neue, Arial, sans-serif;
+
+// Button
+@button-mini-height: 2.4rem;
+@button-mini-font-size: @font-size-xs;
+@button-small-height: 3.2rem;
+@button-small-font-size: @font-size-sm;
+@button-normal-font-size: @font-size-md;
+@button-large-height: 5rem;
+@button-default-height: 4.4rem;
+@button-default-line-height: 1.2;
+@button-default-font-size: @font-size-lg;
+@button-default-color: @text-color;
+@button-default-background-color: @white;
+@button-default-border-color: @border-color;
+@button-primary-color: @white;
+@button-primary-background-color: @green;
+@button-primary-border-color: @green;
+@button-info-color: @white;
+@button-info-background-color: @primary-color;
+@button-info-border-color: @primary-color;
+@button-danger-color: @white;
+@button-danger-background-color: @red;
+@button-danger-border-color: @red;
+@button-warning-color: @white;
+@button-warning-background-color: @orange;
+@button-warning-border-color: @orange;
+@button-border-width: @border-width-base;
+@button-border-radius: @border-radius-sm;
+@button-round-border-radius: @border-radius-max;
+@button-plain-background-color: @white;
+@button-disabled-opacity: @disabled-opacity;
+
+// Calendar
+@calendar-background-color: @white;
+@calendar-popup-height: 80%;
+@calendar-header-box-shadow: 0 2px 10px rgba(125, 126, 128, 0.16);
+@calendar-header-title-height: 4.4rem;
+@calendar-header-title-font-size: @font-size-lg;
+@calendar-header-subtitle-font-size: @font-size-md;
+@calendar-weekdays-height: 3rem;
+@calendar-weekdays-font-size: @font-size-sm;
+@calendar-month-title-font-size: @font-size-md;
+@calendar-month-mark-color: fade(@gray-2, 80%);
+@calendar-month-mark-font-size: 16rem;
+@calendar-day-height: 6.4rem;
+@calendar-day-font-size: @font-size-lg;
+@calendar-range-edge-color: @white;
+@calendar-range-edge-background-color: @red;
+@calendar-range-middle-color: @red;
+@calendar-range-middle-background-opacity: 0.1;
+@calendar-selected-day-size: 5.4rem;
+@calendar-selected-day-color: @white;
+@calendar-info-font-size: @font-size-xs;
+@calendar-info-line-height: @line-height-xs;
+@calendar-selected-day-background-color: @red;
+@calendar-day-disabled-color: @gray-5;
+@calendar-confirm-button-height: 3.6rem;
+@calendar-confirm-button-margin: .7rem 0;
+
+// Card
+@card-padding: @padding-xs @padding-md;
+@card-font-size: @font-size-sm;
+@card-text-color: @text-color;
+@card-background-color: @background-color-light;
+@card-thumb-size: 8.8rem;
+@card-thumb-border-radius: @border-radius-lg;
+@card-title-line-height: 1.6rem;
+@card-desc-color: @gray-7;
+@card-desc-line-height: @line-height-md;
+@card-price-color: @gray-8;
+@card-origin-price-color: @gray-6;
+@card-num-color: @gray-6;
+@card-origin-price-font-size: @font-size-xs;
+@card-price-font-size: @font-size-sm;
+@card-price-integer-font-size: @font-size-lg;
+@card-price-font-family: @price-integer-font-family;
+
+// Cascader
+@cascader-header-height: 4.8rem;
+@cascader-title-font-size: @font-size-lg;
+@cascader-title-line-height: 2rem;
+@cascader-close-icon-size: 2.2rem;
+@cascader-close-icon-color: @gray-5;
+@cascader-close-icon-active-color: @gray-6;
+@cascader-selected-icon-size: 1.8rem;
+@cascader-tabs-height: 4.8rem;
+@cascader-active-color: @red;
+@cascader-options-height: 38.4rem;
+@cascader-tab-color: @text-color;
+@cascader-unselected-tab-color: @gray-6;
+
+// Cell
+@cell-font-size: @font-size-md;
+@cell-line-height: 2.4rem;
+@cell-vertical-padding: 1rem;
+@cell-horizontal-padding: @padding-md;
+@cell-text-color: @text-color;
+@cell-background-color: @white;
+@cell-border-color: @border-color;
+@cell-active-color: @active-color;
+@cell-required-color: @red;
+@cell-label-color: @gray-6;
+@cell-label-font-size: @font-size-sm;
+@cell-label-line-height: @line-height-sm;
+@cell-label-margin-top: @padding-base;
+@cell-value-color: @gray-6;
+@cell-icon-size: 1.6rem;
+@cell-right-icon-color: @gray-6;
+@cell-large-vertical-padding: @padding-sm;
+@cell-large-title-font-size: @font-size-lg;
+@cell-large-label-font-size: @font-size-md;
+
+// CellGroup
+@cell-group-background-color: @white;
+@cell-group-title-color: @gray-6;
+@cell-group-title-padding: @padding-md @padding-md @padding-xs;
+@cell-group-title-font-size: @font-size-md;
+@cell-group-title-line-height: 1.6rem;
+@cell-group-inset-padding: 0 @padding-md;
+@cell-group-inset-border-radius: @border-radius-lg;
+@cell-group-inset-title-padding: @padding-md @padding-md @padding-xs @padding-xl;
+
+// Checkbox
+@checkbox-size: 2rem;
+@checkbox-border-color: @gray-5;
+@checkbox-transition-duration: @animation-duration-fast;
+@checkbox-label-margin: @padding-xs;
+@checkbox-label-color: @text-color;
+@checkbox-checked-icon-color: @primary-color;
+@checkbox-disabled-icon-color: @gray-5;
+@checkbox-disabled-label-color: @gray-5;
+@checkbox-disabled-background-color: @border-color;
+
+// Circle
+@circle-size: 10rem;
+@circle-color: @primary-color;
+@circle-layer-color: @white;
+@circle-text-color: @text-color;
+@circle-text-font-weight: @font-weight-bold;
+@circle-text-font-size: @font-size-md;
+@circle-text-line-height: @line-height-md;
+
+// Collapse
+@collapse-item-transition-duration: @animation-duration-base;
+@collapse-item-content-padding: @padding-sm @padding-md;
+@collapse-item-content-font-size: @font-size-md;
+@collapse-item-content-line-height: 1.5;
+@collapse-item-content-text-color: @gray-6;
+@collapse-item-content-background-color: @white;
+@collapse-item-title-disabled-color: @gray-5;
+
+// ContactCard
+@contact-card-padding: @padding-md;
+@contact-card-add-icon-size: 4rem;
+@contact-card-add-icon-color: @primary-color;
+@contact-card-value-line-height: @line-height-md;
+
+// ContactEdit
+@contact-edit-padding: @padding-md;
+@contact-edit-fields-radius: @border-radius-md;
+@contact-edit-buttons-padding: @padding-xl 0;
+@contact-edit-button-margin-bottom: @padding-sm;
+@contact-edit-button-font-size: 1.6rem;
+@contact-edit-field-label-width: 4.1em;
+
+// ContactList
+@contact-list-edit-icon-size: 1.6rem;
+@contact-list-add-button-z-index: 999;
+@contact-list-item-padding: @padding-md;
+
+// CountDown
+@count-down-text-color: @text-color;
+@count-down-font-size: @font-size-md;
+@count-down-line-height: @line-height-md;
+
+// Coupon
+@coupon-margin: 0 @padding-sm @padding-sm;
+@coupon-content-height: 8.4rem;
+@coupon-content-padding: 1.4rem 0;
+@coupon-background-color: @white;
+@coupon-active-background-color: @active-color;
+@coupon-border-radius: @border-radius-lg;
+@coupon-box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
+@coupon-head-width: 9.6rem;
+@coupon-amount-color: @red;
+@coupon-amount-font-size: 3rem;
+@coupon-currency-font-size: 40%;
+@coupon-name-font-size: @font-size-md;
+@coupon-disabled-text-color: @gray-6;
+@coupon-description-padding: @padding-xs @padding-md;
+@coupon-description-border-color: @border-color;
+
+// CouponCell
+@coupon-cell-selected-text-color: @text-color;
+
+// CouponList
+@coupon-list-background-color: @background-color;
+@coupon-list-field-padding: 5px 0 5px @padding-md;
+@coupon-list-exchange-button-height: 3.2rem;
+@coupon-list-close-button-height: 4rem;
+@coupon-list-empty-image-size: 20rem;
+@coupon-list-empty-tip-color: @gray-6;
+@coupon-list-empty-tip-font-size: @font-size-md;
+@coupon-list-empty-tip-line-height: @line-height-md;
+
+// Dialog
+@dialog-width: 32rem;
+@dialog-small-screen-width: 90%;
+@dialog-font-size: @font-size-lg;
+@dialog-transition: @animation-duration-base;
+@dialog-border-radius: 1.6rem;
+@dialog-background-color: @white;
+@dialog-header-font-weight: @font-weight-bold;
+@dialog-header-line-height: 2.4rem;
+@dialog-header-padding-top: 2.6rem;
+@dialog-header-isolated-padding: @padding-lg 0;
+@dialog-message-padding: @padding-lg;
+@dialog-message-font-size: @font-size-md;
+@dialog-message-line-height: @line-height-md;
+@dialog-message-max-height: 60vh;
+@dialog-has-title-message-text-color: @gray-7;
+@dialog-has-title-message-padding-top: @padding-xs;
+@dialog-button-height: 4.8rem;
+@dialog-round-button-height: 3.6rem;
+@dialog-confirm-button-text-color: @red;
+
+// Divider
+@divider-margin: @padding-md 0;
+@divider-text-color: @gray-6;
+@divider-font-size: @font-size-md;
+@divider-line-height: 2.4rem;
+@divider-border-color: @border-color;
+@divider-content-padding: @padding-md;
+@divider-content-left-width: 10%;
+@divider-content-right-width: 10%;
+
+// DropdownMenu
+@dropdown-menu-height: 4.8rem;
+@dropdown-menu-background-color: @white;
+@dropdown-menu-box-shadow: 0 2px 12px fade(@gray-7, 12);
+@dropdown-menu-title-font-size: 1.5rem;
+@dropdown-menu-title-text-color: @text-color;
+@dropdown-menu-title-active-text-color: @red;
+@dropdown-menu-title-disabled-text-color: @gray-6;
+@dropdown-menu-title-padding: 0 @padding-xs;
+@dropdown-menu-title-line-height: @line-height-lg;
+@dropdown-menu-option-active-color: @red;
+@dropdown-menu-content-max-height: 80%;
+@dropdown-item-z-index: 10;
+
+// Empty
+@empty-padding: @padding-xl 0;
+@empty-image-size: 16rem;
+@empty-description-margin-top: @padding-md;
+@empty-description-padding: 0 6rem;
+@empty-description-color: @gray-6;
+@empty-description-font-size: @font-size-md;
+@empty-description-line-height: @line-height-md;
+@empty-bottom-margin-top: 2.4rem;
+
+// Field
+@field-label-width: 6.2em;
+@field-label-color: @gray-7;
+@field-label-margin-right: @padding-sm;
+@field-input-text-color: @text-color;
+@field-input-error-text-color: @red;
+@field-input-disabled-text-color: @gray-5;
+@field-placeholder-text-color: @gray-5;
+@field-icon-size: 1.6rem;
+@field-clear-icon-size: 1.6rem;
+@field-clear-icon-color: @gray-5;
+@field-right-icon-color: @gray-6;
+@field-error-message-color: @red;
+@field-error-message-font-size: 1.2rem;
+@field-text-area-min-height: 6rem;
+@field-word-limit-color: @gray-7;
+@field-word-limit-font-size: @font-size-sm;
+@field-word-limit-line-height: 1.6rem;
+@field-disabled-text-color: @gray-5;
+
+// GridItem
+@grid-item-content-padding: @padding-md @padding-xs;
+@grid-item-content-background-color: @white;
+@grid-item-content-active-color: @active-color;
+@grid-item-icon-size: 2.8rem;
+@grid-item-text-color: @gray-7;
+@grid-item-text-font-size: @font-size-sm;
+
+// GoodsAction
+@goods-action-background-color: @white;
+@goods-action-height: 5rem;
+@goods-action-icon-width: 4.8rem;
+@goods-action-icon-height: 100%;
+@goods-action-icon-color: @text-color;
+@goods-action-icon-size: 1.8rem;
+@goods-action-icon-font-size: @font-size-xs;
+@goods-action-icon-active-color: @active-color;
+@goods-action-icon-text-color: @gray-7;
+@goods-action-button-height: 4rem;
+@goods-action-button-warning-color: @gradient-orange;
+@goods-action-button-danger-color: @gradient-red;
+
+// IndexAnchor
+@index-anchor-z-index: 1;
+@index-anchor-padding: 0 @padding-md;
+@index-anchor-text-color: @text-color;
+@index-anchor-font-weight: @font-weight-bold;
+@index-anchor-font-size: @font-size-md;
+@index-anchor-line-height: 3.2rem;
+@index-anchor-background-color: transparent;
+@index-anchor-sticky-text-color: @red;
+@index-anchor-sticky-background-color: @white;
+
+// IndexBar
+@index-bar-sidebar-z-index: 2;
+@index-bar-index-font-size: @font-size-xs;
+@index-bar-index-line-height: @line-height-xs;
+@index-bar-index-active-color: @red;
+
+// Info
+@info-size: 1.6rem;
+@info-color: @white;
+@info-padding: 0 0.3rem;
+@info-font-size: @font-size-sm;
+@info-font-weight: @font-weight-bold;
+@info-border-width: @border-width-base;
+@info-background-color: @red;
+@info-dot-color: @red;
+@info-dot-size: 0.8rem;
+@info-font-family: -apple-system-font, Helvetica Neue, Arial, sans-serif;
+
+// Image
+@image-placeholder-text-color: @gray-6;
+@image-placeholder-font-size: @font-size-md;
+@image-placeholder-background-color: @background-color;
+@image-loading-icon-size: 3.2rem;
+@image-loading-icon-color: @gray-4;
+@image-error-icon-size: 3.2rem;
+@image-error-icon-color: @gray-4;
+
+// ImagePreview
+@image-preview-index-text-color: @white;
+@image-preview-index-font-size: @font-size-md;
+@image-preview-index-line-height: @line-height-md;
+@image-preview-index-text-shadow: 0 1px 1px @gray-8;
+@image-preview-overlay-background-color: rgba(0, 0, 0, 0.9);
+@image-preview-close-icon-size: 2.2rem;
+@image-preview-close-icon-color: @gray-5;
+@image-preview-close-icon-active-color: @gray-6;
+@image-preview-close-icon-margin: @padding-md;
+@image-preview-close-icon-z-index: 1;
+
+// List
+@list-text-color: @gray-6;
+@list-text-font-size: @font-size-md;
+@list-text-line-height: 5rem;
+
+// Loading
+@loading-text-color: @gray-6;
+@loading-text-font-size: @font-size-md;
+@loading-spinner-color: @gray-5;
+@loading-spinner-size: 3rem;
+@loading-spinner-animation-duration: 0.8s;
+
+// NavBar
+@nav-bar-height: 4.6rem;
+@nav-bar-background-color: @white;
+@nav-bar-arrow-size: 1.6rem;
+@nav-bar-icon-color: @primary-color;
+@nav-bar-text-color: @primary-color;
+@nav-bar-title-font-size: @font-size-lg;
+@nav-bar-title-text-color: @text-color;
+@nav-bar-z-index: 1;
+
+// NoticeBar
+@notice-bar-height: 4rem;
+@notice-bar-padding: 0 @padding-md;
+@notice-bar-wrapable-padding: @padding-xs @padding-md;
+@notice-bar-text-color: @orange-dark;
+@notice-bar-font-size: @font-size-md;
+@notice-bar-line-height: 2.4rem;
+@notice-bar-background-color: @orange-light;
+@notice-bar-icon-size: 1.6rem;
+@notice-bar-icon-min-width: 2.4rem;
+
+// Notify
+@notify-text-color: @white;
+@notify-padding: @padding-xs @padding-md;
+@notify-font-size: @font-size-md;
+@notify-line-height: @line-height-md;
+@notify-primary-background-color: @primary-color;
+@notify-success-background-color: @green;
+@notify-danger-background-color: @red;
+@notify-warning-background-color: @orange;
+
+// NumberKeyboard
+@number-keyboard-background-color: @gray-2;
+@number-keyboard-key-height: 4.8rem;
+@number-keyboard-key-font-size: 2.8rem;
+@number-keyboard-key-active-color: @gray-3;
+@number-keyboard-delete-font-size: @font-size-lg;
+@number-keyboard-title-color: @gray-7;
+@number-keyboard-title-height: 3.4rem;
+@number-keyboard-title-font-size: @font-size-lg;
+@number-keyboard-close-padding: 0 @padding-md;
+@number-keyboard-close-color: @text-link-color;
+@number-keyboard-close-font-size: @font-size-md;
+@number-keyboard-button-text-color: @white;
+@number-keyboard-button-background-color: @primary-color;
+@number-keyboard-cursor-color: @text-color;
+@number-keyboard-cursor-width: 1px;
+@number-keyboard-cursor-height: 40%;
+@number-keyboard-cursor-animation-duration: 1s;
+@number-keyboard-z-index: 100;
+
+// Overlay
+@overlay-z-index: 1;
+@overlay-background-color: rgba(0, 0, 0, 0.7);
+
+// Pagination
+@pagination-height: 4rem;
+@pagination-font-size: @font-size-md;
+@pagination-item-width: 3.6rem;
+@pagination-item-default-color: @primary-color;
+@pagination-item-disabled-color: @gray-7;
+@pagination-item-disabled-background-color: @background-color;
+@pagination-background-color: @white;
+@pagination-desc-color: @gray-7;
+@pagination-disabled-opacity: @disabled-opacity;
+
+// Panel
+@panel-background-color: @white;
+@panel-header-value-color: @red;
+@panel-footer-padding: @padding-xs @padding-md;
+
+// PasswordInput
+@password-input-height: 5rem;
+@password-input-margin: 0 @padding-md;
+@password-input-font-size: 2rem;
+@password-input-border-radius: .6rem;
+@password-input-background-color: @white;
+@password-input-info-color: @gray-6;
+@password-input-info-font-size: @font-size-md;
+@password-input-error-info-color: @red;
+@password-input-dot-size: 1rem;
+@password-input-dot-color: @black;
+
+// Picker
+@picker-background-color: @white;
+@picker-toolbar-height: 4.4rem;
+@picker-title-font-size: @font-size-lg;
+@picker-title-line-height: @line-height-md;
+@picker-action-padding: 0 @padding-md;
+@picker-action-font-size: @font-size-md;
+@picker-confirm-action-color: @text-link-color;
+@picker-cancel-action-color: @gray-6;
+@picker-option-font-size: @font-size-lg;
+@picker-option-text-color: @black;
+@picker-option-disabled-opacity: 0.3;
+@picker-loading-icon-color: @primary-color;
+@picker-loading-mask-color: rgba(255, 255, 255, 0.9);
+
+// Popover
+@popover-arrow-size: 0.6rem;
+@popover-border-radius: @border-radius-lg;
+@popover-action-width: 12.8rem;
+@popover-action-height: 4.4rem;
+@popover-action-font-size: @font-size-md;
+@popover-action-line-height: @line-height-md;
+@popover-action-icon-size: 2rem;
+@popover-light-text-color: @text-color;
+@popover-light-background-color: @white;
+@popover-light-action-disabled-text-color: @gray-5;
+@popover-dark-text-color: @white;
+@popover-dark-background-color: #4a4a4a;
+@popover-dark-action-disabled-text-color: @gray-6;
+
+// Popup
+@popup-background-color: @white;
+@popup-transition: transform @animation-duration-base;
+@popup-round-border-radius: 1.6rem;
+@popup-close-icon-size: 2.2rem;
+@popup-close-icon-color: @gray-5;
+@popup-close-icon-active-color: @gray-6;
+@popup-close-icon-margin: 1.6rem;
+@popup-close-icon-z-index: 1;
+
+// Progress
+@progress-height: .4rem;
+@progress-color: @primary-color;
+@progress-background-color: @gray-3;
+@progress-pivot-padding: 0 .5rem;
+@progress-pivot-text-color: @white;
+@progress-pivot-font-size: @font-size-xs;
+@progress-pivot-line-height: 1.6;
+@progress-pivot-background-color: @primary-color;
+
+// PullRefresh
+@pull-refresh-head-height: 5rem;
+@pull-refresh-head-font-size: @font-size-md;
+@pull-refresh-head-text-color: @gray-6;
+
+// Radio
+@radio-size: 2rem;
+@radio-border-color: @gray-5;
+@radio-transition-duration: @animation-duration-fast;
+@radio-label-margin: @padding-xs;
+@radio-label-color: @text-color;
+@radio-checked-icon-color: @primary-color;
+@radio-disabled-icon-color: @gray-5;
+@radio-disabled-label-color: @gray-5;
+@radio-disabled-background-color: @border-color;
+
+// Rate
+@rate-icon-size: 2rem;
+@rate-icon-gutter: @padding-base;
+@rate-icon-void-color: @gray-5;
+@rate-icon-full-color: @red;
+@rate-icon-disabled-color: @gray-5;
+
+// ShareSheet
+@share-sheet-header-padding: @padding-sm @padding-md @padding-base;
+@share-sheet-title-color: @text-color;
+@share-sheet-title-font-size: @font-size-md;
+@share-sheet-title-line-height: @line-height-md;
+@share-sheet-description-color: @gray-6;
+@share-sheet-description-font-size: @font-size-sm;
+@share-sheet-description-line-height: 1.6rem;
+@share-sheet-icon-size: 4.8rem;
+@share-sheet-option-name-color: @gray-7;
+@share-sheet-option-name-font-size: @font-size-sm;
+@share-sheet-option-description-color: @gray-5;
+@share-sheet-option-description-font-size: @font-size-sm;
+@share-sheet-cancel-button-font-size: @font-size-lg;
+@share-sheet-cancel-button-height: 4.8rem;
+@share-sheet-cancel-button-background: @white;
+
+// Search
+@search-padding: 1rem @padding-sm;
+@search-background-color: @white;
+@search-content-background-color: @gray-9;
+@search-input-height: 3.6rem;
+@search-label-padding: 0 .5rem;
+@search-label-color: @text-color;
+@search-label-font-size: @font-size-md;
+@search-left-icon-color: @gray-6;
+@search-action-padding: 0 @padding-xs;
+@search-action-text-color: @text-color;
+@search-action-font-size: @font-size-md;
+
+// Sidebar
+@sidebar-width: 8rem;
+@sidebar-font-size: @font-size-md;
+@sidebar-line-height: @line-height-md;
+@sidebar-text-color: @text-color;
+@sidebar-disabled-text-color: @gray-5;
+@sidebar-padding: 2rem @padding-sm;
+@sidebar-active-color: @active-color;
+@sidebar-background-color: @background-color;
+@sidebar-selected-font-weight: @font-weight-bold;
+@sidebar-selected-text-color: @text-color;
+@sidebar-selected-border-width: .4rem;
+@sidebar-selected-border-height: 1.6rem;
+@sidebar-selected-border-color: @red;
+@sidebar-selected-background-color: @white;
+
+// Skeleton
+@skeleton-row-height: 1.6rem;
+@skeleton-row-background-color: @active-color;
+@skeleton-row-margin-top: @padding-sm;
+@skeleton-title-width: 40%;
+@skeleton-avatar-size: 3.2rem;
+@skeleton-avatar-background-color: @active-color;
+@skeleton-animation-duration: 1.2s;
+
+// Slider
+@slider-active-background-color: @primary-color;
+@slider-inactive-background-color: @gray-3;
+@slider-disabled-opacity: @disabled-opacity;
+@slider-bar-height: .2rem;
+@slider-button-width: 2.4rem;
+@slider-button-height: 2.4rem;
+@slider-button-border-radius: 50%;
+@slider-button-background-color: @white;
+@slider-button-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+
+// Step
+@step-text-color: @gray-6;
+@step-active-color: @green;
+@step-process-text-color: @text-color;
+@step-font-size: @font-size-md;
+@step-line-color: @border-color;
+@step-finish-line-color: @green;
+@step-finish-text-color: @text-color;
+@step-icon-size: 1.2rem;
+@step-circle-size: 0.5rem;
+@step-circle-color: @gray-6;
+@step-horizontal-title-font-size: @font-size-sm;
+
+// Steps
+@steps-background-color: @white;
+
+// Sticky
+@sticky-z-index: 99;
+
+// Stepper
+@stepper-active-color: #e8e8e8;
+@stepper-background-color: @active-color;
+@stepper-button-icon-color: @text-color;
+@stepper-button-disabled-color: @background-color;
+@stepper-button-disabled-icon-color: @gray-5;
+@stepper-button-round-theme-color: @red;
+@stepper-input-width: 3.2rem;
+@stepper-input-height: 2.8rem;
+@stepper-input-font-size: @font-size-md;
+@stepper-input-line-height: normal;
+@stepper-input-text-color: @text-color;
+@stepper-input-disabled-text-color: @gray-5;
+@stepper-input-disabled-background-color: @active-color;
+@stepper-border-radius: @border-radius-md;
+
+// SubmitBar
+@submit-bar-height: 5rem;
+@submit-bar-z-index: 100;
+@submit-bar-background-color: @white;
+@submit-bar-button-width: 11rem;
+@submit-bar-price-color: @red;
+@submit-bar-price-font-size: @font-size-md;
+@submit-bar-currency-font-size: @font-size-md;
+@submit-bar-text-color: @text-color;
+@submit-bar-text-font-size: @font-size-md;
+@submit-bar-tip-padding: @padding-xs @padding-sm;
+@submit-bar-tip-font-size: @font-size-sm;
+@submit-bar-tip-line-height: 1.5;
+@submit-bar-tip-color: #f56723;
+@submit-bar-tip-background-color: #fff7cc;
+@submit-bar-tip-icon-size: 1.2rem;
+@submit-bar-button-height: 4rem;
+@submit-bar-padding: 0 @padding-md;
+@submit-bar-price-integer-font-size: 2rem;
+@submit-bar-price-font-family: @price-integer-font-family;
+
+// Swipe
+@swipe-indicator-size: .6rem;
+@swipe-indicator-margin: @padding-sm;
+@swipe-indicator-active-opacity: 1;
+@swipe-indicator-inactive-opacity: 0.3;
+@swipe-indicator-active-background-color: @primary-color;
+@swipe-indicator-inactive-background-color: @border-color;
+
+// Switch
+@switch-size: 3rem;
+@switch-width: 2em;
+@switch-height: 1em;
+@switch-node-size: 1em;
+@switch-node-background-color: @white;
+@switch-node-box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05),
+  0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05);
+@switch-background-color: @white;
+@switch-on-background-color: @primary-color;
+@switch-transition-duration: @animation-duration-base;
+@switch-disabled-opacity: @disabled-opacity;
+@switch-border: @border-width-base solid rgba(0, 0, 0, 0.1);
+
+// SwitchCell
+@switch-cell-padding-top: @cell-vertical-padding - 1px;
+@switch-cell-padding-bottom: @cell-vertical-padding - 1px;
+@switch-cell-large-padding-top: @cell-large-vertical-padding - 1px;
+@switch-cell-large-padding-bottom: @cell-large-vertical-padding - 1px;
+
+// Tabbar
+// @tabbar-height: 5rem;
+@tabbar-height: 6.6rem;
+@tabbar-z-index: 1;
+@tabbar-background-color: @white;
+
+// TabbarItem
+@tabbar-item-font-size: @font-size-sm;
+@tabbar-item-text-color: @gray-7;
+@tabbar-item-active-color: @primary-color;
+@tabbar-item-active-background-color: @tabbar-background-color;
+@tabbar-item-line-height: 1;
+@tabbar-item-icon-size: 2.2rem;
+@tabbar-item-margin-bottom: .4rem;
+
+// Tab
+@tab-text-color: @gray-7;
+@tab-active-text-color: @text-color;
+@tab-disabled-text-color: @gray-5;
+@tab-font-size: @font-size-md;
+@tab-line-height: @line-height-md;
+
+// Tabs
+@tabs-default-color: @red;
+@tabs-line-height: 4.4rem;
+@tabs-card-height: 3rem;
+@tabs-nav-background-color: @white;
+@tabs-bottom-bar-width: 4rem;
+@tabs-bottom-bar-height: .3rem;
+@tabs-bottom-bar-color: @tabs-default-color;
+
+// Tag
+@tag-padding: 0 @padding-base;
+@tag-text-color: @white;
+@tag-font-size: @font-size-sm;
+@tag-border-radius: .2rem;
+@tag-line-height: 1.6rem;
+@tag-medium-padding: .2rem .6rem;
+@tag-large-padding: @padding-base @padding-xs;
+@tag-large-border-radius: @border-radius-md;
+@tag-large-font-size: @font-size-md;
+@tag-round-border-radius: @border-radius-max;
+@tag-danger-color: @red;
+@tag-primary-color: @primary-color;
+@tag-success-color: @green;
+@tag-warning-color: @orange;
+@tag-default-color: @gray-6;
+@tag-plain-background-color: @white;
+
+// Toast
+@toast-max-width: 70%;
+@toast-font-size: @font-size-md;
+@toast-text-color: @white;
+@toast-loading-icon-color: @white;
+@toast-line-height: @line-height-md;
+@toast-border-radius: @border-radius-lg;
+@toast-background-color: fade(@black, 70%);
+@toast-icon-size: 3.6rem;
+@toast-text-min-width: 9.6rem;
+@toast-text-padding: @padding-xs @padding-sm;
+@toast-default-padding: @padding-md;
+@toast-default-width: 8.8rem;
+@toast-default-min-height: 8.8rem;
+@toast-position-top-distance: 20%;
+@toast-position-bottom-distance: 20%;
+
+// TreeSelect
+@tree-select-font-size: @font-size-md;
+@tree-select-nav-background-color: @background-color;
+@tree-select-content-background-color: @white;
+@tree-select-nav-item-padding: 1.4rem @padding-sm;
+@tree-select-item-height: 4.8rem;
+@tree-select-item-active-color: @red;
+@tree-select-item-disabled-color: @gray-5;
+@tree-select-item-selected-size: 1.6rem;
+
+// Uploader
+@uploader-size: 8rem;
+@uploader-icon-size: 2.4rem;
+@uploader-icon-color: @gray-4;
+@uploader-text-color: @gray-6;
+@uploader-text-font-size: @font-size-sm;
+@uploader-upload-background-color: @gray-1;
+@uploader-upload-active-color: @active-color;
+@uploader-delete-color: @white;
+@uploader-delete-icon-size: 1.4rem;
+@uploader-delete-background-color: rgba(0, 0, 0, 0.7);
+@uploader-file-background-color: @background-color;
+@uploader-file-icon-size: 2rem;
+@uploader-file-icon-color: @gray-7;
+@uploader-file-name-padding: 0 @padding-base;
+@uploader-file-name-margin-top: @padding-xs;
+@uploader-file-name-font-size: @font-size-sm;
+@uploader-file-name-text-color: @gray-7;
+@uploader-mask-background-color: fade(@gray-8, 88%);
+@uploader-mask-icon-size: 2.2rem;
+@uploader-mask-message-font-size: @font-size-sm;
+@uploader-mask-message-line-height: @line-height-xs;
+@uploader-loading-icon-size: 2.2rem;
+@uploader-loading-icon-color: @white;
+@uploader-disabled-opacity: @disabled-opacity;
+
+// Sku
+@sku-item-background-color: @background-color;
+@sku-icon-gray-color: @gray-4;
+@sku-upload-mask-color: rgba(50, 50, 51, 0.8);
+
+.van-search__content {
+  height: 3.6rem;
+}
+.van-field__left-icon {
+  color: @primary-color;
+}

+ 29 - 0
src/utils/auth.js

@@ -0,0 +1,29 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+const ExpiresInKey = 'Admin-Expires-In'
+
+export function getToken() {
+  return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey)
+}
+
+export function getExpiresIn() {
+  return Cookies.get(ExpiresInKey) || -1
+}
+
+export function setExpiresIn(time) {
+  return Cookies.set(ExpiresInKey, time)
+}
+
+export function removeExpiresIn() {
+  return Cookies.remove(ExpiresInKey)
+}

+ 33 - 0
src/utils/common.js

@@ -0,0 +1,33 @@
+//使用递归的方式实现数组、对象的深拷贝
+export function deepClone(obj, hash = new WeakMap()) {
+  if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
+  if (obj instanceof Date) return new Date(obj)
+  if (obj instanceof RegExp) return new RegExp(obj)
+  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
+  if (typeof obj !== "object") return obj
+  // 是对象的话就要进行深拷贝
+  if (hash.get(obj)) return hash.get(obj)
+  let cloneObj = new obj.constructor()
+  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
+  hash.set(obj, cloneObj)
+  for (let key in obj) {
+    if (Object.prototype.hasOwnProperty.call(obj, key)) {
+      // 实现递归拷贝
+      cloneObj[key] = deepClone(obj[key], hash)
+    }
+  }
+  return cloneObj
+}
+
+//跳转页面
+export function gotoPage(url) {
+  this.$router.push(url)
+}
+
+//rem根据屏幕大小算大小(用于echarts等)/100
+export function getCount(res) {
+  let clientWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
+  if (!clientWidth) return;
+  let fontSize = 100 * (clientWidth / 3.75);
+  return Math.round(res * fontSize);
+}

+ 6 - 0
src/utils/errorCode.js

@@ -0,0 +1,6 @@
+export default {
+  '401': '认证失败,无法访问系统资源',
+  '403': '当前操作没有权限',
+  '404': '访问资源不存在',
+  'default': '系统未知错误,请反馈给管理员'
+}

+ 31 - 0
src/utils/jsencrypt.js

@@ -0,0 +1,31 @@
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
+
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+// const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+//   'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+const publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCXcWJBYncdJOVlOBLc3piwfJqX5JnbZHByThhWsm5IOb8UtIY7Chya3p6xvu0Px8NIEZpwBOPCq0EA4LtaejbKLDzwAvsNU++EjgXZyZhDMGFwVBIhtkLpCd9owfDggStI7WJzmdDf8y0bKAhkzJG2gHnewIAISlXqG1F4oq1l1QIDAQAB'
+
+const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+  '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+  'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+  'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+  'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+  'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+  'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+  'UP8iWi1Qw0Y='
+
+// 加密
+export function encrypt(txt) {
+  const encryptor = new JSEncrypt()
+  encryptor.setPublicKey(publicKey) // 设置公钥
+  return encryptor.encrypt(txt) // 对数据进行加密
+}
+
+// 解密
+export function decrypt(txt) {
+  const encryptor = new JSEncrypt()
+  encryptor.setPrivateKey(privateKey) // 设置私钥
+  return encryptor.decrypt(txt) // 对数据进行解密
+}
+

+ 155 - 0
src/utils/request.js

@@ -0,0 +1,155 @@
+import axios from 'axios'
+import { Toast  } from 'vant'
+// import store from '@/store'
+import { getToken } from './auth'
+import errorCode from './errorCode'
+// import { tansParams, blobValidate } from "@/utils/web_public";
+import { tansParams } from "./webPublic"
+import cache from '@/plugins/cache'
+// import { saveAs } from 'file-saver'
+
+// let downloadLoadingInstance;
+// 是否显示重新登录
+export let isRelogin = { show: false };
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+// 创建axios实例
+const service = axios.create({
+  // axios中请求配置有baseURL选项,表示请求URL公共部分
+  baseURL: process.env.VUE_APP_BASE_API,
+  // 超时
+  timeout: 50000
+})
+
+// request拦截器
+service.interceptors.request.use(config => {
+  // 是否需要设置 token
+  const isToken = (config.headers || {}).isToken === false
+  // 是否需要防止数据重复提交
+  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
+  if (getToken() && !isToken) {
+    console.log(getToken())
+    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+  }
+  // get请求映射params参数
+  if (config.method === 'get' && config.params) {
+    console.log(tansParams(config.params))
+    let url = config.url + '?' + tansParams(config.params);
+    url = url.slice(0, -1);
+    config.params = {};
+    config.url = url;
+  }
+  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+    const requestObj = {
+      url: config.url,
+      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+      time: new Date().getTime()
+    }
+    const sessionObj = cache.session.getJSON('sessionObj')
+    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+      cache.session.setJSON('sessionObj', requestObj)
+    } else {
+      const s_url = sessionObj.url;                  // 请求地址
+      const s_data = sessionObj.data;                // 请求数据
+      const s_time = sessionObj.time;                // 请求时间
+      const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交
+      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+        const message = '数据正在处理,请勿重复提交';
+        console.warn(`[${s_url}]: ` + message)
+        return Promise.reject(new Error(message))
+      } else {
+        cache.session.setJSON('sessionObj', requestObj)
+      }
+    }
+  }
+  return config
+}, error => {
+    console.log(error)
+    Promise.reject(error)
+})
+
+// 响应拦截器
+service.interceptors.response.use(res => {
+    // 未设置状态码则默认成功状态
+    const code = res.data.code || 200;
+    // 获取错误信息
+    const msg = errorCode[code] || res.data.msg || errorCode['default']
+    // 二进制数据则直接返回
+    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
+      return res.data
+    }
+    if (code === 401) {
+    //   if (!isRelogin.show) {
+    //     isRelogin.show = true;
+    //     Dialog.confirm({
+    //       message: '登录状态已过期,您可以继续留在该页面,或者重新登录', 
+    //       title: '系统提示',
+    //       confirmButtonText: '重新登录',
+    //       cancelButtonText: '取消',
+    //       type: 'warning'
+    //     }
+    //   ).then(() => {
+    //     isRelogin.show = false;
+    //     store.dispatch('LogOut').then(() => {
+    //       location.href = '/';
+    //     })
+    //   }).catch(() => {
+    //     isRelogin.show = false;
+    //   })
+    // }
+      Toast.fail('登录状态已过期,您可以继续留在该页面,或者重新登录')
+      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
+    } else if (code === 500) {
+      Toast.fail(msg)
+      return Promise.reject(new Error(msg))
+    } else if (code !== 200) {
+      Toast.fail(msg)
+      return Promise.reject('error')
+    } else {
+      return res.data
+    }
+  },
+  error => {
+    console.log('err' + error)
+    let { message } = error;
+    if (message == "Network Error") {
+      message = "后端接口连接异常";
+    }
+    else if (message.includes("timeout")) {
+      message = "系统接口请求超时";
+    }
+    else if (message.includes("Request failed with status code")) {
+      message = "系统接口" + message.substr(message.length - 3) + "异常";
+    }
+    Toast.fail(message)
+    return Promise.reject(error)
+  }
+)
+
+// 通用下载方法
+// export function download(url, params, filename) {
+//   downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
+//   return service.post(url, params, {
+//     transformRequest: [(params) => { return tansParams(params) }],
+//     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+//     responseType: 'blob'
+//   }).then(async (data) => {
+//     const isLogin = await blobValidate(data);
+//     if (isLogin) {
+//       const blob = new Blob([data])
+//       saveAs(blob, filename)
+//     } else {
+//       const resText = await data.text();
+//       const rspObj = JSON.parse(resText);
+//       const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+//       Message.error(errMsg);
+//     }
+//     downloadLoadingInstance.close();
+//   }).catch((r) => {
+//     console.error(r)
+//     Message.error('下载文件出现错误,请联系管理员!')
+//     downloadLoadingInstance.close();
+//   })
+// }
+
+export default service

+ 6 - 0
src/utils/settings.js

@@ -0,0 +1,6 @@
+// vant-list-提示
+export const listToastObj = {
+    finishedText: "没有更多了",
+    loadingText: "加载中...",
+    errorText: "加载失败!",
+};

+ 7 - 0
src/utils/validate.js

@@ -0,0 +1,7 @@
+/**
+ * @param {string} path  svg转换使用
+ * @returns {Boolean}
+ */
+ export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}

+ 262 - 0
src/utils/webPublic.js

@@ -0,0 +1,262 @@
+
+
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2022 yuzhi
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+  if (this.$refs[refName]) {
+    this.$refs[refName].resetFields();
+  }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+  let search = params;
+  search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+  dateRange = Array.isArray(dateRange) ? dateRange : [];
+  if (typeof (propName) === 'undefined') {
+    search.params['beginTime'] = dateRange[0];
+    search.params['endTime'] = dateRange[1];
+  } else {
+    search.params['begin' + propName] = dateRange[0];
+    search.params['end' + propName] = dateRange[1];
+  }
+  return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+  if (value === undefined) {
+    return "";
+  }
+  var actions = [];
+  Object.keys(datas).some((key) => {
+    if (datas[key].value == ('' + value)) {
+      actions.push(datas[key].label);
+      return true;
+    }
+  })
+  if (actions.length === 0) {
+    actions.push(value);
+  }
+  return actions.join('');
+}
+
+// 回显数据字典(字符串数组)
+export function selectDictLabels(datas, value, separator) {
+  if (value === undefined) {
+    return "";
+  }
+  var actions = [];
+  var currentSeparator = undefined === separator ? "," : separator;
+  var temp = value.split(currentSeparator);
+  Object.keys(value.split(currentSeparator)).some((val) => {
+    var match = false;
+    Object.keys(datas).some((key) => {
+      if (datas[key].value == ('' + temp[val])) {
+        actions.push(datas[key].label + currentSeparator);
+        match = true;
+      }
+    })
+    if (!match) {
+      actions.push(temp[val] + currentSeparator);
+    }
+  })
+  return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+  var args = arguments, flag = true, i = 1;
+  str = str.replace(/%s/g, function () {
+    var arg = args[i++];
+    if (typeof arg === 'undefined') {
+      flag = false;
+      return '';
+    }
+    return arg;
+  });
+  return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+  if (!str || str == "undefined" || str == "null") {
+    return "";
+  }
+  return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+  for (var p in target) {
+    try {
+      if (target[p].constructor == Object) {
+        source[p] = mergeRecursive(source[p], target[p]);
+      } else {
+        source[p] = target[p];
+      }
+    } catch (e) {
+      source[p] = target[p];
+    }
+  }
+  return source;
+}
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[parentId].push(d);
+  }
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (nodeIds[parentId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params  参数
+*/
+export function tansParams(params) {
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    var part = encodeURIComponent(propName) + "=";
+    if (value !== null && value !== "" && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && value !== "" && typeof (value[key]) !== 'undefined') {
+            let params = propName + '[' + key + ']';
+            var subPart = encodeURIComponent(params) + "=";
+            result += subPart + encodeURIComponent(value[key]) + "&";
+          }
+        }
+      } else {
+        result += part + encodeURIComponent(value) + "&";
+      }
+    }
+  }
+  return result
+}
+
+// 验证是否为blob格式
+export async function blobValidate(data) {
+  try {
+    const text = await data.text();
+    JSON.parse(text);
+    return false;
+  } catch (error) {
+    return true;
+  }
+}
+
+// 判断是不是本周内的时间
+export function isCurrentWeek(past) {
+  const pastTime = new Date(past).getTime()
+  const today = new Date(new Date().toLocaleDateString())
+  let day = today.getDay()
+  day = day == 0 ? 7 : day
+  const oneDayTime = 60*60*24*1000
+  const monday = new Date(today.getTime() - (oneDayTime * (day - 1)))
+  const nextMonday = new Date(today.getTime() + (oneDayTime * (8 - day)))
+  if(monday.getTime() <= pastTime && nextMonday.getTime() > pastTime) {
+    return true
+  } else {
+    return false
+  }
+}
+
+// 判断是否当天
+export function isCurrentDay(date) {
+  return new Date().toDateString() === new Date(date).toDateString()
+}
+
+// 判断是否是昨天
+export function isYesterDay(date) {
+  return new Date(date).getDate() === (new Date().getDate() - 1)
+}

+ 259 - 0
src/views/topic/index.vue

@@ -0,0 +1,259 @@
+<template>
+  <div class="com-bg topic">
+    <div class="com-title"></div>
+    <div class="com-btn" v-show="selectFlag !== 2" @click="fnSubmit">确定</div>
+    <div class="com-btn" v-show="selectFlag === 2 && selectFlag !== 2" @click="fnNext">下一题</div>
+    <div class="com-btn" v-show="selectFlag === 2 && selectFlag === 2" @click="fnNext">完成</div>
+    <div class="com-card">
+      <div class="title">
+        单选题
+        <div class="right"><span>{{ actIndex + 1 }}</span>/{{ topicList.length || 0 }}</div>
+      </div>
+      <div class="content">
+        <div class="tit">
+          {{ topicList[actIndex].title || '' }}
+        </div>
+        <div class="option-cont">
+          <div
+            class="com-option"
+            :class="{
+              'act': selectVal == index && selectFlag == 0,
+              'true': selectVal == index && selectFlag == 1,
+              'false': selectVal == index && selectFlag == 2,
+            }"
+            v-for="(item, index) in topicList[actIndex].option || []"
+            :key="index"
+            @click="fnSelect(index)"
+          >
+            <span>{{ optionIndex[index] }}. </span>{{ item || '' }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 解析 -->
+    <div class="com-card" v-if="selectFlag === 2">
+      <div class="title">正确答案</div>
+      <div class="content">
+        <div class="tit">
+          正确答案:{{ optionIndex[topicList[actIndex].answer] || '' }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Topic",
+  data() {
+    return {
+      optionIndex: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
+      fakeJson: require("../../api/topic.json"),
+      topicList: [],
+      actIndex: 0,
+      selectFlag: 0,//0-未选择,1-选择正确,2-选择错误
+      selectVal: null,
+      trueCount: 0,
+      len: 1,//题目数据个数
+    }
+  },
+  created() {
+    this.fnInit();
+  },
+  methods: {
+    fnInit() {
+      if(this.len >= this.fakeJson.topicList.length) {// 排错判断
+        this.topicList = this.fakeJson.topicList;
+        return;
+      }
+      let indexArr = [], topicList = [];
+      for(var i = 0; i < this.len; i++) {
+        this.fnGetIndex(indexArr, topicList);
+      }
+      this.topicList = topicList;
+    },
+    fnGetIndex(indexArr, topicList) {
+      let index = Math.floor(Math.random() * this.fakeJson.topicList.length);//取0~所有数据总数之间的随机整数
+      if(indexArr.indexOf(index) === -1) {
+        indexArr.push(index);
+        topicList.push(this.fakeJson.topicList[index]);
+      }else {
+        this.fnGetIndex(indexArr, topicList);
+      }
+    },
+    fnSubmit() {
+      if(!(this.selectVal || this.selectVal === 0)) {
+        this.$toast("请选择答案!");
+        return;
+      }
+      let that = this;
+      if(this.topicList[this.actIndex].answer == this.selectVal) {//正确
+        this.selectFlag = 1;
+        this.trueCount++;
+        let setTimeoutPage =  setTimeout(() => {
+          that.fnNext();
+          clearTimeout(setTimeoutPage);
+        }, 300);
+      }else {//错误
+        this.selectFlag = 2;
+      }
+    },
+    fnNext() {
+      if(this.actIndex < this.topicList.length - 1) {
+        this.actIndex++;
+        this.fnClear();
+      }else {
+        this.$router.push(`/result?all=${this.topicList.length}&num=${this.trueCount}`);
+      }
+    },
+    fnClear() {
+      this.selectFlag = 0;
+      this.selectVal = null;
+    },
+    fnSelect(index) {
+      if(this.selectFlag === 0) this.selectVal = index;
+    }
+  }
+};
+</script>
+
+<style lang="less" scoped>
+.topic {
+  min-height: 100vh;
+  box-sizing: border-box;
+  padding: 29px 16px;
+  text-align: right;
+}
+.com-title {
+  height: 18px;
+  background: url(../../assets/images/title.png) center no-repeat;
+  background-size: auto 100%;
+  margin-bottom: 26px;
+}
+.com-bg {
+  background: url(../../assets/images/bg.png) center top no-repeat;
+  background-size: 100% auto;
+}
+.com-btn {
+  cursor: pointer;
+  display: inline-block;
+  min-width: 70px;
+  text-align: center;
+  box-sizing: border-box;
+  padding: 0 14px;
+  background: #b59f87;
+  border-radius: 4px;
+  height: 32px;
+  line-height: 32px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #ffffff;
+  margin-left: 10px;
+}
+.com-card {
+  margin-top: 17px;
+  background: #ffffff;
+  box-shadow: 0px 4px 4px 0px rgba(225, 225, 225, 0.5);
+  border-radius: 4px;
+  box-sizing: border-box;
+  padding-bottom: 15px;
+  font-size: 16px;
+  font-weight: 400;
+  color: #181e24;
+  line-height: 30px;
+  text-align: left;
+  .title {
+    font-size: 19px;
+    font-weight: 600;
+    color: #181e24;
+    line-height: 58px;
+    overflow: hidden;
+    box-sizing: border-box;
+    padding: 0 12px 0 25px;
+    border-bottom: 1px solid #ededed;
+    position: relative;
+    &::before {
+      content: "";
+      position: absolute;
+      left: 14px;
+      top: 20px;
+      width: 3px;
+      height: 18px;
+      background: #e51d0e;
+    }
+    .right {
+      float: right;
+      font-size: 19px;
+      color: #999999;
+      line-height: 50px;
+      & > span {
+        font-size: 35px;
+        font-weight: 500;
+        color: #181e24;
+        line-height: 50px;
+      }
+    }
+  }
+  .content {
+    box-sizing: border-box;
+    padding: 20px 12px 0;
+    .tit {
+      margin-bottom: 25px;
+    }
+  }
+}
+.com-option {
+  background: #fbfcfe;
+  border: 1px solid #f3f4f6;
+  border-radius: 4px;
+  box-sizing: border-box;
+  margin-bottom: 12px;
+  padding: 11px 19px;
+  font-size: 16px;
+  font-weight: 400;
+  color: #999999;
+  line-height: 28px;
+  overflow: hidden;
+  &>span {
+    float: left;
+    width: 28px;
+    height: 28px;
+    text-align: right;
+    box-sizing: border-box;
+    padding-right: 10px;
+  }
+  &.act {
+    background: #F6F5F3;
+    border: 1px solid #B4A387;
+    font-weight: 500;
+    color: #B7A08B;
+    &>span {
+      color: #ffffff;
+      background: url(../../assets/images/fingerprint.png) left center no-repeat;
+      background-size: auto 100%;
+    }
+  }
+  &.true {
+    background: #E7F7EC;
+    font-weight: 500;
+    color: #2CC638;
+    border-color: #E7F7EC;
+    &>span {
+      color: transparent;
+      background: url(../../assets/images/true.png) left center no-repeat;
+      background-size: 15px auto;
+    }
+  }
+  &.false {
+    background: #FBEDED;
+    font-weight: 500;
+    color: #E55352;
+    border-color: #FBEDED;
+    &>span {
+      color: transparent;
+      background: url(../../assets/images/false.png) left center no-repeat;
+      background-size: 15px auto;
+    }
+  }
+}
+</style>

+ 240 - 0
src/views/topic/result.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="com-bg topic">
+    <div class="com-title"></div>
+    <div class="com-card">
+      <div class="result">
+        <div class="top">
+          <p>本次答对题目数</p>
+          <h6>{{ num || 0 }}</h6>
+        </div>
+        <div class="text">
+          <div class="li">正确率:{{ Math.round(num/all * 100) }}%</div>
+          <div class="li">错题数:{{ all - num }}</div>
+        </div>
+        <div class="btn-cont">
+          <div class="com-btn" @click="fnGoTopic">再来一组</div>
+          <div class="com-btn" @click="fnGoOut">点击进入</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Result",
+  data() {
+    return {
+      all: 0,
+      num: 0,
+    }
+  },
+  created() {
+    this.all = this.$route.query.all ? Number(this.$route.query.all) : 0;
+    this.num = this.$route.query.num ? Number(this.$route.query.num) : 0;
+  },
+  methods: {
+    fnGoTopic() {
+      this.$router.push(`/topic`);
+    },
+    fnGoOut() {
+      // 跳转到安徽小康
+      // window.location.href = 'http://wap.ahjlxkgc.cn';
+      
+      window.parent.document.postMessage('test', '*')
+    }
+  }
+};
+</script>
+
+<style lang="less" scoped>
+.topic {
+  min-height: 100vh;
+  box-sizing: border-box;
+  padding: 29px 16px;
+  text-align: right;
+}
+.result {
+  .top {
+    min-height: 144px;
+    background: url(../../assets/images/contbg.png) top center no-repeat;
+    background-size: 100% auto;
+    box-sizing: border-box;
+    padding: 30px 0 0 33px;
+    font-size: 16px;
+    font-weight: 400;
+    color: #56390F;
+    line-height: 24px;
+    h6 {
+      font-size: 40px;
+      font-weight: 400;
+      color: #56390F;
+      line-height: 50px;
+      margin-top: 15px;
+    }
+  }
+  .text {
+    font-size: 16px;
+    font-weight: 400;
+    color: #181E24;
+    line-height: 26px;
+    margin-top: 30px;
+    margin-bottom: 60px;
+    box-sizing: border-box;
+    padding: 0 30px;
+    .li {
+      display: inline-block;
+      width: 50%;
+      box-sizing: border-box;
+      padding-left: 10px;
+    }
+  }
+  .btn-cont {
+    .com-btn {
+      width: 145px;
+      height: 43px;
+      line-height: 43px;
+      margin-left: 18px;
+      &:nth-child(2n+1) {
+        box-sizing: border-box;
+        border: 1px solid #B4A387;
+        background: #ffffff;
+        color: #B59F87;
+        line-height: 41px;
+      }
+    }
+  }
+}
+.com-title {
+  height: 18px;
+  background: url(../../assets/images/title.png) center no-repeat;
+  background-size: auto 100%;
+  margin-bottom: 26px;
+}
+.com-bg {
+  background: #F6F6F6 url(../../assets/images/bg.png) center top no-repeat;
+  background-size: 100% auto;
+}
+.com-btn {
+  cursor: pointer;
+  display: inline-block;
+  min-width: 70px;
+  text-align: center;
+  box-sizing: border-box;
+  padding: 0 14px;
+  background: #b59f87;
+  border-radius: 4px;
+  height: 32px;
+  line-height: 32px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #ffffff;
+  margin-left: 10px;
+}
+.com-card {
+  margin-top: 17px;
+  background: #ffffff;
+  box-shadow: 0px 4px 4px 0px rgba(225, 225, 225, 0.5);
+  border-radius: 4px;
+  box-sizing: border-box;
+  padding-bottom: 15px;
+  font-size: 16px;
+  font-weight: 400;
+  color: #181e24;
+  line-height: 30px;
+  text-align: left;
+  .title {
+    font-size: 19px;
+    font-weight: 600;
+    color: #181e24;
+    line-height: 58px;
+    overflow: hidden;
+    box-sizing: border-box;
+    padding: 0 12px 0 25px;
+    border-bottom: 1px solid #ededed;
+    position: relative;
+    &::before {
+      content: "";
+      position: absolute;
+      left: 14px;
+      top: 20px;
+      width: 3px;
+      height: 18px;
+      background: #e51d0e;
+    }
+    .right {
+      float: right;
+      font-size: 19px;
+      color: #999999;
+      line-height: 50px;
+      & > span {
+        font-size: 35px;
+        font-weight: 500;
+        color: #181e24;
+        line-height: 50px;
+      }
+    }
+  }
+  .content {
+    box-sizing: border-box;
+    padding: 20px 12px 0;
+    .tit {
+      margin-bottom: 25px;
+    }
+  }
+}
+.com-option {
+  background: #fbfcfe;
+  border: 1px solid #f3f4f6;
+  border-radius: 4px;
+  box-sizing: border-box;
+  margin-bottom: 12px;
+  padding: 11px 19px;
+  font-size: 16px;
+  font-weight: 400;
+  color: #999999;
+  line-height: 28px;
+  overflow: hidden;
+  &>span {
+    float: left;
+    width: 28px;
+    height: 28px;
+    text-align: right;
+    box-sizing: border-box;
+    padding-right: 10px;
+  }
+  &.act {
+    background: #F6F5F3;
+    border: 1px solid #B4A387;
+    font-weight: 500;
+    color: #B7A08B;
+    &>span {
+      color: #ffffff;
+      background: url(../../assets/images/fingerprint.png) left center no-repeat;
+      background-size: auto 100%;
+    }
+  }
+  &.true {
+    background: #E7F7EC;
+    font-weight: 500;
+    color: #2CC638;
+    border-color: #E7F7EC;
+    &>span {
+      color: transparent;
+      background: url(../../assets/images/true.png) left center no-repeat;
+      background-size: 15px auto;
+    }
+  }
+  &.false {
+    background: #FBEDED;
+    font-weight: 500;
+    color: #E55352;
+    border-color: #FBEDED;
+    &>span {
+      color: transparent;
+      background: url(../../assets/images/false.png) left center no-repeat;
+      background-size: 15px auto;
+    }
+  }
+}
+</style>

+ 41 - 0
tailwind.config.js

@@ -0,0 +1,41 @@
+module.exports = {
+  purge: [
+    './public/**/*.html',
+    './src/**/*.vue'
+  ],
+  darkMode: false, // or 'media' or 'class'
+  theme: {
+    fontSize: {
+      'fs12': '1.2rem',
+      'fs14': '1.4rem',
+      'fs16': '1.6rem',
+      'fs18': '1.8rem',
+      'fs20': '2rem',
+      'fs24': '2.4rem'
+    },
+    // magin: {
+    //   '08': '0.8rem',
+    //   '06': '0.6rem',
+    //   '04': '0.4rem'
+    // },
+    // padding: {
+    //   '08': '0.8rem',
+    //   '06': '0.6rem',
+    //   '04': '0.4rem',
+    // },
+    extend: {
+      spacing: {
+        '02': '0.2rem',
+        '04': '0.4rem',
+        '06': '0.6rem',
+        '08': '0.8rem',
+        '4.8': '1.2rem',
+        '5.6': '1.4rem',
+        '6.4': '1.6rem',
+        '7.2': '1.8rem',
+      }
+    },
+  },
+  variants: {},
+  plugins: [],
+}

+ 102 - 0
vue.config.js

@@ -0,0 +1,102 @@
+const path = require('path')
+const resolve = dir => path.join(__dirname, '.', dir)
+module.exports = {
+  /**
+   * baseUrl等价于webpack打包的资源引用的publishPath和全局的BASER_URL变量
+   */
+  publicPath: "/anhuiwell-h5",
+
+  // 打包的静态资源相对outputDir的路径,outputDir使用默认值'dist'
+  assetsDir: 'static',
+  /**
+   * 开启保存自动代码检测,设置true可配合derServer overlay浏览器上显示错误,强制进行代码检测
+   * 也可设置error可直接阻断打包
+   */
+  lintOnSave: 'error',
+  // 默认不使用template渲染模式,明确只使用运行时打包项目
+  runtimeCompiler: false,
+  /**
+   * 开发环境还是使用默认的 inline-source-map
+   */
+  productionSourceMap: false,
+  /**
+   * 给插入index.html的link、script标签添加crossorigin属性
+   */
+  // crossorigin: 'anonymous',
+  
+  css: {
+    loaderOptions: {
+      less: {
+        lessOptions: {
+          modifyVars: {
+            hack: `true; @import "@/styles/vant-ui.less";`
+          }
+        }
+      }
+    }
+  },
+  chainWebpack: config => {
+    // config
+    // .plugin('webpack-bundle-analyzer')
+    // .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
+    // 提取 manifest(runtime)
+    config.optimization
+      .runtimeChunk({
+        name: 'manifest'
+      })
+    // 代码分包调整
+    config.optimization
+      .splitChunks({
+        chunks: 'all',
+        cacheGroups: {
+          libs: {
+            name: 'chunk-libs',
+            test: /[\\/]node_modules[\\/]/,
+            chunks: 'initial' // 只打包初始时依赖的第三方
+          },
+          commons: {
+            name: 'chunk-commons',
+            test: resolve('src'), // 可自定义拓展你的规则
+            chunks: 'initial',
+            reuseExistingChunk: true
+          }
+        }
+      })
+    // config.module
+    //   .rule('scss')
+    //   .loader('sass-resources-loader')
+    //   .options({
+    //     resources: path.resolve('src/styles/global.scss')
+    //   })
+    //   .end()
+    config.module
+      .rule('svg')
+      .exclude.add(resolve('src/icons'))
+      .end()
+    config.module
+      .rule('icons')
+      .test(/\.svg$/)
+      .include.add(resolve('src/icons'))
+      .end()
+      .use('svg-sprite-loader')
+      .loader('svg-sprite-loader')
+      .options({
+        symbolId: 'icon-[name]'
+      })
+      .end()
+
+    config.resolve.alias
+      .set('src', resolve('src'))
+      .set('@', resolve('src'))
+      .set('@api', resolve('src/api'))
+      .set('@components', resolve("src/components"))
+      .set('@assets', resolve('src/assets'))
+      .set('@store', resolve('src/store')) 
+      .set('@utils', resolve('src/utils'))
+      .set('@views', resolve('src/views'))
+      // .set('@', resolve('public'))
+      
+    // 给异步加载的静态资源标签添加crossorigin属性
+    config.output.crossOriginLoading('anonymous')
+  },
+}