feat: 问卷列表新增类型和状态筛选 (#51)
This commit is contained in:
parent
642f4f08eb
commit
4d88a33856
@ -183,3 +183,10 @@ npm run serve
|
|||||||
|
|
||||||
## QQ
|
## QQ
|
||||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="300" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="300" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
||||||
|
|
||||||
|
# Feature
|
||||||
|
[官方Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||||
|
|
||||||
|
# CHANGELOG
|
||||||
|
[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
"@types/koa-bodyparser": "^4.3.10",
|
"@types/koa-bodyparser": "^4.3.10",
|
||||||
"@types/koa-router": "^7.4.4",
|
"@types/koa-router": "^7.4.4",
|
||||||
"@types/koa-static": "^4.0.4",
|
"@types/koa-static": "^4.0.4",
|
||||||
"@types/node": "^20.10.8",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
"@typescript-eslint/parser": "^6.15.0",
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -33,7 +32,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.9.2",
|
||||||
"jsonwebtoken": "^9.0.1",
|
"jsonwebtoken": "^9.0.1",
|
||||||
|
@ -141,7 +141,7 @@ export default class SurveyManage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getFilter(filterList: Array<FilterItem>) {
|
private getFilter(filterList: Array<FilterItem>) {
|
||||||
const allowFilterField = ['title', 'remark', 'surveyType', 'curStatus.status'];
|
const allowFilterField = ['title', 'remark', 'questionType', 'curStatus.status'];
|
||||||
return filterList.reduce((preItem, curItem) => {
|
return filterList.reduce((preItem, curItem) => {
|
||||||
const condition = curItem.condition.filter(item => allowFilterField.includes(item.field)).reduce((pre, cur) => {
|
const condition = curItem.condition.filter(item => allowFilterField.includes(item.field)).reduce((pre, cur) => {
|
||||||
switch(cur.comparator) {
|
switch(cur.comparator) {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
|
|
||||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||||
|
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 { captchaService } from './service/captchaService';
|
||||||
import { getValidateValue } from './utils/index';
|
import { getValidateValue } from './utils/index';
|
||||||
|
|
||||||
import { CommonError } from '../../types/index';
|
import { CommonError } from '../../types/index';
|
||||||
|
|
||||||
@SurveyApp('/api/user')
|
@SurveyApp('/api/user')
|
||||||
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 }) {
|
async register({ req }: { 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(),
|
||||||
@ -33,7 +34,7 @@ export default class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/login' })
|
@SurveyServer({ type: 'http', method: 'post', routerName: '/login' })
|
||||||
async login({ req }) {
|
async login({ req }: { 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(),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { mongo } from '../db/mongo';
|
import { mongo } from '../db/mongo';
|
||||||
import { create } from 'svg-captcha';
|
import { create } from 'svg-captcha';
|
||||||
|
|
||||||
class CaptchaService {
|
class CaptchaService {
|
||||||
|
|
||||||
createCaptcha() {
|
createCaptcha() {
|
||||||
@ -39,4 +38,3 @@ class CaptchaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const captchaService = new CaptchaService();
|
export const captchaService = new CaptchaService();
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
|
|||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValidateValue<T = unknown>(validationResult: Joi.ValidationResult<T>): T {
|
export function getValidateValue<T = unknown>(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());
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as Koa from 'koa';
|
import * as Koa from 'koa';
|
||||||
import * as KoaBodyparser from 'koa-bodyparser';
|
import * as KoaBodyparser from 'koa-bodyparser';
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tableview-root">
|
<div class="tableview-root">
|
||||||
<div class="filter-wrap">
|
<div class="filter-wrap">
|
||||||
|
<div class="select">
|
||||||
|
<text-select
|
||||||
|
v-for="item in Object.keys(selectOptionsDict)"
|
||||||
|
:effect-fun="onSelectChange"
|
||||||
|
:effect-key="item"
|
||||||
|
:options="selectOptionsDict[item]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<text-search
|
<text-search
|
||||||
placeholder="请输入问卷标题"
|
placeholder="请输入问卷标题"
|
||||||
:value="searchVal"
|
:value="searchVal"
|
||||||
@search="onSearchText" />
|
@search="onSearchText"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
@ -92,7 +101,8 @@ import Tag from './tag';
|
|||||||
import State from './state';
|
import State from './state';
|
||||||
import ToolBar from './toolBar';
|
import ToolBar from './toolBar';
|
||||||
import TextSearch from './textSearch'
|
import TextSearch from './textSearch'
|
||||||
import { fieldConfig, thead, noListDataConfig, noSearchDataConfig } from '../config';
|
import TextSelect from './textSelect'
|
||||||
|
import { fieldConfig, thead, noListDataConfig, noSearchDataConfig, selectOptionsDict } from '../config';
|
||||||
import { CODE_MAP } from '@/management/api/base';
|
import { CODE_MAP } from '@/management/api/base';
|
||||||
import { QOP_MAP } from '@/management/utils/constant';
|
import { QOP_MAP } from '@/management/utils/constant';
|
||||||
import { getSurveyList, deleteSurvey } from '@/management/api/survey';
|
import { getSurveyList, deleteSurvey } from '@/management/api/survey';
|
||||||
@ -112,7 +122,12 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
data: [],
|
data: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
searchVal: ''
|
searchVal: '',
|
||||||
|
selectOptionsDict,
|
||||||
|
selectValueMap: {
|
||||||
|
questionType: '',
|
||||||
|
'curStatus.status': ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -126,11 +141,31 @@ export default {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
comparator:"",
|
comparator:"",
|
||||||
condition:[{
|
condition:[
|
||||||
"field":"title",
|
{
|
||||||
"value":this.searchVal,
|
field: "title",
|
||||||
"comparator":"$regex"
|
value: this.searchVal,
|
||||||
}]
|
comparator: "$regex"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
condition: [
|
||||||
|
{
|
||||||
|
field: "curStatus.status",
|
||||||
|
value: this.selectValueMap["curStatus.status"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
condition: [
|
||||||
|
{
|
||||||
|
field: "questionType",
|
||||||
|
value: this.selectValueMap.questionType
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -142,7 +177,9 @@ export default {
|
|||||||
async init() {
|
async init() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const filter = JSON.stringify(this.filter)
|
const filter = JSON.stringify(this.filter.filter(item => {
|
||||||
|
return item.condition[0].field === 'title' || item.condition[0].value
|
||||||
|
}))
|
||||||
const res = await getSurveyList(this.currentPage, filter);
|
const res = await getSurveyList(this.currentPage, filter);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (res.code === CODE_MAP.SUCCESS) {
|
if (res.code === CODE_MAP.SUCCESS) {
|
||||||
@ -247,6 +284,11 @@ export default {
|
|||||||
this.searchVal = e
|
this.searchVal = e
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
this.init()
|
this.init()
|
||||||
|
},
|
||||||
|
onSelectChange(selectValue, selectKey){
|
||||||
|
this.selectValueMap[selectKey] = selectValue
|
||||||
|
this.currentPage = 1
|
||||||
|
this.init()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@ -255,6 +297,7 @@ export default {
|
|||||||
Tag,
|
Tag,
|
||||||
ToolBar,
|
ToolBar,
|
||||||
TextSearch,
|
TextSearch,
|
||||||
|
TextSelect,
|
||||||
State,
|
State,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -264,8 +307,12 @@ export default {
|
|||||||
.tableview-root {
|
.tableview-root {
|
||||||
.filter-wrap{
|
.filter-wrap{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: right;
|
justify-content: space-between;
|
||||||
|
.select{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.search{
|
.search{
|
||||||
|
display: flex;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,4 +349,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.el-select-dropdown__wrap{
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
.el-select-dropdown__item.hover{
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
57
web/src/management/pages/list/components/textButton.vue
Normal file
57
web/src/management/pages/list/components/textButton.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-button-root" @click="onClick">
|
||||||
|
<el-button type="text" :icon="currentOption.icon" size="mini">
|
||||||
|
<slot>{{ currentOption.label }}</slot>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default{
|
||||||
|
name: 'TextButton',
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
optionIndex: 0,
|
||||||
|
optionValue: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
effectFun: {
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
effectKey: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
optionsLength(){
|
||||||
|
return this.options.length
|
||||||
|
},
|
||||||
|
currentOption(){
|
||||||
|
return this.options[this.optionIndex % this.optionsLength]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentOption(newOption){
|
||||||
|
typeof this.effectFun === 'function' && this.effectFun(newOption, this.effectKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick(){
|
||||||
|
if(this.optionIndex > this.optionsLength){
|
||||||
|
this.optionIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.optionIndex++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-button{
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
59
web/src/management/pages/list/components/textSelect.vue
Normal file
59
web/src/management/pages/list/components/textSelect.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-select-root">
|
||||||
|
<el-select v-model="selectValue" :placeholder="options.label">
|
||||||
|
<el-option
|
||||||
|
v-for="item in options.value"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TextSelect',
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
selectValue: this.options.default
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
effectFun:{
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
effectKey: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectValue(newSelect){
|
||||||
|
const {effectFun} = this
|
||||||
|
typeof effectFun === 'function' && effectFun(newSelect, this.effectKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-select{
|
||||||
|
width: 105px;
|
||||||
|
line-height: 35px;
|
||||||
|
margin-right: 20px;
|
||||||
|
::v-deep .el-input__inner{
|
||||||
|
border: none;
|
||||||
|
height: 35px;
|
||||||
|
// line-height: 35px;
|
||||||
|
}
|
||||||
|
::v-deep .el-icon-arrow-up:before{
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
@ -72,3 +72,66 @@ export const statusMaps = {
|
|||||||
removed: '',
|
removed: '',
|
||||||
pausing: '',
|
pausing: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 问卷类型
|
||||||
|
export const questionTypeSelect = {
|
||||||
|
label: '问卷类型',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: '全部类型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'normal',
|
||||||
|
label: '基础调查'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// value: 'exam',
|
||||||
|
// label: '在线考试'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: 'nps',
|
||||||
|
// label: 'NPS评分'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
value: 'vote',
|
||||||
|
label: '投票评选'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'register',
|
||||||
|
label: '在线报名'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 问卷状态
|
||||||
|
export const curStatusSelect = {
|
||||||
|
label: '问卷状态',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: '全部状态'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'new',
|
||||||
|
label: '未发布'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'published',
|
||||||
|
label: '已发布'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'editing',
|
||||||
|
label: '修改中'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectOptionsDict = Object.freeze({
|
||||||
|
questionType: questionTypeSelect,
|
||||||
|
'curStatus.status': curStatusSelect
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user