Update main.js to import Element Plus styles, enhance parent.html with new input types and improved layout

This commit is contained in:
HoshinoKoji 2025-03-03 18:07:09 +08:00
parent 10e6944532
commit 1341a32484
4 changed files with 157 additions and 72 deletions

View File

@ -4,7 +4,12 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- <title>Vite App</title> --> <style>
body {
font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
</style>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -8,17 +8,41 @@
type: 'spec', type: 'spec',
title: 'Contact Form', title: 'Contact Form',
items: [{ items: [{
type: 'text', type: 'display',
title: 'Name' title: 'Instructions',
desc: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
}, { }, {
type: 'text', type: 'text',
title: 'Email' title: 'Text',
required: true,
desc: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
}, { }, {
type: 'text', type: 'text',
title: 'Phone' title: 'Long text',
required: true,
desc: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}, {
type: 'checkbox',
title: 'Checkbox',
required: true,
optTexts: ['Option 1', 'Option 2', 'Option 3'],
optValues: ['Option 1', 'Option 2', 'Option 3'],
}, {
type: 'scale',
title: 'Scale with auto-next',
required: true,
optTexts: ['Never', 'Almost never', 'Sometimes', 'Almost always', 'Always'],
optValues: [1, 2, 3, 4, 5],
}, { }, {
type: 'radio', type: 'radio',
title: 'Test', title: 'Radio with auto-next',
required: true,
optTexts: ['Option 1', 'Option 2', 'Option 3'],
optValues: ['Option 1', 'Option 2', 'Option 3']
}, {
type: 'radio',
title: 'Radio without auto-next',
required: true,
optTexts: ['Option 1', 'Option 2', 'Option 3'], optTexts: ['Option 1', 'Option 2', 'Option 3'],
optValues: ['Option 1', 'Option 2', 'Option 3'] optValues: ['Option 1', 'Option 2', 'Option 3']
}] }]
@ -31,11 +55,17 @@
iframe.contentWindow.postMessage(data, window.origin); iframe.contentWindow.postMessage(data, window.origin);
}; };
</script> </script>
<style>
body {
font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
</style>
</head> </head>
<body> <body>
<iframe <iframe
src="/index.html" src="/index.html"
style="width: 100%; height: 100vh; border: none;" style="width: 99vw; height: 95vh; border: none;"
id="iframe"></iframe> id="iframe"></iframe>
</body> </body>
</html> </html>

View File

@ -1,4 +1,7 @@
<script> <script>
import { ElMessage } from 'element-plus';
import { isProxy } from 'vue';
/* /*
* Specification of item data structure: * Specification of item data structure:
* { * {
@ -6,12 +9,12 @@
* title: string, * title: string,
* optTexts: string[], // only for 'radio', 'checkbox', 'scale' * optTexts: string[], // only for 'radio', 'checkbox', 'scale'
* optValues: string[], // only for 'radio', 'checkbox', 'scale' * optValues: string[], // only for 'radio', 'checkbox', 'scale'
* required: boolean, // optional, whether the user must answer the question, default is true * required: boolean, // whether the user must answer the question
* answer: string, // user's answer, appended after user submits * answer: string, // user's answer, appended after user submits
* refilled: boolean, // whether the user refilled the answer, appended after user submits * refilled: boolean, // whether the user refilled the answer, appended after user submits
* } * }
*/ */
const ITEM_TYPES = ['text', 'radio', 'checkbox', 'scale']; const ITEM_TYPES = ['text', 'radio', 'checkbox', 'scale', 'display'];
function isValidItem({ type, title, optTexts, optValues = null }) { function isValidItem({ type, title, optTexts, optValues = null }) {
if (!title || !ITEM_TYPES.includes(type)) { if (!title || !ITEM_TYPES.includes(type)) {
return false; return false;
@ -31,13 +34,14 @@ export default {
title: undefined, title: undefined,
started: undefined, started: undefined,
items: [], items: [],
currentTitle: undefined,
currentIdx: undefined, currentIdx: undefined,
currentItem: undefined, itemStatus: {
currentAnswer: undefined, item: undefined,
currentAnswerValue: undefined, timestamp: undefined,
currentRefilled: false, title: undefined,
timestamp: undefined, answer: undefined,
refilled: false,
},
uiStatus: { uiStatus: {
backButtonDisabled: true, backButtonDisabled: true,
nextButtonText: 'Next', nextButtonText: 'Next',
@ -67,7 +71,7 @@ export default {
}, },
computed: { computed: {
isReady() { isReady() {
return this.items && this.title; return this.items && this.items.length > 0;
}, },
}, },
methods: { methods: {
@ -84,30 +88,45 @@ export default {
} }
}, },
updateTitle() { updateTitle() {
this.currentTitle = this.currentItem ? this.itemStatus.title = this.itemStatus.item ?
`${this.currentIdx + 1}. ${this.currentItem.title}` : ''; `${this.currentIdx + 1}. ${this.itemStatus.item.title}` : '';
}, },
updateItem() { updateItem() {
this.currentItem = this.items[this.currentIdx]; this.itemStatus.item = this.items[this.currentIdx];
this.currentAnswer = this.items[this.currentIdx].answer; this.itemStatus.answer = this.items[this.currentIdx].answer;
this.currentRefilled = (this.currentItem.answer !== undefined); this.itemStatus.refilled = (this.itemStatus.answer !== undefined);
this.timestamp = new Date().getTime(); this.itemStatus.timestamp = new Date().getTime();
this.updateTitle(); this.updateTitle();
this.updateBackButton(); this.updateBackButton();
this.updateNextButton(); this.updateNextButton();
}, },
iterOptions() { iterOptions() {
return this.currentItem.optTexts.map((optText, index) => [ return this.itemStatus.item.optTexts.map((optText, index) => [index, optText]);
index,
optText,
]);
}, },
clickNext() { clickNext() {
const item = this.items[this.currentIdx]; const item = this.items[this.currentIdx];
item.answer = this.currentAnswer; if (item.type !== 'display' && item.type !== 'checkbox' && item.required && this.itemStatus.answer === undefined) {
item.refilled = this.currentRefilled; ElMessage({
item.responseTime = new Date().getTime() - this.timestamp; message: 'This question is required.',
type: 'error',
});
return;
}
item.answer = this.itemStatus.answer;
item.refilled = this.itemStatus.refilled;
item.responseTime = new Date().getTime() - this.itemStatus.timestamp;
if (item.type === 'radio' && item.answer !== undefined) {
item.answerText = item.optTexts[item.answer];
item.answerValue = item.optValues[item.answer];
} else if (item.type === 'checkbox') {
item.answerText = item.answer.map(idx => item.optTexts[idx]).join(', ');
item.answerValue = item.answer.map(idx => item.optValues[idx]).join(', ');
} if (item.type === 'scale' && item.answer !== undefined) {
item.answerText = item.optTexts[item.answer - 1];
item.answerValue = item.optValues[item.answer - 1];
}
if (this.currentIdx === this.items.length - 1) { if (this.currentIdx === this.items.length - 1) {
this.submit(); this.submit();
} else { } else {
@ -116,7 +135,7 @@ export default {
} }
}, },
autoNext() { autoNext() {
if (this.currentIdx < this.items.length - 1) { if (this.currentIdx < this.items.length - 1 && !this.itemStatus.refilled) {
this.clickNext(); this.clickNext();
} }
}, },
@ -127,10 +146,13 @@ export default {
} }
}, },
submit() { submit() {
const results = this.items.map((item, index) => ({ const results = this.items.map(item => ({
index: index,
title: item.title, title: item.title,
answer: item.answer || '', key: item.key ? item.key : item.title,
type: item.type,
answer: isProxy(item.answer) ? [...item.answer] : (item.answer || 'null'),
answerText: isProxy(item.answerText) ? [...item.answerText] : (item.answerText || 'null'),
answerValue: isProxy(item.answerValue) ? [...item.answerValue] : (item.answerValue || 'null'),
refilled: item.refilled, refilled: item.refilled,
responseTime: item.responseTime, responseTime: item.responseTime,
})); }));
@ -152,48 +174,61 @@ export default {
<template> <template>
<div v-if="!isReady">Loading...</div> <div v-if="!isReady">Loading...</div>
<el-container id="main" v-if="isReady"> <el-container id="main" v-if="isReady">
<el-header height=22pt id="display-title">{{ currentTitle }}</el-header> <el-header height=24pt id="display-title">{{ itemStatus.title }}</el-header>
<el-container id="display-desc" v-if="true"> <el-main id="main-inner">
<el-text class="mx-1" size="large">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor <el-container id="display-desc" v-if="itemStatus.item.desc">
incididunt ut <el-text class="mx-1" size="large"><span v-html="itemStatus.item.desc"></span></el-text>
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut </el-container>
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit <el-main id="display-content" :class="{ nodesc: !itemStatus.item.desc }">
anim id est laborum.</el-text> <template v-if="itemStatus.item.type === 'text'">
</el-container> <el-input v-model="itemStatus.answer" @keyup.enter="clickNext" autosize autofocus
<el-main id="display-content"> placeholder="Please input" />
<template v-if="currentItem.type === 'text'"> </template>
<el-input v-model="currentAnswer" autosize placeholder="Please input" /> <template v-else-if="itemStatus.item.type === 'radio'">
</template> <el-radio-group v-model="itemStatus.answer" v-for="[index, optText] in iterOptions()" :key="index"
<template v-else-if="currentItem.type === 'radio'"> @change="autoNext">
<el-radio-group v-model="currentAnswer" v-for="[index, optText] in iterOptions()" :key="index" <el-radio-button :label="optText" :value="index" />
@change="autoNext"> </el-radio-group>
<el-radio-button :label="optText" :value="index" /> </template>
</el-radio-group> <template v-else-if="itemStatus.item.type === 'checkbox'">
</template> <el-checkbox-group v-model="itemStatus.answer" v-for="[index, optText] in iterOptions()" :key="index">
<el-checkbox-button :label="optText" :value="index" />
</el-checkbox-group>
</template>
<template v-else-if="itemStatus.item.type === 'scale'">
<el-rate v-model="itemStatus.answer" :texts="itemStatus.item.optTexts"
show-text size="large" void-icon="ArrowRightBold"
:icons="['ArrowRightBold', 'ArrowRightBold', 'ArrowRightBold']"
@change="autoNext" />
</template>
</el-main>
<el-footer id="display-buttons">
<el-button-group>
<el-button type="info" round :disabled="uiStatus.backButtonDisabled" @click="clickBack">
<el-icon class="el-icon--left">
<ArrowLeft />
</el-icon>Back
</el-button>
<el-button :type="uiStatus.nextButtonStatus" round @click="clickNext">
{{ uiStatus.nextButtonText }}<el-icon class="el-icon--right">
<ArrowRight />
</el-icon>
</el-button>
</el-button-group>
</el-footer>
</el-main> </el-main>
<el-footer id="display-buttons">
<el-button-group>
<el-button type="info" round :disabled="uiStatus.backButtonDisabled" @click="clickBack">
<el-icon class="el-icon--left">
<ArrowLeft />
</el-icon>Back
</el-button>
<el-button :type="uiStatus.nextButtonStatus" round @click="clickNext">
{{ uiStatus.nextButtonText }}<el-icon class="el-icon--right">
<ArrowRight />
</el-icon>
</el-button>
</el-button-group>
</el-footer>
</el-container> </el-container>
</template> </template>
<style scoped> <style scoped>
#main { #main {
width: 50%; width: 50vw;
max-height: 75vh;
overflow: hidden;
background-color: #FAFCFF; background-color: #FAFCFF;
margin: 10% 25%; margin: 12vh auto;
padding-top: 4ex; padding-top: 4ex;
padding-left: 2ex; padding-left: 2ex;
padding-right: 2ex; padding-right: 2ex;
@ -202,6 +237,11 @@ export default {
font-family: var(--el-font-family); font-family: var(--el-font-family);
} }
#main-inner {
margin-top: 0ex;
scrollbar-width: none;
}
#display-title { #display-title {
text-align: left; text-align: left;
font-size: 18pt; font-size: 18pt;
@ -210,8 +250,9 @@ export default {
} }
#display-desc { #display-desc {
width: 90%; width: 95%;
margin: 1ex auto; margin: 0ex auto 0ex 2ex;
padding-top: 0ex;
} }
#display-buttons { #display-buttons {
@ -219,11 +260,20 @@ export default {
} }
#display-content { #display-content {
padding-top: 1ex; margin-top: 2ex;
padding-top: 0ex;
}
#display-content.nodesc {
margin-top: 0ex;
} }
#display-content el-input { #display-content el-input {
width: 100%; width: 100%;
font-size: 16pt; font-size: 16pt;
} }
#display-content el-rate {
font-size: 24pt;
}
</style> </style>

View File

@ -1,4 +1,4 @@
// import './assets/main.css' import 'element-plus/dist/index.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'