feat: 登陆新增图片验证码功能 (#31)
This commit is contained in:
parent
2df12109a5
commit
8dbcca68a8
@ -4,11 +4,10 @@
|
|||||||
"description": "survey server template",
|
"description": "survey server template",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"copy": "mkdir -p ./build/ && cp -rf ./src/* ./build/",
|
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start:stable": "npm run copy && npm run build && SERVER_ENV=stable node ./build/index.js",
|
"start:stable": "npm run build && SERVER_ENV=stable node ./build/index.js",
|
||||||
"start:preonline": "npm run copy && npm run build && SERVER_ENV=preonline node ./build/index.js",
|
"start:preonline": "npm run build && SERVER_ENV=preonline node ./build/index.js",
|
||||||
"start:online": "npm run copy && npm run build && SERVER_ENV=online node ./build/index.js",
|
"start:online": "npm run build && SERVER_ENV=online node ./build/index.js",
|
||||||
"start": "npm run start:online",
|
"start": "npm run start:online",
|
||||||
"local": "npx ts-node scripts/run-local.ts",
|
"local": "npx ts-node scripts/run-local.ts",
|
||||||
"dev": "npx ts-node-dev ./src/index.ts"
|
"dev": "npx ts-node-dev ./src/index.ts"
|
||||||
@ -35,7 +34,8 @@
|
|||||||
"koa-router": "^12.0.0",
|
"koa-router": "^12.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongodb": "^5.7.0"
|
"mongodb": "^5.7.0",
|
||||||
|
"svg-captcha": "^1.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.21.0",
|
"node": ">=14.21.0",
|
||||||
|
@ -2,17 +2,29 @@ import { SurveyServer } from '../../decorator'
|
|||||||
import { Request, Response } from 'koa'
|
import { Request, Response } from 'koa'
|
||||||
import * as Joi from 'joi'
|
import * as Joi from 'joi'
|
||||||
import { userService } from './service/userService'
|
import { userService } from './service/userService'
|
||||||
|
import { captchaService } from './service/captchaService'
|
||||||
import { getValidateValue } from './utils/index'
|
import { getValidateValue } from './utils/index'
|
||||||
|
|
||||||
|
import { CommonError } from '../../types/index'
|
||||||
export default class User {
|
export default class User {
|
||||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
|
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
|
||||||
async register({ req, res }: { req: Request, res: Response }) {
|
async register({ req, res }: { req: Request, res: Response }) {
|
||||||
const userInfo = getValidateValue(Joi.object({
|
const userInfo = getValidateValue(Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
password: Joi.string().required(),
|
password: Joi.string().required(),
|
||||||
|
captchaId: Joi.string().required(),
|
||||||
|
captcha: Joi.string().required(),
|
||||||
}).validate(req.body, { allowUnknown: true }));
|
}).validate(req.body, { allowUnknown: true }));
|
||||||
const userRegisterRes = await userService.register(userInfo)
|
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId })
|
||||||
|
if (!isCorrect) {
|
||||||
|
throw new CommonError('验证码不正确')
|
||||||
|
}
|
||||||
|
const userRegisterRes = await userService.register({
|
||||||
|
username: userInfo.username,
|
||||||
|
password: userInfo.password,
|
||||||
|
})
|
||||||
|
// 删除验证码
|
||||||
|
captchaService.deleteCaptcha({ id: userInfo.captchaId })
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data: userRegisterRes,
|
data: userRegisterRes,
|
||||||
@ -24,8 +36,19 @@ export default class User {
|
|||||||
const userInfo = getValidateValue(Joi.object({
|
const userInfo = getValidateValue(Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
password: Joi.string().required(),
|
password: Joi.string().required(),
|
||||||
|
captchaId: Joi.string().required(),
|
||||||
|
captcha: Joi.string().required(),
|
||||||
}).validate(req.body, { allowUnknown: true }));
|
}).validate(req.body, { allowUnknown: true }));
|
||||||
const data = await userService.login(userInfo)
|
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId })
|
||||||
|
if (!isCorrect) {
|
||||||
|
throw new CommonError('验证码不正确')
|
||||||
|
}
|
||||||
|
const data = await userService.login({
|
||||||
|
username: userInfo.username,
|
||||||
|
password: userInfo.password,
|
||||||
|
})
|
||||||
|
// 删除验证码
|
||||||
|
captchaService.deleteCaptcha({ id: userInfo.captchaId })
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
data,
|
data,
|
||||||
@ -40,4 +63,21 @@ export default class User {
|
|||||||
context, // 上下文主要是传递调用方信息使用,比如traceid
|
context, // 上下文主要是传递调用方信息使用,比如traceid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SurveyServer({ type: 'http', method: 'post', routerName: '/captcha' })
|
||||||
|
async refreshCaptcha({ req }) {
|
||||||
|
const captchaData = captchaService.createCaptcha()
|
||||||
|
const res = await captchaService.addCaptchaData({ text: captchaData.text })
|
||||||
|
if (req.body && req.body.captchaId) {
|
||||||
|
// 删除验证码
|
||||||
|
captchaService.deleteCaptcha({ id: req.body.captchaId })
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
id: res.insertedId,
|
||||||
|
img: captchaData.data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
40
server/src/apps/user/service/captchaService.ts
Normal file
40
server/src/apps/user/service/captchaService.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { mongo } from '../db/mongo'
|
||||||
|
import { create } from 'svg-captcha'
|
||||||
|
class CaptchaService {
|
||||||
|
|
||||||
|
createCaptcha() {
|
||||||
|
return create({
|
||||||
|
size: 4, // 验证码长度
|
||||||
|
ignoreChars: '0o1i', // 忽略字符
|
||||||
|
noise: 3, // 干扰线数量
|
||||||
|
color: true, // 启用彩色
|
||||||
|
background: '#f0f0f0', // 背景色
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCaptchaData({ text }) {
|
||||||
|
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||||
|
const addRes = await captchaDb.insertOne({
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
return addRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCaptchaIsCorrect({ captcha, id }) {
|
||||||
|
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||||
|
const captchaData = await captchaDb.findOne({
|
||||||
|
_id: mongo.getObjectIdByStr(id),
|
||||||
|
});
|
||||||
|
return captcha.toLowerCase() === captchaData?.text?.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCaptcha({ id }) {
|
||||||
|
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||||
|
const _id = mongo.getObjectIdByStr(id)
|
||||||
|
await captchaDb.deleteOne({
|
||||||
|
_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const captchaService = new CaptchaService()
|
@ -1,15 +1,15 @@
|
|||||||
import { SURVEY_STATUS, CommonError } from '../../../types/index'
|
import { SURVEY_STATUS, CommonError } from '../../../types/index'
|
||||||
import * as Joi from 'joi'
|
import * as Joi from 'joi'
|
||||||
export function getStatusObject({status}:{status:SURVEY_STATUS}) {
|
export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
id: status,
|
id: status,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function getValidateValue<T=any>(validationResult:Joi.ValidationResult<T>):T {
|
export function getValidateValue<T = any>(validationResult: Joi.ValidationResult<T>): T {
|
||||||
if(validationResult.error) {
|
if (validationResult.error) {
|
||||||
throw new CommonError(validationResult.error.details.map(e=>e.message).join())
|
throw new CommonError(validationResult.error.details.map(e => e.message).join())
|
||||||
}
|
}
|
||||||
return validationResult.value;
|
return validationResult.value;
|
||||||
}
|
}
|
5
web/src/management/api/captcha.js
Normal file
5
web/src/management/api/captcha.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import axios from './base';
|
||||||
|
|
||||||
|
export const refreshCaptcha = ({ captchaId }) => {
|
||||||
|
return axios.post('/user/captcha', { captchaId });
|
||||||
|
};
|
@ -1,15 +1,9 @@
|
|||||||
import axios from './base';
|
import axios from './base';
|
||||||
|
|
||||||
export const register = ({ username, password }) => {
|
export const register = (data) => {
|
||||||
return axios.post('/user/register', {
|
return axios.post('/user/register', data);
|
||||||
username,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const login = ({ username, password }) => {
|
export const login = (data) => {
|
||||||
return axios.post('/user/login', {
|
return axios.post('/user/login', data);
|
||||||
username,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -12,19 +12,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<el-form
|
<el-form
|
||||||
:model="ruleForm"
|
:model="formData"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
ref="ruleForm"
|
ref="formData"
|
||||||
label-width="100px"
|
label-width="100px"
|
||||||
class="login-form"
|
class="login-form"
|
||||||
@submit.native.prevent
|
@submit.native.prevent
|
||||||
>
|
>
|
||||||
<el-form-item label="账号" prop="name">
|
<el-form-item label="账号" prop="name">
|
||||||
<el-input v-model="ruleForm.name"></el-input>
|
<el-input v-model="formData.name"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="密码" prop="password">
|
<el-form-item label="密码" prop="password">
|
||||||
<el-input type="password" v-model="ruleForm.password"></el-input>
|
<el-input type="password" v-model="formData.password"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="验证码" prop="captcha">
|
||||||
|
<div class="captcha-wrapper">
|
||||||
|
<el-input
|
||||||
|
style="width: 150px"
|
||||||
|
v-model="formData.captcha"
|
||||||
|
></el-input>
|
||||||
|
<div
|
||||||
|
class="captcha-img"
|
||||||
|
@click="refreshCaptcha"
|
||||||
|
v-html="captchaImgData"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item class="button-group">
|
<el-form-item class="button-group">
|
||||||
@ -33,14 +47,14 @@
|
|||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
class="button"
|
class="button"
|
||||||
@click="submitForm('ruleForm', 'login')"
|
@click="submitForm('formData', 'login')"
|
||||||
>登录</el-button
|
>登录</el-button
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
:loading="registerPending"
|
:loading="registerPending"
|
||||||
size="small"
|
size="small"
|
||||||
class="button register-button"
|
class="button register-button"
|
||||||
@click="submitForm('ruleForm', 'register')"
|
@click="submitForm('formData', 'register')"
|
||||||
>注册</el-button
|
>注册</el-button
|
||||||
>
|
>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -50,14 +64,17 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { login, register } from '@/management/api/user';
|
import { login, register } from '@/management/api/user';
|
||||||
|
import { refreshCaptcha } from '@/management/api/captcha';
|
||||||
import { CODE_MAP } from '@/management/api/base';
|
import { CODE_MAP } from '@/management/api/base';
|
||||||
export default {
|
export default {
|
||||||
name: 'loginPage',
|
name: 'loginPage',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ruleForm: {
|
formData: {
|
||||||
name: '',
|
name: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
captcha: '',
|
||||||
|
captchaId: '',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
name: [
|
name: [
|
||||||
@ -78,11 +95,22 @@ export default {
|
|||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
captcha: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入验证码',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
loginPending: false,
|
loginPending: false,
|
||||||
registerPending: false,
|
registerPending: false,
|
||||||
|
captchaImgData: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.refreshCaptcha();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submitForm(formName, type) {
|
submitForm(formName, type) {
|
||||||
this.$refs[formName].validate(async (valid) => {
|
this.$refs[formName].validate(async (valid) => {
|
||||||
@ -94,8 +122,10 @@ export default {
|
|||||||
};
|
};
|
||||||
this[`${type}Pending`] = true;
|
this[`${type}Pending`] = true;
|
||||||
const res = await submitTypes[type]({
|
const res = await submitTypes[type]({
|
||||||
username: this.ruleForm.name,
|
username: this.formData.name,
|
||||||
password: this.ruleForm.password,
|
password: this.formData.password,
|
||||||
|
captcha: this.formData.captcha,
|
||||||
|
captchaId: this.formData.captchaId,
|
||||||
});
|
});
|
||||||
this[`${type}Pending`] = false;
|
this[`${type}Pending`] = false;
|
||||||
if (res.code !== CODE_MAP.SUCCESS) {
|
if (res.code !== CODE_MAP.SUCCESS) {
|
||||||
@ -123,6 +153,14 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async refreshCaptcha() {
|
||||||
|
const res = await refreshCaptcha({ captchaId: this.formData.captchaId });
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { id, img } = res.data;
|
||||||
|
this.formData.captchaId = id;
|
||||||
|
this.captchaImgData = img;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -130,6 +168,7 @@ export default {
|
|||||||
.login-page {
|
.login-page {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
.login-top {
|
.login-top {
|
||||||
color: #4a4c5b;
|
color: #4a4c5b;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@ -138,10 +177,12 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -149,28 +190,31 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 60px 60px 60px 0;
|
padding: 60px 60px 60px 0;
|
||||||
width: 500px;
|
|
||||||
height: 300px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 4px 0 20px 0 rgba(82, 82, 102, 0.15);
|
box-shadow: 4px 0 20px 0 rgba(82, 82, 102, 0.15);
|
||||||
margin-top: -150px;
|
margin-top: -150px;
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-button {
|
.register-button {
|
||||||
border-color: #faa600;
|
border-color: #faa600;
|
||||||
color: #faa600;
|
color: #faa600;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tips {
|
.tips {
|
||||||
color: #999;
|
color: #999;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -179,5 +223,17 @@ export default {
|
|||||||
margin-left: 50%;
|
margin-left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.captcha-img {
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
::v-deep > svg {
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,7 +3,7 @@ import { getBannerData } from '@/management/api/skin.js';
|
|||||||
export default {
|
export default {
|
||||||
async getBannerData({ state, commit }) {
|
async getBannerData({ state, commit }) {
|
||||||
if (state.bannerList && state.bannerList.length > 0) {
|
if (state.bannerList && state.bannerList.length > 0) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const res = await getBannerData();
|
const res = await getBannerData();
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
@ -3,10 +3,10 @@ import { cloneDeep as _cloneDeep, get as _get } from 'lodash';
|
|||||||
import { getSurveyById } from '@/management/api/survey';
|
import { getSurveyById } from '@/management/api/survey';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
init({ state, dispatch }) {
|
async init({ state, dispatch }) {
|
||||||
const metaData = _get(state, 'schema.metaData');
|
const metaData = _get(state, 'schema.metaData');
|
||||||
if (!metaData || metaData._id !== state.surveyId) {
|
if (!metaData || metaData._id !== state.surveyId) {
|
||||||
dispatch('getSchemaFromRemote');
|
await dispatch('getSchemaFromRemote');
|
||||||
}
|
}
|
||||||
dispatch('resetState');
|
dispatch('resetState');
|
||||||
},
|
},
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { get as _get} from 'lodash';
|
import { get as _get } from 'lodash';
|
||||||
import { formatLink } from '../utils/index.js';
|
import { formatLink } from '../utils/index.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -73,11 +73,11 @@ export default {
|
|||||||
window.open(formatLink(jumpLink));
|
window.open(formatLink(jumpLink));
|
||||||
},
|
},
|
||||||
play() {
|
play() {
|
||||||
const video = document.getElementById("video");
|
const video = document.getElementById('video');
|
||||||
document.querySelector('.play-icon').style.display = 'none';
|
document.querySelector('.play-icon').style.display = 'none';
|
||||||
document.querySelector(".video-modal").style.display = 'none';
|
document.querySelector('.video-modal').style.display = 'none';
|
||||||
video.play();
|
video.play();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div v-if="logoImage" class="logo-wrapper">
|
<div v-if="logoImage" class="logo-wrapper">
|
||||||
<img
|
<img
|
||||||
:style="{ width: !isMobile ? '20%' : logoImageWidth || '20%' }"
|
:style="{ width: !isMobile ? '20%' : logoImageWidth || '20%' }"
|
||||||
:src="logoImage"
|
:src="logoImage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@ -25,10 +25,11 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||||
.container{
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -20,7 +20,6 @@ import materialGroup from './materialGroup';
|
|||||||
export default {
|
export default {
|
||||||
name: 'mainRenderer',
|
name: 'mainRenderer',
|
||||||
computed: {
|
computed: {
|
||||||
// ...mapState('questionData'),
|
|
||||||
questionData() {
|
questionData() {
|
||||||
return this.$store.state.questionData;
|
return this.$store.state.questionData;
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user