fix: 断点续答and编辑检测代码cr优化 (#428)

* fix: 优化代码以及去掉无用字段

* fix: 断点续答验收问题优化

* fix: 优化字段

* fix: lint

* fix: 编辑检测相关问题优化

* fix: lint

* fix: 第二批cr优化

* fix: 去掉无用代码

* fix: 调整session守卫位置

* fix: 文件大小写
This commit is contained in:
dayou 2024-09-20 13:26:59 +08:00 committed by sudoooooo
parent 628872f27c
commit bf5db3f47b
53 changed files with 462 additions and 630 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@ node_modules
dist dist
package-lock.json package-lock.json
yarn.lock
# local env files # local env files
.env.local .env.local

View File

@ -48,8 +48,7 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"svg-captcha": "^1.4.0", "svg-captcha": "^1.4.0",
"typeorm": "^0.3.19", "typeorm": "^0.3.19"
"xss": "^1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",

View File

@ -6,7 +6,7 @@ export enum RECORD_STATUS {
PUBLISHED = 'published', // 发布 PUBLISHED = 'published', // 发布
REMOVED = 'removed', // 删除 REMOVED = 'removed', // 删除
FORCE_REMOVED = 'forceRemoved', // 从回收站删除 FORCE_REMOVED = 'forceRemoved', // 从回收站删除
COMOPUTETING = 'computing', // 计算中 COMPUTING = 'computing', // 计算中
FINISHED = 'finished', // 已完成 FINISHED = 'finished', // 已完成
ERROR = 'error', // 错误 ERROR = 'error', // 错误
} }

View File

@ -2,25 +2,16 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { get } from 'lodash'; import { get } from 'lodash';
import { NoPermissionException } from 'src/exceptions/noPermissionException'; import { NoPermissionException } from 'src/exceptions/noPermissionException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { SessionService } from 'src/modules/survey/services/session.service'; import { SessionService } from 'src/modules/survey/services/session.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
@Injectable() @Injectable()
export class SessionGuard implements CanActivate { export class SessionGuard implements CanActivate {
constructor( constructor(
private reflector: Reflector, private reflector: Reflector,
private readonly sessionService: SessionService, private readonly sessionService: SessionService,
private readonly surveyMetaService: SurveyMetaService,
private readonly workspaceMemberService: WorkspaceMemberService,
private readonly collaboratorService: CollaboratorService,
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const user = request.user;
const sessionIdKey = this.reflector.get<string>( const sessionIdKey = this.reflector.get<string>(
'sessionId', 'sessionId',
context.getHandler(), context.getHandler(),
@ -31,64 +22,8 @@ export class SessionGuard implements CanActivate {
if (!sessionId) { if (!sessionId) {
throw new NoPermissionException('没有权限'); throw new NoPermissionException('没有权限');
} }
const sessionInfo = await this.sessionService.findOne(sessionId);
const saveSession = await this.sessionService.findOne(sessionId); request.sessionInfo = sessionInfo;
return true;
request.saveSession = saveSession;
const surveyId = saveSession.surveyId;
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
if (!surveyMeta) {
throw new SurveyNotFoundException('问卷不存在');
}
request.surveyMeta = surveyMeta;
// 兼容老的问卷没有ownerId
if (
surveyMeta.ownerId === user._id.toString() ||
surveyMeta.owner === user.username
) {
// 问卷的owner可以访问和操作问卷
return true;
}
if (surveyMeta.workspaceId) {
const memberInfo = await this.workspaceMemberService.findOne({
workspaceId: surveyMeta.workspaceId,
userId: user._id.toString(),
});
if (!memberInfo) {
throw new NoPermissionException('没有权限');
}
return true;
}
const permissions = this.reflector.get<string[]>(
'surveyPermission',
context.getHandler(),
);
if (!Array.isArray(permissions) || permissions.length === 0) {
throw new NoPermissionException('没有权限');
}
const info = await this.collaboratorService.getCollaborator({
surveyId,
userId: user._id.toString(),
});
if (!info) {
throw new NoPermissionException('没有权限');
}
request.collaborator = info;
if (
permissions.some((permission) => info.permissions.includes(permission))
) {
return true;
}
throw new NoPermissionException('没有权限');
} }
} }

View File

@ -19,7 +19,4 @@ export class SurveyHistory extends BaseEntity {
username: string; username: string;
_id: string; _id: string;
}; };
@Column('string')
sessionId: string;
} }

View File

@ -35,13 +35,4 @@ export class AuthService {
} }
return user; return user;
} }
async expiredCheck(token: string) {
try {
verify(token, this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'));
} catch (err) {
return true;
}
return false;
}
} }

View File

@ -83,7 +83,6 @@ describe('SurveyHistoryService', () => {
schema, schema,
type, type,
user, user,
sessionId: '',
}); });
expect(spyCreate).toHaveBeenCalledWith({ expect(spyCreate).toHaveBeenCalledWith({

View File

@ -67,7 +67,9 @@ export class SessionController {
@Post('/seize') @Post('/seize')
@HttpCode(200) @HttpCode(200)
@UseGuards(SurveyGuard)
@UseGuards(SessionGuard) @UseGuards(SessionGuard)
@SetMetadata('sessionId', 'body.sessionId') @SetMetadata('sessionId', 'body.sessionId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE]) @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication) @UseGuards(Authentication)
@ -75,11 +77,11 @@ export class SessionController {
@Request() @Request()
req, req,
) { ) {
const saveSession = req.saveSession; const sessionInfo = req.sessionInfo;
await this.sessionService.updateSessionToEditing({ await this.sessionService.updateSessionToEditing({
sessionId: saveSession._id.toString(), sessionId: sessionInfo._id.toString(),
surveyId: saveSession.surveyId, surveyId: sessionInfo.surveyId,
}); });
return { return {

View File

@ -156,7 +156,7 @@ export class DownloadTaskService {
{ {
$set: { $set: {
curStatus: { curStatus: {
status: RECORD_STATUS.COMOPUTETING, status: RECORD_STATUS.COMPUTING,
date: Date.now(), date: Date.now(),
}, },
}, },

View File

@ -43,7 +43,6 @@
"options": [ "options": [
{ {
"text": "选项1", "text": "选项1",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",
@ -52,7 +51,6 @@
}, },
{ {
"text": "选项2", "text": "选项2",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",

View File

@ -47,7 +47,6 @@
{ {
"text": "课程1", "text": "课程1",
"hash": "115019", "hash": "115019",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",
@ -56,7 +55,6 @@
{ {
"text": "课程2", "text": "课程2",
"hash": "115020", "hash": "115020",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",
@ -65,7 +63,6 @@
{ {
"text": "课程3", "text": "课程3",
"hash": "115021", "hash": "115021",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",
@ -74,7 +71,6 @@
{ {
"text": "课程4", "text": "课程4",
"hash": "115022", "hash": "115022",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",

View File

@ -46,7 +46,6 @@
"options": [ "options": [
{ {
"text": "选项1", "text": "选项1",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",
@ -55,7 +54,6 @@
}, },
{ {
"text": "选项2", "text": "选项2",
"imageUrl": "",
"others": false, "others": false,
"mustOthers": false, "mustOthers": false,
"othersKey": "", "othersKey": "",

View File

@ -51,6 +51,7 @@
}, },
"pageConf": [], "pageConf": [],
"logicConf": { "logicConf": {
"showLogicConf": [] "showLogicConf": [],
"jumpLogicConf": []
} }
} }

View File

@ -1,53 +0,0 @@
import xss from 'xss';
const myxss = new (xss as any).FilterXSS({
onIgnoreTagAttr(tag, name, value) {
if (name === 'style' || name === 'class') {
return `${name}="${value}"`;
}
return undefined;
},
onIgnoreTag(tag, html) {
// <xxx>过滤为空,否则不过滤为空
const re1 = new RegExp('<.+?>', 'g');
if (re1.test(html)) {
return '';
} else {
return html;
}
},
});
export const cleanRichTextWithMediaTag = (text) => {
if (!text) {
return text === 0 ? 0 : '';
}
const html = transformHtmlTag(text)
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
.replace(/<video.*\/video>/g, '[视频]');
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '');
return content;
};
export function escapeHtml(html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export const transformHtmlTag = (html) => {
if (!html) return '';
if (typeof html !== 'string') return html + '';
return html
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\\\n/g, '\\n');
//.replace(/&nbsp;/g, "")
};
const filterXSSClone = myxss.process.bind(myxss);
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html));
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html));

View File

@ -0,0 +1,58 @@
interface ExtendedItem {
value: any
expires?: number // 可选属性
}
const localstorage = {
// 检查是否支持localStorage
isSupported(): boolean {
return typeof window !== 'undefined' && 'localStorage' in window
},
// 设置值
setItem(key: string, value: any, expires?: number): void {
if (!this.isSupported()) return
let item: ExtendedItem = { value }
if (expires !== undefined) {
item = { ...item, expires: Date.now() + expires * 1000 }
}
const serializedValue = JSON.stringify(item)
localStorage.setItem(key, serializedValue)
},
// 获取值
getItem<T>(key: string): T | null {
if (!this.isSupported()) return null
const serializedValue = localStorage.getItem(key) as string
if (!serializedValue) return null
let item: any
try {
item = JSON.parse(serializedValue)
} catch (e) {
console.error('Error parsing JSON from localStorage')
return null
}
if (item.expires && item.expires < Date.now()) {
this.removeItem(key)
return null
}
return item.value as T
},
// 移除值
removeItem(key: string): void {
if (!this.isSupported()) return
localStorage.removeItem(key)
}
}
export default localstorage

View File

@ -9,7 +9,7 @@ const myxss = new xss.FilterXSS({
}, },
onIgnoreTag(tag, html) { onIgnoreTag(tag, html) {
// <xxx>过滤为空,否则不过滤为空 // <xxx>过滤为空,否则不过滤为空
var re1 = new RegExp('<.+?>', 'g') const re1 = new RegExp('<.+?>', 'g')
if (re1.test(html)) { if (re1.test(html)) {
return '' return ''
} else { } else {

View File

@ -3,7 +3,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch } from 'vue' import { watch, onBeforeUnmount } from 'vue'
import { get as _get } from 'lodash-es' import { get as _get } from 'lodash-es'
import { useUserStore } from '@/management/stores/user' import { useUserStore } from '@/management/stores/user'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -23,11 +23,11 @@ const showConfirmBox = () => {
showClose: false, showClose: false,
callback: (action: Action) => { callback: (action: Action) => {
if (action === 'confirm') { if (action === 'confirm') {
userStore.logout(); userStore.logout()
router.replace({ name: 'login' }); router.replace({ name: 'login' })
} }
} }
}); })
} }
const checkAuth = async () => { const checkAuth = async () => {
@ -41,32 +41,40 @@ const checkAuth = async () => {
} }
}) })
if (res.data.code !== 200) { if (res.data.code !== 200) {
showConfirmBox(); showConfirmBox()
} else { } else {
timer = setTimeout(() => { timer = setTimeout(
checkAuth() () => {
}, 30 * 60 * 1000); checkAuth()
},
30 * 60 * 1000
)
} }
} catch (error) { } catch (error) {
const e = error as any const e = error as any
ElMessage.error(e.message) ElMessage.error(e.message)
} }
} }
watch(() => userStore.hasLogined, (hasLogined) => { watch(
if (hasLogined) { () => userStore.hasLogined,
timer = setTimeout(() => { (hasLogined) => {
checkAuth() if (hasLogined) {
}, 30 * 60 * 1000); timer = setTimeout(
} else { () => {
clearTimeout(timer); checkAuth()
},
30 * 60 * 1000
)
} else {
clearTimeout(timer)
}
} }
)
onBeforeUnmount(() => {
clearTimeout(timer)
}) })
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -16,4 +16,3 @@ export const getStatisticList = (data) => {
} }
}) })
} }

View File

@ -7,11 +7,10 @@ export const createDownloadSurveyResponseTask = ({ surveyId, isDesensitive }) =>
}) })
} }
export const getDownloadTask = taskId => { export const getDownloadTask = (taskId) => {
return axios.get('/downloadTask/getDownloadTask', { params: { taskId } }) return axios.get('/downloadTask/getDownloadTask', { params: { taskId } })
} }
export const getDownloadTaskList = ({ pageIndex, pageSize }) => { export const getDownloadTaskList = ({ pageIndex, pageSize }) => {
return axios.get('/downloadTask/getDownloadTaskList', { return axios.get('/downloadTask/getDownloadTaskList', {
params: { params: {
@ -24,6 +23,6 @@ export const getDownloadTaskList = ({ pageIndex, pageSize }) => {
//问卷删除 //问卷删除
export const deleteDownloadTask = (taskId) => { export const deleteDownloadTask = (taskId) => {
return axios.post('/downloadTask/deleteDownloadTask', { return axios.post('/downloadTask/deleteDownloadTask', {
taskId, taskId
}) })
} }

View File

@ -80,7 +80,7 @@ const popoverContent = ref('')
const getContent = (content) => { const getContent = (content) => {
if (Array.isArray(content)) { if (Array.isArray(content)) {
return content.map(item => getContent(item)).join(','); return content.map((item) => getContent(item)).join(',')
} }
if (content === null || content === undefined) { if (content === null || content === undefined) {
return '' return ''

View File

@ -28,13 +28,8 @@
<EmptyIndex :data="noDataConfig" /> <EmptyIndex :data="noDataConfig" />
</div> </div>
<el-dialog <el-dialog v-model="downloadDialogVisible" title="导出确认" width="500" style="padding: 40px">
v-model="downloadDialogVisible" <el-form :model="downloadForm" label-width="100px" label-position="left">
title="导出确认"
width="500"
style="padding: 40px;"
>
<el-form :model="downloadForm" label-width="100px" label-position="left" >
<el-form-item label="导出内容"> <el-form-item label="导出内容">
<el-radio-group v-model="downloadForm.isDesensitive"> <el-radio-group v-model="downloadForm.isDesensitive">
<el-radio :value="true">脱敏数据</el-radio> <el-radio :value="true">脱敏数据</el-radio>
@ -52,9 +47,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="downloadDialogVisible = false">取消</el-button> <el-button @click="downloadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmDownload()"> <el-button type="primary" @click="confirmDownload()"> 确认 </el-button>
确认
</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -85,11 +78,12 @@ const dataTableState = reactive({
isDownloading: false, isDownloading: false,
downloadDialogVisible: false, downloadDialogVisible: false,
downloadForm: { downloadForm: {
isDesensitive: true, isDesensitive: true
}, }
}) })
const { mainTableLoading, tableData, isShowOriginData, downloadDialogVisible, isDownloading } = toRefs(dataTableState) const { mainTableLoading, tableData, isShowOriginData, downloadDialogVisible, isDownloading } =
toRefs(dataTableState)
const downloadForm = dataTableState.downloadForm const downloadForm = dataTableState.downloadForm
const route = useRoute() const route = useRoute()
@ -168,7 +162,10 @@ const confirmDownload = async () => {
} }
try { try {
isDownloading.value = true isDownloading.value = true
const createRes = await createDownloadSurveyResponseTask({ surveyId: route.params.id, isDesensitive: downloadForm.isDesensitive }) const createRes = await createDownloadSurveyResponseTask({
surveyId: route.params.id,
isDesensitive: downloadForm.isDesensitive
})
dataTableState.downloadDialogVisible = false dataTableState.downloadDialogVisible = false
if (createRes.code === 200) { if (createRes.code === 200) {
ElMessage.success(`下载文件计算中,可前往“下载中心”查看`) ElMessage.success(`下载文件计算中,可前往“下载中心”查看`)
@ -176,12 +173,11 @@ const confirmDownload = async () => {
const taskInfo = await checkIsTaskFinished(createRes.data.taskId) const taskInfo = await checkIsTaskFinished(createRes.data.taskId)
if (taskInfo.url) { if (taskInfo.url) {
window.open(taskInfo.url) window.open(taskInfo.url)
ElMessage.success("导出成功") ElMessage.success('导出成功')
} }
} catch (error) { } catch (error) {
ElMessage.error('导出失败,请重试') ElMessage.error('导出失败,请重试')
} }
} else { } else {
ElMessage.error('导出失败,请重试') ElMessage.error('导出失败,请重试')
} }
@ -190,13 +186,12 @@ const confirmDownload = async () => {
} finally { } finally {
isDownloading.value = false isDownloading.value = false
} }
} }
const checkIsTaskFinished = (taskId) => { const checkIsTaskFinished = (taskId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const run = () => { const run = () => {
getDownloadTask(taskId).then(res => { getDownloadTask(taskId).then((res) => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const status = res.data.curStatus.status const status = res.data.curStatus.status
if (status === 'new' || status === 'computing') { if (status === 'new' || status === 'computing') {
@ -207,15 +202,13 @@ const checkIsTaskFinished = (taskId) => {
resolve(res.data) resolve(res.data)
} }
} else { } else {
reject("导出失败"); reject('导出失败')
} }
}) })
} }
run() run()
}) })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -25,7 +25,13 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200"> <el-table-column label="操作" width="200">
<template v-slot="{ row }"> <template v-slot="{ row }">
<span v-if="row.curStatus?.status === 'finished'" class="text-btn download-btn" @click="handleDownload(row)"> 下载 </span> <span
v-if="row.curStatus?.status === 'finished'"
class="text-btn download-btn"
@click="handleDownload(row)"
>
下载
</span>
<span class="text-btn delete-btn" @click="openDeleteDialog(row)"> 删除 </span> <span class="text-btn delete-btn" @click="openDeleteDialog(row)"> 删除 </span>
</template> </template>
</el-table-column> </el-table-column>
@ -71,14 +77,14 @@ const getList = async ({ pageIndex }: { pageIndex: number }) => {
} }
const params = { const params = {
pageSize: pageSize.value, pageSize: pageSize.value,
pageIndex, pageIndex
} }
const res: Record<string, any> = await getDownloadTaskList(params) const res: Record<string, any> = await getDownloadTaskList(params)
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
total.value = res.data.total total.value = res.data.total
const list = res.data.list as any const list = res.data.list as any
dataList.splice(0, dataList.length, ...list); dataList.splice(0, dataList.length, ...list)
} }
loading.value = false loading.value = false
} }
@ -87,8 +93,8 @@ const statusTextMap: Record<string, string> = {
new: '排队中', new: '排队中',
computing: '计算中', computing: '计算中',
finished: '已完成', finished: '已完成',
removed: '已删除', removed: '已删除'
}; }
let currentDelRow: Record<string, any> = {} let currentDelRow: Record<string, any> = {}
// //
@ -123,11 +129,11 @@ const confirmDelete = async () => {
if (res.code !== CODE_MAP.SUCCESS) { if (res.code !== CODE_MAP.SUCCESS) {
ElMessage.error(res.errmsg) ElMessage.error(res.errmsg)
} else { } else {
ElMessage.success('删除成功'); ElMessage.success('删除成功')
await getList({ pageIndex: 1 }) await getList({ pageIndex: 1 })
} }
} catch (error) { } catch (error) {
ElMessage.error("删除失败,请刷新重试") ElMessage.error('删除失败,请刷新重试')
} }
} }
@ -162,7 +168,7 @@ const downloadListConfig = {
formatter(row: Record<string, any>, column: Record<string, any>) { formatter(row: Record<string, any>, column: Record<string, any>) {
console.log({ console.log({
row, row,
column, column
}) })
return statusTextMap[get(row, column.rawColumnKey)] return statusTextMap[get(row, column.rawColumnKey)]
} }

View File

@ -18,10 +18,15 @@
</CooperationPanel> </CooperationPanel>
<PreviewPanel></PreviewPanel> <PreviewPanel></PreviewPanel>
<HistoryPanel></HistoryPanel> <HistoryPanel></HistoryPanel>
<SavePanel :updateLogicConf="updateLogicConf" :updateWhiteConf="updateWhiteConf"></SavePanel> <SavePanel
:updateLogicConf="updateLogicConf"
:updateWhiteConf="updateWhiteConf"
:seize="seize"
></SavePanel>
<PublishPanel <PublishPanel
:updateLogicConf="updateLogicConf" :updateLogicConf="updateLogicConf"
:updateWhiteConf="updateWhiteConf" :updateWhiteConf="updateWhiteConf"
:seize="seize"
></PublishPanel> ></PublishPanel>
</div> </div>
</div> </div>
@ -30,6 +35,8 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useEditStore } from '@/management/stores/edit' import { useEditStore } from '@/management/stores/edit'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import BackPanel from '../modules/generalModule/BackPanel.vue' import BackPanel from '../modules/generalModule/BackPanel.vue'
import TitlePanel from '../modules/generalModule/TitlePanel.vue' import TitlePanel from '../modules/generalModule/TitlePanel.vue'
@ -39,6 +46,7 @@ import PreviewPanel from '../modules/contentModule/PreviewPanel.vue'
import SavePanel from '../modules/contentModule/SavePanel.vue' import SavePanel from '../modules/contentModule/SavePanel.vue'
import PublishPanel from '../modules/contentModule/PublishPanel.vue' import PublishPanel from '../modules/contentModule/PublishPanel.vue'
import CooperationPanel from '../modules/contentModule/CooperationPanel.vue' import CooperationPanel from '../modules/contentModule/CooperationPanel.vue'
import { seizeSession } from '@/management/api/survey'
const editStore = useEditStore() const editStore = useEditStore()
const { schema, changeSchema } = editStore const { schema, changeSchema } = editStore
@ -68,7 +76,7 @@ const updateLogicConf = () => {
} }
const showLogicConf = showLogicEngine.value.toJson() const showLogicConf = showLogicEngine.value.toJson()
if(JSON.stringify(schema.logicConf.showLogicConf) !== JSON.stringify(showLogicConf)) { if (JSON.stringify(schema.logicConf.showLogicConf) !== JSON.stringify(showLogicConf)) {
// //
changeSchema({ key: 'logicConf', value: { showLogicConf } }) changeSchema({ key: 'logicConf', value: { showLogicConf } })
} }
@ -76,7 +84,7 @@ const updateLogicConf = () => {
return res return res
} }
const jumpLogicConf = jumpLogicEngine.value.toJson() const jumpLogicConf = jumpLogicEngine.value.toJson()
if(JSON.stringify(schema.logicConf.jumpLogicConf) !== JSON.stringify(jumpLogicConf)){ if (JSON.stringify(schema.logicConf.jumpLogicConf) !== JSON.stringify(jumpLogicConf)) {
changeSchema({ key: 'logicConf', value: { jumpLogicConf } }) changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
} }
@ -106,6 +114,16 @@ const updateWhiteConf = () => {
} }
return res return res
} }
// sessionid
const seize = async (sessionId: string) => {
const seizeRes: Record<string, any> = await seizeSession({ sessionId })
if (seizeRes.code === 200) {
location.reload()
} else {
ElMessage.error('获取权限失败,请重试')
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url('@/management/styles/edit-btn.scss'); @import url('@/management/styles/edit-btn.scss');

View File

@ -24,7 +24,6 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
import CommonTemplate from './components/CommonTemplate.vue' import CommonTemplate from './components/CommonTemplate.vue'
import Navbar from './components/ModuleNavbar.vue' import Navbar from './components/ModuleNavbar.vue'
const editStore = useEditStore() const editStore = useEditStore()
const { init, setSurveyId } = editStore const { init, setSurveyId } = editStore

View File

@ -9,7 +9,7 @@ import { useEditStore } from '@/management/stores/edit'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
import { publishSurvey, saveSurvey, seizeSession } from '@/management/api/survey' import { publishSurvey, saveSurvey } from '@/management/api/survey'
import buildData from './buildData' import buildData from './buildData'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { CODE_MAP } from '@/management/api/base' import { CODE_MAP } from '@/management/api/base'
@ -17,6 +17,7 @@ import { CODE_MAP } from '@/management/api/base'
interface Props { interface Props {
updateLogicConf: any updateLogicConf: any
updateWhiteConf: any updateWhiteConf: any
seize: any
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -29,15 +30,6 @@ const saveData = computed(() => {
return buildData(schema.value, sessionId.value) return buildData(schema.value, sessionId.value)
}) })
const seize = async () => {
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
if (seizeRes.code === 200) {
location.reload();
} else {
ElMessage.error('获取权限失败,请重试')
}
}
const router = useRouter() const router = useRouter()
const validate = () => { const validate = () => {
@ -61,7 +53,6 @@ const validate = () => {
} }
const onSave = async () => { const onSave = async () => {
if (!saveData.value.sessionId) { if (!saveData.value.sessionId) {
ElMessage.error('未获取到sessionId') ElMessage.error('未获取到sessionId')
return null return null
@ -74,7 +65,7 @@ const onSave = async () => {
try { try {
const res: any = await saveSurvey(saveData.value) const res: any = await saveSurvey(saveData.value)
if(!res) { if (!res) {
return null return null
} }
if (res.code === 200) { if (res.code === 200) {
@ -85,10 +76,10 @@ const onSave = async () => {
confirmButtonText: '刷新同步', confirmButtonText: '刷新同步',
callback: (action: string) => { callback: (action: string) => {
if (action === 'confirm') { if (action === 'confirm') {
seize(); props.seize(sessionId.value)
} }
} }
}); })
return null return null
} else { } else {
ElMessage.error(res.errmsg) ElMessage.error(res.errmsg)
@ -98,7 +89,6 @@ const onSave = async () => {
ElMessage.error('保存问卷失败') ElMessage.error('保存问卷失败')
return null return null
} }
} }
const handlePublish = async () => { const handlePublish = async () => {
if (isPublishing.value) { if (isPublishing.value) {

View File

@ -21,12 +21,13 @@ import { useEditStore } from '@/management/stores/edit'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
import { saveSurvey, seizeSession } from '@/management/api/survey' import { saveSurvey } from '@/management/api/survey'
import buildData from './buildData' import buildData from './buildData'
interface Props { interface Props {
updateLogicConf: any updateLogicConf: any
updateWhiteConf: any updateWhiteConf: any
seize: any
} }
const route = useRoute() const route = useRoute()
@ -46,7 +47,6 @@ const saveText = computed(
const editStore = useEditStore() const editStore = useEditStore()
const { schemaUpdateTime, schema, sessionId } = storeToRefs(editStore) const { schemaUpdateTime, schema, sessionId } = storeToRefs(editStore)
const validate = () => { const validate = () => {
let checked = true let checked = true
let msg = '' let msg = ''
@ -69,7 +69,7 @@ const validate = () => {
} }
const onSave = async () => { const onSave = async () => {
const saveData = buildData(schema.value, sessionId.value); const saveData = buildData(schema.value, sessionId.value)
if (!saveData.sessionId) { if (!saveData.sessionId) {
ElMessage.error('sessionId有误') ElMessage.error('sessionId有误')
return null return null
@ -85,15 +85,6 @@ const onSave = async () => {
return res return res
} }
const seize = async () => {
const seizeRes: Record<string, any> = await seizeSession({ sessionId: sessionId.value })
if (seizeRes.code === 200) {
location.reload();
} else {
ElMessage.error('获取权限失败,请重试')
}
}
const timerHandle = ref<NodeJS.Timeout | number | null>(null) const timerHandle = ref<NodeJS.Timeout | number | null>(null)
const triggerAutoSave = () => { const triggerAutoSave = () => {
if (autoSaveStatus.value === 'saving') { if (autoSaveStatus.value === 'saving') {
@ -147,7 +138,7 @@ const handleSave = async () => {
try { try {
const res: any = await onSave() const res: any = await onSave()
if(!res) { if (!res) {
return return
} }
if (res.code === 200) { if (res.code === 200) {
@ -158,10 +149,10 @@ const handleSave = async () => {
confirmButtonText: '刷新同步', confirmButtonText: '刷新同步',
callback: (action: string) => { callback: (action: string) => {
if (action === 'confirm') { if (action === 'confirm') {
seize(); props.seize(sessionId.value)
} }
} }
}); })
} else { } else {
ElMessage.error(res.errmsg) ElMessage.error(res.errmsg)
} }

View File

@ -141,20 +141,13 @@ export default {
this.initCurOption() this.initCurOption()
}, },
addOption(text = '选项', others = false, index = -1, fieldId) { addOption(text = '选项', others = false, index = -1, fieldId) {
let addOne let addOne = {
if (this.curOptions[0]) { text: '',
addOne = _cloneDeep(this.curOptions[0]) hash: '',
} else { others: false,
addOne = { mustOthers: false,
text: '', othersKey: '',
hash: '', placeholderDesc: ''
others: false,
mustOthers: false,
othersKey: '',
placeholderDesc: '',
score: 0,
limit: ''
}
} }
for (const i in addOne) { for (const i in addOne) {
if (i === 'others') { if (i === 'others') {

View File

@ -7,7 +7,7 @@ export default [
{ {
title: '提交限制', title: '提交限制',
key: 'limitConfig', key: 'limitConfig',
formList: ['limit_tLimit', 'limit_breakAnswer', 'limit_backAnswer'] formList: ['limit_tLimit', 'limit_fillAnswer', 'limit_fillSubmitAnswer']
}, },
{ {
title: '作答限制', title: '作答限制',

View File

@ -22,21 +22,21 @@ export default {
type: 'QuestionTimeHour', type: 'QuestionTimeHour',
placement: 'top' placement: 'top'
}, },
limit_breakAnswer: { limit_fillAnswer: {
key: 'breakAnswer', key: 'fillAnswer',
label: '允许断点续答', label: '允许断点续答',
tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)', tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
placement: 'top', placement: 'top',
type: 'CustomedSwitch', type: 'CustomedSwitch',
value: false, value: false
}, },
limit_backAnswer: { limit_fillSubmitAnswer: {
key: 'backAnswer', key: 'fillSubmitAnswer',
label: '自动填充上次提交内容', label: '自动填充上次提交内容',
tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)', tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
placement: 'top', placement: 'top',
type: 'CustomedSwitch', type: 'CustomedSwitch',
value: false, value: false
}, },
interview_pwd_switch: { interview_pwd_switch: {
key: 'passwordSwitch', key: 'passwordSwitch',
@ -112,5 +112,5 @@ export default {
relyFunc: (data) => { relyFunc: (data) => {
return data.whitelistType === 'MEMBER' return data.whitelistType === 'MEMBER'
} }
}, }
} }

View File

@ -39,7 +39,7 @@
<el-form-item label="验证码" prop="captcha"> <el-form-item label="验证码" prop="captcha">
<div class="captcha-wrapper"> <div class="captcha-wrapper">
<el-input style="width: 280px" v-model="formData.captcha" size="large"></el-input> <el-input style="width: 280px" v-model="formData.captcha" size="large"></el-input>
<div class="captcha-img" click="refreshCaptcha" v-html="captchaImgData"></div> <div class="captcha-img" @click="refreshCaptcha" v-html="captchaImgData"></div>
</div> </div>
</el-form-item> </el-form-item>

View File

@ -6,6 +6,7 @@ import {
set as _set, set as _set,
isNumber as _isNumber isNumber as _isNumber
} from 'lodash-es' } from 'lodash-es'
import { ElMessageBox } from 'element-plus'
import { QUESTION_TYPE } from '@/common/typeEnum' import { QUESTION_TYPE } from '@/common/typeEnum'
import { getQuestionByType } from '@/management/utils/index' import { getQuestionByType } from '@/management/utils/index'
import { filterQuestionPreviewData } from '@/management/utils/index' import { filterQuestionPreviewData } from '@/management/utils/index'
@ -24,7 +25,6 @@ import { CODE_MAP } from '../api/base'
import { RuleBuild } from '@/common/logicEngine/RuleBuild' import { RuleBuild } from '@/common/logicEngine/RuleBuild'
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo' import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
import { useJumpLogicInfo } from '@/management/hooks/useJumpLogicInfo' import { useJumpLogicInfo } from '@/management/hooks/useJumpLogicInfo'
import { ElMessageBox } from 'element-plus'
const innerMetaConfig = { const innerMetaConfig = {
submit: { submit: {
@ -69,7 +69,6 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
begTime: '', begTime: '',
endTime: '', endTime: '',
language: 'chinese', language: 'chinese',
showVoteProcess: 'allow',
tLimit: 0, tLimit: 0,
answerBegTime: '', answerBegTime: '',
answerEndTime: '', answerEndTime: '',
@ -107,6 +106,21 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
schema.pageConf = codeData.pageConf schema.pageConf = codeData.pageConf
} }
const sessionId = ref('')
async function initSessionId() {
const sessionIdKey = `${surveyId.value}_sessionId`
const localSessionId = sessionStorage.getItem(sessionIdKey)
if (localSessionId) {
sessionId.value = localSessionId
} else {
const res: Record<string, any> = await getSessionId({ surveyId: surveyId.value })
if (res.code === 200) {
sessionId.value = res.data.sessionId
sessionStorage.setItem(sessionIdKey, sessionId.value)
}
}
}
async function getSchemaFromRemote() { async function getSchemaFromRemote() {
const res: any = await getSurveyById(surveyId.value) const res: any = await getSurveyById(surveyId.value)
if (res.code === 200) { if (res.code === 200) {
@ -149,10 +163,11 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
return { return {
schema, schema,
initSchema,
getSchemaFromRemote, getSchemaFromRemote,
showLogicEngine, showLogicEngine,
jumpLogicEngine, jumpLogicEngine,
sessionId,
initSessionId
} }
} }
@ -479,7 +494,6 @@ function useLogicEngine(schema: any) {
} }
} }
type IBannerItem = { type IBannerItem = {
name: string name: string
key: string key: string
@ -487,37 +501,54 @@ type IBannerItem = {
} }
type IBannerList = Record<string, IBannerItem> type IBannerList = Record<string, IBannerItem>
export const useEditStore = defineStore('edit', () => { export const useEditStore = defineStore('edit', () => {
const surveyId = ref('')
const bannerList: Ref<IBannerList> = ref({}) const bannerList: Ref<IBannerList> = ref({})
const cooperPermissions = ref(Object.values(SurveyPermissions)) const fetchBannerData = async () => {
const schemaUpdateTime = ref(Date.now()) const res: any = await getBannerData()
if (res.code === CODE_MAP.SUCCESS) {
bannerList.value = res.data
}
}
const cooperPermissions = ref(Object.values(SurveyPermissions))
const fetchCooperPermissions = async (id: string) => {
const res: any = await getCollaboratorPermissions(id)
if (res.code === CODE_MAP.SUCCESS) {
cooperPermissions.value = res.data.permissions
}
}
const schemaUpdateTime = ref(Date.now())
function updateTime() {
schemaUpdateTime.value = Date.now()
}
const surveyId = ref('')
function setSurveyId(id: string) { function setSurveyId(id: string) {
surveyId.value = id surveyId.value = id
} }
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } = // 初始化schem相关
useInitializeSchema(surveyId, () => { const {
editGlobalBaseConf.initCounts() schema,
sessionId,
initSessionId,
getSchemaFromRemote,
showLogicEngine,
jumpLogicEngine
} = useInitializeSchema(surveyId, () => {
editGlobalBaseConf.initCounts()
})
function changeSchema({ key, value }: { key: string; value: any }) {
_set(schema, key, value)
updateTime()
}
function changeThemePreset(presets: any) {
Object.keys(presets).forEach((key) => {
_set(schema, key, presets[key])
}) })
const sessionId = ref('')
async function initSessionId() {
const sessionIdKey = `${surveyId.value}_sessionId`;
const localSessionId = sessionStorage.getItem(sessionIdKey)
if (localSessionId) {
sessionId.value = localSessionId
} else {
const res: Record<string, any> = await getSessionId({ surveyId: surveyId.value })
if (res.code === 200) {
sessionId.value = res.data.sessionId
sessionStorage.setItem(sessionIdKey, sessionId.value)
}
}
} }
const questionDataList = toRef(schema, 'questionDataList') const questionDataList = toRef(schema, 'questionDataList')
@ -527,86 +558,14 @@ export const useEditStore = defineStore('edit', () => {
schema.questionDataList = data schema.questionDataList = data
} }
const createNewQuestion = ({ type }: { type: QUESTION_TYPE }) => {
const fetchBannerData = async () => { const fields = questionDataList.value.map((item: any) => item.field)
const res: any = await getBannerData() const newQuestion = getQuestionByType(type, fields)
if (res.code === CODE_MAP.SUCCESS) { newQuestion.title = newQuestion.title = `标题${newQuestionIndex.value + 1}`
bannerList.value = res.data if (type === QUESTION_TYPE.VOTE) {
newQuestion.innerType = QUESTION_TYPE.RADIO
} }
} return newQuestion
const fetchCooperPermissions = async (id: string) => {
const res: any = await getCollaboratorPermissions(id)
if (res.code === CODE_MAP.SUCCESS) {
cooperPermissions.value = res.data.permissions
}
}
// const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } = useLogicEngine(schema)
const {
currentEditOne,
currentEditKey,
currentEditStatus,
moduleConfig,
formConfigList,
currentEditMeta,
setCurrentEditOne,
changeCurrentEditStatus
} = useCurrentEdit({ schema, questionDataList })
async function init() {
const { metaData } = schema
if (!metaData || (metaData as any)?._id !== surveyId.value) {
await getSchemaFromRemote()
await initSessionId()
}
currentEditOne.value = null
currentEditStatus.value = 'Success'
}
function updateTime() {
schemaUpdateTime.value = Date.now()
}
const {
pageEditOne,
pageConf,
isFinallyPage,
pageCount,
pageQuestionData,
getSorter,
updatePageEditOne,
deletePage,
pageOperations,
addPage,
getPageQuestionData,
copyPage,
swapArrayRanges,
setPage
} = usePageEdit({ schema, questionDataList }, updateTime)
const { copyQuestion, addQuestion, deleteQuestion, moveQuestion } = useQuestionDataListOperations(
{
questionDataList,
updateTime,
pageOperations,
updateCounts: editGlobalBaseConf.updateCounts
}
)
function moveQuestionDataList(data: any) {
const { startIndex, endIndex } = getSorter()
const newData = [
...questionDataList.value.slice(0, startIndex),
...data,
...questionDataList.value.slice(endIndex)
]
const countTotal: number = (schema.pageConf as Array<number>).reduce(
(v: number, i: number) => v + i
)
if (countTotal != newData.length) {
schema.pageConf[pageEditOne.value - 1] = (schema.pageConf[pageEditOne.value - 1] + 1) as never
}
setQuestionDataList(newData)
} }
const compareQuestionSeq = (val: Array<any>) => { const compareQuestionSeq = (val: Array<any>) => {
@ -644,25 +603,70 @@ export const useEditStore = defineStore('edit', () => {
} }
}) })
const createNewQuestion = ({ type }: { type: QUESTION_TYPE }) => { // 当前编辑题目
const fields = questionDataList.value.map((item: any) => item.field) const {
const newQuestion = getQuestionByType(type, fields) currentEditOne,
newQuestion.title = newQuestion.title = `标题${newQuestionIndex.value + 1}` currentEditKey,
if (type === QUESTION_TYPE.VOTE) { currentEditStatus,
newQuestion.innerType = QUESTION_TYPE.RADIO moduleConfig,
formConfigList,
currentEditMeta,
setCurrentEditOne,
changeCurrentEditStatus
} = useCurrentEdit({ schema, questionDataList })
// 初始化问卷
async function init() {
const { metaData } = schema
if (!metaData || (metaData as any)?._id !== surveyId.value) {
await Promise.all([getSchemaFromRemote(), initSessionId()])
} }
return newQuestion currentEditOne.value = null
currentEditStatus.value = 'Success'
} }
function changeSchema({ key, value }: { key: string; value: any }) { // 分页相关
_set(schema, key, value) const {
updateTime() pageEditOne,
} pageConf,
isFinallyPage,
pageCount,
pageQuestionData,
getSorter,
updatePageEditOne,
deletePage,
pageOperations,
addPage,
getPageQuestionData,
copyPage,
swapArrayRanges,
setPage
} = usePageEdit({ schema, questionDataList }, updateTime)
function changeThemePreset(presets: any) { // 问卷列表相关操作
Object.keys(presets).forEach((key) => { const { copyQuestion, addQuestion, deleteQuestion, moveQuestion } = useQuestionDataListOperations(
_set(schema, key, presets[key]) {
}) questionDataList,
updateTime,
pageOperations,
updateCounts: editGlobalBaseConf.updateCounts
}
)
function moveQuestionDataList(data: any) {
const { startIndex, endIndex } = getSorter()
const newData = [
...questionDataList.value.slice(0, startIndex),
...data,
...questionDataList.value.slice(endIndex)
]
const countTotal: number = (schema.pageConf as Array<number>).reduce(
(v: number, i: number) => v + i
)
if (countTotal != newData.length) {
schema.pageConf[pageEditOne.value - 1] = (schema.pageConf[pageEditOne.value - 1] + 1) as never
}
setQuestionDataList(newData)
} }
return { return {
@ -670,7 +674,6 @@ export const useEditStore = defineStore('edit', () => {
surveyId, surveyId,
sessionId, sessionId,
setSurveyId, setSurveyId,
initSessionId,
bannerList, bannerList,
fetchBannerData, fetchBannerData,
cooperPermissions, cooperPermissions,
@ -703,7 +706,6 @@ export const useEditStore = defineStore('edit', () => {
setQuestionDataList, setQuestionDataList,
moveQuestionDataList, moveQuestionDataList,
init, init,
initSchema,
getSchemaFromRemote, getSchemaFromRemote,
copyQuestion, copyQuestion,
addQuestion, addQuestion,

View File

@ -1,6 +1,8 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import localstorage from '@/common/localstorage'
type IUserInfo = { type IUserInfo = {
username: string username: string
token: string token: string
@ -17,12 +19,12 @@ export const useUserStore = defineStore('user', () => {
const initialized = ref(false) const initialized = ref(false)
const init = () => { const init = () => {
const localData = localStorage.getItem(USER_INFO_KEY) const localData = localstorage.getItem(USER_INFO_KEY)
if (localData) { if (localData) {
try { try {
const { userInfo: info, loginTime: time } = JSON.parse(localData) const { userInfo: info, loginTime: time } = localData as any
if (Date.now() - time > 7 * 3600000) { if (Date.now() - time > 7 * 3600000) {
localStorage.removeItem(USER_INFO_KEY) localstorage.removeItem(USER_INFO_KEY)
} else { } else {
login(info) login(info)
} }
@ -36,18 +38,15 @@ export const useUserStore = defineStore('user', () => {
userInfo.value = data userInfo.value = data
hasLogined.value = true hasLogined.value = true
loginTime.value = Date.now() loginTime.value = Date.now()
localStorage.setItem( localstorage.setItem(USER_INFO_KEY, {
USER_INFO_KEY, userInfo: data,
JSON.stringify({ loginTime: loginTime
userInfo: data, })
loginTime: loginTime
})
)
} }
const logout = () => { const logout = () => {
userInfo.value = null userInfo.value = null
hasLogined.value = false hasLogined.value = false
localStorage.removeItem(USER_INFO_KEY) localstorage.removeItem(USER_INFO_KEY)
} }
return { userInfo, hasLogined, loginTime, initialized, init, login, logout } return { userInfo, hasLogined, loginTime, initialized, init, login, logout }

View File

@ -19,7 +19,6 @@ export function getRandom(len) {
const optionListItem = [ const optionListItem = [
'text', 'text',
'imageUrl',
'others', 'others',
'mustOthers', 'mustOthers',
'limit', 'limit',

View File

@ -25,14 +25,12 @@
padding: 0 0.2rem !important; padding: 0 0.2rem !important;
box-sizing: border-box; box-sizing: border-box;
// .choice-content, .choice-item { // .choice-content, .choice-item {
// width: 100%; // width: 100%;
// } // }
&.vertical { &.vertical {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.choice-outer { .choice-outer {

View File

@ -54,7 +54,6 @@ const meta = {
defaultValue: [ defaultValue: [
{ {
text: '对', text: '对',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -63,7 +62,6 @@ const meta = {
}, },
{ {
text: '错', text: '错',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -79,29 +77,32 @@ const meta = {
defaultValue: 'vertical' defaultValue: 'vertical'
} }
], ],
formConfig: [basicConfig, { formConfig: [
name: 'optionConfig', basicConfig,
title: '选项配置', {
type: 'Customed', name: 'optionConfig',
content: [ title: '选项配置',
{ type: 'Customed',
label: '排列方式', content: [
type: 'RadioGroup', {
key: 'layout', label: '排列方式',
value: 'vertical', type: 'RadioGroup',
options: [ key: 'layout',
{ value: 'vertical',
label: '竖排', options: [
value: 'vertical' {
}, label: '竖排',
{ value: 'vertical'
label: '横排', },
value: 'horizontal' {
}, label: '横排',
] value: 'horizontal'
}, }
] ]
}], }
]
}
],
editConfigure: { editConfigure: {
optionEdit: { optionEdit: {
show: false show: false

View File

@ -1,4 +1,4 @@
import { computed, defineComponent, shallowRef, defineAsyncComponent, watch } from 'vue' import { computed, defineComponent, shallowRef, defineAsyncComponent } from 'vue'
import { includes } from 'lodash-es' import { includes } from 'lodash-es'
import BaseChoice from '../BaseChoice' import BaseChoice from '../BaseChoice'

View File

@ -53,7 +53,6 @@ const meta = {
defaultValue: [ defaultValue: [
{ {
text: '选项1', text: '选项1',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -62,7 +61,6 @@ const meta = {
}, },
{ {
text: '选项2', text: '选项2',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',

View File

@ -12,7 +12,7 @@ function useOptionBase(options) {
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
placeholderDesc: '', placeholderDesc: ''
} }
if (typeof text !== 'string') { if (typeof text !== 'string') {
text = '选项' text = '选项'

View File

@ -1,4 +1,4 @@
import { defineComponent, shallowRef, watch, defineAsyncComponent } from 'vue' import { defineComponent, shallowRef, defineAsyncComponent } from 'vue'
import BaseChoice from '../BaseChoice' import BaseChoice from '../BaseChoice'
/** /**

View File

@ -54,7 +54,6 @@ const meta = {
defaultValue: [ defaultValue: [
{ {
text: '选项1', text: '选项1',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -63,7 +62,6 @@ const meta = {
}, },
{ {
text: '选项2', text: '选项2',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',

View File

@ -48,9 +48,9 @@ export default defineComponent({
watch(status, (v) => { watch(status, (v) => {
if (v === 'edit') { if (v === 'edit') {
document.addEventListener('click', handleDocumentClick, {capture: true}) document.addEventListener('click', handleDocumentClick, { capture: true })
} else { } else {
document.removeEventListener('click', handleDocumentClick, {capture: true}) document.removeEventListener('click', handleDocumentClick, { capture: true })
} }
}) })

View File

@ -54,7 +54,6 @@ const meta = {
defaultValue: [ defaultValue: [
{ {
text: '选项1', text: '选项1',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -63,7 +62,6 @@ const meta = {
}, },
{ {
text: '选项2', text: '选项2',
imageUrl: '',
others: false, others: false,
mustOthers: false, mustOthers: false,
othersKey: '', othersKey: '',
@ -120,7 +118,9 @@ const meta = {
key: 'minNum', key: 'minNum',
value: '', value: '',
min: 0, min: 0,
max: moduleConfig => { return moduleConfig?.maxNum || 0 }, max: (moduleConfig) => {
return moduleConfig?.maxNum || 0
},
contentClass: 'input-number-config' contentClass: 'input-number-config'
}, },
{ {
@ -128,8 +128,12 @@ const meta = {
type: 'InputNumber', type: 'InputNumber',
key: 'maxNum', key: 'maxNum',
value: '', value: '',
min: moduleConfig => { return moduleConfig?.minNum || 0 }, min: (moduleConfig) => {
max: moduleConfig => { return moduleConfig?.options?.length || 0 }, return moduleConfig?.minNum || 0
},
max: (moduleConfig) => {
return moduleConfig?.options?.length || 0
},
contentClass: 'input-number-config' contentClass: 'input-number-config'
} }
] ]

View File

@ -13,7 +13,6 @@ import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/message.scss'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant' import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
interface Props { interface Props {
formConfig: any formConfig: any
moduleConfig: any moduleConfig: any
@ -66,9 +65,12 @@ const handleInputChange = (value: number) => {
emit(FORM_CHANGE_EVENT_KEY, { key, value }) emit(FORM_CHANGE_EVENT_KEY, { key, value })
} }
watch(() => props.moduleConfig, (newVal) => { watch(
myModuleConfig.value = newVal () => props.moduleConfig,
}) (newVal) => {
myModuleConfig.value = newVal
}
)
watch( watch(
() => props.formConfig.value, () => props.formConfig.value,
(newVal) => { (newVal) => {

View File

@ -1,79 +0,0 @@
<template>
<div class="mask" v-show="visible">
<div class="box">
<div class="title">{{ title }}</div>
<div class="btn-box">
<div class="btn cancel" @click="handleCancel">{{ cancelBtnText }}</div>
<div class="btn confirm" @click="handleConfirm">{{ confirmBtnText }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
visible?: boolean
cancelBtnText?: string
confirmBtnText?: string
title?: string
}
interface Emit {
(ev: 'confirm', callback: () => void): void
(ev: 'cancel', callback: () => void): void
(ev: 'close'): void
}
const emit = defineEmits<Emit>()
withDefaults(defineProps<Props>(), {
visible: false,
cancelBtnText: '取消',
confirmBtnText: '确定',
title: ''
})
const handleConfirm = () => {
emit('confirm', () => {
emit('close')
})
}
const handleCancel = () => {
emit('cancel', () => {
emit('close')
})
}
</script>
<style lang="scss" scoped>
@import url('../styles/dialog.scss');
.btn-box {
padding: 20px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
.btn {
width: 48%;
font-size: 0.28rem;
border-radius: 0.04rem;
text-align: center;
padding: 0.16rem 0;
line-height: 0.4rem;
cursor: pointer;
&.cancel {
background: #fff;
color: #92949d;
border: 1px solid #e3e4e8;
}
&.confirm {
background-color: #4a4c5b;
border: 1px solid #4a4c5b;
color: #fff;
}
}
}
</style>

View File

@ -9,17 +9,18 @@
></QuestionRuleContainer> ></QuestionRuleContainer>
</template> </template>
<script setup> <script setup>
import { unref, computed, watch } from 'vue' import { unref, computed, watch, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer' import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer'
import { useVoteMap } from '@/render/hooks/useVoteMap' import { useVoteMap } from '@/render/hooks/useVoteMap'
import { useShowOthers } from '@/render/hooks/useShowOthers' import { useShowOthers } from '@/render/hooks/useShowOthers'
import { useShowInput } from '@/render/hooks/useShowInput' import { useShowInput } from '@/render/hooks/useShowInput'
import { cloneDeep } from 'lodash-es' import { debounce, cloneDeep } from 'lodash-es'
import { useQuestionStore } from '../stores/question' import { useQuestionStore } from '../stores/question'
import { useSurveyStore } from '../stores/survey' import { useSurveyStore } from '../stores/survey'
import { FORMDATA_SUFFIX, SUBMIT_FLAG } from '../utils/constant'
import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts' import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
import localstorage from '@/common/localstorage'
const props = defineProps({ const props = defineProps({
indexNumber: { indexNumber: {
@ -46,7 +47,7 @@ const { changeField, changeIndex, needHideFields } = storeToRefs(questionStore)
const questionConfig = computed(() => { const questionConfig = computed(() => {
let moduleConfig = props.moduleConfig let moduleConfig = props.moduleConfig
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig) const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
// console.log(field,'formValuechange')
let alloptions = options let alloptions = options
if (type === QUESTION_TYPE.VOTE) { if (type === QUESTION_TYPE.VOTE) {
@ -126,14 +127,28 @@ const handleChange = (data) => {
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) { if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
questionStore.updateVoteData(data) questionStore.updateVoteData(data)
} }
//
localStorageBack()
processJumpSkip() processJumpSkip()
valueTemp.value = data.value
debounceStorageSave()
}
const valueTemp = ref()
const handleInput = (e) => {
valueTemp.value = e.target.value
debounceStorageSave()
} }
const handleInput = () => { const debounceStorageSave = debounce(() => {
localStorageBack() let data = {
} key: props.moduleConfig.field,
value: valueTemp.value
}
const formData = cloneDeep(formValues.value)
let { key, value } = data
if (key in formData) {
formData[key] = value
}
localStorageSave(formData)
}, 500)
const processJumpSkip = () => { const processJumpSkip = () => {
const targetResult = surveyStore.jumpLogicEngine const targetResult = surveyStore.jumpLogicEngine
@ -175,12 +190,9 @@ const processJumpSkip = () => {
.map((item) => item.field) .map((item) => item.field)
questionStore.addNeedHideFields(skipKey) questionStore.addNeedHideFields(skipKey)
} }
const localStorageBack = () => { const localStorageSave = (formData) => {
var formData = Object.assign({}, surveyStore.formValues) localstorage.removeItem(surveyStore.surveyPath + FORMDATA_SUFFIX)
localstorage.setItem(surveyStore.surveyPath + FORMDATA_SUFFIX, formData)
// localstorage.setItem(SUBMIT_FLAG, false)
localStorage.removeItem(surveyStore.surveyPath + '_questionData')
localStorage.setItem(surveyStore.surveyPath + '_questionData', JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(false))
} }
</script> </script>

View File

@ -4,15 +4,17 @@ export const useQuestionInfo = (field: string) => {
const questionstore = useQuestionStore() const questionstore = useQuestionStore()
const questionTitle = cleanRichText(questionstore.questionData[field]?.title) const questionTitle = cleanRichText(questionstore.questionData[field]?.title)
const getOptionTitle = (value:any) => { const getOptionTitle = (value: any) => {
const options = questionstore.questionData[field]?.options || [] const options = questionstore.questionData[field]?.options || []
if (value instanceof Array) { if (value instanceof Array) {
return options return options
.filter((item:any) => value.includes(item.hash)) .filter((item: any) => value.includes(item.hash))
.map((item:any) => cleanRichText(item.text)) .map((item: any) => cleanRichText(item.text))
} else { } else {
return options.filter((item:any) => item.hash === value).map((item:any) => cleanRichText(item.text)) return options
} .filter((item: any) => item.hash === value)
.map((item: any) => cleanRichText(item.text))
} }
}
return { questionTitle, getOptionTitle } return { questionTitle, getOptionTitle }
} }

View File

@ -9,9 +9,9 @@
<script> <script>
;(function () { ;(function () {
function resetRemUnit() { function resetRemUnit() {
var PC_W = 750 const PC_W = 750
var docEl = window.document.documentElement let docEl = window.document.documentElement
var width = docEl.getBoundingClientRect().width || 375 let width = docEl.getBoundingClientRect().width || 375
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) { if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {
width = width < PC_W ? width : PC_W width = width < PC_W ? width : PC_W
@ -20,15 +20,15 @@
} }
} }
var f = Math.min(width / 7.5, 50) const f = Math.min(width / 7.5, 50)
docEl.style.fontSize = f + 'px' docEl.style.fontSize = f + 'px'
var d = window.document.createElement('div') let d = window.document.createElement('div')
d.style.width = '1rem' d.style.width = '1rem'
d.style.display = 'none' d.style.display = 'none'
var head = window.document.getElementsByTagName('head')[0] let head = window.document.getElementsByTagName('head')[0]
head.appendChild(d) head.appendChild(d)
var realf = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width')) const realf = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'))
if (f !== realf) { if (f !== realf) {
docEl.style.fontSize = f * (f / realf) + 'px' docEl.style.fontSize = f * (f / realf) + 'px'

View File

@ -38,7 +38,8 @@ import encrypt from '../utils/encrypt'
import useCommandComponent from '../hooks/useCommandComponent' import useCommandComponent from '../hooks/useCommandComponent'
import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey' import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey'
import { useQuestionInfo } from '../hooks/useQuestionInfo' import { FORMDATA_SUFFIX, SUBMIT_FLAG } from '@/render/utils/constant'
import localstorage from '@/common/localstorage'
interface Props { interface Props {
questionInfo?: any questionInfo?: any
@ -151,13 +152,13 @@ const normalizationRequestBody = () => {
} }
// //
localStorage.removeItem(surveyPath.value + '_questionData') localstorage.removeItem(surveyPath.value + FORMDATA_SUFFIX)
localStorage.removeItem('isSubmit') localstorage.removeItem(SUBMIT_FLAG)
// //
var formData: Record<string, any> = Object.assign({}, surveyStore.formValues) let formData: Record<string, any> = Object.assign({}, surveyStore.formValues)
localStorage.setItem(surveyPath.value + '_questionData', JSON.stringify(formData)) localstorage.setItem(surveyPath.value + FORMDATA_SUFFIX, formData)
localStorage.setItem('isSubmit', JSON.stringify(true)) localstorage.setItem(SUBMIT_FLAG, true)
if (encryptInfo?.encryptType) { if (encryptInfo?.encryptType) {
result.encryptType = encryptInfo.encryptType result.encryptType = encryptInfo.encryptType

View File

@ -3,9 +3,9 @@ import { defineStore } from 'pinia'
import { set } from 'lodash-es' import { set } from 'lodash-es'
import { useSurveyStore } from '@/render/stores/survey' import { useSurveyStore } from '@/render/stores/survey'
import { queryVote } from '@/render/api/survey' import { queryVote } from '@/render/api/survey'
import { QUESTION_TYPE, NORMAL_CHOICES } from '@/common/typeEnum' import { QUESTION_TYPE } from '@/common/typeEnum'
import { VOTE_INFO_KEY } from '@/render/utils/constant'
const VOTE_INFO_KEY = 'voteinfo' import localstorage from '@/common/localstorage'
// 投票进度逻辑聚合 // 投票进度逻辑聚合
const usevVoteMap = (questionData) => { const usevVoteMap = (questionData) => {
@ -28,19 +28,16 @@ const usevVoteMap = (questionData) => {
return return
} }
try { try {
localStorage.removeItem(VOTE_INFO_KEY) localstorage.removeItem(VOTE_INFO_KEY)
const voteRes = await queryVote({ const voteRes = await queryVote({
surveyPath, surveyPath,
fieldList: fieldList.join(',') fieldList: fieldList.join(',')
}) })
if (voteRes.code === 200) { if (voteRes.code === 200) {
localStorage.setItem( localstorage.setItem(VOTE_INFO_KEY, {
VOTE_INFO_KEY, ...voteRes.data
JSON.stringify({ })
...voteRes.data
})
)
setVoteMap(voteRes.data) setVoteMap(voteRes.data)
} }
} catch (error) { } catch (error) {
@ -60,9 +57,8 @@ const usevVoteMap = (questionData) => {
} }
const updateVoteData = (data) => { const updateVoteData = (data) => {
const { key: questionKey, value: questionVal } = data const { key: questionKey, value: questionVal } = data
// 更新前获取接口缓存在localStorage中的数据 // 更新前获取接口缓存在localstorage中的数据
const localData = localStorage.getItem(VOTE_INFO_KEY) const voteinfo = localstorage.getItem(VOTE_INFO_KEY)
const voteinfo = JSON.parse(localData)
const currentQuestion = questionData.value[questionKey] const currentQuestion = questionData.value[questionKey]
const options = currentQuestion.options const options = currentQuestion.options
const voteTotal = voteinfo?.[questionKey]?.total || 0 const voteTotal = voteinfo?.[questionKey]?.total || 0

View File

@ -2,23 +2,24 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { cloneDeep, pick } from 'lodash-es' import { cloneDeep, pick } from 'lodash-es'
import { isMobile as isInMobile } from '@/render/utils/index'
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
import { useQuestionStore } from '@/render/stores/question'
import { useErrorInfo } from '@/render/stores/errorInfo'
import moment from 'moment' import moment from 'moment'
// 引入中文 // 引入中文
import 'moment/locale/zh-cn' import 'moment/locale/zh-cn'
// 设置中文 // 设置中文
import { isMobile as isInMobile } from '@/render/utils/index'
import { getEncryptInfo as getEncryptInfoApi } from '@/render/api/survey'
import { useQuestionStore } from '@/render/stores/question'
import { useErrorInfo } from '@/render/stores/errorInfo'
import { FORMDATA_SUFFIX, SUBMIT_FLAG } from '@/render/utils/constant'
import adapter from '../adapter' import adapter from '../adapter'
import { RuleMatch } from '@/common/logicEngine/RulesMatch' import { RuleMatch } from '@/common/logicEngine/RulesMatch'
import useCommandComponent from '../hooks/useCommandComponent' import useCommandComponent from '../hooks/useCommandComponent'
import BackAnswerDialog from '../components/BackAnswerDialog.vue' import ConfirmDialog from '../components/ConfirmDialog.vue'
import localstorage from '@/common/localstorage'
const confirm = useCommandComponent(BackAnswerDialog) const confirm = useCommandComponent(ConfirmDialog)
moment.locale('zh-cn') moment.locale('zh-cn')
/** /**
@ -134,7 +135,7 @@ export const useSurveyStore = defineStore('survey', () => {
'pageConf' 'pageConf'
]) ])
) )
// todo: 建议通过questionStore提供setqueationdata方法修改属性否则不好跟踪变化
questionStore.questionData = questionData questionStore.questionData = questionData
questionStore.questionSeq = questionSeq questionStore.questionSeq = questionSeq
@ -168,52 +169,29 @@ export const useSurveyStore = defineStore('survey', () => {
// 加载空白问卷 // 加载空白问卷
clearFormData(option) clearFormData(option)
const { breakAnswer, backAnswer } = option.baseConf const { fillAnswer, fillSubmitAnswer } = option.baseConf
const localData = JSON.parse(localStorage.getItem(surveyPath.value + '_questionData')) const localData = localstorage.getItem(surveyPath.value + FORMDATA_SUFFIX)
const isSubmit = JSON.parse(localStorage.getItem('isSubmit')) const isSubmit = localstorage.getItem(SUBMIT_FLAG)
// 开启了断点续答 or 回填上一次提交内容
if (localData) { if ((fillAnswer || (fillSubmitAnswer && isSubmit)) && localData) {
// 断点续答 const title = fillAnswer ? '是否继续上次填写的内容?' : '是否继续上次提交的内容?'
if (breakAnswer) { confirm({
confirm({ title: title,
title: '是否继续上次填写的内容?', onConfirm: async () => {
onConfirm: async () => { try {
try { // 回填答题内容
// 回填答题内容 fillFormData(localData)
fillFormData(localData) } catch (error) {
} catch (error) { console.error(error)
console.log(error) } finally {
} finally {
confirm.close()
}
},
onCancel: async () => {
confirm.close() confirm.close()
} }
}) },
} else if (backAnswer) { onClose: async () => {
if (isSubmit) { confirm.close()
confirm({
title: '是否继续上次提交的内容?',
onConfirm: async () => {
try {
// 回填答题内容
fillFormData(localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async () => {
confirm.close()
}
})
} }
} })
} else {
clearFormData(option)
} }
} }

View File

@ -0,0 +1,5 @@
export const VOTE_INFO_KEY = 'voteinfo'
export const QUOTA_INFO_KEY = 'limitinfo'
export const SUBMIT_FLAG = 'isSubmit'
export const FORMDATA_SUFFIX = '_questionData'

View File

@ -16,7 +16,7 @@ export default class EventBus {
off(eventName, fn) { off(eventName, fn) {
if (this.events[eventName]) { if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) { for (let i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) { if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1) this.events[eventName].splice(i, 1)
break break