427 lines
13 KiB
Vue
427 lines
13 KiB
Vue
<template>
|
||
<div class="im-biz-record-message">
|
||
<div v-if="showNoPermission" class="im-biz-record-no-permission">暂无当前消息权限</div>
|
||
|
||
<template v-else>
|
||
<!-- 单条:详情表 -->
|
||
<template v-if="isSingleItem">
|
||
<div class="im-biz-record-item">
|
||
<div class="im-biz-record-table-wrap">
|
||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||
<tbody>
|
||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||
<th>{{ field.label }}</th>
|
||
<td>{{ field.value }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 审批卡片:底部操作按钮 -->
|
||
<div v-if="isApprovalCard" class="im-biz-record-actions">
|
||
<a-button size="small" @click="handleDetail(singleItem)">
|
||
<Icon icon="ant-design:file-search-outlined" />
|
||
<span>查看详情</span>
|
||
</a-button>
|
||
<template v-if="liveActionable">
|
||
<a-button size="small" type="primary" :loading="approving" @click="handleApprove(singleItem)">
|
||
<Icon icon="ant-design:check-outlined" />
|
||
<span>{{ singleItem.actionLabel || '审批' }}</span>
|
||
</a-button>
|
||
<a-button size="small" danger @click="openReject(singleItem)">
|
||
<Icon icon="ant-design:close-outlined" />
|
||
<span>拒绝</span>
|
||
</a-button>
|
||
</template>
|
||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
||
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||
<Icon icon="ant-design:unordered-list-outlined" />
|
||
<span>跳转至列表</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 普通分享卡片:定位链接 -->
|
||
<a v-else class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||
<Icon icon="ant-design:link-outlined" />
|
||
<span>查看并定位到此数据</span>
|
||
</a>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 多条:列表表,第一列为定位链接 -->
|
||
<template v-else>
|
||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||
<table class="im-biz-record-table im-biz-record-table--list">
|
||
<thead>
|
||
<tr>
|
||
<th class="im-biz-record-link-col">链接</th>
|
||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||
<td class="im-biz-record-link-col">
|
||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||
<Icon icon="ant-design:link-outlined" />
|
||
<span>定位</span>
|
||
</a>
|
||
</td>
|
||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||
{{ getFieldValue(item, columnLabel) }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||
</template>
|
||
|
||
<!-- 查看详情弹窗 -->
|
||
<ImApprovalDetailModal ref="detailModalRef" />
|
||
|
||
<!-- 驳回理由弹窗 -->
|
||
<a-modal v-model:open="rejectOpen" title="驳回审批" :confirmLoading="rejecting" okText="确认驳回" @ok="confirmReject">
|
||
<a-form layout="vertical">
|
||
<a-form-item label="驳回理由" required>
|
||
<a-textarea v-model:value="rejectReason" :rows="3" placeholder="请填写驳回理由" :maxlength="500" show-count />
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, ref, onMounted, watch } from 'vue';
|
||
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
||
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||
import { navigateImBizRecordLink } from './imRecordLocate';
|
||
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import { approveApproval, rejectApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
||
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
||
|
||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||
|
||
const props = defineProps<{
|
||
payload: ImBizRecordPayload;
|
||
mine?: boolean;
|
||
receiverHasBizPagePermission?: boolean;
|
||
}>();
|
||
|
||
// 办理成功后通知父级(ImChat)刷新当前会话,使下一节点卡片/结果通知即时出现
|
||
const emit = defineEmits(['handled']);
|
||
|
||
const { createMessage } = useMessage();
|
||
|
||
const detailModalRef = ref();
|
||
const approving = ref(false);
|
||
const rejecting = ref(false);
|
||
const rejectOpen = ref(false);
|
||
const rejectReason = ref('');
|
||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||
// 本地办理结果:approved / rejected(卡片消息为静态,办理后本地标记)
|
||
const actionDone = ref<'' | 'approved' | 'rejected'>('');
|
||
|
||
// 审批实例实时状态(用于旧节点卡片置灰)
|
||
interface LiveStatus {
|
||
exists: boolean;
|
||
status?: string;
|
||
statusText?: string;
|
||
currentNodeId?: string;
|
||
currentHandlersText?: string;
|
||
canApprove?: boolean;
|
||
}
|
||
const liveStatus = ref<LiveStatus | null>(null);
|
||
const liveLoaded = ref(false);
|
||
|
||
const isSingleItem = computed(() => props.payload.items.length === 1);
|
||
const singleItem = computed(() => props.payload.items[0]);
|
||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||
|
||
// 审批卡片:单条且带审批实例ID
|
||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||
|
||
// 是否仍可办理:本地未办理 且 实例审批中 且 卡片节点==当前节点 且 本人为当前处理人
|
||
const liveActionable = computed(() => {
|
||
if (!isApprovalCard.value || actionDone.value || props.mine) {
|
||
return false;
|
||
}
|
||
const s = liveStatus.value;
|
||
if (!s || !s.exists || s.status !== '0' || !s.canApprove) {
|
||
return false;
|
||
}
|
||
// 携带 nodeId 时严格比对当前节点,区分同一实例的新旧卡片
|
||
const cardNodeId = singleItem.value?.nodeId;
|
||
if (cardNodeId) {
|
||
return s.currentNodeId === cardNodeId;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
// 不可办理时的置灰提示文案
|
||
const disabledText = computed(() => {
|
||
if (actionDone.value) {
|
||
return actionDone.value === 'rejected' ? '已驳回' : '已处理';
|
||
}
|
||
const s = liveStatus.value;
|
||
if (!s) {
|
||
return liveLoaded.value ? '加载失败' : '';
|
||
}
|
||
if (!s.exists) {
|
||
return '审批已失效';
|
||
}
|
||
if (s.status === '1') return '已通过';
|
||
if (s.status === '2') return '已驳回';
|
||
if (s.status === '3') return '已撤销';
|
||
// 审批中但本卡片不可办理
|
||
const cardNodeId = singleItem.value?.nodeId;
|
||
if (cardNodeId && s.currentNodeId !== cardNodeId) {
|
||
return '已流转,无需处理';
|
||
}
|
||
return '等待他人处理';
|
||
});
|
||
|
||
async function loadLiveStatus() {
|
||
const id = singleItem.value?.instanceId;
|
||
if (!isApprovalCard.value || props.mine || !id) {
|
||
return;
|
||
}
|
||
try {
|
||
const res: any = await getApprovalStatus(id);
|
||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||
} catch {
|
||
liveStatus.value = null;
|
||
} finally {
|
||
liveLoaded.value = true;
|
||
}
|
||
}
|
||
|
||
onMounted(loadLiveStatus);
|
||
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
||
|
||
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
||
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||
const canLocate = computed(() => props.mine || hasPagePermission.value);
|
||
|
||
const showPeerNoPermissionTip = computed(() => !!props.mine && props.receiverHasBizPagePermission === false);
|
||
|
||
function resolveItemFields(item: ImBizRecordItem) {
|
||
return resolveImBizRecordItemFields(item);
|
||
}
|
||
|
||
function getFieldValue(item: ImBizRecordItem, label: string) {
|
||
return getImBizRecordFieldValueByLabel(item, label);
|
||
}
|
||
|
||
async function handleLinkClick(linkPath: string) {
|
||
if (!linkPath || showNoPermission.value) {
|
||
return;
|
||
}
|
||
await navigateImBizRecordLink(linkPath);
|
||
}
|
||
|
||
function handleDetail(item: ImBizRecordItem) {
|
||
if (!item.instanceId) return;
|
||
detailModalRef.value?.openModal(item.instanceId);
|
||
}
|
||
|
||
async function handleApprove(item: ImBizRecordItem) {
|
||
if (!item.instanceId || approving.value) return;
|
||
try {
|
||
approving.value = true;
|
||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||
createMessage.success(typeof res === 'string' ? res : '已审批');
|
||
actionDone.value = 'approved';
|
||
// 立即刷新本卡片状态(置灰),再通知父级刷新会话
|
||
await loadLiveStatus();
|
||
emit('handled');
|
||
} finally {
|
||
approving.value = false;
|
||
}
|
||
}
|
||
|
||
function openReject(item: ImBizRecordItem) {
|
||
rejectItem.value = item;
|
||
rejectReason.value = '';
|
||
rejectOpen.value = true;
|
||
}
|
||
|
||
async function confirmReject() {
|
||
const item = rejectItem.value;
|
||
if (!item?.instanceId) return;
|
||
if (!rejectReason.value.trim()) {
|
||
createMessage.warning('请填写驳回理由');
|
||
return;
|
||
}
|
||
try {
|
||
rejecting.value = true;
|
||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||
createMessage.success('已驳回');
|
||
actionDone.value = 'rejected';
|
||
rejectOpen.value = false;
|
||
await loadLiveStatus();
|
||
emit('handled');
|
||
} finally {
|
||
rejecting.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.im-biz-record-message {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
min-width: 280px;
|
||
max-width: 420px;
|
||
}
|
||
|
||
.im-biz-record-no-permission {
|
||
padding: 12px 10px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: #8c8c8c;
|
||
text-align: center;
|
||
}
|
||
|
||
.im-biz-record-peer-tip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
align-self: flex-start;
|
||
margin-top: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
background: #fff7e6;
|
||
border: 1px solid #ffd591;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: #d46b08;
|
||
}
|
||
|
||
.im-biz-record-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 审批办理按钮栏 */
|
||
.im-biz-record-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding-top: 4px;
|
||
border-top: 1px dashed #f0f0f0;
|
||
|
||
.im-biz-record-done {
|
||
font-size: 12px;
|
||
color: #52c41a;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 不可办理置灰提示 */
|
||
.im-biz-record-disabled {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 1px 8px;
|
||
border-radius: 10px;
|
||
background: #f5f5f5;
|
||
border: 1px solid #e0e0e0;
|
||
font-size: 12px;
|
||
color: #bfbfbf;
|
||
}
|
||
}
|
||
|
||
.im-biz-record-table-wrap {
|
||
overflow: hidden;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
|
||
&--list {
|
||
overflow-x: auto;
|
||
}
|
||
}
|
||
|
||
.im-biz-record-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
|
||
th,
|
||
td {
|
||
padding: 8px 10px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
vertical-align: top;
|
||
word-break: break-word;
|
||
}
|
||
|
||
tr:last-child {
|
||
th,
|
||
td {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
&--detail {
|
||
table-layout: fixed;
|
||
|
||
th {
|
||
width: 38%;
|
||
background: #fafafa;
|
||
color: #595959;
|
||
font-weight: 500;
|
||
text-align: left;
|
||
}
|
||
|
||
td {
|
||
color: #262626;
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
&--list {
|
||
min-width: 100%;
|
||
table-layout: auto;
|
||
|
||
thead th {
|
||
background: #fafafa;
|
||
color: #595959;
|
||
font-weight: 500;
|
||
text-align: left;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
tbody td {
|
||
color: #262626;
|
||
background: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.im-biz-record-link-col {
|
||
width: 72px;
|
||
min-width: 72px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.im-biz-record-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
color: #1677ff;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: #0958d9;
|
||
}
|
||
}
|
||
</style>
|