feat(oa): add pending list and approve reject return add-sign transfer
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
29560b743c
commit
d5287765ec
4 files changed
+833
-18
No files matched your search
@@ -79,7 +79,9 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "待办中心",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black"
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,424 @@
|
||||
<template>
|
||||
<view class="page-shell" style="padding:24rpx">
|
||||
<text>{{ title }}</text>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-row">
|
||||
<view class="search-box" :class="{ 'is-focus': appNameFocused }">
|
||||
<view class="search-ico" aria-hidden="true" />
|
||||
<input
|
||||
class="search-input"
|
||||
:value="appName"
|
||||
confirm-type="search"
|
||||
placeholder="搜索流程名称"
|
||||
placeholder-class="search-ph"
|
||||
@input="onAppNameInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="appNameFocused = true"
|
||||
@blur="appNameFocused = false"
|
||||
/>
|
||||
<view v-if="appName" class="search-clear" @click="clearAppName">
|
||||
<text class="search-clear__x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="toolbar-row toolbar-row--sub">
|
||||
<view class="search-box" :class="{ 'is-focus': initiatorFocused }">
|
||||
<input
|
||||
class="search-input"
|
||||
:value="initiatorName"
|
||||
confirm-type="search"
|
||||
placeholder="搜索申请人"
|
||||
placeholder-class="search-ph"
|
||||
@input="onInitiatorInput"
|
||||
@confirm="onSearchConfirm"
|
||||
@focus="initiatorFocused = true"
|
||||
@blur="initiatorFocused = false"
|
||||
/>
|
||||
<view v-if="initiatorName" class="search-clear" @click="clearInitiator">
|
||||
<text class="search-clear__x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !list.length" class="state state-loading">
|
||||
<view class="pulse-dots" aria-hidden="true">
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
</view>
|
||||
<text class="state-loading__text">加载待办…</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!list.length" class="oa-task-list__empty">暂无待办</view>
|
||||
|
||||
<view v-else class="oa-task-list">
|
||||
<OaTaskCard
|
||||
v-for="row in list"
|
||||
:key="row.id"
|
||||
:title="cardTitle(row)"
|
||||
:status-text="statusText(row.instanceStatus)"
|
||||
:status-tone="statusTone(row.instanceStatus)"
|
||||
:meta-lines="metaLines(row)"
|
||||
:actions="cardActions"
|
||||
@open="openApprove(row)"
|
||||
@action="openApprove(row)"
|
||||
/>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return { title: '待办中心' }
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import { fetchOaTasks } from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const STATUS_MAP = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
returned: '已打回',
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OaTaskCard },
|
||||
data() {
|
||||
return {
|
||||
appName: '',
|
||||
initiatorName: '',
|
||||
appNameFocused: false,
|
||||
initiatorFocused: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
_searchTimer: null,
|
||||
cardActions: [{ key: 'approve', label: '办理', primary: true }]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
return
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadList(true)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
onReachBottom() {
|
||||
if (!this.hasMore || this.loading || this.loadingMore) return
|
||||
this.loadList(false)
|
||||
},
|
||||
onUnload() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
this._searchTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cardTitle(row) {
|
||||
return (row && (row.appName || row.title)) || '-'
|
||||
},
|
||||
statusText(s) {
|
||||
if (!s) return '-'
|
||||
return STATUS_MAP[s] || s
|
||||
},
|
||||
statusTone(s) {
|
||||
if (s === 'pending' || s === 'running') return 'warn'
|
||||
if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
|
||||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||||
return 'info'
|
||||
},
|
||||
metaLines(row) {
|
||||
return [
|
||||
{
|
||||
label: '申请人',
|
||||
value: (row && row.initiatorUserName) || '-'
|
||||
},
|
||||
{
|
||||
label: '申请时间',
|
||||
value: (row && row.applyTime) || '-'
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
value: (row && row.currentNodeName) || '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
scheduleSearch() {
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this.loadList(true)
|
||||
}, 400)
|
||||
},
|
||||
onAppNameInput(e) {
|
||||
this.appName = this._inputVal(e)
|
||||
this.scheduleSearch()
|
||||
},
|
||||
onInitiatorInput(e) {
|
||||
this.initiatorName = this._inputVal(e)
|
||||
this.scheduleSearch()
|
||||
},
|
||||
_inputVal(e) {
|
||||
if (e && e.detail && e.detail.value != null) return String(e.detail.value)
|
||||
if (e && e.target) return String(e.target.value || '')
|
||||
return ''
|
||||
},
|
||||
onSearchConfirm() {
|
||||
if (this._searchTimer) {
|
||||
clearTimeout(this._searchTimer)
|
||||
this._searchTimer = null
|
||||
}
|
||||
this.loadList(true)
|
||||
},
|
||||
clearAppName() {
|
||||
this.appName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
clearInitiator() {
|
||||
this.initiatorName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
openApprove(row) {
|
||||
if (!row) return
|
||||
const instanceId = String(row.instanceId || '').trim()
|
||||
if (!instanceId) {
|
||||
uni.showToast({ title: '缺少实例 ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// row.id = 待办审批任务 id(对齐办理动作 taskId;Web pending 仅跳实例页,移动端需带 taskId)
|
||||
const taskId = String(row.id || '').trim()
|
||||
stashOaDetailTitle(row.appName || row.title)
|
||||
let url =
|
||||
'/pages/oa/webview/index?instanceId=' +
|
||||
encodeURIComponent(instanceId) +
|
||||
'&mode=approve'
|
||||
if (taskId) {
|
||||
url += '&taskId=' + encodeURIComponent(taskId)
|
||||
}
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
async loadList(reset) {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
return
|
||||
}
|
||||
const isReset = !!reset
|
||||
if (isReset) {
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.loading = true
|
||||
} else {
|
||||
this.loadingMore = true
|
||||
}
|
||||
try {
|
||||
const res = await fetchOaTasks('pending', {
|
||||
appName: (this.appName || '').trim() || undefined,
|
||||
initiatorName: (this.initiatorName || '').trim() || undefined,
|
||||
page: this.page,
|
||||
pageSize: PAGE_SIZE
|
||||
})
|
||||
const rows = (res && res.list) || []
|
||||
const total = (res && res.total) != null ? Number(res.total) : 0
|
||||
this.total = total
|
||||
if (isReset) this.list = rows
|
||||
else this.list = this.list.concat(rows)
|
||||
const loaded = this.list.length
|
||||
this.hasMore = loaded < total && rows.length > 0
|
||||
if (rows.length > 0) this.page += 1
|
||||
} catch (e) {
|
||||
if (isReset) this.list = []
|
||||
uni.showToast({
|
||||
title: (e && e.message) || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.loadingMore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 48rpx;
|
||||
background: #f5f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar-row--sub {
|
||||
padding-top: 12rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #e8eaed;
|
||||
background: #fafafa;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.search-box.is-focus {
|
||||
border-color: #cbd5e1;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 6rpx rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.search-ico {
|
||||
position: relative;
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
border: 3rpx solid #94a3b8;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-ico::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12rpx;
|
||||
height: 3rpx;
|
||||
right: -8rpx;
|
||||
bottom: -5rpx;
|
||||
background: #94a3b8;
|
||||
border-radius: 3rpx;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2937;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-ph {
|
||||
color: #b0b7c3;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-clear__x {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
color: #6b7280;
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.state-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28rpx;
|
||||
min-height: 50vh;
|
||||
padding: 96rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pulse-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #22c55e);
|
||||
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pulse-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.pulse-dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.state-loading__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes oa-pulse-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-12rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -104,6 +104,8 @@ const props = defineProps({
|
||||
disabled: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: true },
|
||||
max: { type: [Number, String], default: null },
|
||||
/** 不可选用户 id(如转交时排除自己及本节点已有待办审批人) */
|
||||
excludeIds: { type: Array, default: () => [] },
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
@@ -117,14 +119,26 @@ const nameMap = ref(new Map())
|
||||
const userOptions = ref([])
|
||||
|
||||
const multiple = computed(() => !!props.multiple)
|
||||
const excludeSet = computed(() => {
|
||||
const set = new Set()
|
||||
for (const x of props.excludeIds || []) {
|
||||
const id = String(x || '').trim()
|
||||
if (id) set.add(id)
|
||||
}
|
||||
return set
|
||||
})
|
||||
const normalized = computed(() =>
|
||||
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
||||
)
|
||||
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
||||
const filtered = computed(() => {
|
||||
const excluded = excludeSet.value
|
||||
const base = excluded.size
|
||||
? userOptions.value.filter((u) => !excluded.has(u.id))
|
||||
: userOptions.value
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return userOptions.value
|
||||
return userOptions.value.filter(
|
||||
if (!q) return base
|
||||
return base.filter(
|
||||
(u) =>
|
||||
u.label.toLowerCase().includes(q) ||
|
||||
String(u.sub || '')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<OaPulseLoading text="加载审批详情…" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="oa-detail__body">
|
||||
<div class="oa-detail__body" :class="{ 'oa-detail__body--approve': canApprove }">
|
||||
<header class="oa-detail__hd">
|
||||
<h1 class="oa-detail__title">{{ detail?.title || '审批详情' }}</h1>
|
||||
<span
|
||||
@@ -28,8 +28,139 @@
|
||||
<van-button class="oa-detail__btn oa-detail__btn--close" @click="onClose">
|
||||
关闭
|
||||
</van-button>
|
||||
<template v-if="canApprove">
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="openComment('reject')"
|
||||
>
|
||||
拒绝
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="detail?.allowReturn !== false"
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="openComment('return')"
|
||||
>
|
||||
打回
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="detail?.allowAddSign !== false"
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="openAddSign"
|
||||
>
|
||||
加签
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="openTransfer"
|
||||
>
|
||||
转交
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--primary"
|
||||
type="primary"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="openComment('approve')"
|
||||
>
|
||||
同意
|
||||
</van-button>
|
||||
</template>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<!-- 同意 / 拒绝 / 打回意见 -->
|
||||
<van-dialog
|
||||
v-model:show="commentVisible"
|
||||
:title="commentTitle"
|
||||
show-cancel-button
|
||||
confirm-button-text="确定"
|
||||
cancel-button-text="取消"
|
||||
:before-close="onCommentBeforeClose"
|
||||
>
|
||||
<div class="oa-detail__dialog-body">
|
||||
<van-field
|
||||
v-model="commentText"
|
||||
type="textarea"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:placeholder="commentPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
</van-dialog>
|
||||
|
||||
<!-- 加签 -->
|
||||
<van-popup
|
||||
v-model:show="addSignVisible"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ height: '72%' }"
|
||||
>
|
||||
<div class="oa-act-sheet">
|
||||
<div class="oa-act-sheet__hd">
|
||||
<span @click="addSignVisible = false">取消</span>
|
||||
<strong>加签</strong>
|
||||
<span class="oa-act-sheet__ok" @click="submitAddSign">确定</span>
|
||||
</div>
|
||||
<div class="oa-act-sheet__body">
|
||||
<p class="oa-act-sheet__label">加签人</p>
|
||||
<OaPersonSelect v-model="addSignUsers" :multiple="true" />
|
||||
<p class="oa-act-sheet__label">说明(可选)</p>
|
||||
<van-field
|
||||
v-model="addSignComment"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="加签说明"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 转交 -->
|
||||
<van-popup
|
||||
v-model:show="transferVisible"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ height: '72%' }"
|
||||
>
|
||||
<div class="oa-act-sheet">
|
||||
<div class="oa-act-sheet__hd">
|
||||
<span @click="transferVisible = false">取消</span>
|
||||
<strong>转交</strong>
|
||||
<span class="oa-act-sheet__ok" @click="submitTransfer">确定</span>
|
||||
</div>
|
||||
<div class="oa-act-sheet__body">
|
||||
<p class="oa-act-sheet__label">转交给</p>
|
||||
<OaPersonSelect
|
||||
v-model="transferUser"
|
||||
:multiple="false"
|
||||
:exclude-ids="transferExcludeIds"
|
||||
/>
|
||||
<p class="oa-act-sheet__label">说明(可选)</p>
|
||||
<van-field
|
||||
v-model="transferComment"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
autosize
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="转交说明"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,21 +168,48 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchOaInstanceDetail } from '../api/oa.js'
|
||||
import {
|
||||
fetchOaInstanceDetail,
|
||||
fetchCurrentUser,
|
||||
fetchApproveOaTask,
|
||||
fetchRejectOaTask,
|
||||
fetchReturnOaTask,
|
||||
fetchAddSignOaTask,
|
||||
fetchTransferOaTask
|
||||
} from '../api/oa.js'
|
||||
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
||||
import { statusText, statusTone } from '../oa/status-label.js'
|
||||
import OaProgressTimeline from '../oa/components/OaProgressTimeline.vue'
|
||||
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
import OaPersonSelect from '../oa/components/OaPersonSelect.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const acting = ref(false)
|
||||
const detail = ref(null)
|
||||
const formData = ref({})
|
||||
const formRule = ref([])
|
||||
const formOption = ref({})
|
||||
const currentUserId = ref('')
|
||||
|
||||
const commentVisible = ref(false)
|
||||
const commentKind = ref('approve') // approve | reject | return
|
||||
const commentText = ref('')
|
||||
|
||||
const addSignVisible = ref(false)
|
||||
const addSignUsers = ref([])
|
||||
const addSignComment = ref('')
|
||||
|
||||
const transferVisible = ref(false)
|
||||
const transferUser = ref(null)
|
||||
const transferComment = ref('')
|
||||
|
||||
/** instanceId 必填;mode 默认 view;taskId 本任务忽略办理动作 */
|
||||
const instanceId = computed(() => String(route.query.instanceId || '').trim())
|
||||
const queryTaskId = computed(() => String(route.query.taskId || '').trim())
|
||||
const mode = computed(() => {
|
||||
const m = String(route.query.mode || 'view').trim()
|
||||
return m || 'view'
|
||||
})
|
||||
|
||||
const timelineItems = computed(() => {
|
||||
const list = detail.value?.progress
|
||||
@@ -74,6 +232,50 @@ const timelineItems = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
function isPendingApprovalForMe(t) {
|
||||
const uid = currentUserId.value
|
||||
if (!uid || !t) return false
|
||||
return (
|
||||
t.taskType === 'approval' &&
|
||||
t.status === 'pending' &&
|
||||
String(t.assigneeUserId || '') === uid
|
||||
)
|
||||
}
|
||||
|
||||
/** 优先 query.taskId(须为本用户待办审批任务),否则取本用户第一条 pending approval */
|
||||
const myPendingTask = computed(() => {
|
||||
const list = detail.value?.tasks || []
|
||||
const qid = queryTaskId.value
|
||||
if (qid) {
|
||||
const matched = list.find((t) => String(t.id || '') === qid && isPendingApprovalForMe(t))
|
||||
if (matched) return matched
|
||||
}
|
||||
return list.find((t) => isPendingApprovalForMe(t))
|
||||
})
|
||||
|
||||
const canApprove = computed(() => !!myPendingTask.value && mode.value === 'approve')
|
||||
|
||||
/** 转交:排除自己 + 本节点已有 pending approval 的办理人 */
|
||||
const transferExcludeIds = computed(() => {
|
||||
const me = currentUserId.value
|
||||
const pendingIds = (detail.value?.tasks || [])
|
||||
.filter((t) => t.status === 'pending' && t.taskType === 'approval')
|
||||
.map((t) => String(t.assigneeUserId || '').trim())
|
||||
.filter(Boolean)
|
||||
return [...new Set([me, ...pendingIds].filter(Boolean))]
|
||||
})
|
||||
|
||||
const commentTitle = computed(() => {
|
||||
if (commentKind.value === 'reject') return '拒绝'
|
||||
if (commentKind.value === 'return') return '打回'
|
||||
return '同意'
|
||||
})
|
||||
|
||||
const commentPlaceholder = computed(() => {
|
||||
if (commentKind.value === 'return') return '请填写打回意见(必填)'
|
||||
return '审批意见(可选)'
|
||||
})
|
||||
|
||||
/** 通知宿主关闭(对齐 OaStart closeToMini) */
|
||||
function closeToMini(payload) {
|
||||
const data = payload || { action: 'oa-detail-close' }
|
||||
@@ -119,6 +321,125 @@ function parseFormData(d) {
|
||||
return {}
|
||||
}
|
||||
|
||||
function openComment(kind) {
|
||||
if (!myPendingTask.value) return
|
||||
commentKind.value = kind
|
||||
commentText.value = ''
|
||||
commentVisible.value = true
|
||||
}
|
||||
|
||||
function onCommentBeforeClose(action) {
|
||||
if (action !== 'confirm') return true
|
||||
if (commentKind.value === 'return') {
|
||||
const c = String(commentText.value || '').trim()
|
||||
if (!c) {
|
||||
showToast('打回意见必填')
|
||||
return false
|
||||
}
|
||||
}
|
||||
void submitComment()
|
||||
return false
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const task = myPendingTask.value
|
||||
if (!task) {
|
||||
commentVisible.value = false
|
||||
return
|
||||
}
|
||||
const comment = String(commentText.value || '').trim()
|
||||
acting.value = true
|
||||
try {
|
||||
const kind = commentKind.value
|
||||
if (kind === 'approve') {
|
||||
await fetchApproveOaTask(task.id, comment || undefined)
|
||||
commentVisible.value = false
|
||||
showToast('已同意')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已同意' })
|
||||
} else if (kind === 'reject') {
|
||||
await fetchRejectOaTask(task.id, comment || undefined)
|
||||
commentVisible.value = false
|
||||
showToast('已拒绝')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已拒绝' })
|
||||
} else if (kind === 'return') {
|
||||
await fetchReturnOaTask(task.id, comment)
|
||||
commentVisible.value = false
|
||||
showToast('已打回')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已打回' })
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e?.message || '操作失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAddSign() {
|
||||
if (!myPendingTask.value) return
|
||||
addSignUsers.value = []
|
||||
addSignComment.value = ''
|
||||
addSignVisible.value = true
|
||||
}
|
||||
|
||||
async function submitAddSign() {
|
||||
const task = myPendingTask.value
|
||||
if (!task) return
|
||||
const userIds = (Array.isArray(addSignUsers.value) ? addSignUsers.value : [])
|
||||
.map((u) => String(u?.id || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!userIds.length) {
|
||||
showToast('请选择加签人')
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
const c = String(addSignComment.value || '').trim()
|
||||
await fetchAddSignOaTask(task.id, {
|
||||
userIds,
|
||||
comment: c || undefined
|
||||
})
|
||||
addSignVisible.value = false
|
||||
showToast('已加签')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已加签' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加签失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openTransfer() {
|
||||
if (!myPendingTask.value) return
|
||||
transferUser.value = null
|
||||
transferComment.value = ''
|
||||
transferVisible.value = true
|
||||
}
|
||||
|
||||
async function submitTransfer() {
|
||||
const task = myPendingTask.value
|
||||
if (!task) return
|
||||
const targetUserId = String(transferUser.value?.id || '').trim()
|
||||
if (!targetUserId) {
|
||||
showToast('请选择转交人')
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
const c = String(transferComment.value || '').trim()
|
||||
await fetchTransferOaTask(task.id, {
|
||||
targetUserId,
|
||||
comment: c || undefined
|
||||
})
|
||||
transferVisible.value = false
|
||||
showToast('已转交')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已转交' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '转交失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -127,9 +448,12 @@ async function bootstrap() {
|
||||
showToast('缺少 instanceId')
|
||||
return
|
||||
}
|
||||
// mode 默认 view;taskId 本任务忽略(办理动作留给后续)
|
||||
|
||||
const d = await fetchOaInstanceDetail(id)
|
||||
const [me, d] = await Promise.all([
|
||||
fetchCurrentUser().catch(() => null),
|
||||
fetchOaInstanceDetail(id)
|
||||
])
|
||||
currentUserId.value = String(me?.id || me?.userId || '').trim()
|
||||
detail.value = d
|
||||
try {
|
||||
document.title = d?.title || d?.appName || '审批详情'
|
||||
@@ -178,6 +502,9 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
padding: 12px 12px 96px;
|
||||
}
|
||||
.oa-detail__body--approve {
|
||||
padding-bottom: 128px;
|
||||
}
|
||||
.oa-detail__hd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -230,20 +557,26 @@ onMounted(() => {
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||
gap: 8px;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #eef2f6;
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.oa-detail__btn {
|
||||
margin: 0;
|
||||
height: 44px;
|
||||
height: 40px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 12px !important;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
.oa-detail__btn--close {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
min-width: 64px;
|
||||
border: none !important;
|
||||
background: #f1f5f9 !important;
|
||||
color: #334155 !important;
|
||||
@@ -252,4 +585,58 @@ onMounted(() => {
|
||||
.oa-detail__btn--close:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.oa-detail__btn--ghost {
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
background: #fff !important;
|
||||
color: #334155 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-detail__btn--primary {
|
||||
flex: 1 1 88px;
|
||||
border: none !important;
|
||||
background: var(--brand-green, #22c55e) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
.oa-detail__dialog-body {
|
||||
padding: 8px 16px 4px;
|
||||
}
|
||||
.oa-act-sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
.oa-act-sheet__hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
}
|
||||
.oa-act-sheet__hd strong {
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
}
|
||||
.oa-act-sheet__ok {
|
||||
color: var(--brand-green, #22c55e);
|
||||
font-weight: 700;
|
||||
}
|
||||
.oa-act-sheet__body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px 16px 24px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.oa-act-sheet__label {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
.oa-act-sheet__label:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user