表单生成器(职业组)

介绍

小蓝现在遇到一个需求,要求是设计在线动态表单生成器,通过请求到的 JSON 数据即快速生成网页表单,这可把他难倒了。请你按照题目中给出的 Vue 组件与 JSON 数据格式,帮助小蓝完成文本输入框和单选框表单组件的代码编写。

准备

开始答题前,需要先打开本题的项目代码文件夹,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── components
│ ├── input.js
│ ├── radio.js
│ └── rate.js
├── css
│ ├── icon
│ │ ├── star.svg
│ │ └── star_full.svg
│ ├── index.css
│ ├── radio.css
│ ├── rate.css
│ └── reset.css
├── data.json
├── effect-1.gif
├── effect-2.gif
├── index.html
└── lib
├── axios.min.js
└── vue.min.js

其中:

  • index.html 是主页面。
  • components 是项目组件文件夹。
  • lib 是存放项目依赖的文件夹。
  • css 是存放项目样式的文件夹。
  • data.json 是请求需要用到 JSON 数据。
  • effect-1.gifeffect-2.gif 是项目完成后的效果图。

请通过 VS Code 中的 live server 插件启动本项目,让项目运行起来,效果如下:

初始效果

注意:一定要通过 live server 插件启动项目,避免项目无法访问,影响做题。

项目中的评分组件(rate.js)已完成。

目标

找到 index.html 以及 components 下面的输入框组件(input.js)、单选框组件(radio.js)中的 TODO 部分,达成以下目标:

  1. 请仔细阅读接口设计,完成 index.html 中的 TODO 部分,根据请求回来的 JSON 数据中 type 的不同,动态生成对应的表单组件。完成输入框组件(input.js)、单选框组件(radio.js) 初始化时需要定义的 data 以及方法,将数据正确显示到对应的组中,确保页面无报错且组件正常显示。

接口数据设计如下:

  • type 表示组件类型
  • title 为表单项的名称
  • field 为数据所对应的原始字段名
  • value 为表单项对应的值,有多种类型
  • options 有选项的表单选择内容

具体各个组件的数据结构说明如下:

  • 输入框
1
2
3
4
5
6
{
type: 'input'
title: 表单项名称, 例:姓名
field: 对应接口字段, 例:name
value: 输入框值, 例:小蓝
}
  • 单选框
1
2
3
4
5
6
7
8
9
10
11
{
type: 'radio'
title: 表单项名称, 例:性别
field: 对应接口字段, 例:gender
value: 选中值, 例:3
options: 选项值,例:[
{ "label": "1", "name": "男" },
{ "label": "2", "name": "女" },
{ "label": "3", "name": "保密" }
]
}
  • 评分组件
1
2
3
4
5
6
{
type: 'rate'
title: 表单项名称, 例:评分
field: 对应接口字段, 例:score
value: 分值 1~5, 例:5
}

目标 1 完成后效果如下:

目标1

  1. 继续完善输入框组件(input.js)、单选框组件(radio.js), 使组件数据改变时在 index.html(即父组件中)点击获取数据按钮能够正常获取表单数据。

目标 2 完成后效果

图片描述

  1. index.html(即父组件) 中点击渲染数据,表单中的数据(即对应的子组件数据)需要正确显示。

目标 3 完成后效果(考生可以根据提供的 JSON 数据自行测试)

图片描述

规定

  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径、class 名、id 名、图片名等,以免造成无法判题通过。
  • 满足需求后,保持 Web 服务处于可以正常访问状态,点击「提交检测」系统会自动检测。

判分标准

  • 完成目标 1,得 10 分。
  • 目标 2 中每完成一个组件,得 5 分,共 10 分。
  • 完成目标 3,得 5 分。

总通过次数: 2 | 总提交次数: 2 | 通过率: 100%

难度: 中等 标签: 蓝桥杯, 2023, 国赛, Web 前端, Vue.js

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!DOCTYPE html>
<html lang="zn-CH">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./lib/vue.min.js"></script>
<script src="./lib/axios.min.js"></script>
<link rel="stylesheet" href="./css/index.css" />
<title>表单生成器</title>
</head>

<body>
<div id="app">
<form>
<div class="left-box">
<!-- 组件容器 -->
<div v-for="(item, i) in formData" :key="`${i}item`" class="item">
<!-- 此处 label 为表单的标题 -->
<div class="label">{{item.title}}</div>
<!-- TODO: 根据 formData 中的 type 不同完成组件渲染 -->
<my-input v-if="item.type === 'input'" v-model="item.value"></my-input>
<my-radio v-if="item.type === 'radio'" v-for="radioChild in item.options" :key="radioChild" v-model="item.value" :label="radioChild.label">{{ radioChild.name}}</my-radio>
<my-rate v-if="item.type === 'rate'" v-model="item.value"></my-rate>
</div>
</div>
<!-- 表单设置 -->
<div class="right-box">
<div>
<button class="btn" @click="fetchData">初始化</button>
<button class="btn" id="result" @click="confirm">获得数据</button>
<button class="btn" id="render" @click="setData">渲染数据</button
><br /><br />
</div>
<textarea v-model="testData" cols="45" rows="12"></textarea>
<div class="error" v-if="hasError">JSON 格式输入有误,请仔细检查</div>
</div>
</form>
</div>
<script src="./components/input.js"></script>
<script src="./components/radio.js"></script>
<script src="./components/rate.js"></script>
<script>
var app = new Vue({
el: "#app",
data: {
formData: [],
testData: "",
hasError: false,
},
created() {
this.fetchData();
},
methods: {
async fetchData() {
// 请求 JSON 数据
const { data } = await axios.get("./data.json");
this.formData = data.result;
this.confirm();
},
// 设置数据
setData(e) {
e && e.preventDefault();
try {
const newData = JSON.parse(this.testData);
this.formData = this.formData.map((x) => {
x.value = newData[x.field];
return x;
});
this.hasError = false;
} catch (err) {
if (err) {
this.hasError = true;
}
}
},
// 获得数据
confirm(e) {
e && e.preventDefault();
const params = {};
this.formData.forEach(({ field, value }) => {
params[field] = value;
});
this.testData = JSON.stringify(params, null, 2);
},
},
});
</script>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 输入框组件
*/
const template = `
<input v-model="content" type="text" placeholder="请输入内容" >
`;
Vue.component("my-input", {
template,
props: ["value"],
// TODO 请在此继续完成该组件的代码编写
data() {
return {
content: ''
}
},
watch: {
value(newVal) {
this.content = newVal;
},
content(newVal) {
this.$emit('input', newVal);
}
}
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* 单选框组件
*/
const radioTemplate = `
<div class="radio-box">
<input type="radio" :checked="isCheck" @change="change" name="default" :id="label">
<label :for="label" class="radio-stype radio"></label> <span> <slot /> </span>
</div>
`;
Vue.component("my-radio", {
template: radioTemplate,
props: ["label", "value"],
// TODO 请在此继续完成该组件的代码编写
data() {
return {
isCheck: false
}
},
mounted() {
this.isCheck = this.label === this.value
},
watch: {
value() {
this.isCheck = this.label === this.value
}
},
methods: {
change() {
this.$emit('input', this.label)
}
}
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* 评分组件
星星 UI 文件路径说明:
1. 点亮:`css/icon/star_full.svg`
2. 未点亮:`css/icon/star.svg`
*/
const rateTemplate = `
<div class="rate">
<div @click="select(i)" v-for="i in 5" :key="i+'s'" class="item">
<object class="icon" :data="getPath(i)" type="image/svg+xml"></object>
</div>
</div>
`;
Vue.component("my-rate", {
template: rateTemplate,
props: ["value"],
data() {
return {
curCount: 0,
};
},
watch: {
value() {
this.curCount = this.value;
},
},
mounted() {
this.curCount = this.value;
},
methods: {
select(i) {
this.curCount === i ? (this.curCount = 0) : (this.curCount = i);
this.$emit("input", this.curCount);
},
getPath(i) {
return `css/icon/star${i <= this.curCount ? "_full" : ""}.svg`;
},
},
});