Compare commits

7 Commits

6 changed files with 537 additions and 23 deletions

View File

@ -157,9 +157,14 @@ def create_app():
try: try:
from app.services.permission_service import PermissionService from app.services.permission_service import PermissionService
with app.app_context(): with app.app_context():
# 先清理旧版菜单,再初始化新版
PermissionService.cleanup_legacy_stocktake_menus()
PermissionService.init_audit_menu() PermissionService.init_audit_menu()
PermissionService.init_stocktake_menus()
# 初始化所有菜单的层级结构
PermissionService.init_all_menus()
except Exception as e: except Exception as e:
print(f"⚠️ 审计日志菜单初始化跳过: {e}") print(f"⚠️ 菜单初始化跳过: {e}")
# ----------------------------------------------------- # -----------------------------------------------------
# 2.10 注册盘盈盘亏管理模块 (Stock Adjustment) # 2.10 注册盘盈盘亏管理模块 (Stock Adjustment)

View File

@ -1,6 +1,6 @@
import uuid # .material -> .base refactor checked import uuid # .material -> .base refactor checked
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from sqlalchemy import or_, func, desc from sqlalchemy import or_, func, desc, and_
from app.extensions import db from app.extensions import db
from app.models.outbound import TransOutbound from app.models.outbound import TransOutbound
@ -195,25 +195,92 @@ class OutboundService:
def get_grouped_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None): def get_grouped_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None):
""" """
查询出库记录(按出库单号分组),包含详细物品信息 查询出库记录(按出库单号分组),包含详细物品信息
支持跨表搜索单号、领用人、SKU、物料名称、规格型号
""" """
# 1. 查询分页单号 # 1. 构建基础查询
# 如果有关键词,需要联表搜索物料名称和规格型号
if keyword:
# 由于 TransOutbound 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
# 需要使用 union 或分别查询后合并
# 方案:分别查询三种来源的 matching outbound_no然后合并
# 查询 stock_buy 路径匹配的名称/规格
buy_match = db.session.query(TransOutbound.outbound_no).join(
StockBuy, and_(
TransOutbound.stock_id == StockBuy.id,
TransOutbound.source_table == 'stock_buy'
)
).join(
MaterialBase, StockBuy.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 查询 stock_semi 路径匹配的名称/规格
semi_match = db.session.query(TransOutbound.outbound_no).join(
StockSemi, and_(
TransOutbound.stock_id == StockSemi.id,
TransOutbound.source_table == 'stock_semi'
)
).join(
MaterialBase, StockSemi.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 查询 stock_product 路径匹配的名称/规格
product_match = db.session.query(TransOutbound.outbound_no).join(
StockProduct, and_(
TransOutbound.stock_id == StockProduct.id,
TransOutbound.source_table == 'stock_product'
)
).join(
MaterialBase, StockProduct.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 合并三种来源的匹配单号
all_matches = db.session.query(buy_match.c.outbound_no).union(
db.session.query(semi_match.c.outbound_no),
db.session.query(product_match.c.outbound_no)
).subquery()
# 主搜索条件单号、领用人、SKU + 物料名称/规格匹配的单号
keyword_conditions = or_(
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
TransOutbound.sku.ilike(f'%{keyword}%'),
TransOutbound.outbound_no.in_(all_matches)
)
else:
keyword_conditions = None
stmt = db.session.query( stmt = db.session.query(
TransOutbound.outbound_no, TransOutbound.outbound_no,
func.max(TransOutbound.outbound_time).label('max_time') func.max(TransOutbound.outbound_time).label('max_time')
).group_by(TransOutbound.outbound_no) ).group_by(TransOutbound.outbound_no)
if keyword: if keyword_conditions is not None:
stmt = stmt.filter(or_( stmt = stmt.filter(keyword_conditions)
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
TransOutbound.sku.ilike(f'%{keyword}%')
))
if start_date and end_date: if start_date and end_date:
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date)) stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
stmt = stmt.order_by(desc('max_time')) stmt = stmt.order_by(desc('max_time'))
# 使用 distinct 确保跨表查询不重复
stmt = stmt.distinct()
pagination = stmt.paginate(page=page, per_page=per_page, error_out=False) pagination = stmt.paginate(page=page, per_page=per_page, error_out=False)
outbound_nos = [row.outbound_no for row in pagination.items] outbound_nos = [row.outbound_no for row in pagination.items]

View File

@ -159,7 +159,7 @@ class PermissionService:
# 1. 检查并创建审计日志菜单 # 1. 检查并创建审计日志菜单
menu_code = 'system_audit' menu_code = 'system_audit'
existing_menu = SysMenu.query.filter_by(code=menu_code).first() existing_menu = SysMenu.query.filter_by(code=menu_code).first()
if not existing_menu: if not existing_menu:
new_menu = SysMenu( new_menu = SysMenu(
parent_id=0, parent_id=0,
@ -201,3 +201,310 @@ class PermissionService:
db.session.rollback() db.session.rollback()
print(f"❌ 初始化审计日志菜单失败: {str(e)}") print(f"❌ 初始化审计日志菜单失败: {str(e)}")
raise e raise e
@staticmethod
def init_stocktake_menus():
"""
初始化盘点管理菜单和权限
包括:顶级菜单、盲盘作业、盈亏调整、以及操作权限元素
"""
try:
role_code = 'SUPER_ADMIN'
# 1. 创建顶级菜单:盘点管理
stocktake_mgmt_code = 'stocktake_mgmt'
stocktake_menu = SysMenu.query.filter_by(code=stocktake_mgmt_code).first()
if not stocktake_menu:
stocktake_menu = SysMenu(
parent_id=0,
name='盘点管理',
code=stocktake_mgmt_code,
path='/stocktake',
sort_order=30,
is_visible=True
)
db.session.add(stocktake_menu)
db.session.flush()
print(f"✅ 盘点管理顶级菜单已创建")
else:
print(f" 盘点管理顶级菜单已存在")
# 2. 创建子菜单:盲盘作业
stocktake_op_code = 'inventory_stocktake'
stocktake_op_menu = SysMenu.query.filter_by(code=stocktake_op_code).first()
if not stocktake_op_menu:
stocktake_op_menu = SysMenu(
parent_id=stocktake_menu.id,
name='盲盘作业',
code=stocktake_op_code,
path='/stocktake/operation',
sort_order=1,
is_visible=True
)
db.session.add(stocktake_op_menu)
db.session.flush()
print(f"✅ 盲盘作业菜单已创建")
else:
print(f" 盲盘作业菜单已存在")
# 3. 为盲盘作业添加操作权限元素
stocktake_op_element = SysElement.query.filter_by(
menu_code=stocktake_op_code,
code='inventory_stocktake:operation'
).first()
if not stocktake_op_element:
stocktake_op_element = SysElement(
menu_code=stocktake_op_code,
name='盲盘操作',
code='inventory_stocktake:operation',
element_type='operation'
)
db.session.add(stocktake_op_element)
print(f"✅ 盲盘作业操作权限已创建")
else:
print(f" 盲盘作业操作权限已存在")
# 4. 创建子菜单:盈亏调整
adjustment_code = 'stock_adjustment'
adjustment_menu = SysMenu.query.filter_by(code=adjustment_code).first()
if not adjustment_menu:
adjustment_menu = SysMenu(
parent_id=stocktake_menu.id,
name='盈亏调整',
code=adjustment_code,
path='/stocktake/adjustment',
sort_order=2,
is_visible=True
)
db.session.add(adjustment_menu)
db.session.flush()
print(f"✅ 盈亏调整菜单已创建")
else:
print(f" 盈亏调整菜单已存在")
# 5. 为盈亏调整添加列表权限元素 (stock_adjustment:list)
adjustment_list_element = SysElement.query.filter_by(
menu_code=adjustment_code,
code='stock_adjustment:list'
).first()
if not adjustment_list_element:
adjustment_list_element = SysElement(
menu_code=adjustment_code,
name='盈亏列表',
code='stock_adjustment:list',
element_type='element'
)
db.session.add(adjustment_list_element)
print(f"✅ 盈亏调整列表权限已创建")
else:
print(f" 盈亏调整列表权限已存在")
# 6. 为盈亏调整添加操作权限元素 (stock_adjustment:operation)
adjustment_op_element = SysElement.query.filter_by(
menu_code=adjustment_code,
code='stock_adjustment:operation'
).first()
if not adjustment_op_element:
adjustment_op_element = SysElement(
menu_code=adjustment_code,
name='盈亏操作',
code='stock_adjustment:operation',
element_type='operation'
)
db.session.add(adjustment_op_element)
print(f"✅ 盈亏调整操作权限已创建")
else:
print(f" 盈亏调整操作权限已存在")
# 7. 为超级管理员分配所有盘点相关权限
menu_codes = [stocktake_mgmt_code, stocktake_op_code, adjustment_code]
for mc in menu_codes:
existing_perm = SysRolePermission.query.filter_by(
role_code=role_code,
target_code=mc,
type='menu'
).first()
if not existing_perm:
new_perm = SysRolePermission(
role_code=role_code,
target_code=mc,
type='menu'
)
db.session.add(new_perm)
print(f"✅ 超级管理员已赋予 {mc} 菜单权限")
# 8. 分配操作权限
op_codes = ['inventory_stocktake:operation', 'stock_adjustment:list', 'stock_adjustment:operation']
for oc in op_codes:
existing_perm = SysRolePermission.query.filter_by(
role_code=role_code,
target_code=oc,
type='element'
).first()
if not existing_perm:
new_perm = SysRolePermission(
role_code=role_code,
target_code=oc,
type='element'
)
db.session.add(new_perm)
print(f"✅ 超级管理员已赋予 {oc} 操作权限")
# 9. 提交
db.session.commit()
return True
except Exception as e:
db.session.rollback()
print(f"❌ 初始化盘点管理菜单失败: {str(e)}")
raise e
@staticmethod
def cleanup_legacy_stocktake_menus():
"""
清理残留的旧版库存盘点菜单
如果数据库中存在挂在入库管理下的旧版库存盘点菜单,清理掉
"""
try:
# 查找可能存在的旧版库存盘点菜单(入库管理下的 stocktake
# 旧版可能在入库管理 (path like '%inventory%stocktake') 或者 code 包含 stocktake 但不是新的
legacy_menus = SysMenu.query.filter(
SysMenu.code.in_(['stocktake', 'inventory_stocktake_old'])
).all()
for menu in legacy_menus:
# 删除关联的权限
SysRolePermission.query.filter_by(target_code=menu.code).delete()
# 删除关联的元素
SysElement.query.filter_by(menu_code=menu.code).delete()
# 删除菜单
db.session.delete(menu)
print(f"🗑️ 已清理旧版库存盘点菜单: {menu.code} ({menu.name})")
db.session.commit()
return True
except Exception as e:
db.session.rollback()
print(f"⚠️ 清理旧版菜单失败: {str(e)}")
return False
@staticmethod
def init_all_menus():
"""
初始化所有菜单的层级结构,确保权限配置页面显示正确的树形结构
按照侧边栏顺序:基础信息 -> 入库 -> 盘点 -> 出库 -> BOM -> 借库 -> 报废 -> 系统
"""
try:
role_code = 'SUPER_ADMIN'
# 定义菜单结构 (code, name, path, parent_code, sort_order)
menu_defs = [
# 顶级菜单 (按侧边栏顺序)
('material_mgmt', '基础信息管理', '/material', None, 10),
('inventory_mgmt', '入库管理', '/inventory', None, 20),
('stocktake_mgmt', '盘点管理', '/stocktake', None, 30),
('outbound_mgmt', '出库管理', '/outbound', None, 40),
('bom_mgmt', 'BOM管理', '/bom', None, 50),
('operation_mgmt', '借库管理', '/operation', None, 60),
('scrap_mgmt', '报废管理', '/scrap', None, 70),
('system_mgmt', '系统管理', '/system', None, 80),
# 基础信息子菜单
('material_base', '基础信息', '/material/index', 'material_mgmt', 1),
# 入库管理子菜单
('inbound_buy', '采购入库', '/inventory/buy', 'inventory_mgmt', 1),
('inbound_semi', '半成品入库', '/inventory/semi', 'inventory_mgmt', 2),
('inbound_product', '成品入库', '/inventory/product', 'inventory_mgmt', 3),
('inbound_service', '服务权益', '/inventory/service', 'inventory_mgmt', 4),
('inbound_summary', '入库记录', '/inventory/summary', 'inventory_mgmt', 5),
# 盘点管理子菜单
('inventory_stocktake', '盲盘作业', '/stocktake/operation', 'stocktake_mgmt', 1),
('stock_adjustment', '盈亏调整', '/stocktake/adjustment', 'stocktake_mgmt', 2),
# 出库管理子菜单
('outbound_selection', '出库选单', '/outbound/selection', 'outbound_mgmt', 1),
('outbound_create', '扫码出库', '/outbound/create', 'outbound_mgmt', 2),
('outbound_list', '出库记录', '/outbound/index', 'outbound_mgmt', 3),
# BOM管理子菜单
('bom_manage', 'BOM配方管理', '/bom/manage', 'bom_mgmt', 1),
# 借库管理子菜单
('op_borrow', '借库操作', '/operation/borrow', 'operation_mgmt', 1),
('op_return', '归还操作', '/operation/repair', 'operation_mgmt', 2),
('op_records', '借还记录', '/operation/records', 'operation_mgmt', 3),
# 报废管理子菜单
('scrap_create', '新建报废', '/scrap/create', 'scrap_mgmt', 1),
('scrap_list', '报废记录', '/scrap/index', 'scrap_mgmt', 2),
# 系统管理子菜单
('system_user', '员工账号管理', '/system/user-create', 'system_mgmt', 1),
('system_permission', '权限分配', '/system/permission', 'system_mgmt', 2),
('system_audit', '审计日志', '/system/audit', 'system_mgmt', 3),
]
# 第一步:清理根级别的冗余子菜单(这些本应是子节点,但可能之前被错误地创建为根节点)
child_codes = [m[0] for m in menu_defs if m[3] is not None] # 所有子菜单的code
orphaned_menus = SysMenu.query.filter(
SysMenu.code.in_(child_codes),
(SysMenu.parent_id == 0) | (SysMenu.parent_id.is_(None))
).all()
for menu in orphaned_menus:
print(f"🗑️ 清理根级别冗余菜单: {menu.code} ({menu.name})")
# 删除关联的权限
SysRolePermission.query.filter_by(target_code=menu.code).delete()
db.session.delete(menu)
# 创建或更新菜单
menu_map = {} # code -> menu obj
for code, name, path, parent_code, sort_order in menu_defs:
menu = SysMenu.query.filter_by(code=code).first()
if not menu:
menu = SysMenu(code=code, name=name, path=path, sort_order=sort_order, is_visible=True)
db.session.add(menu)
db.session.flush()
print(f"✅ 菜单已创建: {name} ({code})")
else:
# 更新已有菜单的属性
menu.name = name
menu.path = path
menu.sort_order = sort_order
menu_map[code] = menu
# 设置 parent_id
for code, name, path, parent_code, sort_order in menu_defs:
if parent_code and parent_code in menu_map:
menu = menu_map[code]
parent = menu_map[parent_code]
menu.parent_id = parent.id
# 为超级管理员分配所有菜单权限
for code, name, path, parent_code, sort_order in menu_defs:
if parent_code is None: # 只分配顶级菜单
existing_perm = SysRolePermission.query.filter_by(
role_code=role_code,
target_code=code,
type='menu'
).first()
if not existing_perm:
new_perm = SysRolePermission(
role_code=role_code,
target_code=code,
type='menu'
)
db.session.add(new_perm)
db.session.commit()
print(f"✅ 所有菜单初始化完成")
return True
except Exception as e:
db.session.rollback()
print(f"❌ 初始化菜单失败: {str(e)}")
raise e

View File

@ -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_, and_
class TransService: class TransService:
@ -189,15 +190,79 @@ 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:
# TransBorrow 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
# 需要使用 union 或分别查询后合并
# 查询 stock_buy 路径匹配的名称/规格
buy_match = db.session.query(TransBorrow.id).join(
StockBuy, and_(
TransBorrow.stock_id == StockBuy.id,
TransBorrow.source_table == 'stock_buy'
)
).join(
MaterialBase, StockBuy.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 查询 stock_semi 路径匹配的名称/规格
semi_match = db.session.query(TransBorrow.id).join(
StockSemi, and_(
TransBorrow.stock_id == StockSemi.id,
TransBorrow.source_table == 'stock_semi'
)
).join(
MaterialBase, StockSemi.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 查询 stock_product 路径匹配的名称/规格
product_match = db.session.query(TransBorrow.id).join(
StockProduct, and_(
TransBorrow.stock_id == StockProduct.id,
TransBorrow.source_table == 'stock_product'
)
).join(
MaterialBase, StockProduct.base_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
)
).subquery()
# 合并三种来源的匹配 ID
all_matches = db.session.query(buy_match.c.id).union(
db.session.query(semi_match.c.id),
db.session.query(product_match.c.id)
).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_(all_matches)
)
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)

View File

@ -3,11 +3,12 @@
<div class="filter-container"> <div class="filter-container">
<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>

View File

@ -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>