diff --git a/.gitignore b/.gitignore index e64c75db..98aad444 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules dist package-lock.json -yarn.lock # local env files .env.local diff --git a/server/package.json b/server/package.json index fc06b434..74f81e04 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,7 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.1", "ali-oss": "^6.20.0", - "cheerio": "1.0.0-rc.12", + "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dotenv": "^16.3.2", "fs-extra": "^11.2.0", @@ -48,8 +48,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "svg-captcha": "^1.4.0", - "typeorm": "^0.3.19", - "xss": "^1.0.15" + "typeorm": "^0.3.19" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/server/src/enums/index.ts b/server/src/enums/index.ts index 897a4b90..5dc930dc 100644 --- a/server/src/enums/index.ts +++ b/server/src/enums/index.ts @@ -6,7 +6,7 @@ export enum RECORD_STATUS { PUBLISHED = 'published', // 发布 REMOVED = 'removed', // 删除 FORCE_REMOVED = 'forceRemoved', // 从回收站删除 - COMOPUTETING = 'computing', // 计算中 + COMPUTING = 'computing', // 计算中 FINISHED = 'finished', // 已完成 ERROR = 'error', // 错误 } diff --git a/server/src/modules/survey/services/downloadTask.service.ts b/server/src/modules/survey/services/downloadTask.service.ts index 7f276075..fe81043b 100644 --- a/server/src/modules/survey/services/downloadTask.service.ts +++ b/server/src/modules/survey/services/downloadTask.service.ts @@ -156,7 +156,7 @@ export class DownloadTaskService { { $set: { curStatus: { - status: RECORD_STATUS.COMOPUTETING, + status: RECORD_STATUS.COMPUTING, date: Date.now(), }, }, diff --git a/server/src/modules/survey/template/surveyTemplate/survey/normal.json b/server/src/modules/survey/template/surveyTemplate/survey/normal.json index 606c6e08..5d940a75 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/normal.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/normal.json @@ -43,7 +43,6 @@ "options": [ { "text": "选项1", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", @@ -52,7 +51,6 @@ }, { "text": "选项2", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", diff --git a/server/src/modules/survey/template/surveyTemplate/survey/register.json b/server/src/modules/survey/template/surveyTemplate/survey/register.json index d7fa4a1b..a5e502d2 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/register.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/register.json @@ -47,7 +47,6 @@ { "text": "课程1", "hash": "115019", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", @@ -56,7 +55,6 @@ { "text": "课程2", "hash": "115020", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", @@ -65,7 +63,6 @@ { "text": "课程3", "hash": "115021", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", @@ -74,7 +71,6 @@ { "text": "课程4", "hash": "115022", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", diff --git a/server/src/modules/survey/template/surveyTemplate/survey/vote.json b/server/src/modules/survey/template/surveyTemplate/survey/vote.json index 34de3afe..4187c6d2 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/vote.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/vote.json @@ -46,7 +46,6 @@ "options": [ { "text": "选项1", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", @@ -55,7 +54,6 @@ }, { "text": "选项2", - "imageUrl": "", "others": false, "mustOthers": false, "othersKey": "", diff --git a/server/src/modules/survey/template/surveyTemplate/templateBase.json b/server/src/modules/survey/template/surveyTemplate/templateBase.json index fd3e76e9..70726a46 100644 --- a/server/src/modules/survey/template/surveyTemplate/templateBase.json +++ b/server/src/modules/survey/template/surveyTemplate/templateBase.json @@ -51,6 +51,7 @@ }, "pageConf": [], "logicConf": { - "showLogicConf": [] + "showLogicConf": [], + "jumpLogicConf": [] } } diff --git a/server/src/utils/xss.ts b/server/src/utils/xss.ts deleted file mode 100644 index b7ea7ee5..00000000 --- a/server/src/utils/xss.ts +++ /dev/null @@ -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) { - // 过滤为空,否则不过滤为空 - 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(//g, '[图片]') - .replace(//g, '[视频]'); - const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, ''); - - return content; -}; - -export function escapeHtml(html) { - return html.replace(//g, '>'); -} -export const transformHtmlTag = (html) => { - if (!html) return ''; - if (typeof html !== 'string') return html + ''; - return html - .replace(html ? /&(?!#?\w+;)/g : /&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\\\n/g, '\\n'); - //.replace(/ /g, "") -}; - -const filterXSSClone = myxss.process.bind(myxss); - -export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html)); - -export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html)); diff --git a/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue b/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue index e235f475..c47e0048 100644 --- a/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue +++ b/web/src/management/pages/edit/modules/questionModule/components/AdvancedConfig/OptionConfig.vue @@ -141,20 +141,13 @@ export default { this.initCurOption() }, addOption(text = '选项', others = false, index = -1, fieldId) { - let addOne - if (this.curOptions[0]) { - addOne = _cloneDeep(this.curOptions[0]) - } else { - addOne = { - text: '', - hash: '', - others: false, - mustOthers: false, - othersKey: '', - placeholderDesc: '', - score: 0, - limit: '' - } + let addOne = { + text: '', + hash: '', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '' } for (const i in addOne) { if (i === 'others') { diff --git a/web/src/management/pages/edit/setterConfig/baseConfig.js b/web/src/management/pages/edit/setterConfig/baseConfig.js index d4c6ce79..5fdd540d 100644 --- a/web/src/management/pages/edit/setterConfig/baseConfig.js +++ b/web/src/management/pages/edit/setterConfig/baseConfig.js @@ -7,7 +7,7 @@ export default [ { title: '提交限制', key: 'limitConfig', - formList: ['limit_tLimit', 'limit_breakAnswer', 'limit_backAnswer'] + formList: ['limit_tLimit', 'limit_breakpointAnswer', 'limit_fillsubmitAnswer'] }, { title: '作答限制', diff --git a/web/src/management/pages/edit/setterConfig/baseFormConfig.js b/web/src/management/pages/edit/setterConfig/baseFormConfig.js index 0f4239c2..c0eefaa0 100644 --- a/web/src/management/pages/edit/setterConfig/baseFormConfig.js +++ b/web/src/management/pages/edit/setterConfig/baseFormConfig.js @@ -22,16 +22,16 @@ export default { type: 'QuestionTimeHour', placement: 'top' }, - limit_breakAnswer: { - key: 'breakAnswer', + limit_breakpointAnswer: { + key: 'breakpointAnswer', label: '允许断点续答', tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)', placement: 'top', type: 'CustomedSwitch', value: false, }, - limit_backAnswer: { - key: 'backAnswer', + limit_fillsubmitAnswer: { + key: 'fillsubmitAnswer', label: '自动填充上次提交内容', tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)', placement: 'top', diff --git a/web/src/management/stores/edit.ts b/web/src/management/stores/edit.ts index 2abb03a0..74b0c918 100644 --- a/web/src/management/stores/edit.ts +++ b/web/src/management/stores/edit.ts @@ -69,7 +69,6 @@ function useInitializeSchema(surveyId: Ref, initializeSchemaCallBack: () begTime: '', endTime: '', language: 'chinese', - showVoteProcess: 'allow', tLimit: 0, answerBegTime: '', answerEndTime: '', diff --git a/web/src/materials/questions/common/utils/index.js b/web/src/materials/questions/common/utils/index.js index 2262b92f..511c8a27 100644 --- a/web/src/materials/questions/common/utils/index.js +++ b/web/src/materials/questions/common/utils/index.js @@ -19,7 +19,6 @@ export function getRandom(len) { const optionListItem = [ 'text', - 'imageUrl', 'others', 'mustOthers', 'limit', diff --git a/web/src/materials/questions/widgets/BinaryChoiceModule/meta.js b/web/src/materials/questions/widgets/BinaryChoiceModule/meta.js index a14dd76b..48c3baf0 100644 --- a/web/src/materials/questions/widgets/BinaryChoiceModule/meta.js +++ b/web/src/materials/questions/widgets/BinaryChoiceModule/meta.js @@ -54,7 +54,6 @@ const meta = { defaultValue: [ { text: '对', - imageUrl: '', others: false, mustOthers: false, othersKey: '', @@ -63,7 +62,6 @@ const meta = { }, { text: '错', - imageUrl: '', others: false, mustOthers: false, othersKey: '', diff --git a/web/src/materials/questions/widgets/CheckboxModule/index.jsx b/web/src/materials/questions/widgets/CheckboxModule/index.jsx index 72b9d9cb..874f7ced 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/index.jsx +++ b/web/src/materials/questions/widgets/CheckboxModule/index.jsx @@ -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 BaseChoice from '../BaseChoice' diff --git a/web/src/materials/questions/widgets/CheckboxModule/meta.js b/web/src/materials/questions/widgets/CheckboxModule/meta.js index 53dd5206..9022964c 100644 --- a/web/src/materials/questions/widgets/CheckboxModule/meta.js +++ b/web/src/materials/questions/widgets/CheckboxModule/meta.js @@ -53,7 +53,6 @@ const meta = { defaultValue: [ { text: '选项1', - imageUrl: '', others: false, mustOthers: false, othersKey: '', @@ -62,7 +61,6 @@ const meta = { }, { text: '选项2', - imageUrl: '', others: false, mustOthers: false, othersKey: '', diff --git a/web/src/materials/questions/widgets/RadioModule/index.jsx b/web/src/materials/questions/widgets/RadioModule/index.jsx index 8f8129c4..6985478c 100644 --- a/web/src/materials/questions/widgets/RadioModule/index.jsx +++ b/web/src/materials/questions/widgets/RadioModule/index.jsx @@ -1,4 +1,4 @@ -import { defineComponent, shallowRef, watch, defineAsyncComponent } from 'vue' +import { defineComponent, shallowRef, defineAsyncComponent } from 'vue' import BaseChoice from '../BaseChoice' /** diff --git a/web/src/materials/questions/widgets/RadioModule/meta.js b/web/src/materials/questions/widgets/RadioModule/meta.js index ce253bf2..69594de4 100644 --- a/web/src/materials/questions/widgets/RadioModule/meta.js +++ b/web/src/materials/questions/widgets/RadioModule/meta.js @@ -54,21 +54,19 @@ const meta = { defaultValue: [ { text: '选项1', - imageUrl: '', others: false, mustOthers: false, - othersKey: '', - placeholderDesc: '', - hash: '115019' + othersKey: "", + placeholderDesc: "", + hash: "115019" }, { - text: '选项2', - imageUrl: '', + text: "选项2", others: false, mustOthers: false, - othersKey: '', - placeholderDesc: '', - hash: '115020' + othersKey: "", + placeholderDesc: "", + hash: "115020" } ] }, diff --git a/web/src/materials/questions/widgets/VoteModule/meta.js b/web/src/materials/questions/widgets/VoteModule/meta.js index f649e490..d79e810a 100644 --- a/web/src/materials/questions/widgets/VoteModule/meta.js +++ b/web/src/materials/questions/widgets/VoteModule/meta.js @@ -54,7 +54,6 @@ const meta = { defaultValue: [ { text: '选项1', - imageUrl: '', others: false, mustOthers: false, othersKey: '', @@ -63,7 +62,6 @@ const meta = { }, { text: '选项2', - imageUrl: '', others: false, mustOthers: false, othersKey: '', diff --git a/web/src/render/components/BackAnswerDialog.vue b/web/src/render/components/BackAnswerDialog.vue deleted file mode 100644 index eeafa7ee..00000000 --- a/web/src/render/components/BackAnswerDialog.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - \ No newline at end of file diff --git a/web/src/render/components/QuestionWrapper.vue b/web/src/render/components/QuestionWrapper.vue index 4edd67ca..416a737a 100644 --- a/web/src/render/components/QuestionWrapper.vue +++ b/web/src/render/components/QuestionWrapper.vue @@ -15,10 +15,10 @@ import QuestionRuleContainer from '../../materials/questions/QuestionRuleContain import { useVoteMap } from '@/render/hooks/useVoteMap' import { useShowOthers } from '@/render/hooks/useShowOthers' import { useShowInput } from '@/render/hooks/useShowInput' -import { cloneDeep } from 'lodash-es' +import { debounce, cloneDeep } from 'lodash-es' import { useQuestionStore } from '../stores/question' import { useSurveyStore } from '../stores/survey' - +import { FORMDATA_SUFFIX, SUBMIT_FLAG } from '../utils/constant' import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts' const props = defineProps({ @@ -46,7 +46,7 @@ const { changeField, changeIndex, needHideFields } = storeToRefs(questionStore) const questionConfig = computed(() => { let moduleConfig = props.moduleConfig const { type, field, options = [], ...rest } = cloneDeep(moduleConfig) - // console.log(field,'这里依赖的formValue,所以change时会触发重新计算') + let alloptions = options if (type === QUESTION_TYPE.VOTE) { @@ -126,13 +126,24 @@ const handleChange = (data) => { if (props.moduleConfig.type === QUESTION_TYPE.VOTE) { questionStore.updateVoteData(data) } - // 断点续答的的数据缓存 - localStorageBack() processJumpSkip() + debounceLocalStorageSave(data) +} +const handleInput = (e) => { + let data = { + key: props.moduleConfig.field, + value: e.target.value + } + debounceLocalStorageSave(data) } -const handleInput = () => { - localStorageBack() +const debounceLocalStorageSave = (data) => { + const formData = cloneDeep(formValues.value) + let { key, value } = data + if (key in formData) { + formData[key] = value + } + debounce(() => localStorageSave(formData), 500)() } const processJumpSkip = () => { @@ -175,12 +186,10 @@ const processJumpSkip = () => { .map((item) => item.field) questionStore.addNeedHideFields(skipKey) } -const localStorageBack = () => { - var formData = Object.assign({}, surveyStore.formValues) - +const localStorageSave = (formData) => { //浏览器存储 - localStorage.removeItem(surveyStore.surveyPath + '_questionData') - localStorage.setItem(surveyStore.surveyPath + '_questionData', JSON.stringify(formData)) - localStorage.setItem('isSubmit', JSON.stringify(false)) + localStorage.removeItem(surveyStore.surveyPath + FORMDATA_SUFFIX) + localStorage.setItem(surveyStore.surveyPath + FORMDATA_SUFFIX, JSON.stringify(formData)) + localStorage.setItem(SUBMIT_FLAG, JSON.stringify(false)) } diff --git a/web/src/render/pages/RenderPage.vue b/web/src/render/pages/RenderPage.vue index 1a6ddec7..4eb41f70 100644 --- a/web/src/render/pages/RenderPage.vue +++ b/web/src/render/pages/RenderPage.vue @@ -39,7 +39,7 @@ import encrypt from '../utils/encrypt' import useCommandComponent from '../hooks/useCommandComponent' import { getPublishedSurveyInfo, getPreviewSchema } from '../api/survey' -import { useQuestionInfo } from '../hooks/useQuestionInfo' +import { FORMDATA_SUFFIX, SUBMIT_FLAG } from '@/render/utils/constant' interface Props { questionInfo?: any @@ -152,14 +152,14 @@ const normalizationRequestBody = () => { } //浏览器缓存数据 - localStorage.removeItem(surveyPath.value + '_questionData') - localStorage.removeItem('isSubmit') + localStorage.removeItem(surveyPath.value + FORMDATA_SUFFIX) + localStorage.removeItem(SUBMIT_FLAG) //数据加密 - var formData: Record = Object.assign({}, surveyStore.formValues) - - localStorage.setItem(surveyPath.value + '_questionData', JSON.stringify(formData)) - localStorage.setItem('isSubmit', JSON.stringify(true)) - + let formData : Record = Object.assign({}, surveyStore.formValues) + + localStorage.setItem(surveyPath.value + FORMDATA_SUFFIX, JSON.stringify(formData)) + localStorage.setItem(SUBMIT_FLAG, JSON.stringify(true)) + if (encryptInfo?.encryptType) { result.encryptType = encryptInfo.encryptType result.data = encrypt[result.encryptType as 'rsa']({ diff --git a/web/src/render/stores/question.js b/web/src/render/stores/question.js index 3b2b0030..f1a8189c 100644 --- a/web/src/render/stores/question.js +++ b/web/src/render/stores/question.js @@ -3,9 +3,10 @@ import { defineStore } from 'pinia' import { set } from 'lodash-es' import { useSurveyStore } from '@/render/stores/survey' import { queryVote } from '@/render/api/survey' -import { QUESTION_TYPE, NORMAL_CHOICES } from '@/common/typeEnum' +import { QUESTION_TYPE } from '@/common/typeEnum' +import { parseJson } from '@/render/utils/index' +import { VOTE_INFO_KEY } from '@/render/utils/constant' -const VOTE_INFO_KEY = 'voteinfo' // 投票进度逻辑聚合 const usevVoteMap = (questionData) => { @@ -62,7 +63,7 @@ const usevVoteMap = (questionData) => { const { key: questionKey, value: questionVal } = data // 更新前获取接口缓存在localStorage中的数据 const localData = localStorage.getItem(VOTE_INFO_KEY) - const voteinfo = JSON.parse(localData) + const voteinfo = parseJson(localData) const currentQuestion = questionData.value[questionKey] const options = currentQuestion.options const voteTotal = voteinfo?.[questionKey]?.total || 0 diff --git a/web/src/render/stores/survey.js b/web/src/render/stores/survey.js index e90529cd..3641bdc6 100644 --- a/web/src/render/stores/survey.js +++ b/web/src/render/stores/survey.js @@ -3,10 +3,11 @@ import { useRouter } from 'vue-router' import { defineStore } from 'pinia' import { cloneDeep, pick } from 'lodash-es' -import { isMobile as isInMobile } from '@/render/utils/index' +import { isMobile as isInMobile, parseJson } 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 moment from 'moment' // 引入中文 @@ -16,9 +17,10 @@ import 'moment/locale/zh-cn' import adapter from '../adapter' import { RuleMatch } from '@/common/logicEngine/RulesMatch' import useCommandComponent from '../hooks/useCommandComponent' -import BackAnswerDialog from '../components/BackAnswerDialog.vue' +import ConfirmDialog from '../components/ConfirmDialog.vue' + +const confirm = useCommandComponent(ConfirmDialog) -const confirm = useCommandComponent(BackAnswerDialog) moment.locale('zh-cn') /** @@ -134,7 +136,7 @@ export const useSurveyStore = defineStore('survey', () => { 'pageConf' ]) ) - // todo: 建议通过questionStore提供setqueationdata方法修改属性,否则不好跟踪变化 + questionStore.questionData = questionData questionStore.questionSeq = questionSeq @@ -168,52 +170,29 @@ export const useSurveyStore = defineStore('survey', () => { // 加载空白问卷 clearFormData(option) - const { breakAnswer, backAnswer } = option.baseConf - const localData = JSON.parse(localStorage.getItem(surveyPath.value + '_questionData')) - - const isSubmit = JSON.parse(localStorage.getItem('isSubmit')) - - if (localData) { - // 断点续答 - if (breakAnswer) { - confirm({ - title: '是否继续上次填写的内容?', - onConfirm: async () => { - try { - // 回填答题内容 - fillFormData(localData) - } catch (error) { - console.log(error) - } finally { - confirm.close() - } - }, - onCancel: async () => { - confirm.close() + const { breakpointAnswer, fillsubmitAnswer } = option.baseConf + const localData = parseJson(localStorage.getItem(surveyPath.value + FORMDATA_SUFFIX)) + + const isSubmit = parseJson(localStorage.getItem(SUBMIT_FLAG)) + // 开启了断点续答 or 回填上一次提交内容 + if((breakpointAnswer || (fillsubmitAnswer && isSubmit)) && localData) { + const title = breakpointAnswer ? '是否继续上次填写的内容?' : '是否继续上次提交的内容?' + confirm({ + title: title, + onConfirm: async () => { + try { + // 回填答题内容 + fillFormData(localData); + } catch (error) { + console.error(error); + } finally { + confirm.close(); } - }) - } else if (backAnswer) { - if (isSubmit) { - confirm({ - title: '是否继续上次提交的内容?', - onConfirm: async () => { - try { - // 回填答题内容 - fillFormData(localData) - } catch (error) { - console.log(error) - } finally { - confirm.close() - } - }, - onCancel: async () => { - confirm.close() - } - }) + }, + onClose: async () => { + confirm.close(); } - } - } else { - clearFormData(option) + }); } } diff --git a/web/src/render/utils/constant.ts b/web/src/render/utils/constant.ts new file mode 100644 index 00000000..a98f20c4 --- /dev/null +++ b/web/src/render/utils/constant.ts @@ -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' \ No newline at end of file diff --git a/web/src/render/utils/eventbus.js b/web/src/render/utils/eventbus.js index 455d3332..8f9161f4 100644 --- a/web/src/render/utils/eventbus.js +++ b/web/src/render/utils/eventbus.js @@ -16,7 +16,7 @@ export default class EventBus { off(eventName, fn) { 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) { this.events[eventName].splice(i, 1) break diff --git a/web/src/render/utils/index.js b/web/src/render/utils/index.js index 610e506f..9887644a 100644 --- a/web/src/render/utils/index.js +++ b/web/src/render/utils/index.js @@ -30,3 +30,19 @@ export const formatLink = (url) => { } return `http://${url}` } + +/** + * 安全地解析 JSON 字符串 + * @param {string} jsonString - JSON 字符串 + * @returns {any} 解析后的对象或 null + */ +export function parseJson(jsonString) { + try { + return JSON.parse(jsonString); + } catch (error) { + console.error("Error parsing JSON:", error); + return null; + } +} + +export default parseJson; \ No newline at end of file