YunaiV 2025-08-16 13:27:23 +08:00
commit efbc51659b
25 changed files with 423 additions and 236 deletions

View File

@ -106,6 +106,11 @@ export const copyTask = async (data: any) => {
return await request.put({ url: '/bpm/task/copy', data })
}
// 撤回
export const withdrawTask = async (taskId: string) => {
return await request.put({ url: '/bpm/task/withdraw', params: { taskId } })
}
// 获取我的待办任务
export const myTodoTask = async (processInstanceId: string) => {
return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })

View File

@ -4,7 +4,9 @@ export interface MailLogVO {
id: number
userId: number
userType: number
toMail: string
toMails: string[]
ccMails?: string[]
bccMails?: string[]
accountId: number
fromMail: string
templateId: number

View File

@ -14,7 +14,9 @@ export interface MailTemplateVO {
}
export interface MailSendReqVO {
mail: string
toMails: string[]
ccMails?: string[]
bccMails?: string[]
templateCode: string
templateParams: Map<String, Object>
}
@ -46,7 +48,10 @@ export const deleteMailTemplate = async (id: number) => {
// 批量删除邮件模版
export const deleteMailTemplateList = async (ids: number[]) => {
return await request.delete({ url: '/system/mail-template/delete-list', params: { ids: ids.join(',') } })
return await request.delete({
url: '/system/mail-template/delete-list',
params: { ids: ids.join(',') }
})
}
// 发送邮件

View File

@ -17,7 +17,7 @@
:group="{ name: 'component', pull: 'clone', put: false }"
:clone="handleCloneComponent"
:animation="200"
:force-fallback="true"
:force-fallback="false"
>
<template #item="{ element }">
<div>

View File

@ -73,7 +73,7 @@
<draggable
v-model="pageComponents"
:animation="200"
:force-fallback="true"
:force-fallback="false"
class="page-prop-area drag-area"
filter=".component-toolbar"
ghost-class="draggable-ghost"

View File

@ -2,7 +2,7 @@
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
<VueDraggable
:list="formData"
:force-fallback="true"
:force-fallback="false"
:animation="200"
handle=".drag-icon"
class="m-t-8px"

View File

@ -236,7 +236,7 @@
<el-divider />
<div>
<el-button type="primary" @click="saveConfig"> </el-button>
<el-button @click="closeDrawer"> </el-button>
<el-button @click="cancelConfig"> </el-button>
</div>
</template>
</el-drawer>
@ -467,6 +467,13 @@ const saveConfig = async () => {
return true
}
/** 取消配置 */
const cancelConfig = () => {
//
currentNode.value.triggerSetting = originalSetting
closeDrawer()
}
/** 获取节点展示内容 */
const getShowText = (): string => {
let showText = ''
@ -498,7 +505,7 @@ const getShowText = (): string => {
/** 显示触发器节点配置, 由父组件传过来 */
const showTriggerNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
originalSetting = node.triggerSetting ? JSON.parse(JSON.stringify(node.triggerSetting)) : {}
originalSetting = cloneDeep(node.triggerSetting)
if (node.triggerSetting) {
configForm.value = {
type: node.triggerSetting.type,

View File

@ -36,14 +36,15 @@
* Verify 验证码组件
* @description 分发验证码使用
* */
import { VerifyPoints, VerifySlide } from './Verify'
import {VerifyPictureWord, VerifyPoints, VerifySlide} from './Verify'
import { computed, ref, toRefs, watchEffect } from 'vue'
export default {
name: 'Vue3Verify',
components: {
VerifySlide,
VerifyPoints
VerifyPoints,
VerifyPictureWord
},
props: {
captchaType: {
@ -118,6 +119,10 @@ export default {
}
watchEffect(() => {
switch (captchaType.value) {
case 'pictureWord':
verifyType.value = '3'
componentType.value = 'VerifyPictureWord'
break
case 'blockPuzzle':
verifyType.value = '2'
componentType.value = 'VerifySlide'
@ -438,4 +443,4 @@ export default {
content: ' ';
inset: 0;
}
</style>
</style>

View File

@ -0,0 +1,196 @@
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'
}"
class="verify-img-panel"
>
<div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh">
<i class="iconfont icon-refresh"></i>
</div>
<img
@click="refresh"
ref="canvas"
:src="'data:image/png;base64,' + verificationCodeImg"
alt=""
style="display: block; width: 100%; height: 100%"
/>
</div>
</div>
<div
:style="{
width: setSize.imgWidth,
color: barAreaColor,
'border-color': barAreaBorderColor
// 'line-height': barSize.height
}"
class="verify-bar-area"
>
<div class="verify-msg">{{ text }}</div>
<div
:style="{
'line-height': barSize.height
}"
>
<input class="verify-input" type="text" v-model="userCode" />
</div>
<button type="button" class="verify-btn" @click="submit" :disabled="checking">{{
t('captcha.verify')
}}</button>
</div>
</div>
</template>
<script setup type="text/babel">
/**
* VerifyPictureWord
* @description 输入文字
* */
import { resetSize } from '../utils/util'
import { aesEncrypt } from '../utils/ase'
import { getCode, reqCheck } from '@/api/login'
import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs } from 'vue'
const props = defineProps({
// pop fixed
mode: {
type: String,
default: 'fixed'
},
captchaType: {
type: String
},
//
vSpace: {
type: Number,
default: 5
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
})
const { t } = useI18n()
const { mode, captchaType } = toRefs(props)
const { proxy } = getCurrentInstance()
let secretKey = ref(''), // ase
userCode = ref(''), // pointJson
verificationCodeImg = ref(''), //
backToken = ref(''), // token
setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
}),
text = ref(''),
barAreaColor = ref('#000'),
barAreaBorderColor = ref('#ddd'),
showRefresh = ref(true),
// bindingClick = ref(true)
checking = ref(false)
const init = () => {
//
getPicture()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
const submit = () => {
checking.value = true
//
const captchaVerification = secretKey.value
? aesEncrypt(backToken.value + '---' + userCode.value, secretKey.value)
: backToken.value + '---' + userCode.value
let data = {
captchaType: captchaType.value,
pointJson: userCode.value,
token: backToken.value
}
reqCheck(data).then((res) => {
if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = t('captcha.success')
// bindingClick.value = false
if (mode.value === 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
proxy.$parent.$emit('success', { captchaVerification })
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = t('captcha.fail')
setTimeout(() => {
refresh()
}, 700)
}
checking.value = false
})
}
const refresh = async function () {
barAreaColor.value = '#000'
barAreaBorderColor.value = '#ddd'
checking.value = false
userCode.value = ''
await getPicture()
showRefresh.value = true
}
//
const getPicture = async () => {
let data = {
captchaType: captchaType.value
}
const res = await getCode(data)
if (res.repCode === '0000') {
verificationCodeImg.value = res.repData.originalImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
text.value = t('captcha.code')
} else {
text.value = res.repMsg
}
}
</script>

View File

@ -1,4 +1,5 @@
import VerifySlide from './VerifySlide.vue'
import VerifyPoints from './VerifyPoints.vue'
import VerifyPictureWord from './VerifyPictureWord.vue'
export { VerifySlide, VerifyPoints }
export { VerifySlide, VerifyPoints, VerifyPictureWord }

View File

@ -146,9 +146,11 @@ export default {
invalidTenantName:"Invalid Tenant Name"
},
captcha: {
verify: 'Verify',
verification: 'Please complete security verification',
slide: 'Swipe right to complete verification',
point: 'Please click',
code: 'Please enter the verification code',
success: 'Verification succeeded',
fail: 'verification failed'
},
@ -457,4 +459,4 @@ export default {
btn_zoom_out: 'Zoom out',
preview: 'Preivew'
}
}
}

View File

@ -147,9 +147,11 @@ export default {
invalidTenantName: '无效的租户名称'
},
captcha: {
verify: '验证',
verification: '请完成安全验证',
slide: '向右滑动完成验证',
point: '请依次点击',
code: '请输入验证码',
success: '验证成功',
fail: '验证失败'
},
@ -453,4 +455,4 @@ export default {
preview: '预览'
},
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
}
}

View File

@ -2,6 +2,7 @@
* https://github.com/xaboy/form-create-designer 封装的工具类
*/
import { isRef } from 'vue'
import formCreate from '@form-create/element-ui'
// 编码表单 Conf
export const encodeConf = (designerRef: object) => {
@ -24,7 +25,7 @@ export const encodeFields = (designerRef: object) => {
export const decodeFields = (fields: string[]) => {
const rule: object[] = []
fields.forEach((item) => {
rule.push(JSON.parse(item))
rule.push(formCreate.parseJson(item))
})
return rule
}
@ -32,7 +33,7 @@ export const decodeFields = (fields: string[]) => {
// 设置表单的 Conf 和 Fields适用 FcDesigner 场景
export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
// @ts-ignore
designerRef.value.setOption(JSON.parse(conf))
designerRef.value.setOption(formCreate.parseJson(conf))
// @ts-ignore
designerRef.value.setRule(decodeFields(fields))
}
@ -49,154 +50,10 @@ export const setConfAndFields2 = (
detailPreview = detailPreview.value
}
// 修复所有函数类型(解决设计器保存后函数变成字符串的问题)。例如说:
// https://t.zsxq.com/rADff
// https://t.zsxq.com/ZfbGt
// https://t.zsxq.com/mHOoj
// https://t.zsxq.com/BSylB
const option = JSON.parse(conf)
const rule = decodeFields(fields)
// 🔧 修复所有函数类型 - 解决设计器保存后函数变成字符串的问题
const fixFunctions = (obj: any) => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
// 检查是否是函数相关的属性
if (isFunctionProperty(key)) {
// 如果不是函数类型,重新构建为函数
if (typeof obj[key] !== 'function') {
obj[key] = createDefaultFunction(key)
}
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
// 递归处理嵌套对象
fixFunctions(obj[key])
}
})
}
}
// 判断是否是函数属性
const isFunctionProperty = (key: string): boolean => {
const functionKeys = [
'beforeFetch', // 请求前处理
'afterFetch', // 请求后处理
'onSubmit', // 表单提交
'onReset', // 表单重置
'onChange', // 值变化
'onInput', // 输入事件
'onClick', // 点击事件
'onFocus', // 获取焦点
'onBlur', // 失去焦点
'onMounted', // 组件挂载
'onCreated', // 组件创建
'onReload', // 重新加载
'remoteMethod', // 远程搜索方法
'parseFunc', // 解析函数
'validator', // 验证器
'asyncValidator', // 异步验证器
'formatter', // 格式化函数
'parser', // 解析函数
'beforeUpload', // 上传前处理
'onSuccess', // 成功回调
'onError', // 错误回调
'onProgress', // 进度回调
'onPreview', // 预览回调
'onRemove', // 移除回调
'onExceed', // 超出限制回调
'filterMethod', // 过滤方法
'sortMethod', // 排序方法
'loadData', // 加载数据
'renderContent', // 渲染内容
'render' // 渲染函数
]
// 检查是否以函数相关前缀开头
const functionPrefixes = ['on', 'before', 'after', 'handle']
return functionKeys.includes(key) || functionPrefixes.some((prefix) => key.startsWith(prefix))
}
// 根据函数名创建默认函数
const createDefaultFunction = (key: string): Function => {
switch (key) {
case 'beforeFetch':
return (config: any) => {
// 添加 Token 认证头。例如说:
// https://t.zsxq.com/hK3FO
const token = localStorage.getItem('token')
if (token) {
config.headers = {
...config.headers,
Authorization: 'Bearer ' + token
}
}
// 添加通用请求头
config.headers = {
...config.headers,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
// 添加时间戳防止缓存
config.params = {
...config.params,
_t: Date.now()
}
return config
}
case 'afterFetch':
return (data: any) => {
return data
}
case 'onSubmit':
return (_formData: any) => {
return true
}
case 'onReset':
return () => {
return true
}
case 'onChange':
return (_value: any, _oldValue: any) => {}
case 'remoteMethod':
return (query: string) => {
console.log('remoteMethod被调用:', query)
}
case 'parseFunc':
return (data: any) => {
// 默认解析逻辑如果是数组直接返回否则尝试获取list属性
if (Array.isArray(data)) {
return data
}
return data?.list || data?.data || []
}
case 'validator':
return (_rule: any, _value: any, callback: Function) => {
callback()
}
case 'beforeUpload':
return (_file: any) => {
return true
}
default:
// 通用默认函数
return (...args: any[]) => {
// 对于事件处理函数返回true表示继续执行
if (key.startsWith('on') || key.startsWith('handle')) {
return true
}
// 对于其他函数,返回第一个参数(通常是数据传递)
return args[0]
}
}
}
// 修复 option 中的所有函数
fixFunctions(option)
// 修复 rule 中的所有函数(包括组件的 props
if (Array.isArray(rule)) {
rule.forEach((item: any) => {
fixFunctions(item)
})
}
// @ts-ignore
detailPreview.option = option
detailPreview.option = formCreate.parseJson(conf)
// @ts-ignore
detailPreview.rule = rule
detailPreview.rule = decodeFields(fields)
if (value) {
// @ts-ignore

View File

@ -185,7 +185,7 @@ const { push } = useRouter()
const permissionStore = usePermissionStore()
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord pictureWord
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)

View File

@ -143,7 +143,7 @@ const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsResetPassword)
const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord pictureWord
const validatePass2 = (_rule, value, callback) => {
if (value === '') {

View File

@ -47,10 +47,7 @@
/>
</el-form-item>
</el-col>
<el-col
:span="24"
class="px-10px mt-[-20px] mb-[-20px]"
>
<el-col :span="24" class="px-10px mt-[-20px] mb-[-20px]">
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<el-col :span="6">
@ -177,7 +174,7 @@ const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord pictureWord
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)

View File

@ -120,7 +120,7 @@ const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord pictureWord
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)

View File

@ -11,6 +11,17 @@
</div>
</div>
</el-form-item>
<el-form-item class="mb-20px">
<template #label>
<el-text size="large" tag="b">审批人权限</el-text>
</template>
<div class="flex flex-col">
<el-checkbox v-model="modelData.allowWithdrawTask" label="允许审批人撤回任务" />
<div class="ml-22px">
<el-text type="info"> 审批人可撤回正在审批节点的前一节点 </el-text>
</div>
</div>
</el-form-item>
<el-form-item v-if="modelData.processIdRule" class="mb-20px">
<template #label>
<el-text size="large" tag="b">流程编码</el-text>
@ -232,34 +243,6 @@ import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/co
import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
const modelData = defineModel<any>()
const formFields = ref<string[]>([])
const props = defineProps({
// ID
modelFormId: {
type: Number,
required: false,
default: undefined,
}
})
// modelFormId
watch(
() => props.modelFormId,
async (newVal) => {
if (newVal) {
const form = await FormApi.getForm(newVal);
formFields.value = form?.fields;
} else {
// modelFormId
formFields.value = [];
}
},
{ immediate: true },
);
// 使
provide('formFields', formFields)
/** 自定义 ID 流程编码 */
const timeOptions = ref([
@ -374,10 +357,10 @@ const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
}
}
/** 表单选项 */
const formField = ref<Array<{ field: string; title: string }>>([])
/** 已解析表单字段 */
const formFields = ref<Array<{ field: string; title: string }>>([])
const formFieldOptions4Title = computed(() => {
let cloneFormField = formField.value.map((item) => {
let cloneFormField = formFields.value.map((item) => {
return {
label: item.title,
value: item.field
@ -399,7 +382,7 @@ const formFieldOptions4Title = computed(() => {
return cloneFormField
})
const formFieldOptions4Summary = computed(() => {
return formField.value.map((item) => {
return formFields.value.map((item) => {
return {
label: item.title,
value: item.field
@ -407,6 +390,11 @@ const formFieldOptions4Summary = computed(() => {
})
})
/** 未解析的表单字段 */
const unParsedFormFields = ref<string[]>([])
/** 暴露给子组件 HttpRequestSetting 使用 */
provide('formFields', unParsedFormFields)
/** 兼容以前未配置更多设置的流程 */
const initData = () => {
if (!modelData.value.processIdRule) {
@ -445,6 +433,9 @@ const initData = () => {
if (modelData.value.taskAfterTriggerSetting) {
taskAfterTriggerEnable.value = true
}
if (modelData.value.allowWithdrawTask) {
modelData.value.allowWithdrawTask = false
}
}
defineExpose({ initData })
@ -456,13 +447,15 @@ watch(
const data = await FormApi.getForm(newFormId)
const result: Array<{ field: string; title: string }> = []
if (data.fields) {
unParsedFormFields.value = data.fields
data.fields.forEach((fieldStr: string) => {
parseFormFields(JSON.parse(fieldStr), result)
})
}
formField.value = result
formFields.value = result
} else {
formField.value = []
formFields.value = []
unParsedFormFields.value = []
}
},
{ immediate: true }

View File

@ -77,10 +77,7 @@
<!-- 第四步更多设置 -->
<div v-show="currentStep === 3" class="mx-auto w-700px">
<ExtraSettings
ref="extraSettingsRef"
v-model="formData"
:model-form-id="formData.formId"/>
<ExtraSettings ref="extraSettingsRef" v-model="formData" />
</div>
</div>
</div>
@ -176,7 +173,8 @@ const formData: any = ref({
summarySetting: {
enable: false,
summary: []
}
},
allowWithdrawTask: false
})
//

View File

@ -51,8 +51,10 @@
>
<div class="ml-10px -mt-15px -mb-35px">
<ProcessInstanceTimeline
ref="nextAssigneesTimelineRef"
:activity-nodes="nextAssigneesActivityNode"
:show-status-icon="false"
:enable-approve-user-select="true"
@select-user-confirm="selectNextAssigneesConfirm"
/>
</div>
@ -571,6 +573,7 @@ const approveFormRef = ref<FormInstance>()
const signRef = ref()
const approveSignFormRef = ref()
const nextAssigneesActivityNode = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) //
const nextAssigneesTimelineRef = ref() // 线
const approveReasonForm = reactive({
reason: '',
signPicUrl: '',
@ -717,6 +720,10 @@ const closePopover = (type: string, formRef: FormInstance | undefined) => {
}
popOverVisible.value[type] = false
nextAssigneesActivityNode.value = []
// Timeline
if (nextAssigneesTimelineRef.value) {
nextAssigneesTimelineRef.value.batchSetCustomApproveUsers({})
}
}
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
@ -729,6 +736,7 @@ const initNextAssigneesFormField = async () => {
processVariablesStr: JSON.stringify(variables)
})
if (data && data.length > 0) {
const customApproveUsersData: Record<string, any[]> = {} // Timeline
data.forEach((node: any) => {
if (
//
@ -740,7 +748,18 @@ const initNextAssigneesFormField = async () => {
) {
nextAssigneesActivityNode.value.push(node)
}
// candidateUsers customApproveUsers
if (node.candidateUsers && node.candidateUsers.length > 0) {
customApproveUsersData[node.id] = node.candidateUsers
}
})
// candidateUsers Timeline
await nextTick() // tick Timeline
if (nextAssigneesTimelineRef.value && Object.keys(customApproveUsersData).length > 0) {
nextAssigneesTimelineRef.value.batchSetCustomApproveUsers(customApproveUsersData)
}
}
}
@ -803,6 +822,10 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
await TaskApi.approveTask(data)
popOverVisible.value.approve = false
nextAssigneesActivityNode.value = []
// Timeline
if (nextAssigneesTimelineRef.value) {
nextAssigneesTimelineRef.value.batchSetCustomApproveUsers({})
}
message.success('审批通过成功')
} else {
//

View File

@ -15,7 +15,7 @@
>
<img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
<div
v-if="showStatusIcon"
v-if="props.showStatusIcon"
class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
>
@ -55,13 +55,13 @@
class="flex flex-wrap gap2 items-center"
v-if="
isEmpty(activity.tasks) &&
isEmpty(activity.candidateUsers) &&
(CandidateStrategy.START_USER_SELECT === activity.candidateStrategy ||
CandidateStrategy.APPROVE_USER_SELECT === activity.candidateStrategy)
((CandidateStrategy.START_USER_SELECT === activity.candidateStrategy &&
isEmpty(activity.candidateUsers)) ||
(props.enableApproveUserSelect &&
CandidateStrategy.APPROVE_USER_SELECT === activity.candidateStrategy))
"
>
<!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
<el-tooltip content="添加用户" placement="left">
<el-button
class="!px-6px"
@ -119,7 +119,7 @@
</template>
<!-- 信息任务 ICON -->
<div
v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
v-if="props.showStatusIcon && onlyStatusIconShow.includes(task.status)"
class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2[task.status]?.color }"
>
@ -165,7 +165,7 @@
<!-- 信息任务 ICON -->
<div
v-if="showStatusIcon"
v-if="props.showStatusIcon"
class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2['-1']?.color }"
>
@ -198,13 +198,15 @@ import transactorSvg from '@/assets/svgs/bpm/transactor.svg'
import childProcessSvg from '@/assets/svgs/bpm/child-process.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
withDefaults(
const props = withDefaults(
defineProps<{
activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] //
showStatusIcon?: boolean //
enableApproveUserSelect?: boolean //
}>(),
{
showStatusIcon: true // true
showStatusIcon: true, // true
enableApproveUserSelect: false // false
}
)
const { push } = useRouter() //
@ -341,4 +343,19 @@ const handleChildProcess = (activity: any) => {
}
})
}
/** 设置自定义审批人 */
const setCustomApproveUsers = (activityId: string, users: any[]) => {
customApproveUsers.value[activityId] = users || []
}
/** 批量设置多个节点的自定义审批人 */
const batchSetCustomApproveUsers = (data: Record<string, any[]>) => {
Object.keys(data).forEach((activityId) => {
customApproveUsers.value[activityId] = data[activityId] || []
})
}
//
defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers })
</script>

View File

@ -184,8 +184,9 @@
:show-overflow-tooltip="true"
/>
<el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
<el-table-column align="center" label="操作" fixed="right" width="80">
<el-table-column align="center" label="操作" fixed="right" width="130">
<template #default="scope">
<el-button link type="warning" @click="handleWithdraw(scope.row)"></el-button>
<el-button link type="primary" @click="handleAudit(scope.row)"></el-button>
</template>
</el-table-column>
@ -209,6 +210,7 @@ import * as DefinitionApi from '@/api/bpm/definition'
defineOptions({ name: 'BpmDoneTask' })
const { push } = useRouter() //
const message = useMessage()
const loading = ref(true) //
const total = ref(0) //
@ -262,6 +264,14 @@ const handleAudit = (row: any) => {
})
}
/** 测回按钮 */
const handleWithdraw = (row: any) => {
TaskApi.withdrawTask(row.id).then(() => {
message.success('撤回成功')
getList()
})
}
/** 初始化 **/
onMounted(async () => {
await getList()

View File

@ -13,12 +13,34 @@
<el-descriptions-item label="模版发送人名称">
{{ detailData.templateNickname }}
</el-descriptions-item>
<el-descriptions-item label="用户信息">
{{ detailData.toMail }}
<el-descriptions-item label="接收用户">
<span v-if="detailData.userType && detailData.userId">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
({{ detailData.userId }})
</span>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="接收信息">
<div>
<div v-if="detailData.toMails && detailData.toMails.length > 0">
收件
<span v-for="(mail, index) in detailData.toMails" :key="mail">
{{ mail }}<span v-if="index < detailData.toMails.length - 1"></span>
</span>
</div>
<div v-if="detailData.ccMails && detailData.ccMails.length > 0">
抄送
<span v-for="(mail, index) in detailData.ccMails" :key="mail">
{{ mail }}<span v-if="index < detailData.ccMails.length - 1"></span>
</span>
</div>
<div v-if="detailData.bccMails && detailData.bccMails.length > 0">
密送
<span v-for="(mail, index) in detailData.bccMails" :key="mail">
{{ mail }}<span v-if="index < detailData.bccMails.length - 1"></span>
</span>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="邮件标题">
{{ detailData.templateTitle }}
@ -58,7 +80,7 @@ defineOptions({ name: 'SystemMailLogDetail' })
const dialogVisible = ref(false) //
const detailLoading = ref(false) //
const detailData = ref() //
const accountList = ref([]) //
const accountList = ref<MailAccountApi.MailAccountVO[]>([]) //
/** 打开弹窗 */
const open = async (data: MailLogApi.MailLogVO) => {

View File

@ -119,12 +119,36 @@
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="接收邮箱" align="center" prop="toMail" width="200">
<el-table-column label="接收用户" align="center" width="150">
<template #default="scope">
<div>{{ scope.row.toMail }}</div>
<div v-if="scope.row.userType && scope.row.userId">
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
{{ '(' + scope.row.userId + ')' }}
<div>{{ '(' + scope.row.userId + ')' }}</div>
</div>
<div v-else>-</div>
</template>
</el-table-column>
<el-table-column label="接收信息" align="center" width="300">
<template #default="scope">
<div class="text-left">
<div v-if="scope.row.toMails && scope.row.toMails.length > 0">
收件
<span v-for="(mail, index) in scope.row.toMails" :key="mail">
{{ mail }}<span v-if="index < scope.row.toMails.length - 1"></span>
</span>
</div>
<div v-if="scope.row.ccMails && scope.row.ccMails.length > 0">
抄送
<span v-for="(mail, index) in scope.row.ccMails" :key="mail">
{{ mail }}<span v-if="index < scope.row.ccMails.length - 1"></span>
</span>
</div>
<div v-if="scope.row.bccMails && scope.row.bccMails.length > 0">
密送
<span v-for="(mail, index) in scope.row.bccMails" :key="mail">
{{ mail }}<span v-if="index < scope.row.bccMails.length - 1"></span>
</span>
</div>
</div>
</template>
</el-table-column>
@ -185,15 +209,15 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
toMail: '',
accountId: null,
templateId: null,
sendStatus: null,
userId: null,
userType: null,
accountId: undefined,
templateId: undefined,
sendStatus: undefined,
userId: undefined,
userType: undefined,
sendTime: []
})
const exportLoading = ref(false) //
const accountList = ref([]) //
const accountList = ref<MailAccountApi.MailAccountVO[]>([]) //
/** 查询列表 */
const getList = async () => {

View File

@ -10,8 +10,26 @@
<el-form-item label="模板内容" prop="content">
<Editor :model-value="formData.content" height="150px" readonly />
</el-form-item>
<el-form-item label="收件邮箱" prop="mail">
<el-input v-model="formData.mail" placeholder="请输入收件邮箱" />
<el-form-item label="收件邮箱" prop="toMails">
<el-input-tag
v-model="formData.toMails"
placeholder="请输入收件邮箱,多个邮箱用回车分隔"
class="!w-full"
/>
</el-form-item>
<el-form-item label="抄送邮箱" prop="ccMails">
<el-input-tag
v-model="formData.ccMails"
placeholder="请输入抄送邮箱,多个邮箱用回车分隔"
class="!w-full"
/>
</el-form-item>
<el-form-item label="密送邮箱" prop="bccMails">
<el-input-tag
v-model="formData.bccMails"
placeholder="请输入密送邮箱,多个邮箱用回车分隔"
class="!w-full"
/>
</el-form-item>
<el-form-item
v-for="param in formData.params"
@ -43,12 +61,13 @@ const formLoading = ref(false) // 表单的加载中1修改时的数据加
const formData = ref({
content: '',
params: {},
mail: '',
toMails: [],
ccMails: [],
bccMails: [],
templateCode: '',
templateParams: new Map()
})
const formRules = reactive({
mail: [{ required: true, message: '邮箱不能为空', trigger: 'blur' }],
templateCode: [{ required: true, message: '模版编号不能为空', trigger: 'blur' }],
templateParams: {}
})
@ -105,7 +124,9 @@ const resetForm = () => {
formData.value = {
content: '',
params: {},
mail: '',
toMails: [],
ccMails: [],
bccMails: [],
templateCode: '',
templateParams: new Map()
}