Files
shanxia2.0/miniprogram/components/bx-ui/bx-wf-config-page.vue
T

471 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="bx-page">
<view class="topbar" :style="safeAreaStyle">
<view class="topbar-left">
<text class="topbar-back" @click="onBack">←</text>
<text class="topbar-title">{{ pageTitle }}</text>
</view>
<view class="topbar-right" />
</view>
<view class="fab-add" @click="openAddDialog">
<view class="fab-add__bar fab-add__bar--h" />
<view class="fab-add__bar fab-add__bar--v" />
</view>
<view class="page-padded">
<view class="detail-grid" style="margin-top: 12px">
<view class="detail-item">
<view class="di-label">流程名称</view>
<view class="di-value">{{ wfDef.wfName || '-' }}</view>
</view>
<view class="detail-item">
<view class="di-label">状态</view>
<view class="di-value">
<text class="status-tag" :class="adminStatusTagClass(wfDef.status)">
{{ statusLabel(wfDef.status) }}
</text>
</view>
</view>
</view>
<view v-if="loading" class="page-body-loading">
<text>加载中...</text>
</view>
<template v-if="!loading">
<view v-if="nodes.length" class="section-title">审批节点</view>
<view v-if="nodes.length" class="flow-card">
<view
v-for="(node, index) in nodes"
:key="node.id ?? index"
class="flow-step"
>
<view class="flow-avatar av-done">{{ index + 1 }}</view>
<view class="flow-info">
<view class="flow-info__head">
<text class="flow-name">{{ node.nodeName }}</text>
<text
class="status-tag"
:class="
node.approverId || node.approver_id ? 's-approved' : 's-pending'
"
>
{{ node.approverId || node.approver_id ? '已配置' : '未配置' }}
</text>
</view>
<view class="flow-role"
>审批人:{{ node.approverName ?? node.approver_name ?? '-' }}</view
>
<view class="flow-time">节点顺序:{{ node.nodeOrder }}</view>
<view class="op-actions" style="margin-top: 8px">
<button class="btn btn-sm btn-primary" @click="openEditDialog(node)">
编辑
</button>
<button
class="btn btn-sm btn-danger"
:disabled="!!loadingMap[`delete-${nodeKey(node)}`]"
@click="handleDelete(node)"
>
{{ loadingMap[`delete-${nodeKey(node)}`] ? '删除中...' : '删除' }}
</button>
<button
class="btn btn-sm btn-secondary"
:disabled="index === 0 || !!loadingMap[`up-${nodeKey(node)}`]"
@click="moveUp(node)"
>
{{ loadingMap[`up-${nodeKey(node)}`] ? '...' : '上移' }}
</button>
<button
class="btn btn-sm btn-secondary"
:disabled="
index === nodes.length - 1 || !!loadingMap[`down-${nodeKey(node)}`]
"
@click="moveDown(node)"
>
{{ loadingMap[`down-${nodeKey(node)}`] ? '...' : '下移' }}
</button>
</view>
</view>
</view>
</view>
<view v-if="!nodes.length" class="empty-state empty-state--fill">
<text>暂无流程节点,点击右下角新增</text>
</view>
</template>
</view>
<!-- 节点表单 -->
<view v-if="showForm" class="bx-sheet-mask" @click="showForm = false">
<view class="bx-sheet" @click.stop>
<view class="bx-sheet__title">{{ isEdit ? '编辑节点' : '新增节点' }}</view>
<view class="bx-sheet__body bx-sheet__body--form">
<view class="form-card" style="margin: 0; border: none; box-shadow: none">
<view class="field">
<text class="field-label">节点名称<span class="req">*</span></text>
<input
v-model="formData.nodeName"
class="form-input"
placeholder="请输入节点名称"
/>
</view>
<view class="field">
<text class="field-label">审批人</text>
<view class="picker-trigger" @click="openApproverPicker">
<text :class="approverDisplay ? 'value' : 'ph'">{{
approverDisplay || '请选择审批人'
}}</text>
<text class="arrow">›</text>
</view>
<view v-if="!userOptions.length && approverFallback" class="field-tip">
用户列表不可用时,可手动填写审批人 ID / 姓名
</view>
<view v-if="!userOptions.length && approverFallback" class="field" style="margin-top: 10px">
<input
v-model="formData.approverId"
class="form-input"
placeholder="审批人 ID(可选)"
/>
</view>
<view v-if="!userOptions.length && approverFallback" class="field">
<input
v-model="formData.approverName"
class="form-input"
placeholder="审批人姓名(可选)"
/>
</view>
</view>
</view>
</view>
<view class="bx-sheet__footer" style="display: flex; gap: 10px">
<button class="btn btn-secondary" style="flex: 1" @click="showForm = false">
取消
</button>
<button
class="btn btn-primary"
style="flex: 1"
:disabled="!!loadingMap.submit"
@click="handleSubmit"
>
{{ loadingMap.submit ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
<!-- 审批人选择 -->
<view v-if="showApproverPicker" class="bx-sheet-mask" @click="closeApproverPicker">
<view class="bx-sheet bx-sheet--tall" @click.stop>
<view class="bx-sheet__title">选择审批人</view>
<view class="search-wrap" style="padding: 8px 16px 0">
<view class="search-box">
<input
v-model="approverKeyword"
class="search-input"
placeholder="搜索姓名或账号"
confirm-type="search"
/>
<view
v-if="approverKeyword.trim()"
class="search-box__clear"
@click="approverKeyword = ''"
>×</view
>
</view>
</view>
<scroll-view scroll-y class="bx-sheet__body">
<view v-if="!filteredUsers.length" class="bx-sheet__empty">无匹配人员</view>
<view
v-for="u in filteredUsers"
:key="u.id"
class="bx-sheet__item"
:class="{ 'is-selected': String(formData.approverId) === u.id }"
@click="onApproverPick(u)"
>
{{ u.displayName }}
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { workflowApi, fetchPortalUserOptions } from '@/api/bx'
import { statusLabel, adminStatusTagClass } from '@/utils/bxFormat'
import { PAGE_PATHS } from '@/config/api'
import { getSafeAreaStyle } from '@/utils/safeArea'
const safeAreaStyle = getSafeAreaStyle()
const props = defineProps({
bizType: { type: String, required: true },
pageTitle: { type: String, required: true }
})
const wfDef = ref({})
const nodes = computed(() => wfDef.value.nodes || [])
const loading = ref(false)
const showForm = ref(false)
const showApproverPicker = ref(false)
const isEdit = ref(false)
const loadingMap = reactive({})
const approverKeyword = ref('')
const approverFallback = ref(false)
const userOptions = ref([])
const filteredUsers = computed(() => {
const kw = approverKeyword.value.trim().toLowerCase()
if (!kw) return userOptions.value
return userOptions.value.filter((u) => {
const name = (u.realName || '').toLowerCase()
const account = (u.account || '').toLowerCase()
const display = (u.displayName || '').toLowerCase()
return name.includes(kw) || account.includes(kw) || display.includes(kw)
})
})
const formData = reactive({
editingNodeId: null,
nodeName: '',
approverId: null,
approverName: ''
})
const approverDisplay = computed(() => {
if (formData.approverId == null || formData.approverId === '') return ''
return (
userOptions.value.find((u) => u.id === String(formData.approverId))?.displayName ??
formData.approverName
)
})
function nodeKey(node) {
return String(node.id ?? node.nodeOrder ?? node.nodeName ?? Date.now())
}
async function loadUsers() {
try {
const data = await fetchPortalUserOptions()
userOptions.value = (Array.isArray(data) ? data : []).map((u) => {
const name = u.label || u.nickName || u.realName || ''
const account = u.userNo || u.username || ''
return {
id: String(u.value ?? u.id ?? ''),
realName: name,
account,
displayName: account ? `${name}(${account})` : name
}
}).filter((u) => u.id)
approverFallback.value = userOptions.value.length === 0
} catch (e) {
userOptions.value = []
approverFallback.value = true
}
}
async function loadWfDef() {
loading.value = true
try {
const res = await workflowApi.getByBizType(props.bizType)
wfDef.value = res.data || res || {}
} catch (e) {
wfDef.value = {}
} finally {
loading.value = false
}
}
function buildNodePayload(list) {
return list.map((n, i) => ({
id: n.id != null ? String(n.id) : undefined,
actionType: n.actionType ?? n.action_type ?? 'approve',
nodeName: n.nodeName,
nodeOrder: i + 1,
approverId: n.approverId ?? n.approver_id ?? null,
approverName: n.approverName ?? n.approver_name ?? ''
}))
}
function openAddDialog() {
if (!wfDef.value?.id) {
uni.showToast({ title: '工作流未初始化,无法保存节点', icon: 'none' })
return
}
isEdit.value = false
Object.assign(formData, {
editingNodeId: null,
nodeName: '',
approverId: null,
approverName: ''
})
showForm.value = true
}
function openEditDialog(node) {
isEdit.value = true
const aid = node.approverId ?? node.approver_id ?? null
Object.assign(formData, {
editingNodeId: node.id != null ? String(node.id) : null,
nodeName: node.nodeName ?? '',
approverId: aid != null && aid !== '' ? String(aid) : null,
approverName: node.approverName ?? node.approver_name ?? ''
})
showForm.value = true
}
function openApproverPicker() {
if (!userOptions.value.length) {
if (approverFallback.value) {
uni.showToast({ title: '请手动填写审批人', icon: 'none' })
return
}
uni.showToast({ title: '暂无人员', icon: 'none' })
return
}
approverKeyword.value = ''
showApproverPicker.value = true
}
function closeApproverPicker() {
showApproverPicker.value = false
approverKeyword.value = ''
}
function onApproverPick(u) {
formData.approverId = u.id
formData.approverName = (u.displayName || '').split('(')[0]
closeApproverPicker()
}
function validateForm() {
if (!String(formData.nodeName ?? '').trim()) {
uni.showToast({ title: '请输入节点名称', icon: 'none' })
return false
}
return true
}
async function handleSubmit() {
if (!validateForm()) return
if (!wfDef.value?.id) {
uni.showToast({ title: '工作流未初始化', icon: 'none' })
return
}
loadingMap.submit = true
try {
if (isEdit.value && formData.editingNodeId != null) {
const newNodes = nodes.value.map((n) =>
String(n.id) === formData.editingNodeId
? {
...n,
nodeName: formData.nodeName,
approverId: formData.approverId,
approverName: formData.approverName
}
: n
)
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
uni.showToast({ title: '编辑成功', icon: 'success' })
} else {
const newNodes = [
...nodes.value,
{
nodeName: formData.nodeName,
approverId: formData.approverId,
approverName: formData.approverName
}
]
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
uni.showToast({ title: '新增成功', icon: 'success' })
}
showForm.value = false
await loadWfDef()
} catch (e) {
// toasted
} finally {
loadingMap.submit = false
}
}
function handleDelete(node) {
const key = `delete-${nodeKey(node)}`
uni.showModal({
title: '删除确认',
content: `确定要删除节点「${node.nodeName}」吗?`,
confirmText: '确定删除',
confirmColor: '#ef4444',
success: async (r) => {
if (!r.confirm) return
loadingMap[key] = true
try {
const newNodes = nodes.value.filter((n) => n.id !== node.id)
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
uni.showToast({ title: '删除成功', icon: 'success' })
await loadWfDef()
} catch (e) {
// toasted
} finally {
loadingMap[key] = false
}
}
})
}
async function moveUp(node) {
const idx = nodes.value.findIndex((n) => n.id === node.id)
if (idx <= 0) return
const arr = [...nodes.value]
const temp = arr[idx]
arr[idx] = arr[idx - 1]
arr[idx - 1] = temp
const key = `up-${nodeKey(node)}`
try {
loadingMap[key] = true
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
uni.showToast({ title: '上移成功', icon: 'success' })
await loadWfDef()
} catch (e) {
// toasted
} finally {
loadingMap[key] = false
}
}
async function moveDown(node) {
const arr = [...nodes.value]
const idx = arr.findIndex((n) => n.id === node.id)
if (idx < 0 || idx >= arr.length - 1) return
const temp = arr[idx]
arr[idx] = arr[idx + 1]
arr[idx + 1] = temp
const key = `down-${nodeKey(node)}`
try {
loadingMap[key] = true
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
uni.showToast({ title: '下移成功', icon: 'success' })
await loadWfDef()
} catch (e) {
// toasted
} finally {
loadingMap[key] = false
}
}
function onBack() {
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
}
onMounted(() => {
loadWfDef()
loadUsers()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
</style>