merge: 一期线下审批(#18-#26)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import type { ApprovalAttachment } from '../model';
|
||||
import { TENANT_ID, THINK_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 上传附件:走审批域端点,header 带登录态与租户
|
||||
*/
|
||||
export function uploadAttachment(
|
||||
instanceId: number,
|
||||
filePath: string,
|
||||
category: string
|
||||
): Promise<ApprovalAttachment> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = uni.getStorageSync('access_token');
|
||||
uni.uploadFile({
|
||||
url:
|
||||
THINK_API_URL +
|
||||
'/approval/attachment/upload?instanceId=' +
|
||||
instanceId,
|
||||
name: 'file',
|
||||
filePath,
|
||||
formData: { category },
|
||||
header: {
|
||||
TenantId: TENANT_ID,
|
||||
...(token ? { Authorization: token } : {})
|
||||
},
|
||||
success: res => {
|
||||
try {
|
||||
const body =
|
||||
typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||
if (body.code === 0 && body.data) {
|
||||
resolve(body.data as ApprovalAttachment);
|
||||
return;
|
||||
}
|
||||
reject(new Error(body.message || '上传失败'));
|
||||
} catch (err) {
|
||||
reject(new Error('上传失败'));
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error('上传失败'))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 逻辑删除附件
|
||||
*/
|
||||
export async function deleteAttachment(id: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
THINK_API_URL + '/approval/attachment/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return true;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import type { ApprovalOrganization } from '../model';
|
||||
import { THINK_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 启用组织(内部协调单受理部门选择)
|
||||
*/
|
||||
export async function getOrganizations() {
|
||||
const res = await request.get<ApiResult<ApprovalOrganization[]>>(
|
||||
THINK_API_URL + '/approval/directory/organizations'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type {
|
||||
ApprovalCompletionForm,
|
||||
ApprovalCoordinationForm,
|
||||
ApprovalInstance,
|
||||
ApprovalItem,
|
||||
ApprovalMineParam,
|
||||
ApprovalTaskListParam
|
||||
} from '../model';
|
||||
import { THINK_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 新建或保存业务完成确认单草稿,返回实例id
|
||||
*/
|
||||
export async function saveCompletion(data: ApprovalCompletionForm) {
|
||||
const res = await request.post<ApiResult<number>>(
|
||||
THINK_API_URL + '/approval/instance/completion',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as number;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建或保存内部协调单草稿,返回实例id
|
||||
*/
|
||||
export async function saveCoordination(data: ApprovalCoordinationForm) {
|
||||
const res = await request.post<ApiResult<number>>(
|
||||
THINK_API_URL + '/approval/instance/coordination',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as number;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交或重提
|
||||
*/
|
||||
export async function submitInstance(id: number) {
|
||||
const res = await request.post<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/instance/' + id + '/submit'
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单据详情
|
||||
*/
|
||||
export async function getInstance(id: number) {
|
||||
const res = await request.get<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/instance/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 我发起的
|
||||
*/
|
||||
export async function pageMine(params: ApprovalMineParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
|
||||
THINK_API_URL + '/approval/mine/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 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));
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 审批附件
|
||||
*/
|
||||
export interface ApprovalAttachment {
|
||||
id: number;
|
||||
fileId: number;
|
||||
name: string;
|
||||
url: string;
|
||||
length: number;
|
||||
category: string;
|
||||
categoryName: string;
|
||||
uploaderId: number;
|
||||
uploaderName?: string;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理记录
|
||||
*/
|
||||
export interface ApprovalRecord {
|
||||
id: number;
|
||||
taskId?: number;
|
||||
nodeKey?: string;
|
||||
nodeName?: string;
|
||||
operatorId: number;
|
||||
operatorName?: string;
|
||||
action: string;
|
||||
actionName: string;
|
||||
opinion?: string;
|
||||
round: number;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前轮存档确认状态
|
||||
*/
|
||||
export interface ApprovalConfirm {
|
||||
userId: number;
|
||||
userName?: string;
|
||||
status: string;
|
||||
statusName: string;
|
||||
handleTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户待办任务
|
||||
*/
|
||||
export interface ApprovalMyTask {
|
||||
id: number;
|
||||
nodeKey: string;
|
||||
nodeType: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情动作权限
|
||||
*/
|
||||
export interface ApprovalPermissions {
|
||||
canEdit: boolean;
|
||||
canSubmit: boolean;
|
||||
canWithdraw: boolean;
|
||||
canApprove: boolean;
|
||||
canReject: boolean;
|
||||
canConfirm: boolean;
|
||||
canArchive: boolean;
|
||||
canDispatch: boolean;
|
||||
canAssign: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表行
|
||||
*/
|
||||
export interface ApprovalItem {
|
||||
id: number;
|
||||
bizNo?: string;
|
||||
docType: string;
|
||||
docTypeName: string;
|
||||
status: string;
|
||||
statusName: string;
|
||||
title?: string;
|
||||
submitTime?: string;
|
||||
currentNodeName?: string;
|
||||
reporterId: number;
|
||||
reporterName?: string;
|
||||
reportOrgName?: string;
|
||||
acceptOrgName?: string;
|
||||
archiveTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单据详情
|
||||
*/
|
||||
export interface ApprovalInstance extends ApprovalItem {
|
||||
flowCode: string;
|
||||
round: number;
|
||||
reportOrgId?: number;
|
||||
acceptOrgId?: number;
|
||||
currentNodeKey?: string;
|
||||
currentNodeType?: string;
|
||||
approveTime?: string;
|
||||
archivistId?: number;
|
||||
archivistName?: string;
|
||||
name?: string;
|
||||
implementOrg?: string;
|
||||
fee?: number;
|
||||
serviceContent?: string;
|
||||
content?: string;
|
||||
remark?: string;
|
||||
dispatchTargetId?: number;
|
||||
dispatchTargetName?: string;
|
||||
attachments: ApprovalAttachment[];
|
||||
records: ApprovalRecord[];
|
||||
confirms: ApprovalConfirm[];
|
||||
myTask?: ApprovalMyTask | null;
|
||||
permissions: ApprovalPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务完成确认单草稿参数
|
||||
*/
|
||||
export interface ApprovalCompletionForm {
|
||||
id?: number;
|
||||
name: string;
|
||||
implementOrg: string;
|
||||
fee: number | string;
|
||||
serviceContent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部协调单草稿参数
|
||||
*/
|
||||
export interface ApprovalCoordinationForm {
|
||||
id?: number;
|
||||
acceptOrgId?: number;
|
||||
title: string;
|
||||
content: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我发起的查询参数
|
||||
*/
|
||||
export interface ApprovalMineParam extends PageParam {
|
||||
status?: string;
|
||||
docType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待办/已办查询参数
|
||||
*/
|
||||
export interface ApprovalTaskListParam extends PageParam {
|
||||
status?: string;
|
||||
docType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务动作参数(通过/驳回)
|
||||
*/
|
||||
export interface ApprovalTaskActionParam {
|
||||
opinion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组织
|
||||
*/
|
||||
export interface ApprovalOrganization {
|
||||
organizationId: number;
|
||||
parentId?: number;
|
||||
organizationName: string;
|
||||
organizationFullName?: string;
|
||||
organizationCode?: string;
|
||||
leaderId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批目录用户(派单候选池)
|
||||
*/
|
||||
export interface ApprovalUser {
|
||||
userId: number;
|
||||
username?: string;
|
||||
nickname?: string;
|
||||
realName?: string;
|
||||
organizationId?: number;
|
||||
organizationName?: string;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import type { ApprovalInstance, ApprovalUser } from '../model';
|
||||
import { THINK_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 待办数量(个人中心红点)
|
||||
*/
|
||||
export async function getTodoCount() {
|
||||
const res = await request.get<ApiResult<number>>(
|
||||
THINK_API_URL + '/approval/todo/count'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data !== undefined) {
|
||||
return res.data.data;
|
||||
}
|
||||
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 confirmTask(id: number) {
|
||||
const res = await request.post<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/confirm'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档(或签,归档后单据只读)
|
||||
*/
|
||||
export async function archiveTask(id: number) {
|
||||
const res = await request.post<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/archive'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单候选池(仅派单节点、本人待办)
|
||||
*/
|
||||
export async function listDispatchCandidates(id: number) {
|
||||
const res = await request.get<ApiResult<ApprovalUser[]>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/candidates'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单(选定 1 人即完成派单节点)
|
||||
*/
|
||||
export async function dispatchTask(id: number, targetUserId: number) {
|
||||
const res = await request.post<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/dispatch',
|
||||
{ targetUserId }
|
||||
);
|
||||
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));
|
||||
}
|
||||
@@ -82,7 +82,6 @@ export interface UserCard {
|
||||
createTime?: number;
|
||||
//
|
||||
updateTime?: number;
|
||||
@TableField("IDCard")
|
||||
// 身份证号码
|
||||
idcard?: string;
|
||||
// 备注
|
||||
|
||||
@@ -80,7 +80,6 @@ export interface ThinkCard {
|
||||
createTime?: number;
|
||||
//
|
||||
updateTime?: number;
|
||||
@TableField("IDCard")
|
||||
// 身份证号码
|
||||
idcard?: string;
|
||||
// 备注
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface ThinkGatePassreord {
|
||||
deleteStatus?: string;
|
||||
//
|
||||
siteId?: number;
|
||||
@TableField("consume_Money")
|
||||
// 本次消费金额
|
||||
consumeMoney?: string;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ export interface ThinkInvoice {
|
||||
createTime?: number;
|
||||
//
|
||||
updateTime?: number;
|
||||
@TableField("InvoiceType")
|
||||
// 1.普票,2是专票
|
||||
invoicetype?: number;
|
||||
//
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<view v-if="detail" class="content">
|
||||
<uni-card :border="false" :padding="'16px'">
|
||||
<view class="head">
|
||||
<text class="title">{{ detail.title || detail.name || '单据详情' }}</text>
|
||||
<text class="status" :class="'status-' + detail.status">
|
||||
{{ detail.statusName }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="rows">
|
||||
<view class="row">
|
||||
<text class="label">单号</text>
|
||||
<text class="value">{{ detail.bizNo || '—' }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">单据类型</text>
|
||||
<text class="value">{{ detail.docTypeName }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">报送人</text>
|
||||
<text class="value">{{ detail.reporterName || '—' }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">报送部门</text>
|
||||
<text class="value">{{ detail.reportOrgName || '—' }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">当前节点</text>
|
||||
<text class="value">{{ detail.currentNodeName || '—' }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">提交时间</text>
|
||||
<text class="value">{{ formatTime(detail.submitTime) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">审批完成时间</text>
|
||||
<text class="value">{{ formatTime(detail.approveTime) }}</text>
|
||||
</view>
|
||||
<view v-if="detail.archiveTime" class="row">
|
||||
<text class="label">归档时间</text>
|
||||
<text class="value">{{ formatTime(detail.archiveTime) }}</text>
|
||||
</view>
|
||||
<view v-if="detail.archiveTime" class="row">
|
||||
<text class="label">归档人</text>
|
||||
<text class="value">{{ detail.archivistName || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :border="false" :padding="'16px'" title="单据信息">
|
||||
<view v-if="detail.docType === 'completion'" class="rows">
|
||||
<view class="row">
|
||||
<text class="label">名称</text>
|
||||
<text class="value">{{ detail.name }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">实施单位</text>
|
||||
<text class="value">{{ detail.implementOrg }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">费用(元)</text>
|
||||
<text class="value">{{ formatFee(detail.fee) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">服务内容</text>
|
||||
<text class="value multiline">{{ detail.serviceContent }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="rows">
|
||||
<view class="row">
|
||||
<text class="label">受理部门</text>
|
||||
<text class="value">{{ detail.acceptOrgName || '—' }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">标题</text>
|
||||
<text class="value">{{ detail.title }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">内容</text>
|
||||
<text class="value multiline">{{ detail.content }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">备注</text>
|
||||
<text class="value multiline">{{ detail.remark || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :border="false" :padding="'16px'" title="附件">
|
||||
<view
|
||||
v-for="item in detail.attachments"
|
||||
:key="item.id"
|
||||
class="attachment"
|
||||
@click="openAttachment(item.url)"
|
||||
>
|
||||
<view class="attachment-main">
|
||||
<text class="attachment-name">{{ item.name }}</text>
|
||||
<text class="attachment-meta">
|
||||
{{ item.categoryName }} · {{ formatSize(item.length) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="link">查看</text>
|
||||
</view>
|
||||
<view v-if="detail.attachments.length === 0" class="empty">暂无附件</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :border="false" :padding="'16px'" title="处理记录">
|
||||
<view v-for="item in detail.records" :key="item.id" class="timeline-item">
|
||||
<view class="timeline-dot" :class="dotClass(item.action)"></view>
|
||||
<view class="timeline-body">
|
||||
<view class="timeline-head">
|
||||
<text class="timeline-operator">{{ item.operatorName || '—' }}</text>
|
||||
<text class="timeline-action">{{ item.actionName }}</text>
|
||||
</view>
|
||||
<text class="timeline-node">{{ item.nodeName || '提交' }}</text>
|
||||
<text v-if="item.opinion" class="timeline-opinion">意见:{{ item.opinion }}</text>
|
||||
<text class="timeline-time">{{ formatTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="detail.records.length === 0" class="empty">暂无记录</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card
|
||||
v-if="detail.confirms && detail.confirms.length > 0"
|
||||
:border="false"
|
||||
:padding="'16px'"
|
||||
title="存档确认"
|
||||
>
|
||||
<view v-for="item in detail.confirms" :key="item.userId" class="confirm-item">
|
||||
<view class="confirm-main">
|
||||
<text class="confirm-name">{{ item.userName || '—' }}</text>
|
||||
<text class="confirm-time">{{ item.handleTime ? formatTime(item.handleTime) : '—' }}</text>
|
||||
</view>
|
||||
<text class="confirm-status" :class="'confirm-' + item.status">
|
||||
{{ item.statusName }}
|
||||
</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<view v-if="hasActions" class="actions">
|
||||
<button
|
||||
v-if="detail.myTask && detail.permissions.canApprove"
|
||||
class="action-button primary"
|
||||
@click="openApprove"
|
||||
>
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
v-if="detail.myTask && detail.permissions.canDispatch"
|
||||
class="action-button primary"
|
||||
@click="openDispatch"
|
||||
>
|
||||
派单
|
||||
</button>
|
||||
<button
|
||||
v-if="detail.myTask && detail.permissions.canReject"
|
||||
class="action-button danger"
|
||||
@click="openReject"
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
<button
|
||||
v-if="detail.myTask && detail.permissions.canConfirm"
|
||||
class="action-button primary"
|
||||
:loading="submitting"
|
||||
@click="confirmArchiveAction('confirm')"
|
||||
>
|
||||
存档确认
|
||||
</button>
|
||||
<button
|
||||
v-if="detail.myTask && detail.permissions.canArchive"
|
||||
class="action-button primary"
|
||||
:loading="submitting"
|
||||
@click="confirmArchiveAction('archive')"
|
||||
>
|
||||
归档
|
||||
</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"
|
||||
class="action-button primary"
|
||||
: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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { getInstance, submitInstance } from '@/api/approval/instance';
|
||||
import {
|
||||
approveTask,
|
||||
archiveTask,
|
||||
confirmTask,
|
||||
rejectTask,
|
||||
withdrawInstance
|
||||
} from '@/api/approval/task';
|
||||
import type { ApprovalInstance } from '@/api/approval/model';
|
||||
import { showError } from '@/utils/app';
|
||||
|
||||
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 || detail.value?.status === 'ARCHIVED') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
permissions.canApprove ||
|
||||
permissions.canReject ||
|
||||
permissions.canDispatch ||
|
||||
permissions.canConfirm ||
|
||||
permissions.canArchive ||
|
||||
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 加载;从派单/编辑页返回时重新拉取,避免展示过期节点
|
||||
let entered = false;
|
||||
|
||||
onLoad(options => {
|
||||
instanceId.value = Number(options?.id) || 0;
|
||||
load();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (!entered) {
|
||||
entered = true;
|
||||
return;
|
||||
}
|
||||
load();
|
||||
});
|
||||
|
||||
async function load() {
|
||||
if (!instanceId.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
detail.value = await getInstance(instanceId.value);
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '加载失败', null);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function editAndResubmit() {
|
||||
const docType = detail.value?.docType;
|
||||
const page =
|
||||
docType === 'completion'
|
||||
? 'form-completion'
|
||||
: docType === 'coordination'
|
||||
? 'form-coordination'
|
||||
: '';
|
||||
if (!page) {
|
||||
showError('该单据类型暂不支持在线修改', null);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/packageApproval/pages/${page}?mode=edit&id=` + instanceId.value
|
||||
});
|
||||
}
|
||||
|
||||
function openApprove() {
|
||||
opinion.value = '';
|
||||
actionDialog.value = 'approve';
|
||||
}
|
||||
|
||||
function openDispatch() {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
showError('任务已处理,请刷新后重试', null);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/packageApproval/pages/dispatch?taskId=' + taskId
|
||||
});
|
||||
}
|
||||
|
||||
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 confirmArchiveAction(action: 'confirm' | 'archive') {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
showError('任务已处理,请刷新后重试', null);
|
||||
return;
|
||||
}
|
||||
const isConfirm = action === 'confirm';
|
||||
uni.showModal({
|
||||
title: isConfirm ? '存档确认' : '归档确认',
|
||||
content: isConfirm
|
||||
? '存档只能确认,确认后单据进入归档流程。确认存档?'
|
||||
: '归档后单据为只读终态,不可退回或修改。确认归档?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const response = isConfirm
|
||||
? await confirmTask(taskId)
|
||||
: await archiveTask(taskId);
|
||||
uni.showToast({
|
||||
title: response.message || (isConfirm ? '已存档' : '已归档'),
|
||||
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: '提交确认',
|
||||
content: '提交后字段与附件将锁定,确认提交?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const response = await submitInstance(instanceId.value);
|
||||
uni.showToast({
|
||||
title: response.message || '提交成功',
|
||||
icon: 'none'
|
||||
});
|
||||
await load();
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '提交失败', null);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openAttachment(url?: string) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank');
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: result => {
|
||||
if (result.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: result.tempFilePath,
|
||||
showMenu: true
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => uni.showToast({ title: '附件打开失败', icon: 'none' })
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) {
|
||||
return '—';
|
||||
}
|
||||
return value.replace('T', ' ').substring(0, 19);
|
||||
}
|
||||
|
||||
/** 处理记录圆点:通过/存档/归档绿色,驳回红色,其余蓝色 */
|
||||
function dotClass(action?: string) {
|
||||
if (action === 'REJECT') {
|
||||
return 'dot-reject';
|
||||
}
|
||||
if (action === 'APPROVE' || action === 'CONFIRM' || action === 'ARCHIVE') {
|
||||
return 'dot-approve';
|
||||
}
|
||||
return 'dot-default';
|
||||
}
|
||||
|
||||
function formatFee(value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '—';
|
||||
}
|
||||
return Number(value).toFixed(2);
|
||||
}
|
||||
|
||||
function formatSize(value?: number) {
|
||||
if (!value) {
|
||||
return '0 B';
|
||||
}
|
||||
if (value < 1024) {
|
||||
return value + ' B';
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return (value / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (value / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f2f3f5;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.status-PENDING {
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.status-REJECTED {
|
||||
background: #fff1f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.status-APPROVED {
|
||||
background: #f6ffed;
|
||||
color: #389e0d;
|
||||
}
|
||||
|
||||
.status-ARCHIVED {
|
||||
background: #e6f4ff;
|
||||
color: #0958d9;
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 160rpx;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.attachment-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 26rpx;
|
||||
color: #d81e06;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
margin: 10rpx 16rpx 0 0;
|
||||
border-radius: 50%;
|
||||
background: #3c9cff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-dot.dot-approve {
|
||||
background: #19be6b;
|
||||
}
|
||||
|
||||
.timeline-dot.dot-reject {
|
||||
background: #d81e06;
|
||||
}
|
||||
|
||||
.timeline-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.timeline-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.timeline-operator {
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-action {
|
||||
font-size: 24rpx;
|
||||
color: #d81e06;
|
||||
}
|
||||
|
||||
.timeline-node,
|
||||
.timeline-opinion,
|
||||
.timeline-time {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.confirm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.confirm-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.confirm-name {
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.confirm-time {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.confirm-status {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f2f3f5;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.confirm-PENDING {
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.confirm-DONE {
|
||||
background: #f6ffed;
|
||||
color: #389e0d;
|
||||
}
|
||||
|
||||
.confirm-CANCELLED {
|
||||
background: #f2f3f5;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #d81e06;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-button.ghost {
|
||||
background: #ffffff;
|
||||
color: #303133;
|
||||
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;
|
||||
color: #909399;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<view class="dispatch-page">
|
||||
<uni-card :border="false" :padding="'16px'" title="选择派单对象">
|
||||
<text class="hint">
|
||||
派单后由报送人与被派单人分别存档,请从受理部门启用成员中选择 1 人。
|
||||
</text>
|
||||
|
||||
<view
|
||||
v-for="item in candidates"
|
||||
:key="item.userId"
|
||||
class="candidate"
|
||||
:class="{ active: selectedId === item.userId }"
|
||||
@click="select(item)"
|
||||
>
|
||||
<view class="candidate-main">
|
||||
<text class="candidate-name">{{ displayName(item) }}</text>
|
||||
<text class="candidate-meta">
|
||||
{{ item.username }}
|
||||
<text v-if="item.organizationName"> · {{ item.organizationName }}</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="candidate-radio" :class="{ checked: selectedId === item.userId }">
|
||||
<view v-if="selectedId === item.userId" class="candidate-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="empty">加载中...</view>
|
||||
<view v-else-if="candidates.length === 0" class="empty">
|
||||
暂无可派单人员,请联系管理员维护受理部门成员
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<view class="footer">
|
||||
<button
|
||||
class="footer-button primary"
|
||||
:loading="submitting"
|
||||
:disabled="!selectedId || submitting"
|
||||
@click="confirmDispatch"
|
||||
>
|
||||
确认派单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { dispatchTask, listDispatchCandidates } from '@/api/approval/task';
|
||||
import type { ApprovalUser } from '@/api/approval/model';
|
||||
import { showError } from '@/utils/app';
|
||||
|
||||
const taskId = ref(0);
|
||||
const candidates = ref<ApprovalUser[]>([]);
|
||||
const selectedId = ref(0);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
onLoad(options => {
|
||||
taskId.value = Number(options?.taskId) || 0;
|
||||
load();
|
||||
});
|
||||
|
||||
async function load() {
|
||||
if (!taskId.value) {
|
||||
showError('任务不存在,请返回重试', null);
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
candidates.value = await listDispatchCandidates(taskId.value);
|
||||
} catch (error: any) {
|
||||
candidates.value = [];
|
||||
showError(error?.message || '候选人员加载失败', null);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function displayName(item: ApprovalUser) {
|
||||
return item.realName || item.nickname || item.username || '—';
|
||||
}
|
||||
|
||||
function select(item: ApprovalUser) {
|
||||
if (submitting.value) {
|
||||
return;
|
||||
}
|
||||
selectedId.value = item.userId;
|
||||
}
|
||||
|
||||
function confirmDispatch() {
|
||||
const target = candidates.value.find(item => item.userId === selectedId.value);
|
||||
if (!target) {
|
||||
showError('请选择派单对象', null);
|
||||
return;
|
||||
}
|
||||
if (!taskId.value) {
|
||||
showError('任务不存在,请返回重试', null);
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '派单确认',
|
||||
content: '确定将单据派给「' + displayName(target) + '」?派单后不能更改。',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const response = await dispatchTask(taskId.value, target.userId);
|
||||
uni.showToast({ title: response.message || '派单成功', icon: 'none' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ delta: 1 });
|
||||
}, 600);
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '派单失败,请稍后重试', null);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dispatch-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
line-height: 36rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.candidate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 20rpx;
|
||||
margin-top: 12rpx;
|
||||
border: 2rpx solid #ebeef5;
|
||||
border-radius: 12rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.candidate.active {
|
||||
border-color: #1677ff;
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.candidate-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.candidate-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.candidate-meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.candidate-radio {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid #c0c4cc;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.candidate-radio.checked {
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.candidate-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #1677ff;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.footer-button.primary {
|
||||
background-color: #1677ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.footer-button.primary[disabled] {
|
||||
background-color: #a0c4ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<view class="form-page">
|
||||
<uni-card :border="false" :padding="'16px'" title="基本信息">
|
||||
<view class="auto-row">
|
||||
<text class="label">报送人</text>
|
||||
<text class="value">{{ reporterName || '—' }}</text>
|
||||
</view>
|
||||
<view class="auto-row">
|
||||
<text class="label">报送部门</text>
|
||||
<text class="value">{{ reportOrgName || '未归属部门' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">名称<text class="required">*</text></text>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="field-input"
|
||||
type="text"
|
||||
maxlength="100"
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="field-label">实施单位<text class="required">*</text></text>
|
||||
<input
|
||||
v-model="form.implementOrg"
|
||||
class="field-input"
|
||||
type="text"
|
||||
maxlength="100"
|
||||
placeholder="请输入实施单位"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="field-label">费用(元)<text class="required">*</text></text>
|
||||
<input
|
||||
v-model="form.fee"
|
||||
class="field-input"
|
||||
type="digit"
|
||||
placeholder="0.00,最多两位小数"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="field-label">服务内容<text class="required">*</text></text>
|
||||
<textarea
|
||||
v-model="form.serviceContent"
|
||||
class="field-textarea"
|
||||
maxlength="2000"
|
||||
placeholder="请输入服务内容,最多2000字"
|
||||
/>
|
||||
<text class="counter">{{ form.serviceContent.length }}/2000</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :border="false" :padding="'16px'" title="附件">
|
||||
<view class="category-row">
|
||||
<text class="field-label">上传分类</text>
|
||||
<view class="chips">
|
||||
<view
|
||||
v-for="item in categories"
|
||||
:key="item.value"
|
||||
class="chip"
|
||||
:class="{ active: category === item.value }"
|
||||
@click="category = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-for="item in attachments" :key="item.id" class="attachment">
|
||||
<view class="attachment-main">
|
||||
<text class="attachment-name">{{ item.name }}</text>
|
||||
<text class="attachment-meta">
|
||||
{{ item.categoryName }} · {{ formatSize(item.length) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="delete" @click="removeAttachment(item)">删除</text>
|
||||
</view>
|
||||
|
||||
<view v-if="attachments.length === 0" class="empty">暂无附件</view>
|
||||
|
||||
<button class="upload-button" :loading="uploading" @click="chooseAttachment">
|
||||
上传附件({{ attachments.length }}/9)
|
||||
</button>
|
||||
<text class="hint">支持 jpg/jpeg/png/pdf/doc/docx/xls/xlsx,单张不超过20MB</text>
|
||||
</uni-card>
|
||||
|
||||
<view class="footer">
|
||||
<button class="footer-button ghost" :loading="saving" @click="saveDraft">
|
||||
保存草稿
|
||||
</button>
|
||||
<button class="footer-button primary" :loading="submitting" @click="submit">
|
||||
提交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import {
|
||||
getInstance,
|
||||
saveCompletion,
|
||||
submitInstance
|
||||
} from '@/api/approval/instance';
|
||||
import {
|
||||
deleteAttachment,
|
||||
uploadAttachment
|
||||
} from '@/api/approval/attachment';
|
||||
import type { ApprovalAttachment } from '@/api/approval/model';
|
||||
import { showError, showSuccess } from '@/utils/app';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const categories = [
|
||||
{ value: 'SIGNED', label: '签字件' },
|
||||
{ value: 'OTHER', label: '其他' }
|
||||
];
|
||||
|
||||
const mode = ref<'create' | 'edit'>('create');
|
||||
const instanceId = ref(0);
|
||||
const form = reactive({
|
||||
name: '',
|
||||
implementOrg: '',
|
||||
fee: '',
|
||||
serviceContent: ''
|
||||
});
|
||||
const attachments = ref<ApprovalAttachment[]>([]);
|
||||
const category = ref('SIGNED');
|
||||
const saving = ref(false);
|
||||
const submitting = ref(false);
|
||||
const uploading = ref(false);
|
||||
|
||||
const reporterName = computed(
|
||||
() => userStore.info?.realName || userStore.info?.nickname || ''
|
||||
);
|
||||
const reportOrgName = computed(() => userStore.info?.organizationName || '');
|
||||
|
||||
onLoad(options => {
|
||||
mode.value = options?.mode === 'edit' ? 'edit' : 'create';
|
||||
instanceId.value = Number(options?.id) || 0;
|
||||
if (instanceId.value) {
|
||||
loadDetail();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadDetail() {
|
||||
try {
|
||||
const detail = await getInstance(instanceId.value);
|
||||
if (!detail.permissions?.canEdit) {
|
||||
showError('当前状态不可修改', null);
|
||||
return;
|
||||
}
|
||||
form.name = detail.name || '';
|
||||
form.implementOrg = detail.implementOrg || '';
|
||||
form.fee =
|
||||
detail.fee === undefined || detail.fee === null
|
||||
? ''
|
||||
: Number(detail.fee).toFixed(2);
|
||||
form.serviceContent = detail.serviceContent || '';
|
||||
attachments.value = detail.attachments || [];
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '加载失败', null);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(): string | null {
|
||||
if (!form.name.trim()) {
|
||||
return '请输入名称';
|
||||
}
|
||||
if (form.name.trim().length > 100) {
|
||||
return '名称不能超过100字';
|
||||
}
|
||||
if (!form.implementOrg.trim()) {
|
||||
return '请输入实施单位';
|
||||
}
|
||||
if (form.implementOrg.trim().length > 100) {
|
||||
return '实施单位不能超过100字';
|
||||
}
|
||||
const fee = Number(form.fee);
|
||||
if (!form.fee || Number.isNaN(fee)) {
|
||||
return '请输入费用';
|
||||
}
|
||||
if (!/^\d+(\.\d{1,2})?$/.test(form.fee)) {
|
||||
return '费用必须为0到99999999.99之间的两位小数';
|
||||
}
|
||||
if (fee < 0 || fee > 99999999.99) {
|
||||
return '费用必须为0到99999999.99之间的两位小数';
|
||||
}
|
||||
if (!form.serviceContent.trim()) {
|
||||
return '请输入服务内容';
|
||||
}
|
||||
if (form.serviceContent.trim().length > 2000) {
|
||||
return '服务内容不能超过2000字';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureDraft(): Promise<number> {
|
||||
const message = validate();
|
||||
if (message) {
|
||||
showError(message, null);
|
||||
return 0;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const id = await saveCompletion({
|
||||
id: instanceId.value || undefined,
|
||||
name: form.name.trim(),
|
||||
implementOrg: form.implementOrg.trim(),
|
||||
fee: form.fee,
|
||||
serviceContent: form.serviceContent.trim()
|
||||
});
|
||||
instanceId.value = id;
|
||||
mode.value = 'edit';
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '保存失败', null);
|
||||
return 0;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
showSuccess('草稿已保存', null);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提交确认',
|
||||
content: '提交后字段与附件将锁定,确认提交?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const response = await submitInstance(id);
|
||||
showSuccess(response.message || '提交成功', () => {
|
||||
uni.redirectTo({
|
||||
url: '/packageApproval/pages/detail?id=' + id
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '提交失败', null);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function chooseAttachment() {
|
||||
if (attachments.value.length >= 9) {
|
||||
showError('每单最多上传9张附件', null);
|
||||
return;
|
||||
}
|
||||
const remaining = 9 - attachments.value.length;
|
||||
// #ifdef H5
|
||||
(uni as any).chooseFile({
|
||||
count: remaining,
|
||||
extension: ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx', '.xls', '.xlsx'],
|
||||
success: (result: any) => uploadFiles(result.tempFiles || []),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
(uni as any).chooseMessageFile({
|
||||
count: remaining,
|
||||
type: 'file',
|
||||
success: (result: any) => uploadFiles(result.tempFiles || []),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
(uni as any).chooseImage({
|
||||
count: remaining,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (result: any) =>
|
||||
(result.tempFilePaths || []).forEach((path: string) =>
|
||||
uploadFiles([{ path, name: path.split('/').pop() || '附件' }])
|
||||
),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
async function uploadFiles(files: Array<{ path: string; name?: string; size?: number }>) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
try {
|
||||
for (const file of files) {
|
||||
if (file.size && file.size > 20 * 1024 * 1024) {
|
||||
showError('单个附件不能超过20MB', null);
|
||||
continue;
|
||||
}
|
||||
const uploaded = await uploadAttachment(id, file.path, category.value);
|
||||
attachments.value = [...attachments.value, uploaded];
|
||||
}
|
||||
uni.showToast({ title: '上传成功', icon: 'success' });
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '上传失败', null);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(item: ApprovalAttachment) {
|
||||
uni.showModal({
|
||||
title: '删除附件',
|
||||
content: '确认删除该附件?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAttachment(item.id);
|
||||
attachments.value = attachments.value.filter(
|
||||
attachment => attachment.id !== item.id
|
||||
);
|
||||
uni.showToast({ title: '已删除', icon: 'success' });
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '删除失败', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatSize(value?: number) {
|
||||
if (!value) {
|
||||
return '0 B';
|
||||
}
|
||||
if (value < 1024) {
|
||||
return value + ' B';
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return (value / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (value / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.auto-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 160rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #d81e06;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.category-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 8rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
background: #f2f3f5;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
color: #ffffff;
|
||||
background: #d81e06;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.attachment-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #d81e06;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
background: #f7f8fa;
|
||||
color: #303133;
|
||||
border: 1rpx dashed #dcdfe6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.footer-button.primary {
|
||||
background: #d81e06;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.footer-button.ghost {
|
||||
background: #ffffff;
|
||||
color: #303133;
|
||||
border: 1rpx solid #dcdfe6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<view class="form-page">
|
||||
<uni-card :border="false" :padding="'16px'" title="基本信息">
|
||||
<view class="auto-row">
|
||||
<text class="label">报送人</text>
|
||||
<text class="value">{{ reporterName || '—' }}</text>
|
||||
</view>
|
||||
<view class="auto-row">
|
||||
<text class="label">报送部门</text>
|
||||
<text class="value">{{ reportOrgName || '—' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">受理部门<text class="required">*</text></text>
|
||||
<picker
|
||||
:range="organizationLabels"
|
||||
:value="orgIndex"
|
||||
@change="onOrgChange"
|
||||
>
|
||||
<view class="field-input picker">
|
||||
{{ selectedOrgName || '请选择受理部门' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">标题<text class="required">*</text></text>
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="field-input"
|
||||
type="text"
|
||||
maxlength="100"
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
<text class="counter">{{ form.title.length }}/100</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">内容<text class="required">*</text></text>
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
class="field-textarea"
|
||||
maxlength="2000"
|
||||
placeholder="请输入协调内容"
|
||||
/>
|
||||
<text class="counter">{{ form.content.length }}/2000</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">备注</text>
|
||||
<textarea
|
||||
v-model="form.remark"
|
||||
class="field-textarea"
|
||||
maxlength="500"
|
||||
placeholder="选填"
|
||||
/>
|
||||
<text class="counter">{{ form.remark.length }}/500</text>
|
||||
</view>
|
||||
</uni-card>
|
||||
|
||||
<uni-card :border="false" :padding="'16px'" title="附件">
|
||||
<view class="category-row">
|
||||
<text class="label">附件分类</text>
|
||||
<view class="chips">
|
||||
<view
|
||||
v-for="item in categories"
|
||||
:key="item.value"
|
||||
class="chip"
|
||||
:class="{ active: category === item.value }"
|
||||
@click="category = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-for="item in attachments" :key="item.id" class="attachment">
|
||||
<view class="attachment-main">
|
||||
<text class="attachment-name">{{ item.name }}</text>
|
||||
<text class="attachment-meta">
|
||||
{{ item.categoryName }} · {{ formatSize(item.length) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="delete" @click="removeAttachment(item)">删除</text>
|
||||
</view>
|
||||
|
||||
<view v-if="attachments.length === 0" class="empty">暂无附件</view>
|
||||
|
||||
<button class="upload-button" :loading="uploading" @click="chooseAttachment">
|
||||
上传附件({{ attachments.length }}/9)
|
||||
</button>
|
||||
<text class="hint">支持 jpg/jpeg/png/pdf/doc/docx/xls/xlsx,单张不超过20MB</text>
|
||||
</uni-card>
|
||||
|
||||
<view class="footer">
|
||||
<button class="footer-button ghost" :loading="saving" @click="saveDraft">
|
||||
保存草稿
|
||||
</button>
|
||||
<button class="footer-button primary" :loading="submitting" @click="submit">
|
||||
提交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import {
|
||||
getInstance,
|
||||
saveCoordination,
|
||||
submitInstance
|
||||
} from '@/api/approval/instance';
|
||||
import {
|
||||
deleteAttachment,
|
||||
uploadAttachment
|
||||
} from '@/api/approval/attachment';
|
||||
import { getOrganizations } from '@/api/approval/directory';
|
||||
import type {
|
||||
ApprovalAttachment,
|
||||
ApprovalOrganization
|
||||
} from '@/api/approval/model';
|
||||
import { showError, showSuccess } from '@/utils/app';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const categories = [
|
||||
{ value: 'SIGNED', label: '签字件' },
|
||||
{ value: 'OTHER', label: '其他' }
|
||||
];
|
||||
|
||||
const mode = ref<'create' | 'edit'>('create');
|
||||
const instanceId = ref(0);
|
||||
const organizations = ref<ApprovalOrganization[]>([]);
|
||||
const orgIndex = ref(0);
|
||||
const form = reactive({
|
||||
acceptOrgId: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
remark: ''
|
||||
});
|
||||
const attachments = ref<ApprovalAttachment[]>([]);
|
||||
const category = ref('SIGNED');
|
||||
const saving = ref(false);
|
||||
const submitting = ref(false);
|
||||
const uploading = ref(false);
|
||||
|
||||
const reporterName = computed(
|
||||
() => userStore.info?.realName || userStore.info?.nickname || ''
|
||||
);
|
||||
const reportOrgName = computed(() => userStore.info?.organizationName || '');
|
||||
const organizationLabels = computed(() =>
|
||||
organizations.value.map(item => item.organizationName)
|
||||
);
|
||||
const selectedOrgName = computed(
|
||||
() => organizations.value[orgIndex.value]?.organizationName || ''
|
||||
);
|
||||
|
||||
onLoad(options => {
|
||||
mode.value = options?.mode === 'edit' ? 'edit' : 'create';
|
||||
instanceId.value = Number(options?.id) || 0;
|
||||
loadOrganizations();
|
||||
if (instanceId.value) {
|
||||
loadDetail();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadOrganizations() {
|
||||
try {
|
||||
organizations.value = await getOrganizations();
|
||||
syncOrgIndex();
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '受理部门加载失败', null);
|
||||
}
|
||||
}
|
||||
|
||||
function syncOrgIndex() {
|
||||
if (!form.acceptOrgId) {
|
||||
orgIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
const index = organizations.value.findIndex(
|
||||
item => item.organizationId === form.acceptOrgId
|
||||
);
|
||||
orgIndex.value = index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
function onOrgChange(event: { detail: { value: string | number } }) {
|
||||
orgIndex.value = Number(event.detail.value);
|
||||
const organization = organizations.value[orgIndex.value];
|
||||
form.acceptOrgId = organization ? organization.organizationId : 0;
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
try {
|
||||
const detail = await getInstance(instanceId.value);
|
||||
if (detail.docType !== 'coordination') {
|
||||
showError('单据类型不匹配', null);
|
||||
return;
|
||||
}
|
||||
if (!detail.permissions?.canEdit) {
|
||||
showError('当前状态不可修改', null);
|
||||
return;
|
||||
}
|
||||
form.acceptOrgId = detail.acceptOrgId || 0;
|
||||
form.title = detail.title || '';
|
||||
form.content = detail.content || '';
|
||||
form.remark = detail.remark || '';
|
||||
attachments.value = detail.attachments || [];
|
||||
syncOrgIndex();
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '加载失败', null);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(): string | null {
|
||||
if (!form.acceptOrgId) {
|
||||
return '请选择受理部门';
|
||||
}
|
||||
if (!form.title.trim()) {
|
||||
return '请输入标题';
|
||||
}
|
||||
if (form.title.trim().length > 100) {
|
||||
return '标题不能超过100字';
|
||||
}
|
||||
if (!form.content.trim()) {
|
||||
return '请输入内容';
|
||||
}
|
||||
if (form.content.trim().length > 2000) {
|
||||
return '内容不能超过2000字';
|
||||
}
|
||||
if (form.remark.trim().length > 500) {
|
||||
return '备注不能超过500字';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureDraft(): Promise<number> {
|
||||
const message = validate();
|
||||
if (message) {
|
||||
showError(message, null);
|
||||
return 0;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const id = await saveCoordination({
|
||||
id: instanceId.value || undefined,
|
||||
acceptOrgId: form.acceptOrgId,
|
||||
title: form.title.trim(),
|
||||
content: form.content.trim(),
|
||||
remark: form.remark.trim() || undefined
|
||||
});
|
||||
instanceId.value = id;
|
||||
mode.value = 'edit';
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '保存失败', null);
|
||||
return 0;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
showSuccess('草稿已保存', null);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提交确认',
|
||||
content: '提交后字段与附件将锁定,确认提交?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const response = await submitInstance(id);
|
||||
showSuccess(response.message || '提交成功', () => {
|
||||
uni.redirectTo({
|
||||
url: '/packageApproval/pages/detail?id=' + id
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '提交失败', null);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function chooseAttachment() {
|
||||
if (attachments.value.length >= 9) {
|
||||
showError('每单最多上传9张附件', null);
|
||||
return;
|
||||
}
|
||||
const remaining = 9 - attachments.value.length;
|
||||
// #ifdef H5
|
||||
(uni as any).chooseFile({
|
||||
count: remaining,
|
||||
extension: ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx', '.xls', '.xlsx'],
|
||||
success: (result: any) => uploadFiles(result.tempFiles || []),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
(uni as any).chooseMessageFile({
|
||||
count: remaining,
|
||||
type: 'file',
|
||||
success: (result: any) => uploadFiles(result.tempFiles || []),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
(uni as any).chooseImage({
|
||||
count: remaining,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (result: any) =>
|
||||
(result.tempFilePaths || []).forEach((path: string) =>
|
||||
uploadFiles([{ path, name: path.split('/').pop() || '附件' }])
|
||||
),
|
||||
fail: () => undefined
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
async function uploadFiles(files: Array<{ path: string; name?: string; size?: number }>) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
const id = await ensureDraft();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
try {
|
||||
for (const file of files) {
|
||||
if (file.size && file.size > 20 * 1024 * 1024) {
|
||||
showError('单个附件不能超过20MB', null);
|
||||
continue;
|
||||
}
|
||||
const uploaded = await uploadAttachment(id, file.path, category.value);
|
||||
attachments.value = [...attachments.value, uploaded];
|
||||
}
|
||||
uni.showToast({ title: '上传成功', icon: 'success' });
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '上传失败', null);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(item: ApprovalAttachment) {
|
||||
uni.showModal({
|
||||
title: '删除附件',
|
||||
content: '确认删除该附件?',
|
||||
success: async result => {
|
||||
if (!result.confirm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAttachment(item.id);
|
||||
attachments.value = attachments.value.filter(
|
||||
attachment => attachment.id !== item.id
|
||||
);
|
||||
uni.showToast({ title: '已删除', icon: 'success' });
|
||||
} catch (error: any) {
|
||||
showError(error?.message || '删除失败', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatSize(value?: number) {
|
||||
if (!value) {
|
||||
return '0 B';
|
||||
}
|
||||
if (value < 1024) {
|
||||
return value + ' B';
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return (value / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (value / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.auto-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 160rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #d81e06;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 72rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.field-input.picker {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.category-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 8rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
background: #f2f3f5;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
color: #ffffff;
|
||||
background: #d81e06;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.attachment-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
font-size: 26rpx;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-meta {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #d81e06;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
background: #f7f8fa;
|
||||
color: #303133;
|
||||
border: 1rpx dashed #dcdfe6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.footer-button {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.footer-button.primary {
|
||||
background: #d81e06;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.footer-button.ghost {
|
||||
background: #ffffff;
|
||||
color: #303133;
|
||||
border: 1rpx solid #dcdfe6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,410 @@
|
||||
<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 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 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>{{ emptyText }}</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' && canSubmit" class="create-bar">
|
||||
<button class="create-button" @click="createCompletion">
|
||||
新建确认单
|
||||
</button>
|
||||
<button class="create-button primary" @click="createCoordination">
|
||||
新建协调单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
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';
|
||||
import { hasPermission } from '@/utils/permission';
|
||||
|
||||
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>('todo');
|
||||
const canSubmit = computed(() => hasPermission('approval:instance:submit'));
|
||||
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[]>([]);
|
||||
const page = ref(1);
|
||||
const limit = 10;
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
let requestSeq = 0;
|
||||
|
||||
onLoad(() => {
|
||||
load(true);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
load(true);
|
||||
});
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await load(true);
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value && !finished.value) {
|
||||
load(false);
|
||||
}
|
||||
});
|
||||
|
||||
function switchTab(key: TabKey) {
|
||||
if (activeTab.value === key) {
|
||||
return;
|
||||
}
|
||||
activeTab.value = key;
|
||||
list.value = [];
|
||||
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 && !reset) {
|
||||
return;
|
||||
}
|
||||
const seq = ++requestSeq;
|
||||
const tab = activeTab.value;
|
||||
loading.value = true;
|
||||
try {
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
}
|
||||
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;
|
||||
if (!finished.value) {
|
||||
page.value += 1;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (seq === requestSeq) {
|
||||
uni.showToast({ title: error?.message || '加载失败', icon: 'none' });
|
||||
}
|
||||
} finally {
|
||||
if (seq === requestSeq) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
load(true);
|
||||
}
|
||||
|
||||
function openDetail(id: number) {
|
||||
uni.navigateTo({ url: '/packageApproval/pages/detail?id=' + id });
|
||||
}
|
||||
|
||||
function createCompletion() {
|
||||
uni.navigateTo({
|
||||
url: '/packageApproval/pages/form-completion?mode=create'
|
||||
});
|
||||
}
|
||||
|
||||
function createCoordination() {
|
||||
uni.navigateTo({
|
||||
url: '/packageApproval/pages/form-coordination?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;
|
||||
}
|
||||
|
||||
.empty,
|
||||
.loading,
|
||||
.finished {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.create-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.create-button {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #ffffff;
|
||||
color: #303133;
|
||||
border: 1rpx solid #dcdfe6;
|
||||
}
|
||||
|
||||
.create-button.primary {
|
||||
background: #d81e06;
|
||||
color: #ffffff;
|
||||
border-color: #d81e06;
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,46 @@
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "packageApproval",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "审批中心",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "单据详情",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/form-completion",
|
||||
"style": {
|
||||
"navigationBarTitleText": "业务完成确认单",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/form-coordination",
|
||||
"style": {
|
||||
"navigationBarTitleText": "内部协调单",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dispatch",
|
||||
"style": {
|
||||
"navigationBarTitleText": "派单",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "package",
|
||||
"pages": [
|
||||
|
||||
+119
-2
@@ -4,6 +4,29 @@
|
||||
<view class="header" :style="'height: ' + statusBarHeight + 'px'" @click="phoneLogin"></view>
|
||||
<!-- 用户信息 -->
|
||||
<ws-user-card :data="userInfo" :users="users" :isLogin="isLogin" @openLogin="openLogin" @done="reload"/>
|
||||
<uni-card
|
||||
v-if="hasApprovalPermission"
|
||||
class="approval-card"
|
||||
:border="false"
|
||||
:padding="'16px'"
|
||||
@click="openApprovalCenter"
|
||||
>
|
||||
<view class="approval-entry">
|
||||
<view class="approval-entry-left">
|
||||
<view class="approval-icon">审</view>
|
||||
<view class="approval-entry-text">
|
||||
<text class="approval-title">审批中心</text>
|
||||
<text class="approval-desc">待办 / 我发起的 / 已办</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="approval-entry-right">
|
||||
<view v-if="approvalTodoCount > 0" class="approval-dot">
|
||||
{{ approvalTodoCount > 99 ? '99+' : approvalTodoCount }}
|
||||
</view>
|
||||
<text class="approval-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
<block v-if="hideTools">
|
||||
<uni-card :border="false" :padding="'18px 8px'">
|
||||
<uv-skeleton rows="2" :loading="hideTools" :title="false"></uv-skeleton>
|
||||
@@ -62,8 +85,10 @@ import {useTenantStore} from '@/store/modules/tenant';
|
||||
import {authUser, loginByPhoneReq} from "@/api/user";
|
||||
import {getThinkOrderByCode, thinkOrderCheckCode} from "@/api/think/thinkOrder";
|
||||
import {detectFace} from "@/api/think/thinkGate";
|
||||
import {setHasFace} from "@/api/think/thinkUsers";
|
||||
import {uploadFaceByPath} from "@/api/system/file";
|
||||
import {setHasFace} from '@/api/think/thinkUsers';
|
||||
import {uploadFaceByPath} from '@/api/system/file';
|
||||
import {getTodoCount} from '@/api/approval/task';
|
||||
import {hasPermission} from '@/utils/permission';
|
||||
|
||||
const tenantStore = useTenantStore();
|
||||
const mpStore = useMpStore();
|
||||
@@ -88,6 +113,9 @@ export default {
|
||||
hasMerchantClerkRole: false,
|
||||
// 是否拥有超级管理员权限
|
||||
hasSuperAdminRole: false,
|
||||
// 审批中心入口与待办红点
|
||||
hasApprovalPermission: false,
|
||||
approvalTodoCount: 0,
|
||||
loginCount: 0
|
||||
};
|
||||
},
|
||||
@@ -218,6 +246,9 @@ export default {
|
||||
onLoad() {
|
||||
this.reload();
|
||||
},
|
||||
onShow() {
|
||||
this.loadApprovalEntry();
|
||||
},
|
||||
methods: {
|
||||
async reload(e) {
|
||||
const app = this;
|
||||
@@ -326,8 +357,26 @@ export default {
|
||||
app.isLogin = false;
|
||||
app.userInfo = {};
|
||||
}
|
||||
await app.loadApprovalEntry();
|
||||
app.hideTools = false;
|
||||
},
|
||||
async loadApprovalEntry() {
|
||||
const allowed = hasPermission('approval:instance:list');
|
||||
this.hasApprovalPermission = !!allowed;
|
||||
if (!allowed) {
|
||||
this.approvalTodoCount = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const count = await getTodoCount();
|
||||
this.approvalTodoCount = Number(count) || 0;
|
||||
} catch (e) {
|
||||
this.approvalTodoCount = 0;
|
||||
}
|
||||
},
|
||||
openApprovalCenter() {
|
||||
uni.navigateTo({url: '/packageApproval/pages/index'});
|
||||
},
|
||||
onLayout(data = {}) {
|
||||
this.mpMenus = data.mpMenus || {};
|
||||
this.hideTools = false;
|
||||
@@ -546,6 +595,74 @@ page {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.approval-card {
|
||||
margin: 12rpx 16rpx;
|
||||
}
|
||||
|
||||
.approval-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.approval-entry-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.approval-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #d81e06 0%, #ff7a45 100%);
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.approval-entry-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.approval-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.approval-desc {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.approval-entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.approval-dot {
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 10rpx;
|
||||
margin-right: 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ff4d4f;
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.approval-arrow {
|
||||
font-size: 40rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 24rpx 16rpx;
|
||||
font-size: 30rpx;
|
||||
|
||||
@@ -23,7 +23,6 @@ export const useMpStore = defineStore({
|
||||
async fetchMpInfo() {
|
||||
const info = await getMpInfo().catch((err => {
|
||||
console.log(err.code,'fetchMpInfo()')
|
||||
uni.clearStorageSync()
|
||||
}));
|
||||
if (!info) {
|
||||
return {};
|
||||
|
||||
+11
-3
@@ -5,6 +5,14 @@ import {
|
||||
} from '@/config/setting';
|
||||
|
||||
const http = new Request();
|
||||
|
||||
// 只有主 API(core) 的 401 才代表登录态失效;shop/booking 等独立服务有自己的令牌体系
|
||||
function isCoreRequest(url?: string) {
|
||||
if (!url) {
|
||||
return true;
|
||||
}
|
||||
return url.startsWith(SERVER_API_URL) || url.startsWith('/');
|
||||
}
|
||||
/**
|
||||
* @description 修改全局默认配置
|
||||
* @param {Function}
|
||||
@@ -48,8 +56,8 @@ http.interceptors.response.use((response) => {
|
||||
// return response.data
|
||||
// }
|
||||
|
||||
// token过期
|
||||
if (response.data.code == 401) {
|
||||
// token过期(仅主 API)
|
||||
if (response.data.code == 401 && isCoreRequest(response.config.url)) {
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('phone')
|
||||
@@ -57,7 +65,7 @@ http.interceptors.response.use((response) => {
|
||||
if (response.data.code == 1) {
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
if (response.data.message === '请先登录') {
|
||||
if (response.data.message === '请先登录' && isCoreRequest(response.config.url)) {
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('user_id')
|
||||
uni.removeStorageSync('phone')
|
||||
|
||||
Reference in New Issue
Block a user