前端新增白名单功能 (#356)

This commit is contained in:
chaorenluo 2024-07-19 22:45:40 +08:00 committed by GitHub
parent 2ed5b64b18
commit 9596cd07a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 635 additions and 22 deletions

1
web/components.d.ts vendored
View File

@ -41,6 +41,7 @@ declare module 'vue' {
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
IEpBottom: typeof import('~icons/ep/bottom')['default']
IEpCheck: typeof import('~icons/ep/check')['default']
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']

View File

@ -0,0 +1,11 @@
export const regexpMap = {
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
m: /^[1]([3-9])[0-9]{9}$/,
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
strictIdcard:
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
licensePlate:
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
}

View File

@ -18,6 +18,10 @@ export const getSpaceDetail = (workspaceId: string) => {
return axios.get(`/workspace/${workspaceId}`)
}
export const getMemberList = () => {
return axios.get('/workspace/member/list')
}
export const deleteSpace = (workspaceId: string) => {
return axios.delete(`/workspace/${workspaceId}`)
}

View File

@ -1,18 +1,18 @@
<template>
<div class="main">
<div class="nav" v-if="slots.hasOwnProperty('nav')">
<div class="nav" v-if="slots.nav">
<slot name="nav"></slot>
</div>
<div class="body">
<slot v-if="slots.hasOwnProperty('body')" name="body"></slot>
<slot v-if="slots.body" name="body"></slot>
<template v-else>
<div class="left" v-if="slots.hasOwnProperty('left')">
<div class="left" v-if="slots.left">
<slot name="left"></slot>
</div>
<div class="center" v-if="slots.hasOwnProperty('center')">
<div class="center" v-if="slots.center">
<slot name="center"></slot>
</div>
<div class="right" v-if="slots.hasOwnProperty('right')">
<div class="right" v-if="slots.right">
<slot name="right"></slot>
</div>
</template>

View File

@ -113,7 +113,7 @@ const normalizationValues = (configList: Array<any> = []) => {
if (item.hidden) {
return false
}
//
if (_isFunction(item.relyFunc)) {
return item.relyFunc(props.moduleConfig)

View File

@ -31,6 +31,17 @@ const updateLogicConf = () => {
}
}
const updateWhiteConf = () => {
const baseConf = store.state.edit.schema.baseConf || {};
if (baseConf.passwordSwitch && !baseConf.password) {
return true;
}
if (baseConf.whitelistType!='ALL' && !baseConf.whitelist?.length) {
return true;
}
return false
}
const handlePublish = async () => {
if (isPublishing.value) {
return
@ -53,6 +64,12 @@ const handlePublish = async () => {
return
}
if(updateWhiteConf()){
isPublishing.value = false
ElMessage.error('请检查问卷设置是否有误')
return
}
try {
const saveRes: any = await saveSurvey(saveData)
if (saveRes.code !== 200) {

View File

@ -63,6 +63,17 @@ const updateLogicConf = () => {
}
}
const updateWhiteConf = () => {
const baseConf = store.state.edit.schema.baseConf || {};
if (baseConf.passwordSwitch && !baseConf.password) {
return true;
}
if (baseConf.whitelistType!='ALL' && !baseConf.whitelist?.length) {
return true;
}
return false
}
const timerHandle = ref<NodeJS.Timeout | number | null>(null)
const triggerAutoSave = () => {
if (autoSaveStatus.value === 'saving') {
@ -114,6 +125,12 @@ const handleSave = async () => {
return
}
if(updateWhiteConf()){
isSaving.value = false
ElMessage.error('请检查问卷设置是否有误')
return
}
try {
const res: any = await saveData()
if (res.code === 200) {

View File

@ -39,7 +39,7 @@
<script setup lang="ts">
import { computed, ref, onMounted, shallowRef } from 'vue'
import { useStore } from 'vuex'
import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get } from 'lodash-es'
import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get,isFunction as _isFunction} from 'lodash-es'
import baseConfig from './config/baseConfig'
import baseFormConfig from './config/baseFormConfig'
@ -49,6 +49,9 @@ import setterLoader from '@/materials/setters/setterLoader'
const formConfigList = ref<Array<any>>([])
const components = shallowRef<any>({})
const registerTypes = ref<any>({})
const store = useStore()
const schemaBaseConf = computed(() => store.state.edit?.schema?.baseConf || {})
const setterList = computed(() => {
const list = _cloneDeep(formConfigList.value)
@ -71,6 +74,13 @@ const setterList = computed(() => {
}
formItem.value = formValue
}
//
form.formList = form.formList.filter((item:any) => {
if (_isFunction(item.relyFunc)) {
return item.relyFunc(schemaBaseConf.value)
}
return true
})
form.dataConfig = dataConfig
@ -78,7 +88,7 @@ const setterList = computed(() => {
})
})
const store = useStore()
const handleFormChange = (data: any) => {
store.dispatch('edit/changeSchema', {
key: data.key,

View File

@ -8,5 +8,10 @@ export default [
title: '提交限制',
key: 'limitConfig',
formList: ['limit_tLimit']
},
{
title: '作答限制',
key: 'respondConfig',
formList: ['interview_pwd','answer_type','white_placeholder','white_list','team_list']
}
]

View File

@ -21,5 +21,44 @@ export default {
tip: '问卷仅在指定时间段内可填写',
type: 'QuestionTimeHour',
placement: 'top'
},
interview_pwd: {
keys: ['baseConf.passwordSwitch', 'baseConf.password'],
label: '访问密码',
type: 'SwitchInput',
placeholder: '请输入6位字符串类型访问密码 ',
maxLength: 6,
},
answer_type: {
key: 'baseConf.whitelistType',
label: '答题名单',
type: 'AnswerRadio',
},
white_placeholder:{
key: 'baseConf.whitelistTip',
label: '名单登录提示语',
placeholder:'请输入名单提示语',
type: 'InputWordLimit',
maxLength: 40,
relyFunc: (data) => {
return ['CUSTOM','MEMBER'].includes(data.whitelistType)
}
},
white_list:{
keys: ['baseConf.whitelist','baseConf.memberType'],
label: '白名单列表',
type: 'whiteList',
relyFunc: (data) => {
return data.whitelistType == 'CUSTOM'
}
},
team_list:{
key: 'baseConf.whitelist',
label: '团队空间成员选择',
type: 'teamMemberList',
relyFunc: (data) => {
return data.whitelistType == 'MEMBER'
}
}
}

View File

@ -0,0 +1,41 @@
<template>
<div class="answer-radio-wrap">
<el-radio-group v-model="whitelistType" @change="handleRadioGroupChange">
<el-radio value="ALL">所有人</el-radio>
<el-radio value="MEMBER">空间成员</el-radio>
<el-radio value="CUSTOM">白名单</el-radio>
</el-radio-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
const props = defineProps({
formConfig: Object,
})
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
const whitelistType = ref(props.formConfig?.value || 'ALL')
const handleRadioGroupChange = (value) => {
const key = props.formConfig.key
emit(FORM_CHANGE_EVENT_KEY, { key, value })
emit(FORM_CHANGE_EVENT_KEY, { key:'baseConf.whitelist', value: [] })
emit(FORM_CHANGE_EVENT_KEY, { key: 'baseConf.memberType', value: 'MOBILE' })
if (whitelistType.value == 'ALL') {
emit(FORM_CHANGE_EVENT_KEY, { key:'baseConf.whitelistTip', value:'' })
}
}
</script>
<style lang="scss" scoped>
.switch-input-wrap{
width: 100%;
.mt16{
margin-top: 16px;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<el-input
:maxlength="maxLength"
v-model="modelValue"
:placeholder="placeholder"
show-word-limit
type="text"
@change="handleInputChange"
/>
</template>
<script setup>
import { computed,ref } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
const props = defineProps({
formConfig: Object,
})
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
const modelValue = ref(props.formConfig.value || '')
const maxLength = computed(() => props.formConfig.maxLength || 10)
const placeholder = computed(() => props.formConfig.placeholder || '')
const handleInputChange = (value) => {
const key = props.formConfig.key
modelValue.value = value
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="switch-input-wrap">
<el-switch v-model="passwordSwitch" @change="changeData(props.formConfig.keys[0],passwordSwitch)" />
<InputWordLimit
v-if="passwordSwitch"
class="mt16"
@form-change="handleFormChange"
:formConfig="{
...props.formConfig,
key: props.formConfig.keys[1],
value:props.formConfig?.value[1]
}"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useStore } from 'vuex'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
import InputWordLimit from './InputWordLimit.vue'
const store = useStore();
const props = defineProps({
formConfig: Object,
})
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
const passwordSwitch = ref(props.formConfig?.value[0] || false);
const changeData = (key, value) => {
emit(FORM_CHANGE_EVENT_KEY, {
key,
value
})
}
const handleFormChange = (data) => {
store.dispatch('edit/changeSchema', {
key: data.key,
value: data.value
})
}
</script>
<style lang="scss" scoped>
.switch-input-wrap{
width: 100%;
.mt16{
margin-top: 16px;
}
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="team-member-wrap">
<div class="team-tree-wrap">
<el-tree ref="treeRef" :default-expanded-keys="defaultCheckedKeys" :default-checked-keys="defaultCheckedKeys"
:data="treeData" empty-text="暂无数据" @check="handleChange" style="height:201px" highlight-current show-checkbox
node-key="id" :props="defaultProps" />
</div>
<div class="member-count">已选择 <span>{{ selectCount }}</span> </div>
</div>
</template>
<script setup>
import { ref, computed, defineProps, defineEmits, onMounted } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
import {
getMemberList
} from '@/management/api/space'
import { ElMessage } from 'element-plus'
const props = defineProps({
formConfig: Object,
})
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
const treeRef = ref(null)
const treeData = ref([])
const defaultCheckedKeys = ref([])
const defaultProps = {
children: 'children',
label: 'label',
}
const handleChange = () => {
const key = props.formConfig.key;
const userKeys = treeRef.value?.getCheckedKeys(true);
if (userKeys.length > 100) {
ElMessage.error('最多添加100个')
return;
}
emit(FORM_CHANGE_EVENT_KEY, { key: key, value: userKeys });
}
const selectCount = computed(() => {
return treeRef.value?.getCheckedKeys(true).length || 0
})
const getSpaceMenus = async () => {
const res = await getMemberList();
if (res.code != 200) {
ElMessage.error('获取空间成员列表失败');
return
}
const data = res.data;
data.map((v) => {
const members = v.members || [];
treeData.value.push({
id: v.ownerId,
label: v.name,
children: members?.map(v => ({
id: v.userId,
label: v.role,
}))
})
})
defaultCheckedKeys.value = props.formConfig.value;
}
onMounted(() => {
getSpaceMenus();
})
</script>
<style lang="scss" scoped>
.team-member-wrap {
width: 508px;
.team-tree-wrap {
background: #FFFFFF;
border: 1px solid rgba(227, 228, 232, 1);
border-radius: 2px;
min-height: 204px;
max-height: 204px;
overflow-x: auto;
}
.member-count {
text-align: right;
span {
color: $primary-color;
}
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="white-list-wrap">
<el-button class="create-btn" type="primary" @click="whiteVisible=true">
添加
</el-button>
<el-button v-if="whitelist.length>0" class="create-btn" color="#4A4C5B" @click="delAllList">
全部删除
</el-button>
<el-table class="table-wrap" empty-text="暂无数据" :data="whitelist" height="240" style="width: 426px">
<el-table-column label="名单" width="350" >
<template #default="scope">
<div>{{ whitelist[scope.$index] }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="74" >
<template #default="scope">
<div @click="delRowItem(scope.$index)" class="flex cursor"><i-ep-delete :size="16" /></div>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="whiteVisible" title="添加白名单" width="600" @closed="handleClose">
<div>
<el-form-item label-position="top" label="类型选择" label-width="auto">
<el-radio-group v-model="memberType" >
<el-radio value="MOBILE">手机号</el-radio>
<el-radio value="EMAIL">邮箱</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label-position="top" class="flex-column" label="名单录入" label-width="auto">
<el-input v-model="whiteTextarea" placeholder="多个用逗号(半角)“,”隔开" rows="7" resize="none" type="textarea" />
</el-form-item>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="whiteVisible = false">取消</el-button>
<el-button type="primary" @click="handleChange">
确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref,nextTick } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
import { ElMessage } from 'element-plus'
import { regexpMap } from '@/common/regexpMap.ts'
const props = defineProps({
formConfig: Object,
})
const emit = defineEmits([FORM_CHANGE_EVENT_KEY])
const whitelist = ref(props.formConfig.value[0] || [])
const memberType = ref(props.formConfig.value[1] || 'MOBILE')
const whiteVisible = ref(false)
const whiteTextarea = ref(whitelist.value.join(','))
const regularMap = {
MOBILE:regexpMap.m,
EMAIL:regexpMap.e
}
const checkValRule = (list) => {
let status = false;
if (list.length > 100) {
ElMessage.error('最多添加100个')
return true;
};
const pattern = regularMap[memberType.value];
if(!pattern) return false;
for (let i = 0; i < list.length; i++) {
if (!pattern.test(list[i])) {
status = true;
ElMessage.error('数据格式错误,请检查后重新输入~')
break;
}
}
return status;
}
const handleChange = () => {
const keys = props.formConfig.keys;
const list = whiteTextarea.value ? whiteTextarea.value.split(',') : []
if(checkValRule(list)) return
emit(FORM_CHANGE_EVENT_KEY, { key:keys[0], value: list });
emit(FORM_CHANGE_EVENT_KEY, { key: keys[1], value: memberType.value })
whiteVisible.value = false
}
const handleClose = () => {
nextTick(() => {
whitelist.value = props.formConfig.value[0] || []
whiteTextarea.value = whitelist.value.join(',')
memberType.value = props.formConfig.value[1] || 'MOBILE'
})
}
const delRowItem = (index) => {
whitelist.value.splice(index, 1);
whiteTextarea.value = whitelist.value.join(',')
const keys = props.formConfig.keys;
emit(FORM_CHANGE_EVENT_KEY, { key:keys[0], value: whitelist.value });
}
const delAllList = () => {
whitelist.value = []
whiteTextarea.value = ''
handleChange();
}
</script>
<style lang="scss" scoped>
.white-list-wrap {
.flex-column{
flex-direction: column;
}
:deep(th){
padding:4px 0;
background: #F6F7F9;
}
:deep(td){
padding:6px 0;
}
.table-wrap{
margin-top: 16px;
border: 1px solid #ebeef5;
border-radius: 2px;
overflow-x: hidden;
}
.cursor{
cursor: pointer;
}
.flex{
display: flex;
}
}
</style>

View File

@ -6,18 +6,7 @@ import {
set as _set
} from 'lodash-es'
import { INPUT, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
const regexpMap = {
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
m: /^[1]([3-9])[0-9]{9}$/,
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
strictIdcard:
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
licensePlate:
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
}
import { regexpMap } from '@/common/regexpMap.ts'
const msgMap = {
'*': '必填',

View File

@ -32,3 +32,10 @@ export const queryVote = ({ surveyPath, fieldList }) => {
export const getEncryptInfo = () => {
return axios.get('/clientEncrypt/getEncryptInfo')
}
export const validate = ({ surveyPath,password, whitelist }) => {
return axios.post(`/responseSchema/${surveyPath}/validate`, {
password,
whitelist
})
}

View File

@ -0,0 +1,138 @@
<template>
<el-dialog
v-model="whiteVisible"
title="验证"
:show-close="false"
class="verify-white-wrap"
width="315"
:close-on-press-escape="false"
:close-on-click-modal="false"
align-center
>
<template #header>
<div class="verify-white-head">
<div class="verify-white-title">验证</div>
<div v-if="whitelistTip" class="verify-white-tips">{{ whitelistTip }}</div>
</div>
</template>
<div class="verify-white-body">
<el-input v-if="isPwd" v-model="state.password" class="wd255 mb16" placeholder="请输入6位字符串类型访问密码" />
<el-input v-if="isValue" v-model="state.value" class="wd255 mb16" :placeholder="placeholder" />
<div class="submit-btn" @click="handleSubmit">验证并开始答题</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref,reactive,computed,watch} from 'vue'
import { validate } from '../api/survey'
import { useStore } from 'vuex'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
const whiteVisible = ref(false)
const store = useStore()
const state = reactive({
password: '',
value: '',
is_submit:false
})
const baseConf = computed(() => store.state.baseConf || {})
const isPwd = computed(() => baseConf.value.passwordSwitch)
const whitelistType = computed(() => baseConf.value.whitelistType)
const memberType = computed(() => baseConf.value.memberType)
const whitelistTip = computed(() => baseConf.value.whitelistTip)
const surveyPath = computed(() => store.state?.surveyPath || '')
const isValue = computed(() => {
if(!whitelistType.value) return false
return whitelistType.value!='ALL'
})
const placeholder = computed(() => {
if (whitelistType.value == 'MEMBER') {
return '请输入用户名'
}
if(memberType.value == 'MOBILE'){
return '请输入手机号'
}
if(memberType.value == 'EMAIL'){
return '请输入邮箱'
}
return ''
})
const handleSubmit = async() => {
if (state.is_submit) return;
const params = {
surveyPath:surveyPath.value
}
if (isValue.value) {
params.whitelist = state.value
}
if(isPwd.value){
params.password = state.password
}
const res = await validate(params)
if (res.code != 200) {
ElMessage.error(res.errmsg || '验证失败')
return
}
whiteVisible.value = false
store.commit('setWhiteData',params)
}
watch(()=>baseConf.value, () => {
if (whiteVisible.value) return
if(isValue.value || isPwd.value){
whiteVisible.value = true;
}
})
</script>
<style lang="scss" scoped>
.verify-white-wrap{
.verify-white-body{
padding:0 14px
}
.verify-white-head{
padding:0 14px;
margin-bottom: 8px;
margin-top:2px;
}
.mb16{
margin-bottom:16px;
}
.verify-white-tips{
text-align: center;
margin-top:8px;
font-size: 14px;
color: #92949D;
}
.verify-white-title{
font-size: 16px;
color: #292A36;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn{
background: #FAA600;
border-radius: 2px;
width:255px;
height:32px;
color:#fff;
display: flex;
align-items: center;
justify-content: center;
margin-top:4px;
margin-bottom:14px;
}
}
</style>

View File

@ -63,6 +63,7 @@ const renderData = computed(() => store.getters.renderData)
const submitConf = computed(() => store.state?.submitConf || {})
const logoConf = computed(() => store.state?.bottomConf || {})
const surveyPath = computed(() => store.state?.surveyPath || '')
const whiteData = computed(() => store.state?.whiteData || {})
const validate = (cbk: (v: boolean) => void) => {
const index = 0
@ -78,7 +79,8 @@ const normalizationRequestBody = () => {
surveyPath: surveyPath.value,
data: JSON.stringify(formValues),
difTime: Date.now() - enterTime,
clientTime: Date.now()
clientTime: Date.now(),
...whiteData.value
}
if (encryptInfo?.encryptType) {

View File

@ -49,5 +49,8 @@ export default {
},
setRuleEgine(state, ruleEngine) {
state.ruleEngine = ruleEngine
},
setWhiteData(state, data) {
state.whiteData = data
}
}

View File

@ -12,5 +12,8 @@ export default {
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
voteMap: {},
encryptInfo: null,
ruleEngine: null
ruleEngine: null,
whiteData: {
}
}