mirror of
https://github.com/grey-cat-1908/formaptix-web.git
synced 2024-09-22 19:21:59 +03:00
base for view && base for scale question type
This commit is contained in:
parent
924d7ec691
commit
751f9eabad
18 changed files with 3652 additions and 1011 deletions
|
@ -1,15 +0,0 @@
|
||||||
/* eslint-env node */
|
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
'extends': [
|
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
'@vue/eslint-config-typescript',
|
|
||||||
'@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<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>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
3403
package-lock.json
generated
Normal file
3403
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,6 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint formaptix-web --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
||||||
"format": "prettier --write src"
|
"format": "prettier --write src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -18,15 +17,10 @@
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^20.14.5",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
|
||||||
"npm-run-all2": "^6.2.0",
|
"npm-run-all2": "^6.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"typescript": "~5.4.0",
|
"typescript": "~5.4.0",
|
||||||
|
|
12
src/App.vue
12
src/App.vue
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="root" id="root">
|
<div class="root" id="root">
|
||||||
<Header/>
|
<Header />
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<transition name="page" mode="out-in">
|
<transition name="page" mode="out-in">
|
||||||
|
@ -20,23 +20,23 @@ import Header from '@/components/Header.vue'
|
||||||
.page-leave-active {
|
.page-leave-active {
|
||||||
transition-timing-function: ease;
|
transition-timing-function: ease;
|
||||||
transition-delay: 15ms;
|
transition-delay: 15ms;
|
||||||
transition-duration: .15s;
|
transition-duration: 0.15s;
|
||||||
transition-property: opacity;
|
transition-property: opacity;
|
||||||
overflow: hidden
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.page-enter-from,
|
.page-enter-from,
|
||||||
.page-leave-to {
|
.page-leave-to {
|
||||||
opacity: 0
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.layout {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1
|
flex: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,11 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getTitle} from "@/utils/env";
|
import { getTitle } from '@/utils/env'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>{{ getTitle() }}</h1>
|
<h1>{{ getTitle() }}</h1>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
99
src/components/forms/Scale.vue
Normal file
99
src/components/forms/Scale.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div class="rating-component">
|
||||||
|
<div class="labels">
|
||||||
|
<span>{{ minLabel }}</span>
|
||||||
|
<span>{{ maxLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating-options">
|
||||||
|
<label v-for="n in range" :key="n" class="rating-option">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:value="n"
|
||||||
|
v-model="selectedValue"
|
||||||
|
:required="isRequired"
|
||||||
|
@change="updateValue"
|
||||||
|
/>
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button v-if="selectedValue !== null" @click="cancelSelection">Отменить выбор</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
min: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
minLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
maxLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
isRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['input'])
|
||||||
|
const selectedValue = ref(props.value)
|
||||||
|
|
||||||
|
const range = computed(() => {
|
||||||
|
return Array.from({ length: props.max - props.min + 1 }, (_, i) => props.min + i)
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateValue() {
|
||||||
|
emit('input', selectedValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelSelection() {
|
||||||
|
selectedValue.value = null
|
||||||
|
emit('input', selectedValue.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rating-component {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,26 +1,31 @@
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
name: "Home",
|
name: 'Home',
|
||||||
component: () => import("@/views/Index.vue"),
|
component: () => import('@/views/Index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/form/view',
|
||||||
|
name: 'View Form',
|
||||||
|
component: () => import('@/views/form/View.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
scrollBehavior(to) {
|
scrollBehavior(to) {
|
||||||
if (to.hash) {
|
if (to.hash) {
|
||||||
return {
|
return {
|
||||||
top: 120,
|
top: 120,
|
||||||
el: to.hash,
|
el: to.hash
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
top: 0,
|
top: 0
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default router;
|
export default router
|
||||||
|
|
|
@ -44,10 +44,10 @@ textarea {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"]::-webkit-search-decoration,
|
input[type='search']::-webkit-search-decoration,
|
||||||
input[type="search"]::-webkit-search-cancel-button,
|
input[type='search']::-webkit-search-cancel-button,
|
||||||
input[type="search"]::-webkit-search-results-button,
|
input[type='search']::-webkit-search-results-button,
|
||||||
input[type="search"]::-webkit-search-results-decoration {
|
input[type='search']::-webkit-search-results-decoration {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export function getTitle () {
|
export function getTitle() {
|
||||||
console.log(import.meta.env.VITE_APP_NAME)
|
return import.meta.env.VITE_APP_NAME ? import.meta.env.VITE_APP_NAME : 'Formaptix'
|
||||||
return import.meta.env.VITE_APP_NAME ? import.meta.env.VITE_APP_NAME: 'Formaptix';
|
|
||||||
}
|
}
|
35
src/utils/http.ts
Normal file
35
src/utils/http.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export function makeAPIRequest(
|
||||||
|
path: string = '',
|
||||||
|
method: string = 'GET',
|
||||||
|
query: any = {},
|
||||||
|
body: any = {},
|
||||||
|
useAuthorization: boolean = false
|
||||||
|
): Promise<any> {
|
||||||
|
return new Promise(async (resolve, _) => {
|
||||||
|
let options: any = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useAuthorization) options.headers.Authorization = `${localStorage.getItem('auth_token')}`
|
||||||
|
|
||||||
|
if (method !== 'GET') {
|
||||||
|
options.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalPath = import.meta.env.VITE_API_URL + path
|
||||||
|
if (query) {
|
||||||
|
finalPath += '?' + new URLSearchParams(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await fetch(finalPath, options)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
return resolve({ error: 'CRITICAL_ERROR' })
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolve(r.data)
|
||||||
|
})
|
||||||
|
}
|
5
src/views/Auth.vue
Normal file
5
src/views/Auth.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template></template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -1,9 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template></template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
63
src/views/form/View.vue
Normal file
63
src/views/form/View.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { makeAPIRequest } from '@/utils/http'
|
||||||
|
import Scale from '@/components/forms/Scale.vue'
|
||||||
|
|
||||||
|
const data = ref({})
|
||||||
|
const currentPageNumber = ref(0)
|
||||||
|
const currentPage = ref({})
|
||||||
|
const answers = ref([])
|
||||||
|
|
||||||
|
async function prepareNewPage() {
|
||||||
|
currentPage.value = data.value.pages[currentPageNumber.value]
|
||||||
|
currentPage.value.questions.forEach((q) => {
|
||||||
|
answers.value[q.id] = {
|
||||||
|
question_id: q.id,
|
||||||
|
question_type: q.question_type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (currentPageNumber.value !== data.value.pages.length - 1) {
|
||||||
|
currentPageNumber.value += 1
|
||||||
|
await prepareNewPage()
|
||||||
|
} else {
|
||||||
|
await makeAPIRequest(
|
||||||
|
'/answer/create',
|
||||||
|
'POST',
|
||||||
|
{ form_id: 1 },
|
||||||
|
{
|
||||||
|
values: Array.from(Object.keys(answers.value).map((val) => answers.value[val]))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
data.value = await makeAPIRequest('/form/get', 'GET', { id: 1 })
|
||||||
|
await prepareNewPage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h2>{{ data.name }}</h2>
|
||||||
|
<p>{{ currentPage.text }}</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="" v-for="question in currentPage.questions">
|
||||||
|
<Scale
|
||||||
|
:min="question.min_value"
|
||||||
|
:max="question.max_value"
|
||||||
|
:minLabel="question.min_label"
|
||||||
|
:maxLabel="question.max_label"
|
||||||
|
:isRequired="question.required"
|
||||||
|
v-model="answers[question.id].value"
|
||||||
|
@input="answers[question.id].value = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Отправить</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -1,13 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": [
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"env.d.ts",
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"src/**/*",
|
|
||||||
"src/**/*.vue"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"src/**/__tests__/*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
|
@ -5,9 +5,7 @@ import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [vue()],
|
||||||
vue(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|
Loading…
Reference in a new issue