提交 5a957c6c 编写于 作者: D dolymood

chore(example): add Questionnaire demo

上级 bf360ac8
<template>
<div class="demo-agreement">
<p v-if="desc" v-html="desc"></p>
<cube-checkbox v-model="checked">
<span v-html="text"></span> <a :href="link.href" target="_blank" v-if="link">{{link.text}}</a>
</cube-checkbox>
</div>
</template>
<script>
export default {
props: {
value: Boolean,
desc: String,
text: String,
link: Object
},
data() {
return {
checked: this.value
}
},
watch: {
checked(newV) {
this.$emit('input', newV)
}
}
}
</script>
<style lang="stylus">
.demo-agreement
color: #666
> p
line-height: 1.5
margin-bottom: 10px
.cube-checkbox-label
font-weight: bold
a
span
font-weight: inherit
a
position: relative
z-index: 1
a
color: #fc9153
</style>
<template>
<div class="demo-questionnaire">
<p class="demo-questionnaire-tip cubeic-warn" v-if="tip" v-html="tip"></p>
<cube-form
:model="model"
:schema="schema"
:immediate-validate="false"
:options="options"
:submit-always-validate="true"
@validate="validateHandler"
@submit="submitHandler" />
</div>
</template>
<script>
import transform from './transform'
export default {
props: {
tip: String,
questions: {
type: Array,
default() {
return []
}
},
submit: {
type: Object,
default() {
return {
text: 'Submit'
}
}
}
},
data() {
const model = {}
// init model value
this.questions.forEach((question) => {
model[question.model] = undefined
if (question.type === 'checkbox' || question.type === 'upload') {
model[question.model] = []
}
})
return {
validity: {},
valid: undefined,
model,
options: {
scrollToInvalidField: true,
layout: 'classic'
}
}
},
computed: {
schema() {
const fields = []
this.eachQuestion(question => {
const field = transform(question)
fields.push(field)
})
fields.push({
type: 'submit',
label: this.submit.text
})
return {
fields
}
}
},
watch: {
validity(newVal) {
this.$emit('validity', newVal)
}
},
methods: {
eachQuestion(eachFn) {
const model = this.model
const questions = this.questions
questions.forEach(question => {
let skip = false
// check on
let on = question.on
if (on) {
if (typeof on === 'string') {
on = {
model: on
}
}
const modelValue = model[on.model]
if (on.options) {
// 判断选项是否匹配
if (Array.isArray(modelValue)) {
// 交叉判断
let isInOptions = false
modelValue.some((val) => {
const inOptions = on.options.indexOf(val) >= 0
if (inOptions) {
isInOptions = true
return true
}
})
skip = !isInOptions
} else {
skip = on.options.indexOf(modelValue) < 0
}
} else {
skip = !modelValue
}
}
if (!skip) {
eachFn(question)
}
})
},
submitHandler(e) {
e.preventDefault()
// 校验通过,提交事件
this.$emit('submit', this.getModel())
},
getModel() {
const allModel = this.model
const model = {}
this.eachQuestion(question => {
const k = question.model
model[k] = this.processModel(question, allModel[k])
})
return model
},
processModel(question, value) {
const model = question.model
const casesMap = {
city(cityVal) {
// ["140000", "140100", "140107"]
return cityVal
},
upload(uploadVal) {
return uploadVal
}
}
return casesMap[model] ? casesMap[model](value) : value
},
validateHandler(result) {
this.validity = result.validity
this.valid = result.valid
}
}
}
</script>
<style lang="stylus">
.demo-questionnaire
.cube-form
counter-reset: cube-form-index
.cube-form-item
padding: 20px
.cube-form-item
counter-increment: cube-form-index
.cube-form-label
&::before
order: -2
margin-left: -14px
margin-right: 4px
&::after
content: counter(cube-form-index) '.'
display: block
order: -1
white-space: nowrap
margin-right: 6px
.cube-radio-group[data-col="true"]
.cube-checkbox-group[data-col="true"]
&::before
&::after
display: none
.cube-radio
.cube-checkbox
padding-left 0
padding-right 0
.cube-radio-wrap
.cube-checkbox-wrap
justify-content: initial
.demo-questionnaire-tip
position: relative
padding: 10px 15px 10px 30px
font-size: 14px
line-height: 1.5
color: #999
background: #f7f7f7
&::before
position: absolute
margin-left: -15px
transform: scale(1.1)
</style>
import { provinceList, cityList, areaList } from 'example/data/area'
const fieldRulesMap = {}
export function setRules(field, model, rules) {
let globalRules = fieldRulesMap[model]
if (!globalRules) {
globalRules = fieldRulesMap[model] = {}
}
if (!field.rules) {
field.rules = globalRules
}
Object.assign(field.rules, rules)
}
export function setMessages(field, messages) {
if (!field.messages) {
field.messages = {}
}
Object.assign(field.messages, messages)
}
const addressData = provinceList
addressData.forEach(province => {
province.children = cityList[province.value]
province.children.forEach(city => {
city.children = areaList[city.value]
})
})
export function loadCityData() {
// fake request
return new Promise(resolve => {
setTimeout(() => {
resolve(addressData)
}, 500)
})
}
import { setRules, setMessages } from './_helper'
const createRulesAndMessages = (rule, message) => {
return {
rules: {
custom: rule
},
messages: {
custom: message
}
}
}
const customMap = {
tel(config) {
const rule = function (val) {
return typeof val === 'string' && /^1[0-9]{10}$/.test(val)
}
const message = config.errMsg || '请输入正确的手机号码'
return createRulesAndMessages(rule, message)
},
upload(config) {
const rule = function (files) {
if (files.length) {
return Promise.all(files.map((file) => {
return new Promise((resolve, reject) => {
// check 状态
const check = () => {
if (file.status === 'error') {
reject(file)
} else if (file.status === 'success') {
resolve(file)
} else {
// waiting
setTimeout(check, 100)
}
}
check()
})
})).then(() => {
return true
})
} else {
return true
}
}
const message = config.errMsg || '上传失败'
return createRulesAndMessages(rule, message)
}
}
export default function transformCustom(config, field) {
const custom = customMap[config.type]
if (custom) {
const { rules, messages } = custom(config)
setRules(field, config.model, rules)
setMessages(field, messages)
}
}
import { setMessages } from './_helper'
export default function transformErrMsg(config, field) {
const isAgreement = config.type === 'agreement'
if (!field.messages) {
field.messages = {}
}
setMessages(field, {
required: config.errMsg || (isAgreement ? '请同意' : '此题为必做题')
})
}
import transformType from './type'
import transformModel from './model'
import transformTitle from './title'
import transformOptions from './options'
import transformRequired from './required'
import transformErrMsg from './err'
import transformCustom from './custom'
export default function transform(config) {
const field = {}
const transforms = [
transformType,
transformModel,
transformTitle,
transformOptions,
transformRequired,
transformErrMsg,
transformCustom
]
transforms.forEach((transformFn) => {
transformFn(config, field)
})
return field
}
export default function transformModel(config, field) {
field.modelKey = config.model
field.key = config.model
}
import { formatDate } from '../../../../src/common/lang/date'
import { loadCityData } from './_helper'
const resetCases = {
switch: {
hollowStyle: true
},
radio: {
hollowStyle: true
},
checkbox: {
hollowStyle: true
}
}
const optionsCases = {
switch(options) {
// 是 否
return {
colNum: 2,
options: [
{
label: '',
value: 1
},
{
label: '',
value: 0
}
]
}
},
select(options, loadData) {
const placeholder = options.placeholder || '请选择'
const propOptions = {
component: options.component || 'picker',
options: options.props || {
title: placeholder,
data: [options.options.map((option) => {
return {
text: option,
value: option
}
})]
}
}
return {
placeholder,
icon: options.icon || 'cubeic-select',
loadData,
options: propOptions,
formatValue: options.formatValue || function (vals, _, texts) {
return {
value: vals[0],
text: texts[0]
}
}
}
},
date(options) {
const props = {
value: new Date(),
title: '选择日期'
}
if (options.min) {
props.min = parseStringDate(options.min)
} else {
props.min = [2010, 1, 1]
}
if (options.max) {
props.max = parseStringDate(options.max)
} else {
props.max = [2100, 12, 31]
}
return optionsCases.select({
placeholder: options.placeholder,
icon: 'cubeic-calendar',
component: 'date-picker',
props,
formatValue(date) {
return formatDate(date, options.format || 'YYYY-MM-DD')
}
})
},
time(options) {
const props = {
value: new Date(),
title: '选择时间',
startColumn: 'hour',
columnCount: 2
}
if (options.min) {
props.min = parseStringDate(options.min, ':')
} else {
props.min = [0, 0]
}
if (options.max) {
props.max = parseStringDate(options.max, ':')
} else {
props.max = [23, 59]
}
return optionsCases.select({
placeholder: options.placeholder,
icon: 'cubeic-time',
component: 'date-picker',
props,
formatValue(date) {
return formatDate(date, options.format || 'hh:mm')
}
})
},
radio(options) {
return options
},
checkbox(options) {
return options
},
tel(options) {
return {
type: 'tel',
...options
}
},
city(options) {
const props = {}
const cityPromise = loadCityData()
const loadData = () => {
return cityPromise.then(cityList => {
props.data = cityList
return cityList
})
}
return optionsCases.select({
placeholder: options.placeholder,
icon: 'cubeic-location',
component: 'cascade-picker',
props,
formatValue(selectedVal, _, selectedText) {
return {
text: selectedText.join(''),
value: selectedVal
}
}
}, loadData)
},
upload(options) {
if (!options.max) {
options.max = 1
}
return {
...options
}
}
}
export default function transformOptions(config, field) {
if (!field.props) {
field.props = {}
}
Object.assign(field.props, resetCases[config.type] || {})
if (config.row) {
field.props.colNum = 2
}
let options = config.options || {}
if (Array.isArray(options)) {
options = {
options: options
}
}
options = Object.assign({}, options)
const caseFn = optionsCases[config.type]
Object.assign(field.props, (caseFn && caseFn(options)) || options)
}
function parseStringDate(val, split = '-') {
return val.split(split).map(n => +n)
}
import { setRules } from './_helper'
export default function transformRequired(config, field) {
let required = config.required
const isAgreement = config.type === 'agreement'
if (isAgreement) {
required = true
}
if (required) {
const rules = {}
if (isAgreement) {
rules.required = function (val) {
return val
}
} else {
rules.required = required
}
setRules(field, config.model, rules)
}
}
const appendTitleMap = {
radio: '(单选)',
checkbox: '(多选)'
}
export default function transformTitle(config, field) {
const appendTitle = appendTitleMap[config.type] || ''
field.label = config.title ? config.title + appendTitle : ''
}
import Select from '../../select/select.vue'
import Agreement from '../components/agreement.vue'
const componentMap = {
switch: 'radio-group', // 是 否
date: Select,
time: Select,
select: Select,
city: Select,
radio: 'radio-group',
checkbox: 'checkbox-group',
tel: 'input',
agreement: Agreement
}
export default function transformType(config, field) {
const realComponent = componentMap[config.type] || config.type
field[typeof realComponent === 'string' ? 'type' : 'component'] = realComponent
}
<template>
<div class="demo-select" @click="clickHandler">
<cube-input ref="input" v-model="modelText" v-bind="$attrs">
<i :class="icon" slot="append" v-if="icon"></i>
</cube-input>
</div>
</template>
<script>
import { camelize } from '../../../src/common/lang/string'
export default {
props: {
// v-model 值
value: null,
// 图标
icon: String,
// 配置项
options: Object,
// 加载数据函数
// default: (options) => Promise
loadData: Function,
// 控制行为函数,不建议自定义使用
action: Function,
// 格式化 value 值,用于格式化从配置组件的 select 事件中格式化结果
formatValue: Function
},
data() {
const formatedValue = this._formatValue()
return {
model: formatedValue,
modelText: formatedValue.text,
isActive: false
}
},
computed: {
computedValue() {
return this._formatValue()
}
},
watch: {
isActive(newV) {
this.$refs.input.isFocus = newV
},
model(newV) {
this.modelText = newV.text
this.modelMap[newV.value] = newV.text
// value 发生了变化
// @arg value select结果
this.$emit('input', newV.value)
}
},
beforeCreate() {
this.modelMap = {}
},
methods: {
_formatValue(value = this.value) {
if (value === Object(value)) {
return value
}
return {
text: this.modelMap[value] || value,
value: value
}
},
defaultAction() {
const { component, options } = this.options
return this[camelize(`$create-${component}`)](options || {})
},
showComponent() {
if (!this.actionComponent) {
const component = this.actionComponent = this.action ? this.action(this) : this.defaultAction(this)
component.$on('select', this.onSelect)
component.$on('hide', this.onHide)
component.$on('cancel', this.onHide)
component.$on('close', this.onHide)
}
this.actionComponent.show()
this.isActive = true
},
clickHandler() {
if (this.loadData) {
this.loadData(this.options).then(() => {
this.showComponent()
})
} else {
this.showComponent()
}
},
onSelect(...args) {
this.isActive = false
this.model = this._formatValue(this.formatValue ? this.formatValue(...args) : {
text: args[0],
value: args[2]
})
},
onHide() {
this.isActive = false
}
}
}
</script>
<style lang="stylus">
.demo-select
.cube-input
pointer-events: none
.cube-input-append
padding: 10px
</style>
......@@ -6,6 +6,7 @@
<cube-button @click="goTo('custom')">Custom</cube-button>
<cube-button @click="goTo('classic')">Classic style</cube-button>
<cube-button @click="goTo('fresh')">Fresh style</cube-button>
<cube-button @click="goTo('questionnaire')">Questionnaire</cube-button>
</cube-button-group>
<cube-view></cube-view>
</div>
......
<template>
<cube-page class="page-questionnaire" title="Questionnaire">
<demo-questionnaire
slot="content"
:tip="tip"
:questions="questions"
:submit="submit"
@submit="submitHandler"
/>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../../components/cube-page.vue'
import DemoQuestionnaire from '../../components/questionnaire/questionnaire.vue'
export default {
data() {
return {
tip: '请配合如实填写问卷,确保xxxx相关文案',
questions: [
{
type: 'switch',
model: 'switch',
title: '询问是否?'
// required: true
},
{
type: 'input',
model: 'input',
title: '输入',
options: {
placeholder: '请输入'
},
on: 'switch',
required: true
},
{
type: 'date',
model: 'date',
title: '日期',
options: {
// min: '2020-01-01',
// max: '2020-02-18'
},
required: true
},
{
type: 'time',
model: 'time',
title: '时间',
options: {
min: '01:00',
max: '23:59'
},
required: true
},
{
type: 'select',
model: 'select',
title: '选择',
options: [
'option1',
'option2',
'option3'
],
required: true
},
{
type: 'radio',
model: 'radio',
title: '单选',
options: [
'单选1',
'单选2',
'单选3'
],
required: true
},
{
type: 'checkbox',
model: 'checkbox',
title: '多选',
options: [
'多选1',
'多选2',
'多选3'
],
required: true
},
{
type: 'textarea',
model: 'textarea',
title: '多行文本',
on: {
model: 'checkbox',
options: ['多选1', '多选3']
},
required: true
},
{
type: 'checkbox',
row: true,
model: 'checkbox2',
title: '多选-横',
options: [
'多选-横1',
'多选-横2',
'多选-横3'
],
required: true
},
{
type: 'tel',
model: 'tel',
title: '手机号',
options: {
placeholder: '请输入手机号'
},
required: true
},
{
type: 'rate',
model: 'rate',
title: '级别',
options: {
max: 10
},
required: true
},
{
type: 'city',
model: 'city',
title: '城市',
required: true
},
{
type: 'upload',
model: 'upload',
title: '上传',
options: {
action: '//jsonplaceholder.typicode.com/photos/',
max: 2
},
required: true
},
{
type: 'agreement',
model: 'agreement',
options: {
text: '请同意',
link: {
text: '《xx协议》',
href: 'https://github.com/didi/cube-ui'
},
desc: '说明:本人承诺xx xxxxx xxx xx。'
},
required: true,
errMsg: '请同意协议'
}
],
submit: {
text: 'Submit'
}
}
},
components: {
CubePage,
DemoQuestionnaire
},
methods: {
submitHandler(model) {
console.log('submit', model)
}
}
}
</script>
<style lang="stylus">
.cube-page
&.page-questionnaire
.wrapper
.content
margin: 0
</style>
......@@ -15,6 +15,7 @@ import FormDefault from '../pages/form/default.vue'
import FormCustom from '../pages/form/custom.vue'
import FormClassic from '../pages/form/classic.vue'
import FormFresh from '../pages/form/fresh.vue'
import Questionnaire from '../pages/form/questionnaire.vue'
import Picker from '../pages/picker.vue'
import CascadePicker from '../pages/cascade-picker.vue'
import SegmentPicker from '../pages/segment-picker.vue'
......@@ -125,6 +126,10 @@ const routes = [
{
path: 'fresh',
component: FormFresh
},
{
path: 'questionnaire',
component: Questionnaire
}
]
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册