merge: t20 员工端待办已办与通过驳回撤回

This commit is contained in:
2026-10-30 20:46:30 +08:00
5 changed files with 352 additions and 58 deletions
+30 -1
View File
@@ -4,7 +4,8 @@ import type {
ApprovalCompletionForm,
ApprovalInstance,
ApprovalItem,
ApprovalMineParam
ApprovalMineParam,
ApprovalTaskListParam
} from '../model';
import { THINK_API_URL } from '@/config/setting';
@@ -61,3 +62,31 @@ export async function pageMine(params: ApprovalMineParam) {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 我的待办
*/
export async function pageTodo(params: ApprovalTaskListParam) {
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
THINK_API_URL + '/approval/todo/page',
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 我的已办
*/
export async function pageDone(params: ApprovalTaskListParam) {
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
THINK_API_URL + '/approval/done/page',
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
+15
View File
@@ -136,6 +136,21 @@ export interface ApprovalMineParam extends PageParam {
docType?: string;
}
/**
* 待办/已办查询参数
*/
export interface ApprovalTaskListParam extends PageParam {
status?: string;
docType?: string;
}
/**
* 任务动作参数(通过/驳回)
*/
export interface ApprovalTaskActionParam {
opinion?: string;
}
/**
* 组织
*/
+42
View File
@@ -1,5 +1,6 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import type { ApprovalInstance } from '../model';
import { THINK_API_URL } from '@/config/setting';
/**
@@ -14,3 +15,44 @@ export async function getTodoCount() {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 通过(意见可选)
*/
export async function approveTask(id: number, opinion?: string) {
const res = await request.post<ApiResult<ApprovalInstance>>(
THINK_API_URL + '/approval/task/' + id + '/approve',
{ opinion }
);
if (res.data.code === 0 && res.data.data) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 驳回(意见必填)
*/
export async function rejectTask(id: number, opinion: string) {
const res = await request.post<ApiResult<ApprovalInstance>>(
THINK_API_URL + '/approval/task/' + id + '/reject',
{ opinion }
);
if (res.data.code === 0 && res.data.data) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 撤回
*/
export async function withdrawInstance(id: number) {
const res = await request.post<ApiResult<ApprovalInstance>>(
THINK_API_URL + '/approval/instance/' + id + '/withdraw'
);
if (res.data.code === 0 && res.data.data) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
+217 -8
View File
@@ -109,13 +109,35 @@
<view v-if="detail.records.length === 0" class="empty">暂无记录</view>
</uni-card>
<view v-if="detail.permissions.canEdit || detail.permissions.canSubmit" class="actions">
<view v-if="hasActions" class="actions">
<button
v-if="detail.permissions.canEdit"
class="action-button ghost"
@click="editDraft"
v-if="detail.myTask && detail.permissions.canApprove"
class="action-button primary"
@click="openApprove"
>
编辑
通过
</button>
<button
v-if="detail.myTask && detail.permissions.canReject"
class="action-button danger"
@click="openReject"
>
驳回
</button>
<button
v-if="detail.permissions.canWithdraw"
class="action-button ghost"
:loading="submitting"
@click="withdraw"
>
撤回
</button>
<button
v-if="showEditButton"
class="action-button ghost"
@click="editAndResubmit"
>
{{ editLabel }}
</button>
<button
v-if="detail.permissions.canSubmit"
@@ -123,9 +145,27 @@
:loading="submitting"
@click="submitDraft"
>
提交
{{ detail.status === 'DRAFT' ? '提交' : '重新提交' }}
</button>
</view>
<view v-if="actionDialog" class="dialog-mask">
<view class="dialog">
<text class="dialog-title">{{ actionDialog === 'approve' ? '审批意见' : '驳回意见' }}</text>
<textarea
v-model="opinion"
class="dialog-input"
:maxlength="500"
:placeholder="actionDialog === 'approve' ? '请输入意见(可选)' : '请输入驳回意见(必填)'"
/>
<view class="dialog-actions">
<button class="dialog-button ghost" @click="closeDialog">取消</button>
<button class="dialog-button primary" :loading="submitting" @click="confirmAction">
确定
</button>
</view>
</view>
</view>
</view>
<view v-else class="loading">
<text>{{ loading ? '加载中...' : '单据不存在' }}</text>
@@ -134,9 +174,14 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getInstance, submitInstance } from '@/api/approval/instance';
import {
approveTask,
rejectTask,
withdrawInstance
} from '@/api/approval/task';
import type { ApprovalInstance } from '@/api/approval/model';
import { showError } from '@/utils/app';
@@ -144,6 +189,28 @@ const detail = ref<ApprovalInstance | null>(null);
const loading = ref(false);
const submitting = ref(false);
const instanceId = ref(0);
const actionDialog = ref<'' | 'approve' | 'reject'>('');
const opinion = ref('');
const hasActions = computed(() => {
const permissions = detail.value?.permissions;
if (!permissions) {
return false;
}
return (
permissions.canApprove ||
permissions.canReject ||
permissions.canWithdraw ||
permissions.canSubmit ||
permissions.canEdit
);
});
const showEditButton = computed(() => !!detail.value?.permissions.canEdit);
const editLabel = computed(() =>
detail.value?.status === 'REJECTED' || detail.value?.status === 'WITHDRAWN'
? '修改并重新提交'
: '编辑'
);
onLoad(options => {
instanceId.value = Number(options?.id) || 0;
@@ -164,13 +231,85 @@ async function load() {
}
}
function editDraft() {
function editAndResubmit() {
if (detail.value?.docType !== 'completion') {
showError('该单据类型暂不支持在线修改', null);
return;
}
uni.navigateTo({
url:
'/packageApproval/pages/form-completion?mode=edit&id=' + instanceId.value
});
}
function openApprove() {
opinion.value = '';
actionDialog.value = 'approve';
}
function openReject() {
opinion.value = '';
actionDialog.value = 'reject';
}
function closeDialog() {
if (submitting.value) {
return;
}
actionDialog.value = '';
}
async function confirmAction() {
const taskId = detail.value?.myTask?.id;
if (!taskId) {
showError('任务已处理,请刷新后重试', null);
return;
}
const isApprove = actionDialog.value === 'approve';
if (!isApprove && !opinion.value.trim()) {
uni.showToast({ title: '请填写驳回意见', icon: 'none' });
return;
}
submitting.value = true;
try {
const response = isApprove
? await approveTask(taskId, opinion.value.trim() || undefined)
: await rejectTask(taskId, opinion.value.trim());
actionDialog.value = '';
uni.showToast({
title: response.message || (isApprove ? '已通过' : '已驳回'),
icon: 'none'
});
await load();
} catch (error: any) {
showError(error?.message || '操作失败', null);
} finally {
submitting.value = false;
}
}
function withdraw() {
uni.showModal({
title: '撤回确认',
content: '撤回后单据回到报送人,可修改后重新提交。确认撤回?',
success: async result => {
if (!result.confirm) {
return;
}
submitting.value = true;
try {
const response = await withdrawInstance(instanceId.value);
uni.showToast({ title: response.message || '撤回成功', icon: 'none' });
await load();
} catch (error: any) {
showError(error?.message || '撤回失败', null);
} finally {
submitting.value = false;
}
}
});
}
function submitDraft() {
uni.showModal({
title: '提交确认',
@@ -411,12 +550,14 @@ function formatSize(value?: number) {
.actions {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
padding: 24rpx 32rpx 48rpx;
}
.action-button {
flex: 1;
min-width: 200rpx;
font-size: 30rpx;
border-radius: 12rpx;
}
@@ -432,6 +573,74 @@ function formatSize(value?: number) {
border: 1rpx solid #dcdfe6;
}
.action-button.danger {
background: #ffffff;
color: #cf1322;
border: 1rpx solid #cf1322;
}
.dialog-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog {
width: 600rpx;
padding: 32rpx;
border-radius: 16rpx;
background: #ffffff;
display: flex;
flex-direction: column;
}
.dialog-title {
font-size: 30rpx;
font-weight: 600;
color: #303133;
}
.dialog-input {
width: 100%;
height: 180rpx;
margin-top: 20rpx;
padding: 16rpx;
box-sizing: border-box;
font-size: 26rpx;
background: #f5f6f8;
border-radius: 12rpx;
}
.dialog-actions {
display: flex;
gap: 20rpx;
margin-top: 24rpx;
}
.dialog-button {
flex: 1;
font-size: 28rpx;
border-radius: 12rpx;
}
.dialog-button.primary {
background: #d81e06;
color: #ffffff;
}
.dialog-button.ghost {
background: #ffffff;
color: #303133;
border: 1rpx solid #dcdfe6;
}
.loading {
padding: 160rpx 0;
text-align: center;
+48 -49
View File
@@ -12,7 +12,7 @@
</view>
</view>
<view v-if="activeTab === 'mine'" class="toolbar">
<view class="toolbar">
<picker :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="filter">{{ statusLabels[statusIndex] }}</view>
</picker>
@@ -20,18 +20,13 @@
v-model="keywords"
class="search"
type="text"
placeholder="搜索单号或名称"
placeholder="搜索单号、名称或标题"
confirm-type="search"
@confirm="reload"
/>
</view>
<view v-if="activeTab !== 'mine'" class="placeholder">
<text class="placeholder-title">{{ activeLabel }}功能建设中</text>
<text class="placeholder-desc">当前版本先开放「我发起的」</text>
</view>
<view v-else class="list">
<view class="list">
<uni-card
v-for="item in list"
:key="item.id"
@@ -61,7 +56,7 @@
</uni-card>
<view v-if="!loading && list.length === 0" class="empty">
<text>暂无单据</text>
<text>{{ emptyText }}</text>
</view>
<view v-if="loading" class="loading">
<text>加载中...</text>
@@ -79,9 +74,14 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onLoad, onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app';
import { pageMine } from '@/api/approval/instance';
import type { ApprovalItem } from '@/api/approval/model';
import {
onLoad,
onPullDownRefresh,
onReachBottom,
onShow
} from '@dcloudio/uni-app';
import { pageDone, pageMine, pageTodo } from '@/api/approval/instance';
import type { ApprovalItem, ApprovalTaskListParam } from '@/api/approval/model';
type TabKey = 'todo' | 'mine' | 'done';
@@ -102,10 +102,16 @@ const statusOptions = [
];
const statusLabels = statusOptions.map(option => option.label);
const activeTab = ref<TabKey>('mine');
const activeLabel = computed(
() => tabs.find(tab => tab.key === activeTab.value)?.label || ''
);
const activeTab = ref<TabKey>('todo');
const emptyText = computed(() => {
if (activeTab.value === 'todo') {
return '暂无待办';
}
if (activeTab.value === 'done') {
return '暂无已办';
}
return '暂无单据';
});
const statusIndex = ref(0);
const keywords = ref('');
const list = ref<ApprovalItem[]>([]);
@@ -114,26 +120,23 @@ const limit = 10;
const total = ref(0);
const loading = ref(false);
const finished = ref(false);
let requestSeq = 0;
onLoad(() => {
load(true);
});
onShow(() => {
if (activeTab.value === 'mine') {
load(true);
}
load(true);
});
onPullDownRefresh(async () => {
if (activeTab.value === 'mine') {
await load(true);
}
await load(true);
uni.stopPullDownRefresh();
});
onReachBottom(() => {
if (activeTab.value === 'mine' && !loading.value && !finished.value) {
if (!loading.value && !finished.value) {
load(false);
}
});
@@ -143,9 +146,8 @@ function switchTab(key: TabKey) {
return;
}
activeTab.value = key;
if (key === 'mine' && list.value.length === 0) {
load(true);
}
list.value = [];
load(true);
}
function onStatusChange(event: { detail: { value: string | number } }) {
@@ -154,21 +156,32 @@ function onStatusChange(event: { detail: { value: string | number } }) {
}
async function load(reset: boolean) {
if (loading.value) {
if (loading.value && !reset) {
return;
}
const seq = ++requestSeq;
const tab = activeTab.value;
loading.value = true;
try {
if (reset) {
page.value = 1;
finished.value = false;
}
const result = await pageMine({
const params: ApprovalTaskListParam = {
page: page.value,
limit,
keywords: keywords.value || undefined,
status: statusOptions[statusIndex.value].value || undefined
});
};
const result =
tab === 'todo'
? await pageTodo(params)
: tab === 'done'
? await pageDone(params)
: await pageMine(params);
if (seq !== requestSeq) {
return;
}
total.value = Number(result.count) || 0;
list.value = reset ? result.list || [] : [...list.value, ...(result.list || [])];
finished.value = list.value.length >= total.value;
@@ -176,9 +189,13 @@ async function load(reset: boolean) {
page.value += 1;
}
} catch (error: any) {
uni.showToast({ title: error?.message || '加载失败', icon: 'none' });
if (seq === requestSeq) {
uni.showToast({ title: error?.message || '加载失败', icon: 'none' });
}
} finally {
loading.value = false;
if (seq === requestSeq) {
loading.value = false;
}
}
}
@@ -342,24 +359,6 @@ function formatTime(value?: string) {
word-break: break-all;
}
.placeholder {
padding: 160rpx 40rpx;
text-align: center;
}
.placeholder-title {
display: block;
font-size: 30rpx;
color: #606266;
}
.placeholder-desc {
display: block;
margin-top: 16rpx;
font-size: 26rpx;
color: #909399;
}
.empty,
.loading,
.finished {