first commit

This commit is contained in:
2025-02-20 15:04:05 +08:00
commit 1793865396
2243 changed files with 357784 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: '主页' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
+193
View File
@@ -0,0 +1,193 @@
<template>
<div>
<el-form ref="editPram" :model="editPram" label-width="130px">
<el-form-item
label="分类名称"
prop="name"
:rules="[{ required:true,message:'请输入分类名称',trigger:['blur','change'] }]"
>
<el-input v-model="editPram.name"  :maxlength="biztype.value === 1 ? 8 : 20" placeholder="分类名称" />
</el-form-item>
<el-form-item label="URL" v-if="biztype.value!==1 && biztype.value!==3">
<el-input v-model="editPram.url" placeholder="URL" />
</el-form-item>
<el-form-item label="父级" v-if="biztype.value!==3">
<el-cascader v-model="editPram.pid" :disabled="isCreate ===1 && editPram.pid ===0" :options="biztype.value === 5 ? allTreeList : parentOptions" :props="categoryProps" style="width:100%" />
</el-form-item>
<el-form-item label="菜单图标" v-if="biztype.value===5">
<el-input placeholder="请选择菜单图标" v-model="editPram.extra">
<el-button slot="append" icon="el-icon-circle-plus-outline" @click="addIcon"></el-button>
</el-input>
</el-form-item>
<el-form-item label="分类图标(180*180)" v-if="biztype.value === 1 || biztype.value === 3">
<div class="upLoadPicBox" @click="modalPicTap('1')">
<div v-if="editPram.extra" class="pictrue">
<img :src="editPram.extra">
</div>
<div v-else class="upLoad">
<i class="el-icon-camera cameraIconfont" />
</div>
</div>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="editPram.sort" :min="0"/>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="editPram.status" active-text="显示"
inactive-text="隐藏" :active-value="true" :inactive-value="false" />
</el-form-item>
<el-form-item label="扩展字段" v-if="biztype.value !== 1 && biztype.value !== 3 && biztype.value !== 5">
<el-input v-model="editPram.extra" type="textarea" placeholder="扩展字段" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loadingBtn" @click="handlerSubmit('editPram')" v-hasPermi="['admin:category:update']">确定</el-button>
<el-button @click="close">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<!--创建和编辑公用一个组件-->
<script>
import * as categoryApi from '@/api/categoryApi.js'
import * as selfUtil from '@/utils/ZBKJIutil.js'
export default {
// name: "edit"
props: {
prent: {
type: Object,
required: true
},
isCreate: {
type: Number,
default: 0
},
editData: {
type: Object
},
biztype: {
type: Object,
required: true
},
allTreeList: {
type: Array
}
},
data() {
return {
loadingBtn: false,
constants: this.$constants,
editPram: {
extra: null,
name: null,
pid: null,
sort: 0,
status: true,
type: this.biztype.value,
url: null,
id: 0
},
categoryProps: {
value: 'id',
label: 'name',
children: 'child',
expandTrigger: 'hover',
checkStrictly: true,
emitPath: false
},
parentOptions: []
}
},
mounted() {
this.initEditData()
},
methods: {
// 点击图标
addIcon() {
const _this = this
_this.$modalIcon(function(icon) {
_this.editPram.extra = icon
})
},
// 点击商品图
modalPicTap (tit, num, i) {
const _this = this
const attr = []
this.$modalUpload(function(img) {
if(tit==='1'&& !num){
_this.editPram.extra = img[0].sattDir
}
if(tit==='2'&& !num){
img.map((item) => {
attr.push(item.attachment_src)
_this.formValidate.slider_image.push(item)
});
}
},tit, 'store')
},
close() {
this.$emit('hideEditDialog')
},
initEditData() {
this.parentOptions = [...this.allTreeList]
this.addTreeListLabelForCasCard(this.parentOptions, 'child')
const { extra, name, pid, sort, status, type, id, url } = this.editData
if(this.isCreate === 1){
this.editPram.extra = extra
this.editPram.name = name
this.editPram.pid = pid
this.editPram.sort = sort
this.editPram.status = status
this.editPram.type = type
this.editPram.url = url
this.editPram.id = id
}else{
this.editPram.pid = this.prent.id
this.editPram.type = this.biztype.value
}
},
addTreeListLabelForCasCard(arr, child) {
arr.forEach((o,i) => {
if (o.child && o.child.length) {
// o.disabled = true
o.child.forEach((j) => {
j.disabled = true
})
}
})
},
handlerSubmit(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) return
this.handlerSaveOrUpdate(this.isCreate === 0)
})
},
handlerSaveOrUpdate(isSave) {
if (isSave) {
// this.editPram.pid = this.prent.id
this.loadingBtn = true
categoryApi.addCategroy(this.editPram).then(data => {
this.$emit('hideEditDialog')
this.$message.success('创建目录成功')
this.loadingBtn = false
}).catch(() => {
this.loadingBtn = false
})
} else {
this.editPram.pid = Array.isArray(this.editPram.pid) ? this.editPram.pid[0] : this.editPram.pid
this.loadingBtn = true
categoryApi.updateCategroy(this.editPram).then(data => {
this.$emit('hideEditDialog')
this.$message.success('更新目录成功')
this.loadingBtn = false
}).catch(() => {
this.loadingBtn = false
})
}
}
}
}
</script>
<style scoped>
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div>
<el-tree :data="ddd" :props="defaultProps" @node-click="handleNodeClick" />
</div>
</template>
<script>
import * as categoryApi from '@/api/categoryApi.js'
export default {
// name: "info"
props: {
id: {
type: Number,
required: true
}
},
data() {
return {
defaultProps: {
children: 'children',
label: 'label'
},
ddd: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1'
}]
}]
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1'
}]
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1'
}]
}]
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1'
}]
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1'
}]
}]
}],
dataList: {// 数据结果
page: 0,
limit: 0,
totalPage: 0,
total: 0,
list: []
}
}
},
mounted() {
this.handlerGetTreeList(this.id)
},
methods: {
handlerGetTreeList(id) {
if (!id) {
this.$message.error('当前数据id不正确')
return
}
categoryApi.treeCategroy({ pid: id }).then(data => {
this.dataList = data
})
},
handleNodeClick(data) {
console.log('data:', data)
}
}
}
</script>
<style scoped>
</style>
+329
View File
@@ -0,0 +1,329 @@
<template>
<div>
<template v-if="selectModel">
<el-tree
ref="tree"
:data="treeList"
show-checkbox
node-key="id"
@check="getCurrentNode"
:default-checked-keys="selectModelKeysNew"
:props="treeProps">
</el-tree>
</template>
<template v-else>
<div class="divBox">
<el-card class="box-card">
<div slot="header" class="clearfix">
<div class="container">
<el-form inline size="small">
<el-form-item label="状态:">
<el-select v-model="listPram.status" placeholder="状态" class="selWidth" @change="handlerGetList">
<el-option label="全部" :value="-1"></el-option>
<el-option label="显示" :value="1"></el-option>
<el-option label="不显示" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="名称:">
<el-input v-model="listPram.name" placeholder="请输入名称" class="selWidth" size="small" clearable>
<el-button slot="append" icon="el-icon-search" @click="handlerGetList" size="small"/>
</el-input>
</el-form-item>
</el-form>
</div>
<el-button size="mini" type="primary" @click="handleAddMenu({id:0,name:'顶层目录'})" v-hasPermi="['admin:category:save']" >新增{{ biztype.name }}</el-button>
</div>
<el-table
ref="treeList"
:data="treeList"
size="mini"
class="table"
highlight-current-row
row-key="id"
:tree-props="{children: 'child', hasChildren: 'hasChildren'}"
>
<el-table-column prop="name" label="名称" min-width="240">
<template slot-scope="scope">
{{ scope.row.name }} | {{ scope.row.id }}
</template>
</el-table-column>
<template v-if="!selectModel">
<el-table-column label="类型" min-width="150">
<template slot-scope="scope">
<span>{{ scope.row.type | filterCategroyType | filterEmpty }}</span>
</template>
</el-table-column>
<el-table-column label="分类图标" min-width="80">
<template slot-scope="scope">
<div class="listPic" v-if=" biztype.value === 5 ">
<i :class="'el-icon-' + scope.row.extra" style="font-size: 20px;" />
</div>
<div class="demo-image__preview" v-else>
<el-image
style="width: 36px; height: 36px"
:src="scope.row.extra"
:preview-src-list="[scope.row.extra]"
v-if="scope.row.extra"
/>
<img style="width: 36px; height: 36px" v-else :src="defaultImg" alt="">
</div>
</template>
</el-table-column>
<el-table-column label="Url" min-width="250" v-if="biztype.value === 5" key="2">
<template slot-scope="scope">
<span>{{ scope.row.url }}</span>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="150" />
<el-table-column
label="状态"
min-width="150"
>
<!-- -->
<template slot-scope="scope" v-if="checkPermi(['admin:category:update:status'])">
<el-switch
v-model="scope.row.status"
:active-value="true"
:inactive-value="false"
active-text="显示"
inactive-text="隐藏"
@change="onchangeIsShow(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="200" fixed="right">
<template slot-scope="scope">
<el-button
v-if="(biztype.value === 1 && scope.row.pid === 0) || biztype.value === 5"
type="text"
size="small"
@click="handleAddMenu(scope.row)"
>添加子目录</el-button>
<el-button type="text" size="small" @click="handleEditMenu(scope.row)" v-hasPermi="['admin:category:info']">编辑</el-button>
<el-button type="text" size="small" @click="handleDelMenu(scope.row)" v-hasPermi="['admin:category:delete']">删除</el-button>
</template>
</el-table-column>
</template>
</el-table>
</el-card>
</div>
</template>
<el-dialog
:title="(editDialogConfig.isCreate===0?`创建${biztype.name}`:`编辑${biztype.name}`)"
:visible.sync="editDialogConfig.visible"
destroy-on-close
:close-on-click-modal="false"
>
<edit
v-if="editDialogConfig.visible"
:prent="editDialogConfig.prent"
:is-create="editDialogConfig.isCreate"
:edit-data="editDialogConfig.data"
:biztype="editDialogConfig.biztype"
:all-tree-list="treeList"
@hideEditDialog="hideEditDialog"
/>
</el-dialog>
</div>
</template>
<script>
import * as categoryApi from '@/api/categoryApi.js'
import info from './info'
import edit from './edit'
import * as selfUtil from '@/utils/ZBKJIutil.js'
import { checkPermi, checkRole } from "@/utils/permission";
export default {
// name: "list"
components: { info, edit },
props: {
biztype: { // 类型,1 产品分类,2 附件分类,3 文章分类, 4 设置分类, 5 菜单分类
type: Object,
default: { value: -1 },
validator: (obj) => {
return obj.value > 0
}
},
pid: {
type: Number,
default: 0,
validator: (value) => {
return value >= 0
}
},
selectModel: { // 是否选择模式
type: Boolean,
default: false
},
selectModelKeys: {
type: Array
},
rowSelect: {}
},
data() {
return {
selectModelKeysNew: this.selectModelKeys,
loading: false,
constants: this.$constants,
treeProps: {
label: 'name',
children: 'child',
// expandTrigger: 'hover',
// checkStrictly: false,
// emitPath: false
},
// treeCheckedKeys:[],// 选择模式下的属性结构默认选中
multipleSelection: [],
editDialogConfig: {
visible: false,
isCreate: 0, // 0=创建,1=编辑
prent: {}, // 父级对象
data: {},
biztype: this.biztype // 统一主业务中的目录类型
},
dataList: [],
treeList: [],
listPram: {
pid: this.pid,
type: this.biztype.value,
status: -1,
name: '',
page: this.$constants.page.page,
limit: this.$constants.page.limit[0]
},
viewInfoConfig: {
data: null,
visible: false
},
defaultImg:require('@/assets/imgs/moren.jpg')
}
},
mounted() {
/* if(this.biztype.value === 3){
this.listPram.pageSize = constants.page.pageSize[4]
this.handlerGetList()
}else{*/
this.handlerGetTreeList()
// }
},
methods: {
checkPermi, //权限控制
onchangeIsShow(row){
categoryApi.categroyUpdateStatus( row.id ).then(() => {
this.$message.success('修改成功')
this.handlerGetTreeList()
}).catch(()=>{
row.status = !row.status
})
},
handleEditMenu(rowData) {
this.editDialogConfig.isCreate = 1
this.editDialogConfig.data = rowData
this.editDialogConfig.prent = rowData
this.editDialogConfig.visible = true
},
handleAddMenu(rowData) {
this.editDialogConfig.isCreate = 0
this.editDialogConfig.prent = rowData
this.editDialogConfig.data = {}
this.editDialogConfig.biztype = this.biztype
this.editDialogConfig.visible = true
},
getCurrentNode(data) {
let node = this.$refs.tree.getNode(data);
this.childNodes(node);
// this.parentNodes(node);
//是否编辑的表示
// this.ruleForm.isEditorFlag = true;
//编辑时候使用
this.$emit('rulesSelect', this.$refs.tree.getCheckedKeys());
// this.selectModelKeys = this.$refs.tree.getCheckedKeys();
//无论编辑和新增点击了就传到后台这个值
// this.$emit('rulesSelect', this.$refs.tree.getCheckedKeys().concat(this.$refs.tree.getHalfCheckedKeys()));
// this.ruleForm.menuIdsisEditor = this.$refs.tree.getCheckedKeys().concat(this.$refs.tree.getHalfCheckedKeys());
},
//具体方法可以看element官网api
childNodes(node){
let len = node.childNodes.length;
for(let i = 0; i < len; i++){
node.childNodes[i].checked = node.checked;
this.childNodes(node.childNodes[i]);
}
},
parentNodes(node){
if(node.parent){
for(let key in node){
if(key == "parent"){
node[key].checked = true;
this.parentNodes(node[key]);
}
}
}
},
handleDelMenu(rowData) {
this.$confirm('确定删除当前数据?').then(() => {
categoryApi.deleteCategroy(rowData).then(data => {
this.handlerGetTreeList()
this.$message.success('删除成功')
})
})
},
handlerGetList() {
this.handlerGetTreeList();
// categoryApi.listCategroy({status:this.listPram.status, type: 1 }).then(data => {
// this.treeList = data.list
// })
},
handlerGetTreeList() {
// this.biztype.value === 5 && !this.selectModel) ? -1 : 1
// const _pram = { type: this.biztype.value, status: !this.selectModel ? -1 : (this.biztype.value === 5 ? -1 : 1) }
const _pram = { type: this.biztype.value, status: this.listPram.status, name: this.listPram.name }
this.loading = true
this.biztype.value!==3 ? categoryApi.treeCategroy(_pram).then(data => {
this.treeList = this.handleAddArrt(data)
this.loading = false
}).catch(()=>{
this.loading = false
}) : categoryApi.listCategroy({ type: 3, status: this.listPram.status, pid: this.listPram.pid, name: this.listPram.name}).then(data => {
this.treeList = data.list
})
},
handlerGetInfo(id) {
this.viewInfoConfig.data = id
this.viewInfoConfig.visible = true
},
handleNodeClick(data) {
console.log('data:', data)
},
handleAddArrt(treeData) {
const _result = selfUtil.addTreeListLabel(treeData)
return _result
},
hideEditDialog() {
setTimeout(() => {
this.editDialogConfig.prent = {}
this.editDialogConfig.type = 0
this.editDialogConfig.visible = false
this.handlerGetTreeList()
}, 200)
},
handleSelectionChange(d1, { checkedNodes, checkedKeys, halfCheckedNodes, halfCheckedKeys }) {
// this.multipleSelection = checkedKeys.concat(halfCheckedKeys)
this.multipleSelection = checkedKeys
this.$emit('rulesSelect', this.multipleSelection)
},
}
}
</script>
<style scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<div v-if="errorLogs.length>0">
<el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
<el-button style="padding: 8px 10px;" size="small" type="danger">
<svg-icon icon-class="bug" />
</el-button>
</el-badge>
<el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
<div slot="title">
<span style="padding-right: 10px;">Error Log</span>
<el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
</div>
<el-table :data="errorLogs" border>
<el-table-column label="Message">
<template slot-scope="{row}">
<div>
<span class="message-title">Msg:</span>
<el-tag type="danger">
{{ row.err.message }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 10px;">Info: </span>
<el-tag type="warning">
{{ row.vm.$vnode.tag }} error in {{ row.info }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 16px;">Url: </span>
<el-tag type="success">
{{ row.url }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="Stack">
<template slot-scope="scope">
{{ scope.row.err.stack }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ErrorLog',
data() {
return {
dialogTableVisible: false
}
},
computed: {
errorLogs() {
return this.$store.getters.errorLogs
}
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
}
}
}
</script>
<style scoped>
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
padding-right: 8px;
}
</style>
@@ -0,0 +1,73 @@
<template>
<div
v-if="isExternal"
:style="styleExternalIcon"
class="svg-external-icon svg-icon"
v-on="$listeners"
/>
<svg
v-else
:class="svgClass"
aria-hidden="true"
v-on="$listeners"
>
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return `svg-icon ${this.className}`
}
return 'svg-icon'
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
@@ -0,0 +1,708 @@
import { getToken } from '@/utils/auth'
import SettingMer from '@/utils/settingMer'
// 表单属性【右面板】
export const formConf = {
formRef: 'elForm',
formModel: 'formData',
size: 'medium',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true
}
// 输入型组件 【左面板】
export const inputComponents = [
{
// 组件的自定义配置
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
defaultValue: undefined,
required: true,
tips:false, //tooltip描述是否开启
tipsDesc:'', //tooltip描述内容
tipsIsLink:false,//是否开启描述链接
tipsLink:'', //描述链接
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
// 正则校验规则
regList: []
},
// 组件的插槽属性
__slot__: {
prepend: '',
append: ''
},
// 其余的为可直接写在组件标签上的属性
placeholder: '请输入',
style: { width: '95%' },
clearable: true,
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false
},
{
__config__: {
label: '多行文本',
labelWidth: null,
showLabel: true,
tag: 'el-input',
tagIcon: 'textarea',
defaultValue: undefined,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
layout: 'colFormItem',
span: 24,
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/input'
},
type: 'textarea',
placeholder: '请输入',
autosize: {
minRows: 4,
maxRows: 4
},
style: { width: '95%' },
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false
},
{
__config__: {
label: '密码',
showLabel: true,
labelWidth: null,
changeTag: true,
tag: 'el-input',
tagIcon: 'password',
defaultValue: undefined,
layout: 'colFormItem',
span: 24,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
document: 'https://element.eleme.cn/#/zh-CN/component/input'
},
__slot__: {
prepend: '',
append: ''
},
placeholder: '请输入',
'show-password': true,
style: { width: '100%' },
clearable: true,
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false
},
{
__config__: {
label: '计数器',
showLabel: true,
changeTag: true,
labelWidth: null,
tag: 'el-input-number',
tagIcon: 'number',
defaultValue: undefined,
span: 24,
layout: 'colFormItem',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
document: 'https://element.eleme.cn/#/zh-CN/component/input-number'
},
placeholder: '',
min: undefined,
max: undefined,
step: 1,
'step-strictly': false,
precision: undefined,
'controls-position': '',
disabled: false
}
// {
// __config__: {
// label: '编辑器',
// showLabel: true,
// changeTag: true,
// labelWidth: null,
// tag: 'tinymce',
// tagIcon: 'rich-text',
// defaultValue: null,
// span: 24,
// layout: 'colFormItem',
// required: true,
// regList: [],
// document: 'http://tinymce.ax-z.cn'
// },
// height: 300, // 编辑器高度
// branding: false // 隐藏右下角品牌烙印
// }
]
// 选择型组件 【左面板】
export const selectComponents = [
{
__config__: {
label: '下拉选择',
showLabel: true,
labelWidth: null,
tag: 'el-select',
tagIcon: 'select',
defaultValue: undefined,
layout: 'colFormItem',
span: 24,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/select'
},
__slot__: {
options: [{
label: '选项一',
value: 1
}, {
label: '选项二',
value: 2
}]
},
placeholder: '请选择',
style: { width: '100%' },
clearable: true,
disabled: false,
filterable: false,
multiple: false
},
{
__config__: {
label: '级联选择',
showLabel: true,
labelWidth: null,
tag: 'el-cascader',
tagIcon: 'cascader',
layout: 'colFormItem',
defaultValue: [],
dataType: 'dynamic',
span: 24,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/cascader'
},
options: [{
id: 1,
value: 1,
label: '选项1',
children: [{
id: 2,
value: 2,
label: '选项1-1'
}]
}],
placeholder: '请选择',
style: { width: '100%' },
props: {
props: {
multiple: false,
label: 'label',
value: 'value',
children: 'children'
}
},
'show-all-levels': true,
disabled: false,
clearable: true,
filterable: false,
separator: '/'
},
{
__config__: {
label: '单选框组',
labelWidth: null,
showLabel: true,
tag: 'el-radio-group',
tagIcon: 'radio',
changeTag: true,
defaultValue: undefined,
layout: 'colFormItem',
span: 24,
optionType: 'default',
regList: [],
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
border: false,
// bindInput:false, //是否开启绑定输入
// bindValve:'', //绑定输入内容
document: 'https://element.eleme.cn/#/zh-CN/component/radio'
},
__slot__: {
options: [{
label: '选项一',
value: 1
}, {
label: '选项二',
value: 2
}]
},
style: {},
size: 'medium',
disabled: false
},
{
__config__: {
label: '多选框组',
tag: 'el-checkbox-group',
tagIcon: 'checkbox',
defaultValue: [],
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
optionType: 'default',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
border: false,
document: 'https://element.eleme.cn/#/zh-CN/component/checkbox'
},
__slot__: {
options: [{
label: '选项一',
value: 1
}, {
label: '选项二',
value: 2
}]
},
style: {},
size: 'medium',
min: null,
max: null,
disabled: false
},
{
__config__: {
label: '开关',
tag: 'el-switch',
tagIcon: 'switch',
defaultValue: false,
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/switch'
},
style: {},
disabled: false,
'active-text': '',
'inactive-text': '',
'active-color': null,
'inactive-color': null,
'active-value': true,
'inactive-value': false
},
{
__config__: {
label: '滑块',
tag: 'el-slider',
tagIcon: 'slider',
defaultValue: null,
span: 24,
showLabel: true,
layout: 'colFormItem',
labelWidth: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/slider'
},
disabled: false,
min: 0,
max: 100,
step: 1,
'show-stops': false,
range: false
},
{
__config__: {
label: '时间选择',
tag: 'el-time-picker',
tagIcon: 'time',
defaultValue: null,
span: 24,
showLabel: true,
layout: 'colFormItem',
labelWidth: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
},
placeholder: '请选择',
style: { width: '100%' },
disabled: false,
clearable: true,
'picker-options': {
selectableRange: '00:00:00-23:59:59'
},
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss'
},
{
__config__: {
label: '时间范围',
tag: 'el-time-picker',
tagIcon: 'time-range',
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
defaultValue: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
},
style: { width: '100%' },
disabled: false,
clearable: true,
'is-range': true,
'range-separator': '至',
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss'
},
{
__config__: {
label: '固定时间范围',
tag: 'time-select',
tagIcon: 'time-select',
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
defaultValue: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/time-picker'
},
style: { width: '100%' },
disabled: false,
clearable: true,
placeholder: '请选择',
format: 'HH:mm',
'value-format': 'HH:mm'
},
{
__config__: {
label: '日期选择',
tag: 'el-date-picker',
tagIcon: 'date',
defaultValue: null,
showLabel: true,
labelWidth: null,
span: 24,
layout: 'colFormItem',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
},
placeholder: '请选择',
type: 'date',
style: { width: '100%' },
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false
},
{
__config__: {
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
showLabel: true,
labelWidth: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
layout: 'colFormItem',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/date-picker'
},
style: { width: '100%' },
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false
},
{
__config__: {
label: '评分',
tag: 'el-rate',
tagIcon: 'rate',
defaultValue: 0,
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/rate'
},
style: {},
max: 5,
'allow-half': false,
'show-text': false,
'show-score': false,
disabled: false
},
{
__config__: {
label: '颜色选择',
tag: 'el-color-picker',
tagIcon: 'color',
span: 24,
defaultValue: null,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/color-picker'
},
'show-alpha': false,
'color-format': '',
disabled: false,
size: 'medium'
},
{
__config__: {
label: '上传文件',
tag: 'upload-file',
tagIcon: 'uploadPicture',
layout: 'colFormItem',
defaultValue: null,
showLabel: true,
labelWidth: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
span: 24,
showTip: false,
buttonText: '点击上传',
regList: [],
changeTag: true,
// fileSize: 2,
// sizeUnit: 'MB',
document: 'https://element.eleme.cn/#/zh-CN/component/upload'
},
__slot__: {
'list-type': true
},
accept: '',
// headers: { 'Authori-zation': getToken() },
// data: { model: 'product', pid: 0 },
// action: SettingMer.apiBaseURL + 'admin/upload/image?model=product&pid=0',
// disabled: false,
// accept: '',
name: 'upfile',
// 'auto-upload': false,
// 'list-type': 'text',
// multiple: false
},
{
__config__: {
label: '自定义上传',
tag: 'self-upload',
tagIcon: 'selfUpload',
layout: 'colFormItem',
defaultValue: null,
showLabel: true,
labelWidth: null,
required: true,
tips:false,
tipsDesc:'',
tipsIsLink:false,
tipsLink:'',
span: 24,
showTip: false,
buttonText: '',
regList: [],
changeTag: true,
// fileSize: 2,
// sizeUnit: 'MB',
document: 'https://element.eleme.cn/#/zh-CN/component/upload'
},
__slot__: {
'list-type': true
},
// action: 'https://jsonplaceholder.typicode.com/posts/',
disabled: true,
accept: 'image',
name: 'file',
// 'auto-upload': true,
// 'list-type': 'text',
multiple: false
},
// {
// __config__: {
// label: '富文本编辑器',
// tag: 'tinymce',
// tagIcon: 'rich-text',
// layout: 'colFormItem',
// defaultValue: null,
// showLabel: true,
// labelWidth: null,
// required: false,
// tips:false,
// tipsDesc:'',
// tipsIsLink:false,
// tipsLink:'',
// span: 24,
// showTip: false,
// regList: [],
// document: "http://tinymce.ax-z.cn",
// renderKey: 1636077154813,
// changeTag: true,
// },
// height: 300, // 编辑器高度
// name: 'tinymce',
// disabled: false
// }
]
// 布局型组件 【左面板】
export const layoutComponents = [
{
__config__: {
layout: 'rowFormItem',
tagIcon: 'row',
label: '行容器',
layoutTree: true,
children: [],
document: 'https://element.eleme.cn/#/zh-CN/component/layout'
},
type: 'default',
justify: 'start',
align: 'top'
},
{
__config__: {
label: '按钮',
showLabel: true,
changeTag: true,
labelWidth: null,
tag: 'el-button',
tagIcon: 'button',
defaultValue: undefined,
span: 24,
layout: 'colFormItem',
document: 'https://element.eleme.cn/#/zh-CN/component/button'
},
__slot__: {
default: '主要按钮'
},
type: 'primary',
icon: 'el-icon-search',
round: false,
size: 'medium',
plain: false,
circle: false,
disabled: false
}
]
@@ -0,0 +1,18 @@
const styles = {
'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}',
'el-upload': '.el-upload__tip{line-height: 1.2;}'
}
function addCss(cssList, el) {
const css = styles[el.__config__.tag]
css && cssList.indexOf(css) === -1 && cssList.push(css)
if (el.__config__.children) {
el.__config__.children.forEach(el2 => addCss(cssList, el2))
}
}
export function makeUpCss(conf) {
const cssList = []
conf.fields.forEach(el => addCss(cssList, el))
return cssList.join('\n')
}
@@ -0,0 +1,37 @@
export default [
{
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
defaultValue: undefined,
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
// 正则校验规则
regList: [{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}]
},
// 组件的插槽属性
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: { width: '100%' },
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
}
]
@@ -0,0 +1,432 @@
/* eslint-disable max-len */
import ruleTrigger from './ruleTrigger'
import { getToken } from '@/utils/auth'
let confGlobal
let someSpanIsNot24
export function dialogWrapper(str) {
return `<el-dialog v-bind="$attrs" v-on="$listeners" @open="onOpen" @close="onClose" title="Dialog Titile">
${str}
<div slot="footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="handelConfirm">确定</el-button>
</div>
</el-dialog>`
}
export function vueTemplate(str) {
return `<template>
<div>
${str}
</div>
</template>`
}
export function vueScript(str) {
return `<script>
${str}
</script>`
}
export function cssStyle(cssStr) {
return `<style>
${cssStr}
</style>`
}
function buildFormTemplate(scheme, child, type) {
let labelPosition = ''
if (scheme.labelPosition !== 'right') {
labelPosition = `label-position="${scheme.labelPosition}"`
}
const disabled = scheme.disabled ? `:disabled="${scheme.disabled}"` : ''
let str = `<el-form ref="${scheme.formRef}" :model="${scheme.formModel}" :rules="${scheme.formRules}" size="${scheme.size}" ${disabled} label-width="${scheme.labelWidth}px" ${labelPosition}>
${child}
${buildFromBtns(scheme, type)}
</el-form>`
if (someSpanIsNot24) {
str = `<el-row :gutter="${scheme.gutter}">
${str}
</el-row>`
}
return str
}
function buildFromBtns(scheme, type) {
let str = ''
if (scheme.formBtns && type === 'file') {
str = `<el-form-item size="large">
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">取消</el-button>
</el-form-item>`
if (someSpanIsNot24) {
str = `<el-col :span="24">
${str}
</el-col>`
}
}
return str
}
// span不为24的用el-col包裹
function colWrapper(scheme, str) {
if (someSpanIsNot24 || scheme.__config__.span !== 24) {
return `<el-col :span="${scheme.__config__.span}">
${str}
</el-col>`
}
return str
}
const layouts = {
colFormItem(scheme) {
const config = scheme.__config__
let labelWidth = ''
let label = `label="${config.label}"`
if (config.labelWidth && config.labelWidth !== confGlobal.labelWidth) {
labelWidth = `label-width="${config.labelWidth}px"`
}
if (config.showLabel === false) {
labelWidth = 'label-width="0"'
label = ''
}
const required = !ruleTrigger[config.tag] && config.required ? 'required' : ''
const tagDom = tags[config.tag] ? tags[config.tag](scheme) : null
let str = `<el-form-item ${labelWidth} ${label} prop="${scheme.__vModel__}" ${required}>
${tagDom}
</el-form-item>`
str = colWrapper(scheme, str)
return str
},
rowFormItem(scheme) {
const config = scheme.__config__
const type = scheme.type === 'default' ? '' : `type="${scheme.type}"`
const justify = scheme.type === 'default' ? '' : `justify="${scheme.justify}"`
const align = scheme.type === 'default' ? '' : `align="${scheme.align}"`
const gutter = scheme.gutter ? `:gutter="${scheme.gutter}"` : ''
const children = config.children.map(el => layouts[el.__config__.layout](el))
let str = `<el-row ${type} ${justify} ${align} ${gutter}>
${children.join('\n')}
</el-row>`
str = colWrapper(scheme, str)
return str
}
}
const tags = {
'el-button': el => {
const {
tag, disabled
} = attrBuilder(el)
const type = el.type ? `type="${el.type}"` : ''
const icon = el.icon ? `icon="${el.icon}"` : ''
const round = el.round ? 'round' : ''
const size = el.size ? `size="${el.size}"` : ''
const plain = el.plain ? 'plain' : ''
const circle = el.circle ? 'circle' : ''
let child = buildElButtonChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${type} ${icon} ${round} ${size} ${plain} ${disabled} ${circle}>${child}</${tag}>`
},
'el-input': el => {
const {
tag, disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
const readonly = el.readonly ? 'readonly' : ''
const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
const showPassword = el['show-password'] ? 'show-password' : ''
const type = el.type ? `type="${el.type}"` : ''
const autosize = el.autosize && el.autosize.minRows
? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
: ''
let child = buildElInputChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${tag}>`
},
'el-input-number': el => {
const {
tag, disabled, vModel, placeholder
} = attrBuilder(el)
const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
const precision = el.precision ? `:precision='${el.precision}'` : ''
return `<${tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${tag}>`
},
'el-select': el => {
const {
tag, disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const filterable = el.filterable ? 'filterable' : ''
const multiple = el.multiple ? 'multiple' : ''
let child = buildElSelectChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${tag}>`
},
'el-radio-group': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
let child = buildElRadioGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${vModel} ${size} ${disabled}>${child}</${tag}>`
},
'el-checkbox-group': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const min = el.min ? `:min="${el.min}"` : ''
const max = el.max ? `:max="${el.max}"` : ''
let child = buildElCheckboxGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${tag}>`
},
'el-switch': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
return `<${tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${tag}>`
},
'el-cascader': el => {
const {
tag, disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const options = el.options ? `:options="${el.__vModel__}Options"` : ''
const props = el.props ? `:props="${el.__vModel__}Props"` : ''
const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
const filterable = el.filterable ? 'filterable' : ''
const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
return `<${tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${tag}>`
},
'el-slider': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const range = el.range ? 'range' : ''
const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
return `<${tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${tag}>`
},
'el-time-picker': el => {
const {
tag, disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const isRange = el['is-range'] ? 'is-range' : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
return `<${tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${tag}>`
},
'el-date-picker': el => {
const {
tag, disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const type = el.type === 'date' ? '' : `type="${el.type}"`
const readonly = el.readonly ? 'readonly' : ''
return `<${tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${tag}>`
},
'el-rate': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const max = el.max ? `:max='${el.max}'` : ''
const allowHalf = el['allow-half'] ? 'allow-half' : ''
const showText = el['show-text'] ? 'show-text' : ''
const showScore = el['show-score'] ? 'show-score' : ''
return `<${tag} ${vModel} ${max} ${allowHalf} ${showText} ${showScore} ${disabled}></${tag}>`
},
'el-color-picker': el => {
const { tag, disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
return `<${tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${tag}>`
},
'el-upload': el => {
const { tag } = el.__config__
const disabled = el.disabled ? ':disabled=\'true\'' : ''
const action = el.action ? `:action="${el.__vModel__}Action"` : ''
const multiple = el.multiple ? 'multiple' : ''
const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
const accept = el.accept ? `accept="${el.accept}"` : ''
const name = el.name !== 'file' ? `name="${el.name}"` : ''
const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
const beforeUpload = `:before-upload="${el.__vModel__}BeforeUpload"`
const fileList = `:file-list="${el.__vModel__}fileList"`
const ref = `ref="${el.__vModel__}"`
const headers = { 'Authori-zation': getToken() }
const data = el.data ? 'data' : ''
let child = buildElUploadChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${tag} ${headers} ${data} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${tag}>`
},
'self-upload': el => {
const { tag, vModel } = attrBuilder(el)
const height = el.height ? `:height="${el.height}"` : ''
const multiple = el.multiple ? 'multiple' : ''
const branding = el.branding ? `:branding="${el.branding}"` : ''
return `<${tag} ${vModel} ${height} ${branding} ${multiple}></${tag}>`
},
'ueditor-from': el => {
const { tag, vModel } = attrBuilder(el)
const height = el.height ? `:height="${el.height}"` : ''
return `<${tag} ${vModel}${height} >`
},
'upload-file': el => {
const { tag, vModel } = attrBuilder(el)
const height = el.height ? `:height="${el.height}"` : ''
return `<${tag} ${vModel}${height} >`
},
'time-select': el => {
const { tag, vModel } = attrBuilder(el)
const height = el.height ? `:height="${el.height}"` : ''
return `<${tag} ${vModel}${height} >`
},
tinymce: el => {
const { tag, vModel } = attrBuilder(el)
const branding = el.branding ? `:branding="${el.branding}"` : ''
return `<${tag} ${vModel} ${branding}></${tag}>`
}
}
function attrBuilder(el) {
return {
tag: el.__config__.tag,
vModel: `v-model="${confGlobal.formModel}.${el.__vModel__}"`,
clearable: el.clearable ? 'clearable' : '',
placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
disabled: el.disabled ? ':disabled=\'true\'' : ''
}
}
// el-buttin 子级
function buildElButtonChild(scheme) {
const children = []
const slot = scheme.__slot__ || {}
if (slot.default) {
children.push(slot.default)
}
return children.join('\n')
}
// el-input 子级
function buildElInputChild(scheme) {
const children = []
const slot = scheme.__slot__
if (slot && slot.prepend) {
children.push(`<template slot="prepend">${slot.prepend}</template>`)
}
if (slot && slot.append) {
children.push(`<template slot="append">${slot.append}</template>`)
}
return children.join('\n')
}
// el-select 子级
function buildElSelectChild(scheme) {
const children = []
const slot = scheme.__slot__
if (slot && slot.options && slot.options.length) {
children.push(`<el-option v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
}
return children.join('\n')
}
// el-radio-group 子级
function buildElRadioGroupChild(scheme) {
const children = []
const slot = scheme.__slot__
const config = scheme.__config__
if (slot && slot.options && slot.options.length) {
const tag = config.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = config.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
}
return children.join('\n')
}
// el-checkbox-group 子级
function buildElCheckboxGroupChild(scheme) {
const children = []
const slot = scheme.__slot__
const config = scheme.__config__
if (slot && slot.options && slot.options.length) {
const tag = config.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = config.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${scheme.__vModel__}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
}
return children.join('\n')
}
// el-upload 子级
function buildElUploadChild(scheme) {
const list = []
const config = scheme.__config__
if (scheme['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${config.buttonText}</el-button>`)
if (config.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${config.fileSize}${config.sizeUnit}${scheme.accept}文件</div>`)
return list.join('\n')
}
// el-upload 子级
// function buildSelfUploadChild(scheme) {
// const list = []
// const config = scheme.__config__
// if (scheme['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
// else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${config.buttonText}</el-button>`)
// if (config.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${config.fileSize}${config.sizeUnit} 的${scheme.accept}文件</div>`)
// return list.join('\n')
// }
/**
* 组装html代码。【入口函数】
* @param {Object} formConfig 整个表单配置
* @param {String} type 生成类型,文件或弹窗等
*/
export function makeUpHtml(formConfig, type) {
const htmlList = []
confGlobal = formConfig
// 判断布局是否都沾满了24个栅格,以备后续简化代码结构
someSpanIsNot24 = formConfig.fields.some(item => item.__config__.span !== 24)
// 遍历渲染每个组件成html
formConfig.fields.forEach(el => {
htmlList.push(layouts[el.__config__.layout](el))
})
const htmlStr = htmlList.join('\n')
// 将组件代码放进form标签
let temp = buildFormTemplate(formConfig, htmlStr, type)
// dialog标签包裹代码
if (type === 'dialog') {
temp = dialogWrapper(temp)
}
confGlobal = null
return temp
}
@@ -0,0 +1,253 @@
import { isArray } from 'util'
import { exportDefault, titleCase } from '@/utils/index'
import ruleTrigger from './ruleTrigger'
const units = {
KB: '1024',
MB: '1024 / 1024',
GB: '1024 / 1024 / 1024'
}
let confGlobal
const inheritAttrs = {
file: '',
dialog: 'inheritAttrs: false,'
}
/**
* 组装js 【入口函数】
* @param {Object} formConfig 整个表单配置
* @param {String} type 生成类型,文件或弹窗等
*/
export function makeUpJs(formConfig, type) {
confGlobal = formConfig = JSON.parse(JSON.stringify(formConfig))
const dataList = []
const ruleList = []
const optionsList = []
const propsList = []
const methodList = mixinMethod(type)
const uploadVarList = []
formConfig.fields.forEach(el => {
buildAttributes(el, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
})
const script = buildexport(
formConfig,
type,
dataList.join('\n'),
ruleList.join('\n'),
optionsList.join('\n'),
uploadVarList.join('\n'),
propsList.join('\n'),
methodList.join('\n')
)
confGlobal = null
return script
}
// 构建组件属性
function buildAttributes(scheme, dataList, ruleList, optionsList, methodList, propsList, uploadVarList) {
const config = scheme.__config__
const slot = scheme.__slot__
buildData(scheme, dataList)
buildRules(scheme, ruleList)
// 特殊处理options属性
if (scheme.options || (slot && slot.options && slot.options.length)) {
buildOptions(scheme, optionsList)
if (config.dataType === 'dynamic') {
const model = `${scheme.__vModel__}Options`
const options = titleCase(model)
buildOptionMethod(`get${options}`, model, methodList)
}
}
// 处理props
if (scheme.props && scheme.props.props) {
buildProps(scheme, propsList)
}
// 处理el-upload的action
if (scheme.action && config.tag === 'el-upload') {
uploadVarList.push(
`${scheme.__vModel__}Action: '${scheme.action}',
${scheme.__vModel__}fileList: [],`
)
methodList.push(buildBeforeUpload(scheme))
// 非自动上传时,生成手动上传的函数
if (!scheme['auto-upload']) {
methodList.push(buildSubmitUpload(scheme))
}
}
// 构建子级组件属性
if (config.children) {
config.children.forEach(item => {
buildAttributes(item, dataList, ruleList, optionsList, methodList, propsList, uploadVarList)
})
}
}
// 混入处理函数
function mixinMethod(type) {
const list = []; const
minxins = {
file: confGlobal.formBtns ? {
submitForm: `submitForm() {
this.$refs['${confGlobal.formRef}'].validate(valid => {
if(!valid) return
// TODO 提交表单
})
},`,
resetForm: `resetForm() {
this.$refs['${confGlobal.formRef}'].resetFields()
},`
} : null,
dialog: {
onOpen: 'onOpen() {},',
onClose: `onClose() {
this.$refs['${confGlobal.formRef}'].resetFields()
},`,
close: `close() {
this.$emit('update:visible', false)
},`,
handelConfirm: `handelConfirm() {
this.$refs['${confGlobal.formRef}'].validate(valid => {
if(!valid) return
this.close()
})
},`
}
}
const methods = minxins[type]
if (methods) {
Object.keys(methods).forEach(key => {
list.push(methods[key])
})
}
return list
}
// 构建data
function buildData(scheme, dataList) {
const config = scheme.__config__
if (scheme.__vModel__ === undefined) return
const defaultValue = JSON.stringify(config.defaultValue)
dataList.push(`${scheme.__vModel__}: ${defaultValue},`)
}
// 构建校验规则
function buildRules(scheme, ruleList) {
const config = scheme.__config__
if (scheme.__vModel__ === undefined) return
const rules = []
if (ruleTrigger[config.tag]) {
if (config.required) {
const type = isArray(config.defaultValue) ? 'type: \'array\',' : ''
let message = isArray(config.defaultValue) ? `请至少选择一个${config.label}` : scheme.placeholder
if (message === undefined) message = `${config.label}不能为空`
rules.push(`{ required: true, ${type} message: '${message}', trigger: '${ruleTrigger[config.tag]}' }`)
}
if (config.regList && isArray(config.regList)) {
config.regList.forEach(item => {
if (item.pattern) {
rules.push(
`{ pattern: ${eval(item.pattern)}, message: '${item.message}', trigger: '${ruleTrigger[config.tag]}' }`
)
}
})
}
ruleList.push(`${scheme.__vModel__}: [${rules.join(',')}],`)
}
}
// 构建options
function buildOptions(scheme, optionsList) {
if (scheme.__vModel__ === undefined) return
// el-cascader直接有options属性,其他组件都是定义在slot中,所以有两处判断
let { options } = scheme
if (!options) options = scheme.__slot__.options
if (scheme.__config__.dataType === 'dynamic') { options = [] }
const str = `${scheme.__vModel__}Options: ${JSON.stringify(options)},`
optionsList.push(str)
}
function buildProps(scheme, propsList) {
const str = `${scheme.__vModel__}Props: ${JSON.stringify(scheme.props.props)},`
propsList.push(str)
}
// el-upload的BeforeUpload
function buildBeforeUpload(scheme) {
const config = scheme.__config__
const unitNum = units[config.sizeUnit]; let rightSizeCode = ''; let acceptCode = ''; const
returnList = []
if (config.fileSize) {
rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${config.fileSize}
if(!isRightSize){
this.$message.error('文件大小超过 ${config.fileSize}${config.sizeUnit}')
}`
returnList.push('isRightSize')
}
if (scheme.accept) {
acceptCode = `let isAccept = new RegExp('${scheme.accept}').test(file.type)
if(!isAccept){
this.$message.error('应该选择${scheme.accept}类型的文件')
}`
returnList.push('isAccept')
}
const str = `${scheme.__vModel__}BeforeUpload(file) {
${rightSizeCode}
${acceptCode}
return ${returnList.join('&&')}
},`
return returnList.length ? str : ''
}
// el-upload的submit
function buildSubmitUpload(scheme) {
const str = `submitUpload() {
this.$refs['${scheme.__vModel__}'].submit()
},`
return str
}
function buildOptionMethod(methodName, model, methodList) {
const str = `${methodName}() {
// TODO 发起请求获取数据
this.${model}
},`
methodList.push(str)
}
// js整体拼接
function buildexport(conf, type, data, rules, selectOptions, uploadVar, props, methods) {
const str = `${exportDefault}{
${inheritAttrs[type]}
components: {},
props: [],
data () {
return {
${conf.formModel}: {
${data}
},
${conf.formRules}: {
${rules}
},
${uploadVar}
${selectOptions}
${props}
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
${methods}
}
}`
return str
}
@@ -0,0 +1,19 @@
/**
* 用于生成表单校验,指定正则规则的触发方式。
* 未在此处声明无触发方式的组件将不生成rule!!
*/
export default {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change',
tinymce: 'blur',
'time-select': 'change'
}
@@ -0,0 +1,235 @@
<script>
import render from '@/components/FormGenerator/components/render/render.js'
const ruleTrigger = {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change'
}
function renderFrom(h) {
const { formConfCopy } = this
return (
<el-row gutter={formConfCopy.gutter}>
<el-form
size={formConfCopy.size}
label-position={formConfCopy.labelPosition}
disabled={formConfCopy.disabled}
label-width={`${formConfCopy.labelWidth}px`}
ref={formConfCopy.formRef}
// model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
props={{ model: this[formConfCopy.formModel] }}
rules={this[formConfCopy.formRules]}
>
{renderFormItem.call(this, h, formConfCopy.fields)}
{formConfCopy.formBtns && formBtns.call(this, h)}
</el-form>
</el-row>
)
}
function formBtns(h) {
return <el-col>
<el-form-item size='mini'>
<el-button type='primary' onClick={this.submitForm}>提交</el-button>
</el-form-item>
</el-col>
}
function renderFormItem(h, elementList) {
return elementList.map(scheme => {
const config = scheme.__config__
const layout = layouts[config.layout]
if (layout) {
return layout.call(this, h, scheme)
}
throw new Error(`没有与${config.layout}匹配的layout`)
})
}
function renderChildren(h, scheme) {
const config = scheme.__config__
if (!Array.isArray(config.children)) return null
return renderFormItem.call(this, h, config.children)
}
function setValue(event, config, scheme) {
this.$set(config, 'defaultValue', event)
this.$set(this[this.formConf.formModel], scheme.__vModel__, event)
}
function buildListeners(scheme) {
const config = scheme.__config__
const methods = this.formConf.__methods__ || {}
const listeners = {}
// 给__methods__中的方法绑定this和event
Object.keys(methods).forEach(key => {
listeners[key] = event => methods[key].call(this, event)
})
// 响应 render.js 中的 vModel $emit('input', val)
listeners.input = event => setValue.call(this, event, config, scheme)
return listeners
}
const layouts = {
colFormItem(h, scheme) {
const config = scheme.__config__
const listeners = buildListeners.call(this, scheme)
let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
if (config.showLabel === false) labelWidth = '0'
if(config.tips && !config.tipsIsLink){
return (
<el-col span={config.span}>
<el-form-item label-width={labelWidth} prop={scheme.__vModel__}
label={config.showLabel ? config.label : ''}>
<el-tooltip effect="dark" placement="top-start" style="padding:10px 5px 0 0;">
            <i class="el-icon-warning-outline" />
            <div slot="content" style="max-width:400px;">{config.tipsDesc}</div>
         </el-tooltip>
<render conf={scheme} {...{ on: listeners }} />
</el-form-item>
</el-col>
)
}else if(config.tips && config.tipsIsLink){
return (
<el-col span={config.span}>
<el-form-item label-width={labelWidth} prop={scheme.__vModel__}
label={config.showLabel ? config.label : ''}>
<el-tooltip effect="dark" placement="top-start" style="padding:10px 5px 0 0;">
            <i class="el-icon-warning-outline" />
            <div slot="content" style="max-width:400px;">
<a href={config.tipsLink} target="_blank">{config.tipsDesc}</a>
</div>
         </el-tooltip>
<render conf={scheme} {...{ on: listeners }} />
</el-form-item>
</el-col>
)
}else{
return (
<el-col span={config.span}>
<el-form-item label-width={labelWidth} prop={scheme.__vModel__}
label={config.showLabel ? config.label : ''}>
<render conf={scheme} {...{ on: listeners }} />
</el-form-item>
</el-col>
)
}
},
rowFormItem(h, scheme) {
let child = renderChildren.apply(this, arguments)
if (scheme.type === 'flex') {
child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
{child}
</el-row>
}
return (
<el-col span={scheme.span}>
<el-row gutter={scheme.gutter}>
{child}
</el-row>
</el-col>
)
}
}
export default {
components: {
render
},
props: {
formConf: {
type: Object,
required: true
},
formEditData: {
type: Object
},
isEdit: {
type: Boolean,
default: false
}
},
data() {
if (this.isEdit) { // 初始化待编辑数据
this.formConf.fields.forEach(conf => {
// 设置现有的数据
const hasValueForEdit = this.formEditData[conf.__vModel__]
if(hasValueForEdit){
conf.__config__.defaultValue = hasValueForEdit
}
// 如果是el-select标签 判断数据后改变实现默认选中效果
if(conf.__config__.tag === 'el-select' || conf.__config__.tag === 'el-radio-group'){
const perValue = conf.__slot__.options.filter(option => option.value == this.formEditData[conf.__vModel__])
if(perValue.length > 0){ // 有表单数据
conf.__config__.defaultValue = perValue[0].value
}
}
})
}
const data = {
formConfCopy: JSON.parse(JSON.stringify(this.formConf)),
[this.formConf.formModel]: {},
[this.formConf.formRules]: {}
}
this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
return data
},
methods: {
initFormData(componentList, formData) {
componentList.forEach(cur => {
const config = cur.__config__
if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
if (config.children) this.initFormData(config.children, formData)
})
},
buildRules(componentList, rules) {
componentList.forEach(cur => {
const config = cur.__config__
if (Array.isArray(config.regList)) {
if (config.required) {
const required = { required: config.required, message: cur.placeholder }
if (Array.isArray(config.defaultValue)) {
required.type = 'array'
required.message = `请至少选择一个${config.label}`
}
required.message === undefined && (required.message = `${config.label}不能为空`)
config.regList.push(required)
}
rules[cur.__vModel__] = config.regList.map(item => {
item.pattern && (item.pattern = eval(item.pattern))
item.trigger = ruleTrigger && ruleTrigger[config.tag]
return item
})
}
if (config.children) this.buildRules(config.children, rules)
})
},
resetForm() {
this.$emit('resetForm', this.formConf)
this.formConfCopy = JSON.parse(JSON.stringify(this.formConf))
this.$refs[this.formConf.formRef].resetFields()
},
submitForm() {
this.$refs[this.formConf.formRef].validate(valid => {
if (!valid) return false
// 触发sumit事件
this.$emit('submit', this[this.formConf.formModel])
return true
})
}
},
render(h) {
return renderFrom.call(this, h)
}
}
</script>
@@ -0,0 +1,17 @@
## form-generator JSON 解析器
>用于将form-generator导出的JSON解析成一个表单。
### 安装组件
```
npm i form-gen-parser
```
或者
```
yarn add form-gen-parser
```
### 使用示例
> [查看在线示例](https://mrhj.gitee.io/form-generator/#/parser)
示例代码:
> [src\components\parser\example\Index.vue](https://github.com/JakHuang/form-generator/blob/dev/src/components/parser/example/Index.vue)
@@ -0,0 +1,74 @@
<template>
<div>
<parser
v-if="formConf.fields.length > 0"
v-loading="loading"
:is-edit="isCreate === 1"
:form-conf="formConf"
:form-edit-data="editData"
@submit="handlerSubmit"
@resetForm="resetForm"
/>
<!-- editData:{{ editData }}-->
<!-- formConf:{{ formConf }}-->
<!-- isCreate:{{ isCreate }}-->
</div>
</template>
<script>
/**
* 注意:和Parser唯一的区别就是这里仅仅传入表单配置id即可自动加载已配置的表单
* 数据后渲染表单,
* 其他业务和Parser保持一致
*/
import * as systemFormConfigApi from '@/api/systemFormConfig.js'
import parser from '@/components/FormGenerator/components/parser/Parser'
export default {
// name: "ZBParser"
components: { parser },
props: {
formId: {
type: Number,
required: true
},
isCreate: {
type: Number,
default: 0 // 0=create 1=edit
},
editData: {
type: Object
},
},
data() {
return {
loading: false,
formConf: { fields: [] }
}
},
mounted() {
this.handlerGetFormConfig(this.formId)
},
methods: {
handlerGetFormConfig(formId) { // 获取表单配置后生成table列
this.loading = true
const _pram = { id: formId }
systemFormConfigApi.getFormConfigInfo(_pram).then(data => {
this.formConf = JSON.parse(data.content)
this.loading = false
}).catch(()=>{
this.loading = false
})
},
handlerSubmit(formValue) {
this.$emit('submit', formValue)
},
resetForm(formValue){
this.$emit('resetForm', formValue)
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,264 @@
<template>
<div class="test-form">
<parser :form-conf="formConf" @submit="sumbitForm1" />
<parser :key="key2" :form-conf="formConf" @submit="sumbitForm2" />
<el-button @click="change">
change
</el-button>
</div>
</template>
<script>
import Parser from '../Parser'
// 若parser是通过安装npm方式集成到项目中的,使用此行引入
// import Parser from 'form-gen-parser'
export default {
components: {
Parser
},
props: {},
data() {
return {
key2: +new Date(),
formConf: {
fields: [
{
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
regList: [
{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}
]
},
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: {
width: '100%'
},
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
},
{
__config__: {
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
showLabel: true,
labelWidth: null,
required: true,
layout: 'colFormItem',
regList: [],
changeTag: true,
document:
'https://element.eleme.cn/#/zh-CN/component/date-picker',
formId: 101,
renderKey: 1585980082729
},
style: {
width: '100%'
},
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false,
__vModel__: 'field101'
},
{
__config__: {
layout: 'rowFormItem',
tagIcon: 'row',
label: '行容器',
layoutTree: true,
children: [
{
__config__: {
label: '评分',
tag: 'el-rate',
tagIcon: 'rate',
defaultValue: 0,
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
required: true,
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/rate',
formId: 102,
renderKey: 1586839671259
},
style: {},
max: 5,
'allow-half': false,
'show-text': false,
'show-score': false,
disabled: false,
__vModel__: 'field102'
}
],
document: 'https://element.eleme.cn/#/zh-CN/component/layout',
formId: 101,
span: 24,
renderKey: 1586839668999,
componentName: 'row101',
gutter: 15
},
type: 'default',
justify: 'start',
align: 'top'
}
],
formRef: 'elForm',
formModel: 'formData',
size: 'small',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
unFocusedComponentBorder: false
},
formConf2: {
fields: [
{
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
regList: [
{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}
]
},
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: {
width: '100%'
},
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
},
{
__config__: {
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
showLabel: true,
labelWidth: null,
required: true,
layout: 'colFormItem',
regList: [],
changeTag: true,
document:
'https://element.eleme.cn/#/zh-CN/component/date-picker',
formId: 101,
renderKey: 1585980082729
},
style: {
width: '100%'
},
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false,
__vModel__: 'field101'
}
],
formRef: 'elForm',
formModel: 'formData',
size: 'small',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
unFocusedComponentBorder: false
}
}
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {
change() {
this.key2 = +new Date()
const t = this.formConf
this.formConf = this.formConf2
this.formConf2 = t
},
sumbitForm1(data) {
console.log('sumbitForm1提交数据:', data)
},
sumbitForm2(data) {
console.log('sumbitForm2提交数据:', data)
}
}
}
</script>
<style lang="scss" scoped>
.test-form {
margin: 15px auto;
width: 800px;
padding: 15px;
}
</style>
@@ -0,0 +1,3 @@
import Parser from './Parser'
export default Parser
@@ -0,0 +1,25 @@
{
"name": "form-gen-parser",
"version": "1.0.3",
"description": "表单json解析器",
"main": "lib/form-gen-parser.umd.js",
"directories": {
"example": "example"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JakHuang/form-generator.git"
},
"dependencies": {
"form-gen-render": "^1.0.0"
},
"author": "jakHuang",
"license": "MIT",
"bugs": {
"url": "https://github.com/JakHuang/form-generator/issues"
},
"homepage": "https://github.com/JakHuang/form-generator#readme"
}
@@ -0,0 +1,19 @@
{
"name": "form-gen-render",
"version": "1.0.4",
"description": "表单核心render",
"main": "lib/form-gen-render.umd.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JakHuang/form-generator.git"
},
"author": "jakhuang",
"license": "MIT",
"bugs": {
"url": "https://github.com/JakHuang/form-generator/issues"
},
"homepage": "https://github.com/JakHuang/form-generator#readme"
}
@@ -0,0 +1,59 @@
function vModel(self, dataObject, defaultValue) {
dataObject.props.value = defaultValue
dataObject.on.input = val => {
self.$emit('input', val)
}
}
const componentChild = {}
/**
* 将./slots中的文件挂载到对象componentChild上
* 文件名为key,对应JSON配置中的__config__.tag
* 文件内容为value,解析JSON配置中的__slot__
*/
const slotsFiles = require.context('./slots', true, /\.js$/)
const keys = slotsFiles.keys() || []
keys.forEach(key => {
const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = slotsFiles(key).default
componentChild[tag] = value
})
export default {
render(h) {
const dataObject = {
attrs: {},
props: {},
on: {},
style: {}
}
const confClone = JSON.parse(JSON.stringify(this.conf))
const children = []
const childObjs = componentChild[confClone.__config__.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
if (confClone.__slot__ && confClone.__slot__[key]) {
children.push(childFunc(h, confClone, key))
}
})
}
Object.keys(confClone).forEach(key => {
const val = confClone[key]
if (key === '__vModel__') {
vModel(this, dataObject, confClone.__config__.defaultValue)
} else if (dataObject[key]) {
dataObject[key] = { ...dataObject[key], ...val }
} else {
dataObject.attrs[key] = val
}
})
delete dataObject.attrs.__config__
delete dataObject.attrs.__slot__
return h(this.conf.__config__.tag, dataObject, children)
},
props: ['conf']
}
@@ -0,0 +1,5 @@
export default {
default(h, conf, key) {
return conf.__slot__[key]
}
}
@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
} else {
list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
}
})
return list
}
}
@@ -0,0 +1,8 @@
export default {
prepend(h, conf, key) {
return <template slot='prepend'>{conf.__slot__[key]}</template>
},
append(h, conf, key) {
return <template slot='append'>{conf.__slot__[key]}</template>
}
}
@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
} else {
list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
}
})
return list
}
}
@@ -0,0 +1,9 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
})
return list
}
}
@@ -0,0 +1,17 @@
export default {
'list-type': (h, conf, key) => {
const list = []
const config = conf.__config__
if (conf['list-type'] === 'picture-card') {
list.push(<i class='el-icon-plus'></i>)
} else {
list.push(<el-button size='small' type='primary' icon='el-icon-upload'>{config.buttonText}</el-button>)
}
if (config.showTip) {
list.push(
<div slot='tip' class='el-upload__tip'>只能上传不超过 {config.fileSize}{config.sizeUnit} {conf.accept}文件</div>
)
}
return list
}
}
@@ -0,0 +1,8 @@
/* eslint-disable max-len */
export const plugins = [
'advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount'
]
export const toolbar = [
'code searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote removeformat subscript superscript codesample hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'
]
@@ -0,0 +1,3 @@
import Index from './index.vue'
export default Index
@@ -0,0 +1,93 @@
<template>
<textarea :id="tinymceId" class="textarea" />
</template>
<script>
import loadTinymce from '@/components/FormGenerator/utils/loadTinymce'
import { plugins, toolbar } from './config'
import { debounce } from 'throttle-debounce'
let num = 1
export default {
props: {
id: {
type: String,
default: () => {
num === 10000 && (num = 1)
return `tinymce${+new Date()}${num++}`
}
},
value: {
type: [String, Number, Boolean],
default: ''
}
},
data() {
return {
tinymceId: this.id
}
},
mounted() {
loadTinymce(tinymce => {
import('./zh_CN').then(() => {
tinymce.init({
selector: `#${this.tinymceId}`,
language: 'zh_CN',
menubar: 'file edit insert view format table',
plugins,
toolbar,
height: this.$attrs.height || 300,
branding: this.$attrs.branding || false,
object_resizing: false,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true,
init_instance_callback: editor => {
if (this.value) editor.setContent(this.value)
this.vModel(editor)
}
})
})
})
},
destroyed() {
this.destroyTinymce()
},
methods: {
vModel(editor) {
// 控制连续写入时setContent的触发频率
const debounceSetContent = debounce(250, editor.setContent)
this.$watch('value', (val, prevVal) => {
if (editor && val !== prevVal && val !== editor.getContent()) {
if (typeof val !== 'string') val = val.toString()
debounceSetContent.call(editor, val)
}
})
editor.on('change keyup undo redo', () => {
this.$emit('input', editor.getContent())
})
},
destroyTinymce() {
if (!window.tinymce) return
const tinymce = window.tinymce.get(this.tinymceId)
if (tinymce) {
tinymce.destroy()
}
}
}
}
</script>
<style lang="scss" scoped>
.textarea{
visibility: hidden;
}
</style>
@@ -0,0 +1,420 @@
/* eslint-disable */
tinymce.addI18n('zh_CN',{
"Redo": "\u91cd\u505a",
"Undo": "\u64a4\u9500",
"Cut": "\u526a\u5207",
"Copy": "\u590d\u5236",
"Paste": "\u7c98\u8d34",
"Select all": "\u5168\u9009",
"New document": "\u65b0\u6587\u4ef6",
"Ok": "\u786e\u5b9a",
"Cancel": "\u53d6\u6d88",
"Visual aids": "\u7f51\u683c\u7ebf",
"Bold": "\u7c97\u4f53",
"Italic": "\u659c\u4f53",
"Underline": "\u4e0b\u5212\u7ebf",
"Strikethrough": "\u5220\u9664\u7ebf",
"Superscript": "\u4e0a\u6807",
"Subscript": "\u4e0b\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Close": "\u5173\u95ed",
"Formats": "\u683c\u5f0f",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
"Headers": "\u6807\u9898",
"Header 1": "\u6807\u98981",
"Header 2": "\u6807\u98982",
"Header 3": "\u6807\u98983",
"Header 4": "\u6807\u98984",
"Header 5": "\u6807\u98985",
"Header 6": "\u6807\u98986",
"Headings": "\u6807\u9898",
"Heading 1": "\u6807\u98981",
"Heading 2": "\u6807\u98982",
"Heading 3": "\u6807\u98983",
"Heading 4": "\u6807\u98984",
"Heading 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
"Div": "Div",
"Pre": "Pre",
"Code": "\u4ee3\u7801",
"Paragraph": "\u6bb5\u843d",
"Blockquote": "\u5f15\u6587\u533a\u5757",
"Inline": "\u6587\u672c",
"Blocks": "\u57fa\u5757",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Fonts": "\u5b57\u4f53",
"Font Sizes": "\u5b57\u53f7",
"Class": "\u7c7b\u578b",
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
"OR": "\u6216",
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
"Upload": "\u4e0a\u4f20",
"Block": "\u5757",
"Align": "\u5bf9\u9f50",
"Default": "\u9ed8\u8ba4",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Square": "\u65b9\u5757",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Anchor...": "\u951a\u70b9...",
"Name": "\u540d\u79f0",
"Id": "\u6807\u8bc6\u7b26",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special character...": "\u7279\u6b8a\u5b57\u7b26...",
"Source code": "\u6e90\u4ee3\u7801",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"Language": "\u8bed\u8a00",
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
"Color Picker": "\u9009\u8272\u5668",
"R": "R",
"G": "G",
"B": "B",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Description": "\u63cf\u8ff0",
"Robots": "\u673a\u5668\u4eba",
"Author": "\u4f5c\u8005",
"Encoding": "\u7f16\u7801",
"Fullscreen": "\u5168\u5c4f",
"Action": "\u64cd\u4f5c",
"Shortcut": "\u5feb\u6377\u952e",
"Help": "\u5e2e\u52a9",
"Address": "\u5730\u5740",
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
"Plugins": "\u63d2\u4ef6",
"Handy Shortcuts": "\u5feb\u6377\u952e",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Source": "\u5730\u5740",
"Dimensions": "\u5927\u5c0f",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Style": "\u6837\u5f0f",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Border": "\u8fb9\u6846",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image...": "\u56fe\u7247...",
"Image list": "\u56fe\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Image options": "\u56fe\u7247\u9009\u9879",
"Zoom in": "\u653e\u5927",
"Zoom out": "\u7f29\u5c0f",
"Crop": "\u88c1\u526a",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Orientation": "\u65b9\u5411",
"Brightness": "\u4eae\u5ea6",
"Sharpen": "\u9510\u5316",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Gamma": "\u4f3d\u9a6c\u503c",
"Invert": "\u53cd\u8f6c",
"Apply": "\u5e94\u7528",
"Back": "\u540e\u9000",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Url": "\u5730\u5740",
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
"Current window": "\u5f53\u524d\u7a97\u53e3",
"None": "\u65e0",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Anchors": "\u951a\u70b9",
"Link...": "\u94fe\u63a5...",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Link list": "\u94fe\u63a5\u5217\u8868",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Embed": "\u5185\u5d4c",
"Media...": "\u591a\u5a92\u4f53...",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Print...": "\u6253\u5370...",
"Save": "\u4fdd\u5b58",
"Find": "\u67e5\u627e",
"Replace with": "\u66ff\u6362\u4e3a",
"Replace": "\u66ff\u6362",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Previous": "\u4e0a\u4e00\u4e2a",
"Next": "\u4e0b\u4e00\u4e2a",
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Find whole words only": "\u5168\u5b57\u5339\u914d",
"Spell check": "\u62fc\u5199\u68c0\u67e5",
"Ignore": "\u5ffd\u7565",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Finish": "\u5b8c\u6210",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Delete table": "\u5220\u9664\u8868\u683c",
"Cell": "\u5355\u5143\u683c",
"Row": "\u884c",
"Column": "\u5217",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Delete row": "\u5220\u9664\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Cut row": "\u526a\u5207\u884c",
"Copy row": "\u590d\u5236\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Delete column": "\u5220\u9664\u5217",
"Cols": "\u5217",
"Rows": "\u884c",
"Width": "\u5bbd",
"Height": "\u9ad8",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Show caption": "\u663e\u793a\u6807\u9898",
"Left": "\u5de6\u5bf9\u9f50",
"Center": "\u5c45\u4e2d",
"Right": "\u53f3\u5bf9\u9f50",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Scope": "\u8303\u56f4",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Row group": "\u884c\u7ec4",
"Column group": "\u5217\u7ec4",
"Row type": "\u884c\u7c7b\u578b",
"Header": "\u8868\u5934",
"Body": "\u8868\u4f53",
"Footer": "\u8868\u5c3e",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Insert template...": "\u63d2\u5165\u6a21\u677f...",
"Templates": "\u6a21\u677f",
"Template": "\u6a21\u677f",
"Text color": "\u6587\u5b57\u989c\u8272",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Remove color": "\u79fb\u9664\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Word count": "\u5b57\u6570",
"Count": "\u8ba1\u6570",
"Document": "\u6587\u6863",
"Selection": "\u9009\u62e9",
"Words": "\u5355\u8bcd",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"{0} words": "{0} \u5b57",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Insert": "\u63d2\u5165",
"View": "\u89c6\u56fe",
"Format": "\u683c\u5f0f",
"Table": "\u8868\u683c",
"Tools": "\u5de5\u5177",
"Powered by {0}": "\u7531{0}\u9a71\u52a8",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Image title": "\u56fe\u7247\u6807\u9898",
"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
"Border style": "\u8fb9\u6846\u6837\u5f0f",
"Error": "\u9519\u8bef",
"Warn": "\u8b66\u544a",
"Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
"System Font": "\u7cfb\u7edf\u5b57\u4f53",
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
"example": "\u793a\u4f8b",
"Search": "\u641c\u7d22",
"All": "\u5168\u90e8",
"Currency": "\u8d27\u5e01",
"Text": "\u6587\u5b57",
"Quotations": "\u5f15\u7528",
"Mathematical": "\u6570\u5b66",
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
"Symbols": "\u7b26\u53f7",
"Arrows": "\u7bad\u5934",
"User Defined": "\u81ea\u5b9a\u4e49",
"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
"currency sign": "\u8d27\u5e01\u7b26\u53f7",
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
"colon sign": "\u5192\u53f7",
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
"lira sign": "\u91cc\u62c9\u7b26\u53f7",
"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
"naira sign": "\u5948\u62c9\u7b26\u53f7",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
"won sign": "\u97e9\u5143\u7b26\u53f7",
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
"austral sign": "\u6fb3\u5143\u7b26\u53f7",
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
"cedi sign": "\u585e\u5730\u7b26\u53f7",
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
"spesmilo sign": "spesmilo\u7b26\u53f7",
"tenge sign": "\u575a\u6208\u7b26\u53f7",
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
"ruble sign": "\u5362\u5e03\u7b26\u53f7",
"yen character": "\u65e5\u5143\u5b57\u6837",
"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
"People": "\u4eba\u7c7b",
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
"Activity": "\u6d3b\u52a8",
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
"Objects": "\u7269\u4ef6",
"Flags": "\u65d7\u5e1c",
"Characters": "\u5b57\u7b26",
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
"{0} characters": "{0} \u4e2a\u5b57\u7b26",
"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
"Update": "\u66f4\u65b0",
"Color swatch": "\u989c\u8272\u6837\u672c",
"Turquoise": "\u9752\u7eff\u8272",
"Green": "\u7eff\u8272",
"Blue": "\u84dd\u8272",
"Purple": "\u7d2b\u8272",
"Navy Blue": "\u6d77\u519b\u84dd",
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
"Dark Green": "\u6df1\u7eff\u8272",
"Medium Blue": "\u4e2d\u84dd\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272",
"Midnight Blue": "\u6df1\u84dd\u8272",
"Yellow": "\u9ec4\u8272",
"Orange": "\u6a59\u8272",
"Red": "\u7ea2\u8272",
"Light Gray": "\u6d45\u7070\u8272",
"Gray": "\u7070\u8272",
"Dark Yellow": "\u6697\u9ec4\u8272",
"Dark Orange": "\u6df1\u6a59\u8272",
"Dark Red": "\u6df1\u7ea2\u8272",
"Medium Gray": "\u4e2d\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272",
"Light Green": "\u6d45\u7eff\u8272",
"Light Yellow": "\u6d45\u9ec4\u8272",
"Light Red": "\u6d45\u7ea2\u8272",
"Light Purple": "\u6d45\u7d2b\u8272",
"Light Blue": "\u6d45\u84dd\u8272",
"Dark Purple": "\u6df1\u7d2b\u8272",
"Dark Blue": "\u6df1\u84dd\u8272",
"Black": "\u9ed1\u8272",
"White": "\u767d\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
"history": "\u5386\u53f2",
"styles": "\u6837\u5f0f",
"formatting": "\u683c\u5f0f\u5316",
"alignment": "\u5bf9\u9f50",
"indentation": "\u7f29\u8fdb",
"permanent pen": "\u8bb0\u53f7\u7b14",
"comments": "\u5907\u6ce8",
"Format Painter": "\u683c\u5f0f\u5237",
"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6",
"Capitalization": "\u5927\u5199",
"lowercase": "\u5c0f\u5199",
"UPPERCASE": "\u5927\u5199",
"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199",
"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027",
"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...",
"Font": "\u5b57\u4f53",
"Size": "\u5b57\u53f7",
"More...": "\u66f4\u591a...",
"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00",
"Select...": "\u9009\u62e9...",
"Preferences": "\u9996\u9009\u9879",
"Yes": "\u662f",
"No": "\u5426",
"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15",
"Version": "\u7248\u672c",
"Anchor": "\u951a\u70b9",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b",
"Color": "\u989c\u8272",
"Emoticons": "\u8868\u60c5",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Image": "\u56fe\u7247",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"Link": "\u94fe\u63a5",
"Poster": "\u5c01\u9762",
"Media": "\u5a92\u4f53",
"Print": "\u6253\u5370",
"Prev": "\u4e0a\u4e00\u4e2a",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Caption": "\u6807\u9898",
"Insert template": "\u63d2\u5165\u6a21\u677f"
});
@@ -0,0 +1,110 @@
<template>
<div>
<el-dialog
v-bind="$attrs"
width="500px"
:close-on-click-modal="false"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<el-row :gutter="15">
<el-form
ref="elForm"
:model="formData"
:rules="rules"
size="medium"
label-width="100px"
>
<el-col :span="24">
<el-form-item label="生成类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button
v-for="(item, index) in typeOptions"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="showFileName" label="文件名" prop="fileName">
<el-input v-model="formData.fileName" placeholder="请输入文件名" clearable />
</el-form-item>
</el-col>
</el-form>
</el-row>
<div slot="footer">
<el-button @click="close">
取消
</el-button>
<el-button type="primary" @click="handelConfirm">
确定
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: ['showFileName'],
data() {
return {
formData: {
fileName: undefined,
type: 'file'
},
rules: {
fileName: [{
required: true,
message: '请输入文件名',
trigger: 'blur'
}],
type: [{
required: true,
message: '生成类型不能为空',
trigger: 'change'
}]
},
typeOptions: [{
label: '页面',
value: 'file'
}, {
label: '弹窗',
value: 'dialog'
}]
}
},
computed: {
},
watch: {},
mounted() {},
methods: {
onOpen() {
if (this.showFileName) {
this.formData.fileName = `${+new Date()}.vue`
}
},
onClose() {
},
close(e) {
this.$emit('update:visible', false)
},
handelConfirm() {
this.$refs.elForm.validate(valid => {
if (!valid) return
this.$emit('confirm', { ...this.formData })
this.close()
})
}
}
}
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,128 @@
<script>
import draggable from 'vuedraggable'
import render from '@/components/FormGenerator/components/render/render'
const components = {
itemBtns(h, element, index, parent) {
const { copyItem, deleteItem } = this.$listeners
return [
<span class='drawing-item-copy' title='复制' onClick={event => {
copyItem(element, parent); event.stopPropagation()
}}>
<i class='el-icon-copy-document' />
</span>,
<span class='drawing-item-delete' title='删除' onClick={event => {
deleteItem(index, parent); event.stopPropagation()
}}>
<i class='el-icon-delete' />
</span>
]
}
}
const layouts = {
colFormItem(h, element, index, parent) {
const { activeItem } = this.$listeners
const config = element.__config__
let className = this.activeId === config.formId ? 'drawing-item active-from-item' : 'drawing-item'
if (this.formConf.unFocusedComponentBorder) className += ' unfocus-bordered'
let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
if (config.showLabel === false) labelWidth = '0'
if(config.tips == undefined){
this.$set(config,'tips',false);//如果以前的表单没有tooltip配置,就赋值一个默认值用来读取
}
if(config.tips){
return (
<el-col span={config.span} class={className}
nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
<el-form-item label-width={labelWidth}
label={config.showLabel ? config.label : ''} required={config.required}>
<el-tooltip effect="dark" placement="top-start" style="padding:10px 5px 0 0;">
            <i class="el-icon-warning-outline" />
            <div slot="content" style="max-width:400px;">{config.tipsDesc}</div>
         </el-tooltip>
<render key={config.renderKey} conf={element} onInput={ event => {
this.$set(config, 'defaultValue', event)
}} />
</el-form-item>
{components.itemBtns.apply(this, arguments)}
</el-col>
)
}else{
return (
<el-col span={config.span} class={className}
nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
<el-form-item label-width={labelWidth}
label={config.showLabel ? config.label : ''} required={config.required}>
<render key={config.renderKey} conf={element} onInput={ event => {
this.$set(config, 'defaultValue', event)
}} />
</el-form-item>
{components.itemBtns.apply(this, arguments)}
</el-col>
)
}
},
rowFormItem(h, element, index, parent) {
const { activeItem } = this.$listeners
const className = this.activeId === element.__config__.formId
? 'drawing-row-item active-from-item'
: 'drawing-row-item'
let child = renderChildren.apply(this, arguments)
if (element.type === 'flex') {
child = <el-row type={element.type} justify={element.justify} align={element.align}>
{child}
</el-row>
}
return (
<el-col span={element.__config__.span}>
<el-row gutter={element.__config__.gutter} class={className}
nativeOnClick={event => { activeItem(element); event.stopPropagation() }}>
<span class='component-name'>{element.__config__.componentName}</span>
<draggable list={element.__config__.children} animation={340} group='componentsGroup' class='drag-wrapper'>
{child}
</draggable>
{components.itemBtns.apply(this, arguments)}
</el-row>
</el-col>
)
}
}
function renderChildren(h, element, index, parent) {
const config = element.__config__
if (!Array.isArray(config.children)) return null
return config.children.map((el, i) => {
const layout = layouts[el.__config__.layout]
if (layout) {
return layout.call(this, h, el, i, config.children)
}
return layoutIsNotFound.call(this)
})
}
function layoutIsNotFound() {
throw new Error(`没有与${this.element.__config__.layout}匹配的layout`)
}
export default {
components: {
render,
draggable
},
props: [
'element',
'index',
'drawingList',
'activeId',
'formConf'
],
render(h) {
const layout = layouts[this.element.__config__.layout]
if (layout) {
return layout.call(this, h, this.element, this.index, this.drawingList)
}
return layoutIsNotFound.call(this)
}
}
</script>
@@ -0,0 +1,332 @@
<template>
<div>
<el-drawer v-bind="$attrs" v-on="$listeners" @opened="onOpen" @close="onClose">
<div style="height:100%">
<el-row style="height:100%;overflow:auto">
<el-col :md="24" :lg="12" class="left-editor">
<div class="setting" title="资源引用" @click="showResource">
<el-badge :is-dot="!!resources.length" class="item">
<i class="el-icon-setting" />
</el-badge>
</div>
<el-tabs v-model="activeTab" type="card" class="editor-tabs">
<el-tab-pane name="html">
<span slot="label">
<i v-if="activeTab==='html'" class="el-icon-edit" />
<i v-else class="el-icon-document" />
template
</span>
</el-tab-pane>
<el-tab-pane name="js">
<span slot="label">
<i v-if="activeTab==='js'" class="el-icon-edit" />
<i v-else class="el-icon-document" />
script
</span>
</el-tab-pane>
<el-tab-pane name="css">
<span slot="label">
<i v-if="activeTab==='css'" class="el-icon-edit" />
<i v-else class="el-icon-document" />
css
</span>
</el-tab-pane>
</el-tabs>
<div v-show="activeTab==='html'" id="editorHtml" class="tab-editor" />
<div v-show="activeTab==='js'" id="editorJs" class="tab-editor" />
<div v-show="activeTab==='css'" id="editorCss" class="tab-editor" />
</el-col>
<el-col :md="24" :lg="12" class="right-preview">
<div class="action-bar" :style="{'text-align': 'left'}">
<span class="bar-btn" @click="runCode">
<i class="el-icon-refresh" />
刷新
</span>
<span class="bar-btn" @click="exportFile">
<i class="el-icon-download" />
导出vue文件
</span>
<span ref="copyBtn" class="bar-btn copy-btn">
<i class="el-icon-document-copy" />
复制代码
</span>
<span class="bar-btn delete-btn" @click="$emit('update:visible', false)">
<i class="el-icon-circle-close" />
关闭
</span>
</div>
<iframe
v-show="isIframeLoaded"
ref="previewPage"
class="result-wrapper"
frameborder="0"
src="preview.html"
@load="iframeLoad"
/>
<div v-show="!isIframeLoaded" v-loading="true" class="result-wrapper" />
</el-col>
</el-row>
</div>
</el-drawer>
<resource-dialog
:visible.sync="resourceVisible"
:origin-resource="resources"
@save="setResource"
/>
</div>
</template>
<script>
import { parse } from '@babel/parser'
import ClipboardJS from 'clipboard'
import { saveAs } from 'file-saver'
import {
makeUpHtml, vueTemplate, vueScript, cssStyle
} from '@/components/FormGenerator/components/generator/html'
import { makeUpJs } from '@/components/FormGenerator/components/generator/js'
import { makeUpCss } from '@/components/FormGenerator/components/generator/css'
import { exportDefault, beautifierConf, titleCase } from '../utils/index'
import ResourceDialog from './ResourceDialog'
import loadMonaco from '../utils/loadMonaco'
import loadBeautifier from '../utils/loadBeautifier'
const editorObj = {
html: null,
js: null,
css: null
}
const mode = {
html: 'html',
js: 'javascript',
css: 'css'
}
let beautifier
let monaco
export default {
components: { ResourceDialog },
props: ['formData', 'generateConf'],
data() {
return {
activeTab: 'html',
htmlCode: '',
jsCode: '',
cssCode: '',
codeFrame: '',
isIframeLoaded: false,
isInitcode: false, // 保证open后两个异步只执行一次runcode
isRefreshCode: false, // 每次打开都需要重新刷新代码
resourceVisible: false,
scripts: [],
links: [],
monaco: null
}
},
computed: {
resources() {
return this.scripts.concat(this.links)
}
},
watch: {},
created() {
},
mounted() {
window.addEventListener('keydown', this.preventDefaultSave)
const clipboard = new ClipboardJS('.copy-btn', {
text: trigger => {
const codeStr = this.generateCode()
this.$notify({
title: '成功',
message: '代码已复制到剪切板,可粘贴。',
type: 'success'
})
return codeStr
}
})
clipboard.on('error', e => {
this.$message.error('代码复制失败')
})
},
beforeDestroy() {
window.removeEventListener('keydown', this.preventDefaultSave)
},
methods: {
preventDefaultSave(e) {
if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
}
},
onOpen() {
const { type } = this.generateConf
this.htmlCode = makeUpHtml(this.formData, type)
this.jsCode = makeUpJs(this.formData, type)
this.cssCode = makeUpCss(this.formData)
loadBeautifier(btf => {
beautifier = btf
this.htmlCode = beautifier.html(this.htmlCode, beautifierConf.html)
this.jsCode = beautifier.js(this.jsCode, beautifierConf.js)
this.cssCode = beautifier.css(this.cssCode, beautifierConf.html)
loadMonaco(val => {
monaco = val
this.setEditorValue('editorHtml', 'html', this.htmlCode)
this.setEditorValue('editorJs', 'js', this.jsCode)
this.setEditorValue('editorCss', 'css', this.cssCode)
if (!this.isInitcode) {
this.isRefreshCode = true
this.isIframeLoaded && (this.isInitcode = true) && this.runCode()
}
})
})
},
onClose() {
this.isInitcode = false
this.isRefreshCode = false
this.isIframeLoaded = false
},
iframeLoad() {
if (!this.isInitcode) {
this.isIframeLoaded = true
this.isRefreshCode && (this.isInitcode = true) && this.runCode()
}
},
setEditorValue(id, type, codeStr) {
if (editorObj[type]) {
editorObj[type].setValue(codeStr)
} else {
editorObj[type] = monaco.editor.create(document.getElementById(id), {
value: codeStr,
theme: 'vs-dark',
language: mode[type],
automaticLayout: true
})
}
// ctrl + s 刷新
editorObj[type].onKeyDown(e => {
if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
this.runCode()
}
})
},
runCode() {
const jsCodeStr = editorObj.js.getValue()
try {
const ast = parse(jsCodeStr, { sourceType: 'module' })
const astBody = ast.program.body
if (astBody.length > 1) {
this.$confirm(
'js格式不能识别,仅支持修改export default的对象内容',
'提示',
{
type: 'warning'
}
)
return
}
if (astBody[0].type === 'ExportDefaultDeclaration') {
const postData = {
type: 'refreshFrame',
data: {
generateConf: this.generateConf,
html: editorObj.html.getValue(),
js: jsCodeStr.replace(exportDefault, ''),
css: editorObj.css.getValue(),
scripts: this.scripts,
links: this.links
}
}
this.$refs.previewPage.contentWindow.postMessage(
postData,
location.origin
)
} else {
this.$message.error('请使用export default')
}
} catch (err) {
this.$message.error(`js错误:${err}`)
}
},
generateCode() {
const html = vueTemplate(editorObj.html.getValue())
const script = vueScript(editorObj.js.getValue())
const css = cssStyle(editorObj.css.getValue())
return beautifier.html(html + script + css, beautifierConf.html)
},
exportFile() {
this.$prompt('文件名:', '导出文件', {
inputValue: `${+new Date()}.vue`,
closeOnClickModal: false,
inputPlaceholder: '请输入文件名'
}).then(({ value }) => {
if (!value) value = `${+new Date()}.vue`
const codeStr = this.generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
saveAs(blob, value)
})
},
showResource() {
this.resourceVisible = true
},
setResource(arr) {
const scripts = []; const
links = []
if (Array.isArray(arr)) {
arr.forEach(item => {
if (item.endsWith('.css')) {
links.push(item)
} else {
scripts.push(item)
}
})
this.scripts = scripts
this.links = links
} else {
this.scripts = []
this.links = []
}
}
}
}
</script>
<style lang="scss" scoped>
@import '../styles/mixin';
.tab-editor {
position: absolute;
top: 33px;
bottom: 0;
left: 0;
right: 0;
font-size: 14px;
}
.left-editor {
position: relative;
height: 100%;
background: #1e1e1e;
overflow: hidden;
}
.setting{
position: absolute;
right: 15px;
top: 3px;
color: #a9f122;
font-size: 18px;
cursor: pointer;
z-index: 1;
}
.right-preview {
height: 100%;
.result-wrapper {
height: calc(100vh - 33px);
width: 100%;
overflow: auto;
padding: 12px;
box-sizing: border-box;
}
}
@include action-bar;
::v-deep .el-drawer__header {
display: none;
}
</style>
@@ -0,0 +1,494 @@
<template>
<div class="container-FromGen">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<span>领淘</span>
<!-- <img :src="logo" alt="logo"> Form Generator
<a class="github" href="https://github.com/JakHuang/form-generator" target="_blank">
<img src="https://github.githubassets.com/pinned-octocat.svg" alt>
</a> -->
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div v-for="(item, listIndex) in leftComponents" :key="listIndex">
<div class="components-title">
<svg-icon icon-class="component" />
{{ item.title }}
</div>
<draggable
class="components-draggable"
:list="item.list"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
>
<div
v-for="(element, index) in item.list"
:key="index"
class="components-item"
@click="addComponent(element)"
>
<div class="components-body">
<svg-icon :icon-class="element.__config__.tagIcon" />
{{ element.__config__.label }}
</div>
</div>
</draggable>
</div>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<!-- <el-button icon="el-icon-video-play" type="text" @click="run">-->
<!-- 运行-->
<!-- </el-button>-->
<!-- <el-button icon="el-icon-view" type="text" @click="showJson">-->
<!-- 查看json-->
<!-- </el-button>-->
<!-- <el-button icon="el-icon-download" type="text" @click="download"> -->
<!-- 导出vue文件 -->
<!-- </el-button> -->
<!-- <el-button class="copy-btn-main" icon="el-icon-document-copy" type="text" @click="copy">-->
<!-- 复制代码-->
<!-- </el-button>-->
<!-- <el-button class="delete-btn" icon="el-icon-delete" type="text" @click="empty">-->
<!-- 清空-->
<!-- </el-button>-->
<el-form ref="selfForm" inline :model="selfForm">
<el-form-item
label="名称"
prop="name"
:rules="[{ required:true, message:'请填写名称', trigger:['blur','change'] }]"
>
<el-input v-model="selfForm.name" placeholder="名称" />
</el-form-item>
<el-form-item
label="描述"
prop="info"
:rules="[{ required:true, message:'请填写描述', trigger:['blur','change'] }]"
>
<el-input v-model="selfForm.info" placeholder="描述" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerSaveJSON('selfForm')" v-hasPermi="['admin:system:form:update']">保存</el-button>
</el-form-item>
</el-form>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form
:size="formConf.size"
:label-position="formConf.labelPosition"
:disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'"
>
<draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup">
<draggable-item
v-for="(element, index) in drawingList"
:key="element.renderKey"
:drawing-list="drawingList"
:element="element"
:index="index"
:active-id="activeId"
:form-conf="formConf"
@activeItem="activeFormItem"
@copyItem="drawingItemCopy"
@deleteItem="drawingItemDelete"
/>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
<right-panel
:active-data="activeData"
:form-conf="formConf"
:show-field="!!drawingList.length"
@tag-change="tagChange"
/>
<form-drawer
:visible.sync="drawerVisible"
:form-data="formData"
size="100%"
:generate-conf="generateConf"
/>
<json-drawer
size="60%"
:visible.sync="jsonDrawerVisible"
:json-str="JSON.stringify(formData)"
@refresh="refreshJson"
/>
<code-type-dialog
:visible.sync="dialogVisible"
title="选择生成类型"
:show-file-name="showFileName"
@confirm="generate"
/>
<input id="copyNode" type="hidden">
</div>
</template>
<script>
import draggable from 'vuedraggable'
import { debounce } from 'throttle-debounce'
import { saveAs } from 'file-saver'
import ClipboardJS from 'clipboard'
import render from '@/components/FormGenerator/components/render/render'
import FormDrawer from './FormDrawer'
import JsonDrawer from './JsonDrawer'
import RightPanel from './RightPanel'
import {
inputComponents, selectComponents, layoutComponents, formConf
} from '@/components/FormGenerator/components/generator/config'
import {
exportDefault, beautifierConf, isNumberStr, titleCase
} from '../utils/index'
import {
makeUpHtml, vueTemplate, vueScript, cssStyle
} from '@/components/FormGenerator/components/generator/html'
import { makeUpJs } from '@/components/FormGenerator/components/generator/js'
import { makeUpCss } from '@/components/FormGenerator/components/generator/css'
import drawingDefalut from '@/components/FormGenerator/components/generator/drawingDefalut'
// import logo from '@/assets/logo.png'
import CodeTypeDialog from './CodeTypeDialog'
import DraggableItem from './DraggableItem'
import {
getDrawingList, saveDrawingList, getIdGlobal, saveIdGlobal, getFormConf, getFormConfSelf
} from '../utils/db'
import loadBeautifier from '../utils/loadBeautifier'
import {Debounce} from '@/utils/validate'
let beautifier
const emptyActiveData = { style: {}, autosize: {}}
let oldActiveId
let tempActiveData
const drawingListInDB = getDrawingList()
const formConfInDB = getFormConf()
const idGlobal = getIdGlobal()
export default {
components: {
draggable,
render,
FormDrawer,
JsonDrawer,
RightPanel,
CodeTypeDialog,
DraggableItem
},
props: {
editData: {
type: Object,
default: {}
},
isCreate: {
type: Number,
default: 0 // 0=创建,1=编辑
}
},
data() {
return {
// logo,
idGlobal,
formConf,
inputComponents,
selectComponents,
layoutComponents,
labelWidth: 100,
drawingList: drawingDefalut,
drawingData: {},
activeId: drawingDefalut[0].formId,
drawerVisible: false,
formData: {},
dialogVisible: false,
jsonDrawerVisible: false,
generateConf: null,
showFileName: false,
activeData: drawingDefalut[0],
saveDrawingListDebounce: debounce(340, saveDrawingList),
saveIdGlobalDebounce: debounce(340, saveIdGlobal),
leftComponents: [
{
title: '输入型组件',
list: inputComponents
},
{
title: '选择型组件',
list: selectComponents
},
{
title: '布局型组件',
list: layoutComponents
}
],
selfForm: {
name: null,
info: null,
id: null
}
}
},
computed: {
},
watch: {
// eslint-disable-next-line func-names
'activeData.__config__.label': function(val, oldVal) {
if (
this.activeData.placeholder === undefined ||
!this.activeData.__config__.tag ||
oldActiveId !== this.activeId
) {
return
}
this.activeData.placeholder = this.activeData.placeholder.replace(oldVal, '') + val
},
activeId: {
handler(val) {
oldActiveId = val
},
immediate: true
},
drawingList: {
handler(val) {
this.saveDrawingListDebounce(val)
if (val.length === 0) this.idGlobal = 100
},
deep: true
},
idGlobal: {
handler(val) {
this.saveIdGlobalDebounce(val)
},
immediate: true
}
},
mounted() {
if (this.editData.content) {
let { id, name, info, content } = this.editData
this.selfForm.name = name
this.selfForm.id = id
this.selfForm.info = info
content = JSON.parse(content)
this.drawingList = content.fields
const _content = JSON.parse(JSON.stringify(content))
delete _content.fields
this.formConf = _content
}
// if (Array.isArray(drawingListInDB) && drawingListInDB.length > 0) {
// this.drawingList = drawingListInDB
// } else {
// this.drawingList = drawingDefalut
// }
this.activeFormItem(this.drawingList[0])
// if (formConfInDB) {
// this.formConf = formConfInDB
// }
loadBeautifier(btf => {
beautifier = btf
})
const clipboard = new ClipboardJS('#copyNode', {
text: trigger => {
const codeStr = this.generateCode()
this.$notify({
title: '成功',
message: '代码已复制到剪切板,可粘贴。',
type: 'success'
})
return codeStr
}
})
clipboard.on('error', e => {
this.$message.error('代码复制失败')
})
},
methods: {
activeFormItem(element) {
this.activeData = element
this.activeId = element.__config__.formId
},
onEnd(obj) {
if (obj.from !== obj.to) {
this.activeData = tempActiveData
this.activeId = this.idGlobal
}
},
addComponent(item) {
const clone = this.cloneComponent(item)
this.drawingList.push(clone)
this.activeFormItem(clone)
},
cloneComponent(origin) {
const clone = JSON.parse(JSON.stringify(origin))
const config = clone.__config__
config.formId = ++this.idGlobal
config.span = this.formConf.span
config.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
if (config.layout === 'colFormItem') {
clone.__vModel__ = `field${this.idGlobal}`
clone.placeholder !== undefined && (clone.placeholder += config.label)
} else if (config.layout === 'rowFormItem') {
config.componentName = `row${this.idGlobal}`
config.gutter = this.formConf.gutter
}
tempActiveData = clone
return tempActiveData
},
AssembleFormData() {
this.formData = {
fields: JSON.parse(JSON.stringify(this.drawingList)),
...this.formConf
}
},
generate(data) {
const func = this[`exec${titleCase(this.operationType)}`]
this.generateConf = data
func && func(data)
},
execRun(data) {
this.AssembleFormData()
this.drawerVisible = true
},
execDownload(data) {
const codeStr = this.generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
saveAs(blob, data.fileName)
},
execCopy(data) {
document.getElementById('copyNode').click()
},
empty() {
this.$confirm('确定要清空所有组件吗?', '提示', { type: 'warning' }).then(
() => {
this.drawingList = []
this.idGlobal = 100
}
)
},
drawingItemCopy(item, parent) {
let clone = JSON.parse(JSON.stringify(item))
clone = this.createIdAndKey(clone)
parent.push(clone)
this.activeFormItem(clone)
},
createIdAndKey(item) {
const config = item.__config__
config.formId = ++this.idGlobal
config.renderKey = +new Date()
if (config.layout === 'colFormItem') {
item.__vModel__ = `field${this.idGlobal}`
} else if (config.layout === 'rowFormItem') {
config.componentName = `row${this.idGlobal}`
}
if (Array.isArray(config.children)) {
config.children = config.children.map(childItem => this.createIdAndKey(childItem))
}
return item
},
drawingItemDelete(index, parent) {
parent.splice(index, 1)
this.$nextTick(() => {
const len = this.drawingList.length
if (len) {
this.activeFormItem(this.drawingList[len - 1])
}
})
},
generateCode() {
const { type } = this.generateConf
this.AssembleFormData()
const script = vueScript(makeUpJs(this.formData, type))
const html = vueTemplate(makeUpHtml(this.formData, type))
const css = cssStyle(makeUpCss(this.formData))
return beautifier.html(html + script + css, beautifierConf.html)
},
showJson() {
this.AssembleFormData()
this.jsonDrawerVisible = true
},
handlerSaveJSON:Debounce(function(form) {
// this.AssembleFormData()
// loadBeautifier(btf => {
// beautifier = btf
// let jsonStr = JSON.stringify(this.formData)
// this.beautifierJson = beautifier.js(jsonStr, beautifierConf.js)
//
// })
this.$refs[form].validate(result => {
if (!result) return
const formConfig = getFormConfSelf()
if (formConfig.fields.length === 0) {
this.$message.error('表单配置数据不能为空')
return
}
this.selfForm.content = JSON.stringify(formConfig)
this.$emit('getFormConfigDataResult', this.selfForm)
})
}),
download() {
this.dialogVisible = true
this.showFileName = true
this.operationType = 'download'
},
run() {
this.dialogVisible = true
this.showFileName = false
this.operationType = 'run'
},
copy() {
this.dialogVisible = true
this.showFileName = false
this.operationType = 'copy'
},
tagChange(newTag) {
newTag = this.cloneComponent(newTag)
const config = newTag.__config__
newTag.__vModel__ = this.activeData.__vModel__
config.formId = this.activeId
config.span = this.activeData.__config__.span
this.activeData.__config__.tag = config.tag
this.activeData.__config__.tagIcon = config.tagIcon
this.activeData.__config__.document = config.document
if (typeof this.activeData.__config__.defaultValue === typeof config.defaultValue) {
config.defaultValue = this.activeData.__config__.defaultValue
}
Object.keys(newTag).forEach(key => {
if (this.activeData[key] !== undefined) {
newTag[key] = this.activeData[key]
}
})
this.activeData = newTag
this.updateDrawingList(newTag, this.drawingList)
},
updateDrawingList(newTag, list) {
const index = list.findIndex(item => item.__config__.formId === this.activeId)
if (index > -1) {
list.splice(index, 1, newTag)
} else {
list.forEach(item => {
if (Array.isArray(item.__config__.children)) this.updateDrawingList(newTag, item.__config__.children)
})
}
},
refreshJson(data) {
this.drawingList = JSON.parse(JSON.stringify(data.fields))
delete data.fields
this.formConf = data
}
}
}
</script>
<style lang='scss'>
@import '../styles/home';
</style>
@@ -0,0 +1,123 @@
<template>
<div class="icon-dialog">
<el-dialog
v-bind="$attrs"
width="980px"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<div slot="title">
选择图标
<el-input
v-model="key"
size="mini"
:style="{width: '260px'}"
placeholder="请输入图标名称"
prefix-icon="el-icon-search"
clearable
/>
</div>
<ul class="icon-ul">
<li
v-for="icon in iconList"
:key="icon"
:class="active===icon?'active-item':''"
@click="onSelect(icon)"
>
<i :class="icon" />
<div>{{ icon }}</div>
</li>
</ul>
</el-dialog>
</div>
</template>
<script>
import iconList from '../utils/icon.json'
const originList = iconList.map(name => `el-icon-${name}`)
export default {
inheritAttrs: false,
props: ['current'],
data() {
return {
iconList: originList,
active: null,
key: ''
}
},
watch: {
key(val) {
if (val) {
this.iconList = originList.filter(name => name.indexOf(val) > -1)
} else {
this.iconList = originList
}
}
},
methods: {
onOpen() {
this.active = this.current
this.key = ''
},
onClose() {},
onSelect(icon) {
this.active = icon
this.$emit('select', icon)
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.icon-ul {
margin: 0;
padding: 0;
font-size: 0;
li {
list-style-type: none;
text-align: center;
font-size: 14px;
display: inline-block;
width: 16.66%;
box-sizing: border-box;
height: 108px;
padding: 15px 6px 6px 6px;
cursor: pointer;
overflow: hidden;
&:hover {
background: #f2f2f2;
}
&.active-item{
background: #e1f3fb;
color: #7a6df0
}
> i {
font-size: 30px;
line-height: 50px;
}
}
}
.icon-dialog {
::v-deep .el-dialog {
border-radius: 8px;
margin-bottom: 0;
margin-top: 4vh !important;
display: flex;
flex-direction: column;
max-height: 92vh;
overflow: hidden;
box-sizing: border-box;
.el-dialog__header {
padding-top: 14px;
}
.el-dialog__body {
margin: 0 20px 20px 20px;
padding: 0;
overflow: auto;
}
}
}
</style>
@@ -0,0 +1,158 @@
<template>
<div>
<el-drawer v-bind="$attrs" append-to-body v-on="$listeners" @opened="onOpen" @close="onClose">
<div class="action-bar" :style="{'text-align': 'left'}">
<span class="bar-btn" @click="refresh">
<i class="el-icon-refresh" />
刷新
</span>
<span ref="copyBtn" class="bar-btn copy-json-btn">
<i class="el-icon-document-copy" />
复制JSON
</span>
<span class="bar-btn" @click="exportJsonFile">
<i class="el-icon-download" />
导出JSON文件
</span>
<span class="bar-btn delete-btn" @click="$emit('update:visible', false)">
<i class="el-icon-circle-close" />
关闭
</span>
</div>
<div id="editorJson" class="json-editor" />
</el-drawer>
</div>
</template>
<script>
import { beautifierConf } from '../utils/index'
import ClipboardJS from 'clipboard'
import { saveAs } from 'file-saver'
import loadMonaco from '../utils/loadMonaco'
import loadBeautifier from '../utils/loadBeautifier'
// import * as monaco from "monaco-editor";
let beautifier
let monaco
export default {
components: {},
props: {
jsonStr: {
type: String,
required: true,
beautifier: null,
jsonEditor: null
}
},
data() {
return {}
},
computed: {},
watch: {},
created() {},
mounted() {
window.addEventListener('keydown', this.preventDefaultSave)
const clipboard = new ClipboardJS('.copy-json-btn', {
text: trigger => {
this.$notify({
title: '成功',
message: '代码已复制到剪切板,可粘贴。',
type: 'success'
})
return this.beautifierJson
}
})
clipboard.on('error', e => {
this.$message.error('代码复制失败')
})
},
beforeDestroy() {
window.removeEventListener('keydown', this.preventDefaultSave)
},
methods: {
preventDefaultSave(e) {
if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
}
},
onOpen() {
loadBeautifier(btf => {
beautifier = btf
this.beautifierJson = beautifier.js(this.jsonStr, beautifierConf.js)
loadMonaco(val => {
monaco = val
this.setEditorValue('editorJson', this.beautifierJson)
})
})
// monaco.editor.create(document.getElementById("editorJson"), {
// value: [
// "function x() {",
// '\tconsole.integralLog("Hello world!");',
// "}"].join(
// "\n"
// ),
// language: "javascript",
// theme: 'vs-dark',
// automaticLayout: true
// });
},
onClose() {},
setEditorValue(id, codeStr) {
if (this.jsonEditor) {
this.jsonEditor.setValue(codeStr)
} else {
this.jsonEditor = monaco.editor.create(document.getElementById(id), {
value: codeStr,
theme: 'vs-dark',
language: 'json',
automaticLayout: true
})
// ctrl + s 刷新
this.jsonEditor.onKeyDown(e => {
if (e.keyCode === 49 && (e.metaKey || e.ctrlKey)) {
this.refresh()
}
})
}
},
exportJsonFile() {
this.$prompt('文件名:', '导出文件', {
inputValue: `${+new Date()}.json`,
closeOnClickModal: false,
inputPlaceholder: '请输入文件名'
}).then(({ value }) => {
if (!value) value = `${+new Date()}.json`
const codeStr = this.jsonEditor.getValue()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
saveAs(blob, value)
})
},
refresh() {
try {
this.$emit('refresh', JSON.parse(this.jsonEditor.getValue()))
} catch (error) {
this.$notify({
title: '错误',
message: 'JSON格式错误,请检查',
type: 'error'
})
}
}
}
}
</script>
<style lang="scss" scoped>
@import '../styles/mixin';
::v-deep .el-drawer__header {
display: none;
}
@include action-bar;
.json-editor{
height: calc(100vh - 33px);
}
</style>
@@ -0,0 +1,114 @@
<template>
<div>
<el-dialog
v-bind="$attrs"
title="外部资源引用"
width="600px"
:close-on-click-modal="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<el-input
v-for="(item, index) in resources"
:key="index"
v-model="resources[index]"
class="url-item"
placeholder="请输入 css 或 js 资源路径"
prefix-icon="el-icon-link"
clearable
>
<el-button
slot="append"
icon="el-icon-delete"
@click="deleteOne(index)"
/>
</el-input>
<el-button-group class="add-item">
<el-button
plain
@click="addOne('https://cdn.bootcss.com/jquery/1.8.3/jquery.min.js')"
>
jQuery1.8.3
</el-button>
<el-button
plain
@click="addOne('https://unpkg.com/http-vue-loader')"
>
http-vue-loader
</el-button>
<el-button
icon="el-icon-circle-plus-outline"
plain
@click="addOne('')"
>
添加其他
</el-button>
</el-button-group>
<div slot="footer">
<el-button @click="close">
取消
</el-button>
<el-button
type="primary"
@click="handelConfirm"
>
确定
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
components: {},
inheritAttrs: false,
props: ['originResource'],
data() {
return {
resources: null
}
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {
onOpen() {
this.resources = this.originResource.length ? JSON.parse(JSON.stringify(this.originResource)) : ['']
},
onClose() {
},
close() {
this.$emit('update:visible', false)
},
handelConfirm() {
const results = this.resources.filter(item => !!item) || []
this.$emit('save', results)
this.close()
if (results.length) {
this.resources = results
}
},
deleteOne(index) {
this.resources.splice(index, 1)
},
addOne(url) {
if (this.resources.indexOf(url) > -1) {
this.$message('资源已存在')
} else {
this.resources.push(url)
}
}
}
}
</script>
<style lang="scss" scoped>
.add-item{
margin-top: 8px;
}
.url-item{
margin-bottom: 12px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,158 @@
<template>
<div>
<el-dialog
v-bind="$attrs"
:close-on-click-modal="false"
:modal-append-to-body="false"
v-on="$listeners"
@open="onOpen"
@close="onClose"
>
<el-row :gutter="0">
<el-form
ref="elForm"
:model="formData"
:rules="rules"
size="small"
label-width="100px"
>
<el-col :span="24">
<el-form-item
label="选项名"
prop="label"
>
<el-input
v-model="formData.label"
placeholder="请输入选项名"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item
label="选项值"
prop="value"
>
<el-input
v-model="formData.value"
placeholder="请输入选项值"
clearable
>
<el-select
slot="append"
v-model="dataType"
:style="{width: '100px'}"
>
<el-option
v-for="(item, index) in dataTypeOptions"
:key="index"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
/>
</el-select>
</el-input>
</el-form-item>
</el-col>
</el-form>
</el-row>
<div slot="footer">
<el-button
type="primary"
@click="handelConfirm"
>
确定
</el-button>
<el-button @click="close">
取消
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { isNumberStr } from '../utils/index'
import { getTreeNodeId, saveTreeNodeId } from '../utils/db'
const id = getTreeNodeId()
export default {
components: {},
inheritAttrs: false,
props: [],
data() {
return {
id,
formData: {
label: undefined,
value: undefined
},
rules: {
label: [
{
required: true,
message: '请输入选项名',
trigger: 'blur'
}
],
value: [
{
required: true,
message: '请输入选项值',
trigger: 'blur'
}
]
},
dataType: 'string',
dataTypeOptions: [
{
label: '字符串',
value: 'string'
},
{
label: '数字',
value: 'number'
}
]
}
},
computed: {},
watch: {
// eslint-disable-next-line func-names
'formData.value': function(val) {
this.dataType = isNumberStr(val) ? 'number' : 'string'
},
id(val) {
saveTreeNodeId(val)
}
},
created() {},
mounted() {},
methods: {
onOpen() {
this.formData = {
label: undefined,
value: undefined
}
},
onClose() {},
close() {
this.$emit('update:visible', false)
},
handelConfirm() {
this.$refs.elForm.validate(valid => {
if (!valid) return
if (this.dataType === 'number') {
this.formData.value = parseFloat(this.formData.value)
}
this.formData.id = this.id++
this.$emit('commit', this.formData)
this.close()
})
}
}
}
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,22 @@
<template>
<div>
<router-view />
</div>
</template>
<script>
export default {
mounted() {
// 取消开始的loading动画
const preLoader = document.querySelector('#pre-loader')
preLoader.style.display = 'none'
// fix: firefox 下 拖拽 会新打卡一个选项卡
// https://github.com/JakHuang/form-generator/issues/15
document.body.ondrop = event => {
event.preventDefault()
event.stopPropagation()
}
}
}
</script>
@@ -0,0 +1,59 @@
import Vue from 'vue'
import { loadScriptQueue } from '@/utils/loadScript'
import Tinymce from '@/components/tinymce'
Vue.component('tinymce', Tinymce)
const $previewApp = document.getElementById('previewApp')
const childAttrs = {
file: '',
dialog: ' width="600px" class="dialog-width" v-if="visible" :visible.sync="visible" :modal-append-to-body="false" '
}
window.addEventListener('message', init, false)
function buildLinks(links) {
let strs = ''
links.forEach(url => {
strs += `<link href="${url}" rel="stylesheet">`
})
return strs
}
function init(event) {
if (event.data.type === 'refreshFrame') {
const code = event.data.data
const attrs = childAttrs[code.generateConf.type]
let links = ''
if (Array.isArray(code.links) && code.links.length > 0) {
links = buildLinks(code.links)
}
$previewApp.innerHTML = `${links}<style>${code.css}</style><div id="app"></div>`
if (Array.isArray(code.scripts) && code.scripts.length > 0) {
loadScriptQueue(code.scripts, () => {
newVue(attrs, code.js, code.html)
})
} else {
newVue(attrs, code.js, code.html)
}
}
}
function newVue(attrs, main, html) {
main = eval(`(${main})`)
main.template = `<div>${html}</div>`
new Vue({
components: {
child: main
},
data() {
return {
visible: true
}
},
template: `<div><child ${attrs}/></div>`
}).$mount('#app')
}
@@ -0,0 +1,271 @@
$selectedColor: #f6f7ff;
$lighterBlue: #409EFF;
.container-FromGen {
position: relative;
width: 100%;
height: 100%;
}
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
}
}
.components-draggable{
padding-bottom: 20px;
}
.components-title{
font-size: 14px;
color: #222;
margin: 6px 2px;
.svg-icon{
color: #666;
font-size: 18px;
}
}
.components-body {
padding: 8px 10px;
background: $selectedColor;
font-size: 12px;
cursor: move;
border: 1px dashed $selectedColor;
border-radius: 3px;
.svg-icon{
color: #777;
font-size: 15px;
}
&:hover {
border: 1px dashed #787be8;
color: #787be8;
.svg-icon {
color: #787be8;
}
}
}
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: 100vh;
}
.left-scrollbar{
height: calc(100vh - 42px);
overflow: hidden;
}
.center-scrollbar {
height: calc(100vh - 42px);
overflow: hidden;
border-left: 1px solid #f1e8e8;
border-right: 1px solid #f1e8e8;
box-sizing: border-box;
}
.center-board {
height: 100vh;
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
}
.empty-info{
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: #ccb1ea;
letter-spacing: 4px;
}
.action-bar{
position: relative;
height: 42px;
text-align: right;
padding: 0 15px;
box-sizing: border-box;;
border: 1px solid #f1e8e8;
border-top: none;
border-left: none;
.delete-btn{
color: #F56C6C;
}
}
.logo-wrapper{
position: relative;
height: 42px;
background: #fff;
border-bottom: 1px solid #f1e8e8;
box-sizing: border-box;
}
.logo{
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: #00afff;
font-weight: 600;
font-size: 17px;
white-space: nowrap;
> img{
width: 30px;
height: 30px;
vertical-align: top;
}
.github{
display: inline-block;
vertical-align: sub;
margin-left: 15px;
> img{
height: 22px;
}
}
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
& > .el-form {
// 69 = 12+15+42
height: calc(100vh - 69px);
}
}
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: rgb(89, 89, 223);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background-color: $selectedColor;
}
.active-from-item {
& > .el-form-item{
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: initial;
}
& > .component-name{
color: $lighterBlue;
}
}
.el-form-item{
margin-bottom: 15px;
}
}
.drawing-item{
position: relative;
cursor: move;
&.unfocus-bordered:not(.active-from-item) > div:first-child {
border: 1px dashed #ccc;
}
.el-form-item{
padding: 12px 10px;
}
}
.drawing-row-item{
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed #ccc;
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col{
margin-top: 22px;
}
.el-form-item{
margin-bottom: 0;
}
.drag-wrapper{
min-height: 80px;
}
&.active-from-item{
border: 1px dashed $lighterBlue;
}
.component-name{
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: #bbb;
display: inline-block;
padding: 0 6px;
}
}
.drawing-item, .drawing-row-item{
&:hover {
& > .el-form-item{
background: $selectedColor;
border-radius: 6px;
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: initial;
}
}
& > .drawing-item-copy, & > .drawing-item-delete{
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
& > .drawing-item-copy{
right: 56px;
border-color: $lighterBlue;
color: $lighterBlue;
background: #fff;
&:hover{
background: $lighterBlue;
color: #fff;
}
}
& > .drawing-item-delete{
right: 24px;
border-color: #F56C6C;
color: #F56C6C;
background: #fff;
&:hover{
background: #F56C6C;
color: #fff;
}
}
}
@@ -0,0 +1,137 @@
$editorTabsborderColor: #121315;
body, html{
margin: 0;
padding: 0;
background: #fff;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
}
input, textarea{
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
}
.editor-tabs{
background: $editorTabsborderColor;
.el-tabs__header{
margin: 0;
border-bottom-color: $editorTabsborderColor;
.el-tabs__nav{
border-color: $editorTabsborderColor;
}
}
.el-tabs__item{
height: 32px;
line-height: 32px;
color: #888a8e;
border-left: 1px solid $editorTabsborderColor!important;
background: #363636;
margin-right: 5px;
user-select: none;
}
.el-tabs__item.is-active{
background: #1e1e1e;
border-bottom-color: #1e1e1e!important;
color: #fff;
}
.el-icon-edit{
color: #f1fa8c;
}
.el-icon-document{
color: #a95812;
}
}
// home
.right-scrollbar {
.el-scrollbar__view {
padding: 12px 18px 15px 15px;
}
}
.el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
}
.center-tabs{
.el-tabs__header{
margin-bottom: 0!important;
}
.el-tabs__item{
width: 50%;
text-align: center;
}
.el-tabs__nav{
width: 100%;
}
}
.reg-item{
padding: 12px 6px;
background: #f8f8f8;
position: relative;
border-radius: 4px;
.close-btn{
position: absolute;
right: -6px;
top: -6px;
display: block;
width: 16px;
height: 16px;
line-height: 16px;
background: rgba(0, 0, 0, 0.2);
border-radius: 50%;
color: #fff;
text-align: center;
z-index: 1;
cursor: pointer;
font-size: 12px;
&:hover{
background: rgba(210, 23, 23, 0.5)
}
}
& + .reg-item{
margin-top: 18px;
}
}
.action-bar{
& .el-button+.el-button {
margin-left: 15px;
}
& i {
font-size: 20px;
vertical-align: middle;
position: relative;
top: -1px;
}
}
.custom-tree-node{
width: 100%;
font-size: 14px;
.node-operation{
float: right;
}
i[class*="el-icon"] + i[class*="el-icon"]{
margin-left: 6px;
}
.el-icon-plus{
color: #409EFF;
}
.el-icon-delete{
color: #157a0c;
}
}
.el-scrollbar__view{
overflow-x: hidden;
}
.el-rate{
display: inline-block;
vertical-align: text-top;
}
.el-upload__tip{
line-height: 1.2;
}
@@ -0,0 +1,33 @@
@mixin action-bar {
.action-bar {
height: 33px;
background: #f2fafb;
padding: 0 15px;
box-sizing: border-box;
.bar-btn {
display: inline-block;
padding: 0 6px;
line-height: 32px;
color: #8285f5;
cursor: pointer;
font-size: 14px;
user-select: none;
& i {
font-size: 20px;
}
&:hover {
color: #4348d4;
}
}
.bar-btn + .bar-btn {
margin-left: 8px;
}
.delete-btn {
color: #f56c6c;
&:hover {
color: #ea0b30;
}
}
}
}
@@ -0,0 +1,67 @@
const DRAWING_ITEMS = 'drawingItems'
const DRAWING_ITEMS_VERSION = '1.1'
const DRAWING_ITEMS_VERSION_KEY = 'DRAWING_ITEMS_VERSION'
const DRAWING_ID = 'idGlobal'
const TREE_NODE_ID = 'treeNodeId'
const FORM_CONF = 'formConf'
export function getDrawingList() {
// 加入缓存版本的概念,保证缓存数据与程序匹配
const version = localStorage.getItem(DRAWING_ITEMS_VERSION_KEY)
if (version !== DRAWING_ITEMS_VERSION) {
localStorage.setItem(DRAWING_ITEMS_VERSION_KEY, DRAWING_ITEMS_VERSION)
saveDrawingList([])
return null
}
const str = localStorage.getItem(DRAWING_ITEMS)
if (str) return JSON.parse(str)
return null
}
export function saveDrawingList(list) {
localStorage.setItem(DRAWING_ITEMS, JSON.stringify(list))
}
export function getIdGlobal() {
const str = localStorage.getItem(DRAWING_ID)
if (str) return parseInt(str, 10)
return 100
}
export function saveIdGlobal(id) {
localStorage.setItem(DRAWING_ID, `${id}`)
}
export function getTreeNodeId() {
const str = localStorage.getItem(TREE_NODE_ID)
if (str) return parseInt(str, 10)
return 100
}
export function saveTreeNodeId(id) {
localStorage.setItem(TREE_NODE_ID, `${id}`)
}
export function getFormConf() {
const str = localStorage.getItem(FORM_CONF)
if (str) return JSON.parse(str)
return null
}
export function saveFormConf(obj) {
localStorage.setItem(FORM_CONF, JSON.stringify(obj))
}
/**
* 根据自己的需求获取配置的表单信息
*/
export function getFormConfSelf() {
let formConfig = localStorage.getItem(FORM_CONF)
formConfig = JSON.parse(formConfig)
let formItemConfig = localStorage.getItem(DRAWING_ITEMS)
if (!formConfig && !formItemConfig) return 'Error'
formItemConfig = JSON.parse(formItemConfig)
formConfig.fields = formItemConfig
return formConfig
}
@@ -0,0 +1 @@
["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"]
@@ -0,0 +1,102 @@
/**
* num 小于0,左缩进num*2个空格; 大于0,右缩进num*2个空格。
* @param {string} str 代码
* @param {number} num 缩进次数
* @param {number} len 【可选】缩进单位,空格数
*/
export function indent(str, num, len = 2) {
if (num === 0) return str
const isLeft = num < 0; const result = []; let reg; let
spaces = ''
if (isLeft) {
num *= -1
reg = new RegExp(`(^\\s{0,${num * len}})`, 'g')
} else {
for (let i = 0; i < num * len; i++) spaces += ' '
}
str.split('\n').forEach(line => {
line = isLeft ? line.replace(reg, '') : spaces + line
result.push(line)
})
return result.join('\n')
}
// 首字母大小
export function titleCase(str) {
return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
}
// 下划转驼峰
export function camelCase(str) {
return str.replace(/-[a-z]/g, str1 => str1.substr(-1).toUpperCase())
}
export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
}
export const exportDefault = 'export default '
export const beautifierConf = {
html: {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '-1',
preserve_newlines: false,
keep_array_indentation: false,
break_chained_methods: false,
indent_scripts: 'separate',
brace_style: 'end-expand',
space_before_conditional: true,
unescape_strings: false,
jslint_happy: false,
end_with_newline: true,
wrap_line_length: '110',
indent_inner_html: true,
comma_first: false,
e4x: true,
indent_empty_lines: true
},
js: {
indent_size: '2',
indent_char: ' ',
max_preserve_newlines: '-1',
preserve_newlines: false,
keep_array_indentation: false,
break_chained_methods: false,
indent_scripts: 'normal',
brace_style: 'end-expand',
space_before_conditional: true,
unescape_strings: false,
jslint_happy: true,
end_with_newline: true,
wrap_line_length: '110',
indent_inner_html: true,
comma_first: false,
e4x: true,
indent_empty_lines: true
}
}
function stringify(obj) {
return JSON.stringify(obj, (key, val) => {
if (typeof val === 'function') {
return `${val}`
}
return val
})
}
function parse(str) {
JSON.parse(str, (k, v) => {
if (v.indexOf && v.indexOf('function') > -1) {
return eval(`(${v})`)
}
return v
})
}
export function jsonClone(obj) {
return parse(stringify(obj))
}
@@ -0,0 +1,26 @@
import loadScript from './loadScript'
import ELEMENT from 'element-ui'
let beautifierObj
export default function loadBeautifier(cb) {
if (beautifierObj) {
cb(beautifierObj)
return
}
const loading = ELEMENT.Loading.service({
fullscreen: true,
lock: true,
text: '格式化资源加载中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.5)'
})
loadScript('https://cdn.bootcss.com/js-beautify/1.10.2/beautifier.min.js', () => {
loading.close()
// eslint-disable-next-line no-undef
beautifierObj = beautifier
cb(beautifierObj)
})
}
@@ -0,0 +1,42 @@
import { loadScriptQueue } from './loadScript'
import ELEMENT from 'element-ui'
// monaco-editor单例
let monacoEidtor
/**
* 动态加载monaco-editor cdn资源
* @param {Function} cb 回调,必填
*/
export default function loadMonaco(cb) {
if (monacoEidtor) {
cb(monacoEidtor)
return
}
const vs = 'https://cdn.bootcss.com/monaco-editor/0.18.0/min/vs'
// 使用element ui实现加载提示
const loading = ELEMENT.Loading.service({
fullscreen: true,
lock: true,
text: '编辑器资源初始化中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.5)'
})
!window.require && (window.require = {})
!window.require.paths && (window.require.paths = {})
window.require.paths.vs = vs
loadScriptQueue([
`${vs}/loader.js`,
`${vs}/editor/editor.main.nls.js`,
`${vs}/editor/editor.main.js`
], () => {
loading.close()
// eslint-disable-next-line no-undef
monacoEidtor = monaco
cb(monacoEidtor)
})
}
@@ -0,0 +1,60 @@
const callbacks = {}
/**
* 加载一个远程脚本
* @param {String} src 一个远程脚本
* @param {Function} callback 回调
*/
function loadScript(src, callback) {
const existingScript = document.getElementById(src)
const cb = callback || (() => {})
if (!existingScript) {
callbacks[src] = []
const $script = document.createElement('script')
$script.src = src
$script.id = src
$script.async = 1
document.body.appendChild($script)
const onEnd = 'onload' in $script ? stdOnEnd.bind($script) : ieOnEnd.bind($script)
onEnd($script)
}
callbacks[src].push(cb)
function stdOnEnd(script) {
script.onload = () => {
this.onerror = this.onload = null
callbacks[src].forEach(item => {
item(null, script)
})
delete callbacks[src]
}
script.onerror = () => {
this.onerror = this.onload = null
cb(new Error(`Failed to load ${src}`), script)
}
}
function ieOnEnd(script) {
script.onreadystatechange = () => {
if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
this.onreadystatechange = null
callbacks[src].forEach(item => {
item(null, script)
})
delete callbacks[src]
}
}
}
/**
* 顺序加载一组远程脚本
* @param {Array} list 一组远程脚本
* @param {Function} cb 回调
*/
export function loadScriptQueue(list, cb) {
const first = list.shift()
list.length ? loadScript(first, () => loadScriptQueue(list, cb)) : loadScript(first, cb)
}
export default loadScript
@@ -0,0 +1,26 @@
import loadScript from './loadScript'
import ELEMENT from 'element-ui'
let tinymceObj
export default function loadTinymce(cb) {
if (tinymceObj) {
cb(tinymceObj)
return
}
const loading = ELEMENT.Loading.service({
fullscreen: true,
lock: true,
text: '富文本资源加载中...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.5)'
})
loadScript('https://cdn.bootcdn.net/ajax/libs/tinymce/5.2.2/tinymce.min.js', () => {
loading.close()
// eslint-disable-next-line no-undef
tinymceObj = tinymce
cb(tinymceObj)
})
}
+44
View File
@@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>
+185
View File
@@ -0,0 +1,185 @@
<template>
<div :class="{'show':show}" class="header-search">
<i class="iconfont iconios-search" style="font-size: 20px;" @click.stop="click"></i>
<!--<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />-->
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="搜索菜单"
class="header-search-select"
@change="change"
>
<el-option v-for="item in options" :key="item.url" :value="item" :label="item.name.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js'
import path from 'path'
import { mapGetters } from 'vuex'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
}
},
computed: {
...mapGetters([
'permission_routes'
]),
// routes() {
// return this.$store.getters.permission_routes
// }
},
watch: {
routes(n) {
this.searchPool = this.generateRoutes(this.permission_routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.permission_routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
this.$router.push(val.path)
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'name',
weight: 0.7
}, {
name: 'url',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: path.resolve(basePath, router.url),
name: [...prefixTitle],
children: router.child || []
}
if (router.name) {
data.name = [...data.name, router.name]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.child) {
const tempRoutes = this.generateRoutes(router.child, data.url, data.name)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
display: inline-flex !important;
cursor: pointer;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
/*vertical-align: middle;*/
line-height: 50px;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
/*border-bottom: 1px solid #d9d9d9;*/
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<template>
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
<div class="pan-info">
<div class="pan-info-roles-container">
<slot />
</div>
</div>
<!-- eslint-disable-next-line -->
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
</div>
</template>
<script>
export default {
name: 'PanThumb',
props: {
image: {
type: String,
required: true
},
zIndex: {
type: Number,
default: 1
},
width: {
type: String,
default: '150px'
},
height: {
type: String,
default: '150px'
}
}
}
</script>
<style scoped>
.pan-item {
width: 200px;
height: 200px;
border-radius: 50%;
display: inline-block;
position: relative;
cursor: default;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.pan-info-roles-container {
padding: 20px;
text-align: center;
}
.pan-thumb {
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
border-radius: 50%;
overflow: hidden;
position: absolute;
transform-origin: 95% 40%;
transition: all 0.3s ease-in-out;
}
/* .pan-thumb:after {
content: '';
width: 8px;
height: 8px;
position: absolute;
border-radius: 50%;
top: 40%;
left: 95%;
margin: -4px 0 0 -4px;
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
} */
.pan-info {
position: absolute;
width: inherit;
height: inherit;
border-radius: 50%;
overflow: hidden;
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
}
.pan-info h3 {
color: #fff;
text-transform: uppercase;
position: relative;
letter-spacing: 2px;
font-size: 18px;
margin: 0 60px;
padding: 22px 0 0 0;
height: 85px;
font-family: 'Open Sans', Arial, sans-serif;
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
}
.pan-info p {
color: #fff;
padding: 10px 5px;
font-style: italic;
margin: 0 30px;
font-size: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.pan-info p a {
display: block;
color: #333;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: #fff;
font-style: normal;
font-weight: 700;
text-transform: uppercase;
font-size: 9px;
letter-spacing: 1px;
padding-top: 24px;
margin: 7px auto 0;
font-family: 'Open Sans', Arial, sans-serif;
opacity: 0;
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
transform: translateX(60px) rotate(90deg);
}
.pan-info p a:hover {
background: rgba(255, 255, 255, 0.5);
}
.pan-item:hover .pan-thumb {
transform: rotate(-110deg);
}
.pan-item:hover .pan-info p a {
opacity: 1;
transform: translateX(0px) rotate(0deg);
}
</style>
@@ -0,0 +1,3 @@
<template >
<router-view />
</template>
+149
View File
@@ -0,0 +1,149 @@
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
import { addClass, removeClass } from '@/utils'
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
},
buttonTop: {
default: 250,
type: Number
}
},
computed: {
show: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
theme() {
return this.$store.state.settings.theme
},
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
if (value) {
addClass(document.body, 'showRightPanel')
} else {
removeClass(document.body, 'showRightPanel')
}
}
},
mounted() {
this.insertToBody()
this.addEventClick()
},
beforeDestroy() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.rightPanel')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
},
insertToBody() {
const elx = this.$refs.rightPanel
const body = document.querySelector('body')
body.insertBefore(elx, body.firstChild)
}
}
}
</script>
<style>
.showRightPanel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
}
</style>
<style lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(0, 0, .25, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(0, 0, .25, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.show {
transition: all .3s cubic-bezier(0, 0, .25, 1);
.rightPanel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
}
.rightPanel {
transform: translate(0);
}
}
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>
+61
View File
@@ -0,0 +1,61 @@
<template>
<div>
<i class="iconfont iconios-qr-scanner" style="font-size: 20px;" @click="click"></i>
<!--<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />-->
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.enabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.enabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>
+103
View File
@@ -0,0 +1,103 @@
<template>
<div :class="{active:isActive}" class="share-dropdown-menu">
<div class="share-dropdown-menu-wrapper">
<span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
<div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
<a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
<span v-else>{{ item.title }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: function() {
return []
}
},
title: {
type: String,
default: 'vue'
}
},
data() {
return {
isActive: false
}
},
methods: {
clickTitle() {
this.isActive = !this.isActive
}
}
}
</script>
<style lang="scss" >
$n: 9; //和items.length 相同
$t: .1s;
.share-dropdown-menu {
width: 250px;
position: relative;
z-index: 1;
height: auto!important;
&-title {
width: 100%;
display: block;
cursor: pointer;
background: black;
color: white;
height: 60px;
line-height: 60px;
font-size: 20px;
text-align: center;
z-index: 2;
transform: translate3d(0,0,0);
}
&-wrapper {
position: relative;
}
&-item {
text-align: center;
position: absolute;
width: 100%;
background: #e0e0e0;
color: #000;
line-height: 60px;
height: 60px;
cursor: pointer;
font-size: 18px;
overflow: hidden;
opacity: 1;
transition: transform 0.28s ease;
&:hover {
background: black;
color: white;
}
@for $i from 1 through $n {
&:nth-of-type(#{$i}) {
z-index: -1;
transition-delay: $i*$t;
transform: translate3d(0, -60px, 0);
}
}
}
&.active {
.share-dropdown-menu-wrapper {
z-index: 1;
}
.share-dropdown-menu-item {
@for $i from 1 through $n {
&:nth-of-type(#{$i}) {
transition-delay: ($n - $i)*$t;
transform: translate3d(0, ($i - 1)*60px, 0);
}
}
}
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div :style="{height:height+'px',zIndex:zIndex}">
<div
:class="className"
:style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
>
<slot>
<div>sticky</div>
</slot>
</div>
</div>
</template>
<script>
export default {
name: 'Sticky',
props: {
stickyTop: {
type: Number,
default: 0
},
zIndex: {
type: Number,
default: 1
},
className: {
type: String,
default: ''
}
},
data() {
return {
active: false,
position: '',
width: undefined,
height: undefined,
isSticky: false
}
},
mounted() {
this.height = this.$el.getBoundingClientRect().height
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('resize', this.handleResize)
},
activated() {
this.handleScroll()
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleResize)
},
methods: {
sticky() {
if (this.active) {
return
}
this.position = 'fixed'
this.active = true
this.width = this.width + 'px'
this.isSticky = true
},
handleReset() {
if (!this.active) {
return
}
this.reset()
},
reset() {
this.position = ''
this.width = 'auto'
this.active = false
this.isSticky = false
},
handleScroll() {
const width = this.$el.getBoundingClientRect().width
this.width = width || 'auto'
const offsetTop = this.$el.getBoundingClientRect().top
if (offsetTop < this.stickyTop) {
this.sticky()
return
}
this.handleReset()
},
handleResize() {
if (this.isSticky) {
this.width = this.$el.getBoundingClientRect().width + 'px'
}
}
}
}
</script>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
+174
View File
@@ -0,0 +1,174 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const $message = this.$message({
message: ' Compiling the theme',
customClass: 'theme-message',
type: 'success',
duration: 0,
iconClass: 'el-icon-loading'
})
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
$message.close()
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<div>
<el-time-select
placeholder="起始时间"
v-model="startTime"
:picker-options="{
start: '00:00',
step: '01:00',
end: '24:00'
}">
</el-time-select>
<el-time-select
placeholder="结束时间"
v-model="endTime"
:picker-options="{
start: '00:00',
step: '01:00',
end: '24:00',
minTime: startTime
}">
</el-time-select>
</div>
</template>
<script>
export default {
name: "index",
data() {
return {
startTime: '',
endTime: ''
}
},
props: {
value: {}
},
beforeMount(){
// 接收 v-model 数据
if(this.value){
this.startTime = this.value.split(',')[0]
this.endTime = this.value.split(',')[1]
}
},
watch: {
startTime: function(val) {
this.$emit('input', [val, this.endTime].join(','))
},
endTime: function(val) {
this.$emit('input', [this.startTime, val].join(','))
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,47 @@
<template>
<div class="upload-container">
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click="modalPicTap('2')"> upload</el-button>
</div>
</template>
<script>
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
modalPicTap(tit) {
const _this = this
this.$modalUpload(function(img) {
let arr = [];
if(img.length>10) return this.$message.warning("最多选择10张图片!");
img.map((item) => {
arr.push(item.sattDir)
});
// console.log(arr);
_this.$emit('successCBK', arr)
}, tit, 'content')
},
}
}
</script>
<style lang="scss" scoped>
.editor-slide-upload {
margin-bottom: 20px;
::v-deep .el-upload--picture-card {
width: 100%;
}
}
</style>
@@ -0,0 +1,59 @@
let callbacks = []
function loadedTinymce() {
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
// check is successfully downloaded script
return window.tinymce
}
const dynamicLoadScript = (src, callback) => {
const existingScript = document.getElementById(src)
const cb = callback || function() {}
if (!existingScript) {
const script = document.createElement('script')
script.src = src // src url for the third-party library being loaded.
script.id = src
document.body.appendChild(script)
callbacks.push(cb)
const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
onEnd(script)
}
if (existingScript && cb) {
if (loadedTinymce()) {
cb(null, existingScript)
} else {
callbacks.push(cb)
}
}
function stdOnEnd(script) {
script.onload = function() {
// this.onload = null here is necessary
// because even IE9 works not like others
this.onerror = this.onload = null
for (const cb of callbacks) {
cb(null, script)
}
callbacks = null
}
script.onerror = function() {
this.onerror = this.onload = null
cb(new Error('Failed to load ' + src), script)
}
}
function ieOnEnd(script) {
script.onreadystatechange = function() {
if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
this.onreadystatechange = null
for (const cb of callbacks) {
cb(null, script) // there is no way to catch loading errors in IE8
}
callbacks = null
}
}
}
export default dynamicLoadScript
+231
View File
@@ -0,0 +1,231 @@
<template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea" />
<div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
</div>
</div>
</template>
<script>
import editorImage from './components/EditorImage'
import plugins from './plugins'
import toolbar from './toolbar'
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: Number,
required: false,
default: 400
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
languageTypeList: {
'en': 'en',
'zh': 'zh_CN'
}
}
},
computed: {
language() {
// return this.languageTypeList[this.$store.getters.language]
return this.languageTypeList['zh']
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
language() {
this.destroyTinymce()
this.$nextTick(() => this.initTinymce())
}
},
mounted() {
this.initTinymce()
},
activated() {
this.initTinymce()
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
initTinymce() {
const _this = this
window.tinymce.init({
language: this.language,
selector: `#${this.tinymceId}`,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
convert_urls: false, //防止路径被转化为相对路径
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
}
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
const _this = this;
arr.forEach((v) => {
if (this.getFileType(v) == "video") {
window.tinymce
.get(_this.tinymceId)
.insertContent(
`<video class="wscnph" src="${v}" controls muted></video>`
);
} else {
window.tinymce
.get(_this.tinymceId)
.insertContent(`<img class="wscnph" src="${v}" />`);
}
});
},
getFileType(fileName) {
// 后缀获取
let suffix = "";
// 获取类型结果
let result = "";
try {
const flieArr = fileName.split(".");
suffix = flieArr[flieArr.length - 1];
} catch (err) {
suffix = "";
}
// fileName无后缀返回 false
if (!suffix) {
return false;
}
suffix = suffix.toLocaleLowerCase();
// 图片格式
const imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
// 进行图片匹配
result = imglist.find((item) => item === suffix);
if (result) {
return "image";
}
// 匹配 视频
const videolist = [
"mp4",
"m2v",
"mkv",
"rmvb",
"wmv",
"avi",
"flv",
"mov",
"m4v",
];
result = videolist.find((item) => item === suffix);
if (result) {
return "video";
}
// 其他 文件类型
return "other";
},
}
}
</script>
<style scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container>>>.mce-fullscreen {
z-index: 10000;
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
</style>
+7
View File
@@ -0,0 +1,7 @@
// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
export default plugins
+6
View File
@@ -0,0 +1,6 @@
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample fontsizeselect fontselect', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor ']
export default toolbar
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
</div>
</template>
@@ -0,0 +1,69 @@
<template>
<div class="upload-container">
<el-upload
class="upload-demo mr10 mb15"
action
:http-request="handleUploadForm"
:headers="myHeaders"
:show-file-list="false"
multiple
>
<div v-if="url" class="upLoadPicBox">
<div class="upLoad">
<i class="el-icon-document-checked cameraIconfont" />
</div>
</div>
<el-button v-else size="mini" type="primary" v-hasPermi="['admin:upload:file']">点击上传</el-button>
</el-upload>
</div>
</template>
<script>
import { fileFileApi } from '@/api/systemSetting'
import { getToken } from '@/utils/auth'
export default {
name: 'UploadFile',
props: {
value: {}
},
data() {
return {
myHeaders: { 'X-Token': getToken() },
url: ''
}
},
beforeMount() {
if (this.value) {
this.url = this.value
}
},
methods: {
// 上传
handleUploadForm(param) {
const formData = new FormData()
const data = {
model: this.$route.path.split('/')[1],
pid: 10
}
formData.append('multipart', param.file)
const loading = this.$loading({
lock: true,
text: '上传中,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
fileFileApi(formData, data).then(res => {
loading.close()
this.url = res.url
this.$emit('input', this.url)
this.$message.success('上传成功')
}).catch(res => {
loading.close()
})
}
}
}
</script>
<style scoped>
</style>
+138
View File
@@ -0,0 +1,138 @@
<template>
<div>
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
Drop excel file here or
<el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
Browse
</el-button>
</div>
</div>
</template>
<script>
import XLSX from 'xlsx'
export default {
props: {
beforeUpload: Function, // eslint-disable-line
onSuccess: Function// eslint-disable-line
},
data() {
return {
loading: false,
excelData: {
header: null,
results: null
}
}
},
methods: {
generateData({ header, results }) {
this.excelData.header = header
this.excelData.results = results
this.onSuccess && this.onSuccess(this.excelData)
},
handleDrop(e) {
e.stopPropagation()
e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files
if (files.length !== 1) {
this.$message.error('Only support uploading one file!')
return
}
const rawFile = files[0] // only use files[0]
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
this.upload(rawFile)
e.stopPropagation()
e.preventDefault()
},
handleDragover(e) {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleUpload() {
this.$refs['excel-upload-input'].click()
},
handleClick(e) {
const files = e.target.files
const rawFile = files[0] // only use files[0]
if (!rawFile) return
this.upload(rawFile)
},
upload(rawFile) {
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
if (!this.beforeUpload) {
this.readerData(rawFile)
return
}
const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.getHeaderRow(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateData({ header, results })
this.loading = false
resolve()
}
reader.readAsArrayBuffer(rawFile)
})
},
getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
}
}
}
</script>
<style scoped>
.excel-upload-input{
display: none;
z-index: -9999;
}
.drop{
border: 2px dashed #bbb;
width: 600px;
height: 160px;
line-height: 160px;
margin: 0 auto;
font-size: 24px;
border-radius: 5px;
text-align: center;
color: #bbb;
position: relative;
}
</style>
@@ -0,0 +1,22 @@
import Element from 'element-ui'
import '@/styles/element-variables.scss'
import articleFromComponent from './index.vue'
import Vue from 'vue'
import Cookies from 'js-cookie'
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
const articleFrom = {}
articleFrom.install = function(Vue, options) {
const ToastConstructor = Vue.extend(articleFromComponent)
// 生成一个该子类的实例
const instance = new ToastConstructor()
instance.$mount(document.createElement('div'))
document.body.appendChild(instance.$el)
Vue.prototype.$modalArticle = function(callback, handle) {
instance.visible = true
instance.callback = callback
instance.handle = handle
}
}
export default articleFrom
@@ -0,0 +1,48 @@
<template>
<div>
<el-dialog
title="提示"
:visible.sync="visible"
width="896px"
:before-close="handleClose"
>
<article-list v-if="visible" :handle="handle" :userIds="userIds" :couponId="couponId" @getArticle="getArticle" :keyNum="keyNum"></article-list>
</el-dialog>
</div>
</template>
<script>
import articleList from '../index.vue'
export default {
name: 'CouponFrom',
components:{ articleList },
data() {
return {
visible: false,
callback: function() {},
handle: '',
keyNum: 0,
couponId: [],
userIds: ''
}
},
watch: {
// show() {
// this.visible = this.show
// }
},
methods: {
handleClose() {
this.visible = false
},
getArticle(couponObj) {
this.callback(couponObj)
this.visible = false
}
}
}
</script>
<style scoped>
</style>
+161
View File
@@ -0,0 +1,161 @@
<template>
<div class="divBox">
<div class="header clearfix">
<div class="container">
<el-form inline>
<el-form-item>
<el-input v-model="listPram.keywords" placeholder="请输入关键词" clearable />
</el-form-item>
<el-form-item>
<el-cascader v-model="listPram.cid" :options="categoryTreeData" :props="categoryProps" placeholer="选择分类" clearable />
</el-form-item>
<el-form-item>
<el-button size="mini" type="primary" @click="handerSearch">搜索</el-button>
</el-form-item>
</el-form>
</div>
</div>
<el-table :data="listData.list" style="width: 100%"
size="mini"
max-height="400"
tooltip-effect="dark"
highlight-current-row>
<el-table-column
label=""
width="55">
<template slot-scope="{ row, index }">
<el-radio v-model="templateRadio" :label="row.id" @change.native="getTemplateRow(row)">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column label="图片" min-width="80">
<template slot-scope="scope">
<div class="demo-image__preview">
<el-image
style="width: 36px; height: 36px"
:src="scope.row.imageInput"
:preview-src-list="imgList"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="150" />
<el-table-column prop="visit" label="浏览量" min-width="100">
<template slot-scope="scope">
<span>{{ scope.row.visit | filterEmpty }}</span>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="150"/>
<!--<el-table-column label="操作" min-width="150">-->
<!--<template slot-scope="scope">-->
<!--<el-button type="warning" disabled>关联产品</el-button>-->
<!--</template>-->
<!--</el-table-column>-->
</el-table>
<div class="block mb20">
<el-pagination
:current-page="listPram.page"
:page-sizes="constants.page.limit"
:layout="constants.page.layout"
:total="listData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script>
import * as articleApi from '@/api/article.js'
import * as categoryApi from '@/api/categoryApi.js'
import * as selfUtil from '@/utils/ZBKJIutil.js'
export default {
// name: "list",
props: {
handle: {
type: String,
default: ''
}
},
data() {
return {
templateRadio:'',
imgList: [],
constants: this.$constants,
listPram: {
keywords: null,
cid: null,
page: 1,
limit: this.$constants.page.limit[0]
},
listData: { list: [], total: 0 },
editDialogConfig: {
visible: false,
data: {},
isEdit: 0 // 0=add 1=edit
},
categoryTreeData: [],
categoryProps: {
value: 'id',
label: 'name',
children: 'child',
expandTrigger: 'hover',
checkStrictly: true,
emitPath: false
}
}
},
mounted() {
this.handlerGetListData(this.listPram)
this.handlerGetCategoryTreeData()
},
methods: {
getTemplateRow(row){
this.$emit('getArticle', row)
},
handerSearch() {
this.listPram.page = 1
this.handlerGetListData(this.listPram)
},
handlerGetListData(pram) {
articleApi.ListArticle(pram).then(data => {
this.listData = data
// this.listData.list.map((item) => {
// item.imageInput.map(i => {
// this.imgList.push(i)
// })
// })
})
},
handlerGetCategoryTreeData() {
const _pram = { type: this.constants.categoryType[2].value, status: 1 }
categoryApi.treeCategroy(_pram).then(data => {
this.categoryTreeData = selfUtil.addTreeListLabelForCasCard(data)
})
},
handlerHideDialog() {
this.handlerGetListData(this.listPram)
this.editDialogConfig.visible = false
},
handlerDelete(rowData) {
this.$confirm('确定删除当前数据', '提示').then(result => {
articleApi.DelArticle(rowData).then(data => {
this.$message.success('删除数据成功')
this.handlerGetListData(this.listPram)
})
})
},
handleSizeChange(val) {
this.listPram.limit = val
this.handlerGetListData(this.listPram)
},
handleCurrentChange(val) {
this.listPram.page = val
this.handlerGetListData(this.listPram)
}
}
}
</script>
<style scoped>
</style>
+277
View File
@@ -0,0 +1,277 @@
<template>
<div>
<el-form ref="formDynamic" size="small" :model="formDynamic" v-loading="loading" :rules="rules" class="attrFrom mb20" label-width="100px" @submit.native.prevent>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="规格名称:" prop="ruleName">
<el-input  maxlength="20" v-model="formDynamic.ruleName" placeholder="请输入标题名称" />
</el-form-item>
</el-col>
<el-col v-for="(item, index) in formDynamic.ruleValue" :key="index" :span="24" class="noForm">
<el-form-item>
<div class="acea-row row-middle"><span class="mr5">{{ item.value }}</span><i class="el-icon-circle-close" @click="handleRemove(index)" /></div>
<div class="rulesBox">
<el-tag
v-for="(j, indexn) in item.detail"
:key="indexn"
closable
size="medium"
:disable-transitions="false"
class="mb5 mr10"
@close="handleClose(item.detail,indexn)"
>
{{ j }}
</el-tag>
<el-input
v-if="item.inputVisible"
ref="saveTagInput"
v-model="item.detail.attrsVal"
class="input-new-tag"
size="small"
@keyup.enter.native="createAttr(item.detail.attrsVal,index)"
@blur="createAttr(item.detail.attrsVal,index)"
/>
<el-button v-else class="button-new-tag" size="small" @click="showInput(item)">+ 添加</el-button>
</div>
</el-form-item>
</el-col>
<el-col v-if="isBtn" :span="24" class="mt10" style="padding-left: 0;padding-right: 0;">
<el-col :span="8">
<el-form-item label="规格:">
<el-input v-model="attrsName" placeholder="请输入规格" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="规格值:">
<el-input v-model="attrsVal" placeholder="请输入规格值" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-button type="primary" class="mr10" @click="createAttrName">确定</el-button>
<el-button @click="offAttrName">取消</el-button>
</el-col>
</el-col>
<Spin v-if="spinShow" size="large" fix />
</el-row>
<el-button v-if="!isBtn" type="primary" icon="md-add" class="ml40 mt10" @click="addBtn">添加新规格</el-button>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="dialogFormVisible = false"> </el-button>
</div>
</el-form>
<span class="footer acea-row">
<el-button @click="resetForm('formDynamic')">取消</el-button>
<el-button type="primary" :loading="loadingBtn" @click="handleSubmit('formDynamic')"> </el-button>
</span>
</div>
</template>
<script>
import { attrCreatApi, attrEditApi } from '@/api/store'
export default {
name: 'CreatAttr',
props: {
currentRow: {
type: Object,
default: null
},
keyNum: {
type: Number,
default: 0
}
},
data() {
return {
loadingBtn: false,
loading: false,
dialogVisible: false,
inputVisible: false,
inputValue: '',
spinShow: false,
grid: {
xl: 3,
lg: 3,
md: 12,
sm: 24,
xs: 24
},
modal: false,
index: 1,
rules: {
ruleName: [
{ required: true, message: '请输入规格名称', trigger: 'blur' }
]
},
formDynamic: {
ruleName: '',
ruleValue: []
},
attrsName: '',
attrsVal: '',
isBtn: false,
results: [],
result: [],
ids: 0
}
},
watch: {
currentRow: {
handler: function(val, oldVal) {
this.formDynamic = val
},
immediate: true
},
keyNum: {
deep: true,
handler(val) {
if( val>0 ) this.clear()
}
}
},
mounted() {
this.formDynamic.ruleValue.map(item => {
this.$set(item, 'inputVisible', false)
})
},
methods: {
resetForm(formName) {
this.$msgbox.close()
this.clear()
this.$refs[formName].resetFields()
},
// 添加按钮
addBtn() {
this.isBtn = true
},
handleClose(item, index) {
item.splice(index, 1)
},
// 取消
offAttrName() {
this.isBtn = false
},
// 删除
handleRemove(index) {
this.formDynamic.ruleValue.splice(index, 1)
},
// 添加规则名称
createAttrName() {
if (this.attrsName && this.attrsVal) {
const data = {
value: this.attrsName,
detail: [
this.attrsVal
]
}
this.formDynamic.ruleValue.push(data)
var hash = {}
this.formDynamic.ruleValue = this.formDynamic.ruleValue.reduce(function(item, next) {
/* eslint-disable */
hash[next.value] ? '' : hash[next.value] = true && item.push(next);
return item
}, [])
this.attrsName = '';
this.attrsVal = '';
this.isBtn = false;
} else {
this.$message.warning('请添加规格名称');
}
},
// 添加属性
createAttr (num, idx) {
if (num) {
this.formDynamic.ruleValue[idx].detail.push(num);
var hash = {};
this.formDynamic.ruleValue[idx].detail = this.formDynamic.ruleValue[idx].detail.reduce(function (item, next) {
/* eslint-disable */
hash[next] ? '' : hash[next] = true && item.push(next);
return item
}, [])
this.formDynamic.ruleValue[idx].inputVisible = false
} else {
this.$message.warning('请添加属性');
}
},
showInput(item) {
this.$set(item, 'inputVisible', true)
},
// 提交
handleSubmit (name) {
const data={
"id": this.currentRow.id || 0,
"ruleName": this.formDynamic.ruleName,
"ruleValue": JSON.stringify(this.formDynamic.ruleValue)
}
this.$refs[name].validate((valid) => {
if (valid) {
if (this.formDynamic.ruleValue.length === 0) {
return this.$message.warning('请至少添加一条属性规格!');
}
this.loadingBtn = true
this.loading = true
setTimeout(() => {
this.currentRow.id? attrEditApi(data).then(res => {
this.$message.success('提交成功');
this.$msgbox.close()
this.clear();
this.$emit('getList');
this.loading = false
this.loadingBtn = false
}).catch(() => {
this.loading = false
this.loadingBtn = false
}):attrCreatApi(data).then(res => {
this.$message.success('提交成功');
this.$msgbox.close()
this.$emit('getList');
this.clear();
this.loading = false
this.loadingBtn = false
}).catch(() => {
this.loading = false
this.loadingBtn = false
})
}, 1200);
} else {
this.loading = false
this.loadingBtn = false
return false
}
})
},
clear () {
this.$refs['formDynamic'].resetFields();
this.formDynamic.ruleValue = [];
this.formDynamic.ruleName=''
this.isBtn = false;
this.attrsName = '';
this.attrsVal = '';
},
handleInputConfirm() {
const inputValue = this.inputValue
if (inputValue) {
this.dynamicTags.push(inputValue)
}
this.inputVisible = false
this.inputValue = ''
}
}
}
</script>
<style scoped lang="scss">
.button-new-tag {
height: 28px;
line-height: 26px;
padding-top: 0;
padding-bottom: 0;
}
.input-new-tag {
width: 90px;
margin-left: 10px;
vertical-align: bottom;
}
.footer{
justify-content: flex-end;
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<el-row align="middle" :gutter="20" class="ivu-mt">
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24" class="ivu-mb mb20" v-for="(item, index) in cardLists"
:key="index">
<div class="card_box">
<div class="card_box_cir" :class="item.class">
<span class="iconfont" :class="item.icon" :style="{color:item.color}" v-if="item.icon"></span>
<i class="el-icon-edit" style="color: #fff;" v-else></i>
</div>
<div class="card_box_txt">
<span class="sp2" v-text="item.name"></span>
<span class="sp1" v-text="item.count || 0"></span>
</div>
</div>
</el-col>
</el-row>
</template>
<script>
export default {
name: "index",
props: {
cardLists: Array
},
}
</script>
<style scoped lang="scss">
.one {
background: rgba(24, 144, 255, .1);
}
.two {
background: rgba(162, 119, 255, .1);
}
.three {
background: rgba(232, 182, 0, .1);
}
.four {
background: rgba(27, 190, 107, .1);
}
.five {
background: rgba(75, 202, 213, .1);
}
.six {
background: rgba(239, 156, 32, .1);
}
.one1{
background: #1890FF;
}
.two1{
background: #A277FF;
}
.three1{
background: #EF9C20;
}
.four1{
background: #1BBE6B;
}
.five1{
background: #4BCAD5;
}
.six1{
background: #EF9C20;
}
.card_box {
width: 100%;
height: 110px;
display: flex;
align-items: center;
/*justify-content: center*/
padding: 25px;
box-sizing: border-box;
border-radius: 4px;
background: #fff;
box-sizing: border-box;
.card_box_cir {
width: 50px;
height: 50px;
border-radius: 4px;
overflow: hidden;
margin-right: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.card_box_txt {
.sp1 {
display: block;
color: #333333;
font-size: 28px;
padding-top: 3px;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
}
.sp2 {
display: block;
color: #606266;
font-size: 14px;
font-weight: 400;
}
}
}
.iconfont{
font-size: 23px;
}
</style>
@@ -0,0 +1,26 @@
import Element from 'element-ui'
import '@/styles/element-variables.scss'
import couponFromComponent from './index.vue'
import Vue from 'vue'
import Cookies from 'js-cookie'
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
const couponFrom = {}
couponFrom.install = function(Vue, options) {
const ToastConstructor = Vue.extend(couponFromComponent)
// 生成一个该子类的实例
const instance = new ToastConstructor()
instance.$mount(document.createElement('div'))
document.body.appendChild(instance.$el)
Vue.prototype.$modalCoupon = function(handle, keyNum, coupons=[], callback, userIds='', userType='') {
instance.visible = true
instance.handle = handle
instance.keyNum = keyNum
instance.coupons = coupons
instance.userIds = userIds
instance.callback = callback
instance.userType = userType
}
}
export default couponFrom
@@ -0,0 +1,50 @@
<template>
<div>
<el-dialog
title="优惠劵"
:visible.sync="visible"
width="896px"
:before-close="handleClose"
>
<coupon-list v-if="visible" :handle="handle" :userIds="userIds" :couponData="coupons" @getCouponId="getCouponId" :keyNum="keyNum" :userType="userType"></coupon-list>
<!--<upload-index v-if="visible" :isMore="isMore" @getImage="getImage" />-->
</el-dialog>
</div>
</template>
<script>
import couponList from '../index.vue'
export default {
name: 'CouponFrom',
components:{ couponList },
data() {
return {
visible: false,
callback: function() {},
handle: '',
keyNum: 0,
coupons: [],
userIds: '',
userType: ''
}
},
watch: {
// show() {
// this.visible = this.show
// }
},
methods: {
handleClose() {
this.visible = false
},
getCouponId(couponObj) {
this.callback(couponObj)
this.visible = false
}
}
}
</script>
<style scoped>
</style>
+284
View File
@@ -0,0 +1,284 @@
<template>
<div class="divBox">
<div class="header clearfix">
<div class="container">
<el-form inline size="small">
<el-form-item label="优惠卷名称:">
<el-input v-model="tableFrom.keywords" placeholder="请输入优惠券名称" class="selWidth" size="small">
<el-button slot="append" icon="el-icon-search" size="small" @click="getList(1)" />
</el-input>
</el-form-item>
</el-form>
</div>
</div>
<el-table
ref="table"
v-loading="listLoading"
:data="tableData.data"
style="width: 100%"
size="mini"
max-height="400"
tooltip-effect="dark"
highlight-current-row
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="handle==='wu'"
type="selection"
width="55"
/>
<el-table-column
prop="id"
label="ID"
min-width="50"
/>
<el-table-column
prop="name"
label="优惠券名称"
min-width="90"
/>
<el-table-column
prop="money"
label="优惠券面值"
min-width="90"
/>
<el-table-column
prop="minPrice"
label="最低消费额"
min-width="90"
>
<template slot-scope="scope">
<span>{{ scope.row.minPrice===0?'不限制':scope.row.minPrice }}</span>
</template>
</el-table-column>
<el-table-column
label="有效期限"
min-width="190"
>
<template slot-scope="scope">
<span>{{ scope.row.isFixedTime===1?scope.row.useStartTime+' 一 '+scope.row.useEndTime:'不限制' }}</span>
</template>
</el-table-column>
<el-table-column
label="剩余数量"
min-width="90"
>
<template slot-scope="scope">
<span>{{ !scope.row.isLimited ? '不限量' : scope.row.lastTotal }}</span>
</template>
</el-table-column>
<el-table-column v-if="handle==='send'" label="操作" min-width="120" fixed="right" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" class="mr10" @click="sendGrant(scope.row.id)" v-hasPermi="['admin:coupon:user:receive']">发送</el-button>
</template>
</el-table-column>
</el-table>
<div class="block mb20">
<el-pagination
:page-sizes="[10, 20, 30, 40]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
<div v-if="handle==='wu'">
<el-button size="small" type="primary" class="fr" @click="ok">确定</el-button>
</div>
</div>
</template>
<script>
import { marketingListApi, couponUserApi, marketingSendApi } from '@/api/marketing'
export default {
name: 'CouponList',
props: {
handle: {
type: String,
default: ''
},
couponData: {
type: Array,
default: () => []
},
keyNum: {
type: Number,
default: 0
},
userIds: {
type: String,
default: ''
},
userType: {
type: String,
default: ''
}
},
data() {
return {
listLoading: true,
tableData: {
data: [],
total: 0
},
tableFrom: {
page: 1,
limit: 10,
keywords: '',
type: ''
},
multipleSelection: [],
multipleSelectionAll: [],
idKey: 'id',
nextPageFlag: false,
attr: []
}
},
watch: {
keyNum: {
deep: true,
handler(val) {
this.getList()
}
}
},
mounted() {
this.tableFrom.page = 1
this.getList()
if(!this.couponData) return;
this.couponData.forEach(row => {
this.$refs.table.toggleRowSelection(row);
});
},
methods: {
close() {
this.multipleSelection = []
},
handleSelectionChange(val) {
this.multipleSelection = val
setTimeout(() => {
this.changePageCoreRecordData()
}, 50)
},
// 设置选中的方法
setSelectRow() {
if (!this.multipleSelectionAll || this.multipleSelectionAll.length <= 0) {
return
}
// 标识当前行的唯一键的名称
const idKey = this.idKey
const selectAllIds = []
this.multipleSelectionAll.forEach(row => {
selectAllIds.push(row[idKey])
})
this.$refs.table.clearSelection()
for (var i = 0; i < this.tableData.data.length; i++) {
if (selectAllIds.indexOf(this.tableData.data[i][idKey]) >= 0) {
// 设置选中,记住table组件需要使用ref="table"
this.$refs.table.toggleRowSelection(this.tableData.data[i], true)
}
}
},
// 记忆选择核心方法
changePageCoreRecordData() {
// 标识当前行的唯一键的名称
const idKey = this.idKey
const that = this
// 如果总记忆中还没有选择的数据,那么就直接取当前页选中的数据,不需要后面一系列计算
if (this.multipleSelectionAll.length <= 0) {
this.multipleSelectionAll = this.multipleSelection
return
}
// 总选择里面的key集合
const selectAllIds = []
this.multipleSelectionAll.forEach(row => {
selectAllIds.push(row[idKey])
})
const selectIds = []
// 获取当前页选中的id
this.multipleSelection.forEach(row => {
selectIds.push(row[idKey])
// 如果总选择里面不包含当前页选中的数据,那么就加入到总选择集合里
if (selectAllIds.indexOf(row[idKey]) < 0) {
that.multipleSelectionAll.push(row)
}
})
const noSelectIds = []
// 得到当前页没有选中的id
this.tableData.data.forEach(row => {
if (selectIds.indexOf(row[idKey]) < 0) {
noSelectIds.push(row[idKey])
}
})
noSelectIds.forEach(id => {
if (selectAllIds.indexOf(id) >= 0) {
for (let i = 0; i < that.multipleSelectionAll.length; i++) {
if (that.multipleSelectionAll[i][idKey] == id) {
// 如果总选择中有未被选中的,那么就删除这条
that.multipleSelectionAll.splice(i, 1)
break
}
}
}
})
},
ok() {
if (this.multipleSelection.length > 0) {
this.$emit('getCouponId', this.multipleSelectionAll)
this.close()
} else {
this.$message.warning('请先选择优惠劵')
}
},
// 列表
getList(num) {
this.listLoading = true
this.tableFrom.page = num ? num : this.tableFrom.page
this.userType ? this.tableFrom.type = 1 : this.tableFrom.type = 3;
marketingSendApi(this.tableFrom).then(res => {
this.tableData.data = res.list
this.tableData.total = res.total
this.listLoading = false
this.$nextTick(function() {
this.setSelectRow()// 调用跨页选中方法
})
}).catch(res => {
this.listLoading = false
})
},
pageChange(page) {
this.changePageCoreRecordData()
this.tableFrom.page = page
this.getList()
},
handleSizeChange(val) {
this.changePageCoreRecordData()
this.tableFrom.limit = val
this.getList()
},
// 发送
sendGrant(id){
this.$modalSure('发送优惠劵吗').then(() => {
couponUserApi({ couponId:id, uid:this.userIds }).then(() => {
this.$message.success('发送成功')
this.getList()
})
})
}
}
}
</script>
<style scoped lang="scss">
.selWidth{
width: 219px !important;
}
.seachTiele{
line-height: 35px;
}
.fr{
float: right;
}
</style>
+205
View File
@@ -0,0 +1,205 @@
<template>
<el-dialog v-model="dialogFormVisible" title="请选择管理员" append-to-body :visible.sync="dialogFormVisible" width="1200px" @close="cancel">
<el-form ref="form" inline :model="artFrom">
<el-form-item label="身份:">
<el-select v-model="artFrom.roles" placeholder="请输入身份" clearable class="selWidth">
<el-option
v-for="item in roleList.list"
:key="item.id"
:label="item.roleName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="姓名:">
<el-input v-model="artFrom.realName" size="small" placeholder="请输入姓名或者账号" class="selWidth">
<el-button slot="append" icon="el-icon-search" @click="search" class="">搜索</el-button>
</el-input>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:row-style="{height:'50px'}"
:data="tableData"
max-height="400px"
size="mini"
style="width: 100%;">
<el-table-column
label=""
width="55">
<template slot-scope="{ row, index }">
<el-radio v-model="templateRadio" :label="row.uid" @change.native="getTemplateRow(row)">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column
prop="id"
label="ID"
sortable
width="80">
</el-table-column>
<el-table-column
prop="realName"
label="姓名"
min-Width="120">
</el-table-column>
<!--<el-table-column-->
<!--label="客服头像"-->
<!--min-Width="100">-->
<!--<template slot-scope="{ row, index }" class="picMiddle">-->
<!--<div class="demo-image__preview">-->
<!--<el-image-->
<!--:src="row.avatar"-->
<!--:preview-src-list="[row.avatar]"-->
<!--/>-->
<!--</div>-->
<!--</template>-->
<!--</el-table-column>-->
<el-table-column
prop="account"
label="账号"
min-Width="120"/>
<el-table-column label="身份" prop="realName" min-width="230">
<template slot-scope="scope">
<el-tag size="small" type="info" v-for="(item, index) in scope.row.roleNames.split(',')" :key="index" class="mr5">{{ item }}</el-tag>
</template>
</el-table-column>
<el-table-column label="最后登录时间" prop="lastTime" min-width="180">
<template slot-scope="scope">
<span>{{ scope.row.lastTime | filterEmpty }}</span>
</template>
</el-table-column>
<el-table-column label="最后登录IP" prop="lastIp" min-width="150">
<template slot-scope="scope">
<span>{{ scope.row.lastIp | filterEmpty }}</span>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" min-width="100">
<template slot-scope="scope">
<span>{{ scope.row.status | filterShowOrHide }}</span>
</template>
</el-table-column>
<el-table-column label="删除标记" prop="status" min-width="100">
<template slot-scope="scope">
<span>{{ scope.row.isDel | filterYesOrNo }}</span>
</template>
</el-table-column>
</el-table>
<div class="block">
<el-pagination
class="mt20"
@size-change="sizeChange"
@current-change="pageChange"
:current-page="artFrom.page"
:page-sizes="[20, 40, 60, 100]"
:page-size="artFrom.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-dialog>
</template>
<script>
import * as systemAdminApi from '@/api/systemadmin.js'
import * as roleApi from '@/api/role.js'
export default {
name: "index",
data(){
return {
constants: this.$constants,
loading:false,
templateRadio:'',
dialogFormVisible:false,
tableData:[],
artFrom: {
page: 1,
limit: 20,
status: 1,
realName: '',
roles: ''
},
total:0,
timeVal:'',
roleList: []
}
},
created(){
this.handleGetRoleList()
},
methods:{
handleGetRoleList() {
const _pram = {
page: 1,
limit: 9999
}
roleApi.getRoleList(_pram).then(data => {
this.roleList = data
})
},
getTemplateRow(row){
this.dialogFormVisible = false;
this.$emit("upImgUid", row);
},
tableList(){
this.loading = true;
systemAdminApi.adminList( this.artFrom ).then(data => {
this.tableData = data.list
this.total = data.total
this.loading = false;
}).catch(() => {
this.loading = false;
})
// let that = this;
// that.loading = true;
// userListApi(that.artFrom).then(res=>{
// that.loading = false;
// that.tableData = res.list;
// that.total = res.total
// })
},
//切换显示条数
sizeChange(index){
this.artFrom.limit = index;
this.tableList();
},
//切换页数
pageChange(index){
this.artFrom.page = index;
this.tableList();
},
onchangeTime(e){
this.artFrom.page = 1;
if(e!==null){
this.artFrom.data = e.join(',');
}else {
this.artFrom.data = '';
}
this.tableList();
},
search(){
this.timeVal = '';
this.artFrom.page = 1;
this.tableList();
},
cancel(){
this.artFrom = {
page: 1,
limit: 20,
data: '',
realName: ''
};
this.timeVal = '';
this.templateRadio = ''
}
}
}
</script>
<style lang="scss">
.el-input-group__append, .el-input-group__prepend{
background-color:#1890ff!important;
color: #fff!important;
border-color:#1890ff!important;
border-radius: 0 4px 4px 0;
}
</style>
+195
View File
@@ -0,0 +1,195 @@
<template>
<div>
<div :id="echarts" :style="styles"></div>
</div>
</template>
<script>
import echarts from 'echarts';
export default {
name: 'index',
props: {
// styles: {
// type: Object,
// default: null
// },
seriesData: {
type: Array,
default: () => []
},
xAxis: {
type: Array,
default: () => []
},
echartsTitle: {
type: String,
default: ''
},
yAxisData: {
type: Array,
default: () => []
},
legendData: {
type: Array,
default: () => []
}
},
data() {
return {
styles: 'height:300px',
infoLists: this.infoList,
seriesArray: this.seriesData
}
},
watch: {
seriesData: {
handler(newVal, oldVal) {
this.seriesArray = newVal;
this.handleSetVisitChart();
},
deep: true // 对象内部属性的监听,关键。
}
},
computed: {
echarts() {
return 'echarts' + Math.ceil(Math.random() * 100)
}
},
mounted: function () {
const vm = this
vm.$nextTick(() => {
vm.handleSetVisitChart();
window.addEventListener('resize', this.wsFunc)
})
},
methods: {
wsFunc() {
this.myChart.resize()
},
handleSetVisitChart() {
this.myChart = echarts.init(document.getElementById(this.echarts))
let option = null
if (this.echartsTitle === 'circle') {
option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'right',
data: this.legendData || []
},
series: [
{
name: '访问来源',
type: 'pie',
radius: '70%',
center: ['50%', '60%'],
data: this.seriesArray || [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
} else {
option = {
tooltip: {
trigger: 'axis'
},
toolbox: {},
legend: {
data: this.legendData || []
},
color: ['#1495EB', '#00CC66', '#F9D249', '#ff9900', '#9860DF'],
grid: {
left: 16,
right: 25,
bottom: 10,
top: 40,
containLabel: true
},
xAxis: [
{
type: 'category',
axisLine: {
lineStyle: {
color: '#D7DDE4'
}
},
axisTick: {
alignWithLabel: true,
lineStyle: {
color: '#D7DDE4'
}
},
splitLine: {
show: false,
lineStyle: {
color: '#F5F7F9'
}
},
// axisPointer: {
// type: 'shadow'
// },
axisLabel: {
interval: 0,
rotate: 40,
textStyle: {
color: '#7F8B9C'
}
},
data: this.xAxis
}
],
yAxis: this.yAxisData.length?this.yAxisData:{
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
textStyle: {
color: '#7F8B9C'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#F5F7F9'
}
},
type: 'value'
},
series: this.seriesArray
};
}
// 基于准备好的dom,初始化echarts实例
this.myChart.setOption(option, true)
},
handleResize() {
this.myChart.resize()
}
},
beforeDestroy() {
window.removeEventListener('resize', this.wsFunc)
if (!this.myChart) {
return
}
this.myChart.dispose()
this.myChart = null
}
}
</script>
<style scoped>
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div>
<div :id="echarts" :style="styles" />
</div>
</template>
<script>
import echarts from 'echarts'
export default {
name: 'Index',
props: {
styles: {
type: Object,
default: null
},
optionData: {
type: Object,
default: null
}
},
data() {
return {
myChart: null
}
},
computed: {
echarts() {
return 'echarts' + Math.ceil(Math.random() * 100)
}
},
watch: {
optionData: {
handler(newVal, oldVal) {
this.handleSetVisitChart()
},
deep: true // 对象内部属性的监听,关键。
}
},
mounted: function() {
const vm = this
vm.$nextTick(() => {
vm.handleSetVisitChart()
window.addEventListener('resize', this.wsFunc)
})
},
beforeDestroy() {
window.removeEventListener('resize', this.wsFunc)
if (!this.myChart) {
return
}
this.myChart.dispose()
this.myChart = null
},
methods: {
wsFunc() {
this.myChart.resize()
},
handleSetVisitChart() {
this.myChart = echarts.init(document.getElementById(this.echarts))
let option = null
option = this.optionData
// 基于准备好的dom,初始化echarts实例
this.myChart.setOption(option, true)
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,23 @@
import Element from 'element-ui'
import '@/styles/element-variables.scss'
import uploadFromComponent from './index.vue'
import Vue from 'vue'
import Cookies from 'js-cookie'
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
const goodListFrom = {}
goodListFrom.install = function(Vue, options) {
const ToastConstructor = Vue.extend(uploadFromComponent)
// 生成一个该子类的实例
const instance = new ToastConstructor()
instance.$mount(document.createElement('div'))
document.body.appendChild(instance.$el)
Vue.prototype.$modalGoodList = function(callback, handleNum, row) {
instance.visible = true
instance.callback = callback
instance.handleNum = handleNum
instance.checked = row
}
}
export default goodListFrom
@@ -0,0 +1,41 @@
<template>
<div>
<el-dialog
title="商品列表"
:visible.sync="visible"
width="896px"
:before-close="handleClose"
>
<good-list v-if="visible" @getStoreItem="getStoreItem" :handleNum="handleNum" :checked="checked"></good-list>
</el-dialog>
</div>
</template>
<script>
import goodList from '@/components/goodList/index.vue'
export default {
name: 'GoodListFrom',
components: { goodList },
data() {
return {
handleNum: '',
visible: false,
callback: function() {},
checked: []
}
},
methods: {
handleClose() {
this.visible = false
},
getStoreItem(img) {
this.callback(img)
this.visible = false
}
}
}
</script>
<style scoped>
</style>
+271
View File
@@ -0,0 +1,271 @@
<template>
<div class="divBox">
<div class="header clearfix">
<div class="container">
<el-form inline>
<el-form-item label="商品分类:">
<el-cascader v-model="tableFrom.cateId" :options="merCateList" :props="props" clearable class="selWidth mr20" @change="getList(1)"/>
</el-form-item>
<el-form-item label="商品搜索:">
<el-input v-model="tableFrom.keywords" @input="onInput($event)" placeholder="请输入商品名称,关键字,产品编号" class="selWidth">
<el-button slot="append" icon="el-icon-search" @click="getList(1)"/>
</el-input>
</el-form-item>
</el-form>
</div>
</div>
<el-table
v-loading="listLoading"
:data="tableData.data"
style="width: 100%"
size="mini"
ref="multipleTable"
@selection-change="handleSelectionChange"
>
<el-table-column
key="2"
v-if="handleNum === 'many'"
width="55">
<template slot="header" slot-scope="scope">
<el-checkbox slot="reference" :value="isChecked && checkedPage.indexOf(tableFrom.page) > -1" @change="changeType" />
</template>
<template slot-scope="scope">
<el-checkbox
:value="checkedIds.indexOf(scope.row.id) > -1"
@change="(v)=>changeOne(v,scope.row)"
/>
</template>
</el-table-column>
<el-table-column
width="55"
key="1"
v-if="handleNum !== 'many'"
>
<template slot-scope="scope">
<el-radio :label="scope.row.id" v-model="templateRadio" @change.native="getTemplateRow(scope.row)">&nbsp</el-radio>
</template>
</el-table-column>
<el-table-column
prop="id"
label="ID"
min-width="50"
/>
<el-table-column label="商品图" width="80">
<template slot-scope="scope">
<div class="demo-image__preview">
<el-image
style="width: 36px; height: 36px"
:src="scope.row.image"
:preview-src-list="imgList"
/>
</div>
</template>
</el-table-column>
<el-table-column
prop="storeName"
label="商品名称"
min-width="180"
/>
<el-table-column
label="商品分类"
min-width="150"
>
<template slot-scope="scope">
<span v-for="(item, index) in scope.row.cateValues.split(',')" :key="index" class="mr10">{{ item }}</span>
</template>
</el-table-column>
</el-table>
<div class="block mb20">
<el-pagination
:page-sizes="[10, 20, 30, 40]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
<div v-if="handleNum === 'many'" class="right-align">
<el-button size="small" type="primary" @click="ok">确定</el-button>
</div>
</div>
</template>
<script>
import { productLstApi, productDeleteApi, categoryApi, putOnShellApi, offShellApi, productHeadersApi } from '@/api/store'
export default {
name: 'GoodList',
props: {
handleNum: {
type: String,
default: ''
},
checked: {
type: Array,
default: () => []
}
},
data() {
return {
templateRadio:0,
merCateList: [],
props: {
children: 'child',
label: 'name',
value: 'id',
emitPath: false
},
listLoading: true,
tableData: {
data: [],
total: 0
},
tableFrom: {
page: 1,
limit: 10,
cateId: '',
keywords: '',
type: '1'
},
imgList: [],
multipleSelection: [],
checkedPage: [],
isChecked: false,
isIndex: 0,
checkBox: [],
checkedIds: [] // 订单当前页选中的数据
}
},
mounted() {
this.getList()
if(this.checked.length){
let [ ...arr2 ] = this.checked
this.checkBox = arr2
this.checkedIds = arr2.map(item => {
return item.id
})
}
this.getCategorySelect()
},
methods: {
onInput(e){
this.$forceUpdate();
},
changeType(v) {
this.isChecked = v
const index = this.checkedPage.indexOf(this.tableFrom.page)
this.isIndex = index
this.checkedPage.push(this.tableFrom.page)
if (index > -1) {
this.checkedPage.splice(index, 1)
}
this.syncCheckedId(v)
},
changeOne(v, order) {
if (v) {
const index = this.checkedIds.indexOf(order.id)
if (index === -1){
this.checkedIds.push(order.id)
this.checkBox.push(order)
}
} else {
const index = this.checkedIds.indexOf(order.id)
if (index > -1){
this.checkedIds.splice(index, 1)
this.checkBox.splice(index, 1)
}
}
},
syncCheckedId(o) {
const ids = this.tableData.data.map(v => v.id)
if(o){
this.tableData.data.forEach(item => {
const index = this.checkedIds.indexOf(item.id)
if (index === -1) {
this.checkedIds.push(item.id)
this.checkBox.push(item)
}
})
}else{
this.tableData.data.forEach(item => {
const index = this.checkedIds.indexOf(item.id)
if (index > -1) {
this.checkedIds.splice(index, 1)
this.checkBox.splice(index, 1)
}
})
}
},
handleSelectionChange(val) {
const tables = []
val.map((item) => {
tables.push({ src: item.image,
id: item.id })
})
this.multipleSelection = tables
},
ok() {
this.$emit('getStoreItem', this.checkBox)
},
getTemplateRow(row){
this.$emit('getStoreItem', row)
},
// 商户分类;
getCategorySelect() {
categoryApi({ status: -1, type: 1 }).then(res => {
this.merCateList = res
}).catch(res => {
this.$message.error(res.message)
})
},
getList(num) {
this.listLoading = true
this.tableFrom.page = num ? num : this.tableFrom.page;
productLstApi(this.tableFrom).then(res => {
this.tableData.data = res.list
this.tableData.total = res.total
this.tableData.data.map((item) => {
this.imgList.push(item.image)
})
this.tableData.data.forEach(item => {
this.checked.forEach(element => {
if (Number(item.id) === Number(element.id)) {
this.$nextTick(() => {
this.$refs.multipleTable.toggleRowSelection(item, true)
})
}
})
})
this.listLoading = false
}).catch(res => {
this.listLoading = false
this.$message.error(res.message)
})
},
pageChange(page) {
this.tableFrom.page = page
this.getList()
},
handleSizeChange(val) {
this.tableFrom.limit = val
this.getList()
}
}
}
</script>
<style scoped lang="scss">
.right-align{
float: right;
}
.selWidth{
width: 250px !important;
}
.seachTiele{
line-height: 35px;
}
.fr{
float: right;
}
</style>
@@ -0,0 +1,3 @@
const elementIcons = ['platform-eleme', 'eleme', 'delete-solid', 'delete', 's-tools', 'setting', 'user-solid', 'user', 'phone', 'phone-outline', 'more', 'more-outline', 'star-on', 'star-off', 's-goods', 'goods', 'warning', 'warning-outline', 'question', 'info', 'remove', 'circle-plus', 'success', 'error', 'zoom-in', 'zoom-out', 'remove-outline', 'circle-plus-outline', 'circle-check', 'circle-close', 's-help', 'help', 'minus', 'plus', 'check', 'close', 'picture', 'picture-outline', 'picture-outline-round', 'upload', 'upload2', 'download', 'camera-solid', 'camera', 'video-camera-solid', 'video-camera', 'message-solid', 'bell', 's-cooperation', 's-order', 's-platform', 's-fold', 's-unfold', 's-operation', 's-promotion', 's-home', 's-release', 's-ticket', 's-management', 's-open', 's-shop', 's-marketing', 's-flag', 's-comment', 's-finance', 's-claim', 's-custom', 's-opportunity', 's-data', 's-check', 's-grid', 'menu', 'share', 'd-caret', 'caret-left', 'caret-right', 'caret-bottom', 'caret-top', 'bottom-left', 'bottom-right', 'back', 'right', 'bottom', 'top', 'top-left', 'top-right', 'arrow-left', 'arrow-right', 'arrow-down', 'arrow-up', 'd-arrow-left', 'd-arrow-right', 'video-pause', 'video-play', 'refresh', 'refresh-right', 'refresh-left', 'finished', 'sort', 'sort-up', 'sort-down', 'rank', 'loading', 'view', 'c-scale-to-original', 'date', 'edit', 'edit-outline', 'folder', 'folder-opened', 'folder-add', 'folder-remove', 'folder-delete', 'folder-checked', 'tickets', 'document-remove', 'document-delete', 'document-copy', 'document-checked', 'document', 'document-add', 'printer', 'paperclip', 'takeaway-box', 'search', 'monitor', 'attract', 'mobile', 'scissors', 'umbrella', 'headset', 'brush', 'mouse', 'coordinate', 'magic-stick', 'reading', 'data-line', 'data-board', 'pie-chart', 'data-analysis', 'collection-tag', 'film', 'suitcase', 'suitcase-1', 'receiving', 'collection', 'files', 'notebook-1', 'notebook-2', 'toilet-paper', 'office-building', 'school', 'table-lamp', 'house', 'no-smoking', 'smoking', 'shopping-cart-full', 'shopping-cart-1', 'shopping-cart-2', 'shopping-bag-1', 'shopping-bag-2', 'sold-out', 'sell', 'present', 'box', 'bank-card', 'money', 'coin', 'wallet', 'discount', 'price-tag', 'news', 'guide', 'male', 'female', 'thumb', 'cpu', 'link', 'connection', 'open', 'turn-off', 'set-up', 'chat-round', 'chat-line-round', 'chat-square', 'chat-dot-round', 'chat-dot-square', 'chat-line-square', 'message', 'postcard', 'position', 'turn-off-microphone', 'microphone', 'close-notification', 'bangzhu', 'time', 'odometer', 'crop', 'aim', 'switch-button', 'full-screen', 'copy-document', 'mic', 'stopwatch', 'medal-1', 'medal', 'trophy', 'trophy-1', 'first-aid-kit', 'discover', 'place', 'location', 'location-outline', 'location-information', 'add-location', 'delete-location', 'map-location', 'alarm-clock', 'timer', 'watch-1', 'watch', 'lock', 'unlock', 'key', 'service', 'mobile-phone', 'bicycle', 'truck', 'ship', 'basketball', 'football', 'soccer', 'baseball', 'wind-power', 'light-rain', 'lightning', 'heavy-rain', 'sunrise', 'sunrise-1', 'sunset', 'sunny', 'cloudy', 'partly-cloudy', 'cloudy-and-sunny', 'moon', 'moon-night', 'dish', 'dish-1', 'food', 'chicken', 'fork-spoon', 'knife-fork', 'burger', 'tableware', 'sugar', 'dessert', 'ice-cream', 'hot-water', 'water-cup', 'coffee-cup', 'cold-drink', 'goblet', 'goblet-full', 'goblet-square', 'goblet-square-full', 'refrigerator', 'grape', 'watermelon', 'cherry', 'apple', 'pear', 'orange', 'coffee', 'ice-tea', 'ice-drink', 'milk-tea', 'potato-strips', 'lollipop', 'ice-cream-square', 'ice-cream-round']
export default elementIcons
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="iconBox">
<el-input
ref="search"
v-model="iconVal"
placeholder="输入关键词搜索,注意全是英文"
clearable
style="width: 300px"
@input="upIcon(iconVal)"
/>
<div class="icons-container">
<div class="grid">
<div
v-for="item of list"
:key="item"
@click="handleClipboard(generateElementIconCode(item),$event,item)"
>
<el-tooltip placement="top">
<div slot="content">
{{ generateElementIconCode(item) }}
</div>
<div class="icon-item">
<i :class="'el-icon-' + item" />
<span>{{ item }}</span>
</div>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
<script>
import elementIcons from './element-icons'
export default {
name: 'Index',
data() {
return {
elementIcons,
iconVal: '',
modals2: false,
list: []
}
},
mounted() {
this.list = this.elementIcons
},
methods: {
generateElementIconCode(symbol) {
return `<i class="el-icon-${symbol}" />`
},
handleClipboard(text, event, n) {
this.iconChange(n)
// clipboard(text, event)
},
// 搜索
upIcon(n) {
const arrs = []
for (var i = 0; i < this.elementIcons.length; i++) {
if (this.elementIcons[i].indexOf(n) !== -1) {
arrs.push(this.elementIcons[i])
this.list = arrs
}
}
},
iconChange(n) {
this.$emit('getIcon', n)
this.$msgbox.close()
}
}
}
</script>
<style lang="scss" scoped>
.icons-container {
margin: 10px 20px 0;
overflow: hidden;
.grid {
position: relative;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.icon-item {
margin: 10px 20px;
text-align: center;
width: 100px;
float: left;
font-size: 30px;
color: #24292e;
cursor: pointer;
}
span {
display: block;
font-size: 16px;
margin-top: 10px;
}
.disabled {
pointer-events: none;
}
}
</style>
@@ -0,0 +1,149 @@
<template>
<div>
<div class="acea-row" v-if="multiple">
<div
v-for="(item,index) in imageList"
:key="index"
class="pictrue"
draggable="false"
@dragstart="handleDragStart($event, item)"
@dragover.prevent="handleDragOver($event, item)"
@dragenter="handleDragEnter($event, item)"
@dragend="handleDragEnd($event, item)"
>
<img :src="item.sattDir">
<i class="el-icon-error btndel" @click="handleRemove(index)" />
</div>
<div class="upLoadPicBox" @click="modalPicTap('2')" v-show="imageList.length<20">
<div class="upLoad">
<i class="el-icon-camera cameraIconfont" />
</div>
</div>
</div>
<div class="upLoadPicBox" @click="modalPicTap('1')" v-else>
<div v-if="image" class="pictrue"><img :src="image"></div>
<div v-else class="upLoad">
<i class="el-icon-camera cameraIconfont" />
</div>
</div>
<el-dialog
title="上传图片"
:visible.sync="visible"
width="896px"
:before-close="handleClose"
:modal="false"
>
<upload-index v-if="visible" :checkedMore="imageList" :isMore="isMore" @getImage="getImage" />
</el-dialog>
</div>
</template>
<script>
import UploadIndex from '@/components/uploadPicture/index.vue'
export default {
name: 'UploadFroms',
components: { UploadIndex },
props:{
value:{},
multiple:{
type: Boolean,
default: false
}
},
data() {
return {
image:"",
visible: false,
callback: function() {},
isMore: '',
imageList: []
}
},
beforeMount(){
if( this.multiple ){
// 接收 v-model 数据
if(this.value){
this.imageList = JSON.parse(this.value)
}
}else{
// 接收 v-model 数据
if(this.value){
this.image = this.value
}
}
// 处理多选
this.isMore = this.multiple ? '2':'1'
},
methods: {
handleClose() {
this.visible = false
},
getImage(img) {
if (this.multiple) {
let obj = {};
this.imageList = img.reduce((cur,next) => {
obj[next.attId] ? "" : obj[next.attId] = true && cur.push(next);
return cur;
},[])
this.$emit('input', JSON.stringify(this.imageList))
} else {
this.image = img[0].sattDir
this.$emit('input', this.image)
}
this.visible = false
},
// 点击商品图
modalPicTap (tit, num, i) {
this.visible = true
},
handleRemove (i) {
this.imageList.splice(i, 1)
this.$emit('input', JSON.stringify(this.imageList))
},
// 移动
handleDragStart (e, item) {
this.dragging = item;
},
handleDragEnd (e, item) {
this.dragging = null
},
handleDragOver (e) {
e.dataTransfer.dropEffect = 'move'
},
handleDragEnter (e, item) {
e.dataTransfer.effectAllowed = 'move'
if (item === this.dragging) {
return
}
const newItems = [...this.imageList]
const src = newItems.indexOf(this.dragging)
const dst = newItems.indexOf(item)
newItems.splice(dst, 0, ...newItems.splice(src, 1))
this.imageList = newItems;
}
}
}
</script>
<style scoped lang="scss">
.btndel{
position: absolute;
z-index: 1;
width :20px !important;
height: 20px !important;
left: 46px;
top: -4px;
}
.pictrue{
width: 60px;
height: 60px;
border: 1px dotted rgba(0,0,0,0.1);
margin-right: 10px;
position: relative;
cursor: pointer;
img{
width: 100%;
height: 100%;
}
}
</style>
@@ -0,0 +1,938 @@
<template>
<div>
<el-row :gutter="30">
<el-col v-bind="grid">
<div class="Nav">
<div class="input">
<el-input
v-model="filterText"
placeholder="选择分类"
prefix-icon="el-icon-search"
style="width: 100%"
clearable
/>
</div>
<div class="trees-coadd">
<div class="scollhide">
<div
class="trees"
:style="{ maxHeight: !pictureType ? '300px' : '700px' }"
>
<el-tree
ref="tree"
:data="treeData2"
:filter-node-method="filterNode"
:props="defaultProps"
highlight-current
>
<div
slot-scope="{ node, data }"
class="custom-tree-node"
@click.stop="handleNodeClick(data)"
>
<div>
<span
class="custom-tree-node-label"
:title="node.label"
>{{ node.label }}</span
>
<span
v-if="data.space_property_name"
style="font-size: 11px; color: #3889b1"
>{{ data.name }}</span
>
</div>
<span class="el-ic">
<el-dropdown>
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
@click.native="onAdd(data.id)"
v-if="checkPermi(['admin:category:save'])"
>添加分类</el-dropdown-item
>
<el-dropdown-item
v-if="
node.label !== '全部图片' &&
checkPermi(['admin:category:update'])
"
@click.native="onEdit(data.id)"
>编辑分类</el-dropdown-item
>
<el-dropdown-item
v-if="
node.label !== '全部图片' &&
checkPermi(['admin:category:delete'])
"
@click.native="handleDelete(data.id)"
>删除分类</el-dropdown-item
>
</el-dropdown-menu>
</el-dropdown>
</span>
</div>
</el-tree>
</div>
</div>
</div>
</div>
</el-col>
<el-col v-bind="grid2" class="colLeft">
<div class="conter mb15 relative">
<div class="bnt">
<el-button
v-if="!pictureType"
size="small"
type="primary"
class="mr15 mb20"
@click="checkPics"
>使用选中图片</el-button
>
<div class="mb20">
<el-tooltip
class="item"
effect="dark"
content="上传图片"
placement="top-start"
>
<el-upload
class="upload-demo"
action
:http-request="handleUploadForm"
:on-change="imgSaveToUrl"
:headers="myHeaders"
:show-file-list="false"
multiple
>
<el-button
icon="el-icon-upload2"
size="small"
class="mr15"
v-if="!pictureType"
></el-button>
</el-upload>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="删除图片"
placement="top-start"
>
<el-button
icon="el-icon-delete"
class="mr15"
type="danger"
size="small"
@click.stop="editPicList('图片')"
v-hasPermi="['admin:system:attachment:delete']"
v-if="!pictureType"
></el-button>
</el-tooltip>
</div>
<el-upload
class="upload-demo"
action
:http-request="handleUploadForm"
:on-change="imgSaveToUrl"
:headers="myHeaders"
:show-file-list="false"
multiple
>
<el-button class="mr10" type="primary" v-if="pictureType"
>上传图片</el-button
>
</el-upload>
<div>
<el-button
class="mr10"
type="danger"
@click.stop="editPicList('图片')"
v-if="pictureType"
>删除图片</el-button
>
</div>
<el-select
v-model="sleOptions.attachment_category_name"
placeholder="图片移动至"
class="mb20"
:size="pictureType ? '' : 'small'"
>
<el-option
class="demo"
:label="sleOptions.attachment_category_name"
:value="sleOptions.attachment_category_id"
style="
max-width: 560px;
height: 200px;
overflow: auto;
background-color: #fff;">
<el-tree
ref="tree2"
:data="treeData2"
:filter-node-method="filterNode"
:props="defaultProps"
highlight-current
@node-click="handleSelClick"
/>
</el-option>
</el-select>
<div class="abs_video">
<el-radio-group v-model="typeDate" @change="radioChange" size="small">
<el-radio-button label="pic">图片</el-radio-button>
<el-radio-button label="video">视频</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="pictrueList acea-row" v-loading="loadingPic">
<div v-show="isShowPic" class="imagesNo">
<i
class="el-icon-picture"
style="font-size: 60px; color: rgb(219, 219, 219)"
/>
<span class="imagesNo_sp">图片库为空</span>
</div>
<div
class="conters scrollbarAll"
:style="{ maxHeight: !pictureType ? '500px' : '700px' }"
>
<div
v-for="(item, index) in pictrueList.list"
:key="index"
class="gridPic"
>
<span class="num_badge" v-if="item.num > 0">{{item.num}}</span>
<img
style="object-fit: contain;"
v-lazy="item.sattDir ? item.sattDir : localImg"
:class="item.isSelect ? 'on' : ''"
@click="changImage(item, index, pictrueList.list)"
v-if="item.attType !== 'video/mp4'"
/>
<video
:src="item.sattDir"
:class="item.isSelect ? 'on' : ''"
@click="changImage(item, index, pictrueList.list)"
v-if="item.attType == 'video/mp4'"
></video>
</div>
</div>
</div>
</div>
<div class="block">
<el-pagination
:page-sizes="!pictureType ? [10, 20, 30, 40] : [30, 60, 90, 120]"
:page-size="tableData.limit"
:current-page="tableData.page"
:pager-count="5"
layout="total, sizes, prev, pager, next, jumper"
:total="pictrueList.total"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
</el-col>
</el-row>
<el-dialog
:title="bizTitle"
:visible.sync="visible"
destroy-on-close
:close-on-click-modal="false"
:modal="modals"
@close="closeModel"
>
<el-form
ref="editPram"
:model="editPram"
label-width="100px"
v-loading="loading"
>
<el-form-item
label="上级分类"
prop="pid"
:rules="[
{
type: 'number',
required: true,
message: '请选择上级分类',
trigger: ['blur', 'change'],
},
]"
>
<el-cascader
v-model="editPram.pid"
:options="treeData2"
:props="categoryProps"
style="width: 100%"
/>
</el-form-item>
<el-form-item
label="分类名称"
prop="name"
:rules="[
{
required: true,
message: '请输入分类名称',
trigger: ['blur', 'change'],
},
]"
>
<el-input v-model="editPram.name" placeholder="分类名称" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="editPram.sort" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerSubmit('editPram')"
>确定</el-button
>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import {
addCategroy,
listCategroy,
treeCategroy,
infoCategroy,
updateCategroy,
deleteCategroy,
} from "@/api/categoryApi";
import {
fileImageApi,
fileListApi,
fileDeleteApi,
attachmentMoveApi,
} from "@/api/systemSetting";
import { getToken } from "@/utils/auth";
import { checkPermi } from "@/utils/permission"; // 权限判断函数
export default {
name: "Upload",
props: {
pictureType: {
type: String,
default: "",
},
isMore: {
type: String,
default: "1",
},
modelName: {
type: String,
default: "",
},
checkedMore: {
type: Array,
default: () => [],
},
},
data() {
return {
loadingPic: false,
loading: false,
modals: false,
allTreeList: [],
categoryProps: {
value: "id",
label: "name",
children: "child",
expandTrigger: "hover",
checkStrictly: true,
emitPath: false,
},
editPram: {
pid: 1000,
name: "",
type: 2,
sort: 1,
status: 0,
url: "url",
id: 0,
},
visible: false,
bizTitle: "",
sleOptions: {
attrId: "",
pid: "",
},
list: [],
grid: {
xl: 7,
lg: 7,
md: 7,
sm: 7,
xs: 24,
},
grid2: {
xl: 17,
lg: 17,
md: 17,
sm: 17,
xs: 24,
},
filterText: "",
treeData: [],
treeData2: [],
defaultProps: {
children: "child",
label: "name",
},
tableData: {
page: 1,
limit: 10,
pid: 0,
attType: "jpg,jpeg,gif,png,bmp,PNG,JPG",
},
classifyId: 0,
myHeaders: { "X-Token": getToken() },
treeFrom: {
status: -1,
type: 2,
},
pictrueList: {
list: [],
total: 0,
},
isShowPic: false,
checkPicList: [],
ids: [],
listPram: {
pid: 0,
type: 2,
status: 0,
name: "",
page: 1,
limit: 9999,
},
localImg: "",
videoStatus: false,
typeDate:"pic",
};
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
},
},
mounted() {
this.pictureType
? (this.tableData.limit = 30)
: (this.tableData.limit = 10);
if (this.$route && this.$route.query.field === "dialog")
import("./internal.js");
this.getList();
this.getFileList();
// this.handlerGetList()
},
methods: {
checkPermi,
// 选取图片后自动回调,里面可以获取到文件
imgSaveToUrl(event) {
// 也可以用file
this.localFile = event.raw; // 或者 this.localFile=file.raw
// 转换操作可以不放到这个函数里面,
// 因为这个函数会被多次触发,上传时触发,上传成功也触发
let reader = new FileReader();
reader.readAsDataURL(this.localFile); // 这里也可以直接写参数event.raw
// 转换成功后的操作,reader.result即为转换后的DataURL
// 它不需要自己定义,你可以console.integralLog(reader.result)看一下
reader.onload = () => {
// console.integralLog(reader.result)
};
/* 另外一种本地预览方法 */
let URL = window.URL || window.webkitURL;
this.localImg = URL.createObjectURL(event.raw);
// 转换后的地址为 blob:http://xxx/7bf54338-74bb-47b9-9a7f-7a7093c716b5
},
closeModel() {
this.$refs["editPram"].resetFields();
},
handlerSubmit(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.editPram.pid == 10000) this.editPram.pid = 0;
this.bizTitle === "添加分类"
? addCategroy(this.editPram).then((data) => {
this.$message.success("创建成功");
this.visible = false;
this.getList();
})
: updateCategroy(this.editPram).then((data) => {
this.$message.success("编辑成功");
this.visible = false;
this.getList();
});
} else {
return false;
}
});
},
// 表单分类
handlerGetList() {
let datas = {
name: "全部图片",
id: "",
};
treeCategroy(this.treeFrom).then((data) => {
this.allTreeList = data;
this.allTreeLis.unshift(datas);
});
},
// 搜索分类
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
// 所有分类
getList() {
const data = {
name: "全部图片",
id: 10000,
};
treeCategroy(this.treeFrom).then((res) => {
this.treeData = res;
this.treeData.unshift(data);
this.treeData2 = [...this.treeData];
});
},
// 添加分类
onAdd(id) {
this.tableData.pid = id;
if (this.tableData.pid === 10000) this.tableData.pid = 0;
this.bizTitle = "添加分类";
this.visible = true;
if (id)
this.editPram = {
pid: id,
name: "",
type: 2,
sort: 1,
status: 0,
url: "url",
id: 0,
};
},
// 编辑
onEdit(id) {
if (id === 10000) id = 0;
this.bizTitle = "编辑分类";
this.loading = true;
infoCategroy({ id: id }).then((res) => {
this.editPram = res;
this.loading = false;
});
this.visible = true;
},
// 删除
handleDelete(id) {
if (id === 10000) id = 0;
this.$modalSure().then(() => {
deleteCategroy({ id: id }).then(() => {
this.$message.success("删除成功");
this.getList();
});
});
},
handleNodeClick(data) {
this.checkPicList = [];
this.tableData.pid = data.id;
this.getFileList();
},
// 上传
handleUploadForm(param) {
const formData = new FormData();
const data = {
model: this.modelName ? this.modelName : this.$route.path.split("/")[1],
pid: this.tableData.pid,
};
formData.append("multipart", param.file);
let loading = this.$loading({
lock: true,
text: "上传中,请稍候...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
fileImageApi(formData, data)
.then((res) => {
loading.close();
this.$message.success("上传成功");
this.tableData.page = 1;
this.getFileList();
})
.catch((res) => {
loading.close();
});
},
// 文件列表
getFileList() {
if (this.tableData.pid === 10000) this.tableData.pid = 0;
this.loadingPic = true;
fileListApi(this.tableData)
.then(async (res) => {
this.pictrueList.list = res.list;
if (this.tableData.page === 1 && this.pictrueList.list.length > 0)
this.pictrueList.list[0].localImg = this.localImg;
if (this.pictrueList.list.length) {
this.isShowPic = false;
} else {
this.isShowPic = true;
}
this.pictrueList.total = res.total;
this.loadingPic = false;
})
.catch(() => {
this.loadingPic = false;
});
},
pageChange(page) {
this.tableData.page = page;
this.checkPicList = [];
this.getFileList();
},
handleSizeChange(val) {
this.tableData.limit = val;
this.getFileList();
},
// 选中图片
changImage(item, index, row) {
let activeIndex = 0;
if (!item.isSelect) {
this.$set(item, "isSelect", true);
this.checkPicList.push(item);
} else {
this.$set(item, "isSelect", !item.isSelect);
this.checkPicList.map((el, index) => {
if (el.attId == item.attId) {
activeIndex = index;
}
});
this.checkPicList.splice(activeIndex, 1);
}
this.ids = [];
this.checkPicList.map((item, i) => {
this.ids.push(item.attId);
});
this.pictrueList.list.map((el, i) => {
if (el.isSelect) {
this.checkPicList.filter((el2, j) => {
if (el.attId == el2.attId) {
el.num = j + 1;
this.$nextTick(() => {
this.pictrueList.list;
});
}
});
} else {
el.num = 0;
}
});
},
// 点击使用选中图片
checkPics() {
if (!this.checkPicList.length)
return this.$message.warning("请先选择图片");
if (this.$route && this.$route.query.field === "dialog") {
let str = "";
for (let i = 0; i < this.checkPicList.length; i++) {
str += '<img src="' + this.checkPicList[i].sattDir + '">';
}
/* eslint-disable */
nowEditor.dialog.close(true);
nowEditor.editor.setContent(str, true);
} else {
if (this.isMore === "1" && this.checkPicList.length > 1) {
return this.$message.warning("最多只能选一张图片");
}
this.$emit("getImage", [...this.checkedMore, ...this.checkPicList]);
}
},
// 删除图片
editPicList(tit) {
if (!this.checkPicList.length)
return this.$message.warning("请先选择图片");
this.$modalSure().then(() => {
fileDeleteApi(this.ids.join(",")).then(() => {
this.$message.success("刪除成功");
this.getFileList();
this.checkPicList = [];
});
});
},
// 移动分类点击
handleSelClick(node) {
if (this.ids.length) {
this.sleOptions = {
attrId: this.ids.join(","),
pid: node.id,
};
this.getMove();
} else {
this.$message.warning("请先选择图片");
}
},
getMove() {
attachmentMoveApi(this.sleOptions)
.then(async (res) => {
this.$message.success("操作成功");
this.clearBoth();
this.getFileList();
})
.catch((res) => {
this.clearBoth();
});
},
clearBoth() {
this.sleOptions = {
attrId: "",
pid: "",
};
this.checkPicList = [];
this.ids = [];
},
videoChange(val) {
if (val == false) {
this.$set(this.tableData, "attType", "jpg,jpeg,gif,png,bmp,PNG,JPG");
this.getFileList();
} else {
this.$set(this.tableData, "attType", "video/mp4");
this.getFileList();
}
},
radioChange(val){
if(val === 'video'){
this.videoChange(true)
}else{
this.videoChange(false)
}
}
},
};
</script>
<style scoped lang="scss">
.demo::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.demo {
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
overflow-x: hidden;
overflow-y: auto;
}
.temp {
height: 0;
margin-bottom: 0;
border: none;
padding: 0;
}
.selectTreeClass {
background: #d5e8fc;
}
.treeBox {
width: 100%;
height: 100%;
}
.upload-demo {
display: inline-block !important;
float: left;
}
.tree_w {
padding: 20px 30px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
padding-right: 8px;
color: #4386c6;
}
.custom-tree-node-label {
display: block;
width: 125px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.el-ic {
display: none;
i,
span {
/*padding: 0 14px;*/
font-size: 18px;
font-weight: 600;
}
.svg-icon {
color: #4386c6;
}
}
.el-tree-node__content {
height: 38px;
}
.el-tree-node__expand-icon {
color: #428bca;
/*padding: 10px 10px 0px 10px !important;*/
}
.el-tree-node__content:hover .el-ic {
color: #428bca !important;
display: inline-block;
}
.el-tree-node__content:hover {
font-weight: bold;
}
.el-tree--highlight-current
.el-tree-node.is-current
> .el-tree-node__content
:hover {
.el-tree-node__expand-icon.is-leaf {
color: transparent;
cursor: default;
}
/*background-color: #3998d9;*/
.custom-tree-node {
font-weight: bold;
}
.el-tree-node__expand-icon {
font-weight: bold;
}
}
.el-dialog__body {
.upload-container .image-preview .image-preview-wrapper img {
height: 100px;
}
.el-dialog .el-collapse-item__wrap {
padding-top: 0px;
}
.spatial_img {
.el-collapse-item__wrap {
margin-bottom: 0;
padding-top: 0px;
}
}
.upload-container .image-preview .image-preview-wrapper {
width: 120px;
}
.upload-container .image-preview .image-preview-action {
line-height: 100px;
height: 100px;
}
}
.trees-coadd {
width: 100%;
border-radius: 4px;
overflow: hidden;
position: relative;
.scollhide {
overflow-x: hidden;
overflow-y: scroll;
padding: 10px 0 10px 0;
box-sizing: border-box;
.trees {
width: 100%;
}
}
.scollhide::-webkit-scrollbar {
display: none;
}
}
.conters {
display: flex;
flex-wrap: wrap;
/*max-height: 296px;*/
overflow: auto;
}
.conters:after {
content: "";
width: 410px !important;
}
.gridPic {
margin-right: 15px;
margin-bottom: 10px;
width: 110px;
height: 110px;
position: relative;
cursor: pointer;
img {
width: 100%;
height: 100%;
display: block;
}
video {
width: 100%;
height: 100%;
display: block;
}
.num_badge {
position: absolute;
bottom: 6px;
right: 6px;
display: inline-block;
min-width: 20px;
height: 20px;
border-radius: 10px;
background: #1890FF;
font-size: 14px;
text-align: center;
color: #fff;
line-height: 20px;
}
}
.conter {
width: 99%;
height: 100%;
.bnt {
width: 100%;
padding: 0 13px 10px 7px;
box-sizing: border-box;
display: flex;
}
.pictrueList {
/*padding-left: 15px;*/
width: 100%;
el-image {
width: 100%;
border: 2px solid #fff;
}
.on {
border: 2px solid #1890FF;
}
}
.el-image {
width: 110px;
height: 110px;
cursor: pointer;
}
.imagesNo {
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin: 65px 0;
.imagesNo_sp {
font-size: 13px;
color: #dbdbdb;
line-height: 3;
}
}
}
.relative{
position: relative;
}
.abs_video{
position: absolute;
right: 20px;
top: 0;
}
</style>
@@ -0,0 +1,80 @@
(function () {
/* eslint-disable */
if (window.frameElement.id) {
let parent = window.parent,
dialog = parent.$EDITORUI[window.frameElement.id.replace(/_iframe$/, '')],
editor = dialog.editor,
UE = parent.UE,
domUtils = UE.dom.domUtils,
utils = UE.utils,
browser = UE.browser,
/* eslint-disable */
ajax = UE.ajax,
$G = function (id) {
return document.getElementById(id)
},
$focus = function (node) {
setTimeout(function () {
if (browser.ie) {
var r = node.createTextRange();
r.collapse(false);
r.select();
} else {
node.focus()
}
}, 0)
};
window.nowEditor = {editor: editor, dialog: dialog};
utils.loadFile(document, {
href: editor.options.themePath + editor.options.theme + '/dialogbase.css?cache=' + Math.random(),
tag: 'link',
type: 'text/css',
rel: 'stylesheet'
});
var lang = editor.getLang(dialog.className.split('-')[2]);
if (lang) {
domUtils.on(window, 'load', function () {
var langImgPath = editor.options.langPath + editor.options.lang + '/images/';
// 针对静态资源
for (var i in lang['static']) {
var dom = $G(i);
if (!dom) continue;
let tagName = dom.tagName,
content = lang['static'][i];
if (content.src) {
// clone
content = utils.extend({}, content, false);
content.src = langImgPath + content.src;
}
if (content.style) {
content = utils.extend({}, content, false);
content.style = content.style.replace(/url\s*\(/g, 'url(' + langImgPath)
}
switch (tagName.toLowerCase()) {
case 'var':
dom.parentNode.replaceChild(document.createTextNode(content), dom);
break;
case 'select':
var ops = dom.options;
for (var j = 0, oj; oj = ops[j];) {
oj.innerHTML = content.options[j++];
}
for (var p in content) {
p != 'options' && dom.setAttribute(p, content[p]);
}
break;
default :
domUtils.setAttributes(dom, content);
}
}
});
}
}
})();
@@ -0,0 +1,24 @@
import Element from 'element-ui'
import '@/styles/element-variables.scss'
import uploadFromComponent from './index.vue'
import Vue from 'vue'
import Cookies from 'js-cookie'
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
const uploadFrom = {}
uploadFrom.install = function(Vue, options) {
const ToastConstructor = Vue.extend(uploadFromComponent)
// 生成一个该子类的实例
const instance = new ToastConstructor()
instance.$mount(document.createElement('div'))
document.body.appendChild(instance.$el)
Vue.prototype.$modalUpload = function(callback, isMore, modelName, boolean) {
instance.visible = true
instance.callback = callback
instance.isMore = isMore
instance.modelName = modelName
instance.booleanVal = boolean
}
}
export default uploadFrom
@@ -0,0 +1,50 @@
<template>
<div>
<el-dialog
title="上传图片"
:visible.sync="visible"
width="950px"
:modal="booleanVal"
append-to-body
:before-close="handleClose"
>
<upload-index v-if="visible" :isMore="isMore" :modelName="modelName" @getImage="getImage" />
</el-dialog>
</div>
</template>
<script>
// import UploadIndex from '@/components/uploadPicture/index.vue'
export default {
name: 'UploadFroms',
// components: { UploadIndex },
data() {
return {
visible: false,
callback: function() {},
isMore: '',
modelName: '',
ISmodal: false,
booleanVal: false
}
},
watch: {
// show() {
// this.visible = this.show
// }
},
methods: {
handleClose() {
this.visible = false
},
getImage(img) {
this.callback(img)
this.visible = false
}
}
}
</script>
<style scoped>
</style>
+157
View File
@@ -0,0 +1,157 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div slot="header" class="clearfix">
<el-form inline>
<el-form-item>
<el-input v-model="tableFrom.keywords" placeholder="请输入用户名称" class="selWidth">
<el-button slot="append" icon="el-icon-search" @click="search" />
</el-input>
</el-form-item>
</el-form>
</div>
<el-table
v-loading="loading"
:data="tableData.data"
width="800px"
size="small"
>
<el-table-column label="" width="40">
<template slot-scope="scope">
<el-radio v-model="templateRadio" :label="scope.row.uid" @change.native="getTemplateRow(scope.$index,scope.row)">&nbsp</el-radio>
</template>
</el-table-column>
<el-table-column
prop="uid"
label="ID"
min-width="60"
/>
<el-table-column
prop="nickname"
label="微信用户名称"
min-width="130"
/>
<el-table-column label="用户头像" min-width="80">
<template slot-scope="scope">
<div class="demo-image__preview">
<el-image
class="tabImage"
:src="scope.row.avatar"
:preview-src-list="[scope.row.avatar]"
/>
</div>
</template>
</el-table-column>
<el-table-column
label="性别"
min-width="80"
>
<template slot-scope="scope">
<span>{{ scope.row.sex | saxFilter }}</span>
</template>
</el-table-column>
<el-table-column
label="地区"
min-width="130"
>
<template slot-scope="scope">
<span>{{ scope.row.addres }}</span>
</template>
</el-table-column>
</el-table>
<div class="block">
<el-pagination
:page-sizes="[10, 20, 30, 40]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import { userListApi } from '@/api/user'
export default {
name: 'UserList',
filters: {
saxFilter(status) {
const statusMap = {
0: '未知',
1: '男',
2: '女'
}
return statusMap[status]
},
statusFilter(status) {
const statusMap = {
'wechat': '微信用户',
'routine': '小程序用户'
}
return statusMap[status]
}
},
data() {
return {
templateRadio: 0,
loading: false,
tableData: {
data: [],
total: 0
},
tableFrom: {
page: 1,
limit: 10,
keywords: ''
}
}
},
mounted() {
this.getList()
},
methods: {
getTemplateRow(idx, row) {
this.$emit('getTemplateRow', row);
},
// 列表
getList() {
this.loading = true
userListApi(this.tableFrom).then(res => {
this.tableData.data = res.list
this.tableData.total = res.total
this.loading = false
}).catch(res => {
this.$message.error(res.message)
this.loading = false
})
},
search(){
this.loading = true
userListApi({keywords:this.tableFrom.keywords}).then(res => {
this.tableData.data = res.list
this.tableData.total = res.total
this.loading = false
}).catch(res => {
this.$message.error(res.message)
this.loading = false
})
},
pageChange(page) {
this.tableFrom.page = page
this.getList()
},
handleSizeChange(val) {
this.tableFrom.limit = val
this.getList()
}
}
}
</script>
<style scoped>
</style>