| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- <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) => ({
- 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>
|