Files
qhmes/jeecgboot-vue3/src/views/system/im/ImBizRecordMessageContent.vue

427 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>