Files
jnpf_app/pages/apply/dynamicModelList/form.vue

2396 lines
61 KiB
Vue
Raw Normal View History

2026-01-19 17:34:15 +08:00
<template>
<view class="dynamicModel-form-v jnpf-wrap jnpf-wrap-form" v-if="showPage">
<JnpfParser :formConf="formConf" ref="dynamicForm" @submit="sumbitForm" :key="key" v-if="!loading" />
<!-- 固定的底部审批记录只显示第一条 -->
<view
v-if="hasFixedApproval && showFixedApproval"
class="fixed-approval-bar"
:style="{ bottom: bottomOffset + 'rpx' }"
>
<view class="fixed-approval-content" @tap="scrollToApproval">
<!-- 左侧状态点 -->
<view class="fixed-status-icon" :class="getStatusClass(firstApproval?.result)"></view>
<!-- 中间审批信息 -->
<view class="fixed-approval-info">
<view class="fixed-approval-header">
<text class="fixed-task-name">任务{{ firstApproval?.name || '' }}</text>
<text class="status-tag" :class="getStatusTagClass(firstApproval.result)">
{{ getStatusText(firstApproval.result) }}
<text class="status-time">{{ formatTime(firstApproval.createTime) }}</text>
</text>
</view>
<view class="fixed-approval-user">
<text class="fixed-info-label">审批人</text>
<text class="fixed-user-name">{{ firstApproval?.assigneeUser?.nickname || '' }}</text>
<text class="fixed-dept-tag">{{ firstApproval?.assigneeUser?.deptName || '' }}</text>
</view>
<view class="reason-box" v-if="firstApproval.reason">
{{ firstApproval.reason }}
</view>
</view>
<!-- 右侧向下箭头按钮 -->
<view class="fixed-scroll-btn" @tap.stop="scrollToApproval">
<text class="fixed-btn-text">更多</text>
</view>
</view>
</view>
<!-- 完整的审批记录区域 -->
<view
v-if="approvalData.length && isApproval"
id="approval-record-all"
class="full-approval-section"
:class="{ 'has-fixed-bar': hasFixedApproval && showFixedApproval }"
>
<view class="approval-title">审批记录</view>
<view class="approval-list">
<view
class="approval-item"
v-for="(item, index) in approvalData"
:key="item.id"
>
<!-- 左侧状态线 + 状态点 -->
<view class="approval-line-container">
<!-- 状态点 -->
<view class="status-icon" :class="getStatusClass(item.result)"></view>
<!-- 竖线最后一条隐藏 -->
<view class="approval-line" v-if="index < approvalData.length - 1"></view>
</view>
2026-01-20 18:07:35 +08:00
<!-- 审批内容 -->
<view class="approval-content">
<!-- 任务名称 + 状态标签 -->
<view class="task-header">
<text class="task-name">任务{{ item.name }}</text>
<view class="status-tag" :class="getStatusTagClass(item.result)">
{{ getStatusText(item.result) }}
<text class="status-time">{{ formatTime(item.createTime) }}</text>
</view>
</view>
<!-- 审批人信息 -->
<view class="info-row">
<text class="info-label">审批人</text>
<text class="info-value">{{ item.assigneeUser?.nickname || '' }}</text>
<text class="info-value-tag">{{ item.assigneeUser?.deptName || '' }}</text>
<view class="pending-tag" v-if="item.result === 1">待审批</view>
</view>
<!-- 审批意见有则显示 -->
<view class="reason-box" v-if="item.reason">
{{ item.reason }}
</view>
</view>
</view>
</view>
2026-01-19 17:34:15 +08:00
</view>
<!-- 底部操作按钮区域 -->
<view v-if="!loading && isShowBtn" class="buttom-actions" id="bottom-actions">
2026-01-20 18:07:35 +08:00
<u-button v-if="(!dataForm.approveStatus && dataForm.id) || dataForm.approveStatus == 3" class="buttom-btn launch-flow-btn" type="primary" @click.stop="handleLaunchFlow" :disabled="idDsabled" :loading="btnLoading">
发起流程
2026-01-19 17:34:15 +08:00
</u-button>
2026-01-25 20:22:58 +08:00
<u-button v-if="!config.current && !dataForm.approveStatus" class="buttom-btn" type="primary" @click.stop="submit" :disabled="idDsabled" :loading="btnLoading">
2026-01-20 18:07:35 +08:00
保存
</u-button>
2026-01-25 20:22:58 +08:00
<u-button v-if="!config.current && (dataForm.approveStatus == 1 || dataForm.approveStatus == 2)" class="buttom-btn" type="primary" @click.stop="goProcess">
查看流程
</u-button>
2026-01-27 09:00:58 +08:00
<u-button v-if="config.current == 1 && dataForm.approveStatus && dataForm.approveStatus !==0" class="buttom-btn" type="error" @click.stop="handleCancle">
取消
</u-button>
2026-01-20 18:07:35 +08:00
<u-button v-if="config.current == 2" class="buttom-btn" type="error" @click.stop="handleFail" :disabled="idDsabled" :loading="btnLoading">
不通过
</u-button>
<u-button v-if="config.current == 2" class="buttom-btn launch-flow-btn" type="primary" @click.stop="handleThrough" :disabled="idDsabled" :loading="btnLoading">
通过
</u-button>
<view class="more-btn" @click.stop="toggleMoreMenu" v-if="dataForm.id && moreMenuList.length && !idDsabled || config.current == 2">
2026-01-19 17:34:15 +08:00
<u-icon name="more-dot-fill" size="34"></u-icon>
<text class="more-text">{{$t('common.moreText')}}</text>
<view class="more-menu" v-show="showMoreMenu" @click.stop>
<view
class="menu-item"
v-for="(item, index) in moreMenuList"
:key="index"
:style="{color: item.color || '#333'}"
>
<view @click.stop="handleMoreMenuClick(item)">
<text class="menu-label">{{item.label}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 流程选择弹框 -->
<u-popup v-model="showProcessDialog" mode="center" :round="10" >
<view class="process-dialog">
<view class="dialog-header">
<text class="dialog-title">选择流程表单</text>
<u-icon name="close" size="24" color="#999" @click="closeProcessDialog" class="close-icon"></u-icon>
</view>
<view class="process-list" v-if="!loadingProcess">
2026-01-22 11:01:19 +08:00
<u-radio-group v-model="selectedProcess" @change="selectProcess">
2026-01-19 17:34:15 +08:00
<view
class="process-item"
v-for="(item, index) in processList"
:key="item.id"
2026-01-22 11:01:19 +08:00
:class="{'selected-item': selectedProcess && selectedProcess === item.id}"
2026-01-19 17:34:15 +08:00
>
<view class="process-item-left">
<u-radio :name="item.id">{{ item.name }}</u-radio>
</view>
</view>
</u-radio-group>
<view class="empty-process" v-if="processList.length === 0">
<u-icon name="list" size="60" color="#c0c4cc"></u-icon>
<text class="empty-text">暂无可用流程</text>
</view>
</view>
<view class="loading-process" v-else>
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载中...</text>
</view>
<view class="dialog-footer" v-if="processList.length > 0">
<u-button class="footer-btn cancel-btn" @click="closeProcessDialog">取消</u-button>
<u-button class="footer-btn submit-btn" type="primary" :disabled="!selectedProcess" @click="launchSelectedProcess">提交</u-button>
</view>
</view>
</u-popup>
<!-- 审批人弹框 -->
<u-popup v-model="showApproverDialog" mode="center" :round="10" @close="closeApproverDialog">
<view class="approver-dialog">
<view class="dialog-header">
<text class="dialog-title">发起人选择审批人</text>
<u-icon name="close" size="24" color="#999" @click="closeApproverDialog" class="close-icon"></u-icon>
</view>
<view class="approver-content" v-if="!loadingApprover && approverList.length > 0">
<view class="approver-node" v-for="(node, index) in approverList" :key="node.id">
<view class="node-title">
<text class="node-label">审批节点</text>
<text class="node-name">{{ node.taskDefinitionName || '未命名节点' }}</text>
</view>
<view class="candidate-section">
<view class="section-title">
<text class="section-label">候选人<text style="color:red">*</text></text>
<view>
<user-select
v-model="selectedCandidates"
:placeholder="'请选择审批人'"
:multiple="false"
:selectType="type"
@change="(value, data) => handleUserChange(node, value, data,index)"
style="margin-top: 10rpx;"
/>
</view>
</view>
</view>
</view>
</view>
<view class="loading-approver" v-else-if="loadingApprover">
<u-loading mode="circle" size="40"></u-loading>
<text class="loading-text">加载中...</text>
</view>
<view class="empty-approver" v-else>
<u-icon name="user" size="60" color="#c0c4cc"></u-icon>
<text class="empty-text">暂无审批人</text>
</view>
<view class="dialog-footer" v-if="approverList.length > 0">
<u-button class="footer-btn confirm-btn" type="primary" :disabled="!isAllCandidatesSelected" @click="confirmLaunchFlow">确定</u-button>
</view>
</view>
</u-popup>
<!-- 审批意见输入弹窗 -->
<u-popup
v-model="showApprovalReasonDialog"
mode="center"
:round="10"
:closeOnClickOverlay="false"
>
<view class="approval-reason-dialog">
<!-- 弹窗标题 -->
<view class="dialog-header">
<text class="dialog-title">{{ approvalType === 'through' ? '审批通过' : '审批不通过' }}</text>
<u-icon name="close" size="24" color="#999" @click="showApprovalReasonDialog = false" class="close-icon"></u-icon>
</view>
<!-- 输入框区域 -->
<view class="dialog-content">
2026-01-27 09:00:58 +08:00
<view class="input-label">
审批意见
<text style="color: red;">*</text>
</view>
2026-01-19 17:34:15 +08:00
<u-input
v-model="approvalReason"
type="textarea"
placeholder="请输入审批意见(选填)"
:rows="4"
border="surround"
class="reason-input"
/>
</view>
<!-- 按钮区域 -->
<view class="dialog-footer">
<u-button class="footer-btn cancel-btn" @click="showApprovalReasonDialog = false">取消</u-button>
<u-button
class="footer-btn submit-btn"
type="primary"
@click="submitApprovalResult"
:loading="approvalSubmitting"
>
确认提交
</u-button>
</view>
</view>
</u-popup>
2026-01-20 18:07:35 +08:00
<!-- 抄送规则弹窗 -->
<u-popup v-model="showCopyDialog" mode="center" :round="10">
<view class="copy-dialog">
<view class="dialog-header">
<text class="dialog-title">修改任务规则</text>
<u-icon name="close" size="24" color="#999" @click="showCopyDialog = false" class="close-icon"></u-icon>
</view>
<view class="dialog-content">
<view class="form-item-user required">
2026-01-27 09:00:58 +08:00
<view>
<text class="label">指定用户</text>
<text style="color: red;">*</text>
</view>
2026-01-20 18:07:35 +08:00
<view>
<user-select
v-model="copyForm.options"
placeholder="请选择用户"
:multiple="true"
:selectType="type"
/>
</view>
</view>
<view class="form-item required">
<text class="label">抄送原因</text>
<u-input
v-model="copyForm.reason"
type="textarea"
placeholder="请输入抄送原因"
:rows="3"
border="surround"
/>
</view>
</view>
<view class="dialog-footer">
<u-button class="footer-btn cancel-btn" @click="showCopyDialog = false">取消</u-button>
<u-button class="footer-btn submit-btn" type="primary" @click="submitCopyRule" :loading="copySubmitting">确定</u-button>
</view>
</view>
</u-popup>
2026-01-27 09:00:58 +08:00
<!-- 取消原因弹框 -->
<u-popup v-model="showCacleDialog" mode="center" :round="10">
<view class="copy-dialog">
<view class="dialog-header">
<text class="dialog-title">取消流程</text>
<u-icon name="close" size="24" color="#999" @click="showCacleDialog = false" class="close-icon"></u-icon>
</view>
<view class="dialog-content">
<view class="form-item required">
<text class="label">取消原因</text>
<u-input
v-model="cacleReason"
type="textarea"
placeholder="请输入取消原因"
:rows="3"
border="surround"
/>
</view>
</view>
<view class="dialog-footer">
<u-button class="footer-btn cancel-btn" @click="showCacleDialog = false">取消</u-button>
<u-button class="footer-btn submit-btn" type="primary" @click="submitCancel" :loading="cacleSubmitting">确定</u-button>
</view>
</view>
</u-popup>
2026-01-19 17:34:15 +08:00
</view>
</template>
<script>
import CustomButton from '@/components/CustomButton'
import {
getDesForm,
createModel,
updateModel,
getModelInfo,
getdbformlist,
getAssignList,
getListCreate,
deteleModel,
launchFlow,
getByProcess
} from '@/api/apply/visualDev'
import UserSelect from '@/components/Jnpf/CandidateSelect'
import {getOrganizeSelector} from '@/api/common.js'
2026-01-19 17:34:15 +08:00
export default {
components: {
CustomButton,
UserSelect
},
data() {
return {
webType: '',
type: 'all',
showPage: false,
btnLoading: false,
loading: true,
isPreview: '0',
modelId: '',
formConf: {},
formData: {},
dataForm: {
id: '',
data: ''
},
btnType: '',
formPermissionList: {},
formList: [],
key: +new Date(),
config: {},
clickType: 'submit',
prevDis: false,
nextDis: false,
index: 0,
userInfo: {},
isAdd: false,
showMoreMenu: false,
moreMenuList: [],
createData: {},
2026-01-19 17:34:15 +08:00
deptList: [],
showProcessDialog: false,
processList: [],
selectedProcess: '',
loadingProcess: false,
showApproverDialog: false,
approverList: [],
selectedApprover: null,
loadingApprover: false,
currentProcessId: '',
selectedCandidates: {},
approvalData: [],
isProcess: false,
showApprovalReasonDialog: false,
approvalType: '',
approvalReason: '',
approvalSubmitting: false,
2026-01-20 18:07:35 +08:00
showCopyDialog: false,
copySubmitting: false,
copyForm: {
processInstanceKey: '',
processInstanceName: '',
options: {},
reason: ''
2026-01-27 09:00:58 +08:00
},
showCacleDialog: false,
cacleReason: '',
cacleSubmitting: false,
// 新增:固定审批记录相关
showFixedApproval: true,
isScrollingToApproval: false,
scrollTimer: null,
// bottomOffset: 100,
scrollHandler: null, // 用于存储滚动监听函数
2026-01-19 17:34:15 +08:00
}
},
computed: {
getOkText() {
2026-01-20 18:07:35 +08:00
return this.$t('common.saveText');
2026-01-19 17:34:15 +08:00
},
2026-01-20 18:07:35 +08:00
getProcessText() {
return '发起流程'
2026-01-19 17:34:15 +08:00
},
isAllCandidatesSelected() {
const result = this.approverList.every(item=>{
const {taskDefinitionKey} = item
return !!item[taskDefinitionKey]
})
return result
},
idDsabled() {
const {approveStatus} = this.dataForm
return approveStatus == 2 || approveStatus == 4
2026-01-22 11:01:19 +08:00
},
isApproval(){
const {current} = this.config
2026-01-25 20:22:58 +08:00
return !!current || this.isProcess
2026-01-27 09:00:58 +08:00
},
// 是否显示底部按钮
isShowBtn(){
2026-01-27 15:42:20 +08:00
const {approveStatus} = this.dataForm
2026-01-27 09:00:58 +08:00
const {config,isProcess} = this
2026-01-27 15:42:20 +08:00
if(config.current == 1 && approveStatus == 0) return false
if(config.current == 2) return true
if(config.current == 3 || config.current == 4) return false
2026-01-27 09:00:58 +08:00
if(!isProcess) return true
2026-01-27 15:42:20 +08:00
if(isProcess) return false
2026-01-27 09:00:58 +08:00
return false
},
bottomOffset(){
if(!this.loading && this.isShowBtn) return 85
return 0
},
hasFixedApproval() {
console.log(this.approvalData.length > 0 && this.isApproval,'判断---')
return this.approvalData.length > 0 && this.isApproval
},
firstApproval() {
console.log(this.approvalData[0],'数据--')
return this.approvalData.length > 0 ? this.approvalData[0] : null
},
2026-01-19 17:34:15 +08:00
},
onLoad(option) {
this.init(option)
},
onReady() {
this.calculateBottomOffset()
this.bindPageScroll()
},
onUnload() {
// 清除滚动监听
if (this.scrollHandler) {
uni.$off('scroll', this.scrollHandler)
}
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
},
onPageScroll(e) {
console.log('页面滚动事件触发:', e.scrollTop)
// 如果正在滚动到审批区域,不处理
if (this.isScrollingToApproval) return
// 防抖处理
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
this.scrollTimer = setTimeout(() => {
this.checkApprovalVisibility()
}, 50)
},
2026-01-19 17:34:15 +08:00
methods: {
init(option) {
const parseConfig = (rawConfig) => {
try {
2026-01-29 11:53:50 +08:00
return JSON.parse(rawConfig) || {}
2026-01-19 17:34:15 +08:00
} catch (error) {
return {}
}
}
const config = parseConfig(option.config)
const {
index,
currentMenu,
btnType = '',
modelId,
isPreview = '0',
2026-01-20 18:07:35 +08:00
id = '',
2026-01-19 17:34:15 +08:00
} = config
const formPermissionList =[]
Object.assign(this, {
userInfo: uni.getStorageSync('userInfo') || {},
config,
index,
formPermissionList,
formList: formPermissionList?.formList || [],
btnType,
modelId,
isPreview,
dataForm: {
id
}
})
if (id) {
this.initMoreMenuList()
}
2026-01-22 11:01:19 +08:00
const getNavigationTitle = () =>{
let titleName = ''
2026-01-25 20:22:58 +08:00
if(this.config.current || this.isProcess){
2026-01-22 11:01:19 +08:00
titleName = this.config.name + '详情'
}else {
2026-01-29 14:28:28 +08:00
const text = this.dataForm.id ? this.$t('common.editText') : this.$t('common.addText')
titleName = text + this.config.name
2026-01-22 11:01:19 +08:00
}
return titleName
}
2026-01-19 17:34:15 +08:00
uni.setNavigationBarTitle({
2026-01-22 11:01:19 +08:00
title: getNavigationTitle()
2026-01-19 17:34:15 +08:00
})
this.getDesForm()
},
// 计算底部按钮高度
calculateBottomOffset() {
setTimeout(() => {
const query = uni.createSelectorQuery().in(this)
query.select('#bottom-actions').boundingClientRect(data => {
if (data) {
this.bottomOffset = data.height + 20
}
}).exec()
}, 300)
},
// 监听页面滚动
bindPageScroll() {
// 初始检查一次
this.checkApprovalVisibility()
},
// 检查审批记录是否可见
checkApprovalVisibility() {
if (!this.hasFixedApproval || this.isScrollingToApproval) return
const query = uni.createSelectorQuery().in(this)
query.select('#approval-record-all').boundingClientRect(data => {
if (!data) {
this.showFixedApproval = true
return
}
// 获取窗口高度
const windowHeight = uni.getSystemInfoSync().windowHeight || 667
// 计算审批记录区域底部距离屏幕顶部的距离
const approvalBottom = data.top + data.height
console.log(approvalBottom,'approvalBottom--')
// 判断:如果审批记录区域底部在屏幕内(能看到最下方的审批记录),则隐藏固定底栏
// 这里使用一个阈值比如底部距离屏幕顶部小于窗口高度的90%(即能看到大部分)
const isBottomVisible = approvalBottom <= windowHeight * 0.9
// 或者判断整个区域是否在视口内
const isFullyVisible = data.top >= 0 && data.bottom <= windowHeight
// 只要能看到最下方的审批记录(底部在视口内),就隐藏固定底栏
// 看不到最下方的审批记录时,显示固定底栏
this.showFixedApproval = !isBottomVisible && !isFullyVisible
console.log('审批记录可见性检查:', {
top: data.top,
bottom: data.bottom,
approvalBottom,
windowHeight,
isBottomVisible,
isFullyVisible,
showFixedApproval: this.showFixedApproval
})
}).exec()
},
// 滚动到审批记录区域
scrollToApproval() {
this.isScrollingToApproval = true
this.showFixedApproval = false
// 先滚动到审批记录区域
uni.pageScrollTo({
selector: '#approval-record-all',
duration: 300,
success: () => {
// 滚动完成后,延迟检查可见性
setTimeout(() => {
this.isScrollingToApproval = false
this.checkApprovalVisibility()
}, 500)
},
fail: () => {
this.isScrollingToApproval = false
this.scrollToApprovalByPosition()
}
})
},
// 备选滚动方法
scrollToApprovalByPosition() {
const query = uni.createSelectorQuery().in(this)
query.select('#approval-record-all').boundingClientRect(data => {
if (data) {
uni.pageScrollTo({
scrollTop: data.top - 50,
duration: 300,
success: () => {
setTimeout(() => {
this.isScrollingToApproval = false
this.checkApprovalVisibility()
}, 500)
}
})
}
}).exec()
},
// 获取状态文本
getStatusText(result) {
switch(result) {
case 1: return '审批中'
case 2: return '已提交'
case 3: return '不通过'
default: return '未知'
}
},
handleUserChange(node, value, data,index) {
2026-01-19 17:34:15 +08:00
const name = this.approverList[index]['taskDefinitionKey']
this.approverList[index] = {
2026-01-19 17:34:15 +08:00
...this.approverList[index],
[name] : value
}
2026-01-20 18:07:35 +08:00
},
handleRuleUserChange(node, value, data){
2026-01-19 17:34:15 +08:00
},
confirmLaunchFlow() {
const result = this.approverList.every(item=>{
const {taskDefinitionKey} = item
return !!item[taskDefinitionKey]
})
if(!result){
uni.showToast({
title: '请选择候选人',
icon: 'error'
})
return
}
const assignee = {}
this.approverList.forEach(item=>{
const key = item.taskDefinitionKey;
assignee[key] = item[key];
})
const params = {
processType: 0,
processDefinitionId: this.selectedProcess,
id: this.dataForm.id,
data: {
assignee,
dbFormId : this.modelId,
dataId: this.dataForm.id,
processDefinitionId: this.selectedProcess
}
}
2026-01-20 18:07:35 +08:00
this.getListCreateData(params)
},
// 请求日志
getListCreateData(params){
2026-01-19 17:34:15 +08:00
getListCreate(params,this.modelId).then(res=>{
if(res.code == 0) {
2026-01-25 20:22:58 +08:00
let name = '发起成功'
switch(this.approvalType){
case 'through':
name = '审批通过成功'
break;
case 'fail':
name = '审批不通过成功'
break;
2026-01-27 09:00:58 +08:00
case 'cancel':
name = '取消成功'
break;
2026-01-25 20:22:58 +08:00
}
2026-01-19 17:34:15 +08:00
uni.showToast({
2026-01-25 20:22:58 +08:00
title: name,
2026-01-19 17:34:15 +08:00
icon: 'success'
})
setTimeout(() => {
uni.$emit('refresh')
uni.navigateBack()
}, 1500)
}
})
},
initMoreMenuList() {
let menuList = []
2026-01-20 18:07:35 +08:00
console.log(this.dataForm.approveStatus,'approveStatus--')
if((!this.dataForm.approveStatus && this.dataForm.id) || this.dataForm.approveStatus == 3){
// 未审核 | 驳回
menuList.push({
2026-01-19 17:34:15 +08:00
label: '删除',
value: 'delete',
icon: 'icon-ym icon-ym-app-delete',
color: '#dd524d'
2026-01-20 18:07:35 +08:00
})
2026-01-19 17:34:15 +08:00
}
2026-01-20 18:07:35 +08:00
if(this.config.current == 2){
2026-01-19 17:34:15 +08:00
menuList.unshift({
2026-01-20 18:07:35 +08:00
label: '抄送',
value: 'send',
2026-01-19 17:34:15 +08:00
icon: 'icon-ym icon-ym-flow-launch',
2026-01-20 18:07:35 +08:00
color: '#409eff'
2026-01-19 17:34:15 +08:00
},
{
2026-01-20 18:07:35 +08:00
label: '回退',
value: 'reject',
2026-01-19 17:34:15 +08:00
icon: 'icon-ym icon-ym-flow-launch',
2026-01-20 18:07:35 +08:00
color: '#e6a23c'
2026-01-19 17:34:15 +08:00
})
}
this.moreMenuList = menuList
},
toggleMoreMenu() {
this.showMoreMenu = !this.showMoreMenu
if (this.showMoreMenu) {
setTimeout(() => {
uni.$once('clickOutside', () => {
this.showMoreMenu = false
})
}, 100)
}
},
handleMoreMenuClick(item) {
this.showMoreMenu = false
switch (item.value) {
case 'edit':
this.handleEdit()
break
2026-01-20 18:07:35 +08:00
case 'send':
this.handleSend()
break
case 'reject':
this.handleReject()
2026-01-19 17:34:15 +08:00
break
case 'delete':
this.handleDelete()
break
}
},
handleEdit() {
this.btnType = 'btn_edit'
this.initData()
uni.showToast({
title: '进入编辑状态',
icon: 'none'
})
},
handleLaunchFlow() {
this.getProcessList()
},
async getProcessList() {
try {
this.loadingProcess = true
const res = await getdbformlist(this.modelId)
if (res.code === 0 || res.code === 200) {
this.processList = res.data || []
2026-01-22 11:01:19 +08:00
if(this.processList.length === 1){
2026-01-27 09:00:58 +08:00
this.selectProcess(res.data[0].id)
2026-01-22 11:01:19 +08:00
}
2026-01-19 17:34:15 +08:00
if (this.processList.length === 0) {
uni.showToast({
title: '暂无可用流程',
icon: 'none'
})
return
}
this.showProcessDialog = true
} else {
uni.showToast({
title: res.msg || '获取流程列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取流程列表失败:', error)
uni.showToast({
title: '获取流程列表失败',
icon: 'none'
})
} finally {
this.loadingProcess = false
}
},
2026-01-20 18:07:35 +08:00
// 抄送
handleSend() {
this.showCopyDialog = true
},
// 抄送-提交
submitCopyRule(){
2026-01-27 09:00:58 +08:00
const {options,reason} = this.copyForm
if(!options.length || !reason){
const name = !options.length ? '指定用户不能为空' : '抄送原因不能为空'
uni.showToast({
title: name,
icon: 'none'
})
return
}
2026-01-20 18:07:35 +08:00
const {id,processInstance} = this.approvalData[0]
const params = {
processType: '4',
processDefinitionId:Object.values(this.copyForm.options),
processInstanceId: this.formData.processInstanceId,
id: this.dataForm.id,
data: {
startUserId: processInstance.startUserId,
taskId: id,
...this.copyForm,
options: Object.values(this.copyForm.options),
processInstanceKey:processInstance.id,
processInstanceName:processInstance.name
}
}
this.getListCreateData(params)
},
handleReject() {
const {id,processInstance} = this.approvalData[0]
const params = {
processType: '3',
2026-01-20 18:07:35 +08:00
processInstanceId: this.formData.processInstanceId,
id: this.dataForm.id,
data: {
taskId: id
}
}
this.getListCreateData(params)
},
2026-01-19 17:34:15 +08:00
selectProcess(item) {
2026-01-22 11:01:19 +08:00
console.log(item,'item----')
2026-01-19 17:34:15 +08:00
this.selectedProcess = item
},
async launchSelectedProcess() {
if (!this.selectedProcess) {
uni.showToast({
title: '请选择一个流程',
icon: 'none'
})
return
}
this.showProcessDialog = false
await this.getApproverList()
},
async getApproverList() {
try {
const res = await getAssignList(this.selectedProcess)
if (res.code === 0 || res.code === 200) {
this.approverList = res.data || []
let userSelectRules = res.data?.filter(rule => rule.type === 35)
if(userSelectRules?.length > 0){
this.loadingApprover = true
this.showApproverDialog = true
this.selectedCandidates = {}
}else{
const params = {
processType: 0,
processDefinitionId: this.selectedProcess,
id: this.dataForm.id,
data: {
dbFormId : this.modelId,
dataId: this.dataForm.id,
processDefinitionId: this.selectedProcess
}
}
2026-01-20 18:07:35 +08:00
this.getListCreateData(params)
2026-01-19 17:34:15 +08:00
}
} else {
uni.showToast({
title: res.msg || '获取审批人失败',
icon: 'none'
})
}
} catch (error) {
console.error('获取审批人失败:', error)
uni.showToast({
title: '获取审批人失败',
icon: 'none'
})
} finally {
this.loadingApprover = false
}
},
closeProcessDialog() {
this.showProcessDialog = false
this.selectedProcess = ''
},
closeApproverDialog() {
this.showApproverDialog = false
},
handleDelete() {
uni.showModal({
title: '警告',
content: '删除后数据无法恢复,确认删除吗?',
success: (res) => {
if (res.confirm) {
createModel(this.modelId,{
operateType: 3,
id: this.dataForm.id,
2026-01-29 16:27:15 +08:00
data: this.dataForm.id
2026-01-19 17:34:15 +08:00
}, this.modelId).then(res => {
uni.showToast({
title: res.msg || '删除成功',
icon: 'success',
complete: () => {
setTimeout(() => {
uni.$emit('refresh')
uni.navigateBack()
}, 1500)
}
})
}).catch(err => {
uni.showToast({
title: err.msg || '删除失败',
icon: 'none'
})
})
}
}
})
},
// 点击通过 - 打开审批意见弹窗
handleThrough() {
this.approvalType = 'through'
this.approvalReason = '' // 清空之前的意见
this.showApprovalReasonDialog = true
},
// 点击不通过 - 打开审批意见弹窗
handleFail() {
this.approvalType = 'fail'
this.approvalReason = '' // 清空之前的意见
this.showApprovalReasonDialog = true
},
2026-01-27 09:00:58 +08:00
// 取消
handleCancle() {
this.approvalType = 'cancel'
this.cacleReason = ''
this.showCacleDialog = true
},
// 取消弹框确认
submitCancel() {
if(!this.cacleReason){
uni.showToast({
title: '请输入取消原因',
icon: 'none'
})
return
}
try {
this.cacleSubmitting = true
const params = {
processType: 5,
processInstanceId: this.formData.processInstanceId, // 流程实例ID
id: this.formData.id,
data: {
dbFormId : this.modelId,
id: this.approvalData && this.approvalData[0].id, // 审批记录第一条id
reason: this.cacleReason || '',
}
}
//调用
this.getListCreateData(params)
} catch (err) {
console.error('取消失败:', err)
uni.showToast({
title: '取消失败,请重试',
icon: 'none'
})
} finally {
this.cacleSubmitting = false
}
},
2026-01-25 20:22:58 +08:00
// 查看流程
goProcess(){
this.isProcess = true
console.log(this.config,'config-')
uni.setNavigationBarTitle({
title: `${this.config.name}流程详情`
})
},
2026-01-19 17:34:15 +08:00
2026-01-20 18:07:35 +08:00
// 提交审批结果(通过/不通过)
2026-01-19 17:34:15 +08:00
async submitApprovalResult() {
2026-01-27 09:00:58 +08:00
if(!this.approvalReason){
uni.showToast({
title: '请填写审批意见',
icon: 'none'
})
return
}
2026-01-19 17:34:15 +08:00
try {
this.approvalSubmitting = true
const params = {
processType: this.approvalType === 'through' ? 1 : 2, // 1=通过2=不通过
processInstanceId: this.formData.processInstanceId, // 流程实例ID
id: this.formData.id,
data: {
dbFormId : this.modelId,
id: this.approvalData && this.approvalData[0].id, // 审批记录第一条id
reason: this.approvalReason || '', // 审批意见
}
}
2026-01-20 18:07:35 +08:00
//调用审批接口
this.getListCreateData(params)
2026-01-19 17:34:15 +08:00
} catch (err) {
console.error('提交审批结果失败:', err)
uni.showToast({
title: '提交失败,请重试',
icon: 'none'
})
} finally {
this.approvalSubmitting = false
}
},
2026-01-23 11:09:28 +08:00
// 获取布局和初始值
2026-01-19 17:34:15 +08:00
async getDesForm() {
try {
const res = await getDesForm(this.config.modelId);
if((res.code == 0 || res.code == 200) && res.data !== null){
const data = res.data ? JSON.parse(res.data) : {};
this.formConf = JSON.parse(data.formData);
console.log(this.formConf,'formConf--')
2026-01-23 11:09:28 +08:00
console.log(data,'data--111')
2026-01-19 17:34:15 +08:00
this.formConf.labelWidth = 160;
2026-01-23 11:09:28 +08:00
this.createData = data
2026-01-19 17:34:15 +08:00
await this.getDeptList();
await this.fillFormData(this.formConf, this.formData);
this.showPage = true;
await this.initData();
this.loading = false;
} else {
uni.showToast({
title: '暂无此页面',
icon: 'none',
complete: () => {
setTimeout(() => {
uni.navigateBack();
}, 1500)
}
})
}
} catch (error) {
console.error('获取表单配置失败:', error);
this.loading = false;
}
},
getDeptList() {
return new Promise((resolve) => {
getOrganizeSelector({type:'all'}).then(res => {
if (res.code == 0 && res.data) {
this.deptList = this.formatDeptData(res.data);
}
resolve();
}).catch((error) => {
console.error('获取部门列表失败:', error);
resolve();
})
})
},
formatDeptData(data) {
const result = [];
const formatItem = (item) => {
let deptId = item.depthd || item.id || item.deptId;
let deptName = item.depthName || item.deptName || item.fullName || item.name || item.orgNameTree;
deptId = String(deptId || '');
return {
...item,
deptId: deptId,
deptName: deptName ,
depthd: item.depthd,
depthName: item.depthName,
id: item.id
};
};
if (Array.isArray(data)) {
data.forEach(item => {
const formatted = formatItem(item);
if (formatted.deptId) {
result.push(formatted);
}
if (item.children && Array.isArray(item.children)) {
const children = this.formatDeptData(item.children);
result.push(...children);
}
});
}
return result;
},
async initData() {
try {
await this.$nextTick();
if (this.dataForm.id) {
const extra = {
modelId: this.modelId,
id: this.dataForm.id,
type: 1
};
uni.setStorageSync('dynamicModelExtra', extra);
const res = await getModelInfo(this.modelId, this.dataForm.id);
if(!!res.data.approveStatus&&(res.data.approveStatus !== 0 || res.data.approveStatus == 3)){
// 不是未发起和驳回的都不可编辑
this.disableAllFormFields();
}
this.dataForm = res.data;
if (!res.data) return;
2026-01-22 11:01:19 +08:00
const {fields = []} = this.formConf
const result = fields.reduce((acc,current)=>{
const key = current.__vModel__;
if(res.data.hasOwnProperty(key)){
acc[key] = res.data[key];
}else if(res.data.jeelowcode_subtable_data && res.data.jeelowcode_subtable_data.hasOwnProperty(key)){
acc[key] = res.data.jeelowcode_subtable_data[key];
}
return acc;
},{})
2026-01-29 15:32:10 +08:00
const formData = {
2026-01-19 17:34:15 +08:00
...res.data,
id: this.dataForm.id,
applyDepId: res.data.applyDepId ? String(res.data.applyDepId) : '',
applyDepName: res.data.applyDepName || '',
applyDepData: res.data.applyDepData || null,
2026-01-22 11:01:19 +08:00
...result
2026-01-19 17:34:15 +08:00
};
2026-01-29 16:27:15 +08:00
formData.applyDepId = res.data.applyDepId ? String(res.data.applyDepId) : '';
2026-01-29 15:32:10 +08:00
formData.sDate = res.data.sDate ? Number(new Date(res.data.sDate).getTime()) : null;
formData.eDate = res.data.eDate ? Number(new Date(res.data.eDate).getTime()) : null;
this.formData = {...formData}
2026-01-29 16:39:59 +08:00
await this.fillFormData(this.formConf, this.formData);
2026-01-19 17:34:15 +08:00
this.$nextTick(()=>{
if (this.$refs.dynamicForm && this.$refs.dynamicForm.setFormData) {
this.$refs.dynamicForm.setFormData('applyDepId', this.formData.applyDepId);
}
})
// 获取审批记录
await this.getApprovalData();
this.initMoreMenuList()
} else {
2026-01-23 11:09:28 +08:00
console.log(this.formData,'formData111')
2026-01-19 17:34:15 +08:00
const {user,deptInfoList} = this.userInfo
this.isAdd = true;
this.formData = {
...this.formData,
2026-01-23 11:09:28 +08:00
...this.createData,
2026-01-19 17:34:15 +08:00
applyDepId: deptInfoList ? String(deptInfoList[0].deptId) : '' ,
applyDepName: deptInfoList && deptInfoList[0].deptName,
applyUser: user.nickname
};
}
if (this.deptList && this.deptList.length > 0) {
await this.fillFormData(this.formConf, this.formData);
if (this.$refs.dynamicForm && this.$refs.dynamicForm.setFormData) {
this.$refs.dynamicForm.setFormData(this.formConf.formData);
}
setTimeout(() => {
this.key = +new Date();
}, 200);
} else {
console.warn('部门列表未加载完成,等待重试');
setTimeout(() => {
this.initData();
}, 300);
}
this.loading = false;
} catch (error) {
console.error('初始化数据失败:', error);
this.loading = false;
}
},
disableAllFormFields() {
const disableField = (field) => {
// 禁用当前字段
if (field.__config__) {
this.$set(field.__config__, 'disabled', true);
this.$set(field, 'disabled', true);
}
// 禁用子表字段
if (field.children && Array.isArray(field.children)) {
field.children.forEach(child => disableField(child));
}
// 禁用嵌套字段
if (field.__config__ && field.__config__.children && Array.isArray(field.__config__.children)) {
field.__config__.children.forEach(child => disableField(child));
}
// 特殊处理组件值
if (field.modelValue !== undefined) {
this.$set(field, 'modelValue', field.modelValue);
}
if (field.value !== undefined) {
this.$set(field, 'value', field.value);
}
};
// 遍历所有顶级字段
if (this.formConf && this.formConf.fields && Array.isArray(this.formConf.fields)) {
this.formConf.fields.forEach(field => {
disableField(field);
});
}
// 禁用整个表单组件
if (this.$refs.dynamicForm) {
this.$refs.dynamicForm.disabled = true;
}
},
async fillFormData(form, data) {
return new Promise((resolve) => {
this.$nextTick(() => {
const loop = (list, parent) => {
for (let i = 0; i < list.length; i++) {
let item = list[i];
let vModel = item.__vModel__;
let config = item.__config__;
if (vModel === 'applyDepId' && config.jnpfKey === 'organizeSelect') {
this.type = item.type = config.type
item.options = [...(this.deptList || [])].map(dept => ({
id: String(dept.deptId || dept.id),
orgNameTree: dept.deptName || dept.depthName || dept.orgNameTree ,
deptId: dept.deptId,
deptName: dept.deptName,
...dept
}));
const applyDepId = data.applyDepId ? String(data.applyDepId) : '';
console.log(applyDepId,'applyDepId---')
const applyDepName = data.applyDepName || '';
if (applyDepId && applyDepId !== '') {
let matchedOption = item.options.find(opt =>
String(opt.id) === applyDepId
);
if (!matchedOption && applyDepName) {
matchedOption = {
id: applyDepId,
orgNameTree: applyDepName,
deptId: applyDepId,
deptName: applyDepName
};
item.options.unshift(matchedOption);
}
config.defaultValue = applyDepId;
item.modelValue = applyDepId;
item.value = applyDepId;
item.__value__ = matchedOption ? matchedOption.orgNameTree : applyDepName;
const innerSelectedData = matchedOption ? [
{
id: matchedOption.id,
orgNameTree: matchedOption.orgNameTree,
deptId: matchedOption.deptId,
deptName: matchedOption.deptName
}
] : [];
item.props = {
value: 'id',
label: 'orgNameTree',
innerSelectedData: innerSelectedData
};
item.selectedData = matchedOption ? [
{
id: matchedOption.id,
orgNameTree: matchedOption.orgNameTree,
deptId: matchedOption.deptId,
deptName: matchedOption.deptName
}
] : [];
item.innerSelectedData = item.selectedData;
if (!form.formData) form.formData = {};
form.formData[vModel] = applyDepId;
form.formData['applyDepName'] = item.__value__;
console.log(form.formData,'form.formData---')
console.log(item,'item---')
} else {
item.selectedData = [];
item.modelValue = '';
item.__value__ = '';
config.defaultValue = '';
if (form.formData) {
form.formData[vModel] = '';
form.formData['applyDepName'] = '';
}
}
}
2026-01-23 11:09:28 +08:00
if(vModel == 'applyUser') {
this.$set(item, 'disabled', true);
}
2026-01-19 17:34:15 +08:00
if (vModel) {
let val = data.hasOwnProperty(vModel) ? data[vModel] : config.defaultValue;
if (!config.isSubTable) config.defaultValue = val;
if (this.isAdd || config.isSubTable) {
if (config.defaultCurrent) {
if (config.jnpfKey === 'datePicker') {
if (!data.hasOwnProperty(vModel)) {
let format = this.jnpf.handelFormat(item.format)
let dateStr = this.jnpf.toDate(new Date().getTime(), format)
let time = format === 'yyyy' ? '-01-01 00:00:00' : format === 'yyyy-MM' ?
'-01 00:00:00' : format === 'yyyy-MM-dd' ?
' 00:00:00' : ''
val = new Date(dateStr + time).getTime()
config.defaultValue = val
}
}
if (config.jnpfKey === 'timePicker') {
if (!data.hasOwnProperty(vModel)) {
config.defaultValue = this.jnpf.toDate(new Date(), item.format)
}
}
if (config.jnpfKey === 'organizeSelect' && this.userInfo.organizeIds?.length) {
config.defaultValue = item.multiple ? this.userInfo.organizeIds :
this.userInfo.organizeId
}
if (config.jnpfKey === 'posSelect' && this.userInfo.positionIds?.length) {
config.defaultValue = item.multiple ? this.userInfo.positionIds :
this.userInfo.positionId
}
const userId = this.userInfo.userId
if (config.jnpfKey === 'userSelect' && userId) {
config.defaultValue = item.multiple ? [userId] : userId;
}
if (config.jnpfKey === 'usersSelect' && userId) {
config.defaultValue = [userId + '--user'];
}
if (config.jnpfKey === 'sign' && this.userInfo.signImg) {
config.defaultValue = this.userInfo.signImg
}
}
}
const btn_detail = this.$permission.hasBtnP('btn_detail', this.formPermissionList?.menuId)
const btn_edit = this.$permission.hasBtnP('btn_edit', this.formPermissionList?.menuId)
if (!!this.dataForm.id && !btn_edit && btn_detail) item.disabled = btn_detail
let noShow = !config.noShow ? false : config.noShow
let isVisibility = false
if (!config.visibility || (Array.isArray(config.visibility) && config.visibility.includes('app'))) {
isVisibility = true
}
this.$set(config, 'isVisibility', isVisibility)
if (this.formPermissionList.useFormPermission) {
let id = config.isSubTable ? parent.__vModel__ + '-' + vModel : vModel
noShow = true
if (this.formList && this.formList.length) {
noShow = !this.formList.some(o => o.enCode === id)
}
noShow = config.noShow ? config.noShow : noShow
this.$set(config, 'noShow', noShow)
}
} else {
let noShow = config.noShow ? config.noShow : false,
isVisibility = false
if (!config.visibility || (Array.isArray(config.visibility) && config.visibility.includes('app'))) {
isVisibility = true
}
this.$set(config, 'isVisibility', isVisibility)
this.$set(config, 'noShow', noShow)
}
if (config && config.children && Array.isArray(config.children)) {
loop(config.children, item);
}
}
};
loop(form.fields);
if (!form.formData) {
form.formData = {};
}
const formDataToMerge = {
...data,
applyDepId: data.applyDepId || '',
applyDepName: data.applyDepName || ''
};
form.formData = { ...form.formData, ...formDataToMerge };
setTimeout(() => {
const applyField = form.fields.find(f => f.__vModel__ === 'applyDepId');
if (!applyField || !applyField.selectedData || applyField.selectedData.length === 0) {
console.warn('申请单位无选中数据');
return;
}
const dynamicForm = this.$refs.dynamicForm;
if (!dynamicForm) return;
const findOrganizeSelect = (component) => {
if (component.$options.name === 'jnpf-organize-select') {
return component;
}
2026-01-27 15:42:20 +08:00
const children = component.$children || [];
for (let child of children) {
2026-01-19 17:34:15 +08:00
const res = findOrganizeSelect(child);
if (res) return res;
}
return null;
};
const organizeSelect = findOrganizeSelect(dynamicForm);
if (organizeSelect) {
organizeSelect.selectedData = applyField.selectedData;
} else {
console.warn('未找到jnpf-organize-select组件');
}
}, 1000);
this.key = +new Date();
setTimeout(() => {
const finalApplyField = form.fields.find(f => f.__vModel__ === 'applyDepId');
if (finalApplyField) {
}
}, 100);
resolve();
});
});
},
sumbitForm(data, callback) {
const {billNoPrefix} = this.config
if (!data) return;
2026-01-29 15:32:10 +08:00
const filePathArr =Array(data.attachment) ? [...data.attachment] : [data.attachment]
const attachment = !!filePathArr.length ? filePathArr.map(item => item.fileUrl) : []
2026-01-19 17:34:15 +08:00
this.btnLoading = true;
const applyDepName = this.deptList?.filter(item=>item.deptId === data.applyDepId)[0]?.deptName
const formData = {
...this.formData,
...data,
id: data?.id || '',
applyDepName,
attachment: attachment.join(',')
};
if(!this.dataForm.id){
formData.billNo = billNoPrefix + new Date().getTime()
}
this.dataForm.data = JSON.stringify(formData);
const params = {
id: this.dataForm.id,
operateType: this.dataForm.id ? 1 : 0,
attachments: data.attachment,
data: {
2026-01-29 14:28:28 +08:00
...formData,
2026-01-29 15:32:10 +08:00
...(formData?.eDate ? { eDate: this.formatTime(formData.eDate) } : {}),
...(formData?.sDate ? { sDate: this.formatTime(formData.sDate) } : {})
2026-01-19 17:34:15 +08:00
}
}
const formMethod = createModel;
2026-01-29 14:28:28 +08:00
console.log(params,'params---')
2026-01-19 17:34:15 +08:00
formMethod(this.modelId, params).then(res => {
uni.showToast({
title:'保存成功',
complete: () => {
setTimeout(() => {
if (this.clickType == 'save_add') {
this.key = +new Date();
this.$nextTick(() => {
this.$refs.dynamicForm && this.$refs.dynamicForm.resetForm();
});
}
this.btnLoading = false;
this.initData();
if (this.clickType != 'save_proceed' && this.clickType != 'save_add') {
uni.$emit('refresh');
uni.navigateBack();
}
}, 1500);
}
});
}).catch(() => {
this.btnLoading = false;
});
if (callback && typeof callback === "function") callback();
},
commonSubmit(type) {
this.clickType = type;
this.submit(type);
},
submit(type) {
2026-01-23 15:16:31 +08:00
console.log(type,'type333')
2026-01-19 17:34:15 +08:00
this.clickType = type;
this.$refs.dynamicForm && this.$refs.dynamicForm.submitForm();
},
// 获取审批记录
async getApprovalData() {
try {
const res = await getByProcess(this.formData.processInstanceId);
const result = []
if (res.code === 0) {
res.data.forEach((task) => {
if (task.result !== 4) {
result.push(task)
}
})
result.sort((a, b) => {
// 有已完成的情况,按照完成时间倒序
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
// 都是未完成,按照创建时间倒序
} else {
return b.createTime - a.createTime
}
})
}
this.approvalData = result
// 审批记录加载完成后,检查一次可见性
this.$nextTick(() => {
setTimeout(() => {
this.checkApprovalVisibility()
}, 300)
})
2026-01-19 17:34:15 +08:00
} catch (err) {
console.error('获取审批记录失败:', err);
}
},
// 状态样式
getStatusClass(result) {
return {
'status-processing': result === 1,
'status-pass': result === 2,
'status-reject': result === 3
};
},
getStatusTagClass(result) {
return {
'tag-processing': result === 1,
'tag-pass': result === 2,
'tag-reject': result === 3
};
},
// 时间格式化
formatTime(timestamp) {
if (!timestamp) return '-';
const date = new Date(timestamp);
return `${date.getFullYear()}-${this.padZero(date.getMonth() + 1)}-${this.padZero(date.getDate())} ${this.padZero(date.getHours())}:${this.padZero(date.getMinutes())}:${this.padZero(date.getSeconds())}`;
},
padZero(num) {
return num.toString().padStart(2, '0');
},
// 耗时格式化
formatDuration(millis) {
if (!millis) return '-';
const seconds = Math.floor(millis / 1000);
if (seconds < 60) {
return `${seconds}`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes} 分钟`;
}
const hours = Math.floor(minutes / 60);
return `${hours} 小时 ${minutes % 60} 分钟`;
}
}
}
</script>
<style lang="scss">
page {
background-color: #f0f2f6;
}
/* 固定的审批记录底栏 */
.fixed-approval-bar {
position: fixed;
left: 0;
right: 0;
z-index: 998;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-top: 1rpx solid #e8e8e8;
box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.08);
padding: 20rpx 30rpx;
transition: transform 0.3s ease;
}
.fixed-approval-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.fixed-status-icon {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
&.status-processing {
background-color: #409EFF;
border: 3rpx solid rgba(64, 158, 255, 0.3);
}
&.status-pass {
background-color: #67C23A;
border: 3rpx solid rgba(103, 194, 58, 0.3);
}
&.status-reject {
background-color: #F56C6C;
border: 3rpx solid rgba(245, 108, 108, 0.3);
}
}
.fixed-approval-info {
flex: 1;
overflow: hidden;
}
.fixed-approval-header {
display: flex;
justify-content: space-between;
}
.fixed-task-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fixed-approval-user {
display: flex;
align-items: center;
font-size: 24rpx;
}
.fixed-user-name {
color: #666;
margin-right: 12rpx;
}
.fixed-dept-tag {
background: #f4f4f5;
color: #909399;
padding: 2rpx 10rpx;
border-radius: 4rpx;
font-size: 22rpx;
}
.fixed-scroll-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #409EFF;
color: white;
padding: 12rpx 20rpx;
border-radius: 30rpx;
margin-left: 20rpx;
flex-shrink: 0;
transition: all 0.2s;
&:active {
background: #3a8ee6;
transform: scale(0.95);
}
}
.fixed-scroll-btn .icon-ym {
font-size: 28rpx;
margin-bottom: 4rpx;
}
.fixed-btn-text {
font-size: 22rpx;
}
/* 完整的审批记录区域 */
.full-approval-section {
margin-top: 20rpx;
background: white;
border-radius: 12rpx;
overflow: hidden;
&.has-fixed-bar {
margin-bottom: 120rpx;
}
}
.approval-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #f9fafc;
}
.approval-list {
padding: 0 30rpx;
}
.approval-item {
display: flex;
position: relative;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.approval-line-container {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 24rpx;
}
.status-icon {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
&.status-processing {
background-color: #409EFF;
border: 4rpx solid rgba(64, 158, 255, 0.2);
box-shadow: 0 0 0 4rpx rgba(64, 158, 255, 0.1);
}
&.status-pass {
background-color: #67C23A;
border: 4rpx solid rgba(103, 194, 58, 0.2);
box-shadow: 0 0 0 4rpx rgba(103, 194, 58, 0.1);
}
&.status-reject {
background-color: #F56C6C;
border: 4rpx solid rgba(245, 108, 108, 0.2);
box-shadow: 0 0 0 4rpx rgba(245, 108, 108, 0.1);
}
}
.approval-line {
width: 2rpx;
background-color: #e8e8e8;
flex: 1;
margin-top: 10rpx;
}
.approval-content {
flex: 1;
}
.task-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.task-name {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.status-tag {
display: flex;
align-items: center;
font-size: 24rpx;
padding: 6rpx 16rpx;
border-radius: 30rpx;
&.tag-processing {
background-color: rgba(64, 158, 255, 0.1);
color: #409EFF;
}
&.tag-pass {
background-color: rgba(103, 194, 58, 0.1);
color: #67C23A;
}
&.tag-reject {
background-color: rgba(245, 108, 108, 0.1);
color: #F56C6C;
}
.status-time {
font-size: 22rpx;
margin-left: 12rpx;
opacity: 0.8;
}
}
.info-row {
display: flex;
align-items: center;
font-size: 26rpx;
margin-bottom: 12rpx;
flex-wrap: wrap;
.info-label {
color: #999;
margin-right: 8rpx;
}
.info-value {
color: #666;
margin-right: 12rpx;
}
.info-value-tag {
background: #f4f4f5;
color: #909399;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.pending-tag {
background-color: #fef0f0;
color: #f56c6c;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-size: 24rpx;
margin-left: 12rpx;
}
}
.reason-box {
background: #f9f9f9;
padding: 20rpx;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
line-height: 1.5;
margin-top: 16rpx;
border-left: 4rpx solid #e8e8e8;
}
/* 底部按钮区域 */
2026-01-19 17:34:15 +08:00
.buttom-actions {
display: flex;
align-items: center;
background: #fff;
position: fixed;
bottom: 0;
left: 0;
right: 0;
2026-01-20 18:07:35 +08:00
z-index: 999 !important;
padding: 10rpx 0rpx;
// border-top: 1rpx solid #f0f0f0;
// box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.buttom-btn {
min-width: 200rpx;
padding: 12rpx 15rpx;
flex: 1;
// margin: 0 10rpx;
2026-01-19 17:34:15 +08:00
}
2026-01-20 18:07:35 +08:00
.launch-flow-btn {
background: #19be6b !important;
}
2026-01-20 18:07:35 +08:00
.launch-flow-btn[disabled] {
background: #19be6b !important;
opacity: 0.6;
2026-01-20 18:07:35 +08:00
}
2026-01-19 17:34:15 +08:00
.more-btn {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
2026-01-20 18:07:35 +08:00
flex: 1;
2026-01-19 17:34:15 +08:00
.more-text {
font-size: 22rpx;
color: #606266;
margin-top: 4rpx;
}
2026-01-19 17:34:15 +08:00
.more-menu {
position: absolute;
bottom: 100%;
right: 0;
background: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1);
width: 200rpx;
z-index: 100;
padding: 10rpx 0;
.menu-item {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
font-size: 28rpx;
cursor: pointer;
transition: background-color 0.2s;
2026-01-19 17:34:15 +08:00
&:hover {
background-color: #f5f7fa;
}
&:active {
background-color: #ebeef5;
2026-01-19 17:34:15 +08:00
}
.menu-label {
flex: 1;
}
}
}
}
/* 流程选择弹框样式 */
.process-dialog {
width: 90vw;
max-height: 70vh;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-icon {
padding: 10rpx;
}
.process-list {
flex: 1;
overflow-y: auto;
max-height: 50vh;
padding: 20rpx 0;
}
.process-item {
display: flex;
align-items: flex-start;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f8f8f8;
transition: background-color 0.3s;
cursor: pointer;
&.selected-item {
background-color: #f5f7fa;
border-left: 4rpx solid #2979ff;
}
}
.process-item:active {
background-color: #f0f2f5;
}
.process-item-left {
margin-right: 20rpx;
padding-top: 6rpx;
}
.empty-process {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
.loading-process {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.loading-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
.dialog-footer {
display: flex;
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
gap: 20rpx;
}
.footer-btn {
flex: 1;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.submit-btn {
background: linear-gradient(135deg, #3d6dcc, #2a52b5);
color: #fff;
}
.submit-btn[disabled] {
opacity: 0.6;
}
/* 审批人选择弹框样式 */
.approver-dialog {
width: 90vw;
max-height: 80vh;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.approver-content {
flex: 1;
overflow-y: auto;
max-height: 60vh;
padding: 30rpx;
}
.approver-node {
margin-bottom: 40rpx;
padding: 20rpx;
background: #f9fafc;
border-radius: 8rpx;
}
.node-title {
margin-bottom: 20rpx;
padding-bottom: 10rpx;
border-bottom: 1rpx solid #ebeef5;
display: flex;
justify-content: space-between;
}
.node-label {
font-size: 28rpx;
color: #606266;
}
.node-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.candidate-section {
margin-top: 20rpx;
}
.section-title {
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.section-label {
font-size: 26rpx;
color: #606266;
font-weight: 500;
}
.loading-approver {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.empty-approver {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.confirm-btn {
background: linear-gradient(135deg, #3d6dcc, #2a52b5);
color: #fff;
margin: 0 30rpx 30rpx;
}
.confirm-btn[disabled] {
opacity: 0.6;
}
/* 审批记录样式 */
.approval-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
}
.approval-list {
2026-01-20 18:07:35 +08:00
padding: 10rpx 30rpx;
2026-01-19 17:34:15 +08:00
}
.approval-item {
display: flex;
position: relative;
2026-01-20 18:07:35 +08:00
margin-bottom: 20rpx;
padding-top: 10rpx;
2026-01-19 17:34:15 +08:00
}
.approval-line-container {
display: flex;
flex-direction: column;
align-items: center;
2026-01-20 18:07:35 +08:00
margin-right: 16rpx;
2026-01-19 17:34:15 +08:00
}
.approval-line {
width: 2rpx;
2026-01-20 18:07:35 +08:00
background-color: #8e8585;
2026-01-19 17:34:15 +08:00
flex: 1;
margin-top: 8rpx;
}
.status-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
2026-01-20 18:07:35 +08:00
// background-color: #fff;
border: 2rpx solid;
/* 状态点边框色 */
2026-01-19 17:34:15 +08:00
&.status-processing {
2026-01-20 18:07:35 +08:00
background-color: #409EFF;
border-color: #409EFF;
2026-01-19 17:34:15 +08:00
}
&.status-pass {
2026-01-20 18:07:35 +08:00
background-color: #67C23A;
border-color: #67C23A;
2026-01-19 17:34:15 +08:00
}
&.status-reject {
2026-01-20 18:07:35 +08:00
background-color: #F56C6C;
border-color: #F56C6C;
2026-01-19 17:34:15 +08:00
}
}
.approval-content {
flex: 1;
background-color: #fff;
border-radius: 8rpx;
padding: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
}
.task-header {
display: flex;
align-items: center;
2026-01-20 18:07:35 +08:00
justify-content: space-between;
margin-bottom: 12rpx;
2026-01-19 17:34:15 +08:00
.task-name {
font-size: 28rpx;
color: #333;
}
2026-01-20 18:07:35 +08:00
}
2026-01-19 17:34:15 +08:00
2026-01-20 18:07:35 +08:00
.status-tag {
display: flex;
align-items: center;
font-size: 22rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
margin-left: 12rpx;
/* 状态对应的样式 */
&.tag-processing {
background-color: #E6F7FF;
color: #409EFF;
}
&.tag-pass {
background-color: #F0F9EB;
color: #67C23A;
}
&.tag-reject {
background-color: #FFF2F0;
color: #F56C6C;
}
/* 状态标签内的时间 */
.status-time {
font-size: 20rpx;
margin-left: 8rpx;
2026-01-19 17:34:15 +08:00
}
}
2026-01-20 18:07:35 +08:00
2026-01-19 17:34:15 +08:00
.info-row {
font-size: 24rpx;
color: #666;
line-height: 1.5;
margin-bottom: 12rpx;
.info-label {
color: #999;
}
.info-value {
color: #666;
}
2026-01-20 18:07:35 +08:00
.info-value-tag {
background: #f4f4f5;
color: #a0a4a7;
padding: 2rpx 10rpx;
margin-left: 10rpx;
border: 10rpx;
}
.pending-tag {
display: inline-block;
background-color: #FAFAFA;
border: 1rpx solid #E5E5E5;
color: #666;
padding: 2rpx 8rpx;
border-radius: 4rpx;
margin-left: 10rpx;
font-size: 20rpx;
}
2026-01-19 17:34:15 +08:00
.dept-tag {
display: inline-block;
background-color: #F5F7FA;
color: #666;
padding: 2rpx 8rpx;
border-radius: 4rpx;
margin-left: 8rpx;
font-size: 22rpx;
}
.ml-20 {
margin-left: 20rpx;
}
}
.reason-box {
font-size: 24rpx;
color: #333;
background-color: #F9F9F9;
padding: 12rpx;
border-radius: 4rpx;
margin-top: 8rpx;
}
/* 新增:审批意见弹窗样式 */
.approval-reason-dialog {
width: 80vw;
max-width: 600rpx;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
}
.approval-reason-dialog .dialog-header {
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.approval-reason-dialog .dialog-content {
padding: 30rpx;
}
.approval-reason-dialog .input-label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.approval-reason-dialog .reason-input {
font-size: 26rpx;
}
.approval-reason-dialog .dialog-footer {
padding: 20rpx 30rpx;
border-top: 1rpx solid #f0f0f0;
}
2026-01-20 18:07:35 +08:00
.copy-dialog {
width: 90vw;
max-height: 80vh;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 30rpx;
}
.form-item-user {
display: flex;
justify-content: space-between;
align-items: center;
}
.form-item {
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
}
.form-item.required .label::after {
content: '*';
color: #F56C6C;
margin-left: 4rpx;
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.value {
font-size: 26rpx;
color: #666;
padding: 12rpx;
background: #f9fafc;
border-radius: 6rpx;
}
2026-01-19 17:34:15 +08:00
</style>