feat(outbound): 领用人改为可搜索下拉,选中申请单自动带出申请人
背景:出库的「领用人/客户」一直是自由文本框(create.vue 的 el-input),
后端 consumer_name 必填但内容不限。实测 1497 条出库里有 7 条领用人对不上
任何在职人员,其中就有把姓名打成「刘」这种错字 —— 手输必然产生这类问题。
而同一个表单里紧挨着的「经办人(库管)」已经是下拉选择了。
改动:
前端 outbound/create.vue
· 领用人由 el-input 改为 el-select(filterable + allow-create +
default-first-option),数据源复用统一的在职人员接口 getActiveUsers()
(与出库补发的「补发给谁」同一份,公司隔离由后端负责)。
· 选中审批单时自动带出申请人(fillConsumerFromRequest),仍可改成别人。
★ 按 applicant_id 在在职名单里**反查**姓名,而不是直接用接口返回的
applicant_name —— 后者是 '高雪/gaoxue'(后端 _get_user_name 返回完整
username),直接落库会把台账的姓名口径搞乱(实测 1497 行无一带 '/')。
· 动态模糊匹配:姓名与**拼音**都能搜。默认 filterable 只匹配 label(姓名),
输 gaoxue 匹配不到高雪,故自定义 filter-method 一并比对账号段。
· 软提示而非硬拦截:填了不在名单里的名字会显示
「「X」不在在职人员名单中,请确认没写错」。保留 allow-create 是因为
销售出库的客户可能是外部人员、没有账号,封死会让这类出库没法登记。
后端 common/users.py
· active_user_options() 增加 account 字段(username 里 '/' 之后那段拼音),
供前端拼音搜索。仍是 id/姓名/账号三项,不含邮箱、角色、部门。
★ 没有做后端强校验(拒绝非系统人员):用户明确选择「下拉为主,保留手输兜底」,
硬拦会挡住外部客户。错字的防线是"默认从名单里选 + 不在名单时给提示"。
验证(12 + 6 项断言全过):
· 39 个在职人员,每项含 id/name/account;name 是裸姓名、account 非空
· 拼音搜索:输 gaoxue / GAOXUE → 高雪;输 gao → 高前赐/高闯/高雪;输 高雪 → 高雪
· 在职人员无重名(按姓名绑定才无歧义)
· 已通过的审批单能反查到在职申请人(单 514/513/512/511/509 全部命中)
· 公司隔离:IRIS 用户 20 人只看到「石利」、LICA 用户 19 人只看到
「石利LICA」、超管 39 人 —— 跨公司同名在各自视角下不会同时出现
· 借库人员选择器(共用同一实现)仍正常,新增字段向后兼容
· 日报三天附件 490.5K/193.4K/92.3K 与 6 列结构未变;出库列表接口未受影响
· vue-tsc 与 vite build exit=0
注意:
· 历史 7 条不匹配的领用人**未改动**(其中「常国玺」疑似离职人员)。
· 前端只做了编译验证,未在浏览器实际点过 —— 下拉交互与提示样式需人工确认。
This commit is contained in:
@ -7,7 +7,7 @@ from . import common_bp
|
|||||||
|
|
||||||
def active_user_options():
|
def active_user_options():
|
||||||
"""
|
"""
|
||||||
在职人员名单(id + 姓名)——「选择某人」类下拉框的**共用实现**。
|
在职人员名单(id + 姓名 + 账号)——「选择某人」类下拉框的**共用实现**。
|
||||||
|
|
||||||
★ 为什么单独开一条中性路径,而不是复用 /transactions/borrow/users:
|
★ 为什么单独开一条中性路径,而不是复用 /transactions/borrow/users:
|
||||||
那条路径在语义上属于借库模块,出库补发、报废执行等处若直接复用,
|
那条路径在语义上属于借库模块,出库补发、报废执行等处若直接复用,
|
||||||
@ -17,7 +17,13 @@ def active_user_options():
|
|||||||
★ 公司隔离与业务台账同口径(get_current_company_filter):
|
★ 公司隔离与业务台账同口径(get_current_company_filter):
|
||||||
否则 A 公司的人能在选择器里看到 B 公司人员。
|
否则 A 公司的人能在选择器里看到 B 公司人员。
|
||||||
|
|
||||||
★ 只返回 id 与姓名:不含邮箱 / 角色 / 部门,最小披露。
|
★ 只返回 id / 姓名 / 账号:不含邮箱、角色、部门 —— 仍是最小披露。
|
||||||
|
账号(username 里 '/' 之后那一段,如 'gaoxue')是**拼音**,
|
||||||
|
选择器靠它支持「输拼音找人」(输 gaoxue 匹配到高雪)——
|
||||||
|
这是加它的唯一原因,不是要把登录名铺开。
|
||||||
|
|
||||||
|
★ 姓名口径用 user_display_name('高雪/gaoxue' → '高雪'),
|
||||||
|
与出库/借库台账里存姓名的写法一致;否则同一人在台账里会出现两种写法。
|
||||||
"""
|
"""
|
||||||
from app.utils.decorators import get_current_company_filter
|
from app.utils.decorators import get_current_company_filter
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser
|
||||||
@ -29,8 +35,15 @@ def active_user_options():
|
|||||||
# 与 borrow_service.get_request_list 一致:SysUser.department 即公司维度
|
# 与 borrow_service.get_request_list 一致:SysUser.department 即公司维度
|
||||||
query = query.filter(SysUser.department == company_limit)
|
query = query.filter(SysUser.department == company_limit)
|
||||||
|
|
||||||
return [{'id': u.id, 'name': user_display_name(u)}
|
out = []
|
||||||
for u in query.order_by(SysUser.username).all()]
|
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'])
|
@common_bp.route('/active-users', methods=['GET'])
|
||||||
|
|||||||
@ -235,8 +235,41 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="24" :md="8">
|
<el-col :span="24" :md="8">
|
||||||
|
<!--
|
||||||
|
★ 领用人改为**下拉选择**(此前是自由文本框)。
|
||||||
|
为什么:实测 1497 条出库里有 7 条领用人对不上任何在职人员,
|
||||||
|
其中就有把姓名打成「刘」这种错字 —— 手输必然会有这类问题。
|
||||||
|
现在从人员名单里选,与「经办人(库管)」的交互也统一了。
|
||||||
|
|
||||||
|
★ 选了审批单会自动带出申请人(见 handleRequestChange),
|
||||||
|
仍可改成别人。
|
||||||
|
|
||||||
|
★ 保留 allow-create:销售出库的客户可能是外部人员、没有账号,
|
||||||
|
封死会让这类出库没法登记。但**手输的名字会给出提示**
|
||||||
|
(见下方 consumerUnknown 的告警),既不挡业务也不放任错字。
|
||||||
|
-->
|
||||||
<el-form-item label="领用人/客户" prop="consumer_name">
|
<el-form-item label="领用人/客户" prop="consumer_name">
|
||||||
<el-input v-model="form.consumer_name" placeholder="请输入姓名" />
|
<el-select
|
||||||
|
v-model="form.consumer_name"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
:filter-method="filterUsers"
|
||||||
|
placeholder="选择或输入姓名"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="u in filteredUsers"
|
||||||
|
:key="u.id"
|
||||||
|
:label="u.name"
|
||||||
|
:value="u.name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<!-- ★ 软提示而非硬拦截:不在名单里也能提交(外部客户),
|
||||||
|
但要让库管看见"这个名字系统里没有" —— 错字就是这么挡住的 -->
|
||||||
|
<div v-if="consumerUnknown" class="field-warn">
|
||||||
|
「{{ form.consumer_name }}」不在在职人员名单中,请确认没写错
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
@ -385,6 +418,8 @@ import {
|
|||||||
getScanDraft, saveScanDraft, clearScanDraft, getScanDraftOverview,
|
getScanDraft, saveScanDraft, clearScanDraft, getScanDraftOverview,
|
||||||
} from '@/api/outbound'
|
} from '@/api/outbound'
|
||||||
import { uploadFile } from '@/api/common/upload'
|
import { uploadFile } from '@/api/common/upload'
|
||||||
|
// 在职人员名单 —— 领用人下拉的数据源(与出库补发的「补发给谁」同一份)
|
||||||
|
import { getActiveUsers } from '@/api/common/users'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -416,6 +451,76 @@ const lastY = ref(0)
|
|||||||
|
|
||||||
const operatorOptions = ref<string[]>([])
|
const operatorOptions = ref<string[]>([])
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 领用人选择器
|
||||||
|
//
|
||||||
|
// 数据源是统一的在职人员接口(与「补发给谁」同一个),公司隔离由后端负责。
|
||||||
|
// ============================================================================
|
||||||
|
const activeUsers = ref<Array<{ id: number; name: string; account: string }>>([])
|
||||||
|
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({
|
const form = reactive({
|
||||||
outbound_type: '',
|
outbound_type: '',
|
||||||
consumer_name: '',
|
consumer_name: '',
|
||||||
@ -566,6 +671,8 @@ const handleRequestChange = async (val: number | null) => {
|
|||||||
if (selectedRequest.value?.outbound_type) {
|
if (selectedRequest.value?.outbound_type) {
|
||||||
form.outbound_type = selectedRequest.value.outbound_type
|
form.outbound_type = selectedRequest.value.outbound_type
|
||||||
}
|
}
|
||||||
|
// ★ 领用人默认就是申请人(仍可改成别人)
|
||||||
|
fillConsumerFromRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
cartItems.value = []
|
cartItems.value = []
|
||||||
@ -805,6 +912,8 @@ onMounted(() => {
|
|||||||
operatorOptions.value.push(userStore.username)
|
operatorOptions.value.push(userStore.username)
|
||||||
}
|
}
|
||||||
loadHistoryOperators()
|
loadHistoryOperators()
|
||||||
|
// 领用人下拉的数据源(与「补发给谁」同一个接口)
|
||||||
|
loadActiveUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadHistoryOperators = async () => {
|
const loadHistoryOperators = async () => {
|
||||||
@ -1298,6 +1407,8 @@ onUnmounted(() => {
|
|||||||
/* ★ 审批单选择 */
|
/* ★ 审批单选择 */
|
||||||
.approval-request-select { margin-bottom: 16px; }
|
.approval-request-select { margin-bottom: 16px; }
|
||||||
.select-tip { margin: 6px 0 0 0; color: #909399; font-size: 12px; }
|
.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 {
|
.planned-items-section {
|
||||||
|
|||||||
Reference in New Issue
Block a user