diff --git a/inventory-backend/app/api/v1/common/users.py b/inventory-backend/app/api/v1/common/users.py index 79dea76..adc4150 100644 --- a/inventory-backend/app/api/v1/common/users.py +++ b/inventory-backend/app/api/v1/common/users.py @@ -7,7 +7,7 @@ from . import common_bp def active_user_options(): """ - 在职人员名单(id + 姓名)——「选择某人」类下拉框的**共用实现**。 + 在职人员名单(id + 姓名 + 账号)——「选择某人」类下拉框的**共用实现**。 ★ 为什么单独开一条中性路径,而不是复用 /transactions/borrow/users: 那条路径在语义上属于借库模块,出库补发、报废执行等处若直接复用, @@ -17,7 +17,13 @@ def active_user_options(): ★ 公司隔离与业务台账同口径(get_current_company_filter): 否则 A 公司的人能在选择器里看到 B 公司人员。 - ★ 只返回 id 与姓名:不含邮箱 / 角色 / 部门,最小披露。 + ★ 只返回 id / 姓名 / 账号:不含邮箱、角色、部门 —— 仍是最小披露。 + 账号(username 里 '/' 之后那一段,如 'gaoxue')是**拼音**, + 选择器靠它支持「输拼音找人」(输 gaoxue 匹配到高雪)—— + 这是加它的唯一原因,不是要把登录名铺开。 + + ★ 姓名口径用 user_display_name('高雪/gaoxue' → '高雪'), + 与出库/借库台账里存姓名的写法一致;否则同一人在台账里会出现两种写法。 """ from app.utils.decorators import get_current_company_filter from app.models.system import SysUser @@ -29,8 +35,15 @@ def active_user_options(): # 与 borrow_service.get_request_list 一致:SysUser.department 即公司维度 query = query.filter(SysUser.department == company_limit) - return [{'id': u.id, 'name': user_display_name(u)} - for u in query.order_by(SysUser.username).all()] + out = [] + for u in query.order_by(SysUser.username).all(): + raw = str(u.username or '') + out.append({ + 'id': u.id, + 'name': user_display_name(u), + 'account': raw.split('/')[-1] if '/' in raw else '', + }) + return out @common_bp.route('/active-users', methods=['GET']) diff --git a/inventory-web/src/views/outbound/create.vue b/inventory-web/src/views/outbound/create.vue index ba3d69e..cdde118 100644 --- a/inventory-web/src/views/outbound/create.vue +++ b/inventory-web/src/views/outbound/create.vue @@ -235,8 +235,41 @@ + - + + + + + + 「{{ form.consumer_name }}」不在在职人员名单中,请确认没写错 + @@ -385,6 +418,8 @@ import { getScanDraft, saveScanDraft, clearScanDraft, getScanDraftOverview, } from '@/api/outbound' import { uploadFile } from '@/api/common/upload' +// 在职人员名单 —— 领用人下拉的数据源(与出库补发的「补发给谁」同一份) +import { getActiveUsers } from '@/api/common/users' import { useUserStore } from '@/stores/user' const userStore = useUserStore() @@ -416,6 +451,76 @@ const lastY = ref(0) const operatorOptions = ref([]) +// ============================================================================ +// 领用人选择器 +// +// 数据源是统一的在职人员接口(与「补发给谁」同一个),公司隔离由后端负责。 +// ============================================================================ +const activeUsers = ref>([]) +const userQuery = ref('') + +// 动态模糊匹配:姓名与**拼音**都能搜。 +// ★ 默认的 filterable 只匹配 label(也就是姓名),输 gaoxue 匹配不到高雪; +// 账号字段正是那段拼音,这里一并比对。 +const filterUsers = (query: string) => { + userQuery.value = query || '' +} + +const filteredUsers = computed(() => { + const q = userQuery.value.trim().toLowerCase() + if (!q) return activeUsers.value + return activeUsers.value.filter(u => + u.name.toLowerCase().includes(q) || + (u.account || '').toLowerCase().includes(q) + ) +}) + +// 当前填的领用人是否**不在**在职名单里。 +// ★ 只做提示不做拦截:外部客户确实可能没有账号,但错字必须让人看见。 +const consumerUnknown = computed(() => { + const v = String(form.consumer_name || '').trim() + if (!v || !activeUsers.value.length) return false + return !activeUsers.value.some(u => u.name === v) +}) + +const loadActiveUsers = async () => { + try { + const res: any = await getActiveUsers() + if (res.code === 200 && res.data) { + activeUsers.value = res.data + } + } catch (e) { + console.warn('[出库] 在职人员名单拉取失败,领用人需手动输入', e) + } +} + +/** + * 领用人默认取所选审批单的**申请人**。 + * + * ★ 优先按 applicant_id 在在职名单里反查,而不是直接用接口返回的 + * applicant_name:后者是 '高雪/gaoxue'(后端 _get_user_name 返回完整 + * username),直接落库会把台账里存姓名的口径搞乱(实测 1497 行里 + * 没有一条带 '/' 的领用人)。按 id 反查拿到的姓名与下拉选项**同源**。 + */ +const fillConsumerFromRequest = () => { + const req = selectedRequest.value + if (!req) return + + const hit = activeUsers.value.find(u => u.id === req.applicant_id) + if (hit) { + form.consumer_name = hit.name + return + } + + // 回落:申请人已离职(不在在职名单)或名单还没加载完 + const raw = String(req.applicant_name || '') + const nm = raw.split('/')[0].trim() + // 后端查不到用户时会返回 '未知用户(7)' / '用户(7)',这种不能当姓名用 + if (nm && !nm.startsWith('未知用户') && !nm.startsWith('用户(')) { + form.consumer_name = nm + } +} + const form = reactive({ outbound_type: '', consumer_name: '', @@ -566,6 +671,8 @@ const handleRequestChange = async (val: number | null) => { if (selectedRequest.value?.outbound_type) { form.outbound_type = selectedRequest.value.outbound_type } + // ★ 领用人默认就是申请人(仍可改成别人) + fillConsumerFromRequest() } cartItems.value = [] @@ -805,6 +912,8 @@ onMounted(() => { operatorOptions.value.push(userStore.username) } loadHistoryOperators() + // 领用人下拉的数据源(与「补发给谁」同一个接口) + loadActiveUsers() }) const loadHistoryOperators = async () => { @@ -1298,6 +1407,8 @@ onUnmounted(() => { /* ★ 审批单选择 */ .approval-request-select { margin-bottom: 16px; } .select-tip { margin: 6px 0 0 0; color: #909399; font-size: 12px; } +/* 领用人不在在职名单时的软提示(外部客户不算错,但要让人看见) */ +.field-warn { margin-top: 4px; color: #E6A23C; font-size: 12px; line-height: 1.4; } /* ★ 计划清单 */ .planned-items-section {