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.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.inbound.product import StockProduct
|
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:
|
class TransService:
|
||||||
@ -189,15 +190,36 @@ class TransService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_records(page=1, limit=10, status='all', keyword=None):
|
def get_records(page=1, limit=10, status='all', keyword=None):
|
||||||
q = TransBorrow.query
|
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':
|
if status == 'borrowed':
|
||||||
q = q.filter(TransBorrow.is_returned == False)
|
q = q.filter(TransBorrow.is_returned == False)
|
||||||
elif status == 'returned':
|
elif status == 'returned':
|
||||||
q = q.filter(TransBorrow.is_returned == True)
|
q = q.filter(TransBorrow.is_returned == True)
|
||||||
|
|
||||||
if keyword:
|
# 使用 distinct 防止跨表查询产生重复记录
|
||||||
q = q.filter(TransBorrow.borrower_name.ilike(f'%{keyword}%') |
|
q = q.distinct()
|
||||||
TransBorrow.sku.ilike(f'%{keyword}%') |
|
|
||||||
TransBorrow.borrow_no.ilike(f'%{keyword}%'))
|
|
||||||
|
|
||||||
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
||||||
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|||||||
@ -4,10 +4,11 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="listQuery.keyword"
|
v-model="listQuery.keyword"
|
||||||
placeholder="单号/姓名/SKU/名称/规格"
|
placeholder="单号/姓名/SKU/名称/规格"
|
||||||
style="width: 200px;"
|
style="width: 250px;"
|
||||||
class="filter-item"
|
class="filter-item"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="fetchData"
|
@input="debouncedSearch"
|
||||||
|
@clear="handleClearSearch"
|
||||||
/>
|
/>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="listQuery.dateRange"
|
v-model="listQuery.dateRange"
|
||||||
@ -118,13 +119,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||||
import { getOutboundList } from '@/api/outbound'
|
import { getOutboundList } from '@/api/outbound'
|
||||||
import { Picture } from '@element-plus/icons-vue'
|
import { Picture } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
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)
|
// 列与权限Code的映射关系(数据库中的code)
|
||||||
const permissionMap: Record<string, string> = {
|
const permissionMap: Record<string, string> = {
|
||||||
outbound_no: 'outbound_list:outbound_no',
|
outbound_no: 'outbound_list:outbound_no',
|
||||||
@ -213,6 +237,14 @@ const getTagType = (type: string) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 组件销毁前清理定时器,防止内存泄漏
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -7,7 +7,14 @@
|
|||||||
<el-radio-button label="returned">已归还</el-radio-button>
|
<el-radio-button label="returned">已归还</el-radio-button>
|
||||||
</el-radio-group>
|
</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>
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,15 +111,38 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
import 'dayjs/locale/zh-cn'
|
||||||
dayjs.locale('zh-cn')
|
dayjs.locale('zh-cn')
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
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)
|
// 列与权限Code的映射关系(数据库中的code)
|
||||||
const permissionMap: Record<string, string> = {
|
const permissionMap: Record<string, string> = {
|
||||||
borrow_no: 'op_records:borrow_no',
|
borrow_no: 'op_records:borrow_no',
|
||||||
@ -203,6 +233,14 @@ const formatExpectedTime = (timeStr: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchData)
|
onMounted(fetchData)
|
||||||
|
|
||||||
|
// 组件销毁前清理定时器,防止内存泄漏
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user