feat: implement cross-table search and debounced dynamic search for borrow and return records
This commit is contained in:
@ -5,7 +5,8 @@ from app.models.transaction import TransBorrow
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
from sqlalchemy import desc, func, nullslast, asc
|
||||
from app.models.base import MaterialBase
|
||||
from sqlalchemy import desc, func, nullslast, asc, or_
|
||||
|
||||
|
||||
class TransService:
|
||||
@ -189,15 +190,36 @@ class TransService:
|
||||
@staticmethod
|
||||
def get_records(page=1, limit=10, status='all', keyword=None):
|
||||
q = TransBorrow.query
|
||||
|
||||
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||
if keyword:
|
||||
# 子查询:关联 material_base 表获取物料名称和规格型号
|
||||
material_join = db.session.query(
|
||||
TransBorrow.id
|
||||
).join(
|
||||
MaterialBase,
|
||||
TransBorrow.sku == MaterialBase.sku
|
||||
).filter(or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)).subquery()
|
||||
|
||||
# 主搜索条件:借用人、SKU、单号 + 物料名称、规格型号
|
||||
keyword_conditions = or_(
|
||||
TransBorrow.borrower_name.ilike(f'%{keyword}%'),
|
||||
TransBorrow.sku.ilike(f'%{keyword}%'),
|
||||
TransBorrow.borrow_no.ilike(f'%{keyword}%'),
|
||||
TransBorrow.id.in_(material_join) # 匹配物料名称/规格型号的记录
|
||||
)
|
||||
q = q.filter(keyword_conditions)
|
||||
|
||||
if status == 'borrowed':
|
||||
q = q.filter(TransBorrow.is_returned == False)
|
||||
elif status == 'returned':
|
||||
q = q.filter(TransBorrow.is_returned == True)
|
||||
|
||||
if keyword:
|
||||
q = q.filter(TransBorrow.borrower_name.ilike(f'%{keyword}%') |
|
||||
TransBorrow.sku.ilike(f'%{keyword}%') |
|
||||
TransBorrow.borrow_no.ilike(f'%{keyword}%'))
|
||||
# 使用 distinct 防止跨表查询产生重复记录
|
||||
q = q.distinct()
|
||||
|
||||
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
||||
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
@ -4,10 +4,11 @@
|
||||
<el-input
|
||||
v-model="listQuery.keyword"
|
||||
placeholder="单号/姓名/SKU/名称/规格"
|
||||
style="width: 200px;"
|
||||
style="width: 250px;"
|
||||
class="filter-item"
|
||||
clearable
|
||||
@keyup.enter="fetchData"
|
||||
@input="debouncedSearch"
|
||||
@clear="handleClearSearch"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="listQuery.dateRange"
|
||||
@ -118,13 +119,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||
import { getOutboundList } from '@/api/outbound'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 防抖定时器
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 防抖搜索函数
|
||||
const debouncedSearch = () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
debounceTimer = setTimeout(() => {
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 清空搜索时立即触发查询
|
||||
const handleClearSearch = () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
outbound_no: 'outbound_list:outbound_no',
|
||||
@ -213,6 +237,14 @@ const getTagType = (type: string) => {
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
// 组件销毁前清理定时器,防止内存泄漏
|
||||
onBeforeUnmount(() => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -7,7 +7,14 @@
|
||||
<el-radio-button label="returned">已归还</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-input v-model="keyword" placeholder="搜索借用人/SKU" style="width: 200px" @keyup.enter="fetchData" />
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="单号/借用人/SKU/名称/规格"
|
||||
style="width: 250px"
|
||||
clearable
|
||||
@input="debouncedSearch"
|
||||
@clear="handleClearSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||
</div>
|
||||
|
||||
@ -104,15 +111,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
dayjs.locale('zh-cn')
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 防抖定时器
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 防抖搜索函数
|
||||
const debouncedSearch = () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
debounceTimer = setTimeout(() => {
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 清空搜索时立即触发查询
|
||||
const handleClearSearch = () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
borrow_no: 'op_records:borrow_no',
|
||||
@ -203,6 +233,14 @@ const formatExpectedTime = (timeStr: string) => {
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
|
||||
// 组件销毁前清理定时器,防止内存泄漏
|
||||
onBeforeUnmount(() => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user