feat(approval): 员工端审批中心入口与业务完成确认单页面 (#19)

This commit is contained in:
2026-10-30 20:20:34 +08:00
parent ae8bcb1e57
commit 370091ed6e
9 changed files with 1794 additions and 2 deletions
+391
View File
@@ -0,0 +1,391 @@
<template>
<view class="approval-page">
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</view>
</view>
<view v-if="activeTab === 'mine'" class="toolbar">
<picker :range="statusLabels" :value="statusIndex" @change="onStatusChange">
<view class="filter">{{ statusLabels[statusIndex] }}</view>
</picker>
<input
v-model="keywords"
class="search"
type="text"
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">
<uni-card
v-for="item in list"
:key="item.id"
:border="false"
:padding="'14px 16px'"
class="item-card"
@click="openDetail(item.id)"
>
<view class="item-head">
<text class="item-title">{{ item.title || '未命名单据' }}</text>
<text class="status" :class="'status-' + item.status">
{{ item.statusName }}
</text>
</view>
<view class="item-row">
<text class="label">单号</text>
<text class="value">{{ item.bizNo || '—' }}</text>
</view>
<view class="item-row">
<text class="label">当前节点</text>
<text class="value">{{ item.currentNodeName || '—' }}</text>
</view>
<view class="item-row">
<text class="label">提交时间</text>
<text class="value">{{ formatTime(item.submitTime) }}</text>
</view>
</uni-card>
<view v-if="!loading && list.length === 0" class="empty">
<text>暂无单据</text>
</view>
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<view v-if="finished && list.length > 0" class="finished">
<text>没有更多了</text>
</view>
</view>
<view v-if="activeTab === 'mine'" class="fab" @click="createDraft">
<text class="fab-text">+</text>
</view>
</view>
</template>
<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';
type TabKey = 'todo' | 'mine' | 'done';
const tabs: Array<{ key: TabKey; label: string }> = [
{ key: 'todo', label: '待办' },
{ key: 'mine', label: '我发起的' },
{ key: 'done', label: '已办' }
];
const statusOptions = [
{ value: '', label: '全部状态' },
{ value: 'DRAFT', label: '未提交' },
{ value: 'PENDING', label: '审批中' },
{ value: 'REJECTED', label: '已驳回' },
{ value: 'WITHDRAWN', label: '已撤回' },
{ value: 'APPROVED', label: '审批完成' },
{ value: 'ARCHIVED', label: '已归档' }
];
const statusLabels = statusOptions.map(option => option.label);
const activeTab = ref<TabKey>('mine');
const activeLabel = computed(
() => tabs.find(tab => tab.key === activeTab.value)?.label || ''
);
const statusIndex = ref(0);
const keywords = ref('');
const list = ref<ApprovalItem[]>([]);
const page = ref(1);
const limit = 10;
const total = ref(0);
const loading = ref(false);
const finished = ref(false);
onLoad(() => {
load(true);
});
onShow(() => {
if (activeTab.value === 'mine') {
load(true);
}
});
onPullDownRefresh(async () => {
if (activeTab.value === 'mine') {
await load(true);
}
uni.stopPullDownRefresh();
});
onReachBottom(() => {
if (activeTab.value === 'mine' && !loading.value && !finished.value) {
load(false);
}
});
function switchTab(key: TabKey) {
if (activeTab.value === key) {
return;
}
activeTab.value = key;
if (key === 'mine' && list.value.length === 0) {
load(true);
}
}
function onStatusChange(event: { detail: { value: string | number } }) {
statusIndex.value = Number(event.detail.value);
load(true);
}
async function load(reset: boolean) {
if (loading.value) {
return;
}
loading.value = true;
try {
if (reset) {
page.value = 1;
finished.value = false;
}
const result = await pageMine({
page: page.value,
limit,
keywords: keywords.value || undefined,
status: statusOptions[statusIndex.value].value || undefined
});
total.value = Number(result.count) || 0;
list.value = reset ? result.list || [] : [...list.value, ...(result.list || [])];
finished.value = list.value.length >= total.value;
if (!finished.value) {
page.value += 1;
}
} catch (error: any) {
uni.showToast({ title: error?.message || '加载失败', icon: 'none' });
} finally {
loading.value = false;
}
}
function reload() {
load(true);
}
function openDetail(id: number) {
uni.navigateTo({ url: '/packageApproval/pages/detail?id=' + id });
}
function createDraft() {
uni.navigateTo({
url: '/packageApproval/pages/form-completion?mode=create'
});
}
function formatTime(value?: string) {
if (!value) {
return '—';
}
return value.replace('T', ' ').substring(0, 19);
}
</script>
<style lang="scss">
.approval-page {
min-height: 100vh;
background-color: #f5f6f8;
padding-bottom: 140rpx;
}
.tabs {
display: flex;
background: #ffffff;
padding: 12rpx 24rpx;
position: sticky;
top: 0;
z-index: 10;
}
.tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 28rpx;
color: #606266;
border-bottom: 4rpx solid transparent;
}
.tab.active {
color: #d81e06;
font-weight: 600;
border-bottom-color: #d81e06;
}
.toolbar {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
background: #ffffff;
}
.filter {
padding: 12rpx 20rpx;
font-size: 26rpx;
color: #303133;
background: #f2f3f5;
border-radius: 12rpx;
white-space: nowrap;
}
.search {
flex: 1;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
background: #f2f3f5;
border-radius: 12rpx;
}
.list {
padding: 8rpx 0;
}
.item-card {
margin: 12rpx 16rpx;
}
.item-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.item-title {
flex: 1;
font-size: 30rpx;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
font-size: 24rpx;
padding: 4rpx 14rpx;
border-radius: 999rpx;
background: #f2f3f5;
color: #606266;
}
.status-DRAFT {
background: #f4f4f5;
color: #909399;
}
.status-PENDING {
background: #fff7e6;
color: #d46b08;
}
.status-REJECTED {
background: #fff1f0;
color: #cf1322;
}
.status-WITHDRAWN {
background: #f4f4f5;
color: #909399;
}
.status-APPROVED {
background: #f6ffed;
color: #389e0d;
}
.status-ARCHIVED {
background: #e6f4ff;
color: #0958d9;
}
.item-row {
display: flex;
align-items: center;
margin-top: 8rpx;
font-size: 26rpx;
}
.label {
width: 140rpx;
color: #909399;
}
.value {
flex: 1;
color: #303133;
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 {
padding: 40rpx;
text-align: center;
font-size: 26rpx;
color: #909399;
}
.fab {
position: fixed;
right: 40rpx;
bottom: 80rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #d81e06;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(216, 30, 6, 0.35);
}
.fab-text {
color: #ffffff;
font-size: 56rpx;
line-height: 1;
}
</style>