Просмотр исходного кода

feat(s7): FQC检验员调配改为组织树+账号表选人,替代手填用户Id

检验员调配弹窗原为手填「用户Id」文本框,改为复用平台组织能力选真实账号:
- 新增 FqcInspectorPickerDialog.vue:左组织树(复用 system/org/orgTree.vue,
  computed+JWT兜底锁当前登录租户、隐藏其内置切租户控件),右账号表
  (SysUserApi.apiSysUserPagePost,姓名/账号搜+组织节点过滤+分页),单选radio
  选中抛 {id,realName,account}
- fqcInspectionTaskList.vue:调配弹窗换成该组件,onAssign→onAssignConfirm(user)
  调 assignFqcTaskEntries(ids, user.id)

后端不改(assign 已收 InspectorUserId)。DDL-free。

chore: bump version Web 2.4.306
YY968XX 3 дней назад
Родитель
Сommit
5bcc9ac05b

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.305",
+	"version": "2.4.306",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 181 - 0
Web/src/views/aidop/s7/fqc/components/FqcInspectorPickerDialog.vue

@@ -0,0 +1,181 @@
+<template>
+	<el-dialog :model-value="modelValue" title="检验员调配(组织选人)" width="840px" @update:model-value="(v: boolean) => emit('update:modelValue', v)" @open="onOpen">
+		<div class="picker">
+			<div class="tree-pane">
+				<OrgTree :tenant-id="tenantId" @node-click="onNodeClick" />
+			</div>
+			<div class="user-pane">
+				<el-form :inline="true" :model="query" class="mb8" @submit.prevent>
+					<el-form-item label="姓名"><el-input v-model="query.realName" clearable style="width: 120px" @keyup.enter="doSearch" /></el-form-item>
+					<el-form-item label="账号"><el-input v-model="query.account" clearable style="width: 120px" @keyup.enter="doSearch" /></el-form-item>
+					<el-form-item><el-button type="primary" @click="doSearch">查询</el-button></el-form-item>
+				</el-form>
+				<el-table :data="rows" v-loading="loading" border stripe size="small" height="340" highlight-current-row @current-change="onCurrent">
+					<el-table-column width="46" align="center">
+						<template #default="{ row }">
+							<el-radio v-model="pickedId" :label="row.id" @change="() => onPick(row)"><span /></el-radio>
+						</template>
+					</el-table-column>
+					<el-table-column prop="realName" label="姓名" min-width="100" show-overflow-tooltip />
+					<el-table-column prop="account" label="账号" min-width="120" show-overflow-tooltip />
+					<el-table-column prop="orgName" label="所属组织" min-width="140" show-overflow-tooltip />
+					<el-table-column prop="posName" label="职位" min-width="100" show-overflow-tooltip />
+					<template #empty><el-empty description="该组织下无账号" /></template>
+				</el-table>
+				<div class="pager">
+					<el-pagination v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @current-change="loadUsers" @size-change="loadUsers" />
+				</div>
+			</div>
+		</div>
+		<div class="picked-hint">
+			<template v-if="picked">已选检验员:<b>{{ picked.realName }}({{ picked.account }})</b></template>
+			<span v-else class="muted">请在右侧账号列表勾选一名检验员</span>
+		</div>
+		<template #footer>
+			<el-button @click="emit('update:modelValue', false)">取消</el-button>
+			<el-button type="primary" :disabled="!picked" @click="onConfirm">确定</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts" name="FqcInspectorPickerDialog">
+import { computed, reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysUserApi } from '/@/api-services/api';
+import OrgTree from '/@/views/system/org/component/orgTree.vue';
+import { resolveAidopTenantId } from '/@/views/aidop/api/aidopTenant';
+
+interface PickedUser {
+	id: number;
+	realName: string;
+	account: string;
+}
+
+defineProps<{ modelValue: boolean }>();
+const emit = defineEmits<{
+	(e: 'update:modelValue', v: boolean): void;
+	(e: 'confirm', user: PickedUser): void;
+}>();
+
+// 锁当前登录租户:传给 OrgTree 隐藏其内置 TenantSelect,账号范围= 当前租户。
+// computed 确保在弹窗打开(此时 userInfo store 已就绪)时求值;store 万一仍空则从 JWT 兜底,
+// 双保险避免捕获到初始挂载时的 undefined 而露出「切换租户」控件。
+function currentTenantId(): number | undefined {
+	const v = resolveAidopTenantId();
+	if (v) return v;
+	try {
+		const tok = JSON.parse(localStorage.getItem('admin.net:access-token') || 'null');
+		if (tok) {
+			// base64url → base64(补 padding),兼容 token 轮换后 payload 含 - / _
+			let part = String(tok).split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
+			part += part.length % 4 ? '='.repeat(4 - (part.length % 4)) : '';
+			const payload = JSON.parse(atob(part));
+			const n = Number(payload?.TenantId);
+			return Number.isFinite(n) && n > 0 ? n : undefined;
+		}
+	} catch {
+		/* 解析失败则回落 undefined */
+	}
+	return undefined;
+}
+const tenantId = computed(() => currentTenantId());
+
+const query = reactive({ realName: '', account: '', orgId: undefined as number | undefined, page: 1, pageSize: 10 });
+const rows = ref<any[]>([]);
+const total = ref(0);
+const loading = ref(false);
+const pickedId = ref<number | null>(null);
+const picked = ref<PickedUser | null>(null);
+
+async function loadUsers() {
+	loading.value = true;
+	try {
+		const res = await getAPI(SysUserApi).apiSysUserPagePost({
+			page: query.page,
+			pageSize: query.pageSize,
+			realName: query.realName || undefined,
+			account: query.account || undefined,
+			orgId: query.orgId,
+		} as any);
+		rows.value = res.data.result?.items ?? [];
+		total.value = res.data.result?.total ?? 0;
+	} catch (e: any) {
+		rows.value = [];
+		total.value = 0;
+		ElMessage.error(e?.message || '加载账号失败');
+	} finally {
+		loading.value = false;
+	}
+}
+function doSearch() {
+	query.page = 1;
+	loadUsers();
+}
+function onNodeClick(node: any) {
+	const id = Number(node?.id);
+	query.orgId = Number.isFinite(id) && id > 0 ? id : undefined;
+	query.page = 1;
+	loadUsers();
+}
+function onPick(row: any) {
+	if (!row?.id) return;
+	pickedId.value = Number(row.id);
+	picked.value = { id: Number(row.id), realName: row.realName ?? '', account: row.account ?? '' };
+}
+function onCurrent(row: any) {
+	if (row) onPick(row);
+}
+function onOpen() {
+	query.realName = '';
+	query.account = '';
+	query.orgId = undefined;
+	query.page = 1;
+	pickedId.value = null;
+	picked.value = null;
+	loadUsers();
+}
+function onConfirm() {
+	if (!picked.value) {
+		ElMessage.warning('请选择一名检验员');
+		return;
+	}
+	emit('confirm', picked.value);
+}
+</script>
+
+<style scoped lang="scss">
+.picker {
+	display: flex;
+	gap: 12px;
+	height: 420px;
+}
+.tree-pane {
+	width: 260px;
+	min-width: 260px;
+	height: 100%;
+	overflow: hidden;
+}
+.user-pane {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	min-width: 0;
+}
+.mb8 {
+	margin-bottom: 8px;
+}
+.pager {
+	margin-top: 8px;
+	display: flex;
+	justify-content: flex-end;
+}
+.picked-hint {
+	margin-top: 10px;
+	font-size: 13px;
+	color: #303133;
+	.muted {
+		color: #909399;
+	}
+}
+</style>

+ 5 - 19
Web/src/views/aidop/s7/fqc/fqcInspectionTaskList.vue

@@ -130,15 +130,7 @@
 			@qe-submit="onFlowQe"
 		/>
 
-		<el-dialog v-model="assignVisible" title="检验员调配" width="420px">
-			<el-form label-width="100px">
-				<el-form-item label="用户 Id"><el-input v-model="assignUserId" placeholder="填写检验员用户 Id" /></el-form-item>
-			</el-form>
-			<template #footer>
-				<el-button @click="assignVisible = false">取消</el-button>
-				<el-button type="primary" @click="onAssign">确定</el-button>
-			</template>
-		</el-dialog>
+		<FqcInspectorPickerDialog v-model="assignVisible" @confirm="onAssignConfirm" />
 
 		<el-dialog v-model="priorityVisible" title="优先级调整" width="420px">
 			<el-form label-width="100px">
@@ -163,6 +155,7 @@ import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
 import FqcInspBillDetailDrawer from './components/FqcInspBillDetailDrawer.vue';
+import FqcInspectorPickerDialog from './components/FqcInspectorPickerDialog.vue';
 import { fetchFqcResultList, fetchFqcDetail, type FqcResultRow, type FqcDetail } from '../api/fqcInspBill';
 import {
 	fetchFqcTaskEntryList,
@@ -216,7 +209,6 @@ const selectedEntryIds = ref<number[]>([]);
 const generatingId = ref<number | null>(null);
 
 const assignVisible = ref(false);
-const assignUserId = ref('');
 const priorityVisible = ref(false);
 const priorityValue = ref<'正常' | '紧急'>('正常');
 
@@ -268,18 +260,12 @@ async function onClaim() {
 	}
 }
 function openAssign() {
-	assignUserId.value = '';
 	assignVisible.value = true;
 }
-async function onAssign() {
-	const uid = Number(assignUserId.value);
-	if (!uid) {
-		ElMessage.warning('请填写有效的用户 Id');
-		return;
-	}
+async function onAssignConfirm(user: { id: number; realName: string; account: string }) {
 	try {
-		await assignFqcTaskEntries(selectedEntryIds.value, uid);
-		ElMessage.success('调配成功');
+		await assignFqcTaskEntries(selectedEntryIds.value, user.id);
+		ElMessage.success(`已调配给 ${user.realName || user.account}`);
 		assignVisible.value = false;
 		loadEntryList();
 	} catch (e: any) {