Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm

This commit is contained in:
YunaiV
2024-05-19 10:54:11 +08:00
214 changed files with 16344 additions and 2560 deletions

View File

@ -25,6 +25,9 @@ defineProps({
</template>
<Icon :size="14" class="ml-5px" icon="ep:question-filled" />
</ElTooltip>
<div class="flex flex-grow pl-20px">
<slot name="header"></slot>
</div>
</div>
</template>
<div>

View File

@ -503,9 +503,13 @@ const submit = () => {
emit('update:modelValue', defaultValue.value)
dialogVisible.value = false
}
const inputChange = () => {
emit('update:modelValue', defaultValue.value)
}
</script>
<template>
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs">
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
<template #append>
<el-select v-model="select" placeholder="生成器" style="width: 115px">
<el-option label="每分钟" value="0 * * * * ?" />

View File

@ -0,0 +1,90 @@
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="cellList"
class="m-b-16px"
:rows="1"
:cols="cellCount"
:cube-size="38"
@hot-area-selected="handleHotAreaSelected"
/>
<img src="@/assets/imgs/diy/app-nav-bar-mp.png" alt="" class="h-30px w-76px" v-if="isMp" />
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio>
<el-radio label="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<el-form-item label="内容" :prop="`cell[${cellIndex}].text`">
<el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="颜色" :prop="`cell[${cellIndex}].text`">
<ColorInput v-model="cell!.textColor" />
</el-form-item>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<el-form-item label="图片" :prop="`cell[${cellIndex}].imgUrl`">
<UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" :prop="`cell[${cellIndex}].url`">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<el-form-item label="提示文字" :prop="`cell[${cellIndex}].placeholder`">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="圆角" :prop="`cell[${cellIndex}].borderRadius`">
<el-slider
v-model="cell.borderRadius"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
</template>
</template>
</template>
<script setup lang="ts">
import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarCellProperty' })
const props = defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const { formData: cellList } = usePropertyForm(props.modelValue, emit)
if (!cellList.value) cellList.value = []
// 单元格数量小程序6个右侧胶囊按钮占了2个其它平台8个
const cellCount = computed(() => (props.isMp ? 6 : 8))
// 选中的热区
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
selectedHotAreaIndex.value = index
if (!cellValue.type) {
cellValue.type = 'text'
cellValue.textColor = '#111111'
}
}
</script>
<style scoped lang="scss"></style>

View File

@ -2,22 +2,53 @@ import { DiyComponent } from '@/components/DiyEditor/util'
/** 顶部导航栏属性 */
export interface NavigationBarProperty {
// 页面标题
title: string
// 页面描述
description: string
// 顶部导航高度
navBarHeight: number
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 图片链接
bgImg: string
// 样式类型:默认 | 沉浸式
styleType: 'default' | 'immersion'
styleType: 'normal' | 'inner'
// 常驻显示
alwaysShow: boolean
// 是否显示返回按钮
showGoBack: boolean
// 小程序单元格列表
mpCells: NavigationBarCellProperty[]
// 其它平台单元格列表
otherCells: NavigationBarCellProperty[]
// 本地变量
_local: {
// 预览顶部导航(小程序)
previewMp: boolean
// 预览顶部导航(非小程序)
previewOther: boolean
}
}
/** 顶部导航栏 - 单元格 属性 */
export interface NavigationBarCellProperty {
// 类型:文字 | 图片 | 搜索框
type: 'text' | 'image' | 'search'
// 宽度
width: number
// 高度
height: number
// 顶部位置
top: number
// 左侧位置
left: number
// 文字内容
text: string
// 文字颜色
textColor: string
// 图片地址
imgUrl: string
// 图片链接
url: string
// 搜索框:提示文字
placeholder: string
// 搜索框:边框圆角半径
borderRadius: number
}
// 定义组件
@ -26,13 +57,26 @@ export const component = {
name: '顶部导航栏',
icon: 'tabler:layout-navbar',
property: {
title: '页面标题',
description: '',
navBarHeight: 35,
backgroundColor: '#fff',
backgroundImage: '',
styleType: 'default',
bgType: 'color',
bgColor: '#fff',
bgImg: '',
styleType: 'normal',
alwaysShow: true,
showGoBack: true
mpCells: [
{
type: 'text',
textColor: '#111111'
}
],
otherCells: [
{
type: 'text',
textColor: '#111111'
}
],
_local: {
previewMp: true,
previewOther: false
}
}
} as DiyComponent<NavigationBarProperty>

View File

@ -1,45 +1,73 @@
<template>
<div
class="navigation-bar"
:style="{
height: `${property.navBarHeight}px`,
backgroundColor: property.backgroundColor,
backgroundImage: `url(${property.backgroundImage})`
}"
>
<!-- 左侧 -->
<div class="left">
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
<div class="navigation-bar" :style="bgStyle">
<div class="h-full w-full flex items-center">
<div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
<span v-if="cell.type === 'text'">{{ cell.text }}</span>
<img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
<SearchBar v-else :property="getSearchProp" />
</div>
</div>
<!-- 中间 -->
<div
class="center"
:style="{
height: `${property.navBarHeight}px`,
lineHeight: `${property.navBarHeight}px`
}"
>
{{ property.title }}
</div>
<!-- 右侧 -->
<div class="right"></div>
<img
v-if="property._local?.previewMp"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
alt=""
class="h-30px w-86px"
/>
</div>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { NavigationBarCellProperty, NavigationBarProperty } from './config'
import SearchBar from '@/components/DiyEditor/components/mobile/SearchBar/index.vue'
import { StyleValue } from 'vue'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 页面顶部导航栏 */
defineOptions({ name: 'NavigationBar' })
defineProps<{ property: NavigationBarProperty }>()
const props = defineProps<{ property: NavigationBarProperty }>()
// 背景
const bgStyle = computed(() => {
const background =
props.property.bgType === 'img' && props.property.bgImg
? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
: props.property.bgColor
return { background }
})
// 单元格列表
const cellList = computed(() =>
props.property._local?.previewMp ? props.property.mpCells : props.property.otherCells
)
// 单元格宽度
const cellWidth = computed(() => {
return props.property._local?.previewMp ? (375 - 80 - 86) / 6 : (375 - 90) / 8
})
// 获得单元格样式
const getCellStyle = (cell: NavigationBarCellProperty) => {
return {
width: cell.width * cellWidth.value + (cell.width - 1) * 10 + 'px',
left: cell.left * cellWidth.value + (cell.left + 1) * 10 + 'px',
position: 'absolute'
} as StyleValue
}
// 获得搜索框属性
const getSearchProp = (cell: NavigationBarCellProperty) => {
return {
height: 30,
showScan: false,
placeholder: cell.placeholder,
borderRadius: cell.borderRadius
} as SearchProperty
}
</script>
<style lang="scss" scoped>
.navigation-bar {
display: flex;
height: 35px;
height: 50px;
background: #fff;
justify-content: space-between;
align-items: center;
padding: 0 6px;
/* 左边 */
.left {

View File

@ -1,53 +1,73 @@
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面标题" prop="title">
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
</el-form-item>
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType">
<el-radio label="default">默认</el-radio>
<el-radio label="immersion">沉浸式</el-radio>
<el-radio label="normal">标准</el-radio>
<el-tooltip
content="沉侵式头部仅支持微信小程序、APP建议页面第一个组件为图片展示类组件"
placement="top"
>
<el-radio label="inner">沉浸式</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio>
<el-radio :label="true">开启</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="高度" prop="navBarHeight">
<el-slider
v-model="formData!.navBarHeight"
:max="100"
:min="35"
show-input
input-size="small"
/>
<el-form-item label="背景类型" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="返回按钮" prop="showGoBack">
<el-switch v-model="formData!.showGoBack" />
<el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData!.backgroundColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
<el-form-item label="背景图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
</el-form-item>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容小程序</span>
<el-form-item prop="_local.previewMp" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
</el-card>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容非小程序</span>
<el-form-item prop="_local.previewOther" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
</el-card>
</el-form>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarProperty' })
// 表单校验
@ -58,6 +78,9 @@ const rules = {
const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
if (!formData.value._local) {
formData.value._local = { previewMp: true, previewOther: false }
}
</script>
<style scoped lang="scss"></style>

View File

@ -180,12 +180,12 @@ defineExpose({
</script>
<template>
<div class="z-99 border-1 border-[var(--el-border-color)] border-solid">
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
<!-- 工具栏 -->
<Toolbar
:editor="editorRef"
:editorId="editorId"
class="border-0 b-b-1 border-[var(--el-border-color)] border-solid"
class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
/>
<!-- 编辑器 -->
<Editor

View File

@ -0,0 +1,4 @@
import { useFormCreateDesigner } from './src/useFormCreateDesigner'
import { useApiSelect } from './src/components/useApiSelect'
export { useFormCreateDesigner, useApiSelect }

View File

@ -0,0 +1,59 @@
<!-- 数据字典 Select 选择器 -->
<template>
<el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
<el-option
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
<el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
<el-checkbox
v-for="(dict, index) in getDictOptions"
:key="index"
:label="dict.label"
:value="dict.value"
/>
</el-checkbox-group>
</template>
<script lang="ts" setup>
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
defineOptions({ name: 'DictSelect' })
const attrs = useAttrs()
// 接受父组件参数
interface Props {
dictType: string // 字典类型
valueType?: 'str' | 'int' | 'bool' // 字典值类型
selectType?: 'select' | 'radio' | 'checkbox' // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
formCreateInject?: any
}
const props = withDefaults(defineProps<Props>(), {
valueType: 'str',
selectType: 'select'
})
// 获得字典配置
const getDictOptions = computed(() => {
switch (props.valueType) {
case 'str':
return getStrDictOptions(props.dictType)
case 'int':
return getIntDictOptions(props.dictType)
case 'bool':
return getBoolDictOptions(props.dictType)
default:
return []
}
})
</script>

View File

@ -0,0 +1,143 @@
import request from '@/config/axios'
import { isEmpty } from '@/utils/is'
import { ApiSelectProps } from '@/components/FormCreate/src/type'
import { jsonParse } from '@/utils'
export const useApiSelect = (option: ApiSelectProps) => {
return defineComponent({
name: option.name,
props: {
// 选项标签
labelField: {
type: String,
default: () => option.labelField ?? 'label'
},
// 选项的值
valueField: {
type: String,
default: () => option.valueField ?? 'value'
},
// api 接口
url: {
type: String,
default: () => option.url ?? ''
},
// 请求类型
method: {
type: String,
default: 'GET'
},
// 请求参数
data: {
type: String,
default: ''
},
// 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
selectType: {
type: String,
default: 'select'
},
// 是否多选
multiple: {
type: Boolean,
default: false
}
},
setup(props) {
const attrs = useAttrs()
const options = ref<any[]>([]) // 下拉数据
const getOptions = async () => {
options.value = []
// 接口选择器
if (isEmpty(props.url)) {
return
}
let data = []
switch (props.method) {
case 'GET':
data = await request.get({ url: props.url })
break
case 'POST':
data = await request.post({ url: props.url, data: jsonParse(props.data) })
break
}
if (Array.isArray(data)) {
options.value = data.map((item: any) => ({
label: item[props.labelField],
value: item[props.valueField]
}))
return
}
console.error(`接口[${props.url}] 返回结果不是一个数组`)
}
onMounted(async () => {
await getOptions()
})
const buildSelect = () => {
if (props.multiple) {
// fix多写此步是为了解决 multiple 属性问题
return (
<el-select class="w-1/1" {...attrs} multiple>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
return (
<el-select class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-option key={index} label={item.label} value={item.value} />
))}
</el-select>
)
}
const buildCheckbox = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-checkbox-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-checkbox key={index} label={item.label} value={item.value} />
))}
</el-checkbox-group>
)
}
const buildRadio = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' }
]
}
return (
<el-radio-group class="w-1/1" {...attrs}>
{options.value.map((item, index) => (
<el-radio key={index} value={item.value}>
{item.label}
</el-radio>
))}
</el-radio-group>
)
}
return () => (
<>
{props.selectType === 'select'
? buildSelect()
: props.selectType === 'radio'
? buildRadio()
: props.selectType === 'checkbox'
? buildCheckbox()
: buildSelect()}
</>
)
}
})
}

View File

@ -0,0 +1,15 @@
import { useUploadFileRule } from './useUploadFileRule'
import { useUploadImgRule } from './useUploadImgRule'
import { useUploadImgsRule } from './useUploadImgsRule'
import { useDictSelectRule } from './useDictSelectRule'
import { useEditorRule } from './useEditorRule'
import { useSelectRule } from './useSelectRule'
export {
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule,
useDictSelectRule,
useEditorRule,
useSelectRule
}

View File

@ -0,0 +1,147 @@
const selectRule = [
{
type: 'select',
field: 'selectType',
title: '选择器类型',
value: 'select',
options: [
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '多选框', value: 'checkbox' }
],
// 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
control: [
{
value: 'select',
condition: '=',
method: 'hidden',
rule: ['multiple']
}
]
},
{ type: 'switch', field: 'multiple', title: '是否多选' },
{
type: 'switch',
field: 'disabled',
title: '是否禁用'
},
{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
{
type: 'switch',
field: 'collapseTags',
title: '多选时是否将选中值按文字的形式展示'
},
{
type: 'inputNumber',
field: 'multipleLimit',
title: '多选时用户最多可以选择的项目数,为 0 则不限制',
props: { min: 0 }
},
{
type: 'input',
field: 'autocomplete',
title: 'autocomplete 属性'
},
{ type: 'input', field: 'placeholder', title: '占位符' },
{
type: 'switch',
field: 'filterable',
title: '是否可搜索'
},
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
{
type: 'input',
field: 'noMatchText',
title: '搜索条件无匹配时显示的文字'
},
{
type: 'switch',
field: 'remote',
title: '其中的选项是否从服务器远程加载'
},
{
type: 'Struct',
field: 'remoteMethod',
title: '自定义远程搜索方法'
},
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
{
type: 'switch',
field: 'reserveKeyword',
title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
},
{
type: 'switch',
field: 'defaultFirstOption',
title: '在输入框按下回车,选择第一个匹配项'
},
{
type: 'switch',
field: 'popperAppendToBody',
title: '是否将弹出框插入至 body 元素',
value: true
},
{
type: 'switch',
field: 'automaticDropdown',
title: '对于不可搜索的 Select是否在输入框获得焦点后自动弹出选项菜单'
}
]
const apiSelectRule = [
{
type: 'input',
field: 'url',
title: 'url 地址',
props: {
placeholder: '/system/user/simple-list'
}
},
{
type: 'select',
field: 'method',
title: '请求类型',
value: 'GET',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }
],
control: [
{
value: 'GET',
condition: '!=',
method: 'hidden',
rule: [
{
type: 'input',
field: 'data',
title: '请求参数 JSON 格式',
props: {
autosize: true,
type: 'textarea',
placeholder: '{"type": 1}'
}
}
]
}
]
},
{
type: 'input',
field: 'labelField',
title: 'label 属性',
props: {
placeholder: 'nickname'
}
},
{
type: 'input',
field: 'valueField',
title: 'value 属性',
props: {
placeholder: 'id'
}
}
]
export { selectRule, apiSelectRule }

View File

@ -0,0 +1,62 @@
import { generateUUID } from '@/utils'
import * as DictDataApi from '@/api/system/dict/dict.type'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule
*/
export const useDictSelectRule = () => {
const label = '字典选择器'
const name = 'DictSelect'
const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
onMounted(async () => {
const data = await DictDataApi.getSimpleDictTypeList()
if (!data || data.length === 0) {
return
}
dictOptions.value =
data?.map((item: DictDataApi.DictTypeVO) => ({
label: item.name,
value: item.type
})) ?? []
})
return {
icon: 'icon-doc-text',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'dictType',
title: '字典类型',
value: '',
options: dictOptions.value
},
{
type: 'select',
field: 'dictValueType',
title: '字典值类型',
value: 'str',
options: [
{ label: '数字', value: 'int' },
{ label: '字符串', value: 'str' },
{ label: '布尔值', value: 'bool' }
]
},
...selectRule
])
}
}
}

View File

@ -0,0 +1,32 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useEditorRule = () => {
const label = '富文本'
const name = 'Editor'
return {
icon: 'icon-editor',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'input',
field: 'height',
title: '高度'
},
{ type: 'switch', field: 'readonly', title: '是否只读' }
])
}
}
}

View File

@ -0,0 +1,34 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
import { selectRule } from '@/components/FormCreate/src/config/selectRule'
import { SelectRuleOption } from '@/components/FormCreate/src/type'
/**
* 通用选择器规则 hook
*
* @param option 规则配置
*/
export const useSelectRule = (option: SelectRuleOption) => {
const label = option.label
const name = option.name
return {
icon: option.icon,
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
if (!option.props) {
option.props = []
}
return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule])
}
}
}

View File

@ -0,0 +1,80 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadFileRule = () => {
const label = '文件上传'
const name = 'UploadFile'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'fileType',
title: '文件类型',
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
options: [
{ label: 'doc', value: 'doc' },
{ label: 'xls', value: 'xls' },
{ label: 'ppt', value: 'ppt' },
{ label: 'txt', value: 'txt' },
{ label: 'pdf', value: 'pdf' }
],
props: {
multiple: true
}
},
{
type: 'switch',
field: 'autoUpload',
title: '是否在选取文件后立即进行上传',
value: true
},
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'switch',
field: 'isShowTip',
title: '是否显示提示',
value: true
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'switch',
field: 'disabled',
title: '是否禁用',
value: false
}
])
}
}
}

View File

@ -0,0 +1,89 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgRule = () => {
const label = '单图上传'
const name = 'UploadImg'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
},
{
type: 'switch',
field: 'disabled',
title: '是否显示删除按钮',
value: true
},
{
type: 'switch',
field: 'showBtnText',
title: '是否显示按钮文字',
value: true
}
])
}
}
}

View File

@ -0,0 +1,84 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUploadImgsRule = () => {
const label = '多图上传'
const name = 'UploadImgs'
return {
icon: 'icon-upload',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'switch',
field: 'drag',
title: '拖拽上传',
value: false
},
{
type: 'select',
field: 'fileType',
title: '图片类型限制',
value: ['image/jpeg', 'image/png', 'image/gif'],
options: [
{ label: 'image/apng', value: 'image/apng' },
{ label: 'image/bmp', value: 'image/bmp' },
{ label: 'image/gif', value: 'image/gif' },
{ label: 'image/jpeg', value: 'image/jpeg' },
{ label: 'image/pjpeg', value: 'image/pjpeg' },
{ label: 'image/svg+xml', value: 'image/svg+xml' },
{ label: 'image/tiff', value: 'image/tiff' },
{ label: 'image/webp', value: 'image/webp' },
{ label: 'image/x-icon', value: 'image/x-icon' }
],
props: {
multiple: true
}
},
{
type: 'inputNumber',
field: 'fileSize',
title: '大小限制(MB)',
value: 5,
props: { min: 0 }
},
{
type: 'inputNumber',
field: 'limit',
title: '数量限制',
value: 5,
props: { min: 0 }
},
{
type: 'input',
field: 'height',
title: '组件高度',
value: '150px'
},
{
type: 'input',
field: 'width',
title: '组件宽度',
value: '150px'
},
{
type: 'input',
field: 'borderradius',
title: '组件边框圆角',
value: '8px'
}
])
}
}
}

View File

@ -0,0 +1,50 @@
import { Rule } from '@form-create/element-ui' //左侧拖拽按钮
// 左侧拖拽按钮
export interface MenuItem {
label: string
name: string
icon: string
}
// 左侧拖拽按钮分类
export interface Menu {
title: string
name: string
list: MenuItem[]
}
export interface MenuList extends Array<Menu> {}
// 拖拽组件的规则
export interface DragRule {
icon: string
name: string
label: string
children?: string
inside?: true
drag?: true | String
dragBtn?: false
mask?: false
rule(): Rule
props(v: any, v1: any): Rule[]
}
// 通用下拉组件 Props 类型
export interface ApiSelectProps {
name: string // 组件名称
labelField?: string // 选项标签
valueField?: string // 选项的值
url?: string // url 接口
isDict?: boolean // 是否字典选择器
}
// 选择组件规则配置类型
export interface SelectRuleOption {
label: string // label 名称
name: string // 组件名称
icon: string // 组件图标
props?: any[] // 组件规则
}

View File

@ -0,0 +1,100 @@
import {
useDictSelectRule,
useEditorRule,
useSelectRule,
useUploadFileRule,
useUploadImgRule,
useUploadImgsRule
} from './config'
import { Ref } from 'vue'
import { Menu } from '@/components/FormCreate/src/type'
import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
/**
* 表单设计器增强 hook
* 新增
* - 文件上传
* - 单图上传
* - 多图上传
* - 字典选择器
* - 用户选择器
* - 部门选择器
* - 富文本
*/
export const useFormCreateDesigner = async (designer: Ref) => {
const editorRule = useEditorRule()
const uploadFileRule = useUploadFileRule()
const uploadImgRule = useUploadImgRule()
const uploadImgsRule = useUploadImgsRule()
/**
* 构建表单组件
*/
const buildFormComponents = () => {
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
designer.value?.removeMenuItem('upload')
// 移除自带的富文本组件规则,使用 editorRule 替代
designer.value?.removeMenuItem('fc-editor')
const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
components.forEach((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `main` 分类下
designer.value?.appendMenuItem('main', {
icon: component.icon,
name: component.name,
label: component.label
})
})
}
const userSelectRule = useSelectRule({
name: 'UserSelect',
label: '用户选择器',
icon: 'icon-user-o'
})
const deptSelectRule = useSelectRule({
name: 'DeptSelect',
label: '部门选择器',
icon: 'icon-address-card-o'
})
const dictSelectRule = useDictSelectRule()
const apiSelectRule0 = useSelectRule({
name: 'ApiSelect',
label: '接口选择器',
icon: 'icon-server',
props: [...apiSelectRule]
})
/**
* 构建系统字段菜单
*/
const buildSystemMenu = () => {
// 移除自带的下拉选择器组件,使用 currencySelectRule 替代
designer.value?.removeMenuItem('select')
designer.value?.removeMenuItem('radio')
designer.value?.removeMenuItem('checkbox')
const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
const menu: Menu = {
name: 'system',
title: '系统字段',
list: components.map((component) => {
// 插入组件规则
designer.value?.addComponent(component)
// 插入拖拽按钮到 `system` 分类下
return {
icon: component.icon,
name: component.name,
label: component.label
}
})
}
designer.value?.addMenu(menu)
}
onMounted(async () => {
await nextTick()
buildFormComponents()
buildSystemMenu()
})
}

View File

@ -0,0 +1,79 @@
// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
export function makeRequiredRule() {
return {
type: 'Required',
field: 'formCreate$required',
title: '是否必填'
}
}
export const localeProps = (t, prefix, rules) => {
return rules.map((rule) => {
if (rule.field === 'formCreate$required') {
rule.title = t('props.required') || rule.title
} else if (rule.field && rule.field !== '_optionType') {
rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
}
return rule
})
}
export function upper(str) {
return str.replace(str[0], str[0].toLocaleUpperCase())
}
export function makeOptionsRule(t, to, userOptions) {
console.log(userOptions[0])
const options = [
{ label: t('props.optionsType.struct'), value: 0 },
{ label: t('props.optionsType.json'), value: 1 },
{ label: '用户数据', value: 2 }
]
const control = [
{
value: 0,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 1,
rule: [
{
type: 'Struct',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { defaultValue: [] }
}
]
},
{
value: 2,
rule: [
{
type: 'TableOptions',
field: 'formCreate' + upper(to).replace('.', '>'),
props: { modelValue: [] }
}
]
}
]
options.splice(0, 0)
control.push()
return {
type: 'radio',
title: t('props.options'),
field: '_optionType',
value: 0,
options,
props: {
type: 'button'
},
control
}
}

View File

@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
initialIndex = 0,
infinite = true,
hideOnClickModal = false,
appendToBody = false,
teleported = false,
zIndex = 2000,
show = true
} = options
@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
propsData.initialIndex = initialIndex
propsData.infinite = infinite
propsData.hideOnClickModal = hideOnClickModal
propsData.appendToBody = appendToBody
propsData.teleported = teleported
propsData.zIndex = zIndex
propsData.show = show

View File

@ -13,7 +13,7 @@ const props = defineProps({
initialIndex: propTypes.number.def(0),
infinite: propTypes.bool.def(true),
hideOnClickModal: propTypes.bool.def(false),
appendToBody: propTypes.bool.def(false),
teleported: propTypes.bool.def(false),
show: propTypes.bool.def(false)
})

View File

@ -4,6 +4,6 @@ export interface ImageViewerProps {
initialIndex?: number
infinite?: boolean
hideOnClickModal?: boolean
appendToBody?: boolean
teleported?: boolean
show?: boolean
}

View File

@ -189,7 +189,7 @@ const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
// 热区选中
const selectedHotAreaIndex = ref(-1)
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index)

View File

@ -23,7 +23,7 @@
</template>
<script lang="ts" setup>
import { OperateLogV2VO } from '@/api/system/operatelog'
import { OperateLogVO } from '@/api/system/operatelog'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
import { ElTag } from 'element-plus'
@ -31,7 +31,7 @@ import { ElTag } from 'element-plus'
defineOptions({ name: 'OperateLogV2' })
interface Props {
logList: OperateLogV2VO[] // 操作日志列表
logList: OperateLogVO[] // 操作日志列表
}
withDefaults(defineProps<Props>(), {

View File

@ -53,7 +53,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination'])
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const currentPage = computed({
get() {
return props.page

View File

@ -26,7 +26,7 @@
placeholder="请输入菜单内容"
:remote-method="remoteMethod"
class="overflow-hidden transition-all-600"
:class="showTopSearch ? 'w-220px ml2' : 'w-0'"
:class="showTopSearch ? '!w-220px ml2' : '!w-0'"
@change="handleChange"
>
<el-option

View File

@ -6,7 +6,9 @@
:action="uploadUrl"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="props.limit"
:multiple="props.limit > 1"
:on-error="excelUploadError"
@ -15,15 +17,14 @@
:on-remove="handleRemove"
:on-success="handleFileSuccess"
:show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader"
name="file"
>
<el-button type="primary">
<el-button v-if="!disabled" type="primary">
<Icon icon="ep:upload-filled" />
选取文件
</el-button>
<template v-if="isShowTip" #tip>
<template v-if="isShowTip && !disabled" #tip>
<div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div>
@ -31,6 +32,25 @@
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
</div>
</template>
<template #file="row">
<div class="flex items-center">
<span>{{ row.file.name }}</span>
<div class="ml-10px">
<el-link
:href="row.file.url"
:underline="false"
download
target="_blank"
type="primary"
>
下载
</el-link>
</div>
<div class="ml-10px">
<el-button link type="danger" @click="handleRemove(row.file)"> 删除</el-button>
</div>
</div>
</template>
</el-upload>
</div>
</template>
@ -48,13 +68,13 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
title: propTypes.string.def('文件上传'),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // 大小限制(MB)
limit: propTypes.number.def(5), // 数量限制
autoUpload: propTypes.bool.def(true), // 自动上传
drag: propTypes.bool.def(false), // 拖拽上传
isShowTip: propTypes.bool.def(true) // 是否显示提示
isShowTip: propTypes.bool.def(true), // 是否显示提示
disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false
})
// ========== 上传相关 ==========

View File

@ -6,17 +6,18 @@
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:multiple="false"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false"
:http-request="httpRequest"
>
<template v-if="modelValue">
<img :src="modelValue" class="upload-image" />
<div class="upload-handle" @click.stop>
<div class="handle-icon" @click="editImg" v-if="!disabled">
<div v-if="!disabled" class="handle-icon" @click="editImg">
<Icon icon="ep:edit" />
<span v-if="showBtnText">{{ t('action.edit') }}</span>
</div>
@ -77,10 +78,8 @@ const props = defineProps({
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px
// 是否显示删除按钮
showDelete: propTypes.bool.def(true),
// 是否显示按钮文字
showBtnText: propTypes.bool.def(true)
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗

View File

@ -6,13 +6,14 @@
:action="uploadUrl"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="limit"
:multiple="true"
:on-error="uploadError"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
:http-request="httpRequest"
list-type="picture-card"
>
<div class="upload-empty">

View File

@ -17,7 +17,11 @@ export const useUpload = () => {
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
return axios.put(presignedInfo.uploadUrl, options.file).then(() => {
return axios.put(presignedInfo.uploadUrl, options.file, {
headers: {
'Content-Type': options.file.type,
}
}).then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, fileName, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致

View File

@ -3,13 +3,6 @@
<el-form label-width="90px" :model="needProps" :rules="rules">
<div v-if="needProps.type == 'bpmn:Process'">
<!-- 如果是 Process 信息的时候使用自定义表单 -->
<el-link
href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B"
type="danger"
target="_blank"
>
如何实现实现会签或签
</el-link>
<el-form-item label="流程标识" prop="id">
<el-input
v-model="needProps.id"
@ -139,14 +132,6 @@ const updateBaseInfo = (key) => {
}
}
onMounted(() => {
// 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
setTimeout(() => {
handleKeyUpdate(props.model.key)
handleNameUpdate(props.model.name)
}, 110)
})
watch(
() => props.businessObject,
(val) => {

View File

@ -79,13 +79,13 @@ const resetFlowCondition = () => {
bpmnElement.value = bpmnInstances().bpmnElement
bpmnElementSource.value = bpmnElement.value.source
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
// 初始化默认type为default
flowConditionForm.value = { type: 'default' }
if (
bpmnElementSourceRef.value &&
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id &&
flowConditionForm.value.type == 'default'
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
// 默认
flowConditionForm.value = { type: 'default' }
} else if (!bpmnElement.value.businessObject.conditionExpression) {
// 普通