<script lang="ts" setup>
|
import type { TmsRecordDetail, TmsRecordSignQr } from './types';
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { Table as ATable } from 'ant-design-vue';
|
|
import { getTmsRecordDetail, getTmsRecordSignQr } from '#/api/x/tms/record';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsRecordSign' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
|
const loading = ref(false);
|
const qrLoading = ref(false);
|
const detail = ref<TmsRecordDetail | null>(null);
|
const signQr = ref<TmsRecordSignQr | null>(null);
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
const signColumns = [
|
{ title: '序号', key: 'index', width: 70, align: 'center', customRender: ({ index }: any) => index + 1 },
|
{ title: '姓名', dataIndex: 'userName', key: 'userName', width: 120 },
|
{ title: '部门', dataIndex: 'deptName', key: 'deptName', minWidth: 140 },
|
{ title: '签到时间', dataIndex: 'signTime', key: 'signTime', width: 180 },
|
{ title: '签到方式', dataIndex: 'signMode', key: 'signMode', width: 120 },
|
];
|
|
const modeNames = computed(() =>
|
(signQr.value?.modes || []).map((m) => m.modeName || m.modeCode).filter(Boolean).join('、'),
|
);
|
const showQr = computed(() => Boolean(signQr.value?.canScanSign && signQr.value?.qrContent));
|
|
/** 手机扫码打开的完整签到页地址(内嵌二维码 JSON) */
|
const qrDisplayText = computed(() => {
|
const content = signQr.value?.qrContent;
|
if (!content) return '';
|
const signMode = signQr.value?.modes?.[0]?.modeCode || 'app_scan';
|
const resolved = router.resolve({
|
name: 'TmsRecordScanSign',
|
query: { c: content, signMode },
|
});
|
try {
|
return new URL(resolved.href, window.location.href).href;
|
} catch {
|
return resolved.href;
|
}
|
});
|
|
onMounted(() => {
|
loadData();
|
refreshTimer = setInterval(() => {
|
if (detail.value?.id) {
|
refreshSignList();
|
}
|
}, 15000);
|
});
|
|
onUnmounted(() => {
|
if (refreshTimer) {
|
clearInterval(refreshTimer);
|
refreshTimer = null;
|
}
|
});
|
|
async function loadData() {
|
const id = String(route.params.id || '');
|
if (!id) {
|
router.replace('/tms/record');
|
return;
|
}
|
loading.value = true;
|
try {
|
detail.value = await getTmsRecordDetail(id);
|
if (detail.value.archiveStatus === 'archived') {
|
createMessage.warning('已存档记录不可签到');
|
router.replace(`/tms/record/detail/${id}`);
|
return;
|
}
|
await loadSignQr();
|
} catch (e: any) {
|
createMessage.error(e?.message || '加载培训记录失败');
|
router.replace('/tms/record');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function loadSignQr() {
|
const id = String(route.params.id || detail.value?.id || '');
|
if (!id) return;
|
qrLoading.value = true;
|
try {
|
signQr.value = await getTmsRecordSignQr(id);
|
} catch (e: any) {
|
signQr.value = null;
|
createMessage.error(e?.message || '生成签到码失败');
|
} finally {
|
qrLoading.value = false;
|
}
|
}
|
|
async function refreshSignList() {
|
const id = String(detail.value?.id || route.params.id || '');
|
if (!id) return;
|
try {
|
const next = await getTmsRecordDetail(id);
|
if (detail.value) {
|
detail.value.signList = next.signList || [];
|
}
|
} catch {
|
// 静默刷新失败忽略
|
}
|
}
|
|
function goBack() {
|
router.push('/tms/record');
|
}
|
|
function openScanPage() {
|
if (!qrDisplayText.value) return;
|
window.open(qrDisplayText.value, '_blank');
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper tms-scroll-page">
|
<div class="jnpf-content-wrapper-center tms-scroll-center">
|
<div class="jnpf-content-wrapper-content tms-scroll-body">
|
<a-spin :spinning="loading" class="tms-scroll-spin">
|
<div v-if="detail" class="tms-scroll-inner">
|
<div class="tms-page-header">
|
<div>
|
<div class="tms-page-header__title">培训签到</div>
|
<div class="tms-page-header__sub">{{ detail.recordNo }} · {{ detail.subject }}</div>
|
</div>
|
<div class="tms-page-header__actions">
|
<a-button :loading="qrLoading" @click="loadSignQr">刷新二维码</a-button>
|
<a-button @click="refreshSignList">刷新名单</a-button>
|
<a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
|
</div>
|
</div>
|
|
<div class="qr-panel">
|
<div class="section-title">现场签到码</div>
|
<a-spin :spinning="qrLoading">
|
<template v-if="showQr">
|
<div class="qr-box">
|
<JnpfQrcode data-type="static" :static-text="qrDisplayText" :width="220" />
|
</div>
|
<div class="qr-meta">
|
<div v-if="modeNames">适用方式:{{ modeNames }}</div>
|
<div v-if="signQr?.expireAt">有效至:{{ signQr.expireAt }}(过期请刷新)</div>
|
<div class="qr-hint">学员用手机扫码打开签到页并登录后自动落库;名单约每 15 秒刷新。</div>
|
<a-button type="link" size="small" @click="openScanPage">本机打开签到页(联调)</a-button>
|
</div>
|
</template>
|
<a-alert
|
v-else
|
class="tms-page-tip"
|
type="info"
|
show-icon
|
:message="signQr?.tip || '当前不可生成签到二维码'"
|
description="在线培训学员可在「个人培训任务」完成在线签到。"
|
/>
|
</a-spin>
|
</div>
|
|
<div class="section-title">已签到人员</div>
|
<ATable
|
size="middle"
|
bordered
|
row-key="id"
|
:columns="signColumns"
|
:data-source="detail.signList || []"
|
:pagination="false"
|
:locale="{ emptyText: '暂无签到记录' }"
|
/>
|
</div>
|
</a-spin>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.section-title {
|
margin: 16px 0 12px;
|
font-weight: 600;
|
font-size: 15px;
|
}
|
|
.section-title:first-child {
|
margin-top: 0;
|
}
|
|
.qr-panel {
|
margin-top: 8px;
|
padding: 24px 16px 16px;
|
background: #fafafa;
|
border: 1px solid #f0f0f0;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.qr-panel .section-title {
|
margin-top: 0;
|
align-self: flex-start;
|
}
|
|
.qr-box {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
width: 100%;
|
padding: 12px 0;
|
}
|
|
.qr-box :deep(.jnpf-qrcode) {
|
width: auto !important;
|
max-width: none !important;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
}
|
|
.qr-box :deep(.jnpf-qrcode img),
|
.qr-box :deep(.jnpf-qrcode canvas) {
|
display: block;
|
margin: 0 auto;
|
}
|
|
.qr-meta {
|
width: 100%;
|
text-align: center;
|
color: rgba(0, 0, 0, 0.65);
|
line-height: 1.7;
|
font-size: 13px;
|
}
|
|
.qr-hint {
|
color: rgba(0, 0, 0, 0.45);
|
margin-top: 4px;
|
}
|
</style>
|