Files
fastapi-admin/front/src/components/FormField.vue
long2ice f574ca313b support datetime picker
readonly support
custom fields_type
2020-04-10 14:03:15 +08:00

643 lines
19 KiB
Vue

<template>
<div>
<div class="languages mb-1" v-if="isIntl">
<span
class="badge mr-1 mb-0 pointer"
:class="`badge-${currentLanguage === key ? 'primary' : 'secondary'}`"
v-for="(lang, key) in languages"
:key="key"
@click="changeLanguage(key, name)"
>{{lang}}</span>
</div>
<b-form-select
v-if="['select'].includes(field.type)"
:formatter="getFormatter(field, value)"
:id="id"
:options="options"
v-bind="field"
v-model="model"
:name="name"
/>
<div v-else-if="['select2'].includes(field.type)">
<b-select v-model="model" v-bind="field" :parent="parent"/>
</div>
<b-tree-select
:normalizer="treeSelectNormalizer"
value-consists-of="LEAF_PRIORITY"
v-else-if="['tree', 'treeselect'].includes(field.type)"
v-bind="field"
v-model="model"
/>
<!-- <b-select v-if="['select', 'select2'].includes(field.type)" track-by="value" label="text" @input="model = arguments[0]" :id="id" v-bind="field" :title="value" /> -->
<date-picker
v-else-if="['date', 'datetime'].includes(field.type)"
:name="name"
v-bind="field"
v-model="model"
:type="field.type"
format="YYYY-MM-DDTHH:mm:ss"
value-type="format"
/>
<b-form-radio-group
v-else-if="['radiolist'].includes(field.type)"
:name="name"
v-bind="field"
v-model="model"
>
<!-- <b-form-radio :key="choice.value" :value="choice.value" v-for="choice in field.options">{{choice.text}}</b-form-radio> -->
</b-form-radio-group>
<b-form-checkbox-group
:name="name"
v-else-if="['checkboxlist'].includes(field.type)"
v-bind="field"
v-model="model"
>
<!-- <b-form-checkbox :key="choice.value" :value="choice.value" v-for="choice in field.options">{{choice.text}}</b-form-checkbox> -->
</b-form-checkbox-group>
<div v-else-if="['checkboxtable'].includes(field.type)" class="checkboxtable">
<div v-for="(options, group) in groupedOptions" :key="group" class="mt-1">
<b-form-checkbox-group :name="name" :options="options" v-bind="field" v-model="model"/>
</div>
</div>
<b-form-textarea
:name="name"
v-else-if="['textarea'].includes(field.type)"
:id="id"
v-model="model"
v-bind="field"
:rows="field.rows || 3"
/>
<div v-else-if="['file-picker'].includes(field.type)">
<b-btn v-b-modal.file-manager>选择文件</b-btn>
</div>
<!-- <b-uploader v-else-if="['image', 'file', 'audio'].includes(field.type)" :id="id" v-model="model" v-bind="field" /> -->
<component
:is="field.autoUpload === false ? 'b-form-file' : 'b-form-uploader'"
v-else-if="['image', 'file', 'audio', 'video'].includes(field.type)"
:field="field"
v-model="model"
:id="id"
:name="name"
:parent="parent"
/>
<div v-else-if="['switch', 'checkbox'].includes(field.type)">
<b-form-checkbox
variant="success"
v-bind="field"
size="lg"
pill
type="3d"
:id="id"
v-model="model"
/>
</div>
<!-- <b-ueditor :state="state" v-else-if="['wysiwyg', 'html'].includes(field.type)" :id="id" v-bind="field" v-model="model" /> -->
<div v-else-if="['wysiwyg', 'html'].includes(field.type)">
<!-- <b-btn :id="`cropper_${id}`" variant="success">图片裁剪助手</b-btn> -->
<avatar-cropper
v-if="field.cropper"
:trigger="`#cropper_${id}`"
v-bind="cropperOptions"
ref="cropper"
@uploaded="cropperUploaded"
@completed="cropperUploadComplete"
/>
<b-html-editor
ref="editor"
:state="state"
:id="id"
v-bind="field"
v-model="model"
:content="model"
@open-cropper="$refs.cropper && $refs.cropper.pickImage()"
@change="htmlEditorInput"
@keyup.native.enter="wrapFirstLine"
/>
<!-- <b-cropper v-if="field.showCropper" ref="cropper" ></b-cropper> -->
</div>
<div v-else-if="['json'].includes(field.type)">
<b-json-editor
v-if="model"
:value="model"
@input="model = arguments[0]"
:options="{modes:['tree','view','form','code','text','preview']}" ref="editor"/>
</div>
<div v-else-if="field.fields">
<div v-if="['array'].includes(field.type) || field.is_array || parent.is_array">
<b-table
hover
bordered
:items="model"
:fields="myFields"
v-if="field.is_table || parent.is_table"
>
<template v-for="(child, k) in myFields" :slot="k" slot-scope="row">
<b-form-field
:parent="parent"
v-model="model[row.index][k]"
:field="child"
:name="`input_${row.index}_${k}`"
:key="`input_${row.index}_${k}`"
:id="`input_${row.index}_${k}`"
/>
</template>
<template slot="HEAD__actions">
<b-btn size="sm" @click="addRow">
<i class="icon-plus"></i>
{{$t('actions.add')}}
</b-btn>
</template>
<template slot="_actions" slot-scope="row">
<b-btn size="sm" @click="model.splice(row.index + 1, 0, {})">
<i class="icon-plus"></i>
{{$t('actions.add')}}
</b-btn>
<b-btn size="sm" @click="model.splice(row.index, 1)">
<i class="icon-trash"></i>
{{$t('actions.delete')}}
</b-btn>
</template>
</b-table>
<b-draggable v-model="model" v-else>
<transition-group tag="div" class="row">
<b-col
v-for="(item, i) in model"
:key="`draggable-${name}-${i}`"
cols
:lg="field.item_cols || 6"
>
<b-card class="mb-4">
<b-row slot="header" class="justify-content-between">
<b-col>No. {{i + 1}}</b-col>
<b-col right class="text-right">
<b-btn size="sm" @click="model.splice(i, 1)">
<i class="icon-trash"></i>
{{$t('actions.delete')}}
</b-btn>
</b-col>
</b-row>
<b-form-group
v-for="(child, key) in myFields"
:key="key"
v-bind="child"
:label-for="`input_${name}_${i}_${key}`"
>
<b-form-field
v-model="model[i][key]"
:parent="parent"
:name="`${name}.${i}.${key}`"
:field="child"
:id="`input_${name}_${i}_${key}`"
/>
</b-form-group>
</b-card>
</b-col>
<b-col
cols
:lg="field.item_cols || 6"
:key="-1"
class="d-flex align-items-center justify-content-center"
>
<b-btn
size="lg"
class="p-5"
block
@click="model = !model? [] : model; model.push({})"
>
<i class="fa fa-plus"/>
</b-btn>
</b-col>
</transition-group>
</b-draggable>
</div>
<div v-else-if="['object'].includes(field.type)">
<b-card>
<b-form-builder
:sub-form="name || ''"
v-model="model"
:languages="languages"
:fields="myFields"
:parent="parent"
ref="subForm"
/>
<!-- <b-form-group v-for="(child, key) in myFields" :key="key" v-bind="child" :label-for="`input_${name}_${key}`">
<b-form-field :value="_.get(model, `${key}`)"
@input="model[key] = arguments[0]"
:parent="parent" :name="`${name}_${key}`" :field="child" :id="`input_${name}_${key}`" />
</b-form-group>-->
</b-card>
</div>
<div v-else>
<b-form-group
v-for="(child, key) in myFields"
:key="key"
v-bind="child"
:label-for="`input_${name}_${key}`"
>
<b-form-field
v-model="model[key]"
:parent="parent"
:name="key"
:field="child"
:id="`input_${name}_${key}`"
/>
</b-form-group>
</div>
</div>
<b-input-group v-else>
<b-input-group-prepend is-text v-if="field.prependIcon || field.prepend">
<i :class="field.prependIcon" v-if="field.prependIcon"></i>
<span v-else v-html="field.prepend"></span>
</b-input-group-prepend>
<b-form-input
:state="state"
:id="id"
:name="name"
v-bind="field"
v-model="model"
:formatter="getFormatter(field, value)"
/>
<b-input-group-append is-text v-if="field.appendIcon || field.append">
<i :class="field.appendIcon" v-if="field.appendIcon"></i>
<span v-else v-html="field.append"></span>
</b-input-group-append>
</b-input-group>
</div>
</template>
<style>
.checkboxtable .btn-group > .btn:first-child {
text-align: center;
width: 10em;
margin-right: 2px;
}
.vue-html5-editor .content {
max-height: 500px;
}
</style>
<script>
import Vue from "vue";
import BDraggable from "vuedraggable";
import BTreeSelect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.min.css";
// import BSelect from "vue-multiselect"
import BSelect from "./FormSelect2";
// import BSelect from "@alfsnd/vue-bootstrap-select";
// import "vue-multiselect/dist/vue-multiselect.min.css"
import DatePicker from "vue2-datepicker";
import 'vue2-datepicker/index.css';
import 'vue2-datepicker/locale/zh-cn';
// import BUeditor from "./UEditor"
import BFormUploader from "./FormUploader";
import VueHtml5Editor from "vue-html5-editor";
// import BJsonEditor from "./JsonEditor"
// import BJsonEditor from "vue-jsoneditor"
import _ from "lodash";
// import "jsoneditor/dist/jsoneditor.min.css"
// Vue.use(BJsonEditor);
export default {
components: {
// BUeditor,
DatePicker,
BSelect,
BFormUploader,
// BJsonEditor,
BDraggable,
BTreeSelect
},
props: {
languages: {},
id: {
required: true
},
parent: {},
value: {},
field: {},
state: {},
name: {}
},
computed: {
cropperOptions() {
return {
"upload-url": global.API_URI + "upload",
"upload-headers": {
Authorization: "Bearer " + this.$store.state.auth.token
},
"upload-form-name": "file",
"upload-form-data": {
from: "cropper"
},
"cropper-options": {
viewMode: 2,
aspectRatio: _.get(this.field, "cropper.ratio", 1)
},
"output-options": this.field.cropper,
labels: this.field.labels || {submit: "提交", cancel: "取消"}
};
},
isSelect() {
return ["select", "select2"].includes(this.field.type);
},
isSelect2() {
return ["select2"].includes(this.field.type);
},
groupedOptions() {
return _.groupBy(this.options, "group");
},
myFields() {
let fields = this.field.fields;
if (typeof fields == "string") {
const rel = this.parent[fields];
if (!rel) {
return {};
}
try {
fields = JSON.parse(rel);
} catch (e) {
fields = {};
}
}
if (this.parent.is_table) {
fields._actions = {label: this.$t("actions.actions")};
}
return fields;
},
description() {
if (this.field.limit) {
const {width, height} = this.field.limit;
if (width && height) {
return `尺寸:${width}x${height}`;
}
return;
}
return this.field.description;
},
filteredValue() {
let defaultValue = this.value;
if (!this.defaultValue) {
if (["object", "json"].includes(this.field.type)) {
defaultValue = {};
}
if (["array"].includes(this.field.type) || this.field.multiple) {
defaultValue = [];
}
}
// console.log(defaultValue);
return defaultValue;
},
isArrayValue() {
return (
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table
);
},
isIntl() {
return this.field.intl || this.field.multilingual;
},
selectedValue1() {
let value = this.initSelectedValue
if (this.isArrayValue) {
value = _.filter(
this.options,
v => this.value && this.value.includes(v.value)
);
} else {
value = _.find(this.options, v => this.value == v.value);
}
return value;
},
model: {
get() {
const isArray =
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table;
const isObject = this.field.type == "object";
let ret = this.value;
if (!this.value) {
if (isArray) {
ret = [];
} else if (isObject) {
ret = {};
}
}
if (this.isIntl) {
// console.log(this.name, ret, this.currentLanguage);
return _.get(ret, this.currentLanguage, "");
}
return ret;
},
set(value) {
this.$emit("input", value, this.currentLanguage);
}
}
},
data() {
const isArray =
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table;
return {
currentLanguage: this.field.currentLanguage || "en",
options: this.field.options || [],
initSelectedValue: isArray && !this.value ? [] : this.value,
selectedValue: isArray && !this.value ? [] : this.value,
};
},
methods: {
addRow() {
if (!this.parent[this.name]) {
this.$set(this.parent, this.name, []);
}
this.$nextTick(() => {
this.model.push({});
});
},
initEditor() {
const language = "zh-cn";
window.document.execCommand("defaultParagraphSeparator", false, "p");
const visibleModules = this.field.modules || [
"text",
// "color",
// "font",
"heading",
"align",
"list",
"link",
"unlink",
"tabulation",
"image",
// this.field.cropper ? "cropper" : "image",
"hr",
"eraser",
"undo",
"full-screen",
"cropper"
// "info",
];
const cropperClass = _.get(
this.field,
"cropper.icon",
"fa fa-crop text-danger"
);
Vue.use(VueHtml5Editor, {
name: "b-html-editor",
language,
showModuleName: false,
modules: [
{
name: "cropper",
icon: cropperClass,
i18n: "cropper",
show: true,
handler: function (editor) {
editor.$emit("open-cropper");
}
},
{
name: "heading",
icon: "fa fa-header",
i18n: "heading",
show: true,
dashboard: {
template: `
<div>
<button v-for="h in 6" type="button" @click="setHeading(h)">H{{h}}</button>
</div>
`,
methods: {
setHeading(heading) {
this.$parent.execCommand("formatBlock", `h${heading}`);
}
}
}
}
],
i18n: {
"zh-cn": {
cropper: "图片裁剪",
heading: "标题"
}
},
image: {
sizeLimit: 10 * 1024 * 1024,
upload: {
url: global.API_URI + "upload",
headers: {
Authorization: "Bearer " + this.$store.state.auth.token
},
fieldName: "file"
},
// compress: {
// width: 1600,
// height: 1600,
// quality: 80
// },
uploadHandler(res) {
let data;
try {
data = JSON.parse(res);
} catch (e) {
this.$notify.error("上传失败");
}
return data.url;
}
},
visibleModules: visibleModules
});
},
cropperUploaded(res) {
this.$refs.editor.execCommand("insertHTML", `<img src="${res.url}" />`);
},
cropperUploadComplete(data) {
if (data.message) {
this.$snotify.error(data.message);
}
},
changeLanguage(lang) {
this.currentLanguage = lang;
// this.$emit('change-language', lang, name)
// global.console.log(lang, name)
},
htmlEditorInput(value) {
this.$emit("input", value, this.currentLanguage);
},
wrapFirstLine() {
// const value = String(el.target.innerHTML).replace(/^\s*(.+?)(<?)/i, '<p> $1 </p>$2')
// this.$emit('input', value)
},
treeSelectNormalizer(row) {
return {
id: row.value,
label: row.text
};
},
getFormatter(field) {
if (field.format) {
return eval(field.format);
}
return v => v;
},
fetchAjaxOptions(query = {}) {
const params = this.field.ajaxOptions;
const {url, resource, where = {}, text, depends} = params;
params.where = Object.assign({}, where, query);
if (this.q) {
params.q = this.q;
}
const apiUrl = url
? _.template(url)({item: this.parent})
: resource + "/options";
this.$http.get(apiUrl, {params}).then(({data}) => {
this.options = data;
});
}
},
mounted() {
if (this.field.type == "html") {
// window.onscroll = () => {
// const editor = this.$refs.editor.$el
// const offsetTop = editor.getClientRects()[0].top
// // const scrollTop = document.documentElement.scrollTop
// if (offsetTop <= 0) {
// editor.classList.toggle()
// }
// // global.console.log(scrollTop, offsetTop)
// }
}
},
created() {
if (this.field.type == "html") {
this.initEditor();
}
if (this.field.ajaxOptions && this.field.ajaxOptions.search !== true) {
this.fetchAjaxOptions();
}
if (this.isSelect2) {
// this.initOptionsForSelect2();
// this.$watch("options", () => {
// });
}
}
};
</script>