qaz 1 день назад
Родитель
Сommit
f9d6ef9850

+ 2 - 2
.env.development

@@ -5,8 +5,8 @@ ENV = 'development'
 #线下:http://192.168.100.187:8083
 # 本地环境接口地址 121.36.251.245
 
-VITE_API_URL = 'http://192.168.100.179:8086'
-#VITE_API_URL = 'http://1.94.168.85:8086'
+#VITE_API_URL = 'http://192.168.100.179:8086'
+VITE_API_URL = 'http://1.94.168.85:8086'
 # VITE_API_URL = 'https://backend.qicai321.com'
 VITE_API_WX_URL='https://api.weixin.qq.com/'
 VITE_API_URL_MINLONG='http://117.185.80.170:7861'

+ 1 - 1
dist/index.html

@@ -11,7 +11,7 @@
 		<link rel="icon" href="/assets/facivon.BnFzlJG6.ico" />
 		<title>设备借还系统</title>
 		<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
-		<script type="module" crossorigin src="/assets/index.BBSvAdh_.js"></script>
+		<script type="module" crossorigin src="/assets/index.D5IQyNDE.js"></script>
 		<link rel="modulepreload" crossorigin href="/assets/vue.ClL-MbRM.js">
 		<link rel="stylesheet" crossorigin href="/assets/index.CoS9XZ9g.css">
 	</head>

+ 1 - 1
dist/version-build

@@ -1 +1 @@
-3.0.4.1776940461905
+3.0.4.1777025906976

+ 26 - 8
src/views/system/borrow/approval/SpecialBorrowApp/index.vue

@@ -454,7 +454,7 @@ const timeline = ref({
 });
 
 //审批(与 SpecialBorrowApp 同构;通用借用保留仓库等详情)
-// 课堂借用(borrow_type=1)、特殊借用(=2):仅「创建申请 → 审批通过 → 流程完成」,不展示领取仓库
+// 课堂/特殊:步骤骨架同上;有 warehouse 时在「审批通过」中展示领取仓库
 function formatApprovalRecordTime(t: any): string {
   if (t == null || t === '') return '';
   const d = dayjs(t);
@@ -514,10 +514,15 @@ function findApprovedRecordForWorkflowStep(
     );
     if (hit) return hit;
   }
-  const stepIdx = sortedConfigSteps.findIndex((s) => s === step);
-  if (stepIdx >= 0 && stepIdx < approvedOnly.length) {
-    const r = approvedOnly[stepIdx];
-    if (r?.status === 1) return r;
+  const orderMissing = (v: any) => v == null || v === '';
+  const allRecordsMissingOrder = approvedOnly.every((r: any) => orderMissing(r?.step_order));
+  const allStepsMissingOrder = sortedConfigSteps.every((s: any) => orderMissing(s?.step_order));
+  if (allRecordsMissingOrder && allStepsMissingOrder) {
+    const stepIdx = sortedConfigSteps.findIndex((s) => s === step);
+    if (stepIdx >= 0 && stepIdx < approvedOnly.length) {
+      const r = approvedOnly[stepIdx];
+      if (r?.status === 1) return r;
+    }
   }
   return null;
 }
@@ -527,6 +532,8 @@ const stepActiveIndex = computed(() => {
   if (!s.length) return 0;
   const i = s.findIndex((x) => x.status === 'process' || x.status === 'error' || x.status === 'wait');
   if (i >= 0) return i;
+  const allFinish = s.every((x) => x.status === 'finish');
+  if (allFinish) return s.length;
   return Math.max(0, s.length - 1);
 });
 
@@ -573,7 +580,7 @@ function buildSteps(data: any) {
   if (isClassroomOrSpecial) {
     if (data.status === 1) {
       list.push({
-        title: '审批通过',
+        title: '审批',
         description: `待审批人:${getPendingApproverDisplay(data)} 审批`,
         status: 'process',
       });
@@ -597,9 +604,14 @@ function buildSteps(data: any) {
           ? last.approve_time
           : last.create_datetime
         : null;
+      const hasWarehouse =
+        data.warehouse != null && String(data.warehouse).trim() !== '';
+      const warehousePart = hasWarehouse
+        ? `, 领取仓库:${getWarehouseName(data.warehouse) || '暂无仓库'}`
+        : '';
       list.push({
         title: '审批通过',
-        description: `审批人: ${nameStr}, 时间: ${formatApprovalRecordTime(tRaw) || ''}`,
+        description: `审批人: ${nameStr}, 时间: ${formatApprovalRecordTime(tRaw) || ''}${warehousePart}`,
         status: 'finish',
       });
     }
@@ -657,11 +669,14 @@ function buildSteps(data: any) {
           rec &&
           (rec as any).step_order != null &&
           Number((rec as any).step_order) === maxPassedOrder;
+        const hasWarehouse =
+          data.warehouse != null && String(data.warehouse).trim() !== '';
         const warehouseInfo =
           data.status >= 2 &&
           data.status !== 3 &&
           isLastInChain &&
-          isPickupWarehouseStep(step, rec)
+          hasWarehouse &&
+          (isPickupWarehouseStep(step, rec) || data.borrow_type === 0)
             ? `, 领取仓库:${getWarehouseName(data.warehouse) || '暂无仓库'}`
             : '';
         // 与课堂/特殊借用、查看效果图一致:已通过节点统一展示「审批通过」
@@ -671,6 +686,9 @@ function buildSteps(data: any) {
           status: 'finish',
         });
       } else {
+        if (data.status >= 2 && data.status !== 3) {
+          break;
+        }
         hasPendingInWorkflow = true;
         const names = getApproversTextForNode(step) || getPendingApproverDisplay(data);
         list.push({

+ 2 - 3
src/views/system/borrow/component/ClassroomBorrow/index.vue

@@ -511,10 +511,9 @@ function formatClassDateForCourseLabel(raw: unknown): string {
   return s.split(/[T\s]/)[0] || s;
 }
 
-/** 课程下拉:日期(仅年月日)、节次/时段文案、与借用开始时间一致的具体时刻 */
+/** 课程下拉:日期(仅年月日)、节次/时段 */
 function formatCourseSelectLabel(course: any): string {
-  const clock = getStartTime(Number(course?.time)).slice(0, 5);
-  const dateTime = [formatClassDateForCourseLabel(course?.class_date), course?.time_label, clock]
+  const dateTime = [formatClassDateForCourseLabel(course?.class_date), course?.time_label]
     .filter((s) => s != null && String(s).trim() !== '')
     .join(' ');
   return `${course?.classdetail} - ${course?.teacher_name} - ${course?.classroom} - ${dateTime}`;

+ 30 - 8
src/views/system/borrow/component/CollectEquipment/index.vue

@@ -1158,7 +1158,7 @@ const currentAbnormalData = ref<AbnormalData>({
 });
 
 //审批(与 SpecialBorrowApp 同构;通用借用保留仓库等详情)
-// 课堂借用(borrow_type=1)、特殊借用(=2):仅「创建申请 → 审批通过 → 流程完成」,不展示领取仓库
+// 课堂/特殊借用:步骤骨架同上;若接口返回 warehouse 则在「审批通过」中附带领取仓库(无仓库字段则不展示,不影响原样)
 function formatApprovalRecordTime(t: any): string {
   if (t == null || t === '') return '';
   const d = dayjs(t);
@@ -1218,10 +1218,16 @@ function findApprovedRecordForWorkflowStep(
     );
     if (hit) return hit;
   }
-  const stepIdx = sortedConfigSteps.findIndex((s) => s === step);
-  if (stepIdx >= 0 && stepIdx < approvedOnly.length) {
-    const r = approvedOnly[stepIdx];
-    if (r?.status === 1) return r;
+  /** 仅当记录与流程节点均未带 step_order 时按顺序对齐(旧数据);否则下标对齐会在跳步审批时把唯一一条通过记录错配到第一节点 */
+  const orderMissing = (v: any) => v == null || v === '';
+  const allRecordsMissingOrder = approvedOnly.every((r: any) => orderMissing(r?.step_order));
+  const allStepsMissingOrder = sortedConfigSteps.every((s: any) => orderMissing(s?.step_order));
+  if (allRecordsMissingOrder && allStepsMissingOrder) {
+    const stepIdx = sortedConfigSteps.findIndex((s) => s === step);
+    if (stepIdx >= 0 && stepIdx < approvedOnly.length) {
+      const r = approvedOnly[stepIdx];
+      if (r?.status === 1) return r;
+    }
   }
   return null;
 }
@@ -1231,6 +1237,9 @@ const stepActiveIndex = computed(() => {
   if (!s.length) return 0;
   const i = s.findIndex((x) => x.status === 'process' || x.status === 'error' || x.status === 'wait');
   if (i >= 0) return i;
+  const allFinish = s.every((x) => x.status === 'finish');
+  /** 全部完成时用 steps 长度作为 active,避免末段连线仍为 wait 色 */
+  if (allFinish) return s.length;
   return Math.max(0, s.length - 1);
 });
 
@@ -1277,7 +1286,7 @@ function buildSteps(data: any) {
   if (isClassroomOrSpecial) {
     if (data.status === 1) {
       list.push({
-        title: '审批通过',
+        title: '审批',
         description: `待审批人:${getPendingApproverDisplay(data)} 审批`,
         status: 'process',
       });
@@ -1301,9 +1310,14 @@ function buildSteps(data: any) {
           ? last.approve_time
           : last.create_datetime
         : null;
+      const hasWarehouse =
+        data.warehouse != null && String(data.warehouse).trim() !== '';
+      const warehousePart = hasWarehouse
+        ? `, 领取仓库:${getWarehouseName(data.warehouse) || '暂无仓库'}`
+        : '';
       list.push({
         title: '审批通过',
-        description: `审批人: ${nameStr}, 时间: ${formatApprovalRecordTime(tRaw) || ''}`,
+        description: `审批人: ${nameStr}, 时间: ${formatApprovalRecordTime(tRaw) || ''}${warehousePart}`,
         status: 'finish',
       });
     }
@@ -1361,11 +1375,15 @@ function buildSteps(data: any) {
           rec &&
           (rec as any).step_order != null &&
           Number((rec as any).step_order) === maxPassedOrder;
+        const hasWarehouse =
+          data.warehouse != null && String(data.warehouse).trim() !== '';
+        /** 选仓挂在管理员环节;跳步/代审时通过记录可能仍是「老师」等节点名,只要申请已批准且已选仓库仍应展示 */
         const warehouseInfo =
           data.status >= 2 &&
           data.status !== 3 &&
           isLastInChain &&
-          isPickupWarehouseStep(step, rec)
+          hasWarehouse &&
+          (isPickupWarehouseStep(step, rec) || data.borrow_type === 0)
             ? `, 领取仓库:${getWarehouseName(data.warehouse) || '暂无仓库'}`
             : '';
         // 与课堂/特殊借用、查看效果图一致:已通过节点统一展示「审批通过」
@@ -1375,6 +1393,10 @@ function buildSteps(data: any) {
           status: 'finish',
         });
       } else {
+        // 申请单已批准/已通过时,workflow 快照可能仍含待审节点(跳步审批、实例未同步等),以业务 status 为准,避免已批准后仍出现「待审批」
+        if (data.status >= 2 && data.status !== 3) {
+          break;
+        }
         hasPendingInWorkflow = true;
         const names = getApproversTextForNode(step) || getPendingApproverDisplay(data);
         list.push({

+ 109 - 25
src/views/system/borrow/component/SpecialBorrow/index.vue

@@ -89,6 +89,23 @@
                                 :disabledDate="disabledDate" placeholder="请选择预计归还时间" style="width: 100%"  :disabled="isView"/>
 					</el-form-item>
 				</el-col>
+				<el-col :span="24" v-if="!isView">
+					<el-alert
+						v-if="matchedWorkflowName"
+						type="info"
+						:closable="false"
+						show-icon
+					>
+						<template #title>将使用审批流程:{{ matchedWorkflowName }}</template>
+					</el-alert>
+					<el-alert
+						v-else-if="workflowsLoadDone && form.expected_end_time"
+						type="warning"
+						:closable="false"
+						show-icon
+						title="当前条件未匹配到启用中的审批流程,提交后将由后台默认规则处理;请检查流程管理中的天数区间、借用类型(含特殊借用)等配置"
+					/>
+				</el-col>
 				<el-col :span="4">
 					<el-form-item label="个人/团队" prop="team_type" >
 						<el-select v-model="form.team_type" placeholder="请选择" :disabled="isView">
@@ -138,7 +155,7 @@
 				</el-table-column>
 				<!-- <el-table-column prop="location" label="存放仓库" /> -->
 				<el-table-column label="操作" width="80">
-					<template #default="{ row, $index }">
+					<template #default="{ $index }">
 						<el-button type="text" style="color: red" @click="removeItem($index)" :disabled="isView">移除</el-button>
 					</template>
 				</el-table-column>
@@ -190,12 +207,12 @@
 	<SelectCatgory 
 		v-model:visible="showSelectDeviceDialog" 
 		@confirm="onDeviceSelected" 
-		:disabled-ids="form.items.map((item: any) => item.device_category).filter(Boolean)"
+		:disabled-ids="form.items.map((item) => item.device_category).filter(Boolean)"
 	/>
 </template>
 
 <script setup lang="ts">
-import { ref, watch, defineProps, defineEmits, onMounted,computed } from 'vue';
+import { ref, watch, onMounted, computed } from 'vue';
 import SelectDeviceDialog from './SelectDeviceDialog/index.vue';
 import dayjs from 'dayjs';
 import { ElMessage ,UploadProps, UploadUserFile } from 'element-plus';
@@ -205,6 +222,12 @@ import { request } from '/@/utils/service';
 import { Plus } from '@element-plus/icons-vue';
 import SelectCatgory from '../SelectCatgory/index.vue';
 import { getUserInfo } from '../../../login/api';
+import * as workflowApi from '../../workflow/api';
+import {
+	pickWorkflowIdForSpecialBorrow,
+	findWorkflowNameById,
+	type SpecialBorrowMatchInput,
+} from '../../utils/matchBorrowWorkflow';
 
 // 定义用户和管理员接口
 interface User {
@@ -540,6 +563,54 @@ const form = ref({
 });
 const showSelectDeviceDialog = ref(false);
 
+/** 用于按「流程管理」中的条件自动匹配 workflow id */
+const workflowsForMatch = ref<Record<string, any>[]>([]);
+const workflowsLoadDone = ref(false);
+
+async function loadWorkflowsForMatch() {
+	workflowsLoadDone.value = false;
+	try {
+		const res: any = await workflowApi.GetList({ page: 1, limit: 999 } as any);
+		workflowsForMatch.value = Array.isArray(res?.data) ? res.data : [];
+	} catch {
+		workflowsForMatch.value = [];
+	} finally {
+		workflowsLoadDone.value = true;
+	}
+}
+
+const resolvedAppUserType = computed(() => {
+	if (form.value.borrower_type !== 1 || form.value.app_user_borrower == null) return null;
+	const u = allUserList.value.find((x) => Number(x.id) === Number(form.value.app_user_borrower));
+	return u != null ? (u as any).user_type ?? null : null;
+});
+
+const specialBorrowMatchInput = computed((): SpecialBorrowMatchInput => ({
+	borrow_type: 2,
+	borrower_type: form.value.borrower_type,
+	team_type: form.value.team_type,
+	expected_start_time: form.value.expected_start_time || undefined,
+	expected_end_time: form.value.expected_end_time || undefined,
+	app_user_type: resolvedAppUserType.value,
+}));
+
+const matchedWorkflowId = computed(() => {
+	if (!workflowsForMatch.value.length) return null;
+	return pickWorkflowIdForSpecialBorrow(workflowsForMatch.value, specialBorrowMatchInput.value);
+});
+
+const matchedWorkflowName = computed(() =>
+	findWorkflowNameById(workflowsForMatch.value, matchedWorkflowId.value)
+);
+
+watch(
+	() => props.visible,
+	(v) => {
+		if (v && !isView.value) {
+			void loadWorkflowsForMatch();
+		}
+	}
+);
 
 const rules = {
    external_borrower_name: [
@@ -790,30 +861,43 @@ function onCancel() {
 	emit('update:visible', false);
 }
 function onSubmit() {
-	
-	formRef.value.validate((valid: boolean) => {
-		if (valid) {
-			form.value.items = form.value.items.map((item: any) => ({
-                ...item,
-                quantity: Number(item.quantity) || 0 ,
-            }));
-			const firstLocation = form.value.items[0]?.location || '';
-            form.value.return_location = firstLocation;
-			form.value.expected_start_time = now;
-			form.value.expected_end_time = form.value.expected_end_time
-				? dayjs(form.value.expected_end_time).endOf('day').format('YYYY-MM-DD HH:mm:ss')
-				: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
-			form.value.team_members_count=Number(form.value.team_members_count)||1;
-			form.value.mode = isEdit.value ? 'edit' : 'add';
-			// 确保 borrow_type 设置为 2(特殊借用)
-			form.value.borrow_type = 2;
-			// return
-			emit('submit', { ...form.value });
-			emit('update:visible', false);
-			// ElMessage.success("添加成功!");
+	formRef.value.validate(async (valid: boolean) => {
+		if (!valid) return;
+		if (!workflowsForMatch.value.length) {
+			await loadWorkflowsForMatch();
 		}
+		form.value.items = form.value.items.map((item: any) => ({
+			...item,
+			quantity: Number(item.quantity) || 0,
+		}));
+		const firstLocation = form.value.items[0]?.location || '';
+		form.value.return_location = firstLocation;
+		form.value.expected_start_time = now;
+		const endDay = form.value.expected_end_time;
+		form.value.expected_end_time = endDay
+			? dayjs(endDay).endOf('day').format('YYYY-MM-DD HH:mm:ss')
+			: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
+		form.value.team_members_count = Number(form.value.team_members_count) || 1;
+		form.value.mode = isEdit.value ? 'edit' : 'add';
+		form.value.borrow_type = 2;
+
+		const matchPayload: SpecialBorrowMatchInput = {
+			borrow_type: 2,
+			borrower_type: form.value.borrower_type,
+			team_type: form.value.team_type,
+			expected_start_time: form.value.expected_start_time,
+			expected_end_time: endDay || undefined,
+			app_user_type: resolvedAppUserType.value,
+		};
+		const workflowId = pickWorkflowIdForSpecialBorrow(workflowsForMatch.value, matchPayload);
+
+		const submitBody: Record<string, any> = { ...form.value };
+		if (workflowId != null) {
+			submitBody.workflow = workflowId;
+		}
+		emit('submit', submitBody);
+		emit('update:visible', false);
 	});
-	
 }
 
 </script>

+ 51 - 9
src/views/system/borrow/index.vue

@@ -295,6 +295,42 @@ async function resolveNewBorrowApplicationIdAfterAdd(
 	});
 }
 
+/** 与 crud 行操作「领取」一致:仅 status===2 时可领用;待审批等为其它值 */
+const BORROW_STATUS_PENDING_APPROVAL = 1;
+const BORROW_STATUS_READY_COLLECT = 2;
+
+function parseStatusFromCreateResponse(res: any): number | null {
+	if (!res) return null;
+	const toNum = (v: unknown) => {
+		if (v == null || v === '') return null;
+		const n = Number(v);
+		return Number.isFinite(n) ? n : null;
+	};
+	const d = res.data;
+	if (d != null && typeof d === 'object' && !Array.isArray(d)) {
+		const s = toNum((d as Record<string, unknown>).status);
+		if (s != null) return s;
+	}
+	return null;
+}
+
+async function resolveBorrowStatusAfterCreate(
+	res: any,
+	newId: string | number
+): Promise<number | null> {
+	const fromRes = parseStatusFromCreateResponse(res);
+	if (fromRes != null) return fromRes;
+	try {
+		const detail: any = await api.GetApplicationDetail(Number(newId));
+		if (detail?.code === 2000 && detail?.data?.status != null) {
+			return Number(detail.data.status);
+		}
+	} catch {
+		/* empty */
+	}
+	return null;
+}
+
 function onBorrowTypeSelected(type: number, mode: 'view' | 'edit' | 'add' |'collect',record?: any) {
 	showBorrowTypeDialog.value = false;
 	// 必须先同步写入 borrowForm,再打开子弹窗;若在 nextTick 中赋值,子组件挂载时仍拿到上一次的查看/编辑数据,会误拉详情并残留在表单中
@@ -328,19 +364,25 @@ async function handleBorrowSubmit(formData: any) {
 		} else {
 			const res: any = await api.AddObj(formData);
 			ElMessage.success('添加成功');
-			// 课堂(1) / 特殊(2) 借用:新增成功后直接打开「领取」弹窗,并进入设备清单
+			// 课堂(1) / 特殊(2):仅当已处于「可领取」状态时再自动打开领取(待审批如多步 workflow 不会误开)
 			const addBorrowType = Number(formData.borrow_type);
 			if (addBorrowType === 1 || addBorrowType === 2) {
 				const newId = await resolveNewBorrowApplicationIdAfterAdd(res, formData);
 				if (newId != null) {
-					// 先写入 modelValue 再打开弹窗,领取页首帧即 mode=collect,避免先显示「借用单信息」再切页
-					borrowForm.value = {
-						id: newId,
-						borrow_type: addBorrowType,
-						mode: 'collect',
-						collectInitialTab: 'second',
-					};
-					showCollectDialog.value = true;
+					const status = await resolveBorrowStatusAfterCreate(res, newId);
+					if (status === BORROW_STATUS_READY_COLLECT) {
+						borrowForm.value = {
+							id: newId,
+							borrow_type: addBorrowType,
+							mode: 'collect',
+							collectInitialTab: 'second',
+						};
+						showCollectDialog.value = true;
+					} else if (status === BORROW_STATUS_PENDING_APPROVAL) {
+						ElMessage.info('申请已提交,审批通过后可从列表点击「领取」办理领用');
+					} else {
+						ElMessage.info('申请已提交,请稍后在列表中查看;满足领用条件后可点击「领取」');
+					}
 				} else {
 					console.warn(
 						'[borrow] 新建借用后仍无法确定单主键,请让 POST /application/ 在 data 中返回 id;可暂从列表点击「领取」',

+ 143 - 0
src/views/system/borrow/utils/matchBorrowWorkflow.ts

@@ -0,0 +1,143 @@
+import dayjs from 'dayjs';
+
+/** 与流程管理里「流程类型」一致:设备借用申请 */
+export const WORKFLOW_TYPE_EQUIPMENT_BORROW = '0';
+
+/** 借用类型:与 WorkflowDialog 中 checkbox 值一致 */
+export const BORROW_TYPE_SPECIAL = '2';
+
+export type WorkflowListItem = Record<string, any>;
+
+export interface SpecialBorrowMatchInput {
+	borrow_type: number;
+	borrower_type: number;
+	team_type: number;
+	expected_start_time?: string | null;
+	expected_end_time?: string | null;
+	/** 应用用户借用时,对应 app_user 的 user_type(0 学生 1 教师 3 学院领导等) */
+	app_user_type?: number | null;
+}
+
+function getConditionBlock(w: WorkflowListItem) {
+	const tc = w.trigger_conditions || {};
+	const dr0 =
+		Array.isArray(w.duration_rules) && w.duration_rules[0] ? w.duration_rules[0] : {};
+	return {
+		min_days: dr0.min_days ?? tc.min_days ?? null,
+		max_days: dr0.max_days ?? tc.max_days ?? null,
+		borrow_types: (dr0.borrow_types ?? tc.borrow_types ?? []).map((x: any) => String(x)),
+		team_type: (tc.team_type ?? []).map((x: any) => String(x)),
+		user_types: (tc.user_types ?? []).map((x: any) => String(x)),
+	};
+}
+
+/** 预计开始、结束日之间的借用天数(含起止日,与业务上「借 N 天」常见口径一致) */
+export function getBorrowInclusiveDays(input: SpecialBorrowMatchInput): number | null {
+	const end = input.expected_end_time;
+	if (end == null || String(end).trim() === '') return null;
+	const startRaw =
+		input.expected_start_time && String(input.expected_start_time).trim() !== ''
+			? String(input.expected_start_time)
+			: dayjs().format('YYYY-MM-DD');
+	const start = dayjs(startRaw).startOf('day');
+	const endD = dayjs(end).startOf('day');
+	if (!start.isValid() || !endD.isValid()) return null;
+	const d = endD.diff(start, 'day') + 1;
+	return d >= 1 ? d : null;
+}
+
+/** 申请人角色与流程条件里 user_types(教师/学生/非本院师生)对齐 */
+export function applicantUserTypesForMatch(input: SpecialBorrowMatchInput): string[] {
+	if (input.borrower_type === 2) return ['external'];
+	if (input.borrower_type === 0) return ['teacher'];
+	if (input.borrower_type === 1) {
+		const ut = input.app_user_type;
+		if (ut === 0) return ['student'];
+		if (ut === 1 || ut === 3) return ['teacher'];
+		return [];
+	}
+	return [];
+}
+
+function daysInRange(days: number, minDays: number | null, maxDays: number | null): boolean {
+	if (minDays != null && Number.isFinite(Number(minDays)) && days < Number(minDays)) {
+		return false;
+	}
+	if (maxDays != null && Number.isFinite(Number(maxDays)) && days > Number(maxDays)) {
+		return false;
+	}
+	return true;
+}
+
+function hasUserTypeOverlap(condTypes: string[], applicant: string[]): boolean {
+	if (!condTypes.length) return true;
+	if (!applicant.length) return false;
+	const set = new Set(condTypes);
+	return applicant.some((t) => set.has(t));
+}
+
+function specificityScore(cond: ReturnType<typeof getConditionBlock>): number {
+	let s = 0;
+	if (cond.borrow_types.length) s += 2;
+	if (cond.team_type.length) s += 1;
+	if (cond.user_types.length) s += 1;
+	if (cond.min_days != null && Number(cond.min_days) > 0) s += 1;
+	if (cond.max_days != null) s += 1;
+	return s;
+}
+
+/**
+ * 在已拉取的流程列表中,为当前特殊借用单选出唯一 workflow id。
+ * 规则与流程配置一致:启用、设备借用类型、借用类型含特殊借用、天数区间、团队方式、用户角色等。
+ */
+export function pickWorkflowIdForSpecialBorrow(
+	workflows: WorkflowListItem[],
+	input: SpecialBorrowMatchInput
+): number | null {
+	const days = getBorrowInclusiveDays(input);
+	if (days == null) return null;
+
+	const applicantRoles = applicantUserTypesForMatch(input);
+	const teamStr = String(input.team_type);
+
+	const candidates: { id: number; score: number }[] = [];
+
+	for (const w of workflows) {
+		if (w.is_active === false) continue;
+		if (String(w.workflow_type ?? '') !== WORKFLOW_TYPE_EQUIPMENT_BORROW) continue;
+
+		const cond = getConditionBlock(w);
+		if (cond.borrow_types.length && !cond.borrow_types.includes(BORROW_TYPE_SPECIAL)) {
+			continue;
+		}
+		if (cond.team_type.length && !cond.team_type.includes(teamStr)) {
+			continue;
+		}
+		if (!hasUserTypeOverlap(cond.user_types, applicantRoles)) {
+			continue;
+		}
+		if (!daysInRange(days, cond.min_days, cond.max_days)) {
+			continue;
+		}
+		const id = Number(w.id);
+		if (!Number.isFinite(id)) continue;
+		candidates.push({ id, score: specificityScore(cond) });
+	}
+
+	if (!candidates.length) return null;
+
+	candidates.sort((a, b) => {
+		if (b.score !== a.score) return b.score - a.score;
+		return a.id - b.id;
+	});
+	return candidates[0].id;
+}
+
+export function findWorkflowNameById(
+	workflows: WorkflowListItem[],
+	id: number | null
+): string {
+	if (id == null) return '';
+	const w = workflows.find((x) => Number(x.id) === Number(id));
+	return w?.name ? String(w.name) : '';
+}

+ 2 - 3
src/views/system/timetablemanage/index.vue

@@ -195,10 +195,9 @@ function formatClassDateForDisplay(raw: unknown): string {
   return s.split(/[T\s]/)[0] || s;
 }
 
-/** 与课堂借用下拉里一致:日期(仅年月日)、节次/时段、具体开始时刻 */
+/** 与课堂借用下拉里一致:日期(仅年月日)、节次/时段(格子内不展示具体时刻) */
 function formatCourseDisplayTime(course: any): string {
-  const clock = getStartTime(Number(course?.time)).slice(0, 5);
-  return [formatClassDateForDisplay(course?.class_date), course?.time_label, clock]
+  return [formatClassDateForDisplay(course?.class_date), course?.time_label]
     .filter((s) => s != null && String(s).trim() !== '')
     .join(' ');
 }