first commit
This commit is contained in:
168
src/pages-bpm/category/components/search-form.vue
Normal file
168
src/pages-bpm/category/components/search-form.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入分类名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类标志
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入分类标志"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`分类名:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`分类标志:${formData.code}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程分类'
|
||||
})
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
129
src/pages-bpm/category/detail/index.vue
Normal file
129
src/pages-bpm/category/detail/index.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程分类详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="分类编号" :value="formData?.id" />
|
||||
<wd-cell title="分类名" :value="formData?.name" />
|
||||
<wd-cell title="分类标志" :value="formData?.code" />
|
||||
<wd-cell title="分类描述" :value="formData?.description || '-'" />
|
||||
<wd-cell title="分类状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="分类排序" :value="formData?.sort" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:category:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:category:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteCategory, getCategory } from '@/api/bpm/category'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<Category>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/category/index')
|
||||
}
|
||||
|
||||
/** 加载流程分类详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getCategory(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程分类 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/category/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程分类吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteCategory(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
159
src/pages-bpm/category/form/index.vue
Normal file
159
src/pages-bpm/category/form/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="分类名"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入分类名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="分类标志"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入分类标志"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="分类描述"
|
||||
label-width="180rpx"
|
||||
prop="description"
|
||||
clearable
|
||||
placeholder="请输入分类描述"
|
||||
/>
|
||||
<wd-cell title="分类状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model.number="formData.sort"
|
||||
label="分类排序"
|
||||
label-width="180rpx"
|
||||
prop="sort"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入分类排序"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createCategory, getCategory, updateCategory } from '@/api/bpm/category'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程分类' : '新增流程分类')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Category>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: '',
|
||||
sort: 0,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '分类名不能为空' }],
|
||||
code: [{ required: true, message: '分类标志不能为空' }],
|
||||
status: [{ required: true, message: '分类状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/category/index')
|
||||
}
|
||||
|
||||
/** 加载流程分类详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getCategory(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateCategory(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createCategory(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
167
src/pages-bpm/category/index.vue
Normal file
167
src/pages-bpm/category/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程分类管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程分类列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">分类标志:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">分类描述:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.description || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">分类排序:</text>
|
||||
<text>{{ item.sort }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程分类数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:category:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getCategoryPage } from '@/api/bpm/category'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Category[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程分类列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getCategoryPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程分类 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/category/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Category) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/category/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
168
src/pages-bpm/oa/leave/components/search-form.vue
Normal file
168
src/pages-bpm/oa/leave/components/search-form.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
请假类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
审批结果
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
请假原因
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.reason"
|
||||
placeholder="请输入请假原因"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
type: -1, // -1 表示全部
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
reason: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.BPM_OA_LEAVE_TYPE, formData.type)}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
if (formData.reason) {
|
||||
conditions.push(`原因:${formData.reason}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索请假记录'
|
||||
})
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
reason: formData.reason || undefined,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.type = -1
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
formData.reason = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
285
src/pages-bpm/oa/leave/create/index.vue
Normal file
285
src/pages-bpm/oa/leave/create/index.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<view class="yd-page-container pb-[76rpx]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="发起请假"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 表单内容 -->
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border title="请假信息">
|
||||
<wd-picker
|
||||
v-model="formData.type"
|
||||
:columns="getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
|
||||
label="请假类型"
|
||||
label-width="200rpx"
|
||||
prop="type"
|
||||
:rules="[{ required: true, message: '请选择请假类型' }]"
|
||||
placeholder="请选择请假类型"
|
||||
/>
|
||||
<wd-datetime-picker
|
||||
v-model="formData.startTime"
|
||||
label="开始时间"
|
||||
label-width="200rpx"
|
||||
prop="startTime"
|
||||
:rules="[{ required: true, message: '请选择开始时间' }]"
|
||||
placeholder="请选择开始时间"
|
||||
/>
|
||||
<wd-datetime-picker
|
||||
v-model="formData.endTime"
|
||||
label="结束时间"
|
||||
label-width="200rpx"
|
||||
prop="endTime"
|
||||
:rules="[{ required: true, message: '请选择结束时间' }]"
|
||||
placeholder="请选择结束时间"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
label="请假原因"
|
||||
label-width="200rpx"
|
||||
prop="reason"
|
||||
:rules="[{ required: true, message: '请输入请假原因' }]"
|
||||
placeholder="请输入请假原因"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
<!-- 流程预览卡片 -->
|
||||
<view class="mx-24rpx mb-120rpx mt-24rpx rounded-16rpx bg-white">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<text class="text-28rpx text-[#333] font-bold">流程预览</text>
|
||||
<wd-loading v-if="processTimeLineLoading" size="32rpx" />
|
||||
</view>
|
||||
|
||||
<!-- 流程时间线 -->
|
||||
<ProcessInstanceTimeline
|
||||
v-if="activityNodes.length > 0"
|
||||
:activity-nodes="activityNodes"
|
||||
:show-status-icon="false"
|
||||
@select-user-confirm="selectUserConfirm"
|
||||
/>
|
||||
|
||||
<!-- 无流程数据提示 -->
|
||||
<view v-else-if="!processTimeLineLoading" class="py-40rpx text-center">
|
||||
<text class="text-24rpx text-[#999]">暂无流程预览数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部提交按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button type="primary" class="flex-1" :loading="formLoading" @click="handleSubmit">
|
||||
提交
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useMessage, useToast } from 'wot-design-uni'
|
||||
import { getProcessDefinition } from '@/api/bpm/definition'
|
||||
import { createLeave } from '@/api/bpm/oa/leave'
|
||||
import { getApprovalDetail } from '@/api/bpm/processInstance'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmCandidateStrategyEnum, BpmNodeIdEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const message = useMessage()
|
||||
const formLoading = ref(false)
|
||||
const processTimeLineLoading = ref(false) // 流程预览加载状态
|
||||
|
||||
// 流程相关数据
|
||||
const processDefineKey = 'oa_leave' // 流程定义 Key
|
||||
const processDefinitionId = ref('')
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]) // 审批节点信息
|
||||
const startUserSelectTasks = ref<any[]>([]) // 发起人需要选择审批人的用户任务列表
|
||||
const startUserSelectAssignees = ref<any>({}) // 发起人选择审批人的数据
|
||||
const tempStartUserSelectAssignees = ref<any>({}) // 临时保存的审批人数据
|
||||
|
||||
const formData = ref<Partial<Leave>>({
|
||||
type: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
reason: undefined,
|
||||
})
|
||||
const formRules = {
|
||||
type: [{ required: true, message: '请选择请假类型' }],
|
||||
startTime: [{ required: true, message: '请选择开始时间' }],
|
||||
endTime: [{ required: true, message: '请选择结束时间' }],
|
||||
reason: [{ required: true, message: '请输入请假原因' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 计算请假天数
|
||||
const leaveDays = computed(() => {
|
||||
if (!formData.value.startTime || !formData.value.endTime) {
|
||||
return 0
|
||||
}
|
||||
const start = new Date(formData.value.startTime)
|
||||
const end = new Date(formData.value.endTime)
|
||||
return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
message.confirm({
|
||||
title: '提示',
|
||||
msg: '确定要返回吗?请先保存您填写的信息!',
|
||||
}).then(({ action }) => {
|
||||
if (action === 'confirm') {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取流程审批详情 */
|
||||
async function getProcessApprovalDetail() {
|
||||
if (!processDefinitionId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
processTimeLineLoading.value = true
|
||||
try {
|
||||
const data = await getApprovalDetail({
|
||||
processDefinitionId: processDefinitionId.value,
|
||||
activityId: BpmNodeIdEnum.START_USER_NODE_ID,
|
||||
processVariablesStr: JSON.stringify({
|
||||
day: leaveDays.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
toast.show('查询不到审批详情信息!')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取审批节点,显示 Timeline 的数据
|
||||
activityNodes.value = data.activityNodes || []
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = data.activityNodes?.filter(
|
||||
(node: ApprovalNodeInfo) =>
|
||||
BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy,
|
||||
) || []
|
||||
|
||||
// 恢复之前的选择审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
for (const node of startUserSelectTasks.value) {
|
||||
startUserSelectAssignees.value[node.id]
|
||||
= tempStartUserSelectAssignees.value[node.id]
|
||||
&& tempStartUserSelectAssignees.value[node.id].length > 0
|
||||
? tempStartUserSelectAssignees.value[node.id]
|
||||
: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取流程审批详情失败:', error)
|
||||
} finally {
|
||||
processTimeLineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择审批人确认 */
|
||||
function selectUserConfirm(id: string, userList: any[]) {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id) || []
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
if (formData.value.startTime! >= formData.value.endTime!) {
|
||||
toast.show('结束时间必须大于开始时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验指定审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
for (const userTask of startUserSelectTasks.value) {
|
||||
if (
|
||||
Array.isArray(startUserSelectAssignees.value[userTask.id])
|
||||
&& startUserSelectAssignees.value[userTask.id].length === 0
|
||||
) {
|
||||
toast.show(`请选择${userTask.name}的审批人`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const submitData = { ...formData.value }
|
||||
// 设置指定审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
submitData.startUserSelectAssignees = startUserSelectAssignees.value
|
||||
}
|
||||
|
||||
await createLeave(submitData)
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}, 1500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听表单数据变化,重新预测流程节点
|
||||
watch(
|
||||
() => [formData.value.startTime, formData.value.endTime, formData.value.type],
|
||||
(newValue, oldValue) => {
|
||||
if (!oldValue || !oldValue.some(v => v !== undefined)) {
|
||||
return
|
||||
}
|
||||
if (newValue && newValue.some(v => v !== undefined)) {
|
||||
// 记录之前的节点审批人
|
||||
tempStartUserSelectAssignees.value = { ...startUserSelectAssignees.value }
|
||||
startUserSelectAssignees.value = {}
|
||||
// 加载最新的审批详情,主要用于节点预测
|
||||
getProcessApprovalDetail()
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 组件初始化
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取流程定义详情
|
||||
const processDefinitionDetail = await getProcessDefinition(undefined, processDefineKey)
|
||||
if (!processDefinitionDetail) {
|
||||
toast.show('OA 请假的流程模型未配置,请检查!')
|
||||
return
|
||||
}
|
||||
processDefinitionId.value = processDefinitionDetail.id
|
||||
|
||||
// 获取流程审批详情
|
||||
await getProcessApprovalDetail()
|
||||
} catch (error) {
|
||||
console.error('初始化流程失败:', error)
|
||||
toast.show('初始化流程失败,请稍后重试')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
76
src/pages-bpm/oa/leave/detail/index.vue
Normal file
76
src/pages-bpm/oa/leave/detail/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view :class="embedded ? '' : 'yd-page-container'">
|
||||
<!-- 顶部导航栏(仅路由访问时显示) -->
|
||||
<wd-navbar
|
||||
v-if="!embedded"
|
||||
title="请假详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group :border="!embedded">
|
||||
<wd-cell title="请假类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="formData.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="开始时间" :value="formatDateTime(formData.startTime) || '-'" />
|
||||
<wd-cell title="结束时间" :value="formatDateTime(formData.endTime) || '-'" />
|
||||
<wd-cell title="请假原因" :value="formData.reason || '-'" />
|
||||
<wd-cell title="审批状态">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="formData.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getLeave } from '@/api/bpm/oa/leave'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | string
|
||||
embedded?: boolean // 是否作为嵌入组件使用(非路由访问)
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Partial<Leave>>({})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}
|
||||
|
||||
/** 获取详情数据 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getLeave(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
218
src/pages-bpm/oa/leave/index.vue
Normal file
218
src/pages-bpm/oa/leave/index.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="请假列表"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<LeaveSearchForm @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<view class="bpm-list">
|
||||
<!-- 请假列表 -->
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="bpm-card"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="bpm-card-content">
|
||||
<view class="bpm-card-header">
|
||||
<view class="bpm-card-title">
|
||||
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="bpm-summary">
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">开始时间:</text>
|
||||
<text>{{ formatDateTime(item.startTime) }}</text>
|
||||
</view>
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">结束时间:</text>
|
||||
<text>{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">请假原因:</text>
|
||||
<text>{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bpm-card-info">
|
||||
<view class="bpm-user">
|
||||
<view class="bpm-avatar">
|
||||
{{ userNickname?.[0] }}
|
||||
</view>
|
||||
<text class="bpm-nickname">{{ userNickname }}</text>
|
||||
</view>
|
||||
<text class="bpm-time">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bpm-actions">
|
||||
<view class="bpm-action-btn" @click.stop="handleDetail(item)">
|
||||
<wd-icon name="view" size="32rpx" />
|
||||
<text class="ml-8rpx">详情</text>
|
||||
</view>
|
||||
<view class="bpm-action-btn" @click.stop="handleProgress(item)">
|
||||
<wd-icon name="queue" size="32rpx" />
|
||||
<text class="ml-8rpx">审批进度</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.status === BpmProcessInstanceStatus.RUNNING"
|
||||
class="bpm-action-btn text-[#ff4d4f]!"
|
||||
@click.stop="handleCancel(item)"
|
||||
>
|
||||
<wd-icon name="close" size="32rpx" color="#ff4d4f" />
|
||||
<text class="ml-8rpx">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="bpm-empty">
|
||||
<wd-status-tip image="content" tip="暂无请假记录" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleCreate"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getLeavePage } from '@/api/bpm/oa/leave'
|
||||
import { cancelProcessInstanceByStartUser } from '@/api/bpm/processInstance'
|
||||
import { useUserStore } from '@/store'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import LeaveSearchForm from './components/search-form.vue'
|
||||
import '@/pages/bpm/styles/index.scss'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userNickname = computed(() => userStore.userInfo?.nickname || '')
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<Leave[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getLeavePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Leave) {
|
||||
uni.navigateTo({ url: `/pages-bpm/oa/leave/detail/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
/** 审批进度 */
|
||||
function handleProgress(item: Leave) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.processInstanceId}` })
|
||||
}
|
||||
|
||||
/** 取消请假 */
|
||||
function handleCancel(item: Leave) {
|
||||
uni.showModal({
|
||||
title: '取消流程',
|
||||
editable: true,
|
||||
placeholderText: '请输入取消原因',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
const reason = res.content?.trim()
|
||||
if (!reason) {
|
||||
uni.showToast({ title: '请输入取消原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await cancelProcessInstanceByStartUser(String(item.processInstanceId), reason)
|
||||
// 更新状态
|
||||
uni.showToast({ title: '取消成功', icon: 'success' })
|
||||
item.status = BpmProcessInstanceStatus.CANCEL
|
||||
item.endTime = new Date()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 发起请假 */
|
||||
function handleCreate() {
|
||||
uni.navigateTo({ url: '/pages-bpm/oa/leave/create/index' })
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
153
src/pages-bpm/process-expression/components/search-form.vue
Normal file
153
src/pages-bpm/process-expression/components/search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
表达式名字
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入表达式名字"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
表达式状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名字:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程表达式'
|
||||
})
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
131
src/pages-bpm/process-expression/detail/index.vue
Normal file
131
src/pages-bpm/process-expression/detail/index.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程表达式详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="表达式名字" :value="formData?.name" />
|
||||
<wd-cell title="表达式状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="表达式">
|
||||
<view class="break-all">
|
||||
{{ formData?.expression }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteProcessExpression, getProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<ProcessExpression>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-expression/index')
|
||||
}
|
||||
|
||||
/** 加载流程表达式详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getProcessExpression(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程表达式 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-expression/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程表达式 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程表达式吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteProcessExpression(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
140
src/pages-bpm/process-expression/form/index.vue
Normal file
140
src/pages-bpm/process-expression/form/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="表达式名字"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入表达式名字"
|
||||
/>
|
||||
<wd-cell title="表达式状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.expression"
|
||||
label="表达式"
|
||||
label-width="180rpx"
|
||||
prop="expression"
|
||||
clearable
|
||||
placeholder="请输入表达式"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createProcessExpression, getProcessExpression, updateProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程表达式' : '新增流程表达式')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<ProcessExpression>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
expression: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '表达式名字不能为空' }],
|
||||
status: [{ required: true, message: '表达式状态不能为空' }],
|
||||
expression: [{ required: true, message: '表达式不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-expression/index')
|
||||
}
|
||||
|
||||
/** 加载流程表达式详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getProcessExpression(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateProcessExpression(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createProcessExpression(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
161
src/pages-bpm/process-expression/index.vue
Normal file
161
src/pages-bpm/process-expression/index.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程表达式管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程表达式列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">表达式:</text>
|
||||
<text class="break-all">{{ item.expression }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程表达式数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getProcessExpressionPage } from '@/api/bpm/process-expression'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<ProcessExpression[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程表达式列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessExpressionPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程表达式 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/process-expression/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessExpression) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-expression/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
94
src/pages-bpm/process-listener/components/search-form.vue
Normal file
94
src/pages-bpm/process-listener/components/search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
监听器名字
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入监听器名字"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
监听器类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio value="">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
type: '', // 空字符串表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名字:${formData.name}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE, formData.type)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程监听器'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
type: formData.type || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.type = ''
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
138
src/pages-bpm/process-listener/detail/index.vue
Normal file
138
src/pages-bpm/process-listener/detail/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程监听器详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="监听器名字" :value="formData?.name" />
|
||||
<wd-cell title="监听器类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="监听器状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="监听事件" :value="formData?.event" />
|
||||
<wd-cell title="值类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" :value="formData?.valueType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="值">
|
||||
<view class="break-all">
|
||||
{{ formData?.value }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteProcessListener, getProcessListener } from '@/api/bpm/process-listener'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<ProcessListener>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-listener/index')
|
||||
}
|
||||
|
||||
/** 加载流程监听器详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getProcessListener(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程监听器 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-listener/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程监听器 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程监听器吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteProcessListener(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
220
src/pages-bpm/process-listener/form/index.vue
Normal file
220
src/pages-bpm/process-listener/form/index.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="监听器名字"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入监听器名字"
|
||||
/>
|
||||
<wd-cell title="监听器状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="监听器类型" title-width="180rpx" prop="type" center>
|
||||
<wd-radio-group v-model="formData.type" shape="button" @change="handleTypeChange">
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="监听事件" title-width="180rpx" prop="event" center>
|
||||
<wd-radio-group v-model="formData.event" shape="button">
|
||||
<wd-radio
|
||||
v-for="option in eventOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="值类型" title-width="180rpx" prop="valueType" center>
|
||||
<wd-radio-group v-model="formData.valueType" shape="button" @change="handleValueTypeChange">
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.value"
|
||||
:label="valueLabel"
|
||||
label-width="180rpx"
|
||||
prop="value"
|
||||
clearable
|
||||
:placeholder="valuePlaceholder"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createProcessListener, getProcessListener, updateProcessListener } from '@/api/bpm/process-listener'
|
||||
import { getIntDictOptions, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
/** 执行监听器事件选项 */
|
||||
const EVENT_EXECUTION_OPTIONS = [
|
||||
{ label: '开始', value: 'start' },
|
||||
{ label: '结束', value: 'end' },
|
||||
]
|
||||
|
||||
/** 任务监听器事件选项 */
|
||||
const EVENT_OPTIONS = [
|
||||
{ label: '创建', value: 'create' },
|
||||
{ label: '指派', value: 'assignment' },
|
||||
{ label: '完成', value: 'complete' },
|
||||
{ label: '删除', value: 'delete' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '超时', value: 'timeout' },
|
||||
]
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程监听器' : '新增流程监听器')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<ProcessListener>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
event: '',
|
||||
valueType: '',
|
||||
value: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '监听器名字不能为空' }],
|
||||
type: [{ required: true, message: '监听器类型不能为空' }],
|
||||
status: [{ required: true, message: '监听器状态不能为空' }],
|
||||
event: [{ required: true, message: '监听事件不能为空' }],
|
||||
valueType: [{ required: true, message: '值类型不能为空' }],
|
||||
value: [{ required: true, message: '值不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 根据类型获取事件选项 */
|
||||
const eventOptions = computed(() => {
|
||||
return formData.value.type === 'execution' ? EVENT_EXECUTION_OPTIONS : EVENT_OPTIONS
|
||||
})
|
||||
|
||||
/** 值标签 */
|
||||
const valueLabel = computed(() => {
|
||||
return formData.value.valueType === 'class' ? '类路径' : '表达式'
|
||||
})
|
||||
|
||||
/** 值占位符 */
|
||||
const valuePlaceholder = computed(() => {
|
||||
return formData.value.valueType === 'class' ? '请输入类路径' : '请输入表达式'
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-listener/index')
|
||||
}
|
||||
|
||||
/** 类型变更时清空事件 */
|
||||
function handleTypeChange() {
|
||||
formData.value.event = ''
|
||||
}
|
||||
|
||||
/** 值类型变更时清空值 */
|
||||
function handleValueTypeChange() {
|
||||
formData.value.value = ''
|
||||
}
|
||||
|
||||
/** 加载流程监听器详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getProcessListener(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateProcessListener(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createProcessListener(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
168
src/pages-bpm/process-listener/index.vue
Normal file
168
src/pages-bpm/process-listener/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程监听器管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程监听器列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">监听器类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">监听事件:</text>
|
||||
<text>{{ item.event }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">值类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" :value="item.valueType" />
|
||||
</view>
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">值:</text>
|
||||
<text class="break-all">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程监听器数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getProcessListenerPage } from '@/api/bpm/process-listener'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<ProcessListener[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程监听器列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessListenerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程监听器 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/process-listener/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessListener) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-listener/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
284
src/pages-bpm/processInstance/create/index.vue
Normal file
284
src/pages-bpm/processInstance/create/index.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<!-- TODO vben 对应的地址:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/create/index.vue -->
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="发起申请"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<wd-search
|
||||
v-model="searchName"
|
||||
placeholder="请输入流程名称"
|
||||
placeholder-left
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<wd-tabs
|
||||
v-model="activeCategory"
|
||||
slidable="always"
|
||||
sticky
|
||||
@click="handleTabClick"
|
||||
>
|
||||
<wd-tab v-for="item in categoryList" :key="item.code" :title="item.name" :name="item.code" />
|
||||
</wd-tabs>
|
||||
|
||||
<!-- 流程定义列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="h-[calc(100vh-320rpx)]"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
scroll-with-animation
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<view
|
||||
v-for="item in categoryList"
|
||||
:id="`category-${item.code}`"
|
||||
:key="item.code"
|
||||
class="category-section mx-24rpx mt-24rpx"
|
||||
:data-category="item.code"
|
||||
>
|
||||
<!-- 分类标题 -->
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="text-28rpx text-[#333] font-bold">{{ item.name }}</text>
|
||||
</view>
|
||||
<!-- 流程列表 -->
|
||||
<view v-if="groupedDefinitions[item.code]?.length" class="overflow-hidden rounded-16rpx bg-white">
|
||||
<view
|
||||
v-for="definition in groupedDefinitions[item.code]"
|
||||
:key="definition.id"
|
||||
class="flex items-center border-b border-[#f5f5f5] p-24rpx last:border-b-0"
|
||||
@click="handleSelect(definition)"
|
||||
>
|
||||
<wd-img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
width="64rpx"
|
||||
height="64rpx"
|
||||
mode="aspectFit"
|
||||
radius="24rpx"
|
||||
class="mr-16rpx"
|
||||
/>
|
||||
<view
|
||||
v-else
|
||||
class="mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-12rpx"
|
||||
:style="{ backgroundColor: getIconColor(definition.name) }"
|
||||
>
|
||||
<text class="text-24rpx text-white font-bold">{{ getIconText(definition.name) }}</text>
|
||||
</view>
|
||||
<text class="text-28rpx text-[#333]">{{ definition.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="overflow-hidden rounded-16rpx bg-white p-24rpx text-center">
|
||||
<text class="text-26rpx text-[#999]">该分类下暂无流程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="categoryList.length === 0" class="py-100rpx">
|
||||
<wd-status-tip image="content" tip="暂无可发起的流程" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { ProcessDefinition } from '@/api/bpm/definition'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getCategorySimpleList } from '@/api/bpm/category'
|
||||
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
||||
import { getMobileFormCustomPath } from '@/pages-bpm/utils'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmModelFormType } from '@/utils/constants'
|
||||
|
||||
// TODO @芋艿:【重新发起流程】支持通过 processInstanceId 参数重新发起已有流程
|
||||
// 对应 vben 第 44-60 行:从路由获取 processInstanceId,查询流程实例后自动选中对应流程定义并填充表单数据
|
||||
|
||||
// TODO @芋艿:【流程表单填写】选择流程后跳转到表单填写页面
|
||||
// 对应 vben form.vue 全部:包含以下子功能:
|
||||
// - 表单渲染 (form-create):vben form.vue 第 145-152 行
|
||||
// - 审批流程预览时间线:vben form.vue 第 153-162 行
|
||||
// - 流程图预览 (BPMN/简易):vben form.vue 第 163-178 行
|
||||
// - 发起人自选审批人:vben form.vue 第 30-32, 85-95 行
|
||||
// - 表单字段权限控制 (读/写/隐藏):vben form.vue 第 119-131 行
|
||||
// - 业务表单跳转 (formCustomCreatePath):vben form.vue 第 79-85 行
|
||||
// - 表单值变化重新预测审批节点:vben form.vue 第 87-102 行
|
||||
// - 提交流程实例:vben form.vue 第 56-76 行
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const searchName = ref('')
|
||||
const activeCategory = ref('')
|
||||
const categoryList = ref<Category[]>([])
|
||||
const categoryPositions = ref<{ code: string, top: number }[]>([]) // 分类区域位置信息(用于滚动时自动切换 tab)
|
||||
const scrollIntoView = ref('')
|
||||
const isTabClicking = ref(false) // 是否正在通过点击 tab 触发滚动(避免滚动事件反向更新 tab)
|
||||
|
||||
const definitionList = ref<ProcessDefinition[]>([])
|
||||
|
||||
/** 根据流程名称获取图标背景色 */
|
||||
function getIconColor(name: string): string {
|
||||
const iconColors = ['#D98469', '#7BC67C', '#4A7FEB', '#9B7FEB', '#4A9DEB']
|
||||
// 根据名称 hashcode 取模选择颜色
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = (hash * 31 + name.charCodeAt(i)) | 0
|
||||
}
|
||||
return iconColors[Math.abs(hash) % iconColors.length]
|
||||
}
|
||||
|
||||
/** 获取流程名称的前两个字符作为图标文字 */
|
||||
function getIconText(name: string): string {
|
||||
return name?.slice(0, 2) || ''
|
||||
}
|
||||
|
||||
/** 过滤后的流程定义 */
|
||||
const filteredDefinitions = computed(() => {
|
||||
if (!searchName.value.trim()) {
|
||||
return definitionList.value
|
||||
}
|
||||
return definitionList.value.filter(item =>
|
||||
item.name.toLowerCase().includes(searchName.value.toLowerCase()),
|
||||
)
|
||||
})
|
||||
|
||||
/** 按分类分组的流程定义 */
|
||||
const groupedDefinitions = computed<Record<string, ProcessDefinition[]>>(() => {
|
||||
const grouped: Record<string, ProcessDefinition[]> = {}
|
||||
filteredDefinitions.value.forEach((item) => {
|
||||
if (!item.category)
|
||||
return
|
||||
if (!grouped[item.category])
|
||||
grouped[item.category] = []
|
||||
grouped[item.category].push(item)
|
||||
})
|
||||
return grouped
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages/bpm/index')
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
async function handleSearch() {
|
||||
// 搜索后重新计算分类位置
|
||||
await nextTick()
|
||||
updateCategoryPositions()
|
||||
}
|
||||
|
||||
/** Tab 点击 */
|
||||
function handleTabClick({ name }: { index: number, name: string }) {
|
||||
isTabClicking.value = true
|
||||
// 滚动到对应分类
|
||||
scrollIntoView.value = ''
|
||||
nextTick(() => {
|
||||
scrollIntoView.value = `category-${name}`
|
||||
// 300ms 后恢复滚动监听
|
||||
setTimeout(() => {
|
||||
isTabClicking.value = false
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/** 滚动事件 - 自动切换 tab */
|
||||
function handleScroll(e: { detail: { scrollTop: number } }) {
|
||||
if (isTabClicking.value || categoryPositions.value.length === 0) {
|
||||
return
|
||||
}
|
||||
// 找到当前滚动位置对应的分类
|
||||
const scrollTop = e.detail.scrollTop
|
||||
for (let i = categoryPositions.value.length - 1; i >= 0; i--) {
|
||||
if (scrollTop >= categoryPositions.value[i].top - 20) {
|
||||
if (activeCategory.value !== categoryPositions.value[i].code) {
|
||||
activeCategory.value = categoryPositions.value[i].code
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新分类区域位置信息 */
|
||||
function updateCategoryPositions() {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.selectAll('.category-section').boundingClientRect()
|
||||
query.exec((res) => {
|
||||
if (res && res[0]) {
|
||||
const positions: { code: string, top: number }[] = []
|
||||
const firstTop = res[0][0]?.top || 0
|
||||
res[0].forEach((item: { top: number, dataset?: { category?: string } }, index: number) => {
|
||||
const cat = categoryList.value[index]
|
||||
if (cat) {
|
||||
positions.push({
|
||||
code: cat.code,
|
||||
top: item.top - firstTop,
|
||||
})
|
||||
}
|
||||
})
|
||||
categoryPositions.value = positions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 选择流程定义 */
|
||||
function handleSelect(item: ProcessDefinition) {
|
||||
// 情况一:流程表单,提示仅允许 PC 端发起
|
||||
if (item.formType === BpmModelFormType.NORMAL) {
|
||||
// TODO @jason:业务表单:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue
|
||||
toast.show('流程表单仅支持 PC 端发起')
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:业务表单,跳转到对应的移动端页面
|
||||
if (item.formType === BpmModelFormType.CUSTOM) {
|
||||
const mobilePath = getMobileFormCustomPath(item.formCustomCreatePath)
|
||||
if (mobilePath) {
|
||||
uni.navigateTo({ url: mobilePath })
|
||||
} else {
|
||||
toast.show('该业务表单暂不支持移动端发起')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载分类列表 */
|
||||
async function loadCategoryList() {
|
||||
categoryList.value = await getCategorySimpleList()
|
||||
}
|
||||
|
||||
/** 加载流程定义列表 */
|
||||
async function loadDefinitionList() {
|
||||
definitionList.value = await getProcessDefinitionList({ suspensionState: 1 })
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onLoad(async () => {
|
||||
await Promise.all([loadCategoryList(), loadDefinitionList()])
|
||||
// 默认选中第一个分类
|
||||
if (categoryList.value.length > 0) {
|
||||
activeCategory.value = categoryList.value[0].code
|
||||
}
|
||||
// 等待 DOM 渲染后计算分类位置
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
updateCategoryPositions()
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
143
src/pages-bpm/processInstance/detail/add-sign/index.vue
Normal file
143
src/pages-bpm/processInstance/detail/add-sign/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="加签任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 加签处理人 -->
|
||||
<UserPicker
|
||||
v-model="formData.userIds"
|
||||
prop="userIds"
|
||||
type="checkbox"
|
||||
label="加签处理人:"
|
||||
label-width="200rpx"
|
||||
placeholder="请选择加签处理人"
|
||||
:rules="formRules.userIds"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="200rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx flex gap-16rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
class="flex-1"
|
||||
plain
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit('before')"
|
||||
>
|
||||
向前加签
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="primary"
|
||||
class="flex-1"
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit('after')"
|
||||
>
|
||||
向后加签
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { signCreateTask } from '@/api/bpm/task'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
userIds: [] as number[],
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
userIds: [
|
||||
{ required: true, message: '加签处理人不能为空', validator: (value: number[]) => value.length > 0 },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit(type: 'before' | 'after') {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await signCreateTask({
|
||||
id: taskId.value as string,
|
||||
type,
|
||||
userIds: formData.userIds,
|
||||
reason: formData.reason,
|
||||
})
|
||||
const actionText = type === 'before' ? '向前加签' : '向后加签'
|
||||
toast.success(`${actionText}成功`)
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
327
src/pages-bpm/processInstance/detail/audit/index.vue
Normal file
327
src/pages-bpm/processInstance/detail/audit/index.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="isApprove ? '审批同意' : '审批拒绝'"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 审批表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 下一个节点的审批人 -->
|
||||
<view v-if="isApprove && nextAssigneesActivityNode.length > 0" class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="mr-8rpx text-[#f56c6c]">*</text>
|
||||
<text class="text-28rpx text-[#333]">下一个节点的审批人</text>
|
||||
</view>
|
||||
<ProcessInstanceTimeline
|
||||
:activity-nodes="nextAssigneesActivityNode"
|
||||
:show-status-icon="false"
|
||||
:enable-approve-user-select="true"
|
||||
@select-user-confirm="selectNextAssigneesConfirm"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 签名 -->
|
||||
<view v-if="isApprove && taskInfo?.signEnable" class="border-b border-[#eee] p-24rpx">
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="mr-8rpx text-[#f56c6c]">*</text>
|
||||
<text class="text-28rpx text-[#333]">签名</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-16rpx">
|
||||
<wd-button type="primary" size="small" @click="openSignatureModal">
|
||||
{{ formData.signPicUrl ? '重新签名' : '点击签名' }}
|
||||
</wd-button>
|
||||
<image
|
||||
v-if="formData.signPicUrl"
|
||||
:src="formData.signPicUrl"
|
||||
class="h-80rpx w-192rpx"
|
||||
mode="aspectFit"
|
||||
@click="previewSignature"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
:type="isApprove ? 'primary' : 'error'"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isApprove ? '同意' : '拒绝' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签名弹窗 -->
|
||||
<wd-popup v-model="showSignatureModal" position="bottom" custom-style="height: 60vh;">
|
||||
<view class="h-full flex flex-col">
|
||||
<view class="flex items-center justify-between border-b border-[#eee] p-24rpx">
|
||||
<text class="text-32rpx text-[#333] font-bold">手写签名</text>
|
||||
<wd-icon name="close" size="40rpx" @click="showSignatureModal = false" />
|
||||
</view>
|
||||
<view class="flex-1 p-24rpx">
|
||||
<wd-signature
|
||||
:height="300"
|
||||
:export-scale="2"
|
||||
background-color="#ffffff"
|
||||
@confirm="handleSignatureConfirm"
|
||||
@clear="handleSignatureClear"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApprovalDetail, getNextApproveNodes } from '@/api/bpm/processInstance'
|
||||
import { approveTask, rejectTask } from '@/api/bpm/task'
|
||||
import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
|
||||
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
|
||||
import { BpmCandidateStrategyEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId?: string
|
||||
taskId?: string
|
||||
pass?: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
const taskId = computed(() => props.taskId || '')
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const isApprove = computed(() => props.pass !== 'false') // true: 同意, false: 拒绝
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const taskInfo = ref<Task | null>(null) // 任务信息
|
||||
|
||||
const nextAssigneesActivityNode = ref<ApprovalNodeInfo[]>([]) // 下一个节点审批人列表
|
||||
const approveUserSelectTasks = ref<ApprovalNodeInfo[]>([]) // 需要选择审批人的节点列表
|
||||
const approveUserSelectAssignees = ref<Record<string, number[]>>({}) // 审批人选择的审批人数据
|
||||
|
||||
// 签名相关
|
||||
const showSignatureModal = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
reason: '',
|
||||
signPicUrl: '', // 签名图片 URL
|
||||
})
|
||||
|
||||
const formRules = computed(() => {
|
||||
let rules = {}
|
||||
if (taskInfo.value?.reasonRequire) {
|
||||
rules = {
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 加载任务信息 */
|
||||
async function loadTaskInfo() {
|
||||
const data = await getApprovalDetail({
|
||||
processInstanceId: processInstanceId.value,
|
||||
taskId: taskId.value,
|
||||
})
|
||||
taskInfo.value = data?.todoTask || null
|
||||
}
|
||||
|
||||
/** 加载下一个节点审批人 */
|
||||
async function loadNextApproveNodes() {
|
||||
if (!isApprove.value) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
processInstanceId: processInstanceId.value,
|
||||
taskId: taskId.value,
|
||||
}
|
||||
const data = await getNextApproveNodes(params)
|
||||
if (data && data.length > 0) {
|
||||
nextAssigneesActivityNode.value = data
|
||||
// 获取审批人自选的任务
|
||||
approveUserSelectTasks.value = data.filter(
|
||||
(node: ApprovalNodeInfo) =>
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT === node.candidateStrategy,
|
||||
) || []
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择下一个节点审批人确认 */
|
||||
function selectNextAssigneesConfirm(activityId: string, userList: any[]) {
|
||||
approveUserSelectAssignees.value[activityId] = userList.map(user => user.id)
|
||||
}
|
||||
|
||||
/** 打开签名弹窗 */
|
||||
function openSignatureModal() {
|
||||
showSignatureModal.value = true
|
||||
}
|
||||
|
||||
/** 签名确认 */
|
||||
async function handleSignatureConfirm(result: { tempFilePath: string, base64: string }) {
|
||||
toast.loading('上传中...')
|
||||
try {
|
||||
// 上传签名图片
|
||||
const url = await uploadSignatureFile(result.tempFilePath)
|
||||
formData.signPicUrl = url
|
||||
showSignatureModal.value = false
|
||||
toast.success('签名成功')
|
||||
} catch (err) {
|
||||
console.error('上传失败:', err)
|
||||
toast.show('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传签名文件 */
|
||||
function uploadSignatureFile(tempFilePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${getEnvBaseUrl()}/infra/file/upload`,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
success: (uploadFileRes) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadFileRes.data)
|
||||
if (data.code === 0 && data.data) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.msg || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('上传失败:', err)
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 签名清除 */
|
||||
function handleSignatureClear() {
|
||||
formData.signPicUrl = ''
|
||||
}
|
||||
|
||||
/** 预览签名 */
|
||||
function previewSignature() {
|
||||
if (formData.signPicUrl) {
|
||||
uni.previewImage({
|
||||
urls: [formData.signPicUrl],
|
||||
current: formData.signPicUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交审批 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
// 验证签名
|
||||
if (isApprove.value && taskInfo.value?.signEnable && !formData.signPicUrl) {
|
||||
toast.show('请先进行签名')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证审批人选择
|
||||
if (isApprove.value && approveUserSelectTasks.value.length > 0) {
|
||||
for (const task of approveUserSelectTasks.value) {
|
||||
if (!approveUserSelectAssignees.value[task.id] || approveUserSelectAssignees.value[task.id].length === 0) {
|
||||
toast.show(`请选择「${task.name}」的审批人`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (isApprove.value) {
|
||||
// 审批通过
|
||||
await approveTask({
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
signPicUrl: formData.signPicUrl || undefined,
|
||||
nextAssignees: Object.keys(approveUserSelectAssignees.value).length > 0
|
||||
? approveUserSelectAssignees.value
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
// 审批拒绝
|
||||
await rejectTask({
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
})
|
||||
}
|
||||
toast.success('审批成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 1000)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(async () => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
// 加载任务信息和下一个节点审批人
|
||||
await loadTaskInfo()
|
||||
await loadNextApproveNodes()
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- 表单详情:流程表单/业务表单 -->
|
||||
<template>
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 标题 -->
|
||||
<view class="px-24rpx pt-24rpx text-28rpx text-[#333] font-bold">
|
||||
审批详情
|
||||
</view>
|
||||
<!-- 表单内容:业务表单 -->
|
||||
<template v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
|
||||
<!-- OA 请假详情 -->
|
||||
<LeaveDetail
|
||||
v-if="processDefinition?.formCustomViewPath === '/bpm/oa/leave/detail'"
|
||||
:id="processInstance?.businessKey"
|
||||
embedded
|
||||
/>
|
||||
<!-- 未配置的业务表单 -->
|
||||
<view v-else class="px-24rpx py-32rpx text-26rpx text-[#999]">
|
||||
暂不支持该业务表单,请参考 LeaveDetail 配置
|
||||
</view>
|
||||
</template>
|
||||
<!-- TODO @jason:表单内容:流程表单 -->
|
||||
<template v-else-if="processDefinition?.formType === BpmModelFormType.NORMAL">
|
||||
<view class="px-24rpx py-32rpx text-26rpx text-[#999]">
|
||||
流程表单仅 PC 端支持预览
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessDefinition, ProcessInstance } from '@/api/bpm/processInstance'
|
||||
// 特殊:业务表单组件(uniapp 小程序不支持动态组件,需要静态导入)
|
||||
import LeaveDetail from '@/pages-bpm/oa/leave/detail/index.vue'
|
||||
import { BpmModelFormType } from '@/utils/constants'
|
||||
|
||||
defineProps<{
|
||||
/** 流程定义 */
|
||||
processDefinition?: ProcessDefinition
|
||||
/** 流程实例 */
|
||||
processInstance?: ProcessInstance
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,311 @@
|
||||
<!-- 操作按钮 -->
|
||||
<template>
|
||||
<!-- 有待审批的任务 -->
|
||||
<view v-if="runningTask" class="yd-detail-footer">
|
||||
<view class="w-full flex items-center">
|
||||
<!-- 左侧操作按钮 -->
|
||||
<view v-for="(action, idx) in leftOperations" :key="idx" class="mr-32rpx w-60rpx flex flex-col items-center" @click="handleOperation(action.operationType)">
|
||||
<wd-icon :name="action.iconName" size="40rpx" color="#1890ff" />
|
||||
<text class="mt-4rpx text-22rpx text-[#333]">{{ action.displayName }}</text>
|
||||
</view>
|
||||
<!-- 更多操作按钮 -->
|
||||
<view v-if="moreOperations.length > 0" class="mr-32rpx w-60rpx flex flex-col items-center" @click="handleShowMore">
|
||||
<wd-icon name="ellipsis" size="40rpx" color="#1890ff" />
|
||||
<text class="mt-4rpx text-22rpx text-[#333]">更多</text>
|
||||
</view>
|
||||
<!-- 更多操作 ActionSheet -->
|
||||
<wd-action-sheet v-if="moreOperations.length > 0" v-model="showMoreActions" :actions="moreOperations" title="请选择操作" @select="handleMoreAction" />
|
||||
|
||||
<!-- 右侧按钮,TODO @jason:是否一定要保留两个按钮(需要的哈) -->
|
||||
<view class="flex flex-1 gap-16rpx">
|
||||
<wd-button
|
||||
v-for="(action, idx) in rightOperations"
|
||||
:key="idx"
|
||||
:plain="action.plain"
|
||||
:type="action.btnType"
|
||||
:round="false"
|
||||
class="flex-1"
|
||||
custom-style="min-width: 200rpx; width: 200rpx;"
|
||||
@click="handleOperation(action.operationType)"
|
||||
>
|
||||
{{ action.displayName }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 无待审批的任务 仅显示取消按钮。TODO @jason:看看还需要显示(这个微信交流下) -->
|
||||
<view v-if="!runningTask && isShowProcessStartCancel()" class="yd-detail-footer">
|
||||
<wd-button
|
||||
plain
|
||||
type="primary"
|
||||
:round="false"
|
||||
block
|
||||
@click="handleOperation(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)"
|
||||
>
|
||||
取消
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Action } from 'wot-design-uni/components/wd-action-sheet/types'
|
||||
import type { ButtonType } from 'wot-design-uni/components/wd-button/types'
|
||||
import type { ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { useUserStore } from '@/store'
|
||||
import {
|
||||
BpmProcessInstanceStatus,
|
||||
BpmTaskOperationButtonTypeEnum,
|
||||
BpmTaskStatusEnum,
|
||||
OPERATION_BUTTON_NAME,
|
||||
} from '@/utils/constants'
|
||||
|
||||
const showMoreActions = ref(false)
|
||||
|
||||
type MoreOperationType = Action & {
|
||||
operationType: number
|
||||
}
|
||||
|
||||
interface LeftOperationType {
|
||||
operationType: number
|
||||
iconName: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface RightOperationType {
|
||||
operationType: number
|
||||
btnType: ButtonType
|
||||
displayName: string
|
||||
plain: boolean
|
||||
}
|
||||
const operationIconsMap: Record<number, string> = {
|
||||
[BpmTaskOperationButtonTypeEnum.TRANSFER]: 'transfer',
|
||||
[BpmTaskOperationButtonTypeEnum.ADD_SIGN]: 'add',
|
||||
[BpmTaskOperationButtonTypeEnum.DELEGATE]: 'share',
|
||||
[BpmTaskOperationButtonTypeEnum.RETURN]: 'arrow-left',
|
||||
[BpmTaskOperationButtonTypeEnum.COPY]: 'copy',
|
||||
[BpmTaskOperationButtonTypeEnum.DELETE_SIGN]: 'remove',
|
||||
[BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL]: 'stop-circle',
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const leftOperations = ref<LeftOperationType[]>([]) // 左侧操作按钮 【最多两个】{转办, 委派, 退回, 加签, 抄送等}
|
||||
const rightOperationTypes = [] // 右侧操作按钮【最多两个】{通过,拒绝, 取消}
|
||||
const rightOperations = ref<RightOperationType[]>([])
|
||||
const moreOperations = ref<MoreOperationType[]>([]) // 更多操作
|
||||
const runningTask = ref<Task>()
|
||||
const processInstance = ref<ProcessInstance>()
|
||||
const reasonRequire = ref<boolean>(false)
|
||||
|
||||
/** 初始化 */
|
||||
function init(theProcessInstance: ProcessInstance, task: Task) {
|
||||
processInstance.value = theProcessInstance
|
||||
runningTask.value = task
|
||||
if (task) {
|
||||
reasonRequire.value = task.reasonRequire ?? false
|
||||
// TODO @jason:这里的判断,是否可以简化哈?就是默认计算出按钮,然后根据数量,去渲染具体的按钮。
|
||||
// 右侧按钮
|
||||
if (isHandleTaskStatus() && isShowButton(BpmTaskOperationButtonTypeEnum.REJECT)) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.REJECT)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.REJECT,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.REJECT),
|
||||
btnType: 'error',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
if (isHandleTaskStatus() && isShowButton(BpmTaskOperationButtonTypeEnum.APPROVE)) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.APPROVE)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.APPROVE,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.APPROVE),
|
||||
btnType: 'primary',
|
||||
plain: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 左侧操作,和更多操作
|
||||
Object.keys(task.buttonsSetting || {}).forEach((key) => {
|
||||
const operationType = Number(key)
|
||||
if (task.buttonsSetting[key].enable && isHandleTaskStatus()
|
||||
&& !rightOperationTypes.includes(operationType)) {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(operationType),
|
||||
operationType,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType,
|
||||
iconName: operationIconsMap[operationType],
|
||||
displayName: getButtonDisplayName(operationType),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 减签操作的显示
|
||||
if (isShowDeleteSign()) {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.DELETE_SIGN),
|
||||
operationType: BpmTaskOperationButtonTypeEnum.DELETE_SIGN,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.DELETE_SIGN,
|
||||
iconName: operationIconsMap[BpmTaskOperationButtonTypeEnum.DELETE_SIGN],
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.DELETE_SIGN),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 是否显示流程取消
|
||||
if (isShowProcessStartCancel()) {
|
||||
if (rightOperationTypes.length < 2) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
btnType: 'primary',
|
||||
plain: true,
|
||||
})
|
||||
} else {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
iconName: operationIconsMap[BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL],
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到相应的操作页面 */
|
||||
function handleOperation(operationType: number) {
|
||||
switch (operationType) {
|
||||
case BpmTaskOperationButtonTypeEnum.APPROVE:
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}&pass=true` })
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.REJECT:
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}&pass=false` })
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.DELEGATE:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/reassign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&type=delegate`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.TRANSFER:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/reassign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&type=transfer`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.ADD_SIGN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/add-sign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.RETURN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/return/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.DELETE_SIGN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/delete-sign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&children=${encodeURIComponent(JSON.stringify(runningTask.value.children || []))}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/process-cancel/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}`,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示更多操作 */
|
||||
function handleShowMore() {
|
||||
showMoreActions.value = true
|
||||
}
|
||||
|
||||
/** 处理更多操作选择 */
|
||||
function handleMoreAction(action: { item: MoreOperationType }) {
|
||||
handleOperation(action.item.operationType)
|
||||
showMoreActions.value = false
|
||||
}
|
||||
|
||||
/** 获取按钮的显示名称 */
|
||||
function getButtonDisplayName(btnType: BpmTaskOperationButtonTypeEnum) {
|
||||
let displayName = OPERATION_BUTTON_NAME.get(btnType)
|
||||
if (
|
||||
runningTask.value?.buttonsSetting
|
||||
&& runningTask.value?.buttonsSetting[btnType]
|
||||
) {
|
||||
displayName = runningTask.value.buttonsSetting[btnType].displayName
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
/** 是否显示按钮 */
|
||||
function isShowButton(btnType: BpmTaskOperationButtonTypeEnum): boolean {
|
||||
let isShow = true
|
||||
if (
|
||||
runningTask.value?.buttonsSetting
|
||||
&& runningTask.value?.buttonsSetting[btnType]
|
||||
) {
|
||||
isShow = runningTask.value.buttonsSetting[btnType].enable
|
||||
}
|
||||
return isShow
|
||||
}
|
||||
|
||||
/** 任务是否为处理中状态 */
|
||||
function isHandleTaskStatus() {
|
||||
let canHandle = false
|
||||
if (BpmTaskStatusEnum.RUNNING === runningTask.value?.status) {
|
||||
canHandle = true
|
||||
}
|
||||
return canHandle
|
||||
}
|
||||
|
||||
/** 流程状态是否为结束状态 */
|
||||
function isEndProcessStatus(status: number) {
|
||||
let isEndStatus = false
|
||||
if (
|
||||
BpmProcessInstanceStatus.APPROVE === status
|
||||
|| BpmProcessInstanceStatus.REJECT === status
|
||||
|| BpmProcessInstanceStatus.CANCEL === status
|
||||
) {
|
||||
isEndStatus = true
|
||||
}
|
||||
return isEndStatus
|
||||
}
|
||||
|
||||
/** 流程发起人是否为当前用户 */
|
||||
function isProcessStartUser() {
|
||||
let isStartUser = false
|
||||
if (userStore.userInfo?.userId === processInstance.value?.startUser?.id) {
|
||||
isStartUser = true
|
||||
}
|
||||
return isStartUser
|
||||
}
|
||||
|
||||
/** 是否显示减签 */
|
||||
function isShowDeleteSign() {
|
||||
return runningTask.value?.children?.length > 0
|
||||
}
|
||||
|
||||
/** 是否显示流程发起人取消 */
|
||||
function isShowProcessStartCancel() {
|
||||
return isProcessStartUser() && !isEndProcessStatus(processInstance.value?.status)
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ init })
|
||||
</script>
|
||||
394
src/pages-bpm/processInstance/detail/components/time-line.vue
Normal file
394
src/pages-bpm/processInstance/detail/components/time-line.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<!-- 遍历每个审批节点 -->
|
||||
<view
|
||||
v-for="(activity, index) in activityNodes"
|
||||
:key="activity.id || index"
|
||||
class="relative pb-24rpx pl-80rpx"
|
||||
>
|
||||
<!-- 时间线圆点 -->
|
||||
<view
|
||||
class="absolute left-12rpx top-8rpx h-52rpx w-52rpx flex items-center justify-center rounded-full bg-blue-500"
|
||||
>
|
||||
<!-- 节点类型图标 -->
|
||||
<wd-icon
|
||||
:name="getApprovalNodeTypeIcon(activity.nodeType)"
|
||||
size="32rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 状态小图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon"
|
||||
class="absolute left-48rpx top-44rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="getApprovalNodeIcon(activity.status, activity.nodeType)"
|
||||
size="12rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<view
|
||||
v-if="index < activityNodes.length - 1"
|
||||
class="absolute bottom-0 left-38rpx top-64rpx w-2rpx bg-[#e5e5e5]"
|
||||
/>
|
||||
|
||||
<!-- 节点内容 -->
|
||||
<view class="ml-8rpx">
|
||||
<!-- 第一行:节点名称、时间 -->
|
||||
<view class="mb-8rpx flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-28rpx text-[#333] font-bold">{{ activity.name }}</text>
|
||||
<text v-if="activity.status === BpmTaskStatusEnum.SKIP" class="ml-8rpx text-24rpx text-[#999]">
|
||||
【跳过】
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="activity.status !== BpmTaskStatusEnum.NOT_START && getApprovalNodeTime(activity)"
|
||||
class="text-22rpx text-[#999]"
|
||||
>
|
||||
{{ getApprovalNodeTime(activity) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 子流程节点 -->
|
||||
<view v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE" class="mb-16rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:disabled="!activity.processInstanceId"
|
||||
@click="handleChildProcess(activity)"
|
||||
>
|
||||
查看子流程
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 需要自定义选择审批人 -->
|
||||
<view v-if="shouldShowCustomUserSelect(activity)" class="mb-16rpx">
|
||||
<view class="flex flex-wrap items-center">
|
||||
<!-- 添加用户按钮 -->
|
||||
<UserPicker
|
||||
:model-value="getSelectedUserIds(activity.id)"
|
||||
type="checkbox"
|
||||
use-default-slot
|
||||
@confirm="(users) => handleCustomUserSelectConfirm(activity.id, users)"
|
||||
>
|
||||
<view
|
||||
class="mb-8rpx mr-16rpx h-48rpx w-48rpx flex items-center justify-center border-indigo-500 rounded-lg border-solid"
|
||||
>
|
||||
<wd-icon name="user-add" size="32rpx" color="blue" />
|
||||
</view>
|
||||
</UserPicker>
|
||||
<!-- 已选择的用户 -->
|
||||
<view
|
||||
v-for="(user, userIndex) in customApproveUsers[activity.id]"
|
||||
:key="user.id || userIndex"
|
||||
class="mb-8rpx mr-16rpx flex items-center rounded-32rpx bg-[#f5f5f5] pr-16rpx"
|
||||
>
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ user.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<text class="text-24rpx text-[#333]">{{ user.nickname }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批人员列表 -->
|
||||
<view v-else class="mb-16rpx">
|
||||
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
|
||||
<view v-if="activity.tasks && activity.tasks.length > 0">
|
||||
<view
|
||||
v-for="(task, taskIndex) in activity.tasks"
|
||||
:key="taskIndex"
|
||||
class="mb-16rpx"
|
||||
>
|
||||
<!-- 审批人信息 -->
|
||||
<view v-if="task.assigneeUser || task.ownerUser" class="mb-8rpx flex items-center">
|
||||
<!-- TODO @jason 用户头像显示 -->
|
||||
<view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ (task.assigneeUser?.nickname || task.ownerUser?.nickname)?.[0] || '?' }}
|
||||
|
||||
<!-- 任务状态小图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon && shouldShowTaskStatusIcon(task.status)"
|
||||
class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(task.status) }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="getApprovalNodeIcon(task.status, activity.nodeType)"
|
||||
size="12rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-1">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-26rpx text-[#333]">
|
||||
{{ task.assigneeUser?.nickname || task.ownerUser?.nickname }}
|
||||
</text>
|
||||
<text
|
||||
v-if="task.assigneeUser?.deptName || task.ownerUser?.deptName"
|
||||
class="ml-8rpx text-22rpx text-[#999]"
|
||||
>
|
||||
{{ task.assigneeUser?.deptName || task.ownerUser?.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-4rpx flex items-center">
|
||||
<text :class="getStatusTextClass(task.status)" class="text-24rpx">
|
||||
{{ getStatusText(task.status) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<view
|
||||
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
||||
class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
|
||||
>
|
||||
<text class="text-24rpx text-[#666]">审批意见:{{ task.reason }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 签名 -->
|
||||
<view
|
||||
v-if="task.signPicUrl && activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE"
|
||||
class="mt-8rpx flex items-center rounded-8rpx bg-[#f5f5f5] p-16rpx"
|
||||
>
|
||||
<text class="text-24rpx text-[#666]">签名:</text>
|
||||
<image
|
||||
:src="task.signPicUrl"
|
||||
class="ml-8rpx h-96rpx w-288rpx"
|
||||
mode="aspectFit"
|
||||
@click="previewImage(task.signPicUrl)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
<view v-if="activity.candidateUsers && activity.candidateUsers.length > 0">
|
||||
<view
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="mb-8rpx flex items-center"
|
||||
>
|
||||
<view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ user.nickname?.[0] || '?' }}
|
||||
|
||||
<!-- 候选状态图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon"
|
||||
class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(BpmTaskStatusEnum.NOT_START) }"
|
||||
>
|
||||
<wd-icon name="time" size="12rpx" color="white" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-1">
|
||||
<text class="text-26rpx text-[#333]">{{ user.nickname }}</text>
|
||||
<text v-if="user.deptName" class="ml-8rpx text-22rpx text-[#999]">
|
||||
{{ user.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import { ref } from 'vue'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { BpmCandidateStrategyEnum, BpmNodeTypeEnum, BpmTaskStatusEnum } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activityNodes: ApprovalNodeInfo[]
|
||||
enableApproveUserSelect?: boolean
|
||||
showStatusIcon?: boolean
|
||||
}>(),
|
||||
{
|
||||
showStatusIcon: true,
|
||||
enableApproveUserSelect: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectUserConfirm: [activityId: string, userList: any[]]
|
||||
}>()
|
||||
|
||||
// 状态图标映射
|
||||
const statusIconMap: Record<string, { color: string, icon: string }> = {
|
||||
'-2': { color: '#909398', icon: 'skip-forward' }, // 跳过
|
||||
'-1': { color: '#909398', icon: 'time' }, // 审批未开始
|
||||
'0': { color: '#f59e0b', icon: 'refresh1' }, // 待审批
|
||||
'1': { color: '#f59e0b', icon: 'refresh1' }, // 审批中
|
||||
'2': { color: '#00b32a', icon: 'check' }, // 审批通过
|
||||
'3': { color: '#f46b6c', icon: 'close' }, // 审批不通过
|
||||
'4': { color: '#cccccc', icon: 'delete' }, // 已取消
|
||||
'5': { color: '#f46b6c', icon: 'arrow-left' }, // 退回
|
||||
'6': { color: '#448ef7', icon: 'time' }, // 委派中
|
||||
'7': { color: '#00b32a', icon: 'check' }, // 审批通过中
|
||||
}
|
||||
|
||||
// 节点类型图标映射 TODO 图标重新选一下
|
||||
const nodeTypeSvgMap: Record<number, { color: string, icon: string }> = {
|
||||
[BpmNodeTypeEnum.END_EVENT_NODE]: { color: '#909398', icon: 'poweroff' },
|
||||
[BpmNodeTypeEnum.START_USER_NODE]: { color: '#909398', icon: 'user' },
|
||||
[BpmNodeTypeEnum.USER_TASK_NODE]: { color: '#ff943e', icon: 'user-talk' },
|
||||
[BpmNodeTypeEnum.TRANSACTOR_NODE]: { color: '#ff943e', icon: 'edit' },
|
||||
[BpmNodeTypeEnum.COPY_TASK_NODE]: { color: '#3296fb', icon: 'copy' },
|
||||
[BpmNodeTypeEnum.CONDITION_NODE]: { color: '#14bb83', icon: 'branch' },
|
||||
[BpmNodeTypeEnum.PARALLEL_BRANCH_NODE]: { color: '#14bb83', icon: 'branch' },
|
||||
[BpmNodeTypeEnum.CHILD_PROCESS_NODE]: { color: '#14bb83', icon: 'cluster' },
|
||||
}
|
||||
|
||||
const onlyStatusIconShow = [BpmTaskStatusEnum.NOT_START, BpmTaskStatusEnum.RUNNING, BpmTaskStatusEnum.WAIT] // 只有状态是 -1、0、1 才展示头像右小角状态小 icon
|
||||
|
||||
// 响应式数据
|
||||
const customApproveUsers = ref<Record<string, any[]>>({})
|
||||
const showUserPicker = ref(false)
|
||||
const selectedUserIds = ref<number[]>([])
|
||||
const selectedActivityNodeId = ref<string>()
|
||||
|
||||
/** 获取审批节点类型图标 */
|
||||
function getApprovalNodeTypeIcon(nodeType: number) {
|
||||
return nodeTypeSvgMap[nodeType]?.icon || 'time'
|
||||
}
|
||||
|
||||
/** 获取审批节点图标 */
|
||||
function getApprovalNodeIcon(taskStatus: number, nodeType: number) {
|
||||
if (taskStatus === BpmTaskStatusEnum.NOT_START) {
|
||||
return statusIconMap[taskStatus]?.icon || 'time'
|
||||
}
|
||||
return statusIconMap[taskStatus]?.icon || 'time'
|
||||
}
|
||||
|
||||
/** 获取审批节点颜色 */
|
||||
function getApprovalNodeColor(taskStatus: number) {
|
||||
return statusIconMap[taskStatus]?.color || '#909398'
|
||||
}
|
||||
|
||||
/** 获取审批节点时间 */
|
||||
function getApprovalNodeTime(node: ApprovalNodeInfo) {
|
||||
if (node.nodeType === BpmNodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||
return formatDateTime(node.startTime)
|
||||
}
|
||||
if (node.endTime) {
|
||||
return formatDateTime(node.endTime)
|
||||
}
|
||||
if (node.startTime) {
|
||||
return formatDateTime(node.startTime)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 是否显示任务状态图标 */
|
||||
function shouldShowTaskStatusIcon(status: number) {
|
||||
return onlyStatusIconShow.includes(status)
|
||||
}
|
||||
|
||||
/** 判断是否需要显示自定义选择审批人 */
|
||||
function shouldShowCustomUserSelect(activity: ApprovalNodeInfo) {
|
||||
return (
|
||||
(!activity.tasks || activity.tasks.length === 0)
|
||||
&& ((BpmCandidateStrategyEnum.START_USER_SELECT === activity.candidateStrategy
|
||||
&& (!activity.candidateUsers || activity.candidateUsers.length === 0))
|
||||
|| (props.enableApproveUserSelect
|
||||
&& BpmCandidateStrategyEnum.APPROVE_USER_SELECT === activity.candidateStrategy))
|
||||
)
|
||||
}
|
||||
|
||||
/** 判断是否需要显示审批意见 */
|
||||
function shouldShowApprovalReason(task: any, nodeType: number) {
|
||||
return (
|
||||
task.reason
|
||||
&& [BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(nodeType)
|
||||
)
|
||||
}
|
||||
|
||||
/** 获取状态文本样式类 */
|
||||
function getStatusTextClass(status: number) {
|
||||
const colorMap: Record<number, string> = {
|
||||
[BpmTaskStatusEnum.RUNNING]: 'text-[#ff943e]',
|
||||
[BpmTaskStatusEnum.APPROVE]: 'text-[#00b32a]',
|
||||
[BpmTaskStatusEnum.REJECT]: 'text-[#f46b6c]',
|
||||
[BpmTaskStatusEnum.CANCEL]: 'text-[#cccccc]',
|
||||
[BpmTaskStatusEnum.RETURN]: 'text-[#f46b6c]',
|
||||
}
|
||||
return colorMap[status] || 'text-[#666]'
|
||||
}
|
||||
|
||||
/** 获取状态文本 */
|
||||
function getStatusText(status: number) {
|
||||
const textMap: Record<number, string> = {
|
||||
[BpmTaskStatusEnum.NOT_START]: '未开始',
|
||||
[BpmTaskStatusEnum.RUNNING]: '待审批',
|
||||
[BpmTaskStatusEnum.APPROVE]: '已通过',
|
||||
[BpmTaskStatusEnum.REJECT]: '已拒绝',
|
||||
[BpmTaskStatusEnum.CANCEL]: '已取消',
|
||||
[BpmTaskStatusEnum.RETURN]: '已退回',
|
||||
[BpmTaskStatusEnum.SKIP]: '已跳过',
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
/** 用户选择确认 */
|
||||
function handleCustomUserSelectConfirm(activityId: string, users: any[]) {
|
||||
customApproveUsers.value[activityId] = users || []
|
||||
emit('selectUserConfirm', activityId, users)
|
||||
}
|
||||
|
||||
/** 获取选中的用户ID数组 */
|
||||
function getSelectedUserIds(activityId: string): number[] {
|
||||
const users = customApproveUsers.value[activityId] || []
|
||||
return users.map(user => user.id).filter(id => id !== undefined)
|
||||
}
|
||||
|
||||
/** 跳转子流程 */
|
||||
function handleChildProcess(activity: ApprovalNodeInfo) {
|
||||
if (!activity.processInstanceId) {
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${activity.processInstanceId}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 预览图片 */
|
||||
function previewImage(url: string) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url,
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置自定义审批人 */
|
||||
function setCustomApproveUsers(activityId: string, users: any[]) {
|
||||
customApproveUsers.value[activityId] = users || []
|
||||
}
|
||||
|
||||
/** 批量设置多个节点的自定义审批人 */
|
||||
function batchSetCustomApproveUsers(data: Record<string, any[]>) {
|
||||
Object.keys(data).forEach((activityId) => {
|
||||
customApproveUsers.value[activityId] = data[activityId] || []
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
setCustomApproveUsers,
|
||||
batchSetCustomApproveUsers,
|
||||
})
|
||||
</script>
|
||||
165
src/pages-bpm/processInstance/detail/delete-sign/index.vue
Normal file
165
src/pages-bpm/processInstance/detail/delete-sign/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="减签任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 减签人员选择 -->
|
||||
<wd-picker
|
||||
v-model="formData.deleteSignTaskId"
|
||||
:columns="taskOptions"
|
||||
value-key="id"
|
||||
label-key="label"
|
||||
label="减签人员:"
|
||||
label-width="180rpx"
|
||||
placeholder="请选择减签人员"
|
||||
prop="deleteSignTaskId"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
减签
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { signDeleteTask } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
children?: string // JSON 字符串格式的子任务数据
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const taskOptions = ref<any[]>([])
|
||||
const formData = reactive({
|
||||
deleteSignTaskId: '',
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
deleteSignTaskId: [
|
||||
{ required: true, message: '减签人员不能为空' },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 获取减签人员标签 */
|
||||
function getDeleteSignUserLabel(task: any): string {
|
||||
const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
|
||||
const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
|
||||
return `${nickname} ( 所属部门:${deptName} )`
|
||||
}
|
||||
|
||||
/** 获取可减签的任务列表 */
|
||||
async function loadDeleteSignTaskList() {
|
||||
let childTasks = []
|
||||
// 从 props 中获取子任务数据
|
||||
if (props.children) {
|
||||
try {
|
||||
childTasks = JSON.parse(decodeURIComponent(props.children))
|
||||
} catch (parseError) {
|
||||
console.error('[delete-sign] 解析子任务数据失败:', parseError)
|
||||
}
|
||||
}
|
||||
// 提示没有子任务数据
|
||||
if (childTasks.length === 0) {
|
||||
toast.show('没有可减签的任务')
|
||||
return
|
||||
}
|
||||
|
||||
taskOptions.value = childTasks.map(task => ({
|
||||
id: task.id,
|
||||
label: getDeleteSignUserLabel(task),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
await signDeleteTask({
|
||||
id: formData.deleteSignTaskId,
|
||||
reason: formData.reason,
|
||||
})
|
||||
toast.success('减签成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时,获取可减签任务列表 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
loadDeleteSignTaskList()
|
||||
})
|
||||
</script>
|
||||
147
src/pages-bpm/processInstance/detail/index.vue
Normal file
147
src/pages-bpm/processInstance/detail/index.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<view class="yd-page-container pb-[80rpx]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="审批详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 区域:流程信息(基本信息) -->
|
||||
<view class="relative mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 审批状态图标(盖章效果) -->
|
||||
<image
|
||||
v-if="processInstance?.status !== undefined"
|
||||
:src="getStatusIcon(processInstance?.status)"
|
||||
class="absolute right-20rpx top-20rpx z-10 h-144rpx w-144rpx"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="p-24rpx">
|
||||
<!-- 标题 -->
|
||||
<view class="mb-16rpx pr-160rpx">
|
||||
<text class="text-32rpx text-[#333] font-bold">{{ processInstance?.name }}</text>
|
||||
</view>
|
||||
<!-- 发起人信息 -->
|
||||
<view class="flex items-center">
|
||||
<view class="mr-12rpx h-64rpx w-64rpx flex items-center justify-center rounded-full bg-[#1890ff] text-white">
|
||||
{{ processInstance?.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-28rpx text-[#333]">{{ processInstance?.startUser?.nickname }}</text>
|
||||
<text v-if="processInstance?.startUser?.deptName" class="ml-8rpx text-24rpx text-[#999]">
|
||||
{{ processInstance?.startUser?.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 提交时间 -->
|
||||
<view class="mt-16rpx text-24rpx text-[#999]">
|
||||
提交于 {{ formatDateTime(processInstance?.startTime) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区域:审批详情(表单) -->
|
||||
<FormDetail :process-definition="processDefinition" :process-instance="processInstance" />
|
||||
|
||||
<!-- 区域:审批进度 -->
|
||||
<view class="mx-24rpx mt-24rpx rounded-16rpx bg-white">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex">
|
||||
<text class="text-28rpx text-[#333] font-bold">审批进度</text>
|
||||
</view>
|
||||
<!-- 流程时间线 -->
|
||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- TODO 待开发:区域:流程评论 -->
|
||||
|
||||
<!-- 区域:底部操作栏 -->
|
||||
<ProcessInstanceOperationButton ref="operationButtonRef" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApprovalNodeInfo, ProcessDefinition, ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApprovalDetail } from '@/api/bpm/processInstance'
|
||||
import { getTaskListByProcessInstanceId } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmProcessInstanceStatus } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import FormDetail from './components/form-detail.vue'
|
||||
import ProcessInstanceOperationButton from './components/operation-button.vue'
|
||||
import ProcessInstanceTimeline from './components/time-line.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string // 流程实例的编号
|
||||
taskId?: string // 任务编号
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const processInstance = ref<ProcessInstance>()
|
||||
const processDefinition = ref<ProcessDefinition>()
|
||||
const tasks = ref<Task[]>([])
|
||||
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]) // 审批节点信息
|
||||
|
||||
const operationButtonRef = ref() // 操作按钮组件 ref
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages/bpm/index')
|
||||
}
|
||||
|
||||
/** 获取状态图标 */
|
||||
function getStatusIcon(status?: number): string {
|
||||
// 状态映射: 1-审批中, 2-审批通过, 3-审批不通过, 4-已取消. -1 未开始不会出现
|
||||
const iconMap: Record<number, string> = {
|
||||
[BpmProcessInstanceStatus.RUNNING]: '/static/my-icons/bpm/bpm-running.svg', // 待审批
|
||||
[BpmProcessInstanceStatus.APPROVE]: '/static/my-icons/bpm/bpm-approve.svg', // 审批通过
|
||||
[BpmProcessInstanceStatus.REJECT]: '/static/my-icons/bpm/bpm-reject.svg', // 审批不通过
|
||||
[BpmProcessInstanceStatus.CANCEL]: '/static/my-icons/bpm/bpm-cancel.svg', // 已取消
|
||||
}
|
||||
return iconMap[status ?? 1]
|
||||
}
|
||||
|
||||
/** 加载流程实例 */
|
||||
async function loadProcessInstance() {
|
||||
const data = await getApprovalDetail({
|
||||
processInstanceId: props.id,
|
||||
taskId: props.taskId,
|
||||
})
|
||||
if (!data || !data.processInstance) {
|
||||
toast.show('查询不到审批详情信息')
|
||||
return
|
||||
}
|
||||
processInstance.value = data.processInstance
|
||||
processDefinition.value = data.processDefinition
|
||||
// 获取审批节点,显示 Timeline 的数据
|
||||
activityNodes.value = data.activityNodes
|
||||
|
||||
operationButtonRef.value?.init(data.processInstance, data.todoTask)
|
||||
}
|
||||
|
||||
/** 加载任务列表 */
|
||||
async function loadTasks() {
|
||||
tasks.value = await getTaskListByProcessInstanceId(props.id)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
if (!props.id) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
await Promise.all([loadProcessInstance(), loadTasks()])
|
||||
})
|
||||
</script>
|
||||
126
src/pages-bpm/processInstance/detail/process-cancel/index.vue
Normal file
126
src/pages-bpm/processInstance/detail/process-cancel/index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="取消流程"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 友情提醒 -->
|
||||
<view class="mb-24rpx border border-[#ffd591] rounded-16rpx bg-[#fff7e6] p-24rpx">
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<wd-icon name="warning" color="#faad14" size="32rpx" />
|
||||
<text class="ml-12rpx text-28rpx text-[#faad14] font-bold">友情提醒</text>
|
||||
</view>
|
||||
<text class="text-26rpx text-[#666]">取消后,该审批流程将自动结束。</text>
|
||||
</view>
|
||||
|
||||
<!-- 取消理由 -->
|
||||
<wd-textarea
|
||||
v-model="formData.cancelReason"
|
||||
prop="cancelReason"
|
||||
label="取消理由:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入取消理由"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确认取消
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { cancelProcessInstanceByStartUser } from '@/api/bpm/processInstance'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId?: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const taskId = computed(() => props.taskId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
cancelReason: '',
|
||||
})
|
||||
const formRules = {
|
||||
cancelReason: [
|
||||
{ required: true, message: '取消理由不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
const backUrl = taskId.value
|
||||
? `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`
|
||||
: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`
|
||||
navigateBackPlus(backUrl)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await cancelProcessInstanceByStartUser(
|
||||
processInstanceId.value,
|
||||
formData.cancelReason,
|
||||
)
|
||||
toast.success('流程取消成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
143
src/pages-bpm/processInstance/detail/reassign/index.vue
Normal file
143
src/pages-bpm/processInstance/detail/reassign/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="isDelegate ? '委派任务' : '转办任务'"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 用户选择 -->
|
||||
<UserPicker
|
||||
v-model="formData.userId"
|
||||
prop="userId"
|
||||
type="radio"
|
||||
:label="`${isDelegate ? '接收人' : '新审批人'}:`"
|
||||
:placeholder="`请选择${isDelegate ? '接收人' : '新审批人'}`"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isDelegate ? '委派' : '转办' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { delegateTask, transferTask } from '@/api/bpm/task'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
type: string // 'delegate' 或 'transfer'
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const operationType = computed(() => props.type || 'transfer') // 默认转办
|
||||
const isDelegate = computed(() => operationType.value === 'delegate')
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
userId: undefined as number | undefined,
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
userId: [
|
||||
{ required: true, message: `请选择${isDelegate.value ? '接收人' : '新审批人'}` },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
}
|
||||
if (isDelegate.value) {
|
||||
await delegateTask({
|
||||
...data,
|
||||
delegateUserId: String(formData.userId),
|
||||
})
|
||||
} else {
|
||||
await transferTask({
|
||||
...data,
|
||||
assigneeUserId: String(formData.userId),
|
||||
})
|
||||
}
|
||||
toast.success(`${isDelegate.value ? '委派' : '转办'}成功`)
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
140
src/pages-bpm/processInstance/detail/return/index.vue
Normal file
140
src/pages-bpm/processInstance/detail/return/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="退回任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 退回节点选择 -->
|
||||
<wd-picker
|
||||
v-model="formData.targetActivityId"
|
||||
label="退回节点:"
|
||||
prop="targetActivityId"
|
||||
:columns="activityOptions"
|
||||
value-key="taskDefinitionKey"
|
||||
label-key="name"
|
||||
placeholder="请选择退回节点"
|
||||
/>
|
||||
|
||||
<!-- 退回原因 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="退回原因:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入退回原因"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
退回
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getTaskListByReturn, returnTask } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const activityOptions = ref<any[]>([])
|
||||
const formData = reactive({
|
||||
targetActivityId: '',
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
targetActivityId: [
|
||||
{ required: true, message: '退回节点不能为空' },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '退回原因不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 获取可退回的节点列表 */
|
||||
async function loadReturnTaskList() {
|
||||
const result = await getTaskListByReturn(taskId.value)
|
||||
activityOptions.value = result
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await returnTask({
|
||||
id: taskId.value as string,
|
||||
targetTaskDefinitionKey: formData.targetActivityId,
|
||||
reason: formData.reason,
|
||||
})
|
||||
|
||||
toast.success('退回成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时获取可退回节点列表 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
loadReturnTaskList()
|
||||
})
|
||||
</script>
|
||||
222
src/pages-bpm/processInstance/manager/components/search-form.vue
Normal file
222
src/pages-bpm/processInstance/manager/components/search-form.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发起人
|
||||
</view>
|
||||
<UserPicker
|
||||
v-model="formData.startUserId"
|
||||
type="radio"
|
||||
placeholder="请选择发起人"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入流程名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view v-if="processDefinitionList.length > 0" class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
所属流程
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.processDefinitionId"
|
||||
:columns="processDefinitionList"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
label=""
|
||||
/>
|
||||
</view>
|
||||
<!-- 流程分类 -->
|
||||
<view v-if="categoryList.length > 0" class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程分类
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.category"
|
||||
:columns="categoryList"
|
||||
label-key="name"
|
||||
value-key="code"
|
||||
label=""
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发起时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { ProcessDefinition } from '@/api/bpm/definition'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { getCategorySimpleList } from '@/api/bpm/category'
|
||||
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
startUserId: undefined as number | undefined, // 发起人
|
||||
name: undefined as string | undefined, // 流程名称
|
||||
processDefinitionId: undefined as string | undefined, // 所属流程
|
||||
category: undefined as string | undefined, // 流程分类
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined], // 发起时间
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程实例'
|
||||
})
|
||||
|
||||
const categoryList = ref<Category[]>([])
|
||||
const processDefinitionList = ref<ProcessDefinition[]>([])
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 获取流程分类列表 */
|
||||
async function getCategoryList() {
|
||||
try {
|
||||
categoryList.value = await getCategorySimpleList()
|
||||
} catch (error) {
|
||||
console.error('获取流程分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取流程定义列表 */
|
||||
async function getProcessDefinitions() {
|
||||
try {
|
||||
processDefinitionList.value = await getProcessDefinitionList({ suspensionState: 1 })
|
||||
} catch (error) {
|
||||
console.error('获取流程定义失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.startUserId = undefined
|
||||
formData.name = undefined
|
||||
formData.processDefinitionId = undefined
|
||||
formData.category = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getCategoryList()
|
||||
getProcessDefinitions()
|
||||
})
|
||||
</script>
|
||||
231
src/pages-bpm/processInstance/manager/index.vue
Normal file
231
src/pages-bpm/processInstance/manager/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程实例列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="mr-16rpx flex-1">
|
||||
<view class="line-clamp-1 text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="mt-8rpx text-24rpx text-[#999]">
|
||||
{{ item.categoryName || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
<DictTag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-20rpx text-white">
|
||||
{{ item.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-28rpx text-[#333]">
|
||||
{{ item.startUser?.nickname || '-' }}
|
||||
</view>
|
||||
<view class="text-24rpx text-[#999]">
|
||||
{{ item.startUser?.deptName || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx rounded-8rpx bg-[#f7f8f9] p-16rpx">
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">发起时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.startTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.endTime" class="flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">结束时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.tasks && item.tasks.length > 0" class="mb-12rpx">
|
||||
<view class="mb-8rpx text-26rpx text-[#999]">
|
||||
当前审批任务
|
||||
</view>
|
||||
<view class="flex flex-wrap gap-8rpx">
|
||||
<wd-tag
|
||||
v-for="task in item.tasks"
|
||||
:key="task.id"
|
||||
type="primary"
|
||||
plain
|
||||
@click.stop="handleTaskDetail(item, task)"
|
||||
>
|
||||
{{ task.name }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.status === BpmProcessInstanceStatus.RUNNING"
|
||||
class="flex items-center justify-end border-t border-[#f0f0f0] -mt-8"
|
||||
>
|
||||
<wd-button size="small" type="error" plain @click.stop="handleCancel(item)">
|
||||
取消流程
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程实例" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import {
|
||||
cancelProcessInstanceByAdmin,
|
||||
getProcessInstanceManagerPage,
|
||||
} from '@/api/bpm/processInstance'
|
||||
import DictTag from '@/components/dict-tag/dict-tag.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
// 流程实例状态枚举
|
||||
const BpmProcessInstanceStatus = {
|
||||
RUNNING: 1, // 进行中
|
||||
APPROVE: 2, // 审批通过
|
||||
REJECT: 3, // 审批不通过
|
||||
CANCEL: 4, // 已取消
|
||||
}
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<(ProcessInstance & { tasks?: { id: string, name: string }[] })[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程实例列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessInstanceManagerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessInstance) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
/** 查看任务详情 */
|
||||
function handleTaskDetail(row: ProcessInstance, task: { id: string, name: string }) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${row.id}&taskId=${task.id}` })
|
||||
}
|
||||
|
||||
/** 取消流程实例 */
|
||||
function handleCancel(item: ProcessInstance) {
|
||||
uni.showModal({
|
||||
title: '取消流程',
|
||||
editable: true,
|
||||
placeholderText: '请输入取消原因',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
const reason = res.content?.trim()
|
||||
if (!reason) {
|
||||
toast.error('请输入取消原因')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await cancelProcessInstanceByAdmin(item.id, reason)
|
||||
toast.success('取消成功')
|
||||
// 刷新列表
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error('取消流程失败:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
128
src/pages-bpm/task/manager/components/search-form.vue
Normal file
128
src/pages-bpm/task/manager/components/search-form.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
任务名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined, // 任务名称
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined], // 创建时间
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索任务'
|
||||
})
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
166
src/pages-bpm/task/manager/index.vue
Normal file
166
src/pages-bpm/task/manager/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="任务管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="mr-16rpx flex-1">
|
||||
<view class="line-clamp-1 text-32rpx text-[#333] font-semibold">
|
||||
{{ item.processInstance?.name || '-' }}
|
||||
</view>
|
||||
<view class="mt-8rpx text-24rpx text-[#999]">
|
||||
当前任务:{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<DictTag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-20rpx text-white">
|
||||
{{ item.processInstance?.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-28rpx text-[#333]">
|
||||
发起人:{{ item.processInstance?.startUser?.nickname || '-' }}
|
||||
</view>
|
||||
<view class="text-24rpx text-[#999]">
|
||||
审批人:{{ item.assigneeUser?.nickname || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-8rpx bg-[#f7f8f9] p-16rpx">
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">任务开始时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.endTime" class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">任务结束时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.reason" class="flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">审批建议</text>
|
||||
<text class="line-clamp-1 ml-16rpx flex-1 text-right text-[#333]">{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无任务" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getTaskManagerPage } from '@/api/bpm/task'
|
||||
import DictTag from '@/components/dict-tag/dict-tag.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<Task[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询任务列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getTaskManagerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情(历史) */
|
||||
function handleDetail(item: Task) {
|
||||
if (item.processInstance?.id) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.processInstance.id}` })
|
||||
}
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
153
src/pages-bpm/user-group/components/search-form.vue
Normal file
153
src/pages-bpm/user-group/components/search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
组名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入组名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`组名:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索用户分组'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
155
src/pages-bpm/user-group/detail/index.vue
Normal file
155
src/pages-bpm/user-group/detail/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户分组详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="组名" :value="formData?.name" />
|
||||
<wd-cell title="描述" :value="formData?.description || '-'" />
|
||||
<wd-cell title="成员">
|
||||
<view class="flex flex-wrap gap-8rpx justify-end">
|
||||
<wd-tag
|
||||
v-for="userId in (formData?.userIds || [])"
|
||||
:key="userId"
|
||||
type="primary"
|
||||
plain
|
||||
>
|
||||
{{ getUserNickname(userId) }}
|
||||
</wd-tag>
|
||||
<text v-if="!formData?.userIds?.length">-</text>
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:user-group:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:user-group:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import type { SimpleUser } from '@/api/system/user'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteUserGroup, getUserGroup } from '@/api/bpm/user-group'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<UserGroup>()
|
||||
const deleting = ref(false)
|
||||
const userList = ref<SimpleUser[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/user-group/index')
|
||||
}
|
||||
|
||||
/** 获取用户昵称 */
|
||||
function getUserNickname(userId: number) {
|
||||
const user = userList.value.find(u => u.id === userId)
|
||||
return user?.nickname || userId
|
||||
}
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 加载用户分组详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getUserGroup(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑用户分组 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/user-group/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除用户分组 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该用户分组吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteUserGroup(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
151
src/pages-bpm/user-group/form/index.vue
Normal file
151
src/pages-bpm/user-group/form/index.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="组名"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入组名"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="描述"
|
||||
label-width="180rpx"
|
||||
prop="description"
|
||||
clearable
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
<UserPicker
|
||||
ref="userPickerRef"
|
||||
v-model="formData.userIds"
|
||||
label="成员"
|
||||
type="checkbox"
|
||||
placeholder="请选择成员"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createUserGroup, getUserGroup, updateUserGroup } from '@/api/bpm/user-group'
|
||||
import { UserPicker } from '@/components/system-select'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑用户分组' : '新增用户分组')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<UserGroup>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
userIds: [],
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '组名不能为空' }],
|
||||
userIds: [{ required: true, message: '成员不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
const userPickerRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/user-group/index')
|
||||
}
|
||||
|
||||
/** 加载用户分组详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getUserGroup(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateUserGroup(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createUserGroup(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
190
src/pages-bpm/user-group/index.vue
Normal file
190
src/pages-bpm/user-group/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户分组管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 用户分组列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">描述:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.description || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">成员:</text>
|
||||
<view class="min-w-0 flex flex-1 flex-wrap gap-8rpx">
|
||||
<wd-tag
|
||||
v-for="userId in (item.userIds || []).slice(0, 3)"
|
||||
:key="userId"
|
||||
type="primary"
|
||||
plain
|
||||
>
|
||||
{{ getUserNickname(userId) }}
|
||||
</wd-tag>
|
||||
<wd-tag v-if="(item.userIds || []).length > 3" type="info" plain>
|
||||
+{{ (item.userIds || []).length - 3 }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无用户分组数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:user-group:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import type { SimpleUser } from '@/api/system/user'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getUserGroupPage } from '@/api/bpm/user-group'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<UserGroup[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const userList = ref<SimpleUser[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 获取用户昵称 */
|
||||
function getUserNickname(userId: number) {
|
||||
const user = userList.value.find(u => u.id === userId)
|
||||
return user?.nickname || userId
|
||||
}
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 查询用户分组列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getUserGroupPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增用户分组 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/user-group/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: UserGroup) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/user-group/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
24
src/pages-bpm/utils/index.ts
Normal file
24
src/pages-bpm/utils/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* PC 端业务表单路径 -> 移动端路径映射
|
||||
* - key: PC 端路径 (formCustomCreatePath / formCustomViewPath)
|
||||
* - value: 移动端路径
|
||||
* 原因是:目前暂时没有 mobile 端的自定义表单字段,所以暂时需要硬编码映射关系
|
||||
* 另外:需要在 src/pages-bpm/processInstance/detail/components/form-detail.vue 里,增加使用类似 LeaveDetail 的使用
|
||||
*/
|
||||
const PC_TO_MOBILE_PATH_MAP: Record<string, string> = {
|
||||
// OA 请假
|
||||
'/bpm/oa/leave/create': '/pages-bpm/oa/leave/create/index',
|
||||
'/bpm/oa/leave/detail': '/pages-bpm/oa/leave/detail/index',
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 PC 端路径获取移动端的跳转路径
|
||||
* @param pcPath PC 端的表单路径
|
||||
* @returns 移动端的跳转路径,如果没有映射则返回 undefined
|
||||
*/
|
||||
export function getMobileFormCustomPath(pcPath: string | undefined): string | undefined {
|
||||
if (!pcPath) {
|
||||
return undefined
|
||||
}
|
||||
return PC_TO_MOBILE_PATH_MAP[pcPath]
|
||||
}
|
||||
Reference in New Issue
Block a user