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

# Conflicts:
#	pnpm-lock.yaml
This commit is contained in:
YunaiV
2025-06-11 21:59:45 +08:00
127 changed files with 20603 additions and 3623 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div @click.stop>
<Dialog
v-model="dialogVisible"
:canFullscreen="false"
@@ -181,6 +181,7 @@ function openModal() {
}
function closeModal() {
debugger
dialogVisible.value = false
}

View File

@@ -0,0 +1,122 @@
<template>
<Dialog v-model="dialogVisible" title="部门选择" width="600">
<el-row v-loading="formLoading">
<el-col :span="24">
<ContentWrap class="h-1/1">
<el-tree
ref="treeRef"
:data="deptTree"
:props="defaultProps"
show-checkbox
:check-strictly="checkStrictly"
check-on-click-node
default-expand-all
highlight-current
node-key="id"
@check="handleCheck"
/>
</ContentWrap>
</el-col>
</el-row>
<template #footer>
<el-button
:disabled="formLoading || !selectedDeptIds?.length"
type="primary"
@click="submitForm"
>
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
defineOptions({ name: 'DeptSelectForm' })
const emit = defineEmits<{
confirm: [deptList: any[]]
}>()
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps({
// 是否严格的遵循父子不互相关联
checkStrictly: {
type: Boolean,
default: false
},
// 是否支持多选
multiple: {
type: Boolean,
default: true
}
})
const treeRef = ref()
const deptTree = ref<Tree[]>([]) // 部门树形结构
const selectedDeptIds = ref<number[]>([]) // 选中的部门 ID 列表
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
/** 打开弹窗 */
const open = async (selectedList?: DeptApi.DeptVO[]) => {
resetForm()
formLoading.value = true
try {
// 加载部门列表
const deptData = await DeptApi.getSimpleDeptList()
deptTree.value = handleTree(deptData)
} finally {
formLoading.value = false
}
dialogVisible.value = true
// 设置已选择的部门
if (selectedList?.length) {
await nextTick()
const selectedIds = selectedList
.map((dept) => dept.id)
.filter((id): id is number => id !== undefined)
selectedDeptIds.value = selectedIds
treeRef.value?.setCheckedKeys(selectedIds)
}
}
/** 处理选中状态变化 */
const handleCheck = (data: any, checked: any) => {
selectedDeptIds.value = treeRef.value.getCheckedKeys()
if (!props.multiple && selectedDeptIds.value.length > 1) {
// 单选模式下,只保留最后选择的节点
const lastSelectedId = selectedDeptIds.value[selectedDeptIds.value.length - 1]
selectedDeptIds.value = [lastSelectedId]
treeRef.value.setCheckedKeys([lastSelectedId])
}
}
/** 提交选择 */
const submitForm = async () => {
try {
// 获取选中的完整部门数据
const checkedNodes = treeRef.value.getCheckedNodes()
message.success(t('common.updateSuccess'))
dialogVisible.value = false
emit('confirm', checkedNodes)
} finally {
}
}
/** 重置表单 */
const resetForm = () => {
deptTree.value = []
selectedDeptIds.value = []
if (treeRef.value) {
treeRef.value.setCheckedKeys([])
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>

View File

@@ -68,6 +68,7 @@ const dialogStyle = computed(() => {
draggable
class="com-dialog"
:show-close="false"
@close="$emit('update:modelValue', false)"
>
<template #header="{ close }">
<div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
@@ -90,7 +91,7 @@ const dialogStyle = computed(() => {
icon="ep:close"
hover-color="var(--el-color-primary)"
color="var(--el-color-info)"
@click="close"
@click.stop="close"
/>
</div>
</div>

View File

@@ -13,7 +13,7 @@ export const CouponDiscount = defineComponent({
setup(props) {
const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
// 折扣
let value = coupon.discountPercent + ''
let value = coupon.discountPercent / 10 + ''
let suffix = ' 折'
// 满减
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
@@ -43,7 +43,7 @@ export const CouponDiscountDesc = defineComponent({
const discountDesc =
coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
? `${floatToFixed2(coupon.discountPrice)}`
: `${coupon.discountPercent}`
: `${coupon.discountPercent / 10.0}`
return () => (
<div>
<span>{useCondition}</span>

View File

@@ -49,7 +49,13 @@
<div class="flex flex-col justify-evenly gap-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<!-- 领取说明 -->
<div v-if="coupon.totalCount >= 0">
仅剩{{ coupon.totalCount - coupon.takeCount }}
</div>
<div v-else-if="coupon.totalCount === -1">仅剩不限制</div>
</div>
<div class="flex flex-col">
<div
@@ -67,7 +73,8 @@
<div v-else class="flex flex-col items-center justify-around gap-4px p-4px">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<div>{{ coupon.name }}</div>
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<div
class="rounded-20px p-x-8px p-y-2px"
:style="{
@@ -124,7 +131,7 @@ watch(
() => {
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1)/ 列数
couponWidth.value =
(phoneWidth.value * 0.95 - props.property.space * (props.property.columns - 1)) /
(phoneWidth.value - props.property.space * (props.property.columns - 1)) /
props.property.columns
// 显示滚动条
scrollbarWidth.value = `${

View File

@@ -1,16 +1,19 @@
<template>
<div
class="relative"
:style="{ height: `${rowCount * CUBE_SIZE}px`, width: `${4 * CUBE_SIZE}px` }"
:style="{
height: `${rowCount * CUBE_SIZE}px`,
width: `${4 * CUBE_SIZE}px`,
padding: `${property.space}px`
}"
>
<div
v-for="(item, index) in property.list"
:key="index"
class="absolute"
:style="{
width: `${item.width * CUBE_SIZE - property.space * 2}px`,
height: `${item.height * CUBE_SIZE - property.space * 2}px`,
margin: `${property.space}px`,
width: `${item.width * CUBE_SIZE - property.space}px`,
height: `${item.height * CUBE_SIZE - property.space}px`,
top: `${item.top * CUBE_SIZE}px`,
left: `${item.left * CUBE_SIZE}px`
}"
@@ -63,10 +66,10 @@ const rowCount = computed(() => {
let count = 0
if (props.property.list.length > 0) {
// 最大行号
count = Math.max(...props.property.list.map((item) => item.bottom))
count = Math.max(...props.property.list.map((item) => item.top + item.height))
}
// 行号从 0 开始,所以加 1
return count + 1
// 保证至少有一行
return count == 0 ? 1 : count
})
</script>

View File

@@ -39,7 +39,7 @@
</span>
</div>
</div>
</el-carousel-item>
</el-carousel-item>
</el-carousel>
</template>
@@ -51,7 +51,7 @@ const props = defineProps<{ property: MenuSwiperProperty }>()
// 标题的高度
const TITLE_HEIGHT = 20
// 图标的高度
const ICON_SIZE = 42
const ICON_SIZE = 32
// 垂直间距:一行上下的间距
const SPACE_Y = 16

View File

@@ -29,7 +29,10 @@
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="背景图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
<div class="flex items-center">
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
<span class="text-xs text-gray-400 ml-2 mb-2">建议宽度750</span>
</div>
</el-form-item>
<el-card class="property-group" shadow="never">
<template #header>
@@ -39,8 +42,9 @@
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
预览
</el-checkbox>
</el-form-item>
</div>
</template>
@@ -54,8 +58,9 @@
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
预览
</el-checkbox>
</el-form-item>
</div>
</template>

View File

@@ -82,8 +82,8 @@ export const component = {
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
borderRadiusTop: 6,
borderRadiusBottom: 6,
space: 8,
spuIds: [],
style: {

View File

@@ -14,7 +14,10 @@
:key="index"
>
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<div
v-if="property.badge.show && property.badge.imgUrl"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->

View File

@@ -3,7 +3,7 @@
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-card header="搜索热词" class="property-group" shadow="never">
<Draggable v-model="formData.hotKeywords" :empty-item="''">
<Draggable v-model="formData.hotKeywords" :empty-item="''" :min="0">
<template #default="{ index }">
<el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
</template>
@@ -61,6 +61,7 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
import { isString } from '@/utils/is'
/** 搜索框属性面板 */
defineOptions({ name: 'SearchProperty' })
@@ -68,6 +69,19 @@ defineOptions({ name: 'SearchProperty' })
const props = defineProps<{ modelValue: SearchProperty }>()
const emit = defineEmits(['update:modelValue'])
const formData = useVModel(props, 'modelValue', emit)
// 监听热词数组变化
watch(
() => formData.value.hotKeywords,
(newVal) => {
// 找到非字符串项的索引
const nonStringIndex = newVal.findIndex((item) => !isString(item))
if (nonStringIndex !== -1) {
formData.value.hotKeywords[nonStringIndex] = ''
}
},
{ deep: true, flush: 'post' }
)
</script>
<style scoped lang="scss"></style>

View File

@@ -1,7 +1,9 @@
import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 标题栏属性 */
export interface TitleBarProperty {
// 背景图
bgImgUrl: string
// 偏移
marginLeft: number
// 显示位置
@@ -22,6 +24,8 @@ export interface TitleBarProperty {
titleColor: string
// 描述颜色
descriptionColor: string
// 高度
height: number
// 查看更多
more: {
// 是否显示查看更多
@@ -52,6 +56,8 @@ export const component = {
descriptionWeight: 200,
titleColor: 'rgba(50, 50, 51, 10)',
descriptionColor: 'rgba(150, 151, 153, 10)',
marginLeft: 0,
height: 40,
more: {
//查看更多
show: false,

View File

@@ -1,55 +1,49 @@
<template>
<div
:style="{
background:
property.style.bgType === 'color' ? property.style.bgColor : `url(${property.style.bgImg})`,
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat'
}"
class="title-bar"
>
<!-- 内容 -->
<div>
<div class="title-bar" :style="{ height: `${property.height}px` }">
<el-image v-if="property.bgImgUrl" :src="property.bgImgUrl" fit="cover" class="w-full" />
<div class="absolute left-0 top-0 w-full h-full flex flex-col justify-center">
<!-- 标题 -->
<div
v-if="property.title"
:style="{
fontSize: `${property.titleSize}px`,
fontWeight: property.titleWeight,
color: property.titleColor,
textAlign: property.textAlign
textAlign: property.textAlign,
marginLeft: `${property.marginLeft}px`,
marginBottom: '4px'
}"
v-if="property.title"
>
{{ property.title }}
</div>
<!-- 副标题 -->
<div
v-if="property.description"
:style="{
fontSize: `${property.descriptionSize}px`,
fontWeight: property.descriptionWeight,
color: property.descriptionColor,
textAlign: property.textAlign
textAlign: property.textAlign,
marginLeft: `${property.marginLeft}px`
}"
class="m-t-8px"
v-if="property.description"
>
{{ property.description }}
</div>
</div>
<!-- 更多 -->
<div
class="more"
v-show="property.more.show"
:style="{
color: property.descriptionColor
}"
class="more"
>
<span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span>
<Icon v-if="property.more.type !== 'text'" icon="ep:arrow-right" />
<Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
</div>
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { TitleBarProperty } from './config'
/** 标题栏 */
@@ -57,7 +51,7 @@ defineOptions({ name: 'TitleBar' })
defineProps<{ property: TitleBarProperty }>()
</script>
<style lang="scss" scoped>
<style scoped lang="scss">
.title-bar {
position: relative;
width: 100%;

View File

@@ -1,7 +1,12 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form :model="formData" :rules="rules" label-width="85px">
<el-card class="property-group" header="风格" shadow="never">
<el-form label-width="85px" :model="formData" :rules="rules">
<el-card header="风格" class="property-group" shadow="never">
<el-form-item label="背景图片" prop="bgImgUrl">
<UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
<template #tip>建议尺寸 750*80</template>
</UploadImg>
</el-form-item>
<el-form-item label="标题位置" prop="textAlign">
<el-radio-group v-model="formData!.textAlign">
<el-tooltip content="居左" placement="top">
@@ -16,66 +21,84 @@
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="偏移量" prop="marginLeft" label-width="70px">
<el-slider
v-model="formData.marginLeft"
:max="100"
:min="0"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="高度" prop="height" label-width="70px">
<el-slider
v-model="formData.height"
:max="200"
:min="20"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="主标题" shadow="never">
<el-form-item label="文字" label-width="40px" prop="title">
<el-card header="主标题" class="property-group" shadow="never">
<el-form-item label="文字" prop="title" label-width="40px">
<InputWithColor
v-model="formData.title"
v-model:color="formData.titleColor"
maxlength="20"
show-word-limit
maxlength="20"
/>
</el-form-item>
<el-form-item label="大小" label-width="40px" prop="titleSize">
<el-form-item label="大小" prop="titleSize" label-width="40px">
<el-slider
v-model="formData.titleSize"
:max="60"
:min="10"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="粗细" label-width="40px" prop="titleWeight">
<el-form-item label="粗细" prop="titleWeight" label-width="40px">
<el-slider
v-model="formData.titleWeight"
:max="900"
:min="100"
:max="900"
:step="100"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="副标题" shadow="never">
<el-form-item label="文字" label-width="40px" prop="description">
<el-card header="副标题" class="property-group" shadow="never">
<el-form-item label="文字" prop="description" label-width="40px">
<InputWithColor
v-model="formData.description"
v-model:color="formData.descriptionColor"
maxlength="50"
show-word-limit
maxlength="50"
/>
</el-form-item>
<el-form-item label="大小" label-width="40px" prop="descriptionSize">
<el-form-item label="大小" prop="descriptionSize" label-width="40px">
<el-slider
v-model="formData.descriptionSize"
:max="60"
:min="10"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="粗细" label-width="40px" prop="descriptionWeight">
<el-form-item label="粗细" prop="descriptionWeight" label-width="40px">
<el-slider
v-model="formData.descriptionWeight"
:max="900"
:min="100"
:max="900"
:step="100"
input-size="small"
show-input
input-size="small"
/>
</el-form-item>
</el-card>
<el-card class="property-group" header="查看更多" shadow="never">
<el-card header="查看更多" class="property-group" shadow="never">
<el-form-item label="是否显示" prop="more.show">
<el-checkbox v-model="formData.more.show" />
</el-form-item>
@@ -88,7 +111,7 @@
<el-radio value="all">文字+图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-show="formData.more.type !== 'icon'" label="更多文字" prop="more.text">
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
<el-input v-model="formData.more.text" />
</el-form-item>
<el-form-item label="跳转链接" prop="more.url">
@@ -99,7 +122,7 @@
</el-form>
</ComponentContainerProperty>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { TitleBarProperty } from './config'
import { useVModel } from '@vueuse/core'
// 导航栏属性面板
@@ -113,4 +136,4 @@ const formData = useVModel(props, 'modelValue', emit)
const rules = {}
</script>
<style lang="scss" scoped></style>
<style scoped lang="scss"></style>

View File

@@ -28,7 +28,7 @@
<Icon
icon="ep:delete"
class="cursor-pointer text-red-5"
v-if="formData.length > 1"
v-if="formData.length > min"
@click="handleDelete(index)"
/>
</el-tooltip>
@@ -69,7 +69,9 @@ const props = defineProps({
// 空的元素:点击添加按钮时,创建元素并添加到列表;默认为空对象
emptyItem: any<unknown>().def({}),
// 数量限制默认为0表示不限制
limit: propTypes.number.def(0)
limit: propTypes.number.def(0),
// 最小数量默认为1
min: propTypes.number.def(1)
})
// 定义事件
const emit = defineEmits(['update:modelValue'])

View File

@@ -69,11 +69,18 @@ export const useApiSelect = (option: ApiSelectProps) => {
if (isEmpty(props.url)) {
return
}
switch (props.method) {
case 'GET':
let url: string = props.url
if (props.remote) {
url = `${url}?${props.remoteField}=${queryParam.value}`
if (queryParam.value != undefined) {
if (url.includes('?')) {
url = `${url}&${props.remoteField}=${queryParam.value}`
} else {
url = `${url}?${props.remoteField}=${queryParam.value}`
}
}
}
parseOptions(await request.get({ url: url }))
break

View File

@@ -17,6 +17,7 @@ export const useSelectRule = (option: SelectRuleOption) => {
icon: option.icon,
label,
name,
event: option.event,
rule() {
return {
type: name,

View File

@@ -46,5 +46,6 @@ export interface SelectRuleOption {
label: string // label 名称
name: string // 组件名称
icon: string // 组件图标
props?: any[] // 组件规则
props?: any[], // 组件规则
event?: any[] // 事件配置
}

View File

@@ -63,7 +63,8 @@ export const useFormCreateDesigner = async (designer: Ref) => {
name: 'ApiSelect',
label: '接口选择器',
icon: 'icon-server',
props: [...apiSelectRule]
props: [...apiSelectRule],
event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus']
})
/**

View File

@@ -35,13 +35,13 @@
>
<!-- 右上角热区删除按钮 -->
<div
v-if="selectedHotAreaIndex === index"
v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
class="btn-delete"
@click="handleDeleteHotArea(index)"
>
<Icon icon="ep:circle-close-filled" />
</div>
{{ `${hotArea.width}×${hotArea.height}` }}
<span v-if="hotArea.width">{{ `${hotArea.width}×${hotArea.height}` }}</span>
</div>
</table>
</div>

View File

@@ -91,6 +91,7 @@ import {
DEFAULT_CONDITION_GROUP_VALUE
} from './consts'
import { generateUUID } from '@/utils'
import { cloneDeep } from 'lodash-es'
defineOptions({
name: 'NodeHandler'
@@ -184,7 +185,7 @@ const addNode = (type: number) => {
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
},
{
@@ -242,7 +243,7 @@ const addNode = (type: number) => {
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
},
{

View File

@@ -59,6 +59,11 @@ const props = defineProps({
startUserIds: {
type: Array,
required: false
},
// 可发起流程的部门编号
startDeptIds: {
type: Array,
required: false
}
})
@@ -82,6 +87,7 @@ provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
provide('startUserIds', props.startUserIds)
provide('startDeptIds', props.startDeptIds)
provide('tasks', [])
provide('processInstance', {})
const message = useMessage() // 国际化

View File

@@ -25,21 +25,46 @@
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user">
<el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text>
<el-text v-else-if="startUserIds.length == 1">
{{ getUserNicknames(startUserIds) }} 可发起流程
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="getUserNicknames(startUserIds)"
>
{{ getUserNicknames(startUserIds.slice(0, 2)) }}
{{ startUserIds.length }} 人可发起流程
</el-tooltip>
<el-text
v-if="
(!startUserIds || startUserIds.length === 0) &&
(!startDeptIds || startDeptIds.length === 0)
"
>
全部成员可以发起流程
</el-text>
<div v-else-if="startUserIds && startUserIds.length > 0">
<el-text v-if="startUserIds.length == 1">
{{ getUserNicknames(startUserIds) }} 可发起流程
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="getUserNicknames(startUserIds)"
>
{{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
{{ startUserIds.length }} 人可发起流程
</el-tooltip>
</el-text>
</div>
<div v-else-if="startDeptIds && startDeptIds.length > 0">
<el-text v-if="startDeptIds.length == 1">
{{ getDeptNames(startDeptIds) }} 可发起流程
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="getDeptNames(startDeptIds)"
>
{{ getDeptNames(startDeptIds.slice(0, 2)) }} 等
{{ startDeptIds.length }} 个部门可发起流程
</el-tooltip>
</el-text>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
@@ -107,6 +132,7 @@
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
import * as UserApi from '@/api/system/user'
import * as DeptApi from '@/api/system/dept'
defineOptions({
name: 'StartUserNodeConfig'
})
@@ -118,8 +144,12 @@ const props = defineProps({
})
// 可发起流程的用户编号
const startUserIds = inject<Ref<any[]>>('startUserIds')
// 可发起流程的部门编号
const startDeptIds = inject<Ref<any[]>>('startDeptIds')
// 用户列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
// 部门列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList')
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
@@ -145,6 +175,19 @@ const getUserNicknames = (userIds: number[]): string => {
})
return nicknames.join(',')
}
const getDeptNames = (deptIds: number[]): string => {
if (!deptIds || deptIds.length === 0) {
return ''
}
const deptNames: string[] = []
deptIds.forEach((deptId) => {
const found = deptOptions?.value.find((item) => item.id === deptId)
if (found && found.name) {
deptNames.push(found.name)
}
})
return deptNames.join(',')
}
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'

View File

@@ -254,6 +254,7 @@ import {
import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
import HttpRequestSetting from './components/HttpRequestSetting.vue'
import ConditionDialog from './components/ConditionDialog.vue'
import { cloneDeep } from 'lodash-es'
const { proxy } = getCurrentInstance() as any
defineOptions({
@@ -290,7 +291,7 @@ const configForm = ref<TriggerSetting>({
},
formSettings: [
{
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
updateFormFields: {},
deleteFields: []
}
@@ -346,7 +347,7 @@ const changeTriggerType = () => {
? originalSetting.formSettings
: [
{
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
updateFormFields: {},
deleteFields: []
}
@@ -361,7 +362,7 @@ const changeTriggerType = () => {
? originalSetting.formSettings
: [
{
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
updateFormFields: undefined,
deleteFields: []
}
@@ -374,7 +375,7 @@ const changeTriggerType = () => {
/** 添加新的修改表单设置 */
const addFormSetting = () => {
configForm.value.formSettings!.push({
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
updateFormFields: {},
deleteFields: []
})
@@ -509,7 +510,7 @@ const showTriggerNodeConfig = (node: SimpleFlowNode) => {
},
formSettings: node.triggerSetting.formSettings || [
{
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
updateFormFields: {},
deleteFields: []
}

View File

@@ -154,6 +154,7 @@ import {
} from '../../consts'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFieldsAndStartUser } from '../../node'
import { cloneDeep } from 'lodash-es'
const props = defineProps({
modelValue: {
@@ -196,7 +197,7 @@ const formRef = ref() // 表单 Ref
const changeConditionType = () => {
if (condition.value.conditionType === ConditionType.RULE) {
if (!condition.value.conditionGroups) {
condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
}
}

View File

@@ -1,5 +1,5 @@
<!-- TODO @jason有可能它里面套 Condition -->
<!-- TODO 怕影响其它节点功能后面看看如何如何复用 Condtion -->
<!-- TODO 怕影响其它节点功能后面看看如何如何复用 Condtion -->
<template>
<Dialog v-model="dialogVisible" title="条件配置" width="600px" :fullscreen="false">
<div class="h-410px">
@@ -165,6 +165,7 @@ import {
} from '../../consts'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFieldsAndStartUser } from '../../node'
import { cloneDeep } from 'lodash-es'
defineOptions({
name: 'ConditionDialog'
})
@@ -175,7 +176,7 @@ const condition = ref<{
conditionGroups?: ConditionGroup
}>({
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
})
const emit = defineEmits<{
@@ -210,7 +211,7 @@ const formRef = ref() // 表单 Ref
const changeConditionType = () => {
if (condition.value.conditionType === ConditionType.RULE) {
if (!condition.value.conditionGroups) {
condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
}
}

View File

@@ -108,11 +108,18 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import {
SimpleFlowNode,
NodeType,
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE,
NODE_DEFAULT_TEXT
} from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
import { cloneDeep } from 'lodash-es'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ExclusiveNode'
@@ -149,7 +156,8 @@ const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
conditionNode.name ||
getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
@@ -181,7 +189,7 @@ const addCondition = () => {
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
}
conditionNodes.splice(lastIndex, 0, conditionData)

View File

@@ -110,11 +110,18 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import {
SimpleFlowNode,
NodeType,
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE,
NODE_DEFAULT_TEXT
} from '../consts'
import { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
import { cloneDeep } from 'lodash-es'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'InclusiveNode'
@@ -153,7 +160,8 @@ const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
conditionNode.name ||
getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
@@ -185,7 +193,7 @@ const addCondition = () => {
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
}
}
conditionNodes.splice(lastIndex, 0, conditionData)

View File

@@ -0,0 +1,63 @@
<template>
<div ref="divRef" :class="['tinyflow', className]" :style="style" style="height: 100%"> </div>
</template>
<script setup lang="ts">
import { Item, Tinyflow as TinyflowNative } from './ui'
import './ui/index.css'
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps<{
className?: string
style?: Record<string, string>
data?: Record<string, any>
provider?: {
llm?: () => Item[] | Promise<Item[]>
knowledge?: () => Item[] | Promise<Item[]>
internal?: () => Item[] | Promise<Item[]>
}
}>()
const divRef = ref<HTMLDivElement | null>(null)
let tinyflow: TinyflowNative | null = null
// 定义默认的 provider 方法
const defaultProvider = {
llm: () => [] as Item[],
knowledge: () => [] as Item[],
internal: () => [] as Item[]
}
onMounted(() => {
if (divRef.value) {
// 合并默认 provider 和传入的 props.provider
const mergedProvider = {
...defaultProvider,
...props.provider
}
tinyflow = new TinyflowNative({
element: divRef.value as Element,
data: props.data || {},
provider: mergedProvider
})
}
})
onUnmounted(() => {
if (tinyflow) {
tinyflow.destroy()
tinyflow = null
}
})
const getData = () => {
if (tinyflow) {
return tinyflow.getData()
}
console.warn('Tinyflow instance is not initialized')
return null
}
defineExpose({
getData
})
</script>

File diff suppressed because one or more lines are too long

41
src/components/Tinyflow/ui/index.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
import { Edge } from '@xyflow/svelte';
import { Node as Node_2 } from '@xyflow/svelte';
import { useSvelteFlow } from '@xyflow/svelte';
import { Viewport } from '@xyflow/svelte';
export declare type Item = {
value: number | string;
label: string;
children?: Item[];
};
export declare class Tinyflow {
private options;
private rootEl;
private svelteFlowInstance;
constructor(options: TinyflowOptions);
private _init;
private _setOptions;
getOptions(): TinyflowOptions;
getData(): {
nodes: Node_2[];
edges: Edge[];
viewport: Viewport;
};
setData(data: TinyflowData): void;
destroy(): void;
}
export declare type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
export declare type TinyflowOptions = {
element: string | Element;
data?: TinyflowData;
provider?: {
llm?: () => Item[] | Promise<Item[]>;
knowledge?: () => Item[] | Promise<Item[]>;
internal?: () => Item[] | Promise<Item[]>;
};
};
export { }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -86,7 +86,8 @@ const props = defineProps({
autoUpload: propTypes.bool.def(true), // 自动上传
drag: propTypes.bool.def(false), // 拖拽上传
isShowTip: propTypes.bool.def(true), // 是否显示提示
disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false
directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined
})
// ========== 上传相关 ==========
@@ -95,7 +96,7 @@ const uploadList = ref<UploadUserFile[]>([])
const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0)
const { uploadUrl, httpRequest } = useUpload()
const { uploadUrl, httpRequest } = useUpload(props.directory)
// 文件上传之前判断
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {

View File

@@ -79,7 +79,8 @@ const props = defineProps({
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
showBtnText: propTypes.bool.def(true), // 是否显示按钮文字
directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
@@ -99,7 +100,7 @@ const deleteImg = () => {
emit('update:modelValue', '')
}
const { uploadUrl, httpRequest } = useUpload()
const { uploadUrl, httpRequest } = useUpload(props.directory)
const editImg = () => {
const dom = document.querySelector(`#${uuid.value} .el-upload__input`)

View File

@@ -81,10 +81,11 @@ const props = defineProps({
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px
borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px
directory: propTypes.string.def(undefined) // 上传目录 ==> 非必传(默认为 undefined
})
const { uploadUrl, httpRequest } = useUpload()
const { uploadUrl, httpRequest } = useUpload(props.directory)
const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0)

View File

@@ -1,5 +1,5 @@
import * as FileApi from '@/api/infra/file'
import CryptoJS from 'crypto-js'
// import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import axios from 'axios'
@@ -10,7 +10,7 @@ export const getUploadUrl = (): string => {
return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
}
export const useUpload = () => {
export const useUpload = (directory?: string) => {
// 后端上传地址
const uploadUrl = getUploadUrl()
// 是否使用前端直连上传
@@ -22,7 +22,7 @@ export const useUpload = () => {
// 1.1 生成文件名称
const fileName = await generateFileName(options.file)
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
// 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传Minio 不支持)
return axios
.put(presignedInfo.uploadUrl, options.file, {
@@ -32,7 +32,7 @@ export const useUpload = () => {
})
.then(() => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, fileName, options.file)
createFile(presignedInfo, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致
return { data: presignedInfo.url }
})
@@ -40,7 +40,7 @@ export const useUpload = () => {
// 模式二:后端上传
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
return new Promise((resolve, reject) => {
FileApi.updateFile({ file: options.file })
FileApi.updateFile({ file: options.file, directory })
.then((res) => {
if (res.code === 0) {
resolve(res)
@@ -67,11 +67,11 @@ export const useUpload = () => {
* @param name 文件名称
* @param file 文件
*/
function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {
function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile) {
const fileVo = {
configId: vo.configId,
url: vo.url,
path: name,
path: vo.path,
name: file.name,
type: file.type,
size: file.size
@@ -85,14 +85,15 @@ function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: Uplo
* @param file 要上传的文件
*/
async function generateFileName(file: UploadRawFile) {
// 读取文件内容
const data = await file.arrayBuffer()
const wordArray = CryptoJS.lib.WordArray.create(data)
// 计算SHA256
const sha256 = CryptoJS.SHA256(wordArray).toString()
// 拼接后缀
const ext = file.name.substring(file.name.lastIndexOf('.'))
return `${sha256}${ext}`
// // 读取文件内容
// const data = await file.arrayBuffer()
// const wordArray = CryptoJS.lib.WordArray.create(data)
// // 计算SHA256
// const sha256 = CryptoJS.SHA256(wordArray).toString()
// // 拼接后缀
// const ext = file.name.substring(file.name.lastIndexOf('.'))
// return `${sha256}${ext}`
return file.name
}
/**

View File

@@ -237,7 +237,7 @@ const props = defineProps({
const prefix = inject('prefix')
const width = inject('width')
const formKey = ref('')
const formKey = ref(undefined)
const businessKey = ref('')
const optionModelTitle = ref('')
const fieldList = ref<any[]>([])
@@ -462,6 +462,7 @@ const updateElementExtensions = () => {
const formList = ref([]) // 流程表单的下拉框的数据
onMounted(async () => {
formList.value = await FormApi.getFormSimpleList()
formKey.value = parseInt(formKey.value)
})
watch(

View File

@@ -370,7 +370,6 @@ const removeListenerField = (index) => {
}
// 移除监听器
const removeListener = (index) => {
debugger
ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消'

View File

@@ -2,7 +2,6 @@ import { toRaw } from 'vue'
const bpmnInstances = () => (window as any)?.bpmnInstances
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
debugger
const listenerObj = Object.create(null)
listenerObj.event = options.event
isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段