import type { BpmSummaryItem, DictData } from "../../api/bpm";
|
|
/** 字典 type(与 web 端 @vben/constants 的 DICT_TYPE 逐字一致) */
|
export const DICT = {
|
BPM_PROCESS_INSTANCE_STATUS: "bpm_process_instance_status",
|
BPM_TASK_STATUS: "bpm_task_status",
|
} as const;
|
|
/** 流程实例状态 */
|
export const PROCESS_INSTANCE_STATUS = {
|
NOT_START: -1,
|
RUNNING: 1,
|
APPROVE: 2,
|
REJECT: 3,
|
CANCEL: 4,
|
} as const;
|
|
/** 任务状态 */
|
export const TASK_STATUS = {
|
SKIP: -2,
|
NOT_START: -1,
|
WAIT: 0,
|
RUNNING: 1,
|
APPROVE: 2,
|
REJECT: 3,
|
CANCEL: 4,
|
RETURN: 5,
|
APPROVING: 7,
|
} as const;
|
|
/** 节点类型 */
|
export const NODE_TYPE = {
|
END_EVENT_NODE: 1,
|
START_USER_NODE: 10,
|
USER_TASK_NODE: 11,
|
COPY_TASK_NODE: 12,
|
TRANSACTOR_NODE: 13,
|
DELAY_TIMER_NODE: 14,
|
TRIGGER_NODE: 15,
|
CHILD_PROCESS_NODE: 20,
|
CONDITION_NODE: 50,
|
CONDITION_BRANCH_NODE: 51,
|
PARALLEL_BRANCH_NODE: 52,
|
INCLUSIVE_BRANCH_NODE: 53,
|
ROUTER_BRANCH_NODE: 54,
|
} as const;
|
|
/** 表单字段权限 */
|
export const FIELD_PERMISSION = {
|
READ: "1",
|
WRITE: "2",
|
NONE: "3",
|
} as const;
|
|
/** 模型表单类型 */
|
export const MODEL_FORM_TYPE = {
|
NORMAL: 10,
|
CUSTOM: 20,
|
} as const;
|
|
/** 候选人策略:发起人自选审批人 */
|
export const CANDIDATE_STRATEGY_START_USER_SELECT = 35;
|
|
/** 审批按钮类型(与 web BpmTaskOperationButtonTypeEnum 一致) */
|
export const OPERATION_BUTTON = {
|
APPROVE: 1,
|
REJECT: 2,
|
TRANSFER: 3,
|
DELEGATE: 4,
|
ADD_SIGN: 5,
|
RETURN: 6,
|
COPY: 7,
|
} as const;
|
|
export const OPERATION_BUTTON_NAME: Record<number, string> = {
|
1: "通过",
|
2: "拒绝",
|
3: "转办",
|
4: "委派",
|
5: "加签",
|
6: "退回",
|
7: "抄送",
|
};
|
|
/** 流程实例状态回退文案 */
|
export function processStatusText(status?: number) {
|
return (
|
(
|
{
|
[-1]: "未开始",
|
1: "审批中",
|
2: "已通过",
|
3: "已拒绝",
|
4: "已取消",
|
} as Record<number, string>
|
)[Number(status)] ?? "--"
|
);
|
}
|
|
/** 任务状态回退文案 */
|
export function taskStatusText(status?: number) {
|
return (
|
(
|
{
|
[-2]: "跳过",
|
[-1]: "未开始",
|
0: "待审批",
|
1: "审批中",
|
2: "审批通过",
|
3: "审批不通过",
|
4: "已取消",
|
5: "已退回",
|
7: "审批通过中",
|
} as Record<number, string>
|
)[Number(status)] ?? "--"
|
);
|
}
|
|
/** 时间线节点状态文案与颜色(照搬 web 端 time-line.vue 映射表) */
|
export const NODE_STATUS_META: Record<number, { label: string; color: string }> = {
|
[-2]: { label: "跳过", color: "#909398" },
|
[-1]: { label: "未开始", color: "#909398" },
|
0: { label: "待审批", color: "#ff943e" },
|
1: { label: "审批中", color: "#448ef7" },
|
2: { label: "审批通过", color: "#00b32a" },
|
3: { label: "审批不通过", color: "#f46b6c" },
|
4: { label: "已取消", color: "#cccccc" },
|
5: { label: "退回", color: "#f46b6c" },
|
6: { label: "委派中", color: "#448ef7" },
|
7: { label: "审批通过中", color: "#00b32a" },
|
};
|
|
/** 流程是否已结束(通过 / 拒绝 / 取消) */
|
export function isEndProcessStatus(status?: number) {
|
return [
|
PROCESS_INSTANCE_STATUS.APPROVE,
|
PROCESS_INSTANCE_STATUS.REJECT,
|
PROCESS_INSTANCE_STATUS.CANCEL,
|
].includes(Number(status) as any);
|
}
|
|
/**
|
* 时间戳 / 时间字符串 / Date 解析为 Date。
|
*
|
* 后端在 BPM 各接口里返回的时间**不统一**:
|
* - `process-instance` 系列多为毫秒时间戳(number)
|
* - `bpm/task/list-by-process-instance-id`(流转记录)返回的是
|
* 形如 "2026-09-22 10:30:00" 的字符串
|
* 旧实现只做 `new Date(Number(value))`,字符串会被转成 NaN 导致
|
* 时间列全部显示 "--"(流转记录不显示开始/结束时间)。
|
* 这里与 `mes/pd/shared.ts` 的处理保持一致:字符串走 Date 解析,
|
* 并把 "YYYY-MM-DD HH:mm:ss" 的空格替换成 "T" 以兼容 iOS / 小程序。
|
*/
|
function parseDate(value?: number | string | Date): Date | undefined {
|
if (value === undefined || value === null || value === "") return undefined;
|
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value;
|
if (typeof value === "number") {
|
const date = new Date(value);
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
}
|
const text = String(value).trim();
|
// 纯数字字符串按时间戳处理
|
if (/^\d+$/.test(text)) {
|
const date = new Date(Number(text));
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
}
|
const date = new Date(text.replace(" ", "T"));
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
}
|
|
/** 时间戳/时间字符串格式化为 yyyy-MM-dd HH:mm:ss */
|
export function dateTimeText(value?: number | string | Date) {
|
const date = parseDate(value);
|
if (!date) return "--";
|
const pad = (n: number) => String(n).padStart(2, "0");
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
date.getHours(),
|
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
}
|
|
/** 时间戳/时间字符串格式化为 yyyy-MM-dd */
|
export function dateOnly(value?: number | string | Date) {
|
const date = parseDate(value);
|
if (!date) return "--";
|
const pad = (n: number) => String(n).padStart(2, "0");
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
}
|
|
/** 耗时格式化(对应 web 端 formatPast2) */
|
export function durationText(millis?: number) {
|
if (!millis || millis < 0) return "--";
|
const seconds = Math.floor(millis / 1000);
|
if (seconds < 60) return `${seconds}秒`;
|
const minutes = Math.floor(seconds / 60);
|
if (minutes < 60) return `${minutes}分钟`;
|
const hours = Math.floor(minutes / 60);
|
if (hours < 24) return `${hours}小时${minutes % 60}分钟`;
|
const days = Math.floor(hours / 24);
|
return `${days}天${hours % 24}小时`;
|
}
|
|
/** 摘要渲染:数组 -> "key : value" 用 | 分隔(我的流程列) */
|
export function summaryInline(summary?: BpmSummaryItem[]) {
|
if (!summary || !summary.length) return "-";
|
return summary.map(item => `${item.key} : ${item.value}`).join(" | ");
|
}
|
|
/** 摘要渲染:数组 -> "key : value" 用换行分隔(待办/已办/抄送列) */
|
export function summaryLines(summary?: BpmSummaryItem[]) {
|
if (!summary || !summary.length) return "-";
|
return summary.map(item => `${item.key} : ${item.value}`).join("\n");
|
}
|
|
/** 字典取值 */
|
export function dictLabel(
|
dicts: DictData[],
|
type: string,
|
value?: number | string,
|
fallback = "--",
|
) {
|
if (value === undefined || value === null || value === "") return fallback;
|
const hit = dicts.find(
|
item => item.dictType === type && String(item.value) === String(value),
|
);
|
return hit?.label ?? fallback;
|
}
|
|
/** 字典下拉项 */
|
export function dictOptions(dicts: DictData[], type: string) {
|
return dicts
|
.filter(item => item.dictType === type)
|
.map(item => ({ key: item.value, label: item.label }));
|
}
|
|
/** 参与建树的节点最小结构 */
|
export interface TreeNodeLike {
|
id?: number | string;
|
parentId?: number | string;
|
children?: any[];
|
}
|
|
/**
|
* 扁平列表 -> 树(移植 web 端 `@vben/utils` 的 handleTree)。
|
*
|
* 后端 `/system/dept/simple-list` 返回的是**扁平列表**(靠 parentId 关联),
|
* web 端 `form-create/components/dept-select.vue` 也是先 `handleTree(data)`
|
* 再交给 antd TreeSelect。App 端早期实现直接读 `dept.children` 递归,
|
* 而扁平数据里根本没有 children,于是永远只能渲染出一层、没有层级关系。
|
*/
|
export function buildTree<T extends object>(
|
list: T[],
|
idKey: string = "id",
|
parentKey: string = "parentId",
|
childrenKey: string = "children",
|
): T[] {
|
if (!Array.isArray(list)) return [];
|
const read = (item: any, key: string) =>
|
item === null || item === undefined ? undefined : item[key];
|
|
// 先浅拷贝一层:下面会就地写 children,不能污染调用方的原始数据
|
// (否则原始列表项被挂上 children 后,再次拷贝会把整棵子树一起带出去)
|
const nodes = list.map(item => ({ ...(item as any) }) as T);
|
const childrenMap: Record<string, T[]> = {};
|
const nodeIds: Record<string, boolean> = {};
|
|
nodes.forEach(item => {
|
const pid = String(read(item, parentKey));
|
if (!childrenMap[pid]) childrenMap[pid] = [];
|
childrenMap[pid].push(item);
|
nodeIds[String(read(item, idKey))] = true;
|
});
|
|
// 防止脏数据(父子互相指向 / 自身为父 / 同一节点重复出现)建出带环的对象图:
|
// 全局记录已挂载过的节点,挂 children 时把已挂过的剪掉。
|
const attached = new Set<string>();
|
const attach = (node: T) => {
|
const key = String(read(node, idKey));
|
if (attached.has(key)) return;
|
attached.add(key);
|
const children = (childrenMap[key] || []).filter(
|
child => !attached.has(String(read(child, idKey))),
|
);
|
if (children.length) {
|
(node as any)[childrenKey] = children;
|
children.forEach(attach);
|
}
|
};
|
|
const roots: T[] = [];
|
nodes.forEach(item => {
|
// 父节点不在本集合内 => 视为根(与 handleTree 一致)
|
if (!nodeIds[String(read(item, parentKey))]) roots.push(item);
|
});
|
// 数据异常(构不成根)时兜底:至少保证有内容可渲染
|
if (!roots.length) roots.push(...nodes);
|
roots.forEach(attach);
|
return roots;
|
}
|
|
/** 深度优先拍平树,带层级(用于回显:需要能查到父级 id 对应的名称) */
|
export function flattenTree<T extends object>(
|
nodes: T[],
|
depth = 0,
|
childrenKey: string = "children",
|
): Array<{ node: T; depth: number }> {
|
const result: Array<{ node: T; depth: number }> = [];
|
(Array.isArray(nodes) ? nodes : []).forEach(node => {
|
result.push({ node, depth });
|
const children = (node as any)?.[childrenKey];
|
if (Array.isArray(children) && children.length) {
|
result.push(...flattenTree(children, depth + 1, childrenKey));
|
}
|
});
|
return result;
|
}
|
|
/** 底部选择面板选项结构(与 process-design/PickerSheet 对齐) */
|
export interface SheetOption {
|
key: number | string;
|
label: string;
|
sub?: string;
|
extra?: string;
|
}
|
|
/** 二次确认弹窗 */
|
export function confirmModal(content: string, title = "系统提示") {
|
return new Promise<boolean>(resolve => {
|
uni.showModal({
|
title,
|
content,
|
success: res => resolve(Boolean(res.confirm)),
|
fail: () => resolve(false),
|
});
|
});
|
}
|
|
/** 轻提示 */
|
export function toast(title: string) {
|
uni.showToast({ title, icon: "none" });
|
}
|