feat(records): 出库/借还记录统一高级搜索版式

三个记录页(出库/借还/报废)此前搜索区形态各异:出库用裸 div + 分散样式,
借还缺日期范围,报废只有 SKU 输入框。现统一为 el-form :inline="true" +
.filter-form 版式,并补齐缺失的过滤维度。

借还记录(新增日期范围能力):
  · 后端 get_records 增加 start_date/end_date 参数,按「借出时间」过滤;
  · 边界补全时分秒(YYYY-MM-DD → 当日 00:00:00 / 23:59:59),
    与出库记录同口径,解决零点截断导致当天记录漏查的问题;
  · API 层透传两个新参数。

出库记录:
  · 版式统一为 inline 表单,日期选择器加 label;
  · 新增「重置」按钮;
  · 搜索类型切换由 500ms 防抖改为立即查询。

借还记录:
  · 新增「重置」按钮;搜索类型/状态切换改为立即查询(取消防抖)。

实测(SUPER_ADMIN):
  报废 全部/单号/SKU/操作人/物料名/日期 六类过滤均 200 且命中正确;
  借还 无过滤 52 单 / 本月 18 单 / 空区间 0 单(日期过滤生效);
  出库 无过滤 395 单 / SKU 277 / 姓名 7 / 物料名 16 / 单号 OUT-2026 命中 395。
This commit is contained in:
yueli
2026-09-10 12:06:08 +08:00
parent 23a7fc65f1
commit c35ec9a659
4 changed files with 187 additions and 71 deletions

View File

@ -162,6 +162,8 @@ def get_records():
page = int(request.args.get('page', 1)) page = int(request.args.get('page', 1))
keyword = request.args.get('keyword', '') keyword = request.args.get('keyword', '')
search_type = request.args.get('search_type', 'all') search_type = request.args.get('search_type', 'all')
start_date = request.args.get('start_date', '')
end_date = request.args.get('end_date', '')
# ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部 # ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部
borrower_name = None borrower_name = None
@ -175,7 +177,8 @@ def get_records():
res = TransService.get_records( res = TransService.get_records(
page=page, limit=10, status=status, keyword=keyword, page=page, limit=10, status=status, keyword=keyword,
search_type=search_type, borrower_name=borrower_name search_type=search_type, borrower_name=borrower_name,
start_date=start_date, end_date=end_date,
) )
# ★ service 层异常时code==500 的字典(带 traceback需要直通到前端便于排查 # ★ service 层异常时code==500 的字典(带 traceback需要直通到前端便于排查

View File

@ -414,7 +414,8 @@ class TransService:
raise e raise e
@staticmethod @staticmethod
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all', borrower_name=None): def get_records(page=1, limit=10, status='all', keyword=None, search_type='all',
borrower_name=None, start_date=None, end_date=None):
""" """
获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize 获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize
@ -429,7 +430,14 @@ class TransService:
状态过滤按"单号聚合"判定: 状态过滤按"单号聚合"判定:
- borrowed: 单号下至少有一条 is_returned=False - borrowed: 单号下至少有一条 is_returned=False
- returned: 单号下所有明细 is_returned=True - returned: 单号下所有明细 is_returned=True
日期范围按「借出时间」过滤,边界补全时分秒以解决零点截断问题。
""" """
# 日期补全:与出库记录同口径
if end_date and len(str(end_date).strip()) == 10:
end_date = f"{str(end_date).strip()} 23:59:59"
if start_date and len(str(start_date).strip()) == 10:
start_date = f"{str(start_date).strip()} 00:00:00"
try: try:
# ==================================================================== # ====================================================================
# 步骤 1a构造"单号维度"基础子查询GROUP BY borrow_no 在这里完成) # 步骤 1a构造"单号维度"基础子查询GROUP BY borrow_no 在这里完成)
@ -680,6 +688,20 @@ class TransService:
order_subq.c.borrow_no.in_(company_borrow_nos_subq) order_subq.c.borrow_no.in_(company_borrow_nos_subq)
) )
# 日期范围过滤(按借出时间;边界已在入口补全时分秒)
if start_date:
borrow_no_q = borrow_no_q.filter(order_subq.c.borrow_no.in_(
db.session.query(TransBorrow.borrow_no)
.filter(TransBorrow.borrow_time >= start_date)
.distinct()
))
if end_date:
borrow_no_q = borrow_no_q.filter(order_subq.c.borrow_no.in_(
db.session.query(TransBorrow.borrow_no)
.filter(TransBorrow.borrow_time <= end_date)
.distinct()
))
# 状态过滤(按"单号聚合"判定) # 状态过滤(按"单号聚合"判定)
if status == 'borrowed': if status == 'borrowed':
# 单号下至少一条未还 # 单号下至少一条未还

View File

@ -1,41 +1,51 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="filter-container"> <!-- 高级搜索与借还记录 / 报废记录统一版式 -->
<CompanySelector v-model="listQuery.company" @change="fetchData" style="margin-right: 10px;" /> <el-form :inline="true" class="filter-form" @submit.prevent>
<el-form-item>
<CompanySelector v-model="listQuery.company" @change="fetchData" />
</el-form-item>
<el-input <el-form-item label="搜索">
v-model="listQuery.keyword" <el-input
placeholder="请输入关键词" v-model="listQuery.keyword"
style="width: 250px;" placeholder="输入关键词"
class="filter-item" style="width: 240px;"
clearable clearable
@input="debouncedSearch" @input="debouncedSearch"
@clear="handleClearSearch" @clear="handleClearSearch"
> >
<template #prepend> <template #prepend>
<el-select v-model="listQuery.search_type" placeholder="搜索类型" style="width: 110px" @change="debouncedSearch"> <el-select v-model="listQuery.search_type" placeholder="搜索类型" style="width: 110px" @change="handleSearchImmediate">
<el-option label="全部" value="all" /> <el-option label="全部" value="all" />
<el-option label="单号" value="no" /> <el-option label="单号" value="no" />
<el-option label="姓名" value="name" /> <el-option label="姓名" value="name" />
<el-option label="SKU" value="sku" /> <el-option label="SKU" value="sku" />
<el-option label="物料名称" value="material_name" /> <el-option label="物料名称" value="material_name" />
<el-option label="规格型号" value="spec_model" /> <el-option label="规格型号" value="spec_model" />
</el-select> </el-select>
</template> </template>
</el-input> </el-input>
<el-date-picker </el-form-item>
v-model="listQuery.dateRange"
type="daterange" <el-form-item label="出库日期">
range-separator="" <el-date-picker
start-placeholder="开始日期" v-model="listQuery.dateRange"
end-placeholder="结束日期" type="daterange"
class="filter-item" range-separator=""
style="margin-left: 10px;" start-placeholder="开始日期"
value-format="YYYY-MM-DD" end-placeholder="结束日期"
/> value-format="YYYY-MM-DD"
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="fetchData">查询</el-button> style="width: 260px"
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" class="filter-item" @click="$router.push('/outbound/create')">新建出库</el-button> />
</div> </el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" @click="$router.push('/outbound/create')">新建出库</el-button>
</el-form-item>
</el-form>
<el-table <el-table
:data="list" :data="list"
@ -247,6 +257,22 @@ const handlePageChange = (val: number) => {
fetchData() fetchData()
} }
// 立即查询(搜索类型切换用,取消防抖)
const handleSearchImmediate = () => {
if (debounceTimer) clearTimeout(debounceTimer)
listQuery.page = 1
fetchData()
}
const resetFilter = () => {
if (debounceTimer) clearTimeout(debounceTimer)
listQuery.keyword = ''
listQuery.search_type = 'all'
listQuery.dateRange = []
listQuery.page = 1
fetchData()
}
const formatType = (type: string) => { const formatType = (type: string) => {
const map: any = { const map: any = {
'SALES': '销售出库', 'SALES': '销售出库',
@ -285,6 +311,17 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* ★ 统一高级搜索版式(与借还记录 / 报废记录一致) */
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.signature-cell { .signature-cell {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,33 +1,54 @@
<template> <template>
<div v-if="userStore.hasPermission('op_records')" class="app-container"> <div v-if="userStore.hasPermission('op_records')" class="app-container">
<div class="filter-container"> <!-- 高级搜索与出库记录 / 报废记录统一版式 -->
<el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px"> <el-form :inline="true" class="filter-form" @submit.prevent>
<el-radio-button label="all">全部</el-radio-button> <el-form-item>
<el-radio-button label="borrowed">未归还</el-radio-button> <el-radio-group v-model="status" @change="handleSearchImmediate">
<el-radio-button label="returned">已归还</el-radio-button> <el-radio-button label="all">全部</el-radio-button>
</el-radio-group> <el-radio-button label="borrowed">未归还</el-radio-button>
<el-radio-button label="returned">已归还</el-radio-button>
</el-radio-group>
</el-form-item>
<el-input <el-form-item label="搜索">
v-model="keyword" <el-input
placeholder="请输入关键词" v-model="keyword"
style="width: 250px" placeholder="输入关键词"
clearable style="width: 240px"
@input="debouncedSearch" clearable
@clear="handleClearSearch" @input="debouncedSearch"
> @clear="handleClearSearch"
<template #prepend> >
<el-select v-model="searchType" placeholder="类型" style="width: 110px" @change="debouncedSearch"> <template #prepend>
<el-option label="全部" value="all" /> <el-select v-model="searchType" placeholder="类型" style="width: 110px" @change="handleSearchImmediate">
<el-option label="单号" value="no" /> <el-option label="全部" value="all" />
<el-option label="借用人" value="name" /> <el-option label="单号" value="no" />
<el-option label="SKU" value="sku" /> <el-option label="借用人" value="name" />
<el-option label="物料名称" value="material_name" /> <el-option label="SKU" value="sku" />
<el-option label="规格型号" value="spec_model" /> <el-option label="物料名称" value="material_name" />
</el-select> <el-option label="规格型号" value="spec_model" />
</template> </el-select>
</el-input> </template>
<el-button type="primary" @click="fetchData">查询</el-button> </el-input>
</div> </el-form-item>
<el-form-item label="借出日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
<el-table <el-table
:data="list" :data="list"
@ -262,6 +283,7 @@ const total = ref(0)
const status = ref('borrowed') const status = ref('borrowed')
const keyword = ref('') const keyword = ref('')
const searchType = ref('all') const searchType = ref('all')
const dateRange = ref<string[]>([])
const page = ref(1) const page = ref(1)
const loading = ref(false) const loading = ref(false)
@ -329,15 +351,20 @@ const confirmScrap = async () => {
const fetchData = async () => { const fetchData = async () => {
loading.value = true loading.value = true
try { try {
const params: any = {
page: page.value,
status: status.value,
keyword: keyword.value,
search_type: searchType.value
}
if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0]
params.end_date = dateRange.value[1]
}
const res = await request({ const res = await request({
url: '/v1/transactions/records', url: '/v1/transactions/records',
method: 'get', method: 'get',
params: { params
page: page.value,
status: status.value,
keyword: keyword.value,
search_type: searchType.value
}
}) })
// ★ 按 borrow_no 分组聚合为主子表结构 // ★ 按 borrow_no 分组聚合为主子表结构
@ -371,6 +398,23 @@ const handlePage = (val: number) => {
fetchData() fetchData()
} }
// 立即查询(状态/类型切换用,取消防抖)
const handleSearchImmediate = () => {
if (debounceTimer) clearTimeout(debounceTimer)
page.value = 1
fetchData()
}
const resetFilter = () => {
if (debounceTimer) clearTimeout(debounceTimer)
status.value = 'borrowed'
keyword.value = ''
searchType.value = 'all'
dateRange.value = []
page.value = 1
fetchData()
}
// ★ 新增:格式化预计归还时间及倒计时逻辑(交通灯预警系统) // ★ 新增:格式化预计归还时间及倒计时逻辑(交通灯预警系统)
const formatExpectedTime = (timeStr: string) => { const formatExpectedTime = (timeStr: string) => {
if (!timeStr) return { text: '无限期', diffText: '', style: {} } if (!timeStr) return { text: '无限期', diffText: '', style: {} }
@ -418,5 +462,15 @@ onBeforeUnmount(() => {
}) })
</script> </script>
<style> <style scoped>
/* ★ 统一高级搜索版式(与出库记录 / 报废记录一致) */
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
</style> </style>